feat: new config setting added - fetch-timeout (#3390)

close #3387
This commit is contained in:
Zoltan Kochan
2021-04-24 19:09:52 +03:00
committed by GitHub
parent 5ebbdd89bc
commit 05baaa6e74
23 changed files with 107 additions and 9 deletions

View File

@@ -0,0 +1,11 @@
---
"@pnpm/config": minor
"@pnpm/plugin-commands-audit": minor
"@pnpm/plugin-commands-installation": minor
"@pnpm/plugin-commands-outdated": minor
"@pnpm/plugin-commands-publishing": minor
"pnpm": minor
"@pnpm/store-connection-manager": minor
---
Add new config setting: `fetch-timeout`.

View File

@@ -0,0 +1,10 @@
---
"@pnpm/fetch": minor
"@pnpm/audit": minor
"@pnpm/client": minor
"@pnpm/fetching-types": minor
"@pnpm/npm-resolver": minor
"@pnpm/tarball-fetcher": minor
---
Add new option: timeout.

View File

@@ -13,6 +13,7 @@ export default async function audit (
include?: { [dependenciesField in DependenciesField]: boolean }
registry: string
retry?: RetryTimeoutOptions
timeout?: number
}
) {
const auditTree = lockfileToAuditTree(lockfile, { include: opts.include })
@@ -23,6 +24,7 @@ export default async function audit (
headers: { 'Content-Type': 'application/json' },
method: 'post',
retry: opts.retry,
timeout: opts.timeout,
})
if (res.status !== 200) {
throw new PnpmError('AUDIT_BAD_RESPONSE', `The audit endpoint (at ${auditUrl}) responded with ${res.status}: ${await res.text()}`)

View File

@@ -14,6 +14,7 @@ export { ResolveFunction }
export type ClientOptions = {
authConfig: Record<string, string>
retry?: RetryTimeoutOptions
timeout?: number
userAgent?: string
} & ResolverFactoryOptions & AgentOptions

View File

@@ -38,6 +38,7 @@ export interface Config {
fetchRetryFactor?: number
fetchRetryMintimeout?: number
fetchRetryMaxtimeout?: number
fetchTimeout?: number
saveExact?: boolean
savePrefix?: string
shellEmulator?: boolean

View File

@@ -37,6 +37,7 @@ export const types = Object.assign({
dir: String,
'enable-modules-dir': Boolean,
'enable-pre-post-scripts': Boolean,
'fetch-timeout': Number,
'fetching-concurrency': Number,
filter: [String, Array],
'filter-prod': [String, Array],
@@ -160,6 +161,7 @@ export default async (
'fetch-retry-factor': 10,
'fetch-retry-maxtimeout': 60000,
'fetch-retry-mintimeout': 10000,
'fetch-timeout': 60000,
globalconfig: npmDefaults.globalconfig,
hoist: true,
'hoist-pattern': ['*'],

View File

@@ -19,6 +19,7 @@ export type RequestInfo = string | URLLike | Request
export interface RequestInit extends NodeRequestInit {
retry?: RetryTimeoutOptions
timeout?: number
}
export const isRedirect = fetch.isRedirect

View File

@@ -47,6 +47,7 @@ export default function (
headers,
redirect: 'manual',
retry: opts?.retry,
timeout: opts?.timeout ?? 60000,
})
if (!isRedirect(response.status) || redirects >= MAX_FOLLOWED_REDIRECTS) {
return response

View File

@@ -8,6 +8,7 @@ export type FetchFromRegistry = (
opts?: {
authHeaderValue?: string
retry?: RetryTimeoutOptions
timeout?: number
}
) => Promise<Response>

View File

@@ -41,16 +41,20 @@ export class RegistryResponseError extends FetchError {
export default async function fromRegistry (
fetch: FetchFromRegistry,
retryOpts: RetryTimeoutOptions,
fetchOpts: { retry: RetryTimeoutOptions, timeout: number },
pkgName: string,
registry: string,
authHeaderValue?: string
): Promise<PackageMeta> {
const uri = toUri(pkgName, registry)
const op = retry.operation(retryOpts)
const op = retry.operation(fetchOpts.retry)
return new Promise((resolve, reject) =>
op.attempt(async (attempt) => {
const response = await fetch(uri, { authHeaderValue, retry: retryOpts }) as RegistryResponse
const response = await fetch(uri, {
authHeaderValue,
retry: fetchOpts.retry,
timeout: fetchOpts.timeout,
}) as RegistryResponse
if (response.status > 400) {
const request = {
authHeaderValue,
@@ -75,7 +79,7 @@ export default async function fromRegistry (
requestRetryLogger.debug({
attempt,
error,
maxRetries: retryOpts.retries!,
maxRetries: fetchOpts.retry.retries!,
method: 'GET',
timeout,
url: uri,

View File

@@ -59,6 +59,7 @@ export interface ResolverFactoryOptions {
offline?: boolean
preferOffline?: boolean
retry?: RetryTimeoutOptions
timeout?: number
}
export default function createResolver (
@@ -69,7 +70,11 @@ export default function createResolver (
if (typeof opts.storeDir !== 'string') { // eslint-disable-line
throw new TypeError('`opts.storeDir` is required and needs to be a string')
}
const fetch = pMemoize(fromRegistry.bind(null, fetchFromRegistry, opts.retry ?? {}), {
const fetchOpts = {
retry: opts.retry ?? {},
timeout: opts.timeout ?? 60000,
}
const fetch = pMemoize(fromRegistry.bind(null, fetchFromRegistry, fetchOpts), {
cacheKey: (...args) => JSON.stringify(args),
maxAge: 1000 * 20, // 20 seconds
})

View File

@@ -93,7 +93,7 @@ export async function handler (
json?: boolean
lockfileDir?: string
registries: Registries
} & Pick<Config, 'fetchRetries' | 'fetchRetryMaxtimeout' | 'fetchRetryMintimeout' | 'fetchRetryFactor' | 'production' | 'dev' | 'optional'>
} & Pick<Config, 'fetchRetries' | 'fetchRetryMaxtimeout' | 'fetchRetryMintimeout' | 'fetchRetryFactor' | 'fetchTimeout' | 'production' | 'dev' | 'optional'>
) {
const lockfile = await readWantedLockfile(opts.lockfileDir ?? opts.dir, { ignoreIncompatible: true })
if (lockfile == null) {
@@ -113,6 +113,7 @@ export async function handler (
minTimeout: opts.fetchRetryMintimeout,
retries: opts.fetchRetries,
},
timeout: opts.fetchTimeout,
})
const vulnerabilities = auditReport.metadata.vulnerabilities
const totalVulnerabilityCount = Object.values(vulnerabilities)

View File

@@ -15,6 +15,7 @@ export function rcOptionsTypes () {
'fetch-retry-factor',
'fetch-retry-maxtimeout',
'fetch-retry-mintimeout',
'fetch-timeout',
'force',
'global-dir',
'global-pnpmfile',

View File

@@ -17,6 +17,7 @@ export function rcOptionsTypes () {
'fetch-retry-factor',
'fetch-retry-maxtimeout',
'fetch-retry-mintimeout',
'fetch-timeout',
'frozen-lockfile',
'global-dir',
'global-pnpmfile',

View File

@@ -25,6 +25,7 @@ export function rcOptionsTypes () {
'fetch-retry-factor',
'fetch-retry-maxtimeout',
'fetch-retry-mintimeout',
'fetch-timeout',
'force',
'global-dir',
'global-pnpmfile',
@@ -188,6 +189,13 @@ async function interactiveUpdate (
...opts,
compatible: opts.latest !== true,
include,
retry: {
factor: opts.fetchRetryFactor,
maxTimeout: opts.fetchRetryMaxtimeout,
minTimeout: opts.fetchRetryMintimeout,
retries: opts.fetchRetries,
},
timeout: opts.fetchTimeout,
})
const choices = getUpdateChoices(R.unnest(outdatedPkgsOfProjects))
if (choices.length === 0) {

View File

@@ -132,6 +132,7 @@ export type OutdatedCommandOptions = {
| 'fetchRetryFactor'
| 'fetchRetryMaxtimeout'
| 'fetchRetryMintimeout'
| 'fetchTimeout'
| 'global'
| 'httpProxy'
| 'httpsProxy'
@@ -175,6 +176,13 @@ export async function handler (
...opts,
fullMetadata: opts.long,
include,
retry: {
factor: opts.fetchRetryFactor,
maxTimeout: opts.fetchRetryMaxtimeout,
minTimeout: opts.fetchRetryMintimeout,
retries: opts.fetchRetries,
},
timeout: opts.fetchTimeout,
})
if (outdatedPackages.length === 0) return { output: '', exitCode: 0 }

View File

@@ -49,7 +49,17 @@ export default async (
opts: OutdatedCommandOptions & { include: IncludedDependencies }
) => {
const outdatedMap = {} as Record<string, OutdatedInWorkspace>
const outdatedPackagesByProject = await outdatedDepsOfProjects(pkgs, params, { ...opts, fullMetadata: opts.long })
const outdatedPackagesByProject = await outdatedDepsOfProjects(pkgs, params, {
...opts,
fullMetadata: opts.long,
retry: {
factor: opts.fetchRetryFactor,
maxTimeout: opts.fetchRetryMaxtimeout,
minTimeout: opts.fetchRetryMintimeout,
retries: opts.fetchRetries,
},
timeout: opts.fetchTimeout,
})
for (let i = 0; i < outdatedPackagesByProject.length; i++) {
const { dir, manifest } = pkgs[i]
outdatedPackagesByProject[i].forEach((outdatedPkg) => {

View File

@@ -20,6 +20,7 @@ Partial<Pick<Config,
| 'tag'
| 'ca'
| 'cert'
| 'fetchTimeout'
| 'force'
| 'dryRun'
| 'extraBinPaths'
@@ -52,10 +53,18 @@ export default async function (
) {
const pkgs = Object.values(opts.selectedProjectsGraph).map((wsPkg) => wsPkg.package)
const storeDir = await storePath(opts.workspaceDir, opts.storeDir)
const resolve = createResolver(Object.assign(opts, {
const resolve = createResolver({
...opts,
authConfig: opts.rawConfig,
retry: {
factor: opts.fetchRetryFactor,
maxTimeout: opts.fetchRetryMaxtimeout,
minTimeout: opts.fetchRetryMintimeout,
retries: opts.fetchRetries,
},
storeDir,
})) as unknown as ResolveFunction
timeout: opts.fetchTimeout,
}) as unknown as ResolveFunction
const pkgsToPublish = await pFilter(pkgs, async (pkg) => {
if (!pkg.manifest.name || !pkg.manifest.version || pkg.manifest.private) return false
if (opts.force) return true

View File

@@ -31,7 +31,14 @@ export default async function (config: Config) {
const resolve = createResolver({
...config,
authConfig: config.rawConfig,
retry: {
factor: config.fetchRetryFactor,
maxTimeout: config.fetchRetryMaxtimeout,
minTimeout: config.fetchRetryMintimeout,
retries: config.fetchRetries,
},
storeDir,
timeout: config.fetchTimeout,
})
const resolution = await resolve({ alias: packageManager.name, pref: 'latest' }, {
lockfileDir: config.lockfileDir ?? config.dir,

View File

@@ -434,3 +434,11 @@ test('installing in a CI environment', async () => {
await execPnpm(['install', '--no-prefer-frozen-lockfile'], { env: { CI: 'true' } })
})
test('installation fails with a timeout error', async () => {
prepare()
await expect(
execPnpm(['add', 'typescript@2.4.2', '--fetch-timeout=1', '--fetch-retries=0'])
).rejects.toThrow()
})

View File

@@ -17,6 +17,7 @@ type CreateResolverOptions = Pick<Config,
export type CreateNewStoreControllerOptions = CreateResolverOptions & Pick<Config,
| 'ca'
| 'cert'
| 'fetchTimeout'
| 'httpProxy'
| 'httpsProxy'
| 'key'
@@ -57,6 +58,7 @@ export default async (
},
storeDir: opts.storeDir,
strictSSL: opts.strictSsl ?? true,
timeout: opts.fetchTimeout,
userAgent: opts.userAgent,
maxSockets: opts.networkConcurrency != null
? (opts.networkConcurrency * 3)

View File

@@ -74,6 +74,7 @@ export default (
maxTimeout?: number
randomize?: boolean
}
timeout?: number
}
): DownloadFunction => {
const retryOpts = {
@@ -142,6 +143,7 @@ export default (
// Hence, we tell fetch to not retry,
// and we perform the retries from this function instead.
retry: { retries: 0 },
timeout: gotOpts.timeout,
})
if (res.status !== 200) {

View File

@@ -27,12 +27,14 @@ export default function (
fetchFromRegistry: FetchFromRegistry,
getCredentials: GetCredentials,
opts: {
timeout?: number
retry?: RetryTimeoutOptions
offline?: boolean
}
): { tarball: FetchFunction } {
const download = createDownloader(fetchFromRegistry, {
retry: opts.retry,
timeout: opts.timeout,
})
return {
tarball: fetchFromTarball.bind(null, {