131 lines
3.7 KiB
JavaScript
131 lines
3.7 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
const {HttpClient, HttpClientResponse} = require('./HttpClient');
|
||
|
|
||
|
/**
|
||
|
* HTTP client which uses a `fetch` function to issue requests.
|
||
|
*
|
||
|
* By default relies on the global `fetch` function, but an optional function
|
||
|
* can be passed in. If passing in a function, it is expected to match the Web
|
||
|
* Fetch API. As an example, this could be the function provided by the
|
||
|
* node-fetch package (https://github.com/node-fetch/node-fetch).
|
||
|
*/
|
||
|
class FetchHttpClient extends HttpClient {
|
||
|
constructor(fetchFn) {
|
||
|
super();
|
||
|
this._fetchFn = fetchFn;
|
||
|
}
|
||
|
|
||
|
/** @override. */
|
||
|
getClientName() {
|
||
|
return 'fetch';
|
||
|
}
|
||
|
|
||
|
makeRequest(
|
||
|
host,
|
||
|
port,
|
||
|
path,
|
||
|
method,
|
||
|
headers,
|
||
|
requestData,
|
||
|
protocol,
|
||
|
timeout
|
||
|
) {
|
||
|
const isInsecureConnection = protocol === 'http';
|
||
|
|
||
|
const url = new URL(
|
||
|
path,
|
||
|
`${isInsecureConnection ? 'http' : 'https'}://${host}`
|
||
|
);
|
||
|
url.port = port;
|
||
|
|
||
|
const fetchFn = this._fetchFn || fetch;
|
||
|
const fetchPromise = fetchFn(url.toString(), {
|
||
|
method,
|
||
|
headers,
|
||
|
body: requestData || undefined,
|
||
|
});
|
||
|
|
||
|
// The Fetch API does not support passing in a timeout natively, so a
|
||
|
// timeout promise is constructed to race against the fetch and preempt the
|
||
|
// request, simulating a timeout.
|
||
|
//
|
||
|
// This timeout behavior differs from Node:
|
||
|
// - Fetch uses a single timeout for the entire length of the request.
|
||
|
// - Node is more fine-grained and resets the timeout after each stage of
|
||
|
// the request.
|
||
|
//
|
||
|
// As an example, if the timeout is set to 30s and the connection takes 20s
|
||
|
// to be established followed by 20s for the body, Fetch would timeout but
|
||
|
// Node would not. The more fine-grained timeout cannot be implemented with
|
||
|
// fetch.
|
||
|
let pendingTimeoutId;
|
||
|
const timeoutPromise = new Promise((_, reject) => {
|
||
|
pendingTimeoutId = setTimeout(() => {
|
||
|
pendingTimeoutId = null;
|
||
|
reject(HttpClient.makeTimeoutError());
|
||
|
}, timeout);
|
||
|
});
|
||
|
|
||
|
return Promise.race([fetchPromise, timeoutPromise])
|
||
|
.then((res) => {
|
||
|
return new FetchHttpClientResponse(res);
|
||
|
})
|
||
|
.finally(() => {
|
||
|
if (pendingTimeoutId) {
|
||
|
clearTimeout(pendingTimeoutId);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class FetchHttpClientResponse extends HttpClientResponse {
|
||
|
constructor(res) {
|
||
|
super(
|
||
|
res.status,
|
||
|
FetchHttpClientResponse._transformHeadersToObject(res.headers)
|
||
|
);
|
||
|
this._res = res;
|
||
|
}
|
||
|
|
||
|
getRawResponse() {
|
||
|
return this._res;
|
||
|
}
|
||
|
|
||
|
toStream(streamCompleteCallback) {
|
||
|
// Unfortunately `fetch` does not have event handlers for when the stream is
|
||
|
// completely read. We therefore invoke the streamCompleteCallback right
|
||
|
// away. This callback emits a response event with metadata and completes
|
||
|
// metrics, so it's ok to do this without waiting for the stream to be
|
||
|
// completely read.
|
||
|
streamCompleteCallback();
|
||
|
|
||
|
// Fetch's `body` property is expected to be a readable stream of the body.
|
||
|
return this._res.body;
|
||
|
}
|
||
|
|
||
|
toJSON() {
|
||
|
return this._res.json();
|
||
|
}
|
||
|
|
||
|
static _transformHeadersToObject(headers) {
|
||
|
// Fetch uses a Headers instance so this must be converted to a barebones
|
||
|
// JS object to meet the HttpClient interface.
|
||
|
const headersObj = {};
|
||
|
|
||
|
for (const entry of headers) {
|
||
|
if (!Array.isArray(entry) || entry.length != 2) {
|
||
|
throw new Error(
|
||
|
'Response objects produced by the fetch function given to FetchHttpClient do not have an iterable headers map. Response#headers should be an iterable object.'
|
||
|
);
|
||
|
}
|
||
|
|
||
|
headersObj[entry[0]] = entry[1];
|
||
|
}
|
||
|
|
||
|
return headersObj;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = {FetchHttpClient, FetchHttpClientResponse};
|