diff options
author | Matt Mastracci <matthew@mastracci.com> | 2023-08-28 13:29:34 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-08-28 13:29:34 -0600 |
commit | 9198bbd454c39f4d62f43ea729affe8cb789304a (patch) | |
tree | 6b644622d4788ca7626939b6e9fd26518d299a4d /cli | |
parent | 539e5032d372679869dcee4e2e63f2116288b435 (diff) |
fix(ext/http): don't panic on stream responses in cancelled requests (#20316)
When a TCP connection is force-closed (ie: browser refresh), the
underlying future we pass to Hyper is dropped which may cause us to try
to drop the body resource while the OpState lock is still held.
Preconditions for this bug to trigger:
- The body resource must have been taken
- The response must return a resource (which requires us to take the
OpState lock)
- The TCP connection must have been dropped before this
Fixes #20315 and #20298
Diffstat (limited to 'cli')
-rw-r--r-- | cli/tests/unit/serve_test.ts | 62 |
1 files changed, 62 insertions, 0 deletions
diff --git a/cli/tests/unit/serve_test.ts b/cli/tests/unit/serve_test.ts index 05affc4f8..d1ac82696 100644 --- a/cli/tests/unit/serve_test.ts +++ b/cli/tests/unit/serve_test.ts @@ -2403,6 +2403,68 @@ Deno.test( }, ); +for (const url of ["text", "file", "stream"]) { + // Ensure that we don't panic when the incoming TCP request was dropped + // https://github.com/denoland/deno/issues/20315 + Deno.test({ + permissions: { read: true, write: true, net: true }, + name: `httpServerTcpCancellation_${url}`, + fn: async function () { + const ac = new AbortController(); + const listeningPromise = deferred(); + const waitForAbort = deferred(); + const waitForRequest = deferred(); + const server = Deno.serve({ + port: servePort, + signal: ac.signal, + onListen: onListen(listeningPromise), + handler: async (req: Request) => { + waitForRequest.resolve(); + await waitForAbort; + // Allocate the request body + let _body = req.body; + if (req.url.includes("/text")) { + return new Response("text"); + } else if (req.url.includes("/file")) { + return new Response((await makeTempFile(1024)).readable); + } else if (req.url.includes("/stream")) { + return new Response( + new ReadableStream({ + start(controller) { + _body = null; + controller.enqueue(new Uint8Array([1])); + controller.close(); + }, + }), + ); + } else { + fail(); + } + }, + }); + + await listeningPromise; + + // Create a POST request and drop it once the server has received it + const conn = await Deno.connect({ port: servePort }); + const writer = conn.writable.getWriter(); + writer.write(new TextEncoder().encode(`POST /${url} HTTP/1.0\n\n`)); + await waitForRequest; + writer.close(); + + // Give it a few milliseconds for the serve machinery to work + await new Promise((r) => setTimeout(r, 10)); + waitForAbort.resolve(); + + // Give it a few milliseconds for the serve machinery to work + await new Promise((r) => setTimeout(r, 10)); + + ac.abort(); + await server.finished; + }, + }); +} + Deno.test( { permissions: { read: true, net: true } }, async function httpServerWithTls() { |