|
@@ -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<string, any>): Record<string, string> =>
|
|
|
+const normalizeHeaders = (
|
|
|
+ headers: Record<string, any>
|
|
|
+): Record<string, string> =>
|
|
|
pipe(
|
|
|
headers,
|
|
|
R.filterWithIndex((_, v) => v !== undefined),
|
|
@@ -32,39 +34,30 @@ const normalizeHeaders = (headers: Record<string, any>): Record<string, string>
|
|
|
export const implementation: VersionedAPI<RelayV1> = {
|
|
|
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<RelayV1> = {
|
|
|
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<RelayV1> = {
|
|
|
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<RelayV1> = {
|
|
|
kind: "unsupported_feature",
|
|
|
feature: "security",
|
|
|
message: "Client certificates are not supported",
|
|
|
- relay: "axios"
|
|
|
+ relay: "axios",
|
|
|
})
|
|
|
}
|
|
|
|
|
@@ -109,7 +105,7 @@ export const implementation: VersionedAPI<RelayV1> = {
|
|
|
kind: "unsupported_feature",
|
|
|
feature: "proxy",
|
|
|
message: "Proxy is not supported",
|
|
|
- relay: "axios"
|
|
|
+ relay: "axios",
|
|
|
})
|
|
|
}
|
|
|
|
|
@@ -121,150 +117,173 @@ export const implementation: VersionedAPI<RelayV1> = {
|
|
|
const emitter: RelayEventEmitter<RelayRequestEvents> = {
|
|
|
on: () => () => {},
|
|
|
once: () => () => {},
|
|
|
- off: () => {}
|
|
|
+ off: () => {},
|
|
|
}
|
|
|
|
|
|
- const response: Promise<E.Either<RelayError, RelayResponse>> = (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<string, string> = {}
|
|
|
- signed.headers.forEach((value, key) => {
|
|
|
- headers[key] = value
|
|
|
- })
|
|
|
- config.headers = {
|
|
|
- ...config.headers,
|
|
|
- ...headers
|
|
|
- }
|
|
|
- }
|
|
|
- break
|
|
|
- }
|
|
|
+ const response: Promise<E.Either<RelayError, RelayResponse>> =
|
|
|
+ (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<string, string> = {}
|
|
|
+ // signed.headers.forEach((value, key) => {
|
|
|
+ // headers[key] = value
|
|
|
+ // })
|
|
|
+ // config.headers = {
|
|
|
+ // ...config.headers,
|
|
|
+ // ...headers,
|
|
|
+ // }
|
|
|
+ // }
|
|
|
+ // break
|
|
|
+ // }
|
|
|
+ // }
|
|
|
+ // }
|
|
|
+
|
|
|
+ const axiosResponse = await axios(config)
|
|
|
+ const endTime = Date.now()
|
|
|
+
|
|
|
+ if (!isStatusCode(axiosResponse.status)) {
|
|
|
+ return E.left({
|
|
|
+ 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 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,
|
|
|
+ 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),
|
|
|
+ },
|
|
|
},
|
|
|
- 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' })
|
|
|
- }
|
|
|
+ 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 (axios.isAxiosError(error)) {
|
|
|
+ if (error.code === "ECONNABORTED") {
|
|
|
+ return E.left({
|
|
|
+ kind: "timeout",
|
|
|
+ message: "Request timed out",
|
|
|
+ phase: "response",
|
|
|
+ })
|
|
|
+ }
|
|
|
return E.left({
|
|
|
- kind: 'timeout',
|
|
|
- message: 'Request timed out',
|
|
|
- phase: 'response'
|
|
|
+ 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,
|
|
|
}
|
|
|
- }
|
|
|
- }
|
|
|
+ },
|
|
|
+ },
|
|
|
}
|