Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Breaking: use Request and Response objects in endpoints and hooks #3384

Merged
merged 41 commits into from
Jan 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
37ad3e2
this should be an internal type
Rich-Harris Jan 17, 2022
df834ca
change app.render return type to Response
Rich-Harris Jan 17, 2022
2f4ca67
update adapters
Rich-Harris Jan 17, 2022
1cc7e35
Merge branch 'master' into streams
Rich-Harris Jan 17, 2022
cc4be79
merge master -> streams
Rich-Harris Jan 17, 2022
b7eb5b9
lint
Rich-Harris Jan 17, 2022
46b36dd
fix tests
Rich-Harris Jan 17, 2022
b3b00d8
app.render takes a Request as input
Rich-Harris Jan 17, 2022
d6fcac3
only read body once
Rich-Harris Jan 17, 2022
44c9ae2
update adapters, remove host/protocol options
Rich-Harris Jan 18, 2022
a1bccb3
lint
Rich-Harris Jan 18, 2022
3ce5ac3
remove obsolete origin test
Rich-Harris Jan 18, 2022
2fbb868
change endpoint signature
Rich-Harris Jan 18, 2022
365f65e
fix vercel adapter
Rich-Harris Jan 18, 2022
0f1e6b1
add setResponse helper
Rich-Harris Jan 18, 2022
b006980
allow returning Response or Headers from endpoints
Rich-Harris Jan 18, 2022
d4d7ac9
fixes
Rich-Harris Jan 18, 2022
fb4d608
lint
Rich-Harris Jan 18, 2022
aed1edc
update docs
Rich-Harris Jan 18, 2022
90c3090
update adapter-node docs
Rich-Harris Jan 18, 2022
c2e9399
docs
Rich-Harris Jan 18, 2022
50d2b5e
merge master -> streams
Rich-Harris Jan 18, 2022
f05e724
whoops
Rich-Harris Jan 18, 2022
62c6a77
changesets
Rich-Harris Jan 18, 2022
d216ebb
pointless commit to try and trick netlify into working
Rich-Harris Jan 18, 2022
75abe9f
update template
Rich-Harris Jan 18, 2022
8bbfc98
changeset
Rich-Harris Jan 18, 2022
865d124
work around zip-it-and-ship-it bug
Rich-Harris Jan 18, 2022
5293c71
Update .changeset/large-icons-complain.md
Rich-Harris Jan 18, 2022
501dd33
Update .changeset/mighty-pandas-search.md
Rich-Harris Jan 18, 2022
9c20e7e
Update .changeset/strong-schools-rule.md
Rich-Harris Jan 18, 2022
f3286d7
Update documentation/docs/04-hooks.md
Rich-Harris Jan 19, 2022
ca2760e
Update packages/adapter-node/README.md
Rich-Harris Jan 19, 2022
2672f50
reduce indentation
Rich-Harris Jan 19, 2022
53fb367
merge master -> streams
Rich-Harris Jan 19, 2022
9b506f1
add more types to adapters, to reflect these changes
Rich-Harris Jan 19, 2022
84f6136
Update documentation/docs/10-adapters.md
Rich-Harris Jan 19, 2022
e05cf2c
better error messages
Rich-Harris Jan 19, 2022
3abcaec
helpful errors for removed config options
Rich-Harris Jan 19, 2022
57b4670
Merge branch 'streams' of github.com:sveltejs/kit into streams
Rich-Harris Jan 19, 2022
7a5a03a
fix tests
Rich-Harris Jan 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems funny to me that we have url and request.url. would it be possible to extend the request by adding params and locals to it to avoid this duplication?

I've always hated that the serverless environments call it an event. I think the idea is that you can have serverless endpoints which receive events that are not HTTP requests, but that's not applicable here. I'd love if we could find a way to avoid renaming request to event in all our docs

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

request.url is a string, unfortunately, and not a URL instance. Rich and I just briefly chatted about this on Discord, and it seemed nicer to expose the parsed URL to users, since we are doing it anyway, and since it's reasonable to assume that they will want this data.

Extending official documented-in-MDN classes with additional fields doesn't sit well with me. I really don't think that's a good idea. I'm not wild about the name event, but am not sure what would be better.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extending Request is a non-starter — it'd be very confusing, mixing and matching stuff that's documented on MDN with our own monkey-patched properties, and in any case we don't own the request object (the adapters do).

request.url is a string, whereas url is a URL object. An endpoint could do this if they needed it...

-const foo = url.searchParams.get('foo');
+const foo = new URL(request.url).searchParams.get('foo');

...but since we're already generating the URL object ourselves, it probably makes sense to just expose it.

I don't love event either, but since we do need to have an object with a request property (since monkey-patching is a no-go), and request.request sounds silly, I couldn't come up with anything better. Open to suggestions though

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