├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .huskyrc ├── .xo-config.json ├── History.md ├── LICENSE ├── README.md ├── examples ├── cjs │ └── index.example.cjs ├── esm │ └── index.example.mjs └── typescript │ └── index.example.ts ├── jest.config.ts ├── package.json ├── src ├── body-parser.ts ├── body-parser.types.ts ├── body-parser.utils.ts └── index.ts ├── test ├── fixtures │ └── raw.json ├── middleware.test.ts └── test-utils.ts ├── tsconfig.json └── tsup.config.ts /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | ci: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node-version: 13 | - 16 14 | - 18 15 | - 20 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v3 20 | - name: Install dependencies 21 | run: yarn install 22 | - name: Check linter 23 | run: yarn lint 24 | - name: Run tests 25 | run: yarn test-ci 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS # 2 | ################### 3 | .DS_Store 4 | .idea 5 | Thumbs.db 6 | tmp/ 7 | temp/ 8 | 9 | 10 | # Node.js # 11 | ################### 12 | node_modules 13 | package-lock.json 14 | yarn.lock 15 | npm-debug.log 16 | yarn-debug.log 17 | yarn-error.log 18 | 19 | # Build # 20 | ################### 21 | dist 22 | build 23 | 24 | # NYC # 25 | ################### 26 | coverage 27 | *.lcov 28 | .nyc_output 29 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "npm run lint" 4 | } 5 | } -------------------------------------------------------------------------------- /.xo-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier": true, 3 | "space": true, 4 | "extends": [ 5 | "xo-lass" 6 | ], 7 | "rules": { 8 | "node/no-deprecated-api": "off", 9 | "no-unused-vars": "off", 10 | "no-prototype-builtins": "off", 11 | "prefer-rest-params": "off", 12 | "n/prefer-global/process": "off", 13 | "@typescript-eslint/restrict-template-expressions": "off", 14 | "@typescript-eslint/naming-convention": "off", 15 | "@typescript-eslint/prefer-nullish-coalescing": "off", 16 | "unicorn/no-array-reduce": "off" 17 | }, 18 | "ignores": [ 19 | "test/**", 20 | "examples/**" 21 | ] 22 | } -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 5.1.2 / 2025-05-22 2 | ================== 3 | 4 | **fixes** 5 | * [[`031348a`](https://github.com/koajs/bodyparser/commit/031348a2d469dd288dc21cd10d8de280d9936f2c)] - fix: add check for closed requests (#162) (zbrydon) 6 | 7 | 5.1.1 / 2024-04-09 8 | ================== 9 | 10 | **fixes** 11 | * [[`f27be45`](https://github.com/koajs/bodyparser/commit/f27be45528d6885f08675ee1c650897e44d3dbfa)] - fix: Add koa as peerDeps and fix the encoding option propagation (#160) (3imed-jaberi) 12 | 13 | 5.1.0 / 2024-03-25 14 | ================== 15 | 16 | **features** 17 | * [[`9c18179`](http://github.com/koajs/bodyparser/commit/9c181798ef1467df9109ed54bbd4a612382a6490)] - feat: set name of middleware function to improve interop with monitoring tools (#159) (André Cruz <>) 18 | 19 | **others** 20 | * [[`664cd7c`](http://github.com/koajs/bodyparser/commit/664cd7c413250d5e12eb5bb0fbf4e52d31ef24f5)] - docs: fix usage for @koa/bodyparser v5 (#154) (狼叔 <>) 21 | * [[`1eb0532`](http://github.com/koajs/bodyparser/commit/1eb053267c9e092ada6c316489e20596edd819f4)] - chore: typo npm-url (fengmk2 <>) 22 | * [[`55783fc`](http://github.com/koajs/bodyparser/commit/55783fc0ce77c0235d74244efe4069e4f9ca5850)] - chore: add publishConfig access to public (fengmk2 <>) 23 | 24 | 5.0.0 / 2023-06-25 25 | ================== 26 | 27 | **features** 28 | * [[`b89581a`](http://github.com/koajs/bodyparser/commit/b89581adc53257d8e4d949735402dc8e3c18a7e8)] - feat: Re-create the module with TypeScript (#152) (Imed Jaberi <>) 29 | 30 | 4.4.1 / 2023-06-22 31 | ================== 32 | 33 | **fixes** 34 | * [[`5a551b1`](http://github.com/koajs/bodyparser/commit/5a551b1de6f5e2200b8a838207b56ea1198bdb96)] - fix: compatible extra semicolon on content-type header (#153) (fengmk2 <>) 35 | 36 | 4.4.0 / 2023-03-15 37 | ================== 38 | 39 | **features** 40 | * [[`a9a6476`](http://github.com/koajs/bodyparser/commit/a9a647641bb883746c9691e86b8f87739df4e374)] - feat: Support scim json format (#151) (ask me anything :) <>) 41 | 42 | **fixes** 43 | * [[`4d931c6`](http://github.com/koajs/bodyparser/commit/4d931c634e9b59a843152f56d68b3ef2e1719675)] - fix: revert html parser, use text directly (dead-horse <>) 44 | 45 | **others** 46 | * [[`c02ec0c`](http://github.com/koajs/bodyparser/commit/c02ec0c062f92e1114b4196534669367eae14ccc)] - Update README.md (#149) (sgywzy <<44345776+sgywzy@users.noreply.github.com>>) 47 | * [[`85b426f`](http://github.com/koajs/bodyparser/commit/85b426fea3d98481fd4acbafce0857189199426e)] - Recommend @koa/multer for multipart/form-data (#145) (Jim Fisher <>) 48 | * [[`afecb1a`](http://github.com/koajs/bodyparser/commit/afecb1ab7303ebd36d1a50d6bfe5fc3125759e43)] - Update Repo + Add Html Parser (#134) (imed jaberi <>) 49 | * [[`ecc6ebf`](http://github.com/koajs/bodyparser/commit/ecc6ebfad7179e0009501723e7b2227d25c9603d)] - docs: fix broken npmjs link (#132) (Joel Colucci <>) 50 | * [[`336b287`](http://github.com/koajs/bodyparser/commit/336b2879dc7c0e048d79e28bf23d4b8fe2589376)] - Update README.md (haoxin <>) 51 | * [[`e02cb7d`](http://github.com/koajs/bodyparser/commit/e02cb7dd2c798a116ef12c776da30c710697dea5)] - Update README.md (#125) (thaiworldgame <<36978149+thaiworldgame@users.noreply.github.com>>) 52 | 53 | 4.3.0 / 2020-03-24 54 | ================== 55 | 56 | **features** 57 | * [[`705673d`](http://github.com/koajs/bodyparser/commit/705673d634818727dbdb25ee999560970bd268a2)] - feat: support xml (#131) (TZ | 天猪 <>) 58 | 59 | **others** 60 | * [[`6fd7e9c`](http://github.com/koajs/bodyparser/commit/6fd7e9c321684adc239d2afb270782c21d0b6231)] - docs: add multipart tips (dead-horse <>) 61 | * [[`57c0022`](http://github.com/koajs/bodyparser/commit/57c00225d54b5b5dd1a7526478ad3eae8495222f)] - Fix typo in README.md (#112) (Adrian Pascu <<1521321+adipascu@users.noreply.github.com>>) 62 | 63 | 4.2.1 / 2018-05-21 64 | ================== 65 | 66 | **others** 67 | * [[`b270d5d`](http://github.com/koajs/bodyparser/commit/b270d5d138662f41dc63527505ea02dea0c1e7e8)] - deps: upgrade co-body (#104) (Haoliang Gao <>) 68 | * [[`d234345`](http://github.com/koajs/bodyparser/commit/d234345ffa2dadbab2ef0ce970fb8a58059e5f47)] - docs(readme): update opts encode -> encoding (#103) (Matthew Scragg <>) 69 | * [[`db193f5`](http://github.com/koajs/bodyparser/commit/db193f5d46860393521ad38f90a554968b2ba98a)] - chore:replace indexOf with includes (#90) (coderzzp <>) 70 | 71 | 4.2.0 / 2017-03-21 72 | ================== 73 | 74 | * feat: ctx.request.rawBody to get raw request body (#70) 75 | 76 | 4.1.0 / 2017-03-02 77 | ================== 78 | 79 | * deps: upgrade co-body@5 (#64) 80 | 81 | 4.0.0 / 2017-02-27 82 | ================== 83 | 84 | * refactor: use async function and support koa@2 (#62) 85 | 86 | 2.3.0 / 2016-11-14 87 | ================== 88 | 89 | * feat: support dynamic disable body parser 90 | 91 | 2.2.0 / 2016-05-16 92 | ================== 93 | 94 | * feat: support enableTypes and text (#44) 95 | 96 | 2.1.0 / 2016-05-10 97 | ================== 98 | 99 | * deps: co-body@4 100 | 101 | 2.0.1 / 2015-08-12 102 | ================== 103 | 104 | * chore: upgrade co-body@3.1.0 105 | 106 | 2.0.0 / 2015-05-07 107 | ================== 108 | 109 | * deps: co-body@2, default to strict mode 110 | 111 | 1.6.0 / 2015-05-01 112 | ================== 113 | 114 | * feat: support custom error handler 115 | 116 | 1.5.0 / 2015-04-04 117 | ================== 118 | 119 | * Use an empty object instead of null, if no body is parsed 120 | 121 | 1.4.1 / 2015-03-10 122 | ================== 123 | 124 | * bump co-body@1.1.0 125 | 126 | 1.4.0 / 2015-02-26 127 | ================== 128 | 129 | * feat: custom json request detect 130 | 131 | 1.3.1 / 2015-01-27 132 | ================== 133 | 134 | * fix: extend 135 | 136 | 1.3.0 / 2014-11-27 137 | ================== 138 | 139 | * support extendTypes 140 | * Merge pull request #8 from coderhaoxin/json-patch 141 | * add support for json patch 142 | 143 | 1.2.0 / 2014-11-07 144 | ================== 145 | 146 | * add example.js 147 | * bump dependencies 148 | * Merge pull request #7 from rudijs/develop 149 | * Add support for JSON-API 150 | 151 | 1.1.0 / 2014-10-28 152 | ================== 153 | 154 | * Merge pull request #6 from tunnckoCore/master 155 | * resolve https://github.com/tunnckoCore/koa-better-body/issues/3#issuecomment-60458238 156 | 157 | 1.0.0 / 2014-04-23 158 | ================== 159 | 160 | * update readme 161 | * refactor 162 | 163 | 0.1.0 / 2014-03-06 164 | ================== 165 | 166 | * Merge pull request #2 from fengmk2/remove-co 167 | * Remove co deps and improve coverage to 100% 168 | 169 | 0.0.2 / 2014-02-26 170 | ================== 171 | 172 | * Merge pull request #1 from fengmk2/jsonLimit 173 | * add jsonLimit options to fix json and form body limit confuse 174 | 175 | 0.0.1 / 2014-02-18 176 | ================== 177 | 178 | * update package name, merge middleware into module.exports 179 | * complete readme 180 | * complete bodyparser and bodyparser.middleware 181 | * Initial commit 182 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 dead_horse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [**@koa/bodyparser**](https://github.com/koajs/bodyparser) 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | ![build status][github-action-image] 5 | [![Coveralls][coveralls-image]][coveralls-url] 6 | [![node version][node-image]][node-url] 7 | 8 | [npm-image]: https://img.shields.io/npm/v/@koa/bodyparser.svg?style=flat-square 9 | [npm-url]: https://www.npmjs.com/package/@koa/bodyparser 10 | [github-action-image]: https://github.com/koajs/bodyparser/actions/workflows/ci.yml/badge.svg?style=flat-square 11 | [coveralls-image]: https://img.shields.io/coveralls/koajs/bodyparser.svg?style=flat-square 12 | [coveralls-url]: https://coveralls.io/r/koajs/bodyparser?branch=master 13 | [node-image]: https://img.shields.io/badge/node.js-%3E=_14-green.svg?style=flat-square 14 | [node-url]: http://nodejs.org/download/ 15 | 16 | Koa body parsing middleware, based on [co-body](https://github.com/tj/co-body). support `json`, `form` and `text` type body. 17 | 18 | Parse incoming request bodies in a middleware before your handlers, available under the `ctx.request.body` property. 19 | 20 | > ⚠ Notice: **This module doesn't support parsing multipart format data**, please use [`@koa/multer`](https://github.com/koajs/multer) to parse multipart format data. 21 | 22 | ## Install 23 | 24 | [![NPM](https://nodei.co/npm/@koa/bodyparser.png?downloads=true)](https://nodei.co/npm/@koa/bodyparser) 25 | 26 | ```bash 27 | $ npm i @koa/bodyparser --save 28 | ``` 29 | 30 | ## Usage 31 | 32 | ```js 33 | const Koa = require("koa"); 34 | const { bodyParser } = require("@koa/bodyparser"); 35 | 36 | const app = new Koa(); 37 | app.use(bodyParser()); 38 | 39 | app.use((ctx) => { 40 | // the parsed body will store in ctx.request.body 41 | // if nothing was parsed, body will be an empty object {} 42 | ctx.body = ctx.request.body; 43 | }); 44 | ``` 45 | 46 | ## Options 47 | 48 | - **patchNode**: patch request body to Node's `ctx.req`, default is `false`. 49 | - **enableTypes**: parser will only parse when request type hits enableTypes, support `json/form/text/xml`, default is `['json', 'form']`. 50 | - **encoding**: requested encoding. Default is `utf-8` by `co-body`. 51 | - **formLimit**: limit of the `urlencoded` body. If the body ends up being larger than this limit, a 413 error code is returned. Default is `56kb`. 52 | - **jsonLimit**: limit of the `json` body. Default is `1mb`. 53 | - **textLimit**: limit of the `text` body. Default is `1mb`. 54 | - **xmlLimit**: limit of the `xml` body. Default is `1mb`. 55 | - **jsonStrict**: when set to true, JSON parser will only accept arrays and objects. Default is `true`. See [strict mode](https://github.com/cojs/co-body#options) in `co-body`. In strict mode, `ctx.request.body` will always be an object(or array), this avoid lots of type judging. But text body will always return string type. 56 | - **detectJSON**: custom json request detect function. Default is `null`. 57 | 58 | ```js 59 | app.use( 60 | bodyParser({ 61 | detectJSON(ctx) { 62 | return /\.json$/i.test(ctx.path); 63 | }, 64 | }) 65 | ); 66 | ``` 67 | 68 | - **extendTypes**: support extend types: 69 | 70 | ```js 71 | app.use( 72 | bodyParser({ 73 | extendTypes: { 74 | // will parse application/x-javascript type body as a JSON string 75 | json: ["application/x-javascript"], 76 | }, 77 | }) 78 | ); 79 | ``` 80 | 81 | - **onError**: support custom error handle, if `koa-bodyparser` throw an error, you can customize the response like: 82 | 83 | ```js 84 | app.use( 85 | bodyParser({ 86 | onError(err, ctx) { 87 | ctx.throw(422, "body parse error"); 88 | }, 89 | }) 90 | ); 91 | ``` 92 | 93 | - **enableRawChecking**: support the already parsed body on the raw request by override and prioritize the parsed value over the sended payload. (default is `false`) 94 | 95 | - **parsedMethods**: declares the HTTP methods where bodies will be parsed, default `['POST', 'PUT', 'PATCH']`. 96 | 97 | - **disableBodyParser**: you can dynamic disable body parser by set `ctx.disableBodyParser = true`. 98 | 99 | ```js 100 | app.use((ctx, next) => { 101 | if (ctx.path === "/disable") ctx.disableBodyParser = true; 102 | return next(); 103 | }); 104 | app.use(bodyParser()); 105 | ``` 106 | 107 | ## Raw Body 108 | 109 | You can access raw request body by `ctx.request.rawBody` after `koa-bodyparser` when: 110 | 111 | 1. `koa-bodyparser` parsed the request body. 112 | 2. `ctx.request.rawBody` is not present before `koa-bodyparser`. 113 | 114 | ## Koa v1.x.x Support 115 | 116 | To use `koa-bodyparser` with koa@1.x.x, please use [bodyparser 2.x](https://github.com/koajs/bodyparser/tree/2.x). 117 | 118 | ```bash 119 | $ npm install koa-bodyparser@2 --save 120 | ``` 121 | 122 | usage 123 | 124 | ```js 125 | const Koa = require("koa"); 126 | const bodyParser = require("@koa/bodyparser"); 127 | 128 | const app = new Koa(); 129 | app.use(bodyParser()); 130 | 131 | app.use((ctx) => { 132 | // the parsed body will store in ctx.request.body 133 | // if nothing was parsed, body will be an empty object {} 134 | ctx.body = ctx.request.body; 135 | }); 136 | ``` 137 | 138 | ## Licences 139 | 140 | [MIT](LICENSE) 141 | -------------------------------------------------------------------------------- /examples/cjs/index.example.cjs: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const bodyParser = require('../../dist').default; 3 | 4 | const app = new Koa(); 5 | app.use(bodyParser()); 6 | 7 | app.use((ctx) => { 8 | // the parsed body will store in this.request.body 9 | ctx.body = ctx.request.body; 10 | }); 11 | 12 | const PORT = process.env.PORT || 3000; 13 | 14 | app.listen(PORT, () => 15 | console.log(`Server ready at http://localhost:${PORT} 🚀 ..`), 16 | ); 17 | -------------------------------------------------------------------------------- /examples/esm/index.example.mjs: -------------------------------------------------------------------------------- 1 | import Koa from 'koa'; 2 | import bodyParser from '../../dist/index.mjs'; 3 | 4 | const app = new Koa(); 5 | app.use(bodyParser()); 6 | 7 | app.use((ctx) => { 8 | // the parsed body will store in this.request.body 9 | ctx.body = ctx.request.body; 10 | }); 11 | 12 | const PORT = process.env.PORT || 3000; 13 | 14 | app.listen(PORT, () => console.log(`Server ready at http://localhost:${PORT} 🚀 ..`)); 15 | -------------------------------------------------------------------------------- /examples/typescript/index.example.ts: -------------------------------------------------------------------------------- 1 | import Koa from 'koa'; 2 | import {bodyParser} from '../../src'; 3 | 4 | const app = new Koa(); 5 | app.use(bodyParser()); 6 | 7 | app.use((ctx) => { 8 | // the parsed body will store in this.request.body 9 | ctx.body = ctx.request.body; // eslint-disable-line @typescript-eslint/no-unsafe-assignment 10 | }); 11 | 12 | const PORT = process.env.PORT || 3000; 13 | 14 | app.listen(PORT, () => { 15 | console.log(`Server ready at http://localhost:${PORT} 🚀 ..`); 16 | }); 17 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type {Config} from 'jest'; 2 | 3 | const jestConfig: Config = { 4 | preset: 'ts-jest', 5 | testEnvironment: 'node', 6 | testMatch: ['**/*.test.ts'], 7 | }; 8 | 9 | export default jestConfig; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@koa/bodyparser", 3 | "version": "5.1.2", 4 | "description": "Koa body parsing middleware", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.mjs", 7 | "types": "./dist/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "require": "./dist/index.js", 11 | "import": "./dist/index.mjs", 12 | "types": "./dist/index.d.ts" 13 | } 14 | }, 15 | "files": [ 16 | "dist" 17 | ], 18 | "scripts": { 19 | "build": "tsup", 20 | "lint": "xo", 21 | "lint:fix": "xo --fix", 22 | "test": "jest --detectOpenHandles", 23 | "test-ci": "npm run test -- --coverage", 24 | "prepublishOnly": "npm run build" 25 | }, 26 | "keywords": [ 27 | "koa", 28 | "body", 29 | "request-body", 30 | "bodyParser", 31 | "json", 32 | "urlencoded", 33 | "text", 34 | "xml" 35 | ], 36 | "author": { 37 | "name": "dead_horse", 38 | "email": "dead_horse@qq.com", 39 | "url": " http://deadhorse.me" 40 | }, 41 | "contributors": [ 42 | { 43 | "name": "Imed Jaberi", 44 | "email": "imed_jebari@hotmail.fr" 45 | } 46 | ], 47 | "license": "MIT", 48 | "devDependencies": { 49 | "@types/jest": "^29.5.0", 50 | "@types/koa": "^2.13.6", 51 | "@types/lodash.merge": "^4.6.7", 52 | "@types/node": "^18.15.11", 53 | "@types/supertest": "^2.0.12", 54 | "@types/type-is": "^1.6.3", 55 | "eslint-config-xo-lass": "^2.0.1", 56 | "husky": "^8.0.3", 57 | "jest": "^29.5.0", 58 | "koa": "^2.14.1", 59 | "supertest": "^6.3.3", 60 | "ts-jest": "^29.0.5", 61 | "ts-node": "^10.9.1", 62 | "tsup": "^6.7.0", 63 | "typescript": "^5.0.3", 64 | "xo": "^0.54.2" 65 | }, 66 | "dependencies": { 67 | "@types/co-body": "^6.1.0", 68 | "co-body": "^6.1.0", 69 | "lodash.merge": "^4.6.2", 70 | "type-is": "^1.6.18" 71 | }, 72 | "peerDependencies": { 73 | "koa": "^2.14.1" 74 | }, 75 | "engines": { 76 | "node": ">= 16" 77 | }, 78 | "repository": { 79 | "type": "git", 80 | "url": "git://github.com/koajs/bodyparser.git" 81 | }, 82 | "bugs": { 83 | "url": "https://github.com/koajs/body-parser/issues" 84 | }, 85 | "homepage": "https://github.com/koajs/body-parser", 86 | "publishConfig": { 87 | "access": "public" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/body-parser.ts: -------------------------------------------------------------------------------- 1 | import parser from 'co-body'; 2 | import type * as Koa from 'koa'; 3 | import type {BodyParserOptions, BodyType} from './body-parser.types'; 4 | import {getIsEnabledBodyAs, getMimeTypes, isTypes} from './body-parser.utils'; 5 | 6 | /** 7 | * Global declaration for the added properties to the 'ctx.request' 8 | */ 9 | declare module 'koa' { 10 | // eslint-disable-next-line @typescript-eslint/consistent-type-definitions 11 | interface Request { 12 | body?: any; 13 | rawBody: string; 14 | } 15 | } 16 | 17 | declare module 'http' { 18 | // eslint-disable-next-line @typescript-eslint/consistent-type-definitions 19 | interface IncomingMessage { 20 | body?: any; 21 | rawBody: string; 22 | } 23 | } 24 | /** 25 | * Middleware wrapper which delegate options to the core code 26 | */ 27 | export function bodyParserWrapper(opts: BodyParserOptions = {}) { 28 | const { 29 | patchNode = false, 30 | parsedMethods = ['POST', 'PUT', 'PATCH'], 31 | detectJSON, 32 | onError, 33 | enableTypes = ['json', 'form'], 34 | extendTypes = {} as NonNullable, 35 | enableRawChecking = false, 36 | ...restOpts 37 | } = opts; 38 | const isEnabledBodyAs = getIsEnabledBodyAs(enableTypes); 39 | const mimeTypes = getMimeTypes(extendTypes); 40 | 41 | /** 42 | * Handler to parse the request coming data 43 | */ 44 | async function parseBody(ctx: Koa.Context) { 45 | const shouldParseBodyAs = (type: BodyType) => { 46 | return Boolean( 47 | isEnabledBodyAs[type] && 48 | isTypes(ctx.request.get('content-type'), mimeTypes[type]), 49 | ); 50 | }; 51 | 52 | const bodyType = 53 | detectJSON?.(ctx) || shouldParseBodyAs('json') 54 | ? 'json' 55 | : shouldParseBodyAs('form') 56 | ? 'form' 57 | : shouldParseBodyAs('text') || shouldParseBodyAs('xml') 58 | ? 'text' 59 | : null; 60 | 61 | // eslint-disable-next-line @typescript-eslint/consistent-type-assertions 62 | if (!bodyType) return {} as Record; 63 | const parserOptions = { 64 | // force co-body return raw body 65 | returnRawBody: true, 66 | strict: bodyType === 'json' ? restOpts.jsonStrict : undefined, 67 | [`${bodyType}Types`]: mimeTypes[bodyType], 68 | limit: restOpts[`${shouldParseBodyAs('xml') ? 'xml' : bodyType}Limit`], 69 | // eslint-disable-next-line unicorn/text-encoding-identifier-case 70 | encoding: restOpts.encoding || 'utf-8', 71 | }; 72 | 73 | return parser[bodyType](ctx, parserOptions) as Promise< 74 | Record 75 | >; 76 | } 77 | 78 | // eslint-disable-next-line func-names 79 | return async function bodyParser(ctx: Koa.Context, next: Koa.Next) { 80 | if ( 81 | // method souldn't be parsed 82 | !parsedMethods.includes(ctx.method.toUpperCase()) || 83 | // patchNode enabled and raw request already parsed 84 | (patchNode && ctx.req.body !== undefined) || 85 | // koa request body already parsed 86 | ctx.request.body !== undefined || 87 | // bodyparser disabled 88 | ctx.disableBodyParser 89 | ) 90 | return next(); 91 | // raw request parsed and contain 'body' values and it's enabled to override the koa request 92 | if (enableRawChecking && ctx.req.body !== undefined) { 93 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 94 | ctx.request.body = ctx.req.body; 95 | return next(); 96 | } 97 | 98 | if (ctx.req.closed) { 99 | ctx.status = 499; 100 | ctx.body = 'Request already closed'; 101 | return; 102 | } 103 | 104 | try { 105 | const response = await parseBody(ctx); 106 | // patch node 107 | if (patchNode) { 108 | ctx.req.body = 'parsed' in response ? response.parsed : {}; 109 | if (ctx.req.rawBody === undefined) ctx.req.rawBody = response.raw; 110 | } 111 | 112 | // patch koa 113 | ctx.request.body = 'parsed' in response ? response.parsed : {}; 114 | if (ctx.request.rawBody === undefined) ctx.request.rawBody = response.raw; 115 | } catch (err: unknown) { 116 | if (!onError) throw err; 117 | onError(err as Error, ctx); 118 | } 119 | 120 | return next(); 121 | }; 122 | } 123 | -------------------------------------------------------------------------------- /src/body-parser.types.ts: -------------------------------------------------------------------------------- 1 | import type {Options as CoBodyOptions} from 'co-body'; 2 | import type * as Koa from 'koa'; 3 | 4 | /** 5 | * List of supported body types 6 | */ 7 | export const supportedBodyTypes = ['json', 'form', 'text', 'xml'] as const; 8 | export type BodyType = (typeof supportedBodyTypes)[number]; 9 | 10 | /** 11 | * BodyParser Options 12 | */ 13 | export type BodyParserOptions = { 14 | /** 15 | * declares the HTTP methods where bodies will be parsed. 16 | * @default ['POST', 'PUT', 'PATCH'] 17 | */ 18 | parsedMethods?: string[]; 19 | /** 20 | * patch request body to Node's 'ctx.req' 21 | * @default false 22 | */ 23 | patchNode?: boolean; 24 | /** 25 | * json detector function, can help to detect request json type based on custom logic 26 | */ 27 | detectJSON?: (ctx: Koa.Context) => boolean; 28 | /** 29 | * error handler, can help to customize the response on error case 30 | */ 31 | onError?: (error: Error, ctx: Koa.Context) => void; 32 | /** 33 | * false to disable the raw request body checking to prevent koa request override 34 | * @default false 35 | */ 36 | enableRawChecking?: boolean; 37 | /** 38 | * co-body parser will only parse when request type hits enableTypes 39 | * @default ['json', 'form'] 40 | */ 41 | enableTypes?: BodyType[]; 42 | /** 43 | * extend parser types, can help to enhance the base mime types with custom types 44 | */ 45 | extendTypes?: { 46 | [K in BodyType]?: string[]; 47 | }; 48 | /** 49 | * When set to true, JSON parser will only accept arrays and objects. 50 | * When false will accept anything JSON.parse accepts. 51 | * 52 | * @default true 53 | */ 54 | jsonStrict?: CoBodyOptions['strict']; 55 | /** 56 | * limit of the `json` body 57 | * @default '1mb' 58 | */ 59 | jsonLimit?: CoBodyOptions['limit']; 60 | /** 61 | * limit of the `urlencoded` body 62 | * @default '56kb' 63 | */ 64 | formLimit?: CoBodyOptions['limit']; 65 | /** 66 | * limit of the `text` body 67 | * @default '1mb' 68 | */ 69 | textLimit?: CoBodyOptions['limit']; 70 | /** 71 | * limit of the `xml` body 72 | * @default '1mb' 73 | */ 74 | xmlLimit?: CoBodyOptions['limit']; 75 | } & Pick< 76 | CoBodyOptions, 77 | /** 78 | * requested encoding. 79 | * @default 'utf-8' by 'co-body'. 80 | */ 81 | 'encoding' 82 | >; 83 | -------------------------------------------------------------------------------- /src/body-parser.utils.ts: -------------------------------------------------------------------------------- 1 | import deepMerge from 'lodash.merge'; 2 | import typeis from 'type-is'; 3 | import { 4 | type BodyParserOptions, 5 | supportedBodyTypes, 6 | type BodyType, 7 | } from './body-parser.types'; 8 | 9 | /** 10 | * UnsupportedBodyTypeError 11 | */ 12 | export class UnsupportedBodyTypeError extends Error { 13 | constructor(wrongType: string) { 14 | super(); 15 | this.name = 'UnsupportedBodyTypeError'; 16 | this.message = 17 | `Invalid enabled type '${wrongType}'.` + 18 | ` make sure to pass an array contains ` + 19 | `supported types ([${supportedBodyTypes}]).`; 20 | } 21 | } 22 | 23 | /** 24 | * Utility which help us to check if the body type enabled 25 | */ 26 | export function getIsEnabledBodyAs(enableTypes: BodyType[]) { 27 | for (const enabledType of enableTypes) { 28 | if (!supportedBodyTypes.includes(enabledType)) { 29 | throw new UnsupportedBodyTypeError(enabledType); 30 | } 31 | } 32 | 33 | const isEnabledBodyAs = supportedBodyTypes.reduce( 34 | (prevResult, currentType) => ({ 35 | ...prevResult, 36 | [currentType]: enableTypes.includes(currentType), 37 | }), 38 | {} as NonNullable, 39 | ); 40 | 41 | return isEnabledBodyAs; 42 | } 43 | 44 | /** 45 | * Utility which help us to merge the extended mime types with our base 46 | */ 47 | export function getMimeTypes( 48 | extendTypes: NonNullable, 49 | ) { 50 | for (const extendedTypeKey of Object.keys(extendTypes) as BodyType[]) { 51 | const extendedType = extendTypes[extendedTypeKey]; 52 | 53 | if ( 54 | !supportedBodyTypes.includes(extendedTypeKey) || 55 | !Array.isArray(extendedType) 56 | ) { 57 | throw new UnsupportedBodyTypeError(extendedTypeKey); 58 | } 59 | } 60 | 61 | const defaultMimeTypes = { 62 | // default json mime types 63 | json: [ 64 | 'application/json', 65 | 'application/json-patch+json', 66 | 'application/vnd.api+json', 67 | 'application/csp-report', 68 | 'application/reports+json', 69 | 'application/scim+json', 70 | ], 71 | // default form mime types 72 | form: ['application/x-www-form-urlencoded'], 73 | // default text mime types 74 | text: ['text/plain'], 75 | // default xml mime types 76 | xml: ['text/xml', 'application/xml'], 77 | }; 78 | const mimeTypes = deepMerge(defaultMimeTypes, extendTypes); 79 | 80 | return mimeTypes; 81 | } 82 | 83 | /** 84 | * Check if the incoming request contains the "Content-Type" header 85 | * field, and it contains any of the give mime types. If there 86 | * is no request body, null is returned. If there is no content type, 87 | * false is returned. Otherwise, it returns the first type that matches. 88 | */ 89 | export function isTypes(contentTypeValue: string, types: string[]) { 90 | if (typeof contentTypeValue === 'string') { 91 | // trim extra semicolon 92 | contentTypeValue = contentTypeValue.replace(/;$/, ''); 93 | } 94 | 95 | return typeis.is(contentTypeValue, types); 96 | } 97 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | /* istanbul ignore next */ 3 | bodyParserWrapper as bodyParser, 4 | bodyParserWrapper as default, 5 | } from './body-parser'; 6 | -------------------------------------------------------------------------------- /test/fixtures/raw.json: -------------------------------------------------------------------------------- 1 | {"_id":"mk2testmodule","name":"mk2testmodule","description":"","dist-tags":{"latest":"0.0.1"},"versions":{"0.0.1":{"name":"mk2testmodule","version":"0.0.1","description":"","main":"index.js","scripts":{"test":"echo \"Error: no test specified\" && exit 1"},"author":"","license":"ISC","readme":"ERROR: No README data found!","_id":"mk2testmodule@0.0.1","dist":{"shasum":"fa475605f88bab9b1127833633ca3ae0a477224c","tarball":"http://127.0.0.1:7001/mk2testmodule/-/mk2testmodule-0.0.1.tgz"},"_from":".","_npmVersion":"1.4.3","_npmUser":{"name":"fengmk2","email":"fengmk2@gmail.com"},"maintainers":[{"name":"fengmk2","email":"fengmk2@gmail.com"}]}},"readme":"ERROR: No README data found!","maintainers":[{"name":"fengmk2","email":"fengmk2@gmail.com"}],"_attachments":{"mk2testmodule-0.0.1.tgz":{"content_type":"application/octet-stream","data":"H4sIAAAAAAAAA+2SsWrDMBCGPfspDg2ZinOyEgeylg6Zu2YR8rVRHEtGkkOg5N0jWaFdujVQAv6W4/7/dHcSGqTq5Ccthxyro7emeDCI2KxWkOKmaaaIdc4TouZQ8FqgwI3AdVMgF8ijho9e5DdGH6SLq/y1T74LfMcn4asEYEb2xLbA+q4O5ENv2/FE7CVZZ3JeW5NcrLDiWW3JK6eHcHey2Es9Zdq0dIkfKau50EcjjYpCmpDKSB0s7Nmbc9ZtwVhIBviBlP7Q1O4ZLBZAFx2As3jyOnWTYzhY9zPzpBUZPy2/e39l5bX87wedmZmZeRJuheTX2wAIAAA=","length":251}}} 2 | -------------------------------------------------------------------------------- /test/middleware.test.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import request from "supertest"; 3 | import Koa from "koa"; 4 | 5 | import bodyParser from "../src"; 6 | import { UnsupportedBodyTypeError } from "../src/body-parser.utils"; 7 | 8 | import { createApp, fixtures } from "./test-utils"; 9 | 10 | describe("test/body-parser.test.ts", () => { 11 | let server: ReturnType["listen"]>; 12 | 13 | afterEach(() => { 14 | if (server?.listening) server.close(); 15 | }); 16 | 17 | describe("json body", () => { 18 | let app: ReturnType; 19 | beforeEach(() => { 20 | app = createApp(); 21 | }); 22 | 23 | it("should parse json body ok", async () => { 24 | // should work when use body parser again 25 | app.use(bodyParser()); 26 | 27 | app.use(async (ctx) => { 28 | expect(ctx.request.body).toEqual({ foo: "bar" }); 29 | expect(ctx.request.rawBody).toEqual('{"foo":"bar"}'); 30 | ctx.body = ctx.request.body; 31 | }); 32 | 33 | server = app.listen(); 34 | 35 | await request(server) 36 | .post("/") 37 | .send({ foo: "bar" }) 38 | .expect({ foo: "bar" }); 39 | }); 40 | 41 | it("should parse json body with json-api headers ok", async () => { 42 | // should work when use body parser again 43 | app.use(bodyParser()); 44 | 45 | app.use(async (ctx) => { 46 | expect(ctx.request.body).toEqual({ foo: "bar" }); 47 | expect(ctx.request.rawBody).toEqual('{"foo": "bar"}'); 48 | ctx.body = ctx.request.body; 49 | }); 50 | server = app.listen(); 51 | await request(server) 52 | .post("/") 53 | .set("Accept", "application/vnd.api+json") 54 | .set("Content-type", "application/vnd.api+json") 55 | .send('{"foo": "bar"}') 56 | .expect({ foo: "bar" }); 57 | }); 58 | 59 | it("should parse json body with `content-type: application/json;charset=utf-8;` headers ok", async () => { 60 | app.use(bodyParser()); 61 | 62 | app.use(async (ctx) => { 63 | expect(ctx.request.body).toEqual({ foo: "bar" }); 64 | expect(ctx.request.rawBody).toEqual('{"foo": "bar"}'); 65 | ctx.body = ctx.request.body; 66 | }); 67 | 68 | server = app.listen(); 69 | await request(server) 70 | .post("/") 71 | .set("Content-type", "application/json;charset=utf-8;") 72 | .send('{"foo": "bar"}') 73 | .expect({ foo: "bar" }); 74 | }); 75 | 76 | it("should parse json patch", async () => { 77 | const app = createApp(); 78 | app.use(async (ctx) => { 79 | expect(ctx.request.body).toEqual([ 80 | { op: "add", path: "/foo", value: "bar" }, 81 | ]); 82 | expect(ctx.request.rawBody).toEqual( 83 | '[{"op": "add", "path": "/foo", "value": "bar"}]' 84 | ); 85 | ctx.body = ctx.request.body; 86 | }); 87 | server = app.listen(); 88 | await request(server) 89 | .patch("/") 90 | .set("Content-type", "application/json-patch+json") 91 | .send('[{"op": "add", "path": "/foo", "value": "bar"}]') 92 | .expect([{ op: "add", path: "/foo", value: "bar" }]); 93 | }); 94 | 95 | it("should json body reach the limit size", async () => { 96 | const app = createApp({ jsonLimit: 100 }); 97 | app.use(async (ctx) => { 98 | ctx.body = ctx.request.body; 99 | }); 100 | server = app.listen(); 101 | await request(server) 102 | .post("/") 103 | .send(require(path.join(fixtures, "raw.json"))) 104 | .expect(413); 105 | }); 106 | 107 | it("should json body error with string in strict mode", async () => { 108 | const app = createApp({ jsonLimit: 100 }); 109 | app.use(async (ctx) => { 110 | expect(ctx.request.rawBody).toEqual('"invalid"'); 111 | ctx.body = ctx.request.body; 112 | }); 113 | server = app.listen(); 114 | await request(server) 115 | .post("/") 116 | .set("Content-type", "application/json") 117 | .send('"invalid"') 118 | .expect(400); 119 | }); 120 | 121 | it("should json body ok with string not in strict mode", async () => { 122 | const app = createApp({ jsonLimit: 100, jsonStrict: false }); 123 | app.use(async (ctx) => { 124 | expect(ctx.request.rawBody).toEqual('"valid"'); 125 | ctx.body = ctx.request.body; 126 | }); 127 | server = app.listen(); 128 | await request(server) 129 | .post("/") 130 | .set("Content-type", "application/json") 131 | .send('"valid"') 132 | .expect(200) 133 | .expect("valid"); 134 | }); 135 | 136 | describe("opts.detectJSON", () => { 137 | it("should parse json body on /foo.json request", async () => { 138 | const app = createApp({ 139 | detectJSON(ctx) { 140 | return /\.json/i.test(ctx.path); 141 | }, 142 | }); 143 | 144 | app.use(async (ctx) => { 145 | expect(ctx.request.body).toEqual({ foo: "bar" }); 146 | expect(ctx.request.rawBody).toEqual('{"foo":"bar"}'); 147 | ctx.body = ctx.request.body; 148 | }); 149 | 150 | server = app.listen(); 151 | await request(server) 152 | .post("/foo.json") 153 | .send(JSON.stringify({ foo: "bar" })) 154 | .expect({ foo: "bar" }); 155 | }); 156 | 157 | it("should not parse json body on /foo request", async () => { 158 | const app = createApp({ 159 | detectJSON(ctx) { 160 | return /\.json/i.test(ctx.path); 161 | }, 162 | }); 163 | 164 | app.use(async (ctx) => { 165 | expect(ctx.request.rawBody).toEqual('{"foo":"bar"}'); 166 | ctx.body = ctx.request.body; 167 | }); 168 | 169 | server = app.listen(); 170 | await request(server) 171 | .post("/foo") 172 | .send(JSON.stringify({ foo: "bar" })) 173 | .expect({ '{"foo":"bar"}': "" }); 174 | }); 175 | }); 176 | }); 177 | 178 | describe("form body", () => { 179 | const app = createApp(); 180 | 181 | it("should parse form body ok", async () => { 182 | app.use(async (ctx) => { 183 | expect(ctx.request.body).toEqual({ foo: { bar: "baz" } }); 184 | expect(ctx.request.rawBody).toEqual("foo%5Bbar%5D=baz"); 185 | ctx.body = ctx.request.body; 186 | }); 187 | server = app.listen(); 188 | await request(server) 189 | .post("/") 190 | .type("form") 191 | .send({ foo: { bar: "baz" } }) 192 | .expect({ foo: { bar: "baz" } }); 193 | }); 194 | 195 | it("should parse form body reach the limit size", async () => { 196 | const app = createApp({ formLimit: 10 }); 197 | server = app.listen(); 198 | await request(server) 199 | .post("/") 200 | .type("form") 201 | .send({ foo: { bar: "bazzzzzzz" } }) 202 | .expect(413); 203 | }); 204 | }); 205 | 206 | describe("text body", () => { 207 | it("should parse text body ok", async () => { 208 | const app = createApp({ 209 | enableTypes: ["text", "json"], 210 | }); 211 | app.use(async (ctx) => { 212 | expect(ctx.request.body).toEqual("body"); 213 | expect(ctx.request.rawBody).toEqual("body"); 214 | ctx.body = ctx.request.body; 215 | }); 216 | server = app.listen(); 217 | await request(server).post("/").type("text").send("body").expect("body"); 218 | }); 219 | 220 | it("should not parse text body when disable", async () => { 221 | const app = createApp(); 222 | app.use(async (ctx) => { 223 | ctx.body = ctx.request.body; 224 | }); 225 | server = app.listen(); 226 | await request(server).post("/").type("text").send("body").expect({}); 227 | }); 228 | }); 229 | 230 | describe("xml body", () => { 231 | it("should parse xml body ok", async () => { 232 | const app = createApp({ 233 | enableTypes: ["xml"], 234 | }); 235 | app.use(async (ctx) => { 236 | expect(ctx.headers["content-type"]).toEqual("application/xml"); 237 | expect(ctx.request.body).toEqual("abc"); 238 | expect(ctx.request.rawBody).toEqual("abc"); 239 | ctx.body = ctx.request.body; 240 | }); 241 | server = app.listen(); 242 | await request(server) 243 | .post("/") 244 | .type("xml") 245 | .send("abc") 246 | .expect("abc"); 247 | }); 248 | 249 | it("should not parse text body when disable", async () => { 250 | const app = createApp(); 251 | app.use(async (ctx) => { 252 | expect(ctx.headers["content-type"]).toEqual("application/xml"); 253 | ctx.body = ctx.request.body; 254 | }); 255 | server = app.listen(); 256 | await request(server) 257 | .post("/") 258 | .type("xml") 259 | .send("abc") 260 | .expect({}); 261 | }); 262 | 263 | it("should xml body reach the limit size", async () => { 264 | const app = createApp({ 265 | enableTypes: ["xml"], 266 | xmlLimit: 10, 267 | }); 268 | app.use(async (ctx) => { 269 | expect(ctx.headers["content-type"]).toEqual("application/xml"); 270 | ctx.body = ctx.request.body; 271 | }); 272 | server = app.listen(); 273 | await request(server) 274 | .post("/") 275 | .type("xml") 276 | .send("abcdefghijklmn") 277 | .expect(413); 278 | }); 279 | }); 280 | 281 | describe("html body by text parser", () => { 282 | it("should parse html body ok", async () => { 283 | const app = createApp({ 284 | extendTypes: { 285 | text: ["text/html"], 286 | }, 287 | enableTypes: ["text"], 288 | }); 289 | app.use(async (ctx) => { 290 | expect(ctx.headers["content-type"]).toEqual("text/html"); 291 | expect(ctx.request.body).toEqual("

abc

"); 292 | expect(ctx.request.rawBody).toEqual("

abc

"); 293 | ctx.body = ctx.request.body; 294 | }); 295 | server = app.listen(); 296 | await request(server) 297 | .post("/") 298 | .type("html") 299 | .send("

abc

") 300 | .expect("

abc

"); 301 | }); 302 | 303 | it("should not parse html body when disable", async () => { 304 | const app = createApp(); 305 | app.use(async (ctx) => { 306 | expect(ctx.headers["content-type"]).toEqual("text/html"); 307 | ctx.body = ctx.request.body; 308 | }); 309 | server = app.listen(); 310 | await request(server) 311 | .post("/") 312 | .type("html") 313 | .send("

abc

") 314 | .expect({}); 315 | }); 316 | }); 317 | 318 | describe("patchNode", () => { 319 | it("should patch Node raw request with supported type", async () => { 320 | const app = createApp({ patchNode: true }); 321 | 322 | app.use(async (ctx) => { 323 | expect(ctx.request.body).toEqual({ foo: "bar" }); 324 | expect(ctx.request.rawBody).toEqual('{"foo":"bar"}'); 325 | expect(ctx.req.body).toEqual({ foo: "bar" }); 326 | expect(ctx.req.rawBody).toEqual('{"foo":"bar"}'); 327 | 328 | ctx.body = ctx.req.body; 329 | }); 330 | server = app.listen(); 331 | await request(server) 332 | .post("/") 333 | .send({ foo: "bar" }) 334 | .expect({ foo: "bar" }); 335 | }); 336 | 337 | it("should patch Node raw request with unsupported type", async () => { 338 | const app = createApp({ patchNode: true }); 339 | 340 | app.use(async (ctx) => { 341 | expect(ctx.request.body).toEqual({}); 342 | expect(ctx.request.rawBody).toEqual(undefined); 343 | expect(ctx.req.body).toEqual({}); 344 | expect(ctx.req.rawBody).toEqual(undefined); 345 | 346 | ctx.body = ctx.req.body; 347 | }); 348 | server = app.listen(); 349 | await request(server) 350 | .post("/") 351 | .type("application/x-unsupported-type") 352 | .send("x-unsupported-type") 353 | .expect({}); 354 | }); 355 | }); 356 | 357 | describe("extend type", () => { 358 | it("should extend json ok", async () => { 359 | const app = createApp({ 360 | extendTypes: { 361 | json: ["application/x-javascript"], 362 | }, 363 | }); 364 | app.use(async (ctx) => { 365 | ctx.body = ctx.request.body; 366 | }); 367 | 368 | server = app.listen(); 369 | await request(server) 370 | .post("/") 371 | .type("application/x-javascript") 372 | .send(JSON.stringify({ foo: "bar" })) 373 | .expect({ foo: "bar" }); 374 | }); 375 | 376 | it("should extend json with array ok", async () => { 377 | const app = createApp({ 378 | extendTypes: { 379 | json: ["application/x-javascript", "application/y-javascript"], 380 | }, 381 | }); 382 | app.use(async (ctx) => { 383 | ctx.body = ctx.request.body; 384 | }); 385 | 386 | server = app.listen(); 387 | await request(server) 388 | .post("/") 389 | .type("application/x-javascript") 390 | .send(JSON.stringify({ foo: "bar" })) 391 | .expect({ foo: "bar" }); 392 | }); 393 | 394 | it("should extend xml ok", async () => { 395 | const app = createApp({ 396 | enableTypes: ["xml"], 397 | extendTypes: { 398 | xml: ["application/xml-custom"], 399 | }, 400 | }); 401 | app.use(async (ctx) => { 402 | ctx.body = ctx.request.body; 403 | }); 404 | 405 | server = app.listen(); 406 | await request(server) 407 | .post("/") 408 | .type("application/xml-custom") 409 | .send("abc") 410 | .expect("abc"); 411 | }); 412 | 413 | it("should throw when pass unsupported types", () => { 414 | try { 415 | createApp({ 416 | extendTypes: { 417 | "any-other-type": ["application/any-other-type"], 418 | } as any, 419 | }); 420 | } catch (error) { 421 | expect(error instanceof UnsupportedBodyTypeError).toBe(true); 422 | } 423 | }); 424 | 425 | it("should throw when pass supported types with string value instead of array", () => { 426 | try { 427 | createApp({ 428 | extendTypes: { 429 | "any-other-type": "application/any-other-type", 430 | } as any, 431 | }); 432 | } catch (error) { 433 | expect(error instanceof UnsupportedBodyTypeError).toBe(true); 434 | } 435 | }); 436 | 437 | it("should throw when pass supported types with array contain falsy values", () => { 438 | try { 439 | createApp({ 440 | extendTypes: { 441 | json: ["", 0, false, null, undefined], 442 | } as any, 443 | }); 444 | } catch (error) { 445 | expect(error instanceof UnsupportedBodyTypeError).toBe(true); 446 | } 447 | }); 448 | }); 449 | 450 | describe("enableTypes", () => { 451 | it("should disable json success", async () => { 452 | const app = createApp({ 453 | enableTypes: ["form"], 454 | }); 455 | 456 | app.use(async (ctx) => { 457 | ctx.body = ctx.request.body; 458 | }); 459 | server = app.listen(); 460 | await request(server) 461 | .post("/") 462 | .type("json") 463 | .send({ foo: "bar" }) 464 | .expect({}); 465 | }); 466 | 467 | it("should throw when pass unsupported types", () => { 468 | try { 469 | createApp({ 470 | enableTypes: ["any-other-type" as any], 471 | }); 472 | } catch (error) { 473 | expect(error instanceof UnsupportedBodyTypeError).toBe(true); 474 | } 475 | }); 476 | }); 477 | 478 | describe("other type", () => { 479 | const app = createApp(); 480 | 481 | it("should get body null", async () => { 482 | app.use(async (ctx) => { 483 | expect(ctx.request.body).toBeUndefined(); 484 | ctx.body = ctx.request.body; 485 | }); 486 | server = app.listen(); 487 | await request(server).get("/").expect({}); 488 | }); 489 | }); 490 | 491 | describe("onError", () => { 492 | const app = createApp({ 493 | onError({}, ctx) { 494 | ctx.throw("custom parse error", 422); 495 | }, 496 | }); 497 | 498 | it("should get custom error message", async () => { 499 | app.use(async () => {}); 500 | server = app.listen(); 501 | await request(server) 502 | .post("/") 503 | .send("test") 504 | .set("content-type", "application/json") 505 | .expect(422) 506 | .expect("custom parse error"); 507 | }); 508 | }); 509 | 510 | describe("disableBodyParser", () => { 511 | it("should not parse body when disableBodyParser set to true", async () => { 512 | const app = new Koa(); 513 | app.use(async (ctx, next) => { 514 | ctx.disableBodyParser = true; 515 | await next(); 516 | }); 517 | app.use(bodyParser()); 518 | app.use(async (ctx) => { 519 | expect(undefined === ctx.request.rawBody).toEqual(true); 520 | ctx.body = ctx.request.body ? "parsed" : "empty"; 521 | }); 522 | server = app.listen(); 523 | await request(server) 524 | .post("/") 525 | .send({ foo: "bar" }) 526 | .set("content-type", "application/json") 527 | .expect(200) 528 | .expect("empty"); 529 | }); 530 | }); 531 | 532 | describe("enableRawChecking", () => { 533 | it("should override koa request with raw request body if exist and enableRawChecking is truthy", async () => { 534 | const rawParsedBody = { rawFoo: "rawBar" }; 535 | const app = createApp({ rawParsedBody, enableRawChecking: true }); 536 | app.use(async (ctx) => { 537 | ctx.body = ctx.request.body; 538 | }); 539 | 540 | server = app.listen(); 541 | await request(server) 542 | .post("/") 543 | .send({ foo: "bar" }) 544 | .expect(rawParsedBody); 545 | }); 546 | 547 | it("shouldn't override koa request with raw request body if not exist and enableRawChecking is truthy", async () => { 548 | const rawParsedBody = undefined; 549 | const app = createApp({ rawParsedBody, enableRawChecking: true }); 550 | app.use(async (ctx) => { 551 | ctx.body = ctx.request.body; 552 | }); 553 | 554 | server = app.listen(); 555 | await request(server) 556 | .post("/") 557 | .send({ foo: "bar" }) 558 | .expect({ foo: "bar" }); 559 | }); 560 | }); 561 | 562 | describe("request closed", () => { 563 | it("should return 499 on request closed", async () => { 564 | const app = new Koa(); 565 | 566 | app.use(async (ctx, next) => { 567 | Object.defineProperty(ctx.req, "closed", { value: true }); 568 | await next(); 569 | }); 570 | app.use(bodyParser()); 571 | server = app.listen(); 572 | 573 | await request(server).post("/").send({ foo: "bar" }).expect(499); 574 | }); 575 | }); 576 | }); 577 | -------------------------------------------------------------------------------- /test/test-utils.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import Koa from "koa"; 3 | 4 | import bodyParser from "../src"; 5 | import type { BodyParserOptions } from "../src/body-parser.types"; 6 | 7 | export const fixtures = path.join(__dirname, "fixtures"); 8 | type CreateAppConfig = BodyParserOptions & { 9 | rawParsedBody?: Record; 10 | }; 11 | 12 | export const createApp = (config: CreateAppConfig = {}) => { 13 | const { rawParsedBody, ...options } = config; 14 | const app = new Koa(); 15 | rawParsedBody && 16 | app.use((ctx, next) => { 17 | ctx.req.body = rawParsedBody; 18 | return next(); 19 | }); 20 | 21 | app.use(bodyParser(options)); 22 | return app; 23 | }; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": [ 6 | "dom", 7 | "es6", 8 | "es2017", 9 | "esnext.asynciterable" 10 | ], 11 | "skipLibCheck": true, 12 | "sourceMap": true, 13 | "outDir": "./dist", 14 | "moduleResolution": "node", 15 | "removeComments": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "strictFunctionTypes": true, 19 | "noImplicitThis": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noImplicitReturns": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "allowSyntheticDefaultImports": true, 25 | "esModuleInterop": true, 26 | "emitDecoratorMetadata": true, 27 | "experimentalDecorators": true, 28 | "resolveJsonModule": true, 29 | "baseUrl": ".", 30 | "types": ["jest", "node"] 31 | }, 32 | "include": [ 33 | "./src/**/*.ts" 34 | ], 35 | "exclude": [ 36 | "dist", 37 | "node_modules", 38 | "src/**/*.test.tsx" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'tsup'; 2 | 3 | const tsupConfig = defineConfig({ 4 | name: '@koa/body-parser', 5 | entry: ['src/*.ts'], 6 | target: 'esnext', 7 | format: ['cjs', 'esm'], 8 | dts: true, 9 | splitting: false, 10 | sourcemap: false, 11 | clean: true, 12 | platform: 'node', 13 | }); 14 | 15 | export default tsupConfig; 16 | --------------------------------------------------------------------------------