398 lines
13 KiB
JavaScript
398 lines
13 KiB
JavaScript
/* 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 "<scheme>://" 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;
|
||
}
|