mirror of
https://github.com/pnpm/pnpm.git
synced 2026-01-30 17:52:25 -05:00
105 lines
2.7 KiB
TypeScript
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
|
|
}
|
|
}
|