diff --git a/.changeset/bright-icons-own.md b/.changeset/bright-icons-own.md new file mode 100644 index 000000000000..3ea59d3c1119 --- /dev/null +++ b/.changeset/bright-icons-own.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +Allow endpoints to return a Response, or an object with Headers diff --git a/.changeset/large-icons-complain.md b/.changeset/large-icons-complain.md new file mode 100644 index 000000000000..91597f40feea --- /dev/null +++ b/.changeset/large-icons-complain.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +Breaking: Expose standard Request object to endpoints and hooks diff --git a/.changeset/mighty-pandas-search.md b/.changeset/mighty-pandas-search.md new file mode 100644 index 000000000000..39992e23c76f --- /dev/null +++ b/.changeset/mighty-pandas-search.md @@ -0,0 +1,10 @@ +--- +'@sveltejs/adapter-cloudflare': patch +'@sveltejs/adapter-cloudflare-workers': patch +'@sveltejs/adapter-netlify': patch +'@sveltejs/adapter-node': patch +'@sveltejs/adapter-vercel': patch +'@sveltejs/kit': patch +--- + +Breaking: change app.render signature to (request: Request) => Promise diff --git a/.changeset/strong-schools-rule.md b/.changeset/strong-schools-rule.md new file mode 100644 index 000000000000..034c3d4aa68b --- /dev/null +++ b/.changeset/strong-schools-rule.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/adapter-node': patch +'@sveltejs/kit': patch +--- + +Breaking: Remove protocol/host configuration options from Kit to adapter-node diff --git a/documentation/docs/01-routing.md b/documentation/docs/01-routing.md index 54d49fb53c2d..964a9930224e 100644 --- a/documentation/docs/01-routing.md +++ b/documentation/docs/01-routing.md @@ -54,28 +54,19 @@ Endpoints are modules written in `.js` (or `.ts`) files that export functions co // type of string[] is only for set-cookie // everything else must be a type of string type ResponseHeaders = Record; -type RequestHeaders = Record; -export type RawBody = null | Uint8Array; - -type ParameterizedBody = Body extends FormData - ? ReadOnlyFormData - : (string | RawBody | ReadOnlyFormData) & Body; - -export interface Request, Body = unknown> { +export interface RequestEvent> { + request: Request; url: URL; - method: string; - headers: RequestHeaders; - rawBody: RawBody; params: Record; - body: ParameterizedBody; locals: Locals; } -type DefaultBody = JSONResponse | Uint8Array; -export interface EndpointOutput { +type Body = JSONResponse | Uint8Array | string | ReadableStream | stream.Readable; + +export interface EndpointOutput { status?: number; - headers?: ResponseHeaders; + headers?: HeadersInit; body?: Body; } @@ -103,9 +94,7 @@ import db from '$lib/database'; export async function get({ params }) { // the `slug` parameter is available because this file // is called [slug].json.js - const { slug } = params; - - const article = await db.get(slug); + const article = await db.get(params.slug); if (article) { return { @@ -114,6 +103,10 @@ export async function get({ params }) { } }; } + + return { + status: 404 + }; } ``` @@ -152,12 +145,13 @@ return { #### Body parsing -The `body` property of the request object will be provided in the case of POST requests: +The `request` object is an instance of the standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) class. As such, accessing the request body is easy: -- Text data (with content-type `text/plain`) will be parsed to a `string` -- JSON data (with content-type `application/json`) will be parsed to a `JSONValue` (an `object`, `Array`, or primitive). -- Form data (with content-type `application/x-www-form-urlencoded` or `multipart/form-data`) will be parsed to a read-only version of the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object. -- All other data will be provided as a `Uint8Array` +```js +export async function post({ request }) { + const data = await request.formData(); // or .json(), or .text(), etc +} +``` #### HTTP Method Overrides diff --git a/documentation/docs/04-hooks.md b/documentation/docs/04-hooks.md index 7a3dcec0d897..cbdde3afb989 100644 --- a/documentation/docs/04-hooks.md +++ b/documentation/docs/04-hooks.md @@ -8,11 +8,11 @@ An optional `src/hooks.js` (or `src/hooks.ts`, or `src/hooks/index.js`) file exp ### handle -This function runs every time SvelteKit receives a request — whether that happens while the app is running, or during [prerendering](#page-options-prerender) — and determines the response. It receives the `request` object and a function called `resolve`, which invokes SvelteKit's router and generates a response (rendering a page, or invoking an endpoint) accordingly. This allows you to modify response headers or bodies, or bypass SvelteKit entirely (for implementing endpoints programmatically, for example). +This function runs every time SvelteKit receives a request — whether that happens while the app is running, or during [prerendering](#page-options-prerender) — and determines the response. It receives an `event` object representing the request and a function called `resolve`, which invokes SvelteKit's router and generates a response (rendering a page, or invoking an endpoint) accordingly. This allows you to modify response headers or bodies, or bypass SvelteKit entirely (for implementing endpoints programmatically, for example). > Requests for static assets — which includes pages that were already prerendered — are _not_ handled by SvelteKit. -If unimplemented, defaults to `({ request, resolve }) => resolve(request)`. +If unimplemented, defaults to `({ event, resolve }) => resolve(event)`. ```ts // Declaration types for Hooks @@ -21,41 +21,23 @@ If unimplemented, defaults to `({ request, resolve }) => resolve(request)`. // type of string[] is only for set-cookie // everything else must be a type of string type ResponseHeaders = Record; -type RequestHeaders = Record; -export type RawBody = null | Uint8Array; - -type ParameterizedBody = Body extends FormData - ? ReadOnlyFormData - : (string | RawBody | ReadOnlyFormData) & Body; - -export interface Request, Body = unknown> { +export interface RequestEvent> { + request: Request; url: URL; - method: string; - headers: RequestHeaders; - rawBody: RawBody; params: Record; - body: ParameterizedBody; locals: Locals; } -type StrictBody = string | Uint8Array; - -export interface Response { - status: number; - headers: ResponseHeaders; - body?: StrictBody; -} - export interface ResolveOpts { ssr?: boolean; } -export interface Handle, Body = unknown> { +export interface Handle> { (input: { - request: ServerRequest; - resolve(request: ServerRequest, opts?: ResolveOpts): MaybePromise; - }): MaybePromise; + event: RequestEvent; + resolve(event: RequestEvent, opts?: ResolveOpts): MaybePromise; + }): MaybePromise; } ``` @@ -67,14 +49,9 @@ export async function handle({ request, resolve }) { request.locals.user = await getUserInformation(request.headers.cookie); const response = await resolve(request); + response.headers.set('x-custom-header', 'potato'); - return { - ...response, - headers: { - ...response.headers, - 'x-custom-header': 'potato' - } - }; + return response; } ``` @@ -107,16 +84,16 @@ If unimplemented, SvelteKit will log the error with default formatting. ```ts // Declaration types for handleError hook -export interface HandleError, Body = unknown> { - (input: { error: Error & { frame?: string }; request: Request }): void; +export interface HandleError> { + (input: { error: Error & { frame?: string }; event: RequestEvent }): void; } ``` ```js /** @type {import('@sveltejs/kit').HandleError} */ -export async function handleError({ error, request }) { +export async function handleError({ error, event }) { // example integration with https://sentry.io/ - Sentry.captureException(error, { request }); + Sentry.captureException(error, { event }); } ``` @@ -124,29 +101,29 @@ export async function handleError({ error, request }) { ### getSession -This function takes the `request` object and returns a `session` object that is [accessible on the client](#modules-$app-stores) and therefore must be safe to expose to users. It runs whenever SvelteKit server-renders a page. +This function takes the `event` object and returns a `session` object that is [accessible on the client](#modules-$app-stores) and therefore must be safe to expose to users. It runs whenever SvelteKit server-renders a page. If unimplemented, session is `{}`. ```ts // Declaration types for getSession hook -export interface GetSession, Body = unknown, Session = any> { - (request: Request): Session | Promise; +export interface GetSession, Session = any> { + (event: RequestEvent): Session | Promise; } ``` ```js /** @type {import('@sveltejs/kit').GetSession} */ -export function getSession(request) { - return request.locals.user +export function getSession(event) { + return event.locals.user ? { user: { // only include properties needed client-side — // exclude anything else attached to the user // like access tokens etc - name: request.locals.user.name, - email: request.locals.user.email, - avatar: request.locals.user.avatar + name: event.locals.user.name, + email: event.locals.user.email, + avatar: event.locals.user.avatar } } : {}; diff --git a/documentation/docs/05-modules.md b/documentation/docs/05-modules.md index 13b979faa7da..d536dd0a0002 100644 --- a/documentation/docs/05-modules.md +++ b/documentation/docs/05-modules.md @@ -94,13 +94,13 @@ This module provides a helper function to sequence multiple `handle` calls. ```js import { sequence } from '@sveltejs/kit/hooks'; -async function first({ request, resolve }) { +async function first({ event, resolve }) { console.log('first'); - return await resolve(request); + return await resolve(event); } -async function second({ request, resolve }) { +async function second({ event, resolve }) { console.log('second'); - return await resolve(request); + return await resolve(event); } export const handle = sequence(first, second); diff --git a/documentation/docs/10-adapters.md b/documentation/docs/10-adapters.md index adc83623dfa9..db8a2af158ba 100644 --- a/documentation/docs/10-adapters.md +++ b/documentation/docs/10-adapters.md @@ -92,7 +92,7 @@ Within the `adapt` method, there are a number of things that an adapter should d - Output code that: - Imports `App` from `${builder.getServerDirectory()}/app.js` - Instantiates the app with a manifest generated with `builder.generateManifest({ relativePath })` - - Listens for requests from the platform, converts them to a a [SvelteKit request](#hooks-handle), calls the `render` function to generate a [SvelteKit response](#hooks-handle) and responds with it + - Listens for requests from the platform, converts them to a standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) if necessary, calls the `render` function to generate a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) and responds with it - Globally shims `fetch` to work on the target platform, if necessary. SvelteKit provides a `@sveltejs/kit/install-fetch` helper for platforms that can use `node-fetch` - Bundle the output to avoid needing to install dependencies on the target platform, if necessary - Put the user's static files and the generated JS/CSS in the correct location for the target platform diff --git a/documentation/docs/14-configuration.md b/documentation/docs/14-configuration.md index 28bad4f8573e..aa3d1274af91 100644 --- a/documentation/docs/14-configuration.md +++ b/documentation/docs/14-configuration.md @@ -26,11 +26,6 @@ const config = { template: 'src/app.html' }, floc: false, - headers: { - host: null, - protocol: null - }, - host: null, hydrate: true, inlineStyleThreshold: 0, methodOverride: { @@ -55,7 +50,6 @@ const config = { entries: ['*'], onError: 'fail' }, - protocol: null, router: true, serviceWorker: { register: true, @@ -111,30 +105,6 @@ Permissions-Policy: interest-cohort=() > This only applies to server-rendered responses — headers for prerendered pages (e.g. created with [adapter-static](https://github.com/sveltejs/kit/tree/master/packages/adapter-static)) are determined by the hosting platform. -### headers - -The current page or endpoint's `url` is, in some environments, derived from the request protocol (normally `https`) and the host, which is taken from the `Host` header by default. - -If your app is behind a reverse proxy (think load balancers and CDNs) then the `Host` header will be incorrect. In most cases, the underlying protocol and host are exposed via the [`X-Forwarded-Host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host) and [`X-Forwarded-Proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto) headers, which can be specified in your config: - -```js -// svelte.config.js -export default { - kit: { - headers: { - host: 'X-Forwarded-Host', - protocol: 'X-Forwarded-Proto' - } - } -}; -``` - -**You should only do this if you trust the reverse proxy**, which is why it isn't the default. - -### host - -A value that overrides the one derived from [`config.kit.headers.host`](#configuration-headers). - ### hydrate Whether to [hydrate](#page-options-hydrate) the server-rendered HTML with a client-side app. (It's rare that you would set this to `false` on an app-wide basis.) @@ -220,10 +190,6 @@ See [Prerendering](#page-options-prerender). An object containing zero or more o }; ``` -### protocol - -The protocol is assumed to be `'https'` (unless you're developing locally without the `--https` flag) unless [`config.kit.headers.protocol`](#configuration-headers) is set. If necessary, you can override it here. - ### router Enables or disables the client-side [router](#page-options-router) app-wide. diff --git a/packages/adapter-cloudflare-workers/files/entry.d.ts b/packages/adapter-cloudflare-workers/files/entry.d.ts new file mode 100644 index 000000000000..c0ee559ee90c --- /dev/null +++ b/packages/adapter-cloudflare-workers/files/entry.d.ts @@ -0,0 +1,11 @@ +declare module 'APP' { + import { App } from '@sveltejs/kit'; + export { App }; +} + +declare module 'MANIFEST' { + import { SSRManifest } from '@sveltejs/kit'; + + export const manifest: SSRManifest; + export const prerendered: Set; +} diff --git a/packages/adapter-cloudflare-workers/files/entry.js b/packages/adapter-cloudflare-workers/files/entry.js index 4fbe5789eadd..2ce945ce377c 100644 --- a/packages/adapter-cloudflare-workers/files/entry.js +++ b/packages/adapter-cloudflare-workers/files/entry.js @@ -1,5 +1,5 @@ import { App } from 'APP'; -import { manifest, prerendered } from './manifest.js'; +import { manifest, prerendered } from 'MANIFEST'; import { getAssetFromKV } from '@cloudflare/kv-asset-handler'; const app = new App(manifest); @@ -50,46 +50,8 @@ async function handle(event) { // dynamically-generated pages try { - const rendered = await app.render({ - url: request.url, - rawBody: await read(request), - headers: Object.fromEntries(request.headers), - method: request.method - }); - - if (rendered) { - return new Response(rendered.body, { - status: rendered.status, - headers: make_headers(rendered.headers) - }); - } + return await app.render(request); } catch (e) { return new Response('Error rendering route:' + (e.message || e.toString()), { status: 500 }); } - - return new Response('Not Found', { - status: 404, - statusText: 'Not Found' - }); -} - -/** @param {Request} request */ -async function read(request) { - return new Uint8Array(await request.arrayBuffer()); -} - -/** @param {Record} headers */ -function make_headers(headers) { - const result = new Headers(); - for (const header in headers) { - const value = headers[header]; - if (typeof value === 'string') { - result.set(header, value); - continue; - } - for (const sub of value) { - result.append(header, sub); - } - } - return result; } diff --git a/packages/adapter-cloudflare-workers/index.js b/packages/adapter-cloudflare-workers/index.js index 279caa10e642..e47af92ff445 100644 --- a/packages/adapter-cloudflare-workers/index.js +++ b/packages/adapter-cloudflare-workers/index.js @@ -39,7 +39,8 @@ export default function () { builder.copy(`${files}/entry.js`, `${tmp}/entry.js`, { replace: { - APP: `${relativePath}/app.js` + APP: `${relativePath}/app.js`, + MANIFEST: './manifest.js' } }); diff --git a/packages/adapter-cloudflare/files/worker.d.ts b/packages/adapter-cloudflare/files/worker.d.ts new file mode 100644 index 000000000000..c0ee559ee90c --- /dev/null +++ b/packages/adapter-cloudflare/files/worker.d.ts @@ -0,0 +1,11 @@ +declare module 'APP' { + import { App } from '@sveltejs/kit'; + export { App }; +} + +declare module 'MANIFEST' { + import { SSRManifest } from '@sveltejs/kit'; + + export const manifest: SSRManifest; + export const prerendered: Set; +} diff --git a/packages/adapter-cloudflare/files/worker.js b/packages/adapter-cloudflare/files/worker.js index c10c2b217328..9829bb501ffb 100644 --- a/packages/adapter-cloudflare/files/worker.js +++ b/packages/adapter-cloudflare/files/worker.js @@ -1,5 +1,5 @@ -import { App } from '../output/server/app.js'; -import { manifest, prerendered } from './manifest.js'; +import { App } from 'APP'; +import { manifest, prerendered } from 'MANIFEST'; const app = new App(manifest); @@ -45,41 +45,9 @@ export default { // dynamically-generated pages try { - const rendered = await app.render({ - url, - rawBody: new Uint8Array(await req.arrayBuffer()), - headers: Object.fromEntries(req.headers), - method: req.method - }); - - if (rendered) { - return new Response(rendered.body, { - status: rendered.status, - headers: make_headers(rendered.headers) - }); - } + return await app.render(req); } catch (e) { return new Response('Error rendering route: ' + (e.message || e.toString()), { status: 500 }); } - - return new Response({ - status: 404, - statusText: 'Not Found' - }); } }; - -function make_headers(headers) { - const result = new Headers(); - for (const header in headers) { - const value = headers[header]; - if (typeof value === 'string') { - result.set(header, value); - continue; - } - for (const sub of value) { - result.append(header, sub); - } - } - return result; -} diff --git a/packages/adapter-cloudflare/index.js b/packages/adapter-cloudflare/index.js index 8f6061f4efe2..b3a256a7ae25 100644 --- a/packages/adapter-cloudflare/index.js +++ b/packages/adapter-cloudflare/index.js @@ -32,7 +32,8 @@ export default function (options = {}) { builder.copy(`${files}/worker.js`, `${tmp}/_worker.js`, { replace: { - APP: `${relativePath}/app.js` + APP: `${relativePath}/app.js`, + MANIFEST: './manifest.js' } }); diff --git a/packages/adapter-cloudflare/tsconfig.json b/packages/adapter-cloudflare/tsconfig.json new file mode 100644 index 000000000000..dab7ee9050dc --- /dev/null +++ b/packages/adapter-cloudflare/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "noImplicitAny": true, + "target": "es2020", + "module": "es2020", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true + }, + "include": ["index.js", "files"] +} diff --git a/packages/adapter-netlify/index.js b/packages/adapter-netlify/index.js index de939673c8b6..2aa36a2452c4 100644 --- a/packages/adapter-netlify/index.js +++ b/packages/adapter-netlify/index.js @@ -50,8 +50,12 @@ export default function ({ split = false } = {}) { /** @type {string[]} */ const redirects = []; + const replace = { + APP: './server/app.js' + }; + if (esm) { - builder.copy(`${files}/esm`, '.netlify'); + builder.copy(`${files}/esm`, '.netlify', { replace }); } else { glob('**/*.js', { cwd: '.netlify/server' }).forEach((file) => { const filepath = `.netlify/server/${file}`; @@ -60,7 +64,7 @@ export default function ({ split = false } = {}) { writeFileSync(filepath, output); }); - builder.copy(`${files}/cjs`, '.netlify'); + builder.copy(`${files}/cjs`, '.netlify', { replace }); writeFileSync(join('.netlify', 'package.json'), JSON.stringify({ type: 'commonjs' })); } diff --git a/packages/adapter-netlify/rollup.config.js b/packages/adapter-netlify/rollup.config.js index d211e9860e7f..2d88582be5e4 100644 --- a/packages/adapter-netlify/rollup.config.js +++ b/packages/adapter-netlify/rollup.config.js @@ -19,6 +19,6 @@ export default [ } ], plugins: [nodeResolve(), commonjs(), json()], - external: ['./server/app.js', ...require('module').builtinModules] + external: ['APP', ...require('module').builtinModules] } ]; diff --git a/packages/adapter-netlify/src/handler.d.ts b/packages/adapter-netlify/src/handler.d.ts new file mode 100644 index 000000000000..a34e560311af --- /dev/null +++ b/packages/adapter-netlify/src/handler.d.ts @@ -0,0 +1,4 @@ +declare module 'APP' { + import { App } from '@sveltejs/kit'; + export { App }; +} diff --git a/packages/adapter-netlify/src/handler.js b/packages/adapter-netlify/src/handler.js index 67d673649699..0835a3f6beff 100644 --- a/packages/adapter-netlify/src/handler.js +++ b/packages/adapter-netlify/src/handler.js @@ -1,13 +1,11 @@ import './shims'; -import { App } from './server/app.js'; +import { App } from 'APP'; /** - * * @param {import('@sveltejs/kit').SSRManifest} manifest * @returns {import('@netlify/functions').Handler} */ export function init(manifest) { - /** @type {import('@sveltejs/kit').App} */ const app = new App(manifest); return async (event) => { @@ -16,25 +14,20 @@ export function init(manifest) { const encoding = isBase64Encoded ? 'base64' : 'utf-8'; const rawBody = typeof body === 'string' ? Buffer.from(body, encoding) : body; - const rendered = await app.render({ - url: rawUrl, - method: httpMethod, - headers, - rawBody - }); - - if (!rendered) { - return { - statusCode: 404, - body: 'Not found' - }; - } + const rendered = await app.render( + new Request(rawUrl, { + method: httpMethod, + headers: new Headers(headers), + body: rawBody + }) + ); const partial_response = { statusCode: rendered.status, ...split_headers(rendered.headers) }; + // TODO this is probably wrong now? if (rendered.body instanceof Uint8Array) { // Function responses should be strings (or undefined), and responses with binary // content should be base64 encoded and set isBase64Encoded to true. @@ -48,14 +41,14 @@ export function init(manifest) { return { ...partial_response, - body: rendered.body + body: await rendered.text() }; }; } /** * Splits headers into two categories: single value and multi value - * @param {Record} headers + * @param {Headers} headers * @returns {{ * headers: Record, * multiValueHeaders: Record @@ -68,11 +61,14 @@ function split_headers(headers) { /** @type {Record} */ const m = {}; - for (const key in headers) { - const value = headers[key]; - const target = Array.isArray(value) ? m : h; - target[key] = value; - } + headers.forEach((value, key) => { + if (key === 'set-cookie') { + m[key] = value.split(', '); + } else { + h[key] = value; + } + }); + return { headers: h, multiValueHeaders: m diff --git a/packages/adapter-node/README.md b/packages/adapter-node/README.md index 71cf7d353f2d..a343608b9a20 100644 --- a/packages/adapter-node/README.md +++ b/packages/adapter-node/README.md @@ -17,8 +17,14 @@ export default { out: 'build', precompress: false, env: { + path: 'SOCKET_PATH', host: 'HOST', - port: 'PORT' + port: 'PORT', + base: undefined, + headers: { + protocol: undefined, + host: 'host' + } } }) } @@ -43,17 +49,39 @@ By default, the server will accept connections on `0.0.0.0` using port 3000. The HOST=127.0.0.1 PORT=4000 node build ``` -You can specify different environment variables if necessary using the `env` option: +HTTP doesn't give SvelteKit a reliable way to know the URL that is currently being requested. The simplest way to tell SvelteKit where the app is being served is to set the `BASE` environment variable: + +``` +BASE=https://my.site node build +``` + +With this, a request for the `/stuff` pathname will correctly resolve to `https://my.site/stuff`. Alternatively, you can specify headers that tell SvelteKit about the request protocol and host, from which it can construct the base URL: + +``` +PROTOCOL_HEADER=x-forwarded-proto HOST_HEADER=x-forwarded-host node build +``` + +> [`x-forwarded-proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto) and [`x-forwarded-host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host) are de facto standard headers that forward the original protocol and host if you're using a reverse proxy (think load balancers and CDNs). You should only set these variables if you trust the reverse proxy. + +All of these environment variables can be changed, if necessary, using the `env` option: ```js env: { host: 'MY_HOST_VARIABLE', - port: 'MY_PORT_VARIABLE' + port: 'MY_PORT_VARIABLE', + base: 'MY_BASEURL', + headers: { + protocol: 'MY_PROTOCOL_HEADER', + host: 'MY_HOST_HEADER' + } } ``` ``` -MY_HOST_VARIABLE=127.0.0.1 MY_PORT_VARIABLE=4000 node build +MY_HOST_VARIABLE=127.0.0.1 \ +MY_PORT_VARIABLE=4000 \ +MY_BASEURL=https://my.site \ +node build ``` ## Custom server diff --git a/packages/adapter-node/index.d.ts b/packages/adapter-node/index.d.ts index f8799d634759..7564c68954d4 100644 --- a/packages/adapter-node/index.d.ts +++ b/packages/adapter-node/index.d.ts @@ -7,6 +7,11 @@ interface AdapterOptions { path?: string; host?: string; port?: string; + base?: string; + headers?: { + protocol?: string; + host?: string; + }; }; } diff --git a/packages/adapter-node/index.js b/packages/adapter-node/index.js index 9d7f400090b3..af1c59dd5127 100644 --- a/packages/adapter-node/index.js +++ b/packages/adapter-node/index.js @@ -13,7 +13,16 @@ const files = fileURLToPath(new URL('./files', import.meta.url)); export default function ({ out = 'build', precompress, - env: { path: path_env = 'SOCKET_PATH', host: host_env = 'HOST', port: port_env = 'PORT' } = {} + env: { + path: path_env = 'SOCKET_PATH', + host: host_env = 'HOST', + port: port_env = 'PORT', + base: base_env, + headers: { + protocol: protocol_header_env = 'PROTOCOL_HEADER', + host: host_header_env = 'HOST_HEADER' + } + } = {} } = {}) { return { name: '@sveltejs/adapter-node', @@ -44,7 +53,10 @@ export default function ({ MANIFEST: './manifest.js', PATH_ENV: JSON.stringify(path_env), HOST_ENV: JSON.stringify(host_env), - PORT_ENV: JSON.stringify(port_env) + PORT_ENV: JSON.stringify(port_env), + BASE: base_env ? `process.env[${JSON.stringify(base_env)}]` : 'undefined', + PROTOCOL_HEADER: JSON.stringify(protocol_header_env), + HOST_HEADER: JSON.stringify(host_header_env) } }); diff --git a/packages/adapter-node/src/ambient.d.ts b/packages/adapter-node/src/ambient.d.ts new file mode 100644 index 000000000000..279516ccb427 --- /dev/null +++ b/packages/adapter-node/src/ambient.d.ts @@ -0,0 +1,9 @@ +declare module 'APP' { + import { App } from '@sveltejs/kit'; + export { App }; +} + +declare module 'MANIFEST' { + import { SSRManifest } from '@sveltejs/kit'; + export const manifest: SSRManifest; +} diff --git a/packages/adapter-node/src/handler.js b/packages/adapter-node/src/handler.js index faf6b7bbe1a8..33c5529ba772 100644 --- a/packages/adapter-node/src/handler.js +++ b/packages/adapter-node/src/handler.js @@ -3,13 +3,16 @@ import fs from 'fs'; import path from 'path'; import sirv from 'sirv'; import { fileURLToPath } from 'url'; -import { getRawBody } from '@sveltejs/kit/node'; - -// @ts-ignore +import { getRequest, setResponse } from '@sveltejs/kit/node'; import { App } from 'APP'; import { manifest } from 'MANIFEST'; +/* global BASE_ENV, PROTOCOL_HEADER, HOST_HEADER */ + const app = new App(manifest); +const base = BASE_ENV && process.env[BASE_ENV]; +const protocol_header = PROTOCOL_HEADER && process.env[PROTOCOL_HEADER]; +const host_header = (HOST_HEADER && process.env[HOST_HEADER]) || 'host'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -33,32 +36,16 @@ function serve(path, max_age, immutable = false) { /** @type {import('polka').Middleware} */ const ssr = async (req, res) => { - let body; + let request; try { - body = await getRawBody(req); + request = await getRequest(base || get_base(req.headers), req); } catch (err) { res.statusCode = err.status || 400; return res.end(err.reason || 'Invalid request body'); } - const rendered = await app.render({ - url: req.url, - method: req.method, - headers: req.headers, // TODO: what about repeated headers, i.e. string[] - rawBody: body - }); - - if (rendered) { - res.writeHead(rendered.status, rendered.headers); - if (rendered.body) { - res.write(rendered.body); - } - res.end(); - } else { - res.statusCode = 404; - res.end('Not found'); - } + setResponse(res, await app.render(request)); }; /** @param {import('polka').Middleware[]} handlers */ @@ -77,6 +64,16 @@ function sequence(handlers) { }; } +/** + * @param {import('http').IncomingHttpHeaders} headers + * @returns + */ +function get_base(headers) { + const protocol = (protocol_header && headers[protocol_header]) || 'https'; + const host = headers[host_header]; + return `${protocol}://${host}`; +} + export const handler = sequence( [ serve(path.join(__dirname, '/client'), 31536000, true), diff --git a/packages/adapter-node/src/types.d.ts b/packages/adapter-node/src/types.d.ts index 4fc8a2cd7a87..66ab40e7a8cf 100644 --- a/packages/adapter-node/src/types.d.ts +++ b/packages/adapter-node/src/types.d.ts @@ -2,6 +2,10 @@ declare global { const PATH_ENV: string; const HOST_ENV: string; const PORT_ENV: string; + const BASE_ENV: string; + + const PROTOCOL_HEADER: string; + const HOST_HEADER: string; } export {}; diff --git a/packages/adapter-vercel/files/ambient.d.ts b/packages/adapter-vercel/files/ambient.d.ts new file mode 100644 index 000000000000..279516ccb427 --- /dev/null +++ b/packages/adapter-vercel/files/ambient.d.ts @@ -0,0 +1,9 @@ +declare module 'APP' { + import { App } from '@sveltejs/kit'; + export { App }; +} + +declare module 'MANIFEST' { + import { SSRManifest } from '@sveltejs/kit'; + export const manifest: SSRManifest; +} diff --git a/packages/adapter-vercel/files/entry.js b/packages/adapter-vercel/files/entry.js index d3d2a26fefe0..9b42dbc79320 100644 --- a/packages/adapter-vercel/files/entry.js +++ b/packages/adapter-vercel/files/entry.js @@ -1,31 +1,23 @@ import './shims'; -import { getRawBody } from '@sveltejs/kit/node'; +import { getRequest, setResponse } from '@sveltejs/kit/node'; import { App } from 'APP'; import { manifest } from 'MANIFEST'; const app = new App(manifest); +/** + * @param {import('http').IncomingMessage} req + * @param {import('http').ServerResponse} res + */ export default async (req, res) => { - let body; + let request; try { - body = await getRawBody(req); + request = await getRequest(`https://${req.headers.host}`, req); } catch (err) { res.statusCode = err.status || 400; return res.end(err.reason || 'Invalid request body'); } - const rendered = await app.render({ - url: req.url, - method: req.method, - headers: req.headers, - rawBody: body - }); - - if (rendered) { - const { status, headers, body } = rendered; - return res.writeHead(status, headers).end(body); - } - - return res.writeHead(404).end(); + setResponse(res, await app.render(request)); }; diff --git a/packages/create-svelte/templates/default/README.md b/packages/create-svelte/templates/default/README.md index cbee96e2b786..9c8dc797a0be 100644 --- a/packages/create-svelte/templates/default/README.md +++ b/packages/create-svelte/templates/default/README.md @@ -1,4 +1,4 @@ -# Default template +# Default SvelteKit template This README isn't part of the template; it is ignored, and replaced with [the shared README](../../shared/README.md) when a project is created. diff --git a/packages/create-svelte/templates/default/netlify.toml b/packages/create-svelte/templates/default/netlify.toml index 88a39717479d..7dca1fcad039 100644 --- a/packages/create-svelte/templates/default/netlify.toml +++ b/packages/create-svelte/templates/default/netlify.toml @@ -4,3 +4,6 @@ [build.environment] NPM_FLAGS = "--version" # this prevents npm install from happening + +[functions] + node_bundler = "esbuild" \ No newline at end of file diff --git a/packages/create-svelte/templates/default/src/hooks.ts b/packages/create-svelte/templates/default/src/hooks.ts index ce345fed108f..d767555dd9ea 100644 --- a/packages/create-svelte/templates/default/src/hooks.ts +++ b/packages/create-svelte/templates/default/src/hooks.ts @@ -2,19 +2,22 @@ import cookie from 'cookie'; import { v4 as uuid } from '@lukeed/uuid'; import type { Handle } from '@sveltejs/kit'; -export const handle: Handle = async ({ request, resolve }) => { - const cookies = cookie.parse(request.headers.cookie || ''); - request.locals.userid = cookies.userid || uuid(); +export const handle: Handle = async ({ event, resolve }) => { + const cookies = cookie.parse(event.request.headers.get('cookie') || ''); + event.locals.userid = cookies.userid || uuid(); - const response = await resolve(request); + const response = await resolve(event); if (!cookies.userid) { // if this is the first time the user has visited this app, // set a cookie so that we recognise them when they return - response.headers['set-cookie'] = cookie.serialize('userid', request.locals.userid, { - path: '/', - httpOnly: true - }); + response.headers.set( + 'set-cookie', + cookie.serialize('userid', event.locals.userid, { + path: '/', + httpOnly: true + }) + ); } return response; diff --git a/packages/create-svelte/templates/default/src/routes/todos/[uid].json.ts b/packages/create-svelte/templates/default/src/routes/todos/[uid].json.ts index 17891faf5979..0ca276dbcbf3 100644 --- a/packages/create-svelte/templates/default/src/routes/todos/[uid].json.ts +++ b/packages/create-svelte/templates/default/src/routes/todos/[uid].json.ts @@ -3,14 +3,16 @@ import type { RequestHandler } from '@sveltejs/kit'; import type { Locals } from '$lib/types'; // PATCH /todos/:uid.json -export const patch: RequestHandler = async (request) => { - return api(request, `todos/${request.locals.userid}/${request.params.uid}`, { - text: request.body.get('text'), - done: request.body.has('done') ? !!request.body.get('done') : undefined +export const patch: RequestHandler = async (event) => { + const data = await event.request.formData(); + + return api(event, `todos/${event.locals.userid}/${event.params.uid}`, { + text: data.get('text'), + done: data.has('done') ? !!data.get('done') : undefined }); }; // DELETE /todos/:uid.json -export const del: RequestHandler = async (request) => { - return api(request, `todos/${request.locals.userid}/${request.params.uid}`); +export const del: RequestHandler = async (event) => { + return api(event, `todos/${event.locals.userid}/${event.params.uid}`); }; diff --git a/packages/create-svelte/templates/default/src/routes/todos/_api.ts b/packages/create-svelte/templates/default/src/routes/todos/_api.ts index 2fd3385d1ed8..4e13eb029573 100644 --- a/packages/create-svelte/templates/default/src/routes/todos/_api.ts +++ b/packages/create-svelte/templates/default/src/routes/todos/_api.ts @@ -1,4 +1,4 @@ -import type { EndpointOutput, Request } from '@sveltejs/kit'; +import type { EndpointOutput, RequestEvent } from '@sveltejs/kit'; import type { Locals } from '$lib/types'; /* @@ -15,17 +15,17 @@ import type { Locals } from '$lib/types'; const base = 'https://api.svelte.dev'; export async function api( - request: Request, + event: RequestEvent, resource: string, data?: Record ): Promise { // user must have a cookie set - if (!request.locals.userid) { + if (!event.locals.userid) { return { status: 401 }; } const res = await fetch(`${base}/${resource}`, { - method: request.method, + method: event.request.method, headers: { 'content-type': 'application/json' }, @@ -36,7 +36,11 @@ export async function api( // behaviour is to show the URL corresponding to the form's "action" // attribute. in those cases, we want to redirect them back to the // /todos page, rather than showing the response - if (res.ok && request.method !== 'GET' && request.headers.accept !== 'application/json') { + if ( + res.ok && + event.request.method !== 'GET' && + event.request.headers.get('accept') !== 'application/json' + ) { return { status: 303, headers: { diff --git a/packages/create-svelte/templates/default/src/routes/todos/index.json.ts b/packages/create-svelte/templates/default/src/routes/todos/index.json.ts index 57d68a91dc2f..c300fd971491 100644 --- a/packages/create-svelte/templates/default/src/routes/todos/index.json.ts +++ b/packages/create-svelte/templates/default/src/routes/todos/index.json.ts @@ -3,9 +3,9 @@ import type { RequestHandler } from '@sveltejs/kit'; import type { Locals } from '$lib/types'; // GET /todos.json -export const get: RequestHandler = async (request) => { - // request.locals.userid comes from src/hooks.js - const response = await api(request, `todos/${request.locals.userid}`); +export const get: RequestHandler = async (event) => { + // event.locals.userid comes from src/hooks.js + const response = await api(event, `todos/${event.locals.userid}`); if (response.status === 404) { // user hasn't created a todo list. @@ -17,13 +17,15 @@ export const get: RequestHandler = async (request) => { }; // POST /todos.json -export const post: RequestHandler = async (request) => { - const response = await api(request, `todos/${request.locals.userid}`, { +export const post: RequestHandler = async (event) => { + const data = await event.request.formData(); + + const response = await api(event, `todos/${event.locals.userid}`, { // because index.svelte posts a FormData object, // request.body is _also_ a (readonly) FormData // object, which allows us to get form data // with the `body.get(key)` method - text: request.body.get('text') + text: data.get('text') }); return response; diff --git a/packages/kit/src/core/adapt/prerender/prerender.js b/packages/kit/src/core/adapt/prerender/prerender.js index 1fcb5222796a..469c0b79314a 100644 --- a/packages/kit/src/core/adapt/prerender/prerender.js +++ b/packages/kit/src/core/adapt/prerender/prerender.js @@ -4,7 +4,6 @@ import { pathToFileURL, URL } from 'url'; import { mkdirp } from '../../../utils/filesystem.js'; import { __fetch_polyfill } from '../../../install-fetch.js'; import { SVELTE_KIT } from '../../constants.js'; -import { get_single_valued_header } from '../../../utils/http.js'; import { is_root_relative, resolve } from '../../../utils/url.js'; import { queue } from './queue.js'; import { crawl } from './crawl.js'; @@ -130,28 +129,19 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a * @param {string?} referrer */ async function visit(path, decoded_path, referrer) { - /** @type {Map} */ + /** @type {Map} */ const dependencies = new Map(); - const rendered = await app.render( - { - url: `${config.kit.protocol || 'http'}://${config.kit.host || 'prerender'}${path}`, - method: 'GET', - headers: {}, - rawBody: null - }, - { - prerender: { - all, - dependencies - } + const rendered = await app.render(new Request(`http://sveltekit-prerender${path}`), { + prerender: { + all, + dependencies } - ); + }); if (rendered) { const response_type = Math.floor(rendered.status / 100); - const headers = rendered.headers; - const type = headers && headers['content-type']; + const type = rendered.headers.get('content-type'); const is_html = response_type === REDIRECT || type === 'text/html'; const parts = decoded_path.split('/'); @@ -162,7 +152,7 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a const file = `${out}${parts.join('/')}`; if (response_type === REDIRECT) { - const location = get_single_valued_header(headers, 'location'); + const location = rendered.headers.get('location'); if (location) { mkdirp(dirname(file)); @@ -181,20 +171,22 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a return; } + const text = await rendered.text(); + if (rendered.status === 200) { mkdirp(dirname(file)); log.info(`${rendered.status} ${decoded_path}`); - writeFileSync(file, rendered.body || ''); + writeFileSync(file, text); paths.push(normalize(decoded_path)); } else if (response_type !== OK) { error({ status: rendered.status, path, referrer, referenceType: 'linked' }); } - dependencies.forEach((result, dependency_path) => { + for (const [dependency_path, result] of dependencies) { const response_type = Math.floor(result.status / 100); - const is_html = result.headers['content-type'] === 'text/html'; + const is_html = result.headers.get('content-type') === 'text/html'; const parts = dependency_path.split('/'); if (is_html && parts[parts.length - 1] !== 'index.html') { @@ -205,7 +197,7 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a mkdirp(dirname(file)); if (result.body) { - writeFileSync(file, result.body); + writeFileSync(file, await result.text()); paths.push(dependency_path); } @@ -219,10 +211,10 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a referenceType: 'fetched' }); } - }); + } if (is_html && config.kit.prerender.crawl) { - for (const href of crawl(/** @type {string} */ (rendered.body))) { + for (const href of crawl(text)) { if (href.startsWith('data:') || href.startsWith('#')) continue; const resolved = resolve(path, href); @@ -265,25 +257,17 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a } if (fallback) { - const rendered = await app.render( - { - url: `${config.kit.protocol || 'http'}://${config.kit.host || 'prerender'}/[fallback]`, - method: 'GET', - headers: {}, - rawBody: null - }, - { - prerender: { - fallback, - all: false, - dependencies: new Map() - } + const rendered = await app.render(new Request('http://sveltekit-prerender/[fallback]'), { + prerender: { + fallback, + all: false, + dependencies: new Map() } - ); + }); const file = join(out, fallback); mkdirp(dirname(file)); - writeFileSync(file, rendered.body || ''); + writeFileSync(file, await rendered.text()); } return { diff --git a/packages/kit/src/core/adapt/test/fixtures/prerender/.svelte-kit/output/server/app.js b/packages/kit/src/core/adapt/test/fixtures/prerender/.svelte-kit/output/server/app.js index a59d5d30f0a5..554862072253 100644 --- a/packages/kit/src/core/adapt/test/fixtures/prerender/.svelte-kit/output/server/app.js +++ b/packages/kit/src/core/adapt/test/fixtures/prerender/.svelte-kit/output/server/app.js @@ -1,12 +1,11 @@ export class App { render() { - return { + return new Response('', { status: 200, headers: { 'content-type': 'text/html' - }, - body: '' - }; + } + }); } } diff --git a/packages/kit/src/core/build/build_server.js b/packages/kit/src/core/build/build_server.js index 86a1f3c35dd5..0e8f9b6cbd1d 100644 --- a/packages/kit/src/core/build/build_server.js +++ b/packages/kit/src/core/build/build_server.js @@ -38,7 +38,7 @@ set_paths(${s(config.kit.paths)}); // named imports without triggering Rollup's missing import detection const get_hooks = hooks => ({ getSession: hooks.getSession || (() => ({})), - handle: hooks.handle || (({ request, resolve }) => resolve(request)), + handle: hooks.handle || (({ event, resolve }) => resolve(event)), handleError: hooks.handleError || (({ error }) => console.error(error.stack)), externalFetch: hooks.externalFetch || fetch }); @@ -63,8 +63,17 @@ export class App { dev: false, floc: ${config.kit.floc}, get_stack: error => String(error), // for security - handle_error: (error, request) => { - hooks.handleError({ error, request }); + handle_error: (error, event) => { + hooks.handleError({ + error, + event, + + // TODO remove for 1.0 + // @ts-expect-error + get request() { + throw new Error('request in handleError has been replaced with event. See https://github.com/sveltejs/kit/pull/3384 for details'); + } + }); error.stack = this.options.get_stack(error); }, hooks, @@ -87,25 +96,11 @@ export class App { render(request, { prerender } = {}) { - // TODO remove this for 1.0 - if (Object.keys(request).sort().join() !== 'headers,method,rawBody,url') { - throw new Error('Adapters should call app.render({ url, method, headers, rawBody })'); + if (!(request instanceof Request)) { + throw new Error('The first argument to app.render must be a Request object. See https://github.com/sveltejs/kit/pull/3384 for details'); } - const host = ${ - config.kit.host - ? s(config.kit.host) - : `request.headers[${s(config.kit.headers.host || 'host')}]` - }; - const protocol = ${ - config.kit.protocol - ? s(config.kit.protocol) - : config.kit.headers.protocol - ? `request.headers[${s(config.kit.headers.protocol)}] || default_protocol` - : 'default_protocol' - }; - - return respond({ ...request, url: new URL(request.url, protocol + '://' + host) }, this.options, { prerender }); + return respond(request, this.options, { prerender }); } } `; diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index 636163b9f41d..df7f0a0e527a 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -29,11 +29,8 @@ test('fills in defaults', () => { template: 'src/app.html' }, floc: false, - headers: { - host: null, - protocol: null - }, - host: null, + headers: undefined, + host: undefined, hydrate: true, inlineStyleThreshold: 0, methodOverride: { @@ -60,7 +57,7 @@ test('fills in defaults', () => { onError: 'fail', pages: undefined }, - protocol: null, + protocol: undefined, router: true, ssr: null, target: null, @@ -141,11 +138,8 @@ test('fills in partial blanks', () => { template: 'src/app.html' }, floc: false, - headers: { - host: null, - protocol: null - }, - host: null, + headers: undefined, + host: undefined, hydrate: true, inlineStyleThreshold: 0, methodOverride: { @@ -172,7 +166,7 @@ test('fills in partial blanks', () => { onError: 'fail', pages: undefined }, - protocol: null, + protocol: undefined, router: true, ssr: null, target: null, diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index 0eaf5e8567e6..96d1b8ad9ca1 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -66,12 +66,23 @@ const options = object( floc: boolean(false), - headers: object({ - host: string(null), - protocol: string(null) + // TODO: remove this for the 1.0 release + headers: validate(undefined, (input, keypath) => { + if (typeof input !== undefined) { + throw new Error( + `${keypath} has been removed. See https://github.com/sveltejs/kit/pull/3384 for details` + ); + } }), - host: string(null), + // TODO: remove this for the 1.0 release + host: validate(undefined, (input, keypath) => { + if (typeof input !== undefined) { + throw new Error( + `${keypath} has been removed. See https://github.com/sveltejs/kit/pull/3384 for details` + ); + } + }), hydrate: boolean(true), @@ -152,6 +163,7 @@ const options = object( return input; }), + // TODO: remove this for the 1.0 release force: validate(undefined, (input, keypath) => { if (typeof input !== undefined) { @@ -164,6 +176,7 @@ const options = object( ); } }), + onError: validate('fail', (input, keypath) => { if (typeof input === 'function') return input; if (['continue', 'fail'].includes(input)) return input; @@ -171,6 +184,7 @@ const options = object( `${keypath} should be either a custom function or one of "continue" or "fail"` ); }), + // TODO: remove this for the 1.0 release pages: validate(undefined, (input, keypath) => { if (typeof input !== undefined) { @@ -179,7 +193,14 @@ const options = object( }) }), - protocol: string(null), + // TODO: remove this for the 1.0 release + protocol: validate(undefined, (input, keypath) => { + if (typeof input !== undefined) { + throw new Error( + `${keypath} has been removed. See https://github.com/sveltejs/kit/pull/3384 for details` + ); + } + }), router: boolean(true), diff --git a/packages/kit/src/core/config/test/index.js b/packages/kit/src/core/config/test/index.js index c85d2e60c2ed..02cb1fd55703 100644 --- a/packages/kit/src/core/config/test/index.js +++ b/packages/kit/src/core/config/test/index.js @@ -31,11 +31,8 @@ test('load default config (esm)', async () => { template: join(cwd, 'src/app.html') }, floc: false, - headers: { - host: null, - protocol: null - }, - host: null, + headers: undefined, + host: undefined, hydrate: true, inlineStyleThreshold: 0, methodOverride: { @@ -59,7 +56,7 @@ test('load default config (esm)', async () => { onError: 'fail', pages: undefined }, - protocol: null, + protocol: undefined, router: true, ssr: null, target: null, diff --git a/packages/kit/src/core/dev/plugin.js b/packages/kit/src/core/dev/plugin.js index 765ae7abc525..7fa63353e2e4 100644 --- a/packages/kit/src/core/dev/plugin.js +++ b/packages/kit/src/core/dev/plugin.js @@ -7,7 +7,7 @@ import { respond } from '../../runtime/server/index.js'; import { __fetch_polyfill } from '../../install-fetch.js'; import { create_app } from '../create_app/index.js'; import create_manifest_data from '../create_manifest_data/index.js'; -import { getRawBody } from '../../node.js'; +import { getRawBody, setResponse } from '../../node.js'; import { SVELTE_KIT, SVELTE_KIT_ASSETS } from '../constants.js'; import { get_mime_lookup, resolve_entry, runtime } from '../utils.js'; import { coalesce_to_error } from '../../utils/error.js'; @@ -170,7 +170,7 @@ export async function create_plugin(config, cwd) { /** @type {import('types/internal').Hooks} */ const hooks = { getSession: user_hooks.getSession || (() => ({})), - handle: user_hooks.handle || (({ request, resolve }) => resolve(request)), + handle: user_hooks.handle || (({ event, resolve }) => resolve(event)), handleError: user_hooks.handleError || (({ /** @type {Error & { frame?: string }} */ error }) => { @@ -217,12 +217,11 @@ export async function create_plugin(config, cwd) { } const rendered = await respond( - { - url, - headers: /** @type {import('types/helper').RequestHeaders} */ (req.headers), + new Request(url.href, { + headers: /** @type {Record} */ (req.headers), method: req.method, - rawBody: body - }, + body + }), { amp: config.kit.amp, dev: true, @@ -231,9 +230,20 @@ export async function create_plugin(config, cwd) { vite.ssrFixStacktrace(error); return error.stack; }, - handle_error: (error, request) => { + handle_error: (error, event) => { vite.ssrFixStacktrace(error); - hooks.handleError({ error, request }); + hooks.handleError({ + error, + event, + + // TODO remove for 1.0 + // @ts-expect-error + get request() { + throw new Error( + 'request in handleError has been replaced with event. See https://github.com/sveltejs/kit/pull/3384 for details' + ); + } + }); }, hooks, hydrate: config.kit.hydrate, @@ -306,9 +316,7 @@ export async function create_plugin(config, cwd) { ); if (rendered) { - res.writeHead(rendered.status, rendered.headers); - if (rendered.body) res.write(rendered.body); - res.end(); + setResponse(res, rendered); } else { not_found(res); } diff --git a/packages/kit/src/core/preview/index.js b/packages/kit/src/core/preview/index.js index b833d4eee0a6..cd8ef1b7178d 100644 --- a/packages/kit/src/core/preview/index.js +++ b/packages/kit/src/core/preview/index.js @@ -7,6 +7,7 @@ import { pathToFileURL } from 'url'; import { getRawBody } from '../../node.js'; import { __fetch_polyfill } from '../../install-fetch.js'; import { SVELTE_KIT, SVELTE_KIT_ASSETS } from '../constants.js'; +import { to_headers } from '../../utils/http.js'; /** @param {string} dir */ const mutable = (dir) => @@ -90,18 +91,20 @@ export async function preview({ return res.end(err.reason || 'Invalid request body'); } - const rendered = - initial_url.startsWith(config.kit.paths.base) && - (await app.render({ - url: initial_url, - method: req.method, - headers: /** @type {import('types/helper').RequestHeaders} */ (req.headers), - rawBody: body - })); - - if (rendered) { - res.writeHead(rendered.status, rendered.headers); - if (rendered.body) res.write(rendered.body); + if (initial_url.startsWith(config.kit.paths.base)) { + const protocol = use_https ? 'https' : 'http'; + const host = req.headers['host']; + + const rendered = await app.render( + new Request(`${protocol}://${host}${initial_url}`, { + method: req.method, + headers: to_headers(req.headers), + body + }) + ); + + res.writeHead(rendered.status, Object.fromEntries(rendered.headers)); + if (rendered.body) res.write(new Uint8Array(await rendered.arrayBuffer())); res.end(); } else { res.statusCode = 404; diff --git a/packages/kit/src/hooks.js b/packages/kit/src/hooks.js index 822c09b21e02..5fb1d7d7be3f 100644 --- a/packages/kit/src/hooks.js +++ b/packages/kit/src/hooks.js @@ -4,22 +4,22 @@ */ export function sequence(...handlers) { const length = handlers.length; - if (!length) return ({ request, resolve }) => resolve(request); + if (!length) return ({ event, resolve }) => resolve(event); - return ({ request, resolve }) => { - return apply_handle(0, request); + return ({ event, resolve }) => { + return apply_handle(0, event); /** * @param {number} i - * @param {import('types/hooks').ServerRequest} request - * @returns {import('types/helper').MaybePromise} + * @param {import('types/hooks').RequestEvent} event + * @returns {import('types/helper').MaybePromise} */ - function apply_handle(i, request) { + function apply_handle(i, event) { const handle = handlers[i]; return handle({ - request, - resolve: i < length - 1 ? (request) => apply_handle(i + 1, request) : resolve + event, + resolve: i < length - 1 ? (event) => apply_handle(i + 1, event) : resolve }); } }; diff --git a/packages/kit/src/node.js b/packages/kit/src/node.js index 4d7ca271a53b..b7cd213d9d59 100644 --- a/packages/kit/src/node.js +++ b/packages/kit/src/node.js @@ -1,3 +1,5 @@ +import { Readable } from 'stream'; + /** @type {import('@sveltejs/kit/node').GetRawBody} */ export function getRawBody(req) { return new Promise((fulfil, reject) => { @@ -47,3 +49,27 @@ export function getRawBody(req) { }); }); } + +/** @type {import('@sveltejs/kit/node').GetRequest} */ +export async function getRequest(base, req) { + return new Request(base + req.url, { + method: req.method, + headers: /** @type {Record} */ (req.headers), + body: await getRawBody(req) + }); +} + +/** @type {import('@sveltejs/kit/node').SetResponse} */ +export async function setResponse(res, response) { + res.writeHead(response.status, Object.fromEntries(response.headers)); + + if (response.body instanceof Readable) { + response.body.pipe(res); + } else { + if (response.body) { + res.write(await response.arrayBuffer()); + } + + res.end(); + } +} diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js index dede4fb26ff2..794217e2cb55 100644 --- a/packages/kit/src/runtime/server/endpoint.js +++ b/packages/kit/src/runtime/server/endpoint.js @@ -1,13 +1,12 @@ -import { get_single_valued_header } from '../../utils/http.js'; -import { decode_params, lowercase_keys } from './utils.js'; +import { to_headers } from '../../utils/http.js'; +import { hash } from '../hash.js'; +import { decode_params } from './utils.js'; /** @param {string} body */ function error(body) { - return { - status: 500, - body, - headers: {} - }; + return new Response(body, { + status: 500 + }); } /** @param {unknown} s */ @@ -36,16 +35,16 @@ export function is_text(content_type) { } /** - * @param {import('types/hooks').ServerRequest} request + * @param {import('types/hooks').RequestEvent} event * @param {import('types/internal').SSREndpoint} route * @param {RegExpExecArray} match - * @returns {Promise} + * @returns {Promise} */ -export async function render_endpoint(request, route, match) { +export async function render_endpoint(event, route, match) { const mod = await route.load(); /** @type {import('types/endpoint').RequestHandler} */ - const handler = mod[request.method.toLowerCase().replace('delete', 'del')]; // 'delete' is a reserved word + const handler = mod[event.request.method.toLowerCase().replace('delete', 'del')]; // 'delete' is a reserved word if (!handler) { return; @@ -54,10 +53,10 @@ export async function render_endpoint(request, route, match) { // we're mutating `request` so that we don't have to do { ...request, params } // on the next line, since that breaks the getters that replace path, query and // origin. We could revert that once we remove the getters - request.params = route.params ? decode_params(route.params(match)) : {}; + event.params = route.params ? decode_params(route.params(match)) : {}; - const response = await handler(request); - const preface = `Invalid response from route ${request.url.pathname}`; + const response = await handler(event); + const preface = `Invalid response from route ${event.url.pathname}`; if (typeof response !== 'object') { return error(`${preface}: expected an object, got ${typeof response}`); @@ -67,10 +66,11 @@ export async function render_endpoint(request, route, match) { return; } - let { status = 200, body, headers = {} } = response; + const { status = 200, body = {} } = response; + const headers = + response.headers instanceof Headers ? response.headers : to_headers(response.headers); - headers = lowercase_keys(headers); - const type = get_single_valued_header(headers, 'content-type'); + const type = headers.get('content-type'); if (!is_text(type) && !(body instanceof Uint8Array || is_string(body))) { return error( @@ -81,17 +81,43 @@ export async function render_endpoint(request, route, match) { /** @type {import('types/hooks').StrictBody} */ let normalized_body; - // ensure the body is an object - if ( - (typeof body === 'object' || typeof body === 'undefined') && - !(body instanceof Uint8Array) && - (!type || type.startsWith('application/json')) - ) { - headers = { ...headers, 'content-type': 'application/json; charset=utf-8' }; - normalized_body = JSON.stringify(typeof body === 'undefined' ? {} : body); + if (is_pojo(body) && (!type || type.startsWith('application/json'))) { + headers.set('content-type', 'application/json; charset=utf-8'); + normalized_body = JSON.stringify(body); } else { normalized_body = /** @type {import('types/hooks').StrictBody} */ (body); } - return { status, body: normalized_body, headers }; + if ( + (typeof normalized_body === 'string' || normalized_body instanceof Uint8Array) && + !headers.has('etag') + ) { + const cache_control = headers.get('cache-control'); + if (!cache_control || !/(no-store|immutable)/.test(cache_control)) { + headers.set('etag', `"${hash(normalized_body)}"`); + } + } + + return new Response(normalized_body, { + status, + headers + }); +} + +/** @param {any} body */ +function is_pojo(body) { + if (typeof body !== 'object') return false; + + if (body) { + if (body instanceof Uint8Array) return false; + + // body could be a node Readable, but we don't want to import + // node built-ins, so we use duck typing + if (body._readableState && body._writableState && body._events) return false; + + // similarly, it could be a web ReadableStream + if (body[Symbol.toStringTag] === 'ReadableStream') return false; + } + + return true; } diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 9692866bbfa7..ff095219af26 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -2,101 +2,115 @@ import { render_endpoint } from './endpoint.js'; import { render_page } from './page/index.js'; import { render_response } from './page/render.js'; import { respond_with_error } from './page/respond_with_error.js'; -import { parse_body } from './parse_body/index.js'; -import { lowercase_keys } from './utils.js'; -import { hash } from '../hash.js'; -import { get_single_valued_header } from '../../utils/http.js'; import { coalesce_to_error } from '../../utils/error.js'; -/** @type {import('@sveltejs/kit/ssr').Respond} */ -export async function respond(incoming, options, state = {}) { - if (incoming.url.pathname !== '/' && options.trailing_slash !== 'ignore') { - const has_trailing_slash = incoming.url.pathname.endsWith('/'); +/** @type {import('types/internal').Respond} */ +export async function respond(request, options, state = {}) { + const url = new URL(request.url); + + if (url.pathname !== '/' && options.trailing_slash !== 'ignore') { + const has_trailing_slash = url.pathname.endsWith('/'); if ( (has_trailing_slash && options.trailing_slash === 'never') || (!has_trailing_slash && options.trailing_slash === 'always' && - !(incoming.url.pathname.split('/').pop() || '').includes('.')) + !(url.pathname.split('/').pop() || '').includes('.')) ) { - incoming.url.pathname = has_trailing_slash - ? incoming.url.pathname.slice(0, -1) - : incoming.url.pathname + '/'; + url.pathname = has_trailing_slash ? url.pathname.slice(0, -1) : url.pathname + '/'; - if (incoming.url.search === '?') incoming.url.search = ''; + if (url.search === '?') url.search = ''; - return { + return new Response(undefined, { status: 301, headers: { - location: incoming.url.pathname + incoming.url.search + location: url.pathname + url.search } - }; + }); } } - const headers = lowercase_keys(incoming.headers); - const request = { - ...incoming, - headers, - body: parse_body(incoming.rawBody, headers), - params: {}, - locals: {} - }; - const { parameter, allowed } = options.method_override; - const method_override = incoming.url.searchParams.get(parameter)?.toUpperCase(); + const method_override = url.searchParams.get(parameter)?.toUpperCase(); if (method_override) { - if (request.method.toUpperCase() === 'POST') { + if (request.method === 'POST') { if (allowed.includes(method_override)) { - request.method = method_override; + request = new Proxy(request, { + get: (target, property, _receiver) => { + if (property === 'method') return method_override; + return Reflect.get(target, property, target); + } + }); } else { const verb = allowed.length === 0 ? 'enabled' : 'allowed'; const body = `${parameter}=${method_override} is not ${verb}. See https://kit.svelte.dev/docs#configuration-methodoverride`; - return { - status: 400, - headers: {}, - body - }; + return new Response(body, { + status: 400 + }); } } else { throw new Error(`${parameter}=${method_override} is only allowed with POST requests`); } } + /** @type {import('types/hooks').RequestEvent} */ + const event = { + request, + url, + params: {}, + locals: {} + }; + // TODO remove this for 1.0 /** * @param {string} property * @param {string} replacement + * @param {string} suffix */ - const print_error = (property, replacement) => { - Object.defineProperty(request, property, { - get: () => { - throw new Error(`request.${property} has been replaced by request.url.${replacement}`); - } - }); + const removed = (property, replacement, suffix = '') => ({ + get: () => { + throw new Error(`event.${property} has been replaced by event.${replacement}` + suffix); + } + }); + + const details = '. See https://github.com/sveltejs/kit/pull/3384 for details'; + + const body_getter = { + get: () => { + throw new Error( + 'To access the request body use the text/json/arrayBuffer/formData methods, e.g. `body = await request.json()`' + + details + ); + } }; - print_error('origin', 'origin'); - print_error('path', 'pathname'); - print_error('query', 'searchParams'); + Object.defineProperties(event, { + method: removed('method', 'request.method', details), + headers: removed('headers', 'request.headers', details), + origin: removed('origin', 'url.origin'), + path: removed('path', 'url.pathname'), + query: removed('query', 'url.searchParams'), + body: body_getter, + rawBody: body_getter + }); let ssr = true; try { return await options.hooks.handle({ - request, - resolve: async (request, opts) => { + event, + resolve: async (event, opts) => { if (opts && 'ssr' in opts) ssr = /** @type {boolean} */ (opts.ssr); if (state.prerender && state.prerender.fallback) { return await render_response({ - url: request.url, - params: request.params, + url: event.url, + params: event.params, options, state, - $session: await options.hooks.getSession(request), + $session: await options.hooks.getSession(event), page_config: { router: true, hydrate: true }, stuff: {}, status: 200, @@ -105,7 +119,7 @@ export async function respond(incoming, options, state = {}) { }); } - let decoded = decodeURI(request.url.pathname); + let decoded = decodeURI(event.url.pathname); if (options.paths.base) { if (!decoded.startsWith(options.paths.base)) return; @@ -118,47 +132,40 @@ export async function respond(incoming, options, state = {}) { const response = route.type === 'endpoint' - ? await render_endpoint(request, route, match) - : await render_page(request, route, match, options, state, ssr); + ? await render_endpoint(event, route, match) + : await render_page(event, route, match, options, state, ssr); if (response) { - // inject ETags for 200 responses, if the endpoint - // doesn't have its own ETag handling - if (response.status === 200 && !response.headers.etag) { - const cache_control = get_single_valued_header(response.headers, 'cache-control'); - if (!cache_control || !/(no-store|immutable)/.test(cache_control)) { - let if_none_match_value = request.headers['if-none-match']; - // ignore W/ prefix https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives - if (if_none_match_value?.startsWith('W/"')) { - if_none_match_value = if_none_match_value.substring(2); - } + // respond with 304 if etag matches + if (response.status === 200 && response.headers.has('etag')) { + let if_none_match_value = request.headers.get('if-none-match'); - const etag = `"${hash(response.body || '')}"`; - - if (if_none_match_value === etag) { - /** @type {import('types/helper').ResponseHeaders} */ - const headers = { etag }; - - // https://datatracker.ietf.org/doc/html/rfc7232#section-4.1 - for (const key of [ - 'cache-control', - 'content-location', - 'date', - 'expires', - 'vary' - ]) { - if (key in response.headers) { - headers[key] = /** @type {string} */ (response.headers[key]); - } - } - - return { - status: 304, - headers - }; + // ignore W/ prefix https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives + if (if_none_match_value?.startsWith('W/"')) { + if_none_match_value = if_none_match_value.substring(2); + } + + const etag = /** @type {string} */ (response.headers.get('etag')); + + if (if_none_match_value === etag) { + const headers = new Headers({ etag }); + + // https://datatracker.ietf.org/doc/html/rfc7232#section-4.1 + for (const key of [ + 'cache-control', + 'content-location', + 'date', + 'expires', + 'vary' + ]) { + const value = response.headers.get(key); + if (value) headers.set(key, value); } - response.headers['etag'] = etag; + return new Response(undefined, { + status: 304, + headers + }); } } @@ -169,28 +176,34 @@ export async function respond(incoming, options, state = {}) { // if this request came direct from the user, rather than // via a `fetch` in a `load`, render a 404 page if (!state.initiator) { - const $session = await options.hooks.getSession(request); + const $session = await options.hooks.getSession(event); return await respond_with_error({ - request, + event, options, state, $session, status: 404, - error: new Error(`Not found: ${request.url.pathname}`), + error: new Error(`Not found: ${event.url.pathname}`), ssr }); } + }, + + // TODO remove for 1.0 + // @ts-expect-error + get request() { + throw new Error('request in handle has been replaced with event' + details); } }); } catch (/** @type {unknown} */ e) { const error = coalesce_to_error(e); - options.handle_error(error, request); + options.handle_error(error, event); try { - const $session = await options.hooks.getSession(request); + const $session = await options.hooks.getSession(event); return await respond_with_error({ - request, + event, options, state, $session, @@ -201,11 +214,9 @@ export async function respond(incoming, options, state = {}) { } catch (/** @type {unknown} */ e) { const error = coalesce_to_error(e); - return { - status: 500, - headers: {}, - body: options.dev ? error.stack : error.message - }; + return new Response(options.dev ? error.stack : error.message, { + status: 500 + }); } } } diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index 1359cdb0eadb..8ead58326e12 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -2,30 +2,28 @@ import { decode_params } from '../utils.js'; import { respond } from './respond.js'; /** - * @param {import('types/hooks').ServerRequest} request + * @param {import('types/hooks').RequestEvent} event * @param {import('types/internal').SSRPage} route * @param {RegExpExecArray} match * @param {import('types/internal').SSRRenderOptions} options * @param {import('types/internal').SSRRenderState} state * @param {boolean} ssr - * @returns {Promise} + * @returns {Promise} */ -export async function render_page(request, route, match, options, state, ssr) { +export async function render_page(event, route, match, options, state, ssr) { if (state.initiator === route) { // infinite request cycle detected - return { - status: 404, - headers: {}, - body: `Not found: ${request.url.pathname}` - }; + return new Response(`Not found: ${event.url.pathname}`, { + status: 404 + }); } const params = route.params ? decode_params(route.params(match)) : {}; - const $session = await options.hooks.getSession(request); + const $session = await options.hooks.getSession(event); const response = await respond({ - request, + event, options, state, $session, @@ -43,10 +41,8 @@ export async function render_page(request, route, match, options, state, ssr) { // rather than render the error page — which could lead to an // infinite loop, if the `load` belonged to the root layout, // we respond with a bare-bones 500 - return { - status: 500, - headers: {}, - body: `Bad request in load function: failed to fetch ${state.fetched}` - }; + return new Response(`Bad request in load function: failed to fetch ${state.fetched}`, { + status: 500 + }); } } diff --git a/packages/kit/src/runtime/server/page/load_node.js b/packages/kit/src/runtime/server/page/load_node.js index 94c80eb0e08a..cf87f94d3743 100644 --- a/packages/kit/src/runtime/server/page/load_node.js +++ b/packages/kit/src/runtime/server/page/load_node.js @@ -7,7 +7,7 @@ import { create_prerendering_url_proxy } from './utils.js'; /** * @param {{ - * request: import('types/hooks').ServerRequest; + * event: import('types/hooks').RequestEvent; * options: import('types/internal').SSRRenderOptions; * state: import('types/internal').SSRRenderState; * route: import('types/internal').SSRPage | null; @@ -23,7 +23,7 @@ import { create_prerendering_url_proxy } from './utils.js'; * @returns {Promise} undefined for fallthrough */ export async function load_node({ - request, + event, options, state, route, @@ -94,7 +94,7 @@ export async function load_node({ opts.headers = new Headers(opts.headers); - const resolved = resolve(request.url.pathname, requested.split('?')[0]); + const resolved = resolve(event.url.pathname, requested.split('?')[0]); let response; @@ -130,12 +130,15 @@ export async function load_node({ if (opts.credentials !== 'omit') { uses_credentials = true; - if (request.headers.cookie) { - opts.headers.set('cookie', request.headers.cookie); + const cookie = event.request.headers.get('cookie'); + const authorization = event.request.headers.get('authorization'); + + if (cookie) { + opts.headers.set('cookie', cookie); } - if (request.headers.authorization && !opts.headers.has('authorization')) { - opts.headers.set('authorization', request.headers.authorization); + if (authorization && !opts.headers.has('authorization')) { + opts.headers.set('authorization', authorization); } } @@ -148,12 +151,7 @@ export async function load_node({ } const rendered = await respond( - { - url: new URL(requested, request.url), - method: opts.method || 'GET', - headers: Object.fromEntries(opts.headers), - rawBody: opts.body == null ? null : new TextEncoder().encode(opts.body) - }, + new Request(new URL(requested, event.url).href, opts), options, { fetched: requested, @@ -166,17 +164,11 @@ export async function load_node({ state.prerender.dependencies.set(relative, rendered); } - // Set-Cookie must be filtered out (done below) and that's the only header value that - // can be an array so we know we have only simple values - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie - response = new Response(rendered.body, { - status: rendered.status, - headers: /** @type {Record} */ (rendered.headers) - }); + response = rendered; } else { // we can't load the endpoint from our own manifest, // so we need to make an actual HTTP request - return fetch(new URL(requested, request.url).href, { + return fetch(new URL(requested, event.url).href, { method: opts.method || 'GET', headers: opts.headers }); @@ -199,11 +191,13 @@ export async function load_node({ // ports do not affect the resolution // leading dot prevents mydomain.com matching domain.com if ( - `.${new URL(requested).hostname}`.endsWith(`.${request.url.hostname}`) && + `.${new URL(requested).hostname}`.endsWith(`.${event.url.hostname}`) && opts.credentials !== 'omit' ) { uses_credentials = true; - opts.headers.set('cookie', request.headers.cookie); + + const cookie = event.request.headers.get('cookie'); + if (cookie) opts.headers.set('cookie', cookie); } const external_request = new Request(requested, /** @type {RequestInit} */ (opts)); diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index db66d9949841..0cb59e443144 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -200,32 +200,29 @@ export async function render_response({ } } - /** @type {import('types/helper').ResponseHeaders} */ - const headers = { - 'content-type': 'text/html' - }; + const segments = url.pathname.slice(options.paths.base.length).split('/').slice(2); + const assets = + options.paths.assets || (segments.length > 0 ? segments.map(() => '..').join('/') : '.'); + + const html = options.template({ head, body, assets }); + + const headers = new Headers({ + 'content-type': 'text/html', + etag: `"${hash(html)}"` + }); if (maxage) { - headers['cache-control'] = `${is_private ? 'private' : 'public'}, max-age=${maxage}`; + headers.set('cache-control', `${is_private ? 'private' : 'public'}, max-age=${maxage}`); } if (!options.floc) { - headers['permissions-policy'] = 'interest-cohort=()'; + headers.set('permissions-policy', 'interest-cohort=()'); } - const segments = url.pathname.slice(options.paths.base.length).split('/').slice(2); - const assets = - options.paths.assets || (segments.length > 0 ? segments.map(() => '..').join('/') : '.'); - - return { + return new Response(html, { status, - headers, - body: options.template({ - head, - body, - assets - }) - }; + headers + }); } /** diff --git a/packages/kit/src/runtime/server/page/respond.js b/packages/kit/src/runtime/server/page/respond.js index c38fb7264365..791f19158122 100644 --- a/packages/kit/src/runtime/server/page/respond.js +++ b/packages/kit/src/runtime/server/page/respond.js @@ -5,7 +5,6 @@ import { coalesce_to_error } from '../../../utils/error.js'; /** * @typedef {import('./types.js').Loaded} Loaded - * @typedef {import('types/hooks').ServerResponse} ServerResponse * @typedef {import('types/internal').SSRNode} SSRNode * @typedef {import('types/internal').SSRRenderOptions} SSRRenderOptions * @typedef {import('types/internal').SSRRenderState} SSRRenderState @@ -13,7 +12,7 @@ import { coalesce_to_error } from '../../../utils/error.js'; /** * @param {{ - * request: import('types/hooks').ServerRequest; + * event: import('types/hooks').RequestEvent; * options: SSRRenderOptions; * state: SSRRenderState; * $session: any; @@ -21,10 +20,10 @@ import { coalesce_to_error } from '../../../utils/error.js'; * params: Record; * ssr: boolean; * }} opts - * @returns {Promise} + * @returns {Promise} */ export async function respond(opts) { - const { request, options, state, $session, route, ssr } = opts; + const { event, options, state, $session, route, ssr } = opts; /** @type {Array} */ let nodes; @@ -38,7 +37,7 @@ export async function respond(opts) { router: true }, status: 200, - url: request.url, + url: event.url, stuff: {} }); } @@ -50,10 +49,10 @@ export async function respond(opts) { } catch (err) { const error = coalesce_to_error(err); - options.handle_error(error, request); + options.handle_error(error, event); return await respond_with_error({ - request, + event, options, state, $session, @@ -71,10 +70,9 @@ export async function respond(opts) { if (!leaf.prerender && state.prerender && !state.prerender.all) { // if the page has `export const prerender = true`, continue, // otherwise bail out at this point - return { - status: 204, - headers: {} - }; + return new Response(undefined, { + status: 204 + }); } /** @type {Array} */ @@ -102,7 +100,7 @@ export async function respond(opts) { try { loaded = await load_node({ ...opts, - url: request.url, + url: event.url, node, stuff, is_error: false @@ -114,12 +112,12 @@ export async function respond(opts) { if (loaded.loaded.redirect) { return with_cookies( - { + new Response(undefined, { status: loaded.loaded.status, headers: { location: encodeURI(loaded.loaded.redirect) } - }, + }), set_cookie_headers ); } @@ -130,7 +128,7 @@ export async function respond(opts) { } catch (err) { const e = coalesce_to_error(err); - options.handle_error(e, request); + options.handle_error(e, event); status = 500; error = e; @@ -156,7 +154,7 @@ export async function respond(opts) { const error_loaded = /** @type {import('./types').Loaded} */ ( await load_node({ ...opts, - url: request.url, + url: event.url, node: error_node, stuff: node_loaded.stuff, is_error: true, @@ -176,7 +174,7 @@ export async function respond(opts) { } catch (err) { const e = coalesce_to_error(err); - options.handle_error(e, request); + options.handle_error(e, event); continue; } @@ -188,7 +186,7 @@ export async function respond(opts) { // for now just return regular error page return with_cookies( await respond_with_error({ - request, + event, options, state, $session, @@ -215,7 +213,7 @@ export async function respond(opts) { await render_response({ ...opts, stuff, - url: request.url, + url: event.url, page_config, status, error, @@ -226,7 +224,7 @@ export async function respond(opts) { } catch (err) { const error = coalesce_to_error(err); - options.handle_error(error, request); + options.handle_error(error, event); return with_cookies( await respond_with_error({ @@ -258,12 +256,14 @@ function get_page_config(leaf, options) { } /** - * @param {ServerResponse} response + * @param {Response} response * @param {string[]} set_cookie_headers */ function with_cookies(response, set_cookie_headers) { if (set_cookie_headers.length) { - response.headers['set-cookie'] = set_cookie_headers; + set_cookie_headers.forEach((value) => { + response.headers.append('set-cookie', value); + }); } return response; } diff --git a/packages/kit/src/runtime/server/page/respond_with_error.js b/packages/kit/src/runtime/server/page/respond_with_error.js index cfd05eec5dc4..8eb5c1730e29 100644 --- a/packages/kit/src/runtime/server/page/respond_with_error.js +++ b/packages/kit/src/runtime/server/page/respond_with_error.js @@ -10,7 +10,7 @@ import { coalesce_to_error } from '../../../utils/error.js'; /** * @param {{ - * request: import('types/hooks').ServerRequest; + * event: import('types/hooks').RequestEvent; * options: SSRRenderOptions; * state: SSRRenderState; * $session: any; @@ -19,15 +19,7 @@ import { coalesce_to_error } from '../../../utils/error.js'; * ssr: boolean; * }} opts */ -export async function respond_with_error({ - request, - options, - state, - $session, - status, - error, - ssr -}) { +export async function respond_with_error({ event, options, state, $session, status, error, ssr }) { try { const default_layout = await options.manifest._.nodes[0](); // 0 is always the root layout const default_error = await options.manifest._.nodes[1](); // 1 is always the root error @@ -37,11 +29,11 @@ export async function respond_with_error({ const layout_loaded = /** @type {Loaded} */ ( await load_node({ - request, + event, options, state, route: null, - url: request.url, // TODO this is redundant, no? + url: event.url, // TODO this is redundant, no? params, node: default_layout, $session, @@ -52,11 +44,11 @@ export async function respond_with_error({ const error_loaded = /** @type {Loaded} */ ( await load_node({ - request, + event, options, state, route: null, - url: request.url, + url: event.url, params, node: default_error, $session, @@ -79,19 +71,17 @@ export async function respond_with_error({ status, error, branch: [layout_loaded, error_loaded], - url: request.url, + url: event.url, params, ssr }); } catch (err) { const error = coalesce_to_error(err); - options.handle_error(error, request); + options.handle_error(error, event); - return { - status: 500, - headers: {}, - body: error.stack - }; + return new Response(error.stack, { + status: 500 + }); } } diff --git a/packages/kit/src/runtime/server/parse_body/index.js b/packages/kit/src/runtime/server/parse_body/index.js deleted file mode 100644 index 0d8f31582242..000000000000 --- a/packages/kit/src/runtime/server/parse_body/index.js +++ /dev/null @@ -1,109 +0,0 @@ -import { read_only_form_data } from './read_only_form_data.js'; - -/** - * @param {import('types/app').RawBody} raw - * @param {import('types/helper').RequestHeaders} headers - */ -export function parse_body(raw, headers) { - if (!raw) return raw; - - const content_type = headers['content-type']; - const [type, ...directives] = content_type ? content_type.split(/;\s*/) : []; - - const text = () => new TextDecoder(headers['content-encoding'] || 'utf-8').decode(raw); - - switch (type) { - case 'text/plain': - return text(); - - case 'application/json': - return JSON.parse(text()); - - case 'application/x-www-form-urlencoded': - return get_urlencoded(text()); - - case 'multipart/form-data': { - const boundary = directives.find((directive) => directive.startsWith('boundary=')); - if (!boundary) throw new Error('Missing boundary'); - return get_multipart(text(), boundary.slice('boundary='.length)); - } - default: - return raw; - } -} - -/** @param {string} text */ -function get_urlencoded(text) { - const { data, append } = read_only_form_data(); - - text - .replace(/\+/g, ' ') - .split('&') - .forEach((str) => { - const [key, value] = str.split('='); - append(decodeURIComponent(key), decodeURIComponent(value)); - }); - - return data; -} - -/** - * @param {string} text - * @param {string} boundary - */ -function get_multipart(text, boundary) { - const parts = text.split(`--${boundary}`); - - if (parts[0] !== '' || parts[parts.length - 1].trim() !== '--') { - throw new Error('Malformed form data'); - } - - const { data, append } = read_only_form_data(); - - parts.slice(1, -1).forEach((part) => { - const match = /\s*([\s\S]+?)\r\n\r\n([\s\S]*)\s*/.exec(part); - if (!match) { - throw new Error('Malformed form data'); - } - const raw_headers = match[1]; - const body = match[2].trim(); - - let key; - - /** @type {Record} */ - const headers = {}; - raw_headers.split('\r\n').forEach((str) => { - const [raw_header, ...raw_directives] = str.split('; '); - let [name, value] = raw_header.split(': '); - - name = name.toLowerCase(); - headers[name] = value; - - /** @type {Record} */ - const directives = {}; - raw_directives.forEach((raw_directive) => { - const [name, value] = raw_directive.split('='); - directives[name] = JSON.parse(value); // TODO is this right? - }); - - if (name === 'content-disposition') { - if (value !== 'form-data') throw new Error('Malformed form data'); - - if (directives.filename) { - // TODO we probably don't want to do this automatically - throw new Error('File upload is not yet implemented'); - } - - if (directives.name) { - key = directives.name; - } - } - }); - - if (!key) throw new Error('Malformed form data'); - - append(key, body); - }); - - return data; -} diff --git a/packages/kit/src/runtime/server/parse_body/read_only_form_data.js b/packages/kit/src/runtime/server/parse_body/read_only_form_data.js deleted file mode 100644 index 43d47f6dc966..000000000000 --- a/packages/kit/src/runtime/server/parse_body/read_only_form_data.js +++ /dev/null @@ -1,78 +0,0 @@ -export function read_only_form_data() { - /** @type {Map} */ - const map = new Map(); - - return { - /** - * @param {string} key - * @param {string} value - */ - append(key, value) { - const existing_values = map.get(key); - if (existing_values) { - existing_values.push(value); - } else { - map.set(key, [value]); - } - }, - - data: new ReadOnlyFormData(map) - }; -} - -class ReadOnlyFormData { - /** @type {Map} */ - #map; - - /** @param {Map} map */ - constructor(map) { - this.#map = map; - } - - /** @param {string} key */ - get(key) { - const value = this.#map.get(key); - if (!value) { - return null; - } - return value[0]; - } - - /** @param {string} key */ - getAll(key) { - return this.#map.get(key) || []; - } - - /** @param {string} key */ - has(key) { - return this.#map.has(key); - } - - *[Symbol.iterator]() { - for (const [key, value] of this.#map) { - for (let i = 0; i < value.length; i += 1) { - yield [key, value[i]]; - } - } - } - - *entries() { - for (const [key, value] of this.#map) { - for (let i = 0; i < value.length; i += 1) { - yield [key, value[i]]; - } - } - } - - *keys() { - for (const [key] of this.#map) yield key; - } - - *values() { - for (const [, value] of this.#map) { - for (let i = 0; i < value.length; i += 1) { - yield value[i]; - } - } - } -} diff --git a/packages/kit/src/runtime/server/parse_body/read_only_form_data.spec.js b/packages/kit/src/runtime/server/parse_body/read_only_form_data.spec.js deleted file mode 100644 index 62d88d9f39d4..000000000000 --- a/packages/kit/src/runtime/server/parse_body/read_only_form_data.spec.js +++ /dev/null @@ -1,65 +0,0 @@ -import { test } from 'uvu'; -import * as assert from 'uvu/assert'; -import { read_only_form_data } from './read_only_form_data.js'; - -test('ro-fd get returns null and getAll an empty array for no values at given key', () => { - const { data } = read_only_form_data(); - assert.is(data.get('foo'), null); - assert.equal(data.getAll('foo'), []); -}); - -const { data, append } = read_only_form_data(); -append('foo', '1'), append('foo', '2'), append('foo', '3'); -append('bar', '2'), append('bar', '1'); - -test('ro-fd get returns first value', () => { - assert.equal(data.get('foo'), '1'); - assert.equal(data.get('bar'), '2'); -}); - -test('ro-fd getAll returns array', () => { - assert.equal(data.getAll('foo'), ['1', '2', '3']); - assert.equal(data.getAll('bar'), ['2', '1']); -}); - -test('ro-fd has returns boolean flag', () => { - assert.equal(data.has('foo'), true); - assert.equal(data.has('bar'), true); - assert.equal(data.has('baz'), false); -}); - -test('ro-fd iterator yields all key-value pairs', () => { - const values = []; - for (const [key, val] of data) values.push({ key, val }); - - assert.equal(values.length, 5); - assert.equal(values[0], { key: 'foo', val: '1' }); - assert.equal(values[3], { key: 'bar', val: '2' }); -}); - -test('ro-fd entries() yields all key-value pairs', () => { - const values = []; - for (const [key, val] of data.entries()) values.push({ key, val }); - - assert.equal(values.length, 5); - assert.equal(values[0], { key: 'foo', val: '1' }); - assert.equal(values[3], { key: 'bar', val: '2' }); -}); - -test('ro-fd keys() yields all unique keys', () => { - const values = []; - for (const key of data.keys()) values.push(key); - - assert.equal(values.length, 2); - assert.equal(values, ['foo', 'bar']); -}); - -test('ro-fd values() yields all nested values', () => { - const values = []; - for (const val of data.values()) values.push(val); - - assert.equal(values.length, 5); - assert.equal(values, ['1', '2', '3', '2', '1']); -}); - -test.run(); diff --git a/packages/kit/src/utils/http.js b/packages/kit/src/utils/http.js index b2c0c44bbf10..497d20951364 100644 --- a/packages/kit/src/utils/http.js +++ b/packages/kit/src/utils/http.js @@ -1,21 +1,21 @@ -/** - * @param {Record} headers - * @param {string} key - * @returns {string | undefined} - * @throws {Error} - */ -export function get_single_valued_header(headers, key) { - const value = headers[key]; - if (Array.isArray(value)) { - if (value.length === 0) { - return undefined; +/** @param {Partial | undefined} object */ +export function to_headers(object) { + const headers = new Headers(); + + if (object) { + for (const key in object) { + const value = object[key]; + if (!value) continue; + + if (typeof value === 'string') { + headers.set(key, value); + } else { + value.forEach((value) => { + headers.append(key, value); + }); + } } - if (value.length > 1) { - throw new Error( - `Multiple headers provided for ${key}. Multiple may be provided only for set-cookie` - ); - } - return value[0]; } - return value; + + return headers; } diff --git a/packages/kit/src/utils/http.spec.js b/packages/kit/src/utils/http.spec.js deleted file mode 100644 index 028ac2a4e832..000000000000 --- a/packages/kit/src/utils/http.spec.js +++ /dev/null @@ -1,14 +0,0 @@ -import { test } from 'uvu'; -import * as assert from 'uvu/assert'; -import { get_single_valued_header } from './http.js'; - -test('get_single_valued_header', () => { - assert.equal('123', get_single_valued_header({ key: '123' }, 'key')); - assert.equal('world', get_single_valued_header({ hello: 'world' }, 'hello')); - assert.equal(undefined, get_single_valued_header({}, 'key')); - assert.equal('a', get_single_valued_header({ name: ['a'] }, 'name')); - assert.equal(undefined, get_single_valued_header({ name: ['a'] }, 'undefinedName')); - assert.throws(() => get_single_valued_header({ name: ['a', 'b', 'c'] }, 'name')); -}); - -test.run(); diff --git a/packages/kit/test/apps/basics/src/hooks.js b/packages/kit/test/apps/basics/src/hooks.js index d82a0f8379bc..373522078de2 100644 --- a/packages/kit/test/apps/basics/src/hooks.js +++ b/packages/kit/test/apps/basics/src/hooks.js @@ -8,7 +8,7 @@ export function getSession(request) { } /** @type {import('@sveltejs/kit').HandleError} */ -export const handleError = ({ request, error }) => { +export const handleError = ({ event, error }) => { // TODO we do this because there's no other way (that i'm aware of) // to communicate errors back to the test suite. even if we could // capture stderr, attributing an error to a specific request @@ -16,34 +16,29 @@ export const handleError = ({ request, error }) => { const errors = fs.existsSync('test/errors.json') ? JSON.parse(fs.readFileSync('test/errors.json', 'utf8')) : {}; - errors[request.url.pathname] = error.stack || error.message; + errors[event.url.pathname] = error.stack || error.message; fs.writeFileSync('test/errors.json', JSON.stringify(errors)); }; export const handle = sequence( - ({ request, resolve }) => { - request.locals.answer = 42; - return resolve(request); + ({ event, resolve }) => { + event.locals.answer = 42; + return resolve(event); }, - ({ request, resolve }) => { - const cookies = cookie.parse(request.headers.cookie || ''); - request.locals.name = cookies.name; - return resolve(request); + ({ event, resolve }) => { + const cookies = cookie.parse(event.request.headers.get('cookie') || ''); + event.locals.name = cookies.name; + return resolve(event); }, - async ({ request, resolve }) => { - if (request.url.pathname === '/errors/error-in-handle') { + async ({ event, resolve }) => { + if (event.url.pathname === '/errors/error-in-handle') { throw new Error('Error in handle'); } - const response = await resolve(request, { ssr: !request.url.pathname.startsWith('/no-ssr') }); + const response = await resolve(event, { ssr: !event.url.pathname.startsWith('/no-ssr') }); + response.headers.set('set-cookie', 'name=SvelteKit; path=/; HttpOnly'); - return { - ...response, - headers: { - ...response.headers, - 'set-cookie': 'name=SvelteKit; path=/; HttpOnly' - } - }; + return response; } ); diff --git a/packages/kit/test/apps/basics/src/routes/endpoint-output/fetched.js b/packages/kit/test/apps/basics/src/routes/endpoint-output/fetched.js new file mode 100644 index 000000000000..a845c86ad2f6 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/endpoint-output/fetched.js @@ -0,0 +1,8 @@ +export function get() { + return { + headers: { + 'x-foo': 'bar' + }, + body: 'ok' + }; +} diff --git a/packages/kit/test/apps/basics/src/routes/endpoint-output/headers-object.js b/packages/kit/test/apps/basics/src/routes/endpoint-output/headers-object.js new file mode 100644 index 000000000000..e6be7f259d62 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/endpoint-output/headers-object.js @@ -0,0 +1,8 @@ +/** @type {import('@sveltejs/kit').RequestHandler} */ +export function get() { + return { + headers: new Headers({ + 'X-Foo': 'bar' + }) + }; +} diff --git a/packages/kit/test/apps/basics/src/routes/endpoint-output/proxy.js b/packages/kit/test/apps/basics/src/routes/endpoint-output/proxy.js new file mode 100644 index 000000000000..2a6715734643 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/endpoint-output/proxy.js @@ -0,0 +1,2 @@ +/** @type {import('@sveltejs/kit').RequestHandler} */ +export const get = ({ url }) => fetch(`http://localhost:${url.searchParams.get('port')}`); diff --git a/packages/kit/test/apps/basics/src/routes/etag/custom.js b/packages/kit/test/apps/basics/src/routes/etag/custom.js index 71136e3a2f13..8815e4104b13 100644 --- a/packages/kit/test/apps/basics/src/routes/etag/custom.js +++ b/packages/kit/test/apps/basics/src/routes/etag/custom.js @@ -1,6 +1,6 @@ /** @type {import('@sveltejs/kit').RequestHandler} */ -export function get({ headers }) { - if (headers['if-none-match'] === '@1234@') return { status: 304 }; +export function get({ request }) { + if (request.headers.get('if-none-match') === '@1234@') return { status: 304 }; return { body: `${Math.random()}`, headers: { diff --git a/packages/kit/test/apps/basics/src/routes/headers/echo.js b/packages/kit/test/apps/basics/src/routes/headers/echo.js index 629b0579ef05..e9d31c7c9ca0 100644 --- a/packages/kit/test/apps/basics/src/routes/headers/echo.js +++ b/packages/kit/test/apps/basics/src/routes/headers/echo.js @@ -1,6 +1,12 @@ /** @type {import('@sveltejs/kit').RequestHandler} */ -export function get({ headers }) { - delete headers.cookie; +export function get({ request }) { + /** @type {Record} */ + const headers = {}; + request.headers.forEach((value, key) => { + if (key !== 'cookie') { + headers[key] = value; + } + }); return { body: headers diff --git a/packages/kit/test/apps/basics/src/routes/load/raw-body.json.js b/packages/kit/test/apps/basics/src/routes/load/raw-body.json.js index abd0fb74908e..b3e65bc0b035 100644 --- a/packages/kit/test/apps/basics/src/routes/load/raw-body.json.js +++ b/packages/kit/test/apps/basics/src/routes/load/raw-body.json.js @@ -1,9 +1,12 @@ /** @type {import('@sveltejs/kit').RequestHandler} */ -export function post(request) { +export async function post({ request }) { + const text = await request.text(); + const json = JSON.parse(text); + return { body: { - body: /** @type {string} */ (request.body), - rawBody: new TextDecoder().decode(/** @type {Uint8Array} */ (request.rawBody)) + body: json, + rawBody: text } }; } diff --git a/packages/kit/test/apps/basics/src/routes/load/serialization-post.json.js b/packages/kit/test/apps/basics/src/routes/load/serialization-post.json.js index 5ea0ae3ff398..c613becb0d1a 100644 --- a/packages/kit/test/apps/basics/src/routes/load/serialization-post.json.js +++ b/packages/kit/test/apps/basics/src/routes/load/serialization-post.json.js @@ -1,6 +1,8 @@ -/** @type {import('@sveltejs/kit').RequestHandler} */ -export function post(request) { +/** @type {import('@sveltejs/kit').RequestHandler} */ +export async function post({ request }) { + const body = await request.text(); + return { - body: request.body.toUpperCase() + body: body.toUpperCase() }; } diff --git a/packages/kit/test/apps/basics/src/routes/method-override/fetch.json.js b/packages/kit/test/apps/basics/src/routes/method-override/fetch.json.js index f33471c06b09..39823db30d38 100644 --- a/packages/kit/test/apps/basics/src/routes/method-override/fetch.json.js +++ b/packages/kit/test/apps/basics/src/routes/method-override/fetch.json.js @@ -6,21 +6,21 @@ const buildResponse = (/** @type {string} */ method) => ({ }); /** @type {import('@sveltejs/kit').RequestHandler} */ -export const get = (request) => { +export const get = ({ request }) => { return buildResponse(request.method); }; /** @type {import('@sveltejs/kit').RequestHandler} */ -export const post = (request) => { +export const post = ({ request }) => { return buildResponse(request.method); }; /** @type {import('@sveltejs/kit').RequestHandler} */ -export const patch = (request) => { +export const patch = ({ request }) => { return buildResponse(request.method); }; /** @type {import('@sveltejs/kit').RequestHandler} */ -export const del = (request) => { +export const del = ({ request }) => { return buildResponse(request.method); }; diff --git a/packages/kit/test/apps/basics/src/routes/routing/shadow/action.js b/packages/kit/test/apps/basics/src/routes/routing/shadow/action.js index bbd535eee3b4..7aea26efcc70 100644 --- a/packages/kit/test/apps/basics/src/routes/routing/shadow/action.js +++ b/packages/kit/test/apps/basics/src/routes/routing/shadow/action.js @@ -1,7 +1,8 @@ let random = 0; -/** @type {import('@sveltejs/kit').RequestHandler} */ -export function post({ body }) { +/** @type {import('@sveltejs/kit').RequestHandler} */ +export async function post({ request }) { + const body = await request.formData(); random = +(body.get('random') || '0'); return { fallthrough: true }; } diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 469ebe3d9eeb..b03ffe539864 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -419,6 +419,31 @@ test.describe.parallel('Endpoints', () => { expect(await page.textContent('h1')).toBe(random); }); + + test('allows headers to be a Headers object', async ({ request }) => { + const response = await request.get('/endpoint-output/headers-object'); + + expect(response.headers()['x-foo']).toBe('bar'); + }); + + test('allows return value to be a Response', async ({ request }) => { + const { server, port } = await start_server((req, res) => { + res.writeHead(200, { + 'X-Foo': 'bar' + }); + + res.end('ok'); + }); + + try { + const response = await request.get(`/endpoint-output/proxy?port=${port}`); + + expect(await response.text()).toBe('ok'); + expect(response.headers()['x-foo']).toBe('bar'); + } finally { + server.close(); + } + }); }); test.describe.parallel('Encoded paths', () => { diff --git a/packages/kit/test/apps/options/source/pages/origin/index.json.js b/packages/kit/test/apps/options/source/pages/origin/index.json.js deleted file mode 100644 index 7930839485c8..000000000000 --- a/packages/kit/test/apps/options/source/pages/origin/index.json.js +++ /dev/null @@ -1,6 +0,0 @@ -/** @type {import('@sveltejs/kit').RequestHandler} */ -export function get({ url }) { - return { - body: { origin: url.origin } - }; -} diff --git a/packages/kit/test/apps/options/source/pages/origin/index.svelte b/packages/kit/test/apps/options/source/pages/origin/index.svelte deleted file mode 100644 index ecdf102ef558..000000000000 --- a/packages/kit/test/apps/options/source/pages/origin/index.svelte +++ /dev/null @@ -1,30 +0,0 @@ - - - - -

{origin}

-

{$page.url.origin}

-

{data.origin}

diff --git a/packages/kit/test/apps/options/svelte.config.js b/packages/kit/test/apps/options/svelte.config.js index 5891eff90787..f916490ddc01 100644 --- a/packages/kit/test/apps/options/svelte.config.js +++ b/packages/kit/test/apps/options/svelte.config.js @@ -15,7 +15,6 @@ const config = { appDir: '_wheee', floc: true, target: '#content-goes-here', - host: 'example.com', inlineStyleThreshold: 1024, trailingSlash: 'always', vite: { @@ -32,8 +31,7 @@ const config = { paths: { base: '/path-base', assets: 'https://cdn.example.com/stuff' - }, - protocol: 'https' + } } }; diff --git a/packages/kit/test/apps/options/test/test.js b/packages/kit/test/apps/options/test/test.js index 6d5ebafbc3cd..d2d7b818fa3f 100644 --- a/packages/kit/test/apps/options/test/test.js +++ b/packages/kit/test/apps/options/test/test.js @@ -110,18 +110,6 @@ test.describe.parallel('Headers', () => { }); }); -test.describe.parallel('Origin', () => { - test('sets origin', async ({ baseURL, page }) => { - await page.goto('/path-base/origin/'); - - const origin = process.env.DEV ? baseURL : 'https://example.com'; - - expect(await page.textContent('[data-source="load"]')).toBe(origin); - expect(await page.textContent('[data-source="store"]')).toBe(origin); - expect(await page.textContent('[data-source="endpoint"]')).toBe(origin); - }); -}); - test.describe.parallel('trailingSlash', () => { test('adds trailing slash', async ({ baseURL, page, clicknav }) => { await page.goto('/path-base/slash'); diff --git a/packages/kit/types/ambient-modules.d.ts b/packages/kit/types/ambient-modules.d.ts index c4c9671554af..a5e4aeb39ff9 100644 --- a/packages/kit/types/ambient-modules.d.ts +++ b/packages/kit/types/ambient-modules.d.ts @@ -174,27 +174,22 @@ declare module '@sveltejs/kit/hooks' { } declare module '@sveltejs/kit/node' { - import { IncomingMessage } from 'http'; - import { RawBody } from '@sveltejs/kit'; + import { IncomingMessage, ServerResponse } from 'http'; export interface GetRawBody { - (request: IncomingMessage): Promise; + (request: IncomingMessage): Promise; } export const getRawBody: GetRawBody; -} -declare module '@sveltejs/kit/ssr' { - import { IncomingRequest, Response } from '@sveltejs/kit'; - // TODO import from public types, right now its heavily coupled with internal - type Options = import('@sveltejs/kit/types/internal').SSRRenderOptions; - type State = import('@sveltejs/kit/types/internal').SSRRenderState; + export interface GetRequest { + (base: string, request: IncomingMessage): Promise; + } + export const getRequest: GetRequest; - export interface Respond { - (incoming: IncomingRequest & { url: URL }, options: Options, state?: State): Promise< - Response | undefined - >; + export interface SetResponse { + (res: ServerResponse, response: Response): void; } - export const respond: Respond; + export const setResponse: SetResponse; } declare module '@sveltejs/kit/install-fetch' { diff --git a/packages/kit/types/app.d.ts b/packages/kit/types/app.d.ts index c62a12e8f9c2..818b89eee68d 100644 --- a/packages/kit/types/app.d.ts +++ b/packages/kit/types/app.d.ts @@ -1,31 +1,17 @@ -import { ReadOnlyFormData, RequestHeaders } from './helper'; -import { ServerResponse } from './hooks'; import { PrerenderOptions, SSRNodeLoader, SSRRoute } from './internal'; export class App { constructor(manifest: SSRManifest); - render(incoming: IncomingRequest): Promise; + render(request: Request): Promise; } export class InternalApp extends App { render( - incoming: IncomingRequest, + request: Request, options?: { prerender: PrerenderOptions; } - ): Promise; -} - -export type RawBody = null | Uint8Array; -export type ParameterizedBody = Body extends FormData - ? ReadOnlyFormData - : (string | RawBody | ReadOnlyFormData) & Body; - -export interface IncomingRequest { - url: string | URL; - method: string; - headers: RequestHeaders; - rawBody: RawBody; + ): Promise; } export interface SSRManifest { diff --git a/packages/kit/types/config.d.ts b/packages/kit/types/config.d.ts index 7bc00b3bdaf1..fefeb68343ae 100644 --- a/packages/kit/types/config.d.ts +++ b/packages/kit/types/config.d.ts @@ -125,11 +125,6 @@ export interface Config { template?: string; }; floc?: boolean; - headers?: { - host?: string; - protocol?: string; - }; - host?: string; hydrate?: boolean; inlineStyleThreshold?: number; methodOverride?: { @@ -153,7 +148,6 @@ export interface Config { entries?: string[]; onError?: PrerenderOnErrorValue; }; - protocol?: string; router?: boolean; serviceWorker?: { register?: boolean; diff --git a/packages/kit/types/endpoint.d.ts b/packages/kit/types/endpoint.d.ts index 05e6f50530d4..8f2b979202d6 100644 --- a/packages/kit/types/endpoint.d.ts +++ b/packages/kit/types/endpoint.d.ts @@ -1,20 +1,19 @@ -import { ServerRequest } from './hooks'; +import { RequestEvent } from './hooks'; import { JSONString, MaybePromise, ResponseHeaders, Either, Fallthrough } from './helper'; type DefaultBody = JSONString | Uint8Array; export interface EndpointOutput { status?: number; - headers?: Partial; + headers?: Headers | Partial; body?: Body; } export interface RequestHandler< Locals = Record, - Input = unknown, Output extends DefaultBody = DefaultBody > { - (request: ServerRequest): MaybePromise< - Either, Fallthrough> + (request: RequestEvent): MaybePromise< + Either, Fallthrough> >; } diff --git a/packages/kit/types/helper.d.ts b/packages/kit/types/helper.d.ts index 1b58d4d51018..02ccfe84d8c8 100644 --- a/packages/kit/types/helper.d.ts +++ b/packages/kit/types/helper.d.ts @@ -21,7 +21,6 @@ export type JSONString = /** `string[]` is only for set-cookie, everything else must be type of `string` */ export type ResponseHeaders = Record; -export type RequestHeaders = Record; // Utility Types export type InferValue = T extends Record diff --git a/packages/kit/types/hooks.d.ts b/packages/kit/types/hooks.d.ts index 3dc01a6afca5..cb3e7d4e0558 100644 --- a/packages/kit/types/hooks.d.ts +++ b/packages/kit/types/hooks.d.ts @@ -1,53 +1,40 @@ -import { ParameterizedBody, RawBody } from './app'; -import { MaybePromise, RequestHeaders, ResponseHeaders } from './helper'; +import { MaybePromise } from './helper'; export type StrictBody = string | Uint8Array; -export interface ServerRequest, Body = unknown> { +export interface RequestEvent> { + request: Request; url: URL; - method: string; - headers: RequestHeaders; - rawBody: RawBody; params: Record; - body: ParameterizedBody; locals: Locals; } -export interface ServerResponse { - status: number; - headers: Partial; - body?: StrictBody; -} - -export interface GetSession, Body = unknown, Session = any> { - (request: ServerRequest): MaybePromise; +export interface GetSession, Session = any> { + (event: RequestEvent): MaybePromise; } export interface ResolveOpts { ssr?: boolean; } -export interface Handle, Body = unknown> { +export interface Handle> { (input: { - request: ServerRequest; - resolve(request: ServerRequest, opts?: ResolveOpts): MaybePromise; - }): MaybePromise; + event: RequestEvent; + resolve(event: RequestEvent, opts?: ResolveOpts): MaybePromise; + }): MaybePromise; } // internally, `resolve` could return `undefined`, so we differentiate InternalHandle // from the public Handle type -export interface InternalHandle, Body = unknown> { +export interface InternalHandle> { (input: { - request: ServerRequest; - resolve( - request: ServerRequest, - opts?: ResolveOpts - ): MaybePromise; - }): MaybePromise; + event: RequestEvent; + resolve(event: RequestEvent, opts?: ResolveOpts): MaybePromise; + }): MaybePromise; } -export interface HandleError, Body = unknown> { - (input: { error: Error & { frame?: string }; request: ServerRequest }): void; +export interface HandleError> { + (input: { error: Error & { frame?: string }; event: RequestEvent }): void; } export interface ExternalFetch { diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index ac820b00b414..efd53489c127 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -3,16 +3,8 @@ import './ambient-modules'; -export { App, IncomingRequest, RawBody, SSRManifest } from './app'; +export { App, SSRManifest } from './app'; export { Adapter, Builder, Config, PrerenderErrorHandler, ValidatedConfig } from './config'; export { EndpointOutput, RequestHandler } from './endpoint'; export { ErrorLoad, ErrorLoadInput, Load, LoadInput, LoadOutput } from './page'; -export { - ExternalFetch, - GetSession, - Handle, - HandleError, - ServerRequest as Request, - ServerResponse as Response, - ResolveOpts -} from './hooks'; +export { ExternalFetch, GetSession, Handle, HandleError, RequestEvent, ResolveOpts } from './hooks'; diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index 2d34d85ad43d..b82ff4b26720 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -1,14 +1,7 @@ import { OutputAsset, OutputChunk } from 'rollup'; import { RequestHandler } from './endpoint'; import { InternalApp, SSRManifest } from './app'; -import { - ExternalFetch, - GetSession, - HandleError, - InternalHandle, - ServerRequest, - ServerResponse -} from './hooks'; +import { ExternalFetch, GetSession, HandleError, InternalHandle, RequestEvent } from './hooks'; import { Load } from './page'; import { Either, Fallthrough } from './helper'; @@ -17,7 +10,7 @@ type PageId = string; export interface PrerenderOptions { fallback?: string; all: boolean; - dependencies: Map; + dependencies: Map; } export interface AppModule { @@ -127,7 +120,7 @@ export interface SSRRenderOptions { dev: boolean; floc: boolean; get_stack: (error: Error) => string | undefined; - handle_error(error: Error & { frame?: string }, request: ServerRequest): void; + handle_error(error: Error & { frame?: string }, event: RequestEvent): void; hooks: Hooks; hydrate: boolean; manifest: SSRManifest; @@ -235,3 +228,9 @@ export interface MethodOverride { parameter: string; allowed: string[]; } + +export interface Respond { + (request: Request, options: SSRRenderOptions, state?: SSRRenderState): Promise< + Response | undefined + >; +}