diff --git a/packages/hoppscotch-kernel/src/relay/impl/web/v/1.ts b/packages/hoppscotch-kernel/src/relay/impl/web/v/1.ts index 478d8b8d..0ea81e24 100644 --- a/packages/hoppscotch-kernel/src/relay/impl/web/v/1.ts +++ b/packages/hoppscotch-kernel/src/relay/impl/web/v/1.ts @@ -9,20 +9,22 @@ import { type RelayV1, type StatusCode, body, -} from '@relay/v/1' -import type { VersionedAPI } from '@type/versioning' +} from "@relay/v/1" +import type { VersionedAPI } from "@type/versioning" -import { AwsV4Signer } from 'aws4fetch' -import axios, { AxiosRequestConfig } from 'axios' +import { AwsV4Signer } from "aws4fetch" +import axios, { AxiosRequestConfig } from "axios" -import * as E from 'fp-ts/Either' -import * as R from 'fp-ts/Record' -import { pipe } from 'fp-ts/function' +import * as E from "fp-ts/Either" +import * as R from "fp-ts/Record" +import { pipe } from "fp-ts/function" const isStatusCode = (status: number): status is StatusCode => status >= 100 && status < 600 -const normalizeHeaders = (headers: Record): Record => +const normalizeHeaders = ( + headers: Record +): Record => pipe( headers, R.filterWithIndex((_, v) => v !== undefined), @@ -32,39 +34,30 @@ const normalizeHeaders = (headers: Record): Record export const implementation: VersionedAPI = { version: { major: 1, minor: 0, patch: 0 }, api: { - id: 'axios', + id: "axios", capabilities: { method: new Set([ - 'GET', - 'POST', - 'PUT', - 'DELETE', - 'PATCH', - 'HEAD', - 'OPTIONS' - ]), - header: new Set([ - 'stringvalue', - 'arrayvalue', - 'multivalue' + "GET", + "POST", + "PUT", + "DELETE", + "PATCH", + "HEAD", + "OPTIONS", ]), + header: new Set(["stringvalue", "arrayvalue", "multivalue"]), content: new Set([ - 'text', - 'json', - 'xml', - 'form', - 'urlencoded', - 'compression' - ]), - auth: new Set([ - 'basic', - 'bearer', - 'apikey', - 'aws' + "text", + "json", + "xml", + "form", + "urlencoded", + "compression", ]), + auth: new Set(["basic", "bearer", "apikey", "aws"]), security: new Set([]), proxy: new Set([]), - advanced: new Set([]) + advanced: new Set([]), }, canHandle(request: RelayRequest) { @@ -73,16 +66,19 @@ export const implementation: VersionedAPI = { kind: "unsupported_feature", feature: "method", message: `Method ${request.method} is not supported`, - relay: "axios" + relay: "axios", }) } - if (request.content && !this.capabilities.content.has(request.content.kind)) { + if ( + request.content && + !this.capabilities.content.has(request.content.kind) + ) { return E.left({ kind: "unsupported_feature", feature: "content", message: `Content type ${request.content.kind} is not supported`, - relay: "axios" + relay: "axios", }) } @@ -91,7 +87,7 @@ export const implementation: VersionedAPI = { kind: "unsupported_feature", feature: "authentication", message: `Authentication type ${request.auth.kind} is not supported`, - relay: "axios" + relay: "axios", }) } @@ -100,7 +96,7 @@ export const implementation: VersionedAPI = { kind: "unsupported_feature", feature: "security", message: "Client certificates are not supported", - relay: "axios" + relay: "axios", }) } @@ -109,7 +105,7 @@ export const implementation: VersionedAPI = { kind: "unsupported_feature", feature: "proxy", message: "Proxy is not supported", - relay: "axios" + relay: "axios", }) } @@ -121,150 +117,173 @@ export const implementation: VersionedAPI = { const emitter: RelayEventEmitter = { on: () => () => {}, once: () => () => {}, - off: () => {} + off: () => {}, } - const response: Promise> = (async () => { - try { - const startTime = Date.now() - const config: AxiosRequestConfig = { - url: request.url, - method: request.method, - headers: request.headers, - params: request.params, - data: request.content?.content, - maxRedirects: request.meta?.options?.maxRedirects, - timeout: request.meta?.options?.timeout, - decompress: request.meta?.options?.decompress ?? true, - validateStatus: null, - cancelToken: cancelTokenSource.token, - responseType: 'arraybuffer' - } - - if (request.auth) { - switch (request.auth.kind) { - case 'basic': - config.auth = { - username: request.auth.username, - password: request.auth.password - } - break - case 'bearer': - config.headers = { - ...config.headers, - Authorization: `Bearer ${request.auth.token}` - } - break - case 'apikey': - if (request.auth.in === 'header') { - config.headers = { - ...config.headers, - [request.auth.key]: request.auth.value - } - } else { - config.params = { - ...config.params, - [request.auth.key]: request.auth.value - } - } - break - case 'aws': { - const { accessKey, secretKey, region, service, sessionToken, in: location } = request.auth - const signer = new AwsV4Signer({ - url: request.url, - method: request.method, - accessKeyId: accessKey, - secretAccessKey: secretKey, - region, - service, - sessionToken, - datetime: new Date().toISOString().replace(/[:-]|\.\d{3}/g, ""), - signQuery: false - }) - const signed = await signer.sign() - if (location === "query") { - config.url = signed.url.toString() - } else { - const headers: Record = {} - signed.headers.forEach((value, key) => { - headers[key] = value - }) - config.headers = { - ...config.headers, - ...headers - } - } - break - } + const response: Promise> = + (async () => { + try { + const startTime = Date.now() + const config: AxiosRequestConfig = { + url: request.url, + method: request.method, + headers: request.headers, + params: request.params, + data: request.content?.content, + maxRedirects: request.meta?.options?.maxRedirects, + timeout: request.meta?.options?.timeout, + decompress: request.meta?.options?.decompress ?? true, + validateStatus: null, + cancelToken: cancelTokenSource.token, + responseType: "arraybuffer", } - } - const axiosResponse = await axios(config) - const endTime = Date.now() + // The following code is temporarily commented out because the auth has been pre-processed in EffectiveURL.ts and added in header + // and preprocessing here will cause the environment variables not parsed since the auth object only has the raw value - if (!isStatusCode(axiosResponse.status)) { - return E.left({ - kind: 'version', - message: `Invalid status code: ${axiosResponse.status}` - }) - } + // if (request.auth) { + // switch (request.auth.kind) { + // case "basic": + // config.auth = { + // username: request.auth.username, + // password: request.auth.password, + // } + // break + // case "bearer": + // config.headers = { + // ...config.headers, + // Authorization: `Bearer ${request.auth.token}`, + // } + // break + // case "apikey": + // if (request.auth.in === "header") { + // config.headers = { + // ...config.headers, + // [request.auth.key]: request.auth.value, + // } + // } else { + // config.params = { + // ...config.params, + // [request.auth.key]: request.auth.value, + // } + // } + // break + // case "aws": { + // const { + // accessKey, + // secretKey, + // region, + // service, + // sessionToken, + // in: location, + // } = request.auth + // const signer = new AwsV4Signer({ + // url: request.url, + // method: request.method, + // accessKeyId: accessKey, + // secretAccessKey: secretKey, + // region, + // service, + // sessionToken, + // datetime: new Date() + // .toISOString() + // .replace(/[:-]|\.\d{3}/g, ""), + // signQuery: false, + // }) + // const signed = await signer.sign() + // if (location === "query") { + // config.url = signed.url.toString() + // } else { + // const headers: Record = {} + // signed.headers.forEach((value, key) => { + // headers[key] = value + // }) + // config.headers = { + // ...config.headers, + // ...headers, + // } + // } + // break + // } + // } + // } - const normalizedHeaders = normalizeHeaders(axiosResponse.headers) - const contentType = normalizedHeaders['content-type'] || normalizedHeaders['Content-Type'] || normalizedHeaders['CONTENT-TYPE'] + const axiosResponse = await axios(config) + const endTime = Date.now() - const response: RelayResponse = { - id: request.id, - status: axiosResponse.status, - statusText: axiosResponse.statusText, - version: request.version, - headers: normalizedHeaders, - body: body.body(axiosResponse.data, contentType), - meta: { - timing: { - start: startTime, - end: endTime, - }, - size: { - headers: JSON.stringify(axiosResponse.headers).length, - body: axiosResponse.data?.length ?? 0, - total: JSON.stringify(axiosResponse.headers).length + (axiosResponse.data?.length ?? 0) - } - } - } - - return E.right(response) - } catch (error) { - if (axios.isCancel(error)) { - return E.left({ kind: 'abort', message: 'Request cancelled' }) - } - - if (axios.isAxiosError(error)) { - if (error.code === 'ECONNABORTED') { + if (!isStatusCode(axiosResponse.status)) { return E.left({ - kind: 'timeout', - message: 'Request timed out', - phase: 'response' + kind: "version", + message: `Invalid status code: ${axiosResponse.status}`, }) } + + const normalizedHeaders = normalizeHeaders(axiosResponse.headers) + const contentType = + normalizedHeaders["content-type"] || + normalizedHeaders["Content-Type"] || + normalizedHeaders["CONTENT-TYPE"] + + const response: RelayResponse = { + id: request.id, + status: axiosResponse.status, + statusText: axiosResponse.statusText, + version: request.version, + headers: normalizedHeaders, + body: body.body(axiosResponse.data, contentType), + meta: { + timing: { + start: startTime, + end: endTime, + }, + size: { + headers: JSON.stringify(axiosResponse.headers).length, + body: axiosResponse.data?.length ?? 0, + total: + JSON.stringify(axiosResponse.headers).length + + (axiosResponse.data?.length ?? 0), + }, + }, + } + + return E.right(response) + } catch (error) { + if (axios.isCancel(error)) { + return E.left({ kind: "abort", message: "Request cancelled" }) + } + + if (axios.isAxiosError(error)) { + if (error.code === "ECONNABORTED") { + return E.left({ + kind: "timeout", + message: "Request timed out", + phase: "response", + }) + } + return E.left({ + kind: "network", + message: error.message, + }) + } + return E.left({ - kind: 'network', - message: error.message + kind: "network", + message: + error instanceof Error + ? error.message + : "Unknown error occurred", + cause: error, }) } - - return E.left({ - kind: 'network', - message: error instanceof Error ? error.message : 'Unknown error occurred', - cause: error - }) - } - })() + })() return { - cancel: async () => { cancelTokenSource.cancel() }, + cancel: async () => { + cancelTokenSource.cancel() + }, emitter, - response + response, } - } - } + }, + }, }