@sveltejs/kit 1.0.0-next.233 → 1.0.0-next.237

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.
@@ -1,37 +1,44 @@
1
- /**
2
- * @param {Record<string, string | string[] | undefined>} headers
3
- * @param {string} key
4
- * @returns {string | undefined}
5
- * @throws {Error}
6
- */
7
- function get_single_valued_header(headers, key) {
8
- const value = headers[key];
9
- if (Array.isArray(value)) {
10
- if (value.length === 0) {
11
- return undefined;
12
- }
13
- if (value.length > 1) {
14
- throw new Error(
15
- `Multiple headers provided for ${key}. Multiple may be provided only for set-cookie`
16
- );
1
+ /** @param {Partial<import('types/helper').ResponseHeaders> | undefined} object */
2
+ function to_headers(object) {
3
+ const headers = new Headers();
4
+
5
+ if (object) {
6
+ for (const key in object) {
7
+ const value = object[key];
8
+ if (!value) continue;
9
+
10
+ if (typeof value === 'string') {
11
+ headers.set(key, value);
12
+ } else {
13
+ value.forEach((value) => {
14
+ headers.append(key, value);
15
+ });
16
+ }
17
17
  }
18
- return value[0];
19
18
  }
20
- return value;
19
+
20
+ return headers;
21
21
  }
22
22
 
23
- /** @param {Record<string, any>} obj */
24
- function lowercase_keys(obj) {
25
- /** @type {Record<string, any>} */
26
- const clone = {};
23
+ /**
24
+ * Hash using djb2
25
+ * @param {import('types/hooks').StrictBody} value
26
+ */
27
+ function hash(value) {
28
+ let hash = 5381;
29
+ let i = value.length;
27
30
 
28
- for (const key in obj) {
29
- clone[key.toLowerCase()] = obj[key];
31
+ if (typeof value === 'string') {
32
+ while (i) hash = (hash * 33) ^ value.charCodeAt(--i);
33
+ } else {
34
+ while (i) hash = (hash * 33) ^ value[--i];
30
35
  }
31
36
 
32
- return clone;
37
+ return (hash >>> 0).toString(36);
33
38
  }
34
39
 
40
+ /** @param {Record<string, any>} obj */
41
+
35
42
  /** @param {Record<string, string>} params */
36
43
  function decode_params(params) {
37
44
  for (const key in params) {
@@ -56,11 +63,9 @@ function decode_params(params) {
56
63
 
57
64
  /** @param {string} body */
58
65
  function error(body) {
59
- return {
60
- status: 500,
61
- body,
62
- headers: {}
63
- };
66
+ return new Response(body, {
67
+ status: 500
68
+ });
64
69
  }
65
70
 
66
71
  /** @param {unknown} s */
@@ -89,16 +94,16 @@ function is_text(content_type) {
89
94
  }
90
95
 
91
96
  /**
92
- * @param {import('types/hooks').ServerRequest} request
97
+ * @param {import('types/hooks').RequestEvent} event
93
98
  * @param {import('types/internal').SSREndpoint} route
94
99
  * @param {RegExpExecArray} match
95
- * @returns {Promise<import('types/hooks').ServerResponse | undefined>}
100
+ * @returns {Promise<Response | undefined>}
96
101
  */
97
- async function render_endpoint(request, route, match) {
102
+ async function render_endpoint(event, route, match) {
98
103
  const mod = await route.load();
99
104
 
100
105
  /** @type {import('types/endpoint').RequestHandler} */
101
- const handler = mod[request.method.toLowerCase().replace('delete', 'del')]; // 'delete' is a reserved word
106
+ const handler = mod[event.request.method.toLowerCase().replace('delete', 'del')]; // 'delete' is a reserved word
102
107
 
103
108
  if (!handler) {
104
109
  return;
@@ -107,10 +112,10 @@ async function render_endpoint(request, route, match) {
107
112
  // we're mutating `request` so that we don't have to do { ...request, params }
108
113
  // on the next line, since that breaks the getters that replace path, query and
109
114
  // origin. We could revert that once we remove the getters
110
- request.params = route.params ? decode_params(route.params(match)) : {};
115
+ event.params = route.params ? decode_params(route.params(match)) : {};
111
116
 
112
- const response = await handler(request);
113
- const preface = `Invalid response from route ${request.url.pathname}`;
117
+ const response = await handler(event);
118
+ const preface = `Invalid response from route ${event.url.pathname}`;
114
119
 
115
120
  if (typeof response !== 'object') {
116
121
  return error(`${preface}: expected an object, got ${typeof response}`);
@@ -120,10 +125,11 @@ async function render_endpoint(request, route, match) {
120
125
  return;
121
126
  }
122
127
 
123
- let { status = 200, body, headers = {} } = response;
128
+ const { status = 200, body = {} } = response;
129
+ const headers =
130
+ response.headers instanceof Headers ? response.headers : to_headers(response.headers);
124
131
 
125
- headers = lowercase_keys(headers);
126
- const type = get_single_valued_header(headers, 'content-type');
132
+ const type = headers.get('content-type');
127
133
 
128
134
  if (!is_text(type) && !(body instanceof Uint8Array || is_string(body))) {
129
135
  return error(
@@ -134,19 +140,45 @@ async function render_endpoint(request, route, match) {
134
140
  /** @type {import('types/hooks').StrictBody} */
135
141
  let normalized_body;
136
142
 
137
- // ensure the body is an object
138
- if (
139
- (typeof body === 'object' || typeof body === 'undefined') &&
140
- !(body instanceof Uint8Array) &&
141
- (!type || type.startsWith('application/json'))
142
- ) {
143
- headers = { ...headers, 'content-type': 'application/json; charset=utf-8' };
144
- normalized_body = JSON.stringify(typeof body === 'undefined' ? {} : body);
143
+ if (is_pojo(body) && (!type || type.startsWith('application/json'))) {
144
+ headers.set('content-type', 'application/json; charset=utf-8');
145
+ normalized_body = JSON.stringify(body);
145
146
  } else {
146
147
  normalized_body = /** @type {import('types/hooks').StrictBody} */ (body);
147
148
  }
148
149
 
149
- return { status, body: normalized_body, headers };
150
+ if (
151
+ (typeof normalized_body === 'string' || normalized_body instanceof Uint8Array) &&
152
+ !headers.has('etag')
153
+ ) {
154
+ const cache_control = headers.get('cache-control');
155
+ if (!cache_control || !/(no-store|immutable)/.test(cache_control)) {
156
+ headers.set('etag', `"${hash(normalized_body)}"`);
157
+ }
158
+ }
159
+
160
+ return new Response(normalized_body, {
161
+ status,
162
+ headers
163
+ });
164
+ }
165
+
166
+ /** @param {any} body */
167
+ function is_pojo(body) {
168
+ if (typeof body !== 'object') return false;
169
+
170
+ if (body) {
171
+ if (body instanceof Uint8Array) return false;
172
+
173
+ // body could be a node Readable, but we don't want to import
174
+ // node built-ins, so we use duck typing
175
+ if (body._readableState && body._writableState && body._events) return false;
176
+
177
+ // similarly, it could be a web ReadableStream
178
+ if (body[Symbol.toStringTag] === 'ReadableStream') return false;
179
+ }
180
+
181
+ return true;
150
182
  }
151
183
 
152
184
  var chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$';
@@ -439,23 +471,6 @@ function coalesce_to_error(err) {
439
471
  : new Error(JSON.stringify(err));
440
472
  }
441
473
 
442
- /**
443
- * Hash using djb2
444
- * @param {import('types/hooks').StrictBody} value
445
- */
446
- function hash(value) {
447
- let hash = 5381;
448
- let i = value.length;
449
-
450
- if (typeof value === 'string') {
451
- while (i) hash = (hash * 33) ^ value.charCodeAt(--i);
452
- } else {
453
- while (i) hash = (hash * 33) ^ value[--i];
454
- }
455
-
456
- return (hash >>> 0).toString(36);
457
- }
458
-
459
474
  /** @type {Record<string, string>} */
460
475
  const escape_json_string_in_html_dict = {
461
476
  '"': '\\"',
@@ -723,12 +738,7 @@ async function render_response({
723
738
  params: ${devalue(params)}
724
739
  }` : 'null'}
725
740
  });
726
- </script>${options.service_worker ? `
727
- <script>
728
- if ('serviceWorker' in navigator) {
729
- navigator.serviceWorker.register('${options.service_worker}');
730
- }
731
- </script>` : ''}`;
741
+ </script>`;
732
742
 
733
743
  body += serialized_data
734
744
  .map(({ url, body, json }) => {
@@ -741,34 +751,41 @@ async function render_response({
741
751
  })
742
752
  .join('\n\n\t');
743
753
  }
754
+
755
+ if (options.service_worker) {
756
+ // always include service worker unless it's turned off explicitly
757
+ head += `
758
+ <script>
759
+ if ('serviceWorker' in navigator) {
760
+ navigator.serviceWorker.register('${options.service_worker}');
761
+ }
762
+ </script>`;
763
+ }
744
764
  }
745
765
 
746
- /** @type {import('types/helper').ResponseHeaders} */
747
- const headers = {
748
- 'content-type': 'text/html'
749
- };
766
+ const segments = url.pathname.slice(options.paths.base.length).split('/').slice(2);
767
+ const assets =
768
+ options.paths.assets || (segments.length > 0 ? segments.map(() => '..').join('/') : '.');
769
+
770
+ const html = options.template({ head, body, assets });
771
+
772
+ const headers = new Headers({
773
+ 'content-type': 'text/html',
774
+ etag: `"${hash(html)}"`
775
+ });
750
776
 
751
777
  if (maxage) {
752
- headers['cache-control'] = `${is_private ? 'private' : 'public'}, max-age=${maxage}`;
778
+ headers.set('cache-control', `${is_private ? 'private' : 'public'}, max-age=${maxage}`);
753
779
  }
754
780
 
755
781
  if (!options.floc) {
756
- headers['permissions-policy'] = 'interest-cohort=()';
782
+ headers.set('permissions-policy', 'interest-cohort=()');
757
783
  }
758
784
 
759
- const segments = url.pathname.slice(options.paths.base.length).split('/').slice(2);
760
- const assets =
761
- options.paths.assets || (segments.length > 0 ? segments.map(() => '..').join('/') : '.');
762
-
763
- return {
785
+ return new Response(html, {
764
786
  status,
765
- headers,
766
- body: options.template({
767
- head,
768
- body,
769
- assets
770
- })
771
- };
787
+ headers
788
+ });
772
789
  }
773
790
 
774
791
  /**
@@ -906,7 +923,7 @@ function is_root_relative(path) {
906
923
 
907
924
  /**
908
925
  * @param {{
909
- * request: import('types/hooks').ServerRequest;
926
+ * event: import('types/hooks').RequestEvent;
910
927
  * options: import('types/internal').SSRRenderOptions;
911
928
  * state: import('types/internal').SSRRenderState;
912
929
  * route: import('types/internal').SSRPage | null;
@@ -922,7 +939,7 @@ function is_root_relative(path) {
922
939
  * @returns {Promise<import('./types').Loaded | undefined>} undefined for fallthrough
923
940
  */
924
941
  async function load_node({
925
- request,
942
+ event,
926
943
  options,
927
944
  state,
928
945
  route,
@@ -993,7 +1010,7 @@ async function load_node({
993
1010
 
994
1011
  opts.headers = new Headers(opts.headers);
995
1012
 
996
- const resolved = resolve(request.url.pathname, requested.split('?')[0]);
1013
+ const resolved = resolve(event.url.pathname, requested.split('?')[0]);
997
1014
 
998
1015
  let response;
999
1016
 
@@ -1029,12 +1046,15 @@ async function load_node({
1029
1046
  if (opts.credentials !== 'omit') {
1030
1047
  uses_credentials = true;
1031
1048
 
1032
- if (request.headers.cookie) {
1033
- opts.headers.set('cookie', request.headers.cookie);
1049
+ const cookie = event.request.headers.get('cookie');
1050
+ const authorization = event.request.headers.get('authorization');
1051
+
1052
+ if (cookie) {
1053
+ opts.headers.set('cookie', cookie);
1034
1054
  }
1035
1055
 
1036
- if (request.headers.authorization && !opts.headers.has('authorization')) {
1037
- opts.headers.set('authorization', request.headers.authorization);
1056
+ if (authorization && !opts.headers.has('authorization')) {
1057
+ opts.headers.set('authorization', authorization);
1038
1058
  }
1039
1059
  }
1040
1060
 
@@ -1047,12 +1067,7 @@ async function load_node({
1047
1067
  }
1048
1068
 
1049
1069
  const rendered = await respond(
1050
- {
1051
- url: new URL(requested, request.url),
1052
- method: opts.method || 'GET',
1053
- headers: Object.fromEntries(opts.headers),
1054
- rawBody: opts.body == null ? null : new TextEncoder().encode(opts.body)
1055
- },
1070
+ new Request(new URL(requested, event.url).href, opts),
1056
1071
  options,
1057
1072
  {
1058
1073
  fetched: requested,
@@ -1065,17 +1080,11 @@ async function load_node({
1065
1080
  state.prerender.dependencies.set(relative, rendered);
1066
1081
  }
1067
1082
 
1068
- // Set-Cookie must be filtered out (done below) and that's the only header value that
1069
- // can be an array so we know we have only simple values
1070
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
1071
- response = new Response(rendered.body, {
1072
- status: rendered.status,
1073
- headers: /** @type {Record<string, string>} */ (rendered.headers)
1074
- });
1083
+ response = rendered;
1075
1084
  } else {
1076
1085
  // we can't load the endpoint from our own manifest,
1077
1086
  // so we need to make an actual HTTP request
1078
- return fetch(new URL(requested, request.url).href, {
1087
+ return fetch(new URL(requested, event.url).href, {
1079
1088
  method: opts.method || 'GET',
1080
1089
  headers: opts.headers
1081
1090
  });
@@ -1098,11 +1107,13 @@ async function load_node({
1098
1107
  // ports do not affect the resolution
1099
1108
  // leading dot prevents mydomain.com matching domain.com
1100
1109
  if (
1101
- `.${new URL(requested).hostname}`.endsWith(`.${request.url.hostname}`) &&
1110
+ `.${new URL(requested).hostname}`.endsWith(`.${event.url.hostname}`) &&
1102
1111
  opts.credentials !== 'omit'
1103
1112
  ) {
1104
1113
  uses_credentials = true;
1105
- opts.headers.set('cookie', request.headers.cookie);
1114
+
1115
+ const cookie = event.request.headers.get('cookie');
1116
+ if (cookie) opts.headers.set('cookie', cookie);
1106
1117
  }
1107
1118
 
1108
1119
  const external_request = new Request(requested, /** @type {RequestInit} */ (opts));
@@ -1211,7 +1222,7 @@ async function load_node({
1211
1222
 
1212
1223
  /**
1213
1224
  * @param {{
1214
- * request: import('types/hooks').ServerRequest;
1225
+ * event: import('types/hooks').RequestEvent;
1215
1226
  * options: SSRRenderOptions;
1216
1227
  * state: SSRRenderState;
1217
1228
  * $session: any;
@@ -1220,15 +1231,7 @@ async function load_node({
1220
1231
  * ssr: boolean;
1221
1232
  * }} opts
1222
1233
  */
1223
- async function respond_with_error({
1224
- request,
1225
- options,
1226
- state,
1227
- $session,
1228
- status,
1229
- error,
1230
- ssr
1231
- }) {
1234
+ async function respond_with_error({ event, options, state, $session, status, error, ssr }) {
1232
1235
  try {
1233
1236
  const default_layout = await options.manifest._.nodes[0](); // 0 is always the root layout
1234
1237
  const default_error = await options.manifest._.nodes[1](); // 1 is always the root error
@@ -1238,11 +1241,11 @@ async function respond_with_error({
1238
1241
 
1239
1242
  const layout_loaded = /** @type {Loaded} */ (
1240
1243
  await load_node({
1241
- request,
1244
+ event,
1242
1245
  options,
1243
1246
  state,
1244
1247
  route: null,
1245
- url: request.url, // TODO this is redundant, no?
1248
+ url: event.url, // TODO this is redundant, no?
1246
1249
  params,
1247
1250
  node: default_layout,
1248
1251
  $session,
@@ -1253,11 +1256,11 @@ async function respond_with_error({
1253
1256
 
1254
1257
  const error_loaded = /** @type {Loaded} */ (
1255
1258
  await load_node({
1256
- request,
1259
+ event,
1257
1260
  options,
1258
1261
  state,
1259
1262
  route: null,
1260
- url: request.url,
1263
+ url: event.url,
1261
1264
  params,
1262
1265
  node: default_error,
1263
1266
  $session,
@@ -1280,26 +1283,23 @@ async function respond_with_error({
1280
1283
  status,
1281
1284
  error,
1282
1285
  branch: [layout_loaded, error_loaded],
1283
- url: request.url,
1286
+ url: event.url,
1284
1287
  params,
1285
1288
  ssr
1286
1289
  });
1287
1290
  } catch (err) {
1288
1291
  const error = coalesce_to_error(err);
1289
1292
 
1290
- options.handle_error(error, request);
1293
+ options.handle_error(error, event);
1291
1294
 
1292
- return {
1293
- status: 500,
1294
- headers: {},
1295
- body: error.stack
1296
- };
1295
+ return new Response(error.stack, {
1296
+ status: 500
1297
+ });
1297
1298
  }
1298
1299
  }
1299
1300
 
1300
1301
  /**
1301
1302
  * @typedef {import('./types.js').Loaded} Loaded
1302
- * @typedef {import('types/hooks').ServerResponse} ServerResponse
1303
1303
  * @typedef {import('types/internal').SSRNode} SSRNode
1304
1304
  * @typedef {import('types/internal').SSRRenderOptions} SSRRenderOptions
1305
1305
  * @typedef {import('types/internal').SSRRenderState} SSRRenderState
@@ -1307,7 +1307,7 @@ async function respond_with_error({
1307
1307
 
1308
1308
  /**
1309
1309
  * @param {{
1310
- * request: import('types/hooks').ServerRequest;
1310
+ * event: import('types/hooks').RequestEvent;
1311
1311
  * options: SSRRenderOptions;
1312
1312
  * state: SSRRenderState;
1313
1313
  * $session: any;
@@ -1315,10 +1315,10 @@ async function respond_with_error({
1315
1315
  * params: Record<string, string>;
1316
1316
  * ssr: boolean;
1317
1317
  * }} opts
1318
- * @returns {Promise<ServerResponse | undefined>}
1318
+ * @returns {Promise<Response | undefined>}
1319
1319
  */
1320
1320
  async function respond$1(opts) {
1321
- const { request, options, state, $session, route, ssr } = opts;
1321
+ const { event, options, state, $session, route, ssr } = opts;
1322
1322
 
1323
1323
  /** @type {Array<SSRNode | undefined>} */
1324
1324
  let nodes;
@@ -1332,7 +1332,7 @@ async function respond$1(opts) {
1332
1332
  router: true
1333
1333
  },
1334
1334
  status: 200,
1335
- url: request.url,
1335
+ url: event.url,
1336
1336
  stuff: {}
1337
1337
  });
1338
1338
  }
@@ -1344,10 +1344,10 @@ async function respond$1(opts) {
1344
1344
  } catch (err) {
1345
1345
  const error = coalesce_to_error(err);
1346
1346
 
1347
- options.handle_error(error, request);
1347
+ options.handle_error(error, event);
1348
1348
 
1349
1349
  return await respond_with_error({
1350
- request,
1350
+ event,
1351
1351
  options,
1352
1352
  state,
1353
1353
  $session,
@@ -1365,10 +1365,9 @@ async function respond$1(opts) {
1365
1365
  if (!leaf.prerender && state.prerender && !state.prerender.all) {
1366
1366
  // if the page has `export const prerender = true`, continue,
1367
1367
  // otherwise bail out at this point
1368
- return {
1369
- status: 204,
1370
- headers: {}
1371
- };
1368
+ return new Response(undefined, {
1369
+ status: 204
1370
+ });
1372
1371
  }
1373
1372
 
1374
1373
  /** @type {Array<Loaded>} */
@@ -1396,7 +1395,7 @@ async function respond$1(opts) {
1396
1395
  try {
1397
1396
  loaded = await load_node({
1398
1397
  ...opts,
1399
- url: request.url,
1398
+ url: event.url,
1400
1399
  node,
1401
1400
  stuff,
1402
1401
  is_error: false
@@ -1408,12 +1407,12 @@ async function respond$1(opts) {
1408
1407
 
1409
1408
  if (loaded.loaded.redirect) {
1410
1409
  return with_cookies(
1411
- {
1410
+ new Response(undefined, {
1412
1411
  status: loaded.loaded.status,
1413
1412
  headers: {
1414
- location: encodeURI(loaded.loaded.redirect)
1413
+ location: loaded.loaded.redirect
1415
1414
  }
1416
- },
1415
+ }),
1417
1416
  set_cookie_headers
1418
1417
  );
1419
1418
  }
@@ -1424,7 +1423,7 @@ async function respond$1(opts) {
1424
1423
  } catch (err) {
1425
1424
  const e = coalesce_to_error(err);
1426
1425
 
1427
- options.handle_error(e, request);
1426
+ options.handle_error(e, event);
1428
1427
 
1429
1428
  status = 500;
1430
1429
  error = e;
@@ -1450,7 +1449,7 @@ async function respond$1(opts) {
1450
1449
  const error_loaded = /** @type {import('./types').Loaded} */ (
1451
1450
  await load_node({
1452
1451
  ...opts,
1453
- url: request.url,
1452
+ url: event.url,
1454
1453
  node: error_node,
1455
1454
  stuff: node_loaded.stuff,
1456
1455
  is_error: true,
@@ -1470,7 +1469,7 @@ async function respond$1(opts) {
1470
1469
  } catch (err) {
1471
1470
  const e = coalesce_to_error(err);
1472
1471
 
1473
- options.handle_error(e, request);
1472
+ options.handle_error(e, event);
1474
1473
 
1475
1474
  continue;
1476
1475
  }
@@ -1482,7 +1481,7 @@ async function respond$1(opts) {
1482
1481
  // for now just return regular error page
1483
1482
  return with_cookies(
1484
1483
  await respond_with_error({
1485
- request,
1484
+ event,
1486
1485
  options,
1487
1486
  state,
1488
1487
  $session,
@@ -1509,7 +1508,7 @@ async function respond$1(opts) {
1509
1508
  await render_response({
1510
1509
  ...opts,
1511
1510
  stuff,
1512
- url: request.url,
1511
+ url: event.url,
1513
1512
  page_config,
1514
1513
  status,
1515
1514
  error,
@@ -1520,7 +1519,7 @@ async function respond$1(opts) {
1520
1519
  } catch (err) {
1521
1520
  const error = coalesce_to_error(err);
1522
1521
 
1523
- options.handle_error(error, request);
1522
+ options.handle_error(error, event);
1524
1523
 
1525
1524
  return with_cookies(
1526
1525
  await respond_with_error({
@@ -1552,41 +1551,41 @@ function get_page_config(leaf, options) {
1552
1551
  }
1553
1552
 
1554
1553
  /**
1555
- * @param {ServerResponse} response
1554
+ * @param {Response} response
1556
1555
  * @param {string[]} set_cookie_headers
1557
1556
  */
1558
1557
  function with_cookies(response, set_cookie_headers) {
1559
1558
  if (set_cookie_headers.length) {
1560
- response.headers['set-cookie'] = set_cookie_headers;
1559
+ set_cookie_headers.forEach((value) => {
1560
+ response.headers.append('set-cookie', value);
1561
+ });
1561
1562
  }
1562
1563
  return response;
1563
1564
  }
1564
1565
 
1565
1566
  /**
1566
- * @param {import('types/hooks').ServerRequest} request
1567
+ * @param {import('types/hooks').RequestEvent} event
1567
1568
  * @param {import('types/internal').SSRPage} route
1568
1569
  * @param {RegExpExecArray} match
1569
1570
  * @param {import('types/internal').SSRRenderOptions} options
1570
1571
  * @param {import('types/internal').SSRRenderState} state
1571
1572
  * @param {boolean} ssr
1572
- * @returns {Promise<import('types/hooks').ServerResponse | undefined>}
1573
+ * @returns {Promise<Response | undefined>}
1573
1574
  */
1574
- async function render_page(request, route, match, options, state, ssr) {
1575
+ async function render_page(event, route, match, options, state, ssr) {
1575
1576
  if (state.initiator === route) {
1576
1577
  // infinite request cycle detected
1577
- return {
1578
- status: 404,
1579
- headers: {},
1580
- body: `Not found: ${request.url.pathname}`
1581
- };
1578
+ return new Response(`Not found: ${event.url.pathname}`, {
1579
+ status: 404
1580
+ });
1582
1581
  }
1583
1582
 
1584
1583
  const params = route.params ? decode_params(route.params(match)) : {};
1585
1584
 
1586
- const $session = await options.hooks.getSession(request);
1585
+ const $session = await options.hooks.getSession(event);
1587
1586
 
1588
1587
  const response = await respond$1({
1589
- request,
1588
+ event,
1590
1589
  options,
1591
1590
  state,
1592
1591
  $session,
@@ -1604,290 +1603,119 @@ async function render_page(request, route, match, options, state, ssr) {
1604
1603
  // rather than render the error page — which could lead to an
1605
1604
  // infinite loop, if the `load` belonged to the root layout,
1606
1605
  // we respond with a bare-bones 500
1607
- return {
1608
- status: 500,
1609
- headers: {},
1610
- body: `Bad request in load function: failed to fetch ${state.fetched}`
1611
- };
1612
- }
1613
- }
1614
-
1615
- function read_only_form_data() {
1616
- /** @type {Map<string, string[]>} */
1617
- const map = new Map();
1618
-
1619
- return {
1620
- /**
1621
- * @param {string} key
1622
- * @param {string} value
1623
- */
1624
- append(key, value) {
1625
- const existing_values = map.get(key);
1626
- if (existing_values) {
1627
- existing_values.push(value);
1628
- } else {
1629
- map.set(key, [value]);
1630
- }
1631
- },
1632
-
1633
- data: new ReadOnlyFormData(map)
1634
- };
1635
- }
1636
-
1637
- class ReadOnlyFormData {
1638
- /** @type {Map<string, string[]>} */
1639
- #map;
1640
-
1641
- /** @param {Map<string, string[]>} map */
1642
- constructor(map) {
1643
- this.#map = map;
1644
- }
1645
-
1646
- /** @param {string} key */
1647
- get(key) {
1648
- const value = this.#map.get(key);
1649
- if (!value) {
1650
- return null;
1651
- }
1652
- return value[0];
1653
- }
1654
-
1655
- /** @param {string} key */
1656
- getAll(key) {
1657
- return this.#map.get(key) || [];
1658
- }
1659
-
1660
- /** @param {string} key */
1661
- has(key) {
1662
- return this.#map.has(key);
1663
- }
1664
-
1665
- *[Symbol.iterator]() {
1666
- for (const [key, value] of this.#map) {
1667
- for (let i = 0; i < value.length; i += 1) {
1668
- yield [key, value[i]];
1669
- }
1670
- }
1671
- }
1672
-
1673
- *entries() {
1674
- for (const [key, value] of this.#map) {
1675
- for (let i = 0; i < value.length; i += 1) {
1676
- yield [key, value[i]];
1677
- }
1678
- }
1679
- }
1680
-
1681
- *keys() {
1682
- for (const [key] of this.#map) yield key;
1683
- }
1684
-
1685
- *values() {
1686
- for (const [, value] of this.#map) {
1687
- for (let i = 0; i < value.length; i += 1) {
1688
- yield value[i];
1689
- }
1690
- }
1691
- }
1692
- }
1693
-
1694
- /**
1695
- * @param {import('types/app').RawBody} raw
1696
- * @param {import('types/helper').RequestHeaders} headers
1697
- */
1698
- function parse_body(raw, headers) {
1699
- if (!raw) return raw;
1700
-
1701
- const content_type = headers['content-type'];
1702
- const [type, ...directives] = content_type ? content_type.split(/;\s*/) : [];
1703
-
1704
- const text = () => new TextDecoder(headers['content-encoding'] || 'utf-8').decode(raw);
1705
-
1706
- switch (type) {
1707
- case 'text/plain':
1708
- return text();
1709
-
1710
- case 'application/json':
1711
- return JSON.parse(text());
1712
-
1713
- case 'application/x-www-form-urlencoded':
1714
- return get_urlencoded(text());
1715
-
1716
- case 'multipart/form-data': {
1717
- const boundary = directives.find((directive) => directive.startsWith('boundary='));
1718
- if (!boundary) throw new Error('Missing boundary');
1719
- return get_multipart(text(), boundary.slice('boundary='.length));
1720
- }
1721
- default:
1722
- return raw;
1723
- }
1724
- }
1725
-
1726
- /** @param {string} text */
1727
- function get_urlencoded(text) {
1728
- const { data, append } = read_only_form_data();
1729
-
1730
- text
1731
- .replace(/\+/g, ' ')
1732
- .split('&')
1733
- .forEach((str) => {
1734
- const [key, value] = str.split('=');
1735
- append(decodeURIComponent(key), decodeURIComponent(value));
1606
+ return new Response(`Bad request in load function: failed to fetch ${state.fetched}`, {
1607
+ status: 500
1736
1608
  });
1737
-
1738
- return data;
1739
- }
1740
-
1741
- /**
1742
- * @param {string} text
1743
- * @param {string} boundary
1744
- */
1745
- function get_multipart(text, boundary) {
1746
- const parts = text.split(`--${boundary}`);
1747
-
1748
- if (parts[0] !== '' || parts[parts.length - 1].trim() !== '--') {
1749
- throw new Error('Malformed form data');
1750
1609
  }
1751
-
1752
- const { data, append } = read_only_form_data();
1753
-
1754
- parts.slice(1, -1).forEach((part) => {
1755
- const match = /\s*([\s\S]+?)\r\n\r\n([\s\S]*)\s*/.exec(part);
1756
- if (!match) {
1757
- throw new Error('Malformed form data');
1758
- }
1759
- const raw_headers = match[1];
1760
- const body = match[2].trim();
1761
-
1762
- let key;
1763
-
1764
- /** @type {Record<string, string>} */
1765
- const headers = {};
1766
- raw_headers.split('\r\n').forEach((str) => {
1767
- const [raw_header, ...raw_directives] = str.split('; ');
1768
- let [name, value] = raw_header.split(': ');
1769
-
1770
- name = name.toLowerCase();
1771
- headers[name] = value;
1772
-
1773
- /** @type {Record<string, string>} */
1774
- const directives = {};
1775
- raw_directives.forEach((raw_directive) => {
1776
- const [name, value] = raw_directive.split('=');
1777
- directives[name] = JSON.parse(value); // TODO is this right?
1778
- });
1779
-
1780
- if (name === 'content-disposition') {
1781
- if (value !== 'form-data') throw new Error('Malformed form data');
1782
-
1783
- if (directives.filename) {
1784
- // TODO we probably don't want to do this automatically
1785
- throw new Error('File upload is not yet implemented');
1786
- }
1787
-
1788
- if (directives.name) {
1789
- key = directives.name;
1790
- }
1791
- }
1792
- });
1793
-
1794
- if (!key) throw new Error('Malformed form data');
1795
-
1796
- append(key, body);
1797
- });
1798
-
1799
- return data;
1800
1610
  }
1801
1611
 
1802
- /** @type {import('@sveltejs/kit/ssr').Respond} */
1803
- async function respond(incoming, options, state = {}) {
1804
- if (incoming.url.pathname !== '/' && options.trailing_slash !== 'ignore') {
1805
- const has_trailing_slash = incoming.url.pathname.endsWith('/');
1612
+ /** @type {import('types/internal').Respond} */
1613
+ async function respond(request, options, state = {}) {
1614
+ const url = new URL(request.url);
1615
+
1616
+ if (url.pathname !== '/' && options.trailing_slash !== 'ignore') {
1617
+ const has_trailing_slash = url.pathname.endsWith('/');
1806
1618
 
1807
1619
  if (
1808
1620
  (has_trailing_slash && options.trailing_slash === 'never') ||
1809
1621
  (!has_trailing_slash &&
1810
1622
  options.trailing_slash === 'always' &&
1811
- !(incoming.url.pathname.split('/').pop() || '').includes('.'))
1623
+ !(url.pathname.split('/').pop() || '').includes('.'))
1812
1624
  ) {
1813
- incoming.url.pathname = has_trailing_slash
1814
- ? incoming.url.pathname.slice(0, -1)
1815
- : incoming.url.pathname + '/';
1625
+ url.pathname = has_trailing_slash ? url.pathname.slice(0, -1) : url.pathname + '/';
1816
1626
 
1817
- if (incoming.url.search === '?') incoming.url.search = '';
1627
+ if (url.search === '?') url.search = '';
1818
1628
 
1819
- return {
1629
+ return new Response(undefined, {
1820
1630
  status: 301,
1821
1631
  headers: {
1822
- location: incoming.url.pathname + incoming.url.search
1632
+ location: url.pathname + url.search
1823
1633
  }
1824
- };
1634
+ });
1825
1635
  }
1826
1636
  }
1827
1637
 
1828
- const headers = lowercase_keys(incoming.headers);
1829
- const request = {
1830
- ...incoming,
1831
- headers,
1832
- body: parse_body(incoming.rawBody, headers),
1833
- params: {},
1834
- locals: {}
1835
- };
1836
-
1837
1638
  const { parameter, allowed } = options.method_override;
1838
- const method_override = incoming.url.searchParams.get(parameter)?.toUpperCase();
1639
+ const method_override = url.searchParams.get(parameter)?.toUpperCase();
1839
1640
 
1840
1641
  if (method_override) {
1841
- if (request.method.toUpperCase() === 'POST') {
1642
+ if (request.method === 'POST') {
1842
1643
  if (allowed.includes(method_override)) {
1843
- request.method = method_override;
1644
+ request = new Proxy(request, {
1645
+ get: (target, property, _receiver) => {
1646
+ if (property === 'method') return method_override;
1647
+ return Reflect.get(target, property, target);
1648
+ }
1649
+ });
1844
1650
  } else {
1845
1651
  const verb = allowed.length === 0 ? 'enabled' : 'allowed';
1846
1652
  const body = `${parameter}=${method_override} is not ${verb}. See https://kit.svelte.dev/docs#configuration-methodoverride`;
1847
1653
 
1848
- return {
1849
- status: 400,
1850
- headers: {},
1851
- body
1852
- };
1654
+ return new Response(body, {
1655
+ status: 400
1656
+ });
1853
1657
  }
1854
1658
  } else {
1855
1659
  throw new Error(`${parameter}=${method_override} is only allowed with POST requests`);
1856
1660
  }
1857
1661
  }
1858
1662
 
1663
+ /** @type {import('types/hooks').RequestEvent} */
1664
+ const event = {
1665
+ request,
1666
+ url,
1667
+ params: {},
1668
+ locals: {}
1669
+ };
1670
+
1859
1671
  // TODO remove this for 1.0
1860
1672
  /**
1861
1673
  * @param {string} property
1862
1674
  * @param {string} replacement
1675
+ * @param {string} suffix
1863
1676
  */
1864
- const print_error = (property, replacement) => {
1865
- Object.defineProperty(request, property, {
1866
- get: () => {
1867
- throw new Error(`request.${property} has been replaced by request.url.${replacement}`);
1868
- }
1869
- });
1677
+ const removed = (property, replacement, suffix = '') => ({
1678
+ get: () => {
1679
+ throw new Error(`event.${property} has been replaced by event.${replacement}` + suffix);
1680
+ }
1681
+ });
1682
+
1683
+ const details = '. See https://github.com/sveltejs/kit/pull/3384 for details';
1684
+
1685
+ const body_getter = {
1686
+ get: () => {
1687
+ throw new Error(
1688
+ 'To access the request body use the text/json/arrayBuffer/formData methods, e.g. `body = await request.json()`' +
1689
+ details
1690
+ );
1691
+ }
1870
1692
  };
1871
1693
 
1872
- print_error('origin', 'origin');
1873
- print_error('path', 'pathname');
1874
- print_error('query', 'searchParams');
1694
+ Object.defineProperties(event, {
1695
+ method: removed('method', 'request.method', details),
1696
+ headers: removed('headers', 'request.headers', details),
1697
+ origin: removed('origin', 'url.origin'),
1698
+ path: removed('path', 'url.pathname'),
1699
+ query: removed('query', 'url.searchParams'),
1700
+ body: body_getter,
1701
+ rawBody: body_getter
1702
+ });
1875
1703
 
1876
1704
  let ssr = true;
1877
1705
 
1878
1706
  try {
1879
1707
  return await options.hooks.handle({
1880
- request,
1881
- resolve: async (request, opts) => {
1708
+ event,
1709
+ resolve: async (event, opts) => {
1882
1710
  if (opts && 'ssr' in opts) ssr = /** @type {boolean} */ (opts.ssr);
1883
1711
 
1884
1712
  if (state.prerender && state.prerender.fallback) {
1885
1713
  return await render_response({
1886
- url: request.url,
1887
- params: request.params,
1714
+ url: event.url,
1715
+ params: event.params,
1888
1716
  options,
1889
1717
  state,
1890
- $session: await options.hooks.getSession(request),
1718
+ $session: await options.hooks.getSession(event),
1891
1719
  page_config: { router: true, hydrate: true },
1892
1720
  stuff: {},
1893
1721
  status: 200,
@@ -1896,7 +1724,7 @@ async function respond(incoming, options, state = {}) {
1896
1724
  });
1897
1725
  }
1898
1726
 
1899
- let decoded = decodeURI(request.url.pathname);
1727
+ let decoded = decodeURI(event.url.pathname);
1900
1728
 
1901
1729
  if (options.paths.base) {
1902
1730
  if (!decoded.startsWith(options.paths.base)) return;
@@ -1909,47 +1737,40 @@ async function respond(incoming, options, state = {}) {
1909
1737
 
1910
1738
  const response =
1911
1739
  route.type === 'endpoint'
1912
- ? await render_endpoint(request, route, match)
1913
- : await render_page(request, route, match, options, state, ssr);
1740
+ ? await render_endpoint(event, route, match)
1741
+ : await render_page(event, route, match, options, state, ssr);
1914
1742
 
1915
1743
  if (response) {
1916
- // inject ETags for 200 responses, if the endpoint
1917
- // doesn't have its own ETag handling
1918
- if (response.status === 200 && !response.headers.etag) {
1919
- const cache_control = get_single_valued_header(response.headers, 'cache-control');
1920
- if (!cache_control || !/(no-store|immutable)/.test(cache_control)) {
1921
- let if_none_match_value = request.headers['if-none-match'];
1922
- // ignore W/ prefix https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives
1923
- if (if_none_match_value?.startsWith('W/"')) {
1924
- if_none_match_value = if_none_match_value.substring(2);
1925
- }
1744
+ // respond with 304 if etag matches
1745
+ if (response.status === 200 && response.headers.has('etag')) {
1746
+ let if_none_match_value = request.headers.get('if-none-match');
1926
1747
 
1927
- const etag = `"${hash(response.body || '')}"`;
1928
-
1929
- if (if_none_match_value === etag) {
1930
- /** @type {import('types/helper').ResponseHeaders} */
1931
- const headers = { etag };
1932
-
1933
- // https://datatracker.ietf.org/doc/html/rfc7232#section-4.1
1934
- for (const key of [
1935
- 'cache-control',
1936
- 'content-location',
1937
- 'date',
1938
- 'expires',
1939
- 'vary'
1940
- ]) {
1941
- if (key in response.headers) {
1942
- headers[key] = /** @type {string} */ (response.headers[key]);
1943
- }
1944
- }
1748
+ // ignore W/ prefix https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives
1749
+ if (if_none_match_value?.startsWith('W/"')) {
1750
+ if_none_match_value = if_none_match_value.substring(2);
1751
+ }
1945
1752
 
1946
- return {
1947
- status: 304,
1948
- headers
1949
- };
1753
+ const etag = /** @type {string} */ (response.headers.get('etag'));
1754
+
1755
+ if (if_none_match_value === etag) {
1756
+ const headers = new Headers({ etag });
1757
+
1758
+ // https://datatracker.ietf.org/doc/html/rfc7232#section-4.1
1759
+ for (const key of [
1760
+ 'cache-control',
1761
+ 'content-location',
1762
+ 'date',
1763
+ 'expires',
1764
+ 'vary'
1765
+ ]) {
1766
+ const value = response.headers.get(key);
1767
+ if (value) headers.set(key, value);
1950
1768
  }
1951
1769
 
1952
- response.headers['etag'] = etag;
1770
+ return new Response(undefined, {
1771
+ status: 304,
1772
+ headers
1773
+ });
1953
1774
  }
1954
1775
  }
1955
1776
 
@@ -1960,28 +1781,34 @@ async function respond(incoming, options, state = {}) {
1960
1781
  // if this request came direct from the user, rather than
1961
1782
  // via a `fetch` in a `load`, render a 404 page
1962
1783
  if (!state.initiator) {
1963
- const $session = await options.hooks.getSession(request);
1784
+ const $session = await options.hooks.getSession(event);
1964
1785
  return await respond_with_error({
1965
- request,
1786
+ event,
1966
1787
  options,
1967
1788
  state,
1968
1789
  $session,
1969
1790
  status: 404,
1970
- error: new Error(`Not found: ${request.url.pathname}`),
1791
+ error: new Error(`Not found: ${event.url.pathname}`),
1971
1792
  ssr
1972
1793
  });
1973
1794
  }
1795
+ },
1796
+
1797
+ // TODO remove for 1.0
1798
+ // @ts-expect-error
1799
+ get request() {
1800
+ throw new Error('request in handle has been replaced with event' + details);
1974
1801
  }
1975
1802
  });
1976
1803
  } catch (/** @type {unknown} */ e) {
1977
1804
  const error = coalesce_to_error(e);
1978
1805
 
1979
- options.handle_error(error, request);
1806
+ options.handle_error(error, event);
1980
1807
 
1981
1808
  try {
1982
- const $session = await options.hooks.getSession(request);
1809
+ const $session = await options.hooks.getSession(event);
1983
1810
  return await respond_with_error({
1984
- request,
1811
+ event,
1985
1812
  options,
1986
1813
  state,
1987
1814
  $session,
@@ -1992,11 +1819,9 @@ async function respond(incoming, options, state = {}) {
1992
1819
  } catch (/** @type {unknown} */ e) {
1993
1820
  const error = coalesce_to_error(e);
1994
1821
 
1995
- return {
1996
- status: 500,
1997
- headers: {},
1998
- body: options.dev ? error.stack : error.message
1999
- };
1822
+ return new Response(options.dev ? error.stack : error.message, {
1823
+ status: 500
1824
+ });
2000
1825
  }
2001
1826
  }
2002
1827
  }