156 lines
6.4 KiB
JavaScript
156 lines
6.4 KiB
JavaScript
// Licensed to the .NET Foundation under one or more agreements.
|
|
// The .NET Foundation licenses this file to you under the MIT license.
|
|
import { AbortError, HttpError, TimeoutError } from "./Errors";
|
|
import { HttpClient, HttpResponse } from "./HttpClient";
|
|
import { LogLevel } from "./ILogger";
|
|
import { Platform, getGlobalThis, isArrayBuffer } from "./Utils";
|
|
export class FetchHttpClient extends HttpClient {
|
|
constructor(logger) {
|
|
super();
|
|
this._logger = logger;
|
|
// Node added a fetch implementation to the global scope starting in v18.
|
|
// We need to add a cookie jar in node to be able to share cookies with WebSocket
|
|
if (typeof fetch === "undefined" || Platform.isNode) {
|
|
// In order to ignore the dynamic require in webpack builds we need to do this magic
|
|
// @ts-ignore: TS doesn't know about these names
|
|
const requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require;
|
|
// Cookies aren't automatically handled in Node so we need to add a CookieJar to preserve cookies across requests
|
|
this._jar = new (requireFunc("tough-cookie")).CookieJar();
|
|
if (typeof fetch === "undefined") {
|
|
this._fetchType = requireFunc("node-fetch");
|
|
}
|
|
else {
|
|
// Use fetch from Node if available
|
|
this._fetchType = fetch;
|
|
}
|
|
// node-fetch doesn't have a nice API for getting and setting cookies
|
|
// fetch-cookie will wrap a fetch implementation with a default CookieJar or a provided one
|
|
this._fetchType = requireFunc("fetch-cookie")(this._fetchType, this._jar);
|
|
}
|
|
else {
|
|
this._fetchType = fetch.bind(getGlobalThis());
|
|
}
|
|
if (typeof AbortController === "undefined") {
|
|
// In order to ignore the dynamic require in webpack builds we need to do this magic
|
|
// @ts-ignore: TS doesn't know about these names
|
|
const requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require;
|
|
// Node needs EventListener methods on AbortController which our custom polyfill doesn't provide
|
|
this._abortControllerType = requireFunc("abort-controller");
|
|
}
|
|
else {
|
|
this._abortControllerType = AbortController;
|
|
}
|
|
}
|
|
/** @inheritDoc */
|
|
async send(request) {
|
|
// Check that abort was not signaled before calling send
|
|
if (request.abortSignal && request.abortSignal.aborted) {
|
|
throw new AbortError();
|
|
}
|
|
if (!request.method) {
|
|
throw new Error("No method defined.");
|
|
}
|
|
if (!request.url) {
|
|
throw new Error("No url defined.");
|
|
}
|
|
const abortController = new this._abortControllerType();
|
|
let error;
|
|
// Hook our abortSignal into the abort controller
|
|
if (request.abortSignal) {
|
|
request.abortSignal.onabort = () => {
|
|
abortController.abort();
|
|
error = new AbortError();
|
|
};
|
|
}
|
|
// If a timeout has been passed in, setup a timeout to call abort
|
|
// Type needs to be any to fit window.setTimeout and NodeJS.setTimeout
|
|
let timeoutId = null;
|
|
if (request.timeout) {
|
|
const msTimeout = request.timeout;
|
|
timeoutId = setTimeout(() => {
|
|
abortController.abort();
|
|
this._logger.log(LogLevel.Warning, `Timeout from HTTP request.`);
|
|
error = new TimeoutError();
|
|
}, msTimeout);
|
|
}
|
|
if (request.content === "") {
|
|
request.content = undefined;
|
|
}
|
|
if (request.content) {
|
|
// Explicitly setting the Content-Type header for React Native on Android platform.
|
|
request.headers = request.headers || {};
|
|
if (isArrayBuffer(request.content)) {
|
|
request.headers["Content-Type"] = "application/octet-stream";
|
|
}
|
|
else {
|
|
request.headers["Content-Type"] = "text/plain;charset=UTF-8";
|
|
}
|
|
}
|
|
let response;
|
|
try {
|
|
response = await this._fetchType(request.url, {
|
|
body: request.content,
|
|
cache: "no-cache",
|
|
credentials: request.withCredentials === true ? "include" : "same-origin",
|
|
headers: {
|
|
"X-Requested-With": "XMLHttpRequest",
|
|
...request.headers,
|
|
},
|
|
method: request.method,
|
|
mode: "cors",
|
|
redirect: "follow",
|
|
signal: abortController.signal,
|
|
});
|
|
}
|
|
catch (e) {
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
this._logger.log(LogLevel.Warning, `Error from HTTP request. ${e}.`);
|
|
throw e;
|
|
}
|
|
finally {
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
if (request.abortSignal) {
|
|
request.abortSignal.onabort = null;
|
|
}
|
|
}
|
|
if (!response.ok) {
|
|
const errorMessage = await deserializeContent(response, "text");
|
|
throw new HttpError(errorMessage || response.statusText, response.status);
|
|
}
|
|
const content = deserializeContent(response, request.responseType);
|
|
const payload = await content;
|
|
return new HttpResponse(response.status, response.statusText, payload);
|
|
}
|
|
getCookieString(url) {
|
|
let cookies = "";
|
|
if (Platform.isNode && this._jar) {
|
|
// @ts-ignore: unused variable
|
|
this._jar.getCookies(url, (e, c) => cookies = c.join("; "));
|
|
}
|
|
return cookies;
|
|
}
|
|
}
|
|
function deserializeContent(response, responseType) {
|
|
let content;
|
|
switch (responseType) {
|
|
case "arraybuffer":
|
|
content = response.arrayBuffer();
|
|
break;
|
|
case "text":
|
|
content = response.text();
|
|
break;
|
|
case "blob":
|
|
case "document":
|
|
case "json":
|
|
throw new Error(`${responseType} is not supported.`);
|
|
default:
|
|
content = response.text();
|
|
break;
|
|
}
|
|
return content;
|
|
}
|
|
//# sourceMappingURL=FetchHttpClient.js.map
|