From 7d89c44e9ee385f794da9ebc9b0d306c9b341ccb Mon Sep 17 00:00:00 2001 From: Luca Casonato Date: Wed, 30 Apr 2025 11:32:55 +0200 Subject: [PATCH 1/3] Revert "fix(ext/node): work correctly with wrapper Response objects, use correct `rawHeaders` structure (#29056)" This reverts commit 05574665b2940b76eb59e3245e502bac3632b803. --- ext/http/00_serve.ts | 29 +---- ext/node/polyfills/http.ts | 18 +-- .../node/wrapped_http_response/__test__.jsonc | 8 -- .../specs/node/wrapped_http_response/main.ts | 54 --------- .../node/wrapped_http_response/response.ts | 113 ------------------ tests/unit_node/http_test.ts | 39 ------ 6 files changed, 8 insertions(+), 253 deletions(-) delete mode 100644 tests/specs/node/wrapped_http_response/__test__.jsonc delete mode 100644 tests/specs/node/wrapped_http_response/main.ts delete mode 100644 tests/specs/node/wrapped_http_response/response.ts diff --git a/ext/http/00_serve.ts b/ext/http/00_serve.ts index cde00ba558ff73..5b55e8347bceb2 100644 --- a/ext/http/00_serve.ts +++ b/ext/http/00_serve.ts @@ -58,7 +58,6 @@ import { ResponsePrototype, toInnerResponse, } from "ext:deno_fetch/23_response.js"; -import { headerListFromHeaders } from "ext:deno_fetch/20_headers.js"; import { abortRequest, fromInnerRequest, @@ -501,15 +500,8 @@ function fastSyncResponseOrStream( return; } - let stream; - let body; - if (respBody.streamOrStatic) { - stream = respBody.streamOrStatic; - body = stream.body; - } else { - stream = respBody; - body = respBody; - } + const stream = respBody.streamOrStatic; + const body = stream.body; if (body !== undefined) { // We ensure the response has not been consumed yet in the caller of this // function. @@ -640,19 +632,8 @@ function mapToCallback(context, callback, onError) { return; } - let status; - let headers; - let body; - if (inner) { - status = inner.status; - headers = inner.headerList; - body = inner.body; - } else { - status = response.status; - headers = headerListFromHeaders(response.headers); - body = response.body; - } - + const status = inner.status; + const headers = inner.headerList; if (headers && headers.length > 0) { if (headers.length == 1) { op_http_set_response_header(req, headers[0][0], headers[0][1]); @@ -661,7 +642,7 @@ function mapToCallback(context, callback, onError) { } } - fastSyncResponseOrStream(req, body, status, innerRequest); + fastSyncResponseOrStream(req, inner.body, status, innerRequest); }; if (TRACING_ENABLED) { diff --git a/ext/node/polyfills/http.ts b/ext/node/polyfills/http.ts index 97b4d36f078178..706534e3be4533 100644 --- a/ext/node/polyfills/http.ts +++ b/ext/node/polyfills/http.ts @@ -1780,8 +1780,6 @@ Object.defineProperty(ServerResponse.prototype, "connection", { ), }); -const kRawHeaders = Symbol("rawHeaders"); - // TODO(@AaronO): optimize export class IncomingMessageForServer extends NodeReadable { #headers: Record; @@ -1821,7 +1819,7 @@ export class IncomingMessageForServer extends NodeReadable { this.method = ""; this.socket = socket; this.upgrade = null; - this[kRawHeaders] = []; + this.rawHeaders = []; socket?.on("error", (e) => { if (this.listenerCount("error") > 0) { this.emit("error", e); @@ -1844,7 +1842,7 @@ export class IncomingMessageForServer extends NodeReadable { get headers() { if (!this.#headers) { this.#headers = {}; - const entries = headersEntries(this[kRawHeaders]); + const entries = headersEntries(this.rawHeaders); for (let i = 0; i < entries.length; i++) { const entry = entries[i]; this.#headers[entry[0]] = entry[1]; @@ -1857,16 +1855,6 @@ export class IncomingMessageForServer extends NodeReadable { this.#headers = val; } - get rawHeaders() { - const entries = headersEntries(this[kRawHeaders]); - const out = new Array(entries.length * 2); - for (let i = 0; i < entries.length; i++) { - out[i * 2] = entries[i][0]; - out[i * 2 + 1] = entries[i][1]; - } - return out; - } - // connection is deprecated, but still tested in unit test. get connection() { return this.socket; @@ -1971,7 +1959,7 @@ export class ServerImpl extends EventEmitter { req.upgrade = request.headers.get("connection")?.toLowerCase().includes("upgrade") && request.headers.get("upgrade"); - req[kRawHeaders] = request.headers; + req.rawHeaders = request.headers; if (req.upgrade && this.listenerCount("upgrade") > 0) { const { conn, response } = upgradeHttpRaw(request); diff --git a/tests/specs/node/wrapped_http_response/__test__.jsonc b/tests/specs/node/wrapped_http_response/__test__.jsonc deleted file mode 100644 index 610d3ac8297928..00000000000000 --- a/tests/specs/node/wrapped_http_response/__test__.jsonc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "tempDir": true, - - "steps": [{ - "args": "run -A main.ts", - "output": "done\n" - }] -} diff --git a/tests/specs/node/wrapped_http_response/main.ts b/tests/specs/node/wrapped_http_response/main.ts deleted file mode 100644 index 3f7f14deca514f..00000000000000 --- a/tests/specs/node/wrapped_http_response/main.ts +++ /dev/null @@ -1,54 +0,0 @@ -// Adapted from https://github.com/honojs/node-server/blob/1eb73c6d985665e75458ddd08c23bbc1dbdc7bcd/src/listener.ts -// and https://github.com/honojs/node-server/blob/1eb73c6d985665e75458ddd08c23bbc1dbdc7bcd/src/server.ts -import { - buildOutgoingHttpHeaders, - Response as WrappedResponse, -} from "./response.ts"; -import { createServer, OutgoingHttpHeaders, ServerResponse } from "node:http"; - -Object.defineProperty(globalThis, "Response", { - value: WrappedResponse, -}); - -const { promise, resolve } = Promise.withResolvers(); - -const responseViaResponseObject = async ( - res: Response, - outgoing: ServerResponse, -) => { - const resHeaderRecord: OutgoingHttpHeaders = buildOutgoingHttpHeaders( - res.headers, - ); - - if (res.body) { - const buffer = await res.arrayBuffer(); - resHeaderRecord["content-length"] = buffer.byteLength; - - outgoing.writeHead(res.status, resHeaderRecord); - outgoing.end(new Uint8Array(buffer)); - } else { - outgoing.writeHead(res.status, resHeaderRecord); - outgoing.end(); - } -}; - -const server = createServer((_req, res) => { - const response = new Response("Hello, world!"); - return responseViaResponseObject(response, res); -}); - -using _server = { - [Symbol.dispose]() { - server.close(); - }, -}; - -server.listen(0, async () => { - const { port } = server.address() as { port: number }; - const response = await fetch(`http://localhost:${port}`); - await response.text(); - resolve(); -}); - -await promise; -console.log("done"); diff --git a/tests/specs/node/wrapped_http_response/response.ts b/tests/specs/node/wrapped_http_response/response.ts deleted file mode 100644 index 63f3b48df74e1d..00000000000000 --- a/tests/specs/node/wrapped_http_response/response.ts +++ /dev/null @@ -1,113 +0,0 @@ -// Adapted from https://github.com/honojs/node-server/blob/1eb73c6d985665e75458ddd08c23bbc1dbdc7bcd/src/response.ts -// deno-lint-ignore-file no-explicit-any -// -import type { OutgoingHttpHeaders } from "node:http"; - -interface InternalBody { - source: string | Uint8Array | FormData | Blob | null; - stream: ReadableStream; - length: number | null; -} - -const GlobalResponse = globalThis.Response; - -const responseCache = Symbol("responseCache"); -const getResponseCache = Symbol("getResponseCache"); -export const cacheKey = Symbol("cache"); - -export const buildOutgoingHttpHeaders = ( - headers: Headers | HeadersInit | null | undefined, -): OutgoingHttpHeaders => { - const res: OutgoingHttpHeaders = {}; - if (!(headers instanceof Headers)) { - headers = new Headers(headers ?? undefined); - } - - const cookies = []; - for (const [k, v] of headers) { - if (k === "set-cookie") { - cookies.push(v); - } else { - res[k] = v; - } - } - if (cookies.length > 0) { - res["set-cookie"] = cookies; - } - res["content-type"] ??= "text/plain; charset=UTF-8"; - - return res; -}; - -export class Response { - #body?: BodyInit | null; - #init?: ResponseInit; - - [getResponseCache](): typeof GlobalResponse { - delete (this as any)[cacheKey]; - return ((this as any)[responseCache] ||= new GlobalResponse( - this.#body, - this.#init, - )); - } - - constructor(body?: BodyInit | null, init?: ResponseInit) { - this.#body = body; - if (init instanceof Response) { - const cachedGlobalResponse = (init as any)[responseCache]; - if (cachedGlobalResponse) { - this.#init = cachedGlobalResponse; - // instantiate GlobalResponse cache and this object always returns value from global.Response - this[getResponseCache](); - return; - } else { - this.#init = init.#init; - } - } else { - this.#init = init; - } - - if ( - typeof body === "string" || - typeof (body as ReadableStream)?.getReader !== "undefined" - ) { - let headers = - (init?.headers || { "content-type": "text/plain; charset=UTF-8" }) as - | Record - | Headers - | OutgoingHttpHeaders; - if (headers instanceof Headers) { - headers = buildOutgoingHttpHeaders(headers); - } - - (this as any)[cacheKey] = [init?.status || 200, body, headers]; - } - } -} -[ - "body", - "bodyUsed", - "headers", - "ok", - "redirected", - "status", - "statusText", - "trailers", - "type", - "url", -].forEach((k) => { - Object.defineProperty(Response.prototype, k, { - get() { - return this[getResponseCache]()[k]; - }, - }); -}); -["arrayBuffer", "blob", "clone", "formData", "json", "text"].forEach((k) => { - Object.defineProperty(Response.prototype, k, { - value: function () { - return this[getResponseCache]()[k](); - }, - }); -}); -Object.setPrototypeOf(Response, GlobalResponse); -Object.setPrototypeOf(Response.prototype, GlobalResponse.prototype); diff --git a/tests/unit_node/http_test.ts b/tests/unit_node/http_test.ts index 2ea23ab8435238..0867f5cc771b72 100644 --- a/tests/unit_node/http_test.ts +++ b/tests/unit_node/http_test.ts @@ -2041,42 +2041,3 @@ Deno.test("[node/http] 'close' event is emitted on ServerResponse object when th await new Promise((resolve) => server.close(resolve)); assert(responseCloseEmitted); }); - -Deno.test("[node/http] rawHeaders are in flattened format", async () => { - const getHeader = (req: IncomingMessage, name: string) => { - const idx = req.rawHeaders.indexOf(name); - if (idx < 0) { - throw new Error(`Header ${name} not found`); - } - return [name, req.rawHeaders[idx + 1]]; - }; - const { promise, resolve } = Promise.withResolvers(); - const server = http.createServer((req, res) => { - resolve(); - // TODO(nathanwhit): the raw headers should not be lowercased, they should be - // exactly as they appeared in the request - assertEquals(getHeader(req, "content-type"), [ - "content-type", - "text/plain", - ]); - assertEquals(getHeader(req, "set-cookie"), [ - "set-cookie", - "foo=bar", - ]); - res.end(); - }); - - server.listen(0, async () => { - const { port } = server.address() as { port: number }; - const response = await fetch(`http://localhost:${port}`, { - headers: { - "Set-Cookie": "foo=bar", - "Content-Type": "text/plain", - }, - }); - await response.body?.cancel(); - }); - - await promise; - await new Promise((resolve) => server.close(resolve)); -}); From 621805c20457b357167c727fdc32ef459bd8611b Mon Sep 17 00:00:00 2001 From: Luca Casonato Date: Wed, 30 Apr 2025 11:37:53 +0200 Subject: [PATCH 2/3] fix(ext/node): don't use user manipulated Response objects, use correct `rawHeaders` structure Fixes https://github.com/denoland/deno/issues/28022 Previously, when a user manipulated the global `Response` object, we would use that object when passing the response from the Node shim to `Deno.serve`. This caused issues when the user manipulated the `Response` object in a way that would break `Deno.serve`. The raw headers bug is that we were exposing the `rawHeaders` field on `Incoming` as a `Headers` object, instead it's supposed to be a flat array of the header keys + values. I.e. `["Content-Type:", "application/json", "Host:", "http://localhost"]` Co-authored-by: Nathan Whitaker --- ext/node/polyfills/http.ts | 19 ++- .../node/wrapped_http_response/__test__.jsonc | 8 ++ .../specs/node/wrapped_http_response/main.ts | 54 +++++++++ .../node/wrapped_http_response/response.ts | 113 ++++++++++++++++++ tests/unit_node/http_test.ts | 39 ++++++ 5 files changed, 230 insertions(+), 3 deletions(-) create mode 100644 tests/specs/node/wrapped_http_response/__test__.jsonc create mode 100644 tests/specs/node/wrapped_http_response/main.ts create mode 100644 tests/specs/node/wrapped_http_response/response.ts diff --git a/ext/node/polyfills/http.ts b/ext/node/polyfills/http.ts index 706534e3be4533..ef3be50b74caea 100644 --- a/ext/node/polyfills/http.ts +++ b/ext/node/polyfills/http.ts @@ -67,6 +67,7 @@ import { import { getTimerDuration } from "ext:deno_node/internal/timers.mjs"; import { serve, upgradeHttpRaw } from "ext:deno_http/00_serve.ts"; import { headersEntries } from "ext:deno_fetch/20_headers.js"; +import { Response } from "ext:deno_fetch/23_response.js"; import { builtinTracer, ContextManager, @@ -1780,6 +1781,8 @@ Object.defineProperty(ServerResponse.prototype, "connection", { ), }); +const kRawHeaders = Symbol("rawHeaders"); + // TODO(@AaronO): optimize export class IncomingMessageForServer extends NodeReadable { #headers: Record; @@ -1819,7 +1822,7 @@ export class IncomingMessageForServer extends NodeReadable { this.method = ""; this.socket = socket; this.upgrade = null; - this.rawHeaders = []; + this[kRawHeaders] = []; socket?.on("error", (e) => { if (this.listenerCount("error") > 0) { this.emit("error", e); @@ -1842,7 +1845,7 @@ export class IncomingMessageForServer extends NodeReadable { get headers() { if (!this.#headers) { this.#headers = {}; - const entries = headersEntries(this.rawHeaders); + const entries = headersEntries(this[kRawHeaders]); for (let i = 0; i < entries.length; i++) { const entry = entries[i]; this.#headers[entry[0]] = entry[1]; @@ -1855,6 +1858,16 @@ export class IncomingMessageForServer extends NodeReadable { this.#headers = val; } + get rawHeaders() { + const entries = headersEntries(this[kRawHeaders]); + const out = new Array(entries.length * 2); + for (let i = 0; i < entries.length; i++) { + out[i * 2] = entries[i][0]; + out[i * 2 + 1] = entries[i][1]; + } + return out; + } + // connection is deprecated, but still tested in unit test. get connection() { return this.socket; @@ -1959,7 +1972,7 @@ export class ServerImpl extends EventEmitter { req.upgrade = request.headers.get("connection")?.toLowerCase().includes("upgrade") && request.headers.get("upgrade"); - req.rawHeaders = request.headers; + req[kRawHeaders] = request.headers; if (req.upgrade && this.listenerCount("upgrade") > 0) { const { conn, response } = upgradeHttpRaw(request); diff --git a/tests/specs/node/wrapped_http_response/__test__.jsonc b/tests/specs/node/wrapped_http_response/__test__.jsonc new file mode 100644 index 00000000000000..610d3ac8297928 --- /dev/null +++ b/tests/specs/node/wrapped_http_response/__test__.jsonc @@ -0,0 +1,8 @@ +{ + "tempDir": true, + + "steps": [{ + "args": "run -A main.ts", + "output": "done\n" + }] +} diff --git a/tests/specs/node/wrapped_http_response/main.ts b/tests/specs/node/wrapped_http_response/main.ts new file mode 100644 index 00000000000000..3f7f14deca514f --- /dev/null +++ b/tests/specs/node/wrapped_http_response/main.ts @@ -0,0 +1,54 @@ +// Adapted from https://github.com/honojs/node-server/blob/1eb73c6d985665e75458ddd08c23bbc1dbdc7bcd/src/listener.ts +// and https://github.com/honojs/node-server/blob/1eb73c6d985665e75458ddd08c23bbc1dbdc7bcd/src/server.ts +import { + buildOutgoingHttpHeaders, + Response as WrappedResponse, +} from "./response.ts"; +import { createServer, OutgoingHttpHeaders, ServerResponse } from "node:http"; + +Object.defineProperty(globalThis, "Response", { + value: WrappedResponse, +}); + +const { promise, resolve } = Promise.withResolvers(); + +const responseViaResponseObject = async ( + res: Response, + outgoing: ServerResponse, +) => { + const resHeaderRecord: OutgoingHttpHeaders = buildOutgoingHttpHeaders( + res.headers, + ); + + if (res.body) { + const buffer = await res.arrayBuffer(); + resHeaderRecord["content-length"] = buffer.byteLength; + + outgoing.writeHead(res.status, resHeaderRecord); + outgoing.end(new Uint8Array(buffer)); + } else { + outgoing.writeHead(res.status, resHeaderRecord); + outgoing.end(); + } +}; + +const server = createServer((_req, res) => { + const response = new Response("Hello, world!"); + return responseViaResponseObject(response, res); +}); + +using _server = { + [Symbol.dispose]() { + server.close(); + }, +}; + +server.listen(0, async () => { + const { port } = server.address() as { port: number }; + const response = await fetch(`http://localhost:${port}`); + await response.text(); + resolve(); +}); + +await promise; +console.log("done"); diff --git a/tests/specs/node/wrapped_http_response/response.ts b/tests/specs/node/wrapped_http_response/response.ts new file mode 100644 index 00000000000000..63f3b48df74e1d --- /dev/null +++ b/tests/specs/node/wrapped_http_response/response.ts @@ -0,0 +1,113 @@ +// Adapted from https://github.com/honojs/node-server/blob/1eb73c6d985665e75458ddd08c23bbc1dbdc7bcd/src/response.ts +// deno-lint-ignore-file no-explicit-any +// +import type { OutgoingHttpHeaders } from "node:http"; + +interface InternalBody { + source: string | Uint8Array | FormData | Blob | null; + stream: ReadableStream; + length: number | null; +} + +const GlobalResponse = globalThis.Response; + +const responseCache = Symbol("responseCache"); +const getResponseCache = Symbol("getResponseCache"); +export const cacheKey = Symbol("cache"); + +export const buildOutgoingHttpHeaders = ( + headers: Headers | HeadersInit | null | undefined, +): OutgoingHttpHeaders => { + const res: OutgoingHttpHeaders = {}; + if (!(headers instanceof Headers)) { + headers = new Headers(headers ?? undefined); + } + + const cookies = []; + for (const [k, v] of headers) { + if (k === "set-cookie") { + cookies.push(v); + } else { + res[k] = v; + } + } + if (cookies.length > 0) { + res["set-cookie"] = cookies; + } + res["content-type"] ??= "text/plain; charset=UTF-8"; + + return res; +}; + +export class Response { + #body?: BodyInit | null; + #init?: ResponseInit; + + [getResponseCache](): typeof GlobalResponse { + delete (this as any)[cacheKey]; + return ((this as any)[responseCache] ||= new GlobalResponse( + this.#body, + this.#init, + )); + } + + constructor(body?: BodyInit | null, init?: ResponseInit) { + this.#body = body; + if (init instanceof Response) { + const cachedGlobalResponse = (init as any)[responseCache]; + if (cachedGlobalResponse) { + this.#init = cachedGlobalResponse; + // instantiate GlobalResponse cache and this object always returns value from global.Response + this[getResponseCache](); + return; + } else { + this.#init = init.#init; + } + } else { + this.#init = init; + } + + if ( + typeof body === "string" || + typeof (body as ReadableStream)?.getReader !== "undefined" + ) { + let headers = + (init?.headers || { "content-type": "text/plain; charset=UTF-8" }) as + | Record + | Headers + | OutgoingHttpHeaders; + if (headers instanceof Headers) { + headers = buildOutgoingHttpHeaders(headers); + } + + (this as any)[cacheKey] = [init?.status || 200, body, headers]; + } + } +} +[ + "body", + "bodyUsed", + "headers", + "ok", + "redirected", + "status", + "statusText", + "trailers", + "type", + "url", +].forEach((k) => { + Object.defineProperty(Response.prototype, k, { + get() { + return this[getResponseCache]()[k]; + }, + }); +}); +["arrayBuffer", "blob", "clone", "formData", "json", "text"].forEach((k) => { + Object.defineProperty(Response.prototype, k, { + value: function () { + return this[getResponseCache]()[k](); + }, + }); +}); +Object.setPrototypeOf(Response, GlobalResponse); +Object.setPrototypeOf(Response.prototype, GlobalResponse.prototype); diff --git a/tests/unit_node/http_test.ts b/tests/unit_node/http_test.ts index 0867f5cc771b72..2ea23ab8435238 100644 --- a/tests/unit_node/http_test.ts +++ b/tests/unit_node/http_test.ts @@ -2041,3 +2041,42 @@ Deno.test("[node/http] 'close' event is emitted on ServerResponse object when th await new Promise((resolve) => server.close(resolve)); assert(responseCloseEmitted); }); + +Deno.test("[node/http] rawHeaders are in flattened format", async () => { + const getHeader = (req: IncomingMessage, name: string) => { + const idx = req.rawHeaders.indexOf(name); + if (idx < 0) { + throw new Error(`Header ${name} not found`); + } + return [name, req.rawHeaders[idx + 1]]; + }; + const { promise, resolve } = Promise.withResolvers(); + const server = http.createServer((req, res) => { + resolve(); + // TODO(nathanwhit): the raw headers should not be lowercased, they should be + // exactly as they appeared in the request + assertEquals(getHeader(req, "content-type"), [ + "content-type", + "text/plain", + ]); + assertEquals(getHeader(req, "set-cookie"), [ + "set-cookie", + "foo=bar", + ]); + res.end(); + }); + + server.listen(0, async () => { + const { port } = server.address() as { port: number }; + const response = await fetch(`http://localhost:${port}`, { + headers: { + "Set-Cookie": "foo=bar", + "Content-Type": "text/plain", + }, + }); + await response.body?.cancel(); + }); + + await promise; + await new Promise((resolve) => server.close(resolve)); +}); From a85823c94497e41d82565c78c338f3fbcfecf35b Mon Sep 17 00:00:00 2001 From: Luca Casonato Date: Wed, 30 Apr 2025 11:47:42 +0200 Subject: [PATCH 3/3] fix(ext/http): require genuuine `Response` returned from `Deno.serve` --- ext/http/00_serve.ts | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/ext/http/00_serve.ts b/ext/http/00_serve.ts index 5b55e8347bceb2..6d08195fc6cc5d 100644 --- a/ext/http/00_serve.ts +++ b/ext/http/00_serve.ts @@ -54,8 +54,8 @@ import { InnerBody } from "ext:deno_fetch/22_body.js"; import { Event } from "ext:deno_web/02_event.js"; import { fromInnerResponse, + InnerResponse, newInnerResponse, - ResponsePrototype, toInnerResponse, } from "ext:deno_fetch/23_response.js"; import { @@ -556,6 +556,7 @@ function mapToCallback(context, callback, onError) { // 500 error. let innerRequest; let response; + let innerResponse: InnerResponse | undefined; try { innerRequest = new InnerRequest(req, context); const request = fromInnerRequest(innerRequest, "immutable"); @@ -567,8 +568,8 @@ function mapToCallback(context, callback, onError) { response = await callback(request, new ServeHandlerInfo(innerRequest)); - // Throwing Error if the handler return value is not a Response class - if (!ObjectPrototypeIsPrototypeOf(ResponsePrototype, response)) { + innerResponse = toInnerResponse(response); + if (innerResponse === undefined) { throw new TypeError( "Return value from serve handler must be a response or a promise resolving to a response", ); @@ -588,9 +589,23 @@ function mapToCallback(context, callback, onError) { } catch (error) { try { response = await onError(error); - if (!ObjectPrototypeIsPrototypeOf(ResponsePrototype, response)) { + + innerResponse = toInnerResponse(response); + if (innerResponse === undefined) { + throw new TypeError( + "Return value from the onError handler must be a response or a promise resolving to a response", + ); + } + + if (response.type === "error") { + throw new TypeError( + "Return value from the onError handler must not be an error response (like Response.error())", + ); + } + + if (response.bodyUsed) { throw new TypeError( - "Return value from onError handler must be a response or a promise resolving to a response", + "The body of the Response returned from the onError handler has already been consumed", ); } } catch (error) { @@ -603,6 +618,7 @@ function mapToCallback(context, callback, onError) { error, ); response = internalServerError(); + innerResponse = toInnerResponse(response); } } @@ -610,7 +626,6 @@ function mapToCallback(context, callback, onError) { updateSpanFromResponse(span, response); } - const inner = toInnerResponse(response); if (innerRequest?.[_upgraded]) { // We're done here as the connection has been upgraded during the callback and no longer requires servicing. if (response !== UPGRADE_RESPONSE_SENTINEL) { @@ -632,8 +647,8 @@ function mapToCallback(context, callback, onError) { return; } - const status = inner.status; - const headers = inner.headerList; + const status = innerResponse.status; + const headers = innerResponse.headerList; if (headers && headers.length > 0) { if (headers.length == 1) { op_http_set_response_header(req, headers[0][0], headers[0][1]); @@ -642,7 +657,7 @@ function mapToCallback(context, callback, onError) { } } - fastSyncResponseOrStream(req, inner.body, status, innerRequest); + fastSyncResponseOrStream(req, innerResponse.body, status, innerRequest); }; if (TRACING_ENABLED) {