Skip to content

Commit 9e117d9

Browse files
committed
fix: fixed retry status codes, added redis error support, updated dns error codes
1 parent 46c1e75 commit 9e117d9

File tree

12 files changed

+126
-7433
lines changed

12 files changed

+126
-7433
lines changed

.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
!.*.js

.gitattributes

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
* text=auto
1+
* text=auto eol=lf

.github/workflows/ci.yml

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: CI
2+
on:
3+
- push
4+
- pull_request
5+
jobs:
6+
build:
7+
runs-on: ${{ matrix.os }}
8+
strategy:
9+
matrix:
10+
os:
11+
- ubuntu-latest
12+
node_version:
13+
- 14
14+
- 16
15+
- 18
16+
name: Node ${{ matrix.node_version }} on ${{ matrix.os }}
17+
steps:
18+
- uses: actions/checkout@v3
19+
- name: Setup node
20+
uses: actions/setup-node@v3
21+
with:
22+
node-version: ${{ matrix.node_version }}
23+
- name: Install dependencies
24+
run: npm install
25+
- name: Run tests
26+
run: npm run test

.gitignore

+10-14
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
1-
# OS #
2-
###################
31
.DS_Store
2+
*.log
43
.idea
5-
Thumbs.db
6-
tmp
7-
temp
8-
9-
10-
# Node.js #
11-
###################
124
node_modules
13-
package-lock.json
14-
15-
16-
# NYC #
17-
###################
185
coverage
196
.nyc_output
7+
locales/
8+
package-lock.json
9+
yarn.lock
10+
11+
Thumbs.db
12+
tmp/
13+
temp/
14+
*.lcov
15+
.env

.husky/commit-msg

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#!/usr/bin/env sh
1+
#!/bin/sh
22
. "$(dirname "$0")/_/husky.sh"
33

4-
yarn commitlint --edit $1
4+
npx --no-install commitlint --edit $1

.husky/pre-commit

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#!/usr/bin/env sh
2-
. "$(dirname -- "$0")/_/husky.sh"
1+
#!/bin/sh
2+
. "$(dirname "$0")/_/husky.sh"
33

4-
yarn lint-staged && yarn test
4+
npx --no-install lint-staged && npm test

.npmrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
package-lock=false
1+
package-lock=false

.travis.yml

-7
This file was deleted.

README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# koa-better-error-handler
22

3-
[![build status](https://img.shields.io/travis/ladjs/koa-better-error-handler.svg)](https://travis-ci.org/ladjs/koa-better-error-handler)
4-
[![code coverage](https://img.shields.io/codecov/c/github/ladjs/koa-better-error-handler.svg)](https://codecov.io/gh/ladjs/koa-better-error-handler)
3+
[![build status](https://github.com/ladjs/koa-better-error-handler/actions/workflows/ci.yml/badge.svg)](https://github.com/ladjs/koa-better-error-handler/actions/workflows/ci.yml)
54
[![code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo)
65
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
76
[![made with lass](https://img.shields.io/badge/made_with-lass-95CC28.svg)](https://lass.js.org)
@@ -24,6 +23,8 @@
2423

2524
## Features
2625

26+
* Detects Node.js DNS errors (e.g. `ETIMEOUT` and `EBADFAMILY`) and sends 408 Client Timeout error
27+
* Detects Redis errors (e.g. ioredis' MaxRetriesPerRequestError) and sends 408 Client Timeout error
2728
* Uses [Boom][boom] for making error messages beautiful (see [User Friendly Responses](#user-friendly-responses) below)
2829
* Simply a better error handler (doesn't remove all headers [like the built-in one does][gh-issue])
2930
* Doesn't make all status codes 500 ([like the built-in Koa error handler does][gh-500-issue])

index.js

+66-55
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const fastSafeStringify = require('fast-safe-stringify');
1111
const humanize = require('humanize-string');
1212
const statuses = require('statuses');
1313
const toIdentifier = require('toidentifier');
14+
const { RedisError } = require('redis-errors');
1415
const { convert } = require('html-to-text');
1516

1617
// lodash
@@ -22,32 +23,33 @@ const _isString = require('lodash.isstring');
2223
const _map = require('lodash.map');
2324
const _values = require('lodash.values');
2425

25-
// NOTE: if you change this, be sure to sync in `forward-email`
2626
// <https://github.com/nodejs/node/blob/08dd4b1723b20d56fbedf37d52e736fe09715f80/lib/dns.js#L296-L320>
27-
const CODES_TO_RESPONSE_CODES = {
28-
EADDRGETNETWORKPARAMS: 421,
29-
EADDRINUSE: 421,
30-
EAI_AGAIN: 421,
31-
EBADFLAGS: 421,
32-
EBADHINTS: 421,
33-
ECANCELLED: 421,
34-
ECONNREFUSED: 421,
35-
ECONNRESET: 442,
36-
EDESTRUCTION: 421,
37-
EFORMERR: 421,
38-
ELOADIPHLPAPI: 421,
39-
ENETUNREACH: 421,
40-
ENODATA: 421,
41-
ENOMEM: 421,
42-
ENOTFOUND: 421,
43-
ENOTINITIALIZED: 421,
44-
EPIPE: 421,
45-
EREFUSED: 421,
46-
ESERVFAIL: 421,
47-
ETIMEOUT: 420
48-
};
49-
50-
const RETRY_CODES = Object.keys(CODES_TO_RESPONSE_CODES);
27+
const DNS_RETRY_CODES = new Set([
28+
'EADDRGETNETWORKPARAMS',
29+
'EBADFAMILY',
30+
'EBADFLAGS',
31+
'EBADHINTS',
32+
'EBADNAME',
33+
'EBADQUERY',
34+
'EBADRESP',
35+
'EBADSTR',
36+
'ECANCELLED',
37+
'ECONNREFUSED',
38+
'EDESTRUCTION',
39+
'EFILE',
40+
'EFORMERR',
41+
'ELOADIPHLPAPI',
42+
'ENODATA',
43+
'ENOMEM',
44+
'ENONAME',
45+
'ENOTFOUND',
46+
'ENOTIMP',
47+
'ENOTINITIALIZED',
48+
'EOF',
49+
'EREFUSED',
50+
'ESERVFAIL',
51+
'ETIMEOUT'
52+
]);
5153

5254
const opts = {
5355
encoding: 'utf8'
@@ -73,13 +75,19 @@ const passportLocalMongooseErrorNames = new Set([
7375
'UserExistsError'
7476
]);
7577

78+
const passportLocalMongooseTooManyRequests = new Set([
79+
'AttemptTooSoonError',
80+
'TooManyAttemptsError'
81+
]);
82+
83+
//
7684
// initialize try/catch error handling right away
7785
// adapted from: https://github.com/koajs/onerror/blob/master/index.js
7886
// https://github.com/koajs/examples/issues/20#issuecomment-31568401
7987
//
8088
// inspired by:
81-
// https://goo.gl/62oU7P
82-
// https://goo.gl/8Z7aMe
89+
// https://github.com/koajs/koa/blob/9f80296fc49fa0c03db939e866215f3721fcbbc6/lib/context.js#L101-L139
90+
//
8391

8492
function errorHandler(
8593
cookiesKey = false,
@@ -105,34 +113,46 @@ function errorHandler(
105113
return;
106114
}
107115

116+
// translate messages
117+
const translate = (message) =>
118+
_isFunction(this.request.t) ? this.request.t(message) : message;
119+
108120
const logger = useCtxLogger && this.logger ? this.logger : _logger;
109121

110122
if (!_isError(err)) err = new Error(err);
111123

112124
const type = this.accepts(['text', 'json', 'html']);
113125

114126
if (!type) {
115-
logger.warn('invalid type, sending 406 error');
116127
err.status = 406;
117-
err.message = Boom.notAcceptable().output.payload;
128+
err.message = translate(Boom.notAcceptable().output.payload);
118129
}
119130

120-
// parse mongoose validation errors
121-
err = parseValidationError(this, err);
122-
123-
// check if we threw just a status code in order to keep it simple
124131
const val = Number.parseInt(err.message, 10);
125-
if (_isNumber(val) && val >= 400)
132+
if (_isNumber(val) && val >= 400 && val < 600) {
133+
// check if we threw just a status code in order to keep it simple
126134
err = Boom[camelCase(toIdentifier(statuses.message[val]))]();
135+
err.message = translate(err.message);
136+
} else if (err instanceof RedisError) {
137+
// redis errors (e.g. ioredis' MaxRetriesPerRequestError)
138+
err.status = 408;
139+
err.message = translate(Boom.clientTimeout().output.payload);
140+
} else {
141+
// parse mongoose validation errors
142+
err = parseValidationError(this, err, translate);
143+
}
144+
145+
// TODO: mongodb errors that are not Mongoose ValidationError
127146

128147
// check if we have a boom error that specified
129148
// a status code already for us (and then use it)
130149
if (_isObject(err.output) && _isNumber(err.output.statusCode)) {
131150
err.status = err.output.statusCode;
132-
} else if (_isString(err.code) && RETRY_CODES.includes(err.code)) {
151+
} else if (_isString(err.code) && DNS_RETRY_CODES.has(err.code)) {
133152
// check if this was a DNS error and if so
134153
// then set status code for retries appropriately
135-
err.status = CODES_TO_RESPONSE_CODES[err.code];
154+
err.status = 408;
155+
err.message = translate(Boom.clientTimeout().output.payload);
136156
}
137157

138158
if (!_isNumber(err.status)) err.status = 500;
@@ -169,13 +189,8 @@ function errorHandler(
169189
// fix page title and description
170190
if (!this.api) {
171191
this.state.meta = this.state.meta || {};
172-
if (!err.no_translate && _isFunction(this.request.t)) {
173-
this.state.meta.title = this.request.t(this.body.error);
174-
this.state.meta.description = this.request.t(err.message);
175-
} else {
176-
this.state.meta.title = this.body.error;
177-
this.state.meta.description = err.message;
178-
}
192+
this.state.meta.title = this.body.error;
193+
this.state.meta.description = err.message;
179194
}
180195

181196
switch (type) {
@@ -295,21 +310,14 @@ function makeAPIFriendly(ctx, message) {
295310
: message;
296311
}
297312

298-
function parseValidationError(ctx, err) {
299-
// translate messages
300-
const translate = (message) =>
301-
!err.no_translate && _isFunction(ctx.request.t)
302-
? ctx.request.t(message)
303-
: message;
304-
313+
function parseValidationError(ctx, err, translate) {
305314
// passport-local-mongoose support
306315
if (passportLocalMongooseErrorNames.has(err.name)) {
307-
err.message = translate(err.message);
316+
if (!err.no_translate) err.message = translate(err.message);
308317
// this ensures the error shows up client-side
309318
err.status = 400;
310319
// 429 = too many requests
311-
if (['AttemptTooSoonError', 'TooManyAttemptsError'].includes(err.name))
312-
err.status = 429;
320+
if (passportLocalMongooseTooManyRequests.has(err.name)) err.status = 429;
313321
return err;
314322
}
315323

@@ -335,9 +343,12 @@ function parseValidationError(ctx, err) {
335343
// loop over the errors object of the Validation Error
336344
// with support for HTML error lists
337345
if (_values(err.errors).length === 1) {
338-
err.message = translate(_values(err.errors)[0].message);
346+
err.message = _values(err.errors)[0].message;
347+
if (!err.no_translate) err.message = translate(err.message);
339348
} else {
340-
const errors = _map(_map(_values(err.errors), 'message'), translate);
349+
const errors = _map(_map(_values(err.errors), 'message'), (message) =>
350+
err.no_translate ? message : translate(message)
351+
);
341352
err.message = makeAPIFriendly(
342353
ctx,
343354
`<ul class="text-left mb-0"><li>${errors.join('</li><li>')}</li></ul>`

package.json

+13-15
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
],
3333
"dependencies": {
3434
"@hapi/boom": "^10.0.0",
35-
"camelcase": "^6.3.0",
35+
"camelcase": "6",
3636
"capitalize": "^2.0.4",
3737
"co": "^4.6.0",
3838
"fast-safe-stringify": "^2.1.1",
@@ -45,17 +45,17 @@
4545
"lodash.isstring": "^4.0.1",
4646
"lodash.map": "^4.6.0",
4747
"lodash.values": "^4.3.0",
48+
"redis-errors": "^1.2.0",
4849
"statuses": "^2.0.1",
4950
"toidentifier": "^1.0.1"
5051
},
5152
"devDependencies": {
52-
"@commitlint/cli": "^17.0.2",
53-
"@commitlint/config-conventional": "^17.0.2",
53+
"@commitlint/cli": "^17.0.3",
54+
"@commitlint/config-conventional": "^17.0.3",
5455
"@koa/router": "^10.1.1",
5556
"ava": "^4.3.0",
56-
"codecov": "^3.8.2",
5757
"cross-env": "^7.0.3",
58-
"eslint-config-xo-lass": "^1.0.6",
58+
"eslint-config-xo-lass": "^2.0.1",
5959
"fixpack": "^4.0.0",
6060
"get-port": "5",
6161
"husky": "^8.0.1",
@@ -66,14 +66,14 @@
6666
"koa-convert": "^2.0.0",
6767
"koa-generic-session": "^2.3.0",
6868
"koa-redis": "^4.0.1",
69-
"lint-staged": "^13.0.0",
69+
"lint-staged": "^13.0.3",
7070
"nyc": "^15.1.0",
71-
"redis": "^4.1.0",
72-
"remark-cli": "^10.0.1",
73-
"remark-preset-github": "^4.0.2",
71+
"redis": "^4.1.1",
72+
"remark-cli": "^11.0.0",
73+
"remark-preset-github": "^4.0.4",
7474
"rimraf": "^3.0.2",
7575
"supertest": "^6.2.3",
76-
"xo": "^0.49.0"
76+
"xo": "^0.50.0"
7777
},
7878
"engines": {
7979
"node": ">= 14"
@@ -111,11 +111,9 @@
111111
"main": "index.js",
112112
"repository": "ladjs/koa-better-error-handler",
113113
"scripts": {
114-
"coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov",
115-
"lint": "xo && remark . -qfo",
116-
"precommit": "lint-staged && npm test",
114+
"lint": "xo --fix && remark . -qfo && fixpack",
117115
"prepare": "husky install",
118-
"test": "npm run lint && npm run test-coverage",
119-
"test-coverage": "cross-env NODE_ENV=test nyc ava"
116+
"pretest": "npm run lint",
117+
"test": "cross-env NODE_ENV=test nyc ava"
120118
}
121119
}

0 commit comments

Comments
 (0)