import type { AxiosRequestConfig, Method, RawAxiosRequestHeaders } from 'axios' import axios from 'axios' import { IRestApiContext } from './types' export const NO_NETWORK_ERROR_CODE = 999 export const STREAM_SEPARATOR = '⧉⇋⇋➽⌑⧉§§\n' type GenericValue = string | number | boolean | object | null | undefined export interface IDataObject { [key: string]: GenericValue | IDataObject | GenericValue[] | IDataObject[] } const BROWSER_ID_STORAGE_KEY = 'n8n-browserId' const baseURL = 'dev-n8n.shalu.com' const getBrowserId = () => { let browserId = localStorage.getItem(BROWSER_ID_STORAGE_KEY) if (!browserId) { browserId = crypto.randomUUID() localStorage.setItem(BROWSER_ID_STORAGE_KEY, browserId) } return browserId } export class ResponseError { name?: string // The HTTP status code of response httpStatusCode?: number // The error code in the response errorCode?: number // The stack trace of the server serverStackTrace?: string // Additional metadata from the server (e.g., EULA URL) meta?: Record /** * Creates an instance of ResponseError. * @param {string} message The error message * @param {number} [errorCode] The error code which can be used by frontend to identify the actual error * @param {number} [httpStatusCode] The HTTP status code the response should have * @param {string} [stack] The stack trace * @param {Record} [meta] Additional metadata from the server */ constructor( message: string, options: { errorCode?: number httpStatusCode?: number stack?: string meta?: Record } = {} ) { // super(message); this.name = 'ResponseError' const { errorCode, httpStatusCode, stack, meta } = options if (errorCode) { this.errorCode = errorCode } if (httpStatusCode) { this.httpStatusCode = httpStatusCode } if (stack) { this.serverStackTrace = stack } if (meta) { this.meta = meta } } } // eslint-disable-next-line @typescript-eslint/no-explicit-any const legacyParamSerializer = (params: Record) => Object.keys(params) .filter((key) => params[key] !== undefined) .map((key) => { if (Array.isArray(params[key])) { return params[key].map((v: string) => `${key}[]=${encodeURIComponent(v)}`).join('&') } if (typeof params[key] === 'object') { params[key] = JSON.stringify(params[key]) } return `${key}=${encodeURIComponent(params[key])}` }) .join('&') export async function request( endpoint: string, config: { method: Method // baseURL: string // endpoint: string headers?: RawAxiosRequestHeaders data?: GenericValue | GenericValue[] withCredentials?: boolean } ) { const { method, headers, data } = config const options: AxiosRequestConfig = { method, url: endpoint, baseURL, headers: headers ?? {} } if (baseURL.startsWith('/')) { options.headers!['browser-id'] = getBrowserId() } if ( import.meta.env.NODE_ENV !== 'production' && !baseURL.includes('api.n8n.io') && !baseURL.includes('n8n.cloud') ) { options.withCredentials = options.withCredentials ?? true } if (['POST', 'PATCH', 'PUT'].includes(method)) { options.data = data } else if (data) { options.params = data options.paramsSerializer = legacyParamSerializer } try { const response = await axios.request(options) return response.data } catch (error) { if (error.message === 'Network Error') { throw new ResponseError("Can't connect to n8n.", { errorCode: NO_NETWORK_ERROR_CODE }) } const errorResponseData = error.response?.data if (errorResponseData?.mfaRequired === true) { // throw new MfaRequiredError(); throw errorResponseData } if (errorResponseData?.message !== undefined) { if (errorResponseData.name === 'NodeApiError') { errorResponseData.httpStatusCode = error.response.status throw errorResponseData } throw new ResponseError(errorResponseData.message, { errorCode: errorResponseData.code, httpStatusCode: error.response.status, stack: errorResponseData.stack, meta: errorResponseData.meta }) } throw error } } /** * Sends a request to the API and returns the response without extracting the data key. * @param context Rest API context * @param method HTTP method * @param endpoint relative path to the API endpoint * @param data request data * @returns data and total count */ export async function getFullApiResponse( context: IRestApiContext, method: Method, endpoint: string, data?: GenericValue | GenericValue[] ) { const response = await request({ method, baseURL: context.baseUrl, endpoint, headers: { 'push-ref': context.pushRef }, data }) return response as { count: number; data: T } } export async function makeRestApiRequest( context: IRestApiContext, method: Method, endpoint: string, data?: GenericValue | GenericValue[] ) { const response = await request({ method, baseURL: context.baseUrl, endpoint, headers: { 'push-ref': context.pushRef }, data }) // All cli rest api endpoints return data wrapped in `data` key return response.data as T } export async function get( baseURL: string, endpoint: string, params?: IDataObject, headers?: RawAxiosRequestHeaders ) { return await request({ method: 'GET', baseURL, endpoint, headers, data: params }) } export async function post( baseURL: string, endpoint: string, params?: IDataObject, headers?: RawAxiosRequestHeaders ) { return await request({ method: 'POST', baseURL, endpoint, headers, data: params }) } export async function patch( baseURL: string, endpoint: string, params?: IDataObject, headers?: RawAxiosRequestHeaders ) { return await request({ method: 'PATCH', baseURL, endpoint, headers, data: params }) } export async function streamRequest( context: IRestApiContext, apiEndpoint: string, payload: object, onChunk?: (chunk: T) => void, onDone?: () => void, onError?: (e: Error) => void, separator = STREAM_SEPARATOR, abortSignal?: AbortSignal ): Promise { let onErrorOnce: ((e: Error) => void) | undefined = (e: Error) => { onErrorOnce = undefined onError?.(e) } const headers: Record = { 'browser-id': getBrowserId(), 'Content-Type': 'application/json' } const assistantRequest: RequestInit = { headers, method: 'POST', credentials: 'include', body: JSON.stringify(payload), signal: abortSignal } try { const response = await fetch(`${context.baseUrl}${apiEndpoint}`, assistantRequest) if (response.body) { // Handle the streaming response const reader = response.body.getReader() const decoder = new TextDecoder('utf-8') let buffer = '' async function readStream() { const { done, value } = await reader.read() if (done) { if (response.ok) { onDone?.() } else { onErrorOnce?.( new ResponseError(response.statusText, { httpStatusCode: response.status }) ) } return } const chunk = decoder.decode(value) buffer += chunk const splitChunks = buffer.split(separator) buffer = '' for (const splitChunk of splitChunks) { if (splitChunk) { let data: T try { // data = jsonParse(splitChunk, { errorMessage: 'Invalid json' }); data = JSON.parse(splitChunk) as T } catch (e) { // incomplete json. append to buffer to complete buffer += splitChunk continue } try { if (response.ok) { // Call chunk callback if request was successful onChunk?.(data) } else { // Otherwise, call error callback const message = 'message' in data ? data.message : response.statusText onErrorOnce?.( new ResponseError(String(message), { httpStatusCode: response.status }) ) } } catch (e: unknown) { if (e instanceof Error) { onErrorOnce?.(e) } } } } await readStream() } // Start reading the stream await readStream() } else if (onErrorOnce) { onErrorOnce(new Error(response.statusText)) } } catch (e: unknown) { const condition = e instanceof Error if (!condition) { // eslint-disable-next-line n8n-local-rules/no-plain-errors throw new Error('Assertion failed') } onErrorOnce?.(e) } }