Skip to content

Commit

Permalink
fix(ext/http): fix crash in dropped Deno.serve requests (denoland#21252)
Browse files Browse the repository at this point in the history
Fixes denoland#21250

We were attempting to recycle dropped resource responses too early.
  • Loading branch information
mmastrac committed Nov 18, 2023
1 parent c213ad3 commit 679b7bb
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 62 deletions.
130 changes: 69 additions & 61 deletions cli/tests/unit/serve_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2727,73 +2727,81 @@ 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 and that we correctly
// close/cancel the response
Deno.test({
permissions: { read: true, write: true, net: true },
name: `httpServerTcpCancellation_${url}`,
fn: async function () {
const ac = new AbortController();
const streamCancelled = url == "stream" ? deferred() : undefined;
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) => {
let respBody = null;
if (req.url.includes("/text")) {
respBody = "text";
} else if (req.url.includes("/file")) {
respBody = (await makeTempFile(1024)).readable;
} else if (req.url.includes("/stream")) {
respBody = new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array([1]));
},
cancel(reason) {
streamCancelled!.resolve(reason);
},
});
} else {
fail();
}
waitForRequest.resolve();
await waitForAbort;
// Allocate the request body
req.body;
return new Response(respBody);
},
});
for (const delay of ["delay", "nodelay"]) {
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 and that we correctly
// close/cancel the response
Deno.test({
permissions: { read: true, write: true, net: true },
name: `httpServerTcpCancellation_${url}_${delay}`,
fn: async function () {
const ac = new AbortController();
const streamCancelled = url == "stream" ? deferred() : undefined;
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) => {
let respBody = null;
if (req.url.includes("/text")) {
respBody = "text";
} else if (req.url.includes("/file")) {
respBody = (await makeTempFile(1024)).readable;
} else if (req.url.includes("/stream")) {
respBody = new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array([1]));
},
cancel(reason) {
streamCancelled!.resolve(reason);
},
});
} else {
fail();
}
waitForRequest.resolve();
await waitForAbort;

await listeningPromise;
if (delay == "delay") {
await new Promise((r) => setTimeout(r, 1000));
}
// Allocate the request body
req.body;
return new Response(respBody);
},
});

// 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();
await writer.write(new TextEncoder().encode(`POST /${url} HTTP/1.0\n\n`));
await waitForRequest;
await writer.close();
await listeningPromise;

waitForAbort.resolve();
// 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();
await writer.write(
new TextEncoder().encode(`POST /${url} HTTP/1.0\n\n`),
);
await waitForRequest;
await writer.close();

// Wait for cancellation before we shut the server down
if (streamCancelled !== undefined) {
await streamCancelled;
}
waitForAbort.resolve();

// Since the handler has a chance of creating resources or running async
// ops, we need to use a graceful shutdown here to ensure they have fully
// drained.
await server.shutdown();
// Wait for cancellation before we shut the server down
if (streamCancelled !== undefined) {
await streamCancelled;
}

await server.finished;
},
});
// Since the handler has a chance of creating resources or running async
// ops, we need to use a graceful shutdown here to ensure they have fully
// drained.
await server.shutdown();

await server.finished;
},
});
}
}

Deno.test(
Expand Down
3 changes: 2 additions & 1 deletion ext/http/http_next.rs
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,8 @@ pub async fn op_http_set_response_body_resource(
}
};

*http.needs_close_after_finish() = true;

set_response(
http.clone(),
resource.size_hint().1.map(|s| s as usize),
Expand All @@ -726,7 +728,6 @@ pub async fn op_http_set_response_body_resource(
},
);

*http.needs_close_after_finish() = true;
http.response_body_finished().await;
Ok(())
}
Expand Down

0 comments on commit 679b7bb

Please sign in to comment.