Skip to content

Commit

Permalink
Breaking: use Request and Response objects in endpoints and hooks (
Browse files Browse the repository at this point in the history
…#3384)

* this should be an internal type

* change app.render return type to Response

* update adapters

* lint

* fix tests

* app.render takes a Request as input

* only read body once

* update adapters, remove host/protocol options

* lint

* remove obsolete origin test

* change endpoint signature

* fix vercel adapter

* add setResponse helper

* allow returning Response or Headers from endpoints

* fixes

* lint

* update docs

* update adapter-node docs

* docs

* whoops

* changesets

* pointless commit to try and trick netlify into working

* update template

* changeset

* work around zip-it-and-ship-it bug

* Update .changeset/large-icons-complain.md

* Update .changeset/mighty-pandas-search.md

* Update .changeset/strong-schools-rule.md

* Update documentation/docs/04-hooks.md

Co-authored-by: Ben McCann <[email protected]>

* Update packages/adapter-node/README.md

Co-authored-by: Ben McCann <[email protected]>

* reduce indentation

* add more types to adapters, to reflect these changes

* Update documentation/docs/10-adapters.md

Co-authored-by: Ignatius Bagus <[email protected]>

* better error messages

* helpful errors for removed config options

* fix tests

Co-authored-by: Ben McCann <[email protected]>
Co-authored-by: Ignatius Bagus <[email protected]>
  • Loading branch information
3 people authored Jan 19, 2022
1 parent 7fa43a4 commit ee906a3
Show file tree
Hide file tree
Showing 79 changed files with 809 additions and 1,079 deletions.
5 changes: 5 additions & 0 deletions .changeset/bright-icons-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Allow endpoints to return a Response, or an object with Headers
5 changes: 5 additions & 0 deletions .changeset/large-icons-complain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Breaking: Expose standard Request object to endpoints and hooks
10 changes: 10 additions & 0 deletions .changeset/mighty-pandas-search.md
Original file line number Diff line number Diff line change
@@ -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<Response>
6 changes: 6 additions & 0 deletions .changeset/strong-schools-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@sveltejs/adapter-node': patch
'@sveltejs/kit': patch
---

Breaking: Remove protocol/host configuration options from Kit to adapter-node
40 changes: 17 additions & 23 deletions documentation/docs/01-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string | string[]>;
type RequestHeaders = Record<string, string>;

export type RawBody = null | Uint8Array;

type ParameterizedBody<Body = unknown> = Body extends FormData
? ReadOnlyFormData
: (string | RawBody | ReadOnlyFormData) & Body;

export interface Request<Locals = Record<string, any>, Body = unknown> {
export interface RequestEvent<Locals = Record<string, any>> {
request: Request;
url: URL;
method: string;
headers: RequestHeaders;
rawBody: RawBody;
params: Record<string, string>;
body: ParameterizedBody<Body>;
locals: Locals;
}

type DefaultBody = JSONResponse | Uint8Array;
export interface EndpointOutput<Body extends DefaultBody = DefaultBody> {
type Body = JSONResponse | Uint8Array | string | ReadableStream | stream.Readable;

export interface EndpointOutput {
status?: number;
headers?: ResponseHeaders;
headers?: HeadersInit;
body?: Body;
}

Expand Down Expand Up @@ -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 {
Expand All @@ -114,6 +103,10 @@ export async function get({ params }) {
}
};
}

return {
status: 404
};
}
```

Expand Down Expand Up @@ -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

Expand Down
67 changes: 22 additions & 45 deletions documentation/docs/04-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<string, string | string[]>;
type RequestHeaders = Record<string, string>;

export type RawBody = null | Uint8Array;

type ParameterizedBody<Body = unknown> = Body extends FormData
? ReadOnlyFormData
: (string | RawBody | ReadOnlyFormData) & Body;

export interface Request<Locals = Record<string, any>, Body = unknown> {
export interface RequestEvent<Locals = Record<string, any>> {
request: Request;
url: URL;
method: string;
headers: RequestHeaders;
rawBody: RawBody;
params: Record<string, string>;
body: ParameterizedBody<Body>;
locals: Locals;
}

type StrictBody = string | Uint8Array;

export interface Response {
status: number;
headers: ResponseHeaders;
body?: StrictBody;
}

export interface ResolveOpts {
ssr?: boolean;
}

export interface Handle<Locals = Record<string, any>, Body = unknown> {
export interface Handle<Locals = Record<string, any>> {
(input: {
request: ServerRequest<Locals, Body>;
resolve(request: ServerRequest<Locals, Body>, opts?: ResolveOpts): MaybePromise<ServerResponse>;
}): MaybePromise<ServerResponse>;
event: RequestEvent<Locals>;
resolve(event: RequestEvent<Locals>, opts?: ResolveOpts): MaybePromise<Response>;
}): MaybePromise<Response>;
}
```

Expand All @@ -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;
}
```

Expand Down Expand Up @@ -107,46 +84,46 @@ If unimplemented, SvelteKit will log the error with default formatting.

```ts
// Declaration types for handleError hook
export interface HandleError<Locals = Record<string, any>, Body = unknown> {
(input: { error: Error & { frame?: string }; request: Request<Locals, Body> }): void;
export interface HandleError<Locals = Record<string, any>> {
(input: { error: Error & { frame?: string }; event: RequestEvent<Locals> }): 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 });
}
```

> `handleError` is only called in the case of an uncaught exception. It is not called when pages and endpoints explicitly respond with 4xx and 5xx status codes.
### 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<Locals = Record<string, any>, Body = unknown, Session = any> {
(request: Request<Locals, Body>): Session | Promise<Session>;
export interface GetSession<Locals = Record<string, any>, Session = any> {
(event: RequestEvent<Locals>): Session | Promise<Session>;
}
```

```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
}
}
: {};
Expand Down
8 changes: 4 additions & 4 deletions documentation/docs/05-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion documentation/docs/10-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 0 additions & 34 deletions documentation/docs/14-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ const config = {
template: 'src/app.html'
},
floc: false,
headers: {
host: null,
protocol: null
},
host: null,
hydrate: true,
inlineStyleThreshold: 0,
methodOverride: {
Expand All @@ -55,7 +50,6 @@ const config = {
entries: ['*'],
onError: 'fail'
},
protocol: null,
router: true,
serviceWorker: {
register: true,
Expand Down Expand Up @@ -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.)
Expand Down Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions packages/adapter-cloudflare-workers/files/entry.d.ts
Original file line number Diff line number Diff line change
@@ -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<string>;
}
42 changes: 2 additions & 40 deletions packages/adapter-cloudflare-workers/files/entry.js
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -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<string, string | string[]>} 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;
}
Loading

0 comments on commit ee906a3

Please sign in to comment.