Skip to content

Commit

Permalink
Implement support for request labels (#74)
Browse files Browse the repository at this point in the history
  • Loading branch information
kibertoad authored Oct 31, 2023
1 parent 96b1bfe commit 36c5d2b
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 26 deletions.
27 changes: 26 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,30 @@
"newlines-between": "always"
}
]
}
},
"overrides": [
{
"files": ["test/**/*", "*.spec.ts", "*.test.ts"],
"rules": {
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/indent": "off",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/consistent-type-imports": "warn",
"@typescript-eslint/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
]
}
}
]
}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ All _send_ methods accept a type parameter and the following arguments:
- `throwOnError`;`
- `reqContext`;
- `safeParseJson`, used when the response content-type is `application/json`. If `true`, the response body will be parsed as JSON and a `ResponseError` will be thrown in case of syntax errors. If `false`, errors are not handled;
- `blobResponseBody`, used when the response body should be returned as Blob;
- `requestLabel`, if set, will be returned together with any thrown or returned Error to provide additional context about what request was being executed when the error has happened;
- `disableKeepAlive`;`
- `retryConfig`, defined by:
- `maxAttempts`, the maximum number of times a request should be retried;
Expand Down
20 changes: 10 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,21 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
"pino": "^8.15.6",
"undici": "^5.25.4",
"undici-retry": "^2.1.0"
"pino": "^8.16.1",
"undici": "^5.27.0",
"undici-retry": "^3.1.2"
},
"devDependencies": {
"@types/node": "^20.8.2",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"@types/node": "^20.8.9",
"@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.9.1",
"@vitest/coverage-v8": "^0.34.6",
"auto-changelog": "^2.4.0",
"eslint": "^8.50.0",
"eslint": "^8.52.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vitest": "^0.3.2",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-vitest": "^0.3.8",
"prettier": "^3.0.3",
"typescript": "^5.2.2",
"vitest": "^0.34.6",
Expand Down
3 changes: 2 additions & 1 deletion src/errors/ResponseStatusError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ export class ResponseStatusError extends InternalError {
public readonly response: RequestResult<any>

// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(requestResult: RequestResult<any>) {
constructor(requestResult: RequestResult<any>, requestLabel = 'N/A') {
super({
message: `Response status code ${requestResult.statusCode}`,
details: {
requestLabel,
response: {
statusCode: requestResult.statusCode,
body: requestResult.body,
Expand Down
26 changes: 25 additions & 1 deletion src/http/httpClient.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Interceptable } from 'undici'
import { Client, MockAgent, setGlobalDispatcher } from 'undici'
import { isInternalRequestError } from 'undici-retry'
import { z } from 'zod'

import type { HttpRequestContext } from './httpClient'
Expand Down Expand Up @@ -142,7 +143,7 @@ describe('httpClient', () => {
.reply(200, 'this is not a real json', { headers: JSON_HEADERS })

try {
await sendGet(client, '/products/1', { safeParseJson: true })
await sendGet(client, '/products/1', { safeParseJson: true, requestLabel: 'label' })
} catch (err) {
// This is needed, because built-in error assertions do not assert nested fields
// eslint-disable-next-line vitest/no-conditional-expect
Expand All @@ -151,6 +152,7 @@ describe('httpClient', () => {
errorCode: 'INVALID_HTTP_RESPONSE_JSON',
details: {
rawBody: 'this is not a real json',
requestLabel: 'label',
},
})
}
Expand Down Expand Up @@ -240,6 +242,26 @@ describe('httpClient', () => {
})
})

it('Throws an error with a label on internal error', async () => {
expect.assertions(2)
const query = {
limit: 3,
}

try {
await sendGet(buildClient('http://127.0.0.1'), '/dummy', {
requestLabel: 'label',
query,
})
} catch (err) {
if (!isInternalRequestError(err)) {
throw new Error('Invalid error type')
}
expect(err.message).toBe('connect ECONNREFUSED 127.0.0.1:80')
expect(err.details!.requestLabel).toBe('label')
}
})

it('Returns error response', async () => {
expect.assertions(1)
const query = {
Expand All @@ -257,6 +279,7 @@ describe('httpClient', () => {
await expect(
sendGet(client, '/products', {
query,
requestLabel: 'label',
}),
).rejects.toMatchObject({
message: 'Response status code 400',
Expand All @@ -265,6 +288,7 @@ describe('httpClient', () => {
statusCode: 400,
},
details: {
requestLabel: 'label',
response: {
body: 'Invalid request',
statusCode: 400,
Expand Down
40 changes: 27 additions & 13 deletions src/http/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Readable } from 'stream'

import { Client } from 'undici'
import type { FormData } from 'undici'
import type { RequestResult, RetryConfig } from 'undici-retry'
import type { RequestResult, RequestParams, RetryConfig } from 'undici-retry'
import { DEFAULT_RETRY_CONFIG, sendWithRetry } from 'undici-retry'

import { ResponseStatusError } from '../errors/ResponseStatusError'
Expand All @@ -27,9 +27,13 @@ export type RequestOptions<T> = {
timeout: number | undefined
throwOnError?: boolean
reqContext?: HttpRequestContext

safeParseJson?: boolean
blobResponseBody?: boolean
requestLabel?: string

disableKeepAlive?: boolean
retryConfig?: Omit<RetryConfig, 'safeParseJson'>
retryConfig?: RetryConfig
clientOptions?: Client.Options
responseSchema?: ResponseSchema<T>
validateResponse: boolean
Expand Down Expand Up @@ -73,13 +77,15 @@ export async function sendGet<T>(
throwOnError: false,
},
resolveRetryConfig(options),
resolveRequestConfig(options),
)

return resolveResult(
result,
options.throwOnError ?? DEFAULT_OPTIONS.throwOnError,
options.validateResponse ?? DEFAULT_OPTIONS.validateResponse,
options.responseSchema,
options.requestLabel,
)
}

Expand All @@ -104,6 +110,7 @@ export async function sendDelete<T>(
throwOnError: false,
},
resolveRetryConfig(options),
resolveRequestConfig(options),
)

return resolveResult(
Expand Down Expand Up @@ -137,6 +144,7 @@ export async function sendPost<T>(
throwOnError: false,
},
resolveRetryConfig(options),
resolveRequestConfig(options),
)

return resolveResult(
Expand Down Expand Up @@ -170,6 +178,7 @@ export async function sendPostBinary<T>(
throwOnError: false,
},
resolveRetryConfig(options),
resolveRequestConfig(options),
)

return resolveResult(
Expand Down Expand Up @@ -203,6 +212,7 @@ export async function sendPut<T>(
throwOnError: false,
},
resolveRetryConfig(options),
resolveRequestConfig(options),
)

return resolveResult(
Expand Down Expand Up @@ -236,6 +246,7 @@ export async function sendPutBinary<T>(
throwOnError: false,
},
resolveRetryConfig(options),
resolveRequestConfig(options),
)

return resolveResult(
Expand Down Expand Up @@ -269,6 +280,7 @@ export async function sendPatch<T>(
throwOnError: false,
},
resolveRetryConfig(options),
resolveRequestConfig(options),
)

return resolveResult(
Expand All @@ -279,17 +291,17 @@ export async function sendPatch<T>(
)
}

function resolveRequestConfig(options: Partial<RequestOptions<unknown>>): RequestParams {
return {
safeParseJson: options.safeParseJson ?? false,
blobBody: options.blobResponseBody ?? false,
throwOnInternalError: true,
requestLabel: options.requestLabel,
}
}

function resolveRetryConfig(options: Partial<RequestOptions<unknown>>): RetryConfig {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return options.retryConfig
? {
...options.retryConfig,
safeParseJson: options.safeParseJson ?? false,
}
: {
...DEFAULT_RETRY_CONFIG,
safeParseJson: options.safeParseJson ?? false,
}
return options.retryConfig ?? DEFAULT_RETRY_CONFIG
}

export function buildClient(baseUrl: string, clientOptions?: Client.Options) {
Expand All @@ -305,9 +317,11 @@ function resolveResult<T>(
throwOnError: boolean,
validateResponse: boolean,
validationSchema?: ResponseSchema,
requestLabel?: string,
): DefiniteEither<RequestResult<unknown>, RequestResult<T>> {
// Throw response error
if (requestResult.error && throwOnError) {
throw new ResponseStatusError(requestResult.error)
throw new ResponseStatusError(requestResult.error, requestLabel)
}
if (requestResult.result && validateResponse && validationSchema) {
requestResult.result.body = validationSchema.parse(requestResult.result.body)
Expand Down

0 comments on commit 36c5d2b

Please sign in to comment.