Skip to content

Commit

Permalink
fix: fixed retry status codes, added redis error support, updated dns…
Browse files Browse the repository at this point in the history
… error codes
  • Loading branch information
titanism committed Jul 1, 2022
1 parent 46c1e75 commit 9e117d9
Show file tree
Hide file tree
Showing 12 changed files with 126 additions and 7,433 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!.*.js
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* text=auto
* text=auto eol=lf
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: CI
on:
- push
- pull_request
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os:
- ubuntu-latest
node_version:
- 14
- 16
- 18
name: Node ${{ matrix.node_version }} on ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node_version }}
- name: Install dependencies
run: npm install
- name: Run tests
run: npm run test
24 changes: 10 additions & 14 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
# OS #
###################
.DS_Store
*.log
.idea
Thumbs.db
tmp
temp


# Node.js #
###################
node_modules
package-lock.json


# NYC #
###################
coverage
.nyc_output
locales/
package-lock.json
yarn.lock

Thumbs.db
tmp/
temp/
*.lcov
.env
4 changes: 2 additions & 2 deletions .husky/commit-msg
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

yarn commitlint --edit $1
npx --no-install commitlint --edit $1
6 changes: 3 additions & 3 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

yarn lint-staged && yarn test
npx --no-install lint-staged && npm test
2 changes: 1 addition & 1 deletion .npmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
package-lock=false
package-lock=false
7 changes: 0 additions & 7 deletions .travis.yml

This file was deleted.

5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# koa-better-error-handler

[![build status](https://img.shields.io/travis/ladjs/koa-better-error-handler.svg)](https://travis-ci.org/ladjs/koa-better-error-handler)
[![code coverage](https://img.shields.io/codecov/c/github/ladjs/koa-better-error-handler.svg)](https://codecov.io/gh/ladjs/koa-better-error-handler)
[![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)
[![code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo)
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
[![made with lass](https://img.shields.io/badge/made_with-lass-95CC28.svg)](https://lass.js.org)
Expand All @@ -24,6 +23,8 @@

## Features

* Detects Node.js DNS errors (e.g. `ETIMEOUT` and `EBADFAMILY`) and sends 408 Client Timeout error
* Detects Redis errors (e.g. ioredis' MaxRetriesPerRequestError) and sends 408 Client Timeout error
* Uses [Boom][boom] for making error messages beautiful (see [User Friendly Responses](#user-friendly-responses) below)
* Simply a better error handler (doesn't remove all headers [like the built-in one does][gh-issue])
* Doesn't make all status codes 500 ([like the built-in Koa error handler does][gh-500-issue])
Expand Down
121 changes: 66 additions & 55 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const fastSafeStringify = require('fast-safe-stringify');
const humanize = require('humanize-string');
const statuses = require('statuses');
const toIdentifier = require('toidentifier');
const { RedisError } = require('redis-errors');
const { convert } = require('html-to-text');

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

// NOTE: if you change this, be sure to sync in `forward-email`
// <https://github.com/nodejs/node/blob/08dd4b1723b20d56fbedf37d52e736fe09715f80/lib/dns.js#L296-L320>
const CODES_TO_RESPONSE_CODES = {
EADDRGETNETWORKPARAMS: 421,
EADDRINUSE: 421,
EAI_AGAIN: 421,
EBADFLAGS: 421,
EBADHINTS: 421,
ECANCELLED: 421,
ECONNREFUSED: 421,
ECONNRESET: 442,
EDESTRUCTION: 421,
EFORMERR: 421,
ELOADIPHLPAPI: 421,
ENETUNREACH: 421,
ENODATA: 421,
ENOMEM: 421,
ENOTFOUND: 421,
ENOTINITIALIZED: 421,
EPIPE: 421,
EREFUSED: 421,
ESERVFAIL: 421,
ETIMEOUT: 420
};

const RETRY_CODES = Object.keys(CODES_TO_RESPONSE_CODES);
const DNS_RETRY_CODES = new Set([
'EADDRGETNETWORKPARAMS',
'EBADFAMILY',
'EBADFLAGS',
'EBADHINTS',
'EBADNAME',
'EBADQUERY',
'EBADRESP',
'EBADSTR',
'ECANCELLED',
'ECONNREFUSED',
'EDESTRUCTION',
'EFILE',
'EFORMERR',
'ELOADIPHLPAPI',
'ENODATA',
'ENOMEM',
'ENONAME',
'ENOTFOUND',
'ENOTIMP',
'ENOTINITIALIZED',
'EOF',
'EREFUSED',
'ESERVFAIL',
'ETIMEOUT'
]);

const opts = {
encoding: 'utf8'
Expand All @@ -73,13 +75,19 @@ const passportLocalMongooseErrorNames = new Set([
'UserExistsError'
]);

const passportLocalMongooseTooManyRequests = new Set([
'AttemptTooSoonError',
'TooManyAttemptsError'
]);

//
// initialize try/catch error handling right away
// adapted from: https://github.com/koajs/onerror/blob/master/index.js
// https://github.com/koajs/examples/issues/20#issuecomment-31568401
//
// inspired by:
// https://goo.gl/62oU7P
// https://goo.gl/8Z7aMe
// https://github.com/koajs/koa/blob/9f80296fc49fa0c03db939e866215f3721fcbbc6/lib/context.js#L101-L139
//

function errorHandler(
cookiesKey = false,
Expand All @@ -105,34 +113,46 @@ function errorHandler(
return;
}

// translate messages
const translate = (message) =>
_isFunction(this.request.t) ? this.request.t(message) : message;

const logger = useCtxLogger && this.logger ? this.logger : _logger;

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

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

if (!type) {
logger.warn('invalid type, sending 406 error');
err.status = 406;
err.message = Boom.notAcceptable().output.payload;
err.message = translate(Boom.notAcceptable().output.payload);
}

// parse mongoose validation errors
err = parseValidationError(this, err);

// check if we threw just a status code in order to keep it simple
const val = Number.parseInt(err.message, 10);
if (_isNumber(val) && val >= 400)
if (_isNumber(val) && val >= 400 && val < 600) {
// check if we threw just a status code in order to keep it simple
err = Boom[camelCase(toIdentifier(statuses.message[val]))]();
err.message = translate(err.message);
} else if (err instanceof RedisError) {
// redis errors (e.g. ioredis' MaxRetriesPerRequestError)
err.status = 408;
err.message = translate(Boom.clientTimeout().output.payload);
} else {
// parse mongoose validation errors
err = parseValidationError(this, err, translate);
}

// TODO: mongodb errors that are not Mongoose ValidationError

// check if we have a boom error that specified
// a status code already for us (and then use it)
if (_isObject(err.output) && _isNumber(err.output.statusCode)) {
err.status = err.output.statusCode;
} else if (_isString(err.code) && RETRY_CODES.includes(err.code)) {
} else if (_isString(err.code) && DNS_RETRY_CODES.has(err.code)) {
// check if this was a DNS error and if so
// then set status code for retries appropriately
err.status = CODES_TO_RESPONSE_CODES[err.code];
err.status = 408;
err.message = translate(Boom.clientTimeout().output.payload);
}

if (!_isNumber(err.status)) err.status = 500;
Expand Down Expand Up @@ -169,13 +189,8 @@ function errorHandler(
// fix page title and description
if (!this.api) {
this.state.meta = this.state.meta || {};
if (!err.no_translate && _isFunction(this.request.t)) {
this.state.meta.title = this.request.t(this.body.error);
this.state.meta.description = this.request.t(err.message);
} else {
this.state.meta.title = this.body.error;
this.state.meta.description = err.message;
}
this.state.meta.title = this.body.error;
this.state.meta.description = err.message;
}

switch (type) {
Expand Down Expand Up @@ -295,21 +310,14 @@ function makeAPIFriendly(ctx, message) {
: message;
}

function parseValidationError(ctx, err) {
// translate messages
const translate = (message) =>
!err.no_translate && _isFunction(ctx.request.t)
? ctx.request.t(message)
: message;

function parseValidationError(ctx, err, translate) {
// passport-local-mongoose support
if (passportLocalMongooseErrorNames.has(err.name)) {
err.message = translate(err.message);
if (!err.no_translate) err.message = translate(err.message);
// this ensures the error shows up client-side
err.status = 400;
// 429 = too many requests
if (['AttemptTooSoonError', 'TooManyAttemptsError'].includes(err.name))
err.status = 429;
if (passportLocalMongooseTooManyRequests.has(err.name)) err.status = 429;
return err;
}

Expand All @@ -335,9 +343,12 @@ function parseValidationError(ctx, err) {
// loop over the errors object of the Validation Error
// with support for HTML error lists
if (_values(err.errors).length === 1) {
err.message = translate(_values(err.errors)[0].message);
err.message = _values(err.errors)[0].message;
if (!err.no_translate) err.message = translate(err.message);
} else {
const errors = _map(_map(_values(err.errors), 'message'), translate);
const errors = _map(_map(_values(err.errors), 'message'), (message) =>
err.no_translate ? message : translate(message)
);
err.message = makeAPIFriendly(
ctx,
`<ul class="text-left mb-0"><li>${errors.join('</li><li>')}</li></ul>`
Expand Down
28 changes: 13 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
],
"dependencies": {
"@hapi/boom": "^10.0.0",
"camelcase": "^6.3.0",
"camelcase": "6",
"capitalize": "^2.0.4",
"co": "^4.6.0",
"fast-safe-stringify": "^2.1.1",
Expand All @@ -45,17 +45,17 @@
"lodash.isstring": "^4.0.1",
"lodash.map": "^4.6.0",
"lodash.values": "^4.3.0",
"redis-errors": "^1.2.0",
"statuses": "^2.0.1",
"toidentifier": "^1.0.1"
},
"devDependencies": {
"@commitlint/cli": "^17.0.2",
"@commitlint/config-conventional": "^17.0.2",
"@commitlint/cli": "^17.0.3",
"@commitlint/config-conventional": "^17.0.3",
"@koa/router": "^10.1.1",
"ava": "^4.3.0",
"codecov": "^3.8.2",
"cross-env": "^7.0.3",
"eslint-config-xo-lass": "^1.0.6",
"eslint-config-xo-lass": "^2.0.1",
"fixpack": "^4.0.0",
"get-port": "5",
"husky": "^8.0.1",
Expand All @@ -66,14 +66,14 @@
"koa-convert": "^2.0.0",
"koa-generic-session": "^2.3.0",
"koa-redis": "^4.0.1",
"lint-staged": "^13.0.0",
"lint-staged": "^13.0.3",
"nyc": "^15.1.0",
"redis": "^4.1.0",
"remark-cli": "^10.0.1",
"remark-preset-github": "^4.0.2",
"redis": "^4.1.1",
"remark-cli": "^11.0.0",
"remark-preset-github": "^4.0.4",
"rimraf": "^3.0.2",
"supertest": "^6.2.3",
"xo": "^0.49.0"
"xo": "^0.50.0"
},
"engines": {
"node": ">= 14"
Expand Down Expand Up @@ -111,11 +111,9 @@
"main": "index.js",
"repository": "ladjs/koa-better-error-handler",
"scripts": {
"coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov",
"lint": "xo && remark . -qfo",
"precommit": "lint-staged && npm test",
"lint": "xo --fix && remark . -qfo && fixpack",
"prepare": "husky install",
"test": "npm run lint && npm run test-coverage",
"test-coverage": "cross-env NODE_ENV=test nyc ava"
"pretest": "npm run lint",
"test": "cross-env NODE_ENV=test nyc ava"
}
}
Loading

0 comments on commit 9e117d9

Please sign in to comment.