diff --git a/cli/tests/unit/http_test.ts b/cli/tests/unit/http_test.ts index ab43323bd4..d4c35545f5 100644 --- a/cli/tests/unit/http_test.ts +++ b/cli/tests/unit/http_test.ts @@ -272,3 +272,48 @@ unitTest( await promise; }, ); + +unitTest( + { perms: { net: true } }, + async function httpServerNextRequestErrorExposedInResponse() { + const promise = (async () => { + const listener = Deno.listen({ port: 4501 }); + const conn = await listener.accept(); + const httpConn = Deno.serveHttp(conn); + const event = await httpConn.nextRequest(); + assert(event); + // Start polling for the next request before awaiting response. + const nextRequestPromise = httpConn.nextRequest(); + const { respondWith } = event; + await assertThrowsAsync( + async () => { + let interval = 0; + await respondWith( + new Response( + new ReadableStream({ + start(controller) { + interval = setInterval(() => { + const message = `data: ${Date.now()}\n\n`; + controller.enqueue(new TextEncoder().encode(message)); + }, 200); + }, + cancel() { + clearInterval(interval); + }, + }), + ), + ); + }, + Deno.errors.Http, + "connection closed", + ); + // The error from `op_http_request_next` reroutes to `respondWith()`. + assertEquals(await nextRequestPromise, null); + listener.close(); + })(); + + const resp = await fetch("http://127.0.0.1:4501/"); + await resp.body!.cancel(); + await promise; + }, +); diff --git a/runtime/js/40_http.js b/runtime/js/40_http.js index eb3c58a632..eb4d214ca4 100644 --- a/runtime/js/40_http.js +++ b/runtime/js/40_http.js @@ -14,6 +14,8 @@ return new HttpConn(rid); } + const connErrorSymbol = Symbol("connError"); + class HttpConn { #rid = 0; @@ -35,10 +37,16 @@ this.#rid, ); } catch (error) { + // A connection error seen here would cause disrupted responses to throw + // a generic `BadResource` error. Instead store this error and replace + // those with it. + this[connErrorSymbol] = error; if (error instanceof errors.BadResource) { return null; } else if (error instanceof errors.Interrupted) { return null; + } else if (error.message.includes("connection closed")) { + return null; } throw error; } @@ -66,7 +74,7 @@ ); const request = fromInnerRequest(innerRequest, "immutable"); - const respondWith = createRespondWith(responseSenderRid, this.#rid); + const respondWith = createRespondWith(this, responseSenderRid); return { request, respondWith }; } @@ -97,7 +105,7 @@ ); } - function createRespondWith(responseSenderRid) { + function createRespondWith(httpConn, responseSenderRid) { return async function respondWith(resp) { if (resp instanceof Promise) { resp = await resp; @@ -145,6 +153,11 @@ innerResp.headerList, ], respBody instanceof Uint8Array ? respBody : null); } catch (error) { + const connError = httpConn[connErrorSymbol]; + if (error instanceof errors.BadResource && connError != null) { + // deno-lint-ignore no-ex-assign + error = new connError.constructor(connError.message); + } if (respBody !== null && respBody instanceof ReadableStream) { await respBody.cancel(error); } @@ -173,6 +186,11 @@ value, ); } catch (error) { + const connError = httpConn[connErrorSymbol]; + if (error instanceof errors.BadResource && connError != null) { + // deno-lint-ignore no-ex-assign + error = new connError.constructor(connError.message); + } await reader.cancel(error); throw error; } @@ -180,7 +198,9 @@ } finally { // Once all chunks are sent, and the request body is closed, we can // close the response body. - await Deno.core.opAsync("op_http_response_close", responseBodyRid); + try { + await Deno.core.opAsync("op_http_response_close", responseBodyRid); + } catch { /* pass */ } } } };