diff --git a/CHANGELOG.md b/CHANGELOG.md index 02f3e4f..ba451ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.15.0 (2022/12/01) + +- Breaking change: createHttpError & createJSONHttpError params are no longer optional +- Test with Undici fetch + ## 0.14.2 (2022/12/01) - Update npm packages diff --git a/README.md b/README.md index 90c3d98..a2908fb 100644 --- a/README.md +++ b/README.md @@ -130,8 +130,8 @@ Check [examples/web](examples/web) - `createResponsePromise(body?:` [`BodyInit`](https://fetch.spec.whatwg.org/#bodyinit)`, init?:` [`ResponseInit`](https://fetch.spec.whatwg.org/#responseinit)`): ResponsePromiseWithBodyMethods` - `createJSONResponsePromise(body: object, init?: ResponseInit): ResponsePromiseWithBodyMethods` -- `createHttpError(body?: BodyInit, status = 0, statusText?: string): HttpError` -- `createJSONHttpError(body: object, status = 0, statusText?: string): HttpError` +- `createHttpError(body: BodyInit, status: number, statusText?: string): HttpError` +- `createJSONHttpError(body: object, status: number, statusText?: string): HttpError` ### HttpStatus diff --git a/jest.setup.ts b/jest.setup.ts index ef050cc..0e446cf 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -10,17 +10,27 @@ switch (fetchPolyfill) { case 'whatwg-fetch': { const whatwgFetch = require('whatwg-fetch'); globalThis.fetch = whatwgFetch.fetch; - // Tests are very slow with whatwg-fetch (150s) vs node-fetch (2s) - jest.setTimeout(10_000); + globalThis.Request = whatwgFetch.Request; + globalThis.Response = whatwgFetch.Response; + globalThis.Headers = whatwgFetch.Headers; break; } case 'node-fetch': { const nodeFetch = require('node-fetch'); globalThis.fetch = nodeFetch.default; + globalThis.Request = nodeFetch.Request; globalThis.Response = nodeFetch.Response; globalThis.Headers = nodeFetch.Headers; break; } + case 'undici': { + const undici = require('undici'); + globalThis.fetch = undici.fetch; + globalThis.Request = undici.Request; + globalThis.Response = undici.Response; + globalThis.Headers = undici.Headers; + break; + } default: { assert(false, `Invalid fetch polyfill: '${fetchPolyfill}'`); } diff --git a/package-lock.json b/package-lock.json index 8eeb29f..1123456 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "tslib": "^2.4.1", "typescript": "^4.9.3", "ua-parser-js": "^1.0.32", + "undici": "^5.13.0", "whatwg-fetch": "^3.6.2" } }, @@ -3747,6 +3748,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dev": true, + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -9333,6 +9346,15 @@ "node": ">=8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -9704,6 +9726,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.13.0.tgz", + "integrity": "sha512-UDZKtwb2k7KRsK4SdXWG7ErXiL7yTGgLWvk2AXO1JMjgjh404nFo6tWSCM2xMpJwMPx3J8i/vfqEh1zOqvj82Q==", + "dev": true, + "dependencies": { + "busboy": "^1.6.0" + }, + "engines": { + "node": ">=12.18" + } + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -12771,6 +12805,15 @@ "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", "dev": true }, + "busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dev": true, + "requires": { + "streamsearch": "^1.1.0" + } + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -16887,6 +16930,12 @@ } } }, + "streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "dev": true + }, "string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -17158,6 +17207,15 @@ "which-boxed-primitive": "^1.0.2" } }, + "undici": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.13.0.tgz", + "integrity": "sha512-UDZKtwb2k7KRsK4SdXWG7ErXiL7yTGgLWvk2AXO1JMjgjh404nFo6tWSCM2xMpJwMPx3J8i/vfqEh1zOqvj82Q==", + "dev": true, + "requires": { + "busboy": "^1.6.0" + } + }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", diff --git a/package.json b/package.json index 70e2749..55baf32 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "test:node-fetch": "NODE_EXTRA_CA_CERTS=./src/createTestServer/createTestServer.cert FETCH=node-fetch jest --verbose", "test:coverage:node-fetch": "NODE_EXTRA_CA_CERTS=./src/createTestServer/createTestServer.cert FETCH=node-fetch jest --coverage", "test:whatwg-fetch": "NODE_EXTRA_CA_CERTS=./src/createTestServer/createTestServer.cert FETCH=whatwg-fetch jest --env=jsdom --verbose", + "test:undici": "NODE_EXTRA_CA_CERTS=./src/createTestServer/createTestServer.cert FETCH=undici jest --verbose", "test:coverage:whatwg-fetch": "NODE_EXTRA_CA_CERTS=./src/createTestServer/createTestServer.cert FETCH=whatwg-fetch jest --env=jsdom --coverage", "test:e2e": "playwright test", "test:e2e:debug": "playwright test --debug", @@ -89,6 +90,7 @@ "tslib": "^2.4.1", "typescript": "^4.9.3", "ua-parser-js": "^1.0.32", + "undici": "^5.13.0", "whatwg-fetch": "^3.6.2" } } diff --git a/src/Http.test.ts b/src/Http.test.ts index 888c7fc..ace1f26 100644 --- a/src/Http.test.ts +++ b/src/Http.test.ts @@ -2,7 +2,6 @@ import assert from 'node:assert'; import { createTestServer } from './createTestServer/createTestServer'; import { entriesToObject } from './utils/entriesToObject'; -import { isWhatwgFetch } from './utils/isWhatwgFetch'; import { defaults, del, get, patch, patchJSON, post, postJSON, put, putJSON } from './Http'; const path = '/'; @@ -480,7 +479,7 @@ describe('body methods', () => { const response = await get(url).blob(); expect(response.size).toEqual(3); // FIXME https://github.com/jsdom/jsdom/issues/2555 - if (!isWhatwgFetch) { + if (process.env.FETCH !== 'whatwg-fetch') { // eslint-disable-next-line jest/no-conditional-expect expect(await response.text()).toEqual('*/*'); } @@ -598,11 +597,24 @@ describe('body methods', () => { }); // https://github.com/whatwg/fetch/issues/1147 - // eslint-disable-next-line unicorn/consistent-function-scoping - const nodeFetchBodyAlreadyUsedError = (url: string) => `body used already for: ${url}`; - const whatwgFetchBodyAlreadyUsedError = 'Already read'; - const bodyAlreadyUsedError = (url: string) => - isWhatwgFetch ? whatwgFetchBodyAlreadyUsedError : nodeFetchBodyAlreadyUsedError(url); + let bodyAlreadyUsedError = ''; + switch (process.env.FETCH) { + case 'node-fetch': { + bodyAlreadyUsedError = 'body used already for: '; + break; + } + case 'whatwg-fetch': { + bodyAlreadyUsedError = 'Already read'; + break; + } + case 'undici': { + bodyAlreadyUsedError = 'Body is unusable'; + break; + } + default: { + assert(false, `Unknown FETCH env '${process.env.FETCH}'`); + } + } test('multiple body calls using helpers', async () => { const server = createTestServer(); @@ -614,7 +626,7 @@ describe('body methods', () => { const response = get(url); expect(await response.json()).toEqual({ accept: 'application/json' }); - await expect(response.text()).rejects.toThrow(bodyAlreadyUsedError(url)); + await expect(response.text()).rejects.toThrow(bodyAlreadyUsedError); // FIXME await close() is too slow with Fastify 4.10.2 server.close(); @@ -630,7 +642,7 @@ describe('body methods', () => { const response = await get(url); expect(await response.json()).toEqual({ accept: '*/*' }); - await expect(response.text()).rejects.toThrow(bodyAlreadyUsedError(url)); + await expect(response.text()).rejects.toThrow(bodyAlreadyUsedError); // FIXME await close() is too slow with Fastify 4.10.2 server.close(); @@ -647,7 +659,7 @@ describe('body methods', () => { const response = get(url); expect(await response.json()).toEqual({ accept: 'application/json' }); // eslint-disable-next-line unicorn/no-await-expression-member - await expect((await response).text()).rejects.toThrow(bodyAlreadyUsedError(url)); + await expect((await response).text()).rejects.toThrow(bodyAlreadyUsedError); // FIXME await close() is too slow with Fastify 4.10.2 server.close(); @@ -657,11 +669,24 @@ describe('body methods', () => { test('cannot connect', async () => { const url = 'http://localhost/'; - const nodeFetchRequestFailedError = `request to ${url} failed, reason: connect ECONNREFUSED 127.0.0.1:80`; - const whatwgFetchRequestFailedError = 'Network request failed'; - const requestFailedError = isWhatwgFetch - ? whatwgFetchRequestFailedError - : nodeFetchRequestFailedError; + let requestFailedError = ''; + switch (process.env.FETCH) { + case 'node-fetch': { + requestFailedError = `request to ${url} failed, reason: connect ECONNREFUSED 127.0.0.1:80`; + break; + } + case 'whatwg-fetch': { + requestFailedError = 'Network request failed'; + break; + } + case 'undici': { + requestFailedError = 'fetch failed'; + break; + } + default: { + assert(false, `Unknown FETCH env '${process.env.FETCH}'`); + } + } // Avoid console to be polluted with whatwg-fetch "Error: connect ECONNREFUSED" const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); @@ -671,12 +696,12 @@ test('cannot connect', async () => { } catch (e) { assert(e instanceof Error); /* eslint-disable jest/no-conditional-expect */ - expect(e.name).toEqual(isWhatwgFetch ? 'TypeError' : 'FetchError'); + expect(e.name).toEqual(process.env.FETCH === 'node-fetch' ? 'FetchError' : 'TypeError'); expect(e.message).toEqual(requestFailedError); /* eslint-enable jest/no-conditional-expect */ } - expect(consoleSpy).toHaveBeenCalledTimes(isWhatwgFetch ? 1 : 0); + expect(consoleSpy).toHaveBeenCalledTimes(process.env.FETCH === 'whatwg-fetch' ? 1 : 0); consoleSpy.mockRestore(); }); diff --git a/src/createHttpError.test.ts b/src/createHttpError.test.ts index bc9665f..d6ef489 100644 --- a/src/createHttpError.test.ts +++ b/src/createHttpError.test.ts @@ -1,12 +1,12 @@ /* eslint-disable unicorn/no-null, jest/no-conditional-expect */ +import assert from 'node:assert'; import { Readable } from 'node:stream'; import { entriesToObject } from './utils/entriesToObject'; -import { isWhatwgFetch } from './utils/isWhatwgFetch'; import { createHttpError, createJSONHttpError } from './createHttpError'; -const redirected = isWhatwgFetch ? undefined : false; +const redirected = process.env.FETCH === 'whatwg-fetch' ? undefined : false; // "new Response()" gives a 200 response: // { @@ -54,10 +54,51 @@ const redirected = isWhatwgFetch ? undefined : false; // - Microsoft Edge 44 (EdgeHTML 18) // - IE 11: 'Response' is undefined +function checkBody(body: Body['body']) { + switch (process.env.FETCH) { + case 'node-fetch': { + expect(body).toEqual(expect.any(Readable)); + break; + } + case 'whatwg-fetch': { + expect(body).toEqual(undefined); + break; + } + case 'undici': { + expect(body).toEqual(expect.any(ReadableStream)); + break; + } + default: { + assert(false, `Unknown FETCH env '${process.env.FETCH}'`); + } + } +} + +function checkBodyUsedWithNoBody(bodyUsed: Body['bodyUsed']) { + switch (process.env.FETCH) { + case 'node-fetch': { + // FIXME https://github.com/node-fetch/node-fetch/issues/1684 + expect(bodyUsed).toEqual(true); + break; + } + case 'whatwg-fetch': { + expect(bodyUsed).toEqual(true); + break; + } + case 'undici': { + expect(bodyUsed).toEqual(false); + break; + } + default: { + assert(false, `Unknown FETCH env '${process.env.FETCH}'`); + } + } +} + test('new Response()', async () => { const response = new Response(); // https://github.com/github/fetch/issues/746 - expect(response.body).toEqual(isWhatwgFetch ? undefined : null); // Should be null + expect(response.body).toEqual(process.env.FETCH === 'whatwg-fetch' ? undefined : null); // Should be null expect(response.bodyUsed).toEqual(false); expect(entriesToObject(response.headers)).toEqual({}); expect(response.ok).toEqual(true); @@ -68,13 +109,12 @@ test('new Response()', async () => { expect(response.url).toEqual(''); expect(await response.text()).toEqual(''); - expect(response.bodyUsed).toEqual(true); + checkBodyUsedWithNoBody(response.bodyUsed); }); test("new Response('body')", async () => { const response = new Response('body'); - // https://github.com/github/fetch/issues/746 - expect(response.body).toEqual(isWhatwgFetch ? undefined : expect.any(Readable)); + checkBody(response.body); expect(response.bodyUsed).toEqual(false); expect(entriesToObject(response.headers)).toEqual({ 'content-type': 'text/plain;charset=UTF-8' @@ -92,18 +132,18 @@ test("new Response('body')", async () => { test('Response.error()', async () => { const response = Response.error(); - expect(response.body).toEqual(isWhatwgFetch ? undefined : null); // Should be null + expect(response.body).toEqual(process.env.FETCH === 'whatwg-fetch' ? undefined : null); // Should be null expect(response.bodyUsed).toEqual(false); expect(entriesToObject(response.headers)).toEqual({}); expect(response.ok).toEqual(false); - expect(response.redirected).toEqual(isWhatwgFetch ? undefined : false); // Should be false + expect(response.redirected).toEqual(process.env.FETCH === 'whatwg-fetch' ? undefined : false); // Should be false expect(response.status).toEqual(0); expect(response.statusText).toEqual(''); expect(response.type).toEqual('error'); expect(response.url).toEqual(''); expect(await response.text()).toEqual(''); - expect(response.bodyUsed).toEqual(true); + checkBodyUsedWithNoBody(response.bodyUsed); }); test('200 OK', async () => { @@ -111,7 +151,7 @@ test('200 OK', async () => { const { name, message, response } = createHttpError('body', 200, 'OK'); expect(name).toEqual('HttpError'); expect(message).toEqual('OK'); - expect(response.body).toEqual(isWhatwgFetch ? undefined : expect.any(Readable)); + checkBody(response.body); expect(response.bodyUsed).toEqual(false); expect(entriesToObject(response.headers)).toEqual({ 'content-type': 'text/plain;charset=UTF-8' @@ -131,7 +171,7 @@ test('200 OK', async () => { const { name, message, response } = createJSONHttpError({ body: true }, 200, 'OK'); expect(name).toEqual('HttpError'); expect(message).toEqual('OK'); - expect(response.body).toEqual(isWhatwgFetch ? undefined : expect.any(Readable)); + checkBody(response.body); expect(response.bodyUsed).toEqual(false); expect(entriesToObject(response.headers)).toEqual({ 'content-type': 'application/json' @@ -153,7 +193,7 @@ test('204 No Content', async () => { const { name, message, response } = createHttpError(undefined, 204, 'No Content'); expect(name).toEqual('HttpError'); expect(message).toEqual('No Content'); - expect(response.body).toEqual(isWhatwgFetch ? undefined : null); + expect(response.body).toEqual(process.env.FETCH === 'whatwg-fetch' ? undefined : null); expect(response.bodyUsed).toEqual(false); expect(entriesToObject(response.headers)).toEqual({}); expect(response.ok).toEqual(true); @@ -164,27 +204,30 @@ test('204 No Content', async () => { expect(response.url).toEqual(''); expect(await response.text()).toEqual(''); - expect(response.bodyUsed).toEqual(true); + checkBodyUsedWithNoBody(response.bodyUsed); } - { - const { name, message, response } = createJSONHttpError({}, 204, 'No Content'); - expect(name).toEqual('HttpError'); - expect(message).toEqual('No Content'); - expect(response.body).toEqual(isWhatwgFetch ? undefined : expect.any(Readable)); - expect(response.bodyUsed).toEqual(false); - expect(entriesToObject(response.headers)).toEqual({ - 'content-type': 'application/json' - }); - expect(response.ok).toEqual(true); - expect(response.redirected).toEqual(redirected); - expect(response.status).toEqual(204); - expect(response.statusText).toEqual('No Content'); - expect(response.type).toEqual('default'); - expect(response.url).toEqual(''); - - expect(await response.json()).toEqual({}); - expect(response.bodyUsed).toEqual(true); + switch (process.env.FETCH) { + case 'node-fetch': { + // FIXME https://github.com/node-fetch/node-fetch/issues/1685 + expect(() => createJSONHttpError({}, 204, 'No Content')).not.toThrow(); + break; + } + case 'whatwg-fetch': { + // FIXME https://github.com/github/fetch/issues/1213 + expect(() => createJSONHttpError({}, 204, 'No Content')).not.toThrow(); + break; + } + case 'undici': { + // FIXME Chrome 107: "TypeError: Failed to construct 'Response': Response with null body status cannot have body" + expect(() => createJSONHttpError({}, 204, 'No Content')).toThrow( + 'Response constructor: Invalid response status code 204' + ); + break; + } + default: { + assert(false, `Unknown FETCH env '${process.env.FETCH}'`); + } } }); @@ -193,7 +236,7 @@ test('404 Not Found', async () => { const { name, message, response } = createHttpError('error', 404, 'Not Found'); expect(name).toEqual('HttpError'); expect(message).toEqual('Not Found'); - expect(response.body).toEqual(isWhatwgFetch ? undefined : expect.any(Readable)); + checkBody(response.body); expect(response.bodyUsed).toEqual(false); expect(entriesToObject(response.headers)).toEqual({ 'content-type': 'text/plain;charset=UTF-8' @@ -213,7 +256,7 @@ test('404 Not Found', async () => { const { name, message, response } = createJSONHttpError({ error: 404 }, 404, 'Not Found'); expect(name).toEqual('HttpError'); expect(message).toEqual('Not Found'); - expect(response.body).toEqual(isWhatwgFetch ? undefined : expect.any(Readable)); + checkBody(response.body); expect(response.bodyUsed).toEqual(false); expect(entriesToObject(response.headers)).toEqual({ 'content-type': 'application/json' @@ -235,7 +278,7 @@ test('no statusText', async () => { const { name, message, response } = createHttpError('body', 200); expect(name).toEqual('HttpError'); expect(message).toEqual('200'); - expect(response.body).toEqual(isWhatwgFetch ? undefined : expect.any(Readable)); + checkBody(response.body); expect(response.bodyUsed).toEqual(false); expect(entriesToObject(response.headers)).toEqual({ 'content-type': 'text/plain;charset=UTF-8' @@ -255,7 +298,7 @@ test('no statusText', async () => { const { name, message, response } = createJSONHttpError({ body: true }, 200); expect(name).toEqual('HttpError'); expect(message).toEqual('200'); - expect(response.body).toEqual(isWhatwgFetch ? undefined : expect.any(Readable)); + checkBody(response.body); expect(response.bodyUsed).toEqual(false); expect(entriesToObject(response.headers)).toEqual({ 'content-type': 'application/json' @@ -272,19 +315,66 @@ test('no statusText', async () => { } }); +test('status 0', async () => { + switch (process.env.FETCH) { + case 'node-fetch': { + // FIXME https://github.com/node-fetch/node-fetch/issues/1685 + expect(() => createHttpError('body', 0)).not.toThrow(); + break; + } + case 'whatwg-fetch': { + // FIXME https://github.com/github/fetch/issues/1213 + expect(() => createHttpError('body', 0)).not.toThrow(); + break; + } + case 'undici': { + expect(() => createHttpError('body', 0)).toThrow( + 'init["status"] must be in the range of 200 to 599, inclusive.' + ); + break; + } + default: { + assert(false, `Unknown FETCH env '${process.env.FETCH}'`); + } + } + + switch (process.env.FETCH) { + case 'node-fetch': { + // FIXME https://github.com/node-fetch/node-fetch/issues/1685 + expect(() => createJSONHttpError({ body: true }, 0)).not.toThrow(); + break; + } + case 'whatwg-fetch': { + // FIXME https://github.com/github/fetch/issues/1213 + expect(() => createJSONHttpError({ body: true }, 0)).not.toThrow(); + break; + } + case 'undici': { + expect(() => createJSONHttpError({ body: true }, 0)).toThrow( + 'init["status"] must be in the range of 200 to 599, inclusive.' + ); + break; + } + default: { + assert(false, `Unknown FETCH env '${process.env.FETCH}'`); + } + } +}); + test('no status', async () => { { + // @ts-ignore const { name, message, response } = createHttpError('body'); expect(name).toEqual('HttpError'); - expect(message).toEqual('0'); - expect(response.body).toEqual(isWhatwgFetch ? undefined : expect.any(Readable)); + expect(message).toEqual('200'); + checkBody(response.body); expect(response.bodyUsed).toEqual(false); expect(entriesToObject(response.headers)).toEqual({ 'content-type': 'text/plain;charset=UTF-8' }); - expect(response.ok).toEqual(false); + expect(response.ok).toEqual(true); expect(response.redirected).toEqual(redirected); - expect(response.status).toEqual(0); + expect(response.status).toEqual(200); expect(response.statusText).toEqual(''); expect(response.type).toEqual('default'); expect(response.url).toEqual(''); @@ -294,17 +384,18 @@ test('no status', async () => { } { + // @ts-ignore const { name, message, response } = createJSONHttpError({ body: true }); expect(name).toEqual('HttpError'); - expect(message).toEqual('0'); - expect(response.body).toEqual(isWhatwgFetch ? undefined : expect.any(Readable)); + expect(message).toEqual('200'); + checkBody(response.body); expect(response.bodyUsed).toEqual(false); expect(entriesToObject(response.headers)).toEqual({ 'content-type': 'application/json' }); - expect(response.ok).toEqual(false); + expect(response.ok).toEqual(true); expect(response.redirected).toEqual(redirected); - expect(response.status).toEqual(0); + expect(response.status).toEqual(200); expect(response.statusText).toEqual(''); expect(response.type).toEqual('default'); expect(response.url).toEqual(''); @@ -315,19 +406,20 @@ test('no status', async () => { }); test('no params', async () => { + // @ts-ignore const { name, message, response } = createHttpError(); expect(name).toEqual('HttpError'); - expect(message).toEqual('0'); - expect(response.body).toEqual(isWhatwgFetch ? undefined : null); + expect(message).toEqual('200'); + expect(response.body).toEqual(process.env.FETCH === 'whatwg-fetch' ? undefined : null); expect(response.bodyUsed).toEqual(false); expect(entriesToObject(response.headers)).toEqual({}); - expect(response.ok).toEqual(false); + expect(response.ok).toEqual(true); expect(response.redirected).toEqual(redirected); - expect(response.status).toEqual(0); + expect(response.status).toEqual(200); expect(response.statusText).toEqual(''); expect(response.type).toEqual('default'); expect(response.url).toEqual(''); expect(await response.text()).toEqual(''); - expect(response.bodyUsed).toEqual(true); + checkBodyUsedWithNoBody(response.bodyUsed); }); diff --git a/src/createHttpError.ts b/src/createHttpError.ts index d615bff..19de0f0 100644 --- a/src/createHttpError.ts +++ b/src/createHttpError.ts @@ -8,7 +8,7 @@ import { HttpError } from './HttpError'; * * @see {@link createJSONHttpError()} */ -export function createHttpError(body?: BodyInit, status = 0, statusText?: string) { +export function createHttpError(body: BodyInit | undefined, status: number, statusText?: string) { return new HttpError( new Response(body, { status, @@ -24,8 +24,9 @@ export function createHttpError(body?: BodyInit, status = 0, statusText?: string */ // Record is compatible with "type" not with "interface": "Index signature is missing in type 'MyInterface'" // Best alternative is object, why? https://stackoverflow.com/a/58143592 -export function createJSONHttpError(body: object, status = 0, statusText?: string) { +export function createJSONHttpError(body: object, status: number, statusText?: string) { return new HttpError( + // FIXME Replace with [new Response.json()](https://twitter.com/lcasdev/status/1564598435772342272) new Response(JSON.stringify(body), { status, statusText, diff --git a/src/createResponsePromise.test.ts b/src/createResponsePromise.test.ts index fa6b613..419c53b 100644 --- a/src/createResponsePromise.test.ts +++ b/src/createResponsePromise.test.ts @@ -1,5 +1,6 @@ +import assert from 'node:assert'; + import { entriesToObject } from './utils/entriesToObject'; -import { isWhatwgFetch } from './utils/isWhatwgFetch'; import { wait } from './utils/wait'; import { createJSONResponsePromise, createResponsePromise } from './createResponsePromise'; import * as Http from './Http'; @@ -14,7 +15,7 @@ test('default Response object', async () => { 'content-type': 'text/plain;charset=UTF-8' }); expect(response.ok).toEqual(true); - expect(response.redirected).toEqual(isWhatwgFetch ? undefined : false); + expect(response.redirected).toEqual(process.env.FETCH === 'whatwg-fetch' ? undefined : false); expect(response.status).toEqual(200); expect(response.statusText).toEqual(''); expect(response.url).toEqual(''); @@ -32,7 +33,7 @@ describe('body methods', () => { const blob = await responsePromise.blob(); expect(blob.size).toEqual(4); // FIXME https://github.com/jsdom/jsdom/issues/2555 - if (!isWhatwgFetch) { + if (process.env.FETCH !== 'whatwg-fetch') { // eslint-disable-next-line jest/no-conditional-expect expect(await blob.text()).toEqual('test'); } @@ -73,11 +74,24 @@ describe('body methods', () => { }); // https://github.com/whatwg/fetch/issues/1147 - const nodeFetchBodyAlreadyUsedError = 'body used already for: '; - const whatwgFetchBodyAlreadyUsedError = 'Already read'; - const bodyAlreadyUsedError = isWhatwgFetch - ? whatwgFetchBodyAlreadyUsedError - : nodeFetchBodyAlreadyUsedError; + let bodyAlreadyUsedError = ''; + switch (process.env.FETCH) { + case 'node-fetch': { + bodyAlreadyUsedError = 'body used already for: '; + break; + } + case 'whatwg-fetch': { + bodyAlreadyUsedError = 'Already read'; + break; + } + case 'undici': { + bodyAlreadyUsedError = 'Body is unusable'; + break; + } + default: { + assert(false, `Unknown FETCH env '${process.env.FETCH}'`); + } + } test('multiple body calls using helpers', async () => { const responsePromise = createJSONResponsePromise({ test: 'true' }); diff --git a/src/createTestServer/createTestServer.test.ts b/src/createTestServer/createTestServer.test.ts index c77c518..c297a54 100644 --- a/src/createTestServer/createTestServer.test.ts +++ b/src/createTestServer/createTestServer.test.ts @@ -1,4 +1,3 @@ -import { isWhatwgFetch } from '../utils/isWhatwgFetch'; import { createTestServer } from './createTestServer'; const path = '/'; @@ -56,8 +55,8 @@ test.skip('respond to HTTP/2 requests', async () => { server.close(); }); -// node-fetch does not care about CORS -if (isWhatwgFetch) { +// node-fetch & undici don't care about CORS +if (process.env.FETCH === 'whatwg-fetch') { test('CORS fail', async () => { const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); diff --git a/src/utils/isWhatwgFetch.ts b/src/utils/isWhatwgFetch.ts deleted file mode 100644 index 3183d45..0000000 --- a/src/utils/isWhatwgFetch.ts +++ /dev/null @@ -1,2 +0,0 @@ -// https://github.com/github/fetch/blob/v3.5.0/fetch.js#L598 -export const isWhatwgFetch = (fetch as any).polyfill === true;