├── .eslintignore ├── test ├── .eslintrc.yml └── test.js ├── .eslintrc.yml ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── codeql.yml │ ├── scorecard.yml │ └── ci.yml ├── benchmark ├── index.js └── append.js ├── HISTORY.md ├── LICENSE ├── package.json ├── README.md └── index.js /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | mocha: true 3 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | extends: standard 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /.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"] 18 | -------------------------------------------------------------------------------- /benchmark/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | var spawn = require('child_process').spawn 4 | 5 | var exe = process.argv[0] 6 | var cwd = process.cwd() 7 | 8 | for (var dep in process.versions) { 9 | console.log(' %s@%s', dep, process.versions[dep]) 10 | } 11 | 12 | console.log('') 13 | 14 | runScripts(fs.readdirSync(__dirname)) 15 | 16 | function runScripts (fileNames) { 17 | var fileName = fileNames.shift() 18 | 19 | if (!fileName) return 20 | if (!/\.js$/i.test(fileName)) return runScripts(fileNames) 21 | if (fileName.toLowerCase() === 'index.js') return runScripts(fileNames) 22 | 23 | var fullPath = path.join(__dirname, fileName) 24 | 25 | console.log('> %s %s', exe, path.relative(cwd, fullPath)) 26 | 27 | var proc = spawn(exe, [fullPath], { 28 | stdio: 'inherit' 29 | }) 30 | 31 | proc.on('exit', function () { 32 | runScripts(fileNames) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | 1.1.2 / 2017-09-23 2 | ================== 3 | 4 | * perf: improve header token parsing speed 5 | 6 | 1.1.1 / 2017-03-20 7 | ================== 8 | 9 | * perf: hoist regular expression 10 | 11 | 1.1.0 / 2015-09-29 12 | ================== 13 | 14 | * Only accept valid field names in the `field` argument 15 | - Ensures the resulting string is a valid HTTP header value 16 | 17 | 1.0.1 / 2015-07-08 18 | ================== 19 | 20 | * Fix setting empty header from empty `field` 21 | * perf: enable strict mode 22 | * perf: remove argument reassignments 23 | 24 | 1.0.0 / 2014-08-10 25 | ================== 26 | 27 | * Accept valid `Vary` header string as `field` 28 | * Add `vary.append` for low-level string manipulation 29 | * Move to `jshttp` organization 30 | 31 | 0.1.0 / 2014-06-05 32 | ================== 33 | 34 | * Support array of fields to set 35 | 36 | 0.0.0 / 2014-06-04 37 | ================== 38 | 39 | * Initial release 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2014-2017 Douglas Christopher Wilson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vary", 3 | "description": "Manipulate the HTTP Vary header", 4 | "version": "1.1.2", 5 | "author": "Douglas Christopher Wilson ", 6 | "license": "MIT", 7 | "keywords": [ 8 | "http", 9 | "res", 10 | "vary" 11 | ], 12 | "repository": "jshttp/vary", 13 | "funding": { 14 | "type": "opencollective", 15 | "url": "https://opencollective.com/express" 16 | }, 17 | "devDependencies": { 18 | "beautify-benchmark": "0.2.4", 19 | "benchmark": "2.1.4", 20 | "eslint": "7.14.0", 21 | "eslint-config-standard": "14.1.1", 22 | "eslint-plugin-import": "2.22.0", 23 | "eslint-plugin-markdown": "1.0.2", 24 | "eslint-plugin-node": "11.1.0", 25 | "eslint-plugin-promise": "4.2.1", 26 | "eslint-plugin-standard": "4.1.0", 27 | "mocha": "10.4.0", 28 | "nyc": "^15.1.0", 29 | "supertest": "4.0.2" 30 | }, 31 | "files": [ 32 | "HISTORY.md", 33 | "LICENSE", 34 | "README.md", 35 | "index.js" 36 | ], 37 | "engines": { 38 | "node": ">= 0.8" 39 | }, 40 | "scripts": { 41 | "bench": "node benchmark/index.js", 42 | "lint": "eslint --plugin markdown --ext js,md .", 43 | "test": "mocha --reporter spec --bail --check-leaks test/", 44 | "test-ci": "nyc --reporter=lcov --reporter=text npm test", 45 | "test-cov": "nyc --reporter=html --reporter=text npm test" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /benchmark/append.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var benchmark = require('benchmark') 7 | var benchmarks = require('beautify-benchmark') 8 | 9 | /** 10 | * Globals for benchmark.js 11 | */ 12 | 13 | global.vary = require('..') 14 | 15 | var suite = new benchmark.Suite() 16 | 17 | suite.add({ 18 | name: 'field to *', 19 | minSamples: 100, 20 | fn: 'var header = vary.append("*", "Accept-Encoding")' 21 | }) 22 | 23 | suite.add({ 24 | name: '* to field', 25 | minSamples: 100, 26 | fn: 'var header = vary.append("Accept-Encoding", "*")' 27 | }) 28 | 29 | suite.add({ 30 | name: 'field to empty', 31 | minSamples: 100, 32 | fn: 'var header = vary.append("", "Accept-Encoding")' 33 | }) 34 | 35 | suite.add({ 36 | name: 'fields array to empty', 37 | minSamples: 100, 38 | fn: 'var header = vary.append("", ["Accept", "Accept-Encoding", "Accept-Language"])' 39 | }) 40 | 41 | suite.add({ 42 | name: 'fields string to empty', 43 | minSamples: 100, 44 | fn: 'var header = vary.append("", "Accept, Accept-Encoding, Accept-Language")' 45 | }) 46 | 47 | suite.add({ 48 | name: 'field to fields', 49 | minSamples: 100, 50 | fn: 'var header = vary.append("Accept, Accept-Encoding, Accept-Language", "X-Foo")' 51 | }) 52 | 53 | suite.on('start', function onCycle (event) { 54 | process.stdout.write(' append\n\n') 55 | }) 56 | 57 | suite.on('cycle', function onCycle (event) { 58 | benchmarks.add(event.target) 59 | }) 60 | 61 | suite.on('complete', function onComplete () { 62 | benchmarks.log() 63 | }) 64 | 65 | suite.run({ async: false }) 66 | -------------------------------------------------------------------------------- /.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 | strategy: 36 | fail-fast: false 37 | matrix: 38 | language: ["javascript"] 39 | # CodeQL supports [ $supported-codeql-languages ] 40 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 41 | 42 | steps: 43 | - name: Checkout repository 44 | uses: actions/checkout@v6 45 | 46 | # Initializes the CodeQL tools for scanning. 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@v4.31.5 49 | with: 50 | languages: ${{ matrix.language }} 51 | # If you wish to specify custom queries, you can do so here or in a config file. 52 | # By default, queries listed here will override any specified in a config file. 53 | # Prefix the list here with "+" to use these queries and those in the config file. 54 | 55 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 56 | # If this step fails, then you should remove it and run the build manually (see below) 57 | - name: Autobuild 58 | uses: github/codeql-action/autobuild@v4.31.5 59 | 60 | # ℹ️ Command-line programs to run using the OS shell. 61 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 62 | 63 | # If the Autobuild fails above, remove it and uncomment the following three lines. 64 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 65 | 66 | # - run: | 67 | # echo "Run, Build Application using script" 68 | # ./location_of_script_within_repo/buildscript.sh 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v4.31.5 72 | with: 73 | category: "/language:${{matrix.language}}" 74 | -------------------------------------------------------------------------------- /.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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.1.2 34 | with: 35 | persist-credentials: false 36 | 37 | - name: "Run analysis" 38 | uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 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@ecec1f88769052ebc45aa0affc53ea30d474cffa # v3.31.2 69 | with: 70 | sarif_file: results.sarif 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vary 2 | 3 | [![NPM Version][npm-version-image]][npm-url] 4 | [![NPM Downloads][npm-downloads-image]][npm-url] 5 | [![Node.js Version][node-version-image]][node-version-url] 6 | [![Build Status][ci-image]][ci-url] 7 | [![Coverage Status][coveralls-image]][coveralls-url] 8 | 9 | Manipulate the HTTP Vary header 10 | 11 | ## Installation 12 | 13 | This is a [Node.js](https://nodejs.org/en/) module available through the 14 | [npm registry](https://www.npmjs.com/). Installation is done using the 15 | [`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): 16 | 17 | ```sh 18 | $ npm install vary 19 | ``` 20 | 21 | ## API 22 | 23 | 24 | 25 | ```js 26 | var vary = require('vary') 27 | ``` 28 | 29 | ### vary(res, field) 30 | 31 | Adds the given header `field` to the `Vary` response header of `res`. 32 | This can be a string of a single field, a string of a valid `Vary` 33 | header, or an array of multiple fields. 34 | 35 | This will append the header if not already listed, otherwise leaves 36 | it listed in the current location. 37 | 38 | 39 | 40 | ```js 41 | // Append "Origin" to the Vary header of the response 42 | vary(res, 'Origin') 43 | ``` 44 | 45 | ### vary.append(header, field) 46 | 47 | Adds the given header `field` to the `Vary` response header string `header`. 48 | This can be a string of a single field, a string of a valid `Vary` header, 49 | or an array of multiple fields. 50 | 51 | This will append the header if not already listed, otherwise leaves 52 | it listed in the current location. The new header string is returned. 53 | 54 | 55 | 56 | ```js 57 | // Get header string appending "Origin" to "Accept, User-Agent" 58 | vary.append('Accept, User-Agent', 'Origin') 59 | ``` 60 | 61 | ## Examples 62 | 63 | ### Updating the Vary header when content is based on it 64 | 65 | ```js 66 | var http = require('http') 67 | var vary = require('vary') 68 | 69 | http.createServer(function onRequest (req, res) { 70 | // about to user-agent sniff 71 | vary(res, 'User-Agent') 72 | 73 | var ua = req.headers['user-agent'] || '' 74 | var isMobile = /mobi|android|touch|mini/i.test(ua) 75 | 76 | // serve site, depending on isMobile 77 | res.setHeader('Content-Type', 'text/html') 78 | res.end('You are (probably) ' + (isMobile ? '' : 'not ') + 'a mobile user') 79 | }) 80 | ``` 81 | 82 | ## Testing 83 | 84 | ```sh 85 | $ npm test 86 | ``` 87 | 88 | ## License 89 | 90 | [MIT](LICENSE) 91 | 92 | 93 | [ci-image]: https://badgen.net/github/checks/jshttp/vary/master?label=ci 94 | [ci-url]: https://github.com/jshttp/vary/actions/workflows/ci.yml 95 | [coveralls-image]: https://badgen.net/coveralls/c/github/jshttp/vary/master 96 | [coveralls-url]: https://coveralls.io/r/jshttp/vary?branch=master 97 | [node-version-image]: https://badgen.net/npm/node/vary 98 | [node-version-url]: https://nodejs.org/en/download 99 | [npm-downloads-image]: https://badgen.net/npm/dm/vary 100 | [npm-url]: https://npmjs.org/package/vary 101 | [npm-version-image]: https://badgen.net/npm/v/vary 102 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * vary 3 | * Copyright(c) 2014-2017 Douglas Christopher Wilson 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict' 8 | 9 | /** 10 | * Module exports. 11 | */ 12 | 13 | module.exports = vary 14 | module.exports.append = append 15 | 16 | /** 17 | * RegExp to match field-name in RFC 7230 sec 3.2 18 | * 19 | * field-name = token 20 | * token = 1*tchar 21 | * tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" 22 | * / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" 23 | * / DIGIT / ALPHA 24 | * ; any VCHAR, except delimiters 25 | */ 26 | 27 | var FIELD_NAME_REGEXP = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/ 28 | 29 | /** 30 | * Append a field to a vary header. 31 | * 32 | * @param {String} header 33 | * @param {String|Array} field 34 | * @return {String} 35 | * @public 36 | */ 37 | 38 | function append (header, field) { 39 | if (typeof header !== 'string') { 40 | throw new TypeError('header argument is required') 41 | } 42 | 43 | if (!field) { 44 | throw new TypeError('field argument is required') 45 | } 46 | 47 | // get fields array 48 | var fields = !Array.isArray(field) 49 | ? parse(String(field)) 50 | : field 51 | 52 | // assert on invalid field names 53 | for (var j = 0; j < fields.length; j++) { 54 | if (!FIELD_NAME_REGEXP.test(fields[j])) { 55 | throw new TypeError('field argument contains an invalid header name') 56 | } 57 | } 58 | 59 | // existing, unspecified vary 60 | if (header === '*') { 61 | return header 62 | } 63 | 64 | // enumerate current values 65 | var val = header 66 | var vals = parse(header.toLowerCase()) 67 | 68 | // unspecified vary 69 | if (fields.indexOf('*') !== -1 || vals.indexOf('*') !== -1) { 70 | return '*' 71 | } 72 | 73 | for (var i = 0; i < fields.length; i++) { 74 | var fld = fields[i].toLowerCase() 75 | 76 | // append value (case-preserving) 77 | if (vals.indexOf(fld) === -1) { 78 | vals.push(fld) 79 | val = val 80 | ? val + ', ' + fields[i] 81 | : fields[i] 82 | } 83 | } 84 | 85 | return val 86 | } 87 | 88 | /** 89 | * Parse a vary header into an array. 90 | * 91 | * @param {String} header 92 | * @return {Array} 93 | * @private 94 | */ 95 | 96 | function parse (header) { 97 | var end = 0 98 | var list = [] 99 | var start = 0 100 | 101 | // gather tokens 102 | for (var i = 0, len = header.length; i < len; i++) { 103 | switch (header.charCodeAt(i)) { 104 | case 0x20: /* */ 105 | if (start === end) { 106 | start = end = i + 1 107 | } 108 | break 109 | case 0x2c: /* , */ 110 | list.push(header.substring(start, end)) 111 | start = end = i + 1 112 | break 113 | default: 114 | end = i + 1 115 | break 116 | } 117 | } 118 | 119 | // final token 120 | list.push(header.substring(start, end)) 121 | 122 | return list 123 | } 124 | 125 | /** 126 | * Mark that a request is varied on a header field. 127 | * 128 | * @param {Object} res 129 | * @param {String|Array} field 130 | * @public 131 | */ 132 | 133 | function vary (res, field) { 134 | if (!res || !res.getHeader || !res.setHeader) { 135 | // quack quack 136 | throw new TypeError('res argument is required') 137 | } 138 | 139 | // get existing header 140 | var val = res.getHeader('Vary') || '' 141 | var header = Array.isArray(val) 142 | ? val.join(', ') 143 | : String(val) 144 | 145 | // set new header 146 | if ((val = append(header, field))) { 147 | res.setHeader('Vary', val) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | - pull_request 5 | - push 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | test: 12 | permissions: 13 | checks: write # for coverallsapp/github-action to create new checks 14 | contents: read # for actions/checkout to fetch code 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | name: 19 | - Node.js 0.8 20 | - Node.js 0.10 21 | - Node.js 0.12 22 | - io.js 1.x 23 | - io.js 2.x 24 | - io.js 3.x 25 | - Node.js 4.x 26 | - Node.js 5.x 27 | - Node.js 6.x 28 | - Node.js 7.x 29 | - Node.js 8.x 30 | - Node.js 9.x 31 | - Node.js 10.x 32 | - Node.js 11.x 33 | - Node.js 12.x 34 | - Node.js 13.x 35 | - Node.js 14.x 36 | - Node.js 15.x 37 | - Node.js 16.x 38 | - Node.js 17.x 39 | - Node.js 18.x 40 | - Node.js 19.x 41 | - Node.js 20.x 42 | - Node.js 21.x 43 | - Node.js 22.x 44 | 45 | include: 46 | - name: Node.js 0.8 47 | node-version: "0.8" 48 | npm-i: mocha@2.5.3 supertest@1.1.0 49 | npm-rm: nyc beautify-benchmark benchmark 50 | 51 | - name: Node.js 0.10 52 | node-version: "0.10" 53 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.1 54 | npm-rm: beautify-benchmark benchmark 55 | 56 | - name: Node.js 0.12 57 | node-version: "0.12" 58 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.1 59 | npm-rm: beautify-benchmark benchmark 60 | 61 | - name: io.js 1.x 62 | node-version: "1.8" 63 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.1 64 | npm-rm: beautify-benchmark benchmark 65 | 66 | - name: io.js 2.x 67 | node-version: "2.5" 68 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.1 69 | npm-rm: beautify-benchmark benchmark 70 | 71 | - name: io.js 3.x 72 | node-version: "3.3" 73 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.1 74 | npm-rm: beautify-benchmark benchmark 75 | 76 | - name: Node.js 4.x 77 | node-version: "4.9" 78 | npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 79 | npm-rm: beautify-benchmark benchmark 80 | 81 | - name: Node.js 5.x 82 | node-version: "5.12" 83 | npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 84 | npm-rm: beautify-benchmark benchmark 85 | 86 | - name: Node.js 6.x 87 | node-version: "6.17" 88 | npm-i: mocha@6.2.2 nyc@14.1.1 89 | npm-rm: beautify-benchmark benchmark 90 | 91 | - name: Node.js 7.x 92 | node-version: "7.10" 93 | npm-i: mocha@6.2.2 nyc@14.1.1 94 | npm-rm: beautify-benchmark benchmark 95 | 96 | - name: Node.js 8.x 97 | node-version: "8.17" 98 | npm-i: mocha@7.1.2 nyc@14.1.1 99 | npm-rm: beautify-benchmark benchmark 100 | 101 | - name: Node.js 9.x 102 | node-version: "9.11" 103 | npm-i: mocha@7.1.2 nyc@14.1.1 104 | npm-rm: beautify-benchmark benchmark 105 | 106 | - name: Node.js 10.x 107 | node-version: "10.24" 108 | npm-i: mocha@8.4.0 109 | npm-rm: beautify-benchmark benchmark 110 | 111 | - name: Node.js 11.x 112 | node-version: "11.15" 113 | npm-i: mocha@8.4.0 114 | npm-rm: beautify-benchmark benchmark 115 | 116 | - name: Node.js 12.x 117 | node-version: "12.22" 118 | npm-i: mocha@9.2.2 119 | npm-rm: beautify-benchmark benchmark 120 | 121 | - name: Node.js 13.x 122 | node-version: "13.14" 123 | npm-i: mocha@9.2.2 124 | npm-rm: beautify-benchmark benchmark 125 | 126 | - name: Node.js 14.x 127 | node-version: "14.21" 128 | npm-rm: beautify-benchmark benchmark 129 | 130 | - name: Node.js 15.x 131 | node-version: "15.14" 132 | npm-rm: beautify-benchmark benchmark 133 | 134 | - name: Node.js 16.x 135 | node-version: "16.20" 136 | npm-rm: beautify-benchmark benchmark 137 | 138 | - name: Node.js 17.x 139 | node-version: "17.9" 140 | npm-rm: beautify-benchmark benchmark 141 | 142 | - name: Node.js 18.x 143 | node-version: "18.18" 144 | npm-rm: beautify-benchmark benchmark 145 | 146 | - name: Node.js 19.x 147 | node-version: "19.9" 148 | npm-rm: beautify-benchmark benchmark 149 | 150 | - name: Node.js 20.x 151 | node-version: "20.9" 152 | npm-rm: beautify-benchmark benchmark 153 | 154 | - name: Node.js 21.x 155 | node-version: "21.1" 156 | npm-rm: beautify-benchmark benchmark 157 | 158 | - name: Node.js 22.x 159 | node-version: "22.0" 160 | npm-rm: beautify-benchmark benchmark 161 | 162 | steps: 163 | - uses: actions/checkout@v6 164 | 165 | - name: Install Node.js ${{ matrix.node-version }} 166 | shell: bash -eo pipefail -l {0} 167 | run: | 168 | nvm install --default ${{ matrix.node-version }} 169 | if [[ "${{ matrix.node-version }}" == 0.* && "$(cut -d. -f2 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then 170 | nvm install --alias=npm 0.10 171 | nvm use ${{ matrix.node-version }} 172 | if [[ "$(npm -v)" == 1.1.* ]]; then 173 | nvm exec npm npm install -g npm@1.1 174 | ln -fs "$(which npm)" "$(dirname "$(nvm which npm)")/npm" 175 | else 176 | sed -i '1s;^.*$;'"$(printf '#!%q' "$(nvm which npm)")"';' "$(readlink -f "$(which npm)")" 177 | fi 178 | npm config set strict-ssl false 179 | fi 180 | dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" 181 | 182 | - name: Configure npm 183 | run: | 184 | if [[ "$(npm config get package-lock)" == "true" ]]; then 185 | npm config set package-lock false 186 | else 187 | npm config set shrinkwrap false 188 | fi 189 | 190 | - name: Remove npm module(s) ${{ matrix.npm-rm }} 191 | run: npm rm --silent --save-dev ${{ matrix.npm-rm }} 192 | if: matrix.npm-rm != '' 193 | 194 | - name: Install npm module(s) ${{ matrix.npm-i }} 195 | run: npm install --save-dev ${{ matrix.npm-i }} 196 | if: matrix.npm-i != '' 197 | 198 | - name: Setup Node.js version-specific dependencies 199 | shell: bash 200 | run: | 201 | # eslint for linting 202 | # - remove on Node.js < 12 203 | if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 12 ]]; then 204 | node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ 205 | grep -E '^eslint(-|$)' | \ 206 | sort -r | \ 207 | xargs -n1 npm rm --silent --save-dev 208 | fi 209 | 210 | - name: Install Node.js dependencies 211 | run: npm install 212 | 213 | - name: List environment 214 | id: list_env 215 | shell: bash 216 | run: | 217 | echo "node@$(node -v)" 218 | echo "npm@$(npm -v)" 219 | npm -s ls ||: 220 | (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print $2 "=" $3 }' >> "$GITHUB_OUTPUT" 221 | 222 | - name: Run tests 223 | shell: bash 224 | run: | 225 | if npm -ps ls nyc | grep -q nyc; then 226 | npm run test-ci 227 | else 228 | npm test 229 | fi 230 | 231 | - name: Lint code 232 | if: steps.list_env.outputs.eslint != '' 233 | run: npm run lint 234 | 235 | - name: Collect code coverage 236 | uses: coverallsapp/github-action@master 237 | if: steps.list_env.outputs.nyc != '' 238 | with: 239 | github-token: ${{ secrets.GITHUB_TOKEN }} 240 | flag-name: run-${{ matrix.test_number }} 241 | parallel: true 242 | 243 | coverage: 244 | permissions: 245 | checks: write # for coverallsapp/github-action to create new checks 246 | needs: test 247 | runs-on: ubuntu-latest 248 | steps: 249 | - name: Upload code coverage 250 | uses: coverallsapp/github-action@master 251 | with: 252 | github-token: ${{ secrets.GITHUB_TOKEN }} 253 | parallel-finished: true 254 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require('assert') 3 | var http = require('http') 4 | var request = require('supertest') 5 | var vary = require('..') 6 | 7 | describe('vary(res, field)', function () { 8 | describe('arguments', function () { 9 | describe('res', function () { 10 | it('should be required', function () { 11 | assert.throws(vary.bind(), /res.*required/) 12 | }) 13 | 14 | it('should not allow non-res-like objects', function () { 15 | assert.throws(vary.bind(null, {}), /res.*required/) 16 | }) 17 | }) 18 | 19 | describe('field', function () { 20 | it('should be required', function (done) { 21 | request(createServer(callVary())) 22 | .get('/') 23 | .expect(500, /field.*required/, done) 24 | }) 25 | 26 | it('should accept string', function (done) { 27 | request(createServer(callVary('foo'))) 28 | .get('/') 29 | .expect(200, done) 30 | }) 31 | 32 | it('should accept array of string', function (done) { 33 | request(createServer(callVary(['foo', 'bar']))) 34 | .get('/') 35 | .expect(200, done) 36 | }) 37 | 38 | it('should accept string that is Vary header', function (done) { 39 | request(createServer(callVary('foo, bar'))) 40 | .get('/') 41 | .expect(200, done) 42 | }) 43 | 44 | it('should not allow separator ":"', function (done) { 45 | request(createServer(callVary('invalid:header'))) 46 | .get('/') 47 | .expect(500, /field.*contains.*invalid/, done) 48 | }) 49 | 50 | it('should not allow separator " "', function (done) { 51 | request(createServer(callVary('invalid header'))) 52 | .get('/') 53 | .expect(500, /field.*contains.*invalid/, done) 54 | }) 55 | }) 56 | }) 57 | 58 | describe('when no Vary', function () { 59 | it('should set value', function (done) { 60 | request(createServer(callVary('Origin'))) 61 | .get('/') 62 | .expect('Vary', 'Origin') 63 | .expect(200, done) 64 | }) 65 | 66 | it('should set value with multiple calls', function (done) { 67 | request(createServer(callVary(['Origin', 'User-Agent']))) 68 | .get('/') 69 | .expect('Vary', 'Origin, User-Agent') 70 | .expect(200, done) 71 | }) 72 | 73 | it('should preserve case', function (done) { 74 | request(createServer(callVary(['ORIGIN', 'user-agent', 'AccepT']))) 75 | .get('/') 76 | .expect('Vary', 'ORIGIN, user-agent, AccepT') 77 | .expect(200, done) 78 | }) 79 | 80 | it('should not set Vary on empty array', function (done) { 81 | request(createServer(callVary([]))) 82 | .get('/') 83 | .expect(shouldNotHaveHeader('Vary')) 84 | .expect(200, done) 85 | }) 86 | }) 87 | 88 | describe('when existing Vary', function () { 89 | it('should set value', function (done) { 90 | request(createServer(alterVary('Accept', 'Origin'))) 91 | .get('/') 92 | .expect('Vary', 'Accept, Origin') 93 | .expect(200, done) 94 | }) 95 | 96 | it('should set value with multiple calls', function (done) { 97 | var server = createServer(function (req, res) { 98 | res.setHeader('Vary', 'Accept') 99 | vary(res, 'Origin') 100 | vary(res, 'User-Agent') 101 | }) 102 | request(server) 103 | .get('/') 104 | .expect('Vary', 'Accept, Origin, User-Agent') 105 | .expect(200, done) 106 | }) 107 | 108 | it('should not duplicate existing value', function (done) { 109 | request(createServer(alterVary('Accept', 'Accept'))) 110 | .get('/') 111 | .expect('Vary', 'Accept') 112 | .expect(200, done) 113 | }) 114 | 115 | it('should compare case-insensitive', function (done) { 116 | request(createServer(alterVary('Accept', 'accEPT'))) 117 | .get('/') 118 | .expect('Vary', 'Accept') 119 | .expect(200, done) 120 | }) 121 | 122 | it('should preserve case', function (done) { 123 | request(createServer(alterVary('AccepT', ['accEPT', 'ORIGIN']))) 124 | .get('/') 125 | .expect('Vary', 'AccepT, ORIGIN') 126 | .expect(200, done) 127 | }) 128 | }) 129 | 130 | describe('when existing Vary as array', function () { 131 | it('should set value', function (done) { 132 | request(createServer(alterVary(['Accept', 'Accept-Encoding'], 'Origin'))) 133 | .get('/') 134 | .expect('Vary', 'Accept, Accept-Encoding, Origin') 135 | .expect(200, done) 136 | }) 137 | 138 | it('should not duplicate existing value', function (done) { 139 | request(createServer(alterVary(['Accept', 'Accept-Encoding'], ['accept', 'origin']))) 140 | .get('/') 141 | .expect('Vary', 'Accept, Accept-Encoding, origin') 142 | .expect(200, done) 143 | }) 144 | }) 145 | 146 | describe('when Vary: *', function () { 147 | it('should set value', function (done) { 148 | request(createServer(callVary('*'))) 149 | .get('/') 150 | .expect('Vary', '*') 151 | .expect(200, done) 152 | }) 153 | 154 | it('should act as if all values alread set', function (done) { 155 | request(createServer(alterVary('*', ['Origin', 'User-Agent']))) 156 | .get('/') 157 | .expect('Vary', '*') 158 | .expect(200, done) 159 | }) 160 | 161 | it('should erradicate existing values', function (done) { 162 | request(createServer(alterVary('Accept, Accept-Encoding', '*'))) 163 | .get('/') 164 | .expect('Vary', '*') 165 | .expect(200, done) 166 | }) 167 | 168 | it('should update bad existing header', function (done) { 169 | request(createServer(alterVary('Accept, Accept-Encoding, *', 'Origin'))) 170 | .get('/') 171 | .expect('Vary', '*') 172 | .expect(200, done) 173 | }) 174 | }) 175 | 176 | describe('when field is string', function () { 177 | it('should set value', function (done) { 178 | request(createServer(callVary('Accept'))) 179 | .get('/') 180 | .expect('Vary', 'Accept') 181 | .expect(200, done) 182 | }) 183 | 184 | it('should set value when vary header', function (done) { 185 | request(createServer(callVary('Accept, Accept-Encoding'))) 186 | .get('/') 187 | .expect('Vary', 'Accept, Accept-Encoding') 188 | .expect(200, done) 189 | }) 190 | 191 | it('should acept LWS', function (done) { 192 | request(createServer(callVary(' Accept , Origin '))) 193 | .get('/') 194 | .expect('Vary', 'Accept, Origin') 195 | .expect(200, done) 196 | }) 197 | 198 | it('should handle contained *', function (done) { 199 | request(createServer(callVary('Accept,*'))) 200 | .get('/') 201 | .expect('Vary', '*') 202 | .expect(200, done) 203 | }) 204 | }) 205 | 206 | describe('when field is array', function () { 207 | it('should set value', function (done) { 208 | request(createServer(callVary(['Accept', 'Accept-Language']))) 209 | .get('/') 210 | .expect('Vary', 'Accept, Accept-Language') 211 | .expect(200, done) 212 | }) 213 | 214 | it('should ignore double-entries', function (done) { 215 | request(createServer(callVary(['Accept', 'Accept']))) 216 | .get('/') 217 | .expect('Vary', 'Accept') 218 | .expect(200, done) 219 | }) 220 | 221 | it('should be case-insensitive', function (done) { 222 | request(createServer(callVary(['Accept', 'ACCEPT']))) 223 | .get('/') 224 | .expect('Vary', 'Accept') 225 | .expect(200, done) 226 | }) 227 | 228 | it('should handle contained *', function (done) { 229 | request(createServer(callVary(['Origin', 'User-Agent', '*', 'Accept']))) 230 | .get('/') 231 | .expect('Vary', '*') 232 | .expect(200, done) 233 | }) 234 | 235 | it('should handle existing values', function (done) { 236 | request(createServer(alterVary('Accept, Accept-Encoding', ['origin', 'accept', 'accept-charset']))) 237 | .get('/') 238 | .expect('Vary', 'Accept, Accept-Encoding, origin, accept-charset') 239 | .expect(200, done) 240 | }) 241 | }) 242 | }) 243 | 244 | describe('vary.append(header, field)', function () { 245 | describe('arguments', function () { 246 | describe('header', function () { 247 | it('should be required', function () { 248 | assert.throws(vary.append.bind(), /header.*required/) 249 | }) 250 | 251 | it('should be a string', function () { 252 | assert.throws(vary.append.bind(null, 42), /header.*required/) 253 | }) 254 | }) 255 | 256 | describe('field', function () { 257 | it('should be required', function () { 258 | assert.throws(vary.append.bind(null, ''), /field.*required/) 259 | }) 260 | 261 | it('should accept string', function () { 262 | assert.doesNotThrow(vary.append.bind(null, '', 'foo')) 263 | }) 264 | 265 | it('should accept string that is Vary header', function () { 266 | assert.doesNotThrow(vary.append.bind(null, '', 'foo, bar')) 267 | }) 268 | 269 | it('should accept array of string', function () { 270 | assert.doesNotThrow(vary.append.bind(null, '', ['foo', 'bar'])) 271 | }) 272 | 273 | it('should not allow separator ":"', function () { 274 | assert.throws(vary.append.bind(null, '', 'invalid:header'), /field.*contains.*invalid/) 275 | }) 276 | 277 | it('should not allow separator " "', function () { 278 | assert.throws(vary.append.bind(null, '', 'invalid header'), /field.*contains.*invalid/) 279 | }) 280 | 281 | it('should not allow non-token characters', function () { 282 | assert.throws(vary.append.bind(null, '', 'invalid\nheader'), /field.*contains.*invalid/) 283 | assert.throws(vary.append.bind(null, '', 'invalid\u0080header'), /field.*contains.*invalid/) 284 | }) 285 | }) 286 | }) 287 | 288 | describe('when header empty', function () { 289 | it('should set value', function () { 290 | assert.strictEqual(vary.append('', 'Origin'), 'Origin') 291 | }) 292 | 293 | it('should set value with array', function () { 294 | assert.strictEqual(vary.append('', ['Origin', 'User-Agent']), 'Origin, User-Agent') 295 | }) 296 | 297 | it('should preserve case', function () { 298 | assert.strictEqual(vary.append('', ['ORIGIN', 'user-agent', 'AccepT']), 'ORIGIN, user-agent, AccepT') 299 | }) 300 | }) 301 | 302 | describe('when header has values', function () { 303 | it('should set value', function () { 304 | assert.strictEqual(vary.append('Accept', 'Origin'), 'Accept, Origin') 305 | }) 306 | 307 | it('should set value with array', function () { 308 | assert.strictEqual(vary.append('Accept', ['Origin', 'User-Agent']), 'Accept, Origin, User-Agent') 309 | }) 310 | 311 | it('should not duplicate existing value', function () { 312 | assert.strictEqual(vary.append('Accept', 'Accept'), 'Accept') 313 | }) 314 | 315 | it('should compare case-insensitive', function () { 316 | assert.strictEqual(vary.append('Accept', 'accEPT'), 'Accept') 317 | }) 318 | 319 | it('should preserve case', function () { 320 | assert.strictEqual(vary.append('Accept', 'AccepT'), 'Accept') 321 | }) 322 | }) 323 | 324 | describe('when *', function () { 325 | it('should set value', function () { 326 | assert.strictEqual(vary.append('', '*'), '*') 327 | }) 328 | 329 | it('should act as if all values already set', function () { 330 | assert.strictEqual(vary.append('*', 'Origin'), '*') 331 | }) 332 | 333 | it('should erradicate existing values', function () { 334 | assert.strictEqual(vary.append('Accept, Accept-Encoding', '*'), '*') 335 | }) 336 | 337 | it('should update bad existing header', function () { 338 | assert.strictEqual(vary.append('Accept, Accept-Encoding, *', 'Origin'), '*') 339 | }) 340 | }) 341 | 342 | describe('when field is string', function () { 343 | it('should set value', function () { 344 | assert.strictEqual(vary.append('', 'Accept'), 'Accept') 345 | }) 346 | 347 | it('should set value when vary header', function () { 348 | assert.strictEqual(vary.append('', 'Accept, Accept-Encoding'), 'Accept, Accept-Encoding') 349 | }) 350 | 351 | it('should acept LWS', function () { 352 | assert.strictEqual(vary.append('', ' Accept , Origin '), 'Accept, Origin') 353 | }) 354 | 355 | it('should handle contained *', function () { 356 | assert.strictEqual(vary.append('', 'Accept,*'), '*') 357 | }) 358 | }) 359 | 360 | describe('when field is array', function () { 361 | it('should set value', function () { 362 | assert.strictEqual(vary.append('', ['Accept', 'Accept-Language']), 'Accept, Accept-Language') 363 | }) 364 | 365 | it('should ignore double-entries', function () { 366 | assert.strictEqual(vary.append('', ['Accept', 'Accept']), 'Accept') 367 | }) 368 | 369 | it('should be case-insensitive', function () { 370 | assert.strictEqual(vary.append('', ['Accept', 'ACCEPT']), 'Accept') 371 | }) 372 | 373 | it('should handle contained *', function () { 374 | assert.strictEqual(vary.append('', ['Origin', 'User-Agent', '*', 'Accept']), '*') 375 | }) 376 | 377 | it('should handle existing values', function () { 378 | assert.strictEqual(vary.append('Accept, Accept-Encoding', ['origin', 'accept', 'accept-charset']), 'Accept, Accept-Encoding, origin, accept-charset') 379 | }) 380 | }) 381 | }) 382 | 383 | function alterVary (header, field) { 384 | return function call (req, res) { 385 | res.setHeader('Vary', header) 386 | vary(res, field) 387 | } 388 | } 389 | 390 | function callVary (field) { 391 | return function call (req, res) { 392 | vary(res, field) 393 | } 394 | } 395 | 396 | function createServer (fn) { 397 | return http.createServer(function onRequest (req, res) { 398 | try { 399 | fn(req, res) 400 | res.statusCode = 200 401 | } catch (err) { 402 | res.statusCode = 500 403 | res.write(err.message) 404 | } finally { 405 | res.end() 406 | } 407 | }) 408 | } 409 | 410 | function shouldNotHaveHeader (header) { 411 | return function (res) { 412 | assert.ok(!(header.toLowerCase() in res.headers), 'should not have header ' + header) 413 | } 414 | } 415 | --------------------------------------------------------------------------------