1
0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-11-08 23:25:51 +01:00

fix(api): return 503 to npm/yarn on uplink connection timeout (#1331)

fix  #1328 and #720

Type: bug

The following has been addressed in the PR:

Instead of returning a 404 (Not Found) when npm, yarn, etc requests a package and the package cannot be acquired from an uplink due to a connection timeout, socket timeout, or connection reset problem, a 503 (service unavailable) is returned by Verdaccio instead. In limited testing of a few versions of npm and yarn, both of these clients correctly attempt to retry the request when a 503 is returned.

Added functional tests to verify the behavior (this adds a dev dependency on nock, which provides HTTP request mocking

Description:

This resolves issue #1328 and #720, and ensures npm/yarn install commands don't fail immediately when there is an intermittent network timeout problem with an uplink. Instead Verdaccio will appropriately respond to the client with a 503. A 404 response (current behavior) incorrectly tells the client that the package does not exist (which may or may not be true) and to not try again.
This commit is contained in:
Will Smythe 2019-06-13 15:42:01 -04:00 committed by Juan Picado @jotadeveloper
parent f242d1b261
commit eb7a8e3528
8 changed files with 94 additions and 7 deletions

@ -73,6 +73,7 @@
"jest": "24.7.1", "jest": "24.7.1",
"jest-environment-node": "24.7.1", "jest-environment-node": "24.7.1",
"lint-staged": "8.1.5", "lint-staged": "8.1.5",
"nock": "10.0.6",
"prettier": "1.17.0", "prettier": "1.17.0",
"puppeteer": "1.8.0", "puppeteer": "1.8.0",
"rimraf": "2.6.3", "rimraf": "2.6.3",

@ -411,13 +411,13 @@ class Storage implements IStorageHandler {
returns callback(err, result, uplink_errors) returns callback(err, result, uplink_errors)
*/ */
_syncUplinksMetadata(name: string, packageInfo: Package, options: ISyncUplinks, callback: Callback): void { _syncUplinksMetadata(name: string, packageInfo: Package, options: ISyncUplinks, callback: Callback): void {
let exists = true; let found = true;
const self = this; const self = this;
const upLinks = []; const upLinks = [];
const hasToLookIntoUplinks = _.isNil(options.uplinksLook) || options.uplinksLook; const hasToLookIntoUplinks = _.isNil(options.uplinksLook) || options.uplinksLook;
if (!packageInfo) { if (!packageInfo) {
exists = false; found = false;
packageInfo = generatePackageTemplate(name); packageInfo = generatePackageTemplate(name);
} }
@ -489,14 +489,36 @@ class Storage implements IStorageHandler {
// if we got to this point, assume that the correct package exists // if we got to this point, assume that the correct package exists
// on the uplink // on the uplink
exists = true; found = true;
cb(); cb();
}); });
}, },
(err: Error, upLinksErrors: any) => { (err: Error, upLinksErrors: any) => {
assert(!err && Array.isArray(upLinksErrors)); assert(!err && Array.isArray(upLinksErrors));
if (!exists) {
return callback(ErrorCode.getNotFound(API_ERROR.NO_PACKAGE), null, upLinksErrors); // Check for connection timeout or reset errors with uplink(s)
// (these should be handled differently from the package not being found)
if (!found) {
let uplinkTimeoutError;
for (let i = 0; i < upLinksErrors.length; i++) {
if (upLinksErrors[i]) {
for (let j = 0; j < upLinksErrors[i].length; j++) {
if (upLinksErrors[i][j]) {
const code = upLinksErrors[i][j].code;
if (code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT' || code === 'ECONNRESET') {
uplinkTimeoutError = true;
break;
}
}
}
}
}
if (uplinkTimeoutError) {
return callback(ErrorCode.getServiceUnavailable(), null, upLinksErrors);
} else {
return callback(ErrorCode.getNotFound(API_ERROR.NO_PACKAGE), null, upLinksErrors);
}
} }
if (upLinks.length === 0) { if (upLinks.length === 0) {

@ -0,0 +1,21 @@
const nock = require('nock');
function Plugin(config) {
const self = Object.create(Plugin.prototype);
self._config = config;
return self;
}
Plugin.prototype.register_middlewares = function (app, auth, storage) {
app.get('/test-uplink-timeout-*', function (req, res, next) {
nock('http://localhost:55552')
.get(req.path)
.socketDelay(31000).reply(200); // 31s is greater than the default 30s connection timeout
next();
});
}
module.exports = Plugin;

@ -28,6 +28,7 @@ import race from './performance/race';
import pluginsAuth from './plugins/auth'; import pluginsAuth from './plugins/auth';
import middleware from './plugins/middleware'; import middleware from './plugins/middleware';
import upLinkCache from './uplinks/cache'; import upLinkCache from './uplinks/cache';
import uplinkTimeout from './uplinks/timeout';
describe('functional test verdaccio', function() { describe('functional test verdaccio', function() {
jest.setTimeout(10000); jest.setTimeout(10000);
@ -55,6 +56,7 @@ describe('functional test verdaccio', function() {
addtag(server1); addtag(server1);
pluginsAuth(server2); pluginsAuth(server2);
notify(app); notify(app);
uplinkTimeout(server1, server2, server3);
// requires packages published to server1/server2 // requires packages published to server1/server2
upLinkCache(server1, server2, server3); upLinkCache(server1, server2, server3);
adduser(server1); adduser(server1);

@ -6,6 +6,10 @@ web:
enable: true enable: true
title: verdaccio-server-1 title: verdaccio-server-1
middlewares:
../fixtures/plugins/middlewares.uplink:
message: provides uplink mocking (e.g. simulates socket timeout)
auth: auth:
auth-memory: auth-memory:
users: users:
@ -116,6 +120,12 @@ packages:
publish: $authenticated publish: $authenticated
storage: sub_storage storage: sub_storage
'test-uplink-timeout-*':
access: $all
proxy:
- server2
- server3
'*': '*':
access: test $anonymous access: test $anonymous
publish: test $anonymous publish: test $anonymous

@ -35,6 +35,10 @@ packages:
access: $all access: $all
proxy: server2 proxy: server2
'test-uplink-timeout-*':
access: $all
publish: $all
'*': '*':
access: $all access: $all

@ -0,0 +1,27 @@
import {HTTP_STATUS} from "../../../src/lib/constants";
const PKG_SINGLE_UPLINK = 'test-uplink-timeout-single';
const PKG_MULTIPLE_UPLINKS = 'test-uplink-timeout-multiple';
export default function (server, server2, server3) {
describe('uplink connection timeouts', () => {
beforeAll(async () => {
await server2.addPackage(PKG_SINGLE_UPLINK).status(HTTP_STATUS.CREATED);
await server2.addPackage(PKG_MULTIPLE_UPLINKS).status(HTTP_STATUS.CREATED);
await server3.addPackage(PKG_MULTIPLE_UPLINKS).status(HTTP_STATUS.CREATED);
});
describe('get package', () => {
test('503 response when uplink connection ESOCKETTIMEDOUT', () => {
return server.getPackage(PKG_SINGLE_UPLINK).status(HTTP_STATUS.SERVICE_UNAVAILABLE);
});
test('200 response even though one uplink timesout', () => {
return server.getPackage(PKG_MULTIPLE_UPLINKS).status(HTTP_STATUS.OK)
});
});
});
}

BIN
yarn.lock

Binary file not shown.