ch03-execution-model
對應 03. 執行模型:fetch handler、ctx 與請求生命週期
在 GitHub 上檢視·4 個檔案·4.0 KB
取得並執行
這個範例可以獨立 clone 執行,不依賴其他章節。
npx degit mrsuner/cloudflare-serverless-platform-manuals/examples/ch03-execution-model
cd ch03-execution-model
npm install可用指令
npm run dev # wrangler dev
npm run deploy # wrangler deploy
npm run typecheck # tsc --noEmit
npm run cf-typegen # wrangler types --env-interface CloudflareBindings說明
ch03 — Invocation lifecycle lab
Section titled “ch03 — Invocation lifecycle lab”Companion example for docs/03-execution-model.md.
npm installnpm run cf-typegennpm run devExperiments
Section titled “Experiments”1. What is really on ctx
Section titled “1. What is really on ctx”$ curl -s localhost:8787/ctx{"own":["tracing","access","cache","props","exports"], "prototype":["waitUntil","passThroughOnException","constructor"]}Seven members. The official Context docs page documents four —
tracing, access and cache are undocumented there.
2. Background work without waitUntil is discarded
Section titled “2. Background work without waitUntil is discarded”$ curl -s localhost:8787/reset$ curl -s "localhost:8787/fire?mode=none"{"mode":"none","started":true}$ sleep 4 && curl -s localhost:8787/events{"events":["none:started"]} # none:completed never appears
$ curl -s "localhost:8787/fire?mode=waituntil"{"mode":"waituntil","started":true}$ sleep 4 && curl -s localhost:8787/events{"events":["none:started","waituntil:started","waituntil:completed"]}The only difference between the two paths is the ctx.waitUntil(task) call.
Reproducible locally.
3. request.cf is a convincing fake in local dev
Section titled “3. request.cf is a convincing fake in local dev”$ curl -s localhost:8787/cf{"cf":{"asn":395747,"colo":"DFW","city":"Austin","region":"Texas","postalCode":"78701","country":"US","timezone":"America/Chicago","latitude":"30.27130","longitude":"-97.74260","tlsVersion":"TLSv1.3",...}}Nobody here is in Austin, Texas. Wrangler tries to fetch a real cf object and
falls back to a placeholder, logging only:
[wrangler:warn] Unable to fetch the `Request.cf` object! Falling back to a default placeholder...Every field is well-formed and internally consistent, so geo logic “works” locally against fabricated data. Verify after deploying.
4. passThroughOnException
Section titled “4. passThroughOnException”$ curl -o /dev/null -w "%{http_code}\n" "localhost:8787/boom" # 500$ curl -o /dev/null -w "%{http_code}\n" "localhost:8787/boom?pass=1" # 500Both 500 locally and on workers.dev — there is no origin to pass through to.
The difference only shows on a route in front of a real origin.
Docs vs runtime
Section titled “Docs vs runtime”The Context docs say ctx.exports “requires enable_ctx_exports compatibility
flag”. Adding it makes the Worker fail to start:
✘ [ERROR] The Workers runtime failed to start. Runtime stderr: The compatibility flag enable_ctx_exports became the default as of 2025-11-17 so does not need to be specified anymore.Omit the flag. ctx.exports is already there.
Verified
Section titled “Verified”2026-07-28 · wrangler@4.114.0 · workerd@1.20260722.1 · compatibility_date: 2026-07-24
原始碼
wrangler.jsonc
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "ch03-execution-model",
"main": "src/index.ts",
"compatibility_date": "2026-07-24",
"observability": { "enabled": true }
}package.json
{
"name": "ch03-execution-model",
"private": true,
"type": "module",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"typecheck": "tsc --noEmit",
"cf-typegen": "wrangler types --env-interface CloudflareBindings"
},
"devDependencies": { "typescript": "^5.9.0", "wrangler": "^4.114.0" }
}src/index.ts
// ---------------------------------------------------------------------------
// Chapter 03 — The invocation lifecycle.
//
// Module scope: shared by every request this isolate serves (chapter 01).
// Used here only as an observation log, never for per-request state.
// ---------------------------------------------------------------------------
const events: string[] = [];
const record = (e: string) => void events.push(e);
export default {
async fetch(
request: Request,
env: CloudflareBindings,
ctx: ExecutionContext,
): Promise<Response> {
const url = new URL(request.url);
switch (url.pathname) {
// --- What is actually on `ctx`? --------------------------------------
// The docs page lists four members. The runtime has seven.
case "/ctx":
return Response.json({
own: Object.getOwnPropertyNames(ctx),
prototype: Object.getOwnPropertyNames(Object.getPrototypeOf(ctx)),
});
// --- Geo / TLS metadata ----------------------------------------------
// In `wrangler dev` this is a realistic-looking PLACEHOLDER, not your
// real location. See the README.
case "/cf":
return Response.json({ cf: request.cf ?? null });
// --- Does background work survive the response? -----------------------
// /fire?mode=none -> fire and forget: killed, never completes
// /fire?mode=waituntil -> handed to ctx.waitUntil(): completes
case "/fire": {
const mode = url.searchParams.get("mode") ?? "none";
const task = (async () => {
await scheduler.wait(2000);
record(`${mode}:completed`);
})();
if (mode === "waituntil") ctx.waitUntil(task);
record(`${mode}:started`);
return Response.json({ mode, started: true });
}
// --- Fail open instead of returning 500 -------------------------------
// With ?pass=1 the request is passed through to the origin instead of
// becoming an error. On a workers.dev route there is no origin, so the
// observable difference is in the logs, not the response.
case "/boom":
if (url.searchParams.get("pass") === "1") ctx.passThroughOnException();
throw new Error("deliberate failure");
case "/events":
return Response.json({ events });
case "/reset":
events.length = 0;
return Response.json({ ok: true });
default:
return new Response(
[
"Chapter 03 — invocation lifecycle",
"",
" GET /ctx real shape of the ExecutionContext",
" GET /cf request.cf (placeholder in local dev)",
" GET /fire?mode=none background work WITHOUT waitUntil",
" GET /fire?mode=waituntil background work WITH waitUntil",
" GET /events what actually completed",
" GET /reset clear the log",
" GET /boom[?pass=1] exception, with/without passthrough",
"",
].join("\n"),
{ status: 404, headers: { "content-type": "text/plain; charset=utf-8" } },
);
}
},
} satisfies ExportedHandler<CloudflareBindings>;tsconfig.json
{
"compilerOptions": {
"target": "esnext", "lib": ["esnext"], "module": "esnext",
"moduleResolution": "bundler", "types": ["./worker-configuration.d.ts"],
"strict": true, "skipLibCheck": true, "noEmit": true, "isolatedModules": true
},
"include": ["src/**/*.ts", "worker-configuration.d.ts"]
}