Skip to content

Commit ca6fcfa

Browse files
authored
Merge pull request #8796 from nightscout/bf/activity-read-error
A failed storage read on GET /api/v1/activity or /api/v1/profile/current is answered with a 500 instead of ending the server (BF-155)
2 parents 25fc41d + e20b66b commit ca6fcfa

3 files changed

Lines changed: 93 additions & 5 deletions

File tree

‎lib/api/activity/index.js‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,14 @@ function configure(app, wares, ctx) {
2828
api.get('/activity', function(req, res) {
2929
var ifModifiedSince = req.get('If-Modified-Since');
3030
ctx.activity.list(req.query, function(err, results) {
31-
// This callback never checked err at all: on a failed read `results`
32-
// is null and the forEach below throws, so a refused filter surfaced
33-
// as an unhandled 500 from the express error handler.
34-
if (sendQueryValidationError(res, err)) return;
31+
// On a failed read `results` is null. This callback runs inside a
32+
// promise handler, so reading it would throw an unhandled rejection
33+
// and end the process: answer every error here instead.
34+
if (err) {
35+
if (sendQueryValidationError(res, err)) return;
36+
console.error('Error reading activity', err);
37+
return res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Internal Server Error');
38+
}
3539

3640
var d1 = null;
3741

‎lib/api/profile/index.js‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,14 @@ function configure (app, wares, ctx) {
6565
// List current active record (in current state LAST record is current active)
6666
api.get('/profile/current', function(req, res) {
6767
ctx.profile.last( function(err, records) {
68-
return res.json(records.length > 0 ? records[0] : null);
68+
// Runs inside a promise handler: reading a null `records` here would
69+
// end the process, so a failed read answers an error instead.
70+
if (err) {
71+
if (sendQueryValidationError(res, err)) return;
72+
console.error('Error reading the current profile', err);
73+
return res.sendJSONStatus(res, consts.HTTP_INTERNAL_ERROR, 'Internal Server Error');
74+
}
75+
return res.json(records && records.length > 0 ? records[0] : null);
6976
});
7077
});
7178

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
'use strict';
2+
3+
// A failed storage read reaches the API v1 read callbacks from a promise
4+
// handler (lib/storage/run-with-callback.js). A callback that reads the null
5+
// result there throws an unhandled rejection, which ends the process. These
6+
// tests fail the read the same way and check the request is answered and the
7+
// server keeps serving.
8+
9+
var request = require('supertest');
10+
require('should');
11+
var language = require('../lib/language')();
12+
var runWithCallback = require('../lib/storage/run-with-callback');
13+
14+
describe('API v1 reads when storage fails', function () {
15+
this.timeout(15000);
16+
var self = this;
17+
var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1';
18+
19+
before(function (done) {
20+
process.env.API_SECRET = 'this is my long pass phrase';
21+
self.env = require('../lib/server/env')();
22+
self.env.settings.authDefaultRoles = 'readable';
23+
self.env.settings.enable = ['careportal', 'api'];
24+
self.app = require('express')();
25+
self.app.enable('api');
26+
require('../lib/server/bootevent')(self.env, language).boot(function booted (ctx) {
27+
self.ctx = ctx;
28+
self.ctx.ddata = require('../lib/data/ddata')();
29+
self.app.use('/api', require('../lib/api/')(self.env, ctx));
30+
done();
31+
});
32+
});
33+
34+
// Stands in for a storage call whose promise rejects, delivered the way the
35+
// storage modules deliver it.
36+
function failingRead () {
37+
var fn = Array.prototype.slice.call(arguments).find(function (arg) { return typeof arg === 'function'; });
38+
return runWithCallback(function () { throw new Error('storage unavailable at db.internal:27017'); }, fn);
39+
}
40+
41+
function expectAnsweredAndAlive (path, done) {
42+
request(self.app)
43+
.get(path)
44+
.set('api-secret', known)
45+
.timeout(5000)
46+
.expect(500)
47+
.end(function (err, res) {
48+
if (err) return done(err);
49+
JSON.stringify(res.body).should.not.match(/db\.internal|27017|storage unavailable/);
50+
request(self.app)
51+
.get('/api/status.json')
52+
.timeout(5000)
53+
.expect(200)
54+
.end(done);
55+
});
56+
}
57+
58+
describe('GET /api/v1/activity', function () {
59+
var original;
60+
before(function () { original = self.ctx.activity.list; self.ctx.activity.list = failingRead; });
61+
after(function () { self.ctx.activity.list = original; });
62+
63+
it('answers 500 without the storage error text, and the server keeps serving', function (done) {
64+
expectAnsweredAndAlive('/api/activity/', done);
65+
});
66+
});
67+
68+
describe('GET /api/v1/profile/current', function () {
69+
var original;
70+
before(function () { original = self.ctx.profile.last; self.ctx.profile.last = failingRead; });
71+
after(function () { self.ctx.profile.last = original; });
72+
73+
it('answers 500 without the storage error text, and the server keeps serving', function (done) {
74+
expectAnsweredAndAlive('/api/profile/current', done);
75+
});
76+
});
77+
});

0 commit comments

Comments
 (0)