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

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
  '"': '\\"',
@@ -743,32 +758,29 @@ async function render_response({
743
758
  }
744
759
  }
745
760
 
746
- /** @type {import('types/helper').ResponseHeaders} */
747
- const headers = {
748
- 'content-type': 'text/html'
749
- };
761
+ const segments = url.pathname.slice(options.paths.base.length).split('/').slice(2);
762
+ const assets =
763
+ options.paths.assets || (segments.length > 0 ? segments.map(() => '..').join('/') : '.');
764
+
765
+ const html = options.template({ head, body, assets });
766
+
767
+ const headers = new Headers({
768
+ 'content-type': 'text/html',
769
+ etag: `"${hash(html)}"`
770
+ });
750
771
 
751
772
  if (maxage) {
752
- headers['cache-control'] = `${is_private ? 'private' : 'public'}, max-age=${maxage}`;
773
+ headers.set('cache-control', `${is_private ? 'private' : 'public'}, max-age=${maxage}`);
753
774
  }
754
775
 
755
776
  if (!options.floc) {
756
- headers['permissions-policy'] = 'interest-cohort=()';
777
+ headers.set('permissions-policy', 'interest-cohort=()');
757
778
  }
758
779
 
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 {
780
+ return new Response(html, {
764
781
  status,
765
- headers,
766
- body: options.template({
767
- head,
768
- body,
769
- assets
770
- })
771
- };
782
+ headers
783
+ });
772
784
  }
773
785
 
774
786
  /**
@@ -906,7 +918,7 @@ function is_root_relative(path) {
906
918
 
907
919
  /**
908
920
  * @param {{
909
- * request: import('types/hooks').ServerRequest;
921
+ * event: import('types/hooks').RequestEvent;
910
922
  * options: import('types/internal').SSRRenderOptions;
911
923
  * state: import('types/internal').SSRRenderState;
912
924
  * route: import('types/internal').SSRPage | null;
@@ -922,7 +934,7 @@ function is_root_relative(path) {
922
934
  * @returns {Promise<import('./types').Loaded | undefined>} undefined for fallthrough
923
935
  */
924
936
  async function load_node({
925
- request,
937
+ event,
926
938
  options,
927
939
  state,
928
940
  route,
@@ -993,7 +1005,7 @@ async function load_node({
993
1005
 
994
1006
  opts.headers = new Headers(opts.headers);
995
1007
 
996
- const resolved = resolve(request.url.pathname, requested.split('?')[0]);
1008
+ const resolved = resolve(event.url.pathname, requested.split('?')[0]);
997
1009
 
998
1010
  let response;
999
1011
 
@@ -1029,12 +1041,15 @@ async function load_node({
1029
1041
  if (opts.credentials !== 'omit') {
1030
1042
  uses_credentials = true;
1031
1043
 
1032
- if (request.headers.cookie) {
1033
- opts.headers.set('cookie', request.headers.cookie);
1044
+ const cookie = event.request.headers.get('cookie');
1045
+ const authorization = event.request.headers.get('authorization');
1046
+
1047
+ if (cookie) {
1048
+ opts.headers.set('cookie', cookie);
1034
1049
  }
1035
1050
 
1036
- if (request.headers.authorization && !opts.headers.has('authorization')) {
1037
- opts.headers.set('authorization', request.headers.authorization);
1051
+ if (authorization && !opts.headers.has('authorization')) {
1052
+ opts.headers.set('authorization', authorization);
1038
1053
  }
1039
1054
  }
1040
1055
 
@@ -1047,12 +1062,7 @@ async function load_node({
1047
1062
  }
1048
1063
 
1049
1064
  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
- },
1065
+ new Request(new URL(requested, event.url).href, opts),
1056
1066
  options,
1057
1067
  {
1058
1068
  fetched: requested,
@@ -1065,17 +1075,11 @@ async function load_node({
1065
1075
  state.prerender.dependencies.set(relative, rendered);
1066
1076
  }
1067
1077
 
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
- });
1078
+ response = rendered;
1075
1079
  } else {
1076
1080
  // we can't load the endpoint from our own manifest,
1077
1081
  // so we need to make an actual HTTP request
1078
- return fetch(new URL(requested, request.url).href, {
1082
+ return fetch(new URL(requested, event.url).href, {
1079
1083
  method: opts.method || 'GET',
1080
1084
  headers: opts.headers
1081
1085
  });
@@ -1098,11 +1102,13 @@ async function load_node({
1098
1102
  // ports do not affect the resolution
1099
1103
  // leading dot prevents mydomain.com matching domain.com
1100
1104
  if (
1101
- `.${new URL(requested).hostname}`.endsWith(`.${request.url.hostname}`) &&
1105
+ `.${new URL(requested).hostname}`.endsWith(`.${event.url.hostname}`) &&
1102
1106
  opts.credentials !== 'omit'
1103
1107
  ) {
1104
1108
  uses_credentials = true;
1105
- opts.headers.set('cookie', request.headers.cookie);
1109
+
1110
+ const cookie = event.request.headers.get('cookie');
1111
+ if (cookie) opts.headers.set('cookie', cookie);
1106
1112
  }
1107
1113
 
1108
1114
  const external_request = new Request(requested, /** @type {RequestInit} */ (opts));
@@ -1211,7 +1217,7 @@ async function load_node({
1211
1217
 
1212
1218
  /**
1213
1219
  * @param {{
1214
- * request: import('types/hooks').ServerRequest;
1220
+ * event: import('types/hooks').RequestEvent;
1215
1221
  * options: SSRRenderOptions;
1216
1222
  * state: SSRRenderState;
1217
1223
  * $session: any;
@@ -1220,15 +1226,7 @@ async function load_node({
1220
1226
  * ssr: boolean;
1221
1227
  * }} opts
1222
1228
  */
1223
- async function respond_with_error({
1224
- request,
1225
- options,
1226
- state,
1227
- $session,
1228
- status,
1229
- error,
1230
- ssr
1231
- }) {
1229
+ async function respond_with_error({ event, options, state, $session, status, error, ssr }) {
1232
1230
  try {
1233
1231
  const default_layout = await options.manifest._.nodes[0](); // 0 is always the root layout
1234
1232
  const default_error = await options.manifest._.nodes[1](); // 1 is always the root error
@@ -1238,11 +1236,11 @@ async function respond_with_error({
1238
1236
 
1239
1237
  const layout_loaded = /** @type {Loaded} */ (
1240
1238
  await load_node({
1241
- request,
1239
+ event,
1242
1240
  options,
1243
1241
  state,
1244
1242
  route: null,
1245
- url: request.url, // TODO this is redundant, no?
1243
+ url: event.url, // TODO this is redundant, no?
1246
1244
  params,
1247
1245
  node: default_layout,
1248
1246
  $session,
@@ -1253,11 +1251,11 @@ async function respond_with_error({
1253
1251
 
1254
1252
  const error_loaded = /** @type {Loaded} */ (
1255
1253
  await load_node({
1256
- request,
1254
+ event,
1257
1255
  options,
1258
1256
  state,
1259
1257
  route: null,
1260
- url: request.url,
1258
+ url: event.url,
1261
1259
  params,
1262
1260
  node: default_error,
1263
1261
  $session,
@@ -1280,26 +1278,23 @@ async function respond_with_error({
1280
1278
  status,
1281
1279
  error,
1282
1280
  branch: [layout_loaded, error_loaded],
1283
- url: request.url,
1281
+ url: event.url,
1284
1282
  params,
1285
1283
  ssr
1286
1284
  });
1287
1285
  } catch (err) {
1288
1286
  const error = coalesce_to_error(err);
1289
1287
 
1290
- options.handle_error(error, request);
1288
+ options.handle_error(error, event);
1291
1289
 
1292
- return {
1293
- status: 500,
1294
- headers: {},
1295
- body: error.stack
1296
- };
1290
+ return new Response(error.stack, {
1291
+ status: 500
1292
+ });
1297
1293
  }
1298
1294
  }
1299
1295
 
1300
1296
  /**
1301
1297
  * @typedef {import('./types.js').Loaded} Loaded
1302
- * @typedef {import('types/hooks').ServerResponse} ServerResponse
1303
1298
  * @typedef {import('types/internal').SSRNode} SSRNode
1304
1299
  * @typedef {import('types/internal').SSRRenderOptions} SSRRenderOptions
1305
1300
  * @typedef {import('types/internal').SSRRenderState} SSRRenderState
@@ -1307,7 +1302,7 @@ async function respond_with_error({
1307
1302
 
1308
1303
  /**
1309
1304
  * @param {{
1310
- * request: import('types/hooks').ServerRequest;
1305
+ * event: import('types/hooks').RequestEvent;
1311
1306
  * options: SSRRenderOptions;
1312
1307
  * state: SSRRenderState;
1313
1308
  * $session: any;
@@ -1315,10 +1310,10 @@ async function respond_with_error({
1315
1310
  * params: Record<string, string>;
1316
1311
  * ssr: boolean;
1317
1312
  * }} opts
1318
- * @returns {Promise<ServerResponse | undefined>}
1313
+ * @returns {Promise<Response | undefined>}
1319
1314
  */
1320
1315
  async function respond$1(opts) {
1321
- const { request, options, state, $session, route, ssr } = opts;
1316
+ const { event, options, state, $session, route, ssr } = opts;
1322
1317
 
1323
1318
  /** @type {Array<SSRNode | undefined>} */
1324
1319
  let nodes;
@@ -1332,7 +1327,7 @@ async function respond$1(opts) {
1332
1327
  router: true
1333
1328
  },
1334
1329
  status: 200,
1335
- url: request.url,
1330
+ url: event.url,
1336
1331
  stuff: {}
1337
1332
  });
1338
1333
  }
@@ -1344,10 +1339,10 @@ async function respond$1(opts) {
1344
1339
  } catch (err) {
1345
1340
  const error = coalesce_to_error(err);
1346
1341
 
1347
- options.handle_error(error, request);
1342
+ options.handle_error(error, event);
1348
1343
 
1349
1344
  return await respond_with_error({
1350
- request,
1345
+ event,
1351
1346
  options,
1352
1347
  state,
1353
1348
  $session,
@@ -1365,10 +1360,9 @@ async function respond$1(opts) {
1365
1360
  if (!leaf.prerender && state.prerender && !state.prerender.all) {
1366
1361
  // if the page has `export const prerender = true`, continue,
1367
1362
  // otherwise bail out at this point
1368
- return {
1369
- status: 204,
1370
- headers: {}
1371
- };
1363
+ return new Response(undefined, {
1364
+ status: 204
1365
+ });
1372
1366
  }
1373
1367
 
1374
1368
  /** @type {Array<Loaded>} */
@@ -1396,7 +1390,7 @@ async function respond$1(opts) {
1396
1390
  try {
1397
1391
  loaded = await load_node({
1398
1392
  ...opts,
1399
- url: request.url,
1393
+ url: event.url,
1400
1394
  node,
1401
1395
  stuff,
1402
1396
  is_error: false
@@ -1408,12 +1402,12 @@ async function respond$1(opts) {
1408
1402
 
1409
1403
  if (loaded.loaded.redirect) {
1410
1404
  return with_cookies(
1411
- {
1405
+ new Response(undefined, {
1412
1406
  status: loaded.loaded.status,
1413
1407
  headers: {
1414
1408
  location: encodeURI(loaded.loaded.redirect)
1415
1409
  }
1416
- },
1410
+ }),
1417
1411
  set_cookie_headers
1418
1412
  );
1419
1413
  }
@@ -1424,7 +1418,7 @@ async function respond$1(opts) {
1424
1418
  } catch (err) {
1425
1419
  const e = coalesce_to_error(err);
1426
1420
 
1427
- options.handle_error(e, request);
1421
+ options.handle_error(e, event);
1428
1422
 
1429
1423
  status = 500;
1430
1424
  error = e;
@@ -1450,7 +1444,7 @@ async function respond$1(opts) {
1450
1444
  const error_loaded = /** @type {import('./types').Loaded} */ (
1451
1445
  await load_node({
1452
1446
  ...opts,
1453
- url: request.url,
1447
+ url: event.url,
1454
1448
  node: error_node,
1455
1449
  stuff: node_loaded.stuff,
1456
1450
  is_error: true,
@@ -1470,7 +1464,7 @@ async function respond$1(opts) {
1470
1464
  } catch (err) {
1471
1465
  const e = coalesce_to_error(err);
1472
1466
 
1473
- options.handle_error(e, request);
1467
+ options.handle_error(e, event);
1474
1468
 
1475
1469
  continue;
1476
1470
  }
@@ -1482,7 +1476,7 @@ async function respond$1(opts) {
1482
1476
  // for now just return regular error page
1483
1477
  return with_cookies(
1484
1478
  await respond_with_error({
1485
- request,
1479
+ event,
1486
1480
  options,
1487
1481
  state,
1488
1482
  $session,
@@ -1509,7 +1503,7 @@ async function respond$1(opts) {
1509
1503
  await render_response({
1510
1504
  ...opts,
1511
1505
  stuff,
1512
- url: request.url,
1506
+ url: event.url,
1513
1507
  page_config,
1514
1508
  status,
1515
1509
  error,
@@ -1520,7 +1514,7 @@ async function respond$1(opts) {
1520
1514
  } catch (err) {
1521
1515
  const error = coalesce_to_error(err);
1522
1516
 
1523
- options.handle_error(error, request);
1517
+ options.handle_error(error, event);
1524
1518
 
1525
1519
  return with_cookies(
1526
1520
  await respond_with_error({
@@ -1552,41 +1546,41 @@ function get_page_config(leaf, options) {
1552
1546
  }
1553
1547
 
1554
1548
  /**
1555
- * @param {ServerResponse} response
1549
+ * @param {Response} response
1556
1550
  * @param {string[]} set_cookie_headers
1557
1551
  */
1558
1552
  function with_cookies(response, set_cookie_headers) {
1559
1553
  if (set_cookie_headers.length) {
1560
- response.headers['set-cookie'] = set_cookie_headers;
1554
+ set_cookie_headers.forEach((value) => {
1555
+ response.headers.append('set-cookie', value);
1556
+ });
1561
1557
  }
1562
1558
  return response;
1563
1559
  }
1564
1560
 
1565
1561
  /**
1566
- * @param {import('types/hooks').ServerRequest} request
1562
+ * @param {import('types/hooks').RequestEvent} event
1567
1563
  * @param {import('types/internal').SSRPage} route
1568
1564
  * @param {RegExpExecArray} match
1569
1565
  * @param {import('types/internal').SSRRenderOptions} options
1570
1566
  * @param {import('types/internal').SSRRenderState} state
1571
1567
  * @param {boolean} ssr
1572
- * @returns {Promise<import('types/hooks').ServerResponse | undefined>}
1568
+ * @returns {Promise<Response | undefined>}
1573
1569
  */
1574
- async function render_page(request, route, match, options, state, ssr) {
1570
+ async function render_page(event, route, match, options, state, ssr) {
1575
1571
  if (state.initiator === route) {
1576
1572
  // infinite request cycle detected
1577
- return {
1578
- status: 404,
1579
- headers: {},
1580
- body: `Not found: ${request.url.pathname}`
1581
- };
1573
+ return new Response(`Not found: ${event.url.pathname}`, {
1574
+ status: 404
1575
+ });
1582
1576
  }
1583
1577
 
1584
1578
  const params = route.params ? decode_params(route.params(match)) : {};
1585
1579
 
1586
- const $session = await options.hooks.getSession(request);
1580
+ const $session = await options.hooks.getSession(event);
1587
1581
 
1588
1582
  const response = await respond$1({
1589
- request,
1583
+ event,
1590
1584
  options,
1591
1585
  state,
1592
1586
  $session,
@@ -1604,290 +1598,119 @@ async function render_page(request, route, match, options, state, ssr) {
1604
1598
  // rather than render the error page — which could lead to an
1605
1599
  // infinite loop, if the `load` belonged to the root layout,
1606
1600
  // 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));
1601
+ return new Response(`Bad request in load function: failed to fetch ${state.fetched}`, {
1602
+ status: 500
1736
1603
  });
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
1604
  }
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
1605
  }
1801
1606
 
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('/');
1607
+ /** @type {import('types/internal').Respond} */
1608
+ async function respond(request, options, state = {}) {
1609
+ const url = new URL(request.url);
1610
+
1611
+ if (url.pathname !== '/' && options.trailing_slash !== 'ignore') {
1612
+ const has_trailing_slash = url.pathname.endsWith('/');
1806
1613
 
1807
1614
  if (
1808
1615
  (has_trailing_slash && options.trailing_slash === 'never') ||
1809
1616
  (!has_trailing_slash &&
1810
1617
  options.trailing_slash === 'always' &&
1811
- !(incoming.url.pathname.split('/').pop() || '').includes('.'))
1618
+ !(url.pathname.split('/').pop() || '').includes('.'))
1812
1619
  ) {
1813
- incoming.url.pathname = has_trailing_slash
1814
- ? incoming.url.pathname.slice(0, -1)
1815
- : incoming.url.pathname + '/';
1620
+ url.pathname = has_trailing_slash ? url.pathname.slice(0, -1) : url.pathname + '/';
1816
1621
 
1817
- if (incoming.url.search === '?') incoming.url.search = '';
1622
+ if (url.search === '?') url.search = '';
1818
1623
 
1819
- return {
1624
+ return new Response(undefined, {
1820
1625
  status: 301,
1821
1626
  headers: {
1822
- location: incoming.url.pathname + incoming.url.search
1627
+ location: url.pathname + url.search
1823
1628
  }
1824
- };
1629
+ });
1825
1630
  }
1826
1631
  }
1827
1632
 
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
1633
  const { parameter, allowed } = options.method_override;
1838
- const method_override = incoming.url.searchParams.get(parameter)?.toUpperCase();
1634
+ const method_override = url.searchParams.get(parameter)?.toUpperCase();
1839
1635
 
1840
1636
  if (method_override) {
1841
- if (request.method.toUpperCase() === 'POST') {
1637
+ if (request.method === 'POST') {
1842
1638
  if (allowed.includes(method_override)) {
1843
- request.method = method_override;
1639
+ request = new Proxy(request, {
1640
+ get: (target, property, _receiver) => {
1641
+ if (property === 'method') return method_override;
1642
+ return Reflect.get(target, property, target);
1643
+ }
1644
+ });
1844
1645
  } else {
1845
1646
  const verb = allowed.length === 0 ? 'enabled' : 'allowed';
1846
1647
  const body = `${parameter}=${method_override} is not ${verb}. See https://kit.svelte.dev/docs#configuration-methodoverride`;
1847
1648
 
1848
- return {
1849
- status: 400,
1850
- headers: {},
1851
- body
1852
- };
1649
+ return new Response(body, {
1650
+ status: 400
1651
+ });
1853
1652
  }
1854
1653
  } else {
1855
1654
  throw new Error(`${parameter}=${method_override} is only allowed with POST requests`);
1856
1655
  }
1857
1656
  }
1858
1657
 
1658
+ /** @type {import('types/hooks').RequestEvent} */
1659
+ const event = {
1660
+ request,
1661
+ url,
1662
+ params: {},
1663
+ locals: {}
1664
+ };
1665
+
1859
1666
  // TODO remove this for 1.0
1860
1667
  /**
1861
1668
  * @param {string} property
1862
1669
  * @param {string} replacement
1670
+ * @param {string} suffix
1863
1671
  */
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
- });
1672
+ const removed = (property, replacement, suffix = '') => ({
1673
+ get: () => {
1674
+ throw new Error(`event.${property} has been replaced by event.${replacement}` + suffix);
1675
+ }
1676
+ });
1677
+
1678
+ const details = '. See https://github.com/sveltejs/kit/pull/3384 for details';
1679
+
1680
+ const body_getter = {
1681
+ get: () => {
1682
+ throw new Error(
1683
+ 'To access the request body use the text/json/arrayBuffer/formData methods, e.g. `body = await request.json()`' +
1684
+ details
1685
+ );
1686
+ }
1870
1687
  };
1871
1688
 
1872
- print_error('origin', 'origin');
1873
- print_error('path', 'pathname');
1874
- print_error('query', 'searchParams');
1689
+ Object.defineProperties(event, {
1690
+ method: removed('method', 'request.method', details),
1691
+ headers: removed('headers', 'request.headers', details),
1692
+ origin: removed('origin', 'url.origin'),
1693
+ path: removed('path', 'url.pathname'),
1694
+ query: removed('query', 'url.searchParams'),
1695
+ body: body_getter,
1696
+ rawBody: body_getter
1697
+ });
1875
1698
 
1876
1699
  let ssr = true;
1877
1700
 
1878
1701
  try {
1879
1702
  return await options.hooks.handle({
1880
- request,
1881
- resolve: async (request, opts) => {
1703
+ event,
1704
+ resolve: async (event, opts) => {
1882
1705
  if (opts && 'ssr' in opts) ssr = /** @type {boolean} */ (opts.ssr);
1883
1706
 
1884
1707
  if (state.prerender && state.prerender.fallback) {
1885
1708
  return await render_response({
1886
- url: request.url,
1887
- params: request.params,
1709
+ url: event.url,
1710
+ params: event.params,
1888
1711
  options,
1889
1712
  state,
1890
- $session: await options.hooks.getSession(request),
1713
+ $session: await options.hooks.getSession(event),
1891
1714
  page_config: { router: true, hydrate: true },
1892
1715
  stuff: {},
1893
1716
  status: 200,
@@ -1896,7 +1719,7 @@ async function respond(incoming, options, state = {}) {
1896
1719
  });
1897
1720
  }
1898
1721
 
1899
- let decoded = decodeURI(request.url.pathname);
1722
+ let decoded = decodeURI(event.url.pathname);
1900
1723
 
1901
1724
  if (options.paths.base) {
1902
1725
  if (!decoded.startsWith(options.paths.base)) return;
@@ -1909,47 +1732,40 @@ async function respond(incoming, options, state = {}) {
1909
1732
 
1910
1733
  const response =
1911
1734
  route.type === 'endpoint'
1912
- ? await render_endpoint(request, route, match)
1913
- : await render_page(request, route, match, options, state, ssr);
1735
+ ? await render_endpoint(event, route, match)
1736
+ : await render_page(event, route, match, options, state, ssr);
1914
1737
 
1915
1738
  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
- }
1739
+ // respond with 304 if etag matches
1740
+ if (response.status === 200 && response.headers.has('etag')) {
1741
+ let if_none_match_value = request.headers.get('if-none-match');
1926
1742
 
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
- }
1743
+ // ignore W/ prefix https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives
1744
+ if (if_none_match_value?.startsWith('W/"')) {
1745
+ if_none_match_value = if_none_match_value.substring(2);
1746
+ }
1945
1747
 
1946
- return {
1947
- status: 304,
1948
- headers
1949
- };
1748
+ const etag = /** @type {string} */ (response.headers.get('etag'));
1749
+
1750
+ if (if_none_match_value === etag) {
1751
+ const headers = new Headers({ etag });
1752
+
1753
+ // https://datatracker.ietf.org/doc/html/rfc7232#section-4.1
1754
+ for (const key of [
1755
+ 'cache-control',
1756
+ 'content-location',
1757
+ 'date',
1758
+ 'expires',
1759
+ 'vary'
1760
+ ]) {
1761
+ const value = response.headers.get(key);
1762
+ if (value) headers.set(key, value);
1950
1763
  }
1951
1764
 
1952
- response.headers['etag'] = etag;
1765
+ return new Response(undefined, {
1766
+ status: 304,
1767
+ headers
1768
+ });
1953
1769
  }
1954
1770
  }
1955
1771
 
@@ -1960,28 +1776,34 @@ async function respond(incoming, options, state = {}) {
1960
1776
  // if this request came direct from the user, rather than
1961
1777
  // via a `fetch` in a `load`, render a 404 page
1962
1778
  if (!state.initiator) {
1963
- const $session = await options.hooks.getSession(request);
1779
+ const $session = await options.hooks.getSession(event);
1964
1780
  return await respond_with_error({
1965
- request,
1781
+ event,
1966
1782
  options,
1967
1783
  state,
1968
1784
  $session,
1969
1785
  status: 404,
1970
- error: new Error(`Not found: ${request.url.pathname}`),
1786
+ error: new Error(`Not found: ${event.url.pathname}`),
1971
1787
  ssr
1972
1788
  });
1973
1789
  }
1790
+ },
1791
+
1792
+ // TODO remove for 1.0
1793
+ // @ts-expect-error
1794
+ get request() {
1795
+ throw new Error('request in handle has been replaced with event' + details);
1974
1796
  }
1975
1797
  });
1976
1798
  } catch (/** @type {unknown} */ e) {
1977
1799
  const error = coalesce_to_error(e);
1978
1800
 
1979
- options.handle_error(error, request);
1801
+ options.handle_error(error, event);
1980
1802
 
1981
1803
  try {
1982
- const $session = await options.hooks.getSession(request);
1804
+ const $session = await options.hooks.getSession(event);
1983
1805
  return await respond_with_error({
1984
- request,
1806
+ event,
1985
1807
  options,
1986
1808
  state,
1987
1809
  $session,
@@ -1992,11 +1814,9 @@ async function respond(incoming, options, state = {}) {
1992
1814
  } catch (/** @type {unknown} */ e) {
1993
1815
  const error = coalesce_to_error(e);
1994
1816
 
1995
- return {
1996
- status: 500,
1997
- headers: {},
1998
- body: options.dev ? error.stack : error.message
1999
- };
1817
+ return new Response(options.dev ? error.stack : error.message, {
1818
+ status: 500
1819
+ });
2000
1820
  }
2001
1821
  }
2002
1822
  }