Skip to content

Commit

Permalink
Merge pull request #101 from lifeomic/followNodeFetchSpec
Browse files Browse the repository at this point in the history
Follow node fetch spec
  • Loading branch information
DavidTanner committed Nov 15, 2021
2 parents ef10b3f + d2e3334 commit ccd6b8e
Show file tree
Hide file tree
Showing 11 changed files with 254 additions and 134 deletions.
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

0 comments on commit ccd6b8e

Please sign in to comment.