UNPKG

67.9 kBJavaScriptView Raw
1import { randomBytes, createHash } from 'crypto';
2import http from 'http';
3import https from 'https';
4import zlib from 'zlib';
5import Stream, { PassThrough, pipeline } from 'stream';
6import { types } from 'util';
7import { format, parse, resolve, URLSearchParams as URLSearchParams$1 } from 'url';
8
9var chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$';
10var unsafeChars = /[<>\b\f\n\r\t\0\u2028\u2029]/g;
11var reserved = /^(?:do|if|in|for|int|let|new|try|var|byte|case|char|else|enum|goto|long|this|void|with|await|break|catch|class|const|final|float|short|super|throw|while|yield|delete|double|export|import|native|return|switch|throws|typeof|boolean|default|extends|finally|package|private|abstract|continue|debugger|function|volatile|interface|protected|transient|implements|instanceof|synchronized)$/;
12var escaped = {
13 '<': '\\u003C',
14 '>': '\\u003E',
15 '/': '\\u002F',
16 '\\': '\\\\',
17 '\b': '\\b',
18 '\f': '\\f',
19 '\n': '\\n',
20 '\r': '\\r',
21 '\t': '\\t',
22 '\0': '\\0',
23 '\u2028': '\\u2028',
24 '\u2029': '\\u2029'
25};
26var objectProtoOwnPropertyNames = Object.getOwnPropertyNames(Object.prototype).sort().join('\0');
27function devalue(value) {
28 var counts = new Map();
29 function walk(thing) {
30 if (typeof thing === 'function') {
31 throw new Error("Cannot stringify a function");
32 }
33 if (counts.has(thing)) {
34 counts.set(thing, counts.get(thing) + 1);
35 return;
36 }
37 counts.set(thing, 1);
38 if (!isPrimitive(thing)) {
39 var type = getType(thing);
40 switch (type) {
41 case 'Number':
42 case 'String':
43 case 'Boolean':
44 case 'Date':
45 case 'RegExp':
46 return;
47 case 'Array':
48 thing.forEach(walk);
49 break;
50 case 'Set':
51 case 'Map':
52 Array.from(thing).forEach(walk);
53 break;
54 default:
55 var proto = Object.getPrototypeOf(thing);
56 if (proto !== Object.prototype &&
57 proto !== null &&
58 Object.getOwnPropertyNames(proto).sort().join('\0') !== objectProtoOwnPropertyNames) {
59 throw new Error("Cannot stringify arbitrary non-POJOs");
60 }
61 if (Object.getOwnPropertySymbols(thing).length > 0) {
62 throw new Error("Cannot stringify POJOs with symbolic keys");
63 }
64 Object.keys(thing).forEach(function (key) { return walk(thing[key]); });
65 }
66 }
67 }
68 walk(value);
69 var names = new Map();
70 Array.from(counts)
71 .filter(function (entry) { return entry[1] > 1; })
72 .sort(function (a, b) { return b[1] - a[1]; })
73 .forEach(function (entry, i) {
74 names.set(entry[0], getName(i));
75 });
76 function stringify(thing) {
77 if (names.has(thing)) {
78 return names.get(thing);
79 }
80 if (isPrimitive(thing)) {
81 return stringifyPrimitive(thing);
82 }
83 var type = getType(thing);
84 switch (type) {
85 case 'Number':
86 case 'String':
87 case 'Boolean':
88 return "Object(" + stringify(thing.valueOf()) + ")";
89 case 'RegExp':
90 return "new RegExp(" + stringifyString(thing.source) + ", \"" + thing.flags + "\")";
91 case 'Date':
92 return "new Date(" + thing.getTime() + ")";
93 case 'Array':
94 var members = thing.map(function (v, i) { return i in thing ? stringify(v) : ''; });
95 var tail = thing.length === 0 || (thing.length - 1 in thing) ? '' : ',';
96 return "[" + members.join(',') + tail + "]";
97 case 'Set':
98 case 'Map':
99 return "new " + type + "([" + Array.from(thing).map(stringify).join(',') + "])";
100 default:
101 var obj = "{" + Object.keys(thing).map(function (key) { return safeKey(key) + ":" + stringify(thing[key]); }).join(',') + "}";
102 var proto = Object.getPrototypeOf(thing);
103 if (proto === null) {
104 return Object.keys(thing).length > 0
105 ? "Object.assign(Object.create(null)," + obj + ")"
106 : "Object.create(null)";
107 }
108 return obj;
109 }
110 }
111 var str = stringify(value);
112 if (names.size) {
113 var params_1 = [];
114 var statements_1 = [];
115 var values_1 = [];
116 names.forEach(function (name, thing) {
117 params_1.push(name);
118 if (isPrimitive(thing)) {
119 values_1.push(stringifyPrimitive(thing));
120 return;
121 }
122 var type = getType(thing);
123 switch (type) {
124 case 'Number':
125 case 'String':
126 case 'Boolean':
127 values_1.push("Object(" + stringify(thing.valueOf()) + ")");
128 break;
129 case 'RegExp':
130 values_1.push(thing.toString());
131 break;
132 case 'Date':
133 values_1.push("new Date(" + thing.getTime() + ")");
134 break;
135 case 'Array':
136 values_1.push("Array(" + thing.length + ")");
137 thing.forEach(function (v, i) {
138 statements_1.push(name + "[" + i + "]=" + stringify(v));
139 });
140 break;
141 case 'Set':
142 values_1.push("new Set");
143 statements_1.push(name + "." + Array.from(thing).map(function (v) { return "add(" + stringify(v) + ")"; }).join('.'));
144 break;
145 case 'Map':
146 values_1.push("new Map");
147 statements_1.push(name + "." + Array.from(thing).map(function (_a) {
148 var k = _a[0], v = _a[1];
149 return "set(" + stringify(k) + ", " + stringify(v) + ")";
150 }).join('.'));
151 break;
152 default:
153 values_1.push(Object.getPrototypeOf(thing) === null ? 'Object.create(null)' : '{}');
154 Object.keys(thing).forEach(function (key) {
155 statements_1.push("" + name + safeProp(key) + "=" + stringify(thing[key]));
156 });
157 }
158 });
159 statements_1.push("return " + str);
160 return "(function(" + params_1.join(',') + "){" + statements_1.join(';') + "}(" + values_1.join(',') + "))";
161 }
162 else {
163 return str;
164 }
165}
166function getName(num) {
167 var name = '';
168 do {
169 name = chars[num % chars.length] + name;
170 num = ~~(num / chars.length) - 1;
171 } while (num >= 0);
172 return reserved.test(name) ? name + "_" : name;
173}
174function isPrimitive(thing) {
175 return Object(thing) !== thing;
176}
177function stringifyPrimitive(thing) {
178 if (typeof thing === 'string')
179 return stringifyString(thing);
180 if (thing === void 0)
181 return 'void 0';
182 if (thing === 0 && 1 / thing < 0)
183 return '-0';
184 var str = String(thing);
185 if (typeof thing === 'number')
186 return str.replace(/^(-)?0\./, '$1.');
187 return str;
188}
189function getType(thing) {
190 return Object.prototype.toString.call(thing).slice(8, -1);
191}
192function escapeUnsafeChar(c) {
193 return escaped[c] || c;
194}
195function escapeUnsafeChars(str) {
196 return str.replace(unsafeChars, escapeUnsafeChar);
197}
198function safeKey(key) {
199 return /^[_$a-zA-Z][_$a-zA-Z0-9]*$/.test(key) ? key : escapeUnsafeChars(JSON.stringify(key));
200}
201function safeProp(key) {
202 return /^[_$a-zA-Z][_$a-zA-Z0-9]*$/.test(key) ? "." + key : "[" + escapeUnsafeChars(JSON.stringify(key)) + "]";
203}
204function stringifyString(str) {
205 var result = '"';
206 for (var i = 0; i < str.length; i += 1) {
207 var char = str.charAt(i);
208 var code = char.charCodeAt(0);
209 if (char === '"') {
210 result += '\\"';
211 }
212 else if (char in escaped) {
213 result += escaped[char];
214 }
215 else if (code >= 0xd800 && code <= 0xdfff) {
216 var next = str.charCodeAt(i + 1);
217 // If this is the beginning of a [high, low] surrogate pair,
218 // add the next two characters, otherwise escape
219 if (code <= 0xdbff && (next >= 0xdc00 && next <= 0xdfff)) {
220 result += char + str[++i];
221 }
222 else {
223 result += "\\u" + code.toString(16).toUpperCase();
224 }
225 }
226 else {
227 result += char;
228 }
229 }
230 result += '"';
231 return result;
232}
233
234/**
235 * Returns a `Buffer` instance from the given data URI `uri`.
236 *
237 * @param {String} uri Data URI to turn into a Buffer instance
238 * @return {Buffer} Buffer instance from Data URI
239 * @api public
240 */
241function dataUriToBuffer(uri) {
242 if (!/^data:/i.test(uri)) {
243 throw new TypeError('`uri` does not appear to be a Data URI (must begin with "data:")');
244 }
245 // strip newlines
246 uri = uri.replace(/\r?\n/g, '');
247 // split the URI up into the "metadata" and the "data" portions
248 const firstComma = uri.indexOf(',');
249 if (firstComma === -1 || firstComma <= 4) {
250 throw new TypeError('malformed data: URI');
251 }
252 // remove the "data:" scheme and parse the metadata
253 const meta = uri.substring(5, firstComma).split(';');
254 let charset = '';
255 let base64 = false;
256 const type = meta[0] || 'text/plain';
257 let typeFull = type;
258 for (let i = 1; i < meta.length; i++) {
259 if (meta[i] === 'base64') {
260 base64 = true;
261 }
262 else {
263 typeFull += `;${meta[i]}`;
264 if (meta[i].indexOf('charset=') === 0) {
265 charset = meta[i].substring(8);
266 }
267 }
268 }
269 // defaults to US-ASCII only if type is not provided
270 if (!meta[0] && !charset.length) {
271 typeFull += ';charset=US-ASCII';
272 charset = 'US-ASCII';
273 }
274 // get the encoded data portion and decode URI-encoded chars
275 const encoding = base64 ? 'base64' : 'ascii';
276 const data = unescape(uri.substring(firstComma + 1));
277 const buffer = Buffer.from(data, encoding);
278 // set `.type` and `.typeFull` properties to MIME type
279 buffer.type = type;
280 buffer.typeFull = typeFull;
281 // set the `.charset` property
282 buffer.charset = charset;
283 return buffer;
284}
285var src = dataUriToBuffer;
286
287const {Readable} = Stream;
288
289/**
290 * @type {WeakMap<Blob, {type: string, size: number, parts: (Blob | Buffer)[] }>}
291 */
292const wm = new WeakMap();
293
294async function * read(parts) {
295 for (const part of parts) {
296 if ('stream' in part) {
297 yield * part.stream();
298 } else {
299 yield part;
300 }
301 }
302}
303
304class Blob {
305 /**
306 * The Blob() constructor returns a new Blob object. The content
307 * of the blob consists of the concatenation of the values given
308 * in the parameter array.
309 *
310 * @param {(ArrayBufferLike | ArrayBufferView | Blob | Buffer | string)[]} blobParts
311 * @param {{ type?: string }} [options]
312 */
313 constructor(blobParts = [], options = {type: ''}) {
314 let size = 0;
315
316 const parts = blobParts.map(element => {
317 let buffer;
318 if (element instanceof Buffer) {
319 buffer = element;
320 } else if (ArrayBuffer.isView(element)) {
321 buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength);
322 } else if (element instanceof ArrayBuffer) {
323 buffer = Buffer.from(element);
324 } else if (element instanceof Blob) {
325 buffer = element;
326 } else {
327 buffer = Buffer.from(typeof element === 'string' ? element : String(element));
328 }
329
330 size += buffer.length || buffer.size || 0;
331 return buffer;
332 });
333
334 const type = options.type === undefined ? '' : String(options.type).toLowerCase();
335
336 wm.set(this, {
337 type: /[^\u0020-\u007E]/.test(type) ? '' : type,
338 size,
339 parts
340 });
341 }
342
343 /**
344 * The Blob interface's size property returns the
345 * size of the Blob in bytes.
346 */
347 get size() {
348 return wm.get(this).size;
349 }
350
351 /**
352 * The type property of a Blob object returns the MIME type of the file.
353 */
354 get type() {
355 return wm.get(this).type;
356 }
357
358 /**
359 * The text() method in the Blob interface returns a Promise
360 * that resolves with a string containing the contents of
361 * the blob, interpreted as UTF-8.
362 *
363 * @return {Promise<string>}
364 */
365 async text() {
366 return Buffer.from(await this.arrayBuffer()).toString();
367 }
368
369 /**
370 * The arrayBuffer() method in the Blob interface returns a
371 * Promise that resolves with the contents of the blob as
372 * binary data contained in an ArrayBuffer.
373 *
374 * @return {Promise<ArrayBuffer>}
375 */
376 async arrayBuffer() {
377 const data = new Uint8Array(this.size);
378 let offset = 0;
379 for await (const chunk of this.stream()) {
380 data.set(chunk, offset);
381 offset += chunk.length;
382 }
383
384 return data.buffer;
385 }
386
387 /**
388 * The Blob interface's stream() method is difference from native
389 * and uses node streams instead of whatwg streams.
390 *
391 * @returns {Readable} Node readable stream
392 */
393 stream() {
394 return Readable.from(read(wm.get(this).parts));
395 }
396
397 /**
398 * The Blob interface's slice() method creates and returns a
399 * new Blob object which contains data from a subset of the
400 * blob on which it's called.
401 *
402 * @param {number} [start]
403 * @param {number} [end]
404 * @param {string} [type]
405 */
406 slice(start = 0, end = this.size, type = '') {
407 const {size} = this;
408
409 let relativeStart = start < 0 ? Math.max(size + start, 0) : Math.min(start, size);
410 let relativeEnd = end < 0 ? Math.max(size + end, 0) : Math.min(end, size);
411
412 const span = Math.max(relativeEnd - relativeStart, 0);
413 const parts = wm.get(this).parts.values();
414 const blobParts = [];
415 let added = 0;
416
417 for (const part of parts) {
418 const size = ArrayBuffer.isView(part) ? part.byteLength : part.size;
419 if (relativeStart && size <= relativeStart) {
420 // Skip the beginning and change the relative
421 // start & end position as we skip the unwanted parts
422 relativeStart -= size;
423 relativeEnd -= size;
424 } else {
425 const chunk = part.slice(relativeStart, Math.min(size, relativeEnd));
426 blobParts.push(chunk);
427 added += ArrayBuffer.isView(chunk) ? chunk.byteLength : chunk.size;
428 relativeStart = 0; // All next sequental parts should start at 0
429
430 // don't add the overflow to new blobParts
431 if (added >= span) {
432 break;
433 }
434 }
435 }
436
437 const blob = new Blob([], {type});
438 Object.assign(wm.get(blob), {size: span, parts: blobParts});
439
440 return blob;
441 }
442
443 get [Symbol.toStringTag]() {
444 return 'Blob';
445 }
446
447 static [Symbol.hasInstance](object) {
448 return (
449 typeof object === 'object' &&
450 typeof object.stream === 'function' &&
451 object.stream.length === 0 &&
452 typeof object.constructor === 'function' &&
453 /^(Blob|File)$/.test(object[Symbol.toStringTag])
454 );
455 }
456}
457
458Object.defineProperties(Blob.prototype, {
459 size: {enumerable: true},
460 type: {enumerable: true},
461 slice: {enumerable: true}
462});
463
464var fetchBlob = Blob;
465
466class FetchBaseError extends Error {
467 constructor(message, type) {
468 super(message);
469 // Hide custom error implementation details from end-users
470 Error.captureStackTrace(this, this.constructor);
471
472 this.type = type;
473 }
474
475 get name() {
476 return this.constructor.name;
477 }
478
479 get [Symbol.toStringTag]() {
480 return this.constructor.name;
481 }
482}
483
484/**
485 * @typedef {{ address?: string, code: string, dest?: string, errno: number, info?: object, message: string, path?: string, port?: number, syscall: string}} SystemError
486*/
487
488/**
489 * FetchError interface for operational errors
490 */
491class FetchError extends FetchBaseError {
492 /**
493 * @param {string} message - Error message for human
494 * @param {string} [type] - Error type for machine
495 * @param {SystemError} [systemError] - For Node.js system error
496 */
497 constructor(message, type, systemError) {
498 super(message, type);
499 // When err.type is `system`, err.erroredSysCall contains system error and err.code contains system error code
500 if (systemError) {
501 // eslint-disable-next-line no-multi-assign
502 this.code = this.errno = systemError.code;
503 this.erroredSysCall = systemError.syscall;
504 }
505 }
506}
507
508/**
509 * Is.js
510 *
511 * Object type checks.
512 */
513
514const NAME = Symbol.toStringTag;
515
516/**
517 * Check if `obj` is a URLSearchParams object
518 * ref: https://github.com/node-fetch/node-fetch/issues/296#issuecomment-307598143
519 *
520 * @param {*} obj
521 * @return {boolean}
522 */
523const isURLSearchParameters = object => {
524 return (
525 typeof object === 'object' &&
526 typeof object.append === 'function' &&
527 typeof object.delete === 'function' &&
528 typeof object.get === 'function' &&
529 typeof object.getAll === 'function' &&
530 typeof object.has === 'function' &&
531 typeof object.set === 'function' &&
532 typeof object.sort === 'function' &&
533 object[NAME] === 'URLSearchParams'
534 );
535};
536
537/**
538 * Check if `object` is a W3C `Blob` object (which `File` inherits from)
539 *
540 * @param {*} obj
541 * @return {boolean}
542 */
543const isBlob = object => {
544 return (
545 typeof object === 'object' &&
546 typeof object.arrayBuffer === 'function' &&
547 typeof object.type === 'string' &&
548 typeof object.stream === 'function' &&
549 typeof object.constructor === 'function' &&
550 /^(Blob|File)$/.test(object[NAME])
551 );
552};
553
554/**
555 * Check if `obj` is a spec-compliant `FormData` object
556 *
557 * @param {*} object
558 * @return {boolean}
559 */
560function isFormData(object) {
561 return (
562 typeof object === 'object' &&
563 typeof object.append === 'function' &&
564 typeof object.set === 'function' &&
565 typeof object.get === 'function' &&
566 typeof object.getAll === 'function' &&
567 typeof object.delete === 'function' &&
568 typeof object.keys === 'function' &&
569 typeof object.values === 'function' &&
570 typeof object.entries === 'function' &&
571 typeof object.constructor === 'function' &&
572 object[NAME] === 'FormData'
573 );
574}
575
576/**
577 * Check if `obj` is an instance of AbortSignal.
578 *
579 * @param {*} obj
580 * @return {boolean}
581 */
582const isAbortSignal = object => {
583 return (
584 typeof object === 'object' &&
585 object[NAME] === 'AbortSignal'
586 );
587};
588
589const carriage = '\r\n';
590const dashes = '-'.repeat(2);
591const carriageLength = Buffer.byteLength(carriage);
592
593/**
594 * @param {string} boundary
595 */
596const getFooter = boundary => `${dashes}${boundary}${dashes}${carriage.repeat(2)}`;
597
598/**
599 * @param {string} boundary
600 * @param {string} name
601 * @param {*} field
602 *
603 * @return {string}
604 */
605function getHeader(boundary, name, field) {
606 let header = '';
607
608 header += `${dashes}${boundary}${carriage}`;
609 header += `Content-Disposition: form-data; name="${name}"`;
610
611 if (isBlob(field)) {
612 header += `; filename="${field.name}"${carriage}`;
613 header += `Content-Type: ${field.type || 'application/octet-stream'}`;
614 }
615
616 return `${header}${carriage.repeat(2)}`;
617}
618
619/**
620 * @return {string}
621 */
622const getBoundary = () => randomBytes(8).toString('hex');
623
624/**
625 * @param {FormData} form
626 * @param {string} boundary
627 */
628async function * formDataIterator(form, boundary) {
629 for (const [name, value] of form) {
630 yield getHeader(boundary, name, value);
631
632 if (isBlob(value)) {
633 yield * value.stream();
634 } else {
635 yield value;
636 }
637
638 yield carriage;
639 }
640
641 yield getFooter(boundary);
642}
643
644/**
645 * @param {FormData} form
646 * @param {string} boundary
647 */
648function getFormDataLength(form, boundary) {
649 let length = 0;
650
651 for (const [name, value] of form) {
652 length += Buffer.byteLength(getHeader(boundary, name, value));
653
654 if (isBlob(value)) {
655 length += value.size;
656 } else {
657 length += Buffer.byteLength(String(value));
658 }
659
660 length += carriageLength;
661 }
662
663 length += Buffer.byteLength(getFooter(boundary));
664
665 return length;
666}
667
668const INTERNALS$2 = Symbol('Body internals');
669
670/**
671 * Body mixin
672 *
673 * Ref: https://fetch.spec.whatwg.org/#body
674 *
675 * @param Stream body Readable stream
676 * @param Object opts Response options
677 * @return Void
678 */
679class Body {
680 constructor(body, {
681 size = 0
682 } = {}) {
683 let boundary = null;
684
685 if (body === null) {
686 // Body is undefined or null
687 body = null;
688 } else if (isURLSearchParameters(body)) {
689 // Body is a URLSearchParams
690 body = Buffer.from(body.toString());
691 } else if (isBlob(body)) ; else if (Buffer.isBuffer(body)) ; else if (types.isAnyArrayBuffer(body)) {
692 // Body is ArrayBuffer
693 body = Buffer.from(body);
694 } else if (ArrayBuffer.isView(body)) {
695 // Body is ArrayBufferView
696 body = Buffer.from(body.buffer, body.byteOffset, body.byteLength);
697 } else if (body instanceof Stream) ; else if (isFormData(body)) {
698 // Body is an instance of formdata-node
699 boundary = `NodeFetchFormDataBoundary${getBoundary()}`;
700 body = Stream.Readable.from(formDataIterator(body, boundary));
701 } else {
702 // None of the above
703 // coerce to string then buffer
704 body = Buffer.from(String(body));
705 }
706
707 this[INTERNALS$2] = {
708 body,
709 boundary,
710 disturbed: false,
711 error: null
712 };
713 this.size = size;
714
715 if (body instanceof Stream) {
716 body.on('error', err => {
717 const error = err instanceof FetchBaseError ?
718 err :
719 new FetchError(`Invalid response body while trying to fetch ${this.url}: ${err.message}`, 'system', err);
720 this[INTERNALS$2].error = error;
721 });
722 }
723 }
724
725 get body() {
726 return this[INTERNALS$2].body;
727 }
728
729 get bodyUsed() {
730 return this[INTERNALS$2].disturbed;
731 }
732
733 /**
734 * Decode response as ArrayBuffer
735 *
736 * @return Promise
737 */
738 async arrayBuffer() {
739 const {buffer, byteOffset, byteLength} = await consumeBody(this);
740 return buffer.slice(byteOffset, byteOffset + byteLength);
741 }
742
743 /**
744 * Return raw response as Blob
745 *
746 * @return Promise
747 */
748 async blob() {
749 const ct = (this.headers && this.headers.get('content-type')) || (this[INTERNALS$2].body && this[INTERNALS$2].body.type) || '';
750 const buf = await this.buffer();
751
752 return new fetchBlob([buf], {
753 type: ct
754 });
755 }
756
757 /**
758 * Decode response as json
759 *
760 * @return Promise
761 */
762 async json() {
763 const buffer = await consumeBody(this);
764 return JSON.parse(buffer.toString());
765 }
766
767 /**
768 * Decode response as text
769 *
770 * @return Promise
771 */
772 async text() {
773 const buffer = await consumeBody(this);
774 return buffer.toString();
775 }
776
777 /**
778 * Decode response as buffer (non-spec api)
779 *
780 * @return Promise
781 */
782 buffer() {
783 return consumeBody(this);
784 }
785}
786
787// In browsers, all properties are enumerable.
788Object.defineProperties(Body.prototype, {
789 body: {enumerable: true},
790 bodyUsed: {enumerable: true},
791 arrayBuffer: {enumerable: true},
792 blob: {enumerable: true},
793 json: {enumerable: true},
794 text: {enumerable: true}
795});
796
797/**
798 * Consume and convert an entire Body to a Buffer.
799 *
800 * Ref: https://fetch.spec.whatwg.org/#concept-body-consume-body
801 *
802 * @return Promise
803 */
804async function consumeBody(data) {
805 if (data[INTERNALS$2].disturbed) {
806 throw new TypeError(`body used already for: ${data.url}`);
807 }
808
809 data[INTERNALS$2].disturbed = true;
810
811 if (data[INTERNALS$2].error) {
812 throw data[INTERNALS$2].error;
813 }
814
815 let {body} = data;
816
817 // Body is null
818 if (body === null) {
819 return Buffer.alloc(0);
820 }
821
822 // Body is blob
823 if (isBlob(body)) {
824 body = body.stream();
825 }
826
827 // Body is buffer
828 if (Buffer.isBuffer(body)) {
829 return body;
830 }
831
832 /* c8 ignore next 3 */
833 if (!(body instanceof Stream)) {
834 return Buffer.alloc(0);
835 }
836
837 // Body is stream
838 // get ready to actually consume the body
839 const accum = [];
840 let accumBytes = 0;
841
842 try {
843 for await (const chunk of body) {
844 if (data.size > 0 && accumBytes + chunk.length > data.size) {
845 const err = new FetchError(`content size at ${data.url} over limit: ${data.size}`, 'max-size');
846 body.destroy(err);
847 throw err;
848 }
849
850 accumBytes += chunk.length;
851 accum.push(chunk);
852 }
853 } catch (error) {
854 if (error instanceof FetchBaseError) {
855 throw error;
856 } else {
857 // Other errors, such as incorrect content-encoding
858 throw new FetchError(`Invalid response body while trying to fetch ${data.url}: ${error.message}`, 'system', error);
859 }
860 }
861
862 if (body.readableEnded === true || body._readableState.ended === true) {
863 try {
864 if (accum.every(c => typeof c === 'string')) {
865 return Buffer.from(accum.join(''));
866 }
867
868 return Buffer.concat(accum, accumBytes);
869 } catch (error) {
870 throw new FetchError(`Could not create Buffer from response body for ${data.url}: ${error.message}`, 'system', error);
871 }
872 } else {
873 throw new FetchError(`Premature close of server response while trying to fetch ${data.url}`);
874 }
875}
876
877/**
878 * Clone body given Res/Req instance
879 *
880 * @param Mixed instance Response or Request instance
881 * @param String highWaterMark highWaterMark for both PassThrough body streams
882 * @return Mixed
883 */
884const clone = (instance, highWaterMark) => {
885 let p1;
886 let p2;
887 let {body} = instance;
888
889 // Don't allow cloning a used body
890 if (instance.bodyUsed) {
891 throw new Error('cannot clone body after it is used');
892 }
893
894 // Check that body is a stream and not form-data object
895 // note: we can't clone the form-data object without having it as a dependency
896 if ((body instanceof Stream) && (typeof body.getBoundary !== 'function')) {
897 // Tee instance body
898 p1 = new PassThrough({highWaterMark});
899 p2 = new PassThrough({highWaterMark});
900 body.pipe(p1);
901 body.pipe(p2);
902 // Set instance body to teed body and return the other teed body
903 instance[INTERNALS$2].body = p1;
904 body = p2;
905 }
906
907 return body;
908};
909
910/**
911 * Performs the operation "extract a `Content-Type` value from |object|" as
912 * specified in the specification:
913 * https://fetch.spec.whatwg.org/#concept-bodyinit-extract
914 *
915 * This function assumes that instance.body is present.
916 *
917 * @param {any} body Any options.body input
918 * @returns {string | null}
919 */
920const extractContentType = (body, request) => {
921 // Body is null or undefined
922 if (body === null) {
923 return null;
924 }
925
926 // Body is string
927 if (typeof body === 'string') {
928 return 'text/plain;charset=UTF-8';
929 }
930
931 // Body is a URLSearchParams
932 if (isURLSearchParameters(body)) {
933 return 'application/x-www-form-urlencoded;charset=UTF-8';
934 }
935
936 // Body is blob
937 if (isBlob(body)) {
938 return body.type || null;
939 }
940
941 // Body is a Buffer (Buffer, ArrayBuffer or ArrayBufferView)
942 if (Buffer.isBuffer(body) || types.isAnyArrayBuffer(body) || ArrayBuffer.isView(body)) {
943 return null;
944 }
945
946 // Detect form data input from form-data module
947 if (body && typeof body.getBoundary === 'function') {
948 return `multipart/form-data;boundary=${body.getBoundary()}`;
949 }
950
951 if (isFormData(body)) {
952 return `multipart/form-data; boundary=${request[INTERNALS$2].boundary}`;
953 }
954
955 // Body is stream - can't really do much about this
956 if (body instanceof Stream) {
957 return null;
958 }
959
960 // Body constructor defaults other things to string
961 return 'text/plain;charset=UTF-8';
962};
963
964/**
965 * The Fetch Standard treats this as if "total bytes" is a property on the body.
966 * For us, we have to explicitly get it with a function.
967 *
968 * ref: https://fetch.spec.whatwg.org/#concept-body-total-bytes
969 *
970 * @param {any} obj.body Body object from the Body instance.
971 * @returns {number | null}
972 */
973const getTotalBytes = request => {
974 const {body} = request;
975
976 // Body is null or undefined
977 if (body === null) {
978 return 0;
979 }
980
981 // Body is Blob
982 if (isBlob(body)) {
983 return body.size;
984 }
985
986 // Body is Buffer
987 if (Buffer.isBuffer(body)) {
988 return body.length;
989 }
990
991 // Detect form data input from form-data module
992 if (body && typeof body.getLengthSync === 'function') {
993 return body.hasKnownLength && body.hasKnownLength() ? body.getLengthSync() : null;
994 }
995
996 // Body is a spec-compliant form-data
997 if (isFormData(body)) {
998 return getFormDataLength(request[INTERNALS$2].boundary);
999 }
1000
1001 // Body is stream
1002 return null;
1003};
1004
1005/**
1006 * Write a Body to a Node.js WritableStream (e.g. http.Request) object.
1007 *
1008 * @param {Stream.Writable} dest The stream to write to.
1009 * @param obj.body Body object from the Body instance.
1010 * @returns {void}
1011 */
1012const writeToStream = (dest, {body}) => {
1013 if (body === null) {
1014 // Body is null
1015 dest.end();
1016 } else if (isBlob(body)) {
1017 // Body is Blob
1018 body.stream().pipe(dest);
1019 } else if (Buffer.isBuffer(body)) {
1020 // Body is buffer
1021 dest.write(body);
1022 dest.end();
1023 } else {
1024 // Body is stream
1025 body.pipe(dest);
1026 }
1027};
1028
1029/**
1030 * Headers.js
1031 *
1032 * Headers class offers convenient helpers
1033 */
1034
1035const validateHeaderName = typeof http.validateHeaderName === 'function' ?
1036 http.validateHeaderName :
1037 name => {
1038 if (!/^[\^`\-\w!#$%&'*+.|~]+$/.test(name)) {
1039 const err = new TypeError(`Header name must be a valid HTTP token [${name}]`);
1040 Object.defineProperty(err, 'code', {value: 'ERR_INVALID_HTTP_TOKEN'});
1041 throw err;
1042 }
1043 };
1044
1045const validateHeaderValue = typeof http.validateHeaderValue === 'function' ?
1046 http.validateHeaderValue :
1047 (name, value) => {
1048 if (/[^\t\u0020-\u007E\u0080-\u00FF]/.test(value)) {
1049 const err = new TypeError(`Invalid character in header content ["${name}"]`);
1050 Object.defineProperty(err, 'code', {value: 'ERR_INVALID_CHAR'});
1051 throw err;
1052 }
1053 };
1054
1055/**
1056 * @typedef {Headers | Record<string, string> | Iterable<readonly [string, string]> | Iterable<Iterable<string>>} HeadersInit
1057 */
1058
1059/**
1060 * This Fetch API interface allows you to perform various actions on HTTP request and response headers.
1061 * These actions include retrieving, setting, adding to, and removing.
1062 * A Headers object has an associated header list, which is initially empty and consists of zero or more name and value pairs.
1063 * You can add to this using methods like append() (see Examples.)
1064 * In all methods of this interface, header names are matched by case-insensitive byte sequence.
1065 *
1066 */
1067class Headers extends URLSearchParams {
1068 /**
1069 * Headers class
1070 *
1071 * @constructor
1072 * @param {HeadersInit} [init] - Response headers
1073 */
1074 constructor(init) {
1075 // Validate and normalize init object in [name, value(s)][]
1076 /** @type {string[][]} */
1077 let result = [];
1078 if (init instanceof Headers) {
1079 const raw = init.raw();
1080 for (const [name, values] of Object.entries(raw)) {
1081 result.push(...values.map(value => [name, value]));
1082 }
1083 } else if (init == null) ; else if (typeof init === 'object' && !types.isBoxedPrimitive(init)) {
1084 const method = init[Symbol.iterator];
1085 // eslint-disable-next-line no-eq-null, eqeqeq
1086 if (method == null) {
1087 // Record<ByteString, ByteString>
1088 result.push(...Object.entries(init));
1089 } else {
1090 if (typeof method !== 'function') {
1091 throw new TypeError('Header pairs must be iterable');
1092 }
1093
1094 // Sequence<sequence<ByteString>>
1095 // Note: per spec we have to first exhaust the lists then process them
1096 result = [...init]
1097 .map(pair => {
1098 if (
1099 typeof pair !== 'object' || types.isBoxedPrimitive(pair)
1100 ) {
1101 throw new TypeError('Each header pair must be an iterable object');
1102 }
1103
1104 return [...pair];
1105 }).map(pair => {
1106 if (pair.length !== 2) {
1107 throw new TypeError('Each header pair must be a name/value tuple');
1108 }
1109
1110 return [...pair];
1111 });
1112 }
1113 } else {
1114 throw new TypeError('Failed to construct \'Headers\': The provided value is not of type \'(sequence<sequence<ByteString>> or record<ByteString, ByteString>)');
1115 }
1116
1117 // Validate and lowercase
1118 result =
1119 result.length > 0 ?
1120 result.map(([name, value]) => {
1121 validateHeaderName(name);
1122 validateHeaderValue(name, String(value));
1123 return [String(name).toLowerCase(), String(value)];
1124 }) :
1125 undefined;
1126
1127 super(result);
1128
1129 // Returning a Proxy that will lowercase key names, validate parameters and sort keys
1130 // eslint-disable-next-line no-constructor-return
1131 return new Proxy(this, {
1132 get(target, p, receiver) {
1133 switch (p) {
1134 case 'append':
1135 case 'set':
1136 return (name, value) => {
1137 validateHeaderName(name);
1138 validateHeaderValue(name, String(value));
1139 return URLSearchParams.prototype[p].call(
1140 receiver,
1141 String(name).toLowerCase(),
1142 String(value)
1143 );
1144 };
1145
1146 case 'delete':
1147 case 'has':
1148 case 'getAll':
1149 return name => {
1150 validateHeaderName(name);
1151 return URLSearchParams.prototype[p].call(
1152 receiver,
1153 String(name).toLowerCase()
1154 );
1155 };
1156
1157 case 'keys':
1158 return () => {
1159 target.sort();
1160 return new Set(URLSearchParams.prototype.keys.call(target)).keys();
1161 };
1162
1163 default:
1164 return Reflect.get(target, p, receiver);
1165 }
1166 }
1167 /* c8 ignore next */
1168 });
1169 }
1170
1171 get [Symbol.toStringTag]() {
1172 return this.constructor.name;
1173 }
1174
1175 toString() {
1176 return Object.prototype.toString.call(this);
1177 }
1178
1179 get(name) {
1180 const values = this.getAll(name);
1181 if (values.length === 0) {
1182 return null;
1183 }
1184
1185 let value = values.join(', ');
1186 if (/^content-encoding$/i.test(name)) {
1187 value = value.toLowerCase();
1188 }
1189
1190 return value;
1191 }
1192
1193 forEach(callback) {
1194 for (const name of this.keys()) {
1195 callback(this.get(name), name);
1196 }
1197 }
1198
1199 * values() {
1200 for (const name of this.keys()) {
1201 yield this.get(name);
1202 }
1203 }
1204
1205 /**
1206 * @type {() => IterableIterator<[string, string]>}
1207 */
1208 * entries() {
1209 for (const name of this.keys()) {
1210 yield [name, this.get(name)];
1211 }
1212 }
1213
1214 [Symbol.iterator]() {
1215 return this.entries();
1216 }
1217
1218 /**
1219 * Node-fetch non-spec method
1220 * returning all headers and their values as array
1221 * @returns {Record<string, string[]>}
1222 */
1223 raw() {
1224 return [...this.keys()].reduce((result, key) => {
1225 result[key] = this.getAll(key);
1226 return result;
1227 }, {});
1228 }
1229
1230 /**
1231 * For better console.log(headers) and also to convert Headers into Node.js Request compatible format
1232 */
1233 [Symbol.for('nodejs.util.inspect.custom')]() {
1234 return [...this.keys()].reduce((result, key) => {
1235 const values = this.getAll(key);
1236 // Http.request() only supports string as Host header.
1237 // This hack makes specifying custom Host header possible.
1238 if (key === 'host') {
1239 result[key] = values[0];
1240 } else {
1241 result[key] = values.length > 1 ? values : values[0];
1242 }
1243
1244 return result;
1245 }, {});
1246 }
1247}
1248
1249/**
1250 * Re-shaping object for Web IDL tests
1251 * Only need to do it for overridden methods
1252 */
1253Object.defineProperties(
1254 Headers.prototype,
1255 ['get', 'entries', 'forEach', 'values'].reduce((result, property) => {
1256 result[property] = {enumerable: true};
1257 return result;
1258 }, {})
1259);
1260
1261/**
1262 * Create a Headers object from an http.IncomingMessage.rawHeaders, ignoring those that do
1263 * not conform to HTTP grammar productions.
1264 * @param {import('http').IncomingMessage['rawHeaders']} headers
1265 */
1266function fromRawHeaders(headers = []) {
1267 return new Headers(
1268 headers
1269 // Split into pairs
1270 .reduce((result, value, index, array) => {
1271 if (index % 2 === 0) {
1272 result.push(array.slice(index, index + 2));
1273 }
1274
1275 return result;
1276 }, [])
1277 .filter(([name, value]) => {
1278 try {
1279 validateHeaderName(name);
1280 validateHeaderValue(name, String(value));
1281 return true;
1282 } catch {
1283 return false;
1284 }
1285 })
1286
1287 );
1288}
1289
1290const redirectStatus = new Set([301, 302, 303, 307, 308]);
1291
1292/**
1293 * Redirect code matching
1294 *
1295 * @param {number} code - Status code
1296 * @return {boolean}
1297 */
1298const isRedirect = code => {
1299 return redirectStatus.has(code);
1300};
1301
1302/**
1303 * Response.js
1304 *
1305 * Response class provides content decoding
1306 */
1307
1308const INTERNALS$1 = Symbol('Response internals');
1309
1310/**
1311 * Response class
1312 *
1313 * @param Stream body Readable stream
1314 * @param Object opts Response options
1315 * @return Void
1316 */
1317class Response extends Body {
1318 constructor(body = null, options = {}) {
1319 super(body, options);
1320
1321 const status = options.status || 200;
1322 const headers = new Headers(options.headers);
1323
1324 if (body !== null && !headers.has('Content-Type')) {
1325 const contentType = extractContentType(body);
1326 if (contentType) {
1327 headers.append('Content-Type', contentType);
1328 }
1329 }
1330
1331 this[INTERNALS$1] = {
1332 url: options.url,
1333 status,
1334 statusText: options.statusText || '',
1335 headers,
1336 counter: options.counter,
1337 highWaterMark: options.highWaterMark
1338 };
1339 }
1340
1341 get url() {
1342 return this[INTERNALS$1].url || '';
1343 }
1344
1345 get status() {
1346 return this[INTERNALS$1].status;
1347 }
1348
1349 /**
1350 * Convenience property representing if the request ended normally
1351 */
1352 get ok() {
1353 return this[INTERNALS$1].status >= 200 && this[INTERNALS$1].status < 300;
1354 }
1355
1356 get redirected() {
1357 return this[INTERNALS$1].counter > 0;
1358 }
1359
1360 get statusText() {
1361 return this[INTERNALS$1].statusText;
1362 }
1363
1364 get headers() {
1365 return this[INTERNALS$1].headers;
1366 }
1367
1368 get highWaterMark() {
1369 return this[INTERNALS$1].highWaterMark;
1370 }
1371
1372 /**
1373 * Clone this response
1374 *
1375 * @return Response
1376 */
1377 clone() {
1378 return new Response(clone(this, this.highWaterMark), {
1379 url: this.url,
1380 status: this.status,
1381 statusText: this.statusText,
1382 headers: this.headers,
1383 ok: this.ok,
1384 redirected: this.redirected,
1385 size: this.size
1386 });
1387 }
1388
1389 /**
1390 * @param {string} url The URL that the new response is to originate from.
1391 * @param {number} status An optional status code for the response (e.g., 302.)
1392 * @returns {Response} A Response object.
1393 */
1394 static redirect(url, status = 302) {
1395 if (!isRedirect(status)) {
1396 throw new RangeError('Failed to execute "redirect" on "response": Invalid status code');
1397 }
1398
1399 return new Response(null, {
1400 headers: {
1401 location: new URL(url).toString()
1402 },
1403 status
1404 });
1405 }
1406
1407 get [Symbol.toStringTag]() {
1408 return 'Response';
1409 }
1410}
1411
1412Object.defineProperties(Response.prototype, {
1413 url: {enumerable: true},
1414 status: {enumerable: true},
1415 ok: {enumerable: true},
1416 redirected: {enumerable: true},
1417 statusText: {enumerable: true},
1418 headers: {enumerable: true},
1419 clone: {enumerable: true}
1420});
1421
1422const getSearch = parsedURL => {
1423 if (parsedURL.search) {
1424 return parsedURL.search;
1425 }
1426
1427 const lastOffset = parsedURL.href.length - 1;
1428 const hash = parsedURL.hash || (parsedURL.href[lastOffset] === '#' ? '#' : '');
1429 return parsedURL.href[lastOffset - hash.length] === '?' ? '?' : '';
1430};
1431
1432const INTERNALS = Symbol('Request internals');
1433
1434/**
1435 * Check if `obj` is an instance of Request.
1436 *
1437 * @param {*} obj
1438 * @return {boolean}
1439 */
1440const isRequest = object => {
1441 return (
1442 typeof object === 'object' &&
1443 typeof object[INTERNALS] === 'object'
1444 );
1445};
1446
1447/**
1448 * Request class
1449 *
1450 * @param Mixed input Url or Request instance
1451 * @param Object init Custom options
1452 * @return Void
1453 */
1454class Request extends Body {
1455 constructor(input, init = {}) {
1456 let parsedURL;
1457
1458 // Normalize input and force URL to be encoded as UTF-8 (https://github.com/node-fetch/node-fetch/issues/245)
1459 if (isRequest(input)) {
1460 parsedURL = new URL(input.url);
1461 } else {
1462 parsedURL = new URL(input);
1463 input = {};
1464 }
1465
1466 let method = init.method || input.method || 'GET';
1467 method = method.toUpperCase();
1468
1469 // eslint-disable-next-line no-eq-null, eqeqeq
1470 if (((init.body != null || isRequest(input)) && input.body !== null) &&
1471 (method === 'GET' || method === 'HEAD')) {
1472 throw new TypeError('Request with GET/HEAD method cannot have body');
1473 }
1474
1475 const inputBody = init.body ?
1476 init.body :
1477 (isRequest(input) && input.body !== null ?
1478 clone(input) :
1479 null);
1480
1481 super(inputBody, {
1482 size: init.size || input.size || 0
1483 });
1484
1485 const headers = new Headers(init.headers || input.headers || {});
1486
1487 if (inputBody !== null && !headers.has('Content-Type')) {
1488 const contentType = extractContentType(inputBody, this);
1489 if (contentType) {
1490 headers.append('Content-Type', contentType);
1491 }
1492 }
1493
1494 let signal = isRequest(input) ?
1495 input.signal :
1496 null;
1497 if ('signal' in init) {
1498 signal = init.signal;
1499 }
1500
1501 if (signal !== null && !isAbortSignal(signal)) {
1502 throw new TypeError('Expected signal to be an instanceof AbortSignal');
1503 }
1504
1505 this[INTERNALS] = {
1506 method,
1507 redirect: init.redirect || input.redirect || 'follow',
1508 headers,
1509 parsedURL,
1510 signal
1511 };
1512
1513 // Node-fetch-only options
1514 this.follow = init.follow === undefined ? (input.follow === undefined ? 20 : input.follow) : init.follow;
1515 this.compress = init.compress === undefined ? (input.compress === undefined ? true : input.compress) : init.compress;
1516 this.counter = init.counter || input.counter || 0;
1517 this.agent = init.agent || input.agent;
1518 this.highWaterMark = init.highWaterMark || input.highWaterMark || 16384;
1519 this.insecureHTTPParser = init.insecureHTTPParser || input.insecureHTTPParser || false;
1520 }
1521
1522 get method() {
1523 return this[INTERNALS].method;
1524 }
1525
1526 get url() {
1527 return format(this[INTERNALS].parsedURL);
1528 }
1529
1530 get headers() {
1531 return this[INTERNALS].headers;
1532 }
1533
1534 get redirect() {
1535 return this[INTERNALS].redirect;
1536 }
1537
1538 get signal() {
1539 return this[INTERNALS].signal;
1540 }
1541
1542 /**
1543 * Clone this request
1544 *
1545 * @return Request
1546 */
1547 clone() {
1548 return new Request(this);
1549 }
1550
1551 get [Symbol.toStringTag]() {
1552 return 'Request';
1553 }
1554}
1555
1556Object.defineProperties(Request.prototype, {
1557 method: {enumerable: true},
1558 url: {enumerable: true},
1559 headers: {enumerable: true},
1560 redirect: {enumerable: true},
1561 clone: {enumerable: true},
1562 signal: {enumerable: true}
1563});
1564
1565/**
1566 * Convert a Request to Node.js http request options.
1567 *
1568 * @param Request A Request instance
1569 * @return Object The options object to be passed to http.request
1570 */
1571const getNodeRequestOptions = request => {
1572 const {parsedURL} = request[INTERNALS];
1573 const headers = new Headers(request[INTERNALS].headers);
1574
1575 // Fetch step 1.3
1576 if (!headers.has('Accept')) {
1577 headers.set('Accept', '*/*');
1578 }
1579
1580 // HTTP-network-or-cache fetch steps 2.4-2.7
1581 let contentLengthValue = null;
1582 if (request.body === null && /^(post|put)$/i.test(request.method)) {
1583 contentLengthValue = '0';
1584 }
1585
1586 if (request.body !== null) {
1587 const totalBytes = getTotalBytes(request);
1588 // Set Content-Length if totalBytes is a number (that is not NaN)
1589 if (typeof totalBytes === 'number' && !Number.isNaN(totalBytes)) {
1590 contentLengthValue = String(totalBytes);
1591 }
1592 }
1593
1594 if (contentLengthValue) {
1595 headers.set('Content-Length', contentLengthValue);
1596 }
1597
1598 // HTTP-network-or-cache fetch step 2.11
1599 if (!headers.has('User-Agent')) {
1600 headers.set('User-Agent', 'node-fetch');
1601 }
1602
1603 // HTTP-network-or-cache fetch step 2.15
1604 if (request.compress && !headers.has('Accept-Encoding')) {
1605 headers.set('Accept-Encoding', 'gzip,deflate,br');
1606 }
1607
1608 let {agent} = request;
1609 if (typeof agent === 'function') {
1610 agent = agent(parsedURL);
1611 }
1612
1613 if (!headers.has('Connection') && !agent) {
1614 headers.set('Connection', 'close');
1615 }
1616
1617 // HTTP-network fetch step 4.2
1618 // chunked encoding is handled by Node.js
1619
1620 const search = getSearch(parsedURL);
1621
1622 // Manually spread the URL object instead of spread syntax
1623 const requestOptions = {
1624 path: parsedURL.pathname + search,
1625 pathname: parsedURL.pathname,
1626 hostname: parsedURL.hostname,
1627 protocol: parsedURL.protocol,
1628 port: parsedURL.port,
1629 hash: parsedURL.hash,
1630 search: parsedURL.search,
1631 query: parsedURL.query,
1632 href: parsedURL.href,
1633 method: request.method,
1634 headers: headers[Symbol.for('nodejs.util.inspect.custom')](),
1635 insecureHTTPParser: request.insecureHTTPParser,
1636 agent
1637 };
1638
1639 return requestOptions;
1640};
1641
1642/**
1643 * AbortError interface for cancelled requests
1644 */
1645class AbortError extends FetchBaseError {
1646 constructor(message, type = 'aborted') {
1647 super(message, type);
1648 }
1649}
1650
1651/**
1652 * Index.js
1653 *
1654 * a request API compatible with window.fetch
1655 *
1656 * All spec algorithm step numbers are based on https://fetch.spec.whatwg.org/commit-snapshots/ae716822cb3a61843226cd090eefc6589446c1d2/.
1657 */
1658
1659const supportedSchemas = new Set(['data:', 'http:', 'https:']);
1660
1661/**
1662 * Fetch function
1663 *
1664 * @param {string | URL | import('./request').default} url - Absolute url or Request instance
1665 * @param {*} [options_] - Fetch options
1666 * @return {Promise<import('./response').default>}
1667 */
1668async function fetch(url, options_) {
1669 return new Promise((resolve, reject) => {
1670 // Build request object
1671 const request = new Request(url, options_);
1672 const options = getNodeRequestOptions(request);
1673 if (!supportedSchemas.has(options.protocol)) {
1674 throw new TypeError(`node-fetch cannot load ${url}. URL scheme "${options.protocol.replace(/:$/, '')}" is not supported.`);
1675 }
1676
1677 if (options.protocol === 'data:') {
1678 const data = src(request.url);
1679 const response = new Response(data, {headers: {'Content-Type': data.typeFull}});
1680 resolve(response);
1681 return;
1682 }
1683
1684 // Wrap http.request into fetch
1685 const send = (options.protocol === 'https:' ? https : http).request;
1686 const {signal} = request;
1687 let response = null;
1688
1689 const abort = () => {
1690 const error = new AbortError('The operation was aborted.');
1691 reject(error);
1692 if (request.body && request.body instanceof Stream.Readable) {
1693 request.body.destroy(error);
1694 }
1695
1696 if (!response || !response.body) {
1697 return;
1698 }
1699
1700 response.body.emit('error', error);
1701 };
1702
1703 if (signal && signal.aborted) {
1704 abort();
1705 return;
1706 }
1707
1708 const abortAndFinalize = () => {
1709 abort();
1710 finalize();
1711 };
1712
1713 // Send request
1714 const request_ = send(options);
1715
1716 if (signal) {
1717 signal.addEventListener('abort', abortAndFinalize);
1718 }
1719
1720 const finalize = () => {
1721 request_.abort();
1722 if (signal) {
1723 signal.removeEventListener('abort', abortAndFinalize);
1724 }
1725 };
1726
1727 request_.on('error', err => {
1728 reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err));
1729 finalize();
1730 });
1731
1732 request_.on('response', response_ => {
1733 request_.setTimeout(0);
1734 const headers = fromRawHeaders(response_.rawHeaders);
1735
1736 // HTTP fetch step 5
1737 if (isRedirect(response_.statusCode)) {
1738 // HTTP fetch step 5.2
1739 const location = headers.get('Location');
1740
1741 // HTTP fetch step 5.3
1742 const locationURL = location === null ? null : new URL(location, request.url);
1743
1744 // HTTP fetch step 5.5
1745 switch (request.redirect) {
1746 case 'error':
1747 reject(new FetchError(`uri requested responds with a redirect, redirect mode is set to error: ${request.url}`, 'no-redirect'));
1748 finalize();
1749 return;
1750 case 'manual':
1751 // Node-fetch-specific step: make manual redirect a bit easier to use by setting the Location header value to the resolved URL.
1752 if (locationURL !== null) {
1753 // Handle corrupted header
1754 try {
1755 headers.set('Location', locationURL);
1756 /* c8 ignore next 3 */
1757 } catch (error) {
1758 reject(error);
1759 }
1760 }
1761
1762 break;
1763 case 'follow': {
1764 // HTTP-redirect fetch step 2
1765 if (locationURL === null) {
1766 break;
1767 }
1768
1769 // HTTP-redirect fetch step 5
1770 if (request.counter >= request.follow) {
1771 reject(new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect'));
1772 finalize();
1773 return;
1774 }
1775
1776 // HTTP-redirect fetch step 6 (counter increment)
1777 // Create a new Request object.
1778 const requestOptions = {
1779 headers: new Headers(request.headers),
1780 follow: request.follow,
1781 counter: request.counter + 1,
1782 agent: request.agent,
1783 compress: request.compress,
1784 method: request.method,
1785 body: request.body,
1786 signal: request.signal,
1787 size: request.size
1788 };
1789
1790 // HTTP-redirect fetch step 9
1791 if (response_.statusCode !== 303 && request.body && options_.body instanceof Stream.Readable) {
1792 reject(new FetchError('Cannot follow redirect with body being a readable stream', 'unsupported-redirect'));
1793 finalize();
1794 return;
1795 }
1796
1797 // HTTP-redirect fetch step 11
1798 if (response_.statusCode === 303 || ((response_.statusCode === 301 || response_.statusCode === 302) && request.method === 'POST')) {
1799 requestOptions.method = 'GET';
1800 requestOptions.body = undefined;
1801 requestOptions.headers.delete('content-length');
1802 }
1803
1804 // HTTP-redirect fetch step 15
1805 resolve(fetch(new Request(locationURL, requestOptions)));
1806 finalize();
1807 return;
1808 }
1809 // Do nothing
1810 }
1811 }
1812
1813 // Prepare response
1814 response_.once('end', () => {
1815 if (signal) {
1816 signal.removeEventListener('abort', abortAndFinalize);
1817 }
1818 });
1819
1820 let body = pipeline(response_, new PassThrough(), error => {
1821 reject(error);
1822 });
1823 // see https://github.com/nodejs/node/pull/29376
1824 if (process.version < 'v12.10') {
1825 response_.on('aborted', abortAndFinalize);
1826 }
1827
1828 const responseOptions = {
1829 url: request.url,
1830 status: response_.statusCode,
1831 statusText: response_.statusMessage,
1832 headers,
1833 size: request.size,
1834 counter: request.counter,
1835 highWaterMark: request.highWaterMark
1836 };
1837
1838 // HTTP-network fetch step 12.1.1.3
1839 const codings = headers.get('Content-Encoding');
1840
1841 // HTTP-network fetch step 12.1.1.4: handle content codings
1842
1843 // in following scenarios we ignore compression support
1844 // 1. compression support is disabled
1845 // 2. HEAD request
1846 // 3. no Content-Encoding header
1847 // 4. no content response (204)
1848 // 5. content not modified response (304)
1849 if (!request.compress || request.method === 'HEAD' || codings === null || response_.statusCode === 204 || response_.statusCode === 304) {
1850 response = new Response(body, responseOptions);
1851 resolve(response);
1852 return;
1853 }
1854
1855 // For Node v6+
1856 // Be less strict when decoding compressed responses, since sometimes
1857 // servers send slightly invalid responses that are still accepted
1858 // by common browsers.
1859 // Always using Z_SYNC_FLUSH is what cURL does.
1860 const zlibOptions = {
1861 flush: zlib.Z_SYNC_FLUSH,
1862 finishFlush: zlib.Z_SYNC_FLUSH
1863 };
1864
1865 // For gzip
1866 if (codings === 'gzip' || codings === 'x-gzip') {
1867 body = pipeline(body, zlib.createGunzip(zlibOptions), error => {
1868 reject(error);
1869 });
1870 response = new Response(body, responseOptions);
1871 resolve(response);
1872 return;
1873 }
1874
1875 // For deflate
1876 if (codings === 'deflate' || codings === 'x-deflate') {
1877 // Handle the infamous raw deflate response from old servers
1878 // a hack for old IIS and Apache servers
1879 const raw = pipeline(response_, new PassThrough(), error => {
1880 reject(error);
1881 });
1882 raw.once('data', chunk => {
1883 // See http://stackoverflow.com/questions/37519828
1884 if ((chunk[0] & 0x0F) === 0x08) {
1885 body = pipeline(body, zlib.createInflate(), error => {
1886 reject(error);
1887 });
1888 } else {
1889 body = pipeline(body, zlib.createInflateRaw(), error => {
1890 reject(error);
1891 });
1892 }
1893
1894 response = new Response(body, responseOptions);
1895 resolve(response);
1896 });
1897 return;
1898 }
1899
1900 // For br
1901 if (codings === 'br') {
1902 body = pipeline(body, zlib.createBrotliDecompress(), error => {
1903 reject(error);
1904 });
1905 response = new Response(body, responseOptions);
1906 resolve(response);
1907 return;
1908 }
1909
1910 // Otherwise, use response as-is
1911 response = new Response(body, responseOptions);
1912 resolve(response);
1913 });
1914
1915 writeToStream(request_, request);
1916 });
1917}
1918
1919function noop() { }
1920function safe_not_equal(a, b) {
1921 return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function');
1922}
1923
1924const subscriber_queue = [];
1925/**
1926 * Create a `Writable` store that allows both updating and reading by subscription.
1927 * @param {*=}value initial value
1928 * @param {StartStopNotifier=}start start and stop notifications for subscriptions
1929 */
1930function writable(value, start = noop) {
1931 let stop;
1932 const subscribers = [];
1933 function set(new_value) {
1934 if (safe_not_equal(value, new_value)) {
1935 value = new_value;
1936 if (stop) { // store is ready
1937 const run_queue = !subscriber_queue.length;
1938 for (let i = 0; i < subscribers.length; i += 1) {
1939 const s = subscribers[i];
1940 s[1]();
1941 subscriber_queue.push(s, value);
1942 }
1943 if (run_queue) {
1944 for (let i = 0; i < subscriber_queue.length; i += 2) {
1945 subscriber_queue[i][0](subscriber_queue[i + 1]);
1946 }
1947 subscriber_queue.length = 0;
1948 }
1949 }
1950 }
1951 }
1952 function update(fn) {
1953 set(fn(value));
1954 }
1955 function subscribe(run, invalidate = noop) {
1956 const subscriber = [run, invalidate];
1957 subscribers.push(subscriber);
1958 if (subscribers.length === 1) {
1959 stop = start(set) || noop;
1960 }
1961 run(value);
1962 return () => {
1963 const index = subscribers.indexOf(subscriber);
1964 if (index !== -1) {
1965 subscribers.splice(index, 1);
1966 }
1967 if (subscribers.length === 0) {
1968 stop();
1969 stop = null;
1970 }
1971 };
1972 }
1973 return { set, update, subscribe };
1974}
1975
1976/**
1977 * @param {import('../types').LoadResult} loaded
1978 * @returns {import('../types').LoadResult}
1979 */
1980function normalize(loaded) {
1981 // TODO should this behaviour be dev-only?
1982
1983 if (loaded.error) {
1984 const error = typeof loaded.error === 'string' ? new Error(loaded.error) : loaded.error;
1985 const status = loaded.status;
1986
1987 if (!(error instanceof Error)) {
1988 return {
1989 status: 500,
1990 error: new Error(
1991 `"error" property returned from load() must be a string or instance of Error, received type "${typeof error}"`
1992 )
1993 };
1994 }
1995
1996 if (!status || status < 400 || status > 599) {
1997 console.warn('"error" returned from load() without a valid status code — defaulting to 500');
1998 return { status: 500, error };
1999 }
2000
2001 return { status, error };
2002 }
2003
2004 if (loaded.redirect) {
2005 if (!loaded.status || Math.floor(loaded.status / 100) !== 3) {
2006 return {
2007 status: 500,
2008 error: new Error(
2009 '"redirect" property returned from load() must be accompanied by a 3xx status code'
2010 )
2011 };
2012 }
2013
2014 if (typeof loaded.redirect !== 'string') {
2015 return {
2016 status: 500,
2017 error: new Error('"redirect" property returned from load() must be a string')
2018 };
2019 }
2020 }
2021
2022 return loaded;
2023}
2024
2025/**
2026 * @param {{
2027 * request: import('../../types').Request;
2028 * options: import('../../types').RenderOptions;
2029 * $session: any;
2030 * route: import('../../types').Page;
2031 * status: number;
2032 * error: Error
2033 * }} opts
2034 * @returns {Promise<import('../../types').Response>}
2035 */
2036async function get_response({ request, options, $session, route, status = 200, error }) {
2037 const host = options.host || request.headers[options.host_header];
2038
2039 /** @type {Record<string, import('../../types').Response>} */
2040 const dependencies = {};
2041
2042 const serialized_session = try_serialize($session, (error) => {
2043 throw new Error(`Failed to serialize session data: ${error.message}`);
2044 });
2045
2046 /** @type {Array<{ url: string, payload: string }>} */
2047 const serialized_data = [];
2048
2049 const match = route && route.pattern.exec(request.path);
2050 const params = route && route.params(match);
2051
2052 const page = {
2053 host,
2054 path: request.path,
2055 query: request.query,
2056 params
2057 };
2058
2059 let uses_credentials = false;
2060
2061 /**
2062 * @param {string} url
2063 * @param {import('node-fetch').RequestInit} opts
2064 */
2065 const fetcher = async (url, opts = {}) => {
2066 if (options.local && url.startsWith(options.paths.assets)) {
2067 // when running `start`, or prerendering, `assets` should be
2068 // config.kit.paths.assets, but we should still be able to fetch
2069 // assets directly from `static`
2070 url = url.replace(options.paths.assets, '');
2071 }
2072
2073 const parsed = parse(url);
2074
2075 // TODO: fix type https://github.com/node-fetch/node-fetch/issues/1113
2076 if (opts.credentials !== 'omit') {
2077 uses_credentials = true;
2078 }
2079
2080 let response;
2081
2082 if (parsed.protocol) {
2083 // external fetch
2084 response = await fetch(parsed.href, opts);
2085 } else {
2086 // otherwise we're dealing with an internal fetch
2087 const resolved = resolve(request.path, parsed.pathname);
2088
2089 // is this a request for a static asset?
2090 const filename = resolved.slice(1);
2091 const filename_html = `${filename}/index.html`;
2092 const asset = options.manifest.assets.find(
2093 (d) => d.file === filename || d.file === filename_html
2094 );
2095
2096 if (asset) {
2097 if (options.get_static_file) {
2098 response = new Response(options.get_static_file(asset.file), {
2099 headers: {
2100 'content-type': asset.type
2101 }
2102 });
2103 } else {
2104 // TODO we need to know what protocol to use
2105 response = await fetch(`http://${page.host}/${asset.file}`, opts);
2106 }
2107 }
2108
2109 if (!response) {
2110 const rendered = await ssr(
2111 {
2112 host: request.host,
2113 method: opts.method || 'GET',
2114 headers: opts.headers || {}, // TODO inject credentials...
2115 path: resolved,
2116 body: opts.body,
2117 query: new URLSearchParams$1(parsed.query || '')
2118 },
2119 {
2120 ...options,
2121 fetched: url
2122 }
2123 );
2124
2125 if (rendered) {
2126 // TODO this is primarily for the benefit of the static case,
2127 // but could it be used elsewhere?
2128 dependencies[resolved] = rendered;
2129
2130 response = new Response(rendered.body, {
2131 status: rendered.status,
2132 headers: rendered.headers
2133 });
2134 }
2135 }
2136 }
2137
2138 if (response) {
2139 const clone = response.clone();
2140
2141 /** @type {import('../../types').Headers} */
2142 const headers = {};
2143 clone.headers.forEach((value, key) => {
2144 if (key !== 'etag') headers[key] = value;
2145 });
2146
2147 const payload = JSON.stringify({
2148 status: clone.status,
2149 statusText: clone.statusText,
2150 headers,
2151 body: await clone.text() // TODO handle binary data
2152 });
2153
2154 // TODO i guess we need to sanitize/escape this... somehow?
2155 serialized_data.push({ url, payload });
2156
2157 return response;
2158 }
2159
2160 return new Response('Not found', {
2161 status: 404
2162 });
2163 };
2164
2165 const parts = error ? [options.manifest.layout] : [options.manifest.layout, ...route.parts];
2166
2167 const component_promises = parts.map((loader) => loader());
2168 const components = [];
2169 const props_promises = [];
2170
2171 let context = {};
2172 let maxage;
2173
2174 for (let i = 0; i < component_promises.length; i += 1) {
2175 let loaded;
2176
2177 try {
2178 const mod = await component_promises[i];
2179 components[i] = mod.default;
2180
2181 if (options.only_prerender && !mod.prerender) {
2182 return;
2183 }
2184
2185 if (mod.preload) {
2186 throw new Error(
2187 'preload has been deprecated in favour of load. Please consult the documentation: https://kit.svelte.dev/docs#load'
2188 );
2189 }
2190
2191 loaded =
2192 mod.load &&
2193 (await mod.load.call(null, {
2194 page,
2195 get session() {
2196 uses_credentials = true;
2197 return $session;
2198 },
2199 fetch: fetcher,
2200 context: { ...context }
2201 }));
2202 } catch (e) {
2203 // if load fails when we're already rendering the
2204 // error page, there's not a lot we can do
2205 if (error) throw e;
2206
2207 loaded = { error: e, status: 500 };
2208 }
2209
2210 if (loaded) {
2211 loaded = normalize(loaded);
2212
2213 // TODO there's some logic that's duplicated in the client runtime,
2214 // it would be nice to DRY it out if possible
2215 if (loaded.error) {
2216 return await get_response({
2217 request,
2218 options,
2219 $session,
2220 route,
2221 status: loaded.status,
2222 error: loaded.error
2223 });
2224 }
2225
2226 if (loaded.redirect) {
2227 return {
2228 status: loaded.status,
2229 headers: {
2230 location: loaded.redirect
2231 }
2232 };
2233 }
2234
2235 if (loaded.context) {
2236 context = {
2237 ...context,
2238 ...loaded.context
2239 };
2240 }
2241
2242 maxage = loaded.maxage || 0;
2243
2244 props_promises[i] = loaded.props;
2245 }
2246 }
2247
2248 const session = writable($session);
2249 let session_tracking_active = false;
2250 const unsubscribe = session.subscribe(() => {
2251 if (session_tracking_active) uses_credentials = true;
2252 });
2253 session_tracking_active = true;
2254
2255 if (error) {
2256 if (options.dev) {
2257 error.stack = await options.get_stack(error);
2258 } else {
2259 // remove error.stack in production
2260 error.stack = String(error);
2261 }
2262 }
2263
2264 /** @type {Record<string, any>} */
2265 const props = {
2266 status,
2267 error,
2268 stores: {
2269 page: writable(null),
2270 navigating: writable(null),
2271 session
2272 },
2273 page,
2274 components
2275 };
2276
2277 // leveln (instead of levels[n]) makes it easy to avoid
2278 // unnecessary updates for layout components
2279 for (let i = 0; i < props_promises.length; i += 1) {
2280 props[`props_${i}`] = await props_promises[i];
2281 }
2282
2283 let rendered;
2284
2285 try {
2286 rendered = options.root.render(props);
2287 } catch (e) {
2288 if (error) throw e;
2289
2290 return await get_response({
2291 request,
2292 options,
2293 $session,
2294 route,
2295 status: 500,
2296 error: e
2297 });
2298 }
2299
2300 unsubscribe();
2301
2302 // TODO all the `route &&` stuff is messy
2303 const js_deps = route ? route.js : [];
2304 const css_deps = route ? route.css : [];
2305 const style = route ? route.style : '';
2306
2307 const s = JSON.stringify;
2308 const prefix = `${options.paths.assets}/${options.app_dir}`;
2309
2310 // TODO strip the AMP stuff out of the build if not relevant
2311 const links = options.amp
2312 ? `<style amp-custom>${
2313 style || (await Promise.all(css_deps.map((dep) => options.get_amp_css(dep)))).join('\n')
2314 }</style>`
2315 : [
2316 ...js_deps.map((dep) => `<link rel="modulepreload" href="${prefix}/${dep}">`),
2317 ...css_deps.map((dep) => `<link rel="stylesheet" href="${prefix}/${dep}">`)
2318 ].join('\n\t\t\t');
2319
2320 const init = options.amp
2321 ? `
2322 <style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style>
2323 <noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
2324 <script async src="https://cdn.ampproject.org/v0.js"></script>`
2325 : `
2326 <script type="module">
2327 import { start } from ${s(options.entry)};
2328 ${options.start_global ? `window.${options.start_global} = () => ` : ''}start({
2329 target: ${options.target ? `document.querySelector(${s(options.target)})` : 'document.body'},
2330 host: ${host ? s(host) : 'location.host'},
2331 paths: ${s(options.paths)},
2332 status: ${status},
2333 error: ${serialize_error(error)},
2334 session: ${serialized_session}
2335 });
2336 </script>`;
2337
2338 const head = [
2339 rendered.head,
2340 options.amp ? '' : `<style data-svelte>${style}</style>`,
2341 links,
2342 init
2343 ].join('\n\n');
2344
2345 const body = options.amp
2346 ? rendered.html
2347 : `${rendered.html}
2348
2349 ${serialized_data
2350 .map(({ url, payload }) => `<script type="svelte-data" url="${url}">${payload}</script>`)
2351 .join('\n\n\t\t\t')}
2352 `.replace(/^\t{2}/gm, '');
2353
2354 /** @type {import('../../types').Headers} */
2355 const headers = {
2356 'content-type': 'text/html'
2357 };
2358
2359 if (maxage) {
2360 headers['cache-control'] = `${uses_credentials ? 'private' : 'public'}, max-age=${maxage}`;
2361 }
2362
2363 return {
2364 status,
2365 headers,
2366 body: options.template({ head, body }),
2367 dependencies
2368 };
2369}
2370
2371/**
2372 * @param {import('../../types').Request} request
2373 * @param {any} context
2374 * @param {import('../../types').RenderOptions} options
2375 */
2376async function render_page(request, context, options) {
2377 const route = options.manifest.pages.find((route) => route.pattern.test(request.path));
2378
2379 const $session = await (options.setup.getSession && options.setup.getSession({ context }));
2380
2381 if (!route) {
2382 if (options.fetched) {
2383 // we came here because of a bad request in a `load` function.
2384 // rather than render the error page — which could lead to an
2385 // infinite loop, if the `load` belonged to the root layout,
2386 // we respond with a bare-bones 500
2387 throw new Error(`Bad request in load function: failed to fetch ${options.fetched}`);
2388 }
2389
2390 return await get_response({
2391 request,
2392 options,
2393 $session,
2394 route,
2395 status: 404,
2396 error: new Error(`Not found: ${request.path}`)
2397 });
2398 }
2399
2400 return await get_response({
2401 request,
2402 options,
2403 $session,
2404 route,
2405 status: 200,
2406 error: null
2407 });
2408}
2409
2410/**
2411 * @param {any} data
2412 * @param {(error: Error) => void} [fail]
2413 */
2414function try_serialize(data, fail) {
2415 try {
2416 return devalue(data);
2417 } catch (err) {
2418 if (fail) fail(err);
2419 return null;
2420 }
2421}
2422
2423// Ensure we return something truthy so the client will not re-render the page over the error
2424
2425/** @param {Error} error */
2426function serialize_error(error) {
2427 if (!error) return null;
2428 let serialized = try_serialize(error);
2429 if (!serialized) {
2430 const { name, message, stack } = error;
2431 serialized = try_serialize({ name, message, stack });
2432 }
2433 if (!serialized) {
2434 serialized = '{}';
2435 }
2436 return serialized;
2437}
2438
2439/**
2440 * @param {import('../../types').Request} request
2441 * @param {*} context // TODO
2442 * @param {import('../../types').RenderOptions} options
2443 * @returns {Promise<import('../../types').Response>}
2444 */
2445function render_route(request, context, options) {
2446 const route = options.manifest.endpoints.find((route) => route.pattern.test(request.path));
2447 if (!route) return null;
2448
2449 return route.load().then(async (mod) => {
2450 const handler = mod[request.method.toLowerCase().replace('delete', 'del')]; // 'delete' is a reserved word
2451
2452 if (handler) {
2453 const match = route.pattern.exec(request.path);
2454 const params = route.params(match);
2455
2456 const response = await handler(
2457 {
2458 host: options.host || request.headers[options.host_header || 'host'],
2459 path: request.path,
2460 headers: request.headers,
2461 query: request.query,
2462 body: request.body,
2463 params
2464 },
2465 context
2466 );
2467
2468 if (typeof response !== 'object' || response.body == null) {
2469 return {
2470 status: 500,
2471 body: `Invalid response from route ${request.path}; ${
2472 response.body == null ? 'body is missing' : `expected an object, got ${typeof response}`
2473 }`,
2474 headers: {}
2475 };
2476 }
2477
2478 let { status = 200, body, headers = {} } = response;
2479
2480 headers = lowercase_keys(headers);
2481
2482 if (
2483 (typeof body === 'object' && !('content-type' in headers)) ||
2484 headers['content-type'] === 'application/json'
2485 ) {
2486 headers = { ...headers, 'content-type': 'application/json' };
2487 body = JSON.stringify(body);
2488 }
2489
2490 return { status, body, headers };
2491 } else {
2492 return {
2493 status: 501,
2494 body: `${request.method} is not implemented for ${request.path}`,
2495 headers: {}
2496 };
2497 }
2498 });
2499}
2500
2501/** @param {Record<string, string>} obj */
2502function lowercase_keys(obj) {
2503 /** @type {Record<string, string>} */
2504 const clone = {};
2505
2506 for (const key in obj) {
2507 clone[key.toLowerCase()] = obj[key];
2508 }
2509
2510 return clone;
2511}
2512
2513/** @param {string} body */
2514function md5(body) {
2515 return createHash('md5').update(body).digest('hex');
2516}
2517
2518/**
2519 * @param {import('../../types').Request} request
2520 * @param {import('../../types').RenderOptions} options
2521 */
2522async function ssr(request, options) {
2523 if (request.path.endsWith('/') && request.path !== '/') {
2524 const q = request.query.toString();
2525
2526 return {
2527 status: 301,
2528 headers: {
2529 location: request.path.slice(0, -1) + (q ? `?${q}` : '')
2530 }
2531 };
2532 }
2533
2534 const { context, headers = {} } =
2535 (await (options.setup.prepare && options.setup.prepare({ headers: request.headers }))) || {};
2536
2537 try {
2538 const response = await (render_route(request, context, options) ||
2539 render_page(request, context, options));
2540
2541 if (response) {
2542 // inject ETags for 200 responses
2543 if (response.status === 200) {
2544 if (!/(no-store|immutable)/.test(response.headers['cache-control'])) {
2545 const etag = `"${md5(response.body)}"`;
2546
2547 if (request.headers['if-none-match'] === etag) {
2548 return {
2549 status: 304,
2550 headers: {},
2551 body: null
2552 };
2553 }
2554
2555 response.headers['etag'] = etag;
2556 }
2557 }
2558
2559 return {
2560 status: response.status,
2561 headers: { ...headers, ...response.headers },
2562 body: response.body,
2563 dependencies: response.dependencies
2564 };
2565 }
2566 } catch (e) {
2567 if (e && e.stack) {
2568 e.stack = await options.get_stack(e);
2569 }
2570
2571 console.error((e && e.stack) || e);
2572
2573 return {
2574 status: 500,
2575 headers,
2576 body: options.dev ? e.stack : e.message
2577 };
2578 }
2579}
2580
2581export { ssr };
2582
\No newline at end of file