diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0dd3a589..3b91888e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,6 +54,12 @@ jobs: - os: ubuntu-latest # Test the actively developed version that will become the latest LTS release next October node: current + # The `build` job already runs the testing suite in ubuntu and lts/* + exclude: + - os: ubuntu-latest + # Test the oldest LTS release of Node that's still receiving bugfixes and security patches, versions older than that have reached End-of-Life + node: lts/* + steps: - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3 diff --git a/modules.d.ts b/modules.d.ts index 585f29dc..d05c92af 100644 --- a/modules.d.ts +++ b/modules.d.ts @@ -5,10 +5,6 @@ declare module 'tunnel-agent' { export function httpsOverHttps(options: any): any } -declare module 'same-origin' { - export default function sameOrigin(uri1: string, uri2: string, ieMode?: boolean): boolean -} - declare module 'create-error-class' { interface ErrorClass { new (res: any, ctx: any): Error diff --git a/package-lock.json b/package-lock.json index 0608d1cc..d4a0f9db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "get-it", - "version": "8.0.3", + "version": "8.0.4-esm.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "get-it", - "version": "8.0.3", + "version": "8.0.4-esm.0", "license": "MIT", "dependencies": { "create-error-class": "^3.0.2", @@ -18,10 +18,8 @@ "is-plain-object": "^5.0.0", "is-retry-allowed": "^2.2.0", "is-stream": "^2.0.1", - "nano-pubsub": "^2.0.1", "parse-headers": "^2.0.5", "progress-stream": "^2.0.0", - "same-origin": "^0.1.1", "tunnel-agent": "^0.6.0", "url-parse": "^1.5.10" }, @@ -7864,11 +7862,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/nano-pubsub": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/nano-pubsub/-/nano-pubsub-2.0.1.tgz", - "integrity": "sha512-RWgGP2TdeKZLx+guR5a7/BzYs85sj6yrXXyj0o/znbgzPlz/Ez9wQuKDpwUZ8q+u2RxXpqZ1iTkPXCIU+GHhpA==" - }, "node_modules/nanoid": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", @@ -11917,11 +11910,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/same-origin": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/same-origin/-/same-origin-0.1.1.tgz", - "integrity": "sha512-effkSW9cap879l6CVNdwL5iubVz8tkspqgfiqwgBgFQspV7152WHaLzr5590yR8oFgt7E1d4lO09uUhtAgUPoA==" - }, "node_modules/semantic-release": { "version": "20.0.2", "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-20.0.2.tgz", diff --git a/package.json b/package.json index 161c2bfe..d4b57805 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "get-it", - "version": "8.0.3", + "version": "8.0.4-fetch.0", "description": "Generic HTTP request library for node, browsers and workers", "keywords": [ "request", @@ -105,10 +105,8 @@ "is-plain-object": "^5.0.0", "is-retry-allowed": "^2.2.0", "is-stream": "^2.0.1", - "nano-pubsub": "^2.0.1", "parse-headers": "^2.0.5", "progress-stream": "^2.0.0", - "same-origin": "^0.1.1", "tunnel-agent": "^0.6.0", "url-parse": "^1.5.10" }, diff --git a/src/createRequester.ts b/src/createRequester.ts index ab87018d..5e6800c6 100644 --- a/src/createRequester.ts +++ b/src/createRequester.ts @@ -1,9 +1,8 @@ -import pubsub from 'nano-pubsub' - import {processOptions} from './middleware/defaultOptionsProcessor' import {validateOptions} from './middleware/defaultOptionsValidator' import type {HttpRequest, Middleware, Middlewares, Requester} from './types' import middlewareReducer from './util/middlewareReducer' +import pubsub from './util/pubsub' const channelNames = ['request', 'response', 'progress', 'error', 'abort'] const middlehooks = [ diff --git a/src/request/browser-request.ts b/src/request/browser-request.ts index e48be8d1..acfcba37 100644 --- a/src/request/browser-request.ts +++ b/src/request/browser-request.ts @@ -1,37 +1,18 @@ import parseHeaders from 'parse-headers' -import sameOrigin from 'same-origin' -import FetchXhr from './browser/fetchXhr' +import {FetchXhr} from './browser/fetchXhr' -const noop = function () { - /* intentional noop */ -} - -// eslint-disable-next-line no-var -declare var XDomainRequest: any - -const win = typeof document === 'undefined' || typeof window === 'undefined' ? undefined : window -const adapter = win ? 'xhr' : 'fetch' - -let XmlHttpRequest: any = typeof XMLHttpRequest === 'function' ? XMLHttpRequest : noop -const hasXhr2 = 'withCredentials' in new XmlHttpRequest() -const XDR = typeof XDomainRequest === 'undefined' ? undefined : XDomainRequest -let CrossDomainRequest = hasXhr2 ? XmlHttpRequest : XDR +// Use fetch if it's available, non-browser environments such as Deno, Edge Runtime and more provide fetch as a global but doesn't provide xhr +const adapter = typeof XMLHttpRequest === 'function' ? 'xhr' : 'fetch' // Fallback to fetch-based XHR polyfill for non-browser environments like Workers -if (!win) { - XmlHttpRequest = FetchXhr - CrossDomainRequest = FetchXhr -} +const XmlHttpRequest = adapter === 'xhr' ? XMLHttpRequest : FetchXhr export default (context: any, callback: any) => { const opts = context.options const options = context.applyMiddleware('finalizeOptions', opts) const timers: any = {} - // Deep-checking window.location because of react native, where `location` doesn't exist - const cors = win && win.location && !sameOrigin(win.location.href, options.url) - // Allow middleware to inject a response, for instance in the case of caching or mocking const injectedResponse = context.applyMiddleware('interceptRequest', undefined, { adapter, @@ -47,9 +28,8 @@ export default (context: any, callback: any) => { } // We'll want to null out the request on success/failure - let xhr = cors ? new CrossDomainRequest() : new XmlHttpRequest() + let xhr = new XmlHttpRequest() - const isXdr = win && (win as any).XDomainRequest && xhr instanceof (win as any).XDomainRequest const headers = options.headers const delays = options.timeout @@ -66,17 +46,11 @@ export default (context: any, callback: any) => { aborted = true } - // IE9 must have onprogress be set to a unique function - xhr.onprogress = () => { - /* intentional noop */ - } - - const loadEvent = isXdr ? 'onload' : 'onreadystatechange' - xhr[loadEvent] = () => { + xhr.onreadystatechange = () => { // Prevent request from timing out resetTimers() - if (aborted || (xhr.readyState !== 4 && !isXdr)) { + if (aborted || xhr.readyState !== 4) { return } @@ -106,8 +80,6 @@ export default (context: any, callback: any) => { xhr.setRequestHeader(key, headers[key]) } } - } else if (headers && isXdr) { - throw new Error('Headers cannot be set on an XDomainRequest object') } if (options.rawBody) { @@ -174,7 +146,7 @@ export default (context: any, callback: any) => { // Clean up stopTimers(true) loaded = true - xhr = null + ;(xhr as any) = null // Annoyingly, details are extremely scarce and hidden from us. // We only really know that it is a network error @@ -185,29 +157,13 @@ export default (context: any, callback: any) => { } function reduceResponse() { - let statusCode = xhr.status - let statusMessage = xhr.statusText - - if (isXdr && statusCode === undefined) { - // IE8 CORS GET successful response doesn't have a status field, but body is fine - statusCode = 200 - } else if (statusCode > 12000 && statusCode < 12156) { - // Yet another IE quirk where it emits weird status codes on network errors - // https://support.microsoft.com/en-us/kb/193625 - return onError() - } else { - // Another IE bug where HTTP 204 somehow ends up as 1223 - statusCode = xhr.status === 1223 ? 204 : xhr.status - statusMessage = xhr.status === 1223 ? 'No Content' : statusMessage - } - return { body: xhr.response || xhr.responseText, url: options.url, method: options.method, - headers: isXdr ? {} : parseHeaders(xhr.getAllResponseHeaders()), - statusCode: statusCode, - statusMessage: statusMessage, + headers: parseHeaders(xhr.getAllResponseHeaders()), + statusCode: xhr.status, + statusMessage: xhr.statusText, } } diff --git a/src/request/browser/fetchXhr.ts b/src/request/browser/fetchXhr.ts index 1672ee49..05856c4f 100644 --- a/src/request/browser/fetchXhr.ts +++ b/src/request/browser/fetchXhr.ts @@ -1,72 +1,101 @@ /** * Mimicks the XMLHttpRequest API with only the parts needed for get-it's XHR adapter */ -function FetchXhr(this: any) { - this.readyState = 0 // Unsent -} -FetchXhr.prototype.open = function (method: any, url: any) { - this._method = method - this._url = url - this._resHeaders = '' - this.readyState = 1 // Open - this.onreadystatechange() -} -FetchXhr.prototype.abort = function () { - if (this._controller) { - this._controller.abort() +export class FetchXhr + implements Pick +{ + /** + * Public interface, interop with real XMLHttpRequest + */ + onabort: () => void + onerror: (error?: any) => void + onreadystatechange: () => void + ontimeout: XMLHttpRequest['ontimeout'] + /** + * https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState + */ + readyState: 0 | 1 | 2 | 3 | 4 = 0 + response: XMLHttpRequest['response'] + responseText: XMLHttpRequest['responseText'] + responseType: XMLHttpRequest['responseType'] + status: XMLHttpRequest['status'] + statusText: XMLHttpRequest['statusText'] + withCredentials: XMLHttpRequest['withCredentials'] + + /** + * Private implementation details + */ + #method: string + #url: string + #resHeaders: string + #headers: Record = {} + #controller?: AbortController + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- _async is only declared for typings compatibility + open(method: string, url: string, _async?: boolean) { + this.#method = method + this.#url = url + this.#resHeaders = '' + this.readyState = 1 // Open + this.onreadystatechange() + this.#controller = undefined } -} -FetchXhr.prototype.getAllResponseHeaders = function () { - return this._resHeaders -} -FetchXhr.prototype.setRequestHeader = function (key: any, value: any) { - this._headers = this._headers || {} - this._headers[key] = value -} -FetchXhr.prototype.send = function (body: any) { - const ctrl = (this._controller = typeof AbortController === 'function' && new AbortController()) - const textBody = this.responseType !== 'arraybuffer' - const options: any = { - method: this._method, - headers: this._headers, - signal: (ctrl && ctrl.signal) || undefined, - body, + abort() { + if (this.#controller) { + this.#controller.abort() + } } - - // Some environments (like CloudFlare workers) don't support credentials in - // RequestInitDict, and there doesn't seem to be any easy way to check for it, - // so for now let's just make do with a window check :/ - if (typeof document !== 'undefined') { - options.credentials = this.withCredentials ? 'include' : 'omit' + getAllResponseHeaders() { + return this.#resHeaders } + setRequestHeader(name: string, value: string) { + this.#headers[name] = value + } + send(body: BodyInit) { + const textBody = this.responseType !== 'arraybuffer' + const options: RequestInit = { + method: this.#method, + headers: this.#headers, + signal: null, + body, + } + if (typeof AbortController === 'function') { + this.#controller = new AbortController() + options.signal = this.#controller.signal + } + + // Some environments (like CloudFlare workers) don't support credentials in + // RequestInitDict, and there doesn't seem to be any easy way to check for it, + // so for now let's just make do with a document check :/ + if (typeof document !== 'undefined') { + options.credentials = this.withCredentials ? 'include' : 'omit' + } - fetch(this._url, options) - .then((res: any) => { - res.headers.forEach((value: any, key: any) => { - this._resHeaders += `${key}: ${value}\r\n` + fetch(this.#url, options) + .then((res): Promise => { + res.headers.forEach((value: any, key: any) => { + this.#resHeaders += `${key}: ${value}\r\n` + }) + this.status = res.status + this.statusText = res.statusText + this.readyState = 3 // Loading + return textBody ? res.text() : res.arrayBuffer() + }) + .then((resBody) => { + if (typeof resBody === 'string') { + this.responseText = resBody + } else { + this.response = resBody + } + this.readyState = 4 // Done + this.onreadystatechange() }) - this.status = res.status - this.statusText = res.statusText - this.readyState = 3 // Loading - return textBody ? res.text() : res.arrayBuffer() - }) - .then((resBody) => { - if (textBody) { - this.responseText = resBody - } else { - this.response = resBody - } - this.readyState = 4 // Done - this.onreadystatechange() - }) - .catch((err) => { - if (err.name === 'AbortError') { - this.onabort() - return - } + .catch((err: Error) => { + if (err.name === 'AbortError') { + this.onabort() + return + } - this.onerror(err) - }) + this.onerror?.(err) + }) + } } - -export default FetchXhr diff --git a/src/util/pubsub.ts b/src/util/pubsub.ts new file mode 100644 index 00000000..041aad7d --- /dev/null +++ b/src/util/pubsub.ts @@ -0,0 +1,32 @@ +// Code borrowed from https://github.com/bjoerge/nano-pubsub + +export interface Subscriber { + (event: Event): void +} +export interface PubSub { + publish: (message: Message) => void + subscribe: (subscriber: Subscriber) => () => void +} + +export default function createPubSub(): PubSub { + const subscribers: {[id: string]: Subscriber} = Object.create(null) + let nextId = 0 + function subscribe(subscriber: Subscriber) { + const id = nextId++ + subscribers[id] = subscriber + return function unsubscribe() { + delete subscribers[id] + } + } + + function publish(event: Message) { + for (const id in subscribers) { + subscribers[id](event) + } + } + + return { + publish, + subscribe, + } +} diff --git a/test-deno/import_map.json b/test-deno/import_map.json index c997c663..1feb887d 100644 --- a/test-deno/import_map.json +++ b/test-deno/import_map.json @@ -4,9 +4,7 @@ "debug": "https://esm.sh/debug@^4.3.4", "form-urlencoded": "https://esm.sh/form-urlencoded@^6.1.0", "is-plain-object": "https://esm.sh/is-plain-object@^5.0.0", - "nano-pubsub": "https://esm.sh/nano-pubsub@^2.0.1", "parse-headers": "https://esm.sh/parse-headers@^2.0.5", - "same-origin": "https://esm.sh/same-origin@^0.1.1", "url-parse": "https://esm.sh/url-parse@^1.5.10" } } diff --git a/test/abort.test.ts b/test/abort.test.ts index 40a91b71..b3851b68 100644 --- a/test/abort.test.ts +++ b/test/abort.test.ts @@ -7,7 +7,7 @@ import {baseUrl, debugRequest} from './helpers' describe('aborting requests', () => { it('should be able to abort requests', () => { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const request = getIt([baseUrl, debugRequest]) const req = request({url: '/delay'}) @@ -21,7 +21,7 @@ describe('aborting requests', () => { ) setTimeout(() => req.abort.publish(), 15) - setTimeout(() => resolve(undefined), 250) + setTimeout(resolve, 250) }) }) }) diff --git a/test/basics.test.ts b/test/basics.test.ts index 7e8a4362..cfca642b 100644 --- a/test/basics.test.ts +++ b/test/basics.test.ts @@ -162,7 +162,7 @@ describe( }) it('should be able to clone a requester, keeping the same middleware', () => - new Promise((resolve) => { + new Promise((resolve) => { let i = 0 const onRequest = () => i++ const base = getIt([baseUrl, {onRequest}]) @@ -173,7 +173,7 @@ describe( setTimeout(() => { expect(i).to.equal(2, 'two requests should have been initiated') - resolve(undefined) + resolve() }, 15) })) }, diff --git a/test/debug.test.ts b/test/debug.test.ts index aa8d51c1..4a79144f 100644 --- a/test/debug.test.ts +++ b/test/debug.test.ts @@ -15,14 +15,14 @@ describe('debug middleware', () => { }) it('should be able to pass custom logger', () => - new Promise((resolve) => { + new Promise((resolve) => { const logger = debug({log}) const request = getIt([baseUrl, logger]) request({url: '/plain-text'}).response.subscribe(() => resolve(undefined)) })) it('should be able to pass custom logger (verbose mode)', () => - new Promise((resolve) => { + new Promise((resolve) => { const logger = debug({log, verbose: true}) const request = getIt([baseUrl, logger]) request({url: '/plain-text'}).response.subscribe(() => resolve(undefined)) diff --git a/test/progress.test.ts b/test/progress.test.ts index 37a1ff88..f569a7a9 100644 --- a/test/progress.test.ts +++ b/test/progress.test.ts @@ -113,7 +113,8 @@ describe('progress', () => { expect(events).to.be.above(0, 'should have received progress events') resolve(undefined) }) - }) + }), + {timeout: 10000} ) it( diff --git a/tsconfig.settings.json b/tsconfig.settings.json index 43c716c9..281b36b3 100644 --- a/tsconfig.settings.json +++ b/tsconfig.settings.json @@ -4,13 +4,14 @@ "declaration": true, "declarationMap": true, "sourceMap": true, + "lib": ["dom"], // Strict type-checking "strict": true, "noImplicitAny": true, "strictNullChecks": true, "strictFunctionTypes": true, - "strictPropertyInitialization": true, + "strictPropertyInitialization": false, "noImplicitThis": true, "alwaysStrict": true,