request.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. import type { AxiosRequestConfig, Method, RawAxiosRequestHeaders } from 'axios'
  2. import axios from 'axios'
  3. import { IRestApiContext } from './types'
  4. export const NO_NETWORK_ERROR_CODE = 999
  5. export const STREAM_SEPARATOR = '⧉⇋⇋➽⌑⧉§§\n'
  6. type GenericValue = string | number | boolean | object | null | undefined
  7. export interface IDataObject {
  8. [key: string]: GenericValue | IDataObject | GenericValue[] | IDataObject[]
  9. }
  10. const BROWSER_ID_STORAGE_KEY = 'n8n-browserId'
  11. const baseURL = 'dev-n8n.shalu.com'
  12. const getBrowserId = () => {
  13. let browserId = localStorage.getItem(BROWSER_ID_STORAGE_KEY)
  14. if (!browserId) {
  15. browserId = crypto.randomUUID()
  16. localStorage.setItem(BROWSER_ID_STORAGE_KEY, browserId)
  17. }
  18. return browserId
  19. }
  20. export class ResponseError {
  21. name?: string
  22. // The HTTP status code of response
  23. httpStatusCode?: number
  24. // The error code in the response
  25. errorCode?: number
  26. // The stack trace of the server
  27. serverStackTrace?: string
  28. // Additional metadata from the server (e.g., EULA URL)
  29. meta?: Record<string, unknown>
  30. /**
  31. * Creates an instance of ResponseError.
  32. * @param {string} message The error message
  33. * @param {number} [errorCode] The error code which can be used by frontend to identify the actual error
  34. * @param {number} [httpStatusCode] The HTTP status code the response should have
  35. * @param {string} [stack] The stack trace
  36. * @param {Record<string, unknown>} [meta] Additional metadata from the server
  37. */
  38. constructor(
  39. message: string,
  40. options: {
  41. errorCode?: number
  42. httpStatusCode?: number
  43. stack?: string
  44. meta?: Record<string, unknown>
  45. } = {}
  46. ) {
  47. // super(message);
  48. this.name = 'ResponseError'
  49. const { errorCode, httpStatusCode, stack, meta } = options
  50. if (errorCode) {
  51. this.errorCode = errorCode
  52. }
  53. if (httpStatusCode) {
  54. this.httpStatusCode = httpStatusCode
  55. }
  56. if (stack) {
  57. this.serverStackTrace = stack
  58. }
  59. if (meta) {
  60. this.meta = meta
  61. }
  62. }
  63. }
  64. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  65. const legacyParamSerializer = (params: Record<string, any>) =>
  66. Object.keys(params)
  67. .filter((key) => params[key] !== undefined)
  68. .map((key) => {
  69. if (Array.isArray(params[key])) {
  70. return params[key].map((v: string) => `${key}[]=${encodeURIComponent(v)}`).join('&')
  71. }
  72. if (typeof params[key] === 'object') {
  73. params[key] = JSON.stringify(params[key])
  74. }
  75. return `${key}=${encodeURIComponent(params[key])}`
  76. })
  77. .join('&')
  78. export async function request(
  79. endpoint: string,
  80. config: {
  81. method: Method
  82. // baseURL: string
  83. // endpoint: string
  84. headers?: RawAxiosRequestHeaders
  85. data?: GenericValue | GenericValue[]
  86. withCredentials?: boolean
  87. }
  88. ) {
  89. const { method, headers, data } = config
  90. const options: AxiosRequestConfig = {
  91. method,
  92. url: endpoint,
  93. baseURL,
  94. headers: headers ?? {}
  95. }
  96. if (baseURL.startsWith('/')) {
  97. options.headers!['browser-id'] = getBrowserId()
  98. }
  99. if (
  100. import.meta.env.NODE_ENV !== 'production' &&
  101. !baseURL.includes('api.n8n.io') &&
  102. !baseURL.includes('n8n.cloud')
  103. ) {
  104. options.withCredentials = options.withCredentials ?? true
  105. }
  106. if (['POST', 'PATCH', 'PUT'].includes(method)) {
  107. options.data = data
  108. } else if (data) {
  109. options.params = data
  110. options.paramsSerializer = legacyParamSerializer
  111. }
  112. try {
  113. const response = await axios.request(options)
  114. return response.data
  115. } catch (error) {
  116. if (error.message === 'Network Error') {
  117. throw new ResponseError("Can't connect to n8n.", {
  118. errorCode: NO_NETWORK_ERROR_CODE
  119. })
  120. }
  121. const errorResponseData = error.response?.data
  122. if (errorResponseData?.mfaRequired === true) {
  123. // throw new MfaRequiredError();
  124. throw errorResponseData
  125. }
  126. if (errorResponseData?.message !== undefined) {
  127. if (errorResponseData.name === 'NodeApiError') {
  128. errorResponseData.httpStatusCode = error.response.status
  129. throw errorResponseData
  130. }
  131. throw new ResponseError(errorResponseData.message, {
  132. errorCode: errorResponseData.code,
  133. httpStatusCode: error.response.status,
  134. stack: errorResponseData.stack,
  135. meta: errorResponseData.meta
  136. })
  137. }
  138. throw error
  139. }
  140. }
  141. /**
  142. * Sends a request to the API and returns the response without extracting the data key.
  143. * @param context Rest API context
  144. * @param method HTTP method
  145. * @param endpoint relative path to the API endpoint
  146. * @param data request data
  147. * @returns data and total count
  148. */
  149. export async function getFullApiResponse<T>(
  150. context: IRestApiContext,
  151. method: Method,
  152. endpoint: string,
  153. data?: GenericValue | GenericValue[]
  154. ) {
  155. const response = await request({
  156. method,
  157. baseURL: context.baseUrl,
  158. endpoint,
  159. headers: { 'push-ref': context.pushRef },
  160. data
  161. })
  162. return response as { count: number; data: T }
  163. }
  164. export async function makeRestApiRequest<T>(
  165. context: IRestApiContext,
  166. method: Method,
  167. endpoint: string,
  168. data?: GenericValue | GenericValue[]
  169. ) {
  170. const response = await request({
  171. method,
  172. baseURL: context.baseUrl,
  173. endpoint,
  174. headers: { 'push-ref': context.pushRef },
  175. data
  176. })
  177. // All cli rest api endpoints return data wrapped in `data` key
  178. return response.data as T
  179. }
  180. export async function get(
  181. baseURL: string,
  182. endpoint: string,
  183. params?: IDataObject,
  184. headers?: RawAxiosRequestHeaders
  185. ) {
  186. return await request({ method: 'GET', baseURL, endpoint, headers, data: params })
  187. }
  188. export async function post(
  189. baseURL: string,
  190. endpoint: string,
  191. params?: IDataObject,
  192. headers?: RawAxiosRequestHeaders
  193. ) {
  194. return await request({ method: 'POST', baseURL, endpoint, headers, data: params })
  195. }
  196. export async function patch(
  197. baseURL: string,
  198. endpoint: string,
  199. params?: IDataObject,
  200. headers?: RawAxiosRequestHeaders
  201. ) {
  202. return await request({ method: 'PATCH', baseURL, endpoint, headers, data: params })
  203. }
  204. export async function streamRequest<T extends object>(
  205. context: IRestApiContext,
  206. apiEndpoint: string,
  207. payload: object,
  208. onChunk?: (chunk: T) => void,
  209. onDone?: () => void,
  210. onError?: (e: Error) => void,
  211. separator = STREAM_SEPARATOR,
  212. abortSignal?: AbortSignal
  213. ): Promise<void> {
  214. let onErrorOnce: ((e: Error) => void) | undefined = (e: Error) => {
  215. onErrorOnce = undefined
  216. onError?.(e)
  217. }
  218. const headers: Record<string, string> = {
  219. 'browser-id': getBrowserId(),
  220. 'Content-Type': 'application/json'
  221. }
  222. const assistantRequest: RequestInit = {
  223. headers,
  224. method: 'POST',
  225. credentials: 'include',
  226. body: JSON.stringify(payload),
  227. signal: abortSignal
  228. }
  229. try {
  230. const response = await fetch(`${context.baseUrl}${apiEndpoint}`, assistantRequest)
  231. if (response.body) {
  232. // Handle the streaming response
  233. const reader = response.body.getReader()
  234. const decoder = new TextDecoder('utf-8')
  235. let buffer = ''
  236. async function readStream() {
  237. const { done, value } = await reader.read()
  238. if (done) {
  239. if (response.ok) {
  240. onDone?.()
  241. } else {
  242. onErrorOnce?.(
  243. new ResponseError(response.statusText, {
  244. httpStatusCode: response.status
  245. })
  246. )
  247. }
  248. return
  249. }
  250. const chunk = decoder.decode(value)
  251. buffer += chunk
  252. const splitChunks = buffer.split(separator)
  253. buffer = ''
  254. for (const splitChunk of splitChunks) {
  255. if (splitChunk) {
  256. let data: T
  257. try {
  258. // data = jsonParse<T>(splitChunk, { errorMessage: 'Invalid json' });
  259. data = JSON.parse(splitChunk) as T
  260. } catch (e) {
  261. // incomplete json. append to buffer to complete
  262. buffer += splitChunk
  263. continue
  264. }
  265. try {
  266. if (response.ok) {
  267. // Call chunk callback if request was successful
  268. onChunk?.(data)
  269. } else {
  270. // Otherwise, call error callback
  271. const message = 'message' in data ? data.message : response.statusText
  272. onErrorOnce?.(
  273. new ResponseError(String(message), {
  274. httpStatusCode: response.status
  275. })
  276. )
  277. }
  278. } catch (e: unknown) {
  279. if (e instanceof Error) {
  280. onErrorOnce?.(e)
  281. }
  282. }
  283. }
  284. }
  285. await readStream()
  286. }
  287. // Start reading the stream
  288. await readStream()
  289. } else if (onErrorOnce) {
  290. onErrorOnce(new Error(response.statusText))
  291. }
  292. } catch (e: unknown) {
  293. const condition = e instanceof Error
  294. if (!condition) {
  295. // eslint-disable-next-line n8n-local-rules/no-plain-errors
  296. throw new Error('Assertion failed')
  297. }
  298. onErrorOnce?.(e)
  299. }
  300. }