@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.
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 +190 -95
  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 +26 -10
  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
@@ -58,6 +58,12 @@ export interface SvelteKitApp {
58
58
  hash: boolean;
59
59
 
60
60
  root: typeof SvelteComponent;
61
+
62
+ /**
63
+ * Lazily loads the contents of src/error.html, used as a last-resort
64
+ * error page when the root layout's load function throws during client-side rendering.
65
+ */
66
+ get_error_template: () => Promise<(data: { status: number; message: string }) => string>;
61
67
  }
62
68
 
63
69
  export type NavigationIntent = {
@@ -77,6 +83,7 @@ export type NavigationResult = NavigationRedirect | NavigationFinished;
77
83
 
78
84
  export type NavigationRedirect = {
79
85
  type: 'redirect';
86
+ status: number;
80
87
  location: string;
81
88
  };
82
89
 
@@ -119,7 +126,8 @@ export interface NavigationState {
119
126
  }
120
127
 
121
128
  export interface HydrateOptions {
122
- status: number;
129
+ /** Provided in the case of a form action that returns `fail`, but otherwise derived from `error` */
130
+ status?: number;
123
131
  error: App.Error | null;
124
132
  node_ids: number[];
125
133
  params: Record<string, string>;
@@ -1,6 +1,6 @@
1
1
  import { BROWSER, DEV } from 'esm-env';
2
2
  import { writable } from 'svelte/store';
3
- import { assets } from '$app/paths';
3
+ import { assets } from '$app/paths/internal/client';
4
4
  import { version } from '$app/env';
5
5
  import { noop } from '../../utils/functions.js';
6
6
  import { PRELOAD_PRIORITIES } from './constants.js';
@@ -41,14 +41,11 @@ export function convert_formdata(data) {
41
41
 
42
42
  if (is_array) key = key.slice(0, -2);
43
43
 
44
- if (values.length > 1 && !is_array) {
45
- throw new Error(`Form cannot contain duplicated keys — "${key}" has ${values.length} values`);
46
- }
47
-
48
44
  // an empty `<input type="file">` will submit a non-existent file, bizarrely
49
45
  values = values.filter(
50
46
  (entry) => typeof entry === 'string' || entry.name !== '' || entry.size > 0
51
47
  );
48
+ if (values.length === 0 && !is_array) continue;
52
49
 
53
50
  if (key.startsWith('n:')) {
54
51
  key = key.slice(2);
@@ -58,6 +55,10 @@ export function convert_formdata(data) {
58
55
  values = values.map((v) => v === 'on');
59
56
  }
60
57
 
58
+ if (values.length > 1 && !is_array) {
59
+ throw new Error(`Form cannot contain duplicated keys — "${key}" has ${values.length} values`);
60
+ }
61
+
61
62
  set_nested_value(result, key, is_array ? values : values[0]);
62
63
  }
63
64
 
@@ -608,18 +609,53 @@ function get_type_prefix(field_type, is_array, input_value) {
608
609
  return '';
609
610
  }
610
611
 
612
+ /**
613
+ * A deep-clone implementation specifically for form data, where
614
+ * we don't need to worry about cycles and whatnot
615
+ * @param {any} value
616
+ * @returns {any}
617
+ */
618
+ function deep_clone(value) {
619
+ if (value !== null && typeof value === 'object') {
620
+ if (value instanceof Date) {
621
+ return new Date(value.getTime());
622
+ }
623
+
624
+ if (value instanceof File) {
625
+ return value;
626
+ }
627
+
628
+ if (Array.isArray(value)) {
629
+ return value.map(deep_clone);
630
+ }
631
+
632
+ /** @type {Record<string, any>} */
633
+ const clone = {};
634
+ for (const key of Object.keys(value)) {
635
+ clone[key] = deep_clone(value[key]);
636
+ }
637
+
638
+ return clone;
639
+ }
640
+
641
+ return value;
642
+ }
643
+
611
644
  /**
612
645
  * Creates a proxy-based field accessor for form data
613
646
  * @param {any} target - Function or empty POJO
614
- * @param {() => Record<string, any>} get_input - Function to get current input data
615
- * @param {(path: (string | number)[], value: any) => void} set_input - Function to set input data
647
+ * @param {() => Record<string, any>} get - Function to get current input data
648
+ * @param {(path: (string | number)[], value: any) => void} set - Function to set input data
616
649
  * @param {(path?: (string | number)[], all?: boolean) => Record<string, InternalRemoteFormIssue[]>} get_issues - Function to get current issues
650
+ * @param {() => Record<string, boolean>} get_touched - Function to get touched fields
651
+ * @param {() => Record<string, boolean>} get_dirty - Function to get dirty fields
617
652
  * @param {(string | number)[]} path - Current access path
618
653
  * @returns {any} Proxy object with name(), value(), and issues() methods
619
654
  */
620
- export function create_field_proxy(target, get_input, set_input, get_issues, path = []) {
655
+ export function create_field_proxy(target, get, set, get_issues, get_touched, get_dirty, path) {
621
656
  const get_value = () => {
622
- return deep_get(get_input(), path);
657
+ const value = deep_get(get(), path);
658
+ return deep_clone(value);
623
659
  };
624
660
 
625
661
  return new Proxy(target, {
@@ -628,24 +664,26 @@ export function create_field_proxy(target, get_input, set_input, get_issues, pat
628
664
 
629
665
  // Handle array access like jobs[0]
630
666
  if (/^\d+$/.test(prop)) {
631
- return create_field_proxy({}, get_input, set_input, get_issues, [
667
+ return create_field_proxy({}, get, set, get_issues, get_touched, get_dirty, [
632
668
  ...path,
633
669
  parseInt(prop, 10)
634
670
  ]);
635
671
  }
636
672
 
637
673
  const key = build_path_string(path);
674
+ const next = [...path, prop];
638
675
 
639
676
  if (prop === 'set') {
640
677
  const set_func = function (/** @type {any} */ newValue) {
641
- set_input(path, newValue);
678
+ set(path, newValue);
642
679
  return newValue;
643
680
  };
644
- return create_field_proxy(set_func, get_input, set_input, get_issues, [...path, prop]);
681
+
682
+ return create_field_proxy(set_func, get, set, get_issues, get_touched, get_dirty, next);
645
683
  }
646
684
 
647
685
  if (prop === 'value') {
648
- return create_field_proxy(get_value, get_input, set_input, get_issues, [...path, prop]);
686
+ return create_field_proxy(get_value, get, set, get_issues, get_touched, get_dirty, next);
649
687
  }
650
688
 
651
689
  if (prop === 'issues' || prop === 'allIssues') {
@@ -659,15 +697,45 @@ export function create_field_proxy(target, get_input, set_input, get_issues, pat
659
697
  }));
660
698
  }
661
699
 
662
- return all_issues
700
+ const issues = all_issues
663
701
  ?.filter((issue) => issue.name === key)
664
702
  ?.map((issue) => ({
665
703
  path: issue.path,
666
704
  message: issue.message
667
705
  }));
706
+
707
+ return issues?.length ? issues : undefined;
708
+ };
709
+
710
+ return create_field_proxy(issues_func, get, set, get_issues, get_touched, get_dirty, next);
711
+ }
712
+
713
+ if (prop === 'touched' || prop === 'dirty') {
714
+ const fn = () => {
715
+ const object = prop === 'dirty' ? get_dirty() : get_touched();
716
+
717
+ if (key === '') {
718
+ return Object.keys(object).length > 0;
719
+ }
720
+
721
+ if (Object.hasOwn(object, key)) {
722
+ return true;
723
+ }
724
+
725
+ for (const candidate in object) {
726
+ if (!Object.hasOwn(object, candidate)) continue;
727
+ if (!candidate.startsWith(key)) continue;
728
+
729
+ const next = candidate[key.length];
730
+ if (next === '.' || next === '[') {
731
+ return true;
732
+ }
733
+ }
734
+
735
+ return false;
668
736
  };
669
737
 
670
- return create_field_proxy(issues_func, get_input, set_input, get_issues, [...path, prop]);
738
+ return create_field_proxy(fn, get, set, get_issues, get_touched, get_dirty, next);
671
739
  }
672
740
 
673
741
  if (prop === 'as') {
@@ -833,11 +901,11 @@ export function create_field_proxy(target, get_input, set_input, get_issues, pat
833
901
  });
834
902
  };
835
903
 
836
- return create_field_proxy(as_func, get_input, set_input, get_issues, [...path, 'as']);
904
+ return create_field_proxy(as_func, get, set, get_issues, get_touched, get_dirty, next);
837
905
  }
838
906
 
839
907
  // Handle property access (nested fields)
840
- return create_field_proxy({}, get_input, set_input, get_issues, [...path, prop]);
908
+ return create_field_proxy({}, get, set, get_issues, get_touched, get_dirty, next);
841
909
  }
842
910
  });
843
911
  }
@@ -1,4 +1,4 @@
1
- import { parse, serialize } from 'cookie';
1
+ import { parseCookie, parseSetCookie, stringifySetCookie } from 'cookie';
2
2
  import { DEV } from 'esm-env';
3
3
  import { normalize_path, resolve } from '../../utils/url.js';
4
4
  import { add_data_suffix } from '../pathname.js';
@@ -39,7 +39,7 @@ function generate_cookie_key(domain, path, name) {
39
39
  export function get_cookies(request, url) {
40
40
  const header = request.headers.get('cookie') ?? '';
41
41
  const initial_cookies = /** @type {Record<string, string>} */ (
42
- parse(header, { decode: (value) => value })
42
+ parseCookie(header, { decode: (value) => value })
43
43
  );
44
44
 
45
45
  /** @type {string | undefined} */
@@ -48,7 +48,7 @@ export function get_cookies(request, url) {
48
48
  /** @type {Map<string, import('./page/types.js').Cookie>} */
49
49
  const new_cookies = new Map();
50
50
 
51
- /** @type {import('cookie').SerializeOptions} */
51
+ /** @type {Omit<import('cookie').SetCookie, 'name' | 'value'>} */
52
52
  const defaults = {
53
53
  httpOnly: true,
54
54
  path: '/',
@@ -63,10 +63,6 @@ export function get_cookies(request, url) {
63
63
  // typescript users. `@type {import('@sveltejs/kit').Cookies}` above is not
64
64
  // sufficient to do so.
65
65
 
66
- /**
67
- * @param {string} name
68
- * @param {import('cookie').ParseOptions} [opts]
69
- */
70
66
  get(name, opts) {
71
67
  // Look for the most specific matching cookie from new_cookies
72
68
  const best_match = Array.from(new_cookies.values())
@@ -83,7 +79,7 @@ export function get_cookies(request, url) {
83
79
  return best_match.options.maxAge === 0 ? undefined : best_match.value;
84
80
  }
85
81
 
86
- const req_cookies = parse(header, { decode: opts?.decode });
82
+ const req_cookies = parseCookie(header, { decode: opts?.decode });
87
83
  const cookie = req_cookies[name]; // the decoded string or undefined
88
84
 
89
85
  // in development, if the cookie was set during this session with `cookies.set`,
@@ -106,11 +102,8 @@ export function get_cookies(request, url) {
106
102
  return cookie;
107
103
  },
108
104
 
109
- /**
110
- * @param {import('cookie').ParseOptions} [opts]
111
- */
112
105
  getAll(opts) {
113
- const cookies = parse(header, { decode: opts?.decode });
106
+ const cookies = parseCookie(header, { decode: opts?.decode });
114
107
 
115
108
  // Group cookies by name and find the most specific one for each name
116
109
  const lookup = new Map();
@@ -141,29 +134,17 @@ export function get_cookies(request, url) {
141
134
  );
142
135
  },
143
136
 
144
- /**
145
- * @param {string} name
146
- * @param {string} value
147
- * @param {import('cookie').SerializeOptions} options
148
- */
149
137
  set(name, value, options) {
150
138
  set_internal(name, value, { ...defaults, ...options });
151
139
  },
152
140
 
153
- /**
154
- * @param {string} name
155
- * @param {import('cookie').SerializeOptions} options
156
- */
157
141
  delete(name, options) {
158
142
  cookies.set(name, '', { ...options, maxAge: 0 });
159
143
  },
160
144
 
161
- /**
162
- * @param {string} name
163
- * @param {string} value
164
- * @param {import('cookie').SerializeOptions} options
165
- */
166
- serialize(name, value, options) {
145
+ parse: parseSetCookie,
146
+
147
+ serialize(name, value, { encode, ...options }) {
167
148
  let path = options.path ?? '/';
168
149
 
169
150
  if (!options.domain || options.domain === url.hostname) {
@@ -173,7 +154,7 @@ export function get_cookies(request, url) {
173
154
  path = resolve(normalized_url, path);
174
155
  }
175
156
 
176
- return serialize(name, value, { ...defaults, ...options, path });
157
+ return stringifySetCookie({ name, value, ...defaults, ...options, path }, { encode });
177
158
  }
178
159
  };
179
160
 
@@ -200,7 +181,7 @@ export function get_cookies(request, url) {
200
181
  // explicit header has highest precedence
201
182
  if (header) {
202
183
  const parsed = /** @type {Record<string, string>} */ (
203
- parse(header, { decode: (value) => value })
184
+ parseCookie(header, { decode: (value) => value })
204
185
  );
205
186
  for (const name in parsed) {
206
187
  combined_cookies[name] = parsed[name];
@@ -238,7 +219,8 @@ export function get_cookies(request, url) {
238
219
  new_cookies.set(cookie_key, cookie);
239
220
 
240
221
  if (DEV) {
241
- const serialized = serialize(name, value, cookie.options);
222
+ const { encode, ...rest } = cookie.options;
223
+ const serialized = stringifySetCookie({ name, value, ...rest }, { encode });
242
224
  if (text_encoder.encode(serialized).byteLength > MAX_COOKIE_SIZE) {
243
225
  throw new Error(`Cookie "${name}" is too large, and will be discarded by the browser`);
244
226
  }
@@ -296,15 +278,22 @@ export function path_matches(path, constraint) {
296
278
  */
297
279
  export function add_cookies_to_headers(headers, cookies) {
298
280
  for (const new_cookie of cookies) {
299
- const { name, value, options } = new_cookie;
300
- headers.append('set-cookie', serialize(name, value, options));
281
+ const {
282
+ name,
283
+ value,
284
+ options: { encode, ...options }
285
+ } = new_cookie;
286
+ headers.append('set-cookie', stringifySetCookie({ name, value, ...options }, { encode }));
301
287
 
302
288
  // special case — for routes ending with .html, the route data lives in a sibling
303
289
  // `.html__data.json` file rather than a child `/__data.json` file, which means
304
290
  // we need to duplicate the cookie
305
291
  if (options.path.endsWith('.html')) {
306
292
  const path = add_data_suffix(options.path);
307
- headers.append('set-cookie', serialize(name, value, { ...options, path }));
293
+ headers.append(
294
+ 'set-cookie',
295
+ stringifySetCookie({ name, value, ...options, path }, { encode })
296
+ );
308
297
  }
309
298
  }
310
299
  }
@@ -0,0 +1,65 @@
1
+ import { is_form_content_type } from '../../utils/http.js';
2
+
3
+ const mutating_form_methods = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
4
+
5
+ /**
6
+ * The origin SvelteKit treats as "self" when validating the `Origin` header on
7
+ * cross-site requests.
8
+ *
9
+ * By default (`paths.origin` is `undefined`), SvelteKit derives the origin
10
+ * from `request.url` (which is set by the adapter, and ultimately by the
11
+ * platform). When `paths.origin` is configured — for example so that a preview
12
+ * deployment whose URL isn't known at build time, or an app behind a reverse
13
+ * proxy, can declare a canonical origin — that value takes precedence.
14
+ *
15
+ * @param {string | undefined} paths_origin the configured `kit.paths.origin`
16
+ * @param {string} url_origin the origin derived from `request.url`
17
+ * @returns {string}
18
+ */
19
+ export function get_self_origin(paths_origin, url_origin) {
20
+ return paths_origin || url_origin;
21
+ }
22
+
23
+ /**
24
+ * Determines whether a non-remote request should be rejected as a cross-site
25
+ * form submission (CSRF). Used by `respond.js` to gate form `POST`/`PUT`/
26
+ * `PATCH`/`DELETE` requests whose `Origin` header doesn't match the app's
27
+ * self-origin (and isn't in `trusted_origins`).
28
+ *
29
+ * @param {{
30
+ * request: Request;
31
+ * request_origin: string | null;
32
+ * self_origin: string;
33
+ * trusted_origins: string[];
34
+ * }} input
35
+ * @returns {boolean}
36
+ */
37
+ export function is_csrf_forbidden({ request, request_origin, self_origin, trusted_origins }) {
38
+ return (
39
+ is_form_content_type(request) &&
40
+ mutating_form_methods.has(request.method) &&
41
+ request_origin !== self_origin &&
42
+ (!request_origin || !trusted_origins.includes(request_origin))
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Determines whether a remote-function request should be rejected as cross-site.
48
+ *
49
+ * Unlike form submissions, remote functions accept any content type (e.g.
50
+ * `application/json`), so the check is solely on the request method and origin:
51
+ * a non-`GET` request is forbidden when its `Origin` header doesn't match the
52
+ * app's self-origin. Unlike `is_csrf_forbidden`, entries in `trusted_origins`
53
+ * are *not* honoured — remote function endpoints are an implementation detail,
54
+ * not a public API, so cross-origin calls are forbidden regardless.
55
+ *
56
+ * @param {{
57
+ * request: Request;
58
+ * request_origin: string | null;
59
+ * self_origin: string;
60
+ * }} input
61
+ * @returns {boolean}
62
+ */
63
+ export function is_remote_forbidden({ request, request_origin, self_origin }) {
64
+ return request.method !== 'GET' && request_origin !== self_origin;
65
+ }
@@ -1,5 +1,5 @@
1
1
  import { text } from '@sveltejs/kit';
2
- import { HttpError, SvelteKitError, Redirect } from '@sveltejs/kit/internal';
2
+ import { Redirect } from '@sveltejs/kit/internal';
3
3
  import { normalize_error } from '../../../utils/error.js';
4
4
  import { once } from '../../../utils/functions.js';
5
5
  import { server_data_serializer_json } from '../page/data_serializer.js';
@@ -107,13 +107,11 @@ export async function render_data(
107
107
  // Math.min because array isn't guaranteed to resolve in order
108
108
  length = Math.min(length, i + 1);
109
109
 
110
+ const transformed = await handle_error_and_jsonify(event, event_state, options, error);
111
+
110
112
  return /** @type {import('types').ServerErrorNode} */ ({
111
113
  type: 'error',
112
- error: await handle_error_and_jsonify(event, event_state, options, error),
113
- status:
114
- error instanceof HttpError || error instanceof SvelteKitError
115
- ? error.status
116
- : undefined
114
+ error: transformed
117
115
  });
118
116
  })
119
117
  )
@@ -156,7 +154,8 @@ export async function render_data(
156
154
  if (error instanceof Redirect) {
157
155
  return redirect_json_response(error);
158
156
  } else {
159
- return json_response(await handle_error_and_jsonify(event, event_state, options, error), 500);
157
+ const transformed = await handle_error_and_jsonify(event, event_state, options, error);
158
+ return json_response(transformed, transformed.status);
160
159
  }
161
160
  }
162
161
  }
@@ -182,6 +181,7 @@ export function redirect_json_response(redirect) {
182
181
  return json_response(
183
182
  /** @type {import('types').ServerRedirectNode} */ ({
184
183
  type: 'redirect',
184
+ status: redirect.status,
185
185
  location: redirect.location
186
186
  })
187
187
  );
@@ -16,7 +16,6 @@ let headers;
16
16
  */
17
17
  export function get_public_env(request) {
18
18
  const env = rendered_env;
19
- const script = request.url.endsWith('.script.js');
20
19
 
21
20
  payload ??= devalue.uneval(env);
22
21
  etag ??= `W/${Date.now()}`;
@@ -29,9 +28,5 @@ export function get_public_env(request) {
29
28
  return new Response(undefined, { status: 304, headers });
30
29
  }
31
30
 
32
- if (script) {
33
- return new Response(`globalThis.__sveltekit_sw={env:${payload}}`, { headers });
34
- }
35
-
36
31
  return new Response(`export const env=${payload}`, { headers });
37
32
  }
@@ -120,7 +120,7 @@ export class Server {
120
120
  module.handleValidationError ||
121
121
  (({ issues }) => {
122
122
  console.error('Remote function schema validation failed:', issues);
123
- return { message: 'Bad Request' };
123
+ return { message: 'Bad Request', status: 400 };
124
124
  }),
125
125
  reroute: module.reroute || noop,
126
126
  transport: module.transport || {}
@@ -5,7 +5,7 @@ import { DEV } from 'esm-env';
5
5
  import { json } from '@sveltejs/kit';
6
6
  import { HttpError, Redirect, ActionFailure, SvelteKitError } from '@sveltejs/kit/internal';
7
7
  import { with_request_store, merge_tracing } from '@sveltejs/kit/internal/server';
8
- import { get_status, normalize_error } from '../../../utils/error.js';
8
+ import { normalize_error } from '../../../utils/error.js';
9
9
  import { is_form_content_type, negotiate } from '../../../utils/http.js';
10
10
  import { create_replacer, handle_error_and_jsonify } from '../utils.js';
11
11
  import { record_span } from '../../telemetry/record_span.js';
@@ -36,13 +36,15 @@ export async function handle_action_json_request(event, event_state, options, se
36
36
  `POST method not allowed. No form actions exist for ${DEV ? `the page at ${event.route.id}` : 'this page'}`
37
37
  );
38
38
 
39
+ const error = await handle_error_and_jsonify(event, event_state, options, no_actions_error);
40
+
39
41
  return action_json(
40
42
  {
41
43
  type: 'error',
42
- error: await handle_error_and_jsonify(event, event_state, options, no_actions_error)
44
+ error
43
45
  },
44
46
  {
45
- status: no_actions_error.status,
47
+ status: error.status,
46
48
  headers: {
47
49
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405
48
50
  // "The server must generate an Allow header field in a 405 status code response"
@@ -62,22 +64,27 @@ export async function handle_action_json_request(event, event_state, options, se
62
64
  }
63
65
 
64
66
  if (data instanceof ActionFailure) {
65
- return action_json({
66
- type: 'failure',
67
- status: data.status,
68
- // @ts-expect-error we assign a string to what is supposed to be an object. That's ok
69
- // because we don't use the object outside, and this way we have better code navigation
70
- // through knowing where the related interface is used.
71
- data: stringify_action_response(
72
- data.data,
73
- /** @type {string} */ (event.route.id),
74
- options.hooks.transport
75
- )
76
- });
77
- } else {
67
+ return action_json(
68
+ {
69
+ type: 'failure',
70
+ status: data.status,
71
+ // @ts-expect-error we assign a string to what is supposed to be an object. That's ok
72
+ // because we don't use the object outside, and this way we have better code navigation
73
+ // through knowing where the related interface is used.
74
+ data: stringify_action_response(
75
+ data.data,
76
+ /** @type {string} */ (event.route.id),
77
+ options.hooks.transport
78
+ )
79
+ },
80
+ {
81
+ status: data.status
82
+ }
83
+ );
84
+ } else if (data) {
78
85
  return action_json({
79
86
  type: 'success',
80
- status: data ? 200 : 204,
87
+ status: 200,
81
88
  // @ts-expect-error see comment above
82
89
  data: stringify_action_response(
83
90
  data,
@@ -85,6 +92,9 @@ export async function handle_action_json_request(event, event_state, options, se
85
92
  options.hooks.transport
86
93
  )
87
94
  });
95
+ } else {
96
+ // no data returned — use 204 No Content (without a body, per the spec)
97
+ return new Response(null, { status: 204 });
88
98
  }
89
99
  } catch (e) {
90
100
  const err = normalize_error(e);
@@ -93,18 +103,20 @@ export async function handle_action_json_request(event, event_state, options, se
93
103
  return action_json_redirect(err);
94
104
  }
95
105
 
106
+ const transformed = await handle_error_and_jsonify(
107
+ event,
108
+ event_state,
109
+ options,
110
+ check_incorrect_fail_use(err)
111
+ );
112
+
96
113
  return action_json(
97
114
  {
98
115
  type: 'error',
99
- error: await handle_error_and_jsonify(
100
- event,
101
- event_state,
102
- options,
103
- check_incorrect_fail_use(err)
104
- )
116
+ error: transformed
105
117
  },
106
118
  {
107
- status: get_status(err)
119
+ status: transformed.status
108
120
  }
109
121
  );
110
122
  }
@@ -163,6 +175,7 @@ export async function handle_action_request(event, event_state, server) {
163
175
  });
164
176
  return {
165
177
  type: 'error',
178
+ // We're lying a bit with the types here; this will be transformed into a proper App.Error object later
166
179
  error: new SvelteKitError(
167
180
  405,
168
181
  'Method Not Allowed',
@@ -207,6 +220,7 @@ export async function handle_action_request(event, event_state, server) {
207
220
 
208
221
  return {
209
222
  type: 'error',
223
+ // @ts-expect-error We're lying a bit with the types here; this will be transformed into a proper App.Error object later
210
224
  error: check_incorrect_fail_use(err)
211
225
  };
212
226
  }
@@ -243,11 +257,12 @@ async function call_action(event, event_state, actions) {
243
257
  }
244
258
  }
245
259
 
246
- const action = actions[name];
247
- if (!action) {
260
+ if (!Object.hasOwn(actions, name)) {
248
261
  throw new SvelteKitError(404, 'Not Found', `No action with name '${name}' found`);
249
262
  }
250
263
 
264
+ const action = actions[name];
265
+
251
266
  if (!is_form_content_type(event.request)) {
252
267
  throw new SvelteKitError(
253
268
  415,
@@ -269,6 +269,7 @@ export async function render_page(
269
269
  if (state.prerendering && should_prerender_data) {
270
270
  const body = JSON.stringify({
271
271
  type: 'redirect',
272
+ status: err.status,
272
273
  location: err.location
273
274
  });
274
275
 
@@ -281,8 +282,8 @@ export async function render_page(
281
282
  return redirect_response(err.status, err.location);
282
283
  }
283
284
 
284
- const status = get_status(err);
285
285
  const error = await handle_error_and_jsonify(event, event_state, options, err);
286
+ const status = error.status;
286
287
 
287
288
  while (i--) {
288
289
  if (page.errors[i]) {