KitesurfをCloudflare Workersで動かす

KitesurfをCloudflare Workersで動かす

Cloudflareの新しいヘッドレスブラウザ「Kitesurf」をCloudflare Workers上で動かす方法を試してみました。バインディング未対応の現状で、カスタムWebSocketトランスポートを実装することで実現した手法についてご紹介します。
2026.08.21

こんばんは、情報システム室の夏目です。

CloudflareがKitesurfという超軽量ヘッドレスブラウザを今月頭に発表しました。
まだβではありますが、今回はこれをCloudflare Workers上で動かそうと思います。

Kitesurf

https://developers.cloudflare.com/browser-run/kitesurf/

Kitesurf ↗ ↗ は、Cloudflareが開発したステートレスでスケーラビリティに優れたブラウザです。Workers上で完全に動作するように設計されており、AIエージェント向けに最適化されています。Chromiumのような完全なデスクトップブラウザエンジンをバンドルするのではなく、エージェントにとって重要な機能――トークン数、コンテキストウィンドウ、スケーラビリティ、パフォーマンス、コスト効率――に重点を置き、一方でタブ機能やテーマ、拡張機能、ピクセル単位の正確なレンダリングなど、人間向けの機能は意図的に省いています。

Kitesurfは人間向けの機能を省略して、AIエージェント用に最適化されたヘッドレスブラウザです。

Browser Runの Quick ActionCDP (Chrome DevTool Protocol) で動かすことができます。

Cloudflare Workersで動かすには

wrangler.jsonc
{
	"browser": {
		"binding": "<BINDING_NAME>",
	},
}

https://developers.cloudflare.com/workers/wrangler/configuration/#browser-run

現状 (2026/08/21)、Cloudflare WorkersのバインディングではKitesurfを指定するためのオプションがないので、直接Quick ActionかCDPを使用する必要がありそうです。

今回はCDPを @cloudflare/puppeteer で動かそうと思います。

最初に試したコード

import puppeteer from "@cloudflare/puppeteer";

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    return main(env);
  },
};

async function main(env: Env) {
  const endpoint = makeEndpoint(env);

  const browser = await puppeteer.connect({
    browserWSEndpoint: endpoint,
    headers: { Authorization: `Bearer ${env.CF_API_TOKEN}` },
  });

  try {
    const page = await browser.newPage();
    await page.goto("https://ifconfig.io");

    await sleep(1000);

    const ss = await page.screenshot({ encoding: "binary", type: "png" });
    return new Response(ss, { headers: { "Content-Type": "image/png" } });
  } catch (e) {
    console.error(e);
    throw e;
  } finally {
    await browser.close();
  }
}

function makeEndpoint(env: Env) {
  return `wss://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/browser-run/devtools/browser?browser= kitesurf`;
}

function sleep(ms: number): Promise<void> {
  return new Promise((ok) => {
    setTimeout(ok, ms);
  });
}

Kitesurfのドキュメントにあったように下記エンドポイントにCloudflareのAPI Tokenを使って接続してみます。
wss://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/browser-run/devtools/browser?browser=kitesurf

また、WorkerのSecretとして下記の二つを設定しています。

  • CF_ACCOUNT_ID: CloudflareのアカウントID
  • CF_API_TOKEN: CloudflareのアカウントAPI Token (権限として Browser renderingedit にする)

これをCloudflare Workersにデプロイして発行されたURLを叩いてみました。

fb4d877b-c1da-41c9-807b-4fb53d35d96e

エラーが起きました。
ログを確認すると次のようになっていました。

{
  "message": "Unable to connect to existing session undefined (it may still be in use or not ready yet) - retry or launch a new browser: Error: ws does not work in the browser. Browser clients must use the native WebSocket object",
  "exception": {
    "stack": "    at PuppeteerWorkers.connect (node_modules/.vlt/~npm~@cloudflare+puppeteer@1.4.0/node_modules/@cloudflare/puppeteer/src/cloudflare/PuppeteerWorkers.ts:234:13)\n    at async main (src/index.ts:12:19)",
    "name": "Error",
    "message": "Unable to connect to existing session undefined (it may still be in use or not ready yet) - retry or launch a new browser: Error: ws does not work in the browser. Browser clients must use the native WebSocket object",
    "timestamp": 1787290484629
  },
  "$workers": {
    "truncated": false,
    "event": {
      "request": {
        "url": "https://trial-kitesurf.luciferous.workers.dev/",
        "method": "GET",
        "path": "/"
      }
    },
    "scriptName": "trial-kitesurf",
    "eventType": "fetch",
    "executionModel": "stateless",
    "scriptVersion": {
      "id": "6133ae6b-cfb9-4c6b-b8e5-4e11728809d2"
    },
    "requestId": "a2e74538ea03d423"
  },
  "$metadata": {
    "id": "01M0HD0ZYAB7NBYBD4VDPYYG97",
    "requestId": "a2e74538ea03d423",
    "rayId": "a2e74538ea03d423",
    "trigger": "GET /",
    "service": "trial-kitesurf",
    "level": "error",
    "error": "Unable to connect to existing session undefined (it may still be in use or not ready yet) - retry or launch a new browser: Error: ws does not work in the browser. Browser clients must use the native WebSocket object",
    "message": "Unable to connect to existing session undefined (it may still be in use or not ready yet) - retry or launch a new browser: Error: ws does not work in the browser. Browser clients must use the native WebSocket object",
    "account": "00000000000000000000000000000000",
    "type": "cf-worker",
    "fingerprint": "dc0be69aa466bf5220d44ca4e4c70136",
    "origin": "fetch",
    "messageTemplate": "Unable to connect to existing session undefined (it may still be in use or not ready yet) - retry or launch a new browser: Error: ws does not work in the browser. Browser clients must use the native WebSocket object"
  }
}

エラーを元に調べたところ、 puppeteer.connect() において headers を指定しているとNode.js環境でした動かないとのことでした。

最終的に動いたもの

自分自身でWebSocketを張ってPuppeteerに渡します。

Cloudflare Workersでは fetch(url, { headers: { Upgrade: "websocket" }}) のリクエストを投げることで外向きのWebSocketを張ることができます。
このとき、Authorizationヘッダーを一緒に渡すことで認証付きWebSocketを張ることができます。

puppeteer.connect() には任意のConnectionTransportを渡すことができるので、認証付きWebSocketを使用するtransportを作成して渡します。

import puppeteer, { type ConnectionTransport } from "@cloudflare/puppeteer";

class WorkerWebSocketTransport implements ConnectionTransport {
  onmessage?: (message: string) => void;
  onclose?: () => void;

  constructor(private readonly ws: WebSocket) {
    ws.addEventListener("message", (event) => {
      if (typeof event.data === "string") {
        this.onmessage?.(event.data);
      } else if (event.data instanceof ArrayBuffer) {
        this.onmessage?.(new TextDecoder().decode(event.data));
      }
    });

    ws.addEventListener("close", () => {
      this.onclose?.();
    });
  }

  send(message: string): void {
    this.ws.send(message);
  }

  close() {
    this.ws.close();
  }
}

export default {
  async fetch(request, env): Promise<Response> {
    return main(env);
  },
} satisfies ExportedHandler<Env>;

async function main(env: Env) {
  const transport = await connectKitesurf(env);

  const browser = await puppeteer.connect({ transport });
  try {
    const page = await browser.newPage();
    await page.goto("https://ifconfig.io");

    await sleep(1000);

    const ss = await page.screenshot({ encoding: "binary", type: "png" });
    return new Response(ss, { headers: { "Content-Type": "image/png" } });
  } catch (e) {
    console.error(e);
    throw e;
  } finally {
    await browser.close();
  }
}

function makeEndpoint(env: Env) {
  return `https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/browser-run/devtools/browser?browser=kitesurf`;
}

async function connectKitesurf(env: Env): Promise<WorkerWebSocketTransport> {
  const endpoint = makeEndpoint(env);

  const resp = await fetch(endpoint, {
    headers: {
      Upgrade: "websocket",
      Authorization: `Bearer ${env.CF_API_TOKEN}`,
    },
  });

  if (!resp.webSocket) {
    const body = await resp.text();
    throw new Error(
      `Kitesurf WebSocket connection failed: ${resp.status} ${resp.statusText}: ${body}`,
    );
  }

  const ws = resp.webSocket;
  ws.binaryType = "arraybuffer";
  ws.accept();
  return new WorkerWebSocketTransport(ws);
}

function sleep(ms: number): Promise<void> {
  return new Promise((ok) => {
    setTimeout(ok, ms);
  });
}

このコードをデプロイし、URLを再度叩きます。

c64aa3cd-7839-4b88-93e8-8e929849f0e8

正常にアクセスしてスクリーンショットを撮ることができました。

まとめ

以上、Cloudflare WorkersでKitesurfを動かしてみました。
現状バインディングが対応していないので、自分でWebSocketを張ってトランスポートを作成する必要がありますが動かすことはできそうです。

何かのお役に立てたら幸いです。

この記事をシェアする

関連記事