Skip to content

Commit 15d04fa

Browse files
committed
feat(rest): Improve http error handling
Error are now sent back as json object per HttpError structure Error properties are controlled by `expose` Only log http 5xx errors to console
1 parent 7d15bfe commit 15d04fa

File tree

6 files changed

+120
-16
lines changed

6 files changed

+120
-16
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"build:current": "lerna run --loglevel=silent build:current",
4343
"pretest": "npm run build:current",
4444
"test": "node packages/build/bin/run-nyc npm run mocha",
45-
"mocha": "node packages/build/bin/select-dist mocha --exit --opts test/mocha.opts \"packages/*/DIST/test/**/*.js\"",
45+
"mocha": "node packages/build/bin/select-dist mocha --opts test/mocha.opts \"packages/*/DIST/test/**/*.js\"",
4646
"posttest": "npm run lint"
4747
},
4848
"config": {

packages/rest/src/providers/log-error-provider.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import {ServerRequest} from '../';
99
export class LogErrorProvider implements Provider<BoundValue> {
1010
value() {
1111
return (err: Error, statusCode: number, req: ServerRequest) => {
12-
console.error(
13-
'Unhandled error in %s %s: %s %s',
14-
req.method,
15-
req.url,
16-
statusCode,
17-
err.stack || err,
18-
);
12+
if (statusCode >= 500) {
13+
console.error(
14+
'Unhandled error in %s %s: %s %s',
15+
req.method,
16+
req.url,
17+
statusCode,
18+
err.stack || err,
19+
);
20+
}
1921
};
2022
}
2123
}

packages/rest/src/providers/reject.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {LogError, Reject} from '../internal-types';
77
import {inject} from '@loopback/context';
88
import {ServerResponse, ServerRequest} from 'http';
99
import {HttpError} from 'http-errors';
10+
import {writeErrorToResponse} from '../writer';
1011
import {RestBindings} from '../keys';
1112

1213
export class RejectProvider {
@@ -17,11 +18,9 @@ export class RejectProvider {
1718

1819
value(): Reject {
1920
return (response: ServerResponse, request: ServerRequest, error: Error) => {
20-
const err = error as HttpError;
21+
const err = <HttpError>error;
2122
const statusCode = err.statusCode || err.status || 500;
22-
response.statusCode = statusCode;
23-
response.end();
24-
23+
writeErrorToResponse(response, err);
2524
this.logError(error, statusCode, request);
2625
};
2726
}

packages/rest/src/writer.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import {ServerResponse as Response} from 'http';
77
import {OperationRetval} from './internal-types';
8+
import {HttpError} from 'http-errors';
89
/**
910
* Writes the result from Application controller method
1011
* into the HTTP response
@@ -36,3 +37,39 @@ export function writeResultToResponse(
3637
}
3738
response.end();
3839
}
40+
41+
/**
42+
* Write an error into the HTTP response
43+
* @param response HTTP response
44+
* @param error Error
45+
*/
46+
export function writeErrorToResponse(response: Response, error: Error) {
47+
const e = <HttpError>error;
48+
const statusCode = (response.statusCode = e.statusCode || e.status || 500);
49+
if (e.headers) {
50+
// Set response headers for the error
51+
for (const h in e.headers) {
52+
response.setHeader(h, e.headers[h]);
53+
}
54+
}
55+
// Build an error object
56+
const errObj: Partial<HttpError> = {
57+
statusCode,
58+
};
59+
if (e.expose) {
60+
// Expose other properties if the `expose` flag is set to `true`
61+
for (const p in e) {
62+
if (
63+
p === 'headers' ||
64+
p === 'expose' ||
65+
p === 'status' ||
66+
p === 'statusCode'
67+
)
68+
continue;
69+
errObj[p] = e[p];
70+
}
71+
}
72+
response.setHeader('Content-Type', 'application/json');
73+
response.write(JSON.stringify(errObj));
74+
response.end();
75+
}

packages/rest/test/integration/http-handler.integration.ts

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ import {
1111
ControllerSpec,
1212
parseOperationArgs,
1313
RestBindings,
14+
get,
1415
} from '../..';
1516
import {Context} from '@loopback/context';
1617
import {Client, createClientForHandler} from '@loopback/testlab';
18+
import * as HttpErrors from 'http-errors';
1719
import {ParameterObject} from '@loopback/openapi-spec';
1820
import {anOpenApiSpec, anOperationSpec} from '@loopback/openapi-spec-builder';
1921
import {
@@ -308,7 +310,11 @@ describe('HttpHandler', () => {
308310
return client
309311
.post('/show-body')
310312
.send('key=value')
311-
.expect(415);
313+
.expect(415, {
314+
message:
315+
'Content-type application/x-www-form-urlencoded is not supported.',
316+
statusCode: 415,
317+
});
312318
});
313319

314320
it('returns 400 for malformed JSON body', () => {
@@ -317,7 +323,7 @@ describe('HttpHandler', () => {
317323
.post('/show-body')
318324
.set('content-type', 'application/json')
319325
.send('malformed-json')
320-
.expect(400);
326+
.expect(400, {statusCode: 400});
321327
});
322328

323329
function givenBodyParamController() {
@@ -396,7 +402,9 @@ describe('HttpHandler', () => {
396402
givenControllerClass(ThrowingController, spec);
397403

398404
logErrorsExcept(500);
399-
return client.get('/hello').expect(500);
405+
return client.get('/hello').expect(500, {
406+
statusCode: 500,
407+
});
400408
});
401409

402410
it('handles invocation of an unknown method', async () => {
@@ -413,7 +421,64 @@ describe('HttpHandler', () => {
413421
givenControllerClass(TestController, spec);
414422
logErrorsExcept(404);
415423

416-
await client.get('/hello').expect(404);
424+
await client.get('/hello').expect(404, {
425+
message: 'Controller method not found: TestController.unknownMethod',
426+
statusCode: 404,
427+
});
428+
});
429+
430+
it('handles errors thrown from the method', async () => {
431+
const spec = anOpenApiSpec()
432+
.withOperation(
433+
'get',
434+
'/hello',
435+
anOperationSpec().withOperationName('hello'),
436+
)
437+
.build();
438+
439+
class TestController {
440+
@get('/hello')
441+
hello() {
442+
const err = new HttpErrors.BadRequest('Bad hello');
443+
err.headers = {'X-BAD-REQ': 'hello'};
444+
throw err;
445+
}
446+
}
447+
448+
givenControllerClass(TestController, spec);
449+
logErrorsExcept(400);
450+
451+
await client
452+
.get('/hello')
453+
.expect('X-BAD-REQ', 'hello')
454+
.expect(400, {
455+
message: 'Bad hello',
456+
statusCode: 400,
457+
});
458+
});
459+
460+
it('handles 500 error thrown from the method', async () => {
461+
const spec = anOpenApiSpec()
462+
.withOperation(
463+
'get',
464+
'/hello',
465+
anOperationSpec().withOperationName('hello'),
466+
)
467+
.build();
468+
469+
class TestController {
470+
@get('/hello')
471+
hello() {
472+
throw new HttpErrors.InternalServerError('Bad hello');
473+
}
474+
}
475+
476+
givenControllerClass(TestController, spec);
477+
logErrorsExcept(400);
478+
479+
await client.get('/hello').expect(500, {
480+
statusCode: 500,
481+
});
417482
});
418483
});
419484

test/mocha.opts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
--recursive
2+
--exit

0 commit comments

Comments
 (0)