Skip to content

Commit

Permalink
feat: Add the ability to pass custom fetch functions
Browse files Browse the repository at this point in the history
  • Loading branch information
cuibonobo committed Aug 13, 2023
1 parent 3a04750 commit 4bd2d49
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 46 deletions.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![codecov badge](https://codecov.io/gh/haverstack/axios-fetch-adapter/branch/main/graph/badge.svg?token=J2J0ANDB3F)](https://codecov.io/gh/haverstack/axios-fetch-adapter)
[![license badge](https://img.shields.io/github/license/haverstack/axios-fetch-adapter)](./LICENSE)

An Axios adapter that uses native `fetch`. Useful for Cloudflare Workers and ServiceWorker environments.
An Axios adapter that uses native `fetch` or a custom `fetch` function. Useful for Cloudflare Workers and ServiceWorker environments.

> **Note:** This adapter was designed for version `0.21.1` of Axios, which is still used in prominent e-commerce SDKs.
Expand All @@ -23,6 +23,18 @@ const client = axios.create({
});
```

To use with a custom `fetch` function:
```javascript
import axios from "axios";
import { createFetchAdapter } from "@haverstack/axios-fetch-adapter";
import myCustomFetch from "my-custom-fetch";

const myCustomFetchAdapter = createFetchAdapter({ fetch: myCustomFetch });
const client = axios.create({
adapter: myCustomFetchAdapter
});
```

To use with the Square API:
```javascript
import { Client, Environment } from "square";
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@haverstack/axios-fetch-adapter",
"version": "0.7.0",
"description": "An Axios adapter that uses native fetch. Useful for Cloudflare Workers and ServiceWorker environments.",
"version": "0.8.0",
"description": "An Axios adapter that uses native fetch or custom fetch functions. Useful for Cloudflare Workers and ServiceWorker environments.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"module": "lib/index.esm.js",
Expand Down
13 changes: 12 additions & 1 deletion src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fetchAdapter from ".";
import fetchAdapter, { createFetchAdapter } from ".";
import type { AxiosError } from "axios";
import axios from "axios";

// Test URL
const url = "http://localhost:1";
Expand Down Expand Up @@ -153,6 +154,16 @@ test("Serialization set with `paramsSerializer` produces correct results", async
expect(result.request.url).toBe(`${url}/?foo=100`);
});

test("Can set custom `fetch` functions", async () => {
const testMsg = "Custom fetch!";
const myFetch = async (_: RequestInfo | URL) => {
return new Response(testMsg);
};
const axiosInstance = axios.create({ adapter: createFetchAdapter({ fetch: myFetch }) });
const result = await axiosInstance.get(url);
expect(result.data).toBe(testMsg);
});

test("Invalid request will throw an error", async () => {
// Disables fetch mock for the rest of this file
jest.restoreAllMocks();
Expand Down
101 changes: 59 additions & 42 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,58 +31,75 @@ import { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios";
import buildURL from "axios/lib/helpers/buildURL";
import { buildFullPath } from "./buildFullPath";

export default async function fetchAdapter(config: AxiosRequestConfig): Promise<AxiosResponse> {
const request = createRequest(config);
const promiseChain = [getResponse(request, config)];
let timer: NodeJS.Timeout | null = null;

if (config.timeout && config.timeout > 0) {
promiseChain.push(
new Promise((_, reject) => {
timer = setTimeout(() => {
const message = config.timeoutErrorMessage
? config.timeoutErrorMessage
: "timeout of " + config.timeout + "ms exceeded";
reject(createError(message, config, "ETIMEDOUT", request));
}, config.timeout);
})
);
}
type FetchFunction = (
input: RequestInfo | URL,
init?: RequestInit | undefined
) => Promise<Response>;
type AdapterFunction = (config: AxiosRequestConfig) => Promise<AxiosResponse>;
interface FetchAdapterConfig {
fetch: FetchFunction;
}

const response = await Promise.race(promiseChain);
// Cancel the timeout timer if it's set
if (timer !== null) {
clearTimeout(timer);
}
return new Promise((resolve, reject) => {
if (response instanceof Error) {
reject(response);
} else {
const validateStatus = config.validateStatus;
if (!response.status || !validateStatus || validateStatus(response.status)) {
resolve(response);
export function createFetchAdapter(fetchConfig?: FetchAdapterConfig): AdapterFunction {
const adapterFetch = fetchConfig ? fetchConfig.fetch : undefined;
async function axiosAdapter(config: AxiosRequestConfig): Promise<AxiosResponse> {
const request = createRequest(config);
const promiseChain = [getResponse(request, config, adapterFetch)];
let timer: NodeJS.Timeout | null = null;

if (config.timeout && config.timeout > 0) {
promiseChain.push(
new Promise((_, reject) => {
timer = setTimeout(() => {
const message = config.timeoutErrorMessage
? config.timeoutErrorMessage
: "timeout of " + config.timeout + "ms exceeded";
reject(createError(message, config, "ETIMEDOUT", request));
}, config.timeout);
})
);
}

const response = await Promise.race(promiseChain);
// Cancel the timeout timer if it's set
if (timer !== null) {
clearTimeout(timer);
}
return new Promise((resolve, reject) => {
if (response instanceof Error) {
reject(response);
} else {
reject(
createError(
"Request failed with status code " + response.status,
config,
getErrorCodeFromStatus(response.status),
request,
response
)
);
const validateStatus = config.validateStatus;
if (!response.status || !validateStatus || validateStatus(response.status)) {
resolve(response);
} else {
reject(
createError(
"Request failed with status code " + response.status,
config,
getErrorCodeFromStatus(response.status),
request,
response
)
);
}
}
}
});
});
}
return axiosAdapter;
}

const fetchAdapter = createFetchAdapter();
export default fetchAdapter;

async function getResponse(
request: Request,
config: AxiosRequestConfig
config: AxiosRequestConfig,
adapterFetch: FetchFunction = fetch
): Promise<(AxiosResponse & { ok: boolean }) | AxiosError> {
let stageOne;
try {
stageOne = await fetch(request);
stageOne = await adapterFetch(request);
} catch (e) {
return createError("Network Error", config, "ERR_NETWORK", request);
}
Expand Down

0 comments on commit 4bd2d49

Please sign in to comment.