Files
pnpm/network/fetch/src/fetch.ts
2024-03-28 17:59:10 +01:00

105 lines
2.7 KiB
TypeScript

import assert from 'assert'
import util from 'util'
import { requestRetryLogger } from '@pnpm/core-loggers'
import { operation, type RetryTimeoutOptions } from '@zkochan/retry'
import nodeFetch, { type Request, type RequestInit as NodeRequestInit, Response } from 'node-fetch'
export { isRedirect } from 'node-fetch'
export { Response, type RetryTimeoutOptions }
interface URLLike {
href: string
}
const NO_RETRY_ERROR_CODES = new Set([
'SELF_SIGNED_CERT_IN_CHAIN',
'ERR_OSSL_PEM_NO_START_LINE',
])
export type RequestInfo = string | URLLike | Request
export interface RequestInit extends NodeRequestInit {
retry?: RetryTimeoutOptions
timeout?: number
}
export async function fetch (url: RequestInfo, opts: RequestInit = {}): Promise<Response> {
const retryOpts = opts.retry ?? {}
const maxRetries = retryOpts.retries ?? 2
const op = operation({
factor: retryOpts.factor ?? 10,
maxTimeout: retryOpts.maxTimeout ?? 60000,
minTimeout: retryOpts.minTimeout ?? 10000,
randomize: false,
retries: maxRetries,
})
try {
return await new Promise((resolve, reject) => {
op.attempt(async (attempt) => {
try {
// this will be retried
const res = await nodeFetch(url as any, opts) // eslint-disable-line
// A retry on 409 sometimes helps when making requests to the Bit registry.
if ((res.status >= 500 && res.status < 600) || [408, 409, 420, 429].includes(res.status)) {
throw new ResponseError(res)
} else {
resolve(res)
}
} catch (error: unknown) {
assert(util.types.isNativeError(error))
if (
'code' in error &&
typeof error.code === 'string' &&
NO_RETRY_ERROR_CODES.has(error.code)
) {
throw error
}
const timeout = op.retry(error)
if (timeout === false) {
reject(op.mainError())
return
}
requestRetryLogger.debug({
attempt,
error,
maxRetries,
method: opts.method ?? 'GET',
timeout,
url: url.toString(),
})
}
})
})
} catch (err) {
if (err instanceof ResponseError) {
return err.res
}
throw err
}
}
export class ResponseError extends Error {
public res: Response
public code: number
public status: number
public statusCode: number
public url: string
constructor (res: Response) {
super(res.statusText)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ResponseError)
}
this.name = this.constructor.name
this.res = res
// backward compat
this.code = this.status = this.statusCode = res.status
this.url = res.url
}
}