Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

ローカルで Webhook をテストする

Cloudflare Worker と Cloudflare Tunnel を使い、Cloudflare Stream の Webhook 通知をローカルでテストします。

最終更新 Markdown で表示Agent セットアップ

Cloudflare Stream は、localhost やローカル IP アドレスへ Webhook 通知 を送れません。ローカル開発中に Webhook をテストするには、ローカルマシンへリクエストを転送する公開 URL が必要です。

この例では、次の手順を行います。

  1. Cloudflare Tunnel を起動し、ローカル環境向けの公開 URL を取得します。
  2. その URL を Webhook エンドポイントとして登録し、署名用シークレットを受け取ります。
  3. Stream の Webhook イベントを受け取り、署名を検証する Cloudflare Worker を作成します。

前提条件

1. Worker プロジェクトを作成する

Webhook リクエストを受け取る新しい Worker プロジェクトを作成します。

npm create cloudflare@latest stream-webhook-handler

2. Cloudflare Tunnel を起動する

Webhook URL を登録する前に、ローカルマシンを指す公開 URL が必要です。ターミナルで、Wrangler 開発サーバーのデフォルトポート(8787)へ転送する クイックトンネル を起動します。

npx cloudflared tunnel --url http://localhost:8787

cloudflared は、次のような公開 URL を出力します。

https://example-words-here.trycloudflare.com

この URL をコピーします。トンネルを再起動するたびに変わります。

3. トンネル URL を Webhook エンドポイントとして登録する

Stream API で、トンネル URL を Webhook 通知 URL に設定します。API レスポンスの secret フィールドは、Webhook 署名の検証に使います。

Required API token permissions

At least one of the following token permissions is required:
  • Stream Write
Create VOD webhooksbash
curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/stream/webhook" \
	--request PUT \
	--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
	--json '{
		"notificationUrl": "https://example-words-here.trycloudflare.com"
	}'

レスポンスには secret フィールドが含まれます。

Example responsejson
{
	"result": {
		"notificationUrl": "https://example-words-here.trycloudflare.com",
		"modified": "2024-01-01T00:00:00.000000Z",
		"secret": "85011ed3a913c6ad5f9cf6c5573cc0a7"
	},
	"success": true,
	"errors": [],
	"messages": []
}

secret の値を保存します。次の手順で使います。

4. ローカル開発用に Webhook シークレットを保存する

Worker プロジェクトのルートに .dev.vars ファイルを作成し、API レスポンスの Webhook シークレットを追加します。

.dev.varstxt
WEBHOOK_SECRET=85011ed3a913c6ad5f9cf6c5573cc0a7

値は手順 3 の実際のシークレットに置き換えます。wrangler dev 実行時、Wrangler は .dev.vars を自動で読み込みます。

5. Webhook ハンドラーを追加する

Worker プロジェクトの src/index.ts を次のコードで置き換えます。この Worker は Webhook の POST リクエストを受け取り、署名を検証 し、ペイロードをログに出します。

src/index.tsts
export interface Env {
	WEBHOOK_SECRET: string;
}

async function verifyWebhookSignature(
	request: Request,
	secret: string,
): Promise<{ valid: boolean; body: string }> {
	const signatureHeader = request.headers.get("Webhook-Signature");
	if (!signatureHeader) {
		return { valid: false, body: "" };
	}

	const body = await request.text();

	// Parse "time=<unix_ts>,sig1=<hex_signature>"
	const parts = Object.fromEntries(
		signatureHeader.split(",").map((part) => {
			const [key, value] = part.split("=");
			return [key, value];
		}),
	);

	const time = parts["time"];
	const receivedSig = parts["sig1"];

	if (!time || !receivedSig) {
		return { valid: false, body };
	}

	// Build the source string: "<time>.<body>"
	const sourceString = `${time}.${body}`;
	const encoder = new TextEncoder();

	const key = await crypto.subtle.importKey(
		"raw",
		encoder.encode(secret),
		{ name: "HMAC", hash: "SHA-256" },
		false,
		["sign"],
	);

	const signature = await crypto.subtle.sign(
		"HMAC",
		key,
		encoder.encode(sourceString),
	);

	const expectedSig = [...new Uint8Array(signature)]
		.map((b) => b.toString(16).padStart(2, "0"))
		.join("");

	// Use a timing-safe comparison.
	// Do not return early when lengths differ — that leaks the expected
	// signature's length through timing.  Compare against self and negate instead.
	const expectedBytes = encoder.encode(expectedSig);
	const receivedBytes = encoder.encode(receivedSig);

	const lengthsMatch = expectedBytes.byteLength === receivedBytes.byteLength;
	const signaturesMatch = lengthsMatch
		? crypto.subtle.timingSafeEqual(expectedBytes, receivedBytes)
		: !crypto.subtle.timingSafeEqual(expectedBytes, expectedBytes);

	return { valid: signaturesMatch, body };
}

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		if (request.method !== "POST") {
			return new Response("Method not allowed", { status: 405 });
		}

		if (!env.WEBHOOK_SECRET) {
			console.error("WEBHOOK_SECRET is not set");
			return new Response("Server misconfigured", { status: 500 });
		}

		const { valid, body } = await verifyWebhookSignature(
			request,
			env.WEBHOOK_SECRET,
		);

		if (!valid) {
			console.error("Invalid webhook signature");
			return new Response("Invalid signature", { status: 403 });
		}

		console.log("Webhook signature verified successfully");

		const payload = JSON.parse(body);

		console.log("Stream webhook received:", JSON.stringify(payload, null, 2));
		console.log("Video UID:", payload.uid);
		console.log("Status:", payload.status?.state);
		console.log("Ready to stream:", payload.readyToStream);

		// Add your own processing logic here — for example, update a database
		// or notify a downstream service.

		return new Response("OK", { status: 200 });
	},
} satisfies ExportedHandler<Env>;

6. ローカル開発サーバーを起動する

別のターミナルで(トンネルは起動したまま)、Wrangler で Worker をローカル起動します。

npx wrangler dev

Wrangler は .dev.vars から WEBHOOK_SECRET を自動で読み込みます。

7. テストイベントを発生させる

Stream に動画をアップロードして、Webhook イベントを発生させます。動画の処理が終わると、wrangler dev を実行しているターミナルに Webhook ペイロードと、署名が検証されたことの確認が表示されます。

本番環境へ移す

ローカルテストが終わったら、Worker をデプロイし、Webhook URL を本番エンドポイントに更新します。

npx wrangler deploy

次に、Webhook の購読先をデプロイ済み Worker の URL に更新します。

Required API token permissions

At least one of the following token permissions is required:
  • Stream Write
Create VOD webhooksbash
curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/stream/webhook" \
	--request PUT \
	--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
	--json '{
		"notificationUrl": "https://your-worker.your-subdomain.workers.dev"
	}'

役に立ちましたか?