Skip to content

Commit 02f7aba

Browse files
authored
[chore] separate Headers type for Request and Response (#2248)
1 parent 2144afd commit 02f7aba

File tree

16 files changed

+63
-26
lines changed

16 files changed

+63
-26
lines changed

.changeset/empty-donuts-smell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
[chore] separate RequestHeaders and ResponseHeaders types

packages/kit/src/core/adapt/prerender.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { pathToFileURL, resolve, URL } from 'url';
44
import { mkdirp } from '../../utils/filesystem.js';
55
import { __fetch_polyfill } from '../../install-fetch.js';
66
import { SVELTE_KIT } from '../constants.js';
7+
import { get_single_valued_header } from '../../utils/http.js';
78

89
/**
910
* @typedef {import('types/config').PrerenderErrorHandler} PrerenderErrorHandler
@@ -182,10 +183,14 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a
182183
mkdirp(dirname(file));
183184

184185
if (response_type === REDIRECT) {
185-
const { location } = headers;
186+
const location = get_single_valued_header(headers, 'location');
186187

187-
log.warn(`${rendered.status} ${path} -> ${location}`);
188-
writeFileSync(file, `<meta http-equiv="refresh" content="0;url=${encodeURI(location)}">`);
188+
if (location) {
189+
log.warn(`${rendered.status} ${path} -> ${location}`);
190+
writeFileSync(file, `<meta http-equiv="refresh" content="0;url=${encodeURI(location)}">`);
191+
} else {
192+
log.warn(`location header missing on redirect received from ${path}`);
193+
}
189194

190195
return;
191196
}

packages/kit/src/core/dev/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ async function create_handler(vite, config, dir, cwd, get_manifest) {
367367

368368
const rendered = await respond(
369369
{
370-
headers: /** @type {import('types/helper').Headers} */ (req.headers),
370+
headers: /** @type {import('types/helper').RequestHeaders} */ (req.headers),
371371
method: req.method,
372372
host,
373373
path: parsed.pathname.replace(config.kit.paths.base, ''),

packages/kit/src/core/preview/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export async function preview({
9191
host: /** @type {string} */ (config.kit.host ||
9292
req.headers[config.kit.hostHeader || 'host']),
9393
method: req.method,
94-
headers: /** @type {import('types/helper').Headers} */ (req.headers),
94+
headers: /** @type {import('types/helper').RequestHeaders} */ (req.headers),
9595
path: parsed.pathname.replace(config.kit.paths.base, ''),
9696
query: parsed.searchParams,
9797
rawBody: body

packages/kit/src/runtime/server/endpoint.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { get_single_valued_header } from '../../utils/http.js';
12
import { lowercase_keys } from './utils.js';
23

34
/** @param {string} body */
@@ -62,7 +63,7 @@ export async function render_endpoint(request, route, match) {
6263
let { status = 200, body, headers = {} } = response;
6364

6465
headers = lowercase_keys(headers);
65-
const type = headers['content-type'];
66+
const type = get_single_valued_header(headers, 'content-type');
6667

6768
const is_type_textual = is_content_type_textual(type);
6869

packages/kit/src/runtime/server/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { parse_body } from './parse_body/index.js';
66
import { lowercase_keys } from './utils.js';
77
import { coalesce_to_error } from '../utils.js';
88
import { hash } from '../hash.js';
9+
import { get_single_valued_header } from '../../utils/http.js';
910

1011
/** @type {import('@sveltejs/kit/ssr').Respond} */
1112
export async function respond(incoming, options, state = {}) {
@@ -66,7 +67,8 @@ export async function respond(incoming, options, state = {}) {
6667
if (response) {
6768
// inject ETags for 200 responses
6869
if (response.status === 200) {
69-
if (!/(no-store|immutable)/.test(response.headers['cache-control'])) {
70+
const cache_control = get_single_valued_header(response.headers, 'cache-control');
71+
if (!cache_control || !/(no-store|immutable)/.test(cache_control)) {
7072
const etag = `"${hash(response.body || '')}"`;
7173

7274
if (request.headers['if-none-match'] === etag) {

packages/kit/src/runtime/server/page/load_node.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ export async function load_node({
120120
} else if (resolved.startsWith('/') && !resolved.startsWith('//')) {
121121
const relative = resolved;
122122

123-
const headers = /** @type {import('types/helper').Headers} */ ({ ...opts.headers });
123+
const headers = /** @type {import('types/helper').RequestHeaders} */ ({
124+
...opts.headers
125+
});
124126

125127
// TODO: fix type https://github.com/node-fetch/node-fetch/issues/1113
126128
if (opts.credentials !== 'omit') {
@@ -164,9 +166,12 @@ export async function load_node({
164166
state.prerender.dependencies.set(relative, rendered);
165167
}
166168

169+
// Set-Cookie not available to be set in `fetch` and that's the only header value that
170+
// can be an array so we know we have only simple values
171+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
167172
response = new Response(rendered.body, {
168173
status: rendered.status,
169-
headers: rendered.headers
174+
headers: /** @type {Record<string, string>} */ (rendered.headers)
170175
});
171176
}
172177
} else {
@@ -211,7 +216,7 @@ export async function load_node({
211216
async function text() {
212217
const body = await response.text();
213218

214-
/** @type {import('types/helper').Headers} */
219+
/** @type {import('types/helper').ResponseHeaders} */
215220
const headers = {};
216221
for (const [key, value] of response.headers) {
217222
if (key !== 'etag' && key !== 'set-cookie') headers[key] = value;

packages/kit/src/runtime/server/page/render.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ export async function render_response({
175175
.join('\n\n\t\t\t')}
176176
`.replace(/^\t{2}/gm, '');
177177

178-
/** @type {import('types/helper').Headers} */
178+
/** @type {import('types/helper').ResponseHeaders} */
179179
const headers = {
180180
'content-type': 'text/html'
181181
};

packages/kit/src/runtime/server/parse_body/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { read_only_form_data } from './read_only_form_data.js';
22

33
/**
44
* @param {import('types/app').RawBody} raw
5-
* @param {import('types/helper').Headers} headers
5+
* @param {import('types/helper').RequestHeaders} headers
66
*/
77
export function parse_body(raw, headers) {
88
if (!raw) return raw;

packages/kit/src/runtime/server/utils.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
/** @param {Record<string, string>} obj */
1+
/** @param {Record<string, any>} obj */
22
export function lowercase_keys(obj) {
3-
/** @type {Record<string, string>} */
3+
/** @type {Record<string, any>} */
44
const clone = {};
55

66
for (const key in obj) {

packages/kit/src/utils/http.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @param {Record<string, string | string[]>} headers
3+
* @param {string} key
4+
* @returns {string | undefined}
5+
*/
6+
export function get_single_valued_header(headers, key) {
7+
const value = headers[key];
8+
if (Array.isArray(value)) {
9+
if (value.length === 0) {
10+
return undefined;
11+
}
12+
if (value.length > 1) {
13+
throw new Error(
14+
`Multiple headers provided for ${key}. Multiple may be provided only for set-cookie`
15+
);
16+
}
17+
return value[0];
18+
}
19+
return value;
20+
}

packages/kit/test/apps/basics/src/hooks.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const handle = sequence(
2323
...response,
2424
headers: {
2525
...response.headers,
26-
'Set-Cookie': 'name=SvelteKit; path=/; HttpOnly'
26+
'set-cookie': 'name=SvelteKit; path=/; HttpOnly'
2727
}
2828
};
2929
}

packages/kit/types/app.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Headers, ReadOnlyFormData } from './helper';
1+
import { ReadOnlyFormData, RequestHeaders } from './helper';
22
import { ServerResponse } from './hooks';
33

44
export interface App {
@@ -32,6 +32,6 @@ export interface IncomingRequest {
3232
host: string;
3333
path: string;
3434
query: URLSearchParams;
35-
headers: Headers;
35+
headers: RequestHeaders;
3636
rawBody: RawBody;
3737
}

packages/kit/types/endpoint.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ServerRequest } from './hooks';
2-
import { Headers, MaybePromise } from './helper';
2+
import { MaybePromise, ResponseHeaders } from './helper';
33

44
type ToJSON = { toJSON(...args: any[]): JSONValue };
55
type JSONValue = Exclude<JSONResponse, ToJSON>;
@@ -16,7 +16,7 @@ type DefaultBody = JSONResponse | Uint8Array;
1616

1717
export interface EndpointOutput<Body extends DefaultBody = DefaultBody> {
1818
status?: number;
19-
headers?: Headers;
19+
headers?: ResponseHeaders;
2020
body?: Body;
2121
}
2222

packages/kit/types/helper.d.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@ interface ReadOnlyFormData {
88
[Symbol.iterator](): Generator<[string, string], void>;
99
}
1010

11-
// TODO we want to differentiate between request headers, which
12-
// always follow this type, and response headers, in which
13-
// 'set-cookie' is a `string[]` (or at least `string | string[]`)
14-
// but this can't happen until TypeScript 4.3
15-
export type Headers = Record<string, string>;
11+
export type RequestHeaders = Record<string, string>;
12+
13+
/** Only value that can be an array is set-cookie. For everything else we assume string value */
14+
export type ResponseHeaders = Record<string, string | string[]>;
1615

1716
// Utility Types
1817
export type InferValue<T, Key extends keyof T, Default> = T extends Record<Key, infer Val>

packages/kit/types/hooks.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { IncomingRequest, ParameterizedBody } from './app';
2-
import { Headers, MaybePromise } from './helper';
2+
import { MaybePromise, ResponseHeaders } from './helper';
33

44
export type StrictBody = string | Uint8Array;
55

@@ -12,7 +12,7 @@ export interface ServerRequest<Locals = Record<string, any>, Body = unknown>
1212

1313
export interface ServerResponse {
1414
status: number;
15-
headers: Headers;
15+
headers: ResponseHeaders;
1616
body?: StrictBody;
1717
}
1818

0 commit comments

Comments
 (0)