/* eslint-disable no-plusplus */ /* eslint-disable prefer-template */ /* eslint-disable prefer-arrow-callback */ /* eslint-disable no-var */ /* eslint-disable vars-on-top */ /* eslint-disable no-param-reassign */ /* eslint-disable import/no-extraneous-dependencies */ /** * This is copied from @vespaiach/axios-fetch-adapter, which exposes an ESM * module without setting the "type" field in package.json. */ import axios from "axios"; import { EventStreamContentType, getLines, getBytes, getMessages, } from "./event-source-parse.js"; function tryJsonStringify(data) { try { return JSON.stringify(data); } catch (e) { return data; } } /** * In order to avoid import issues with axios 1.x, copying here the internal * utility functions that we used to import directly from axios. */ // Copied from axios/lib/core/settle.js function settle(resolve, reject, response) { const { validateStatus } = response.config; if (!response.status || !validateStatus || validateStatus(response.status)) { resolve(response); } else { reject(createError(`Request failed with status code ${response.status} and body ${typeof response.data === "string" ? response.data : tryJsonStringify(response.data)}`, response.config, null, response.request, response)); } } // Copied from axios/lib/helpers/isAbsoluteURL.js function isAbsoluteURL(url) { // A URL is considered absolute if it begins with "://" or "//" (protocol-relative URL). // RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed // by any combination of letters, digits, plus, period, or hyphen. return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url); } // Copied from axios/lib/helpers/combineURLs.js function combineURLs(baseURL, relativeURL) { return relativeURL ? baseURL.replace(/\/+$/, "") + "/" + relativeURL.replace(/^\/+/, "") : baseURL; } // Copied from axios/lib/helpers/buildURL.js function encode(val) { return encodeURIComponent(val) .replace(/%3A/gi, ":") .replace(/%24/g, "$") .replace(/%2C/gi, ",") .replace(/%20/g, "+") .replace(/%5B/gi, "[") .replace(/%5D/gi, "]"); } function buildURL(url, params, paramsSerializer) { if (!params) { return url; } var serializedParams; if (paramsSerializer) { serializedParams = paramsSerializer(params); } else if (isURLSearchParams(params)) { serializedParams = params.toString(); } else { var parts = []; forEach(params, function serialize(val, key) { if (val === null || typeof val === "undefined") { return; } if (isArray(val)) { key = `${key}[]`; } else { val = [val]; } forEach(val, function parseValue(v) { if (isDate(v)) { v = v.toISOString(); } else if (isObject(v)) { v = JSON.stringify(v); } parts.push(`${encode(key)}=${encode(v)}`); }); }); serializedParams = parts.join("&"); } if (serializedParams) { var hashmarkIndex = url.indexOf("#"); if (hashmarkIndex !== -1) { url = url.slice(0, hashmarkIndex); } url += (url.indexOf("?") === -1 ? "?" : "&") + serializedParams; } return url; } // Copied from axios/lib/core/buildFullPath.js function buildFullPath(baseURL, requestedURL) { if (baseURL && !isAbsoluteURL(requestedURL)) { return combineURLs(baseURL, requestedURL); } return requestedURL; } // Copied from axios/lib/utils.js function isUndefined(val) { return typeof val === "undefined"; } function isObject(val) { return val !== null && typeof val === "object"; } function isDate(val) { return toString.call(val) === "[object Date]"; } function isURLSearchParams(val) { return toString.call(val) === "[object URLSearchParams]"; } function isArray(val) { return Array.isArray(val); } function forEach(obj, fn) { // Don't bother if no value provided if (obj === null || typeof obj === "undefined") { return; } // Force an array if not already something iterable if (typeof obj !== "object") { obj = [obj]; } if (isArray(obj)) { // Iterate over array values for (var i = 0, l = obj.length; i < l; i++) { fn.call(null, obj[i], i, obj); } } else { // Iterate over object keys for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { fn.call(null, obj[key], key, obj); } } } } function isFormData(val) { return toString.call(val) === "[object FormData]"; } // TODO this needs to be fixed to run in newer browser-like environments // https://github.com/vespaiach/axios-fetch-adapter/issues/20#issue-1396365322 function isStandardBrowserEnv() { if (typeof navigator !== "undefined" && // eslint-disable-next-line no-undef (navigator.product === "ReactNative" || // eslint-disable-next-line no-undef navigator.product === "NativeScript" || // eslint-disable-next-line no-undef navigator.product === "NS")) { return false; } return typeof window !== "undefined" && typeof document !== "undefined"; } /** * - Create a request object * - Get response body * - Check if timeout */ export default async function fetchAdapter(config) { const request = createRequest(config); const data = await getResponse(request, config); return new Promise((resolve, reject) => { if (data instanceof Error) { reject(data); } else { // eslint-disable-next-line no-unused-expressions Object.prototype.toString.call(config.settle) === "[object Function]" ? config.settle(resolve, reject, data) : settle(resolve, reject, data); } }); } /** * Fetch API stage two is to get response body. This funtion tries to retrieve * response body based on response's type */ async function getResponse(request, config) { let stageOne; try { stageOne = await fetch(request); } catch (e) { if (e && e.name === "AbortError") { return createError("Request aborted", config, "ECONNABORTED", request); } if (e && e.name === "TimeoutError") { return createError("Request timeout", config, "ECONNABORTED", request); } return createError("Network Error", config, "ERR_NETWORK", request); } const headers = {}; stageOne.headers.forEach((value, key) => { headers[key] = value; }); const response = { ok: stageOne.ok, status: stageOne.status, statusText: stageOne.statusText, headers, config, request, }; if (stageOne.status >= 200 && stageOne.status !== 204) { if (config.responseType === "stream") { const contentType = stageOne.headers.get("content-type"); if (!contentType?.startsWith(EventStreamContentType)) { // If the content-type is not stream, response is most likely an error if (stageOne.status >= 400) { // If the error is a JSON, parse it. Otherwise, return as text if (contentType?.startsWith("application/json")) { response.data = await stageOne.json(); return response; } else { response.data = await stageOne.text(); return response; } } // If the non-stream response is also not an error, throw throw new Error(`Expected content-type to be ${EventStreamContentType}, Actual: ${contentType}`); } await getBytes(stageOne.body, getLines(getMessages(config.onmessage))); } else { switch (config.responseType) { case "arraybuffer": response.data = await stageOne.arrayBuffer(); break; case "blob": response.data = await stageOne.blob(); break; case "json": response.data = await stageOne.json(); break; case "formData": response.data = await stageOne.formData(); break; default: response.data = await stageOne.text(); break; } } } return response; } /** * This function will create a Request object based on configuration's axios */ function createRequest(config) { const headers = new Headers(config.headers); // HTTP basic authentication if (config.auth) { const username = config.auth.username || ""; const password = config.auth.password ? decodeURI(encodeURIComponent(config.auth.password)) : ""; headers.set("Authorization", `Basic ${btoa(`${username}:${password}`)}`); } const method = config.method.toUpperCase(); const options = { headers, method, }; if (method !== "GET" && method !== "HEAD") { options.body = config.data; // In these cases the browser will automatically set the correct Content-Type, // but only if that header hasn't been set yet. So that's why we're deleting it. if (isFormData(options.body) && isStandardBrowserEnv()) { headers.delete("Content-Type"); } } // Some `fetch` implementations will override the Content-Type to text/plain // when body is a string. // See https://github.com/langchain-ai/langchainjs/issues/1010 if (typeof options.body === "string") { options.body = new TextEncoder().encode(options.body); } if (config.mode) { options.mode = config.mode; } if (config.cache) { options.cache = config.cache; } if (config.integrity) { options.integrity = config.integrity; } if (config.redirect) { options.redirect = config.redirect; } if (config.referrer) { options.referrer = config.referrer; } if (config.timeout && config.timeout > 0) { options.signal = AbortSignal.timeout(config.timeout); } if (config.signal) { // this overrides the timeout signal if both are set options.signal = config.signal; } // This config is similar to XHR’s withCredentials flag, but with three available values instead of two. // So if withCredentials is not set, default value 'same-origin' will be used if (!isUndefined(config.withCredentials)) { options.credentials = config.withCredentials ? "include" : "omit"; } // for streaming if (config.responseType === "stream") { options.headers.set("Accept", EventStreamContentType); } const fullPath = buildFullPath(config.baseURL, config.url); const url = buildURL(fullPath, config.params, config.paramsSerializer); // Expected browser to throw error if there is any wrong configuration value return new Request(url, options); } /** * Note: * * From version >= 0.27.0, createError function is replaced by AxiosError class. * So I copy the old createError function here for backward compatible. * * * * Create an Error with the specified message, config, error code, request and response. * * @param {string} message The error message. * @param {Object} config The config. * @param {string} [code] The error code (for example, 'ECONNABORTED'). * @param {Object} [request] The request. * @param {Object} [response] The response. * @returns {Error} The created error. */ function createError(message, config, code, request, response) { if (axios.AxiosError && typeof axios.AxiosError === "function") { return new axios.AxiosError(message, axios.AxiosError[code], config, request, response); } const error = new Error(message); return enhanceError(error, config, code, request, response); } /** * * Note: * * This function is for backward compatible. * * * Update an Error with the specified config, error code, and response. * * @param {Error} error The error to update. * @param {Object} config The config. * @param {string} [code] The error code (for example, 'ECONNABORTED'). * @param {Object} [request] The request. * @param {Object} [response] The response. * @returns {Error} The error. */ function enhanceError(error, config, code, request, response) { error.config = config; if (code) { error.code = code; } error.request = request; error.response = response; error.isAxiosError = true; error.toJSON = function toJSON() { return { // Standard message: this.message, name: this.name, // Microsoft description: this.description, number: this.number, // Mozilla fileName: this.fileName, lineNumber: this.lineNumber, columnNumber: this.columnNumber, stack: this.stack, // Axios config: this.config, code: this.code, status: this.response && this.response.status ? this.response.status : null, }; }; return error; }