├── .eslintignore ├── .eslintrc.yml ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── codeql.yml │ └── scorecard.yml ├── .gitignore ├── HISTORY.md ├── LICENSE ├── README.md ├── index.js ├── package.json └── test ├── .eslintrc.yml └── compression.js /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | extends: 3 | - standard 4 | - plugin:markdown/recommended 5 | plugins: 6 | - markdown 7 | overrides: 8 | - files: '**/*.md' 9 | processor: 'markdown/markdown' 10 | -------------------------------------------------------------------------------- /.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 15 | ignore: 16 | - dependency-name: "*" 17 | update-types: ["version-update:semver-major"] -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - '*.md' 9 | push: 10 | paths-ignore: 11 | - '*.md' 12 | 13 | permissions: 14 | contents: read 15 | 16 | # Cancel in progress workflows 17 | # in the scenario where we already had a run going for that PR/branch/tag but then triggered a new run 18 | concurrency: 19 | group: "${{ github.workflow }} ✨ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | lint: 24 | name: Lint 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 28 | - name: Setup Node.js 29 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 30 | with: 31 | node-version: 'lts/*' 32 | 33 | - name: Install dependencies 34 | run: npm install --ignore-scripts --only=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.8 46 | - Node.js 0.10 47 | - Node.js 0.12 48 | - io.js 1.x 49 | - io.js 2.x 50 | - io.js 3.x 51 | - Node.js 4.x 52 | - Node.js 5.x 53 | - Node.js 6.x 54 | - Node.js 7.x 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 | 72 | include: 73 | - name: Node.js 0.8 74 | node-version: "0.8" 75 | npm-i: mocha@2.5.3 supertest@1.1.0 76 | npm-rm: nyc 77 | 78 | - name: Node.js 0.10 79 | node-version: "0.10" 80 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 81 | 82 | - name: Node.js 0.12 83 | node-version: "0.12" 84 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 85 | 86 | - name: io.js 1.x 87 | node-version: "1.8" 88 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 89 | 90 | - name: io.js 2.x 91 | node-version: "2.5" 92 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 93 | 94 | - name: io.js 3.x 95 | node-version: "3.3" 96 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 97 | 98 | - name: Node.js 4.x 99 | node-version: "4" 100 | npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 101 | 102 | - name: Node.js 5.x 103 | node-version: "5" 104 | npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 105 | 106 | - name: Node.js 6.x 107 | node-version: "6" 108 | npm-i: mocha@6.2.3 nyc@14.1.1 supertest@6.1.6 109 | 110 | - name: Node.js 7.x 111 | node-version: "7" 112 | npm-i: mocha@6.2.3 nyc@14.1.1 supertest@6.1.6 113 | 114 | - name: Node.js 8.x 115 | node-version: "8" 116 | npm-i: mocha@7.2.0 nyc@14.1.1 supertest@6.1.6 117 | 118 | - name: Node.js 9.x 119 | node-version: "9" 120 | npm-i: mocha@7.2.0 nyc@14.1.1 supertest@6.1.6 121 | 122 | - name: Node.js 10.x 123 | node-version: "10" 124 | npm-i: mocha@8.4.0 supertest@6.1.6 125 | 126 | - name: Node.js 11.x 127 | node-version: "11" 128 | npm-i: mocha@8.4.0 supertest@6.1.6 129 | 130 | - name: Node.js 12.x 131 | node-version: "12" 132 | npm-i: "supertest@6.1.6" 133 | 134 | - name: Node.js 13.x 135 | node-version: "13" 136 | npm-i: "supertest@6.1.6" 137 | 138 | - name: Node.js 14.x 139 | node-version: "14" 140 | 141 | - name: Node.js 15.x 142 | node-version: "15" 143 | npm-i: "supertest@6.1.6" 144 | 145 | - name: Node.js 16.x 146 | node-version: "16" 147 | 148 | - name: Node.js 17.x 149 | node-version: "17" 150 | 151 | - name: Node.js 18.x 152 | node-version: "18" 153 | 154 | - name: Node.js 19.x 155 | node-version: "19" 156 | 157 | - name: Node.js 20.x 158 | node-version: "20" 159 | 160 | - name: Node.js 21.x 161 | node-version: "21" 162 | 163 | - name: Node.js 22.x 164 | node-version: "22" 165 | 166 | - name: Node.js 23.x 167 | node-version: "23" 168 | 169 | steps: 170 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 171 | 172 | - name: Install Node.js ${{ matrix.node-version }} 173 | shell: bash -eo pipefail -l {0} 174 | run: | 175 | nvm install --default ${{ matrix.node-version }} 176 | if [[ "${{ matrix.node-version }}" == 0.* && "$(cut -d. -f2 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then 177 | nvm install --alias=npm 0.10 178 | nvm use ${{ matrix.node-version }} 179 | sed -i '1s;^.*$;'"$(printf '#!%q' "$(nvm which npm)")"';' "$(readlink -f "$(which npm)")" 180 | npm config set strict-ssl false 181 | fi 182 | dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" 183 | 184 | - name: Configure npm 185 | run: | 186 | if [[ "$(npm config get package-lock)" == "true" ]]; then 187 | npm config set package-lock false 188 | else 189 | npm config set shrinkwrap false 190 | fi 191 | 192 | - name: Remove npm module(s) ${{ matrix.npm-rm }} 193 | run: npm rm --silent --save-dev ${{ matrix.npm-rm }} 194 | if: matrix.npm-rm != '' 195 | 196 | - name: Install npm module(s) ${{ matrix.npm-i }} 197 | run: npm install --save-dev ${{ matrix.npm-i }} 198 | if: matrix.npm-i != '' 199 | 200 | - name: Setup Node.js version-specific dependencies 201 | shell: bash 202 | run: | 203 | # eslint for linting 204 | # - remove on Node.js < 10 205 | if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then 206 | node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ 207 | grep -E '^eslint(-|$)' | \ 208 | sort -r | \ 209 | xargs -n1 npm rm --silent --save-dev 210 | fi 211 | 212 | - name: Install Node.js dependencies 213 | run: npm install 214 | 215 | - name: List environment 216 | id: list_env 217 | shell: bash 218 | run: | 219 | echo "node@$(node -v)" 220 | echo "npm@$(npm -v)" 221 | npm -s ls ||: 222 | (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print "::set-output name=" $2 "::" $3 }' 223 | 224 | - name: Run tests 225 | shell: bash 226 | run: | 227 | if npm -ps ls nyc | grep -q nyc; then 228 | npm run test-ci 229 | else 230 | npm test 231 | fi 232 | 233 | - name: Upload code coverage 234 | if: steps.list_env.outputs.nyc != '' 235 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 236 | with: 237 | name: coverage-node-${{ matrix.node-version }} 238 | path: ./coverage/lcov.info 239 | retention-days: 1 240 | 241 | coverage: 242 | needs: test 243 | runs-on: ubuntu-latest 244 | permissions: 245 | contents: read 246 | checks: write 247 | steps: 248 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 249 | 250 | - name: Install lcov 251 | shell: bash 252 | run: sudo apt-get -y install lcov 253 | 254 | - name: Collect coverage reports 255 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 256 | with: 257 | path: ./coverage 258 | pattern: coverage-node-* 259 | 260 | - name: Merge coverage reports 261 | shell: bash 262 | run: find ./coverage -name lcov.info -exec printf '-a %q\n' {} \; | xargs lcov -o ./lcov.info 263 | 264 | - name: Upload coverage report 265 | uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6 266 | with: 267 | github-token: ${{ secrets.GITHUB_TOKEN }} 268 | file: ./lcov.info -------------------------------------------------------------------------------- /.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 | 23 | permissions: 24 | contents: read 25 | 26 | jobs: 27 | analyze: 28 | name: Analyze 29 | runs-on: ubuntu-latest 30 | permissions: 31 | actions: read 32 | contents: read 33 | security-events: write 34 | 35 | steps: 36 | - name: Checkout repository 37 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 42 | with: 43 | languages: javascript 44 | # If you wish to specify custom queries, you can do so here or in a config file. 45 | # By default, queries listed here will override any specified in a config file. 46 | # Prefix the list here with "+" to use these queries and those in the config file. 47 | 48 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 49 | # If this step fails, then you should remove it and run the build manually (see below) 50 | # - name: Autobuild 51 | # uses: github/codeql-action/autobuild@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 52 | 53 | # ℹ️ Command-line programs to run using the OS shell. 54 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 55 | 56 | # If the Autobuild fails above, remove it and uncomment the following three lines. 57 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 58 | 59 | # - run: | 60 | # echo "Run, Build Application using script" 61 | # ./location_of_script_within_repo/buildscript.sh 62 | 63 | - name: Perform CodeQL Analysis 64 | uses: github/codeql-action/analyze@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 65 | with: 66 | category: "/language:javascript" -------------------------------------------------------------------------------- /.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 | 7 | on: 8 | # For Branch-Protection check. Only the default branch is supported. See 9 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 10 | branch_protection_rule: 11 | # To guarantee Maintained check is occasionally updated. See 12 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 13 | schedule: 14 | - cron: '16 21 * * 1' 15 | push: 16 | branches: [ "master" ] 17 | 18 | # Declare default permissions as read only. 19 | permissions: read-all 20 | 21 | jobs: 22 | analysis: 23 | name: Scorecard analysis 24 | runs-on: ubuntu-latest 25 | permissions: 26 | # Needed to upload the results to code-scanning dashboard. 27 | security-events: write 28 | # Needed to publish results and get a badge (see publish_results below). 29 | id-token: write 30 | 31 | steps: 32 | - name: "Checkout code" 33 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.2 34 | with: 35 | persist-credentials: false 36 | 37 | - name: "Run analysis" 38 | uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 39 | with: 40 | results_file: results.sarif 41 | results_format: sarif 42 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 43 | # - you want to enable the Branch-Protection check on a *public* repository, or 44 | # - you are installing Scorecard on a *private* repository 45 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 46 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 47 | 48 | # Public repositories: 49 | # - Publish results to OpenSSF REST API for easy access by consumers 50 | # - Allows the repository to include the Scorecard badge. 51 | # - See https://github.com/ossf/scorecard-action#publishing-results. 52 | # For private repositories: 53 | # - `publish_results` will always be set to `false`, regardless 54 | # of the value entered here. 55 | publish_results: true 56 | 57 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 58 | # format to the repository Actions tab. 59 | - name: "Upload artifact" 60 | uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 61 | with: 62 | name: SARIF file 63 | path: results.sarif 64 | retention-days: 5 65 | 66 | # Upload the results to GitHub's code scanning dashboard. 67 | - name: "Upload to code-scanning" 68 | uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 69 | with: 70 | sarif_file: results.sarif 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage 3 | node_modules 4 | npm-debug.log 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | 1.8.0 / 2025-02-10 2 | ================== 3 | 4 | * Use `res.headersSent` when available 5 | * Replace `_implicitHeader` with `writeHead` property 6 | * add brotli support for versions of node that support it 7 | * Add the enforceEncoding option for requests without `Accept-Encoding` header 8 | 9 | 1.7.5 / 2024-10-31 10 | ================== 11 | 12 | * deps: Replace accepts with negotiator@~0.6.4 13 | - Add preference option 14 | * deps: bytes@3.1.2 15 | - Add petabyte (`pb`) support 16 | - Fix "thousandsSeparator" incorrecting formatting fractional part 17 | - Fix return value for un-parsable strings 18 | * deps: compressible@~2.0.18 19 | - Mark `font/ttf` as compressible 20 | - Remove compressible from `multipart/mixed` 21 | - deps: mime-db@'>= 1.43.0 < 2' 22 | * deps: safe-buffer@5.2.1 23 | 24 | 1.7.4 / 2019-03-18 25 | ================== 26 | 27 | * deps: compressible@~2.0.16 28 | - Mark `text/less` as compressible 29 | - deps: mime-db@'>= 1.38.0 < 2' 30 | * deps: on-headers@~1.0.2 31 | - Fix `res.writeHead` patch missing return value 32 | * perf: prevent unnecessary buffer copy 33 | 34 | 1.7.3 / 2018-07-15 35 | ================== 36 | 37 | * deps: accepts@~1.3.5 38 | - deps: mime-types@~2.1.18 39 | * deps: compressible@~2.0.14 40 | - Mark all XML-derived types as compressible 41 | - deps: mime-db@'>= 1.34.0 < 2' 42 | * deps: safe-buffer@5.1.2 43 | 44 | 1.7.2 / 2018-02-18 45 | ================== 46 | 47 | * deps: compressible@~2.0.13 48 | - deps: mime-db@'>= 1.33.0 < 2' 49 | 50 | 1.7.1 / 2017-09-26 51 | ================== 52 | 53 | * deps: accepts@~1.3.4 54 | - deps: mime-types@~2.1.16 55 | * deps: bytes@3.0.0 56 | * deps: compressible@~2.0.11 57 | - deps: mime-db@'>= 1.29.0 < 2' 58 | * deps: debug@2.6.9 59 | * deps: vary@~1.1.2 60 | - perf: improve header token parsing speed 61 | 62 | 1.7.0 / 2017-07-10 63 | ================== 64 | 65 | * Use `safe-buffer` for improved Buffer API 66 | * deps: bytes@2.5.0 67 | * deps: compressible@~2.0.10 68 | - Fix regex fallback to not override `compressible: false` in db 69 | - deps: mime-db@'>= 1.27.0 < 2' 70 | * deps: debug@2.6.8 71 | - Allow colors in workers 72 | - Deprecated `DEBUG_FD` environment variable set to `3` or higher 73 | - Fix error when running under React Native 74 | - Fix `DEBUG_MAX_ARRAY_LENGTH` 75 | - Use same color for same namespace 76 | - deps: ms@2.0.0 77 | * deps: vary@~1.1.1 78 | - perf: hoist regular expression 79 | 80 | 1.6.2 / 2016-05-12 81 | ================== 82 | 83 | * deps: accepts@~1.3.3 84 | - deps: mime-types@~2.1.11 85 | - deps: negotiator@0.6.1 86 | * deps: bytes@2.3.0 87 | - Drop partial bytes on all parsed units 88 | - Fix parsing byte string that looks like hex 89 | - perf: hoist regular expressions 90 | * deps: compressible@~2.0.8 91 | - deps: mime-db@'>= 1.23.0 < 2' 92 | 93 | 1.6.1 / 2016-01-19 94 | ================== 95 | 96 | * deps: bytes@2.2.0 97 | * deps: compressible@~2.0.7 98 | - deps: mime-db@'>= 1.21.0 < 2' 99 | * deps: accepts@~1.3.1 100 | - deps: mime-types@~2.1.9 101 | 102 | 1.6.0 / 2015-09-29 103 | ================== 104 | 105 | * Skip compression when response has `Cache-Control: no-transform` 106 | * deps: accepts@~1.3.0 107 | - deps: mime-types@~2.1.7 108 | - deps: negotiator@0.6.0 109 | * deps: compressible@~2.0.6 110 | - deps: mime-db@'>= 1.19.0 < 2' 111 | * deps: on-headers@~1.0.1 112 | - perf: enable strict mode 113 | * deps: vary@~1.1.0 114 | - Only accept valid field names in the `field` argument 115 | 116 | 1.5.2 / 2015-07-30 117 | ================== 118 | 119 | * deps: accepts@~1.2.12 120 | - deps: mime-types@~2.1.4 121 | * deps: compressible@~2.0.5 122 | - deps: mime-db@'>= 1.16.0 < 2' 123 | * deps: vary@~1.0.1 124 | - Fix setting empty header from empty `field` 125 | - perf: enable strict mode 126 | - perf: remove argument reassignments 127 | 128 | 1.5.1 / 2015-07-05 129 | ================== 130 | 131 | * deps: accepts@~1.2.10 132 | - deps: mime-types@~2.1.2 133 | * deps: compressible@~2.0.4 134 | - deps: mime-db@'>= 1.14.0 < 2' 135 | - perf: enable strict mode 136 | 137 | 1.5.0 / 2015-06-09 138 | ================== 139 | 140 | * Fix return value from `.end` and `.write` after end 141 | * Improve detection of zero-length body without `Content-Length` 142 | * deps: accepts@~1.2.9 143 | - deps: mime-types@~2.1.1 144 | - perf: avoid argument reassignment & argument slice 145 | - perf: avoid negotiator recursive construction 146 | - perf: enable strict mode 147 | - perf: remove unnecessary bitwise operator 148 | * deps: bytes@2.1.0 149 | - Slight optimizations 150 | - Units no longer case sensitive when parsing 151 | * deps: compressible@~2.0.3 152 | - Fix regex fallback to work if type exists, but is undefined 153 | - deps: mime-db@'>= 1.13.0 < 2' 154 | - perf: hoist regex declaration 155 | - perf: use regex to extract mime 156 | * perf: enable strict mode 157 | * perf: remove flush reassignment 158 | * perf: simplify threshold detection 159 | 160 | 1.4.4 / 2015-05-11 161 | ================== 162 | 163 | * deps: accepts@~1.2.7 164 | - deps: mime-types@~2.0.11 165 | - deps: negotiator@0.5.3 166 | * deps: debug@~2.2.0 167 | - deps: ms@0.7.1 168 | 169 | 1.4.3 / 2015-03-14 170 | ================== 171 | 172 | * deps: accepts@~1.2.5 173 | - deps: mime-types@~2.0.10 174 | * deps: debug@~2.1.3 175 | - Fix high intensity foreground color for bold 176 | - deps: ms@0.7.0 177 | 178 | 1.4.2 / 2015-03-11 179 | ================== 180 | 181 | * Fix error when code calls `res.end(str, encoding)` 182 | - Specific to Node.js 0.8 183 | * deps: debug@~2.1.2 184 | - deps: ms@0.7.0 185 | 186 | 1.4.1 / 2015-02-15 187 | ================== 188 | 189 | * deps: accepts@~1.2.4 190 | - deps: mime-types@~2.0.9 191 | - deps: negotiator@0.5.1 192 | 193 | 1.4.0 / 2015-02-01 194 | ================== 195 | 196 | * Prefer `gzip` over `deflate` on the server 197 | - Not all clients agree on what "deflate" coding means 198 | 199 | 1.3.1 / 2015-01-31 200 | ================== 201 | 202 | * deps: accepts@~1.2.3 203 | - deps: mime-types@~2.0.8 204 | * deps: compressible@~2.0.2 205 | - deps: mime-db@'>= 1.1.2 < 2' 206 | 207 | 1.3.0 / 2014-12-30 208 | ================== 209 | 210 | * Export the default `filter` function for wrapping 211 | * deps: accepts@~1.2.2 212 | - deps: mime-types@~2.0.7 213 | - deps: negotiator@0.5.0 214 | * deps: debug@~2.1.1 215 | 216 | 1.2.2 / 2014-12-10 217 | ================== 218 | 219 | * Fix `.end` to only proxy to `.end` 220 | - Fixes an issue with Node.js 0.11.14 221 | * deps: accepts@~1.1.4 222 | - deps: mime-types@~2.0.4 223 | 224 | 1.2.1 / 2014-11-23 225 | ================== 226 | 227 | * deps: accepts@~1.1.3 228 | - deps: mime-types@~2.0.3 229 | 230 | 1.2.0 / 2014-10-16 231 | ================== 232 | 233 | * deps: debug@~2.1.0 234 | - Implement `DEBUG_FD` env variable support 235 | 236 | 1.1.2 / 2014-10-15 237 | ================== 238 | 239 | * deps: accepts@~1.1.2 240 | - Fix error when media type has invalid parameter 241 | - deps: negotiator@0.4.9 242 | 243 | 1.1.1 / 2014-10-12 244 | ================== 245 | 246 | * deps: accepts@~1.1.1 247 | - deps: mime-types@~2.0.2 248 | - deps: negotiator@0.4.8 249 | * deps: compressible@~2.0.1 250 | - deps: mime-db@1.x 251 | 252 | 1.1.0 / 2014-09-07 253 | ================== 254 | 255 | * deps: accepts@~1.1.0 256 | * deps: compressible@~2.0.0 257 | * deps: debug@~2.0.0 258 | 259 | 1.0.11 / 2014-08-10 260 | =================== 261 | 262 | * deps: on-headers@~1.0.0 263 | * deps: vary@~1.0.0 264 | 265 | 1.0.10 / 2014-08-05 266 | =================== 267 | 268 | * deps: compressible@~1.1.1 269 | - Fix upper-case Content-Type characters prevent compression 270 | 271 | 1.0.9 / 2014-07-20 272 | ================== 273 | 274 | * Add `debug` messages 275 | * deps: accepts@~1.0.7 276 | - deps: negotiator@0.4.7 277 | 278 | 1.0.8 / 2014-06-20 279 | ================== 280 | 281 | * deps: accepts@~1.0.5 282 | - use `mime-types` 283 | 284 | 1.0.7 / 2014-06-11 285 | ================== 286 | 287 | * use vary module for better `Vary` behavior 288 | * deps: accepts@1.0.3 289 | * deps: compressible@1.1.0 290 | 291 | 1.0.6 / 2014-06-03 292 | ================== 293 | 294 | * fix regression when negotiation fails 295 | 296 | 1.0.5 / 2014-06-03 297 | ================== 298 | 299 | * fix listeners for delayed stream creation 300 | - fixes regression for certain `stream.pipe(res)` situations 301 | 302 | 1.0.4 / 2014-06-03 303 | ================== 304 | 305 | * fix adding `Vary` when value stored as array 306 | * fix back-pressure behavior 307 | * fix length check for `res.end` 308 | 309 | 1.0.3 / 2014-05-29 310 | ================== 311 | 312 | * use `accepts` for negotiation 313 | * use `on-headers` to handle header checking 314 | * deps: bytes@1.0.0 315 | 316 | 1.0.2 / 2014-04-29 317 | ================== 318 | 319 | * only version compatible with node.js 0.8 320 | * support headers given to `res.writeHead` 321 | * deps: bytes@0.3.0 322 | * deps: negotiator@0.4.3 323 | 324 | 1.0.1 / 2014-03-08 325 | ================== 326 | 327 | * bump negotiator 328 | * use compressible 329 | * use .headersSent (drops 0.8 support) 330 | * handle identity;q=0 case 331 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2014 Jonathan Ong 4 | Copyright (c) 2014-2015 Douglas Christopher Wilson 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | 'Software'), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # compression 2 | 3 | [![NPM Version][npm-image]][npm-url] 4 | [![NPM Downloads][downloads-image]][downloads-url] 5 | [![Build Status][github-actions-ci-image]][github-actions-ci-url] 6 | [![OpenSSF Scorecard Badge][ossf-scorecard-badge]][ossf-scorecard-visualizer] 7 | [![Funding][funding-image]][funding-url] 8 | 9 | 10 | Node.js compression middleware. 11 | 12 | The following compression codings are supported: 13 | 14 | - deflate 15 | - gzip 16 | - br (brotli) 17 | 18 | **Note** Brotli is supported only since Node.js versions v11.7.0 and v10.16.0. 19 | 20 | ## Install 21 | 22 | This is a [Node.js](https://nodejs.org/en/) module available through the 23 | [npm registry](https://www.npmjs.com/). Installation is done using the 24 | [`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): 25 | 26 | ```bash 27 | $ npm install compression 28 | ``` 29 | 30 | ## API 31 | 32 | ```js 33 | var compression = require('compression') 34 | ``` 35 | 36 | ### compression([options]) 37 | 38 | Returns the compression middleware using the given `options`. The middleware 39 | will attempt to compress response bodies for all requests that traverse through 40 | the middleware, based on the given `options`. 41 | 42 | This middleware will never compress responses that include a `Cache-Control` 43 | header with the [`no-transform` directive](https://tools.ietf.org/html/rfc7234#section-5.2.2.4), 44 | as compressing will transform the body. 45 | 46 | #### Options 47 | 48 | `compression()` accepts these properties in the options object. In addition to 49 | those listed below, [zlib](https://nodejs.org/api/zlib.html) options may be 50 | passed in to the options object or 51 | [brotli](https://nodejs.org/api/zlib.html#zlib_class_brotlioptions) options. 52 | 53 | ##### chunkSize 54 | 55 | Type: `Number`
56 | Default: `zlib.constants.Z_DEFAULT_CHUNK`, or `16384`. 57 | 58 | See [Node.js documentation](https://nodejs.org/api/zlib.html#zlib_memory_usage_tuning) 59 | regarding the usage. 60 | 61 | ##### filter 62 | 63 | Type: `Function` 64 | 65 | A function to decide if the response should be considered for compression. 66 | This function is called as `filter(req, res)` and is expected to return 67 | `true` to consider the response for compression, or `false` to not compress 68 | the response. 69 | 70 | The default filter function uses the [compressible](https://www.npmjs.com/package/compressible) 71 | module to determine if `res.getHeader('Content-Type')` is compressible. 72 | 73 | ##### level 74 | 75 | Type: `Number`
76 | Default: `zlib.constants.Z_DEFAULT_COMPRESSION`, or `-1` 77 | 78 | The level of zlib compression to apply to responses. A higher level will result 79 | in better compression, but will take longer to complete. A lower level will 80 | result in less compression, but will be much faster. 81 | 82 | This is an integer in the range of `0` (no compression) to `9` (maximum 83 | compression). The special value `-1` can be used to mean the "default 84 | compression level", which is a default compromise between speed and 85 | compression (currently equivalent to level 6). 86 | 87 | - `-1` Default compression level (also `zlib.constants.Z_DEFAULT_COMPRESSION`). 88 | - `0` No compression (also `zlib.constants.Z_NO_COMPRESSION`). 89 | - `1` Fastest compression (also `zlib.constants.Z_BEST_SPEED`). 90 | - `2` 91 | - `3` 92 | - `4` 93 | - `5` 94 | - `6` (currently what `zlib.constants.Z_DEFAULT_COMPRESSION` points to). 95 | - `7` 96 | - `8` 97 | - `9` Best compression (also `zlib.constants.Z_BEST_COMPRESSION`). 98 | 99 | **Note** in the list above, `zlib` is from `zlib = require('zlib')`. 100 | 101 | ##### memLevel 102 | 103 | Type: `Number`
104 | Default: `zlib.constants.Z_DEFAULT_MEMLEVEL`, or `8` 105 | 106 | This specifies how much memory should be allocated for the internal compression 107 | state and is an integer in the range of `1` (minimum level) and `9` (maximum 108 | level). 109 | 110 | See [Node.js documentation](https://nodejs.org/api/zlib.html#zlib_memory_usage_tuning) 111 | regarding the usage. 112 | 113 | ##### brotli 114 | 115 | Type: `Object` 116 | 117 | This specifies the options for configuring Brotli. See [Node.js documentation](https://nodejs.org/api/zlib.html#class-brotlioptions) for a complete list of available options. 118 | 119 | 120 | ##### strategy 121 | 122 | Type: `Number`
123 | Default: `zlib.constants.Z_DEFAULT_STRATEGY` 124 | 125 | This is used to tune the compression algorithm. This value only affects the 126 | compression ratio, not the correctness of the compressed output, even if it 127 | is not set appropriately. 128 | 129 | - `zlib.constants.Z_DEFAULT_STRATEGY` Use for normal data. 130 | - `zlib.constants.Z_FILTERED` Use for data produced by a filter (or predictor). 131 | Filtered data consists mostly of small values with a somewhat random 132 | distribution. In this case, the compression algorithm is tuned to 133 | compress them better. The effect is to force more Huffman coding and less 134 | string matching; it is somewhat intermediate between `zlib.constants.Z_DEFAULT_STRATEGY` 135 | and `zlib.constants.Z_HUFFMAN_ONLY`. 136 | - `zlib.constants.Z_FIXED` Use to prevent the use of dynamic Huffman codes, allowing 137 | for a simpler decoder for special applications. 138 | - `zlib.constants.Z_HUFFMAN_ONLY` Use to force Huffman encoding only (no string match). 139 | - `zlib.constants.Z_RLE` Use to limit match distances to one (run-length encoding). 140 | This is designed to be almost as fast as `zlib.constants.Z_HUFFMAN_ONLY`, but give 141 | better compression for PNG image data. 142 | 143 | **Note** in the list above, `zlib` is from `zlib = require('zlib')`. 144 | 145 | ##### threshold 146 | 147 | Type: `Number` or `String`
148 | Default: `1kb` 149 | 150 | The byte threshold for the response body size before compression is considered 151 | for the response. This is a number of bytes or any string 152 | accepted by the [bytes](https://www.npmjs.com/package/bytes) module. 153 | 154 | **Note** this is only an advisory setting; if the response size cannot be determined 155 | at the time the response headers are written, then it is assumed the response is 156 | _over_ the threshold. To guarantee the response size can be determined, be sure 157 | set a `Content-Length` response header. 158 | 159 | ##### windowBits 160 | 161 | Type: `Number`
162 | Default: `zlib.constants.Z_DEFAULT_WINDOWBITS`, or `15` 163 | 164 | See [Node.js documentation](https://nodejs.org/api/zlib.html#zlib_memory_usage_tuning) 165 | regarding the usage. 166 | 167 | ##### enforceEncoding 168 | 169 | Type: `String`
170 | Default: `identity` 171 | 172 | This is the default encoding to use when the client does not specify an encoding in the request's [Accept-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) header. 173 | 174 | #### .filter 175 | 176 | The default `filter` function. This is used to construct a custom filter 177 | function that is an extension of the default function. 178 | 179 | ```js 180 | var compression = require('compression') 181 | var express = require('express') 182 | 183 | var app = express() 184 | 185 | app.use(compression({ filter: shouldCompress })) 186 | 187 | function shouldCompress (req, res) { 188 | if (req.headers['x-no-compression']) { 189 | // don't compress responses with this request header 190 | return false 191 | } 192 | 193 | // fallback to standard filter function 194 | return compression.filter(req, res) 195 | } 196 | ``` 197 | 198 | ### res.flush 199 | 200 | This module adds a `res.flush()` method to force the partially-compressed 201 | response to be flushed to the client. 202 | 203 | ## Examples 204 | 205 | ### express 206 | 207 | When using this module with express, simply `app.use` the module as 208 | high as you like. Requests that pass through the middleware will be compressed. 209 | 210 | ```js 211 | var compression = require('compression') 212 | var express = require('express') 213 | 214 | var app = express() 215 | 216 | // compress all responses 217 | app.use(compression()) 218 | 219 | // add all routes 220 | ``` 221 | 222 | ### Node.js HTTP server 223 | 224 | ```js 225 | var compression = require('compression')({ threshold: 0 }) 226 | var http = require('http') 227 | 228 | function createServer (fn) { 229 | return http.createServer(function (req, res) { 230 | compression(req, res, function (err) { 231 | if (err) { 232 | res.statusCode = err.status || 500 233 | res.end(err.message) 234 | return 235 | } 236 | 237 | fn(req, res) 238 | }) 239 | }) 240 | } 241 | 242 | var server = createServer(function (req, res) { 243 | res.setHeader('Content-Type', 'text/plain') 244 | res.end('hello world!') 245 | }) 246 | 247 | server.listen(3000, () => { 248 | console.log('> Listening at http://localhost:3000') 249 | }) 250 | ``` 251 | 252 | ### Server-Sent Events 253 | 254 | Because of the nature of compression this module does not work out of the box 255 | with server-sent events. To compress content, a window of the output needs to 256 | be buffered up in order to get good compression. Typically when using server-sent 257 | events, there are certain block of data that need to reach the client. 258 | 259 | You can achieve this by calling `res.flush()` when you need the data written to 260 | actually make it to the client. 261 | 262 | ```js 263 | var compression = require('compression') 264 | var express = require('express') 265 | 266 | var app = express() 267 | 268 | // compress responses 269 | app.use(compression()) 270 | 271 | // server-sent event stream 272 | app.get('/events', function (req, res) { 273 | res.setHeader('Content-Type', 'text/event-stream') 274 | res.setHeader('Cache-Control', 'no-cache') 275 | 276 | // send a ping approx every 2 seconds 277 | var timer = setInterval(function () { 278 | res.write('data: ping\n\n') 279 | 280 | // !!! this is the important part 281 | res.flush() 282 | }, 2000) 283 | 284 | res.on('close', function () { 285 | clearInterval(timer) 286 | }) 287 | }) 288 | ``` 289 | 290 | ## Contributing 291 | 292 | The Express.js project welcomes all constructive contributions. Contributions take many forms, 293 | from code for bug fixes and enhancements, to additions and fixes to documentation, additional 294 | tests, triaging incoming pull requests and issues, and more! 295 | 296 | See the [Contributing Guide](https://github.com/expressjs/express/blob/master/Contributing.md) for more technical details on contributing. 297 | 298 | ## License 299 | 300 | [MIT](LICENSE) 301 | 302 | [npm-image]: https://badgen.net/npm/v/compression 303 | [npm-url]: https://npmjs.org/package/compression 304 | [downloads-image]: https://badgen.net/npm/dm/compression 305 | [downloads-url]: https://npmcharts.com/compare/compression?minimal=true 306 | [github-actions-ci-image]: https://badgen.net/github/checks/expressjs/compression/master?label=CI 307 | [github-actions-ci-url]: https://github.com/expressjs/compression/actions?query=workflow%3Aci 308 | [ossf-scorecard-badge]: https://api.scorecard.dev/projects/github.com/expressjs/compression/badge 309 | [ossf-scorecard-visualizer]: https://ossf.github.io/scorecard-visualizer/#/projects/github.com/expressjs/compression 310 | [funding-url]: https://opencollective.com/express 311 | [funding-image]: https://badgen.net/badge/icon/sponsor/pink?icon=github&label=Open%20Collective -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * compression 3 | * Copyright(c) 2010 Sencha Inc. 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * Copyright(c) 2014 Jonathan Ong 6 | * Copyright(c) 2014-2015 Douglas Christopher Wilson 7 | * MIT Licensed 8 | */ 9 | 10 | 'use strict' 11 | 12 | /** 13 | * Module dependencies. 14 | * @private 15 | */ 16 | 17 | var Negotiator = require('negotiator') 18 | var Buffer = require('safe-buffer').Buffer 19 | var bytes = require('bytes') 20 | var compressible = require('compressible') 21 | var debug = require('debug')('compression') 22 | var onHeaders = require('on-headers') 23 | var vary = require('vary') 24 | var zlib = require('zlib') 25 | 26 | /** 27 | * Module exports. 28 | */ 29 | 30 | module.exports = compression 31 | module.exports.filter = shouldCompress 32 | 33 | /** 34 | * @const 35 | * whether current node version has brotli support 36 | */ 37 | var hasBrotliSupport = 'createBrotliCompress' in zlib 38 | 39 | /** 40 | * Module variables. 41 | * @private 42 | */ 43 | var cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/ 44 | var SUPPORTED_ENCODING = hasBrotliSupport ? ['br', 'gzip', 'deflate', 'identity'] : ['gzip', 'deflate', 'identity'] 45 | var PREFERRED_ENCODING = hasBrotliSupport ? ['br', 'gzip'] : ['gzip'] 46 | 47 | var encodingSupported = ['gzip', 'deflate', 'identity', 'br'] 48 | 49 | /** 50 | * Compress response data with gzip / deflate. 51 | * 52 | * @param {Object} [options] 53 | * @return {Function} middleware 54 | * @public 55 | */ 56 | 57 | function compression (options) { 58 | var opts = options || {} 59 | var optsBrotli = {} 60 | 61 | if (hasBrotliSupport) { 62 | Object.assign(optsBrotli, opts.brotli) 63 | 64 | var brotliParams = {} 65 | brotliParams[zlib.constants.BROTLI_PARAM_QUALITY] = 4 66 | 67 | // set the default level to a reasonable value with balanced speed/ratio 68 | optsBrotli.params = Object.assign(brotliParams, optsBrotli.params) 69 | } 70 | 71 | // options 72 | var filter = opts.filter || shouldCompress 73 | var threshold = bytes.parse(opts.threshold) 74 | var enforceEncoding = opts.enforceEncoding || 'identity' 75 | 76 | if (threshold == null) { 77 | threshold = 1024 78 | } 79 | 80 | return function compression (req, res, next) { 81 | var ended = false 82 | var length 83 | var listeners = [] 84 | var stream 85 | 86 | var _end = res.end 87 | var _on = res.on 88 | var _write = res.write 89 | 90 | // flush 91 | res.flush = function flush () { 92 | if (stream) { 93 | stream.flush() 94 | } 95 | } 96 | 97 | // proxy 98 | 99 | res.write = function write (chunk, encoding) { 100 | if (ended) { 101 | return false 102 | } 103 | 104 | if (!headersSent(res)) { 105 | this.writeHead(this.statusCode) 106 | } 107 | 108 | return stream 109 | ? stream.write(toBuffer(chunk, encoding)) 110 | : _write.call(this, chunk, encoding) 111 | } 112 | 113 | res.end = function end (chunk, encoding) { 114 | if (ended) { 115 | return false 116 | } 117 | 118 | if (!headersSent(res)) { 119 | // estimate the length 120 | if (!this.getHeader('Content-Length')) { 121 | length = chunkLength(chunk, encoding) 122 | } 123 | 124 | this.writeHead(this.statusCode) 125 | } 126 | 127 | if (!stream) { 128 | return _end.call(this, chunk, encoding) 129 | } 130 | 131 | // mark ended 132 | ended = true 133 | 134 | // write Buffer for Node.js 0.8 135 | return chunk 136 | ? stream.end(toBuffer(chunk, encoding)) 137 | : stream.end() 138 | } 139 | 140 | res.on = function on (type, listener) { 141 | if (!listeners || type !== 'drain') { 142 | return _on.call(this, type, listener) 143 | } 144 | 145 | if (stream) { 146 | return stream.on(type, listener) 147 | } 148 | 149 | // buffer listeners for future stream 150 | listeners.push([type, listener]) 151 | 152 | return this 153 | } 154 | 155 | function nocompress (msg) { 156 | debug('no compression: %s', msg) 157 | addListeners(res, _on, listeners) 158 | listeners = null 159 | } 160 | 161 | onHeaders(res, function onResponseHeaders () { 162 | // determine if request is filtered 163 | if (!filter(req, res)) { 164 | nocompress('filtered') 165 | return 166 | } 167 | 168 | // determine if the entity should be transformed 169 | if (!shouldTransform(req, res)) { 170 | nocompress('no transform') 171 | return 172 | } 173 | 174 | // vary 175 | vary(res, 'Accept-Encoding') 176 | 177 | // content-length below threshold 178 | if (Number(res.getHeader('Content-Length')) < threshold || length < threshold) { 179 | nocompress('size below threshold') 180 | return 181 | } 182 | 183 | var encoding = res.getHeader('Content-Encoding') || 'identity' 184 | 185 | // already encoded 186 | if (encoding !== 'identity') { 187 | nocompress('already encoded') 188 | return 189 | } 190 | 191 | // head 192 | if (req.method === 'HEAD') { 193 | nocompress('HEAD request') 194 | return 195 | } 196 | 197 | // compression method 198 | var negotiator = new Negotiator(req) 199 | var method = negotiator.encoding(SUPPORTED_ENCODING, PREFERRED_ENCODING) 200 | 201 | // if no method is found, use the default encoding 202 | if (!req.headers['accept-encoding'] && encodingSupported.indexOf(enforceEncoding) !== -1) { 203 | method = enforceEncoding 204 | } 205 | 206 | // negotiation failed 207 | if (!method || method === 'identity') { 208 | nocompress('not acceptable') 209 | return 210 | } 211 | 212 | // compression stream 213 | debug('%s compression', method) 214 | stream = method === 'gzip' 215 | ? zlib.createGzip(opts) 216 | : method === 'br' 217 | ? zlib.createBrotliCompress(optsBrotli) 218 | : zlib.createDeflate(opts) 219 | 220 | // add buffered listeners to stream 221 | addListeners(stream, stream.on, listeners) 222 | 223 | // header fields 224 | res.setHeader('Content-Encoding', method) 225 | res.removeHeader('Content-Length') 226 | 227 | // compression 228 | stream.on('data', function onStreamData (chunk) { 229 | if (_write.call(res, chunk) === false) { 230 | stream.pause() 231 | } 232 | }) 233 | 234 | stream.on('end', function onStreamEnd () { 235 | _end.call(res) 236 | }) 237 | 238 | _on.call(res, 'drain', function onResponseDrain () { 239 | stream.resume() 240 | }) 241 | }) 242 | 243 | next() 244 | } 245 | } 246 | 247 | /** 248 | * Add bufferred listeners to stream 249 | * @private 250 | */ 251 | 252 | function addListeners (stream, on, listeners) { 253 | for (var i = 0; i < listeners.length; i++) { 254 | on.apply(stream, listeners[i]) 255 | } 256 | } 257 | 258 | /** 259 | * Get the length of a given chunk 260 | */ 261 | 262 | function chunkLength (chunk, encoding) { 263 | if (!chunk) { 264 | return 0 265 | } 266 | 267 | return Buffer.isBuffer(chunk) 268 | ? chunk.length 269 | : Buffer.byteLength(chunk, encoding) 270 | } 271 | 272 | /** 273 | * Default filter function. 274 | * @private 275 | */ 276 | 277 | function shouldCompress (req, res) { 278 | var type = res.getHeader('Content-Type') 279 | 280 | if (type === undefined || !compressible(type)) { 281 | debug('%s not compressible', type) 282 | return false 283 | } 284 | 285 | return true 286 | } 287 | 288 | /** 289 | * Determine if the entity should be transformed. 290 | * @private 291 | */ 292 | 293 | function shouldTransform (req, res) { 294 | var cacheControl = res.getHeader('Cache-Control') 295 | 296 | // Don't compress for Cache-Control: no-transform 297 | // https://tools.ietf.org/html/rfc7234#section-5.2.2.4 298 | return !cacheControl || 299 | !cacheControlNoTransformRegExp.test(cacheControl) 300 | } 301 | 302 | /** 303 | * Coerce arguments to Buffer 304 | * @private 305 | */ 306 | 307 | function toBuffer (chunk, encoding) { 308 | return Buffer.isBuffer(chunk) 309 | ? chunk 310 | : Buffer.from(chunk, encoding) 311 | } 312 | 313 | /** 314 | * Determine if the response headers have been sent. 315 | * 316 | * @param {object} res 317 | * @returns {boolean} 318 | * @private 319 | */ 320 | 321 | function headersSent (res) { 322 | return typeof res.headersSent !== 'boolean' 323 | ? Boolean(res._header) 324 | : res.headersSent 325 | } 326 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compression", 3 | "description": "Node.js compression middleware", 4 | "version": "1.8.0", 5 | "contributors": [ 6 | "Douglas Christopher Wilson ", 7 | "Jonathan Ong (http://jongleberry.com)" 8 | ], 9 | "license": "MIT", 10 | "repository": "expressjs/compression", 11 | "keywords": ["compression", "gzip", "deflate", "middleware", "express", "brotli", "http", "stream"], 12 | "dependencies": { 13 | "bytes": "3.1.2", 14 | "compressible": "~2.0.18", 15 | "debug": "2.6.9", 16 | "negotiator": "~0.6.4", 17 | "on-headers": "~1.0.2", 18 | "safe-buffer": "5.2.1", 19 | "vary": "~1.1.2" 20 | }, 21 | "devDependencies": { 22 | "after": "0.8.2", 23 | "eslint": "7.32.0", 24 | "eslint-config-standard": "14.1.1", 25 | "eslint-plugin-import": "2.31.0", 26 | "eslint-plugin-markdown": "2.2.1", 27 | "eslint-plugin-node": "11.1.0", 28 | "eslint-plugin-promise": "5.2.0", 29 | "eslint-plugin-standard": "4.1.0", 30 | "mocha": "9.2.2", 31 | "nyc": "15.1.0", 32 | "supertest": "6.3.4" 33 | }, 34 | "files": [ 35 | "LICENSE", 36 | "HISTORY.md", 37 | "index.js" 38 | ], 39 | "engines": { 40 | "node": ">= 0.8.0" 41 | }, 42 | "scripts": { 43 | "lint": "eslint .", 44 | "test": "mocha --check-leaks --reporter spec", 45 | "test-ci": "nyc --reporter=lcov --reporter=text npm test", 46 | "test-cov": "nyc --reporter=html --reporter=text npm test" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | mocha: true 3 | -------------------------------------------------------------------------------- /test/compression.js: -------------------------------------------------------------------------------- 1 | var after = require('after') 2 | var assert = require('assert') 3 | var Buffer = require('safe-buffer').Buffer 4 | var bytes = require('bytes') 5 | var crypto = require('crypto') 6 | var http = require('http') 7 | var request = require('supertest') 8 | var zlib = require('zlib') 9 | 10 | var describeHttp2 = describe.skip 11 | try { 12 | var http2 = require('http2') 13 | describeHttp2 = describe 14 | } catch (err) { 15 | if (err) { 16 | console.log('http2 tests disabled.') 17 | } 18 | } 19 | 20 | var compression = require('..') 21 | 22 | /** 23 | * @const 24 | * whether current node version has brotli support 25 | */ 26 | var hasBrotliSupport = 'createBrotliCompress' in zlib 27 | var brotli = hasBrotliSupport ? it : it.skip 28 | 29 | describe('compression()', function () { 30 | it('should skip HEAD', function (done) { 31 | var server = createServer({ threshold: 0 }, function (req, res) { 32 | res.setHeader('Content-Type', 'text/plain') 33 | res.end('hello, world') 34 | }) 35 | 36 | request(server) 37 | .head('/') 38 | .set('Accept-Encoding', 'gzip') 39 | .expect(shouldNotHaveHeader('Content-Encoding')) 40 | .expect(200, done) 41 | }) 42 | 43 | it('should skip unknown accept-encoding', function (done) { 44 | var server = createServer({ threshold: 0 }, function (req, res) { 45 | res.setHeader('Content-Type', 'text/plain') 46 | res.end('hello, world') 47 | }) 48 | 49 | request(server) 50 | .get('/') 51 | .set('Accept-Encoding', 'bogus') 52 | .expect(shouldNotHaveHeader('Content-Encoding')) 53 | .expect(200, done) 54 | }) 55 | 56 | it('should skip if content-encoding already set', function (done) { 57 | var server = createServer({ threshold: 0 }, function (req, res) { 58 | res.setHeader('Content-Type', 'text/plain') 59 | res.setHeader('Content-Encoding', 'x-custom') 60 | res.end('hello, world') 61 | }) 62 | 63 | request(server) 64 | .get('/') 65 | .set('Accept-Encoding', 'gzip') 66 | .expect('Content-Encoding', 'x-custom') 67 | .expect(200, 'hello, world', done) 68 | }) 69 | 70 | it('should set Vary', function (done) { 71 | var server = createServer({ threshold: 0 }, function (req, res) { 72 | res.setHeader('Content-Type', 'text/plain') 73 | res.end('hello, world') 74 | }) 75 | 76 | request(server) 77 | .get('/') 78 | .set('Accept-Encoding', 'gzip') 79 | .expect('Content-Encoding', 'gzip') 80 | .expect('Vary', 'Accept-Encoding', done) 81 | }) 82 | 83 | it('should set Vary even if Accept-Encoding is not set', function (done) { 84 | var server = createServer({ threshold: 1000 }, function (req, res) { 85 | res.setHeader('Content-Type', 'text/plain') 86 | res.end('hello, world') 87 | }) 88 | 89 | request(server) 90 | .get('/') 91 | .expect('Vary', 'Accept-Encoding') 92 | .expect(shouldNotHaveHeader('Content-Encoding')) 93 | .expect(200, done) 94 | }) 95 | 96 | it('should not set Vary if Content-Type does not pass filter', function (done) { 97 | var server = createServer(null, function (req, res) { 98 | res.setHeader('Content-Type', 'image/jpeg') 99 | res.end() 100 | }) 101 | 102 | request(server) 103 | .get('/') 104 | .expect(shouldNotHaveHeader('Vary')) 105 | .expect(200, done) 106 | }) 107 | 108 | it('should set Vary for HEAD request', function (done) { 109 | var server = createServer({ threshold: 0 }, function (req, res) { 110 | res.setHeader('Content-Type', 'text/plain') 111 | res.end('hello, world') 112 | }) 113 | 114 | request(server) 115 | .head('/') 116 | .set('Accept-Encoding', 'gzip') 117 | .expect('Vary', 'Accept-Encoding', done) 118 | }) 119 | 120 | it('should transfer chunked', function (done) { 121 | var server = createServer({ threshold: 0 }, function (req, res) { 122 | res.setHeader('Content-Type', 'text/plain') 123 | res.end('hello, world') 124 | }) 125 | 126 | request(server) 127 | .get('/') 128 | .set('Accept-Encoding', 'gzip') 129 | .expect('Transfer-Encoding', 'chunked', done) 130 | }) 131 | 132 | it('should remove Content-Length for chunked', function (done) { 133 | var server = createServer({ threshold: 0 }, function (req, res) { 134 | res.setHeader('Content-Type', 'text/plain') 135 | res.end('hello, world') 136 | }) 137 | 138 | request(server) 139 | .get('/') 140 | .expect('Content-Encoding', 'gzip') 141 | .expect(shouldNotHaveHeader('Content-Length')) 142 | .expect(200, done) 143 | }) 144 | 145 | it('should work with encoding arguments', function (done) { 146 | var server = createServer({ threshold: 0 }, function (req, res) { 147 | res.setHeader('Content-Type', 'text/plain') 148 | res.write('hello, ', 'utf8') 149 | res.end('world', 'utf8') 150 | }) 151 | 152 | request(server) 153 | .get('/') 154 | .set('Accept-Encoding', 'gzip') 155 | .expect('Transfer-Encoding', 'chunked') 156 | .expect(200, 'hello, world', done) 157 | }) 158 | 159 | it('should allow writing after close', function (done) { 160 | // UGH 161 | var server = createServer({ threshold: 0 }, function (req, res) { 162 | res.setHeader('Content-Type', 'text/plain') 163 | res.once('close', function () { 164 | res.write('hello, ') 165 | res.end('world') 166 | done() 167 | }) 168 | res.destroy() 169 | }) 170 | 171 | request(server) 172 | .get('/') 173 | .end(function () {}) 174 | }) 175 | 176 | it('should back-pressure when compressed', function (done) { 177 | var buf 178 | var cb = after(2, done) 179 | var client 180 | var drained = false 181 | var resp 182 | var server = createServer({ threshold: 0 }, function (req, res) { 183 | resp = res 184 | 185 | res.on('drain', function () { 186 | drained = true 187 | }) 188 | 189 | res.setHeader('Content-Type', 'text/plain') 190 | res.write('start') 191 | pressure() 192 | }) 193 | 194 | crypto.randomBytes(1024 * 128, function (err, chunk) { 195 | if (err) return done(err) 196 | buf = chunk 197 | pressure() 198 | }) 199 | 200 | function pressure () { 201 | if (!buf || !resp || !client) return 202 | 203 | assert.ok(!drained) 204 | 205 | while (resp.write(buf) !== false) { 206 | resp.flush() 207 | } 208 | 209 | resp.on('drain', function () { 210 | assert.ok(resp.write('end')) 211 | resp.end() 212 | }) 213 | 214 | resp.on('finish', cb) 215 | client.resume() 216 | } 217 | 218 | request(server) 219 | .get('/') 220 | .request() 221 | .on('response', function (res) { 222 | client = res 223 | assert.strictEqual(res.headers['content-encoding'], 'gzip') 224 | res.pause() 225 | res.on('end', function () { 226 | server.close(cb) 227 | }) 228 | pressure() 229 | }) 230 | .end() 231 | }) 232 | 233 | it('should back-pressure when uncompressed', function (done) { 234 | var buf 235 | var cb = after(2, done) 236 | var client 237 | var drained = false 238 | var resp 239 | var server = createServer({ filter: function () { return false } }, function (req, res) { 240 | resp = res 241 | 242 | res.on('drain', function () { 243 | drained = true 244 | }) 245 | 246 | res.setHeader('Content-Type', 'text/plain') 247 | res.write('start') 248 | pressure() 249 | }) 250 | 251 | crypto.randomBytes(1024 * 128, function (err, chunk) { 252 | if (err) return done(err) 253 | buf = chunk 254 | pressure() 255 | }) 256 | 257 | function pressure () { 258 | if (!buf || !resp || !client) return 259 | 260 | while (resp.write(buf) !== false) { 261 | resp.flush() 262 | } 263 | 264 | resp.on('drain', function () { 265 | assert.ok(drained) 266 | assert.ok(resp.write('end')) 267 | resp.end() 268 | }) 269 | resp.on('finish', cb) 270 | client.resume() 271 | } 272 | 273 | request(server) 274 | .get('/') 275 | .request() 276 | .on('response', function (res) { 277 | client = res 278 | shouldNotHaveHeader('Content-Encoding')(res) 279 | res.pause() 280 | res.on('end', function () { 281 | server.close(cb) 282 | }) 283 | pressure() 284 | }) 285 | .end() 286 | }) 287 | 288 | it('should transfer large bodies', function (done) { 289 | var len = bytes('1mb') 290 | var buf = Buffer.alloc(len, '.') 291 | var server = createServer({ threshold: 0 }, function (req, res) { 292 | res.setHeader('Content-Type', 'text/plain') 293 | res.end(buf) 294 | }) 295 | 296 | request(server) 297 | .get('/') 298 | .set('Accept-Encoding', 'gzip') 299 | .expect('Transfer-Encoding', 'chunked') 300 | .expect('Content-Encoding', 'gzip') 301 | .expect(shouldHaveBodyLength(len)) 302 | .expect(200, buf.toString(), done) 303 | }) 304 | 305 | it('should transfer large bodies with multiple writes', function (done) { 306 | var len = bytes('40kb') 307 | var buf = Buffer.alloc(len, '.') 308 | var server = createServer({ threshold: 0 }, function (req, res) { 309 | res.setHeader('Content-Type', 'text/plain') 310 | res.write(buf) 311 | res.write(buf) 312 | res.write(buf) 313 | res.end(buf) 314 | }) 315 | 316 | request(server) 317 | .get('/') 318 | .set('Accept-Encoding', 'gzip') 319 | .expect('Transfer-Encoding', 'chunked') 320 | .expect('Content-Encoding', 'gzip') 321 | .expect(shouldHaveBodyLength(len * 4)) 322 | .expect(200, done) 323 | }) 324 | 325 | describeHttp2('http2', function () { 326 | it('should work with http2 server', function (done) { 327 | var server = createHttp2Server({ threshold: 0 }, function (req, res) { 328 | res.setHeader(http2.constants.HTTP2_HEADER_CONTENT_TYPE, 'text/plain') 329 | res.end('hello, world') 330 | }) 331 | server.on('listening', function () { 332 | var client = createHttp2Client(server.address().port) 333 | // using ES5 as Node.js <=4.0.0 does not have Computed Property Names 334 | var reqHeaders = {} 335 | reqHeaders[http2.constants.HTTP2_HEADER_ACCEPT_ENCODING] = 'gzip' 336 | var request = client.request(reqHeaders) 337 | request.on('response', function (headers) { 338 | assert.strictEqual(headers[http2.constants.HTTP2_HEADER_STATUS], 200) 339 | assert.strictEqual(headers[http2.constants.HTTP2_HEADER_CONTENT_TYPE], 'text/plain') 340 | assert.strictEqual(headers[http2.constants.HTTP2_HEADER_CONTENT_ENCODING], 'gzip') 341 | }) 342 | var chunks = [] 343 | request.on('data', function (chunk) { 344 | chunks.push(chunk) 345 | }) 346 | request.on('end', function () { 347 | closeHttp2(client, server, function () { 348 | zlib.gunzip(Buffer.concat(chunks), function (err, data) { 349 | assert.ok(!err) 350 | assert.strictEqual(data.toString(), 'hello, world') 351 | done() 352 | }) 353 | }) 354 | }) 355 | request.end() 356 | }) 357 | }) 358 | }) 359 | 360 | describe('threshold', function () { 361 | it('should not compress responses below the threshold size', function (done) { 362 | var server = createServer({ threshold: '1kb' }, function (req, res) { 363 | res.setHeader('Content-Type', 'text/plain') 364 | res.setHeader('Content-Length', '12') 365 | res.end('hello, world') 366 | }) 367 | 368 | request(server) 369 | .get('/') 370 | .set('Accept-Encoding', 'gzip') 371 | .expect(shouldNotHaveHeader('Content-Encoding')) 372 | .expect(200, done) 373 | }) 374 | 375 | it('should compress responses above the threshold size', function (done) { 376 | var server = createServer({ threshold: '1kb' }, function (req, res) { 377 | res.setHeader('Content-Type', 'text/plain') 378 | res.setHeader('Content-Length', '2048') 379 | res.end(Buffer.alloc(2048)) 380 | }) 381 | 382 | request(server) 383 | .get('/') 384 | .set('Accept-Encoding', 'gzip') 385 | .expect('Content-Encoding', 'gzip', done) 386 | }) 387 | 388 | it('should compress when streaming without a content-length', function (done) { 389 | var server = createServer({ threshold: '1kb' }, function (req, res) { 390 | res.setHeader('Content-Type', 'text/plain') 391 | res.write('hello, ') 392 | setTimeout(function () { 393 | res.end('world') 394 | }, 10) 395 | }) 396 | 397 | request(server) 398 | .get('/') 399 | .set('Accept-Encoding', 'gzip') 400 | .expect('Content-Encoding', 'gzip', done) 401 | }) 402 | 403 | it('should not compress when streaming and content-length is lower than threshold', function (done) { 404 | var server = createServer({ threshold: '1kb' }, function (req, res) { 405 | res.setHeader('Content-Type', 'text/plain') 406 | res.setHeader('Content-Length', '12') 407 | res.write('hello, ') 408 | setTimeout(function () { 409 | res.end('world') 410 | }, 10) 411 | }) 412 | 413 | request(server) 414 | .get('/') 415 | .set('Accept-Encoding', 'gzip') 416 | .expect(shouldNotHaveHeader('Content-Encoding')) 417 | .expect(200, done) 418 | }) 419 | 420 | it('should compress when streaming and content-length is larger than threshold', function (done) { 421 | var server = createServer({ threshold: '1kb' }, function (req, res) { 422 | res.setHeader('Content-Type', 'text/plain') 423 | res.setHeader('Content-Length', '2048') 424 | res.write(Buffer.alloc(1024)) 425 | setTimeout(function () { 426 | res.end(Buffer.alloc(1024)) 427 | }, 10) 428 | }) 429 | 430 | request(server) 431 | .get('/') 432 | .set('Accept-Encoding', 'gzip') 433 | .expect('Content-Encoding', 'gzip', done) 434 | }) 435 | 436 | // res.end(str, encoding) broken in node.js 0.8 437 | var run = /^v0\.8\./.test(process.version) ? it.skip : it 438 | run('should handle writing hex data', function (done) { 439 | var server = createServer({ threshold: 6 }, function (req, res) { 440 | res.setHeader('Content-Type', 'text/plain') 441 | res.end('2e2e2e2e', 'hex') 442 | }) 443 | 444 | request(server) 445 | .get('/') 446 | .set('Accept-Encoding', 'gzip') 447 | .expect(shouldNotHaveHeader('Content-Encoding')) 448 | .expect(200, '....', done) 449 | }) 450 | 451 | it('should consider res.end() as 0 length', function (done) { 452 | var server = createServer({ threshold: 1 }, function (req, res) { 453 | res.setHeader('Content-Type', 'text/plain') 454 | res.end() 455 | }) 456 | 457 | request(server) 458 | .get('/') 459 | .set('Accept-Encoding', 'gzip') 460 | .expect(shouldNotHaveHeader('Content-Encoding')) 461 | .expect(200, '', done) 462 | }) 463 | 464 | it('should work with res.end(null)', function (done) { 465 | var server = createServer({ threshold: 1000 }, function (req, res) { 466 | res.setHeader('Content-Type', 'text/plain') 467 | res.end(null) 468 | }) 469 | 470 | request(server) 471 | .get('/') 472 | .set('Accept-Encoding', 'gzip') 473 | .expect(shouldNotHaveHeader('Content-Encoding')) 474 | .expect(200, '', done) 475 | }) 476 | }) 477 | 478 | describe('when "Accept-Encoding: gzip"', function () { 479 | it('should respond with gzip', function (done) { 480 | var server = createServer({ threshold: 0 }, function (req, res) { 481 | res.setHeader('Content-Type', 'text/plain') 482 | res.end('hello, world') 483 | }) 484 | 485 | request(server) 486 | .get('/') 487 | .set('Accept-Encoding', 'gzip') 488 | .expect('Content-Encoding', 'gzip', done) 489 | }) 490 | 491 | it('should return false writing after end', function (done) { 492 | var server = createServer({ threshold: 0 }, function (req, res) { 493 | res.setHeader('Content-Type', 'text/plain') 494 | res.end('hello, world') 495 | assert.ok(res.write() === false) 496 | assert.ok(res.end() === false) 497 | }) 498 | 499 | request(server) 500 | .get('/') 501 | .set('Accept-Encoding', 'gzip') 502 | .expect('Content-Encoding', 'gzip', done) 503 | }) 504 | }) 505 | 506 | describe('when "Accept-Encoding: deflate"', function () { 507 | it('should respond with deflate', function (done) { 508 | var server = createServer({ threshold: 0 }, function (req, res) { 509 | res.setHeader('Content-Type', 'text/plain') 510 | res.end('hello, world') 511 | }) 512 | 513 | request(server) 514 | .get('/') 515 | .set('Accept-Encoding', 'deflate') 516 | .expect('Content-Encoding', 'deflate', done) 517 | }) 518 | }) 519 | 520 | describe('when "Accept-Encoding: br"', function () { 521 | brotli('should respond with br', function (done) { 522 | var server = createServer({ threshold: 0 }, function (req, res) { 523 | res.setHeader('Content-Type', 'text/plain') 524 | res.end('hello, world') 525 | }) 526 | 527 | request(server) 528 | .get('/') 529 | .set('Accept-Encoding', 'br') 530 | .expect('Content-Encoding', 'br', done) 531 | }) 532 | }) 533 | 534 | describe('when "Accept-Encoding: br" and passing compression level', function () { 535 | brotli('should respond with br', function (done) { 536 | var params = {} 537 | params[zlib.constants.BROTLI_PARAM_QUALITY] = 11 538 | 539 | var server = createServer({ threshold: 0, brotli: { params: params } }, function (req, res) { 540 | res.setHeader('Content-Type', 'text/plain') 541 | res.end('hello, world') 542 | }) 543 | 544 | request(server) 545 | .get('/') 546 | .set('Accept-Encoding', 'br') 547 | .expect('Content-Encoding', 'br', done) 548 | }) 549 | 550 | brotli('shouldn\'t break compression when gzip is requested', function (done) { 551 | var params = {} 552 | params[zlib.constants.BROTLI_PARAM_QUALITY] = 8 553 | 554 | var server = createServer({ threshold: 0, brotli: { params: params } }, function (req, res) { 555 | res.setHeader('Content-Type', 'text/plain') 556 | res.end('hello, world') 557 | }) 558 | 559 | request(server) 560 | .get('/') 561 | .set('Accept-Encoding', 'gzip') 562 | .expect('Content-Encoding', 'gzip', done) 563 | }) 564 | }) 565 | 566 | describe('when "Accept-Encoding: gzip, deflate"', function () { 567 | it('should respond with gzip', function (done) { 568 | var server = createServer({ threshold: 0 }, function (req, res) { 569 | res.setHeader('Content-Type', 'text/plain') 570 | res.end('hello, world') 571 | }) 572 | 573 | request(server) 574 | .get('/') 575 | .set('Accept-Encoding', 'gzip, deflate') 576 | .expect('Content-Encoding', 'gzip', done) 577 | }) 578 | }) 579 | 580 | describe('when "Accept-Encoding: deflate, gzip"', function () { 581 | it('should respond with gzip', function (done) { 582 | var server = createServer({ threshold: 0 }, function (req, res) { 583 | res.setHeader('Content-Type', 'text/plain') 584 | res.end('hello, world') 585 | }) 586 | 587 | request(server) 588 | .get('/') 589 | .set('Accept-Encoding', 'deflate, gzip') 590 | .expect('Content-Encoding', 'gzip', done) 591 | }) 592 | }) 593 | 594 | describe('when "Accept-Encoding: gzip, br"', function () { 595 | var brotli = hasBrotliSupport ? it : it.skip 596 | brotli('should respond with br', function (done) { 597 | var server = createServer({ threshold: 0 }, function (req, res) { 598 | res.setHeader('Content-Type', 'text/plain') 599 | res.end('hello, world') 600 | }) 601 | 602 | request(server) 603 | .get('/') 604 | .set('Accept-Encoding', 'gzip, br') 605 | .expect('Content-Encoding', 'br', done) 606 | }) 607 | 608 | brotli = hasBrotliSupport ? it.skip : it 609 | 610 | brotli('should respond with gzip', function (done) { 611 | var server = createServer({ threshold: 0 }, function (req, res) { 612 | res.setHeader('Content-Type', 'text/plain') 613 | res.end('hello, world') 614 | }) 615 | 616 | request(server) 617 | .get('/') 618 | .set('Accept-Encoding', 'br, gzip') 619 | .expect('Content-Encoding', 'gzip', done) 620 | }) 621 | }) 622 | 623 | describe('when "Accept-Encoding: deflate, gzip, br"', function () { 624 | brotli('should respond with br', function (done) { 625 | var server = createServer({ threshold: 0 }, function (req, res) { 626 | res.setHeader('Content-Type', 'text/plain') 627 | res.end('hello, world') 628 | }) 629 | 630 | request(server) 631 | .get('/') 632 | .set('Accept-Encoding', 'deflate, gzip, br') 633 | .expect('Content-Encoding', 'br', done) 634 | }) 635 | }) 636 | 637 | describe('when "Accept-Encoding: gzip;q=1, br;q=0.3"', function () { 638 | brotli('should respond with gzip', function (done) { 639 | var server = createServer({ threshold: 0 }, function (req, res) { 640 | res.setHeader('Content-Type', 'text/plain') 641 | res.end('hello, world') 642 | }) 643 | 644 | request(server) 645 | .get('/') 646 | .set('Accept-Encoding', 'gzip;q=1, br;q=0.3') 647 | .expect('Content-Encoding', 'gzip', done) 648 | }) 649 | }) 650 | 651 | describe('when "Accept-Encoding: gzip, br;q=0.8"', function () { 652 | brotli('should respond with gzip', function (done) { 653 | var server = createServer({ threshold: 0 }, function (req, res) { 654 | res.setHeader('Content-Type', 'text/plain') 655 | res.end('hello, world') 656 | }) 657 | 658 | request(server) 659 | .get('/') 660 | .set('Accept-Encoding', 'gzip, br;q=0.8') 661 | .expect('Content-Encoding', 'gzip', done) 662 | }) 663 | }) 664 | 665 | describe('when "Accept-Encoding: gzip;q=0.001"', function () { 666 | brotli('should respond with gzip', function (done) { 667 | var server = createServer({ threshold: 0 }, function (req, res) { 668 | res.setHeader('Content-Type', 'text/plain') 669 | res.end('hello, world') 670 | }) 671 | 672 | request(server) 673 | .get('/') 674 | .set('Accept-Encoding', 'gzip;q=0.001') 675 | .expect('Content-Encoding', 'gzip', done) 676 | }) 677 | }) 678 | 679 | describe('when "Accept-Encoding: deflate, br"', function () { 680 | brotli('should respond with br', function (done) { 681 | var server = createServer({ threshold: 0 }, function (req, res) { 682 | res.setHeader('Content-Type', 'text/plain') 683 | res.end('hello, world') 684 | }) 685 | 686 | request(server) 687 | .get('/') 688 | .set('Accept-Encoding', 'deflate, br') 689 | .expect('Content-Encoding', 'br', done) 690 | }) 691 | }) 692 | 693 | describe('when "Cache-Control: no-transform" response header', function () { 694 | it('should not compress response', function (done) { 695 | var server = createServer({ threshold: 0 }, function (req, res) { 696 | res.setHeader('Cache-Control', 'no-transform') 697 | res.setHeader('Content-Type', 'text/plain') 698 | res.end('hello, world') 699 | }) 700 | 701 | request(server) 702 | .get('/') 703 | .set('Accept-Encoding', 'gzip') 704 | .expect('Cache-Control', 'no-transform') 705 | .expect(shouldNotHaveHeader('Content-Encoding')) 706 | .expect(200, 'hello, world', done) 707 | }) 708 | 709 | it('should not set Vary headerh', function (done) { 710 | var server = createServer({ threshold: 0 }, function (req, res) { 711 | res.setHeader('Cache-Control', 'no-transform') 712 | res.setHeader('Content-Type', 'text/plain') 713 | res.end('hello, world') 714 | }) 715 | 716 | request(server) 717 | .get('/') 718 | .set('Accept-Encoding', 'gzip') 719 | .expect('Cache-Control', 'no-transform') 720 | .expect(shouldNotHaveHeader('Vary')) 721 | .expect(200, done) 722 | }) 723 | }) 724 | 725 | describe('.filter', function () { 726 | it('should be a function', function () { 727 | assert.strictEqual(typeof compression.filter, 'function') 728 | }) 729 | 730 | it('should return false on empty response', function (done) { 731 | var server = http.createServer(function (req, res) { 732 | res.end(String(compression.filter(req, res))) 733 | }) 734 | 735 | request(server) 736 | .get('/') 737 | .expect(200, 'false', done) 738 | }) 739 | 740 | it('should return true for "text/plain"', function (done) { 741 | var server = http.createServer(function (req, res) { 742 | res.setHeader('Content-Type', 'text/plain') 743 | res.end(String(compression.filter(req, res))) 744 | }) 745 | 746 | request(server) 747 | .get('/') 748 | .expect(200, 'true', done) 749 | }) 750 | 751 | it('should return false for "application/x-bogus"', function (done) { 752 | var server = http.createServer(function (req, res) { 753 | res.setHeader('Content-Type', 'application/x-bogus') 754 | res.end(String(compression.filter(req, res))) 755 | }) 756 | 757 | request(server) 758 | .get('/') 759 | .expect(200, 'false', done) 760 | }) 761 | }) 762 | 763 | describe('res.flush()', function () { 764 | it('should always be present', function (done) { 765 | var server = createServer(null, function (req, res) { 766 | res.statusCode = typeof res.flush === 'function' 767 | ? 200 768 | : 500 769 | res.flush() 770 | res.end() 771 | }) 772 | 773 | request(server) 774 | .get('/') 775 | .expect(200, done) 776 | }) 777 | 778 | it('should flush the response', function (done) { 779 | var chunks = 0 780 | var next 781 | var server = createServer({ threshold: 0 }, function (req, res) { 782 | next = writeAndFlush(res, 2, Buffer.alloc(1024)) 783 | res.setHeader('Content-Type', 'text/plain') 784 | res.setHeader('Content-Length', '2048') 785 | next() 786 | }) 787 | 788 | function onchunk (chunk) { 789 | assert.ok(chunks++ < 2) 790 | assert.strictEqual(chunk.length, 1024) 791 | next() 792 | } 793 | 794 | request(server) 795 | .get('/') 796 | .set('Accept-Encoding', 'gzip') 797 | .request() 798 | .on('response', unchunk('gzip', onchunk, function (err) { 799 | if (err) return done(err) 800 | server.close(done) 801 | })) 802 | .end() 803 | }) 804 | 805 | it('should flush small chunks for gzip', function (done) { 806 | var chunks = 0 807 | var next 808 | var server = createServer({ threshold: 0 }, function (req, res) { 809 | next = writeAndFlush(res, 2, Buffer.from('..')) 810 | res.setHeader('Content-Type', 'text/plain') 811 | next() 812 | }) 813 | 814 | function onchunk (chunk) { 815 | assert.ok(chunks++ < 20) 816 | assert.strictEqual(chunk.toString(), '..') 817 | next() 818 | } 819 | 820 | request(server) 821 | .get('/') 822 | .set('Accept-Encoding', 'gzip') 823 | .request() 824 | .on('response', unchunk('gzip', onchunk, function (err) { 825 | if (err) return done(err) 826 | server.close(done) 827 | })) 828 | .end() 829 | }) 830 | 831 | brotli('should flush small chunks for brotli', function (done) { 832 | var chunks = 0 833 | var next 834 | var server = createServer({ threshold: 0 }, function (req, res) { 835 | next = writeAndFlush(res, 2, Buffer.from('..')) 836 | res.setHeader('Content-Type', 'text/plain') 837 | next() 838 | }) 839 | 840 | function onchunk (chunk) { 841 | assert.ok(chunks++ < 20) 842 | assert.strictEqual(chunk.toString(), '..') 843 | next() 844 | } 845 | 846 | request(server) 847 | .get('/') 848 | .set('Accept-Encoding', 'br') 849 | .request() 850 | .on('response', unchunk('br', onchunk, function (err) { 851 | if (err) return done(err) 852 | server.close(done) 853 | })) 854 | .end() 855 | }) 856 | 857 | it('should flush small chunks for deflate', function (done) { 858 | var chunks = 0 859 | var next 860 | var server = createServer({ threshold: 0 }, function (req, res) { 861 | next = writeAndFlush(res, 2, Buffer.from('..')) 862 | res.setHeader('Content-Type', 'text/plain') 863 | next() 864 | }) 865 | 866 | function onchunk (chunk) { 867 | assert.ok(chunks++ < 20) 868 | assert.strictEqual(chunk.toString(), '..') 869 | next() 870 | } 871 | 872 | request(server) 873 | .get('/') 874 | .set('Accept-Encoding', 'deflate') 875 | .request() 876 | .on('response', unchunk('deflate', onchunk, function (err) { 877 | if (err) return done(err) 878 | server.close(done) 879 | })) 880 | .end() 881 | }) 882 | }) 883 | 884 | describe('enforceEncoding', function () { 885 | it('should compress the provided encoding and not the default encoding', function (done) { 886 | var server = createServer({ threshold: 0, enforceEncoding: 'deflate' }, function (req, res) { 887 | res.setHeader('Content-Type', 'text/plain') 888 | res.end('hello, world') 889 | }) 890 | 891 | request(server) 892 | .get('/') 893 | .set('Accept-Encoding', 'gzip') 894 | .expect('Content-Encoding', 'gzip') 895 | .expect(200, 'hello, world', done) 896 | }) 897 | 898 | it('should not compress when enforceEncoding is identity', function (done) { 899 | var server = createServer({ threshold: 0, enforceEncoding: 'identity' }, function (req, res) { 900 | res.setHeader('Content-Type', 'text/plain') 901 | res.end('hello, world') 902 | }) 903 | 904 | request(server) 905 | .get('/') 906 | .set('Accept-Encoding', '') 907 | .expect(shouldNotHaveHeader('Content-Encoding')) 908 | .expect(200, 'hello, world', done) 909 | }) 910 | 911 | it('should compress when enforceEncoding is gzip', function (done) { 912 | var server = createServer({ threshold: 0, enforceEncoding: 'gzip' }, function (req, res) { 913 | res.setHeader('Content-Type', 'text/plain') 914 | res.end('hello, world') 915 | }) 916 | 917 | request(server) 918 | .get('/') 919 | .set('Accept-Encoding', '') 920 | .expect('Content-Encoding', 'gzip') 921 | .expect(200, 'hello, world', done) 922 | }) 923 | 924 | it('should compress when enforceEncoding is deflate', function (done) { 925 | var server = createServer({ threshold: 0, enforceEncoding: 'deflate' }, function (req, res) { 926 | res.setHeader('Content-Type', 'text/plain') 927 | res.end('hello, world') 928 | }) 929 | 930 | request(server) 931 | .get('/') 932 | .set('Accept-Encoding', '') 933 | .expect('Content-Encoding', 'deflate') 934 | .expect(200, 'hello, world', done) 935 | }) 936 | 937 | brotli('should compress when enforceEncoding is brotli', function (done) { 938 | var server = createServer({ threshold: 0, enforceEncoding: 'br' }, function (req, res) { 939 | res.setHeader('Content-Type', 'text/plain') 940 | res.end('hello, world') 941 | }) 942 | 943 | request(server) 944 | .get('/') 945 | .set('Accept-Encoding', '') 946 | .expect('Content-Encoding', 'br') 947 | .expect(200, done) 948 | }) 949 | 950 | it('should not compress when enforceEncoding is unknown', function (done) { 951 | var server = createServer({ threshold: 0, enforceEncoding: 'bogus' }, function (req, res) { 952 | res.setHeader('Content-Type', 'text/plain') 953 | res.end('hello, world') 954 | }) 955 | 956 | request(server) 957 | .get('/') 958 | .set('Accept-Encoding', '') 959 | .expect(shouldNotHaveHeader('Content-Encoding')) 960 | .expect(200, 'hello, world', done) 961 | }) 962 | 963 | it('should not compress when enforceEnconding is *', function (done) { 964 | var server = createServer({ threshold: 0, enforceEncoding: '*' }, function (req, res) { 965 | res.setHeader('Content-Type', 'text/plain') 966 | res.end('hello, world') 967 | }) 968 | 969 | request(server) 970 | .get('/') 971 | .set('Accept-Encoding', '') 972 | .expect(shouldNotHaveHeader('Content-Encoding')) 973 | .expect(200, done) 974 | }) 975 | }) 976 | }) 977 | 978 | function createServer (opts, fn) { 979 | var _compression = compression(opts) 980 | return http.createServer(function (req, res) { 981 | _compression(req, res, function (err) { 982 | if (err) { 983 | res.statusCode = err.status || 500 984 | res.end(err.message) 985 | return 986 | } 987 | 988 | fn(req, res) 989 | }) 990 | }) 991 | } 992 | 993 | function createHttp2Server (opts, fn) { 994 | var _compression = compression(opts) 995 | var server = http2.createServer(function (req, res) { 996 | _compression(req, res, function (err) { 997 | if (err) { 998 | res.statusCode = err.status || 500 999 | res.end(err.message) 1000 | return 1001 | } 1002 | 1003 | fn(req, res) 1004 | }) 1005 | }) 1006 | server.listen(0, '127.0.0.1') 1007 | return server 1008 | } 1009 | 1010 | function createHttp2Client (port) { 1011 | return http2.connect('http://127.0.0.1:' + port) 1012 | } 1013 | 1014 | function closeHttp2 (client, server, callback) { 1015 | if (typeof client.shutdown === 'function') { 1016 | // this is the node v8.x way of closing the connections 1017 | client.shutdown({}, function () { 1018 | server.close(function () { 1019 | callback() 1020 | }) 1021 | }) 1022 | } else { 1023 | // this is the node v9.x onwards way of closing the connections 1024 | client.close(function () { 1025 | // force existing connections to time out after 1ms. 1026 | // this is done to force the server to close in some cases where it wouldn't do it otherwise. 1027 | server.close(function () { 1028 | callback() 1029 | }) 1030 | }) 1031 | } 1032 | } 1033 | 1034 | function shouldHaveBodyLength (length) { 1035 | return function (res) { 1036 | assert.strictEqual(res.text.length, length, 'should have body length of ' + length) 1037 | } 1038 | } 1039 | 1040 | function shouldNotHaveHeader (header) { 1041 | return function (res) { 1042 | assert.ok(!(header.toLowerCase() in res.headers), 'should not have header ' + header) 1043 | } 1044 | } 1045 | 1046 | function writeAndFlush (stream, count, buf) { 1047 | var writes = 0 1048 | 1049 | return function () { 1050 | if (writes++ >= count) return 1051 | if (writes === count) return stream.end(buf) 1052 | stream.write(buf) 1053 | stream.flush() 1054 | } 1055 | } 1056 | 1057 | function unchunk (encoding, onchunk, onend) { 1058 | return function (res) { 1059 | var stream 1060 | 1061 | assert.strictEqual(res.headers['content-encoding'], encoding) 1062 | 1063 | switch (encoding) { 1064 | case 'deflate': 1065 | stream = res.pipe(zlib.createInflate()) 1066 | break 1067 | case 'gzip': 1068 | stream = res.pipe(zlib.createGunzip()) 1069 | break 1070 | case 'br': 1071 | stream = res.pipe(zlib.createBrotliDecompress()) 1072 | break 1073 | } 1074 | 1075 | stream.on('data', onchunk) 1076 | stream.on('end', onend) 1077 | } 1078 | } 1079 | --------------------------------------------------------------------------------