Skip to content

Commit

Permalink
Throw pretty errors with unexpected new Miniflare() options
Browse files Browse the repository at this point in the history
  • Loading branch information
mrbbot committed Sep 6, 2023
1 parent dc36faa commit 1e1ee49
Show file tree
Hide file tree
Showing 8 changed files with 1,182 additions and 14 deletions.
94 changes: 83 additions & 11 deletions packages/miniflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import os from "os";
import path from "path";
import { Duplex, Transform, Writable } from "stream";
import { ReadableStream } from "stream/web";
import util from "util";
import zlib from "zlib";
import type {
CacheStorage,
Expand Down Expand Up @@ -96,6 +97,7 @@ import {
} from "./shared";
import { Storage } from "./storage";
import { CoreBindings, CoreHeaders } from "./workers";
import { _formatZodError } from "./zod-format";

// ===== `Miniflare` User Options =====
export type MiniflareOptions = SharedOptions &
Expand All @@ -109,12 +111,21 @@ type PluginSharedOptions = {
[Key in keyof Plugins]: OptionalZodTypeOf<Plugins[Key]["sharedOptions"]>;
};

function hasMultipleWorkers(opts: unknown): opts is { workers: unknown[] } {
return (
typeof opts === "object" &&
opts !== null &&
"workers" in opts &&
Array.isArray(opts.workers)
);
}

function validateOptions(
opts: MiniflareOptions
opts: unknown
): [PluginSharedOptions, PluginWorkerOptions[]] {
// Normalise options into shared and worker-specific
const sharedOpts = opts;
const multipleWorkers = "workers" in opts;
const multipleWorkers = hasMultipleWorkers(opts);
const workerOpts = multipleWorkers ? opts.workers : [opts];
if (workerOpts.length === 0) {
throw new MiniflareCoreError("ERR_NO_WORKERS", "No workers defined");
Expand All @@ -127,16 +138,76 @@ function validateOptions(
);

// Validate all options
for (const [key, plugin] of PLUGIN_ENTRIES) {
// @ts-expect-error `QueuesPlugin` doesn't define shared options
pluginSharedOpts[key] = plugin.sharedOptions?.parse(sharedOpts);
for (let i = 0; i < workerOpts.length; i++) {
// Make sure paths are correct in validation errors
const path = multipleWorkers ? ["workers", i] : undefined;
// @ts-expect-error `CoreOptionsSchema` has required options which are
// missing in other plugins' options.
pluginWorkerOpts[i][key] = plugin.options.parse(workerOpts[i], { path });
try {
for (const [key, plugin] of PLUGIN_ENTRIES) {
// @ts-expect-error types of individual plugin options are unknown
pluginSharedOpts[key] = plugin.sharedOptions?.parse(sharedOpts);
for (let i = 0; i < workerOpts.length; i++) {
// Make sure paths are correct in validation errors
const path = multipleWorkers ? ["workers", i] : undefined;
// @ts-expect-error types of individual plugin options are unknown
pluginWorkerOpts[i][key] = plugin.options.parse(workerOpts[i], {
path,
});
}
}
} catch (e) {
if (e instanceof z.ZodError) {
let formatted: string | undefined;
try {
formatted = _formatZodError(e, opts);
} catch (formatError) {
// If formatting failed for some reason, we'd like to know, so log a
// bunch of debugging information, including the full validation error
// so users at least know what was wrong.

const title = "[Miniflare] Validation Error Format Failure";
const message = [
"### Input",
"```",
util.inspect(opts, { depth: null }),
"```",
"",
"### Validation Error",
"```",
e.stack,
"```",
"",
"### Format Error",
"```",
typeof formatError === "object" &&
formatError !== null &&
"stack" in formatError &&
typeof formatError.stack === "string"
? formatError.stack
: String(formatError),
"```",
].join("\n");
const githubIssueUrl = new URL(
"https://github.com/cloudflare/miniflare/issues/new"
);
githubIssueUrl.searchParams.set("title", title);
githubIssueUrl.searchParams.set("body", message);

formatted = [
"Unable to format validation error.",
"Please open the following URL in your browser to create a GitHub issue:",
githubIssueUrl,
"",
message,
"",
].join("\n");
}
const error = new MiniflareCoreError(
"ERR_VALIDATION",
`Unexpected options passed to \`new Miniflare()\` constructor:\n${formatted}`
);
// Add the `cause` as a getter, so it isn't logged automatically with the
// error, but can still be accessed if needed
Object.defineProperty(error, "cause", { get: () => e });
throw error;
}
throw e;
}

// Validate names unique
Expand Down Expand Up @@ -1322,3 +1393,4 @@ export * from "./runtime";
export * from "./shared";
export * from "./storage";
export * from "./workers";
export * from "./zod-format";
1 change: 1 addition & 0 deletions packages/miniflare/src/shared/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type MiniflareCoreErrorCode =
| "ERR_PERSIST_REMOTE_UNSUPPORTED" // Remote storage is not supported for this database
| "ERR_FUTURE_COMPATIBILITY_DATE" // Compatibility date in the future
| "ERR_NO_WORKERS" // No workers defined
| "ERR_VALIDATION" // Options failed to parse
| "ERR_DUPLICATE_NAME" // Multiple workers defined with same name
| "ERR_DIFFERENT_UNIQUE_KEYS" // Multiple Durable Object bindings declared for same class with different unsafe unique keys
| "ERR_MULTIPLE_OUTBOUNDS"; // Both `outboundService` and `fetchMock` specified
Expand Down
Loading

0 comments on commit 1e1ee49

Please sign in to comment.