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

Remove getContext in favour of request.locals #1332

Merged
merged 6 commits into from
May 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/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) {
Conduitry marked this conversation as resolved.
Show resolved Hide resolved
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) {
Conduitry marked this conversation as resolved.
Show resolved Hide resolved
// 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);
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
}

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