├── .gitignore ├── .eslintrc ├── index.js ├── lib ├── utils.js ├── text.js ├── form.js ├── any.js └── json.js ├── .github └── workflows │ └── node.yml ├── package.json ├── LICENSE.txt ├── test ├── text.test.js ├── form.test.js ├── json.test.js └── any.test.js ├── Readme.md └── History.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | *.log 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-egg", 3 | "rules": { 4 | "no-bitwise": "off", 5 | "no-control-regex": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports = module.exports = require('./lib/any'); 4 | exports.json = require('./lib/json'); 5 | exports.form = require('./lib/form'); 6 | exports.text = require('./lib/text'); 7 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | exports.clone = function(opts) { 8 | const options = {}; 9 | opts = opts || {}; 10 | for (const key in opts) { 11 | options[key] = opts[key]; 12 | } 13 | return options; 14 | }; 15 | -------------------------------------------------------------------------------- /.github/workflows/node.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | Job: 11 | name: Node.js 12 | uses: node-modules/github-actions/.github/workflows/node-test.yml@master 13 | with: 14 | os: 'ubuntu-latest' 15 | version: '8, 10, 12, 14, 16, 18, 20, 22' 16 | secrets: 17 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "co-body", 3 | "version": "6.2.0", 4 | "repository": "cojs/co-body", 5 | "description": "request body parsing for co", 6 | "keywords": [ 7 | "request", 8 | "parse", 9 | "parser", 10 | "json", 11 | "co", 12 | "generators", 13 | "urlencoded" 14 | ], 15 | "dependencies": { 16 | "@hapi/bourne": "^3.0.0", 17 | "inflation": "^2.0.0", 18 | "qs": "^6.5.2", 19 | "raw-body": "^2.3.3", 20 | "type-is": "^1.6.16" 21 | }, 22 | "devDependencies": { 23 | "egg-bin": "^4.7.0", 24 | "eslint": "^4.19.1", 25 | "eslint-config-egg": "^7.0.0", 26 | "koa": "^1.6.0", 27 | "safe-qs": "^6.0.1", 28 | "should": "^11.2.0", 29 | "supertest": "^3.1.0" 30 | }, 31 | "license": "MIT", 32 | "scripts": { 33 | "lint": "eslint .", 34 | "test": "egg-bin test -r should", 35 | "cov": "eslint . && egg-bin cov -r should", 36 | "ci": "npm run lint && npm run cov" 37 | }, 38 | "files": [ 39 | "index.js", 40 | "lib/" 41 | ], 42 | "engines": { 43 | "node": ">=8.0.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/text.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | const raw = require('raw-body'); 8 | const inflate = require('inflation'); 9 | const utils = require('./utils'); 10 | 11 | /** 12 | * Return a Promise which parses text/plain requests. 13 | * 14 | * Pass a node request or an object with `.req`, 15 | * such as a koa Context. 16 | * 17 | * @param {Request} req 18 | * @param {Options} [opts] 19 | * @return {Function} 20 | * @api public 21 | */ 22 | 23 | module.exports = async function(req, opts) { 24 | req = req.req || req; 25 | opts = utils.clone(opts); 26 | 27 | // defaults 28 | const len = req.headers['content-length']; 29 | const encoding = req.headers['content-encoding'] || 'identity'; 30 | if (len && encoding === 'identity') opts.length = ~~len; 31 | opts.encoding = opts.encoding === undefined ? 'utf8' : opts.encoding; 32 | opts.limit = opts.limit || '1mb'; 33 | 34 | const str = await raw(inflate(req), opts); 35 | // ensure return the same format with json / form 36 | return opts.returnRawBody ? { parsed: str, raw: str } : str; 37 | }; 38 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2013-present cojs and the contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/form.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | const raw = require('raw-body'); 8 | const inflate = require('inflation'); 9 | const qs = require('qs'); 10 | const utils = require('./utils'); 11 | 12 | /** 13 | * Return a Promise which parses x-www-form-urlencoded requests. 14 | * 15 | * Pass a node request or an object with `.req`, 16 | * such as a koa Context. 17 | * 18 | * @param {Request} req 19 | * @param {Options} [opts] 20 | * @return {Function} 21 | * @api public 22 | */ 23 | 24 | module.exports = async function(req, opts) { 25 | req = req.req || req; 26 | opts = utils.clone(opts); 27 | const queryString = opts.queryString || {}; 28 | 29 | // keep compatibility with qs@4 30 | if (queryString.allowDots === undefined) queryString.allowDots = true; 31 | 32 | // defaults 33 | const len = req.headers['content-length']; 34 | const encoding = req.headers['content-encoding'] || 'identity'; 35 | if (len && encoding === 'identity') opts.length = ~~len; 36 | opts.encoding = opts.encoding || 'utf8'; 37 | opts.limit = opts.limit || '56kb'; 38 | opts.qs = opts.qs || qs; 39 | 40 | const str = await raw(inflate(req), opts); 41 | try { 42 | const parsed = opts.qs.parse(str, queryString); 43 | return opts.returnRawBody ? { parsed, raw: str } : parsed; 44 | } catch (err) { 45 | err.status = 400; 46 | err.body = str; 47 | throw err; 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /lib/any.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | const typeis = require('type-is'); 8 | const json = require('./json'); 9 | const form = require('./form'); 10 | const text = require('./text'); 11 | 12 | const jsonTypes = [ 'json', 'application/*+json', 'application/csp-report' ]; 13 | const formTypes = [ 'urlencoded' ]; 14 | const textTypes = [ 'text' ]; 15 | 16 | /** 17 | * Return a Promise which parses form and json requests 18 | * depending on the Content-Type. 19 | * 20 | * Pass a node request or an object with `.req`, 21 | * such as a koa Context. 22 | * 23 | * @param {Request} req 24 | * @param {Options} [opts] 25 | * @return {Function} 26 | * @api public 27 | */ 28 | 29 | module.exports = async function(req, opts) { 30 | req = req.req || req; 31 | opts = opts || {}; 32 | 33 | // json 34 | const jsonType = opts.jsonTypes || jsonTypes; 35 | if (typeis(req, jsonType)) return json(req, opts); 36 | 37 | // form 38 | const formType = opts.formTypes || formTypes; 39 | if (typeis(req, formType)) return form(req, opts); 40 | 41 | // text 42 | const textType = opts.textTypes || textTypes; 43 | if (typeis(req, textType)) return text(req, opts); 44 | 45 | // invalid 46 | const type = req.headers['content-type'] || ''; 47 | const message = type ? 'Unsupported content-type: ' + type : 'Missing content-type'; 48 | const err = new Error(message); 49 | err.status = 415; 50 | throw err; 51 | }; 52 | -------------------------------------------------------------------------------- /lib/json.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | const raw = require('raw-body'); 8 | const inflate = require('inflation'); 9 | const bourne = require('@hapi/bourne'); 10 | const utils = require('./utils'); 11 | 12 | // Allowed whitespace is defined in RFC 7159 13 | // http://www.rfc-editor.org/rfc/rfc7159.txt 14 | const strictJSONReg = /^[\x20\x09\x0a\x0d]*(\[|\{)/; 15 | 16 | /** 17 | * Return a Promise which parses json requests. 18 | * 19 | * Pass a node request or an object with `.req`, 20 | * such as a koa Context. 21 | * 22 | * @param {Request} req 23 | * @param {Options} [opts] 24 | * @return {Function} 25 | * @api public 26 | */ 27 | 28 | module.exports = async function(req, opts) { 29 | req = req.req || req; 30 | opts = utils.clone(opts); 31 | 32 | // defaults 33 | const len = req.headers['content-length']; 34 | const encoding = req.headers['content-encoding'] || 'identity'; 35 | if (len && encoding === 'identity') opts.length = ~~len; 36 | opts.encoding = opts.encoding || 'utf8'; 37 | opts.limit = opts.limit || '1mb'; 38 | const strict = opts.strict !== false; 39 | const protoAction = opts.onProtoPoisoning || 'error'; 40 | 41 | const str = await raw(inflate(req), opts); 42 | try { 43 | const parsed = parse(str); 44 | return opts.returnRawBody ? { parsed, raw: str } : parsed; 45 | } catch (err) { 46 | err.status = 400; 47 | err.body = str; 48 | throw err; 49 | } 50 | 51 | function parse(str) { 52 | if (!strict) return str ? bourne.parse(str, { protoAction }) : str; 53 | // strict mode always return object 54 | if (!str) return {}; 55 | // strict JSON test 56 | if (!strictJSONReg.test(str)) { 57 | throw new SyntaxError('invalid JSON, only supports object and array'); 58 | } 59 | return bourne.parse(str, { protoAction }); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /test/text.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('supertest'); 4 | const parse = require('..'); 5 | const koa = require('koa'); 6 | const Buffer = require('buffer').Buffer; 7 | 8 | describe('parse.text(req, opts)', function() { 9 | describe('with valid str', function() { 10 | it('should parse', function(done) { 11 | const app = koa(); 12 | 13 | app.use(function* () { 14 | this.body = yield parse.text(this); 15 | }); 16 | 17 | request(app.callback()) 18 | .post('/') 19 | .send('Hello World!') 20 | .expect(200) 21 | .expect('Hello World!', done); 22 | }); 23 | }); 24 | 25 | describe('with invalid content encoding', function() { 26 | it('should throw 415', function(done) { 27 | const app = koa(); 28 | 29 | app.use(function* () { 30 | yield parse.text(this); 31 | this.status = 200; 32 | }); 33 | 34 | request(app.callback()) 35 | .post('/') 36 | .set('content-encoding', 'invalid') 37 | .send('Hello World!') 38 | .expect(415, done); 39 | }); 40 | }); 41 | 42 | describe('returnRawBody', function() { 43 | it('should return raw body when opts.returnRawBody = true', function(done) { 44 | const app = koa(); 45 | 46 | app.use(function* () { 47 | this.body = yield parse.text(this, { returnRawBody: true }); 48 | }); 49 | 50 | request(app.callback()) 51 | .post('/') 52 | .send('Hello World!') 53 | .expect({ parsed: 'Hello World!', raw: 'Hello World!' }) 54 | .expect(200, done); 55 | }); 56 | }); 57 | 58 | describe('use no encoding', function() { 59 | it('should return raw body when opts.returnRawBody = true', function(done) { 60 | const app = koa(); 61 | 62 | app.use(function* () { 63 | const requestBody = yield parse.text(this, { encoding: false }); 64 | this.body = { isBuffer: Buffer.isBuffer(requestBody) }; 65 | }); 66 | 67 | request(app.callback()) 68 | .post('/') 69 | .send('Hello World!') 70 | .expect({ isBuffer: true }) 71 | .expect(200, done); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # co-body 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![CI](https://github.com/cojs/co-body/actions/workflows/node.yml/badge.svg)](https://github.com/cojs/co-body/actions/workflows/node.yml) 5 | [![Test coverage][codecov-image]][codecov-url] 6 | [![npm download][download-image]][download-url] 7 | 8 | [npm-image]: https://img.shields.io/npm/v/co-body.svg?style=flat-square 9 | [npm-url]: https://npmjs.org/package/co-body 10 | [codecov-image]: https://codecov.io/github/cojs/co-body/coverage.svg?branch=master 11 | [codecov-url]: https://codecov.io/github/cojs/co-body?branch=master 12 | [download-image]: https://img.shields.io/npm/dm/co-body.svg?style=flat-square 13 | [download-url]: https://npmjs.org/package/co-body 14 | 15 | > Parse request bodies with generators inspired by [Raynos/body](https://github.com/Raynos/body). 16 | 17 | ## Installation 18 | 19 | ```bash 20 | $ npm install co-body 21 | ``` 22 | 23 | ## Options 24 | 25 | - `limit` number or string representing the request size limit (1mb for json and 56kb for form-urlencoded) 26 | - `strict` when set to `true`, JSON parser will only accept arrays and objects; when `false` will accept anything `JSON.parse` accepts. Defaults to `true`. (also `strict` mode will always return object). 27 | - `onProtoPoisoning` Defines what action the `co-body` lib must take when parsing a JSON object with `__proto__`. This functionality is provided by [bourne](https://github.com/hapijs/bourne). 28 | See [Prototype-Poisoning](https://fastify.dev/docs/latest/Guides/Prototype-Poisoning/) for more details about prototype poisoning attacks. 29 | Possible values are `'error'`, `'remove'` and `'ignore'`. 30 | Default to `'error'`, it will throw a `SyntaxError` when `Prototype-Poisoning` happen. 31 | - `queryString` an object of options when parsing query strings and form data. See [qs](https://github.com/hapijs/qs) for more information. 32 | - `returnRawBody` when set to `true`, the return value of `co-body` will be an object with two properties: `{ parsed: /* parsed value */, raw: /* raw body */}`. 33 | - `jsonTypes` is used to determine what media type **co-body** will parse as **json**, this option is passed directly to the [type-is](https://github.com/jshttp/type-is) library. 34 | - `formTypes` is used to determine what media type **co-body** will parse as **form**, this option is passed directly to the [type-is](https://github.com/jshttp/type-is) library. 35 | - `textTypes` is used to determine what media type **co-body** will parse as **text**, this option is passed directly to the [type-is](https://github.com/jshttp/type-is) library. 36 | 37 | more options available via [raw-body](https://github.com/stream-utils/raw-body#getrawbodystream-options-callback): 38 | 39 | ## Example 40 | 41 | ```js 42 | // application/json 43 | var body = await parse.json(req); 44 | 45 | // explicit limit 46 | var body = await parse.json(req, { limit: '10kb' }); 47 | 48 | // application/x-www-form-urlencoded 49 | var body = await parse.form(req); 50 | 51 | // text/plain 52 | var body = await parse.text(req); 53 | 54 | // either 55 | var body = await parse(req); 56 | 57 | // custom type 58 | var body = await parse(req, { textTypes: ['text', 'html'] }); 59 | ``` 60 | 61 | ## Koa 62 | 63 | This lib also supports `ctx.req` in Koa (or other libraries), 64 | so that you may simply use `this` instead of `this.req`. 65 | 66 | ```js 67 | // application/json 68 | var body = await parse.json(this); 69 | 70 | // application/x-www-form-urlencoded 71 | var body = await parse.form(this); 72 | 73 | // text/plain 74 | var body = await parse.text(this); 75 | 76 | // either 77 | var body = await parse(this); 78 | ``` 79 | 80 | # License 81 | 82 | [MIT](LICENSE.txt) 83 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 6.2.0 / 2024-06-05 3 | ================== 4 | 5 | **features** 6 | * [[`2bc63a5`](http://github.com/cojs/co-body/commit/2bc63a50bdb44b237f94b4d676492fcfc6c8d592)] - feat: Protect against prototype poisoning (#87) (fengmk2 <>) 7 | 8 | **others** 9 | * [[`9cce60c`](http://github.com/cojs/co-body/commit/9cce60c33d0356465c0dc7b2c3544cb21c38c549)] - test: run test on github action (#86) (fengmk2 <>) 10 | * [[`bc50a97`](http://github.com/cojs/co-body/commit/bc50a97567c2562ac18e66d7524c363830d382bb)] - chore: add MIT license file (#79) (Peng Deng <>) 11 | 12 | 6.1.0 / 2020-08-12 13 | ================== 14 | 15 | **features** 16 | * [[`960986e`](http://github.com/cojs/co-body/commit/960986ef7518ae7ed05f4a2deab44395ffeaa796)] - feat: throw one type error for json parse strict = true (#74) (Dmitriy Fedotov <>) 17 | 18 | **others** 19 | * [[`a847bfd`](http://github.com/cojs/co-body/commit/a847bfd6e9648138669791c5106439aa080c03ec)] - update: lib/json.js use const define len (#72) (YuLe <>) 20 | * [[`db6041c`](http://github.com/cojs/co-body/commit/db6041c27ce9a6b280aa49c88d82e3ee0da6a844)] - chore: add codecov (#66) (Haoliang Gao <>) 21 | 22 | 6.0.0 / 2018-05-21 23 | ================== 24 | 25 | **features** 26 | * [[`ad6b34d`](http://github.com/cojs/co-body/commit/ad6b34d72886001215a7ed71861b63dbddbbf40b)] - feat: use async function (#65) (Haoliang Gao <>) 27 | 28 | 5.2.0 / 2018-05-02 29 | ================== 30 | 31 | **features** 32 | * [[`f65a2d8`](http://github.com/cojs/co-body/commit/f65a2d8f7ebf4426138035af6d7e7f02272441f2)] - feat: impl text parser support encoding: false (#64) (killa <>) 33 | 34 | 5.1.1 / 2017-03-24 35 | ================== 36 | 37 | * fix: getOptions change to clone 38 | * fix: ensure options are independent in each request 39 | 40 | 5.1.0 / 2017-03-21 41 | ================== 42 | 43 | * feat: add options to support return raw body (#56) 44 | 45 | 5.0.3 / 2017-03-19 46 | ================== 47 | 48 | * fix: ensure inflate in promise chain (#54) 49 | 50 | 5.0.2 / 2017-03-10 51 | ================== 52 | 53 | * fix: keep compatibility with qs@4 (#53) 54 | 55 | 5.0.1 / 2017-03-06 56 | ================== 57 | 58 | * dpes: qs@6.4.0 59 | 60 | 5.0.0 / 2017-03-02 61 | ================== 62 | 63 | * deps: upgrade qs to 6.x (#52) 64 | 65 | 4.2.0 / 2016-05-05 66 | ================== 67 | 68 | * test: test on node 4, 5, 6 69 | * feat: Added support for request body inflation 70 | 71 | 4.1.0 / 2016-05-05 72 | ================== 73 | 74 | * feat: form parse support custom qs module 75 | 76 | 4.0.0 / 2015-08-15 77 | ================== 78 | 79 | * Switch to Promises instead of thunks 80 | 81 | 3.1.0 / 2015-08-06 82 | ================== 83 | 84 | * travis: add v2, v3, remove 0.11 85 | * add custom types options 86 | * use type-is 87 | 88 | 3.0.0 / 2015-07-25 89 | ================== 90 | 91 | * Updated dependencies. Added qs options support via queryString option key. (@yanickrochon) 92 | * upgrade qs@4.0.0, raw-body@2.1.2 93 | 94 | 2.0.0 / 2015-05-04 95 | ================== 96 | 97 | * json parser support strict mode 98 | 99 | 1.2.0 / 2015-04-29 100 | ================== 101 | 102 | * Add JSON-LD as known JSON-Type (@vanthome) 103 | 104 | 1.1.0 / 2015-02-27 105 | ================== 106 | 107 | * Fix content-length zero should not parse json 108 | * Bump deps, qs@~2.3.3, raw-body@~1.3.3 109 | * add support for `text/plain` 110 | * json support for `application/json-patch+json`, `application/vnd.api+json` and `application/csp-report` 111 | -------------------------------------------------------------------------------- /test/form.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('supertest'); 4 | const parse = require('..'); 5 | const koa = require('koa'); 6 | 7 | describe('parse.form(req, opts)', function() { 8 | describe('with valid form body', function() { 9 | it('should parse', function(done) { 10 | const app = koa(); 11 | 12 | app.use(function* () { 13 | const body = yield parse.form(this); 14 | body.foo.bar.should.equal('baz'); 15 | this.status = 200; 16 | }); 17 | 18 | request(app.callback()) 19 | .post('/') 20 | .type('form') 21 | .send({ foo: { bar: 'baz' } }) 22 | .end(function(err) { done(err); }); 23 | }); 24 | }); 25 | 26 | describe('with invalid content encoding', function() { 27 | it('should throw 415', function(done) { 28 | const app = koa(); 29 | 30 | app.use(function* () { 31 | const body = yield parse.form(this); 32 | body.foo.bar.should.equal('baz'); 33 | this.status = 200; 34 | }); 35 | 36 | request(app.callback()) 37 | .post('/') 38 | .type('form') 39 | .set('content-encoding', 'invalid') 40 | .send({ foo: { bar: 'baz' } }) 41 | .expect(415, done); 42 | }); 43 | }); 44 | 45 | describe('with qs settings', function() { 46 | const data = { level1: { level2: { level3: { level4: { level5: { level6: { level7: 'Hello' } } } } } } }; 47 | 48 | it('should not parse full depth', function(done) { 49 | const app = koa(); 50 | 51 | app.use(function* () { 52 | const body = yield parse.form(this); // default to depth = 5 53 | body.level1.level2.level3.level4.level5.level6['[level7]'].should.equal('Hello'); 54 | this.status = 200; 55 | }); 56 | 57 | request(app.callback()) 58 | .post('/') 59 | .type('form') 60 | .send({ level1: { level2: { level3: { level4: { level5: { level6: { level7: 'Hello' } } } } } } }) 61 | .end(function(err) { done(err); }); 62 | 63 | }); 64 | 65 | it('should parse', function(done) { 66 | const app = koa(); 67 | 68 | app.use(function* () { 69 | const body = yield parse.form(this, { queryString: { depth: 10 } }); 70 | body.level1.level2.level3.level4.level5.level6.level7.should.equal('Hello'); 71 | this.status = 200; 72 | }); 73 | 74 | request(app.callback()) 75 | .post('/') 76 | .type('form') 77 | .send(data) 78 | .end(function(err) { done(err); }); 79 | }); 80 | }); 81 | 82 | describe('with custom qs module', function() { 83 | it('should parse with safe-qs', function(done) { 84 | const app = koa(); 85 | 86 | app.use(function* () { 87 | try { 88 | yield parse.form(this, { 89 | qs: require('safe-qs'), 90 | }); 91 | throw new Error('should not run this'); 92 | } catch (err) { 93 | this.status = err.status; 94 | this.body = err.message; 95 | } 96 | }); 97 | 98 | request(app.callback()) 99 | .post('/') 100 | .type('form') 101 | .send({ a: { 21: 'a' } }) 102 | .expect('Index of array [21] is overstep limit: 20') 103 | .expect(400, done); 104 | }); 105 | }); 106 | 107 | describe('allowDots', function() { 108 | it('should allowDots default to true', function(done) { 109 | const app = koa(); 110 | 111 | app.use(function* () { 112 | this.body = yield parse.form(this); 113 | }); 114 | 115 | request(app.callback()) 116 | .post('/') 117 | .type('form') 118 | .send('a.b=1&a.c=2') 119 | .expect({ a: { b: '1', c: '2' } }) 120 | .expect(200, done); 121 | }); 122 | 123 | it('allowDots can set to false', function(done) { 124 | const app = koa(); 125 | 126 | app.use(function* () { 127 | this.body = yield parse.form(this, { queryString: { allowDots: false } }); 128 | }); 129 | 130 | request(app.callback()) 131 | .post('/') 132 | .type('form') 133 | .send('a.b=1&a.c=2') 134 | .expect({ 'a.b': '1', 'a.c': '2' }) 135 | .expect(200, done); 136 | }); 137 | }); 138 | 139 | describe('returnRawBody', function() { 140 | it('should return raw body when opts.returnRawBody = true', function(done) { 141 | const app = koa(); 142 | 143 | app.use(function* () { 144 | this.body = yield parse.form(this, { returnRawBody: true }); 145 | }); 146 | 147 | request(app.callback()) 148 | .post('/') 149 | .type('form') 150 | .send('a[b]=1&a[c]=2') 151 | .expect({ parsed: { a: { b: '1', c: '2' } }, raw: 'a[b]=1&a[c]=2' }) 152 | .expect(200, done); 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /test/json.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('supertest'); 4 | const parse = require('..'); 5 | const koa = require('koa'); 6 | 7 | describe('parse.json(req, opts)', function() { 8 | describe('with valid json', function() { 9 | it('should parse', function(done) { 10 | const app = koa(); 11 | 12 | app.use(function* () { 13 | const body = yield parse.json(this); 14 | body.should.eql({ foo: 'bar' }); 15 | done(); 16 | }); 17 | 18 | request(app.callback()) 19 | .post('/') 20 | .send({ foo: 'bar' }) 21 | .end(function() {}); 22 | }); 23 | }); 24 | 25 | describe('with invalid content encoding', function() { 26 | it('should throw 415', function(done) { 27 | const app = koa(); 28 | 29 | app.use(function* () { 30 | const body = yield parse.json(this); 31 | body.foo.bar.should.equal('baz'); 32 | this.status = 200; 33 | }); 34 | 35 | request(app.callback()) 36 | .post('/') 37 | .type('json') 38 | .set('content-encoding', 'invalid') 39 | .send({ foo: { bar: 'baz' } }) 40 | .expect(415, done); 41 | }); 42 | }); 43 | 44 | describe('with content-length zero', function() { 45 | describe('and strict === false', function() { 46 | it('should return null', function(done) { 47 | const app = koa(); 48 | 49 | app.use(function* () { 50 | const body = yield parse.json(this, { strict: false }); 51 | body.should.equal(''); 52 | done(); 53 | }); 54 | request(app.callback()) 55 | .post('/') 56 | .set('content-length', 0) 57 | .end(function() {}); 58 | }); 59 | }); 60 | 61 | describe('and strict === true', function() { 62 | it('should return null', function(done) { 63 | const app = koa(); 64 | 65 | app.use(function* () { 66 | const body = yield parse.json(this); 67 | body.should.eql({}); 68 | done(); 69 | }); 70 | request(app.callback()) 71 | .post('/') 72 | .set('content-length', 0) 73 | .end(function() {}); 74 | }); 75 | }); 76 | }); 77 | 78 | describe('with invalid json', function() { 79 | it('should parse error', function(done) { 80 | const app = koa(); 81 | 82 | app.use(function* () { 83 | try { 84 | yield parse.json(this); 85 | } catch (err) { 86 | err.should.be.an.instanceOf(SyntaxError); 87 | err.status.should.equal(400); 88 | err.body.should.equal('{"foo": "bar'); 89 | done(); 90 | } 91 | }); 92 | 93 | request(app.callback()) 94 | .post('/') 95 | .set('content-type', 'application/json') 96 | .send('{"foo": "bar') 97 | .end(function() {}); 98 | }); 99 | }); 100 | 101 | describe('with non-object json', function() { 102 | describe('and strict === false', function() { 103 | it('should parse', function(done) { 104 | const app = koa(); 105 | 106 | app.use(function* () { 107 | const body = yield parse.json(this, { strict: false }); 108 | body.should.equal('foo'); 109 | done(); 110 | }); 111 | 112 | request(app.callback()) 113 | .post('/') 114 | .set('content-type', 'application/json') 115 | .send('"foo"') 116 | .end(function() {}); 117 | }); 118 | }); 119 | 120 | describe('and strict === true', function() { 121 | it('should parse', function(done) { 122 | const app = koa(); 123 | 124 | app.use(function* () { 125 | try { 126 | yield parse.json(this, { strict: true }); 127 | } catch (err) { 128 | err.should.be.an.instanceOf(SyntaxError); 129 | err.status.should.equal(400); 130 | err.body.should.equal('"foo"'); 131 | err.message.should.equal('invalid JSON, only supports object and array'); 132 | done(); 133 | } 134 | }); 135 | 136 | request(app.callback()) 137 | .post('/') 138 | .set('content-type', 'application/json') 139 | .send('"foo"') 140 | .end(function() {}); 141 | }); 142 | }); 143 | }); 144 | 145 | describe('returnRawBody', function() { 146 | it('should return raw body when opts.returnRawBody = true', function(done) { 147 | const app = koa(); 148 | 149 | app.use(function* () { 150 | this.body = yield parse.json(this, { returnRawBody: true }); 151 | }); 152 | 153 | request(app.callback()) 154 | .post('/') 155 | .type('json') 156 | .send({ foo: 'bar' }) 157 | .expect({ parsed: { foo: 'bar' }, raw: '{"foo":"bar"}' }) 158 | .expect(200, done); 159 | }); 160 | }); 161 | 162 | describe('with valid onProtoPoisoning', function() { 163 | it('should parse with onProtoPoisoning = "error" by default', function(done) { 164 | const app = koa(); 165 | 166 | app.use(function* () { 167 | try { 168 | yield parse.json(this); 169 | } catch (err) { 170 | err.should.be.an.instanceOf(SyntaxError); 171 | err.message.should.equal('Object contains forbidden prototype property'); 172 | err.status.should.equal(400); 173 | err.body.should.equal('{ "__proto__": { "boom": "💣" } }'); 174 | done(); 175 | } 176 | }); 177 | 178 | request(app.callback()) 179 | .post('/') 180 | .set('content-type', 'application/json') 181 | .send('{ "__proto__": { "boom": "💣" } }') 182 | .end(function() {}); 183 | }); 184 | 185 | it('should parse with onProtoPoisoning = "ignore"', function(done) { 186 | const app = koa(); 187 | 188 | app.use(function* () { 189 | this.body = yield parse.json(this, { onProtoPoisoning: 'ignore' }); 190 | }); 191 | 192 | request(app.callback()) 193 | .post('/') 194 | .set('content-type', 'application/json') 195 | .send('{ "__proto__": { "boom": "💣" }, "hello": "world" }') 196 | .expect({ ['__proto__']: { boom: '💣' }, hello: 'world' }) 197 | .expect(200, done); 198 | }); 199 | 200 | it('should parse with onProtoPoisoning = "remove"', function(done) { 201 | const app = koa(); 202 | 203 | app.use(function* () { 204 | this.body = yield parse.json(this, { onProtoPoisoning: 'remove' }); 205 | }); 206 | 207 | request(app.callback()) 208 | .post('/') 209 | .set('content-type', 'application/json') 210 | .send('{ "__proto__": { "boom": "💣" }, "hello": "world" }') 211 | .expect({ hello: 'world' }) 212 | .expect(200, done); 213 | }); 214 | }); 215 | }); 216 | -------------------------------------------------------------------------------- /test/any.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('supertest'); 4 | const parse = require('..'); 5 | const koa = require('koa'); 6 | const zlib = require('zlib'); 7 | 8 | describe('parse(req, opts)', function() { 9 | describe('with valid form body', function() { 10 | it('should parse', function(done) { 11 | const app = koa(); 12 | 13 | app.use(function* () { 14 | const body = yield parse(this); 15 | body.foo.bar.should.equal('baz'); 16 | done(); 17 | }); 18 | 19 | request(app.callback()) 20 | .post('/') 21 | .type('form') 22 | .send({ foo: { bar: 'baz' } }) 23 | .end(function() {}); 24 | }); 25 | }); 26 | 27 | describe('with valid json', function() { 28 | it('should parse', function(done) { 29 | const app = koa(); 30 | 31 | app.use(function* () { 32 | const body = yield parse(this); 33 | body.should.eql({ foo: 'bar' }); 34 | done(); 35 | }); 36 | 37 | request(app.callback()) 38 | .post('/') 39 | .send({ foo: 'bar' }) 40 | .end(function() {}); 41 | }); 42 | }); 43 | 44 | describe('with valid text', function() { 45 | it('should parse', function(done) { 46 | const app = koa(); 47 | 48 | app.use(function* () { 49 | this.body = yield parse(this); 50 | }); 51 | 52 | request(app.callback()) 53 | .post('/') 54 | .set('content-type', 'text/plain') 55 | .send('plain text') 56 | .expect(200) 57 | .expect('plain text', done); 58 | }); 59 | }); 60 | 61 | describe('with know json content-type', function() { 62 | const app = koa(); 63 | 64 | app.use(function* () { 65 | this.body = yield parse(this); 66 | }); 67 | 68 | it('should parse application/json-patch+json', function(done) { 69 | request(app.callback()) 70 | .post('/') 71 | .type('application/json-patch+json') 72 | .send(JSON.stringify([{ op: 'replace', path: '/foo', value: 'bar' }])) 73 | .expect(200) 74 | .expect([{ op: 'replace', path: '/foo', value: 'bar' }], done); 75 | }); 76 | 77 | it('should parse application/vnd.api+json', function(done) { 78 | request(app.callback()) 79 | .post('/') 80 | .type('application/vnd.api+json') 81 | .send(JSON.stringify({ posts: '1' })) 82 | .expect(200) 83 | .expect({ posts: '1' }, done); 84 | }); 85 | 86 | it('should parse application/csp-report', function(done) { 87 | request(app.callback()) 88 | .post('/') 89 | .type('application/csp-report') 90 | .send(JSON.stringify({ posts: '1' })) 91 | .expect(200) 92 | .expect({ posts: '1' }, done); 93 | }); 94 | 95 | it('should parse application/ld+json', function(done) { 96 | request(app.callback()) 97 | .post('/') 98 | .type('application/ld+json') 99 | .send(JSON.stringify({ posts: '1' })) 100 | .expect(200) 101 | .expect({ posts: '1' }, done); 102 | }); 103 | }); 104 | 105 | describe('with custom types', function() { 106 | it('should parse html as text', function(done) { 107 | const app = koa(); 108 | 109 | app.use(function* () { 110 | const body = yield parse(this, { textTypes: 'text/html' }); 111 | this.body = body; 112 | }); 113 | 114 | request(app.callback()) 115 | .post('/') 116 | .set('Content-Type', 'text/html') 117 | .send('

html text') 118 | .expect('

html text', done); 119 | }); 120 | 121 | it('should parse graphql as text', function(done) { 122 | const app = koa(); 123 | 124 | app.use(function* () { 125 | const body = yield parse(this, { textTypes: [ 'application/graphql', 'text/html' ] }); 126 | this.body = body; 127 | }); 128 | 129 | const graphql = '{\n user(id: 4) {\n name\n }\n}'; 130 | 131 | request(app.callback()) 132 | .post('/') 133 | .set('Content-Type', 'application/graphql') 134 | .send(graphql) 135 | .expect(graphql, done); 136 | }); 137 | }); 138 | 139 | describe('with missing content-type', function() { 140 | it('should fail with 415', function(done) { 141 | const app = koa(); 142 | 143 | app.use(function* () { 144 | yield parse(this); 145 | }); 146 | 147 | request(app.callback()) 148 | .post('/') 149 | .expect(415, 'Unsupported Media Type', done); 150 | }); 151 | }); 152 | 153 | describe('with content-encoding', function() { 154 | it('should inflate gzip', function(done) { 155 | const app = koa(); 156 | const json = JSON.stringify({ foo: 'bar' }); 157 | 158 | app.use(function* () { 159 | const body = yield parse(this); 160 | body.should.eql({ foo: 'bar' }); 161 | done(); 162 | }); 163 | 164 | const req = request(app.callback()) 165 | .post('/') 166 | .type('json') 167 | .set('Content-Encoding', 'gzip'); 168 | req.write(zlib.gzipSync(json)); 169 | req.end(function() {}); 170 | }); 171 | it('should inflate deflate', function(done) { 172 | const app = koa(); 173 | const json = JSON.stringify({ foo: 'bar' }); 174 | 175 | app.use(function* () { 176 | const body = yield parse(this); 177 | body.should.eql({ foo: 'bar' }); 178 | done(); 179 | }); 180 | 181 | const req = request(app.callback()) 182 | .post('/') 183 | .type('json') 184 | .set('Content-Encoding', 'deflate'); 185 | req.write(zlib.deflateSync(json)); 186 | req.end(function() {}); 187 | }); 188 | 189 | describe('after indentity and with shared options', function() { 190 | const app = koa(); 191 | const options = {}; 192 | app.use(function* () { 193 | this.body = yield parse(this, options); 194 | }); 195 | 196 | before(function(done) { 197 | request(app.callback()) 198 | .post('/') 199 | .set('Content-Encoding', 'identity') 200 | .send({ foo: 'bar', and: 'something extra' }) 201 | .expect(200, done); 202 | }); 203 | 204 | it('should inflate deflate', function(done) { 205 | const json = JSON.stringify({ foo: 'bar' }); 206 | const req = request(app.callback()) 207 | .post('/') 208 | .type('json') 209 | .set('Content-Encoding', 'deflate'); 210 | req.write(zlib.deflateSync(json)); 211 | req.expect(200, done); 212 | }); 213 | }); 214 | 215 | it('should pass-through identity', function(done) { 216 | const app = koa(); 217 | 218 | app.use(function* () { 219 | const body = yield parse(this); 220 | body.should.eql({ foo: 'bar' }); 221 | done(); 222 | }); 223 | 224 | request(app.callback()) 225 | .post('/') 226 | .set('Content-Encoding', 'identity') 227 | .send({ foo: 'bar' }) 228 | .end(function() {}); 229 | }); 230 | }); 231 | 232 | }); 233 | --------------------------------------------------------------------------------