diff --git a/packages/miniflare/src/plugins/d1/index.ts b/packages/miniflare/src/plugins/d1/index.ts index 62435df81..e859d4786 100644 --- a/packages/miniflare/src/plugins/d1/index.ts +++ b/packages/miniflare/src/plugins/d1/index.ts @@ -10,6 +10,7 @@ import { SharedBindings } from "../../workers"; import { PersistenceSchema, Plugin, + SERVICE_LOOPBACK, getPersistPath, kProxyNodeBinding, migrateDatabase, @@ -112,6 +113,10 @@ export const D1_PLUGIN: Plugin< name: SharedBindings.MAYBE_SERVICE_BLOBS, service: { name: D1_STORAGE_SERVICE_NAME }, }, + { + name: SharedBindings.MAYBE_SERVICE_LOOPBACK, + service: { name: SERVICE_LOOPBACK }, + }, ], }, }; diff --git a/packages/miniflare/src/plugins/kv/index.ts b/packages/miniflare/src/plugins/kv/index.ts index baae0a8af..6bf4677f6 100644 --- a/packages/miniflare/src/plugins/kv/index.ts +++ b/packages/miniflare/src/plugins/kv/index.ts @@ -10,6 +10,7 @@ import { SharedBindings } from "../../workers"; import { PersistenceSchema, Plugin, + SERVICE_LOOPBACK, getPersistPath, kProxyNodeBinding, migrateDatabase, @@ -118,6 +119,10 @@ export const KV_PLUGIN: Plugin< name: SharedBindings.MAYBE_SERVICE_BLOBS, service: { name: KV_STORAGE_SERVICE_NAME }, }, + { + name: SharedBindings.MAYBE_SERVICE_LOOPBACK, + service: { name: SERVICE_LOOPBACK }, + }, ], }, }; diff --git a/packages/miniflare/src/plugins/r2/index.ts b/packages/miniflare/src/plugins/r2/index.ts index d92fc8331..46637b3b8 100644 --- a/packages/miniflare/src/plugins/r2/index.ts +++ b/packages/miniflare/src/plugins/r2/index.ts @@ -10,6 +10,7 @@ import { SharedBindings } from "../../workers"; import { PersistenceSchema, Plugin, + SERVICE_LOOPBACK, getPersistPath, kProxyNodeBinding, migrateDatabase, @@ -92,6 +93,10 @@ export const R2_PLUGIN: Plugin< name: SharedBindings.MAYBE_SERVICE_BLOBS, service: { name: R2_STORAGE_SERVICE_NAME }, }, + { + name: SharedBindings.MAYBE_SERVICE_LOOPBACK, + service: { name: SERVICE_LOOPBACK }, + }, ], }, }; diff --git a/packages/miniflare/src/workers/core/proxy.worker.ts b/packages/miniflare/src/workers/core/proxy.worker.ts index f2f1e8c8e..861dd380f 100644 --- a/packages/miniflare/src/workers/core/proxy.worker.ts +++ b/packages/miniflare/src/workers/core/proxy.worker.ts @@ -1,6 +1,6 @@ import assert from "node:assert"; import { parse } from "devalue"; -import { readPrefix } from "miniflare:shared"; +import { readPrefix, reduceError } from "miniflare:shared"; import { CoreHeaders, ProxyAddresses, @@ -42,22 +42,6 @@ const WORKERS_PLATFORM_IMPL: PlatformImpl = { }, }; -interface JsonError { - message?: string; - name?: string; - stack?: string; - cause?: JsonError; -} - -function reduceError(e: any): JsonError { - return { - name: e?.name, - message: e?.message ?? String(e), - stack: e?.stack, - cause: e?.cause === undefined ? undefined : reduceError(e.cause), - }; -} - // Helpers taken from `devalue` (unfortunately not exported): // https://github.com/Rich-Harris/devalue/blob/50af63e2b2c648f6e6ea29904a14faac25a581fc/src/utils.js#L31-L51 const objectProtoNames = Object.getOwnPropertyNames(Object.prototype) diff --git a/packages/miniflare/src/workers/shared/index.worker.ts b/packages/miniflare/src/workers/shared/index.worker.ts index 396056522..156022794 100644 --- a/packages/miniflare/src/workers/shared/index.worker.ts +++ b/packages/miniflare/src/workers/shared/index.worker.ts @@ -63,5 +63,5 @@ export type { DeferredPromiseResolve, DeferredPromiseReject } from "./sync"; export { Timers } from "./timers.worker"; export type { TimerHandle } from "./timers.worker"; -export { maybeApply } from "./types"; -export type { Awaitable, ValueOf } from "./types"; +export { maybeApply, reduceError } from "./types"; +export type { Awaitable, ValueOf, JsonError } from "./types"; diff --git a/packages/miniflare/src/workers/shared/object.worker.ts b/packages/miniflare/src/workers/shared/object.worker.ts index 4f0efc381..72e1dd92a 100644 --- a/packages/miniflare/src/workers/shared/object.worker.ts +++ b/packages/miniflare/src/workers/shared/object.worker.ts @@ -4,6 +4,7 @@ import { LogLevel, SharedBindings, SharedHeaders } from "./constants"; import { Router } from "./router.worker"; import { TypedSql, all, createTypedSql, isTypedValue } from "./sql.worker"; import { Timers } from "./timers.worker"; +import { reduceError } from "./types"; export interface MiniflareDurableObjectEnv { // NOTE: "in-memory" storage is never in-memory. We always back simulator @@ -127,15 +128,38 @@ export abstract class MiniflareDurableObject< this.#name = name; // Dispatch the request to the underlying router - const res = await super.fetch(req); - // Make sure we consume the request body if specified. Otherwise, calls - // which make requests to this object may hang and never resolve. - // See https://github.com/cloudflare/workerd/issues/960. - // Note `Router#fetch()` should never throw, returning 500 responses for - // unhandled exceptions. - if (req.body !== null && !req.bodyUsed) { - await req.body.pipeTo(new WritableStream()); + try { + return await super.fetch(req); + } catch (e) { + // `HttpError`s are handled by `Router`. If we threw another error log it. + const error = reduceError(e); + const fallback = error.stack ?? error.message; + + const loopbackService = this.env[SharedBindings.MAYBE_SERVICE_LOOPBACK]; + if (loopbackService !== undefined) { + // If we have a connected loopback service, log a source mapped error + void loopbackService + .fetch("http://localhost/core/error", { + method: "POST", + body: JSON.stringify(error), + }) + .catch(() => { + // ...falling back to `workerd` logging (requires `--verbose` flag) + console.error(fallback); + }); + } else { + // Otherwise, just use `workerd`'s logging (requires `--verbose` flag) + console.error(fallback); + } + + return new Response(fallback, { status: 500 }); + } finally { + // Make sure we consume the request body if specified. Otherwise, calls + // which make requests to this object may hang and never resolve. + // See https://github.com/cloudflare/workerd/issues/960. + if (req.body !== null && !req.bodyUsed) { + await req.body.pipeTo(new WritableStream()); + } } - return res; } } diff --git a/packages/miniflare/src/workers/shared/router.worker.ts b/packages/miniflare/src/workers/shared/router.worker.ts index 3b217eee5..7e4b678eb 100644 --- a/packages/miniflare/src/workers/shared/router.worker.ts +++ b/packages/miniflare/src/workers/shared/router.worker.ts @@ -42,11 +42,11 @@ export abstract class Router { if (match !== null) return await handlers[key](req, match.groups, url); } return new Response(null, { status: 404 }); - } catch (e: any) { + } catch (e) { if (e instanceof HttpError) { return e.toResponse(); } - return new Response(e?.stack ?? String(e), { status: 500 }); + throw e; } } } diff --git a/packages/miniflare/src/workers/shared/types.ts b/packages/miniflare/src/workers/shared/types.ts index c95dfb944..f9e5eb7f6 100644 --- a/packages/miniflare/src/workers/shared/types.ts +++ b/packages/miniflare/src/workers/shared/types.ts @@ -3,6 +3,22 @@ export type Awaitable = T | Promise; // { a: A, b: B, ... } => A | B | ... export type ValueOf = T[keyof T]; +export interface JsonError { + message?: string; + name?: string; + stack?: string; + cause?: JsonError; +} + +export function reduceError(e: any): JsonError { + return { + name: e?.name, + message: e?.message ?? String(e), + stack: e?.stack, + cause: e?.cause === undefined ? undefined : reduceError(e.cause), + }; +} + export function maybeApply( f: (value: From) => To, maybeValue: From | undefined diff --git a/scripts/build.mjs b/scripts/build.mjs index 4f83191be..37a217145 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -1,4 +1,3 @@ -import assert from "assert"; import fs from "fs/promises"; import path from "path"; import esbuild from "esbuild"; @@ -102,6 +101,8 @@ const embedWorkersPlugin = { format: "esm", target: "esnext", bundle: true, + sourcemap: true, + sourcesContent: false, external: ["miniflare:shared", "miniflare:zod"], metafile: true, incremental: watch, // Allow `rebuild()` calls if watching @@ -134,11 +135,12 @@ const embedWorkersPlugin = { const contents = ` import fs from "fs"; import path from "path"; + import url from "url"; let contents; export default function() { if (contents !== undefined) return contents; - const filePath = path.join(__dirname, "workers", ${outPath}) - contents = fs.readFileSync(filePath, "utf8"); + const filePath = path.join(__dirname, "workers", ${outPath}); + contents = fs.readFileSync(filePath, "utf8") + "//# sourceURL=" + url.pathToFileURL(filePath); return contents; } `;