From 2cc78de1a6af91040c0feeeb103755f25fa30bee Mon Sep 17 00:00:00 2001 From: Okiki Ojo Date: Mon, 8 Jun 2026 07:17:05 +0000 Subject: [PATCH] fix(cloudflare): return debug-friendly worker errors Cloudflare was falling back to its default error page when the worker threw an uncaught exception. That made debugging much harder because users could not see the actual failure coming from bundlejs. Wrap the worker fetch handler in a top-level error boundary and always return an application-controlled response. Reuse the existing HTML error rendering for thrown msg payloads, and return JSON for unexpected failures with useful fields such as the error message, stack, cause, and status when available. This keeps internal worker failures visible to users and makes production issues diagnosable without relying on Cloudflare's branded error pages. The worker typecheck still passes after the change. --- cloudflare/src/index.ts | 349 ++++++++++++++++++++++++---------------- 1 file changed, 207 insertions(+), 142 deletions(-) diff --git a/cloudflare/src/index.ts b/cloudflare/src/index.ts index 4c85928..ca3e775 100644 --- a/cloudflare/src/index.ts +++ b/cloudflare/src/index.ts @@ -2,7 +2,7 @@ import { executePreparedBundle } from "../../edge/execute.ts"; import type { BundleResult } from "../../edge/bundle.ts"; import { prepareBundleRequest } from "../../edge/request.ts"; import { getPackageResultKey, headers } from "../../edge/constants.ts"; -import { generateWorkerResult } from "./result.ts"; +import { generateHTMLMessages, generateWorkerResult } from "./result.ts"; import type { Env } from "./types.ts"; import ESBUILD_WASM_MODULE from "../../core/esbuild.wasm"; import { @@ -79,73 +79,144 @@ function contentTypeForWellKnown(ext: string): string { return "application/json"; } -export default { - async fetch(request: Request, env: Env): Promise { - const url = new URL(request.url); +function isErrorWithMessages(error: unknown): error is { msgs: string[] } { + return typeof error === "object" && error !== null && Array.isArray((error as { msgs?: unknown }).msgs); +} - if (url.pathname === "/favicon.ico") { - return Response.redirect("https://bundlejs.com/favicon/favicon-api.ico"); +function serializeErrorPayload(error: unknown): string { + const seen = new WeakSet(); + + return JSON.stringify(error, (_key, value) => { + if (value instanceof Error) { + return { + error: value.message, + name: value.name, + stack: value.stack, + cause: value.cause, + }; } - if (url.pathname === "/apple-touch-icon.png" || url.pathname === "/apple-touch-icon-precomposed.png") { - return Response.redirect("https://bundlejs.com/favicon/apple-touch-icon.png"); + if (typeof value === "bigint") { + return value.toString(); } - if (url.pathname === "/sw.js" || url.pathname === "/robots.txt" || url.pathname === "/llms.txt") { - return Response.json({ message: "no" }); + if (typeof value === "object" && value !== null) { + if (seen.has(value)) { + return "[Circular]"; + } + + seen.add(value); } - if (url.pathname.startsWith("/.well-known/")) { - const assetResponse = await env.ASSETS.fetch(request); - const ext = url.pathname.includes(".") ? `.${url.pathname.split(".").pop()}` : ""; + return value; + }) ?? JSON.stringify({ error: String(error) }); +} - return new Response(await assetResponse.arrayBuffer(), { - status: assetResponse.status, +function createDebugErrorResponse(error: unknown): Response { + if (isErrorWithMessages(error) && error.msgs.length > 0) { + try { + return new Response(generateHTMLMessages(error.msgs), { + status: 404, headers: [ ...headers, - ["Cache-Control", "max-age=180, public"], - ["Content-Type", contentTypeForWellKnown(ext)], + ["Cache-Control", "no-store"], + ["Content-Type", "text/html"], ], }); + } catch (msgsError) { + console.warn("error-rendering-debug-messages:", msgsError); } + } - if (url.searchParams.has("docs")) { - return Response.redirect("https://blog.okikio.dev/documenting-an-online-bundler-bundlejs#heading-configuration"); - } + const errorStatus = typeof error === "object" && error !== null + ? (error as { status?: unknown }).status + : undefined; + const status = typeof errorStatus === "number" ? errorStatus : 500; + + const payload = typeof error === "object" && error !== null + ? serializeErrorPayload(error) + : JSON.stringify({ error: String(error) }); + + return new Response(payload, { + status, + headers: [ + ...headers, + ["Cache-Control", "no-store"], + ["Content-Type", "application/json"], + ], + }); +} + +export default { + async fetch(request: Request, env: Env): Promise { + try { + const url = new URL(request.url); + + if (url.pathname === "/favicon.ico") { + return Response.redirect("https://bundlejs.com/favicon/favicon-api.ico"); + } + + if (url.pathname === "/apple-touch-icon.png" || url.pathname === "/apple-touch-icon-precomposed.png") { + return Response.redirect("https://bundlejs.com/favicon/apple-touch-icon.png"); + } + + if (url.pathname === "/sw.js" || url.pathname === "/robots.txt" || url.pathname === "/llms.txt") { + return Response.json({ message: "no" }); + } + + if (url.pathname.startsWith("/.well-known/")) { + const assetResponse = await env.ASSETS.fetch(request); + const ext = url.pathname.includes(".") ? `.${url.pathname.split(".").pop()}` : ""; + + return new Response(await assetResponse.arrayBuffer(), { + status: assetResponse.status, + headers: [ + ...headers, + ["Cache-Control", "max-age=180, public"], + ["Content-Type", contentTypeForWellKnown(ext)], + ], + }); + } + + if (url.searchParams.has("docs")) { + return Response.redirect("https://blog.okikio.dev/documenting-an-online-bundler-bundlejs#heading-configuration"); + } + + const badgeQuery = url.searchParams.has("badge") || ["/badge", "/badge/raster", "/badge-raster"].includes(url.pathname); + const fileCheck = url.searchParams.has("file") || url.pathname === "/file"; + + const preparedRequest = await prepareBundleRequest(url); + const { + badgeID, + badgeKey, + bundleKey, + exportAll, + jsonKey, + modules, + mutationQueries, + shareQuery, + textQuery, + } = preparedRequest; + + const artifactKey = getArtifactKey(bundleKey); + const startedAt = Date.now(); + + if (url.pathname === "/clear-all-cache-123") { + const clearArtifacts = url.searchParams.has("gist") || url.searchParams.has("gists"); + + if (clearArtifacts) { + await deleteAllKv(env); + await deleteAllArtifacts(env); + return new Response("Started clearing cache including gists!\n\nCleared entire cache + gists...careful now.", { + headers: { + "Content-Type": "text/plain", + "x-content-type-options": "nosniff", + }, + }); + } - const badgeQuery = url.searchParams.has("badge") || ["/badge", "/badge/raster", "/badge-raster"].includes(url.pathname); - const badgeResult = url.searchParams.get("badge"); - const badgeStyle = url.searchParams.get("badge-style"); - const badgeRasterQuery = - url.searchParams.has("badge-raster") || - url.searchParams.has("png") || - ["/badge/raster", "/badge-raster"].includes(url.pathname); - - const fileCheck = url.searchParams.has("file") || url.pathname === "/file"; - - const preparedRequest = await prepareBundleRequest(url); - const { - badgeID, - badgeKey, - bundleKey, - exportAll, - jsonKey, - modules, - mutationQueries, - shareQuery, - textQuery, - } = preparedRequest; - - const artifactKey = getArtifactKey(bundleKey); - const startedAt = Date.now(); - - if (url.pathname === "/clear-all-cache-123") { - const clearArtifacts = url.searchParams.has("gist") || url.searchParams.has("gists"); - - if (clearArtifacts) { await deleteAllKv(env); - await deleteAllArtifacts(env); - return new Response("Started clearing cache including gists!\n\nCleared entire cache + gists...careful now.", { + return new Response("Started clearing cache!\nCleared entire cache", { headers: { "Content-Type": "text/plain", "x-content-type-options": "nosniff", @@ -153,116 +224,110 @@ export default { }); } - await deleteAllKv(env); - return new Response("Started clearing cache!\nCleared entire cache", { - headers: { - "Content-Type": "text/plain", - "x-content-type-options": "nosniff", - }, - }); - } + if (url.pathname === "/delete-cache") { + try { + const packageCacheKey = modules.length === 1 && exportAll && !mutationQueries && modules[0]?.[1] === "export" + ? `${getPackageResultKey(modules[0][0])}/${jsonKey}` + : null; - if (url.pathname === "/delete-cache") { - try { - const packageCacheKey = modules.length === 1 && exportAll && !mutationQueries && modules[0]?.[1] === "export" - ? `${getPackageResultKey(modules[0][0])}/${jsonKey}` - : null; + await deleteCachedBundleResult(env, jsonKey); + if (packageCacheKey) await deleteCachedBundleResult(env, packageCacheKey); + await deleteCachedBadgeKey(env, badgeKey); + if (hasArtifactStorage(env)) { + await deleteBundleArtifact(env, artifactKey); + } - await deleteCachedBundleResult(env, jsonKey); - if (packageCacheKey) await deleteCachedBundleResult(env, packageCacheKey); - await deleteCachedBadgeKey(env, badgeKey); - if (hasArtifactStorage(env)) { - await deleteBundleArtifact(env, artifactKey); + return new Response("Deleted from cache!"); + } catch (error) { + console.warn(error); + return new Response("Error, deleting from cache"); } - - // Intentionally matches the Deno handler's plain-text response. - return new Response("Deleted from cache!"); - } catch (error) { - console.warn(error); - return new Response("Error, deleting from cache"); } - } - try { - if (url.pathname !== "/no-cache") { - const packageCacheKey = modules.length === 1 && exportAll && !mutationQueries && modules[0]?.[1] === "export" - ? `${getPackageResultKey(modules[0][0])}/${jsonKey}` - : null; + try { + if (url.pathname !== "/no-cache") { + const packageCacheKey = modules.length === 1 && exportAll && !mutationQueries && modules[0]?.[1] === "export" + ? `${getPackageResultKey(modules[0][0])}/${jsonKey}` + : null; - let cachedResult: BundleResult | null = await getCachedBundleResult(env, jsonKey); + let cachedResult: BundleResult | null = await getCachedBundleResult(env, jsonKey); - if (!cachedResult && packageCacheKey) { - cachedResult = await getCachedBundleResult(env, packageCacheKey); - } + if (!cachedResult && packageCacheKey) { + cachedResult = await getCachedBundleResult(env, packageCacheKey); + } - if (badgeQuery && cachedResult) { - const cachedBadge = await getCachedBadge(env, badgeKey, badgeID); - if (cachedBadge) { - return createCachedBadgeResponse(cachedBadge); + if (badgeQuery && cachedResult) { + const cachedBadge = await getCachedBadge(env, badgeKey, badgeID); + if (cachedBadge) { + return createCachedBadgeResponse(cachedBadge); + } + } else if (badgeQuery && !cachedResult) { + await deleteCachedBadgeKey(env, badgeKey); } - } else if (badgeQuery && !cachedResult) { - await deleteCachedBadgeKey(env, badgeKey); - } - const fileAvailable = !fileCheck - ? true - : hasArtifactStorage(env) - ? Boolean(await env.BUNDLE_ARTIFACTS.head(artifactKey)) - : false; - - if (cachedResult && fileAvailable) { - return await generateWorkerResult( - env, - [badgeKey, badgeID], - [cachedResult, undefined], - url, - true, - Date.now() - startedAt, - artifactKey - ); + const fileAvailable = !fileCheck + ? true + : hasArtifactStorage(env) + ? Boolean(await env.BUNDLE_ARTIFACTS.head(artifactKey)) + : false; + + if (cachedResult && fileAvailable) { + return await generateWorkerResult( + env, + [badgeKey, badgeID], + [cachedResult, undefined], + url, + true, + Date.now() - startedAt, + artifactKey + ); + } } + } catch (error) { + console.warn("error-using-cache:", error); } - } catch (error) { - console.warn("error-using-cache:", error); - } - const [response, resultText] = await executePreparedBundle(url, preparedRequest, { wasmModule: ESBUILD_WASM_MODULE }); + const [response, resultText] = await executePreparedBundle(url, preparedRequest, { wasmModule: ESBUILD_WASM_MODULE }); - if (!response.ok) { - const responseHeaders = response.headers; - const status = response.status; - return new Response(await response.arrayBuffer(), { - headers: responseHeaders, - status, - }); - } + if (!response.ok) { + const responseHeaders = response.headers; + const status = response.status; + return new Response(await response.arrayBuffer(), { + headers: responseHeaders, + status, + }); + } - const value: BundleResult = await response.json(); + const value: BundleResult = await response.json(); - try { - if (hasArtifactStorage(env)) { - await putBundleArtifact(env, artifactKey, resultText); - } - await putCachedBundleResult(env, jsonKey, value); + try { + if (hasArtifactStorage(env)) { + await putBundleArtifact(env, artifactKey, resultText); + } + await putCachedBundleResult(env, jsonKey, value); + + if (modules.length === 1 && exportAll && !(shareQuery || textQuery) && modules[0]?.[1] === "export") { + const packageCacheKey = `${getPackageResultKey(modules[0][0])}/${jsonKey}`; + await putCachedBundleResult(env, packageCacheKey, value, { permanent: true }); + } - if (modules.length === 1 && exportAll && !(shareQuery || textQuery) && modules[0]?.[1] === "export") { - const packageCacheKey = `${getPackageResultKey(modules[0][0])}/${jsonKey}`; - await putCachedBundleResult(env, packageCacheKey, value, { permanent: true }); + await deleteCachedBadgeKey(env, badgeKey); + } catch (error) { + console.warn(error); } - await deleteCachedBadgeKey(env, badgeKey); + return await generateWorkerResult( + env, + [badgeKey, badgeID], + [value, resultText], + url, + false, + Date.now() - startedAt, + hasArtifactStorage(env) ? artifactKey : null + ); } catch (error) { - console.warn(error); + console.error("worker-request-error:", error); + return createDebugErrorResponse(error); } - - return await generateWorkerResult( - env, - [badgeKey, badgeID], - [value, resultText], - url, - false, - Date.now() - startedAt, - hasArtifactStorage(env) ? artifactKey : null - ); } } satisfies ExportedHandler; \ No newline at end of file -- 2.51.2