@sveltejs/kit 3.0.0-next.3 → 3.0.0-next.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/core/adapt/builder.js +18 -4
- package/src/core/adapt/index.js +1 -4
- package/src/core/config/index.js +64 -5
- package/src/core/config/options.js +73 -21
- package/src/core/env.js +35 -4
- package/src/core/postbuild/analyse.js +8 -12
- package/src/core/postbuild/crawl.js +22 -6
- package/src/core/postbuild/prerender.js +40 -23
- package/src/core/sync/create_manifest_data/index.js +23 -5
- package/src/core/sync/write_client_manifest.js +7 -0
- package/src/core/sync/write_server.js +13 -10
- package/src/core/sync/write_tsconfig.js +2 -4
- package/src/exports/index.js +32 -10
- package/src/exports/internal/env.js +1 -1
- package/src/exports/internal/index.js +6 -5
- package/src/exports/node/index.js +57 -6
- package/src/exports/public.d.ts +218 -114
- package/src/exports/vite/build/build_server.js +6 -1
- package/src/exports/vite/dev/index.js +10 -20
- package/src/exports/vite/index.js +306 -222
- package/src/exports/vite/preview/index.js +15 -7
- package/src/exports/vite/utils.js +8 -10
- package/src/runtime/app/env/types.d.ts +1 -1
- package/src/runtime/app/forms.js +22 -5
- package/src/runtime/app/paths/client.js +1 -3
- package/src/runtime/app/paths/public.d.ts +0 -28
- package/src/runtime/app/paths/server.js +7 -3
- package/src/runtime/app/server/remote/form.js +10 -3
- package/src/runtime/app/server/remote/query.js +3 -6
- package/src/runtime/app/server/remote/requested.js +8 -4
- package/src/runtime/app/server/remote/shared.js +5 -7
- package/src/runtime/client/client.js +190 -95
- package/src/runtime/client/fetcher.js +3 -2
- package/src/runtime/client/remote-functions/form.svelte.js +134 -24
- package/src/runtime/client/remote-functions/prerender.svelte.js +8 -2
- package/src/runtime/client/remote-functions/query/index.js +3 -2
- package/src/runtime/client/remote-functions/query/instance.svelte.js +26 -10
- package/src/runtime/client/remote-functions/query-batch.svelte.js +4 -3
- package/src/runtime/client/remote-functions/query-live/instance.svelte.js +26 -9
- package/src/runtime/client/remote-functions/shared.svelte.js +17 -7
- package/src/runtime/client/types.d.ts +9 -1
- package/src/runtime/client/utils.js +1 -1
- package/src/runtime/form-utils.js +84 -16
- package/src/runtime/server/cookie.js +22 -33
- package/src/runtime/server/csrf.js +65 -0
- package/src/runtime/server/data/index.js +7 -7
- package/src/runtime/server/env_module.js +0 -5
- package/src/runtime/server/index.js +1 -1
- package/src/runtime/server/page/actions.js +41 -26
- package/src/runtime/server/page/index.js +2 -1
- package/src/runtime/server/page/render.js +21 -23
- package/src/runtime/server/page/respond_with_error.js +7 -8
- package/src/runtime/server/remote.js +64 -27
- package/src/runtime/server/respond.js +81 -50
- package/src/runtime/server/utils.js +7 -7
- package/src/runtime/telemetry/otel.js +1 -1
- package/src/types/ambient.d.ts +5 -4
- package/src/types/global-private.d.ts +4 -4
- package/src/types/internal.d.ts +8 -10
- package/src/types/private.d.ts +33 -1
- package/src/types/synthetic/$lib.md +1 -1
- package/src/utils/error.js +11 -3
- package/src/utils/shared-iterator.js +5 -0
- package/src/utils/url.js +99 -2
- package/src/version.js +1 -1
- package/types/index.d.ts +340 -186
- package/types/index.d.ts.map +10 -9
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** @import { Adapter } from '@sveltejs/kit' */
|
|
2
1
|
import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
3
2
|
import { dirname, join } from 'node:path';
|
|
4
3
|
import { pathToFileURL } from 'node:url';
|
|
@@ -44,14 +43,20 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
|
|
|
44
43
|
/** @type {import('types').ServerInternalModule} */
|
|
45
44
|
const internal = await import(pathToFileURL(`${out}/server/internal.js`).href);
|
|
46
45
|
|
|
47
|
-
/** @type {import('types').ServerModule} */
|
|
48
|
-
const { Server } = await import(pathToFileURL(`${out}/server/index.js`).href);
|
|
49
|
-
|
|
50
46
|
// configure `import { building } from `$app/env` —
|
|
51
47
|
// essential we do this before analysing the code
|
|
52
48
|
internal.set_building();
|
|
53
49
|
internal.set_prerendering();
|
|
54
50
|
|
|
51
|
+
// `set_env` and `Server` live in modules that import the user's `src/env` config. We import them
|
|
52
|
+
// *after* `set_building()` so that `building`-dependent expressions resolve correctly
|
|
53
|
+
/** @type {import('__sveltekit/env')} */
|
|
54
|
+
const { set_env } = await import(pathToFileURL(`${out}/server/env.js`).href);
|
|
55
|
+
set_env(env);
|
|
56
|
+
|
|
57
|
+
/** @type {import('types').ServerModule} */
|
|
58
|
+
const { Server } = await import(pathToFileURL(`${out}/server/index.js`).href);
|
|
59
|
+
|
|
55
60
|
/**
|
|
56
61
|
* @template {{message: string}} T
|
|
57
62
|
* @template {Omit<T, 'message'>} K
|
|
@@ -105,12 +110,14 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
|
|
|
105
110
|
|
|
106
111
|
const config = extract_svelte_config(vite_config).kit;
|
|
107
112
|
|
|
113
|
+
const prerender_origin = config.paths.origin || 'http://sveltekit-prerender';
|
|
114
|
+
|
|
108
115
|
if (hash) {
|
|
109
116
|
const fallback = await generate_fallback({
|
|
110
117
|
manifest_path,
|
|
111
118
|
env,
|
|
112
119
|
out_dir: config.outDir,
|
|
113
|
-
origin:
|
|
120
|
+
origin: prerender_origin,
|
|
114
121
|
assets: config.files.assets
|
|
115
122
|
});
|
|
116
123
|
|
|
@@ -125,13 +132,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
|
|
|
125
132
|
return { prerendered, prerender_map };
|
|
126
133
|
}
|
|
127
134
|
|
|
128
|
-
|
|
129
|
-
/** @type {Adapter | undefined} */
|
|
130
|
-
const adapter = vite_config.plugins.find(
|
|
131
|
-
(plugin) => plugin.name === 'vite-plugin-sveltekit-adapter'
|
|
132
|
-
)?.api?.adapter;
|
|
133
|
-
|
|
134
|
-
const emulator = await adapter?.emulate?.();
|
|
135
|
+
const emulator = await config.adapter?.emulate?.();
|
|
135
136
|
|
|
136
137
|
/** @type {import('types').Logger} */
|
|
137
138
|
const log = logger({ verbose });
|
|
@@ -145,7 +146,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
|
|
|
145
146
|
({ status, path, referrer, referenceType }) => {
|
|
146
147
|
const message =
|
|
147
148
|
status === 404 && !path.startsWith(config.paths.base)
|
|
148
|
-
? `${path} does not begin with \`base
|
|
149
|
+
? `${path} does not begin with \`base\`. You can fix this by using \`resolve('${path}')\` from \`$app/paths\`. The base path is configurable from \`paths.base\` - see https://svelte.dev/docs/kit/configuration#paths for more info`
|
|
149
150
|
: path;
|
|
150
151
|
|
|
151
152
|
return `${status} ${message}${referrer ? ` (${referenceType} from ${referrer})` : ''}`;
|
|
@@ -180,6 +181,14 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
|
|
|
180
181
|
}
|
|
181
182
|
);
|
|
182
183
|
|
|
184
|
+
const handle_invalid_url = normalise_error_handler(
|
|
185
|
+
log,
|
|
186
|
+
config.prerender.handleInvalidUrl,
|
|
187
|
+
({ href, referrer }) => {
|
|
188
|
+
return `Invalid URL ${href}${referrer ? ` (linked from ${referrer})` : ''}`;
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
|
|
183
192
|
const q = queue(config.prerender.concurrency);
|
|
184
193
|
|
|
185
194
|
/**
|
|
@@ -251,7 +260,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
|
|
|
251
260
|
/** @type {Map<string, import('types').PrerenderDependency>} */
|
|
252
261
|
const dependencies = new Map();
|
|
253
262
|
|
|
254
|
-
const response = await server.respond(new Request(
|
|
263
|
+
const response = await server.respond(new Request(prerender_origin + encoded), {
|
|
255
264
|
getClientAddress() {
|
|
256
265
|
throw new Error('Cannot read clientAddress during prerendering');
|
|
257
266
|
},
|
|
@@ -334,16 +343,20 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
|
|
|
334
343
|
|
|
335
344
|
// if it's a 200 HTML response, crawl it. Skip error responses, as we don't save those
|
|
336
345
|
if (response.ok && config.prerender.crawl && headers['content-type'] === 'text/html') {
|
|
337
|
-
const { ids, hrefs } = crawl(body.toString(), decoded);
|
|
346
|
+
const { ids, hrefs, invalid } = crawl(body.toString(), decoded);
|
|
347
|
+
|
|
348
|
+
for (const href of invalid) {
|
|
349
|
+
handle_invalid_url({ href, referrer: decoded });
|
|
350
|
+
}
|
|
338
351
|
|
|
339
352
|
actual_hashlinks.set(decoded, ids);
|
|
340
353
|
|
|
341
354
|
/** @param {string} href */
|
|
342
355
|
const removePrerenderOrigin = (href) => {
|
|
343
|
-
if (href.startsWith(
|
|
344
|
-
if (href ===
|
|
345
|
-
if (href.at(
|
|
346
|
-
return href.slice(
|
|
356
|
+
if (href.startsWith(prerender_origin)) {
|
|
357
|
+
if (href === prerender_origin) return '/';
|
|
358
|
+
if (href.at(prerender_origin.length) !== '/') return href;
|
|
359
|
+
return href.slice(prerender_origin.length);
|
|
347
360
|
}
|
|
348
361
|
return href;
|
|
349
362
|
};
|
|
@@ -388,6 +401,12 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
|
|
|
388
401
|
const type = headers['content-type'];
|
|
389
402
|
const is_html = response_type === REDIRECT || type === 'text/html';
|
|
390
403
|
|
|
404
|
+
if (!is_html && response.status === 200 && decoded.slice(config.paths.base.length + 1) === '') {
|
|
405
|
+
throw new Error(
|
|
406
|
+
`Cannot prerender a root +server.js that returns a non-HTML response - static hosts always serve an HTML file for \`${config.paths.base || '/'}\``
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
391
410
|
const file = output_filename(decoded, is_html);
|
|
392
411
|
const dest = `${config.outDir}/output/prerendered/${category}/${file}`;
|
|
393
412
|
|
|
@@ -497,10 +516,8 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit
|
|
|
497
516
|
}
|
|
498
517
|
}
|
|
499
518
|
|
|
500
|
-
// the user's remote function modules may reference
|
|
501
|
-
//
|
|
502
|
-
// evaluating those modules to avoid potential runtime errors
|
|
503
|
-
internal.set_env(env);
|
|
519
|
+
// the user's remote function modules may reference `read` or the `manifest` at the top-level
|
|
520
|
+
// so we need to set them before evaluating those modules to avoid potential runtime errors
|
|
504
521
|
internal.set_manifest(manifest);
|
|
505
522
|
internal.set_read_implementation((file) => createReadableStream(`${out}/server/${file}`));
|
|
506
523
|
|
|
@@ -221,10 +221,17 @@ function create_routes_and_nodes(cwd, config, fallback) {
|
|
|
221
221
|
|
|
222
222
|
// We can't use withFileTypes because of a NodeJs bug which returns wrong results
|
|
223
223
|
// with isDirectory() in case of symlinks: https://github.com/nodejs/node/issues/30646
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
224
|
+
// We sort the entries because `readdirSync` order is not guaranteed and differs
|
|
225
|
+
// between runtimes (e.g. Node returns entries alphabetically, Bun in directory
|
|
226
|
+
// order). Node indices are assigned from this traversal order, so without sorting
|
|
227
|
+
// the SSR and client manifests can disagree, causing hydration mismatches.
|
|
228
|
+
const files = fs
|
|
229
|
+
.readdirSync(dir)
|
|
230
|
+
.sort()
|
|
231
|
+
.map((name) => ({
|
|
232
|
+
is_dir: fs.statSync(path.join(dir, name)).isDirectory(),
|
|
233
|
+
name
|
|
234
|
+
}));
|
|
228
235
|
|
|
229
236
|
// process files first
|
|
230
237
|
for (const file of files) {
|
|
@@ -368,7 +375,7 @@ function create_routes_and_nodes(cwd, config, fallback) {
|
|
|
368
375
|
const root = routes[0];
|
|
369
376
|
if (!root.leaf && !root.error && !root.layout && !root.endpoint) {
|
|
370
377
|
throw new Error(
|
|
371
|
-
'No routes found. If you are using a custom src/routes directory, make sure it is specified in your
|
|
378
|
+
'No routes found. If you are using a custom src/routes directory, make sure it is specified in your SvelteKit Vite plugin options'
|
|
372
379
|
);
|
|
373
380
|
}
|
|
374
381
|
}
|
|
@@ -495,6 +502,17 @@ function create_routes_and_nodes(cwd, config, fallback) {
|
|
|
495
502
|
if (route.endpoint) {
|
|
496
503
|
route.endpoint.page_options = get_page_options(route.endpoint.file, cwd);
|
|
497
504
|
}
|
|
505
|
+
|
|
506
|
+
if (route.page && route.endpoint) {
|
|
507
|
+
const page = nodes[route.page.leaf];
|
|
508
|
+
if (page.page_options?.prerender || route.endpoint.page_options?.prerender) {
|
|
509
|
+
const endpoint_file = route.endpoint.file.split('/').pop();
|
|
510
|
+
|
|
511
|
+
throw new Error(
|
|
512
|
+
`Cannot prerender a route (${route.id}) with both a \`+page.svelte\` and a \`${endpoint_file}\``
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
498
516
|
}
|
|
499
517
|
|
|
500
518
|
return {
|
|
@@ -139,6 +139,11 @@ export function write_client_manifest(kit, manifest_data, output, metadata) {
|
|
|
139
139
|
write_if_changed(
|
|
140
140
|
`${output}/app.js`,
|
|
141
141
|
dedent`
|
|
142
|
+
// in dev, this makes Vite inject its client as this module's first dependency,
|
|
143
|
+
// so that global constant replacements are installed before any other module
|
|
144
|
+
// (including user hooks) evaluates. In build it's inert.
|
|
145
|
+
import.meta.hot;
|
|
146
|
+
|
|
142
147
|
${
|
|
143
148
|
client_hooks_file
|
|
144
149
|
? `import * as client_hooks from '${relative_path(output, client_hooks_file)}';`
|
|
@@ -177,6 +182,8 @@ export function write_client_manifest(kit, manifest_data, output, metadata) {
|
|
|
177
182
|
export const decode = (type, value) => decoders[type](value);
|
|
178
183
|
|
|
179
184
|
export { default as root } from '../root.js';
|
|
185
|
+
|
|
186
|
+
export const get_error_template = () => import('../shared/error-template.js').then(m => m.default);
|
|
180
187
|
`
|
|
181
188
|
);
|
|
182
189
|
|
|
@@ -15,7 +15,6 @@ import { escape_html } from '../../utils/escape.js';
|
|
|
15
15
|
* config: import('types').ValidatedConfig;
|
|
16
16
|
* has_service_worker: boolean;
|
|
17
17
|
* template: string;
|
|
18
|
-
* error_page: string;
|
|
19
18
|
* }} opts
|
|
20
19
|
*/
|
|
21
20
|
const server_template = ({
|
|
@@ -23,14 +22,13 @@ const server_template = ({
|
|
|
23
22
|
server_hooks,
|
|
24
23
|
universal_hooks,
|
|
25
24
|
has_service_worker,
|
|
26
|
-
template
|
|
27
|
-
error_page
|
|
25
|
+
template
|
|
28
26
|
}) => `
|
|
29
27
|
import root from '../root.js';
|
|
30
28
|
import { set_building, set_prerendering } from '$app/env/internal';
|
|
31
29
|
import { set_assets } from '$app/paths/internal/server';
|
|
32
30
|
import { set_manifest, set_read_implementation } from '__sveltekit/server';
|
|
33
|
-
import
|
|
31
|
+
import error from '../shared/error-template.js';
|
|
34
32
|
|
|
35
33
|
export const options = {
|
|
36
34
|
app_template_contains_nonce: ${template.includes('%sveltekit.nonce%')},
|
|
@@ -42,6 +40,7 @@ export const options = {
|
|
|
42
40
|
hash_routing: ${s(config.kit.router.type === 'hash')},
|
|
43
41
|
hooks: null, // added lazily, via \`get_hooks\`
|
|
44
42
|
link_header_preload: ${s(config.kit.output.linkHeaderPreload)},
|
|
43
|
+
paths_origin: ${s(config.kit.paths.origin)},
|
|
45
44
|
root,
|
|
46
45
|
service_worker: ${has_service_worker},
|
|
47
46
|
service_worker_options: ${config.kit.serviceWorker.register ? s(config.kit.serviceWorker.options) : 'null'},
|
|
@@ -57,9 +56,7 @@ export const options = {
|
|
|
57
56
|
/%sveltekit\.env\.([^%]+)%/g,
|
|
58
57
|
(_match, capture) => `" + (env[${s(capture)}] ?? "") + "`
|
|
59
58
|
)},
|
|
60
|
-
error
|
|
61
|
-
.replace(/%sveltekit\.status%/g, '" + status + "')
|
|
62
|
-
.replace(/%sveltekit\.error\.message%/g, '" + message + "')}
|
|
59
|
+
error
|
|
63
60
|
},
|
|
64
61
|
version_hash: ${s(hash(config.kit.version.name))}
|
|
65
62
|
};
|
|
@@ -87,7 +84,7 @@ export async function get_hooks() {
|
|
|
87
84
|
};
|
|
88
85
|
}
|
|
89
86
|
|
|
90
|
-
export { set_assets, set_building,
|
|
87
|
+
export { set_assets, set_building, set_manifest, set_prerendering, set_read_implementation };
|
|
91
88
|
`;
|
|
92
89
|
|
|
93
90
|
// TODO need to re-run this whenever src/app.html or src/error.html are
|
|
@@ -120,6 +117,13 @@ export function write_server(config, output, root) {
|
|
|
120
117
|
return posixify(path.relative(`${output}/server`, file));
|
|
121
118
|
}
|
|
122
119
|
|
|
120
|
+
write_if_changed(
|
|
121
|
+
`${output}/shared/error-template.js`,
|
|
122
|
+
`export default ({ status, message }) => ${s(load_error_page(config))
|
|
123
|
+
.replace(/%sveltekit\.status%/g, '" + status + "')
|
|
124
|
+
.replace(/%sveltekit\.error\.message%/g, '" + message + "')};`
|
|
125
|
+
);
|
|
126
|
+
|
|
123
127
|
// Contains the stringified version of
|
|
124
128
|
/** @type {import('types').SSROptions} */
|
|
125
129
|
write_if_changed(
|
|
@@ -130,8 +134,7 @@ export function write_server(config, output, root) {
|
|
|
130
134
|
universal_hooks: universal_hooks_file ? relative(universal_hooks_file) : null,
|
|
131
135
|
has_service_worker:
|
|
132
136
|
config.kit.serviceWorker.register && !!resolve_entry(config.kit.files.serviceWorker),
|
|
133
|
-
template: load_template(root, config)
|
|
134
|
-
error_page: load_error_page(config)
|
|
137
|
+
template: load_template(root, config)
|
|
135
138
|
})
|
|
136
139
|
);
|
|
137
140
|
}
|
|
@@ -62,7 +62,6 @@ export function get_tsconfig(kit, cwd) {
|
|
|
62
62
|
'env.d.ts',
|
|
63
63
|
'non-ambient.d.ts',
|
|
64
64
|
'./types/**/$types.d.ts',
|
|
65
|
-
config_relative('svelte.config.js'),
|
|
66
65
|
config_relative('vite.config.js'),
|
|
67
66
|
config_relative('vite.config.ts')
|
|
68
67
|
]);
|
|
@@ -125,8 +124,7 @@ export function get_tsconfig(kit, cwd) {
|
|
|
125
124
|
moduleResolution: 'bundler',
|
|
126
125
|
module: 'esnext',
|
|
127
126
|
noEmit: true, // prevent tsconfig error "overwriting input files" - Vite handles the build and ignores this
|
|
128
|
-
target: 'esnext'
|
|
129
|
-
types: ['node']
|
|
127
|
+
target: 'esnext'
|
|
130
128
|
},
|
|
131
129
|
include: [...include],
|
|
132
130
|
exclude
|
|
@@ -176,7 +174,7 @@ function validate_user_config(cwd, out, config) {
|
|
|
176
174
|
styleText(
|
|
177
175
|
['bold', 'yellow'],
|
|
178
176
|
`You have specified a baseUrl and/or paths in your ${config.kind} which interferes with SvelteKit's auto-generated tsconfig.json. ` +
|
|
179
|
-
'Remove it to avoid problems with intellisense. For path aliases, use `
|
|
177
|
+
'Remove it to avoid problems with intellisense. For path aliases, use `config.alias` instead: https://svelte.dev/docs/kit/configuration#alias'
|
|
180
178
|
)
|
|
181
179
|
);
|
|
182
180
|
}
|
package/src/exports/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
strip_resolution_suffix
|
|
12
12
|
} from '../runtime/pathname.js';
|
|
13
13
|
import { text_encoder } from '../runtime/utils.js';
|
|
14
|
+
import { validate_redirect_location } from '../utils/url.js';
|
|
14
15
|
|
|
15
16
|
export { VERSION } from '../version.js';
|
|
16
17
|
|
|
@@ -26,10 +27,10 @@ export { VERSION } from '../version.js';
|
|
|
26
27
|
* return an error response without invoking `handleError`.
|
|
27
28
|
* Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
|
|
28
29
|
* @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
|
|
29
|
-
* @param {App.Error} body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
|
|
30
|
+
* @param {Omit<App.Error, 'status'> & { status?: App.Error['status'] }} body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
|
|
30
31
|
* @overload
|
|
31
32
|
* @param {number} status
|
|
32
|
-
* @param {App.Error} body
|
|
33
|
+
* @param {Omit<App.Error, 'status'> & { status?: App.Error['status'] }} body
|
|
33
34
|
* @return {never}
|
|
34
35
|
* @throws {import('./public.js').HttpError} This error instructs SvelteKit to initiate HTTP error handling.
|
|
35
36
|
* @throws {Error} If the provided status is invalid (not between 400 and 599).
|
|
@@ -40,10 +41,10 @@ export { VERSION } from '../version.js';
|
|
|
40
41
|
* return an error response without invoking `handleError`.
|
|
41
42
|
* Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
|
|
42
43
|
* @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
|
|
43
|
-
* @param {{ message: string } extends App.Error ?
|
|
44
|
+
* @param {{ status: number; message: string } extends App.Error ? string | void | undefined : never} body The error message.
|
|
44
45
|
* @overload
|
|
45
46
|
* @param {number} status
|
|
46
|
-
* @param {{ message: string } extends App.Error ?
|
|
47
|
+
* @param {{ status: number; message: string } extends App.Error ? string | void | undefined : never} body
|
|
47
48
|
* @return {never}
|
|
48
49
|
* @throws {import('./public.js').HttpError} This error instructs SvelteKit to initiate HTTP error handling.
|
|
49
50
|
* @throws {Error} If the provided status is invalid (not between 400 and 599).
|
|
@@ -54,17 +55,34 @@ export { VERSION } from '../version.js';
|
|
|
54
55
|
* return an error response without invoking `handleError`.
|
|
55
56
|
* Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
|
|
56
57
|
* @param {number} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
|
|
57
|
-
* @param {
|
|
58
|
+
* @param {string} body The error message.
|
|
59
|
+
* @param {{ status: number; message: string } extends App.Error ? never : Omit<App.Error, 'status' | 'message'>} properties Additional properties of the App.Error type.
|
|
60
|
+
* @overload
|
|
61
|
+
* @param {number} status
|
|
62
|
+
* @param {string} body
|
|
63
|
+
* @param {{ status: number; message: string } extends App.Error ? never : Omit<App.Error, 'status' | 'message'>} properties
|
|
64
|
+
* @return {never}
|
|
65
|
+
* @throws {import('./public.js').HttpError} This error instructs SvelteKit to initiate HTTP error handling.
|
|
66
|
+
* @throws {Error} If the provided status is invalid (not between 400 and 599).
|
|
67
|
+
*/
|
|
68
|
+
/**
|
|
69
|
+
* Throws an error with a HTTP status code and an optional message.
|
|
70
|
+
* When called during request handling, this will cause SvelteKit to
|
|
71
|
+
* return an error response without invoking `handleError`.
|
|
72
|
+
* Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.
|
|
73
|
+
* @param {any} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
|
|
74
|
+
* @param {any} [body] An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
|
|
75
|
+
* @param {any} [properties] Additional properties of the App.Error type when passing a string message.
|
|
58
76
|
* @return {never}
|
|
59
77
|
* @throws {import('./public.js').HttpError} This error instructs SvelteKit to initiate HTTP error handling.
|
|
60
78
|
* @throws {Error} If the provided status is invalid (not between 400 and 599).
|
|
61
79
|
*/
|
|
62
|
-
export function error(status, body) {
|
|
80
|
+
export function error(status, body, properties) {
|
|
63
81
|
if ((!BROWSER || DEV) && (isNaN(status) || status < 400 || status > 599)) {
|
|
64
82
|
throw new Error(`HTTP error status codes must be between 400 and 599 — ${status} is invalid`);
|
|
65
83
|
}
|
|
66
84
|
|
|
67
|
-
throw new HttpError(status, body);
|
|
85
|
+
throw new HttpError(status, body, properties);
|
|
68
86
|
}
|
|
69
87
|
|
|
70
88
|
/**
|
|
@@ -92,19 +110,23 @@ export function isHttpError(e, status) {
|
|
|
92
110
|
*
|
|
93
111
|
* @param {300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | ({} & number)} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages). Must be in the range 300-308.
|
|
94
112
|
* @param {string | URL} location The location to redirect to.
|
|
113
|
+
* @param {{ external?: boolean | string[] }} [options] To redirect to an external URL, you must pass `{ external: true }` to allow any external URL except `javascript:` URLs, or `{ external: [...] }` with an allowlist of permitted origins.
|
|
95
114
|
* @throws {import('./public.js').Redirect} This error instructs SvelteKit to redirect to the specified location.
|
|
96
|
-
* @throws {Error} If the provided status is invalid
|
|
115
|
+
* @throws {Error} If the provided status is invalid, the location cannot be used as a header value, or the location is an external URL without permission.
|
|
97
116
|
* @return {never}
|
|
98
117
|
*/
|
|
99
|
-
export function redirect(status, location) {
|
|
118
|
+
export function redirect(status, location, options) {
|
|
100
119
|
if ((!BROWSER || DEV) && (isNaN(status) || status < 300 || status > 308)) {
|
|
101
120
|
throw new Error('Invalid status code');
|
|
102
121
|
}
|
|
103
122
|
|
|
123
|
+
const href = location.toString();
|
|
124
|
+
validate_redirect_location(href, options);
|
|
125
|
+
|
|
104
126
|
throw new Redirect(
|
|
105
127
|
// @ts-ignore
|
|
106
128
|
status,
|
|
107
|
-
|
|
129
|
+
href
|
|
108
130
|
);
|
|
109
131
|
}
|
|
110
132
|
|
|
@@ -16,7 +16,7 @@ const ASYNC_VALIDATOR = {
|
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* @param {Record<string, EnvVarConfig<any
|
|
19
|
+
* @param {Record<string, EnvVarConfig<any> | undefined>} variables
|
|
20
20
|
* @param {string | undefined} value
|
|
21
21
|
* @param {string} name
|
|
22
22
|
* @param {Record<string, StandardSchemaV1.Issue[]>} issues
|
|
@@ -3,16 +3,17 @@
|
|
|
3
3
|
export class HttpError {
|
|
4
4
|
/**
|
|
5
5
|
* @param {number} status
|
|
6
|
-
* @param {
|
|
6
|
+
* @param {Omit<App.Error, 'status'> | string | undefined} body
|
|
7
|
+
* @param {Omit<App.Error, 'status' | 'message'>} [properties]
|
|
7
8
|
*/
|
|
8
|
-
constructor(status, body) {
|
|
9
|
+
constructor(status, body, properties) {
|
|
9
10
|
this.status = status;
|
|
10
11
|
if (typeof body === 'string') {
|
|
11
|
-
this.body = { message: body };
|
|
12
|
+
this.body = { ...properties, message: body, status };
|
|
12
13
|
} else if (body) {
|
|
13
|
-
this.body = body;
|
|
14
|
+
this.body = { ...body, status };
|
|
14
15
|
} else {
|
|
15
|
-
this.body = { message: `Error: ${status}
|
|
16
|
+
this.body = { message: `Error: ${status}`, status };
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -3,6 +3,9 @@ import { Readable } from 'node:stream';
|
|
|
3
3
|
import { SvelteKitError } from '../internal/index.js';
|
|
4
4
|
import { noop } from '../../utils/functions.js';
|
|
5
5
|
|
|
6
|
+
/** @type {WeakMap<import('http').IncomingMessage, (chunk: Buffer) => void>} */
|
|
7
|
+
const body_data_listeners = new WeakMap();
|
|
8
|
+
|
|
6
9
|
/**
|
|
7
10
|
* @param {import('http').IncomingMessage} req
|
|
8
11
|
* @param {number} [body_size_limit]
|
|
@@ -47,17 +50,19 @@ function get_raw_body(req, body_size_limit) {
|
|
|
47
50
|
return;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
/** @param {Error} error */
|
|
54
|
+
const on_error = (error) => {
|
|
51
55
|
cancelled = true;
|
|
52
56
|
controller.error(error);
|
|
53
|
-
}
|
|
57
|
+
};
|
|
54
58
|
|
|
55
|
-
|
|
59
|
+
const on_end = () => {
|
|
56
60
|
if (cancelled) return;
|
|
57
61
|
controller.close();
|
|
58
|
-
}
|
|
62
|
+
};
|
|
59
63
|
|
|
60
|
-
|
|
64
|
+
/** @param {Buffer} chunk */
|
|
65
|
+
const on_data = (chunk) => {
|
|
61
66
|
if (cancelled) return;
|
|
62
67
|
|
|
63
68
|
size += chunk.length;
|
|
@@ -89,7 +94,12 @@ function get_raw_body(req, body_size_limit) {
|
|
|
89
94
|
if (controller.desiredSize === null || controller.desiredSize <= 0) {
|
|
90
95
|
req.pause();
|
|
91
96
|
}
|
|
92
|
-
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
req.on('error', on_error);
|
|
100
|
+
req.on('end', on_end);
|
|
101
|
+
req.on('data', on_data);
|
|
102
|
+
body_data_listeners.set(req, on_data);
|
|
93
103
|
},
|
|
94
104
|
|
|
95
105
|
pull() {
|
|
@@ -154,6 +164,44 @@ export async function getRequest({ request, base, bodySizeLimit }) {
|
|
|
154
164
|
});
|
|
155
165
|
}
|
|
156
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Drains any unconsumed request body once the response has been sent. When a
|
|
169
|
+
* route doesn't read the request body (for example a page route receiving a
|
|
170
|
+
* POST), the unread bytes remain buffered in the socket. On keep-alive
|
|
171
|
+
* connections Node's HTTP parser then reads those leftover bytes as the next
|
|
172
|
+
* request, fails to parse them, and resets the connection — losing any
|
|
173
|
+
* pipelined request. Resuming the request discards the bytes so the connection
|
|
174
|
+
* stays usable.
|
|
175
|
+
*
|
|
176
|
+
* Because `get_raw_body` attaches a `data` listener, Node marks the request as
|
|
177
|
+
* being consumed (`req._consuming`) and skips its own automatic drain, so we
|
|
178
|
+
* have to do it ourselves. The whole remaining body is read and discarded; this
|
|
179
|
+
* is the intended trade-off (keeping the connection reusable) over destroying it.
|
|
180
|
+
* @see https://github.com/sveltejs/kit/issues/14916
|
|
181
|
+
* @see https://github.com/sveltejs/kit/issues/15526
|
|
182
|
+
* @param {import('http').ServerResponse} res
|
|
183
|
+
*/
|
|
184
|
+
function drain_request(res) {
|
|
185
|
+
const req = res.req;
|
|
186
|
+
if (!req || req.readableEnded || req.destroyed) return;
|
|
187
|
+
|
|
188
|
+
// When the body went unread, get_raw_body's `data` listener is still attached
|
|
189
|
+
// and enqueues into a ReadableStream nobody consumes; it pauses the request at
|
|
190
|
+
// the high water mark, so one chunk sits in memory and the rest stays buffered
|
|
191
|
+
// in the socket. Remove only that `data` listener so the resumed stream drops
|
|
192
|
+
// the remaining bytes instead of re-buffering them. The `end` and `error`
|
|
193
|
+
// listeners stay attached so the body's ReadableStream is still closed (or
|
|
194
|
+
// errored) once draining completes, and a consumer that stopped reading
|
|
195
|
+
// mid-body sees a clean end instead of hanging.
|
|
196
|
+
const on_data = body_data_listeners.get(req);
|
|
197
|
+
if (on_data) {
|
|
198
|
+
req.removeListener('data', on_data);
|
|
199
|
+
body_data_listeners.delete(req);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
req.resume();
|
|
203
|
+
}
|
|
204
|
+
|
|
157
205
|
/**
|
|
158
206
|
* @param {import('http').ServerResponse} res
|
|
159
207
|
* @param {Response} response
|
|
@@ -162,6 +210,9 @@ export async function getRequest({ request, base, bodySizeLimit }) {
|
|
|
162
210
|
// TODO 3.0 make the signature synchronous?
|
|
163
211
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
164
212
|
export async function setResponse(res, response) {
|
|
213
|
+
res.once('finish', () => drain_request(res));
|
|
214
|
+
res.once('close', () => drain_request(res));
|
|
215
|
+
|
|
165
216
|
for (const [key, value] of response.headers) {
|
|
166
217
|
try {
|
|
167
218
|
res.setHeader(key, key === 'set-cookie' ? response.headers.getSetCookie() : value);
|