Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Develop #56

Draft
wants to merge 5 commits into
base: 2.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
unreleased
==========

* Add `message` option
* Add `stacktrace` option
* Add `text/plain` fallback response
* Ignore `err.status` with non-error code
* Remove `env` option; use `stacktrace` option instead
* Send complete HTML document

0.4.1 / 2015-12-02
==================

Expand Down
58 changes: 54 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,42 @@ be written out to the `res`.

When an error is written, the following information is added to the response:

* The `res.statusCode` is set from `err.status` (or `err.statusCode`).
* The `res.statusCode` is set from `err.status` (or `err.statusCode`) if the
value is 4xx or 5xx.
* The body will be the HTML of the status code message if `env` is
`'production'`, otherwise will be `err.stack`.

The final handler will also unpipe anything from `req` when it is invoked.

#### options.env
#### options.message

By default, the environment is determined by `NODE_ENV` variable, but it can be
overridden by this option.
Determine if the error message should be displayed. By default this is false unless
`stacktrace` is enabled. When true, the default behavior will be used to display
the message, which is to use `err.message` if there is a valid `err.status` property.

A function can also be provided to customize the message. The `err` value and outgoing
`status` code are passed and a message string is expected to be returned. If a falsy
value is returned, the response will act as if this option was the default.

```js
var done = finalhandler(req, res, {message: function (err, status) {
// only display message when err.public is true
return err.public ? err.message : undefined
}})
```

#### options.onerror

Provide a function to be called with the `err` when it exists. Can be used for
writing errors to a central location without excessive function generation. Called
as `onerror(err, req, res)`.

#### options.stacktrace

Specify if the stack trace of the error should be included in the response. By
default, this is false, but can be enabled when necessary (like in development).
It is not recommend to enable this on a production deployment

## Examples

### always 404
Expand Down Expand Up @@ -99,6 +118,37 @@ var server = http.createServer(function (req, res) {
server.listen(3000)
```

### display err.message to users

```js
var finalhandler = require('finalhandler')
var http = require('http')

var server = http.createServer(function (req, res) {
var done = finalhandler(req, res, {message: true})
var err = new Error('Please try again later')
err.status = 503
done(err)
})

server.listen(3000)
```

### display error stack trace for development debugging

```js
var finalhandler = require('finalhandler')
var http = require('http')

var server = http.createServer(function (req, res) {
var done = finalhandler(req, res, {stacktrace: true})
var err = new Error('oops')
done(err)
})

server.listen(3000)
```

### keep log of all errors

```js
Expand Down
208 changes: 185 additions & 23 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* @private
*/

var accepts = require('accepts')
var debug = require('debug')('finalhandler')
var escapeHtml = require('escape-html')
var http = require('http')
Expand Down Expand Up @@ -48,13 +49,25 @@ module.exports = finalhandler
function finalhandler (req, res, options) {
var opts = options || {}

// get environment
var env = opts.env || process.env.NODE_ENV || 'development'
// get message option
var message = opts.message === true
? getDefaultErrorMessage
: opts.message || false

if (typeof message !== 'boolean' && typeof message !== 'function') {
throw new TypeError('option message must be boolean or function')
}

// get error callback
var onerror = opts.onerror

// get stack trace option
var stacktrace = opts.stacktrace || false

return function (err) {
var body
var constructBody
var msg
var status = res.statusCode

// ignore 404 on in-flight response
Expand All @@ -65,31 +78,21 @@ function finalhandler (req, res, options) {

// unhandled error
if (err) {
// respect err.statusCode
if (err.statusCode) {
status = err.statusCode
}

// respect err.status
if (err.status) {
status = err.status
}
// respect status code from error
status = getErrorStatusCode(err) || status

// default status code to 500
if (!status || status < 400) {
status = 500
}

// production gets a basic error message
var msg = env === 'production'
? http.STATUS_CODES[status]
: err.stack || err.toString()
msg = escapeHtml(msg)
.replace(/\n/g, '<br>')
.replace(/\x20{2}/g, ' &nbsp;') + '\n'
// build a stack trace or normal message
msg = stacktrace
? getErrorStack(err, status, message)
: getErrorMessage(err, status, message)
} else {
status = 404
msg = 'Cannot ' + escapeHtml(req.method) + ' ' + escapeHtml(req.originalUrl || req.url) + '\n'
msg = 'Cannot ' + req.method + ' ' + (req.originalUrl || req.url)
}

debug('default %s', status)
Expand All @@ -104,17 +107,176 @@ function finalhandler (req, res, options) {
return req.socket.destroy()
}

send(req, res, status, msg)
// negotiate
var accept = accepts(req)
var type = accept.types('html', 'text')

// construct body
switch (type) {
case 'html':
constructBody = constructHtmlBody
break
default:
// default to plain text
constructBody = constructTextBody
break
}

// construct body
body = constructBody(status, msg)

// send response
send(req, res, status, body)
}
}

/**
* Get HTML body string
*
* @param {number} status
* @param {string} message
* @return {Buffer}
* @private
*/

function constructHtmlBody (status, message) {
var msg = escapeHtml(message)
.replace(/\n/g, '<br>')
.replace(/\x20{2}/g, ' &nbsp;')

var html = '<!doctype html>\n' +
'<html lang=en>\n' +
'<head>\n' +
'<meta charset=utf-8>\n' +
'<title>' + escapeHtml(http.STATUS_CODES[status]) + '</title>\n' +
'</head>\n' +
'<body>\n' +
msg + '\n' +
'</body>\n'

var body = new Buffer(html, 'utf8')

body.type = 'text/html; charset=utf-8'

return body
}

/**
* Get plain text body string
*
* @param {number} status
* @param {string} message
* @return {Buffer}
* @private
*/

function constructTextBody (status, message) {
var msg = message + '\n'
var body = new Buffer(msg, 'utf8')

body.type = 'text/plain; charset=utf-8'

return body
}

/**
* Get message from error
*
* @param {object} err
* @param {number} status
* @param {function} message
* @return {string}
* @private
*/

function getErrorMessage (err, status, message) {
var msg

if (message) {
msg = message(err, status)
}

return msg || http.STATUS_CODES[status]
}

/**
* Get default message from error
*
* @param {object} err
* @return {string}
* @private
*/

function getDefaultErrorMessage (err) {
return (err.status >= 400 && err.status < 600) || (err.statusCode >= 400 && err.statusCode < 600)
? err.message
: undefined
}

/**
* Get stack from error with custom message
*
* @param {object} err
* @param {number} status
* @param {function} message
* @return {string}
* @private
*/

function getErrorStack (err, status, message) {
var stack = err.stack || ''

if (message) {
var index = stack.indexOf('\n')
var msg = message(err, status) || err.message || String(err)
var name = err.name

// slice implicit message from top of stack
if (index !== -1) {
stack = stack.substr(index)
}

// prepend name and message to stack
stack = name
? name + ': ' + msg + stack
: msg + stack
} else if (!stack) {
// stringify error when no message generator and no stack
stack = String(err)
}

return stack
}

/**
* Get status code from an Error object.
*
* @param {object} err
* @return {number}
* @private
*/

function getErrorStatusCode (err) {
// check err.status
if (err.status >= 400 && err.status < 600) {
return err.status
}

// check err.statusCode
if (err.statusCode >= 400 && err.statusCode < 600) {
return err.statusCode
}

return undefined
}

/**
* Send response.
*
* @param {IncomingMessage} req
* @param {OutgoingMessage} res
* @param {number} status
* @param {string} body
* @param {Buffer} body
* @private
*/

Expand All @@ -126,8 +288,8 @@ function send (req, res, status, body) {
res.setHeader('X-Content-Type-Options', 'nosniff')

// standard headers
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'))
res.setHeader('Content-Type', body.type)
res.setHeader('Content-Length', body.length)

if (req.method === 'HEAD') {
res.end()
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"license": "MIT",
"repository": "pillarjs/finalhandler",
"dependencies": {
"accepts": "~1.2.11",
"debug": "~2.2.0",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
Expand Down
Loading