Skip to content

Commit

Permalink
Remove getContext in favour of request.locals (#1332)
Browse files Browse the repository at this point in the history
* replace request.context with request.locals

* fix unrelated bug

* try this

* gah

* mutation is good, actually

* more mutation
  • Loading branch information
Rich Harris authored May 4, 2021
1 parent ae22f73 commit 051c026
Show file tree
Hide file tree
Showing 16 changed files with 85 additions and 133 deletions.
5 changes: 5 additions & 0 deletions .changeset/tidy-wasps-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Remove getContext in favour of request.locals
2 changes: 1 addition & 1 deletion documentation/docs/01-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ type Request<Context = any> = {
query: URLSearchParams;
rawBody: string | ArrayBuffer;
body: string | ArrayBuffer | ReadOnlyFormData | any;
context: Context; // see getContext, below
locals: Record<string, any>; // see below
};

type Response = {
Expand Down
103 changes: 36 additions & 67 deletions documentation/docs/04-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,117 +2,86 @@
title: Hooks
---

An optional `src/hooks.js` (or `src/hooks.ts`, or `src/hooks/index.js`) file exports three functions, all optional, that run on the server — **getContext**, **getSession** and **handle**.
An optional `src/hooks.js` (or `src/hooks.ts`, or `src/hooks/index.js`) file exports two functions, both optional, that run on the server — **handle** and **getSession**.

> The location of this file can be [configured](#configuration) as `config.kit.files.hooks`
### getContext
### handle

This function runs on every request, and determines the response. It receives the `request` object and `render` method, which calls SvelteKit's default renderer. This allows you to modify response headers or bodies, or bypass SvelteKit entirely (for implementing endpoints programmatically, for example).

This function runs on every incoming request. It generates the `context` object that is available to [endpoint handlers](#routing-endpoints) as `request.context` and that is used to derive the [`session`](#hooks-getsession) object available in the browser.
If unimplemented, defaults to `({ request, render }) => render(request)`.

If unimplemented, context is `{}`.
To add custom data to the request, which is passed to endpoints, populate the `request.locals` object, as shown below.

```ts
type Incoming = {
type Request<Locals = Record<string, any>> = {
method: string;
host: string;
headers: Headers;
path: string;
params: Record<string, string>;
query: URLSearchParams;
body: string | Buffer | ReadOnlyFormData;
rawBody: string | ArrayBuffer;
body: string | ArrayBuffer | ReadOnlyFormData | any;
locals: Locals;
};

type GetContext<Context = any> = {
(incoming: Incoming): Context;
type Response = {
status?: number;
headers?: Headers;
body?: any;
};

type Handle<Locals = Record<string, any>> = ({
request: Request<Locals>,
render: (request: Request<Locals>) => Promise<Response>
}) => Response | Promise<Response>;
```

```js
import * as cookie from 'cookie';
import db from '$lib/db';
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ request, render }) {
request.locals.user = await getUserInformation(request.headers.cookie);

/** @type {import('@sveltejs/kit').GetContext} */
export async function getContext({ headers }) {
const cookies = cookie.parse(headers.cookie || '');
const response = await render(request);

return {
user: (await db.get_user(cookies.session_id)) || { guest: true }
...response,
headers: {
...response.headers,
'x-custom-header': 'potato'
}
};
}
```

### getSession

This function takes the [`context`](#hooks-getcontext) 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 `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.

If unimplemented, session is `{}`.

```ts
type GetSession<Context = any, Session = any> = {
({ context }: { context: Context }): Session | Promise<Session>;
type GetSession<Locals = Record<string, any>, Session = any> = {
(request: Request<Locals>): Session | Promise<Session>;
};
```

```js
/** @type {import('@sveltejs/kit').GetSession} */
export function getSession({ context }) {
export function getSession(request) {
return {
user: {
// only include properties needed client-side —
// exclude anything else attached to the user
// like access tokens etc
name: context.user?.name,
email: context.user?.email,
avatar: context.user?.avatar
name: request.locals.user?.name,
email: request.locals.user?.email,
avatar: request.locals.user?.avatar
}
};
}
```
> `session` must be serializable, which means it must not contain things like functions or custom classes, just built-in JavaScript data types
### handle
This function runs on every request, and determines the response. It receives the `request` object and `render` method, which calls SvelteKit's default renderer. This allows you to modify response headers or bodies, or bypass SvelteKit entirely (for implementing endpoints programmatically, for example).
If unimplemented, defaults to `({ request, render }) => render(request)`.
```ts
type Request<Context = any> = {
method: string;
host: string;
headers: Headers;
path: string;
params: Record<string, string>;
query: URLSearchParams;
rawBody: string | ArrayBuffer;
body: string | ArrayBuffer | ReadOnlyFormData | any;
context: Context;
};

type Response = {
status?: number;
headers?: Headers;
body?: any;
};

type Handle<Context = any> = ({
request: Request<Context>,
render: (request: Request<Context>) => Promise<Response>
}) => Response | Promise<Response>;
```
```js
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ request, render }) {
const response = await render(request);

return {
...response,
headers: {
...response.headers,
'x-custom-header': 'potato'
}
};
}
```
2 changes: 1 addition & 1 deletion packages/create-svelte/templates/default/netlify.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[build]
command = "npm run build"
publish = ".netlify/build/"
functions = ".netlify/functions/"
functions = ".netlify/functions/"
31 changes: 9 additions & 22 deletions packages/create-svelte/templates/default/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,22 @@
import cookie from 'cookie';
import { v4 as uuid } from '@lukeed/uuid';
import type { GetContext, Handle } from '@sveltejs/kit';
import type { Handle } from '@sveltejs/kit';

export const getContext: GetContext = (request) => {
export const handle: Handle = async ({ request, render }) => {
const cookies = cookie.parse(request.headers.cookie || '');
request.locals.userid = cookies.userid || uuid();

return {
is_new: !cookies.userid,
userid: cookies.userid || uuid()
};
};

export const handle: Handle = async ({ request, render }) => {
// TODO https://github.com/sveltejs/kit/issues/1046
const response = await render({
...request,
method: (request.query.get('_method') || request.method).toUpperCase()
});
if (request.query.has('_method')) {
request.method = request.query.get('_method').toUpperCase();
}

const { is_new, userid } = request.context;
const response = await render(request);

if (is_new) {
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
return {
...response,
headers: {
...response.headers,
'set-cookie': `userid=${userid}; Path=/; HttpOnly`
}
};
response.headers['set-cookie'] = `userid=${request.locals.userid}; Path=/; HttpOnly`;
}

return response;
Expand Down
2 changes: 1 addition & 1 deletion packages/create-svelte/templates/default/svelte.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const config = {
};

if (adapter) {
config.kit.adapter = require(adapter)(options);
config.kit.adapter = (await import(adapter)).default(options);
}

export default config;
1 change: 0 additions & 1 deletion packages/kit/src/core/build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,6 @@ async function build_server(
// this looks redundant, but the indirection allows us to access
// named imports without triggering Rollup's missing import detection
const get_hooks = hooks => ({
getContext: hooks.getContext || (() => ({})),
getSession: hooks.getSession || (() => ({})),
handle: hooks.handle || (({ request, render }) => render(request))
});
Expand Down
8 changes: 7 additions & 1 deletion packages/kit/src/core/dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,13 @@ class Watcher extends EventEmitter {
? await this.vite.ssrLoadModule(`/${this.config.kit.files.hooks}`)
: {};

if (/** @type {any} */ (hooks).getContext) {
// TODO remove this for 1.0
throw new Error(
'The getContext hook has been removed. See https://kit.svelte.dev/docs#hooks'
);
}

const root = (await this.vite.ssrLoadModule(`/${this.dir}/generated/root.svelte`))
.default;

Expand Down Expand Up @@ -178,7 +185,6 @@ class Watcher extends EventEmitter {
console.error(colors.gray(error.stack));
},
hooks: {
getContext: hooks.getContext || (() => ({})),
getSession: hooks.getSession || (() => ({})),
handle: hooks.handle || (({ request, render }) => render(request))
},
Expand Down
16 changes: 5 additions & 11 deletions packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,20 @@ export async function respond(incoming, options, state = {}) {
};
}

const incoming_with_body = {
...incoming,
headers: lowercase_keys(incoming.headers),
body: parse_body(incoming)
};

const context = (await options.hooks.getContext(incoming_with_body)) || {};

try {
return await options.hooks.handle({
request: {
...incoming_with_body,
...incoming,
headers: lowercase_keys(incoming.headers),
body: parse_body(incoming),
params: null,
context
locals: {}
},
render: async (request) => {
if (state.prerender && state.prerender.fallback) {
return await render_response({
options,
$session: await options.hooks.getSession({ context }),
$session: await options.hooks.getSession(request),
page_config: { ssr: false, router: true, hydrate: true },
status: 200,
error: null,
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/runtime/server/page/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default async function render_page(request, route, options, state) {
};
}

const $session = await options.hooks.getSession({ context: request.context });
const $session = await options.hooks.getSession(request);

if (route) {
const response = await respond({
Expand Down
19 changes: 7 additions & 12 deletions packages/kit/test/apps/basics/src/hooks.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
import cookie from 'cookie';

/** @type {import('@sveltejs/kit').GetContext} */
export function getContext(request) {
const cookies = cookie.parse(request.headers.cookie || '');

return {
answer: 42,
name: cookies.name
};
}

/** @type {import('@sveltejs/kit').GetSession} */
export function getSession({ context }) {
return context;
export function getSession(request) {
return request.locals;
}

/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ request, render }) {
const cookies = cookie.parse(request.headers.cookie || '');

request.locals.answer = 42;
request.locals.name = cookies.name;

const response = await render(request);

if (response) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
export function get(request) {
return {
body: {
name: request.context.name
name: request.locals.name
}
};
}
8 changes: 4 additions & 4 deletions packages/kit/types/endpoint.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Headers, ParameterizedBody } from './helper';

export type ServerRequest<Context = any, Body = unknown> = {
export type ServerRequest<Locals = Record<string, any>, Body = unknown> = {
method: string;
host: string;
headers: Headers;
Expand All @@ -9,7 +9,7 @@ export type ServerRequest<Context = any, Body = unknown> = {
query: URLSearchParams;
rawBody: string | ArrayBuffer;
body: ParameterizedBody<Body>;
context: Context;
locals: Locals;
};

export type ServerResponse = {
Expand All @@ -18,6 +18,6 @@ export type ServerResponse = {
body?: any;
};

export type RequestHandler<Context = any, Body = unknown> = (
request: ServerRequest<Context, Body>
export type RequestHandler<Locals = Record<string, any>, Body = unknown> = (
request: ServerRequest<Locals, Body>
) => void | ServerResponse | Promise<ServerResponse>;
12 changes: 5 additions & 7 deletions packages/kit/types/hooks.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,11 @@ export type Incoming = {
body?: BaseBody;
};

export type GetContext<Context = any> = (incoming: Incoming) => Context;

export type GetSession<Context = any, Session = any> = {
({ context }: { context: Context }): Session | Promise<Session>;
export type GetSession<Locals = Record<string, any>, Session = any> = {
(request: ServerRequest<Locals>): Session | Promise<Session>;
};

export type Handle<Context = any> = (input: {
request: ServerRequest<Context>;
render: (request: ServerRequest<Context>) => ServerResponse | Promise<ServerResponse>;
export type Handle<Locals = Record<string, any>> = (input: {
request: ServerRequest<Locals>;
render: (request: ServerRequest<Locals>) => ServerResponse | Promise<ServerResponse>;
}) => ServerResponse | Promise<ServerResponse>;
2 changes: 1 addition & 1 deletion packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import './ambient-modules';

export { Adapter, AdapterUtils, Config } from './config';
export { ErrorLoad, Load, Page } from './page';
export { Incoming, GetContext, GetSession, Handle } from './hooks';
export { Incoming, GetSession, Handle } from './hooks';
export { ServerRequest as Request, ServerResponse as Response, RequestHandler } from './endpoint';
Loading

0 comments on commit 051c026

Please sign in to comment.