├── test ├── .eslintrc.yml ├── native.js ├── http.js ├── flowing.js ├── http2.js └── index.js ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── codeql.yml │ ├── scorecard.yml │ └── ci.yml ├── eslint.config.js ├── LICENSE ├── package.json ├── scripts └── version-history.js ├── index.d.ts ├── SECURITY.md ├── README.md ├── HISTORY.md └── index.js /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | mocha: true 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.tgz 2 | .nyc_output 3 | coverage 4 | node_modules 5 | npm-debug.log 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | 8 | - package-ecosystem: npm 9 | directory: / 10 | schedule: 11 | interval: monthly 12 | time: "23:00" 13 | timezone: Europe/London 14 | open-pull-requests-limit: 10 -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const neostandard = require('neostandard') 3 | 4 | module.exports = [ 5 | ...neostandard({ 6 | env: ['mocha'], 7 | }), 8 | { 9 | rules: { 10 | 'object-shorthand': ['off'], // Compatibility with older code 11 | 'no-var': ['off'], // Compatibility with older code 12 | 'no-redeclare': ['off'], // Because we use var for compatibility with node 0.10 13 | } 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2014 Jonathan Ong 4 | Copyright (c) 2014-2022 Douglas Christopher Wilson 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "raw-body", 3 | "description": "Get and validate the raw body of a readable stream.", 4 | "version": "3.0.2", 5 | "author": "Jonathan Ong (http://jongleberry.com)", 6 | "contributors": [ 7 | "Douglas Christopher Wilson ", 8 | "Raynos " 9 | ], 10 | "license": "MIT", 11 | "repository": "stream-utils/raw-body", 12 | "dependencies": { 13 | "bytes": "~3.1.2", 14 | "http-errors": "~2.0.1", 15 | "iconv-lite": "~0.7.0", 16 | "unpipe": "~1.0.0" 17 | }, 18 | "devDependencies": { 19 | "@stylistic/eslint-plugin": "^5.1.0", 20 | "@stylistic/eslint-plugin-js": "^4.1.0", 21 | "bluebird": "3.7.2", 22 | "eslint": "^9.0.0", 23 | "mocha": "10.7.0", 24 | "neostandard": "^0.12.0", 25 | "nyc": "17.0.0", 26 | "readable-stream": "2.3.7", 27 | "safe-buffer": "5.2.1" 28 | }, 29 | "engines": { 30 | "node": ">= 0.10" 31 | }, 32 | "files": [ 33 | "LICENSE", 34 | "README.md", 35 | "index.d.ts", 36 | "index.js" 37 | ], 38 | "scripts": { 39 | "lint": "eslint", 40 | "lint:fix": "eslint --fix", 41 | "test": "mocha --trace-deprecation --reporter spec --check-leaks test/", 42 | "test:ci": "nyc --reporter=lcovonly --reporter=text npm test", 43 | "test:cov": "nyc --reporter=html --reporter=text npm test", 44 | "version": "node scripts/version-history.js && git add HISTORY.md" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /scripts/version-history.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var fs = require('fs') 4 | var path = require('path') 5 | 6 | var HISTORY_FILE_PATH = path.join(__dirname, '..', 'HISTORY.md') 7 | var MD_HEADER_REGEXP = /^====*$/ 8 | var VERSION = process.env.npm_package_version 9 | var VERSION_PLACEHOLDER_REGEXP = /^(?:unreleased|(\d+\.)+x)$/ 10 | 11 | var historyFileLines = fs.readFileSync(HISTORY_FILE_PATH, 'utf-8').split('\n') 12 | 13 | if (!MD_HEADER_REGEXP.test(historyFileLines[1])) { 14 | console.error('Missing header in HISTORY.md') 15 | process.exit(1) 16 | } 17 | 18 | if (!VERSION_PLACEHOLDER_REGEXP.test(historyFileLines[0])) { 19 | console.error('Missing placeholder version in HISTORY.md') 20 | process.exit(1) 21 | } 22 | 23 | if (historyFileLines[0].indexOf('x') !== -1) { 24 | var versionCheckRegExp = new RegExp('^' + historyFileLines[0].replace('x', '.+') + '$') 25 | 26 | if (!versionCheckRegExp.test(VERSION)) { 27 | console.error('Version %s does not match placeholder %s', VERSION, historyFileLines[0]) 28 | process.exit(1) 29 | } 30 | } 31 | 32 | historyFileLines[0] = VERSION + ' / ' + getLocaleDate() 33 | historyFileLines[1] = repeat('=', historyFileLines[0].length) 34 | 35 | fs.writeFileSync(HISTORY_FILE_PATH, historyFileLines.join('\n')) 36 | 37 | function getLocaleDate () { 38 | var now = new Date() 39 | 40 | return zeroPad(now.getFullYear(), 4) + '-' + 41 | zeroPad(now.getMonth() + 1, 2) + '-' + 42 | zeroPad(now.getDate(), 2) 43 | } 44 | 45 | function repeat (str, length) { 46 | var out = '' 47 | 48 | for (var i = 0; i < length; i++) { 49 | out += str 50 | } 51 | 52 | return out 53 | } 54 | 55 | function zeroPad (number, length) { 56 | var num = number.toString() 57 | 58 | while (num.length < length) { 59 | num = '0' + num 60 | } 61 | 62 | return num 63 | } 64 | -------------------------------------------------------------------------------- /test/native.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var Buffer = require('safe-buffer').Buffer 3 | var getRawBody = require('..') 4 | var Readable = require('stream').Readable 5 | var run = Readable ? describe : describe.skip 6 | 7 | run('using native streams', function () { 8 | it('should read contents', function (done) { 9 | var stream = createStream(Buffer.from('hello, streams!')) 10 | 11 | getRawBody(stream, function (err, buf) { 12 | assert.ifError(err) 13 | assert.strictEqual(buf.toString(), 'hello, streams!') 14 | done() 15 | }) 16 | }) 17 | 18 | it('should read pre-buffered contents', function (done) { 19 | var stream = createStream(Buffer.from('hello, streams!')) 20 | stream.push('oh, ') 21 | 22 | getRawBody(stream, function (err, buf) { 23 | assert.ifError(err) 24 | assert.strictEqual(buf.toString(), 'oh, hello, streams!') 25 | done() 26 | }) 27 | }) 28 | 29 | it('should stop the stream on limit', function (done) { 30 | var stream = createStream(Buffer.from('hello, streams!')) 31 | 32 | getRawBody(stream, { limit: 2 }, function (err, buf) { 33 | assert.ok(err) 34 | assert.strictEqual(err.status, 413) 35 | assert.strictEqual(err.limit, 2) 36 | process.nextTick(done) 37 | }) 38 | }) 39 | 40 | it('should throw if stream is not readable', function (done) { 41 | var stream = createStream(Buffer.from('hello, streams!')) 42 | 43 | stream.resume() 44 | stream.on('end', function () { 45 | getRawBody(stream, function (err) { 46 | assert.ok(err) 47 | assert.strictEqual(err.status, 500) 48 | assert.strictEqual(err.type, 'stream.not.readable') 49 | assert.strictEqual(err.message, 'stream is not readable') 50 | process.nextTick(done) 51 | }) 52 | }) 53 | }) 54 | }) 55 | 56 | function createStream (buf) { 57 | var stream = new Readable() 58 | stream._read = function () { 59 | stream.push(buf) 60 | stream.push(null) 61 | } 62 | 63 | return stream 64 | } 65 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace getRawBody { 2 | export type Encoding = string | true; 3 | 4 | export interface Options { 5 | /** 6 | * The expected length of the stream. 7 | */ 8 | length?: number | string | null; 9 | /** 10 | * The byte limit of the body. This is the number of bytes or any string 11 | * format supported by `bytes`, for example `1000`, `'500kb'` or `'3mb'`. 12 | */ 13 | limit?: number | string | null; 14 | /** 15 | * The encoding to use to decode the body into a string. By default, a 16 | * `Buffer` instance will be returned when no encoding is specified. Most 17 | * likely, you want `utf-8`, so setting encoding to `true` will decode as 18 | * `utf-8`. You can use any type of encoding supported by `iconv-lite`. 19 | */ 20 | encoding?: Encoding | null; 21 | } 22 | 23 | export interface RawBodyError extends Error { 24 | /** 25 | * The limit in bytes. 26 | */ 27 | limit?: number; 28 | /** 29 | * The expected length of the stream. 30 | */ 31 | length?: number; 32 | expected?: number; 33 | /** 34 | * The received bytes. 35 | */ 36 | received?: number; 37 | /** 38 | * The encoding. 39 | */ 40 | encoding?: string; 41 | /** 42 | * The corresponding status code for the error. 43 | */ 44 | status: number; 45 | statusCode: number; 46 | /** 47 | * The error type. 48 | */ 49 | type: string; 50 | } 51 | } 52 | 53 | /** 54 | * Gets the entire buffer of a stream either as a `Buffer` or a string. 55 | * Validates the stream's length against an expected length and maximum 56 | * limit. Ideal for parsing request bodies. 57 | */ 58 | declare function getRawBody( 59 | stream: NodeJS.ReadableStream, 60 | callback: (err: getRawBody.RawBodyError, body: Buffer) => void 61 | ): void; 62 | 63 | declare function getRawBody( 64 | stream: NodeJS.ReadableStream, 65 | options: (getRawBody.Options & { encoding: getRawBody.Encoding }) | getRawBody.Encoding, 66 | callback: (err: getRawBody.RawBodyError, body: string) => void 67 | ): void; 68 | 69 | declare function getRawBody( 70 | stream: NodeJS.ReadableStream, 71 | options: getRawBody.Options, 72 | callback: (err: getRawBody.RawBodyError, body: Buffer) => void 73 | ): void; 74 | 75 | declare function getRawBody( 76 | stream: NodeJS.ReadableStream, 77 | options: (getRawBody.Options & { encoding: getRawBody.Encoding }) | getRawBody.Encoding 78 | ): Promise; 79 | 80 | declare function getRawBody( 81 | stream: NodeJS.ReadableStream, 82 | options?: getRawBody.Options 83 | ): Promise; 84 | 85 | export = getRawBody; 86 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policies and Procedures 2 | 3 | This document outlines security procedures and general policies for the raw-body 4 | project. 5 | 6 | - [Security Policies and Procedures](#security-policies-and-procedures) 7 | - [Reporting a Bug or Security Vulnerability](#reporting-a-bug-or-security-vulnerability) 8 | - [Reporting Security Bugs via GitHub Security Advisory](#reporting-security-bugs-via-github-security-advisory) 9 | - [Third-Party Modules](#third-party-modules) 10 | - [Disclosure Policy](#disclosure-policy) 11 | - [Comments on this Policy](#comments-on-this-policy) 12 | 13 | ## Reporting a Bug or Security Vulnerability 14 | 15 | The `raw-body` team and community take all security vulnerabilities seriously. 16 | Thank you for improving the security of raw-body and related projects. 17 | We appreciate your efforts in responsible disclosure and will make every effort 18 | to acknowledge your contributions. 19 | 20 | A member of the team will acknowledge your report as soon as possible. These timelines may extend when our triage volunteers are away on holiday, particularly at the end of the year. 21 | 22 | After the initial response to your report, the owners commit to keeping you informed 23 | about the progress toward a fix and the final announcement, and they may request additional 24 | information or clarification during the process. 25 | 26 | ### Reporting Security Bugs via GitHub Security Advisory 27 | 28 | The preferred way to report security vulnerabilities is through 29 | [GitHub Security Advisories](https://github.com/advisories). 30 | This allows us to collaborate on a fix while maintaining the 31 | confidentiality of the report. 32 | 33 | To report a vulnerability 34 | ([docs](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability)): 35 | 1. Visit the **Security** tab of the affected repository on GitHub. 36 | 2. Click **Report a vulnerability** and follow the provided steps. 37 | 38 | ### Third-Party Modules 39 | 40 | If the security issue pertains to a third-party module, please report it to the maintainers of that module. 41 | 42 | ## Disclosure Policy 43 | 44 | When the raw-body team receives a security bug report, they will assign it to a 45 | primary handler. This person will coordinate the fix and release process, 46 | involving the following steps: 47 | 48 | * Confirm the problem and determine the affected versions. 49 | * Audit code to find any potential similar problems. 50 | * Prepare fixes for all releases still under maintenance. These fixes will be 51 | released as fast as possible to npm. 52 | 53 | ## Comments on this Policy 54 | 55 | If you have suggestions on how this process could be improved please submit a 56 | pull request. 57 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: ["master"] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: ["master"] 20 | schedule: 21 | - cron: "0 0 * * 1" 22 | workflow_dispatch: 23 | 24 | permissions: 25 | contents: read 26 | 27 | jobs: 28 | analyze: 29 | name: Analyze 30 | runs-on: ubuntu-latest 31 | permissions: 32 | actions: read 33 | contents: read 34 | security-events: write 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | language: [javascript, actions] 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0 43 | with: 44 | persist-credentials: false 45 | 46 | # Initializes the CodeQL tools for scanning. 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v3.29.5 49 | with: 50 | languages: ${{ matrix.language }} 51 | config: | 52 | paths-ignore: 53 | - test 54 | # If you wish to specify custom queries, you can do so here or in a config file. 55 | # By default, queries listed here will override any specified in a config file. 56 | # Prefix the list here with "+" to use these queries and those in the config file. 57 | 58 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 59 | # If this step fails, then you should remove it and run the build manually (see below) 60 | # - name: Autobuild 61 | # uses: github/codeql-action/autobuild@3ab4101902695724f9365a384f86c1074d94e18c # v3.24.7 62 | 63 | # ℹ️ Command-line programs to run using the OS shell. 64 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 65 | 66 | # If the Autobuild fails above, remove it and uncomment the following three lines. 67 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 68 | 69 | # - run: | 70 | # echo "Run, Build Application using script" 71 | # ./location_of_script_within_repo/buildscript.sh 72 | 73 | - name: Perform CodeQL Analysis 74 | uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v3.29.5 -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '16 21 * * 1' 14 | push: 15 | branches: [ "master" ] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | # Uncomment the permissions below if installing in a private repository. 30 | # contents: read 31 | # actions: read 32 | 33 | steps: 34 | - name: "Checkout code" 35 | uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0 36 | with: 37 | persist-credentials: false 38 | 39 | - name: "Run analysis" 40 | uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 41 | with: 42 | results_file: results.sarif 43 | results_format: sarif 44 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 45 | # - you want to enable the Branch-Protection check on a *public* repository, or 46 | # - you are installing Scorecard on a *private* repository 47 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 48 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 49 | 50 | # Public repositories: 51 | # - Publish results to OpenSSF REST API for easy access by consumers 52 | # - Allows the repository to include the Scorecard badge. 53 | # - See https://github.com/ossf/scorecard-action#publishing-results. 54 | # For private repositories: 55 | # - `publish_results` will always be set to `false`, regardless 56 | # of the value entered here. 57 | publish_results: true 58 | 59 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 60 | # format to the repository Actions tab. 61 | - name: "Upload artifact" 62 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 63 | with: 64 | name: SARIF file 65 | path: results.sarif 66 | retention-days: 5 67 | 68 | # Upload the results to GitHub's code scanning dashboard. 69 | - name: "Upload to code-scanning" 70 | uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v3.29.5 71 | with: 72 | sarif_file: results.sarif -------------------------------------------------------------------------------- /test/http.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var getRawBody = require('..') 3 | var http = require('http') 4 | var net = require('net') 5 | 6 | describe('using http streams', function () { 7 | it('should read body streams', function (done) { 8 | var server = http.createServer(function onRequest (req, res) { 9 | getRawBody(req, { length: req.headers['content-length'] }, function (err, body) { 10 | if (err) { 11 | req.resume() 12 | res.statusCode = 500 13 | return res.end(err.message) 14 | } 15 | 16 | res.end(body) 17 | }) 18 | }) 19 | 20 | server.listen(function onListen () { 21 | var addr = server.address() 22 | var client = http.request({ method: 'POST', port: addr.port }) 23 | 24 | client.end('hello, world!') 25 | 26 | client.on('response', function onResponse (res) { 27 | getRawBody(res, { encoding: true }, function (err, str) { 28 | server.close(function onClose () { 29 | assert.ifError(err) 30 | assert.strictEqual(str, 'hello, world!') 31 | done() 32 | }) 33 | }) 34 | }) 35 | }) 36 | }) 37 | 38 | it('should throw if stream encoding is set', function (done) { 39 | var server = http.createServer(function onRequest (req, res) { 40 | req.setEncoding('utf8') 41 | getRawBody(req, { length: req.headers['content-length'] }, function (err, body) { 42 | if (err) { 43 | req.resume() 44 | res.statusCode = 500 45 | return res.end(err.message) 46 | } 47 | 48 | res.end(body) 49 | }) 50 | }) 51 | 52 | server.listen(function onListen () { 53 | var addr = server.address() 54 | var client = http.request({ method: 'POST', port: addr.port }) 55 | 56 | client.end('hello, world!') 57 | 58 | client.on('response', function onResponse (res) { 59 | getRawBody(res, { encoding: true }, function (err, str) { 60 | server.close(function onClose () { 61 | assert.ifError(err) 62 | assert.strictEqual(str, 'stream encoding should not be set') 63 | done() 64 | }) 65 | }) 66 | }) 67 | }) 68 | }) 69 | 70 | it('should throw if stream is not readable', function (done) { 71 | var server = http.createServer(function onRequest (req, res) { 72 | getRawBody(req, { length: req.headers['content-length'] }, function (err) { 73 | if (err) { 74 | req.resume() 75 | res.statusCode = 500 76 | res.end(err.message) 77 | return 78 | } 79 | 80 | getRawBody(req, { length: req.headers['content-length'] }, function (err) { 81 | if (err) { 82 | res.statusCode = 500 83 | res.end('[' + err.type + '] ' + err.message) 84 | } else { 85 | res.statusCode = 200 86 | res.end() 87 | } 88 | }) 89 | }) 90 | }) 91 | 92 | server.listen(function onListen () { 93 | var addr = server.address() 94 | var client = http.request({ method: 'POST', port: addr.port }) 95 | 96 | client.end('hello, world!') 97 | 98 | client.on('response', function onResponse (res) { 99 | getRawBody(res, { encoding: true }, function (err, str) { 100 | server.close(function onClose () { 101 | assert.ifError(err) 102 | assert.strictEqual(str, '[stream.not.readable] stream is not readable') 103 | done() 104 | }) 105 | }) 106 | }) 107 | }) 108 | }) 109 | 110 | it('should throw if connection ends', function (done) { 111 | var socket 112 | var server = http.createServer(function onRequest (req, res) { 113 | getRawBody(req, { length: req.headers['content-length'] }, function (err, body) { 114 | server.close() 115 | assert.ok(err) 116 | assert.strictEqual(err.code, 'ECONNABORTED') 117 | assert.strictEqual(err.expected, 50) 118 | assert.strictEqual(err.message, 'request aborted') 119 | assert.strictEqual(err.received, 10) 120 | assert.strictEqual(err.status, 400) 121 | assert.strictEqual(err.type, 'request.aborted') 122 | done() 123 | }) 124 | 125 | setTimeout(socket.destroy.bind(socket), 10) 126 | }) 127 | 128 | server.listen(function onListen () { 129 | socket = net.connect(server.address().port, function () { 130 | socket.write('POST / HTTP/1.0\r\n') 131 | socket.write('Connection: keep-alive\r\n') 132 | socket.write('Content-Length: 50\r\n') 133 | socket.write('\r\n') 134 | socket.write('testing...') 135 | }) 136 | }) 137 | }) 138 | }) 139 | -------------------------------------------------------------------------------- /test/flowing.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var Readable = require('readable-stream').Readable 3 | var Writable = require('readable-stream').Writable 4 | 5 | var getRawBody = require('../') 6 | 7 | var defaultLimit = 1024 * 1024 8 | 9 | // Add Promise to mocha's global list 10 | // eslint-disable-next-line no-self-assign 11 | global.Promise = global.Promise 12 | 13 | describe('stream flowing', function () { 14 | describe('when limit lower then length', function (done) { 15 | it('should stop the steam flow', function (done) { 16 | var stream = createInfiniteStream() 17 | 18 | getRawBody(stream, { 19 | limit: defaultLimit, 20 | length: defaultLimit * 2 21 | }, function (err, body) { 22 | assert.ok(err) 23 | assert.strictEqual(err.type, 'entity.too.large') 24 | assert.strictEqual(err.message, 'request entity too large') 25 | assert.strictEqual(err.statusCode, 413) 26 | assert.strictEqual(err.length, defaultLimit * 2) 27 | assert.strictEqual(err.limit, defaultLimit) 28 | assert.strictEqual(body, undefined) 29 | assert.ok(stream.isPaused) 30 | 31 | done() 32 | }) 33 | }) 34 | 35 | it('should halt flowing stream', function (done) { 36 | var stream = createInfiniteStream(true) 37 | var dest = createBlackholeStream() 38 | 39 | // pipe the stream 40 | stream.pipe(dest) 41 | 42 | getRawBody(stream, { 43 | limit: defaultLimit * 2, 44 | length: defaultLimit 45 | }, function (err, body) { 46 | assert.ok(err) 47 | assert.strictEqual(err.type, 'entity.too.large') 48 | assert.strictEqual(err.message, 'request entity too large') 49 | assert.strictEqual(err.statusCode, 413) 50 | assert.strictEqual(body, undefined) 51 | assert.ok(stream.isPaused) 52 | done() 53 | }) 54 | }) 55 | }) 56 | 57 | describe('when stream has encoding set', function (done) { 58 | it('should stop the steam flow', function (done) { 59 | var stream = createInfiniteStream() 60 | stream.setEncoding('utf8') 61 | 62 | getRawBody(stream, { 63 | limit: defaultLimit 64 | }, function (err, body) { 65 | assert.ok(err) 66 | assert.strictEqual(err.type, 'stream.encoding.set') 67 | assert.strictEqual(err.message, 'stream encoding should not be set') 68 | assert.strictEqual(err.statusCode, 500) 69 | assert.ok(stream.isPaused) 70 | 71 | done() 72 | }) 73 | }) 74 | }) 75 | 76 | describe('when stream has limit', function (done) { 77 | it('should stop the steam flow', function (done) { 78 | var stream = createInfiniteStream() 79 | 80 | getRawBody(stream, { 81 | limit: defaultLimit 82 | }, function (err, body) { 83 | assert.ok(err) 84 | assert.strictEqual(err.type, 'entity.too.large') 85 | assert.strictEqual(err.statusCode, 413) 86 | assert.ok(err.received > defaultLimit) 87 | assert.strictEqual(err.limit, defaultLimit) 88 | assert.ok(stream.isPaused) 89 | 90 | done() 91 | }) 92 | }) 93 | }) 94 | 95 | describe('when stream has limit', function (done) { 96 | it('should stop the steam flow', function (done) { 97 | var stream = createInfiniteStream() 98 | 99 | getRawBody(stream, function (err, body) { 100 | assert.ok(err) 101 | assert.strictEqual(err.message, 'BOOM') 102 | assert.ok(stream.isPaused) 103 | 104 | done() 105 | }) 106 | 107 | setTimeout(function () { 108 | stream.emit('error', new Error('BOOM')) 109 | }, 500) 110 | }) 111 | }) 112 | }) 113 | 114 | function createChunk () { 115 | var base = Math.random().toString(32) 116 | var KB_4 = 32 * 4 117 | var KB_8 = KB_4 * 2 118 | var KB_16 = KB_8 * 2 119 | var KB_64 = KB_16 * 4 120 | 121 | var rand = Math.random() 122 | if (rand < 0.25) { 123 | return repeat(base, KB_4) 124 | } else if (rand < 0.5) { 125 | return repeat(base, KB_8) 126 | } else if (rand < 0.75) { 127 | return repeat(base, KB_16) 128 | } else { 129 | return repeat(base, KB_64) 130 | } 131 | 132 | function repeat (str, num) { 133 | return new Array(num + 1).join(str) 134 | } 135 | } 136 | 137 | function createBlackholeStream () { 138 | var stream = new Writable() 139 | stream._write = function (chunk, encoding, cb) { 140 | cb() 141 | } 142 | 143 | return stream 144 | } 145 | 146 | function createInfiniteStream (paused) { 147 | var stream = new Readable() 148 | stream._read = function () { 149 | var rand = 2 + Math.floor(Math.random() * 10) 150 | 151 | setTimeout(function () { 152 | for (var i = 0; i < rand; i++) { 153 | stream.push(createChunk()) 154 | } 155 | }, 100) 156 | } 157 | 158 | // track paused state for tests 159 | stream.isPaused = false 160 | stream.on('pause', function () { this.isPaused = true }) 161 | stream.on('resume', function () { this.isPaused = false }) 162 | 163 | // immediately put the stream in flowing mode 164 | if (!paused) { 165 | stream.resume() 166 | } 167 | 168 | return stream 169 | } 170 | -------------------------------------------------------------------------------- /test/http2.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var getRawBody = require('..') 3 | var http2 = tryRequire('http2') 4 | var net = require('net') 5 | 6 | var describeHttp2 = !http2 7 | ? describe.skip 8 | : describe 9 | 10 | describeHttp2('using http2 streams', function () { 11 | it('should read from compatibility api', function (done) { 12 | var server = http2.createServer(function onRequest (req, res) { 13 | getRawBody(req, { length: req.headers['content-length'] }, function (err, body) { 14 | if (err) { 15 | req.resume() 16 | res.statusCode = 500 17 | return res.end(err.message) 18 | } 19 | 20 | res.end(body) 21 | }) 22 | }) 23 | 24 | server.listen(function onListen () { 25 | var addr = server.address() 26 | var session = http2.connect('http://localhost:' + addr.port) 27 | var request = session.request({ ':method': 'POST', ':path': '/' }) 28 | 29 | request.end('hello, world!') 30 | 31 | request.on('response', function onResponse (headers) { 32 | getRawBody(request, { encoding: true }, function (err, str) { 33 | http2close(server, session, function onClose () { 34 | assert.ifError(err) 35 | assert.strictEqual(headers[':status'], 200) 36 | assert.strictEqual(str, 'hello, world!') 37 | done() 38 | }) 39 | }) 40 | }) 41 | }) 42 | }) 43 | 44 | it('should read body streams', function (done) { 45 | var server = http2.createServer() 46 | 47 | server.on('stream', function onStream (stream, headers) { 48 | getRawBody(stream, { length: headers['content-length'] }, function (err, body) { 49 | if (err) { 50 | stream.resume() 51 | stream.respond({ ':status': 500 }) 52 | stream.end(err.message) 53 | return 54 | } 55 | 56 | stream.end(body) 57 | }) 58 | }) 59 | 60 | server.listen(function onListen () { 61 | var addr = server.address() 62 | var session = http2.connect('http://localhost:' + addr.port) 63 | var request = session.request({ ':method': 'POST', ':path': '/' }) 64 | 65 | request.end('hello, world!') 66 | 67 | request.on('response', function onResponse (headers) { 68 | getRawBody(request, { encoding: true }, function (err, str) { 69 | http2close(server, session, function onClose () { 70 | assert.ifError(err) 71 | assert.strictEqual(headers[':status'], 200) 72 | assert.strictEqual(str, 'hello, world!') 73 | done() 74 | }) 75 | }) 76 | }) 77 | }) 78 | }) 79 | 80 | it('should throw if stream encoding is set', function (done) { 81 | var server = http2.createServer(function onRequest (req, res) { 82 | req.setEncoding('utf8') 83 | getRawBody(req, { length: req.headers['content-length'] }, function (err, body) { 84 | if (err) { 85 | req.resume() 86 | res.statusCode = 500 87 | return res.end(err.message) 88 | } 89 | 90 | res.end(body) 91 | }) 92 | }) 93 | 94 | server.listen(function onListen () { 95 | var addr = server.address() 96 | var session = http2.connect('http://localhost:' + addr.port) 97 | var request = session.request({ ':method': 'POST', ':path': '/' }) 98 | 99 | request.end('hello, world!') 100 | 101 | request.on('response', function onResponse (headers) { 102 | getRawBody(request, { encoding: true }, function (err, str) { 103 | http2close(server, session, function onClose () { 104 | assert.ifError(err) 105 | assert.strictEqual(headers[':status'], 500) 106 | assert.strictEqual(str, 'stream encoding should not be set') 107 | done() 108 | }) 109 | }) 110 | }) 111 | }) 112 | }) 113 | 114 | it('should throw if connection ends', function (done) { 115 | var socket 116 | var server = http2.createServer(function onRequest (req, res) { 117 | getRawBody(req, { length: req.headers['content-length'] }, function (err, body) { 118 | server.close() 119 | assert.ok(err) 120 | assert.strictEqual(err.code, 'ECONNABORTED') 121 | assert.strictEqual(err.expected, 50) 122 | assert.strictEqual(err.message, 'request aborted') 123 | assert.strictEqual(err.received, 10) 124 | assert.strictEqual(err.status, 400) 125 | assert.strictEqual(err.type, 'request.aborted') 126 | done() 127 | }) 128 | 129 | setTimeout(socket.destroy.bind(socket), 10) 130 | }) 131 | 132 | server.listen(function onListen () { 133 | var addr = server.address() 134 | var session = http2.connect('http://localhost:' + addr.port, { 135 | createConnection: function (authority) { 136 | return (socket = net.connect(authority.port, authority.hostname)) 137 | } 138 | }) 139 | 140 | var request = session.request({ 141 | ':method': 'POST', 142 | ':path': '/', 143 | 'content-length': '50' 144 | }) 145 | 146 | request.write('testing...') 147 | }) 148 | }) 149 | }) 150 | 151 | function http2close (server, session, callback) { 152 | if (typeof session.close === 'function') { 153 | session.close(onSessionClose) 154 | } else { 155 | session.shutdown(onSessionClose) 156 | } 157 | 158 | function onServerClose () { 159 | callback() 160 | } 161 | 162 | function onSessionClose () { 163 | server.close(onServerClose) 164 | } 165 | } 166 | 167 | function tryRequire (module) { 168 | try { 169 | return require(module) 170 | } catch (e) { 171 | return undefined 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # raw-body 2 | 3 | [![NPM Version][npm-image]][npm-url] 4 | [![NPM Downloads][downloads-image]][downloads-url] 5 | [![Node.js Version][node-version-image]][node-version-url] 6 | [![Build status][github-actions-ci-image]][github-actions-ci-url] 7 | [![Test coverage][coveralls-image]][coveralls-url] 8 | 9 | Gets the entire buffer of a stream either as a `Buffer` or a string. 10 | Validates the stream's length against an expected length and maximum limit. 11 | Ideal for parsing request bodies. 12 | 13 | ## Install 14 | 15 | This is a [Node.js](https://nodejs.org/en/) module available through the 16 | [npm registry](https://www.npmjs.com/). Installation is done using the 17 | [`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): 18 | 19 | ```sh 20 | $ npm install raw-body 21 | ``` 22 | 23 | ### TypeScript 24 | 25 | This module includes a [TypeScript](https://www.typescriptlang.org/) 26 | declaration file to enable auto complete in compatible editors and type 27 | information for TypeScript projects. This module depends on the Node.js 28 | types, so install `@types/node`: 29 | 30 | ```sh 31 | $ npm install @types/node 32 | ``` 33 | 34 | ## API 35 | 36 | ```js 37 | var getRawBody = require('raw-body') 38 | ``` 39 | 40 | ### getRawBody(stream, [options], [callback]) 41 | 42 | **Returns a promise if no callback specified and global `Promise` exists.** 43 | 44 | Options: 45 | 46 | - `length` - The length of the stream. 47 | If the contents of the stream do not add up to this length, 48 | an `400` error code is returned. 49 | - `limit` - The byte limit of the body. 50 | This is the number of bytes or any string format supported by 51 | [bytes](https://www.npmjs.com/package/bytes), 52 | for example `1000`, `'500kb'` or `'3mb'`. 53 | If the body ends up being larger than this limit, 54 | a `413` error code is returned. 55 | - `encoding` - The encoding to use to decode the body into a string. 56 | By default, a `Buffer` instance will be returned when no encoding is specified. 57 | Most likely, you want `utf-8`, so setting `encoding` to `true` will decode as `utf-8`. 58 | You can use any type of encoding supported by [iconv-lite](https://www.npmjs.org/package/iconv-lite#readme). 59 | 60 | You can also pass a string in place of options to just specify the encoding. 61 | 62 | If an error occurs, the stream will be paused, everything unpiped, 63 | and you are responsible for correctly disposing the stream. 64 | For HTTP requests, you may need to finish consuming the stream if 65 | you want to keep the socket open for future requests. For streams 66 | that use file descriptors, you should `stream.destroy()` or 67 | `stream.close()` to prevent leaks. 68 | 69 | ## Errors 70 | 71 | This module creates errors depending on the error condition during reading. 72 | The error may be an error from the underlying Node.js implementation, but is 73 | otherwise an error created by this module, which has the following attributes: 74 | 75 | * `limit` - the limit in bytes 76 | * `length` and `expected` - the expected length of the stream 77 | * `received` - the received bytes 78 | * `encoding` - the invalid encoding 79 | * `status` and `statusCode` - the corresponding status code for the error 80 | * `type` - the error type 81 | 82 | ### Types 83 | 84 | The errors from this module have a `type` property which allows for the programmatic 85 | determination of the type of error returned. 86 | 87 | #### encoding.unsupported 88 | 89 | This error will occur when the `encoding` option is specified, but the value does 90 | not map to an encoding supported by the [iconv-lite](https://www.npmjs.org/package/iconv-lite#readme) 91 | module. 92 | 93 | #### entity.too.large 94 | 95 | This error will occur when the `limit` option is specified, but the stream has 96 | an entity that is larger. 97 | 98 | #### request.aborted 99 | 100 | This error will occur when the request stream is aborted by the client before 101 | reading the body has finished. 102 | 103 | #### request.size.invalid 104 | 105 | This error will occur when the `length` option is specified, but the stream has 106 | emitted more bytes. 107 | 108 | #### stream.encoding.set 109 | 110 | This error will occur when the given stream has an encoding set on it, making it 111 | a decoded stream. The stream should not have an encoding set and is expected to 112 | emit `Buffer` objects. 113 | 114 | #### stream.not.readable 115 | 116 | This error will occur when the given stream is not readable. 117 | 118 | ## Examples 119 | 120 | ### Simple Express example 121 | 122 | ```js 123 | var contentType = require('content-type') 124 | var express = require('express') 125 | var getRawBody = require('raw-body') 126 | 127 | var app = express() 128 | 129 | app.use(function (req, res, next) { 130 | getRawBody(req, { 131 | length: req.headers['content-length'], 132 | limit: '1mb', 133 | encoding: contentType.parse(req).parameters.charset 134 | }, function (err, string) { 135 | if (err) return next(err) 136 | req.text = string 137 | next() 138 | }) 139 | }) 140 | 141 | // now access req.text 142 | ``` 143 | 144 | ### Simple Koa example 145 | 146 | ```js 147 | var contentType = require('content-type') 148 | var getRawBody = require('raw-body') 149 | var koa = require('koa') 150 | 151 | var app = koa() 152 | 153 | app.use(function * (next) { 154 | this.text = yield getRawBody(this.req, { 155 | length: this.req.headers['content-length'], 156 | limit: '1mb', 157 | encoding: contentType.parse(this.req).parameters.charset 158 | }) 159 | yield next 160 | }) 161 | 162 | // now access this.text 163 | ``` 164 | 165 | ### Using as a promise 166 | 167 | To use this library as a promise, simply omit the `callback` and a promise is 168 | returned, provided that a global `Promise` is defined. 169 | 170 | ```js 171 | var getRawBody = require('raw-body') 172 | var http = require('http') 173 | 174 | var server = http.createServer(function (req, res) { 175 | getRawBody(req) 176 | .then(function (buf) { 177 | res.statusCode = 200 178 | res.end(buf.length + ' bytes submitted') 179 | }) 180 | .catch(function (err) { 181 | res.statusCode = 500 182 | res.end(err.message) 183 | }) 184 | }) 185 | 186 | server.listen(3000) 187 | ``` 188 | 189 | ### Using with TypeScript 190 | 191 | ```ts 192 | import * as getRawBody from 'raw-body'; 193 | import * as http from 'http'; 194 | 195 | const server = http.createServer((req, res) => { 196 | getRawBody(req) 197 | .then((buf) => { 198 | res.statusCode = 200; 199 | res.end(buf.length + ' bytes submitted'); 200 | }) 201 | .catch((err) => { 202 | res.statusCode = err.statusCode; 203 | res.end(err.message); 204 | }); 205 | }); 206 | 207 | server.listen(3000); 208 | ``` 209 | 210 | ## License 211 | 212 | [MIT](LICENSE) 213 | 214 | [npm-image]: https://img.shields.io/npm/v/raw-body.svg 215 | [npm-url]: https://npmjs.org/package/raw-body 216 | [node-version-image]: https://img.shields.io/node/v/raw-body.svg 217 | [node-version-url]: https://nodejs.org/en/download/ 218 | [coveralls-image]: https://img.shields.io/coveralls/stream-utils/raw-body/master.svg 219 | [coveralls-url]: https://coveralls.io/r/stream-utils/raw-body?branch=master 220 | [downloads-image]: https://img.shields.io/npm/dm/raw-body.svg 221 | [downloads-url]: https://npmjs.org/package/raw-body 222 | [github-actions-ci-image]: https://img.shields.io/github/actions/workflow/status/stream-utils/raw-body/ci.yml?branch=master&label=ci 223 | [github-actions-ci-url]: https://github.com/jshttp/stream-utils/raw-body?query=workflow%3Aci 224 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches: 9 | - master 10 | 11 | permissions: 12 | contents: read 13 | 14 | # Cancel in progress workflows 15 | # in the scenario where we already had a run going for that PR/branch/tag but then triggered a new run 16 | concurrency: 17 | group: "${{ github.workflow }} ✨ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | lint: 22 | name: Lint 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v5 26 | with: 27 | persist-credentials: false 28 | - name: Setup Node.js 29 | uses: actions/setup-node@v6 30 | with: 31 | node-version: 'lts/*' 32 | 33 | - name: Install dependencies 34 | run: npm install --ignore-scripts --include=dev 35 | 36 | - name: Run lint 37 | run: npm run lint 38 | 39 | test: 40 | runs-on: ubuntu-latest 41 | strategy: 42 | fail-fast: false 43 | matrix: 44 | name: 45 | - Node.js 0.10 46 | - Node.js 0.12 47 | - io.js 1.x 48 | - io.js 2.x 49 | - io.js 3.x 50 | - Node.js 4.x 51 | - Node.js 5.x 52 | - Node.js 6.x 53 | - Node.js 7.x 54 | - Node.js 8.0 # test early async_hooks 55 | - Node.js 8.x 56 | - Node.js 9.x 57 | - Node.js 10.x 58 | - Node.js 11.x 59 | - Node.js 12.x 60 | - Node.js 13.x 61 | - Node.js 14.x 62 | - Node.js 15.x 63 | - Node.js 16.x 64 | - Node.js 17.x 65 | - Node.js 18.x 66 | - Node.js 19.x 67 | - Node.js 20.x 68 | - Node.js 21.x 69 | - Node.js 22.x 70 | - Node.js 23.x 71 | - Node.js 24.x 72 | - Node.js 25.x 73 | 74 | include: 75 | - name: Node.js 0.10 76 | node-version: "0.10" 77 | npm-i: mocha@2.5.3 nyc@10.3.2 78 | 79 | - name: Node.js 0.12 80 | node-version: "0.12" 81 | npm-i: mocha@3.5.3 nyc@10.3.2 82 | 83 | - name: io.js 1.x 84 | node-version: "1.8" 85 | npm-i: mocha@3.5.3 nyc@10.3.2 86 | 87 | - name: io.js 2.x 88 | node-version: "2.5" 89 | npm-i: mocha@3.5.3 nyc@10.3.2 90 | 91 | - name: io.js 3.x 92 | node-version: "3.3" 93 | npm-i: mocha@3.5.3 nyc@10.3.2 94 | 95 | - name: Node.js 4.x 96 | node-version: "4.9" 97 | npm-i: mocha@5.2.0 nyc@11.9.0 98 | 99 | - name: Node.js 5.x 100 | node-version: "5.12" 101 | npm-i: mocha@5.2.0 nyc@11.9.0 102 | 103 | - name: Node.js 6.x 104 | node-version: "6.17" 105 | npm-version: "npm@3.8.6" 106 | npm-i: mocha@5.2.0 nyc@11.9.0 107 | 108 | - name: Node.js 7.x 109 | node-version: "7.10" 110 | npm-version: "npm@3.8.6" 111 | npm-i: mocha@5.2.0 nyc@11.9.0 112 | 113 | - name: Node.js 8.0 114 | node-version: "8.0" 115 | npm-i: mocha@6.2.2 nyc@14.1.1 116 | 117 | - name: Node.js 8.x 118 | node-version: "8.17" 119 | npm-i: mocha@7.2.0 nyc@14.1.1 120 | 121 | - name: Node.js 9.x 122 | node-version: "9.11" 123 | npm-i: mocha@7.2.0 nyc@14.1.1 124 | 125 | - name: Node.js 10.x 126 | node-version: "10.24" 127 | npm-i: mocha@8.4.0 nyc@15.1.0 128 | 129 | - name: Node.js 11.x 130 | node-version: "11.15" 131 | npm-i: mocha@8.4.0 nyc@15.1.0 132 | 133 | - name: Node.js 12.x 134 | node-version: "12.22" 135 | npm-i: mocha@9.2.2 nyc@15.1.0 136 | 137 | - name: Node.js 13.x 138 | node-version: "13.14" 139 | npm-i: mocha@9.2.2 nyc@15.1.0 140 | 141 | - name: Node.js 14.x 142 | node-version: "14.21" 143 | npm-i: nyc@15.1.0 144 | 145 | - name: Node.js 15.x 146 | node-version: "15.14" 147 | npm-i: nyc@15.1.0 148 | 149 | - name: Node.js 16.x 150 | node-version: "16.19" 151 | npm-i: nyc@15.1.0 152 | 153 | - name: Node.js 17.x 154 | node-version: "17.9" 155 | npm-i: nyc@15.1.0 156 | 157 | - name: Node.js 18.x 158 | node-version: "18.19" 159 | 160 | - name: Node.js 19.x 161 | node-version: "19.9" 162 | 163 | - name: Node.js 20.x 164 | node-version: "20" 165 | 166 | - name: Node.js 21.x 167 | node-version: "21" 168 | 169 | - name: Node.js 22.x 170 | node-version: "22" 171 | 172 | - name: Node.js 23.x 173 | node-version: "23" 174 | 175 | - name: Node.js 24.x 176 | node-version: "24" 177 | 178 | - name: Node.js 25.x 179 | node-version: "25" 180 | 181 | steps: 182 | - uses: actions/checkout@v5 183 | 184 | - name: Install Node.js ${{ matrix.node-version }} 185 | shell: bash -eo pipefail -l {0} 186 | run: | 187 | nvm install --default ${{ matrix.node-version }} 188 | dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" 189 | 190 | - name: Npm version fixes 191 | if: ${{matrix.npm-version != ''}} 192 | run: npm install -g ${{ matrix.npm-version }} 193 | 194 | - name: Configure npm 195 | run: | 196 | if [[ "$(npm config get package-lock)" == "true" ]]; then 197 | npm config set package-lock false 198 | else 199 | npm config set shrinkwrap false 200 | fi 201 | 202 | - name: Remove npm module(s) ${{ matrix.npm-rm }} 203 | run: npm rm --silent --save-dev neostandard @stylistic/eslint-plugin-js @stylistic/eslint-plugin eslint 204 | 205 | - name: Install npm module(s) ${{ matrix.npm-i }} 206 | run: npm install --save-dev ${{ matrix.npm-i }} 207 | if: matrix.npm-i != '' 208 | 209 | - name: Install Node.js dependencies 210 | run: npm install 211 | 212 | - name: List environment 213 | id: list_env 214 | shell: bash 215 | run: | 216 | echo "node@$(node -v)" 217 | echo "npm@$(npm -v)" 218 | npm -s ls ||: 219 | (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print $2 "=" $3 }' >> "$GITHUB_OUTPUT" 220 | 221 | - name: Run tests 222 | shell: bash 223 | run: | 224 | if npm -ps ls nyc | grep -q nyc; then 225 | npm run test:ci 226 | cp coverage/lcov.info "coverage/${{ matrix.name }}.lcov" 227 | else 228 | npm test 229 | fi 230 | 231 | - name: Upload code coverage 232 | uses: actions/upload-artifact@v5 233 | with: 234 | name: coverage-node-${{ matrix.node-version }}-${{ matrix.os }} 235 | path: ./coverage/lcov.info 236 | retention-days: 1 237 | 238 | coverage: 239 | needs: test 240 | runs-on: ubuntu-latest 241 | steps: 242 | - uses: actions/checkout@v5 243 | 244 | - name: Install lcov 245 | shell: bash 246 | run: sudo apt-get -y install lcov 247 | 248 | - name: Collect coverage reports 249 | uses: actions/download-artifact@v6 250 | with: 251 | path: ./coverage 252 | pattern: coverage-node-* 253 | 254 | - name: Merge coverage reports 255 | shell: bash 256 | run: find ./coverage -name lcov.info -exec printf '-a %q\n' {} \; | xargs lcov -o ./lcov.info 257 | 258 | - name: Upload coverage report 259 | uses: coverallsapp/github-action@v2 260 | with: 261 | file: ./lcov.info 262 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | 3.0.2 / 2025-11-21 2 | ====================== 3 | 4 | * deps: http-errors@2.0.1 5 | * deps: use tilde notation for dependencies 6 | 7 | 3.0.1 / 2025-09-03 8 | ================== 9 | 10 | * deps: iconv-lite@0.7.0 11 | - Avoid false positives in encodingExists by using objects without a prototype 12 | - Remove compatibility check for StringDecoder.end method 13 | * Fix the engines field to reflect support for Node >= 0.10 14 | 15 | 3.0.0 / 2024-07-25 16 | ================== 17 | 18 | * deps: iconv-lite@0.6.3 19 | - Fix HKSCS encoding to prefer Big5 codes 20 | - Fix minor issue in UTF-32 decoder's endianness detection code 21 | - Update 'gb18030' encoding to :2005 edition 22 | 23 | 3.0.0-beta.1 / 2023-02-21 24 | ========================= 25 | 26 | * Change TypeScript argument to `NodeJS.ReadableStream` interface 27 | * Drop support for Node.js 0.8 28 | * deps: iconv-lite@0.5.2 29 | - Add encoding cp720 30 | - Add encoding UTF-32 31 | 32 | 2.5.2 / 2023-02-21 33 | ================== 34 | 35 | * Fix error message for non-stream argument 36 | 37 | 2.5.1 / 2022-02-28 38 | ================== 39 | 40 | * Fix error on early async hooks implementations 41 | 42 | 2.5.0 / 2022-02-21 43 | ================== 44 | 45 | * Prevent loss of async hooks context 46 | * Prevent hanging when stream is not readable 47 | * deps: http-errors@2.0.0 48 | - deps: depd@2.0.0 49 | - deps: statuses@2.0.1 50 | 51 | 2.4.3 / 2022-02-14 52 | ================== 53 | 54 | * deps: bytes@3.1.2 55 | 56 | 2.4.2 / 2021-11-16 57 | ================== 58 | 59 | * deps: bytes@3.1.1 60 | * deps: http-errors@1.8.1 61 | - deps: setprototypeof@1.2.0 62 | - deps: toidentifier@1.0.1 63 | 64 | 2.4.1 / 2019-06-25 65 | ================== 66 | 67 | * deps: http-errors@1.7.3 68 | - deps: inherits@2.0.4 69 | 70 | 2.4.0 / 2019-04-17 71 | ================== 72 | 73 | * deps: bytes@3.1.0 74 | - Add petabyte (`pb`) support 75 | * deps: http-errors@1.7.2 76 | - Set constructor name when possible 77 | - deps: setprototypeof@1.1.1 78 | - deps: statuses@'>= 1.5.0 < 2' 79 | * deps: iconv-lite@0.4.24 80 | - Added encoding MIK 81 | 82 | 2.3.3 / 2018-05-08 83 | ================== 84 | 85 | * deps: http-errors@1.6.3 86 | - deps: depd@~1.1.2 87 | - deps: setprototypeof@1.1.0 88 | - deps: statuses@'>= 1.3.1 < 2' 89 | * deps: iconv-lite@0.4.23 90 | - Fix loading encoding with year appended 91 | - Fix deprecation warnings on Node.js 10+ 92 | 93 | 2.3.2 / 2017-09-09 94 | ================== 95 | 96 | * deps: iconv-lite@0.4.19 97 | - Fix ISO-8859-1 regression 98 | - Update Windows-1255 99 | 100 | 2.3.1 / 2017-09-07 101 | ================== 102 | 103 | * deps: bytes@3.0.0 104 | * deps: http-errors@1.6.2 105 | - deps: depd@1.1.1 106 | * perf: skip buffer decoding on overage chunk 107 | 108 | 2.3.0 / 2017-08-04 109 | ================== 110 | 111 | * Add TypeScript definitions 112 | * Use `http-errors` for standard emitted errors 113 | * deps: bytes@2.5.0 114 | * deps: iconv-lite@0.4.18 115 | - Add support for React Native 116 | - Add a warning if not loaded as utf-8 117 | - Fix CESU-8 decoding in Node.js 8 118 | - Improve speed of ISO-8859-1 encoding 119 | 120 | 2.2.0 / 2017-01-02 121 | ================== 122 | 123 | * deps: iconv-lite@0.4.15 124 | - Added encoding MS-31J 125 | - Added encoding MS-932 126 | - Added encoding MS-936 127 | - Added encoding MS-949 128 | - Added encoding MS-950 129 | - Fix GBK/GB18030 handling of Euro character 130 | 131 | 2.1.7 / 2016-06-19 132 | ================== 133 | 134 | * deps: bytes@2.4.0 135 | * perf: remove double-cleanup on happy path 136 | 137 | 2.1.6 / 2016-03-07 138 | ================== 139 | 140 | * deps: bytes@2.3.0 141 | - Drop partial bytes on all parsed units 142 | - Fix parsing byte string that looks like hex 143 | 144 | 2.1.5 / 2015-11-30 145 | ================== 146 | 147 | * deps: bytes@2.2.0 148 | * deps: iconv-lite@0.4.13 149 | 150 | 2.1.4 / 2015-09-27 151 | ================== 152 | 153 | * Fix masking critical errors from `iconv-lite` 154 | * deps: iconv-lite@0.4.12 155 | - Fix CESU-8 decoding in Node.js 4.x 156 | 157 | 2.1.3 / 2015-09-12 158 | ================== 159 | 160 | * Fix sync callback when attaching data listener causes sync read 161 | - Node.js 0.10 compatibility issue 162 | 163 | 2.1.2 / 2015-07-05 164 | ================== 165 | 166 | * Fix error stack traces to skip `makeError` 167 | * deps: iconv-lite@0.4.11 168 | - Add encoding CESU-8 169 | 170 | 2.1.1 / 2015-06-14 171 | ================== 172 | 173 | * Use `unpipe` module for unpiping requests 174 | 175 | 2.1.0 / 2015-05-28 176 | ================== 177 | 178 | * deps: iconv-lite@0.4.10 179 | - Improved UTF-16 endianness detection 180 | - Leading BOM is now removed when decoding 181 | - The encoding UTF-16 without BOM now defaults to UTF-16LE when detection fails 182 | 183 | 2.0.2 / 2015-05-21 184 | ================== 185 | 186 | * deps: bytes@2.1.0 187 | - Slight optimizations 188 | 189 | 2.0.1 / 2015-05-10 190 | ================== 191 | 192 | * Fix a false-positive when unpiping in Node.js 0.8 193 | 194 | 2.0.0 / 2015-05-08 195 | ================== 196 | 197 | * Return a promise without callback instead of thunk 198 | * deps: bytes@2.0.1 199 | - units no longer case sensitive when parsing 200 | 201 | 1.3.4 / 2015-04-15 202 | ================== 203 | 204 | * Fix hanging callback if request aborts during read 205 | * deps: iconv-lite@0.4.8 206 | - Add encoding alias UNICODE-1-1-UTF-7 207 | 208 | 1.3.3 / 2015-02-08 209 | ================== 210 | 211 | * deps: iconv-lite@0.4.7 212 | - Gracefully support enumerables on `Object.prototype` 213 | 214 | 1.3.2 / 2015-01-20 215 | ================== 216 | 217 | * deps: iconv-lite@0.4.6 218 | - Fix rare aliases of single-byte encodings 219 | 220 | 1.3.1 / 2014-11-21 221 | ================== 222 | 223 | * deps: iconv-lite@0.4.5 224 | - Fix Windows-31J and X-SJIS encoding support 225 | 226 | 1.3.0 / 2014-07-20 227 | ================== 228 | 229 | * Fully unpipe the stream on error 230 | - Fixes `Cannot switch to old mode now` error on Node.js 0.10+ 231 | 232 | 1.2.3 / 2014-07-20 233 | ================== 234 | 235 | * deps: iconv-lite@0.4.4 236 | - Added encoding UTF-7 237 | 238 | 1.2.2 / 2014-06-19 239 | ================== 240 | 241 | * Send invalid encoding error to callback 242 | 243 | 1.2.1 / 2014-06-15 244 | ================== 245 | 246 | * deps: iconv-lite@0.4.3 247 | - Added encodings UTF-16BE and UTF-16 with BOM 248 | 249 | 1.2.0 / 2014-06-13 250 | ================== 251 | 252 | * Passing string as `options` interpreted as encoding 253 | * Support all encodings from `iconv-lite` 254 | 255 | 1.1.7 / 2014-06-12 256 | ================== 257 | 258 | * use `string_decoder` module from npm 259 | 260 | 1.1.6 / 2014-05-27 261 | ================== 262 | 263 | * check encoding for old streams1 264 | * support node.js < 0.10.6 265 | 266 | 1.1.5 / 2014-05-14 267 | ================== 268 | 269 | * bump bytes 270 | 271 | 1.1.4 / 2014-04-19 272 | ================== 273 | 274 | * allow true as an option 275 | * bump bytes 276 | 277 | 1.1.3 / 2014-03-02 278 | ================== 279 | 280 | * fix case when length=null 281 | 282 | 1.1.2 / 2013-12-01 283 | ================== 284 | 285 | * be less strict on state.encoding check 286 | 287 | 1.1.1 / 2013-11-27 288 | ================== 289 | 290 | * add engines 291 | 292 | 1.1.0 / 2013-11-27 293 | ================== 294 | 295 | * add err.statusCode and err.type 296 | * allow for encoding option to be true 297 | * pause the stream instead of dumping on error 298 | * throw if the stream's encoding is set 299 | 300 | 1.0.1 / 2013-11-19 301 | ================== 302 | 303 | * dont support streams1, throw if dev set encoding 304 | 305 | 1.0.0 / 2013-11-17 306 | ================== 307 | 308 | * rename `expected` option to `length` 309 | 310 | 0.2.0 / 2013-11-15 311 | ================== 312 | 313 | * republish 314 | 315 | 0.1.1 / 2013-11-15 316 | ================== 317 | 318 | * use bytes 319 | 320 | 0.1.0 / 2013-11-11 321 | ================== 322 | 323 | * generator support 324 | 325 | 0.0.3 / 2013-10-10 326 | ================== 327 | 328 | * update repo 329 | 330 | 0.0.2 / 2013-09-14 331 | ================== 332 | 333 | * dump stream on bad headers 334 | * listen to events after defining received and buffers 335 | 336 | 0.0.1 / 2013-09-14 337 | ================== 338 | 339 | * Initial release 340 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * raw-body 3 | * Copyright(c) 2013-2014 Jonathan Ong 4 | * Copyright(c) 2014-2022 Douglas Christopher Wilson 5 | * MIT Licensed 6 | */ 7 | 8 | 'use strict' 9 | 10 | /** 11 | * Module dependencies. 12 | * @private 13 | */ 14 | 15 | var asyncHooks = tryRequireAsyncHooks() 16 | var bytes = require('bytes') 17 | var createError = require('http-errors') 18 | var iconv = require('iconv-lite') 19 | var unpipe = require('unpipe') 20 | 21 | /** 22 | * Module exports. 23 | * @public 24 | */ 25 | 26 | module.exports = getRawBody 27 | 28 | /** 29 | * Module variables. 30 | * @private 31 | */ 32 | 33 | var ICONV_ENCODING_MESSAGE_REGEXP = /^Encoding not recognized: / 34 | 35 | /** 36 | * Get the decoder for a given encoding. 37 | * 38 | * @param {string} encoding 39 | * @private 40 | */ 41 | 42 | function getDecoder (encoding) { 43 | if (!encoding) return null 44 | 45 | try { 46 | return iconv.getDecoder(encoding) 47 | } catch (e) { 48 | // error getting decoder 49 | if (!ICONV_ENCODING_MESSAGE_REGEXP.test(e.message)) throw e 50 | 51 | // the encoding was not found 52 | throw createError(415, 'specified encoding unsupported', { 53 | encoding: encoding, 54 | type: 'encoding.unsupported' 55 | }) 56 | } 57 | } 58 | 59 | /** 60 | * Get the raw body of a stream (typically HTTP). 61 | * 62 | * @param {object} stream 63 | * @param {object|string|function} [options] 64 | * @param {function} [callback] 65 | * @public 66 | */ 67 | 68 | function getRawBody (stream, options, callback) { 69 | var done = callback 70 | var opts = options || {} 71 | 72 | // light validation 73 | if (stream === undefined) { 74 | throw new TypeError('argument stream is required') 75 | } else if (typeof stream !== 'object' || stream === null || typeof stream.on !== 'function') { 76 | throw new TypeError('argument stream must be a stream') 77 | } 78 | 79 | if (options === true || typeof options === 'string') { 80 | // short cut for encoding 81 | opts = { 82 | encoding: options 83 | } 84 | } 85 | 86 | if (typeof options === 'function') { 87 | done = options 88 | opts = {} 89 | } 90 | 91 | // validate callback is a function, if provided 92 | if (done !== undefined && typeof done !== 'function') { 93 | throw new TypeError('argument callback must be a function') 94 | } 95 | 96 | // require the callback without promises 97 | if (!done && !global.Promise) { 98 | throw new TypeError('argument callback is required') 99 | } 100 | 101 | // get encoding 102 | var encoding = opts.encoding !== true 103 | ? opts.encoding 104 | : 'utf-8' 105 | 106 | // convert the limit to an integer 107 | var limit = bytes.parse(opts.limit) 108 | 109 | // convert the expected length to an integer 110 | var length = opts.length != null && !isNaN(opts.length) 111 | ? parseInt(opts.length, 10) 112 | : null 113 | 114 | if (done) { 115 | // classic callback style 116 | return readStream(stream, encoding, length, limit, wrap(done)) 117 | } 118 | 119 | return new Promise(function executor (resolve, reject) { 120 | readStream(stream, encoding, length, limit, function onRead (err, buf) { 121 | if (err) return reject(err) 122 | resolve(buf) 123 | }) 124 | }) 125 | } 126 | 127 | /** 128 | * Halt a stream. 129 | * 130 | * @param {Object} stream 131 | * @private 132 | */ 133 | 134 | function halt (stream) { 135 | // unpipe everything from the stream 136 | unpipe(stream) 137 | 138 | // pause stream 139 | if (typeof stream.pause === 'function') { 140 | stream.pause() 141 | } 142 | } 143 | 144 | /** 145 | * Read the data from the stream. 146 | * 147 | * @param {object} stream 148 | * @param {string} encoding 149 | * @param {number} length 150 | * @param {number} limit 151 | * @param {function} callback 152 | * @public 153 | */ 154 | 155 | function readStream (stream, encoding, length, limit, callback) { 156 | var complete = false 157 | var sync = true 158 | 159 | // check the length and limit options. 160 | // note: we intentionally leave the stream paused, 161 | // so users should handle the stream themselves. 162 | if (limit !== null && length !== null && length > limit) { 163 | return done(createError(413, 'request entity too large', { 164 | expected: length, 165 | length: length, 166 | limit: limit, 167 | type: 'entity.too.large' 168 | })) 169 | } 170 | 171 | // streams1: assert request encoding is buffer. 172 | // streams2+: assert the stream encoding is buffer. 173 | // stream._decoder: streams1 174 | // state.encoding: streams2 175 | // state.decoder: streams2, specifically < 0.10.6 176 | var state = stream._readableState 177 | if (stream._decoder || (state && (state.encoding || state.decoder))) { 178 | // developer error 179 | return done(createError(500, 'stream encoding should not be set', { 180 | type: 'stream.encoding.set' 181 | })) 182 | } 183 | 184 | if (typeof stream.readable !== 'undefined' && !stream.readable) { 185 | return done(createError(500, 'stream is not readable', { 186 | type: 'stream.not.readable' 187 | })) 188 | } 189 | 190 | var received = 0 191 | var decoder 192 | 193 | try { 194 | decoder = getDecoder(encoding) 195 | } catch (err) { 196 | return done(err) 197 | } 198 | 199 | var buffer = decoder 200 | ? '' 201 | : [] 202 | 203 | // attach listeners 204 | stream.on('aborted', onAborted) 205 | stream.on('close', cleanup) 206 | stream.on('data', onData) 207 | stream.on('end', onEnd) 208 | stream.on('error', onEnd) 209 | 210 | // mark sync section complete 211 | sync = false 212 | 213 | function done () { 214 | var args = new Array(arguments.length) 215 | 216 | // copy arguments 217 | for (var i = 0; i < args.length; i++) { 218 | args[i] = arguments[i] 219 | } 220 | 221 | // mark complete 222 | complete = true 223 | 224 | if (sync) { 225 | process.nextTick(invokeCallback) 226 | } else { 227 | invokeCallback() 228 | } 229 | 230 | function invokeCallback () { 231 | cleanup() 232 | 233 | if (args[0]) { 234 | // halt the stream on error 235 | halt(stream) 236 | } 237 | 238 | callback.apply(null, args) 239 | } 240 | } 241 | 242 | function onAborted () { 243 | if (complete) return 244 | 245 | done(createError(400, 'request aborted', { 246 | code: 'ECONNABORTED', 247 | expected: length, 248 | length: length, 249 | received: received, 250 | type: 'request.aborted' 251 | })) 252 | } 253 | 254 | function onData (chunk) { 255 | if (complete) return 256 | 257 | received += chunk.length 258 | 259 | if (limit !== null && received > limit) { 260 | done(createError(413, 'request entity too large', { 261 | limit: limit, 262 | received: received, 263 | type: 'entity.too.large' 264 | })) 265 | } else if (decoder) { 266 | buffer += decoder.write(chunk) 267 | } else { 268 | buffer.push(chunk) 269 | } 270 | } 271 | 272 | function onEnd (err) { 273 | if (complete) return 274 | if (err) return done(err) 275 | 276 | if (length !== null && received !== length) { 277 | done(createError(400, 'request size did not match content length', { 278 | expected: length, 279 | length: length, 280 | received: received, 281 | type: 'request.size.invalid' 282 | })) 283 | } else { 284 | var string = decoder 285 | ? buffer + (decoder.end() || '') 286 | : Buffer.concat(buffer) 287 | done(null, string) 288 | } 289 | } 290 | 291 | function cleanup () { 292 | buffer = null 293 | 294 | stream.removeListener('aborted', onAborted) 295 | stream.removeListener('data', onData) 296 | stream.removeListener('end', onEnd) 297 | stream.removeListener('error', onEnd) 298 | stream.removeListener('close', cleanup) 299 | } 300 | } 301 | 302 | /** 303 | * Try to require async_hooks 304 | * @private 305 | */ 306 | 307 | function tryRequireAsyncHooks () { 308 | try { 309 | return require('async_hooks') 310 | } catch (e) { 311 | return {} 312 | } 313 | } 314 | 315 | /** 316 | * Wrap function with async resource, if possible. 317 | * AsyncResource.bind static method backported. 318 | * @private 319 | */ 320 | 321 | function wrap (fn) { 322 | var res 323 | 324 | // create anonymous resource 325 | if (asyncHooks.AsyncResource) { 326 | res = new asyncHooks.AsyncResource(fn.name || 'bound-anonymous-fn') 327 | } 328 | 329 | // incompatible node.js 330 | if (!res || !res.runInAsyncScope) { 331 | return fn 332 | } 333 | 334 | // return bound function 335 | return res.runInAsyncScope.bind(res, fn, null) 336 | } 337 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var asyncHooks = tryRequire('async_hooks') 3 | var fs = require('fs') 4 | var getRawBody = require('..') 5 | var path = require('path') 6 | 7 | var Buffer = require('safe-buffer').Buffer 8 | var EventEmitter = require('events').EventEmitter 9 | var Promise = global.Promise || require('bluebird') 10 | var Readable = require('readable-stream').Readable 11 | 12 | var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function' 13 | ? describe 14 | : describe.skip 15 | 16 | var file = path.join(__dirname, 'index.js') 17 | var length = fs.statSync(file).size 18 | var string = fs.readFileSync(file, 'utf8') 19 | 20 | // Add Promise to mocha's global list 21 | // eslint-disable-next-line no-self-assign 22 | global.Promise = global.Promise 23 | 24 | describe('Raw Body', function () { 25 | it('should validate stream', function () { 26 | assert.throws(function () { getRawBody() }, /argument stream is required/) 27 | assert.throws(function () { getRawBody(null) }, /argument stream must be a stream/) 28 | assert.throws(function () { getRawBody(42) }, /argument stream must be a stream/) 29 | assert.throws(function () { getRawBody('str') }, /argument stream must be a stream/) 30 | assert.throws(function () { getRawBody({}) }, /argument stream must be a stream/) 31 | }) 32 | 33 | it('should work without any options', function (done) { 34 | getRawBody(createStream(), function (err, buf) { 35 | assert.ifError(err) 36 | checkBuffer(buf) 37 | done() 38 | }) 39 | }) 40 | 41 | it('should work with `true` as an option', function (done) { 42 | getRawBody(createStream(), true, function (err, buf) { 43 | assert.ifError(err) 44 | assert.strictEqual(typeof buf, 'string') 45 | done() 46 | }) 47 | }) 48 | 49 | it('should error for bad callback', function () { 50 | assert.throws(function () { 51 | getRawBody(createStream(), true, 'silly') 52 | }, /argument callback.*function/) 53 | }) 54 | 55 | it('should work with length', function (done) { 56 | getRawBody(createStream(), { 57 | length: length 58 | }, function (err, buf) { 59 | assert.ifError(err) 60 | checkBuffer(buf) 61 | done() 62 | }) 63 | }) 64 | 65 | it('should work when length=0', function (done) { 66 | var stream = new EventEmitter() 67 | 68 | getRawBody(stream, { 69 | length: 0, 70 | encoding: true 71 | }, function (err, str) { 72 | assert.ifError(err) 73 | assert.strictEqual(str, '') 74 | done() 75 | }) 76 | 77 | process.nextTick(function () { 78 | stream.emit('end') 79 | }) 80 | }) 81 | 82 | it('should work with limit', function (done) { 83 | getRawBody(createStream(), { 84 | limit: length + 1 85 | }, function (err, buf) { 86 | assert.ifError(err) 87 | checkBuffer(buf) 88 | done() 89 | }) 90 | }) 91 | 92 | it('should work with limit as a string', function (done) { 93 | getRawBody(createStream(), { 94 | limit: '1gb' 95 | }, function (err, buf) { 96 | assert.ifError(err) 97 | checkBuffer(buf) 98 | done() 99 | }) 100 | }) 101 | 102 | it('should work with limit and length', function (done) { 103 | getRawBody(createStream(), { 104 | length: length, 105 | limit: length + 1 106 | }, function (err, buf) { 107 | assert.ifError(err) 108 | checkBuffer(buf) 109 | done() 110 | }) 111 | }) 112 | 113 | it('should check options for limit and length', function (done) { 114 | getRawBody(createStream(), { 115 | length: length, 116 | limit: length - 1 117 | }, function (err, buf) { 118 | assert.strictEqual(err.status, 413) 119 | assert.strictEqual(err.statusCode, 413) 120 | assert.strictEqual(err.expected, length) 121 | assert.strictEqual(err.length, length) 122 | assert.strictEqual(err.limit, length - 1) 123 | assert.strictEqual(err.type, 'entity.too.large') 124 | assert.strictEqual(err.message, 'request entity too large') 125 | done() 126 | }) 127 | }) 128 | 129 | it('should work with an empty stream', function (done) { 130 | var stream = new Readable() 131 | stream.push(null) 132 | 133 | getRawBody(stream, { 134 | length: 0, 135 | limit: 1 136 | }, function (err, buf) { 137 | assert.ifError(err) 138 | assert.strictEqual(buf.length, 0) 139 | done() 140 | }) 141 | 142 | stream.emit('end') 143 | }) 144 | 145 | it('should throw on empty string and incorrect length', function (done) { 146 | var stream = new Readable() 147 | stream.push(null) 148 | 149 | getRawBody(stream, { 150 | length: 1, 151 | limit: 2 152 | }, function (err, buf) { 153 | assert.strictEqual(err.status, 400) 154 | done() 155 | }) 156 | 157 | stream.emit('end') 158 | }) 159 | 160 | it('should throw if length > limit', function (done) { 161 | getRawBody(createStream(), { 162 | limit: length - 1 163 | }, function (err, buf) { 164 | assert.strictEqual(err.status, 413) 165 | done() 166 | }) 167 | }) 168 | 169 | it('should throw if incorrect length supplied', function (done) { 170 | getRawBody(createStream(), { 171 | length: length - 1 172 | }, function (err, buf) { 173 | assert.strictEqual(err.status, 400) 174 | done() 175 | }) 176 | }) 177 | 178 | it('should work with if length is null', function (done) { 179 | getRawBody(createStream(), { 180 | length: null, 181 | limit: length + 1 182 | }, function (err, buf) { 183 | assert.ifError(err) 184 | checkBuffer(buf) 185 | done() 186 | }) 187 | }) 188 | 189 | it('should handle length as string number', function (done) { 190 | var testData = 'test' 191 | var expectedLength = testData.length 192 | 193 | var stream = new Readable() 194 | stream.push(testData) 195 | stream.push(null) 196 | 197 | getRawBody(stream, { 198 | length: String(expectedLength) 199 | }, function (err, buf) { 200 | assert.ifError(err) 201 | assert.ok(buf) 202 | assert.strictEqual(buf.length, expectedLength) 203 | done() 204 | }) 205 | }) 206 | 207 | it('should work with {"test":"å"}', function (done) { 208 | // https://github.com/visionmedia/express/issues/1816 209 | 210 | var stream = new Readable() 211 | stream.push('{"test":"å"}') 212 | stream.push(null) 213 | 214 | getRawBody(stream, { 215 | length: 13 216 | }, function (err, buf) { 217 | if (err) return done(err) 218 | assert.ok(buf) 219 | assert.strictEqual(buf.length, 13) 220 | done() 221 | }) 222 | }) 223 | 224 | it('should throw if stream encoding is set', function (done) { 225 | var stream = new Readable() 226 | stream.push('akl;sdjfklajsdfkljasdf') 227 | stream.push(null) 228 | stream.setEncoding('utf8') 229 | 230 | getRawBody(stream, function (err, buf) { 231 | assert.strictEqual(err.status, 500) 232 | done() 233 | }) 234 | }) 235 | 236 | it('should throw when given an invalid encoding', function (done) { 237 | var stream = new Readable() 238 | stream.push('akl;sdjfklajsdfkljasdf') 239 | stream.push(null) 240 | 241 | getRawBody(stream, 'akljsdflkajsdf', function (err) { 242 | assert.ok(err) 243 | assert.strictEqual(err.message, 'specified encoding unsupported') 244 | assert.strictEqual(err.status, 415) 245 | assert.strictEqual(err.type, 'encoding.unsupported') 246 | done() 247 | }) 248 | }) 249 | 250 | describe('with global Promise', function () { 251 | before(function () { 252 | global.Promise = Promise 253 | }) 254 | 255 | after(function () { 256 | global.Promise = undefined 257 | }) 258 | 259 | it('should work as a promise', function () { 260 | return getRawBody(createStream()) 261 | .then(checkBuffer) 262 | }) 263 | 264 | it('should work as a promise when length > limit', function () { 265 | return getRawBody(createStream(), { 266 | length: length, 267 | limit: length - 1 268 | }).then(throwExpectedError, function (err) { 269 | assert.strictEqual(err.status, 413) 270 | }) 271 | }) 272 | }) 273 | 274 | describe('without global Promise', function () { 275 | before(function () { 276 | global.Promise = undefined 277 | }) 278 | 279 | after(function () { 280 | global.Promise = Promise 281 | }) 282 | 283 | it('should error without callback', function () { 284 | assert.throws(function () { 285 | getRawBody(createStream()) 286 | }, /argument callback.*required/) 287 | }) 288 | 289 | it('should work with callback as second argument', function (done) { 290 | getRawBody(createStream(), function (err, buf) { 291 | assert.ifError(err) 292 | checkBuffer(buf) 293 | done() 294 | }) 295 | }) 296 | 297 | it('should work with callback as third argument', function (done) { 298 | getRawBody(createStream(), true, function (err, str) { 299 | assert.ifError(err) 300 | checkString(str) 301 | done() 302 | }) 303 | }) 304 | }) 305 | 306 | describeAsyncHooks('with async local storage', function () { 307 | it('should presist store in callback', function (done) { 308 | var asyncLocalStorage = new asyncHooks.AsyncLocalStorage() 309 | var store = { foo: 'bar' } 310 | var stream = createStream() 311 | 312 | asyncLocalStorage.run(store, function () { 313 | getRawBody(stream, function (err, buf) { 314 | if (err) return done(err) 315 | assert.ok(buf.length > 0) 316 | assert.strictEqual(asyncLocalStorage.getStore().foo, 'bar') 317 | done() 318 | }) 319 | }) 320 | }) 321 | 322 | it('should presist store in promise', function (done) { 323 | var asyncLocalStorage = new asyncHooks.AsyncLocalStorage() 324 | var store = { foo: 'bar' } 325 | var stream = createStream() 326 | 327 | asyncLocalStorage.run(store, function () { 328 | getRawBody(stream).then(function (buf) { 329 | assert.ok(buf.length > 0) 330 | assert.strictEqual(asyncLocalStorage.getStore().foo, 'bar') 331 | done() 332 | }, done) 333 | }) 334 | }) 335 | }) 336 | 337 | describe('when an encoding is set', function () { 338 | it('should return a string', function (done) { 339 | getRawBody(createStream(), { 340 | encoding: 'utf-8' 341 | }, function (err, str) { 342 | assert.ifError(err) 343 | assert.strictEqual(str, string) 344 | done() 345 | }) 346 | }) 347 | 348 | it('should handle encoding true as utf-8', function (done) { 349 | getRawBody(createStream(), { 350 | encoding: true 351 | }, function (err, str) { 352 | assert.ifError(err) 353 | assert.strictEqual(str, string) 354 | done() 355 | }) 356 | }) 357 | 358 | it('should handle encoding as options string', function (done) { 359 | getRawBody(createStream(), 'utf-8', function (err, str) { 360 | assert.ifError(err) 361 | assert.strictEqual(str, string) 362 | done() 363 | }) 364 | }) 365 | 366 | it('should decode codepage string', function (done) { 367 | var stream = createStream(Buffer.from('bf43f36d6f20657374e1733f', 'hex')) 368 | var string = '¿Cómo estás?' 369 | getRawBody(stream, 'iso-8859-1', function (err, str) { 370 | assert.ifError(err) 371 | assert.strictEqual(str, string) 372 | done() 373 | }) 374 | }) 375 | 376 | it('should decode UTF-8 string', function (done) { 377 | var stream = createStream(Buffer.from('c2bf43c3b36d6f20657374c3a1733f', 'hex')) 378 | var string = '¿Cómo estás?' 379 | getRawBody(stream, 'utf-8', function (err, str) { 380 | assert.ifError(err) 381 | assert.strictEqual(str, string) 382 | done() 383 | }) 384 | }) 385 | 386 | it('should decode UTF-16 string (LE BOM)', function (done) { 387 | // BOM makes this LE 388 | var stream = createStream(Buffer.from('fffebf004300f3006d006f002000650073007400e10073003f00', 'hex')) 389 | var string = '¿Cómo estás?' 390 | getRawBody(stream, 'utf-16', function (err, str) { 391 | assert.ifError(err) 392 | assert.strictEqual(str, string) 393 | done() 394 | }) 395 | }) 396 | 397 | it('should decode UTF-16 string (BE BOM)', function (done) { 398 | // BOM makes this BE 399 | var stream = createStream(Buffer.from('feff00bf004300f3006d006f002000650073007400e10073003f', 'hex')) 400 | var string = '¿Cómo estás?' 401 | getRawBody(stream, 'utf-16', function (err, str) { 402 | assert.ifError(err) 403 | assert.strictEqual(str, string) 404 | done() 405 | }) 406 | }) 407 | 408 | it('should decode UTF-16LE string', function (done) { 409 | // UTF-16LE is different from UTF-16 due to BOM behavior 410 | var stream = createStream(Buffer.from('bf004300f3006d006f002000650073007400e10073003f00', 'hex')) 411 | var string = '¿Cómo estás?' 412 | getRawBody(stream, 'utf-16le', function (err, str) { 413 | assert.ifError(err) 414 | assert.strictEqual(str, string) 415 | done() 416 | }) 417 | }) 418 | 419 | it('should decode UTF-32 string (LE BOM)', function (done) { 420 | // BOM makes this LE 421 | var stream = createStream(Buffer.from('fffe0000bf00000043000000f30000006d0000006f00000020000000650000007300000074000000e1000000730000003f000000', 'hex')) 422 | var string = '¿Cómo estás?' 423 | getRawBody(stream, 'utf-32', function (err, str) { 424 | assert.ifError(err) 425 | assert.strictEqual(str, string) 426 | done() 427 | }) 428 | }) 429 | 430 | it('should decode UTF-32 string (BE BOM)', function (done) { 431 | // BOM makes this BE 432 | var stream = createStream(Buffer.from('0000feff000000bf00000043000000f30000006d0000006f00000020000000650000007300000074000000e1000000730000003f', 'hex')) 433 | var string = '¿Cómo estás?' 434 | getRawBody(stream, 'utf-32', function (err, str) { 435 | assert.ifError(err) 436 | assert.strictEqual(str, string) 437 | done() 438 | }) 439 | }) 440 | 441 | it('should correctly calculate the expected length', function (done) { 442 | var stream = createStream(Buffer.from('{"test":"å"}')) 443 | 444 | getRawBody(stream, { 445 | encoding: 'utf-8', 446 | length: 13 447 | }, done) 448 | }) 449 | }) 450 | 451 | it('should work on streams1 stream', function (done) { 452 | var stream = new EventEmitter() 453 | 454 | getRawBody(stream, { 455 | encoding: true, 456 | length: 19 457 | }, function (err, value) { 458 | assert.ifError(err) 459 | assert.strictEqual(value, 'foobar,foobaz,yay!!') 460 | done() 461 | }) 462 | 463 | process.nextTick(function () { 464 | stream.emit('data', 'foobar,') 465 | stream.emit('data', 'foobaz,') 466 | stream.emit('data', 'yay!!') 467 | stream.emit('end') 468 | }) 469 | }) 470 | }) 471 | 472 | function checkBuffer (buf) { 473 | assert.ok(Buffer.isBuffer(buf)) 474 | assert.strictEqual(buf.length, length) 475 | assert.strictEqual(buf.toString('utf8'), string) 476 | } 477 | 478 | function checkString (str) { 479 | assert.ok(typeof str === 'string') 480 | assert.strictEqual(str, string) 481 | } 482 | 483 | function createStream (buf) { 484 | if (!buf) return fs.createReadStream(file) 485 | 486 | var stream = new Readable() 487 | stream._read = function () { 488 | stream.push(buf) 489 | stream.push(null) 490 | } 491 | 492 | return stream 493 | } 494 | 495 | function throwExpectedError () { 496 | throw new Error('expected error') 497 | } 498 | 499 | function tryRequire (name) { 500 | try { 501 | return require(name) 502 | } catch (e) { 503 | return {} 504 | } 505 | } 506 | --------------------------------------------------------------------------------