-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathhttpClient.js
153 lines (133 loc) · 4.29 KB
/
httpClient.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/*!
* Copyright (c) 2020-2022 Digital Bazaar, Inc. All rights reserved.
*/
import {convertAgent} from './agentCompatibility.js';
export const kyOriginalPromise = import('ky-universal')
.then(({default: ky}) => ky);
export const DEFAULT_HEADERS = {
Accept: 'application/ld+json, application/json'
};
// methods to proxy from ky
const PROXY_METHODS = new Set([
'get', 'post', 'put', 'push', 'patch', 'head', 'delete'
]);
/**
* Returns a custom httpClient instance. Used to specify default headers and
* other default overrides.
*
* @param {object} [options={}] - Options hashmap.
* @param {object} [options.parent] - The ky promise to inherit from.
* @param {object} [options.headers={}] - Default header overrides.
* @param {object} [options.params] - Other default overrides.
*
* @returns {Function} Custom httpClient instance.
*/
export function createInstance({
parent = kyOriginalPromise, headers = {}, ...params
} = {}) {
// convert legacy agent options
params = convertAgent(params);
// create new ky instance that will asynchronously resolve
const kyPromise = parent.then(kyBase => {
let ky;
if(parent === kyOriginalPromise) {
// ensure default headers, allow overrides
ky = kyBase.create({
headers: {...DEFAULT_HEADERS, ...headers},
...params
});
} else {
// extend parent
ky = kyBase.extend({headers, ...params});
}
return ky;
});
return _createHttpClient(kyPromise);
}
function _createHttpClient(kyPromise) {
async function httpClient(...args) {
const ky = await kyPromise;
const method = ((args[1] && args[1].method) || 'get').toLowerCase();
if(PROXY_METHODS.has(method)) {
return httpClient[method].apply(ky[method], args);
}
// convert legacy agent options
args[1] = convertAgent(args[1]);
return ky.apply(ky, args);
}
for(const method of PROXY_METHODS) {
httpClient[method] = async function(...args) {
const ky = await kyPromise;
return _handleResponse(ky[method], ky, args);
};
}
httpClient.create = function({headers = {}, ...params}) {
return createInstance({headers, ...params});
};
httpClient.extend = function({headers = {}, ...params}) {
return createInstance({parent: kyPromise, headers, ...params});
};
// default async `stop` signal getter
Object.defineProperty(httpClient, 'stop', {
async get() {
const ky = await kyPromise;
return ky.stop;
}
});
return httpClient;
}
async function _handleResponse(target, thisArg, args) {
// convert legacy agent options
args[1] = convertAgent(args[1]);
let response;
const [url] = args;
try {
response = await target.apply(thisArg, args);
} catch(error) {
return _handleError({error, url});
}
const {parseBody = true} = args[1] || {};
// always set 'data', default to undefined
let data;
if(parseBody) {
// a 204 will not include a content-type header
const contentType = response.headers.get('content-type');
if(contentType && contentType.includes('json')) {
data = await response.json();
}
}
Object.defineProperty(response, 'data', {value: data});
return response;
}
/**
* @param {object} options - Options hashmap.
* @param {Error} options.error - Error thrown during http operation.
* @param {string} options.url - Target URL of the request.
*
* @returns {Promise} Rejects with a thrown error.
*/
async function _handleError({error, url}) {
error.requestUrl = url;
// handle network errors and system errors that do not have a response
if(!error.response) {
if(error.message === 'Failed to fetch') {
error.message = `Failed to fetch "${url}". Possible CORS error.`;
}
// ky's TimeoutError class
if(error.name === 'TimeoutError') {
error.message = `Request to "${url}" timed out.`;
}
throw error;
}
// always move status up to the root of error
error.status = error.response.status;
const contentType = error.response.headers.get('content-type');
if(contentType && contentType.includes('json')) {
const errorBody = await error.response.json();
// the HTTPError received from ky has a generic message based on status
// use that if the JSON body does not include a message
error.message = errorBody.message || error.message;
error.data = errorBody;
}
throw error;
}