Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
unreleased
==========

* feat: add `text/plain` fallback response

v2.1.0 / 2025-03-05
==================

Expand Down
55 changes: 44 additions & 11 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 encodeUrl = require('encodeurl')
var escapeHtml = require('escape-html')
Expand All @@ -32,21 +33,40 @@ var isFinished = onFinished.isFinished
* @private
*/

function createHtmlDocument (message) {
var body = escapeHtml(message)
function createHtmlBody (message) {
var msg = escapeHtml(message)
.replaceAll('\n', '<br>')
.replaceAll(' ', ' &nbsp;')

return '<!DOCTYPE html>\n' +
var html = '<!DOCTYPE html>\n' +
'<html lang="en">\n' +
'<head>\n' +
'<meta charset="utf-8">\n' +
'<title>Error</title>\n' +
'</head>\n' +
'<body>\n' +
'<pre>' + body + '</pre>\n' +
'<pre>' + msg + '</pre>\n' +
'</body>\n' +
'</html>\n'

var body = Buffer.from(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 createTextBody (message) {
var body = Buffer.from(message + '\n', 'utf8')
body.type = 'text/plain; charset=utf-8'
return body
}

/**
Expand Down Expand Up @@ -79,6 +99,7 @@ function finalhandler (req, res, options) {
var headers
var msg
var status
var body

// ignore 404 on in-flight response
if (!err && res.headersSent) {
Expand Down Expand Up @@ -123,8 +144,23 @@ function finalhandler (req, res, options) {
return
}

// negotiate
var accept = accepts(req)
var type = accept.types('html')

// construct body
switch (type) {
case 'html':
body = createHtmlBody(msg)
break
default:
// default to plain text
body = createTextBody(msg)
break
}
Comment on lines +148 to +160
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this as a breaking change.


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

Expand Down Expand Up @@ -241,11 +277,8 @@ function getResponseStatusCode (res) {
* @private
*/

function send (req, res, status, headers, message) {
function send (req, res, status, headers, body) {
function write () {
// response body
var body = createHtmlDocument(message)

// response status
res.statusCode = status

Expand All @@ -268,8 +301,8 @@ function send (req, res, status, headers, message) {
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": "^2.0.0",
"debug": "^4.4.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
Expand Down
44 changes: 44 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,28 @@ var topDescribe = function (type, createServer) {
test.write(buf)
test.expect(404, done)
})

describe('when HTML acceptable', function () {
it('should respond with HTML', function (done) {
var server = createServer()
wrapper(request(server)
.get('/foo'))
.set('Accept', 'text/html')
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(404, /<html/, done)
})
})

describe('when HTML not acceptable', function () {
it('should respond with plain text', function (done) {
var server = createServer()
wrapper(request(server)
.get('/foo'))
.set('Accept', 'application/x-bogus')
.expect('Content-Type', 'text/plain; charset=utf-8')
.expect(404, 'Cannot GET /foo\n', done)
})
})
})

describe('error response', function () {
Expand Down Expand Up @@ -395,6 +417,28 @@ var topDescribe = function (type, createServer) {
})
})

describe('when HTML acceptable', function () {
it('should respond with HTML', function (done) {
var server = createServer(createError('boom!'))
wrapper(request(server)
.get('/foo'))
.set('Accept', 'text/html')
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(500, /<html/, done)
})
})

describe('when HTML not acceptable', function () {
it('should respond with plain text', function (done) {
var server = createServer(createError('boom!'), { env: 'production' })
wrapper(request(server)
.get('/foo'))
.set('Accept', 'application/x-bogus')
.expect('Content-Type', 'text/plain; charset=utf-8')
.expect(500, 'Internal Server Error\n', done)
})
})

describe('when res.statusCode set', function () {
it('should keep when >= 400', function (done) {
var server = createServer(function (req, res) {
Expand Down