Skip to content

Commit

Permalink
feat: introduce native fetch fallback when http-module is not support…
Browse files Browse the repository at this point in the history
…ed (#515)

First of all, thank you for maintaining the great library!

As described in issue #444, importing `search-insights` on an
'[edge-runtime](https://nextjs.org/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes)'
results in the following error:
`[Error: Could not find a supported HTTP request client in this
environment.]`

The edge runtime however, does [support
`fetch`](https://nextjs.org/docs/pages/api-reference/edge) (and is added
to node [per version
18](https://nodejs.org/dist/latest-v18.x/docs/api/globals.html#fetch)).

This PR adds a fallback to the native `fetch` method when
nodeHttpRequest is not available.

Please let me know if you'd like to see anything changed :-).

Kind regards.

Co-authored-by: Jeffrey <[email protected]>
  • Loading branch information
Jeffrey-Zutt and Jeffrey authored Nov 2, 2023
1 parent 990a051 commit 6a322a4
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 7 deletions.
2 changes: 2 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
console.info = () => {};

require("jest-fetch-mock").enableMocks();
10 changes: 9 additions & 1 deletion lib/utils/__tests__/featureDetection.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
supportsCookies,
supportsSendBeacon,
supportsXMLHttpRequest,
supportsNodeHttpModule
supportsNodeHttpModule,
supportsNativeFetch
} from "../featureDetection";

describe("featureDetection in node env", () => {
Expand All @@ -31,6 +32,13 @@ describe("featureDetection in node env", () => {
});
});

describe("supportsNativeFetch", () => {
it("should return true", () => {
// it is available in node env
expect(supportsNativeFetch()).toBe(true);
});
});

describe("supportsNodeHttpModule", () => {
it("should return true", () => {
// it is available in node env
Expand Down
33 changes: 32 additions & 1 deletion lib/utils/__tests__/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { Socket } from "net";
import {
supportsSendBeacon,
supportsXMLHttpRequest,
supportsNodeHttpModule
supportsNodeHttpModule,
supportsNativeFetch
} from "../featureDetection";
import { getRequesterForBrowser } from "../getRequesterForBrowser";
import { getRequesterForNode } from "../getRequesterForNode";
Expand Down Expand Up @@ -125,6 +126,7 @@ describe("request", () => {
expect(setRequestHeader).not.toHaveBeenCalled();
expect(httpRequest).not.toHaveBeenCalled();
expect(httpsRequest).not.toHaveBeenCalled();
expect(fetch).not.toHaveBeenCalled();
});

it("should send with XMLHttpRequest if sendBeacon is not available", async () => {
Expand All @@ -144,6 +146,7 @@ describe("request", () => {
expect(send).toHaveBeenLastCalledWith(JSON.stringify(data));
expect(httpRequest).not.toHaveBeenCalled();
expect(httpsRequest).not.toHaveBeenCalled();
expect(fetch).not.toHaveBeenCalled();
});

it("should fall back to XMLHttpRequest if sendBeacon returns false", async () => {
Expand All @@ -165,6 +168,7 @@ describe("request", () => {
expect(send).toHaveBeenLastCalledWith(JSON.stringify(data));
expect(httpRequest).not.toHaveBeenCalled();
expect(httpsRequest).not.toHaveBeenCalled();
expect(fetch).not.toHaveBeenCalled();
});

it("should send with nodeHttpRequest if url does not start with https://", async () => {
Expand All @@ -181,6 +185,7 @@ describe("request", () => {
expect(send).not.toHaveBeenCalled();
expect(setRequestHeader).not.toHaveBeenCalled();
expect(httpsRequest).not.toHaveBeenCalled();
expect(fetch).not.toHaveBeenCalled();
expect(httpRequest).toHaveBeenLastCalledWith(
{
protocol: "http:",
Expand Down Expand Up @@ -213,6 +218,7 @@ describe("request", () => {
expect(setRequestHeader).not.toHaveBeenCalled();
expect(send).not.toHaveBeenCalled();
expect(httpRequest).not.toHaveBeenCalled();
expect(fetch).not.toHaveBeenCalled();
expect(httpsRequest).toHaveBeenLastCalledWith(
{
protocol: "https:",
Expand All @@ -231,6 +237,31 @@ describe("request", () => {
expect(write).toHaveBeenCalledTimes(1);
});

it("should send with fetch if nodeHttpRequest is not available", async () => {
jest.mocked(supportsSendBeacon).mockImplementation(() => false);
jest.mocked(supportsXMLHttpRequest).mockImplementation(() => false);
jest.mocked(supportsNodeHttpModule).mockImplementation(() => false);
jest.mocked(supportsNativeFetch).mockImplementation(() => true);

const url = "https://random.url";
const data = { foo: "bar" };
const request = getRequesterForNode();
const sent = await request(url, data);
expect(sent).toBe(true);
expect(navigator.sendBeacon).not.toHaveBeenCalled();
expect(open).not.toHaveBeenCalled();
expect(setRequestHeader).not.toHaveBeenCalled();
expect(send).not.toHaveBeenCalled();
expect(httpRequest).not.toHaveBeenCalled();
expect(httpsRequest).not.toHaveBeenCalled();

expect(fetch).toHaveBeenCalledWith("https://random.url", {
body: '{"foo":"bar"}',
headers: { "Content-Type": "application/json" },
method: "POST"
});
});

it.each([
{ browser: true, beacon: true, url: "http://random.url" },
{ browser: true, beacon: false, url: "http://random.url" },
Expand Down
8 changes: 8 additions & 0 deletions lib/utils/featureDetection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,11 @@ export const supportsNodeHttpModule = (): boolean => {
return false;
}
};

export const supportsNativeFetch = (): boolean => {
try {
return fetch !== undefined;
} catch (e) {
return false;
}
};
16 changes: 14 additions & 2 deletions lib/utils/getRequesterForBrowser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { supportsSendBeacon, supportsXMLHttpRequest } from "./featureDetection";
import {
supportsNativeFetch,
supportsSendBeacon,
supportsXMLHttpRequest
} from "./featureDetection";
import type { RequestFnType } from "./request";
import { requestWithSendBeacon, requestWithXMLHttpRequest } from "./request";
import {
requestWithNativeFetch,
requestWithSendBeacon,
requestWithXMLHttpRequest
} from "./request";

export function getRequesterForBrowser(): RequestFnType {
if (supportsSendBeacon()) {
Expand All @@ -11,6 +19,10 @@ export function getRequesterForBrowser(): RequestFnType {
return requestWithXMLHttpRequest;
}

if (supportsNativeFetch()) {
return requestWithNativeFetch;
}

throw new Error(
"Could not find a supported HTTP request client in this environment."
);
Expand Down
11 changes: 9 additions & 2 deletions lib/utils/getRequesterForNode.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { supportsNodeHttpModule } from "./featureDetection";
import {
supportsNodeHttpModule,
supportsNativeFetch
} from "./featureDetection";
import type { RequestFnType } from "./request";
import { requestWithNodeHttpModule } from "./request";
import { requestWithNodeHttpModule, requestWithNativeFetch } from "./request";

export function getRequesterForNode(): RequestFnType {
if (supportsNodeHttpModule()) {
return requestWithNodeHttpModule;
}

if (supportsNativeFetch()) {
return requestWithNativeFetch;
}

throw new Error(
"Could not find a supported HTTP request client in this environment."
);
Expand Down
18 changes: 18 additions & 0 deletions lib/utils/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,21 @@ export const requestWithNodeHttpModule: RequestFnType = (url, data) => {
req.end();
});
};

export const requestWithNativeFetch: RequestFnType = (url, data) => {
return new Promise((resolve, reject) => {
fetch(url, {
method: "POST",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json"
}
})
.then((response) => {
resolve(response.status === 200);
})
.catch((e) => {
reject(e);
});
});
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.6.4",
"jest-environment-jsdom": "^29.6.4",
"jest-fetch-mock": "^3.0.3",
"jest-localstorage-mock": "^2.4.26",
"jest-watch-typeahead": "^2.2.2",
"markdown-toc": "^1.2.0",
Expand Down
22 changes: 21 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2307,6 +2307,13 @@ [email protected]:
js-yaml "^3.13.1"
parse-json "^4.0.0"

cross-fetch@^3.0.4:
version "3.1.8"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82"
integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==
dependencies:
node-fetch "^2.6.12"

cross-spawn@^6.0.0:
version "6.0.5"
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
Expand Down Expand Up @@ -4331,6 +4338,14 @@ jest-environment-node@^29.6.4:
jest-mock "^29.6.3"
jest-util "^29.6.3"

jest-fetch-mock@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz#31749c456ae27b8919d69824f1c2bd85fe0a1f3b"
integrity sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==
dependencies:
cross-fetch "^3.0.4"
promise-polyfill "^8.1.3"

jest-get-type@^29.6.3:
version "29.6.3"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1"
Expand Down Expand Up @@ -5270,7 +5285,7 @@ no-case@^3.0.3:
lower-case "^2.0.1"
tslib "^1.10.0"

node-fetch@^2.6.0, node-fetch@^2.6.1:
node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.12:
version "2.7.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
Expand Down Expand Up @@ -5905,6 +5920,11 @@ promise-inflight@^1.0.1:
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==

promise-polyfill@^8.1.3:
version "8.3.0"
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.3.0.tgz#9284810268138d103807b11f4e23d5e945a4db63"
integrity sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==

promise-retry@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22"
Expand Down

0 comments on commit 6a322a4

Please sign in to comment.