mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-30 02:28:18 -05:00
5
.changeset/loud-doors-wash.md
Normal file
5
.changeset/loud-doors-wash.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/tarball-fetcher": patch
|
||||
---
|
||||
|
||||
Throw a better error message when a local tarball integrity check fails.
|
||||
5
.changeset/new-eels-visit.md
Normal file
5
.changeset/new-eels-visit.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/error": minor
|
||||
---
|
||||
|
||||
Every error object has an optional "attempts" field.
|
||||
@@ -1,11 +1,20 @@
|
||||
export default class PnpmError extends Error {
|
||||
public readonly code: string
|
||||
public readonly hint?: string
|
||||
public attempts?: number
|
||||
public pkgsStack?: Array<{ id: string, name: string, version: string }>
|
||||
constructor (code: string, message: string, opts?: { hint?: string }) {
|
||||
constructor (
|
||||
code: string,
|
||||
message: string,
|
||||
opts?: {
|
||||
attempts?: number
|
||||
hint?: string
|
||||
}
|
||||
) {
|
||||
super(message)
|
||||
this.code = `ERR_PNPM_${code}`
|
||||
this.hint = opts?.hint
|
||||
this.attempts = opts?.attempts
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1
packages/tarball-fetcher/jest.config.js
Normal file
1
packages/tarball-fetcher/jest.config.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('../../jest.config')
|
||||
@@ -11,7 +11,7 @@
|
||||
"scripts": {
|
||||
"lint": "eslint -c ../../eslint.json src/**/*.ts test/**/*.ts",
|
||||
"prepublishOnly": "pnpm run compile",
|
||||
"_test": "cd ../.. && c8 --reporter lcov --reports-dir packages/tarball-fetcher/coverage ts-node packages/tarball-fetcher/test --type-check",
|
||||
"_test": "jest",
|
||||
"test": "pnpm run compile && pnpm run _test",
|
||||
"compile": "rimraf lib tsconfig.tsbuildinfo && tsc --build"
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ import ssri = require('ssri')
|
||||
|
||||
const BIG_TARBALL_SIZE = 1024 * 1024 * 5 // 5 MB
|
||||
|
||||
class TarballIntegrityError extends PnpmError {
|
||||
export class TarballIntegrityError extends PnpmError {
|
||||
public readonly found: string
|
||||
public readonly expected: string
|
||||
public readonly algorithm: string
|
||||
@@ -22,13 +22,17 @@ class TarballIntegrityError extends PnpmError {
|
||||
public readonly url: string
|
||||
|
||||
constructor (opts: {
|
||||
attempts?: number
|
||||
found: string
|
||||
expected: string
|
||||
algorithm: string
|
||||
sri: string
|
||||
url: string
|
||||
}) {
|
||||
super('TARBALL_INTEGRITY', `Got unexpected checksum for "${opts.url}". Wanted "${opts.expected}". Got "${opts.found}".`)
|
||||
super('TARBALL_INTEGRITY',
|
||||
`Got unexpected checksum for "${opts.url}". Wanted "${opts.expected}". Got "${opts.found}".`,
|
||||
{ attempts: opts.attempts }
|
||||
)
|
||||
this.found = opts.found
|
||||
this.expected = opts.expected
|
||||
this.algorithm = opts.algorithm
|
||||
|
||||
@@ -5,13 +5,16 @@ export default class BadTarballError extends PnpmError {
|
||||
public receivedSize: number
|
||||
constructor (
|
||||
opts: {
|
||||
attempts?: number
|
||||
expectedSize: number
|
||||
receivedSize: number
|
||||
tarballUrl: string
|
||||
}
|
||||
) {
|
||||
const message = `Actual size (${opts.receivedSize}) of tarball (${opts.tarballUrl}) did not match the one specified in 'Content-Length' header (${opts.expectedSize})`
|
||||
super('BAD_TARBALL_SIZE', message)
|
||||
super('BAD_TARBALL_SIZE', message, {
|
||||
attempts: opts?.attempts,
|
||||
})
|
||||
this.expectedSize = opts.expectedSize
|
||||
this.receivedSize = opts.receivedSize
|
||||
}
|
||||
|
||||
@@ -11,11 +11,18 @@ import {
|
||||
GetCredentials,
|
||||
RetryTimeoutOptions,
|
||||
} from '@pnpm/fetching-types'
|
||||
import createDownloader, { DownloadFunction } from './createDownloader'
|
||||
import createDownloader, {
|
||||
DownloadFunction,
|
||||
TarballIntegrityError,
|
||||
} from './createDownloader'
|
||||
import path = require('path')
|
||||
import fs = require('mz/fs')
|
||||
import ssri = require('ssri')
|
||||
|
||||
export { BadTarballError } from './errorTypes'
|
||||
|
||||
export { TarballIntegrityError }
|
||||
|
||||
export default function (
|
||||
fetchFromRegistry: FetchFromRegistry,
|
||||
getCredentials: GetCredentials,
|
||||
@@ -36,7 +43,7 @@ export default function (
|
||||
}
|
||||
}
|
||||
|
||||
function fetchFromTarball (
|
||||
async function fetchFromTarball (
|
||||
ctx: {
|
||||
download: DownloadFunction
|
||||
getCredentialsByURI: (registry: string) => {
|
||||
@@ -101,8 +108,15 @@ async function fetchFromLocalTarball (
|
||||
)
|
||||
return { filesIndex: fetchResult }
|
||||
} catch (err) {
|
||||
err.attempts = 1
|
||||
err.resource = tarball
|
||||
throw err
|
||||
const error = new TarballIntegrityError({
|
||||
attempts: 1,
|
||||
algorithm: err['algorithm'],
|
||||
expected: err['expected'],
|
||||
found: err['found'],
|
||||
sri: err['sri'],
|
||||
url: tarball,
|
||||
})
|
||||
error['resource'] = tarball
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
/// <reference path="../../../typings/index.d.ts" />
|
||||
import createCafs from '@pnpm/cafs'
|
||||
import PnpmError, { FetchError } from '@pnpm/error'
|
||||
import { createFetchFromRegistry } from '@pnpm/fetch'
|
||||
import createFetcher from '@pnpm/tarball-fetcher'
|
||||
import createFetcher, {
|
||||
BadTarballError,
|
||||
TarballIntegrityError,
|
||||
} from '@pnpm/tarball-fetcher'
|
||||
import path = require('path')
|
||||
import cpFile = require('cp-file')
|
||||
import fs = require('mz/fs')
|
||||
import nock = require('nock')
|
||||
import ssri = require('ssri')
|
||||
import test = require('tape')
|
||||
import tempy = require('tempy')
|
||||
|
||||
const cafsDir = tempy.directory()
|
||||
console.log(cafsDir)
|
||||
const cafs = createCafs(cafsDir)
|
||||
|
||||
const tarballPath = path.join(__dirname, 'tars', 'babel-helper-hoist-variables-6.24.1.tgz')
|
||||
@@ -28,7 +30,7 @@ const fetch = createFetcher(fetchFromRegistry, getCredentials, {
|
||||
},
|
||||
})
|
||||
|
||||
test('fail when tarball size does not match content-length', async t => {
|
||||
test('fail when tarball size does not match content-length', async () => {
|
||||
const scope = nock(registry)
|
||||
.get('/foo.tgz')
|
||||
.times(2)
|
||||
@@ -37,7 +39,6 @@ test('fail when tarball size does not match content-length', async t => {
|
||||
})
|
||||
|
||||
process.chdir(tempy.directory())
|
||||
t.comment(`temp dir ${process.cwd()}`)
|
||||
|
||||
const resolution = {
|
||||
// Even though the integrity of the downloaded tarball
|
||||
@@ -48,24 +49,21 @@ test('fail when tarball size does not match content-length', async t => {
|
||||
tarball: `${registry}foo.tgz`,
|
||||
}
|
||||
|
||||
try {
|
||||
await fetch.tarball(cafs, resolution, {
|
||||
await expect(
|
||||
fetch.tarball(cafs, resolution, {
|
||||
lockfileDir: process.cwd(),
|
||||
})
|
||||
t.fail('should have failed')
|
||||
} catch (err) {
|
||||
t.equal(err.message, 'Actual size (1279) of tarball (http://example.com/foo.tgz) did not match the one specified in \'Content-Length\' header (1048576)')
|
||||
t.equal(err['code'], 'ERR_PNPM_BAD_TARBALL_SIZE')
|
||||
t.equal(err['expectedSize'], 1048576)
|
||||
t.equal(err['receivedSize'], tarballSize)
|
||||
t.equal(err['attempts'], 2)
|
||||
|
||||
t.ok(scope.isDone())
|
||||
t.end()
|
||||
}
|
||||
).rejects.toThrow(
|
||||
new BadTarballError({
|
||||
expectedSize: 1048576,
|
||||
receivedSize: tarballSize,
|
||||
tarballUrl: resolution.tarball,
|
||||
})
|
||||
)
|
||||
expect(scope.isDone()).toBeTruthy()
|
||||
})
|
||||
|
||||
test('retry when tarball size does not match content-length', async t => {
|
||||
test('retry when tarball size does not match content-length', async () => {
|
||||
nock(registry)
|
||||
.get('/foo.tgz')
|
||||
.replyWithFile(200, tarballPath, {
|
||||
@@ -79,7 +77,6 @@ test('retry when tarball size does not match content-length', async t => {
|
||||
})
|
||||
|
||||
process.chdir(tempy.directory())
|
||||
t.comment(`testing in ${process.cwd()}`)
|
||||
|
||||
const resolution = { tarball: 'http://example.com/foo.tgz' }
|
||||
|
||||
@@ -87,12 +84,11 @@ test('retry when tarball size does not match content-length', async t => {
|
||||
lockfileDir: process.cwd(),
|
||||
})
|
||||
|
||||
t.ok(result.filesIndex)
|
||||
t.ok(nock.isDone())
|
||||
t.end()
|
||||
expect(result.filesIndex).toBeTruthy()
|
||||
expect(nock.isDone()).toBeTruthy()
|
||||
})
|
||||
|
||||
test('fail when integrity check fails two times in a row', async t => {
|
||||
test('fail when integrity check fails two times in a row', async () => {
|
||||
const scope = nock(registry)
|
||||
.get('/foo.tgz')
|
||||
.times(2)
|
||||
@@ -101,31 +97,29 @@ test('fail when integrity check fails two times in a row', async t => {
|
||||
})
|
||||
|
||||
process.chdir(tempy.directory())
|
||||
t.comment(`testing in ${process.cwd()}`)
|
||||
|
||||
const resolution = {
|
||||
integrity: tarballIntegrity,
|
||||
tarball: 'http://example.com/foo.tgz',
|
||||
}
|
||||
|
||||
try {
|
||||
await fetch.tarball(cafs, resolution, {
|
||||
await expect(
|
||||
fetch.tarball(cafs, resolution, {
|
||||
lockfileDir: process.cwd(),
|
||||
})
|
||||
t.fail('should have failed')
|
||||
} catch (err) {
|
||||
t.equal(err.message, 'Got unexpected checksum for "http://example.com/foo.tgz". Wanted "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=". ' +
|
||||
'Got "sha512-VuFL1iPaIxJK/k3gTxStIkc6+wSiDwlLdnCWNZyapsVLobu/0onvGOZolASZpfBFiDJYrOIGiDzgLIULTW61Vg== sha1-ACjKMFA7S6uRFXSDFfH4aT+4B4Y=".')
|
||||
t.equal(err['code'], 'ERR_PNPM_TARBALL_INTEGRITY')
|
||||
t.equal(err['resource'], 'http://example.com/foo.tgz')
|
||||
t.equal(err['attempts'], 2)
|
||||
|
||||
t.ok(scope.isDone())
|
||||
t.end()
|
||||
}
|
||||
).rejects.toThrow(
|
||||
new TarballIntegrityError({
|
||||
algorithm: 'sha512',
|
||||
expected: 'sha1-HssnaJydJVE+rbyZFKc/VAi+enY=',
|
||||
found: 'sha512-VuFL1iPaIxJK/k3gTxStIkc6+wSiDwlLdnCWNZyapsVLobu/0onvGOZolASZpfBFiDJYrOIGiDzgLIULTW61Vg== sha1-ACjKMFA7S6uRFXSDFfH4aT+4B4Y=',
|
||||
sri: '',
|
||||
url: resolution.tarball,
|
||||
})
|
||||
)
|
||||
expect(scope.isDone()).toBeTruthy()
|
||||
})
|
||||
|
||||
test('retry when integrity check fails', async t => {
|
||||
test('retry when integrity check fails', async () => {
|
||||
const scope = nock(registry)
|
||||
.get('/foo.tgz')
|
||||
.replyWithFile(200, path.join(__dirname, 'tars', 'babel-helper-hoist-variables-7.0.0-alpha.10.tgz'), {
|
||||
@@ -137,7 +131,6 @@ test('retry when integrity check fails', async t => {
|
||||
})
|
||||
|
||||
process.chdir(tempy.directory())
|
||||
t.comment(`testing in ${process.cwd()}`)
|
||||
|
||||
const resolution = {
|
||||
integrity: tarballIntegrity,
|
||||
@@ -152,16 +145,15 @@ test('retry when integrity check fails', async t => {
|
||||
},
|
||||
})
|
||||
|
||||
t.deepEqual(params[0], [1194, 1])
|
||||
t.deepEqual(params[1], [tarballSize, 2])
|
||||
expect(params[0]).toStrictEqual([1194, 1])
|
||||
expect(params[1]).toStrictEqual([tarballSize, 2])
|
||||
|
||||
t.ok(scope.isDone())
|
||||
t.end()
|
||||
expect(scope.isDone()).toBeTruthy()
|
||||
})
|
||||
|
||||
test('fail when integrity check of local file fails', async (t) => {
|
||||
process.chdir(tempy.directory())
|
||||
t.comment(`testing in ${process.cwd()}`)
|
||||
test('fail when integrity check of local file fails', async () => {
|
||||
const storeDir = tempy.directory()
|
||||
process.chdir(storeDir)
|
||||
|
||||
await cpFile(
|
||||
path.join(__dirname, 'tars', 'babel-helper-hoist-variables-7.0.0-alpha.10.tgz'),
|
||||
@@ -172,28 +164,23 @@ test('fail when integrity check of local file fails', async (t) => {
|
||||
tarball: 'file:tar.tgz',
|
||||
}
|
||||
|
||||
let err: Error | null = null
|
||||
try {
|
||||
await fetch.tarball(cafs, resolution, {
|
||||
await expect(
|
||||
fetch.tarball(cafs, resolution, {
|
||||
lockfileDir: process.cwd(),
|
||||
})
|
||||
} catch (_err) {
|
||||
err = _err
|
||||
}
|
||||
|
||||
t.ok(err, 'error thrown')
|
||||
t.equal(err.message, 'sha1-HssnaJydJVE+rbyZFKc/VAi+enY= integrity checksum failed when using sha1: ' +
|
||||
'wanted sha1-HssnaJydJVE+rbyZFKc/VAi+enY= but got sha512-VuFL1iPaIxJK/k3gTxStIkc6+wSiDwlLdnCWNZyapsVLobu/0onvGOZolASZpfBFiDJYrOIGiDzgLIULTW61Vg== sha1-ACjKMFA7S6uRFXSDFfH4aT+4B4Y=. (1194 bytes)')
|
||||
t.equal(err['code'], 'EINTEGRITY')
|
||||
t.equal(err['resource'], path.resolve('tar.tgz'))
|
||||
t.equal(err['attempts'], 1)
|
||||
|
||||
t.end()
|
||||
).rejects.toThrow(
|
||||
new TarballIntegrityError({
|
||||
algorithm: 'sha512',
|
||||
expected: 'sha1-HssnaJydJVE+rbyZFKc/VAi+enY=',
|
||||
found: 'sha512-VuFL1iPaIxJK/k3gTxStIkc6+wSiDwlLdnCWNZyapsVLobu/0onvGOZolASZpfBFiDJYrOIGiDzgLIULTW61Vg== sha1-ACjKMFA7S6uRFXSDFfH4aT+4B4Y=',
|
||||
sri: '',
|
||||
url: path.join(storeDir, 'tar.tgz'),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test("don't fail when integrity check of local file succeeds", async (t) => {
|
||||
test("don't fail when integrity check of local file succeeds", async () => {
|
||||
process.chdir(tempy.directory())
|
||||
t.comment(`testing in ${process.cwd()}`)
|
||||
|
||||
const localTarballLocation = path.resolve('tar.tgz')
|
||||
await cpFile(
|
||||
@@ -209,14 +196,11 @@ test("don't fail when integrity check of local file succeeds", async (t) => {
|
||||
lockfileDir: process.cwd(),
|
||||
})
|
||||
|
||||
t.equal(typeof filesIndex['package.json'], 'object', 'files index returned')
|
||||
|
||||
t.end()
|
||||
expect(typeof filesIndex['package.json']).toBe('object')
|
||||
})
|
||||
|
||||
test("don't fail when fetching a local tarball in offline mode", async (t) => {
|
||||
test("don't fail when fetching a local tarball in offline mode", async () => {
|
||||
process.chdir(tempy.directory())
|
||||
t.comment(`testing in ${process.cwd()}`)
|
||||
|
||||
const tarballAbsoluteLocation = path.join(__dirname, 'tars', 'babel-helper-hoist-variables-7.0.0-alpha.10.tgz')
|
||||
const resolution = {
|
||||
@@ -236,14 +220,11 @@ test("don't fail when fetching a local tarball in offline mode", async (t) => {
|
||||
lockfileDir: process.cwd(),
|
||||
})
|
||||
|
||||
t.equal(typeof filesIndex['package.json'], 'object', 'files index returned')
|
||||
|
||||
t.end()
|
||||
expect(typeof filesIndex['package.json']).toBe('object')
|
||||
})
|
||||
|
||||
test('fail when trying to fetch a non-local tarball in offline mode', async (t) => {
|
||||
test('fail when trying to fetch a non-local tarball in offline mode', async () => {
|
||||
process.chdir(tempy.directory())
|
||||
t.comment(`testing in ${process.cwd()}`)
|
||||
|
||||
const tarballAbsoluteLocation = path.join(__dirname, 'tars', 'babel-helper-hoist-variables-7.0.0-alpha.10.tgz')
|
||||
const resolution = {
|
||||
@@ -251,30 +232,26 @@ test('fail when trying to fetch a non-local tarball in offline mode', async (t)
|
||||
tarball: `${registry}foo.tgz`,
|
||||
}
|
||||
|
||||
let err!: Error
|
||||
try {
|
||||
const fetch = createFetcher(fetchFromRegistry, getCredentials, {
|
||||
offline: true,
|
||||
retry: {
|
||||
maxTimeout: 100,
|
||||
minTimeout: 0,
|
||||
retries: 1,
|
||||
},
|
||||
})
|
||||
await fetch.tarball(cafs, resolution, {
|
||||
const fetch = createFetcher(fetchFromRegistry, getCredentials, {
|
||||
offline: true,
|
||||
retry: {
|
||||
maxTimeout: 100,
|
||||
minTimeout: 0,
|
||||
retries: 1,
|
||||
},
|
||||
})
|
||||
await expect(
|
||||
fetch.tarball(cafs, resolution, {
|
||||
lockfileDir: process.cwd(),
|
||||
})
|
||||
} catch (_err) {
|
||||
err = _err
|
||||
}
|
||||
|
||||
t.ok(err)
|
||||
t.equal(err['code'], 'ERR_PNPM_NO_OFFLINE_TARBALL')
|
||||
|
||||
t.end()
|
||||
).rejects.toThrow(
|
||||
new PnpmError('NO_OFFLINE_TARBALL',
|
||||
`A package is missing from the store but cannot download it in offline mode. \
|
||||
The missing package may be downloaded from ${resolution.tarball}.`)
|
||||
)
|
||||
})
|
||||
|
||||
test('retry on server error', async t => {
|
||||
test('retry on server error', async () => {
|
||||
const scope = nock(registry)
|
||||
.get('/foo.tgz')
|
||||
.reply(500)
|
||||
@@ -284,7 +261,6 @@ test('retry on server error', async t => {
|
||||
})
|
||||
|
||||
process.chdir(tempy.directory())
|
||||
t.comment(`testing in ${process.cwd()}`)
|
||||
|
||||
const resolution = {
|
||||
integrity: tarballIntegrity,
|
||||
@@ -295,47 +271,42 @@ test('retry on server error', async t => {
|
||||
lockfileDir: process.cwd(),
|
||||
})
|
||||
|
||||
t.ok(index)
|
||||
expect(index).toBeTruthy()
|
||||
|
||||
t.ok(scope.isDone())
|
||||
t.end()
|
||||
expect(scope.isDone()).toBeTruthy()
|
||||
})
|
||||
|
||||
test('throw error when accessing private package w/o authorization', async t => {
|
||||
test('throw error when accessing private package w/o authorization', async () => {
|
||||
const scope = nock(registry)
|
||||
.get('/foo.tgz')
|
||||
.reply(403)
|
||||
|
||||
process.chdir(tempy.directory())
|
||||
t.comment(`testing in ${process.cwd()}`)
|
||||
|
||||
const resolution = {
|
||||
integrity: tarballIntegrity,
|
||||
tarball: 'http://example.com/foo.tgz',
|
||||
}
|
||||
|
||||
let err!: Error
|
||||
|
||||
try {
|
||||
await fetch.tarball(cafs, resolution, {
|
||||
await expect(
|
||||
fetch.tarball(cafs, resolution, {
|
||||
lockfileDir: process.cwd(),
|
||||
})
|
||||
} catch (_err) {
|
||||
err = _err
|
||||
}
|
||||
|
||||
t.ok(err)
|
||||
err = err || new Error()
|
||||
t.equal(err.message, 'GET http://example.com/foo.tgz: Forbidden - 403')
|
||||
t.equal(err['hint'], 'No authorization header was set for the request.')
|
||||
t.equal(err['code'], 'ERR_PNPM_FETCH_403')
|
||||
t.equal(err['request']['url'], 'http://example.com/foo.tgz')
|
||||
|
||||
t.ok(scope.isDone())
|
||||
t.end()
|
||||
).rejects.toThrow(
|
||||
new FetchError(
|
||||
{
|
||||
url: resolution.tarball,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
statusText: 'Forbidden',
|
||||
}
|
||||
)
|
||||
)
|
||||
expect(scope.isDone()).toBeTruthy()
|
||||
})
|
||||
|
||||
test('accessing private packages', async t => {
|
||||
test('accessing private packages', async () => {
|
||||
const scope = nock(
|
||||
registry,
|
||||
{
|
||||
@@ -350,7 +321,6 @@ test('accessing private packages', async t => {
|
||||
})
|
||||
|
||||
process.chdir(tempy.directory())
|
||||
t.comment(`testing in ${process.cwd()}`)
|
||||
|
||||
const getCredentials = () => ({
|
||||
alwaysAuth: undefined,
|
||||
@@ -374,10 +344,9 @@ test('accessing private packages', async t => {
|
||||
lockfileDir: process.cwd(),
|
||||
})
|
||||
|
||||
t.ok(index)
|
||||
expect(index).toBeTruthy()
|
||||
|
||||
t.ok(scope.isDone())
|
||||
t.end()
|
||||
expect(scope.isDone()).toBeTruthy()
|
||||
})
|
||||
|
||||
async function getFileIntegrity (filename: string) {
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
/// <reference path="../../../typings/index.d.ts"/>
|
||||
import './download'
|
||||
Reference in New Issue
Block a user