@sveltejs/kit 3.0.0-next.4 → 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.
Files changed (68) hide show
  1. package/package.json +17 -17
  2. package/src/core/adapt/builder.js +18 -4
  3. package/src/core/adapt/index.js +1 -4
  4. package/src/core/config/index.js +64 -5
  5. package/src/core/config/options.js +73 -21
  6. package/src/core/env.js +35 -4
  7. package/src/core/postbuild/analyse.js +8 -12
  8. package/src/core/postbuild/crawl.js +22 -6
  9. package/src/core/postbuild/prerender.js +40 -23
  10. package/src/core/sync/create_manifest_data/index.js +23 -5
  11. package/src/core/sync/write_client_manifest.js +7 -0
  12. package/src/core/sync/write_server.js +13 -10
  13. package/src/core/sync/write_tsconfig.js +2 -4
  14. package/src/exports/index.js +32 -10
  15. package/src/exports/internal/env.js +1 -1
  16. package/src/exports/internal/index.js +6 -5
  17. package/src/exports/node/index.js +57 -6
  18. package/src/exports/public.d.ts +218 -114
  19. package/src/exports/vite/build/build_server.js +6 -1
  20. package/src/exports/vite/dev/index.js +10 -20
  21. package/src/exports/vite/index.js +306 -222
  22. package/src/exports/vite/preview/index.js +15 -7
  23. package/src/exports/vite/utils.js +8 -10
  24. package/src/runtime/app/env/types.d.ts +1 -1
  25. package/src/runtime/app/forms.js +22 -5
  26. package/src/runtime/app/paths/client.js +1 -3
  27. package/src/runtime/app/paths/public.d.ts +0 -28
  28. package/src/runtime/app/paths/server.js +7 -3
  29. package/src/runtime/app/server/remote/form.js +10 -3
  30. package/src/runtime/app/server/remote/query.js +3 -6
  31. package/src/runtime/app/server/remote/requested.js +8 -4
  32. package/src/runtime/app/server/remote/shared.js +5 -7
  33. package/src/runtime/client/client.js +184 -93
  34. package/src/runtime/client/fetcher.js +3 -2
  35. package/src/runtime/client/remote-functions/form.svelte.js +134 -24
  36. package/src/runtime/client/remote-functions/prerender.svelte.js +8 -2
  37. package/src/runtime/client/remote-functions/query/index.js +3 -2
  38. package/src/runtime/client/remote-functions/query/instance.svelte.js +9 -2
  39. package/src/runtime/client/remote-functions/query-batch.svelte.js +4 -3
  40. package/src/runtime/client/remote-functions/query-live/instance.svelte.js +26 -9
  41. package/src/runtime/client/remote-functions/shared.svelte.js +17 -7
  42. package/src/runtime/client/types.d.ts +9 -1
  43. package/src/runtime/client/utils.js +1 -1
  44. package/src/runtime/form-utils.js +84 -16
  45. package/src/runtime/server/cookie.js +22 -33
  46. package/src/runtime/server/csrf.js +65 -0
  47. package/src/runtime/server/data/index.js +7 -7
  48. package/src/runtime/server/env_module.js +0 -5
  49. package/src/runtime/server/index.js +1 -1
  50. package/src/runtime/server/page/actions.js +41 -26
  51. package/src/runtime/server/page/index.js +2 -1
  52. package/src/runtime/server/page/render.js +21 -23
  53. package/src/runtime/server/page/respond_with_error.js +7 -8
  54. package/src/runtime/server/remote.js +64 -27
  55. package/src/runtime/server/respond.js +81 -50
  56. package/src/runtime/server/utils.js +7 -7
  57. package/src/runtime/telemetry/otel.js +1 -1
  58. package/src/types/ambient.d.ts +5 -4
  59. package/src/types/global-private.d.ts +4 -4
  60. package/src/types/internal.d.ts +8 -10
  61. package/src/types/private.d.ts +33 -1
  62. package/src/types/synthetic/$lib.md +1 -1
  63. package/src/utils/error.js +11 -3
  64. package/src/utils/shared-iterator.js +5 -0
  65. package/src/utils/url.js +99 -2
  66. package/src/version.js +1 -1
  67. package/types/index.d.ts +340 -186
  68. 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: config.prerender.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
- // TODO this can just be config.adapter?
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\`, which is configured in \`paths.base\` and can be imported from \`$app/paths\` - see https://svelte.dev/docs/kit/configuration#paths for more info`
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(config.prerender.origin + encoded), {
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(config.prerender.origin)) {
344
- if (href === config.prerender.origin) return '/';
345
- if (href.at(config.prerender.origin.length) !== '/') return href;
346
- return href.slice(config.prerender.origin.length);
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 environment variables,
501
- // `read` or the `manifest` at the top-level so we need to set them before
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
- const files = fs.readdirSync(dir).map((name) => ({
225
- is_dir: fs.statSync(path.join(dir, name)).isDirectory(),
226
- name
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 Svelte config file'
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 { set_env } from '__sveltekit/env';
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: ({ status, message }) => ${s(error_page)
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, set_env, set_manifest, set_prerendering, set_read_implementation };
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 `kit.alias` instead: https://svelte.dev/docs/kit/configuration#alias'
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
  }
@@ -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 ? App.Error | string | undefined : never} [body] An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
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 ? App.Error | string | undefined : never} [body]
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 {{ message: string } extends App.Error ? App.Error | string | undefined : never} body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
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 or the location cannot be used as a header value.
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
- location.toString()
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>>} variables
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 {{message: string} extends App.Error ? (App.Error | string | undefined) : App.Error} body
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
- req.on('error', (error) => {
53
+ /** @param {Error} error */
54
+ const on_error = (error) => {
51
55
  cancelled = true;
52
56
  controller.error(error);
53
- });
57
+ };
54
58
 
55
- req.on('end', () => {
59
+ const on_end = () => {
56
60
  if (cancelled) return;
57
61
  controller.close();
58
- });
62
+ };
59
63
 
60
- req.on('data', (chunk) => {
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);