Skip to content

Commit

Permalink
Add option to limit the size of request bodies for the default body p…
Browse files Browse the repository at this point in the history
…arser
  • Loading branch information
nwoltman committed Jan 7, 2018
1 parent a94ffdc commit fabd2a0
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 14 deletions.
1 change: 1 addition & 0 deletions docs/Routes.md
Expand Up @@ -25,6 +25,7 @@ They need to be in
* `beforeHandler(request, reply, done)`: a [function](https://github.com/fastify/fastify/blob/master/docs/Hooks.md#before-handler) called just before the request handler, useful if you need to perform authentication at route level for example, it could also be and array of functions.
* `handler(request, reply)`: the function that will handle this request.
* `schemaCompiler(schema)`: the function that build the schema for the validations. See [here](https://github.com/fastify/fastify/blob/master/docs/Validation-and-Serialization.md#schema-compiler)
* `jsonBodyLimit`: prevents the default JSON body parser from parsing request bodies larger than this number of bytes. Must be an integer. You may also set this option globally when first creating the Fastify instance with `fastify(options)`. Defaults to `1000000` (1 MB).

`request` is defined in [Request](https://github.com/fastify/fastify/blob/master/docs/Request.md).

Expand Down
26 changes: 24 additions & 2 deletions fastify.js
Expand Up @@ -92,6 +92,16 @@ function build (options) {
server.on('clientError', handleClientError)
}

// JSON body limit option
if (options.jsonBodyLimit !== undefined) {
if (!Number.isInteger(options.jsonBodyLimit)) {
throw new TypeError(`'jsonBodyLimit' option must be an integer. Got: '${options.jsonBodyLimit}'`)
}
fastify._jsonBodyLimit = options.jsonBodyLimit
} else {
fastify._jsonBodyLimit = 1000 * 1000 // 1 MB
}

// shorthand methods
fastify.delete = _delete
fastify.get = _get
Expand Down Expand Up @@ -370,7 +380,8 @@ function build (options) {
schema: options.schema,
beforeHandler: options.beforeHandler,
config: options.config,
schemaCompiler: options.schemaCompiler
schemaCompiler: options.schemaCompiler,
jsonBodyLimit: options.jsonBodyLimit
})
}

Expand All @@ -394,6 +405,14 @@ function build (options) {
throw new Error(`Missing handler function for ${opts.method}:${opts.url} route.`)
}

var jsonBodyLimit = _fastify._jsonBodyLimit
if (opts.jsonBodyLimit !== undefined) {
if (!Number.isInteger(opts.jsonBodyLimit)) {
throw new TypeError(`'jsonBodyLimit' option must be an integer. Got: '${opts.jsonBodyLimit}'`)
}
jsonBodyLimit = opts.jsonBodyLimit
}

_fastify.after((notHandledErr, done) => {
const path = opts.url || opts.path
const prefix = _fastify._routePrefix
Expand All @@ -411,6 +430,7 @@ function build (options) {
config,
_fastify._errorHandler,
_fastify._middie,
jsonBodyLimit,
_fastify
)

Expand Down Expand Up @@ -463,7 +483,7 @@ function build (options) {
return _fastify
}

function Context (schema, handler, Reply, Request, contentTypeParser, config, errorHandler, middie, fastify) {
function Context (schema, handler, Reply, Request, contentTypeParser, config, errorHandler, middie, jsonBodyLimit, fastify) {
this.schema = schema
this.handler = handler
this.Reply = Reply
Expand All @@ -476,6 +496,7 @@ function build (options) {
this.config = config
this.errorHandler = errorHandler
this._middie = middie
this._jsonBodyLimit = jsonBodyLimit
this._fastify = fastify
}

Expand Down Expand Up @@ -614,6 +635,7 @@ function build (options) {
opts.config || {},
this._errorHandler,
this._middie,
this._jsonBodyLimit,
null
)

Expand Down
61 changes: 51 additions & 10 deletions lib/handleRequest.js
Expand Up @@ -23,7 +23,7 @@ function handleRequest (req, res, params, context) {
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
// application/json content type
if (contentType && contentType.indexOf('application/json') > -1) {
return jsonBody(request, reply)
return jsonBody(request, reply, context._jsonBodyLimit)
}

// custom parser for a given content type
Expand All @@ -42,7 +42,7 @@ function handleRequest (req, res, params, context) {

// application/json content type
if (contentType.indexOf('application/json') > -1) {
return jsonBody(request, reply)
return jsonBody(request, reply, context._jsonBodyLimit)
}
// custom parser for a given content type
if (context.contentTypeParser.fastHasHeader(contentType)) {
Expand All @@ -57,19 +57,60 @@ function handleRequest (req, res, params, context) {
return
}

function jsonBody (request, reply) {
var body = ''
var req = request.req
req.on('error', onError)
function jsonBody (request, reply, limit) {
const contentLength = Number.parseInt(request.headers['content-length'], 10)
if (contentLength > limit) {
reply.code(413).send(new Error('Request body is too large'))
return
}

const req = request.req
const chunks = []
var receivedLength = 0

req.on('data', onData)
req.on('end', onEnd)
function onError (err) {
reply.code(422).send(err)
req.on('error', onEnd)

function removeHandlers () {
req.removeListener('data', onData)
req.removeListener('end', onEnd)
req.removeListener('error', onEnd)
}

function onData (chunk) {
body += chunk
receivedLength += chunk.length

if (receivedLength > limit) {
removeHandlers()
reply.code(413).send(new Error('Request body is too large'))
return
}

chunks.push(chunk)
}
function onEnd () {

function onEnd (err) {
removeHandlers()

if (err !== undefined) {
reply.code(400).send(err)
return
}

if (!Number.isNaN(contentLength) && receivedLength !== contentLength) {
reply.code(400).send(new Error('Request body size did not match Content-Length'))
return
}

if (receivedLength === 0) { // Body is invalid JSON
reply.code(422).send(new Error('Unexpected end of JSON input'))
return
}

const body = chunks.length === 1
? chunks[0].toString()
: chunks.join('')
try {
request.body = JSON.parse(body)
} catch (err) {
Expand Down
46 changes: 46 additions & 0 deletions test/fastify-options.test.js
@@ -0,0 +1,46 @@
'use strict'

const Fastify = require('..')
const sget = require('simple-get').concat
const t = require('tap')
const test = t.test

test('jsonBodyLimit option', t => {
t.plan(5)

try {
Fastify({ jsonBodyLimit: 1.3 })
t.fail('option must be an integer')
} catch (err) {
t.ok(err)
}

try {
Fastify({ jsonBodyLimit: [] })
t.fail('option must be an integer')
} catch (err) {
t.ok(err)
}

const fastify = Fastify({ jsonBodyLimit: 1 })

fastify.post('/', (request, reply) => {
reply.send({error: 'handler should not be called'})
})

fastify.listen(0, function (err) {
t.error(err)
fastify.server.unref()

sget({
method: 'POST',
url: 'http://localhost:' + fastify.server.address().port,
headers: { 'Content-Type': 'application/json' },
body: [],
json: true
}, (err, response, body) => {
t.error(err)
t.strictEqual(response.statusCode, 413)
})
})
})
94 changes: 93 additions & 1 deletion test/helper.js
@@ -1,6 +1,7 @@
'use strict'

const sget = require('simple-get').concat
const stream = require('stream')

module.exports.payloadMethod = function (method, t) {
const test = t.test
Expand Down Expand Up @@ -60,6 +61,18 @@ module.exports.payloadMethod = function (method, t) {
}
})

test(`${upMethod} with jsonBodyLimit option`, t => {
t.plan(1)
try {
fastify[loMethod]('/with-limit', { jsonBodyLimit: 1 }, function (req, reply) {
reply.send(req.body)
})
t.pass()
} catch (e) {
t.fail()
}
})

fastify.listen(0, function (err) {
if (err) {
t.error(err)
Expand All @@ -83,6 +96,22 @@ module.exports.payloadMethod = function (method, t) {
})
})

test(`${upMethod} - correctly replies with very large body`, t => {
t.plan(3)

const largeString = 'world'.repeat(13200)
sget({
method: upMethod,
url: 'http://localhost:' + fastify.server.address().port,
body: { hello: largeString },
json: true
}, (err, response, body) => {
t.error(err)
t.strictEqual(response.statusCode, 200)
t.deepEqual(body, { hello: largeString })
})
})

test(`${upMethod} - correctly replies if the content type has the charset`, t => {
t.plan(3)
sget({
Expand Down Expand Up @@ -167,7 +196,8 @@ module.exports.payloadMethod = function (method, t) {
}

test(`${upMethod} returns 422 - Unprocessable Entity`, t => {
t.plan(2)
t.plan(4)

sget({
method: upMethod,
url: 'http://localhost:' + fastify.server.address().port,
Expand All @@ -180,6 +210,68 @@ module.exports.payloadMethod = function (method, t) {
t.error(err)
t.strictEqual(response.statusCode, 422)
})

sget({
method: upMethod,
url: 'http://localhost:' + fastify.server.address().port,
body: '',
headers: { 'Content-Type': 'application/json' },
timeout: 500
}, (err, response, body) => {
t.error(err)
t.strictEqual(response.statusCode, 422)
})
})

test(`${upMethod} returns 413 - Payload Too Large`, t => {
t.plan(6)

sget({
method: upMethod,
url: 'http://localhost:' + fastify.server.address().port,
headers: {
'Content-Type': 'application/json',
'Content-Length': '1000001'
}
}, (err, response, body) => {
t.error(err)
t.strictEqual(response.statusCode, 413)
})

var chunk = Buffer.allocUnsafe(1000 * 1000 + 1)
const largeStream = new stream.Readable({
read () {
this.push(chunk)
chunk = null
}
})
sget({
method: upMethod,
url: 'http://localhost:' + fastify.server.address().port,
headers: { 'Content-Type': 'application/json' },
body: largeStream,
timeout: 500
}, (err, response, body) => {
t.error(err)
if (upMethod === 'OPTIONS') {
// Node errors with a 400 Bad Request for OPTIONS requests
// with a stream body and no Content-Length header
t.strictEqual(response.statusCode, 400)
} else {
t.strictEqual(response.statusCode, 413)
}
})

sget({
method: upMethod,
url: `http://localhost:${fastify.server.address().port}/with-limit`,
headers: { 'Content-Type': 'application/json' },
body: {},
json: true
}, (err, response, body) => {
t.error(err)
t.strictEqual(response.statusCode, 413)
})
})
})
}
2 changes: 1 addition & 1 deletion test/internals/handleRequest.test.js
Expand Up @@ -105,7 +105,7 @@ test('jsonBody should be a function', t => {
t.plan(2)

t.is(typeof internals.jsonBody, 'function')
t.is(internals.jsonBody.length, 2)
t.is(internals.jsonBody.length, 3)
})

test('request should be defined in onSend Hook on post request with content type application/json', t => {
Expand Down
22 changes: 22 additions & 0 deletions test/route.test.js
Expand Up @@ -178,3 +178,25 @@ test('path can be specified in place of uri', t => {
t.deepEqual(JSON.parse(res.payload), { hello: 'world' })
})
})

test('invalid jsonBodyLimit option - route', t => {
t.plan(2)

try {
fastify.route({
jsonBodyLimit: false,
method: 'PUT',
handler: () => null
})
t.fail('jsonBodyLimit must be an integer')
} catch (err) {
t.ok(err)
}

try {
fastify.post('/url', { jsonBodyLimit: 10000.1 }, () => null)
t.fail('jsonBodyLimit must be an integer')
} catch (err) {
t.ok(err)
}
})

0 comments on commit fabd2a0

Please sign in to comment.