-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Description
Describe the bug
The get_raw_body() function in packages/kit/src/exports/node/index.js can leave request bodies undrained on keep-alive connections when routes don't consume the body (like page routes receiving POST requests that return 405). The undrained bytes remain in the TCP stream, so the server parses the leftover body data as the next HTTP request, fails, and resets the connection — any subsequent request pipelined on the same connection is lost.
Two code paths fail to drain the underlying Node.js IncomingMessage:
- Backpressure hang: When the ReadableStream buffer fills (~131KB),
req.pause()is called (line 82) and only resumes via thepull()callback (lines 87-89). If the route handler never reads from the stream, the IncomingMessage remains permanently paused. - No listeners registered: When
content_length > body_size_limit,controller.error()is called and the function returns early (lines 38-51), never registeringdata,end, orerrorlisteners on the IncomingMessage.
Reproduction
Requires @sveltejs/adapter-node. Build and start the production server (npm run build && node build/index.js — default port 3000). Any SvelteKit app with a page route works, since page routes don't consume POST bodies.
Save as repro.mjs and run with node repro.mjs:
import { connect } from 'node:net';
const PORT = 3000; // adapter-node default
const socket = connect({ host: '127.0.0.1', port: PORT }, () => {
const bodySize = 200000; // ~200KB — above Node's internal buffer threshold
const postReq =
`POST / HTTP/1.1\r\n` +
`Host: localhost:${PORT}\r\n` +
`Content-Type: text/plain\r\n` +
`Content-Length: ${bodySize}\r\n` +
`Origin: https://localhost:${PORT}\r\n` +
`Connection: keep-alive\r\n` +
`\r\n`;
const getReq =
`GET / HTTP/1.1\r\nHost: localhost:${PORT}\r\nConnection: close\r\n\r\n`;
// Send POST (with large body) + GET on same keep-alive connection
socket.write(postReq);
socket.write(Buffer.alloc(bodySize, 0x41)); // 200KB of 'A'
socket.write(getReq);
});
let data = '';
socket.on('data', (chunk) => { data += chunk.toString(); });
socket.on('error', (err) => { console.log('Socket error:', err.code); });
socket.on('close', () => {
const responses = (data.match(/HTTP\/1\.1/g) || []).length;
const statuses = data.split('\r\n').filter(l => l.startsWith('HTTP/'));
console.log(`Responses received: ${responses} (expected: 2)`);
statuses.forEach(s => console.log(` ${s}`));
process.exit(0);
});
setTimeout(() => {
console.log('Timeout — connection hung');
socket.destroy();
process.exit(0);
}, 12000);Output:
Socket error: ECONNRESET
Responses received: 1 (expected: 2)
HTTP/1.1 405 Method Not Allowed
The POST gets a 405 (page route doesn't accept POST), but the server doesn't drain the 200KB body. The leftover bytes corrupt the connection — Node's HTTP parser reads them as the next request, fails, and resets the connection. The pipelined GET is never processed.
Logs
No server-side errors logged. The client sees ECONNRESET after receiving only the first response.
System Info
System:
OS: macOS 26.2
CPU: (14) arm64 Apple M4 Pro
Binaries:
Node: 24.13.0
pnpm: 10.28.1
npmPackages:
@sveltejs/adapter-node: ^5.0.0
@sveltejs/kit: ^2.50.2 => 2.53.4
svelte: ^5.51.0 => 5.53.7
vite: ^7.3.1 => 7.3.1
Severity
serious, but I can work around it
Additional Information
After setResponse() completes, unconsumed request bodies should be drained by calling req.resume() or implementing automatic body drainage when the response finishes. This is standard Node.js HTTP server hygiene — the framework should handle cleanup automatically rather than requiring routes to consume bodies they don't need.