👻

Cloudflare WorkersをFetchハンドラーからRPCハンドラーに切り替えたときのメモ

に公開

とある大きめなミスをしていたので忘備録ともし似たようなことをしてしまった人がいた場合たどり着いてくれるように残しておく

Cloudflare Workersは外部からの通常のHTTPアクセスに対して返答するFetchハンドラー以外にも、指定した処理をWorkers同士のService Bindingでつないで呼び出すRPCハンドラーがある

これを利用すると、サンプルコードのようなことができる

worker_b.js
import { WorkerEntrypoint } from "cloudflare:workers";

export default class extends WorkerEntrypoint {
  async fetch() { return new Response("Hello from Worker B"); }

  add(a, b) { return a + b; } // この処理を
}

これをデプロイして、それを

worker_a.js
export default {
  async fetch(request, env) {
    const result = await env.WORKER_B.add(1, 2); // ここから呼び出している
    return new Response(result);
  }
}

あとはBindingの設定を行えば(ここでは省略)、別のWorkerから呼び出すことができる(Fetcherを利用してもできるが、requestオブジェクトを引き回す必要があったりとやや面倒

で、現在運用中の個人サイトもNext.js+Cloudflare Pagesで運用していたのだが、この度next-on-pagesからopennextjs-cloudflareに移行するとして、同時に環境もCloudflare PagesからWorkersにデプロイ先が変わることになり、Pages Functionsを利用して別Workersに逃がしていたCMSへのfetchをRPCハンドラーでコンポーネントの処理の内部から直接呼び出す形に変更することにした

実際のコードとは違うが、以下の感じを想定していた

cms-fetch.ts
export default class CMSFetcher extends WorkerEntrypoint<Env>{
  async fetch(request){
    // 今までの処理(テストとかで使うので残したい

    // 今まで読んでた処理をメソッド化して別途呼び出しに変更
    const result = this.fetchCMSContent(id)
    return Response.json(result)
  }

  async fetchCMSContent(id: string): Promise<CMSResultType>{
    // CMSのクライアントを作ったり認証情報を入れたり
    const result = await cmsClient.fetch(id)
    return result
  }
}

これでデプロイし、Fetchハンドラーの方にアクセスしたところちゃんと動作するが、RPCハンドラー経由のアクセスは失敗してしまった

失敗時はエラーログを見るに限るが、呼び出された側には情報ゼロのエラーログ(messageには呼び出しているRPCハンドラーのエントリーポイントと関数の名前だけの)があり、呼び出し側は Error fetching fetchCMSContent: DataCloneError: Could not serialize object of type "Object". This type does not support serialization.のログがあった

原因と解決法

そもそもRPCはパラメータを自動でJSONに割り当ててやり取りするので、JSONにできない要素を含んでいるとエラーを吐いたりする(これに関しては環境によって違うと思われる。今回はあくまでCloudflare WorkersのRPCを利用したService Binding)

そのため、 fetchCMSContent 関数の返り値である result にJSONにそのままでは変換できないObjectが入っていたのだと思われる

ひとまず簡単に治すのには

const result = await cmsClient.fetch(id)
return JSON.parse(JSON.stringify(result))

と不要な情報を JSON.stringify で削ぎ落としてからもう一度オブジェクトにすると大体の場合は通る

場合によっては通らないかもしれないが、そのときはパラメータにJSONに変換できない情報が混じってないかを確認すると良い

余談

ちなみに関数の定義の変数も、RPCであれば複数型のような事はできない。例えば

pagenation(offset: number | string | null)

みたいなことはできないらしい。デプロイできたので実はできる、という可能性もなくはないがRPCの仕様的にはできないと思っていたほうが良さそう

詳しくないのがバレるので(そもそも今回のエラーも詳しくないからおきた)、良い資料をお持ちの方は紹介していただけると助かります

Discussion