Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/better-rats-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': major
---

breaking: make `getRequest` and `setResponse` synchronous
4 changes: 2 additions & 2 deletions packages/adapter-node/src/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const ssr = async (req, res) => {
let request;

try {
request = await getRequest({
request = getRequest({
base: origin || get_origin(req.headers),
request: req,
bodySizeLimit: body_size_limit
Expand Down Expand Up @@ -159,7 +159,7 @@ const ssr = async (req, res) => {
response.headers.set('x-accel-buffering', 'no');
}

await setResponse(res, response);
setResponse(res, response);
};

/** @param {import('polka').Middleware[]} handlers */
Expand Down
12 changes: 4 additions & 8 deletions packages/kit/src/exports/node/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,9 @@ function get_raw_body(req, body_size_limit) {
* base: string;
* bodySizeLimit?: number;
* }} options
* @returns {Promise<Request>}
* @returns {Request}
*/
// TODO 3.0 make the signature synchronous?
// eslint-disable-next-line @typescript-eslint/require-await
export async function getRequest({ request, base, bodySizeLimit }) {
export function getRequest({ request, base, bodySizeLimit }) {
let headers = /** @type {Record<string, string>} */ (request.headers);
if (request.httpVersionMajor >= 2) {
// the Request constructor rejects headers with ':' in the name
Expand Down Expand Up @@ -205,11 +203,9 @@ function drain_request(res) {
/**
* @param {import('http').ServerResponse} res
* @param {Response} response
* @returns {Promise<void>}
* @returns {void}
*/
// TODO 3.0 make the signature synchronous?
// eslint-disable-next-line @typescript-eslint/require-await
export async function setResponse(res, response) {
export function setResponse(res, response) {
res.once('finish', () => drain_request(res));
res.once('close', () => drain_request(res));

Expand Down
36 changes: 18 additions & 18 deletions packages/kit/src/exports/node/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { getRequest, setResponse } from './index.js';
* bodySizeLimit?: number;
* }} [options]
*/
async function create_request(options = {}) {
function create_request(options = {}) {
const req = new PassThrough();
const incoming = /** @type {import('http').IncomingMessage} */ (/** @type {unknown} */ (req));

Expand All @@ -22,7 +22,7 @@ async function create_request(options = {}) {
incoming.httpVersionMajor = 1;

return {
request: await getRequest({
request: getRequest({
request: incoming,
base: 'http://localhost',
bodySizeLimit: options.bodySizeLimit
Expand All @@ -32,7 +32,7 @@ async function create_request(options = {}) {
}

test('rejects chunked request bodies that exceed body size limit', async () => {
const { request, req } = await create_request({
const { request, req } = create_request({
headers: { 'transfer-encoding': 'chunked' },
bodySizeLimit: 10
});
Expand All @@ -51,7 +51,7 @@ test('rejects chunked request bodies that exceed body size limit', async () => {
});

test('allows chunked request bodies within body size limit', async () => {
const { request, req } = await create_request({
const { request, req } = create_request({
headers: { 'transfer-encoding': 'chunked' },
bodySizeLimit: 10
});
Expand All @@ -65,7 +65,7 @@ test('allows chunked request bodies within body size limit', async () => {
});

test('rejects request bodies that exceed content-length', async () => {
const { request, req } = await create_request({
const { request, req } = create_request({
headers: { 'content-length': '4' }
});

Expand Down Expand Up @@ -104,7 +104,7 @@ function create_response(req) {
* @param {Record<string, string>} [headers]
* @param {import('stream').PassThrough} [stream]
*/
async function setup_post_request(headers = {}, stream) {
function setup_post_request(headers = {}, stream) {
const req = stream ?? new PassThrough();
const incoming = /** @type {import('http').IncomingMessage} */ (/** @type {unknown} */ (req));
incoming.headers = {
Expand All @@ -115,7 +115,7 @@ async function setup_post_request(headers = {}, stream) {
incoming.url = '/';
incoming.httpVersionMajor = 1;

const request = await getRequest({ request: incoming, base: 'http://localhost' });
const request = getRequest({ request: incoming, base: 'http://localhost' });

return { req, incoming, request };
}
Expand All @@ -131,41 +131,41 @@ async function expect_request_drained(req) {
// https://github.com/sveltejs/kit/issues/14916
// https://github.com/sveltejs/kit/issues/15526
test('drains an unconsumed request body once the response finishes', async () => {
const { req, incoming } = await setup_post_request({ 'content-length': '30' });
const { req, incoming } = setup_post_request({ 'content-length': '30' });

// route never reads the body (e.g. a page route returning 405)
req.write(Buffer.from('0123456789'));
req.write(Buffer.from('0123456789'));
req.write(Buffer.from('0123456789'));
req.end();

await setResponse(create_response(incoming), new Response(null, { status: 405 }));
setResponse(create_response(incoming), new Response(null, { status: 405 }));

await expect_request_drained(req);
});

test('drains an unconsumed chunked request body once the response finishes', async () => {
const { req, incoming } = await setup_post_request({ 'transfer-encoding': 'chunked' });
const { req, incoming } = setup_post_request({ 'transfer-encoding': 'chunked' });

req.write(Buffer.from('0123456789'));
req.write(Buffer.from('0123456789'));
req.write(Buffer.from('0123456789'));
req.end();

await setResponse(create_response(incoming), new Response(null, { status: 405 }));
setResponse(create_response(incoming), new Response(null, { status: 405 }));

await expect_request_drained(req);
});

test('closes the request body stream after draining an unconsumed body', async () => {
const { req, incoming, request } = await setup_post_request({ 'content-length': '30' });
const { req, incoming, request } = setup_post_request({ 'content-length': '30' });

req.write(Buffer.from('0123456789'));
req.write(Buffer.from('0123456789'));
req.write(Buffer.from('0123456789'));
req.end();

await setResponse(create_response(incoming), new Response(null, { status: 405 }));
setResponse(create_response(incoming), new Response(null, { status: 405 }));

await expect_request_drained(req);

Expand All @@ -179,7 +179,7 @@ test('closes the request body stream after draining an unconsumed body', async (
});

test('drains the remainder of a partially consumed request body', async () => {
const { req, incoming, request } = await setup_post_request({ 'content-length': '30' });
const { req, incoming, request } = setup_post_request({ 'content-length': '30' });

req.write(Buffer.from('0123456789'));
req.write(Buffer.from('0123456789'));
Expand All @@ -192,7 +192,7 @@ test('drains the remainder of a partially consumed request body', async () => {

req.end();

await setResponse(create_response(incoming), new Response(null, { status: 200 }));
setResponse(create_response(incoming), new Response(null, { status: 200 }));

await expect_request_drained(req);
});
Expand All @@ -202,20 +202,20 @@ test('does not remove unrelated data listeners when draining', async () => {
const unrelated = vi.fn();
req.on('data', unrelated);

const { incoming } = await setup_post_request({ 'content-length': '10' }, req);
const { incoming } = setup_post_request({ 'content-length': '10' }, req);

req.write(Buffer.from('0123456789'));
req.end();

await setResponse(create_response(incoming), new Response(null, { status: 405 }));
setResponse(create_response(incoming), new Response(null, { status: 405 }));

await expect_request_drained(req);
expect(unrelated).toHaveBeenCalled();
});

// Test for fix of CVE-2026-40073
test('requests with no content-length and no transfer-encoding return null body', async () => {
const { request, req } = await create_request({
const { request, req } = create_request({
headers: {},
bodySizeLimit: 10
});
Expand Down
6 changes: 3 additions & 3 deletions packages/kit/src/exports/vite/dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ export async function dev(vite, vite_config, svelte_config, get_remotes, root) {
read: (file) => createReadableStream(from_fs(file))
});

const request = await getRequest({
const request = getRequest({
base,
request: req
});
Expand Down Expand Up @@ -579,10 +579,10 @@ export async function dev(vite, vite_config, svelte_config, get_remotes, root) {
if (rendered.status === 404) {
// @ts-expect-error
serve_static_middleware.handle(req, res, () => {
void setResponse(res, rendered);
setResponse(res, rendered);
});
} else {
void setResponse(res, rendered);
setResponse(res, rendered);
}
} catch (e) {
const error = coalesce_to_error(e);
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/exports/vite/preview/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,12 +204,12 @@ export async function preview(vite, vite_config, svelte_config) {
vite.middlewares.use(async (req, res) => {
const host = req.headers[':authority'] || req.headers.host;

const request = await getRequest({
const request = getRequest({
base: `${protocol}://${host}`,
request: req
});

await setResponse(
setResponse(
res,
await server.respond(request, {
getClientAddress: () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3204,9 +3204,9 @@ declare module '@sveltejs/kit/node' {
request: import("http").IncomingMessage;
base: string;
bodySizeLimit?: number;
}): Promise<Request>;
}): Request;

export function setResponse(res: import("http").ServerResponse, response: Response): Promise<void>;
export function setResponse(res: import("http").ServerResponse, response: Response): void;
/**
* Converts a file on disk to a readable stream
* @since 2.4.0
Expand Down
Loading