@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.
@@ -5,11 +5,11 @@ import { c as create_manifest_data, a as create_app, d as deep_merge } from './i
5
5
  import { c as coalesce_to_error, S as SVELTE_KIT_ASSETS, r as resolve_entry, $, a as SVELTE_KIT, b as runtime, l as load_template, g as get_mime_lookup, d as copy_assets, e as get_aliases, p as print_config_conflicts } from '../cli.js';
6
6
  import fs__default from 'fs';
7
7
  import { URL as URL$1 } from 'url';
8
- import { s as sirv } from './build.js';
9
- import { g as get_single_valued_header, r as resolve, i as is_root_relative } from './url.js';
8
+ import { t as to_headers, s as sirv } from './http.js';
10
9
  import { s } from './misc.js';
10
+ import { r as resolve, i as is_root_relative } from './url.js';
11
11
  import { __fetch_polyfill } from '../install-fetch.js';
12
- import { getRawBody } from '../node.js';
12
+ import { getRawBody, setResponse } from '../node.js';
13
13
  import 'sade';
14
14
  import 'child_process';
15
15
  import 'net';
@@ -21,19 +21,27 @@ import 'node:zlib';
21
21
  import 'node:stream';
22
22
  import 'node:util';
23
23
  import 'node:url';
24
+ import 'stream';
24
25
 
25
- /** @param {Record<string, any>} obj */
26
- function lowercase_keys(obj) {
27
- /** @type {Record<string, any>} */
28
- const clone = {};
26
+ /**
27
+ * Hash using djb2
28
+ * @param {import('types/hooks').StrictBody} value
29
+ */
30
+ function hash(value) {
31
+ let hash = 5381;
32
+ let i = value.length;
29
33
 
30
- for (const key in obj) {
31
- clone[key.toLowerCase()] = obj[key];
34
+ if (typeof value === 'string') {
35
+ while (i) hash = (hash * 33) ^ value.charCodeAt(--i);
36
+ } else {
37
+ while (i) hash = (hash * 33) ^ value[--i];
32
38
  }
33
39
 
34
- return clone;
40
+ return (hash >>> 0).toString(36);
35
41
  }
36
42
 
43
+ /** @param {Record<string, any>} obj */
44
+
37
45
  /** @param {Record<string, string>} params */
38
46
  function decode_params(params) {
39
47
  for (const key in params) {
@@ -58,11 +66,9 @@ function decode_params(params) {
58
66
 
59
67
  /** @param {string} body */
60
68
  function error(body) {
61
- return {
62
- status: 500,
63
- body,
64
- headers: {}
65
- };
69
+ return new Response(body, {
70
+ status: 500
71
+ });
66
72
  }
67
73
 
68
74
  /** @param {unknown} s */
@@ -91,16 +97,16 @@ function is_text(content_type) {
91
97
  }
92
98
 
93
99
  /**
94
- * @param {import('types/hooks').ServerRequest} request
100
+ * @param {import('types/hooks').RequestEvent} event
95
101
  * @param {import('types/internal').SSREndpoint} route
96
102
  * @param {RegExpExecArray} match
97
- * @returns {Promise<import('types/hooks').ServerResponse | undefined>}
103
+ * @returns {Promise<Response | undefined>}
98
104
  */
99
- async function render_endpoint(request, route, match) {
105
+ async function render_endpoint(event, route, match) {
100
106
  const mod = await route.load();
101
107
 
102
108
  /** @type {import('types/endpoint').RequestHandler} */
103
- const handler = mod[request.method.toLowerCase().replace('delete', 'del')]; // 'delete' is a reserved word
109
+ const handler = mod[event.request.method.toLowerCase().replace('delete', 'del')]; // 'delete' is a reserved word
104
110
 
105
111
  if (!handler) {
106
112
  return;
@@ -109,10 +115,10 @@ async function render_endpoint(request, route, match) {
109
115
  // we're mutating `request` so that we don't have to do { ...request, params }
110
116
  // on the next line, since that breaks the getters that replace path, query and
111
117
  // origin. We could revert that once we remove the getters
112
- request.params = route.params ? decode_params(route.params(match)) : {};
118
+ event.params = route.params ? decode_params(route.params(match)) : {};
113
119
 
114
- const response = await handler(request);
115
- const preface = `Invalid response from route ${request.url.pathname}`;
120
+ const response = await handler(event);
121
+ const preface = `Invalid response from route ${event.url.pathname}`;
116
122
 
117
123
  if (typeof response !== 'object') {
118
124
  return error(`${preface}: expected an object, got ${typeof response}`);
@@ -122,10 +128,11 @@ async function render_endpoint(request, route, match) {
122
128
  return;
123
129
  }
124
130
 
125
- let { status = 200, body, headers = {} } = response;
131
+ const { status = 200, body = {} } = response;
132
+ const headers =
133
+ response.headers instanceof Headers ? response.headers : to_headers(response.headers);
126
134
 
127
- headers = lowercase_keys(headers);
128
- const type = get_single_valued_header(headers, 'content-type');
135
+ const type = headers.get('content-type');
129
136
 
130
137
  if (!is_text(type) && !(body instanceof Uint8Array || is_string(body))) {
131
138
  return error(
@@ -136,19 +143,45 @@ async function render_endpoint(request, route, match) {
136
143
  /** @type {import('types/hooks').StrictBody} */
137
144
  let normalized_body;
138
145
 
139
- // ensure the body is an object
140
- if (
141
- (typeof body === 'object' || typeof body === 'undefined') &&
142
- !(body instanceof Uint8Array) &&
143
- (!type || type.startsWith('application/json'))
144
- ) {
145
- headers = { ...headers, 'content-type': 'application/json; charset=utf-8' };
146
- normalized_body = JSON.stringify(typeof body === 'undefined' ? {} : body);
146
+ if (is_pojo(body) && (!type || type.startsWith('application/json'))) {
147
+ headers.set('content-type', 'application/json; charset=utf-8');
148
+ normalized_body = JSON.stringify(body);
147
149
  } else {
148
150
  normalized_body = /** @type {import('types/hooks').StrictBody} */ (body);
149
151
  }
150
152
 
151
- return { status, body: normalized_body, headers };
153
+ if (
154
+ (typeof normalized_body === 'string' || normalized_body instanceof Uint8Array) &&
155
+ !headers.has('etag')
156
+ ) {
157
+ const cache_control = headers.get('cache-control');
158
+ if (!cache_control || !/(no-store|immutable)/.test(cache_control)) {
159
+ headers.set('etag', `"${hash(normalized_body)}"`);
160
+ }
161
+ }
162
+
163
+ return new Response(normalized_body, {
164
+ status,
165
+ headers
166
+ });
167
+ }
168
+
169
+ /** @param {any} body */
170
+ function is_pojo(body) {
171
+ if (typeof body !== 'object') return false;
172
+
173
+ if (body) {
174
+ if (body instanceof Uint8Array) return false;
175
+
176
+ // body could be a node Readable, but we don't want to import
177
+ // node built-ins, so we use duck typing
178
+ if (body._readableState && body._writableState && body._events) return false;
179
+
180
+ // similarly, it could be a web ReadableStream
181
+ if (body[Symbol.toStringTag] === 'ReadableStream') return false;
182
+ }
183
+
184
+ return true;
152
185
  }
153
186
 
154
187
  var chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$';
@@ -430,23 +463,6 @@ function writable(value, start = noop) {
430
463
  return { set, update, subscribe };
431
464
  }
432
465
 
433
- /**
434
- * Hash using djb2
435
- * @param {import('types/hooks').StrictBody} value
436
- */
437
- function hash(value) {
438
- let hash = 5381;
439
- let i = value.length;
440
-
441
- if (typeof value === 'string') {
442
- while (i) hash = (hash * 33) ^ value.charCodeAt(--i);
443
- } else {
444
- while (i) hash = (hash * 33) ^ value[--i];
445
- }
446
-
447
- return (hash >>> 0).toString(36);
448
- }
449
-
450
466
  /** @type {Record<string, string>} */
451
467
  const escape_json_string_in_html_dict = {
452
468
  '"': '\\"',
@@ -732,32 +748,29 @@ async function render_response({
732
748
  }
733
749
  }
734
750
 
735
- /** @type {import('types/helper').ResponseHeaders} */
736
- const headers = {
737
- 'content-type': 'text/html'
738
- };
751
+ const segments = url.pathname.slice(options.paths.base.length).split('/').slice(2);
752
+ const assets =
753
+ options.paths.assets || (segments.length > 0 ? segments.map(() => '..').join('/') : '.');
754
+
755
+ const html = options.template({ head, body, assets });
756
+
757
+ const headers = new Headers({
758
+ 'content-type': 'text/html',
759
+ etag: `"${hash(html)}"`
760
+ });
739
761
 
740
762
  if (maxage) {
741
- headers['cache-control'] = `${is_private ? 'private' : 'public'}, max-age=${maxage}`;
763
+ headers.set('cache-control', `${is_private ? 'private' : 'public'}, max-age=${maxage}`);
742
764
  }
743
765
 
744
766
  if (!options.floc) {
745
- headers['permissions-policy'] = 'interest-cohort=()';
767
+ headers.set('permissions-policy', 'interest-cohort=()');
746
768
  }
747
769
 
748
- const segments = url.pathname.slice(options.paths.base.length).split('/').slice(2);
749
- const assets =
750
- options.paths.assets || (segments.length > 0 ? segments.map(() => '..').join('/') : '.');
751
-
752
- return {
770
+ return new Response(html, {
753
771
  status,
754
- headers,
755
- body: options.template({
756
- head,
757
- body,
758
- assets
759
- })
760
- };
772
+ headers
773
+ });
761
774
  }
762
775
 
763
776
  /**
@@ -856,7 +869,7 @@ function normalize(loaded) {
856
869
 
857
870
  /**
858
871
  * @param {{
859
- * request: import('types/hooks').ServerRequest;
872
+ * event: import('types/hooks').RequestEvent;
860
873
  * options: import('types/internal').SSRRenderOptions;
861
874
  * state: import('types/internal').SSRRenderState;
862
875
  * route: import('types/internal').SSRPage | null;
@@ -872,7 +885,7 @@ function normalize(loaded) {
872
885
  * @returns {Promise<import('./types').Loaded | undefined>} undefined for fallthrough
873
886
  */
874
887
  async function load_node({
875
- request,
888
+ event,
876
889
  options,
877
890
  state,
878
891
  route,
@@ -943,7 +956,7 @@ async function load_node({
943
956
 
944
957
  opts.headers = new Headers(opts.headers);
945
958
 
946
- const resolved = resolve(request.url.pathname, requested.split('?')[0]);
959
+ const resolved = resolve(event.url.pathname, requested.split('?')[0]);
947
960
 
948
961
  let response;
949
962
 
@@ -979,12 +992,15 @@ async function load_node({
979
992
  if (opts.credentials !== 'omit') {
980
993
  uses_credentials = true;
981
994
 
982
- if (request.headers.cookie) {
983
- opts.headers.set('cookie', request.headers.cookie);
995
+ const cookie = event.request.headers.get('cookie');
996
+ const authorization = event.request.headers.get('authorization');
997
+
998
+ if (cookie) {
999
+ opts.headers.set('cookie', cookie);
984
1000
  }
985
1001
 
986
- if (request.headers.authorization && !opts.headers.has('authorization')) {
987
- opts.headers.set('authorization', request.headers.authorization);
1002
+ if (authorization && !opts.headers.has('authorization')) {
1003
+ opts.headers.set('authorization', authorization);
988
1004
  }
989
1005
  }
990
1006
 
@@ -997,12 +1013,7 @@ async function load_node({
997
1013
  }
998
1014
 
999
1015
  const rendered = await respond(
1000
- {
1001
- url: new URL(requested, request.url),
1002
- method: opts.method || 'GET',
1003
- headers: Object.fromEntries(opts.headers),
1004
- rawBody: opts.body == null ? null : new TextEncoder().encode(opts.body)
1005
- },
1016
+ new Request(new URL(requested, event.url).href, opts),
1006
1017
  options,
1007
1018
  {
1008
1019
  fetched: requested,
@@ -1015,17 +1026,11 @@ async function load_node({
1015
1026
  state.prerender.dependencies.set(relative, rendered);
1016
1027
  }
1017
1028
 
1018
- // Set-Cookie must be filtered out (done below) and that's the only header value that
1019
- // can be an array so we know we have only simple values
1020
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
1021
- response = new Response(rendered.body, {
1022
- status: rendered.status,
1023
- headers: /** @type {Record<string, string>} */ (rendered.headers)
1024
- });
1029
+ response = rendered;
1025
1030
  } else {
1026
1031
  // we can't load the endpoint from our own manifest,
1027
1032
  // so we need to make an actual HTTP request
1028
- return fetch(new URL(requested, request.url).href, {
1033
+ return fetch(new URL(requested, event.url).href, {
1029
1034
  method: opts.method || 'GET',
1030
1035
  headers: opts.headers
1031
1036
  });
@@ -1048,11 +1053,13 @@ async function load_node({
1048
1053
  // ports do not affect the resolution
1049
1054
  // leading dot prevents mydomain.com matching domain.com
1050
1055
  if (
1051
- `.${new URL(requested).hostname}`.endsWith(`.${request.url.hostname}`) &&
1056
+ `.${new URL(requested).hostname}`.endsWith(`.${event.url.hostname}`) &&
1052
1057
  opts.credentials !== 'omit'
1053
1058
  ) {
1054
1059
  uses_credentials = true;
1055
- opts.headers.set('cookie', request.headers.cookie);
1060
+
1061
+ const cookie = event.request.headers.get('cookie');
1062
+ if (cookie) opts.headers.set('cookie', cookie);
1056
1063
  }
1057
1064
 
1058
1065
  const external_request = new Request(requested, /** @type {RequestInit} */ (opts));
@@ -1161,7 +1168,7 @@ async function load_node({
1161
1168
 
1162
1169
  /**
1163
1170
  * @param {{
1164
- * request: import('types/hooks').ServerRequest;
1171
+ * event: import('types/hooks').RequestEvent;
1165
1172
  * options: SSRRenderOptions;
1166
1173
  * state: SSRRenderState;
1167
1174
  * $session: any;
@@ -1170,15 +1177,7 @@ async function load_node({
1170
1177
  * ssr: boolean;
1171
1178
  * }} opts
1172
1179
  */
1173
- async function respond_with_error({
1174
- request,
1175
- options,
1176
- state,
1177
- $session,
1178
- status,
1179
- error,
1180
- ssr
1181
- }) {
1180
+ async function respond_with_error({ event, options, state, $session, status, error, ssr }) {
1182
1181
  try {
1183
1182
  const default_layout = await options.manifest._.nodes[0](); // 0 is always the root layout
1184
1183
  const default_error = await options.manifest._.nodes[1](); // 1 is always the root error
@@ -1188,11 +1187,11 @@ async function respond_with_error({
1188
1187
 
1189
1188
  const layout_loaded = /** @type {Loaded} */ (
1190
1189
  await load_node({
1191
- request,
1190
+ event,
1192
1191
  options,
1193
1192
  state,
1194
1193
  route: null,
1195
- url: request.url, // TODO this is redundant, no?
1194
+ url: event.url, // TODO this is redundant, no?
1196
1195
  params,
1197
1196
  node: default_layout,
1198
1197
  $session,
@@ -1203,11 +1202,11 @@ async function respond_with_error({
1203
1202
 
1204
1203
  const error_loaded = /** @type {Loaded} */ (
1205
1204
  await load_node({
1206
- request,
1205
+ event,
1207
1206
  options,
1208
1207
  state,
1209
1208
  route: null,
1210
- url: request.url,
1209
+ url: event.url,
1211
1210
  params,
1212
1211
  node: default_error,
1213
1212
  $session,
@@ -1230,26 +1229,23 @@ async function respond_with_error({
1230
1229
  status,
1231
1230
  error,
1232
1231
  branch: [layout_loaded, error_loaded],
1233
- url: request.url,
1232
+ url: event.url,
1234
1233
  params,
1235
1234
  ssr
1236
1235
  });
1237
1236
  } catch (err) {
1238
1237
  const error = coalesce_to_error(err);
1239
1238
 
1240
- options.handle_error(error, request);
1239
+ options.handle_error(error, event);
1241
1240
 
1242
- return {
1243
- status: 500,
1244
- headers: {},
1245
- body: error.stack
1246
- };
1241
+ return new Response(error.stack, {
1242
+ status: 500
1243
+ });
1247
1244
  }
1248
1245
  }
1249
1246
 
1250
1247
  /**
1251
1248
  * @typedef {import('./types.js').Loaded} Loaded
1252
- * @typedef {import('types/hooks').ServerResponse} ServerResponse
1253
1249
  * @typedef {import('types/internal').SSRNode} SSRNode
1254
1250
  * @typedef {import('types/internal').SSRRenderOptions} SSRRenderOptions
1255
1251
  * @typedef {import('types/internal').SSRRenderState} SSRRenderState
@@ -1257,7 +1253,7 @@ async function respond_with_error({
1257
1253
 
1258
1254
  /**
1259
1255
  * @param {{
1260
- * request: import('types/hooks').ServerRequest;
1256
+ * event: import('types/hooks').RequestEvent;
1261
1257
  * options: SSRRenderOptions;
1262
1258
  * state: SSRRenderState;
1263
1259
  * $session: any;
@@ -1265,10 +1261,10 @@ async function respond_with_error({
1265
1261
  * params: Record<string, string>;
1266
1262
  * ssr: boolean;
1267
1263
  * }} opts
1268
- * @returns {Promise<ServerResponse | undefined>}
1264
+ * @returns {Promise<Response | undefined>}
1269
1265
  */
1270
1266
  async function respond$1(opts) {
1271
- const { request, options, state, $session, route, ssr } = opts;
1267
+ const { event, options, state, $session, route, ssr } = opts;
1272
1268
 
1273
1269
  /** @type {Array<SSRNode | undefined>} */
1274
1270
  let nodes;
@@ -1282,7 +1278,7 @@ async function respond$1(opts) {
1282
1278
  router: true
1283
1279
  },
1284
1280
  status: 200,
1285
- url: request.url,
1281
+ url: event.url,
1286
1282
  stuff: {}
1287
1283
  });
1288
1284
  }
@@ -1294,10 +1290,10 @@ async function respond$1(opts) {
1294
1290
  } catch (err) {
1295
1291
  const error = coalesce_to_error(err);
1296
1292
 
1297
- options.handle_error(error, request);
1293
+ options.handle_error(error, event);
1298
1294
 
1299
1295
  return await respond_with_error({
1300
- request,
1296
+ event,
1301
1297
  options,
1302
1298
  state,
1303
1299
  $session,
@@ -1315,10 +1311,9 @@ async function respond$1(opts) {
1315
1311
  if (!leaf.prerender && state.prerender && !state.prerender.all) {
1316
1312
  // if the page has `export const prerender = true`, continue,
1317
1313
  // otherwise bail out at this point
1318
- return {
1319
- status: 204,
1320
- headers: {}
1321
- };
1314
+ return new Response(undefined, {
1315
+ status: 204
1316
+ });
1322
1317
  }
1323
1318
 
1324
1319
  /** @type {Array<Loaded>} */
@@ -1346,7 +1341,7 @@ async function respond$1(opts) {
1346
1341
  try {
1347
1342
  loaded = await load_node({
1348
1343
  ...opts,
1349
- url: request.url,
1344
+ url: event.url,
1350
1345
  node,
1351
1346
  stuff,
1352
1347
  is_error: false
@@ -1358,12 +1353,12 @@ async function respond$1(opts) {
1358
1353
 
1359
1354
  if (loaded.loaded.redirect) {
1360
1355
  return with_cookies(
1361
- {
1356
+ new Response(undefined, {
1362
1357
  status: loaded.loaded.status,
1363
1358
  headers: {
1364
1359
  location: encodeURI(loaded.loaded.redirect)
1365
1360
  }
1366
- },
1361
+ }),
1367
1362
  set_cookie_headers
1368
1363
  );
1369
1364
  }
@@ -1374,7 +1369,7 @@ async function respond$1(opts) {
1374
1369
  } catch (err) {
1375
1370
  const e = coalesce_to_error(err);
1376
1371
 
1377
- options.handle_error(e, request);
1372
+ options.handle_error(e, event);
1378
1373
 
1379
1374
  status = 500;
1380
1375
  error = e;
@@ -1400,7 +1395,7 @@ async function respond$1(opts) {
1400
1395
  const error_loaded = /** @type {import('./types').Loaded} */ (
1401
1396
  await load_node({
1402
1397
  ...opts,
1403
- url: request.url,
1398
+ url: event.url,
1404
1399
  node: error_node,
1405
1400
  stuff: node_loaded.stuff,
1406
1401
  is_error: true,
@@ -1420,7 +1415,7 @@ async function respond$1(opts) {
1420
1415
  } catch (err) {
1421
1416
  const e = coalesce_to_error(err);
1422
1417
 
1423
- options.handle_error(e, request);
1418
+ options.handle_error(e, event);
1424
1419
 
1425
1420
  continue;
1426
1421
  }
@@ -1432,7 +1427,7 @@ async function respond$1(opts) {
1432
1427
  // for now just return regular error page
1433
1428
  return with_cookies(
1434
1429
  await respond_with_error({
1435
- request,
1430
+ event,
1436
1431
  options,
1437
1432
  state,
1438
1433
  $session,
@@ -1459,7 +1454,7 @@ async function respond$1(opts) {
1459
1454
  await render_response({
1460
1455
  ...opts,
1461
1456
  stuff,
1462
- url: request.url,
1457
+ url: event.url,
1463
1458
  page_config,
1464
1459
  status,
1465
1460
  error,
@@ -1470,7 +1465,7 @@ async function respond$1(opts) {
1470
1465
  } catch (err) {
1471
1466
  const error = coalesce_to_error(err);
1472
1467
 
1473
- options.handle_error(error, request);
1468
+ options.handle_error(error, event);
1474
1469
 
1475
1470
  return with_cookies(
1476
1471
  await respond_with_error({
@@ -1502,41 +1497,41 @@ function get_page_config(leaf, options) {
1502
1497
  }
1503
1498
 
1504
1499
  /**
1505
- * @param {ServerResponse} response
1500
+ * @param {Response} response
1506
1501
  * @param {string[]} set_cookie_headers
1507
1502
  */
1508
1503
  function with_cookies(response, set_cookie_headers) {
1509
1504
  if (set_cookie_headers.length) {
1510
- response.headers['set-cookie'] = set_cookie_headers;
1505
+ set_cookie_headers.forEach((value) => {
1506
+ response.headers.append('set-cookie', value);
1507
+ });
1511
1508
  }
1512
1509
  return response;
1513
1510
  }
1514
1511
 
1515
1512
  /**
1516
- * @param {import('types/hooks').ServerRequest} request
1513
+ * @param {import('types/hooks').RequestEvent} event
1517
1514
  * @param {import('types/internal').SSRPage} route
1518
1515
  * @param {RegExpExecArray} match
1519
1516
  * @param {import('types/internal').SSRRenderOptions} options
1520
1517
  * @param {import('types/internal').SSRRenderState} state
1521
1518
  * @param {boolean} ssr
1522
- * @returns {Promise<import('types/hooks').ServerResponse | undefined>}
1519
+ * @returns {Promise<Response | undefined>}
1523
1520
  */
1524
- async function render_page(request, route, match, options, state, ssr) {
1521
+ async function render_page(event, route, match, options, state, ssr) {
1525
1522
  if (state.initiator === route) {
1526
1523
  // infinite request cycle detected
1527
- return {
1528
- status: 404,
1529
- headers: {},
1530
- body: `Not found: ${request.url.pathname}`
1531
- };
1524
+ return new Response(`Not found: ${event.url.pathname}`, {
1525
+ status: 404
1526
+ });
1532
1527
  }
1533
1528
 
1534
1529
  const params = route.params ? decode_params(route.params(match)) : {};
1535
1530
 
1536
- const $session = await options.hooks.getSession(request);
1531
+ const $session = await options.hooks.getSession(event);
1537
1532
 
1538
1533
  const response = await respond$1({
1539
- request,
1534
+ event,
1540
1535
  options,
1541
1536
  state,
1542
1537
  $session,
@@ -1554,290 +1549,119 @@ async function render_page(request, route, match, options, state, ssr) {
1554
1549
  // rather than render the error page — which could lead to an
1555
1550
  // infinite loop, if the `load` belonged to the root layout,
1556
1551
  // we respond with a bare-bones 500
1557
- return {
1558
- status: 500,
1559
- headers: {},
1560
- body: `Bad request in load function: failed to fetch ${state.fetched}`
1561
- };
1562
- }
1563
- }
1564
-
1565
- function read_only_form_data() {
1566
- /** @type {Map<string, string[]>} */
1567
- const map = new Map();
1568
-
1569
- return {
1570
- /**
1571
- * @param {string} key
1572
- * @param {string} value
1573
- */
1574
- append(key, value) {
1575
- const existing_values = map.get(key);
1576
- if (existing_values) {
1577
- existing_values.push(value);
1578
- } else {
1579
- map.set(key, [value]);
1580
- }
1581
- },
1582
-
1583
- data: new ReadOnlyFormData(map)
1584
- };
1585
- }
1586
-
1587
- class ReadOnlyFormData {
1588
- /** @type {Map<string, string[]>} */
1589
- #map;
1590
-
1591
- /** @param {Map<string, string[]>} map */
1592
- constructor(map) {
1593
- this.#map = map;
1594
- }
1595
-
1596
- /** @param {string} key */
1597
- get(key) {
1598
- const value = this.#map.get(key);
1599
- if (!value) {
1600
- return null;
1601
- }
1602
- return value[0];
1603
- }
1604
-
1605
- /** @param {string} key */
1606
- getAll(key) {
1607
- return this.#map.get(key) || [];
1608
- }
1609
-
1610
- /** @param {string} key */
1611
- has(key) {
1612
- return this.#map.has(key);
1613
- }
1614
-
1615
- *[Symbol.iterator]() {
1616
- for (const [key, value] of this.#map) {
1617
- for (let i = 0; i < value.length; i += 1) {
1618
- yield [key, value[i]];
1619
- }
1620
- }
1621
- }
1622
-
1623
- *entries() {
1624
- for (const [key, value] of this.#map) {
1625
- for (let i = 0; i < value.length; i += 1) {
1626
- yield [key, value[i]];
1627
- }
1628
- }
1629
- }
1630
-
1631
- *keys() {
1632
- for (const [key] of this.#map) yield key;
1633
- }
1634
-
1635
- *values() {
1636
- for (const [, value] of this.#map) {
1637
- for (let i = 0; i < value.length; i += 1) {
1638
- yield value[i];
1639
- }
1640
- }
1641
- }
1642
- }
1643
-
1644
- /**
1645
- * @param {import('types/app').RawBody} raw
1646
- * @param {import('types/helper').RequestHeaders} headers
1647
- */
1648
- function parse_body(raw, headers) {
1649
- if (!raw) return raw;
1650
-
1651
- const content_type = headers['content-type'];
1652
- const [type, ...directives] = content_type ? content_type.split(/;\s*/) : [];
1653
-
1654
- const text = () => new TextDecoder(headers['content-encoding'] || 'utf-8').decode(raw);
1655
-
1656
- switch (type) {
1657
- case 'text/plain':
1658
- return text();
1659
-
1660
- case 'application/json':
1661
- return JSON.parse(text());
1662
-
1663
- case 'application/x-www-form-urlencoded':
1664
- return get_urlencoded(text());
1665
-
1666
- case 'multipart/form-data': {
1667
- const boundary = directives.find((directive) => directive.startsWith('boundary='));
1668
- if (!boundary) throw new Error('Missing boundary');
1669
- return get_multipart(text(), boundary.slice('boundary='.length));
1670
- }
1671
- default:
1672
- return raw;
1673
- }
1674
- }
1675
-
1676
- /** @param {string} text */
1677
- function get_urlencoded(text) {
1678
- const { data, append } = read_only_form_data();
1679
-
1680
- text
1681
- .replace(/\+/g, ' ')
1682
- .split('&')
1683
- .forEach((str) => {
1684
- const [key, value] = str.split('=');
1685
- append(decodeURIComponent(key), decodeURIComponent(value));
1552
+ return new Response(`Bad request in load function: failed to fetch ${state.fetched}`, {
1553
+ status: 500
1686
1554
  });
1687
-
1688
- return data;
1689
- }
1690
-
1691
- /**
1692
- * @param {string} text
1693
- * @param {string} boundary
1694
- */
1695
- function get_multipart(text, boundary) {
1696
- const parts = text.split(`--${boundary}`);
1697
-
1698
- if (parts[0] !== '' || parts[parts.length - 1].trim() !== '--') {
1699
- throw new Error('Malformed form data');
1700
1555
  }
1701
-
1702
- const { data, append } = read_only_form_data();
1703
-
1704
- parts.slice(1, -1).forEach((part) => {
1705
- const match = /\s*([\s\S]+?)\r\n\r\n([\s\S]*)\s*/.exec(part);
1706
- if (!match) {
1707
- throw new Error('Malformed form data');
1708
- }
1709
- const raw_headers = match[1];
1710
- const body = match[2].trim();
1711
-
1712
- let key;
1713
-
1714
- /** @type {Record<string, string>} */
1715
- const headers = {};
1716
- raw_headers.split('\r\n').forEach((str) => {
1717
- const [raw_header, ...raw_directives] = str.split('; ');
1718
- let [name, value] = raw_header.split(': ');
1719
-
1720
- name = name.toLowerCase();
1721
- headers[name] = value;
1722
-
1723
- /** @type {Record<string, string>} */
1724
- const directives = {};
1725
- raw_directives.forEach((raw_directive) => {
1726
- const [name, value] = raw_directive.split('=');
1727
- directives[name] = JSON.parse(value); // TODO is this right?
1728
- });
1729
-
1730
- if (name === 'content-disposition') {
1731
- if (value !== 'form-data') throw new Error('Malformed form data');
1732
-
1733
- if (directives.filename) {
1734
- // TODO we probably don't want to do this automatically
1735
- throw new Error('File upload is not yet implemented');
1736
- }
1737
-
1738
- if (directives.name) {
1739
- key = directives.name;
1740
- }
1741
- }
1742
- });
1743
-
1744
- if (!key) throw new Error('Malformed form data');
1745
-
1746
- append(key, body);
1747
- });
1748
-
1749
- return data;
1750
1556
  }
1751
1557
 
1752
- /** @type {import('@sveltejs/kit/ssr').Respond} */
1753
- async function respond(incoming, options, state = {}) {
1754
- if (incoming.url.pathname !== '/' && options.trailing_slash !== 'ignore') {
1755
- const has_trailing_slash = incoming.url.pathname.endsWith('/');
1558
+ /** @type {import('types/internal').Respond} */
1559
+ async function respond(request, options, state = {}) {
1560
+ const url = new URL(request.url);
1561
+
1562
+ if (url.pathname !== '/' && options.trailing_slash !== 'ignore') {
1563
+ const has_trailing_slash = url.pathname.endsWith('/');
1756
1564
 
1757
1565
  if (
1758
1566
  (has_trailing_slash && options.trailing_slash === 'never') ||
1759
1567
  (!has_trailing_slash &&
1760
1568
  options.trailing_slash === 'always' &&
1761
- !(incoming.url.pathname.split('/').pop() || '').includes('.'))
1569
+ !(url.pathname.split('/').pop() || '').includes('.'))
1762
1570
  ) {
1763
- incoming.url.pathname = has_trailing_slash
1764
- ? incoming.url.pathname.slice(0, -1)
1765
- : incoming.url.pathname + '/';
1571
+ url.pathname = has_trailing_slash ? url.pathname.slice(0, -1) : url.pathname + '/';
1766
1572
 
1767
- if (incoming.url.search === '?') incoming.url.search = '';
1573
+ if (url.search === '?') url.search = '';
1768
1574
 
1769
- return {
1575
+ return new Response(undefined, {
1770
1576
  status: 301,
1771
1577
  headers: {
1772
- location: incoming.url.pathname + incoming.url.search
1578
+ location: url.pathname + url.search
1773
1579
  }
1774
- };
1580
+ });
1775
1581
  }
1776
1582
  }
1777
1583
 
1778
- const headers = lowercase_keys(incoming.headers);
1779
- const request = {
1780
- ...incoming,
1781
- headers,
1782
- body: parse_body(incoming.rawBody, headers),
1783
- params: {},
1784
- locals: {}
1785
- };
1786
-
1787
1584
  const { parameter, allowed } = options.method_override;
1788
- const method_override = incoming.url.searchParams.get(parameter)?.toUpperCase();
1585
+ const method_override = url.searchParams.get(parameter)?.toUpperCase();
1789
1586
 
1790
1587
  if (method_override) {
1791
- if (request.method.toUpperCase() === 'POST') {
1588
+ if (request.method === 'POST') {
1792
1589
  if (allowed.includes(method_override)) {
1793
- request.method = method_override;
1590
+ request = new Proxy(request, {
1591
+ get: (target, property, _receiver) => {
1592
+ if (property === 'method') return method_override;
1593
+ return Reflect.get(target, property, target);
1594
+ }
1595
+ });
1794
1596
  } else {
1795
1597
  const verb = allowed.length === 0 ? 'enabled' : 'allowed';
1796
1598
  const body = `${parameter}=${method_override} is not ${verb}. See https://kit.svelte.dev/docs#configuration-methodoverride`;
1797
1599
 
1798
- return {
1799
- status: 400,
1800
- headers: {},
1801
- body
1802
- };
1600
+ return new Response(body, {
1601
+ status: 400
1602
+ });
1803
1603
  }
1804
1604
  } else {
1805
1605
  throw new Error(`${parameter}=${method_override} is only allowed with POST requests`);
1806
1606
  }
1807
1607
  }
1808
1608
 
1609
+ /** @type {import('types/hooks').RequestEvent} */
1610
+ const event = {
1611
+ request,
1612
+ url,
1613
+ params: {},
1614
+ locals: {}
1615
+ };
1616
+
1809
1617
  // TODO remove this for 1.0
1810
1618
  /**
1811
1619
  * @param {string} property
1812
1620
  * @param {string} replacement
1621
+ * @param {string} suffix
1813
1622
  */
1814
- const print_error = (property, replacement) => {
1815
- Object.defineProperty(request, property, {
1816
- get: () => {
1817
- throw new Error(`request.${property} has been replaced by request.url.${replacement}`);
1818
- }
1819
- });
1623
+ const removed = (property, replacement, suffix = '') => ({
1624
+ get: () => {
1625
+ throw new Error(`event.${property} has been replaced by event.${replacement}` + suffix);
1626
+ }
1627
+ });
1628
+
1629
+ const details = '. See https://github.com/sveltejs/kit/pull/3384 for details';
1630
+
1631
+ const body_getter = {
1632
+ get: () => {
1633
+ throw new Error(
1634
+ 'To access the request body use the text/json/arrayBuffer/formData methods, e.g. `body = await request.json()`' +
1635
+ details
1636
+ );
1637
+ }
1820
1638
  };
1821
1639
 
1822
- print_error('origin', 'origin');
1823
- print_error('path', 'pathname');
1824
- print_error('query', 'searchParams');
1640
+ Object.defineProperties(event, {
1641
+ method: removed('method', 'request.method', details),
1642
+ headers: removed('headers', 'request.headers', details),
1643
+ origin: removed('origin', 'url.origin'),
1644
+ path: removed('path', 'url.pathname'),
1645
+ query: removed('query', 'url.searchParams'),
1646
+ body: body_getter,
1647
+ rawBody: body_getter
1648
+ });
1825
1649
 
1826
1650
  let ssr = true;
1827
1651
 
1828
1652
  try {
1829
1653
  return await options.hooks.handle({
1830
- request,
1831
- resolve: async (request, opts) => {
1654
+ event,
1655
+ resolve: async (event, opts) => {
1832
1656
  if (opts && 'ssr' in opts) ssr = /** @type {boolean} */ (opts.ssr);
1833
1657
 
1834
1658
  if (state.prerender && state.prerender.fallback) {
1835
1659
  return await render_response({
1836
- url: request.url,
1837
- params: request.params,
1660
+ url: event.url,
1661
+ params: event.params,
1838
1662
  options,
1839
1663
  state,
1840
- $session: await options.hooks.getSession(request),
1664
+ $session: await options.hooks.getSession(event),
1841
1665
  page_config: { router: true, hydrate: true },
1842
1666
  stuff: {},
1843
1667
  status: 200,
@@ -1846,7 +1670,7 @@ async function respond(incoming, options, state = {}) {
1846
1670
  });
1847
1671
  }
1848
1672
 
1849
- let decoded = decodeURI(request.url.pathname);
1673
+ let decoded = decodeURI(event.url.pathname);
1850
1674
 
1851
1675
  if (options.paths.base) {
1852
1676
  if (!decoded.startsWith(options.paths.base)) return;
@@ -1859,47 +1683,40 @@ async function respond(incoming, options, state = {}) {
1859
1683
 
1860
1684
  const response =
1861
1685
  route.type === 'endpoint'
1862
- ? await render_endpoint(request, route, match)
1863
- : await render_page(request, route, match, options, state, ssr);
1686
+ ? await render_endpoint(event, route, match)
1687
+ : await render_page(event, route, match, options, state, ssr);
1864
1688
 
1865
1689
  if (response) {
1866
- // inject ETags for 200 responses, if the endpoint
1867
- // doesn't have its own ETag handling
1868
- if (response.status === 200 && !response.headers.etag) {
1869
- const cache_control = get_single_valued_header(response.headers, 'cache-control');
1870
- if (!cache_control || !/(no-store|immutable)/.test(cache_control)) {
1871
- let if_none_match_value = request.headers['if-none-match'];
1872
- // ignore W/ prefix https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives
1873
- if (if_none_match_value?.startsWith('W/"')) {
1874
- if_none_match_value = if_none_match_value.substring(2);
1875
- }
1690
+ // respond with 304 if etag matches
1691
+ if (response.status === 200 && response.headers.has('etag')) {
1692
+ let if_none_match_value = request.headers.get('if-none-match');
1876
1693
 
1877
- const etag = `"${hash(response.body || '')}"`;
1878
-
1879
- if (if_none_match_value === etag) {
1880
- /** @type {import('types/helper').ResponseHeaders} */
1881
- const headers = { etag };
1882
-
1883
- // https://datatracker.ietf.org/doc/html/rfc7232#section-4.1
1884
- for (const key of [
1885
- 'cache-control',
1886
- 'content-location',
1887
- 'date',
1888
- 'expires',
1889
- 'vary'
1890
- ]) {
1891
- if (key in response.headers) {
1892
- headers[key] = /** @type {string} */ (response.headers[key]);
1893
- }
1894
- }
1694
+ // ignore W/ prefix https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives
1695
+ if (if_none_match_value?.startsWith('W/"')) {
1696
+ if_none_match_value = if_none_match_value.substring(2);
1697
+ }
1895
1698
 
1896
- return {
1897
- status: 304,
1898
- headers
1899
- };
1699
+ const etag = /** @type {string} */ (response.headers.get('etag'));
1700
+
1701
+ if (if_none_match_value === etag) {
1702
+ const headers = new Headers({ etag });
1703
+
1704
+ // https://datatracker.ietf.org/doc/html/rfc7232#section-4.1
1705
+ for (const key of [
1706
+ 'cache-control',
1707
+ 'content-location',
1708
+ 'date',
1709
+ 'expires',
1710
+ 'vary'
1711
+ ]) {
1712
+ const value = response.headers.get(key);
1713
+ if (value) headers.set(key, value);
1900
1714
  }
1901
1715
 
1902
- response.headers['etag'] = etag;
1716
+ return new Response(undefined, {
1717
+ status: 304,
1718
+ headers
1719
+ });
1903
1720
  }
1904
1721
  }
1905
1722
 
@@ -1910,28 +1727,34 @@ async function respond(incoming, options, state = {}) {
1910
1727
  // if this request came direct from the user, rather than
1911
1728
  // via a `fetch` in a `load`, render a 404 page
1912
1729
  if (!state.initiator) {
1913
- const $session = await options.hooks.getSession(request);
1730
+ const $session = await options.hooks.getSession(event);
1914
1731
  return await respond_with_error({
1915
- request,
1732
+ event,
1916
1733
  options,
1917
1734
  state,
1918
1735
  $session,
1919
1736
  status: 404,
1920
- error: new Error(`Not found: ${request.url.pathname}`),
1737
+ error: new Error(`Not found: ${event.url.pathname}`),
1921
1738
  ssr
1922
1739
  });
1923
1740
  }
1741
+ },
1742
+
1743
+ // TODO remove for 1.0
1744
+ // @ts-expect-error
1745
+ get request() {
1746
+ throw new Error('request in handle has been replaced with event' + details);
1924
1747
  }
1925
1748
  });
1926
1749
  } catch (/** @type {unknown} */ e) {
1927
1750
  const error = coalesce_to_error(e);
1928
1751
 
1929
- options.handle_error(error, request);
1752
+ options.handle_error(error, event);
1930
1753
 
1931
1754
  try {
1932
- const $session = await options.hooks.getSession(request);
1755
+ const $session = await options.hooks.getSession(event);
1933
1756
  return await respond_with_error({
1934
- request,
1757
+ event,
1935
1758
  options,
1936
1759
  state,
1937
1760
  $session,
@@ -1942,11 +1765,9 @@ async function respond(incoming, options, state = {}) {
1942
1765
  } catch (/** @type {unknown} */ e) {
1943
1766
  const error = coalesce_to_error(e);
1944
1767
 
1945
- return {
1946
- status: 500,
1947
- headers: {},
1948
- body: options.dev ? error.stack : error.message
1949
- };
1768
+ return new Response(options.dev ? error.stack : error.message, {
1769
+ status: 500
1770
+ });
1950
1771
  }
1951
1772
  }
1952
1773
  }
@@ -2108,7 +1929,7 @@ async function create_plugin(config, cwd) {
2108
1929
  /** @type {import('types/internal').Hooks} */
2109
1930
  const hooks = {
2110
1931
  getSession: user_hooks.getSession || (() => ({})),
2111
- handle: user_hooks.handle || (({ request, resolve }) => resolve(request)),
1932
+ handle: user_hooks.handle || (({ event, resolve }) => resolve(event)),
2112
1933
  handleError:
2113
1934
  user_hooks.handleError ||
2114
1935
  (({ /** @type {Error & { frame?: string }} */ error }) => {
@@ -2155,12 +1976,11 @@ async function create_plugin(config, cwd) {
2155
1976
  }
2156
1977
 
2157
1978
  const rendered = await respond(
2158
- {
2159
- url,
2160
- headers: /** @type {import('types/helper').RequestHeaders} */ (req.headers),
1979
+ new Request(url.href, {
1980
+ headers: /** @type {Record<string, string>} */ (req.headers),
2161
1981
  method: req.method,
2162
- rawBody: body
2163
- },
1982
+ body
1983
+ }),
2164
1984
  {
2165
1985
  amp: config.kit.amp,
2166
1986
  dev: true,
@@ -2169,9 +1989,20 @@ async function create_plugin(config, cwd) {
2169
1989
  vite.ssrFixStacktrace(error);
2170
1990
  return error.stack;
2171
1991
  },
2172
- handle_error: (error, request) => {
1992
+ handle_error: (error, event) => {
2173
1993
  vite.ssrFixStacktrace(error);
2174
- hooks.handleError({ error, request });
1994
+ hooks.handleError({
1995
+ error,
1996
+ event,
1997
+
1998
+ // TODO remove for 1.0
1999
+ // @ts-expect-error
2000
+ get request() {
2001
+ throw new Error(
2002
+ 'request in handleError has been replaced with event. See https://github.com/sveltejs/kit/pull/3384 for details'
2003
+ );
2004
+ }
2005
+ });
2175
2006
  },
2176
2007
  hooks,
2177
2008
  hydrate: config.kit.hydrate,
@@ -2244,9 +2075,7 @@ async function create_plugin(config, cwd) {
2244
2075
  );
2245
2076
 
2246
2077
  if (rendered) {
2247
- res.writeHead(rendered.status, rendered.headers);
2248
- if (rendered.body) res.write(rendered.body);
2249
- res.end();
2078
+ setResponse(res, rendered);
2250
2079
  } else {
2251
2080
  not_found(res);
2252
2081
  }