Skip to content

Commit

Permalink
Add internal source-mapped error logging and breakpoint debugging
Browse files Browse the repository at this point in the history
This change massively improves the debuggability of Miniflare's
internal Durable Objects. Uncaught errors throws by Durable Objects
will now be source-mapped and logged. `//# sourceURL` comments are
also added to all Miniflare internal scripts, allowing for breakpoint
debugging.
  • Loading branch information
mrbbot committed Sep 11, 2023
1 parent 42d1ded commit a2e1776
Show file tree
Hide file tree
Showing 9 changed files with 74 additions and 33 deletions.
5 changes: 5 additions & 0 deletions packages/miniflare/src/plugins/d1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { SharedBindings } from "../../workers";
import {
PersistenceSchema,
Plugin,
SERVICE_LOOPBACK,
getPersistPath,
kProxyNodeBinding,
migrateDatabase,
Expand Down Expand Up @@ -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 },
},
],
},
};
Expand Down
5 changes: 5 additions & 0 deletions packages/miniflare/src/plugins/kv/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { SharedBindings } from "../../workers";
import {
PersistenceSchema,
Plugin,
SERVICE_LOOPBACK,
getPersistPath,
kProxyNodeBinding,
migrateDatabase,
Expand Down Expand Up @@ -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 },
},
],
},
};
Expand Down
5 changes: 5 additions & 0 deletions packages/miniflare/src/plugins/r2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { SharedBindings } from "../../workers";
import {
PersistenceSchema,
Plugin,
SERVICE_LOOPBACK,
getPersistPath,
kProxyNodeBinding,
migrateDatabase,
Expand Down Expand Up @@ -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 },
},
],
},
};
Expand Down
18 changes: 1 addition & 17 deletions packages/miniflare/src/workers/core/proxy.worker.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -42,22 +42,6 @@ const WORKERS_PLATFORM_IMPL: PlatformImpl<ReadableStream> = {
},
};

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)
Expand Down
4 changes: 2 additions & 2 deletions packages/miniflare/src/workers/shared/index.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
42 changes: 33 additions & 9 deletions packages/miniflare/src/workers/shared/object.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
}
4 changes: 2 additions & 2 deletions packages/miniflare/src/workers/shared/router.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions packages/miniflare/src/workers/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@ export type Awaitable<T> = T | Promise<T>;
// { a: A, b: B, ... } => A | B | ...
export type ValueOf<T> = 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<From, To>(
f: (value: From) => To,
maybeValue: From | undefined
Expand Down
8 changes: 5 additions & 3 deletions scripts/build.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import assert from "assert";
import fs from "fs/promises";
import path from "path";
import esbuild from "esbuild";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
`;
Expand Down

0 comments on commit a2e1776

Please sign in to comment.