Summary
The HTTP 100-continue handshake does not work end-to-end. A client sending Expect: 100-continue never receives the 'continue' event, and a server's 'checkContinue' listener / res.writeContinue() path never drives the exchange. The program produces no output where node completes the request.
Repro
import http from "node:http";
const server = http.createServer((req, res) => {
let b = "";
req.on("data", (c: any) => (b += c));
req.on("end", () => res.end("got:" + b));
});
server.on("checkContinue", (req: any, res: any) => {
res.writeContinue();
let b = "";
req.on("data", (c: any) => (b += c));
req.on("end", () => res.end("continue:" + b));
});
server.listen(0, () => {
const port = (server.address() as any).port;
const req = http.request(
{ port, method: "POST", headers: { Expect: "100-continue" } },
(res: any) => {
let b = "";
res.on("data", (c: any) => (b += c));
res.on("end", () => {
console.log("body", b);
server.close(() => console.log("closed"));
});
},
);
req.on("continue", () => {
console.log("continue event");
req.end("payload");
});
});
setTimeout(() => {}, 1500);
# node v26
continue event
body continue:payload
closed
# perry
(no output)
Expected surface (node parity)
- Client: when
Expect: 100-continue is set, the body is withheld until the server's interim 100 Continue arrives, at which point the request emits 'continue'. (req.on('continue', …) then sends the body.)
- Server: an incoming
Expect: 100-continue fires server.on('checkContinue', (req, res) => …) instead of the normal 'request' handler; res.writeContinue() writes the HTTP/1.1 100 Continue\r\n\r\n interim response, after which the body is read normally. If no 'checkContinue' listener is registered, node auto-sends 100 Continue and dispatches 'request'.
Notes
This needs both the client-side interim-response handling (reqwest/hyper may auto-consume 100 Continue, so the raw-socket trailer-aware path in plain_client.rs or a dedicated client path may be required to observe it) and the server-side checkContinue/writeContinue wiring (perry-ext-http-server). Relevant pieces: client PendingHttpEvent stream, ServerResponse.writeContinue, and the server request-dispatch branch on the Expect header.
Found while fixing client transport errors (#5078); out of scope for that PR.
Summary
The HTTP
100-continuehandshake does not work end-to-end. A client sendingExpect: 100-continuenever receives the'continue'event, and a server's'checkContinue'listener /res.writeContinue()path never drives the exchange. The program produces no output where node completes the request.Repro
Expected surface (node parity)
Expect: 100-continueis set, the body is withheld until the server's interim100 Continuearrives, at which point the request emits'continue'. (req.on('continue', …)then sends the body.)Expect: 100-continuefiresserver.on('checkContinue', (req, res) => …)instead of the normal'request'handler;res.writeContinue()writes theHTTP/1.1 100 Continue\r\n\r\ninterim response, after which the body is read normally. If no'checkContinue'listener is registered, node auto-sends100 Continueand dispatches'request'.Notes
This needs both the client-side interim-response handling (reqwest/hyper may auto-consume
100 Continue, so the raw-socket trailer-aware path inplain_client.rsor a dedicated client path may be required to observe it) and the server-sidecheckContinue/writeContinuewiring (perry-ext-http-server). Relevant pieces: clientPendingHttpEventstream,ServerResponse.writeContinue, and the server request-dispatch branch on theExpectheader.Found while fixing client transport errors (#5078); out of scope for that PR.