| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333 |
- 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<string, unknown>
- /**
- * 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<string, unknown>} [meta] Additional metadata from the server
- */
- constructor(
- message: string,
- options: {
- errorCode?: number
- httpStatusCode?: number
- stack?: string
- meta?: Record<string, unknown>
- } = {}
- ) {
- // 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<string, any>) =>
- 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<T>(
- 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<T>(
- 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<T extends object>(
- context: IRestApiContext,
- apiEndpoint: string,
- payload: object,
- onChunk?: (chunk: T) => void,
- onDone?: () => void,
- onError?: (e: Error) => void,
- separator = STREAM_SEPARATOR,
- abortSignal?: AbortSignal
- ): Promise<void> {
- let onErrorOnce: ((e: Error) => void) | undefined = (e: Error) => {
- onErrorOnce = undefined
- onError?.(e)
- }
- const headers: Record<string, string> = {
- '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<T>(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)
- }
- }
|