Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Follow node fetch spec #101

Merged
merged 9 commits into from
Nov 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/pr-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@ jobs:
- 14.x
- 16.x
axios-version:
- v0.17
- v0.18
- v0.19
- v0.20.0-0
- v0.21.1
- v0.21.4
- v0.22.0
- v0.23.0
- v0.24.0
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ node_modules/
.idea/
src/**/*.js
src/**/*.d.ts
!src/axios-types.d.ts
test/**/*.js
24 changes: 19 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
# Axios-Fetch

[![Greenkeeper badge](https://badges.greenkeeper.io/lifeomic/axios-fetch.svg)](https://greenkeeper.io/)
[![Build Status](https://travis-ci.org/lifeomic/axios-fetch.svg?branch=master)](https://travis-ci.org/lifeomic/axios-fetch)
[![npm](https://img.shields.io/npm/v/@lifeomic/axios-fetch.svg)](https://www.npmjs.com/package/@lifeomic/axios-fetch)
[![Build Status](https://github.com/lifeomic/axios-fetch/actions/workflows/release.yaml/badge.svg)](https://github.com/lifeomic/axios-fetch/actions/workflows/release.yaml)
[![Coverage Status](https://coveralls.io/repos/github/lifeomic/axios-fetch/badge.svg?branch=master)](https://coveralls.io/github/lifeomic/axios-fetch?branch=master)
![Dependabot Badge](https://flat.badgen.net/dependabot/lifeomic/axios-fetch?icon=dependabot)

This library exposes a Fetch WebAPI implementation backed by a Axios client
This library exposes a Fetch WebAPI implementation backed by an Axios client
instance. This allows a bridge between projects that have pre-configured Axios
clients already to other libraries that require Fetch implementations.

## Global Response object

It is expected that the global Response object will be available. For testing we use the [node-fetch
](https://www.npmjs.com/package/node-fetch) library.

```typescript
import { Response } from 'node-fetch';
// @ts-expect-error node-fetch doesn't exactly match the Response object, but close enough.
global.Response = Response;
```

## Example

One library that wants a Fetch implementation is the [Apollo Link
HTTP](https://www.apollographql.com/docs/link/links/http.html) library. If your
project has an existing Axios client configured, then this project can help you
Expand All @@ -17,7 +31,7 @@ use that client in your apollo-link-http instance. Here is some sample code:
const { buildAxiosFetch } = require("@lifeomic/axios-fetch");
const { createHttpLink } = require("apollo-link-http");
const link = createHttpLink({
uri: "/graphql"
uri: "/graphql",
fetch: buildAxiosFetch(yourAxiosInstance)
});
```
Expand All @@ -36,7 +50,7 @@ const fetch = buildAxiosFetch(yourAxiosInstance, function (config) {
});
```

## Support for IE11
## Support for IE11

To Support IE11 add following dependencies

Expand Down
14 changes: 6 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
],
"scripts": {
"test": "nyc ava",
"lint": "eslint . --ext .js,.ts -f codeframe && tsc --noEmit",
"lint": "eslint . --ext .js,.ts -f codeframe",
"postlint": "yarn tsc --noEmit",
"pretest": "yarn lint",
"coverage": "nyc report --reporter=text-lcov > ./.nyc_output/lcov.info",
"prepublishOnly": "yarn tsc"
Expand Down Expand Up @@ -41,22 +42,19 @@
"@typescript-eslint/eslint-plugin": "^5.1.0",
"@typescript-eslint/parser": "^5.1.0",
"ava": "^3.15.0",
"axios": "^0.23.0",
"axios": "^0.24.0",
"coveralls": "^3.1.1",
"eslint": "^7.32.0",
"form-data": "^4.0.0",
"nock": "^13.1.4",
"node-fetch": "^2.6.1",
"nyc": "^15.1.0",
"sinon": "^11.1.2",
"ts-node": "^10.3.0",
"typescript": "^4.4.4"
},
"dependencies": {
"@types/node-fetch": "^2.5.10",
"form-data": "^2.5.0",
"node-fetch": "^2.6.1"
},
"eslintConfig": {
"extends": "plugin:@lifeomic/node/recommended"
"@types/node-fetch": "^2.5.10"
},
"nyc": {
"check-coverage": true,
Expand Down
3 changes: 2 additions & 1 deletion src/types.ts → src/axios-types.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copied and edited from axios@0.21.1 because types aren't available in version 0.17
/* eslint-disable */
// Copied and edited from axios@0.21.1

export type Method =
| 'get' | 'GET'
Expand Down
115 changes: 53 additions & 62 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,76 +1,67 @@
import { Response, Headers as FetchHeaders } from 'node-fetch';
import FormData from 'form-data';
import { AxiosInstance, AxiosRequestConfig } from './types';
import {
createAxiosHeaders,
createFetchHeaders,
getUrl
} from './typeUtils';
import { AxiosInstance, AxiosRequestConfig } from './axios-types';

export interface FetchInit extends Record<string, any> {
headers?: Record<string, string>;
method?: AxiosRequestConfig['method'];
body?: FormData | any;
extra?: any;
}

export type AxiosTransformer = (config: AxiosRequestConfig, input: string | undefined, init: FetchInit) => AxiosRequestConfig;

export type AxiosFetch = (input?: string, init?: FetchInit) => Promise<Response>;
export type AxiosTransformer<Init extends RequestInit = RequestInit> =
(config: AxiosRequestConfig, input: RequestInfo, init?: Init) => AxiosRequestConfig;

/**
* A Fetch WebAPI implementation based on the Axios client
*/
async function axiosFetch (
const axiosFetch = <Init extends RequestInit = RequestInit>(
axios: AxiosInstance,
// Convert the `fetch` style arguments into a Axios style config
transformer?: AxiosTransformer,
input?: string,
init: FetchInit = {}
) {
const rawHeaders: Record<string, string> = init.headers || {};
const lowerCasedHeaders = Object.keys(rawHeaders).filter(key => key && rawHeaders[key])
.reduce<Record<string, string>>(
(acc, key) => {
acc[key.toLowerCase()] = rawHeaders[key] as string;
return acc;
},
{}
);
transformer: AxiosTransformer<Init> = (config) => config
) => async (
input: RequestInfo,
init?: Init
) => {
const rawHeaders = createAxiosHeaders(init?.headers);
const lowerCasedHeaders: Record<string, string> = {};
Object.entries(rawHeaders).forEach(([name, value]) => {
lowerCasedHeaders[name.toLowerCase()] = value;
});

if (!('content-type' in lowerCasedHeaders)) {
lowerCasedHeaders['content-type'] = 'text/plain;charset=UTF-8';
}
if (!('content-type' in lowerCasedHeaders)) {
lowerCasedHeaders['content-type'] = 'text/plain;charset=UTF-8';
}

const rawConfig: AxiosRequestConfig = {
url: input,
method: init.method || 'GET',
data: typeof init.body === 'undefined' || init.body instanceof FormData ? init.body : String(init.body),
headers: lowerCasedHeaders,
// Force the response to an arraybuffer type. Without this, the Response
// object will try to guess the content type and add headers that weren't in
// the response.
// NOTE: Don't use 'stream' because it's not supported in the browser
responseType: 'arraybuffer'
};
const rawConfig: AxiosRequestConfig = {
url: getUrl(input),
method: (init?.method as AxiosRequestConfig['method']) || 'GET',
data: init?.body,
headers: lowerCasedHeaders,
// Force the response to an arraybuffer type. Without this, the Response
// object will try to guess the content type and add headers that weren't in
// the response.
// NOTE: Don't use 'stream' because it's not supported in the browser
responseType: 'arraybuffer'
};

const config = transformer ? transformer(rawConfig, input, init) : rawConfig;
const config = transformer(rawConfig, input, init);

let result;
try {
result = await axios.request(config);
} catch (err: any) {
if (err.response) {
result = err.response;
} else {
throw err;
let result;
try {
result = await axios.request(config);
} catch (err: any) {
if (err.response) {
result = err.response;
} else {
throw err;
}
}
}

const fetchHeaders = new FetchHeaders(result.headers);

return new Response(result.data, {
status: result.status,
statusText: result.statusText,
headers: fetchHeaders
});
}
return new Response(result.data, {
status: result.status,
statusText: result.statusText,
headers: createFetchHeaders(result.headers)
});
};

export function buildAxiosFetch (axios: AxiosInstance, transformer?: AxiosTransformer): AxiosFetch {
return axiosFetch.bind(undefined, axios, transformer);
}
export const buildAxiosFetch = <Init extends RequestInit = RequestInit> (
axios: AxiosInstance,
transformer?: AxiosTransformer<Init>
) => axiosFetch(axios, transformer);
53 changes: 53 additions & 0 deletions src/typeUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Headers as NodeHeaders } from 'node-fetch';

export type HeadersLike = string[][] | Record<string, string | undefined> | Headers | NodeHeaders;

export type UrlLike = string | {
href?: string;
url?: string;
}

export function createFetchHeaders (axiosHeaders: Record<string, string> = {}): string[][] {
const headers: string[][] = [];
Object.entries(axiosHeaders).forEach(([name, value]) => {
headers.push([name, value]);
});
return headers;
}

const isHeaders = (headers: HeadersLike): headers is Headers => headers.constructor.name === 'Headers';

export function createAxiosHeaders (headers: HeadersLike = {}): Record<string, string> {
const rawHeaders: Record<string, string> = {};

if (isHeaders(headers)) {
headers.forEach((value, name) => {
rawHeaders[name] = value;
});
} else if (Array.isArray(headers)) {
headers.forEach(([name, value]) => {
if (value) {
rawHeaders[name!] = value;
}
});
} else {
Object.entries(headers).forEach(([name, value]) => {
if (value) {
rawHeaders[name] = value;
}
});
}
return rawHeaders;
}

export function getUrl (input?: UrlLike): string | undefined {
let url: string | undefined;
if (typeof input === 'string') {
url = input;
} else if (input?.href) {
url = input.href;
} else if (input?.url) {
url = input.url;
}
return url;
}
Loading