@web-std/fetch
Advanced tools
| import { expectType, expectAssignable } from 'tsd'; | ||
| import AbortController from 'abort-controller'; | ||
| import fetch, { Request, Response, Headers, Body, FetchError, AbortError } from '.'; | ||
| import * as _fetch from '.'; | ||
| import __fetch = require('.'); | ||
| async function run() { | ||
| const getRes = await fetch('https://bigfile.com/test.zip'); | ||
| expectType<boolean>(getRes.ok); | ||
| expectType<number>(getRes.size); | ||
| expectType<number>(getRes.status); | ||
| expectType<string>(getRes.statusText); | ||
| expectType<() => Response>(getRes.clone); | ||
| // Test async iterator over body | ||
| expectType<NodeJS.ReadableStream | null>(getRes.body); | ||
| if (getRes.body) { | ||
| for await (const data of getRes.body) { | ||
| expectType<Buffer | string>(data); | ||
| } | ||
| } | ||
| // Test Buffer | ||
| expectType<Buffer>(await getRes.buffer()); | ||
| // Test arrayBuffer | ||
| expectType<ArrayBuffer>(await getRes.arrayBuffer()); | ||
| // Test JSON, returns unknown | ||
| expectType<unknown>(await getRes.json()); | ||
| // Headers iterable | ||
| expectType<Headers>(getRes.headers); | ||
| // Post | ||
| try { | ||
| const request = new Request('http://byjka.com/buka'); | ||
| expectType<string>(request.url); | ||
| expectType<Headers>(request.headers); | ||
| const headers = new Headers({ byaka: 'buke' }); | ||
| expectType<(a: string, b: string) => void>(headers.append); | ||
| expectType<(a: string) => string | null>(headers.get); | ||
| expectType<(name: string, value: string) => void>(headers.set); | ||
| expectType<(name: string) => void>(headers.delete); | ||
| expectType<() => IterableIterator<string>>(headers.keys); | ||
| expectType<() => IterableIterator<[string, string]>>(headers.entries); | ||
| expectType<() => IterableIterator<[string, string]>>(headers[Symbol.iterator]); | ||
| const postRes = await fetch(request, { method: 'POST', headers }); | ||
| expectType<Blob>(await postRes.blob()); | ||
| } catch (error) { | ||
| if (error instanceof FetchError) { | ||
| throw new TypeError(error.errno); | ||
| } | ||
| if (error instanceof AbortError) { | ||
| throw error; | ||
| } | ||
| } | ||
| // export * | ||
| const wildRes = await _fetch('https://google.com'); | ||
| expectType<boolean>(wildRes.ok); | ||
| expectType<number>(wildRes.size); | ||
| expectType<number>(wildRes.status); | ||
| expectType<string>(wildRes.statusText); | ||
| expectType<() => Response>(wildRes.clone); | ||
| // export = require | ||
| const reqRes = await __fetch('https://google.com'); | ||
| expectType<boolean>(reqRes.ok); | ||
| expectType<number>(reqRes.size); | ||
| expectType<number>(reqRes.status); | ||
| expectType<string>(reqRes.statusText); | ||
| expectType<() => Response>(reqRes.clone); | ||
| // Others | ||
| const response = new Response(); | ||
| expectType<string>(response.url); | ||
| expectAssignable<Body>(response); | ||
| const abortController = new AbortController() | ||
| const request = new Request('url', { signal: abortController.signal }); | ||
| expectAssignable<Body>(request); | ||
| new Headers({ 'Header': 'value' }); | ||
| // new Headers(['header', 'value']); // should not work | ||
| new Headers([['header', 'value']]); | ||
| new Headers(new Headers()); | ||
| new Headers([ | ||
| new Set(['a', '1']), | ||
| ['b', '2'], | ||
| new Map([['a', null], ['3', null]]).keys() | ||
| ]); | ||
| fetch.isRedirect = (code: number) => true; | ||
| } | ||
| run().finally(() => { | ||
| console.log('✅'); | ||
| }); |
+353
-144
@@ -14,5 +14,11 @@ 'use strict'; | ||
| const crypto = require('crypto'); | ||
| const multipartParser = require('@ssttevee/multipart-parser'); | ||
| const url = require('url'); | ||
| require('@web-std/form-data'); | ||
| class FetchBaseError extends Error { | ||
| /** | ||
| * @param {string} message | ||
| * @param {string} type | ||
| */ | ||
| constructor(message, type) { | ||
@@ -36,3 +42,13 @@ super(message); | ||
| /** | ||
| * @typedef {{ address?: string, code: string, dest?: string, errno: number, info?: object, message: string, path?: string, port?: number, syscall: string}} SystemError | ||
| * @typedef {{ | ||
| * address?: string | ||
| * code: string | ||
| * dest?: string | ||
| * errno: number | ||
| * info?: object | ||
| * message: string | ||
| * path?: string | ||
| * port?: number | ||
| * syscall: string | ||
| * }} SystemError | ||
| */ | ||
@@ -46,3 +62,3 @@ | ||
| * @param {string} message - Error message for human | ||
| * @param {string} [type] - Error type for machine | ||
| * @param {string} type - Error type for machine | ||
| * @param {SystemError} [systemError] - For Node.js system error | ||
@@ -73,16 +89,16 @@ */ | ||
| * | ||
| * @param {*} obj | ||
| * @param {any} object | ||
| * @return {obj is URLSearchParams} | ||
| */ | ||
| const isURLSearchParameters = object => { | ||
| const isURLSearchParameters = (object) => { | ||
| return ( | ||
| typeof object === 'object' && | ||
| typeof object.append === 'function' && | ||
| typeof object.delete === 'function' && | ||
| typeof object.get === 'function' && | ||
| typeof object.getAll === 'function' && | ||
| typeof object.has === 'function' && | ||
| typeof object.set === 'function' && | ||
| typeof object.sort === 'function' && | ||
| object[NAME] === 'URLSearchParams' | ||
| typeof object === "object" && | ||
| typeof object.append === "function" && | ||
| typeof object.delete === "function" && | ||
| typeof object.get === "function" && | ||
| typeof object.getAll === "function" && | ||
| typeof object.has === "function" && | ||
| typeof object.set === "function" && | ||
| typeof object.sort === "function" && | ||
| object[NAME] === "URLSearchParams" | ||
| ); | ||
@@ -97,9 +113,9 @@ }; | ||
| */ | ||
| const isBlob = object => { | ||
| const isBlob = (object) => { | ||
| return ( | ||
| typeof object === 'object' && | ||
| typeof object.arrayBuffer === 'function' && | ||
| typeof object.type === 'string' && | ||
| typeof object.stream === 'function' && | ||
| typeof object.constructor === 'function' && | ||
| typeof object === "object" && | ||
| typeof object.arrayBuffer === "function" && | ||
| typeof object.type === "string" && | ||
| typeof object.stream === "function" && | ||
| typeof object.constructor === "function" && | ||
| /^(Blob|File)$/.test(object[NAME]) | ||
@@ -117,13 +133,13 @@ ); | ||
| return ( | ||
| typeof object === 'object' && | ||
| typeof object.append === 'function' && | ||
| typeof object.set === 'function' && | ||
| typeof object.get === 'function' && | ||
| typeof object.getAll === 'function' && | ||
| typeof object.delete === 'function' && | ||
| typeof object.keys === 'function' && | ||
| typeof object.values === 'function' && | ||
| typeof object.entries === 'function' && | ||
| typeof object.constructor === 'function' && | ||
| object[NAME] === 'FormData' | ||
| typeof object === "object" && | ||
| typeof object.append === "function" && | ||
| typeof object.set === "function" && | ||
| typeof object.get === "function" && | ||
| typeof object.getAll === "function" && | ||
| typeof object.delete === "function" && | ||
| typeof object.keys === "function" && | ||
| typeof object.values === "function" && | ||
| typeof object.entries === "function" && | ||
| typeof object.constructor === "function" && | ||
| object[NAME] === "FormData" | ||
| ); | ||
@@ -138,8 +154,8 @@ } | ||
| */ | ||
| const isMultipartFormDataStream = value => { | ||
| const isMultipartFormDataStream = (value) => { | ||
| return ( | ||
| value instanceof Stream && | ||
| typeof value.getBoundary === 'function' && | ||
| typeof value.hasKnownLength === 'function' && | ||
| typeof value.getLengthSync === 'function' | ||
| value instanceof Stream === true && | ||
| typeof value.getBoundary === "function" && | ||
| typeof value.hasKnownLength === "function" && | ||
| typeof value.getLengthSync === "function" | ||
| ); | ||
@@ -151,11 +167,9 @@ }; | ||
| * | ||
| * @param {*} obj | ||
| * @param {any} object | ||
| * @return {obj is AbortSignal} | ||
| */ | ||
| const isAbortSignal = object => { | ||
| const isAbortSignal = (object) => { | ||
| return ( | ||
| typeof object === 'object' && ( | ||
| object[NAME] === 'AbortSignal' || | ||
| object[NAME] === 'EventTarget' | ||
| ) | ||
| typeof object === "object" && | ||
| (object[NAME] === "AbortSignal" || object[NAME] === "EventTarget") | ||
| ); | ||
@@ -170,11 +184,18 @@ }; | ||
| */ | ||
| const isReadableStream = value => { | ||
| const isReadableStream = (value) => { | ||
| return ( | ||
| typeof value === 'object' && | ||
| typeof value.getReader === 'function' && | ||
| typeof value.cancel === 'function' && | ||
| typeof value.tee === 'function' | ||
| typeof value === "object" && | ||
| typeof value.getReader === "function" && | ||
| typeof value.cancel === "function" && | ||
| typeof value.tee === "function" | ||
| ); | ||
| }; | ||
| /** | ||
| * | ||
| * @param {any} value | ||
| * @returns {value is Iterable<unknown>} | ||
| */ | ||
| const isIterable = (value) => value && Symbol.iterator in value; | ||
| const carriage = '\r\n'; | ||
@@ -203,4 +224,5 @@ const dashes = '-'.repeat(2); | ||
| if (isBlob(field)) { | ||
| header += `; filename="${field.name}"${carriage}`; | ||
| header += `Content-Type: ${field.type || 'application/octet-stream'}`; | ||
| const { name = 'blob', type } = /** @type {Blob & {name?:string}} */ (field); | ||
| header += `; filename="${name}"${carriage}`; | ||
| header += `Content-Type: ${type || 'application/octet-stream'}`; | ||
| } | ||
@@ -260,2 +282,24 @@ | ||
| /** | ||
| * @param {Body & {headers?:Headers}} source | ||
| */ | ||
| const toFormData = async ({ body, headers }) => { | ||
| const contentType = headers?.get('Content-Type') || ''; | ||
| const [type, boundary] = contentType.split(/\s*;\s*boundary=/); | ||
| if (type === 'multipart/form-data' && boundary != null && body != null) { | ||
| const form = new FormData(); | ||
| const parts = multipartParser.iterateMultipart(body, boundary); | ||
| for await (const { name, data, filename, contentType } of parts) { | ||
| if (filename) { | ||
| form.append(name, new File([data], filename, { type: contentType })); | ||
| } else { | ||
| form.append(name, new TextDecoder().decode(data), filename); | ||
| } | ||
| } | ||
| return form | ||
| } else { | ||
| throw new TypeError('Could not parse content as FormData.') | ||
| } | ||
| }; | ||
| const encoder = new util.TextEncoder(); | ||
@@ -275,3 +319,2 @@ const decoder = new util.TextDecoder(); | ||
| // @ts-check | ||
| const {readableHighWaterMark} = new Stream.Readable(); | ||
@@ -286,6 +329,3 @@ | ||
| * Ref: https://fetch.spec.whatwg.org/#body | ||
| * | ||
| * @param {BodyInit} body Readable stream | ||
| * @param Object opts Response options | ||
| * @return Void | ||
| * @implements {globalThis.Body} | ||
| */ | ||
@@ -295,3 +335,3 @@ | ||
| /** | ||
| * @param {BodyInit|Stream} body | ||
| * @param {BodyInit|Stream|null} body | ||
| * @param {{size?:number}} options | ||
@@ -385,3 +425,3 @@ */ | ||
| get headers() { | ||
| return null; | ||
| return undefined; | ||
| } | ||
@@ -439,2 +479,10 @@ | ||
| } | ||
| /** | ||
| * @returns {Promise<FormData>} | ||
| */ | ||
| async formData() { | ||
| return toFormData(this) | ||
| } | ||
| } | ||
@@ -449,3 +497,4 @@ | ||
| json: {enumerable: true}, | ||
| text: {enumerable: true} | ||
| text: {enumerable: true}, | ||
| formData: {enumerable: true} | ||
| }); | ||
@@ -482,4 +531,5 @@ | ||
| // get ready to actually consume the body | ||
| /** @type {[Uint8Array|null, Uint8Array[], number]} */ | ||
| const [buffer, chunks, limit] = data.size > 0 ? | ||
| [new Uint8Array(data.size), null, data.size] : | ||
| [new Uint8Array(data.size), [], data.size] : | ||
| [null, [], Infinity]; | ||
@@ -510,3 +560,3 @@ let offset = 0; | ||
| if (offset < buffer.byteLength) { | ||
| throw new FetchError(`Premature close of server response while trying to fetch ${data.url}`); | ||
| throw new FetchError(`Premature close of server response while trying to fetch ${data.url}`, 'premature-close'); | ||
| } else { | ||
@@ -521,7 +571,9 @@ return buffer; | ||
| throw error; | ||
| // @ts-expect-error - we know it will have a name | ||
| } else if (error && error.name === 'AbortError') { | ||
| throw error; | ||
| } else { | ||
| const e = /** @type {import('./errors/fetch-error').SystemError} */(error); | ||
| // Other errors, such as incorrect content-encoding | ||
| throw new FetchError(`Invalid response body while trying to fetch ${data.url}: ${error.message}`, 'system', error); | ||
| throw new FetchError(`Invalid response body while trying to fetch ${data.url}: ${e.message}`, 'system', e); | ||
| } | ||
@@ -545,2 +597,3 @@ } | ||
| // @ts-expect-error - could be null | ||
| const [left, right] = body.tee(); | ||
@@ -601,3 +654,2 @@ instance[INTERNALS$2].body = left; | ||
| this.reader = null; | ||
| this.state = null; | ||
| } | ||
@@ -629,2 +681,5 @@ | ||
| /** | ||
| * @returns {Promise<IteratorResult<T, void>>} | ||
| */ | ||
| async return() { | ||
@@ -638,2 +693,7 @@ if (this.reader) { | ||
| /** | ||
| * | ||
| * @param {any} error | ||
| * @returns {Promise<IteratorResult<T, void>>} | ||
| */ | ||
| async throw(error) { | ||
@@ -701,3 +761,3 @@ await this.getReader().cancel(error); | ||
| try { | ||
| while (controller.desiredSize > 0) { | ||
| while (controller.desiredSize || 0 > 0) { | ||
| // eslint-disable-next-line no-await-in-loop | ||
@@ -717,2 +777,5 @@ const next = await this.source.next(); | ||
| /** | ||
| * @param {any} [reason] | ||
| */ | ||
| cancel(reason) { | ||
@@ -737,4 +800,4 @@ if (reason) { | ||
| const pump = new StreamPump(source); | ||
| const stream = | ||
| /** @type {ReadableStream<Uint8Array>} */(new ReadableStream$1(pump, pump)); | ||
| const stream = new ReadableStream$1(pump, pump); | ||
| // @ts-ignore - web-streams-polyfill API is incompatible | ||
| return stream; | ||
@@ -765,2 +828,6 @@ }; | ||
| /** | ||
| * @param {Uint8Array} chunk | ||
| * @returns | ||
| */ | ||
| size(chunk) { | ||
@@ -785,2 +852,5 @@ return chunk.byteLength; | ||
| /** | ||
| * @param {any} [reason] | ||
| */ | ||
| cancel(reason) { | ||
@@ -807,3 +877,3 @@ if (this.stream.destroy) { | ||
| const available = this.controller.desiredSize - bytes.byteLength; | ||
| const available = (this.controller.desiredSize || 0) - bytes.byteLength; | ||
| this.controller.enqueue(bytes); | ||
@@ -839,2 +909,5 @@ if (available <= 0) { | ||
| /** | ||
| * @param {Error} error | ||
| */ | ||
| error(error) { | ||
@@ -854,4 +927,10 @@ if (this.controller) { | ||
| const validateHeaderName = typeof http.validateHeaderName === 'function' ? | ||
| http.validateHeaderName : | ||
| const validators = /** @type {{validateHeaderName?:(name:string) => any, validateHeaderValue?:(name:string, value:string) => any}} */ | ||
| (http); | ||
| const validateHeaderName = typeof validators.validateHeaderName === 'function' ? | ||
| validators.validateHeaderName : | ||
| /** | ||
| * @param {string} name | ||
| */ | ||
| name => { | ||
@@ -865,4 +944,8 @@ if (!/^[\^`\-\w!#$%&'*+.|~]+$/.test(name)) { | ||
| const validateHeaderValue = typeof http.validateHeaderValue === 'function' ? | ||
| http.validateHeaderValue : | ||
| const validateHeaderValue = typeof validators.validateHeaderValue === 'function' ? | ||
| validators.validateHeaderValue : | ||
| /** | ||
| * @param {string} name | ||
| * @param {string} value | ||
| */ | ||
| (name, value) => { | ||
@@ -887,2 +970,3 @@ if (/[^\t\u0020-\u007E\u0080-\u00FF]/.test(value)) { | ||
| * | ||
| * @implements {globalThis.Headers} | ||
| */ | ||
@@ -905,32 +989,24 @@ class Headers extends URLSearchParams { | ||
| } | ||
| } else if (init == null) ; else if (typeof init === 'object' && !util.types.isBoxedPrimitive(init)) { | ||
| const method = init[Symbol.iterator]; | ||
| // eslint-disable-next-line no-eq-null, eqeqeq | ||
| if (method == null) { | ||
| // Record<ByteString, ByteString> | ||
| result.push(...Object.entries(init)); | ||
| } else { | ||
| if (typeof method !== 'function') { | ||
| throw new TypeError('Header pairs must be iterable'); | ||
| } | ||
| } else if (init == null) ; else if (isIterable(init)) { | ||
| // Sequence<sequence<ByteString>> | ||
| // Note: per spec we have to first exhaust the lists then process them | ||
| result = [...init] | ||
| .map(pair => { | ||
| if ( | ||
| typeof pair !== 'object' || util.types.isBoxedPrimitive(pair) | ||
| ) { | ||
| throw new TypeError('Each header pair must be an iterable object'); | ||
| } | ||
| // Sequence<sequence<ByteString>> | ||
| // Note: per spec we have to first exhaust the lists then process them | ||
| result = [...init] | ||
| .map(pair => { | ||
| if ( | ||
| typeof pair !== 'object' || util.types.isBoxedPrimitive(pair) | ||
| ) { | ||
| throw new TypeError('Each header pair must be an iterable object'); | ||
| } | ||
| return [...pair]; | ||
| }).map(pair => { | ||
| if (pair.length !== 2) { | ||
| throw new TypeError('Each header pair must be a name/value tuple'); | ||
| } | ||
| return [...pair]; | ||
| }).map(pair => { | ||
| if (pair.length !== 2) { | ||
| throw new TypeError('Each header pair must be a name/value tuple'); | ||
| } | ||
| return [...pair]; | ||
| }); | ||
| } | ||
| return [...pair]; | ||
| }); | ||
| } else if (typeof init === "object" && init !== null) { | ||
| // Record<ByteString, ByteString> | ||
| result.push(...Object.entries(init)); | ||
| } else { | ||
@@ -948,3 +1024,3 @@ throw new TypeError('Failed to construct \'Headers\': The provided value is not of type \'(sequence<sequence<ByteString>> or record<ByteString, ByteString>)'); | ||
| }) : | ||
| undefined; | ||
| []; | ||
@@ -960,2 +1036,6 @@ super(result); | ||
| case 'set': | ||
| /** | ||
| * @param {string} name | ||
| * @param {string} value | ||
| */ | ||
| return (name, value) => { | ||
@@ -974,4 +1054,8 @@ validateHeaderName(name); | ||
| case 'getAll': | ||
| /** | ||
| * @param {string} name | ||
| */ | ||
| return name => { | ||
| validateHeaderName(name); | ||
| // @ts-ignore | ||
| return URLSearchParams.prototype[p].call( | ||
@@ -1005,2 +1089,6 @@ receiver, | ||
| /** | ||
| * | ||
| * @param {string} name | ||
| */ | ||
| get(name) { | ||
@@ -1020,2 +1108,7 @@ const values = this.getAll(name); | ||
| /** | ||
| * @param {(value: string, key: string, parent: this) => void} callback | ||
| * @param {any} thisArg | ||
| * @returns {void} | ||
| */ | ||
| forEach(callback, thisArg = undefined) { | ||
@@ -1027,5 +1120,8 @@ for (const name of this.keys()) { | ||
| /** | ||
| * @returns {IterableIterator<string>} | ||
| */ | ||
| * values() { | ||
| for (const name of this.keys()) { | ||
| yield this.get(name); | ||
| yield /** @type {string} */(this.get(name)); | ||
| } | ||
@@ -1035,7 +1131,7 @@ } | ||
| /** | ||
| * @type {() => IterableIterator<[string, string]>} | ||
| * @returns {IterableIterator<[string, string]>} | ||
| */ | ||
| * entries() { | ||
| for (const name of this.keys()) { | ||
| yield [name, this.get(name)]; | ||
| yield [name, /** @type {string} */(this.get(name))]; | ||
| } | ||
@@ -1057,3 +1153,3 @@ } | ||
| return result; | ||
| }, {}); | ||
| }, /** @type {Record<string, string[]>} */({})); | ||
| } | ||
@@ -1076,3 +1172,3 @@ | ||
| return result; | ||
| }, {}); | ||
| }, /** @type {Record<string, string|string[]>} */({})); | ||
| } | ||
@@ -1090,3 +1186,3 @@ } | ||
| return result; | ||
| }, {}) | ||
| }, /** @type {Record<string, {enumerable:true}>} */ ({})) | ||
| ); | ||
@@ -1109,3 +1205,3 @@ | ||
| return result; | ||
| }, []) | ||
| }, /** @type {string[][]} */([])) | ||
| .filter(([name, value]) => { | ||
@@ -1146,8 +1242,16 @@ try { | ||
| * Response class | ||
| * | ||
| * @param Stream body Readable stream | ||
| * @param Object opts Response options | ||
| * @return Void | ||
| * | ||
| * @typedef {Object} Ext | ||
| * @property {number} [size] | ||
| * @property {string} [url] | ||
| * @property {number} [counter] | ||
| * @property {number} [highWaterMark] | ||
| * | ||
| * @implements {globalThis.Response} | ||
| */ | ||
| class Response extends Body { | ||
| /** | ||
| * @param {BodyInit|import('stream').Stream|null} [body] - Readable stream | ||
| * @param {ResponseInit & Ext} [options] - Response options | ||
| */ | ||
| constructor(body = null, options = {}) { | ||
@@ -1171,3 +1275,3 @@ super(body, options); | ||
| headers, | ||
| counter: options.counter, | ||
| counter: options.counter || 0, | ||
| highWaterMark: options.highWaterMark | ||
@@ -1177,2 +1281,9 @@ }; | ||
| /** | ||
| * @type {ResponseType} | ||
| */ | ||
| get type() { | ||
| return "default" | ||
| } | ||
| get url() { | ||
@@ -1212,6 +1323,6 @@ return this[INTERNALS$1].url || ''; | ||
| * | ||
| * @return Response | ||
| * @returns {Response} | ||
| */ | ||
| clone() { | ||
| return new Response(clone(this, this.highWaterMark), { | ||
| return new Response(clone(this), { | ||
| url: this.url, | ||
@@ -1221,4 +1332,2 @@ status: this.status, | ||
| headers: this.headers, | ||
| ok: this.ok, | ||
| redirected: this.redirected, | ||
| size: this.size | ||
@@ -1261,2 +1370,6 @@ }); | ||
| /** | ||
| * @param {URL} parsedURL | ||
| * @returns {string} | ||
| */ | ||
| const getSearch = parsedURL => { | ||
@@ -1287,41 +1400,75 @@ if (parsedURL.search) { | ||
| /** | ||
| * Request class | ||
| * @implements {globalThis.Request} | ||
| * | ||
| * @typedef {Object} RequestState | ||
| * @property {string} method | ||
| * @property {RequestRedirect} redirect | ||
| * @property {globalThis.Headers} headers | ||
| * @property {URL} parsedURL | ||
| * @property {AbortSignal|null} signal | ||
| * | ||
| * @typedef {Object} RequestExtraOptions | ||
| * @property {number} [follow] | ||
| * @property {boolean} [compress] | ||
| * @property {number} [size] | ||
| * @property {number} [counter] | ||
| * @property {Agent} [agent] | ||
| * @property {number} [highWaterMark] | ||
| * @property {boolean} [insecureHTTPParser] | ||
| * | ||
| * @typedef {((url:URL) => import('http').Agent) | import('http').Agent} Agent | ||
| * | ||
| * @typedef {Object} RequestOptions | ||
| * @property {string} [method] | ||
| * @property {ReadableStream<Uint8Array>|null} [body] | ||
| * @property {globalThis.Headers} [headers] | ||
| * @property {RequestRedirect} [redirect] | ||
| * | ||
| */ | ||
| class Request extends Body { | ||
| /** | ||
| * @param {string|Request} input Url or Request instance | ||
| * @param {RequestInit} init Custom options | ||
| * @param {string|Request|URL} info Url or Request instance | ||
| * @param {RequestInit & RequestExtraOptions} init Custom options | ||
| */ | ||
| constructor(input, init = {}) { | ||
| constructor(info, init = {}) { | ||
| let parsedURL; | ||
| /** @type {RequestOptions & RequestExtraOptions} */ | ||
| let settings; | ||
| // Normalize input and force URL to be encoded as UTF-8 (https://github.com/node-fetch/node-fetch/issues/245) | ||
| if (isRequest(input)) { | ||
| parsedURL = new URL(input.url); | ||
| if (isRequest(info)) { | ||
| parsedURL = new URL(info.url); | ||
| settings = (info); | ||
| } else { | ||
| parsedURL = new URL(input); | ||
| input = {}; | ||
| parsedURL = new URL(info); | ||
| settings = {}; | ||
| } | ||
| let method = init.method || input.method || 'GET'; | ||
| let method = init.method || settings.method || 'GET'; | ||
| method = method.toUpperCase(); | ||
| const inputBody = init.body != null | ||
| ? init.body | ||
| : (isRequest(info) && info.body !== null) | ||
| ? clone(info) | ||
| : null; | ||
| // eslint-disable-next-line no-eq-null, eqeqeq | ||
| if (((init.body != null || isRequest(input)) && input.body !== null) && | ||
| (method === 'GET' || method === 'HEAD')) { | ||
| if (inputBody != null && (method === 'GET' || method === 'HEAD')) { | ||
| throw new TypeError('Request with GET/HEAD method cannot have body'); | ||
| } | ||
| const inputBody = init.body ? | ||
| init.body : | ||
| (isRequest(input) && input.body !== null ? | ||
| clone(input) : | ||
| null); | ||
| super(inputBody, { | ||
| size: init.size || input.size || 0 | ||
| size: init.size || settings.size || 0 | ||
| }); | ||
| const input = settings; | ||
| const headers = new Headers(init.headers || input.headers || {}); | ||
| const headers = /** @type {globalThis.Headers} */ | ||
| (new Headers(init.headers || input.headers || {})); | ||
@@ -1335,8 +1482,7 @@ if (inputBody !== null && !headers.has('Content-Type')) { | ||
| let signal = isRequest(input) ? | ||
| input.signal : | ||
| null; | ||
| if ('signal' in init) { | ||
| signal = init.signal; | ||
| } | ||
| let signal = 'signal' in init | ||
| ? init.signal | ||
| : isRequest(input) | ||
| ? input.signal | ||
| : null; | ||
@@ -1348,2 +1494,3 @@ // eslint-disable-next-line no-eq-null, eqeqeq | ||
| /** @type {RequestState} */ | ||
| this[INTERNALS] = { | ||
@@ -1354,14 +1501,63 @@ method, | ||
| parsedURL, | ||
| signal | ||
| signal: signal || null | ||
| }; | ||
| /** @type {boolean} */ | ||
| this.keepalive; | ||
| // Node-fetch-only options | ||
| /** @type {number} */ | ||
| this.follow = init.follow === undefined ? (input.follow === undefined ? 20 : input.follow) : init.follow; | ||
| /** @type {boolean} */ | ||
| this.compress = init.compress === undefined ? (input.compress === undefined ? true : input.compress) : init.compress; | ||
| /** @type {number} */ | ||
| this.counter = init.counter || input.counter || 0; | ||
| /** @type {Agent|undefined} */ | ||
| this.agent = init.agent || input.agent; | ||
| /** @type {number} */ | ||
| this.highWaterMark = init.highWaterMark || input.highWaterMark || 16384; | ||
| /** @type {boolean} */ | ||
| this.insecureHTTPParser = init.insecureHTTPParser || input.insecureHTTPParser || false; | ||
| } | ||
| /** | ||
| * @type {RequestCache} | ||
| */ | ||
| get cache() { | ||
| return "default" | ||
| } | ||
| /** | ||
| * @type {RequestCredentials} | ||
| */ | ||
| get credentials() { | ||
| return "same-origin" | ||
| } | ||
| /** | ||
| * @type {RequestDestination} | ||
| */ | ||
| get destination() { | ||
| return "" | ||
| } | ||
| get integrity() { | ||
| return "" | ||
| } | ||
| /** @type {RequestMode} */ | ||
| get mode() { | ||
| return "cors" | ||
| } | ||
| /** @type {string} */ | ||
| get referrer() { | ||
| return "" | ||
| } | ||
| /** @type {ReferrerPolicy} */ | ||
| get referrerPolicy() { | ||
| return "" | ||
| } | ||
| get method() { | ||
@@ -1372,3 +1568,3 @@ return this[INTERNALS].method; | ||
| /** | ||
| * @type {URL} | ||
| * @type {string} | ||
| */ | ||
@@ -1379,2 +1575,5 @@ get url() { | ||
| /** | ||
| * @type {globalThis.Headers} | ||
| */ | ||
| get headers() { | ||
@@ -1388,3 +1587,7 @@ return this[INTERNALS].headers; | ||
| /** | ||
| * @returns {AbortSignal} | ||
| */ | ||
| get signal() { | ||
| // @ts-ignore | ||
| return this[INTERNALS].signal; | ||
@@ -1396,3 +1599,3 @@ } | ||
| * | ||
| * @return Request | ||
| * @return {globalThis.Request} | ||
| */ | ||
@@ -1421,3 +1624,3 @@ clone() { | ||
| * | ||
| * @param {Request} request - A Request instance | ||
| * @param {Request & Record<INTERNALS, RequestState>} request - A Request instance | ||
| */ | ||
@@ -1484,5 +1687,7 @@ const getNodeRequestOptions = request => { | ||
| search: parsedURL.search, | ||
| // @ts-ignore - it does not has a query | ||
| query: parsedURL.query, | ||
| href: parsedURL.href, | ||
| method: request.method, | ||
| // @ts-ignore - not sure what this supposed to do | ||
| headers: headers[Symbol.for('nodejs.util.inspect.custom')](), | ||
@@ -1500,2 +1705,6 @@ insecureHTTPParser: request.insecureHTTPParser, | ||
| class AbortError extends FetchBaseError { | ||
| /** | ||
| * @param {string} message | ||
| * @param {string} [type] | ||
| */ | ||
| constructor(message, type = 'aborted') { | ||
@@ -1535,3 +1744,3 @@ super(message, type); | ||
| if (options.protocol === 'data:') { | ||
| const data = dataUriToBuffer(request.url); | ||
| const data = dataUriToBuffer(request.url.toString()); | ||
| const response = new Response(data, {headers: {'Content-Type': data.typeFull}}); | ||
@@ -1589,2 +1798,3 @@ resolve(response); | ||
| request_.on('error', err => { | ||
| // @ts-expect-error - err may not be SystemError | ||
| reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err)); | ||
@@ -1644,3 +1854,3 @@ finalize(); | ||
| if (locationURL !== null) { | ||
| headers.set('Location', locationURL); | ||
| headers.set('Location', locationURL.toString()); | ||
| } | ||
@@ -1664,3 +1874,2 @@ | ||
| // Create a new Request object. | ||
| /** @type {RequestInit} */ | ||
| const requestOptions = { | ||
@@ -1667,0 +1876,0 @@ headers: new Headers(request.headers), |
+10
-7
| { | ||
| "name": "@web-std/fetch", | ||
| "version": "2.0.3", | ||
| "version": "2.1.0", | ||
| "description": "Web compatible Fetch API implementation for node.js", | ||
@@ -12,3 +12,4 @@ "main": "./dist/index.cjs", | ||
| "import": "./src/index.js", | ||
| "require": "./dist/index.cjs" | ||
| "require": "./dist/index.cjs", | ||
| "types": "./@types/index.d.ts" | ||
| }, | ||
@@ -20,3 +21,3 @@ "./package.json": "./package.json" | ||
| "dist", | ||
| "@types/index.d.ts" | ||
| "@types" | ||
| ], | ||
@@ -31,5 +32,5 @@ "types": "./@types/index.d.ts", | ||
| "coverage": "c8 report --reporter=text-lcov | coveralls", | ||
| "test-types": "tsd", | ||
| "typecheck": "tsc --build", | ||
| "lint": "xo", | ||
| "prepublishOnly": "node ./test/commonjs/test-artifact.js" | ||
| "prepublishOnly": "node ./test/commonjs/test-artifact.js && tsc --build" | ||
| }, | ||
@@ -73,3 +74,4 @@ "repository": { | ||
| "tsd": "^0.13.1", | ||
| "xo": "^0.33.1" | ||
| "xo": "^0.33.1", | ||
| "typescript": "^4.4.4" | ||
| }, | ||
@@ -79,3 +81,4 @@ "dependencies": { | ||
| "data-uri-to-buffer": "^3.0.1", | ||
| "web-streams-polyfill": "^3.0.2" | ||
| "web-streams-polyfill": "^3.1.1", | ||
| "@ssttevee/multipart-parser": "^0.1.9" | ||
| }, | ||
@@ -82,0 +85,0 @@ "esm": { |
+46
-17
@@ -16,6 +16,5 @@ // @ts-check | ||
| import {FetchBaseError} from './errors/base.js'; | ||
| import {formDataIterator, getBoundary, getFormDataLength} from './utils/form-data.js'; | ||
| import {formDataIterator, getBoundary, getFormDataLength, toFormData} from './utils/form-data.js'; | ||
| import {isBlob, isURLSearchParameters, isFormData, isMultipartFormDataStream, isReadableStream} from './utils/is.js'; | ||
| import * as utf8 from './utils/utf8.js'; | ||
| const {readableHighWaterMark} = new Stream.Readable(); | ||
@@ -30,6 +29,3 @@ | ||
| * Ref: https://fetch.spec.whatwg.org/#body | ||
| * | ||
| * @param {BodyInit} body Readable stream | ||
| * @param Object opts Response options | ||
| * @return Void | ||
| * @implements {globalThis.Body} | ||
| */ | ||
@@ -39,3 +35,3 @@ | ||
| /** | ||
| * @param {BodyInit|Stream} body | ||
| * @param {BodyInit|Stream|null} body | ||
| * @param {{size?:number}} options | ||
@@ -129,3 +125,3 @@ */ | ||
| get headers() { | ||
| return null; | ||
| return undefined; | ||
| } | ||
@@ -183,2 +179,10 @@ | ||
| } | ||
| /** | ||
| * @returns {Promise<FormData>} | ||
| */ | ||
| async formData() { | ||
| return toFormData(this) | ||
| } | ||
| } | ||
@@ -193,3 +197,4 @@ | ||
| json: {enumerable: true}, | ||
| text: {enumerable: true} | ||
| text: {enumerable: true}, | ||
| formData: {enumerable: true} | ||
| }); | ||
@@ -226,4 +231,5 @@ | ||
| // get ready to actually consume the body | ||
| /** @type {[Uint8Array|null, Uint8Array[], number]} */ | ||
| const [buffer, chunks, limit] = data.size > 0 ? | ||
| [new Uint8Array(data.size), null, data.size] : | ||
| [new Uint8Array(data.size), [], data.size] : | ||
| [null, [], Infinity]; | ||
@@ -254,3 +260,3 @@ let offset = 0; | ||
| if (offset < buffer.byteLength) { | ||
| throw new FetchError(`Premature close of server response while trying to fetch ${data.url}`); | ||
| throw new FetchError(`Premature close of server response while trying to fetch ${data.url}`, 'premature-close'); | ||
| } else { | ||
@@ -265,7 +271,9 @@ return buffer; | ||
| throw error; | ||
| // @ts-expect-error - we know it will have a name | ||
| } else if (error && error.name === 'AbortError') { | ||
| throw error; | ||
| } else { | ||
| const e = /** @type {import('./errors/fetch-error').SystemError} */(error) | ||
| // Other errors, such as incorrect content-encoding | ||
| throw new FetchError(`Invalid response body while trying to fetch ${data.url}: ${error.message}`, 'system', error); | ||
| throw new FetchError(`Invalid response body while trying to fetch ${data.url}: ${e.message}`, 'system', e); | ||
| } | ||
@@ -289,2 +297,3 @@ } | ||
| // @ts-expect-error - could be null | ||
| const [left, right] = body.tee(); | ||
@@ -345,3 +354,2 @@ instance[INTERNALS].body = left; | ||
| this.reader = null; | ||
| this.state = null; | ||
| } | ||
@@ -373,2 +381,5 @@ | ||
| /** | ||
| * @returns {Promise<IteratorResult<T, void>>} | ||
| */ | ||
| async return() { | ||
@@ -382,2 +393,7 @@ if (this.reader) { | ||
| /** | ||
| * | ||
| * @param {any} error | ||
| * @returns {Promise<IteratorResult<T, void>>} | ||
| */ | ||
| async throw(error) { | ||
@@ -445,3 +461,3 @@ await this.getReader().cancel(error); | ||
| try { | ||
| while (controller.desiredSize > 0) { | ||
| while (controller.desiredSize || 0 > 0) { | ||
| // eslint-disable-next-line no-await-in-loop | ||
@@ -461,2 +477,5 @@ const next = await this.source.next(); | ||
| /** | ||
| * @param {any} [reason] | ||
| */ | ||
| cancel(reason) { | ||
@@ -481,4 +500,4 @@ if (reason) { | ||
| const pump = new StreamPump(source); | ||
| const stream = | ||
| /** @type {ReadableStream<Uint8Array>} */(new ReadableStream(pump, pump)); | ||
| const stream = new ReadableStream(pump, pump); | ||
| // @ts-ignore - web-streams-polyfill API is incompatible | ||
| return stream; | ||
@@ -509,2 +528,6 @@ }; | ||
| /** | ||
| * @param {Uint8Array} chunk | ||
| * @returns | ||
| */ | ||
| size(chunk) { | ||
@@ -529,2 +552,5 @@ return chunk.byteLength; | ||
| /** | ||
| * @param {any} [reason] | ||
| */ | ||
| cancel(reason) { | ||
@@ -551,3 +577,3 @@ if (this.stream.destroy) { | ||
| const available = this.controller.desiredSize - bytes.byteLength; | ||
| const available = (this.controller.desiredSize || 0) - bytes.byteLength; | ||
| this.controller.enqueue(bytes); | ||
@@ -583,2 +609,5 @@ if (available <= 0) { | ||
| /** | ||
| * @param {Error} error | ||
| */ | ||
| error(error) { | ||
@@ -585,0 +614,0 @@ if (this.controller) { |
@@ -7,2 +7,6 @@ import {FetchBaseError} from './base.js'; | ||
| export class AbortError extends FetchBaseError { | ||
| /** | ||
| * @param {string} message | ||
| * @param {string} [type] | ||
| */ | ||
| constructor(message, type = 'aborted') { | ||
@@ -9,0 +13,0 @@ super(message, type); |
| 'use strict'; | ||
| export class FetchBaseError extends Error { | ||
| /** | ||
| * @param {string} message | ||
| * @param {string} type | ||
| */ | ||
| constructor(message, type) { | ||
@@ -5,0 +9,0 @@ super(message); |
@@ -5,3 +5,13 @@ | ||
| /** | ||
| * @typedef {{ address?: string, code: string, dest?: string, errno: number, info?: object, message: string, path?: string, port?: number, syscall: string}} SystemError | ||
| * @typedef {{ | ||
| * address?: string | ||
| * code: string | ||
| * dest?: string | ||
| * errno: number | ||
| * info?: object | ||
| * message: string | ||
| * path?: string | ||
| * port?: number | ||
| * syscall: string | ||
| * }} SystemError | ||
| */ | ||
@@ -15,3 +25,3 @@ | ||
| * @param {string} message - Error message for human | ||
| * @param {string} [type] - Error type for machine | ||
| * @param {string} type - Error type for machine | ||
| * @param {SystemError} [systemError] - For Node.js system error | ||
@@ -18,0 +28,0 @@ */ |
+64
-40
@@ -9,5 +9,12 @@ /** | ||
| import http from 'http'; | ||
| import { isIterable } from './utils/is.js' | ||
| const validateHeaderName = typeof http.validateHeaderName === 'function' ? | ||
| http.validateHeaderName : | ||
| const validators = /** @type {{validateHeaderName?:(name:string) => any, validateHeaderValue?:(name:string, value:string) => any}} */ | ||
| (http) | ||
| const validateHeaderName = typeof validators.validateHeaderName === 'function' ? | ||
| validators.validateHeaderName : | ||
| /** | ||
| * @param {string} name | ||
| */ | ||
| name => { | ||
@@ -21,4 +28,8 @@ if (!/^[\^`\-\w!#$%&'*+.|~]+$/.test(name)) { | ||
| const validateHeaderValue = typeof http.validateHeaderValue === 'function' ? | ||
| http.validateHeaderValue : | ||
| const validateHeaderValue = typeof validators.validateHeaderValue === 'function' ? | ||
| validators.validateHeaderValue : | ||
| /** | ||
| * @param {string} name | ||
| * @param {string} value | ||
| */ | ||
| (name, value) => { | ||
@@ -43,2 +54,3 @@ if (/[^\t\u0020-\u007E\u0080-\u00FF]/.test(value)) { | ||
| * | ||
| * @implements {globalThis.Headers} | ||
| */ | ||
@@ -63,32 +75,24 @@ export default class Headers extends URLSearchParams { | ||
| // No op | ||
| } else if (typeof init === 'object' && !types.isBoxedPrimitive(init)) { | ||
| const method = init[Symbol.iterator]; | ||
| // eslint-disable-next-line no-eq-null, eqeqeq | ||
| if (method == null) { | ||
| // Record<ByteString, ByteString> | ||
| result.push(...Object.entries(init)); | ||
| } else { | ||
| if (typeof method !== 'function') { | ||
| throw new TypeError('Header pairs must be iterable'); | ||
| } | ||
| } else if (isIterable(init)) { | ||
| // Sequence<sequence<ByteString>> | ||
| // Note: per spec we have to first exhaust the lists then process them | ||
| result = [...init] | ||
| .map(pair => { | ||
| if ( | ||
| typeof pair !== 'object' || types.isBoxedPrimitive(pair) | ||
| ) { | ||
| throw new TypeError('Each header pair must be an iterable object'); | ||
| } | ||
| // Sequence<sequence<ByteString>> | ||
| // Note: per spec we have to first exhaust the lists then process them | ||
| result = [...init] | ||
| .map(pair => { | ||
| if ( | ||
| typeof pair !== 'object' || types.isBoxedPrimitive(pair) | ||
| ) { | ||
| throw new TypeError('Each header pair must be an iterable object'); | ||
| } | ||
| return [...pair]; | ||
| }).map(pair => { | ||
| if (pair.length !== 2) { | ||
| throw new TypeError('Each header pair must be a name/value tuple'); | ||
| } | ||
| return [...pair]; | ||
| }).map(pair => { | ||
| if (pair.length !== 2) { | ||
| throw new TypeError('Each header pair must be a name/value tuple'); | ||
| } | ||
| return [...pair]; | ||
| }); | ||
| } | ||
| return [...pair]; | ||
| }); | ||
| } else if (typeof init === "object" && init !== null) { | ||
| // Record<ByteString, ByteString> | ||
| result.push(...Object.entries(init)); | ||
| } else { | ||
@@ -106,3 +110,3 @@ throw new TypeError('Failed to construct \'Headers\': The provided value is not of type \'(sequence<sequence<ByteString>> or record<ByteString, ByteString>)'); | ||
| }) : | ||
| undefined; | ||
| []; | ||
@@ -118,2 +122,6 @@ super(result); | ||
| case 'set': | ||
| /** | ||
| * @param {string} name | ||
| * @param {string} value | ||
| */ | ||
| return (name, value) => { | ||
@@ -132,4 +140,8 @@ validateHeaderName(name); | ||
| case 'getAll': | ||
| /** | ||
| * @param {string} name | ||
| */ | ||
| return name => { | ||
| validateHeaderName(name); | ||
| // @ts-ignore | ||
| return URLSearchParams.prototype[p].call( | ||
@@ -163,2 +175,6 @@ receiver, | ||
| /** | ||
| * | ||
| * @param {string} name | ||
| */ | ||
| get(name) { | ||
@@ -178,2 +194,7 @@ const values = this.getAll(name); | ||
| /** | ||
| * @param {(value: string, key: string, parent: this) => void} callback | ||
| * @param {any} thisArg | ||
| * @returns {void} | ||
| */ | ||
| forEach(callback, thisArg = undefined) { | ||
@@ -185,5 +206,8 @@ for (const name of this.keys()) { | ||
| /** | ||
| * @returns {IterableIterator<string>} | ||
| */ | ||
| * values() { | ||
| for (const name of this.keys()) { | ||
| yield this.get(name); | ||
| yield /** @type {string} */(this.get(name)); | ||
| } | ||
@@ -193,7 +217,7 @@ } | ||
| /** | ||
| * @type {() => IterableIterator<[string, string]>} | ||
| * @returns {IterableIterator<[string, string]>} | ||
| */ | ||
| * entries() { | ||
| for (const name of this.keys()) { | ||
| yield [name, this.get(name)]; | ||
| yield [name, /** @type {string} */(this.get(name))]; | ||
| } | ||
@@ -215,3 +239,3 @@ } | ||
| return result; | ||
| }, {}); | ||
| }, /** @type {Record<string, string[]>} */({})); | ||
| } | ||
@@ -234,3 +258,3 @@ | ||
| return result; | ||
| }, {}); | ||
| }, /** @type {Record<string, string|string[]>} */({})); | ||
| } | ||
@@ -248,3 +272,3 @@ } | ||
| return result; | ||
| }, {}) | ||
| }, /** @type {Record<string, {enumerable:true}>} */ ({})) | ||
| ); | ||
@@ -267,3 +291,3 @@ | ||
| return result; | ||
| }, []) | ||
| }, /** @type {string[][]} */([])) | ||
| .filter(([name, value]) => { | ||
@@ -270,0 +294,0 @@ try { |
+4
-3
@@ -25,2 +25,3 @@ /** | ||
| import {Blob} from '@web-std/blob'; | ||
| import {FormData} from '@web-std/form-data'; | ||
@@ -50,3 +51,3 @@ const {ReadableStream} = WebStreams; | ||
| if (options.protocol === 'data:') { | ||
| const data = dataUriToBuffer(request.url); | ||
| const data = dataUriToBuffer(request.url.toString()); | ||
| const response = new Response(data, {headers: {'Content-Type': data.typeFull}}); | ||
@@ -104,2 +105,3 @@ resolve(response); | ||
| request_.on('error', err => { | ||
| // @ts-expect-error - err may not be SystemError | ||
| reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err)); | ||
@@ -159,3 +161,3 @@ finalize(); | ||
| if (locationURL !== null) { | ||
| headers.set('Location', locationURL); | ||
| headers.set('Location', locationURL.toString()); | ||
| } | ||
@@ -179,3 +181,2 @@ | ||
| // Create a new Request object. | ||
| /** @type {RequestInit} */ | ||
| const requestOptions = { | ||
@@ -182,0 +183,0 @@ headers: new Headers(request.headers), |
+120
-28
@@ -31,41 +31,75 @@ | ||
| /** | ||
| * Request class | ||
| * @implements {globalThis.Request} | ||
| * | ||
| * @typedef {Object} RequestState | ||
| * @property {string} method | ||
| * @property {RequestRedirect} redirect | ||
| * @property {globalThis.Headers} headers | ||
| * @property {URL} parsedURL | ||
| * @property {AbortSignal|null} signal | ||
| * | ||
| * @typedef {Object} RequestExtraOptions | ||
| * @property {number} [follow] | ||
| * @property {boolean} [compress] | ||
| * @property {number} [size] | ||
| * @property {number} [counter] | ||
| * @property {Agent} [agent] | ||
| * @property {number} [highWaterMark] | ||
| * @property {boolean} [insecureHTTPParser] | ||
| * | ||
| * @typedef {((url:URL) => import('http').Agent) | import('http').Agent} Agent | ||
| * | ||
| * @typedef {Object} RequestOptions | ||
| * @property {string} [method] | ||
| * @property {ReadableStream<Uint8Array>|null} [body] | ||
| * @property {globalThis.Headers} [headers] | ||
| * @property {RequestRedirect} [redirect] | ||
| * | ||
| */ | ||
| export default class Request extends Body { | ||
| /** | ||
| * @param {string|Request} input Url or Request instance | ||
| * @param {RequestInit} init Custom options | ||
| * @param {string|Request|URL} info Url or Request instance | ||
| * @param {RequestInit & RequestExtraOptions} init Custom options | ||
| */ | ||
| constructor(input, init = {}) { | ||
| constructor(info, init = {}) { | ||
| let parsedURL; | ||
| /** @type {RequestOptions & RequestExtraOptions} */ | ||
| let settings | ||
| // Normalize input and force URL to be encoded as UTF-8 (https://github.com/node-fetch/node-fetch/issues/245) | ||
| if (isRequest(input)) { | ||
| parsedURL = new URL(input.url); | ||
| if (isRequest(info)) { | ||
| parsedURL = new URL(info.url); | ||
| settings = (info) | ||
| } else { | ||
| parsedURL = new URL(input); | ||
| input = {}; | ||
| parsedURL = new URL(info); | ||
| settings = {}; | ||
| } | ||
| let method = init.method || input.method || 'GET'; | ||
| let method = init.method || settings.method || 'GET'; | ||
| method = method.toUpperCase(); | ||
| const inputBody = init.body != null | ||
| ? init.body | ||
| : (isRequest(info) && info.body !== null) | ||
| ? clone(info) | ||
| : null; | ||
| // eslint-disable-next-line no-eq-null, eqeqeq | ||
| if (((init.body != null || isRequest(input)) && input.body !== null) && | ||
| (method === 'GET' || method === 'HEAD')) { | ||
| if (inputBody != null && (method === 'GET' || method === 'HEAD')) { | ||
| throw new TypeError('Request with GET/HEAD method cannot have body'); | ||
| } | ||
| const inputBody = init.body ? | ||
| init.body : | ||
| (isRequest(input) && input.body !== null ? | ||
| clone(input) : | ||
| null); | ||
| super(inputBody, { | ||
| size: init.size || input.size || 0 | ||
| size: init.size || settings.size || 0 | ||
| }); | ||
| const input = settings | ||
| const headers = new Headers(init.headers || input.headers || {}); | ||
| const headers = /** @type {globalThis.Headers} */ | ||
| (new Headers(init.headers || input.headers || {})); | ||
@@ -79,8 +113,7 @@ if (inputBody !== null && !headers.has('Content-Type')) { | ||
| let signal = isRequest(input) ? | ||
| input.signal : | ||
| null; | ||
| if ('signal' in init) { | ||
| signal = init.signal; | ||
| } | ||
| let signal = 'signal' in init | ||
| ? init.signal | ||
| : isRequest(input) | ||
| ? input.signal | ||
| : null; | ||
@@ -92,2 +125,3 @@ // eslint-disable-next-line no-eq-null, eqeqeq | ||
| /** @type {RequestState} */ | ||
| this[INTERNALS] = { | ||
@@ -98,14 +132,63 @@ method, | ||
| parsedURL, | ||
| signal | ||
| signal: signal || null | ||
| }; | ||
| /** @type {boolean} */ | ||
| this.keepalive | ||
| // Node-fetch-only options | ||
| /** @type {number} */ | ||
| this.follow = init.follow === undefined ? (input.follow === undefined ? 20 : input.follow) : init.follow; | ||
| /** @type {boolean} */ | ||
| this.compress = init.compress === undefined ? (input.compress === undefined ? true : input.compress) : init.compress; | ||
| /** @type {number} */ | ||
| this.counter = init.counter || input.counter || 0; | ||
| /** @type {Agent|undefined} */ | ||
| this.agent = init.agent || input.agent; | ||
| /** @type {number} */ | ||
| this.highWaterMark = init.highWaterMark || input.highWaterMark || 16384; | ||
| /** @type {boolean} */ | ||
| this.insecureHTTPParser = init.insecureHTTPParser || input.insecureHTTPParser || false; | ||
| } | ||
| /** | ||
| * @type {RequestCache} | ||
| */ | ||
| get cache() { | ||
| return "default" | ||
| } | ||
| /** | ||
| * @type {RequestCredentials} | ||
| */ | ||
| get credentials() { | ||
| return "same-origin" | ||
| } | ||
| /** | ||
| * @type {RequestDestination} | ||
| */ | ||
| get destination() { | ||
| return "" | ||
| } | ||
| get integrity() { | ||
| return "" | ||
| } | ||
| /** @type {RequestMode} */ | ||
| get mode() { | ||
| return "cors" | ||
| } | ||
| /** @type {string} */ | ||
| get referrer() { | ||
| return "" | ||
| } | ||
| /** @type {ReferrerPolicy} */ | ||
| get referrerPolicy() { | ||
| return "" | ||
| } | ||
| get method() { | ||
@@ -116,3 +199,3 @@ return this[INTERNALS].method; | ||
| /** | ||
| * @type {URL} | ||
| * @type {string} | ||
| */ | ||
@@ -123,2 +206,5 @@ get url() { | ||
| /** | ||
| * @type {globalThis.Headers} | ||
| */ | ||
| get headers() { | ||
@@ -132,3 +218,7 @@ return this[INTERNALS].headers; | ||
| /** | ||
| * @returns {AbortSignal} | ||
| */ | ||
| get signal() { | ||
| // @ts-ignore | ||
| return this[INTERNALS].signal; | ||
@@ -140,3 +230,3 @@ } | ||
| * | ||
| * @return Request | ||
| * @return {globalThis.Request} | ||
| */ | ||
@@ -165,3 +255,3 @@ clone() { | ||
| * | ||
| * @param {Request} request - A Request instance | ||
| * @param {Request & Record<INTERNALS, RequestState>} request - A Request instance | ||
| */ | ||
@@ -228,5 +318,7 @@ export const getNodeRequestOptions = request => { | ||
| search: parsedURL.search, | ||
| // @ts-ignore - it does not has a query | ||
| query: parsedURL.query, | ||
| href: parsedURL.href, | ||
| method: request.method, | ||
| // @ts-ignore - not sure what this supposed to do | ||
| headers: headers[Symbol.for('nodejs.util.inspect.custom')](), | ||
@@ -233,0 +325,0 @@ insecureHTTPParser: request.insecureHTTPParser, |
+22
-9
@@ -15,8 +15,16 @@ /** | ||
| * Response class | ||
| * | ||
| * @param Stream body Readable stream | ||
| * @param Object opts Response options | ||
| * @return Void | ||
| * | ||
| * @typedef {Object} Ext | ||
| * @property {number} [size] | ||
| * @property {string} [url] | ||
| * @property {number} [counter] | ||
| * @property {number} [highWaterMark] | ||
| * | ||
| * @implements {globalThis.Response} | ||
| */ | ||
| export default class Response extends Body { | ||
| /** | ||
| * @param {BodyInit|import('stream').Stream|null} [body] - Readable stream | ||
| * @param {ResponseInit & Ext} [options] - Response options | ||
| */ | ||
| constructor(body = null, options = {}) { | ||
@@ -40,3 +48,3 @@ super(body, options); | ||
| headers, | ||
| counter: options.counter, | ||
| counter: options.counter || 0, | ||
| highWaterMark: options.highWaterMark | ||
@@ -46,2 +54,9 @@ }; | ||
| /** | ||
| * @type {ResponseType} | ||
| */ | ||
| get type() { | ||
| return "default" | ||
| } | ||
| get url() { | ||
@@ -81,6 +96,6 @@ return this[INTERNALS].url || ''; | ||
| * | ||
| * @return Response | ||
| * @returns {Response} | ||
| */ | ||
| clone() { | ||
| return new Response(clone(this, this.highWaterMark), { | ||
| return new Response(clone(this), { | ||
| url: this.url, | ||
@@ -90,4 +105,2 @@ status: this.status, | ||
| headers: this.headers, | ||
| ok: this.ok, | ||
| redirected: this.redirected, | ||
| size: this.size | ||
@@ -94,0 +107,0 @@ }); |
| import {randomBytes} from 'crypto'; | ||
| import { iterateMultipart } from '@ssttevee/multipart-parser' | ||
| import {isBlob} from './is.js'; | ||
@@ -28,4 +28,5 @@ | ||
| if (isBlob(field)) { | ||
| header += `; filename="${field.name}"${carriage}`; | ||
| header += `Content-Type: ${field.type || 'application/octet-stream'}`; | ||
| const { name = 'blob', type } = /** @type {Blob & {name?:string}} */ (field); | ||
| header += `; filename="${name}"${carriage}`; | ||
| header += `Content-Type: ${type || 'application/octet-stream'}`; | ||
| } | ||
@@ -84,1 +85,23 @@ | ||
| } | ||
| /** | ||
| * @param {Body & {headers?:Headers}} source | ||
| */ | ||
| export const toFormData = async ({ body, headers }) => { | ||
| const contentType = headers?.get('Content-Type') || '' | ||
| const [type, boundary] = contentType.split(/\s*;\s*boundary=/) | ||
| if (type === 'multipart/form-data' && boundary != null && body != null) { | ||
| const form = new FormData() | ||
| const parts = iterateMultipart(body, boundary) | ||
| for await (const { name, data, filename, contentType } of parts) { | ||
| if (filename) { | ||
| form.append(name, new File([data], filename, { type: contentType })) | ||
| } else { | ||
| form.append(name, new TextDecoder().decode(data), filename) | ||
| } | ||
| } | ||
| return form | ||
| } else { | ||
| throw new TypeError('Could not parse content as FormData.') | ||
| } | ||
| } |
@@ -0,1 +1,5 @@ | ||
| /** | ||
| * @param {URL} parsedURL | ||
| * @returns {string} | ||
| */ | ||
| export const getSearch = parsedURL => { | ||
@@ -2,0 +6,0 @@ if (parsedURL.search) { |
+50
-45
@@ -1,2 +0,2 @@ | ||
| import Stream from 'stream'; | ||
| import Stream from "stream"; | ||
@@ -15,16 +15,16 @@ /** | ||
| * | ||
| * @param {*} obj | ||
| * @param {any} object | ||
| * @return {obj is URLSearchParams} | ||
| */ | ||
| export const isURLSearchParameters = object => { | ||
| export const isURLSearchParameters = (object) => { | ||
| return ( | ||
| typeof object === 'object' && | ||
| typeof object.append === 'function' && | ||
| typeof object.delete === 'function' && | ||
| typeof object.get === 'function' && | ||
| typeof object.getAll === 'function' && | ||
| typeof object.has === 'function' && | ||
| typeof object.set === 'function' && | ||
| typeof object.sort === 'function' && | ||
| object[NAME] === 'URLSearchParams' | ||
| typeof object === "object" && | ||
| typeof object.append === "function" && | ||
| typeof object.delete === "function" && | ||
| typeof object.get === "function" && | ||
| typeof object.getAll === "function" && | ||
| typeof object.has === "function" && | ||
| typeof object.set === "function" && | ||
| typeof object.sort === "function" && | ||
| object[NAME] === "URLSearchParams" | ||
| ); | ||
@@ -39,9 +39,9 @@ }; | ||
| */ | ||
| export const isBlob = object => { | ||
| export const isBlob = (object) => { | ||
| return ( | ||
| typeof object === 'object' && | ||
| typeof object.arrayBuffer === 'function' && | ||
| typeof object.type === 'string' && | ||
| typeof object.stream === 'function' && | ||
| typeof object.constructor === 'function' && | ||
| typeof object === "object" && | ||
| typeof object.arrayBuffer === "function" && | ||
| typeof object.type === "string" && | ||
| typeof object.stream === "function" && | ||
| typeof object.constructor === "function" && | ||
| /^(Blob|File)$/.test(object[NAME]) | ||
@@ -59,13 +59,13 @@ ); | ||
| return ( | ||
| typeof object === 'object' && | ||
| typeof object.append === 'function' && | ||
| typeof object.set === 'function' && | ||
| typeof object.get === 'function' && | ||
| typeof object.getAll === 'function' && | ||
| typeof object.delete === 'function' && | ||
| typeof object.keys === 'function' && | ||
| typeof object.values === 'function' && | ||
| typeof object.entries === 'function' && | ||
| typeof object.constructor === 'function' && | ||
| object[NAME] === 'FormData' | ||
| typeof object === "object" && | ||
| typeof object.append === "function" && | ||
| typeof object.set === "function" && | ||
| typeof object.get === "function" && | ||
| typeof object.getAll === "function" && | ||
| typeof object.delete === "function" && | ||
| typeof object.keys === "function" && | ||
| typeof object.values === "function" && | ||
| typeof object.entries === "function" && | ||
| typeof object.constructor === "function" && | ||
| object[NAME] === "FormData" | ||
| ); | ||
@@ -80,8 +80,8 @@ } | ||
| */ | ||
| export const isMultipartFormDataStream = value => { | ||
| export const isMultipartFormDataStream = (value) => { | ||
| return ( | ||
| value instanceof Stream && | ||
| typeof value.getBoundary === 'function' && | ||
| typeof value.hasKnownLength === 'function' && | ||
| typeof value.getLengthSync === 'function' | ||
| value instanceof Stream === true && | ||
| typeof value.getBoundary === "function" && | ||
| typeof value.hasKnownLength === "function" && | ||
| typeof value.getLengthSync === "function" | ||
| ); | ||
@@ -93,11 +93,9 @@ }; | ||
| * | ||
| * @param {*} obj | ||
| * @param {any} object | ||
| * @return {obj is AbortSignal} | ||
| */ | ||
| export const isAbortSignal = object => { | ||
| export const isAbortSignal = (object) => { | ||
| return ( | ||
| typeof object === 'object' && ( | ||
| object[NAME] === 'AbortSignal' || | ||
| object[NAME] === 'EventTarget' | ||
| ) | ||
| typeof object === "object" && | ||
| (object[NAME] === "AbortSignal" || object[NAME] === "EventTarget") | ||
| ); | ||
@@ -112,9 +110,16 @@ }; | ||
| */ | ||
| export const isReadableStream = value => { | ||
| export const isReadableStream = (value) => { | ||
| return ( | ||
| typeof value === 'object' && | ||
| typeof value.getReader === 'function' && | ||
| typeof value.cancel === 'function' && | ||
| typeof value.tee === 'function' | ||
| typeof value === "object" && | ||
| typeof value.getReader === "function" && | ||
| typeof value.cancel === "function" && | ||
| typeof value.tee === "function" | ||
| ); | ||
| }; | ||
| /** | ||
| * | ||
| * @param {any} value | ||
| * @returns {value is Iterable<unknown>} | ||
| */ | ||
| export const isIterable = (value) => value && Symbol.iterator in value; |
Sorry, the diff of this file is too big to display
Network access
Supply chain riskThis module accesses the network.
Found 3 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 3 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
217110
10.28%20
5.26%3771
14.31%4
33.33%18
5.88%99
6.45%+ Added
+ Added
+ Added
Updated