119 lines
4.4 KiB
JavaScript
119 lines
4.4 KiB
JavaScript
|
import { HttpResponse } from "@smithy/protocol-http";
|
||
|
import { buildQueryString } from "@smithy/querystring-builder";
|
||
|
import { requestTimeout } from "./request-timeout";
|
||
|
export const keepAliveSupport = {
|
||
|
supported: Boolean(typeof Request !== "undefined" && "keepalive" in new Request("https://[::1]")),
|
||
|
};
|
||
|
export class FetchHttpHandler {
|
||
|
static create(instanceOrOptions) {
|
||
|
if (typeof instanceOrOptions?.handle === "function") {
|
||
|
return instanceOrOptions;
|
||
|
}
|
||
|
return new FetchHttpHandler(instanceOrOptions);
|
||
|
}
|
||
|
constructor(options) {
|
||
|
if (typeof options === "function") {
|
||
|
this.configProvider = options().then((opts) => opts || {});
|
||
|
}
|
||
|
else {
|
||
|
this.config = options ?? {};
|
||
|
this.configProvider = Promise.resolve(this.config);
|
||
|
}
|
||
|
}
|
||
|
destroy() {
|
||
|
}
|
||
|
async handle(request, { abortSignal } = {}) {
|
||
|
if (!this.config) {
|
||
|
this.config = await this.configProvider;
|
||
|
}
|
||
|
const requestTimeoutInMs = this.config.requestTimeout;
|
||
|
const keepAlive = this.config.keepAlive === true;
|
||
|
if (abortSignal?.aborted) {
|
||
|
const abortError = new Error("Request aborted");
|
||
|
abortError.name = "AbortError";
|
||
|
return Promise.reject(abortError);
|
||
|
}
|
||
|
let path = request.path;
|
||
|
const queryString = buildQueryString(request.query || {});
|
||
|
if (queryString) {
|
||
|
path += `?${queryString}`;
|
||
|
}
|
||
|
if (request.fragment) {
|
||
|
path += `#${request.fragment}`;
|
||
|
}
|
||
|
let auth = "";
|
||
|
if (request.username != null || request.password != null) {
|
||
|
const username = request.username ?? "";
|
||
|
const password = request.password ?? "";
|
||
|
auth = `${username}:${password}@`;
|
||
|
}
|
||
|
const { port, method } = request;
|
||
|
const url = `${request.protocol}//${auth}${request.hostname}${port ? `:${port}` : ""}${path}`;
|
||
|
const body = method === "GET" || method === "HEAD" ? undefined : request.body;
|
||
|
const requestOptions = {
|
||
|
body,
|
||
|
headers: new Headers(request.headers),
|
||
|
method: method,
|
||
|
};
|
||
|
if (body) {
|
||
|
requestOptions.duplex = "half";
|
||
|
}
|
||
|
if (typeof AbortController !== "undefined") {
|
||
|
requestOptions.signal = abortSignal;
|
||
|
}
|
||
|
if (keepAliveSupport.supported) {
|
||
|
requestOptions.keepalive = keepAlive;
|
||
|
}
|
||
|
const fetchRequest = new Request(url, requestOptions);
|
||
|
const raceOfPromises = [
|
||
|
fetch(fetchRequest).then((response) => {
|
||
|
const fetchHeaders = response.headers;
|
||
|
const transformedHeaders = {};
|
||
|
for (const pair of fetchHeaders.entries()) {
|
||
|
transformedHeaders[pair[0]] = pair[1];
|
||
|
}
|
||
|
const hasReadableStream = response.body != undefined;
|
||
|
if (!hasReadableStream) {
|
||
|
return response.blob().then((body) => ({
|
||
|
response: new HttpResponse({
|
||
|
headers: transformedHeaders,
|
||
|
reason: response.statusText,
|
||
|
statusCode: response.status,
|
||
|
body,
|
||
|
}),
|
||
|
}));
|
||
|
}
|
||
|
return {
|
||
|
response: new HttpResponse({
|
||
|
headers: transformedHeaders,
|
||
|
reason: response.statusText,
|
||
|
statusCode: response.status,
|
||
|
body: response.body,
|
||
|
}),
|
||
|
};
|
||
|
}),
|
||
|
requestTimeout(requestTimeoutInMs),
|
||
|
];
|
||
|
if (abortSignal) {
|
||
|
raceOfPromises.push(new Promise((resolve, reject) => {
|
||
|
abortSignal.onabort = () => {
|
||
|
const abortError = new Error("Request aborted");
|
||
|
abortError.name = "AbortError";
|
||
|
reject(abortError);
|
||
|
};
|
||
|
}));
|
||
|
}
|
||
|
return Promise.race(raceOfPromises);
|
||
|
}
|
||
|
updateHttpClientConfig(key, value) {
|
||
|
this.config = undefined;
|
||
|
this.configProvider = this.configProvider.then((config) => {
|
||
|
config[key] = value;
|
||
|
return config;
|
||
|
});
|
||
|
}
|
||
|
httpHandlerConfigs() {
|
||
|
return this.config ?? {};
|
||
|
}
|
||
|
}
|