diff --git a/.changeset/fix-custom-request-constructor.md b/.changeset/fix-custom-request-constructor.md new file mode 100644 index 000000000..609b0033b --- /dev/null +++ b/.changeset/fix-custom-request-constructor.md @@ -0,0 +1,7 @@ +--- +"@opennextjs/cloudflare": patch +--- + +fix: handle plain Request-like objects in `CustomRequest` constructor + +workerd's `Request` constructor coerces plain objects to `"[object Object]"` instead of reading `.url`, causing `TypeError: Invalid URL`. The fix extracts `.url` and merges method/headers/body from plain objects before passing to `super()`. diff --git a/packages/cloudflare/src/cli/templates/init.ts b/packages/cloudflare/src/cli/templates/init.ts index fc0a1d113..ed3ea6068 100644 --- a/packages/cloudflare/src/cli/templates/init.ts +++ b/packages/cloudflare/src/cli/templates/init.ts @@ -77,7 +77,10 @@ function initRuntime() { return __original_fetch(input, init); }; - const CustomRequest = class extends globalThis.Request { + // workerd calls toString() on plain objects instead of reading .url, + // so capture the native constructor for instanceof checks. + const OriginalRequest = globalThis.Request; + const CustomRequest = class extends OriginalRequest { constructor(input: RequestInfo | URL, init?: RequestInit) { if (init) { delete (init as { cache: unknown }).cache; @@ -88,7 +91,21 @@ function initRuntime() { value: init.body instanceof stream.Readable ? ReadableStream.from(init.body) : init.body, }); } - super(input, init); + if (typeof input === "string" || input instanceof URL || input instanceof OriginalRequest) { + super(input, init); + } else { + // Plain Request like object from third-party middleware or edge adapters + // extract .url and merge properties so they aren't lost. + const req = input as unknown as Request; + const merged = { + method: req.method, + headers: req.headers, + body: req.body, + ...(req.body ? { duplex: "half" as const } : {}), + ...init, + }; + super(req.url, merged as RequestInit); + } } };