diff --git a/ext/http/00_serve.ts b/ext/http/00_serve.ts index 7c665ac154d53b..84b8cd321b0672 100644 --- a/ext/http/00_serve.ts +++ b/ext/http/00_serve.ts @@ -753,26 +753,52 @@ function serveHttpOn(context, addr, callback) { PromisePrototypeCatch(callback(req), promiseErrorHandler); } - if (!context.closing && !context.closed) { - context.closing = op_http_close(rid, false); + try { + if (!context.closing && !context.closed) { + context.closing = await op_http_close(rid, false); + context.close(); + } + + await context.closing; + } catch (error) { + if (ObjectPrototypeIsPrototypeOf(InterruptedPrototype, error)) { + return; + } + if (ObjectPrototypeIsPrototypeOf(BadResourcePrototype, error)) { + return; + } + + throw error; + } finally { context.close(); + context.closed = true; } - - await context.closing; - context.close(); - context.closed = true; })(); return { addr, finished, async shutdown() { - if (!context.closing && !context.closed) { - // Shut this HTTP server down gracefully - context.closing = op_http_close(context.serverRid, true); + try { + if (!context.closing && !context.closed) { + // Shut this HTTP server down gracefully + context.closing = op_http_close(context.serverRid, true); + } + + await context.closing; + } catch (error) { + // The server was interrupted + if (ObjectPrototypeIsPrototypeOf(InterruptedPrototype, error)) { + return; + } + if (ObjectPrototypeIsPrototypeOf(BadResourcePrototype, error)) { + return; + } + + throw error; + } finally { + context.closed = true; } - await context.closing; - context.closed = true; }, ref() { ref = true; diff --git a/ext/node/polyfills/http.ts b/ext/node/polyfills/http.ts index 32e69772d6cdd7..c6c112a508a744 100644 --- a/ext/node/polyfills/http.ts +++ b/ext/node/polyfills/http.ts @@ -1775,8 +1775,12 @@ export class ServerImpl extends EventEmitter { } if (listening && this.#ac) { - this.#ac.abort(); - this.#ac = undefined; + if (this.#server) { + this.#server.shutdown(); + } else if (this.#ac) { + this.#ac.abort(); + this.#ac = undefined; + } } else { this.#serveDeferred!.resolve(); } @@ -1785,6 +1789,26 @@ export class ServerImpl extends EventEmitter { return this; } + closeAllConnections() { + if (this.#hasClosed) { + return; + } + if (this.#ac) { + this.#ac.abort(); + this.#ac = undefined; + } + } + + closeIdleConnections() { + if (this.#hasClosed) { + return; + } + + if (this.#server) { + this.#server.shutdown(); + } + } + address() { return { port: this.#addr.port, diff --git a/tests/unit_node/http_test.ts b/tests/unit_node/http_test.ts index 2b26442721e979..ca3c658adb84e4 100644 --- a/tests/unit_node/http_test.ts +++ b/tests/unit_node/http_test.ts @@ -2,6 +2,7 @@ import EventEmitter from "node:events"; import http, { type RequestOptions } from "node:http"; +import url from "node:url"; import https from "node:https"; import net from "node:net"; import { assert, assertEquals, fail } from "@std/assert/mod.ts"; @@ -1040,3 +1041,66 @@ Deno.test("[node/http] ServerResponse default status code 200", () => { Deno.test("[node/http] maxHeaderSize is defined", () => { assertEquals(http.maxHeaderSize, 16_384); }); + +Deno.test("[node/http] server graceful close", async () => { + const server = http.createServer(function (_, response) { + response.writeHead(200, {}); + response.end("ok"); + server.close(); + }); + + const { promise, resolve } = Promise.withResolvers(); + server.listen(0, function () { + // deno-lint-ignore no-explicit-any + const port = (server.address() as any).port; + const testURL = url.parse( + `http://localhost:${port}`, + ); + + http.request(testURL, function (response) { + assertEquals(response.statusCode, 200); + response.on("data", function () {}); + response.on("end", function () { + resolve(); + }); + }).end(); + }); + + await promise; +}); + +Deno.test("[node/http] server closeAllConnections shutdown", async () => { + const server = http.createServer((_req, res) => { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ + data: "Hello World!", + })); + }); + + server.listen(0); + const { promise, resolve } = Promise.withResolvers(); + setTimeout(() => { + server.close(() => resolve()); + server.closeAllConnections(); + }, 2000); + + await promise; +}); + +Deno.test("[node/http] server closeIdleConnections shutdown", async () => { + const server = http.createServer({ keepAliveTimeout: 60000 }, (_req, res) => { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ + data: "Hello World!", + })); + }); + + server.listen(0); + const { promise, resolve } = Promise.withResolvers(); + setTimeout(() => { + server.close(() => resolve()); + server.closeIdleConnections(); + }, 2000); + + await promise; +});