Hono 4.13.4
@hono/node-server 2.1.1
Nodejs v26.7.0
Steps to reproduce:
- Run the http2 code below:
import {serve} from "@hono/node-server";
import {createServer, type Http2Server} from "node:http2";
import {Hono} from "hono";
const hono = new Hono();
hono.get('/timeout/:millis', (c) => {
c.req.raw.signal.addEventListener('abort', () =>
console.log("Request aborted", c.req.raw.signal.reason)
);
return new Promise<Response>((resolve) =>
setTimeout(() => resolve(c.text("Completed")), Number(c.req.param('millis')))
);
});
const server = serve({
fetch: hono.fetch,
hostname: "0.0.0.0",
port: 8080,
createServer,
}) as Http2Server;
server.on('session', (session) => {
console.log("Session opened");
session.once('close', () => console.log("Session closed"));
});
- Run the following command and abort it with Ctrl+C
curl -v --http2-prior-knowledge http://localhost:8080/timeout/10000
- Notice in the console session events firing but never "Request aborted"
Session opened
Session closed
Session opened
Session closed
- Now run the http1 code below:
import {serve} from "@hono/node-server";
import {Hono} from "hono";
const hono = new Hono();
hono.get('/timeout/:millis', (c) => {
c.req.raw.signal.addEventListener('abort', () =>
console.log("Request aborted", c.req.raw.signal.reason)
);
return new Promise<Response>((resolve) =>
setTimeout(() => resolve(c.text("Completed")), Number(c.req.param('millis')))
);
});
const server = serve({
fetch: hono.fetch,
hostname: "0.0.0.0",
port: 8080,
})
- Run the command and abort it with Ctrl+C
curl -v --http1.1 http://localhost:8080/timeout/10000
- Notice in the console "Request aborted" firing
Request aborted Error: aborted
Request aborted Error: aborted
Expected behavior: c.req.raw.signal should be triggered for http2, not only for http1.
I traced it to the makeCloseHandler.
incoming.errored is true for http1 and null for http2
But
incoming.aborted is true for both http2 and http1
Is there any specific reason why we have these peculiar conditions for triggering AbortSignal?
const makeCloseHandler = (req, incoming, outgoing, needsBodyCleanup) => () => {
if (incoming.errored) {
recordBodyBufferedBeforeDisconnect(incoming);
req[abortRequest](incoming.errored.toString());
} else if (!outgoing.writableFinished) {
recordBodyBufferedBeforeDisconnect(incoming);
req[abortRequest]("Client connection prematurely closed.");
}
if (needsBodyCleanup && !incoming.readableEnded) setTimeout(() => {
if (!incoming.readableEnded) setTimeout(() => {
drainIncoming(incoming);
});
});
};
instead of relying, say, on incoming.aborted?
IncomingMessage.aborted is deprecated, but still works. IncomingMessage.destroyed is recommended by the docs https://nodejs.org/api/http.html#requestaborted as replacement, but doesn't work for http2
Http2ServerRequest.aborted is not deprecated, but destroyed is false in makeCloseHandler
Can we improve the current implementation? Want to hear more opinions
Hono 4.13.4
@hono/node-server 2.1.1
Nodejs v26.7.0
Steps to reproduce:
curl -v --http2-prior-knowledge http://localhost:8080/timeout/10000curl -v --http1.1 http://localhost:8080/timeout/10000Expected behavior: c.req.raw.signal should be triggered for http2, not only for http1.
I traced it to the makeCloseHandler.
incoming.errored is true for http1 and null for http2
But
incoming.aborted is true for both http2 and http1
Is there any specific reason why we have these peculiar conditions for triggering AbortSignal?
instead of relying, say, on incoming.aborted?
IncomingMessage.aborted is deprecated, but still works. IncomingMessage.destroyed is recommended by the docs https://nodejs.org/api/http.html#requestaborted as replacement, but doesn't work for http2
Http2ServerRequest.aborted is not deprecated, but destroyed is false in makeCloseHandler
Can we improve the current implementation? Want to hear more opinions