From 0630c6bf193d0cf7a8baa00457d7a06089b326f9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 8 Jul 2026 11:54:06 -0400 Subject: [PATCH 1/2] breaking: make `getRequest` and `setResponse` synchronous --- .changeset/better-rats-care.md | 5 +++++ packages/kit/src/exports/node/index.js | 12 ++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) create mode 100644 .changeset/better-rats-care.md diff --git a/.changeset/better-rats-care.md b/.changeset/better-rats-care.md new file mode 100644 index 000000000000..1466c192fb1a --- /dev/null +++ b/.changeset/better-rats-care.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: make `getRequest` and `setResponse` synchronous diff --git a/packages/kit/src/exports/node/index.js b/packages/kit/src/exports/node/index.js index 81a836412b23..893f8de77216 100644 --- a/packages/kit/src/exports/node/index.js +++ b/packages/kit/src/exports/node/index.js @@ -119,11 +119,9 @@ function get_raw_body(req, body_size_limit) { * base: string; * bodySizeLimit?: number; * }} options - * @returns {Promise} + * @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} */ (request.headers); if (request.httpVersionMajor >= 2) { // the Request constructor rejects headers with ':' in the name @@ -205,11 +203,9 @@ function drain_request(res) { /** * @param {import('http').ServerResponse} res * @param {Response} response - * @returns {Promise} + * @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)); From 1f6724af850566c96abbc0b059221768887f0af2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 8 Jul 2026 11:58:21 -0400 Subject: [PATCH 2/2] update callsites --- packages/adapter-node/src/handler.js | 4 +-- packages/kit/src/exports/node/index.spec.js | 36 +++++++++---------- packages/kit/src/exports/vite/dev/index.js | 6 ++-- .../kit/src/exports/vite/preview/index.js | 4 +-- packages/kit/types/index.d.ts | 4 +-- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/adapter-node/src/handler.js b/packages/adapter-node/src/handler.js index 0d90c088eaf8..07636d5e9a8b 100644 --- a/packages/adapter-node/src/handler.js +++ b/packages/adapter-node/src/handler.js @@ -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 @@ -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 */ diff --git a/packages/kit/src/exports/node/index.spec.js b/packages/kit/src/exports/node/index.spec.js index 6f592535c73a..5fa0f52cb20e 100644 --- a/packages/kit/src/exports/node/index.spec.js +++ b/packages/kit/src/exports/node/index.spec.js @@ -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)); @@ -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 @@ -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 }); @@ -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 }); @@ -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' } }); @@ -104,7 +104,7 @@ function create_response(req) { * @param {Record} [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 = { @@ -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 }; } @@ -131,7 +131,7 @@ 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')); @@ -139,33 +139,33 @@ test('drains an unconsumed request body once the response finishes', async () => 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); @@ -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')); @@ -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); }); @@ -202,12 +202,12 @@ 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(); @@ -215,7 +215,7 @@ test('does not remove unrelated data listeners when draining', async () => { // 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 }); diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index 39cabeeb09fd..c05e069f5fe7 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -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 }); @@ -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); diff --git a/packages/kit/src/exports/vite/preview/index.js b/packages/kit/src/exports/vite/preview/index.js index 4a5e8d60766d..1b1ebd997acf 100644 --- a/packages/kit/src/exports/vite/preview/index.js +++ b/packages/kit/src/exports/vite/preview/index.js @@ -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: () => { diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 308b9b8bb973..3ad533acf4c1 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -3204,9 +3204,9 @@ declare module '@sveltejs/kit/node' { request: import("http").IncomingMessage; base: string; bodySizeLimit?: number; - }): Promise; + }): Request; - export function setResponse(res: import("http").ServerResponse, response: Response): Promise; + export function setResponse(res: import("http").ServerResponse, response: Response): void; /** * Converts a file on disk to a readable stream * @since 2.4.0