Skip to content

Commit

Permalink
Include the "type" property on all generated errors
Browse files Browse the repository at this point in the history
closes #122
  • Loading branch information
dougwilson committed Sep 8, 2017
1 parent b8f97cd commit 0632e2f
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 26 deletions.
1 change: 1 addition & 0 deletions HISTORY.md
Expand Up @@ -3,6 +3,7 @@ unreleased

* Fix JSON strict violation error to match native parse error
* Include the `body` property on verify errors
* Include the `type` property on all generated errors
* Use `http-errors` to set status code on errors
* deps: bytes@3.0.0
* deps: debug@2.6.8
Expand Down
29 changes: 19 additions & 10 deletions README.md
Expand Up @@ -275,8 +275,9 @@ encoding of the request. The parsing can be aborted by throwing an error.
The middlewares provided by this module create errors depending on the error
condition during parsing. The errors will typically have a `status`/`statusCode`
property that contains the suggested HTTP response code, an `expose` property
to determine if the `message` property should be displayed to the client and a
`body` property containing the read body, if available.
to determine if the `message` property should be displayed to the client, a
`type` property to determine the type of error without matching against the
`message`, and a `body` property containing the read body, if available.

The following are the common errors emitted, though any error can come through
for various reasons.
Expand All @@ -285,54 +286,62 @@ for various reasons.

This error will occur when the request had a `Content-Encoding` header that
contained an encoding but the "inflation" option was set to `false`. The
`status` property is set to `415`.
`status` property is set to `415`, the `type` property is set to
`'encoding.unsupported'`, and the `charset` property will be set to the
encoding that is unsupported.

### request aborted

This error will occur when the request is aborted by the client before reading
the body has finished. The `received` property will be set to the number of
bytes received before the request was aborted and the `expected` property is
set to the number of expected bytes. The `status` property is set to `400`.
set to the number of expected bytes. The `status` property is set to `400`
and `type` property is set to `'request.aborted'`.

### request entity too large

This error will occur when the request body's size is larger than the "limit"
option. The `limit` property will be set to the byte limit and the `length`
property will be set to the request body's length. The `status` property is
set to `413`.
set to `413` and the `type` property is set to `'entity.too.large'`.

### request size did not match content length

This error will occur when the request's length did not match the length from
the `Content-Length` header. This typically occurs when the request is malformed,
typically when the `Content-Length` header was calculated based on characters
instead of bytes. The `status` property is set to `400`.
instead of bytes. The `status` property is set to `400` and the `type` property
is set to `'request.size.invalid'`.

### stream encoding should not be set

This error will occur when something called the `req.setEncoding` method prior
to this middleware. This module operates directly on bytes only and you cannot
call `req.setEncoding` when using this module. The `status` property is set to
`500`.
`500` and the `type` property is set to `'stream.encoding.set'`.

### too many parameters

This error will occur when the content of the request exceeds the configured
`parameterLimit` for the `urlencoded` parser. The `status` property is set to
`413`.
`413` and the `type` property is set to `'parameters.too.many'`.

### unsupported charset "BOGUS"

This error will occur when the request had a charset parameter in the
`Content-Type` header, but the `iconv-lite` module does not support it OR the
parser does not support it. The charset is contained in the message as well
as in the `charset` property. The `status` property is set to `415`.
as in the `charset` property. The `status` property is set to `415`, the
`type` property is set to `'charset.unsupported'`, and the `charset` property
is set to the charset that is unsupported.

### unsupported content encoding "bogus"

This error will occur when the request had a `Content-Encoding` header that
contained an unsupported encoding. The encoding is contained in the message
as well as in the `encoding` property. The `status` property is set to `415`.
as well as in the `encoding` property. The `status` property is set to `415`,
the `type` property is set to `'encoding.unsupported'`, and the `encoding`
property is set to the encoding that is unsupported.

## Examples

Expand Down
20 changes: 14 additions & 6 deletions lib/read.js
Expand Up @@ -67,7 +67,8 @@ function read (req, res, next, parse, debug, options) {
// assert charset is supported
if (opts.encoding === null && encoding !== null && !iconv.encodingExists(encoding)) {
return next(createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', {
charset: encoding.toLowerCase()
charset: encoding.toLowerCase(),
type: 'charset.unsupported'
}))
}

Expand All @@ -78,7 +79,8 @@ function read (req, res, next, parse, debug, options) {
// echo back charset
if (err.type === 'encoding.unsupported') {
err = createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', {
charset: encoding.toLowerCase()
charset: encoding.toLowerCase(),
type: 'charset.unsupported'
})
}

Expand All @@ -97,7 +99,8 @@ function read (req, res, next, parse, debug, options) {
verify(req, res, body, encoding)
} catch (err) {
next(createError(403, err, {
body: body
body: body,
type: err.type || 'entity.verify.failed'
}))
return
}
Expand All @@ -113,7 +116,8 @@ function read (req, res, next, parse, debug, options) {
req.body = parse(str)
} catch (err) {
next(createError(400, err, {
body: str
body: str,
type: err.type || 'entity.parse.failed'
}))
return
}
Expand All @@ -140,7 +144,10 @@ function contentstream (req, debug, inflate) {
debug('content-encoding "%s"', encoding)

if (inflate === false && encoding !== 'identity') {
throw createError(415, 'content encoding unsupported')
throw createError(415, 'content encoding unsupported', {
encoding: encoding,
type: 'encoding.unsupported'
})
}

switch (encoding) {
Expand All @@ -160,7 +167,8 @@ function contentstream (req, debug, inflate) {
break
default:
throw createError(415, 'unsupported content encoding "' + encoding + '"', {
encoding: encoding
encoding: encoding,
type: 'encoding.unsupported'
})
}

Expand Down
47 changes: 42 additions & 5 deletions lib/types/json.js
Expand Up @@ -84,8 +84,14 @@ function json (options) {
}
}

debug('parse json')
return JSON.parse(body, reviver)
try {
debug('parse json')
return JSON.parse(body, reviver)
} catch (e) {
throw normalizeJsonSyntaxError(e, {
stack: e.stack
})
}
}

return function jsonParser (req, res, next) {
Expand Down Expand Up @@ -118,7 +124,8 @@ function json (options) {
if (charset.substr(0, 4) !== 'utf-') {
debug('invalid charset')
next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', {
charset: charset
charset: charset,
type: 'charset.unsupported'
}))
return
}
Expand Down Expand Up @@ -149,8 +156,10 @@ function createStrictSyntaxError (str, char) {
try {
JSON.parse(partial); /* istanbul ignore next */ throw new SyntaxError('strict violation')
} catch (e) {
e.message = e.message.replace('#', char)
return e
return normalizeJsonSyntaxError(e, {
message: e.message.replace('#', char),
stack: e.stack
})
}
}

Expand Down Expand Up @@ -181,6 +190,34 @@ function getCharset (req) {
}
}

/**
* Normalize a SyntaxError for JSON.parse.
*
* @param {SyntaxError} error
* @param {object} obj
* @return {SyntaxError}
*/

function normalizeJsonSyntaxError (error, obj) {
var keys = Object.getOwnPropertyNames(error)

for (var i = 0; i < keys.length; i++) {
var key = keys[i]
if (key !== 'stack' && key !== 'message') {
delete error[key]
}
}

var props = Object.keys(obj)

for (var j = 0; j < props.length; j++) {
var prop = props[j]
error[prop] = obj[prop]
}

return error
}

/**
* Get the simple type checker.
*
Expand Down
11 changes: 8 additions & 3 deletions lib/types/urlencoded.js
Expand Up @@ -106,7 +106,8 @@ function urlencoded (options) {
if (charset !== 'utf-8') {
debug('invalid charset')
next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', {
charset: charset
charset: charset,
type: 'charset.unsupported'
}))
return
}
Expand Down Expand Up @@ -147,7 +148,9 @@ function extendedparser (options) {

if (paramCount === undefined) {
debug('too many parameters')
throw createError(413, 'too many parameters')
throw createError(413, 'too many parameters', {
type: 'parameters.too.many'
})
}

var arrayLimit = Math.max(100, paramCount)
Expand Down Expand Up @@ -257,7 +260,9 @@ function simpleparser (options) {

if (paramCount === undefined) {
debug('too many parameters')
throw createError(413, 'too many parameters')
throw createError(413, 'too many parameters', {
type: 'parameters.too.many'
})
}

debug('parse urlencoding')
Expand Down
75 changes: 75 additions & 0 deletions test/json.js
Expand Up @@ -90,6 +90,15 @@ describe('bodyParser.json()', function () {
.expect(400, parseError('{"user"'), done)
})

it('should error with type = "entity.parse.failed"', function (done) {
request(this.server)
.post('/')
.set('Content-Type', 'application/json')
.set('X-Error-Property', 'type')
.send(' {"user"')
.expect(400, 'entity.parse.failed', done)
})

it('should include original body on error object', function (done) {
request(this.server)
.post('/')
Expand All @@ -111,6 +120,17 @@ describe('bodyParser.json()', function () {
.expect(413, done)
})

it('should error with type = "entity.too.large"', function (done) {
var buf = Buffer.alloc(1024, '.')
request(createServer({ limit: '1kb' }))
.post('/')
.set('Content-Type', 'application/json')
.set('Content-Length', '1034')
.set('X-Error-Property', 'type')
.send(JSON.stringify({ str: buf.toString() }))
.expect(413, 'entity.too.large', done)
})

it('should 413 when over limit with chunked encoding', function (done) {
var buf = Buffer.alloc(1024, '.')
var server = createServer({ limit: '1kb' })
Expand Down Expand Up @@ -244,6 +264,15 @@ describe('bodyParser.json()', function () {
.send(' { "user": "tobi" }')
.expect(200, '{"user":"tobi"}', done)
})

it('should error with type = "entity.parse.failed"', function (done) {
request(this.server)
.post('/')
.set('Content-Type', 'application/json')
.set('X-Error-Property', 'type')
.send('true')
.expect(400, 'entity.parse.failed', done)
})
})
})

Expand Down Expand Up @@ -329,6 +358,19 @@ describe('bodyParser.json()', function () {
.expect(403, 'no arrays', done)
})

it('should error with type = "entity.verify.failed"', function (done) {
var server = createServer({verify: function (req, res, buf) {
if (buf[0] === 0x5b) throw new Error('no arrays')
}})

request(server)
.post('/')
.set('Content-Type', 'application/json')
.set('X-Error-Property', 'type')
.send('["tobi"]')
.expect(403, 'entity.verify.failed', done)
})

it('should allow custom codes', function (done) {
var server = createServer({verify: function (req, res, buf) {
if (buf[0] !== 0x5b) return
Expand All @@ -344,6 +386,22 @@ describe('bodyParser.json()', function () {
.expect(400, 'no arrays', done)
})

it('should allow custom type', function (done) {
var server = createServer({verify: function (req, res, buf) {
if (buf[0] !== 0x5b) return
var err = new Error('no arrays')
err.type = 'foo.bar'
throw err
}})

request(server)
.post('/')
.set('Content-Type', 'application/json')
.set('X-Error-Property', 'type')
.send('["tobi"]')
.expect(403, 'foo.bar', done)
})

it('should include original body on error object', function (done) {
var server = createServer({verify: function (req, res, buf) {
if (buf[0] === 0x5b) throw new Error('no arrays')
Expand Down Expand Up @@ -432,6 +490,14 @@ describe('bodyParser.json()', function () {
test.write(Buffer.from('7b226e616d65223a22cec5d4227d', 'hex'))
test.expect(415, 'unsupported charset "KOI8-R"', done)
})

it('should error with type = "charset.unsupported"', function (done) {
var test = request(this.server).post('/')
test.set('Content-Type', 'application/json; charset=koi8-r')
test.set('X-Error-Property', 'type')
test.write(Buffer.from('7b226e616d65223a22cec5d4227d', 'hex'))
test.expect(415, 'charset.unsupported', done)
})
})

describe('encoding', function () {
Expand Down Expand Up @@ -486,6 +552,15 @@ describe('bodyParser.json()', function () {
test.expect(415, 'unsupported content encoding "nulls"', done)
})

it('should error with type = "encoding.unsupported"', function (done) {
var test = request(this.server).post('/')
test.set('Content-Encoding', 'nulls')
test.set('Content-Type', 'application/json')
test.set('X-Error-Property', 'type')
test.write(Buffer.from('000000000000', 'hex'))
test.expect(415, 'encoding.unsupported', done)
})

it('should 400 on malformed encoding', function (done) {
var test = request(this.server).post('/')
test.set('Content-Encoding', 'gzip')
Expand Down

0 comments on commit 0632e2f

Please sign in to comment.