Repository: restify/node-restify
Branch: master
Commit: 009f40b31612
Files: 193
Total size: 998.8 KB
Directory structure:
gitextract_6dd12z3s/
├── .dir-locals.el
├── .eslintignore
├── .eslintrc.js
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── 1-bug-report.md
│ │ └── 2-feature-request.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── stale.yml
│ └── workflows/
│ ├── ci.yml
│ └── release-please.yml
├── .gitignore
├── .npmignore
├── .prettierignore
├── .prettierrc
├── .tern-project
├── CHANGELOG.md
├── CHANGES.md
├── CONTRIBUTING.md
├── FEATURE_REQUESTS.md
├── LICENSE
├── Makefile
├── README.md
├── SECURITY.md
├── benchmark/
│ ├── benchmarks/
│ │ ├── middleware.js
│ │ ├── response-json.js
│ │ ├── response-text.js
│ │ └── router-heavy.js
│ ├── index.js
│ ├── lib/
│ │ ├── autocannon.js
│ │ └── bench.js
│ └── package.json
├── bin/
│ └── report-latency
├── docs/
│ ├── _api/
│ │ ├── formatters.md
│ │ ├── plugins.md
│ │ ├── request.md
│ │ ├── response.md
│ │ └── server.md
│ ├── api/
│ │ ├── formatters-usage.md
│ │ ├── plugins-usage.md
│ │ ├── request-events.md
│ │ ├── request-log.md
│ │ ├── server-errors.md
│ │ └── server-events.md
│ ├── config/
│ │ ├── formatters.yaml
│ │ ├── plugins.yaml
│ │ ├── request.yaml
│ │ └── server.yaml
│ ├── guides/
│ │ ├── 4TO5GUIDE.md
│ │ ├── 6to7guide.md
│ │ ├── 8to9guide.md
│ │ ├── client.md
│ │ ├── dtrace.md
│ │ └── server.md
│ └── index.md
├── examples/
│ ├── dtrace/
│ │ ├── demo.js
│ │ ├── handler-timing.d
│ │ └── hello.js
│ ├── example.js
│ ├── http2/
│ │ ├── http2.js
│ │ └── keys/
│ │ ├── http2-cert.pem
│ │ ├── http2-csr.pem
│ │ └── http2-key.pem
│ ├── jsonp/
│ │ └── jsonp.js
│ ├── sockio/
│ │ ├── package.json
│ │ └── sockio.js
│ ├── spdy/
│ │ ├── keys/
│ │ │ ├── spdy-cert.pem
│ │ │ ├── spdy-csr.pem
│ │ │ └── spdy-key.pem
│ │ └── spdy.js
│ └── todoapp/
│ ├── README.md
│ ├── lib/
│ │ ├── client.js
│ │ ├── index.js
│ │ └── server.js
│ ├── main.js
│ ├── package.json
│ └── test/
│ └── todo.test.js
├── lib/
│ ├── chain.js
│ ├── deprecationWarnings.js
│ ├── dtrace.js
│ ├── errorTypes.js
│ ├── formatters/
│ │ ├── binary.js
│ │ ├── index.js
│ │ ├── json.js
│ │ ├── jsonp.js
│ │ └── text.js
│ ├── helpers/
│ │ └── chainComposer.js
│ ├── http_date.js
│ ├── index.js
│ ├── plugins/
│ │ ├── accept.js
│ │ ├── audit.js
│ │ ├── authorization.js
│ │ ├── bodyParser.js
│ │ ├── bodyReader.js
│ │ ├── conditionalHandler.js
│ │ ├── conditionalRequest.js
│ │ ├── cpuUsageThrottle.js
│ │ ├── date.js
│ │ ├── fieldedTextBodyParser.js
│ │ ├── formBodyParser.js
│ │ ├── fullResponse.js
│ │ ├── gzip.js
│ │ ├── index.js
│ │ ├── inflightRequestThrottle.js
│ │ ├── jsonBodyParser.js
│ │ ├── jsonp.js
│ │ ├── metrics.js
│ │ ├── multipartBodyParser.js
│ │ ├── oauth2TokenParser.js
│ │ ├── pre/
│ │ │ ├── context.js
│ │ │ ├── dedupeSlashes.js
│ │ │ ├── pause.js
│ │ │ ├── prePath.js
│ │ │ ├── reqIdHeaders.js
│ │ │ ├── strictQueryParams.js
│ │ │ └── userAgent.js
│ │ ├── query.js
│ │ ├── requestExpiry.js
│ │ ├── requestLogger.js
│ │ ├── static.js
│ │ ├── staticFiles.js
│ │ ├── throttle.js
│ │ └── utils/
│ │ ├── hrTimeDurationInMs.js
│ │ ├── httpDate.js
│ │ ├── regex.js
│ │ └── shallowCopy.js
│ ├── request.js
│ ├── response.js
│ ├── router.js
│ ├── routerRegistryRadix.js
│ ├── server.js
│ ├── upgrade.js
│ └── utils.js
├── package.json
├── test/
│ ├── .eslintrc
│ ├── chain.test.js
│ ├── chainComposer.test.js
│ ├── formatter-optional.test.js
│ ├── formatter.test.js
│ ├── index.test.js
│ ├── keys/
│ │ ├── http2-cert.pem
│ │ ├── http2-csr.pem
│ │ └── http2-key.pem
│ ├── lib/
│ │ ├── helper.js
│ │ ├── server-withDisableUncaughtException.js
│ │ └── streamRecorder.js
│ ├── plugins/
│ │ ├── .eslintrc
│ │ ├── accept.test.js
│ │ ├── audit.test.js
│ │ ├── authorization.test.js
│ │ ├── bodyReader.test.js
│ │ ├── conditionalHandler.test.js
│ │ ├── conditionalRequest.test.js
│ │ ├── context.test.js
│ │ ├── cpuUsageThrottle.test.js
│ │ ├── dedupeSlashes.test.js
│ │ ├── fieldedTextParser.test.js
│ │ ├── files/
│ │ │ ├── data-csv.txt
│ │ │ ├── data-tsv.txt
│ │ │ ├── object-csv.json
│ │ │ └── object-tsv.json
│ │ ├── formBodyParser.test.js
│ │ ├── gzip.test.js
│ │ ├── inflightRequestThrottle.test.js
│ │ ├── jsonBodyParser.test.js
│ │ ├── metrics.test.js
│ │ ├── multipart.test.js
│ │ ├── oauth2.test.js
│ │ ├── plugins.test.js
│ │ ├── query.test.js
│ │ ├── reqIdHeaders.test.js
│ │ ├── requestExpiry.test.js
│ │ ├── static.test.js
│ │ ├── staticFiles.test.js
│ │ ├── strictQueryParams.test.js
│ │ ├── testStaticFiles/
│ │ │ ├── docs/
│ │ │ │ ├── doc.md
│ │ │ │ └── index.html
│ │ │ ├── file1.txt
│ │ │ ├── index.html
│ │ │ └── special/
│ │ │ └── $_$/
│ │ │ └── bad (file).txt
│ │ ├── throttle.test.js
│ │ ├── userAgent.test.js
│ │ └── utilsHrTimeDurationInMs.test.js
│ ├── request.test.js
│ ├── response.test.js
│ ├── router.test.js
│ ├── routerRegistryRadix.test.js
│ ├── server.test.js
│ ├── serverHttp2.test.js
│ ├── upgrade.test.js
│ └── utils.test.js
└── tools/
├── docsBuild.js
└── mk/
├── Makefile.defs
├── Makefile.deps
└── Makefile.targ
================================================
FILE CONTENTS
================================================
================================================
FILE: .dir-locals.el
================================================
((nil . ((indent-tabs-mode . nil)
(tab-width . 4)
(fill-column . 80)))
(js-mode . ((js-indent-level . 4)
(indent-tabs-mode . nil)
)))
================================================
FILE: .eslintignore
================================================
# node_modules ignored by default
node_modules/
# other ignored directories
bin/
deps/
docs/
examples/
cover_html/
================================================
FILE: .eslintrc.js
================================================
'use strict';
var OFF = 0;
var ERROR = 2;
var config = {
extends: [],
plugins: ['jsdoc'],
env: {
browser: false,
node: true,
es6: true
},
parserOptions: {
ecmaVersion: 2018
},
rules: {}
};
if (!process.env.NO_LINT) {
// possible errors
config.rules['no-cond-assign'] = ERROR;
config.rules['no-console'] = OFF;
config.rules['no-constant-condition'] = ERROR;
config.rules['no-control-regex'] = ERROR;
config.rules['no-debugger'] = ERROR;
config.rules['no-dupe-args'] = ERROR;
config.rules['no-dupe-keys'] = ERROR;
config.rules['no-duplicate-case'] = ERROR;
config.rules['no-empty'] = ERROR;
config.rules['no-empty-character-class'] = ERROR;
config.rules['no-ex-assign'] = ERROR;
config.rules['no-extra-boolean-cast'] = ERROR;
config.rules['no-extra-semi'] = ERROR;
config.rules['no-func-assign'] = ERROR;
// config.rules['one-var'] = [ERROR, 'always']; // TODO: var overlapping
// this is for variable hoisting, not necessary if we use block scoped declarations
// config.rules['no-inner-declarations'] = [ ERROR, 'both' ];
config.rules['no-invalid-regexp'] = ERROR;
config.rules['no-irregular-whitespace'] = ERROR;
config.rules['no-negated-in-lhs'] = ERROR;
config.rules['no-reserved-keys'] = OFF;
config.rules['no-regex-spaces'] = ERROR;
config.rules['no-sparse-arrays'] = ERROR;
config.rules['no-unreachable'] = ERROR;
config.rules['use-isnan'] = ERROR;
config.rules['valid-jsdoc'] = [
ERROR,
{
requireReturnDescription: false,
prefer: {
return: 'returns'
}
}
];
config.rules['valid-typeof'] = ERROR;
// best practices
config.rules['func-names'] = ERROR;
config.rules['block-scoped-var'] = ERROR;
config.rules['consistent-return'] = ERROR;
config.rules['curly'] = OFF;
config.rules['default-case'] = ERROR;
config.rules['dot-notation'] = [ERROR, { allowKeywords: true }];
config.rules['eqeqeq'] = ERROR;
config.rules['guard-for-in'] = ERROR;
config.rules['no-alert'] = ERROR;
config.rules['no-caller'] = ERROR;
config.rules['no-div-regex'] = ERROR;
config.rules['no-eq-null'] = ERROR;
config.rules['no-eval'] = ERROR;
config.rules['no-extend-native'] = ERROR;
config.rules['no-extra-bind'] = ERROR;
config.rules['no-fallthrough'] = ERROR;
config.rules['no-floating-decimal'] = ERROR;
config.rules['no-implied-eval'] = ERROR;
config.rules['no-iterator'] = ERROR;
config.rules['no-labels'] = ERROR;
config.rules['no-lone-blocks'] = ERROR;
config.rules['no-loop-func'] = OFF;
config.rules['no-multi-spaces'] = OFF;
config.rules['no-multi-str'] = OFF;
config.rules['no-native-reassign'] = ERROR;
config.rules['no-new'] = OFF;
config.rules['no-new-func'] = ERROR;
config.rules['no-new-wrappers'] = ERROR;
config.rules['no-octal'] = ERROR;
config.rules['no-octal-escape'] = ERROR;
config.rules['no-param-reassign'] = OFF;
config.rules['no-proto'] = ERROR;
config.rules['no-process-env'] = OFF;
config.rules['no-redeclare'] = ERROR;
config.rules['no-return-assign'] = ERROR;
config.rules['no-script-url'] = ERROR;
config.rules['no-self-compare'] = ERROR;
config.rules['no-sequences'] = ERROR;
config.rules['no-throw-literal'] = ERROR;
config.rules['no-unused-expressions'] = ERROR;
config.rules['no-warning-comments'] = [1];
config.rules['no-with'] = ERROR;
config.rules['radix'] = ERROR;
config.rules['wrap-iife'] = ERROR;
// strict mode
config.rules['strict'] = [ERROR, 'global'];
// variables
config.rules['no-catch-shadow'] = ERROR;
config.rules['no-delete-var'] = ERROR;
config.rules['no-shadow'] = ERROR;
config.rules['no-shadow-restricted-names'] = ERROR;
config.rules['no-undef'] = ERROR;
config.rules['no-undef-init'] = ERROR;
config.rules['no-undefined'] = OFF;
config.rules['no-unused-vars'] = [ERROR, { vars: 'all', args: 'none' }];
config.rules['no-use-before-define'] = [ERROR, 'nofunc'];
// node.js
config.rules['handle-callback-err'] = [ERROR, '^.*(e|E)rr'];
config.rules['no-mixed-requires'] = ERROR;
config.rules['no-new-require'] = ERROR;
config.rules['no-path-concat'] = OFF;
config.rules['no-process-exit'] = OFF;
}
// stylistic.
if (!process.env.NO_STYLE) {
// Global
config.rules['max-len'] = [ERROR, { code: 80, ignoreComments: true }];
// Prettier
config.extends.push('prettier');
config.plugins.push('prettier');
config.rules['prettier/prettier'] = ERROR;
// JSDoc
config.rules['jsdoc/check-param-names'] = ERROR;
config.rules['jsdoc/check-tag-names'] = ERROR;
config.rules['jsdoc/newline-after-description'] = ERROR;
config.rules['jsdoc/require-hyphen-before-param-description'] = ERROR;
config.rules['jsdoc/require-param'] = ERROR;
config.rules['jsdoc/require-param-description'] = ERROR;
config.rules['jsdoc/require-param-type'] = ERROR;
config.rules['jsdoc/require-returns-description'] = ERROR;
config.rules['jsdoc/require-returns-type'] = ERROR;
}
module.exports = config;
================================================
FILE: .github/ISSUE_TEMPLATE/1-bug-report.md
================================================
---
name: "\U0001F41B Bug report"
about: Create a report to help us improve
---
- [ ] Used appropriate template for the issue type
- [ ] Searched both open and closed issues for duplicates of this issue
- [ ] Title adequately and _concisely_ reflects the feature or the bug
**Restify Version**:
**Node.js Version**:
## Expected behaviour
## Actual behaviour
## Repro case
## Cause
## Are you willing and able to fix this?
================================================
FILE: .github/ISSUE_TEMPLATE/2-feature-request.md
================================================
---
name: "\U0001F680 Feature request"
about: Suggest an idea for this project
---
- [ ] Used appropriate template for the issue type
- [ ] Searched both open and closed issues for duplicates of this issue
- [ ] Title adequately and _concisely_ reflects the feature or the bug
# Feature Request
## Use Case
## Example API
## Are you willing and able to implement this?
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
## Pre-Submission Checklist
- [ ] Opened an issue discussing these changes before opening the PR
- [ ] Ran the linter and tests via `make prepush`
- [ ] Included comprehensive and convincing tests for changes
## Issues
Closes:
* Issue #
* Issue #
* Issue #
> Summarize the issues that discussed these changes
# Changes
> What does this PR do?
================================================
FILE: .github/stale.yml
================================================
daysUntilStale: 60
daysUntilClose: 14
exemptLabels:
- Critical
- Serve
staleLabel: Stale
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
closeComment: >
This issue has been automatically closed as stale because it has not had
recent activity.
================================================
FILE: .github/workflows/ci.yml
================================================
on:
push:
branches:
- master
pull_request:
branches:
- master
name: ci
jobs:
lint:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: install node v16
uses: actions/setup-node@v1
with:
node-version: v16.x
- name: install dependencies
run: npm install
- name: check lint
run: make check-lint
test:
name: test node ${{ matrix.node-version }} on ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
node-version:
- 14.x
- 16.x
- 18.x
- 20.x
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v2
- name: use node ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: install dependencies
run: npm install
- name: test
run: make test
env:
TEST_SKIP_IP_V6: true
================================================
FILE: .github/workflows/release-please.yml
================================================
on:
push:
branches:
- master
- 9.x
name: release-please
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: GoogleCloudPlatform/release-please-action@v3.6.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
release-type: node
package-name: restify
================================================
FILE: .gitignore
================================================
node_modules
docs/*.html
docs/pkg
examples/todoapp/node_modules
*.log
*.tar.gz
*.tgz
build
docs/*.json
nbproject
deps/javascriptlint
deps/jsstyle
package-lock.json
benchmark/results
.nyc_output/
coverage/
cover_html/
================================================
FILE: .npmignore
================================================
.coverage_data
.dir-locals.el
.gitmodules
.github
.travis.yml
Makefile
cover_html
deps
docs
examples
test
tools
.vscode
.idea
benchmark
.dir-locals.el
.eslintignore
.eslintrc.js
.gitignore
.prettierignore
.prettierrc
.tern-project
.travis.yml
CONTRIBUTING.md
FEATURE_REQUESTS.md
*.log
*.tar.gz
*.tgz
node_modules
================================================
FILE: .prettierignore
================================================
cover_html
================================================
FILE: .prettierrc
================================================
{
"tabWidth": 4,
"singleQuote": true
}
================================================
FILE: .tern-project
================================================
{
"libs": [
"ecma5",
"chai"
],
"plugins": {
"node": {},
"complete_strings": {},
"doc_comment": {},
"node_resolve": {}
}
}
================================================
FILE: CHANGELOG.md
================================================
## [9.0.0](https://www.github.com/restify/node-restify/compare/v8.6.1...v9.0.0) (2022-11-15)
### ⚠ BREAKING CHANGES
* remove deprecated usage of pino.child (#1902)
* deprecates and removes re-routing when passing a string parameter to `next()`
* removes `RequestCaptureStream` and replaces `Bunyan` with `Pino`
* adds async/await support to pre, use and handler chains
* drops suppoprt to node 8 and updates linting rules
* **server:** - Server returns `RequestCloseError` instead of `RequestAbortedError`
* **travisci:** dropping support below Node.js 4
### Features
* async/await support ([12be9e2](https://www.github.com/restify/node-restify/commit/12be9e243a407eaf7a30cbb16e399ee2a46dec93))
* deprecate req.closed ([d052b7c](https://www.github.com/restify/node-restify/commit/d052b7cec561133c002211a20dccf7cc2a8a0897))
* provide callback to uncaughtException handler ([#1766](https://www.github.com/restify/node-restify/issues/1766)) ([5e8b5e2](https://www.github.com/restify/node-restify/commit/5e8b5e2b28e32c79c413d9dec2466fe8f1135332))
* remove re-routing from handler ([#1847](https://www.github.com/restify/node-restify/issues/1847)) ([9153587](https://www.github.com/restify/node-restify/commit/9153587c023a876237c1d8bc7491fee4984d9074))
* send 500s for unhandled requests ([#1777](https://www.github.com/restify/node-restify/issues/1777)) ([885cecd](https://www.github.com/restify/node-restify/commit/885cecd7f9753b62faaa930f3cd39329057587f3))
* **audit:** Add the ability to specify a custom audit log serializer (for err, req and res) ([#1746](https://www.github.com/restify/node-restify/issues/1746)) ([6231acd](https://www.github.com/restify/node-restify/commit/6231acda7e16ce64253b08039bd0ad341126c11a))
* **chain:** schedule handlers to the next tick ([#1798](https://www.github.com/restify/node-restify/issues/1798)) ([806ed71](https://www.github.com/restify/node-restify/commit/806ed7119db9ed4cce77aef3d898aae561224dd8))
* **chain:** use nextTick instead of setImmediate ([#1808](https://www.github.com/restify/node-restify/issues/1808)) ([703470a](https://www.github.com/restify/node-restify/commit/703470ad82fd01e7f3b2197ebb7eb1b5b37975f8))
* **deps:** replace cover/istanbul with nyc ([#1823](https://www.github.com/restify/node-restify/issues/1823)) ([361f83e](https://www.github.com/restify/node-restify/commit/361f83e5acd814881c82add3e1bd06ce9ded777c))
* **first:** Handlers that execute ASAP in the req/res lifecycle ([#1756](https://www.github.com/restify/node-restify/issues/1756)) ([8178098](https://www.github.com/restify/node-restify/commit/8178098d3e85ad9bd13c536b504adf940ef08563))
* **http2:** add native HTTP/2 support ([#1489](https://www.github.com/restify/node-restify/issues/1489)) ([6b20285](https://www.github.com/restify/node-restify/commit/6b202853d62394f0448486c9b5bbc18589fd44e2))
* **plugin:** plugin to serve static files ([#1753](https://www.github.com/restify/node-restify/issues/1753)) ([a67b25f](https://www.github.com/restify/node-restify/commit/a67b25f472c7ec99e63f358b3c1e8801d6261148))
* Ability to find a route by a path ([711a489](https://www.github.com/restify/node-restify/commit/711a4897800e2ef8bc4a1a9c6cc833af71cd925d))
* add router.render() back to support hypermedia usecase ([#1752](https://www.github.com/restify/node-restify/issues/1752)) ([0700cfd](https://www.github.com/restify/node-restify/commit/0700cfd445e45401c36c4229e37e12b8220339d9)), closes [#1684](https://www.github.com/restify/node-restify/issues/1684)
* **helpers:** add compose feature ([#1660](https://www.github.com/restify/node-restify/issues/1660)) ([eb60ef4](https://www.github.com/restify/node-restify/commit/eb60ef403ad77b1dd187e199d72e7c80caca248c))
* **plugins:** context, req.get() returns the whole context ([#1739](https://www.github.com/restify/node-restify/issues/1739)) ([6e35e01](https://www.github.com/restify/node-restify/commit/6e35e01eb6d64f80c0e3db2daf4dbf3f66c35e86))
* **plugins:** do not include user-input in UnsupportedMediaTypeError message (VError fails), move it to info ([#1733](https://www.github.com/restify/node-restify/issues/1733)) ([06c220d](https://www.github.com/restify/node-restify/commit/06c220d2d9629e3510aed493a8877629bbc0c4ae))
* **req:** add restifyDone event ([#1740](https://www.github.com/restify/node-restify/issues/1740)) ([4900d6b](https://www.github.com/restify/node-restify/commit/4900d6bdd51fa4e1769678562de69929c38a0c4b))
* add support for non-strict formatters ([#1721](https://www.github.com/restify/node-restify/issues/1721)) ([de1833a](https://www.github.com/restify/node-restify/commit/de1833a44084e5f231de289421518ec646b86f60))
* jsonBodyParser handles extended content types *+json ([#1663](https://www.github.com/restify/node-restify/issues/1663)) ([4537514](https://www.github.com/restify/node-restify/commit/45375144feb6a215ebfdb967ff0944e3aa21f48d))
* **router:** add ignoreTrailingSlash router option ([#1632](https://www.github.com/restify/node-restify/issues/1632)) ([92ffbf5](https://www.github.com/restify/node-restify/commit/92ffbf5cbe49df09d9c59a6081285c12fe5943b4))
* **server:** new router and middleware system ([#1561](https://www.github.com/restify/node-restify/issues/1561)) ([8283277](https://www.github.com/restify/node-restify/commit/82832771826321480e5e524db258668f62b689c2))
* cpuUsageThrottle ([#1460](https://www.github.com/restify/node-restify/issues/1460)) ([84be679](https://www.github.com/restify/node-restify/commit/84be6799c4a80ae67f3aa03165c8031a55bddc97))
* **throttle plugin:** expose rate limit metrics as headers ([#1453](https://www.github.com/restify/node-restify/issues/1453)) ([1627a55](https://www.github.com/restify/node-restify/commit/1627a557bd4ed94ba1c6adbe916c51f83bc46059))
* create inflightRequestThrottle plugin ([#1431](https://www.github.com/restify/node-restify/issues/1431)) ([285faf4](https://www.github.com/restify/node-restify/commit/285faf4b6a2e56f0e4d9fc6dfaa3dd5e311530c1))
* revert async formatters ([#1377](https://www.github.com/restify/node-restify/issues/1377)) ([a2e300f](https://www.github.com/restify/node-restify/commit/a2e300f785edb087da9a52f562bd1f900e9ab47a))
### Bug Fixes
* add support for secureOptions in createServer ([#1575](https://www.github.com/restify/node-restify/issues/1575)) ([656e60e](https://www.github.com/restify/node-restify/commit/656e60e03d5fe2b011f8b2198178bc22d749b21f))
* Allow multiple unmerged set-cookie headers. ([#1570](https://www.github.com/restify/node-restify/issues/1570)) ([df04015](https://www.github.com/restify/node-restify/commit/df04015439becae8e8c48a02cb6e1992d6040037))
* Correct typo in assertion message ([#1904](https://www.github.com/restify/node-restify/issues/1904)) ([195cf13](https://www.github.com/restify/node-restify/commit/195cf136e3a7de2b2720261dfd459c051b5be037))
* documentation typo fix ([#1688](https://www.github.com/restify/node-restify/issues/1688)) ([0fa7132](https://www.github.com/restify/node-restify/commit/0fa71328b8f01f301b0e729f5ef0f00d1b203231))
* don't create empty clientError listener for http.Server ([#1895](https://www.github.com/restify/node-restify/issues/1895)) ([ddc1042](https://www.github.com/restify/node-restify/commit/ddc1042af427fe6383ebea37201c06b7b424e72f))
* emit after event with proper error param for node versions >= 11.4.0 ([#1732](https://www.github.com/restify/node-restify/issues/1732)) ([7a1378b](https://www.github.com/restify/node-restify/commit/7a1378b0353e9b3f1b630e4cab489c8c578000f5))
* examples/todoapp/package.json to reduce vulnerabilities ([#1832](https://www.github.com/restify/node-restify/issues/1832)) ([d9b27c6](https://www.github.com/restify/node-restify/commit/d9b27c602e260fc6c4f0e18e8b6835e89fa2adca))
* format falsy constants properly in json formatter ([#1792](https://www.github.com/restify/node-restify/issues/1792)) ([3002182](https://www.github.com/restify/node-restify/commit/3002182cacc7a9334237a9284a339ba93d3f213c))
* make arity error message actionable ([#1901](https://www.github.com/restify/node-restify/issues/1901)) ([97b6f93](https://www.github.com/restify/node-restify/commit/97b6f936e43860873f847bdd752b8090b3119da0))
* more flaky metrics.test.js fixes ([#1730](https://www.github.com/restify/node-restify/issues/1730)) ([71aac42](https://www.github.com/restify/node-restify/commit/71aac4283a1ae4ebd3c290afb83487b67010666f))
* properly handle non-errors thrown in domains ([#1757](https://www.github.com/restify/node-restify/issues/1757)) ([cb2e717](https://www.github.com/restify/node-restify/commit/cb2e7177c8b735987aed1c0839747f9658c19bb0))
* proxy events into instance var and add test script ([#1661](https://www.github.com/restify/node-restify/issues/1661)) ([de72f49](https://www.github.com/restify/node-restify/commit/de72f49eade48cc14dd916916ea86f88d46d3c8a))
* Re-add support for clientError listeners ([#1897](https://www.github.com/restify/node-restify/issues/1897)) ([05f12a6](https://www.github.com/restify/node-restify/commit/05f12a6864f4fa9aea617a42ae2d5c890478d2df))
* remove invalid triggering of uncaughtException handler ([#1710](https://www.github.com/restify/node-restify/issues/1710)) ([ee69806](https://www.github.com/restify/node-restify/commit/ee69806a338add1ebfef7eaad92a13273826c98e))
* Return 444 status code for closed and aborted requests ([#1579](https://www.github.com/restify/node-restify/issues/1579)) ([644c198](https://www.github.com/restify/node-restify/commit/644c1980aa1a21b0c7fa9aa41e22df9af6eab31e))
* send numbers or bools as payloads ([#1609](https://www.github.com/restify/node-restify/issues/1609)) ([0919f26](https://www.github.com/restify/node-restify/commit/0919f26db5d5614c0b2fa2567ac2ed43ee70b6d5))
* server should fire not acceptable event ([#1627](https://www.github.com/restify/node-restify/issues/1627)) ([8b11b71](https://www.github.com/restify/node-restify/commit/8b11b71b487d0001c96312519298f7f85b196471))
* use close event on response instead of socket ([#1892](https://www.github.com/restify/node-restify/issues/1892)) ([5c7eb95](https://www.github.com/restify/node-restify/commit/5c7eb95319aa54ef3b4b60d000d434824a666e18))
* use more reliable close event ([36318ae](https://www.github.com/restify/node-restify/commit/36318ae4c1fee02d3bc3737e34e1ea33e604f674))
* **benchmark:** force latest restify version ([#1810](https://www.github.com/restify/node-restify/issues/1810)) ([b8ec60e](https://www.github.com/restify/node-restify/commit/b8ec60e335b3ce95be4f2507623d357f4a600331))
* **bodyReader:** Fix memory leak ([#1566](https://www.github.com/restify/node-restify/issues/1566)) ([756b3f0](https://www.github.com/restify/node-restify/commit/756b3f02ba1dec114cf76c4e723ed054170a081c))
* **cpuUsageThrottle:** Always queue a new timeout ([#1484](https://www.github.com/restify/node-restify/issues/1484)) ([e4ffe43](https://www.github.com/restify/node-restify/commit/e4ffe430b47a2b51fe5fbef00dfa8bd3a1fb66c1))
* **cpuUsageThrottle:** Correctly named handler for debugInfo ([#1499](https://www.github.com/restify/node-restify/issues/1499)) ([78b0900](https://www.github.com/restify/node-restify/commit/78b0900b0ffcefa86e541c850d27779c5f656f00))
* **cpuUsageThrottle:** dont include interval in lag ([#1504](https://www.github.com/restify/node-restify/issues/1504)) ([eecb2d2](https://www.github.com/restify/node-restify/commit/eecb2d259deda34c2f297f2ef8b6d4fedc504e9e))
* **cpuUsageThrottle:** support breaking change in pidusage module ([7460064](https://www.github.com/restify/node-restify/commit/7460064fc13e5b977a295a2c939e050129c47797))
* **dev:** pin to exact versions of linting tools and fix lint errors ([3740a6b](https://www.github.com/restify/node-restify/commit/3740a6b7bf6e3bd589d9c1bc0c3d690978270564))
* **dev:** remove nsp since the project merged with npm ([1dc34b4](https://www.github.com/restify/node-restify/commit/1dc34b48de361960d7fa37d8bbc82b9d4a612981))
* **dev:** upgrading modules including restify-errors ([#1755](https://www.github.com/restify/node-restify/issues/1755)) ([3b71229](https://www.github.com/restify/node-restify/commit/3b712298c16577394d16b149be6c9a99044332b2))
* **dtrace:** route probes ([#1659](https://www.github.com/restify/node-restify/issues/1659)) ([84bcded](https://www.github.com/restify/node-restify/commit/84bcded77e9a42d3762146802418a1ae1ece8c30))
* **inflightRequestThrottle:** properly handle next ([#1471](https://www.github.com/restify/node-restify/issues/1471)) ([4db404f](https://www.github.com/restify/node-restify/commit/4db404f979d0da9651c00b076ceefb7b98a4e71f))
* **jsonBodyParser:** fix percent sign causing server fail ([#1411](https://www.github.com/restify/node-restify/issues/1411)) ([bde8fda](https://www.github.com/restify/node-restify/commit/bde8fda646a6f69b57fd72af1f00d6153fe056ec))
* **npm:** exclude extraneous files ([#1818](https://www.github.com/restify/node-restify/issues/1818)) ([e8516c3](https://www.github.com/restify/node-restify/commit/e8516c3735487ad5ebd332bc781404654c8c3cec))
* **npm:** remove unleash dependency ([#1522](https://www.github.com/restify/node-restify/issues/1522)) ([a43aa60](https://www.github.com/restify/node-restify/commit/a43aa60f090d29b8e66a58a9656126cb37bf2ef9))
* **package-lock.json:** remove artifacts.netflix.com repo ([#1526](https://www.github.com/restify/node-restify/issues/1526)) ([3d2f0f7](https://www.github.com/restify/node-restify/commit/3d2f0f7d0ddc14238691944cb9a1a60b02ae5947))
* **plugins:** save req._matchedVersion ([#1642](https://www.github.com/restify/node-restify/issues/1642)) ([69f917a](https://www.github.com/restify/node-restify/commit/69f917a3db66fac58f01c9e16535c2e2fcf2172b))
* **plugins:** use process.hrtime() for duration calculation ([#1507](https://www.github.com/restify/node-restify/issues/1507)) ([e8efd6c](https://www.github.com/restify/node-restify/commit/e8efd6cdcb73e674583e2a7081d2a9b923c72809))
* **request:** date() and time() methods return value ([#1576](https://www.github.com/restify/node-restify/issues/1576)) ([4c2cb1a](https://www.github.com/restify/node-restify/commit/4c2cb1a7edfe6252e68e409d850aef73961338ca))
* **server:** address domain performance regression with Node v12.x ([#1809](https://www.github.com/restify/node-restify/issues/1809)) ([e648d49](https://www.github.com/restify/node-restify/commit/e648d491151484f17263c6774678f1e7ac2fa188))
* **server:** address req and res close event changes in Node v10.x ([#1672](https://www.github.com/restify/node-restify/issues/1672)) ([6be3fb7](https://www.github.com/restify/node-restify/commit/6be3fb7c07483ee1991eba9aaa9ad4897c5a4965))
* **server:** avoid http2 experimental warning without http2 option ([#1555](https://www.github.com/restify/node-restify/issues/1555)) ([12da7fd](https://www.github.com/restify/node-restify/commit/12da7fdfc68dd9467da97ae0b2f45b89cb540b9b))
* **server:** avoiding uncaughtException in _routeErrorResponse by only sending response when not sent ([#1568](https://www.github.com/restify/node-restify/issues/1568)) ([cf65c65](https://www.github.com/restify/node-restify/commit/cf65c65cabd06bd5d17d84cd28999248dada94f7))
* **server:** fix uncaught exceptions triggering route lookups ([#1717](https://www.github.com/restify/node-restify/issues/1717)) ([e49cb3b](https://www.github.com/restify/node-restify/commit/e49cb3b24c3f4d77fa0b3204f3c1a618fb054789))
* **test:** make upgrade test pass ([#1772](https://www.github.com/restify/node-restify/issues/1772)) ([d30b748](https://www.github.com/restify/node-restify/commit/d30b7483c4d035e9a3fa94114557ae9d5f058f79))
* 652 - Incorrect error on route with no versions ([#1465](https://www.github.com/restify/node-restify/issues/1465)) ([ee15490](https://www.github.com/restify/node-restify/commit/ee154908d3ec4fd4a4108019140820c172df66b5))
* Add migration guid to website ([#1402](https://www.github.com/restify/node-restify/issues/1402)) ([5f053c7](https://www.github.com/restify/node-restify/commit/5f053c7efebc414b5a26daac3cc5e89dc0054fe3))
* add node 7-8 travis support ([#1405](https://www.github.com/restify/node-restify/issues/1405)) ([536a473](https://www.github.com/restify/node-restify/commit/536a4735266a7f56c205be4c6cafaa6adf81f480))
* create unit tests for sanitizePath plugin ([#1352](https://www.github.com/restify/node-restify/issues/1352)) ([12714cf](https://www.github.com/restify/node-restify/commit/12714cfce5048c65b4256df660766e863578b90a))
* doc site ([#1393](https://www.github.com/restify/node-restify/issues/1393)) ([76ee548](https://www.github.com/restify/node-restify/commit/76ee5480cfcb7f36e39e3e0955102c04abdac867))
* documentation update for restifyError event example ([#1398](https://www.github.com/restify/node-restify/issues/1398)) ([94fe715](https://www.github.com/restify/node-restify/commit/94fe715173ffcebd8814bed7e17a22a24fac4ae8))
* emit restifyError event even for router errors ([#1420](https://www.github.com/restify/node-restify/issues/1420)) ([f9d02d5](https://www.github.com/restify/node-restify/commit/f9d02d5b358863b9e067da5d6c89b4e283f420ba))
* redirect should work even when hostname or protocol is not specified in req.url ([#1497](https://www.github.com/restify/node-restify/issues/1497)) ([e696a1f](https://www.github.com/restify/node-restify/commit/e696a1f80cd84e7d3db9fb85a18212f970f9a0d3))
* **server:** error in pre handler triggers after event ([#1500](https://www.github.com/restify/node-restify/issues/1500)) ([c2e6dea](https://www.github.com/restify/node-restify/commit/c2e6deae5dab78187a8b09ce5256fb09db390bc9))
* exclude package-lock.json ([#1477](https://www.github.com/restify/node-restify/issues/1477)) ([011fdf0](https://www.github.com/restify/node-restify/commit/011fdf0e2e5b456fe18c9d2ef838819f52586c14))
* **static:** avoid user-provided data in Error messages being interpreted as sprintf codes ([#1384](https://www.github.com/restify/node-restify/issues/1384)) ([#1472](https://www.github.com/restify/node-restify/issues/1472)) ([9906344](https://www.github.com/restify/node-restify/commit/99063447419e7dcd0bf4ff6c38c5ad1867a2e1f3))
* audit timers of same name should accumulate ([#1435](https://www.github.com/restify/node-restify/issues/1435)) ([#1443](https://www.github.com/restify/node-restify/issues/1443)) ([a2d34aa](https://www.github.com/restify/node-restify/commit/a2d34aaa461cabf47147990a1c2910ea9a53b2d8))
* GH-1438, error reponse customization documentation incorrect ([#1439](https://www.github.com/restify/node-restify/issues/1439)) ([dd66088](https://www.github.com/restify/node-restify/commit/dd66088f3067d4b0858a2dd0274c705faf374e0e))
* Honor port for redirect ([#1363](https://www.github.com/restify/node-restify/issues/1363)) ([61c0cb5](https://www.github.com/restify/node-restify/commit/61c0cb5c697bcd84c2f7255bfe158619694fb73d))
* monkey patch getHeaders for pre-v7 Node.js (GH-1409) ([82088a7](https://www.github.com/restify/node-restify/commit/82088a7185331c7de092450ffec52d815c079739))
* package.json version now matches npm ([9944dbd](https://www.github.com/restify/node-restify/commit/9944dbd57795fa312c8f35c4734977698d70c895))
* respect when status code is set with res.status (GH-1429) ([#1440](https://www.github.com/restify/node-restify/issues/1440)) ([5abc067](https://www.github.com/restify/node-restify/commit/5abc06779df3b3ed4faf4d19f0815051a7c3106b))
* test static plugin's handling of sprintf escape sequences ([#1391](https://www.github.com/restify/node-restify/issues/1391)) ([5d7039a](https://www.github.com/restify/node-restify/commit/5d7039a5b97e158347fbb918b866b7aeebd4a14f))
* update chai (^3.4.1 to ^4.0.0) ([f982d0c](https://www.github.com/restify/node-restify/commit/f982d0c71f1b72f79e07f33f6cdf43741242f5d8))
* Update dependency mime to 1.4.0 ([#1467](https://www.github.com/restify/node-restify/issues/1467)) ([6d38b38](https://www.github.com/restify/node-restify/commit/6d38b38c7a67e9b7cb8500fd1a92751e5ea4ee38))
* update http-signature to v1.0.0 ([#1401](https://www.github.com/restify/node-restify/issues/1401)) ([ec88737](https://www.github.com/restify/node-restify/commit/ec887376a8314edbb623db48e6288d5a352a4efd))
* use `Buffer.isBuffer` instead of `util.isBuffer`. ([#1593](https://www.github.com/restify/node-restify/issues/1593)) ([35bd1c2](https://www.github.com/restify/node-restify/commit/35bd1c2b375ea70dc2b4a4549461ff59ff5e4ec4))
* versioned route matching should not throw TypeError ([#1381](https://www.github.com/restify/node-restify/issues/1381)) ([25d10f0](https://www.github.com/restify/node-restify/commit/25d10f00a4c9128b87cda0261aa3a041ac652f63))
* **audit:** use public APIs for accessing response headers ([5169db7](https://www.github.com/restify/node-restify/commit/5169db7b1d2c9979e534b2c27912f5be398bcbca)), closes [/nodejs.org/api/deprecations.html#deprecations_dep0066](https://www.github.com/restify//nodejs.org/api/deprecations.html/issues/deprecations_dep0066)
* Prefer Pino logger over Bunyan (#1841) ([2f5bf87](https://www.github.com/restify/node-restify/commit/2f5bf8722c9e0ba0d45f32af5c2c16ddbaa538b4)), closes [#1841](https://www.github.com/restify/node-restify/issues/1841)
### Miscellaneous Chores
* drop support for node 8 ([bd34988](https://www.github.com/restify/node-restify/commit/bd349884321d3e8af549f4d9da4456774e82ac8b))
* remove deprecated usage of pino.child ([#1902](https://www.github.com/restify/node-restify/issues/1902)) ([0a8cf83](https://www.github.com/restify/node-restify/commit/0a8cf8345de26f8ee98e87c0085f0f9439302d98))
* **travisci:** revisit nodejs version. Change to: LTS active, LTS maintenance (4.x) and stable releases ([#1553](https://www.github.com/restify/node-restify/issues/1553)) ([49eb008](https://www.github.com/restify/node-restify/commit/49eb008d987f1c425989b78e2336e3583e05a88a))
## [11.2.0](https://github.com/restify/node-restify/compare/v11.1.0...v11.2.0) (2023-08-11)
### Features
* allow alternate name for request id in logs ([cbd16ef](https://github.com/restify/node-restify/commit/cbd16efa3be36e7888ecccc15ee28eaa8fa6c5ef))
* support Node.js 20 ([9f1d249](https://github.com/restify/node-restify/commit/9f1d249c3fd023b05ac15c02352ec937ff7d1299))
### Bug Fixes
* tests broke due to find-my-way update ([f8beaae](https://github.com/restify/node-restify/commit/f8beaaef64c0541185bc4c2d864948d3c1299cc9))
## [11.1.0](https://github.com/restify/node-restify/compare/v11.0.0...v11.1.0) (2023-02-24)
### Features
* allow custom alternatives to domains ([54adfcb](https://github.com/restify/node-restify/commit/54adfcbdea1a6be3675dbc05573f8063fc16a05b))
## [11.0.0](https://github.com/restify/node-restify/compare/v10.0.0...v11.0.0) (2023-01-17)
### ⚠ BREAKING CHANGES
* don't override req.log if set during .first
* use req.log on audit plugin
### Features
* don't override req.log if set during .first ([852d2c1](https://github.com/restify/node-restify/commit/852d2c153d1815274db8cdd7799625e9740090b3))
* use req.log on audit plugin ([528ecbc](https://github.com/restify/node-restify/commit/528ecbcec5d70c458749bdd4c4cc3f9e06ab69a2))
## [10.0.0](https://github.com/restify/node-restify/compare/v9.0.0...v10.0.0) (2022-11-29)
### ⚠ BREAKING CHANGES
* support v18.x
### Features
* bump dtrace-provider version to avoid MacOS errors ([fa52f60](https://github.com/restify/node-restify/commit/fa52f60d85c3df8a1babde98be184bb918958ef3))
* support v18.x ([5795223](https://github.com/restify/node-restify/commit/57952239fa1808a6cf6e70deb2754c4c85c1be39))
### 8.5.1 (2019-12-13)
#### Bug Fixes
* **benchmark:** force latest restify version (#1810) ([b8ec60e3](git://github.com/restify/node-restify.git/commit/b8ec60e3))
* **server:** address domain performance regression with Node v12.x (#1809) ([e648d491](git://github.com/restify/node-restify.git/commit/e648d491))
## 8.5.0 (2019-12-02)
#### Features
* **chain:** use nextTick instead of setImmediate (#1808) ([703470ad](git://github.com/restify/node-restify.git/commit/703470ad))
### 8.4.1 (2019-11-27)
## 8.4.0 (2019-07-31)
#### Features
* **chain:** schedule handlers to the next tick (#1798) ([806ed711](git://github.com/restify/node-restify.git/commit/806ed711))
### 8.3.3 (2019-06-04)
### 8.3.2 (2019-05-06)
### 8.3.1 (2019-04-25)
#### Bug Fixes
* **test:** make upgrade test pass (#1772) ([d30b7483](git://github.com/restify/node-restify.git/commit/d30b7483))
## 8.3.0 (2019-04-11)
#### Features
* provide callback to uncaughtException handler (#1766) ([5e8b5e2b](git://github.com/restify/node-restify.git/commit/5e8b5e2b))
## 8.2.0 (2019-03-18)
#### Bug Fixes
* properly handle non-errors thrown in domains (#1757) ([cb2e7177](git://github.com/restify/node-restify.git/commit/cb2e7177))
* **cpuUsageThrottle:** support breaking change in pidusage module ([7460064f](git://github.com/restify/node-restify.git/commit/7460064f))
#### Features
* **first:** Handlers that execute ASAP in the req/res lifecycle (#1756) ([8178098d](git://github.com/restify/node-restify.git/commit/8178098d))
### 8.1.1 (2019-03-14)
#### Bug Fixes
* Published NPM package had a bad dependency on `npm` causing new irrelevant packages to get installed
## 8.1.0 (2019-03-06)
#### Bug Fixes
* **dev:** upgrading modules including restify-errors (#1755) ([3b712298](git://github.com/restify/node-restify.git/commit/3b712298))
#### Features
* add router.render() back to support hypermedia usecase (#1752) ([0700cfd4](git://github.com/restify/node-restify.git/commit/0700cfd4), closes [#1684](git://github.com/restify/node-restify.git/issues/1684))
* **plugin:** plugin to serve static files (#1753) ([a67b25f4](git://github.com/restify/node-restify.git/commit/a67b25f4))
## 8.0.0 (2019-02-20)
#### Breaking Changes
* Dropped Support for Node v4.x and Node v6.x
## 7.7.0 (2019-02-01)
#### Bug Fixes
* **dev:**
* remove nsp since the project merged with npm ([1dc34b48](git://github.com/restify/node-restify.git/commit/1dc34b48))
* pin to exact versions of linting tools and fix lint errors ([3740a6b7](git://github.com/restify/node-restify.git/commit/3740a6b7))
#### Features
* **audit:** Add the ability to specify a custom audit log serializer (for err, req and res) ([6231acda](git://github.com/restify/node-restify.git/commit/6231acda))
## 7.6.0 (2019-01-18)
#### Features
* **req:** add restifyDone event (#1740) ([4900d6bd](git://github.com/restify/node-restify.git/commit/4900d6bd))
## 7.5.0 (2019-01-09)
#### Bug Fixes
* emit after event with proper error param for node versions >= 11.4.0 (#1732) ([7a1378b0](git://github.com/restify/node-restify.git/commit/7a1378b0))
#### Features
* **plugins:** context, req.get() returns the whole context (#1739) ([6e35e01e](git://github.com/restify/node-restify.git/commit/6e35e01e))
## 7.4.0 (2019-01-02)
#### Bug Fixes
* more flaky metrics.test.js fixes (#1730) ([71aac428](git://github.com/restify/node-restify.git/commit/71aac428))
#### Features
* **plugins:** do not include user-input in UnsupportedMediaTypeError message (VError fails), m ([06c220d2](git://github.com/restify/node-restify.git/commit/06c220d2))
## 7.3.0 (2018-12-07)
#### Features
* add support for non-strict formatters (#1721) ([de1833a4](git://github.com/restify/node-restify.git/commit/de1833a4))
### 7.2.3 (2018-11-16)
#### Bug Fixes
* **server:** fix uncaught exceptions triggering route lookups (#1717) ([e49cb3b2](git://github.com/restify/node-restify.git/commit/e49cb3b2))
### 7.2.2 (2018-10-29)
#### Bug Fixes
* documentation typo fix (#1688) ([0fa71328](git://github.com/restify/node-restify.git/commit/0fa71328))
### 7.2.1 (2018-06-07)
#### Bug Fixes
* proxy events into instance var and add test script (#1661) ([de72f49e](git://github.com/restify/node-restify.git/commit/de72f49e))
* **server:** address req and res close event changes in Node v10.x (#1672) ([6be3fb7c](git://github.com/restify/node-restify.git/commit/6be3fb7c))
#### Features
* jsonBodyParser handles extended content types *+json (#1663) ([45375144](git://github.com/restify/node-restify.git/commit/45375144))
## 7.2.0 (2018-05-16)
#### Features
* **helpers:** add compose feature (#1660) ([eb60ef40](git://github.com/restify/node-restify.git/commit/eb60ef40))
### 7.1.2 (2018-05-15)
#### Bug Fixes
* **dtrace:** route probes (#1659) ([84bcded7](git://github.com/restify/node-restify.git/commit/84bcded7))
### 7.1.1 (2018-04-10)
#### Bug Fixes
* **plugins:** save req._matchedVersion (#1642) ([69f917a3](git://github.com/restify/node-restify.git/commit/69f917a3))
## 7.1.0 (2018-03-26)
#### Features
* **router:** add ignoreTrailingSlash router option (#1632) ([92ffbf5c](git://github.com/restify/node-restify.git/commit/92ffbf5c))
## 7.0.0 (2018-03-20)
#### Features
* **server:** new router and middleware system (#1561) ([1161370b](git://github.com/restify/node-restify.git/commit/1161370b))
#### Breaking Changes
*
- Server returns `RequestCloseError` instead of `RequestAbortedError`
- Non-strict routing is gone
- Different `RegExp` usage in router path and wildcards
- Remove already deprecated `next.ifError`
- Disable DTrace probes by default
- Change in calling `next` multiple times
- Router versioning and content type as a separate plugin: `conditionalHandler`
- After event fires when both request is flushed and the last handler is finished
- Metrics plugin latency logic changes and new latencies were added
For more info see the `/guides/6to7guide.md`.
([1161370b](git://github.com/restify/node-restify.git/commit/1161370b))
* dropping support below Node.js 4
([0698f45c](git://github.com/restify/node-restify.git/commit/0698f45c))
## 6.4.0 (2018-03-20)
#### Bug Fixes
* server should fire not acceptable event (#1627) ([8b11b71b](git://github.com/restify/node-restify.git/commit/8b11b71b))
* send numbers or bools as payloads (#1609) ([0919f26d](git://github.com/restify/node-restify.git/commit/0919f26d))
* Allow multiple unmerged set-cookie headers. (#1570) ([df040154](git://github.com/restify/node-restify.git/commit/df040154))
* add support for secureOptions in createServer (#1575) ([656e60e0](git://github.com/restify/node-restify.git/commit/656e60e0))
* use `Buffer.isBuffer` instead of `util.isBuffer`. (#1593) ([35bd1c2b](git://github.com/restify/node-restify.git/commit/35bd1c2b))
* **jsonBodyParser:** fix percent sign causing server fail (#1411) ([bde8fda6](git://github.com/restify/node-restify.git/commit/bde8fda6))
* **request:** date() and time() methods return value (#1576) ([4c2cb1a7](git://github.com/restify/node-restify.git/commit/4c2cb1a7))
### 6.3.4 (2017-11-21)
#### Bug Fixes
* **bodyReader:** Fix memory leak (#1566) ([756b3f02](git://github.com/restify/node-restify.git/commit/756b3f02))
* **server:** avoiding uncaughtException in _routeErrorResponse by only sending response when ([cf65c65c](git://github.com/restify/node-restify.git/commit/cf65c65c))
### 6.3.2 (2017-11-08)
### 6.3.1 (2017-11-03)
#### Bug Fixes
* **server:** avoid http2 experimental warning without http2 option (#1555) ([12da7fdf](git://github.com/restify/node-restify.git/commit/12da7fdf))
## 6.3.0 (2017-11-02)
#### Features
* **http2:** add native HTTP/2 support (#1489) ([6b202853](git://github.com/restify/node-restify.git/commit/6b202853))
### 6.2.3 (2017-10-18)
### 6.2.2 (2017-10-18)
#### Bug Fixes
* **package-lock.json:** remove artifacts.netflix.com repo (#1526) ([3d2f0f7d](git://github.com/restify/node-restify.git/commit/3d2f0f7d))
### 6.2.1 (2017-10-18)
#### Bug Fixes
* **cpuUsageThrottle:** dont include interval in lag (#1504) ([eecb2d25](git://github.com/restify/node-restify.git/commit/eecb2d25))
* **npm:** remove unleash dependency (#1522) ([a43aa60f](git://github.com/restify/node-restify.git/commit/a43aa60f))
* **plugins:** use process.hrtime() for duration calculation (#1507) ([e8efd6cd](git://github.com/restify/node-restify.git/commit/e8efd6cd))
## 6.2.0 (2017-10-16)
#### Bug Fixes
* **cpuUsageThrottle:** dont include interval in lag (#1504) ([eecb2d25](git://github.com/restify/node-restify.git/commit/eecb2d25))
* **plugins:** use process.hrtime() for duration calculation (#1507) ([e8efd6cd](git://github.com/restify/node-restify.git/commit/e8efd6cd))
## 6.1.0 (2017-10-16)
#### Bug Fixes
* **cpuUsageThrottle:** dont include interval in lag (#1504) ([eecb2d25](git://github.com/restify/node-restify.git/commit/eecb2d25))
* **plugins:** use process.hrtime() for duration calculation (#1507) ([e8efd6cd](git://github.com/restify/node-restify.git/commit/e8efd6cd))
### 6.0.1 (2017-09-19)
#### Bug Fixes
* **cpuUsageThrottle:** Correctly named handler for debugInfo (#1499) ([78b0900b](git://github.com/restify/node-restify.git/commit/78b0900b))
* **server:** error in pre handler triggers after event (#1500) ([c2e6deae](git://github.com/restify/node-restify.git/commit/c2e6deae))
## 6.0.0 (2017-09-15)
#### Bug Fixes
* exclude package-lock.json (#1477) ([011fdf0e](git://github.com/restify/node-restify.git/commit/011fdf0e))
* Update dependency mime to 1.4.0 (#1467) ([6d38b38c](git://github.com/restify/node-restify.git/commit/6d38b38c))
* **cpuUsageThrottle:** Always queue a new timeout (#1484) ([e4ffe430](git://github.com/restify/node-restify.git/commit/e4ffe430))
* **inflightRequestThrottle:** properly handle next (#1471) ([4db404f9](git://github.com/restify/node-restify.git/commit/4db404f9))
* **static:** avoid user-provided data in Error messages being interpreted as sprintf codes (# ([99063447](git://github.com/restify/node-restify.git/commit/99063447))
#### Features
* cpuUsageThrottle (#1460) ([84be6799](git://github.com/restify/node-restify.git/commit/84be6799))
* **throttle plugin:** expose rate limit metrics as headers (#1453) ([1627a557](git://github.com/restify/node-restify.git/commit/1627a557))
## 5.2.0 (2017-08-16)
#### Bug Fixes
* package.json version now matches npm ([9944dbd5](git://github.com/restify/node-restify.git/commit/9944dbd5))
* create unit tests for sanitizePath plugin (#1352) ([12714cfc](git://github.com/restify/node-restify.git/commit/12714cfc))
* audit timers of same name should accumulate (#1435) (#1443) ([a2d34aaa](git://github.com/restify/node-restify.git/commit/a2d34aaa))
* respect when status code is set with res.status (GH-1429) (#1440) ([5abc0677](git://github.com/restify/node-restify.git/commit/5abc0677))
* versioned route matching should not throw TypeError (#1381) ([25d10f00](git://github.com/restify/node-restify.git/commit/25d10f00))
### 5.0.1 (2017-07-17)
#### Bug Fixes
* monkey patch getHeaders for pre-v7 Node.js (GH-1409) ([82088a71](git://github.com/restify/node-restify.git/commit/82088a71))
* add node 7-8 travis support (#1405) ([536a4735](git://github.com/restify/node-restify.git/commit/536a4735))
* Add migration guid to website (#1402) ([5f053c7e](git://github.com/restify/node-restify.git/commit/5f053c7e))
* update http-signature to v1.0.0 (#1401) ([ec887376](git://github.com/restify/node-restify.git/commit/ec887376))
* documentation update for restifyError event example (#1398) ([94fe7151](git://github.com/restify/node-restify.git/commit/94fe7151))
* doc site (#1393) ([76ee5480](git://github.com/restify/node-restify.git/commit/76ee5480))
* test static plugin's handling of sprintf escape sequences (#1391) ([5d7039a5](git://github.com/restify/node-restify.git/commit/5d7039a5))
================================================
FILE: CHANGES.md
================================================
# restify Changelog
## 5.0.0
- #1377 Remove async formatters
- #1363 Honor port for `Response.prototype.redirect`
- #1369 Use public APIs for accessing response headers
- #1353 Deprecate `next.ifError`
- #1346 Return plugins to repo
- #1322 Remove duplicate `close` event from `Server`
- #1309 Add `getRoute()` to Request to get the route object. Rajat Kumar
- #1288 add `pre` and `routed` events. Yunong Xiao
- #1281 Add `server.getDebugInfo()` method, Yunong Xiao, Alex Liu
- #1281 `server.unfinishedRequests()` to `server.inflightRequests()`, Yunong Xiao
- #1256 add `req.id()` method, Alex Liu
- #1251 add `req.connectionState()` method, Alex Liu
- #1250 add `server.unfinishedRequests()` method, Alex Liu
- #1247 Update jyoent cloud API link in README, Devinsuit
- #1246 Fix syntax error in plugins.md example, Aria Stewart
- #1241 Rev formidable to remove Node6+ warnings, Alex Liu
- #1234 Update uuid to version 3.0.0, Marc Bachmann
- #1212 Fix typos in plugins.md, Greg Walden
- #1199 Update examples to use ES6, Amila Welihinda
- #1190 Fix minor typo, The-Alchemist
- #1179 Fix typo in comment, Niklas Ingholt
- Fix dtrace demo to not use async formatter, Yunong Xiao
- #1171 Router unmount now works for versioned routes, gcssabbagh
- #1143 add docs about serveStatic plugin, Michael Burguet
- #1135 ability to find a route by a path, Jacob Quatier
- #1129 always invoke res.send callback if one is provided, even when the
selected formatter is sync, Alex Liu
- #1128 don't send domain errors to the client, Alex Liu
- #1123 add deprecation warnings for domain dependent features, Alex Liu
- #1119 set response status code to 0 when the request is terminated by the
client, Alex Liu
- #1118 remove undocumented exports and other unused methods, Alex Liu
- #1113 Fix JSDOC comments, Marc Riegel
- #1111 new documentation guides, Nicolas Artman
- #1092 support for strict routing, lukealbao
- #1089 remove route from LRU cache on when calling `server.rm`, Luis Gómez
- #1086 support re-using request id headers on incoming requests, Alex Liu
- #1081 update documentation, default content-type is now application/json,
Dmitry Kirilyuk
- #1078 send the server name down in the header per documentation, Alex Liu
- #1072 update documentation for accept-version header, Ingo Renner
- #1071 rev node-uuid to address advisory CVE-2015-8851, Alex Liu
- #1056 fix `req.absoluteUri()` to use correct protocol, David Marek
- #1032 fix potential xss vector, Alex Liu
- #1024 **BREAKING** Disabled the uncaughtException handler by default, added
server option 'handleUncaughtExceptions' to allow enabling of it again
(restify 4.x and before used to handle exceptions by default), Todd Whiteman
- #1047 update documentation for spdy example, Tyler Akins
- #1041 add `rejectUnknown` option to restify plugin documentation, Dmitry
Kirilyuk
- #1038 capitalize header field for Location per RFC, Tommi Kyntola
- #1011 update documentation to remove outdated references. fix more links,
lukealbao
- #1010 update documentation to include charSet property for static plugin,
Greg Walker
- #1000 update spdy to 3.2.0, Andy Tzeng
- #1007 remove `defaultResponseHeaders` from documentation, lukealbao
- #999 server `NotFound` handler is now normalized, works like other error
events and no longer flushes responses automatically, Alex Liu
- #991 update documentation links for new plugins and errors repo, Ken Warner
- #987 disallow multiple values for content-type header, James O'Cull
- #985 CORS removed from restify core. support `next(false)` in pre handler
chains, Alex Liu
- #982 allow sending of null body, Felix Milea-Ciobanu
- #973 rev latest restify-errors, fix todoapp examples, Micah Ransdell
- #972 added shrinkwrap+nsp for security vuln checks, Alex Liu
- #971 Fix error creation when error message contain URI encoded characters,
Benjamin Urban
- #969 Fix incorrect usage of assert.AssertionError, Alex Liu
- #965 added unit test for sending null body, Michael Nisi
- #964 Fix cached routes not setting maxVersion, Alex Liu
- #963 enhancements to res.redirect. server now emits `redirect` event, James
Womack
- #960 update documentation for websocket example, Richard Kiene
- #958 RequestCaptureStream now writes triggering record, Gerrard Lindsay,
Yunong Xiao
- #955 update documentation for socket.io example, Thorsten Hans
- #952 Formatters no longer set status codes or inspect payload, Christian
Bongiorno, Alex Liu
- #951 `res.sendRaw()` allows sending of responses without use of formatters,
Matthew Amato, Alex Liu
- #947 update documentation links for readme.md & badges, ReadmeCritic
- #944 Support generic event listener, Alex Liu
- #943 Fix typos in documentation, azollyx
- #939 Fix issue where missing content-type header would hang response, Alex
Liu
- #935 Clearer docs for using certs, Vikram Tiwari
- #932 Update to spdy 2.x, Fedor Indutny
- #924 Update docs for async formatter breaking change, Magnus Wolffelt
- #891 stop processing requests when 'close' event has been fired (early
client termination), Alex Liu
- #883 hypens no longer stripped from route names, Sean Wragg
- #903 Update docs to reflect new error handling, Jacob Quatier
- #889 Bump dependencies to latest, Micah Ransdell
- #845 Support sync and async formatters, Alex Liu
- #844 Move Errors to their own module, Alex Liu
- #855 Clients now live in its own repo and npm module, Alex Liu
- Various documentation improvements from leitsubomi
## 4.3.0
- #1024 Add `handleUncaughtExceptions` server option to supporting disabling
the uncaughtException handler.
## 4.2.0
- #925 Support passing (most) [qs](https://github.com/ljharb/qs#readme) options
to the `restify.queryParser` plugin. Update the "qs" dependency to latest (v6)
while maintaining backward compatibility (see notes in the API doc and
"test/query.test.js" on `allowDots` and `plainObjects`).
## 4.1.1
- update negotiator (NSP advisory #106) and lru-cache (bug fix).
## 4.1.0
- Bump SPDY to latest.
- #959: fix issue where cached routes were not setting maxVersion on the req
## 4.0.4
- #937 Fix missing content-type header causing response to hang
## 4.0.3
- #917 Fix: HTTP 413 status name, Micah Ransdell
## 4.0.2
- #887 Bump dtrace-provider to 0.6.0 for Node 4 support, Corbin Uselton
## 4.0.0
- #877 content-type can be case-insensitive. Yunong Xiao
- #856 update various dependencies. Alex Liu
- #851 **BREAKING** fix formatters such that they always return cb. Yunong Xiao
- #847 fix body parser race condition. Yunong Xiao
- #842 add `req.matchedVersion()` Nathan Peck, Micah Ransdell
- #840 Fix issue with server toString Method. OiNutter, Micah Ransdell
- #836 Add JSDoc comments. Alex Liu
- #835 Update static.js to allow for serving static files that do not use the route as a path. Wavewash, Micah Ransdell
- #831 Support hash option to Formidable for multipart file uploads. blakevanian, ManRueda
- #832 Updated dtrace-provider. yads
- #812 add query parameters to auditlogger. Alex Liu
- #800 Allow 0, false, and null as json body. Alex Dobeck
- #771 q-value choice on wildcards ignores default q-value of 1. Kevin Peno
- #822 Allow optional headers to be added as properties to bunyan logs. Michael Paulson.
- #824 Don't include large coverage files in published packages. Trent Mick
- #819 Add a feature to allow the expiration of old unprocessed requests. Michael Paulson
- #803 Add redirect support to Response. Alex Liu
- #686 `res.send` can't send 0, false and null. Alex Dobeck
## 3.0.3
- #669 Fix socket.io 1.x integration. Mark Doeswijk
- #662 Improve request logger doc. Jordan Klassen
- #793 Update Server API error event listener doc. github.com/durkes
- #795 Remove unused vars in source. James Womack
- #796 Hypermedia API fails when paths have multiple patterns with sub-regexs. Morten Fangel
- #775 Fix UTF-8 corruption in body parser. Michał Moskal
## 3.0.2
- #785 update semver dependency.
## 3.0.1
- #779 set-cookie headers should not include comma separated values. See:
http://tools.ietf.org/html/rfc6265#section-3
## 3.0.0
- Bumping major because of #753
## 2.9.0
- #688 Fix various throttle bugs
- #691 Fix an issue where posting with text/csv content type crashes Restify
- #693 Support multiple response header values
- #704 Allow partial regex for named parameters
- #726 Allow per-request agent overrides
- #726 Ebanle `{agent: false}` option override per request
- #727 Fix JSON body parser behavior when request body is null
- #727 Fix a bug when `req.body === null`
- #731 SVG badges in README
- #734 Add API to track timers for nested handlers
- #744 Fix `request.isUpload` for PATCH requests
- #751 Fix `server.url` property when using IPv6
- #758 Switch to UUID v4
- #758 Use v4 UUIDs for `[x-]request-id`
- #759 Documentation fix
- #762 `res.noCache()` API to prevent all caching
- #767 Prefer the existing `err` serializer for audit logging
- Update dtrace-provider dependency
- #753 **BREAKING** Include `err` parameter for all \*Error events:
Error events will all have the signature `function (req, res, err, cb)` to
become consistent with the handling functionality introduced in 2.8.5.
Error handlers using the `function (req, res, cb)` signature must be updated.
## 2.8.5
- Add ability to listen for error events
- Documentation fixes
## 2.8.4
- Update dtrace-provider, bunyan and backoff
- Fix request URL cache interaction with dtrace probes
## 2.8.3
- Support html5 multiple file uploads
## 2.8.2
- #619 Default to url, if string provided to createClient
- #614 do not compute the MD5 Hash of a partial content
- #516 Allow an `options` object to be passed into the authorization plugin
- Updating dependencies
- #626 Add more built-in errors to doc
- #460 Provide direct access to https server options if needed
- #656 update qs
## 2.8.1
- revert #604, work around by not removing client listener
## 2.8.0
- #604 trap http client errors
- #598 Simplify and correct path segment regexp
- #570 Route matching should only prefer later routes if version is greater
- #564 Using req.accepts() according to implementation
- #504 Helper to render a route with given parameters (for hypermedia APIs)
## 2.7.0
- #547 Added mapFiles option to bodyParser
- #552 PUT JsonClient test should use PUT not POST
- #550 Make router preflight code more readable
- #548 Allow custom handling of multipart data.
## 2.6.3
- Hotfix for CORS plugin if no origin was set in the options
## 2.6.2
- #508 add server option: `ciphers` to pass down to https(tls)
- #502 `server.on('request')` not emitting
- #496 static plugin incorrectly handling `directories`; revert back to 2.6.0
version
- #495 don't override client response code with custom error object
- #494 socket connecting detection logic incorrect
- #492 client `false` needs to actually disable retries
- changed indent from four to eight
- #505 fix audit logger plugin bug
- #510 request timeout support
- #523 added Access-Control-Allow-Credentials to the preflight handler
## 2.6.1
- #478 Add `req.timers` to audit logging plugin.
- #487 RequestCaptureStream: dumpDefault, haveNonRawStreams, zero ring after dump
- #407 - bunyan 0.21.3
- Add CSV/TSV parser (Dominik Lessel)
- Add `req.timers`: a list of hrtime's for each handler
- Set TCP SO_KEEPALIVE when default KeepAliveAgent is on (client)
## 2.6.0
- EXPERIMENTAL: Native websocket support via watershed (Josh Clulow)
- Pass entire route, not just route.name to `after` (Dingman)
- Type coercion bug in Cache Control API (Chris Cannell)
## 2.5.1
- GH-401 RegEx routes stomp one another, resulting in 404
- GH-389 StringClient should handle garbage servers that send neither
`Content-Length` nor `Transfer-Encoding: chunked` headers.
## 2.5.0
- Pick up http-signature@0.10.0 (breaking change, to those using it); see
https://github.com/joyent/node-http-signature/issues/10
- GH-388 JSON client blows up on bad content
- GH-379 Static plugin: NotAuthorizedError for file path with
parentheses (Ricardo Stuven)
- GH-370 Add charSet option for static file plugin (Jonathan Dahan)
## 2.4.1
- Support node 0.10.X TLS options in client(s)
## 2.4.0
- GH-368 Route /\/.*/ does not match request /? (Ben Hutchison)
- GH-366 `req.accepts()` not working with short-hand mime types
- GH-362 Empty body throws TypeError in StringClient (Bryan Donovan)
- GH-355 Serve gzip encoded files from static if they are available
(Nathanael Anderson)
- GH-338 turn `req.body` into an `Object` when content-type is
JSON (Daan Kuijsten)
- GH-336 `res.charSet()` back in
- dependency version bumps
- 0.10.X support in tests (add necessary `resume()` calls)
- client should log request/response pairs
## 2.3.5
- bunyan@0.20.0
- GH-346 `server.toString()` crashes (Alex Whitman)
- GH-193 support `next('name_of_route')`
## 2.3.4
- GH-343 default to 'identity' for accept-encoding
- GH-342 client support for PATCH
- Pick up spdy@1.4.6 (doesn't ship all the example garbage)
## 2.3.3
- Stop logging client_req in bunyan output
- GH-319 make DTrace optional
- GH-335 Content-Type'd routes not accepting array (Pedro Palazón)
## 2.3.2
- pick up bunyan 0.18.3
- server.address() returning null causes server.url to deref null
## 2.3.1
- GH-335 Content-Type'd routes not correct when only Accept-Extension varies,
part deux (switch to `negotiator`, drop `mimeparse`).
## 2.3.0
- GH-335 Content-Type'd routes not correct when only Accept-Extension varies
- GH-332 Cache-Control max-age should show minutes (Ben Hutchison)
- GH-329 Wrong values in res.methods on some cases
- GH-327 server.versionedUse('1.2.3', function (req, res, next) {}) (Tim Kuijsten)
- GH-326 non-default origins not working, Chrome requires allow/origin and
allow users to append to CORS array (John Fieber/Damon Oehlman)
- GH-323 /? broken
- GH-322 add `req.route`, which contains the original params for the
route (Tim Kuijsten)
- GH-312 bodyParser() should return buffers when data is binary (Tim Kuijsten)
- GH-318 Allow use of 'requestBodyOnGet' option in bodyParser (@geebee)
## 2.2.1
- GH-283 broke versioned, typed routes. Fix.
- node-http-signature@0.9.11
## 2.2.0
- GH-316 drop `clone`, and just shallow copy (Trent Mick)
- GH-284 preflight requests not working without access-control-request-headers
- GH-283 versioned routes should use maximum match, not first (Colin O'Brien)
- dtrace probes for restify clients
- node-dtrace-provider@0.2.8
- backoff@2.0.0 and necessary changes
## 2.1.1
- revert to backoff@1.2.0
## 2.1.0
- GH-284 built-in CORS
- GH-290 next.ifError
- GH-291 fix overwriting `options.type` in createJSONClient (Trent Mick)
- GH-297 default document serving in static plugin (Adam Argo)
- GH-299 gzip plugin doesn't play nice with content-length (Ben Hale)
- GH-301 support private keys w/passphrase (Erik Kristensen)
- GH-302 responseTime cleanup
- Move to `node-backoff` and rework retry logic in HttpClient
- Support keep-alive by default in HttpClient
## 2.0.4
- GH-280 req.params cached by router
- RequestCaptureStream should support outputting to multiple streams
- Default uncaughtException handler should check if headers have been sent
## 2.0.2/2.0.3
- GH-278 missing events on route errors
- Undo `RestError` `constructorOpt` from 2.0.1
## 2.0.1
- GH-269 plugin to make curl happy
- RestError not honoring `constructorOpt` from `cause`
- GH-271 bump to dtrace 0.2.6 (fix build on Mountain Lion)
# Legacy Releases
## 1.4.2
- logging typo (Pedro Candel)
- response `beforeSend` event (Paul Bouzakis)
## 1.4.1
- GH-130 Allow restify and express to coexist.
- GH-129 format HttpErrors as well as RestErrors (Domenic Denicola)
- GH-127 add absolute uri to request (Paul Bouzakis)
- GH-124 `req.query` is `undefined` if no query string was sent
- GH-123 Generated DTrace probe names should be valid
- GH-122 Response._writeHead can cause infinite loop (Trent Mick)
- GH-120 Allow server.patch (Paul Bouzakis)
- GH-119 defaultResponseHeaders not settable
- GH-113 document `return next(false)`
## 1.4.0
- GH-116 More friendly error objects (Domenic Denicola)
- GH-115 Client hangs on server "hard kills" (i.e., RST)
- GH-111 JSON parser only works on objects (not arrays)
- GH-110 emit expectContinue (Paul Bouzakis)
- Fix "undefined" log message in string_client.js
- GH-107
- Go back to hacking up http.prototype for performance reasons
- Default to keep-alive on for HTTP/1.1 requests
- Misc fixes after refactoring.
- GH-109 routes not honoring regex flags.
- GH-108 server missing `listening` event.
- Audit logger optionally logs request/response bodies
- Require http-signature@0.9.9/ctype@0.5.0 (node 0.7 compatible)
## 1.3.0
- GH-100 Make DTrace an optional dependency, and stub it out if not found.
- res.link API not allowing sprintf style sets.
- Support for `socketPath` in client API (alternative to url).
- OPTIONS api not returning access-control-allow-methods header (Steve Mason).
- Allow null passwords in HTTP basic auth (Andrew Robinson).
- set `req.files` on multipart file uploads (Andrew Robinson).
## 1.2.0
- Don't rely on instanceof checks for Errors in response.
- Change route.run log level from trace to debug on next(err).
- Add `res.link` API (wrap up sending a Link: response header).
- GH-98 req.secure needs to return a boolean, not an object
- GH-97 Malformed URI results in server crash
- GH-94 leverage `qs` module for object notation in query string.
## 1.1.1
- dependency version bumps
- res.header accepts sprintf-style arguments
- GH-95 Make restify compatible with node-logging (Andrew Robinson)
- GH-93 Minimal port of express pre-conditions (Dominic Barnes)
- GH-92 Make X-Response-Time configurable (Shaun Berryman)
- GH-87 server.listen on port as string (Andrew Sliwinski)
## 1.1.0
- GH-86 Bunyan version bump.
- Conditional Request plugin tests and fixes for some errors (Mike Williams).
- GH-83 pluggable storage engine for throttling, and LRU for default engine.
- GH-77 `server.on('uncaughtException', function (req, res, route, err) {});`
- GH-79 Docs typo(s).
## 1.0.1
- Version bump bunyan to 0.6.4.
## 1.0.0
- Makefile restructure (use Joyent templates)
- GH-20 HttpClient connectTimeout.
- Allow parser plugins to allow "override" params
- Proper handling of Expect: 100
- multipart/form-data plugin
- Added a 'header' event on res.writeHead
- GH-72 Wrong server name in response header on 404/405/...
- RegExp mounts throw a TypeError
- Allow pre handlers to update request url
- try/catch around route running
- Bundled audit logger (via bunyan)
- strict adherence to RFC3986 for URL encoding
- range versioning changed to be an array of explicit versions
- Switch from log4js to [bunyan](https://github.com/trentm/node-bunyan)
- Official version of `ConditionalRequest` plugin (Falco Nogatz)
- order formatters on response such that JSON/text are before custom ones
- RestErrors can use format strings
- date plugin has bad log check
## 1.0.0-rc2
- GH-66 Support for charSets in responses
- GH-65 Initial version of etag plugin (Falco Nogatz)
- GH-68 res.header() can serialize Date objects to RFC1123
- GH-67 Set some default response headers earlier (access-control-*)
- http-client should auto insert the date header
- GH-64 Support for a pre-routing chain
- JsonClient should "upcast" errors to RestErrors if it can
- GH-63 res.send(204) returning a body of 204
- GH-61 Make bodyParser merging into req.params optional
- Make Error objects backwards compatible with older restify (httpCode/restCode)
- GH-57, GH-62 range versioning on server (Diego Torres)
- GH-59 routes with just '/' are returning 404
- DTrace *-done actually firing content-length (was always 0)
- [Issue 56] Support streaming downloads
- Modify server.on('after') to emit the `Route` object, rather than the name.
## 1.0.0-rc1
(Started maintaining this log 21 January 2012. For earlier change information
you'll have to dig into the commit history.)
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Restify
Welcome to the restify community! This document is written both for maintainers and community members!
## Issues and PRs
### Commit Messages
When merging a PR, we squash and merge to keep our commit history clean. Our commit messages use the conventional changelog format (http://conventionalcommits.org/) to automagically manage semver for us.
### Labels and Templates
We try to keep things organized around here. Maintainers have a finite amount of time and are often juggling multiple things in their lives. Keeping things consistent and well labeled helps reduce the amount of concentration and effort required for us to both find and carry out work on the project. Simple things like using our templates and adding the appropriate labels may only take you a few minutes, but it can save cummulative hours worth of work for maintainers trying to digest dozens of issues.
## Website
### Design
The website templates are maintained at https://github.com/restify/restify.github.io and are populated from the docs directory in this repo.
### Releasing a change
To update the documentaiton on the website to reflect the latest version of 5.x simply:
```
git clone --recursive git@github.com:restify/restify.github.io
cd restify.github.io
git submodule update --remote && git add _docs && git commit -m 'bump' && git push origin master
```
The website will automatically deploy itself with the new changes.
### Updating a documentation page
To update docs, simply run:
```
make docs-build
```
### Adding a documentation page
To add a new page, simply give it a [permalink](https://github.com/restify/node-restify/blob/94fe715173ffcebd8814bed7e17a22a24fac4ae8/docs/index.md) and then update [docs.yml](https://github.com/restify/restify.github.io/blob/master/_data/docs.yml) with the new permalink.
## Running a benchmark
```
make benchmark
```
## Cutting a release
Cutting a release is currently a manual process. We use a [Conventional Changelog](http://conventionalcommits.org/) to simplify the process of managing semver on this project. Generally, the following series of commands will cut a release from the `master` branch:
```
$ git fetch
$ git pull origin master # ensure you have the latest changes
$ npx unleash [-p for patch, -m for minor, -M for major] --no-publish -d # do a dry run to verify
$ npx unleash [-p for patch, -m for minor, -M for major] --no-publish
# Unleash doesnt support 2FA, hence we use --no-publish flag here.
# This ensures we have the package.json updated, changelog generated, tag created
# and all the changes into origin
# Next, publish to npm manually and do not forget to provide the 2FA code.
$ npm publish
```
================================================
FILE: FEATURE_REQUESTS.md
================================================
Feature Requests
================
While the maintainers of restify work hard to provide the best possible
framwork for building REST services, there is more work to go around than there
are hours in the day. This document contains a set of features that have been
requested by the community. If you are looking to contribute, the items on
this list -- along with the open bugs on the issues tab -- are a great place to
start!
> The features here are not sorted in any particular order. Each feature links
> to the original GitHub issue requesting it. While feature requests are
> generally closed and moved to this document, discussion around the feature
> still takes place on the original issue. Even if there has been a discussion
> on the issue already, it is still worth declaring your intent to open a PR
> before investing time in writing code.
## Code
* [Support `server.use(route, handler)` and `server.all`][289]
* [Support asynchronous callbacks for throttle][381]
* [Streaming multipart parser without needing temporary files][474]
* [Default client to http protocol][790]
* [Exponential backoff and retry][633]
* [Arbitrary HTTP methods][576]
* [Detect route conflicts][909]
* [Improve upon `next.ifError`][875]
* [Benchmark suite][860]
* [HTTP/2 support][853]
* [Improve performance of route lookup][850]
* [Support HTTP_PROXY][813]
* [IE9 support for `bodyParser`][801]
* [Multipart Client Support][921]
* [Remove `next(err)`][1019]
* [Support `RegExp` for `route.render`][632]
* [Run internal handlers on `NotFound`][708]
* [Support multiple apps on the same port][1035]
* [Multiple versions for routes][1134]
* [sysdig support][1323]
* [Migrate routing DSL to `path-to-regexp`][1292]
* [JQuery Style Query Expansion][895]
* [Support Proxy Protocol][1046]
## Documentation
* [Socket.io support][717]
* [`uncaughtException` handler when opting into domains][829]
* [Forward `req_id`][1101]
* [Document `bodyParser` headers][989]
* [Improve signRequest documentation][737]
* [Better documentation for `client.close`][859]
* [Document all client options][1326]
* [Client tunneling vs. proxying][1327]
* [Explain why Restify is great!][927]
* [`BasicAuth` examples][1099]
* [Document `next` behaviour][1068]
* [Remove defaultResponseHeaders][1040]
* [Properly document req.accepts][957]
* [Plugin custom errors][948]
* [`findByPath` on `Router`][1136]
* [Multiple route handlers][1183]
* [Update new `HttpError` codes][1206]
* [v4 res.headers][1286]
* [Document RegExp DSL for routing][1065]
[289]: https://github.com/restify/node-restify/issues/289
[381]: https://github.com/restify/node-restify/issues/381
[474]: https://github.com/restify/node-restify/issues/474
[575]: https://github.com/restify/node-restify/issues/575
[790]: https://github.com/restify/node-restify/issues/790
[633]: https://github.com/restify/node-restify/issues/663
[717]: https://github.com/restify/node-restify/issues/717#issuecomment-296531086
[576]: https://github.com/restify/node-restify/issues/576
[576]: https://github.com/restify/node-restify/issues/576
[909]: https://github.com/restify/node-restify/issues/909
[875]: https://github.com/restify/node-restify/issues/875
[860]: https://github.com/restify/node-restify/issues/860
[853]: https://github.com/restify/node-restify/issues/853
[850]: https://github.com/restify/node-restify/issues/850
[829]: https://github.com/restify/node-restify/issues/829
[813]: https://github.com/restify/node-restify/issues/813
[801]: https://github.com/restify/node-restify/issues/801
[921]: https://github.com/restify/node-restify/issues/921
[1101]: https://github.com/restify/node-restify/issues/1101
[1019]: https://github.com/restify/node-restify/issues/1019
[989]: https://github.com/restify/node-restify/issues/989
[632]: https://github.com/restify/node-restify/issues/632
[708]: https://github.com/restify/node-restify/issues/708
[737]: https://github.com/restify/node-restify/issues/737
[859]: https://github.com/restify/node-restify/issues/859
[1326]: https://github.com/restify/node-restify/issues/1326
[1327]: https://github.com/restify/node-restify/issues/1327
[927]: https://github.com/restify/node-restify/issues/927
[1099]: https://github.com/restify/node-restify/issues/1099
[1068]: https://github.com/restify/node-restify/issues/1068
[1040]: https://github.com/restify/node-restify/issues/1040
[1035]: https://github.com/restify/node-restify/issues/1035
[957]: https://github.com/restify/node-restify/issues/957
[948]: https://github.com/restify/node-restify/issues/948
[1134]: https://github.com/restify/node-restify/issues/1134
[1136]: https://github.com/restify/node-restify/issues/1136
[1183]: https://github.com/restify/node-restify/issues/1183
[1206]: https://github.com/restify/node-restify/issues/1206
[1286]: https://github.com/restify/node-restify/issues/1286
[1323]: https://github.com/restify/node-restify/issues/1323
[1292]: https://github.com/restify/node-restify/issues/1292
[1065]: https://github.com/restify/node-restify/pull/1065
[895]: https://github.com/restify/node-restify/issues/895
[1046]: https://github.com/restify/node-restify/pull/1046
================================================
FILE: LICENSE
================================================
Copyright (c) 2011 Mark Cavage, All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE
================================================
FILE: Makefile
================================================
#
# Copyright (c) 2012, Joyent, Inc. All rights reserved.
#
# Makefile: basic Makefile for template API service
#
# This Makefile is a template for new repos. It contains only repo-specific
# logic and uses included makefiles to supply common targets (javascriptlint,
# jsstyle, restdown, etc.), which are used by other repos as well. You may well
# need to rewrite most of this file, but you shouldn't need to touch the
# included makefiles.
#
# If you find yourself adding support for new targets that could be useful for
# other projects too, you should add these to the original versions of the
# included Makefiles (in eng.git) so that other teams can use them too.
#
#
# Tools
#
ESLINT := ./node_modules/.bin/eslint
DOCUMENTATION := ./node_modules/.bin/documentation
NODEUNIT := ./node_modules/.bin/nodeunit
MOCHA := ./node_modules/.bin/mocha
NODECOVER := ./node_modules/.bin/nyc
DOCS_BUILD := ./tools/docsBuild.js
NPM := npm
NODE := node
PRETTIER := ./node_modules/.bin/prettier
#
# Files
#
JS_FILES = '.'
CLEAN_FILES += node_modules cscope.files
include ./tools/mk/Makefile.defs
#
# Repo-specific targets
#
.PHONY: all
all: $(NODEUNIT) $(REPO_DEPS)
$(NPM) rebuild
$(NODEUNIT): | $(NPM_EXEC)
$(NPM) install
$(NODECOVER): | $(NPM_EXEC)
$(NPM) install
.PHONY: cover
cover: $(NODECOVER)
@rm -fr ./.coverage_data
$(NODECOVER) --reporter=html --reporter=text-summary --reporter=lcov $(NODEUNIT) ./test/*.js
CLEAN_FILES += $(TAP) ./node_modules/nodeunit
.PHONY: test
test: $(NODEUNIT)
$(NODEUNIT) test/*.test.js
$(MOCHA) --full-trace --no-exit test/plugins/*.test.js
.PHONY: docs-build
docs-build:
@($(NODE) $(DOCS_BUILD))
.PHONY: benchmark
benchmark:
@(cd ./benchmark && $(NPM) i && $(NODE) index.js)
include ./tools/mk/Makefile.deps
include ./tools/mk/Makefile.targ
================================================
FILE: README.md
================================================
[![alt text][1.1]][1]
[1.1]: http://i.imgur.com/tXSoThF.png (twitter icon with padding)
[1.2]: http://i.imgur.com/wWzX9uB.png (twitter icon without padding)
[1]: http://www.twitter.com/restifyjs

[](https://travis-ci.org/restify/node-restify)
[](https://david-dm.org/restify/node-restify)
[](https://david-dm.org/restify/node-restify#info=devDependencies)
[](https://github.com/prettier/prettier)
[restify](http://restify.com) is a framework, utilizing
[connect](https://github.com/senchalabs/connect) style middleware for building
REST APIs. For full details, see http://restify.com
Follow restify on [![alt text][1.2]][1]
# Usage
## Server
```javascript
var restify = require('restify');
const server = restify.createServer({
name: 'myapp',
version: '1.0.0'
});
server.use(restify.plugins.acceptParser(server.acceptable));
server.use(restify.plugins.queryParser());
server.use(restify.plugins.bodyParser());
server.get('/echo/:name', function (req, res, next) {
res.send(req.params);
return next();
});
server.listen(8080, function () {
console.log('%s listening at %s', server.name, server.url);
});
```
## Client
```javascript
var assert = require('assert');
var clients = require('restify-clients');
var client = clients.createJsonClient({
url: 'http://localhost:8080',
version: '~1.0'
});
client.get('/echo/mark', function (err, req, res, obj) {
assert.ifError(err);
console.log('Server returned: %j', obj);
});
```
# Installation
```bash
$ npm install restify
```
## Supported Node Versions
Restify currently works on Node.js v14.x and v16.x.
## License
The MIT License (MIT)
Copyright (c) 2018 restify
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## Bugs
See .
## Other repositories
- For the errors module, please go [here](https://github.com/restify/errors).
## Mailing list
See the
[Google group](https://groups.google.com/forum/?hl=en&fromgroups#!forum/restify)
.
================================================
FILE: SECURITY.md
================================================
# Security Policy
## Reporting a Vulnerability
Do not disclose vulnerabilities in public issues. Please report vulnerabilities to
security@restify.com with steps to reproduce the vulnerability, and a patch to fix
it if possible.
================================================
FILE: benchmark/benchmarks/middleware.js
================================================
'use strict';
var restify = process.argv.includes('version=head')
? require('../../lib')
: require('restify');
var server = restify.createServer();
var path = '/';
var port = 3000;
module.exports = {
url: 'http://localhost:' + port + path
};
function handler(req, res, next) {
next();
}
for (var i = 0; i < 10; i++) {
server.pre(handler);
}
for (var j = 0; j < 10; j++) {
server.use(handler);
}
server.get(path, function get(req, res) {
res.send('hello world');
});
if (!module.parent) {
server.listen(port);
}
================================================
FILE: benchmark/benchmarks/response-json.js
================================================
'use strict';
var restify = process.argv.includes('version=head')
? require('../../lib')
: require('restify');
var server = restify.createServer();
var path = '/';
var port = 3000;
module.exports = {
url: 'http://localhost:' + port + path
};
server.get(path, function onRequest(req, res) {
res.send({ hello: 'world' });
});
if (!module.parent) {
server.listen(port);
}
================================================
FILE: benchmark/benchmarks/response-text.js
================================================
'use strict';
var restify = process.argv.includes('version=head')
? require('../../lib')
: require('restify');
var server = restify.createServer();
var path = '/';
var port = 3000;
module.exports = {
url: 'http://localhost:' + port + path
};
server.get(path, function onRequest(req, res) {
res.send('hello world');
});
if (!module.parent) {
server.listen(port);
}
================================================
FILE: benchmark/benchmarks/router-heavy.js
================================================
'use strict';
var restify = process.argv.includes('version=head')
? require('../../lib')
: require('restify');
var server = restify.createServer();
var path = '/whiskeys/scotch/islay/lagavulin/16-years/50';
var methods = ['post', 'put', 'get', 'del', 'patch'];
var _ = require('lodash');
var port = 3000;
// Disabling cache: it's not fair as it aims to the worst case, when
// cache hit ratio is 0%. However, it's still better than the worst
// as it doesn't require extra time to maintain the LRU cache.
// There is no other way to simulate 100+ different endpoint
// calls with the current benchmark suite.
if (!process.argv.includes('version=head')) {
server.router.cache = {
get: function get() {
return null;
},
set: function get() {
return null;
},
dump: function get() {
return [];
}
};
}
module.exports = {
url: 'http://localhost:' + port + path
};
var routes = {
beers: {
ale: {
'pale-ale': {
'american-pale-ale': [],
'indian-pale-ale': []
},
lambic: [],
stout: {
'american-porter': [],
'imperial-stout': [],
'irish-stout': []
}
},
lager: {
'german-lager': {
marzen: []
},
pilsner: {
'german-pilsner': []
}
}
},
whiskeys: {
american: {
bourbon: {
kentchuky: {
'jim-beam': ['jim-beam', 'bookers', 'old-crow'],
'makers-mark': ['makers-mark'],
'woodford-reserve': ['woodford-reserve']
},
tennessee: {
'jack-daniels': ['jack-daniels']
}
},
rye: {
'beam-suntory': ['jim-beam-rye', 'knob-creek']
}
},
irish: {
'single-malt': {
bushmills: ['bushmills'],
connemare: ['connemare']
},
'single-pot': {
redbreast: ['redbreast'],
jameson: ['jameson-15-year']
}
},
japanese: {
nikka: ['coffeey-malt', 'blended', 'from-the-barrel'],
hibiki: ['japanese-harmony'],
yamazakura: ['blended']
},
scotch: {
islay: {
bruichladdich: ['25-years', 'islay-barley-2009'],
octomore: ['7.2', 'islay-barley-8.3'],
laphroaig: ['lore', '15-years', 'four-oak'],
lagavulin: ['distillers-edition', '8-years', '16-years']
}
}
}
};
function handler(req, res) {
res.send('hello');
}
function attachRoute(parent, routeConfig) {
_.map(routeConfig, function map(route, routeKey) {
var pathChunk = _.isString(routeKey) ? routeKey : route;
var routePath = parent + '/' + pathChunk;
methods.forEach(function forEach(method) {
server[method](routePath, handler);
});
if (_.isObject(route) || _.isArray(route)) {
attachRoute(routePath, route);
}
if (_.isString(route)) {
for (var i = 0; i <= 100; i++) {
methods.forEach(function forEach(method) {
server[method](routePath + '/' + i, handler);
});
}
}
});
}
attachRoute('', routes);
if (!module.parent) {
server.listen(port);
}
================================================
FILE: benchmark/index.js
================================================
#!/usr/bin/env node
'use strict';
var inquirer = require('inquirer');
var bench = require('./lib/bench');
var stableVersion = require('restify/package.json').version;
var BENCHMARKS = [
'response-json',
'response-text',
'router-heavy',
'middleware'
];
function select(callback) {
var choices = BENCHMARKS.map(function map(name) {
return {
name: name,
checked: true
};
});
choices.unshift(new inquirer.Separator(' = The usual ='));
inquirer
.prompt([
{
type: 'checkbox',
message: 'Select packages',
name: 'list',
choices: choices,
validate: function validate(answer) {
if (answer.length < 1) {
return 'You must choose at least one package.';
}
return true;
}
}
])
.then(function onPrompted(answers) {
callback(answers.list);
});
}
inquirer
.prompt([
{
type: 'confirm',
name: 'track',
message: 'Do you want to track progress?',
default: false
},
{
type: 'confirm',
name: 'compare',
message:
'Do you want to compare HEAD with the stable release (' +
stableVersion +
')?',
default: true
},
{
type: 'confirm',
name: 'all',
message: 'Do you want to run all benchmark tests?',
default: true
},
{
type: 'input',
name: 'connection',
message: 'How many connections do you need?',
default: 100,
validate: function validate(value) {
return (
!Number.isNaN(parseFloat(value)) || 'Please enter a number'
);
},
filter: Number
},
{
type: 'input',
name: 'pipelining',
message: 'How many pipelining do you need?',
default: 10,
validate: function validate(value) {
return (
!Number.isNaN(parseFloat(value)) || 'Please enter a number'
);
},
filter: Number
},
{
type: 'input',
name: 'duration',
message: 'How long does it take?',
default: 30,
validate: function validate(value) {
return (
!Number.isNaN(parseFloat(value)) || 'Please enter a number'
);
},
filter: Number
}
])
.then(function validate(opts) {
if (!opts.all) {
select(function onSelected(list) {
bench(opts, list);
});
} else {
bench(opts, BENCHMARKS);
}
});
================================================
FILE: benchmark/lib/autocannon.js
================================================
'use strict';
var autocannon = require('autocannon');
var fs = require('fs');
var autocannonCompare = require('autocannon-compare');
var path = require('path');
var resultsDirectory = path.join(__dirname, '../results');
function writeResult(handler, version, result) {
try {
fs.accessSync(resultsDirectory);
} catch (e) {
fs.mkdirSync(resultsDirectory);
}
result.server = handler;
var dest = path.join(resultsDirectory, handler + '-' + version + '.json');
return fs.writeFileSync(dest, JSON.stringify(result, null, 4));
}
function fire(opts, handler, version, save, cb) {
opts = opts || {};
opts.url = opts.url || 'http://localhost:3000';
var instance = autocannon(opts, function onResult(err, result) {
if (err) {
cb(err);
return;
}
if (save) {
writeResult(handler, version, result);
}
cb();
});
if (opts.track && save) {
autocannon.track(instance);
}
}
function compare(handler) {
var resStable = require(resultsDirectory + '/' + handler + '-stable.json');
var resHead = require(resultsDirectory + '/' + handler + '-head.json');
var comp = autocannonCompare(resStable, resHead);
var result = {
throughput: {
significant: comp.throughput.significant
}
};
if (comp.equal) {
result.throughput.equal = true;
} else if (comp.aWins) {
result.throughput.equal = false;
result.throughput.wins = 'stable';
result.throughput.diff = comp.throughput.difference;
} else {
result.throughput.equal = false;
result.throughput.wins = 'head';
result.throughput.diff = autocannonCompare(
resHead,
resStable
).throughput.difference;
}
return result;
}
module.exports = {
fire: fire,
compare: compare
};
================================================
FILE: benchmark/lib/bench.js
================================================
#!/usr/bin/env node
'use strict';
var fork = require('child_process').fork;
var ora = require('ora');
var path = require('path');
var autocannon = require('./autocannon');
var pipeline = require('vasync').pipeline;
function runBenchmark(opts, handler, version, cb) {
if (opts.track) {
console.log(version.toUpperCase() + ':');
}
var spinner = ora('Started ' + version + '/' + handler).start();
var modulePath = path.join(__dirname, '../benchmarks', handler);
var url = require(modulePath).url;
var forked = fork(modulePath, ['version=' + version]);
pipeline(
{
funcs: [
function warm(_, callback) {
spinner.color = 'magenta';
spinner.text =
'Warming ' + version + '/' + handler + ' for 5s';
var fireOpts = Object.assign({}, opts, {
duration: 5,
url: url
});
autocannon.fire(
fireOpts,
handler,
version,
false,
callback
);
},
function benchmark(_, callback) {
if (opts.track) {
spinner.stop();
} else {
spinner.color = 'yellow';
spinner.text =
'Benchmarking ' +
version +
'/' +
handler +
' for ' +
opts.duration +
's';
}
var fireOpts = Object.assign({}, opts, { url: url });
autocannon.fire(fireOpts, handler, version, true, callback);
}
]
},
function onPipelineFinished(err) {
forked.kill('SIGINT');
if (err) {
spinner.fail();
cb(err);
return;
}
spinner.text = 'Results saved for ' + version + '/' + handler;
spinner.succeed();
cb();
}
);
}
function start(opts, list, index) {
index = index || 0;
// No more item
if (list.length === index) {
return;
}
var handler = list[index];
console.log('---- ' + handler + ' ----');
pipeline(
{
funcs: [
function head(_, callback) {
runBenchmark(opts, handler, 'head', callback);
},
function stable(_, callback) {
if (!opts.compare) {
callback();
return;
}
runBenchmark(opts, handler, 'stable', callback);
}
]
},
function onPipelineFinished(err) {
if (err) {
console.log(err);
return;
}
// Compare versions
if (opts.compare) {
var result = autocannon.compare(handler);
console.log(handler + ' throughput:');
console.log(JSON.stringify(result.throughput, null, 4) + '\n');
}
// Benchmark next handler
start(opts, list, ++index);
}
);
}
module.exports = start;
================================================
FILE: benchmark/package.json
================================================
{
"name": "restify-benchmark",
"homepage": "http://restifyjs.com",
"description": "Restify benchmark",
"version": "0.0.0",
"private": true,
"main": "index.js",
"engines": {
"node": ">=0.10"
},
"dependencies": {
"restify": "latest"
},
"devDependencies": {},
"license": "MIT",
"scripts": {
"start": "node indec"
}
}
================================================
FILE: bin/report-latency
================================================
#!/usr/bin/env node
// -*- mode: js -*-
var fs = require('fs');
var path = require('path');
var spawn = require('child_process').spawn;
var readline = require('readline');
var sprintf = require('util').format;
var nopt = require('nopt');
///--- Globals
var BUCKETS = {};
var REQUEST_IDS = {};
var OPTS = {
'average': Boolean,
'help': Boolean,
'end': Date,
'max-latency': Number,
'max-requests': Number,
'output': String,
'percentile': [Number, Array],
'period': Number,
'requests': Boolean,
'start': Date
};
var SHORT_OPTS = {
'a': ['--average'],
'h': ['--help'],
'i': ['--period'],
'e': ['--end'],
'l': ['--max-latency'],
'n': ['--max-requests'],
'o': ['--output'],
'p': ['--percentile'],
'r': ['--requests'],
's': ['--start']
};
///--- Functions
function percentile(p, vals) {
p = parseInt(p, 10);
return vals[(Math.round(((p / 100) * vals.length) + 1 / 2) - 1)].latency;
}
function report(buckets, output) {
Object.keys(buckets).sort(function (a, b) {
return parseInt(a, 10) - parseInt(b, 10);
}).forEach(function (k) {
var avg = 0;
var perc = [];
var req = buckets[k].length;
var sum = 0;
var t = Math.round(buckets[k]._time);
buckets[k] = buckets[k].sort(function (a, b) {
return a.latency - b.latency;
});
buckets[k].forEach(function (v) {
sum += v.latency;
});
if (sum > 0 && req > 0) {
if (output.average)
output.average.push([t, Math.round(sum / req)]);
if (output.requests)
output.requests.push([t, buckets[k].length]);
Object.keys(output.percentile).forEach(function (p) {
var _p = percentile(p, buckets[k]);
output.percentile[p].push([t, _p]);
});
}
});
return output;
}
function usage(code, message) {
var str = '';
Object.keys(SHORT_OPTS).forEach(function (k) {
if (!Array.isArray(SHORT_OPTS[k]))
return;
var opt = SHORT_OPTS[k][0].replace('--', '');
var type = OPTS[opt].name || 'string';
if (type && type === 'boolean')
type = '';
type = type.toLowerCase();
str += ' [--' + opt + ' ' + type + ']';
});
str += ' [file ...]';
if (message)
console.error(message);
console.error('usage: ' + path.basename(process.argv[1]) + str);
process.exit(code);
}
///--- Mainline
var parsed;
try {
parsed = nopt(OPTS, SHORT_OPTS, process.argv, 2);
} catch (e) {
usage(1, e.toString());
}
if (parsed.help)
usage(0);
if (!parsed.average && !parsed.percentile)
usage(1, '--average or --percentile required');
if (parsed.argv.remain.length < 1)
usage(1, 'log file required');
var config = {
average: parsed.average || false,
maxLatency: parsed['max-latency'] || 1000,
maxRequests: parsed['max-requests'] || 10000,
percentile: parsed.percentile || [],
period: parsed.period || 60,
requests: parsed.requests || false,
start: parsed.start ? (parsed.start.getTime() / 1000) : 0,
end: parsed.end ? (parsed.end.getTime() / 1000) : Number.MAX_VALUE
};
var buckets = {};
var done = 0;
parsed.argv.remain.forEach(function (f) {
var stream = readline.createInterface({
input: fs.createReadStream(f),
output: null
})
stream.on('line', function (l) {
var record;
var t = -1;
try {
record = JSON.parse(l);
} catch (e) {
}
if (!record)
return;
var t = -1;
if (record.time)
t = (new Date(record.time).getTime() / 1000);
if (record._audit !== true ||
REQUEST_IDS[record.req_id] ||
t < config.start ||
t > config.end) {
console.error('Skipping %s', l);
}
REQUEST_IDS[record.req_id] = true;
record.time = t;
var b = Math.round(record.time / config.period) + '';
if (!buckets[b])
buckets[b] = [];
buckets[b].push(record);
buckets[b]._time = record.time // good enough
});
stream.on('end', function () {
if (++done === parsed.argv.remain.length) {
console.error('Generating report...');
var output = {
average: config.average ? [] : false,
requests: config.requests ? [] : false,
percentile: {}
};
config.percentile.forEach(function (p) {
output.percentile[p] = [];
});
output = report(buckets, output);
var finalOutput = [];
if (output.average) {
finalOutput.push({
name: 'avg',
values: output.average
});
}
if (output.requests) {
finalOutput.push({
name: 'n',
values: output.requests
});
}
Object.keys(output.percentile).forEach(function (k) {
finalOutput.push({
name: 'p' + k,
values: output.percentile[k]
});
});
console.log(JSON.stringify(finalOutput));
}
});
});
================================================
FILE: docs/_api/formatters.md
================================================
---
title: Formatters API
permalink: /docs/formatters-api/
---
### Table of Contents
- [Usage][1]
- [Types][2]
- [formatter][3]
- [Included formatters][4]
- [formatText][5]
- [formatJSON][6]
- [formatJSONP][7]
- [formatBinary][8]
## Usage
Restify comes bundled with a selection of useful formatters that prepare your
responses for being sent over the wire, but you are free to include your own!
```js
function formatGraphQL(req, res, body) {
var data = body;
/* Do a thing to data */
res.setHeader('Content-Length', Buffer.byteLength(data));
return data;
}
var server = restify.createServer({
formatters: {
'application/graphql': formatGraphQL
}
});
// Your application now supports content-type 'application/graphql'
```
## Types
### formatter
Format a response for being sent over the wire
Type: [Function][9]
**Parameters**
- `req` **[Object][10]** the request object (not used)
- `res` **[Object][10]** the response object
- `body` **[Object][10]** response body to format
Returns **[String][11]** formatted response data
## Included formatters
restify comes pre-loaded with a standard set of formatters for common
use cases.
### formatText
Formats the body to 'text' by invoking a toString() on the body if it
exists. If it doesn't, then the response is a zero-length string.
**Parameters**
- `req` **[Object][10]** the request object (not used)
- `res` **[Object][10]** the response object
- `body` **[Object][10]** response body. If it has a toString() method this
will be used to make the string representation
Returns **[String][11]** data
### formatJSON
JSON formatter. Will look for a toJson() method on the body. If one does not
exist then a JSON.stringify will be attempted.
**Parameters**
- `req` **[Object][10]** the request object (not used)
- `res` **[Object][10]** the response object
- `body` **[Object][10]** response body
Returns **[String][11]** data
### formatJSONP
JSONP formatter. like JSON, but with a callback invocation.
Unicode escapes line and paragraph separators.
**Parameters**
- `req` **[Object][10]** the request object
- `res` **[Object][10]** the response object
- `body` **[Object][10]** response body
Returns **[String][11]** data
### formatBinary
Binary formatter.
**Parameters**
- `req` **[Object][10]** the request object
- `res` **[Object][10]** the response object
- `body` **[Object][10]** response body
Returns **[Buffer][12]** body
[1]: #usage
[2]: #types
[3]: #formatter
[4]: #included-formatters
[5]: #formattext
[6]: #formatjson
[7]: #formatjsonp
[8]: #formatbinary
[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[11]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[12]: https://nodejs.org/api/buffer.html
================================================
FILE: docs/_api/plugins.md
================================================
---
title: Plugins API
permalink: /docs/plugins-api/
---
### Table of Contents
- [Usage][1]
- [server.pre() plugins][2]
- [context][3]
- [dedupeSlashes][4]
- [pause][5]
- [sanitizePath][6]
- [reqIdHeaders][7]
- [strictQueryParams][8]
- [userAgentConnection][9]
- [server.use() plugins][10]
- [acceptParser][11]
- [authorizationParser][12]
- [dateParser][13]
- [queryParser][14]
- [jsonp][15]
- [bodyParser][16]
- [requestLogger][17]
- [gzipResponse][18]
- [serveStatic][19]
- [serveStaticFiles][20]
- [throttle][21]
- [requestExpiry][22]
- [Using an external storage mechanism for key/bucket mappings.][23]
- [inflightRequestThrottle][24]
- [cpuUsageThrottle][25]
- [conditionalHandler][26]
- [conditionalRequest][27]
- [auditLogger][28]
- [metrics][29]
- [Types][30]
- [metrics~callback][31]
- [req.set][32]
- [req.get][33]
- [req.getAll][34]
## Usage
Restify comes bundled with a selection of useful plugins. These are accessible
off of `restify.plugins` and `restify.pre`.
```js
var server = restify.createServer();
server.use(restify.plugins.acceptParser(server.acceptable));
server.use(restify.plugins.authorizationParser());
server.use(restify.plugins.dateParser());
server.use(restify.plugins.queryParser());
server.use(restify.plugins.jsonp());
server.use(restify.plugins.gzipResponse());
server.use(restify.plugins.bodyParser());
server.use(restify.plugins.requestExpiry());
server.use(restify.plugins.throttle({
burst: 100,
rate: 50,
ip: true,
overrides: {
'192.168.1.1': {
rate: 0, // unlimited
burst: 0
}
}
}));
server.use(restify.plugins.conditionalRequest());
```
## server.pre() plugins
This module includes various pre plugins, which are intended to be used prior
to routing of the URL. To use a plugin before routing, use the `server.pre()`
method.
### context
This plugin creates `req.set(key, val)` and `req.get(key)` methods for
setting and retrieving request specific data.
**Examples**
```javascript
server.pre(restify.plugins.pre.context());
server.get('/', [
function(req, res, next) {
req.set(myMessage, 'hello world');
return next();
},
function two(req, res, next) {
res.send(req.get(myMessage)); // => sends 'hello world'
return next();
}
]);
```
Returns **[Function][35]** Handler
### dedupeSlashes
This plugin deduplicates extra slashes found in the URL. This can help with
malformed URLs that might otherwise get misrouted.
**Examples**
```javascript
server.pre(restify.plugins.pre.dedupeSlashes());
server.get('/hello/:one', function(req, res, next) {
res.send(200);
return next();
});
// the server will now convert requests to /hello//jake => /hello/jake
```
Returns **[Function][35]** Handler
### pause
This pre handler fixes issues with node hanging when an `asyncHandler` is
used prior to `bodyParser`.
[https://github.com/restify/node-restify/issues/287][36]
[https://github.com/restify/node-restify/issues/409][37]
[https://github.com/restify/node-restify/wiki/1.4-to-2.0-Migration-Tips][38]
Returns **[Function][35]** Handler
### sanitizePath
Cleans up sloppy URLs on the request object,
like `/foo////bar///` to `/foo/bar`.
Returns **[Function][35]** Handler
### reqIdHeaders
This plugin pulls the value from an incoming request header and uses it
as the value of the request id. Subsequent calls to `req.id()`
will return the header values.
**Parameters**
- `opts` **[Object][39]** an options object
- `opts.headers` **[Array][40]<[String][41]>** array of headers from where to pull existing
request id headers. Lookup precedence
is left to right (lowest index first)
Returns **[Function][35]** Handler
### strictQueryParams
Prevents `req.urls` non-strict key-value query params
The Request-URI is transmitted in the format specified in section 3.2.1.
If the Request-URI is encoded using the "% HEX HEX" encoding [42],
the origin server MUST decode the Request-URI
in order to properly interpret the request.
Servers SHOULD respond to invalid Request-URIs
with an appropriate status code.
part of Hypertext Transfer Protocol -- HTTP/1.1 | 5.1.2 Request-URI
RFC 2616 Fielding, et al.
**Parameters**
- `options` **[Object][39]?** an options object
- `options.message` **[String][41]?** a custom error message
default value:
"Url query params does not meet strict format"
Returns **[Function][35]** Handler
### userAgentConnection
This basically exists for `curl`. `curl` on `HEAD` requests usually
just sits there and hangs, unless you explicitly set
Connection:close. And in general, you probably want to set
Connection: close to curl anyway.
Also, because curl spits out an annoying message to stderr about
remaining bytes if content-length is set, this plugin also drops
the `content-length` header (some user agents handle it and want it,
curl does not).
To be slightly more generic, the options block takes a user
agent regexp, however.
**Parameters**
- `options` **[Object][39]?** an options object
- `options.userAgentRegExp` **[RegExp][42]** matching any
user-agents applicable (optional, default `/^curl.+/`)
Returns **[Function][35]** Handler
## server.use() plugins
### acceptParser
Parses the `Accept` header, and ensures that the server can respond to what
the client asked for. In almost all cases passing in `server.acceptable` is
all that's required, as that's an array of content types the server knows
how to respond to (with the formatters you've registered). If the request is
for a non-handled type, this plugin will return a `NotAcceptableError` (406).
Note you can get the set of types allowed from a restify server by doing
`server.acceptable`.
**Parameters**
- `accepts` **[Array][40]<[String][41]>** array of accept types.
**Examples**
```javascript
server.use(restify.plugins.acceptParser(server.acceptable));
```
- Throws **NotAcceptableError**
Returns **[Function][35]** restify handler.
### authorizationParser
Parses out the `Authorization` header as best restify can.
Currently only HTTP Basic Auth and
[HTTP Signature][43]
schemes are supported.
**Parameters**
- `options` **[Object][39]?** an optional options object that is
passed to http-signature
**Examples**
Subsequent handlers will see `req.authorization`, which looks like above.
`req.username` will also be set, and defaults to 'anonymous'. If the scheme
is unrecognized, the only thing available in `req.authorization` will be
`scheme` and `credentials` - it will be up to you to parse out the rest.
```javascript
{
scheme: "",
credentials: "",
basic: {
username: $user
password: $password
}
}
```
- Throws **InvalidArgumentError**
Returns **[Function][35]** Handler
### dateParser
Parses out the HTTP Date header (if present) and checks for clock skew.
If the header is invalid, a `InvalidHeaderError` (`400`) is returned.
If the clock skew exceeds the specified value,
a `RequestExpiredError` (`400`) is returned.
Where expired means the request originated at a time
before (`$now - $clockSkew`).
The default clockSkew allowance is 5m (thanks
Kerberos!)
**Parameters**
- `clockSkew` **[Number][44]** allowed clock skew in seconds. (optional, default `300`)
**Examples**
```javascript
// Allows clock skew of 1m
server.use(restify.plugins.dateParser(60));
```
- Throws **RequestExpiredError**
- Throws **InvalidHeaderError**
Returns **[Function][35]** restify handler.
### queryParser
Parses the HTTP query string (i.e., `/foo?id=bar&name=mark`).
If you use this, the parsed content will always be available in `req.query`,
additionally params are merged into `req.params`.
You can disable by passing in `mapParams: false` in the options object.
Many options correspond directly to option defined for the underlying
[`qs.parse`][45].
**Parameters**
- `options` **[Object][39]?** an options object
- `options.mapParams` **[Object][39]** disable passing (optional, default `true`)
- `options.mapParams` **[Boolean][46]** Copies parsed query parameters
into`req.params`. (optional, default `false`)
- `options.overrideParams` **[Boolean][46]** Only applies when if
mapParams true.
When true, will stomp on req.params field when existing value is found. (optional, default `false`)
- `options.allowDots` **[Boolean][46]** Transform `?foo.bar=baz` to a
nested object: `{foo: {bar: 'baz'}}`. (optional, default `false`)
- `options.arrayLimit` **[Number][44]** Only transform `?a[$index]=b`
to an array if `$index` is less than `arrayLimit`. (optional, default `20`)
- `options.depth` **[Number][44]** The depth limit for parsing
nested objects, e.g. `?a[b][c][d][e][f][g][h][i]=j`. (optional, default `5`)
- `options.parameterLimit` **[Number][44]** Maximum number of query
params parsed. Additional params are silently dropped. (optional, default `1000`)
- `options.parseArrays` **[Boolean][46]** Whether to parse
`?a[]=b&a[1]=c` to an array, e.g. `{a: ['b', 'c']}`. (optional, default `true`)
- `options.plainObjects` **[Boolean][46]** Whether `req.query` is a
"plain" object -- does not inherit from `Object`.
This can be used to allow query params whose names collide with Object
methods, e.g. `?hasOwnProperty=blah`. (optional, default `false`)
- `options.strictNullHandling` **[Boolean][46]** If true, `?a&b=`
results in `{a: null, b: ''}`. Otherwise, `{a: '', b: ''}`. (optional, default `false`)
**Examples**
```javascript
server.use(restify.plugins.queryParser({ mapParams: false }));
```
Returns **[Function][35]** Handler
### jsonp
Parses the jsonp callback out of the request.
Supports checking the query string for `callback` or `jsonp` and ensuring
that the content-type is appropriately set if JSONP params are in place.
There is also a default `application/javascript` formatter to handle this.
You _should_ set the `queryParser` plugin to run before this, but if you
don't this plugin will still parse the query string properly.
**Examples**
```javascript
var server = restify.createServer();
server.use(restify.plugins.jsonp());
```
Returns **[Function][35]** Handler
### bodyParser
Blocks your chain on reading and parsing the HTTP request body. Switches on
`Content-Type` and does the appropriate logic. `application/json`,
`application/x-www-form-urlencoded` and `multipart/form-data` are currently
supported.
Parses `POST` bodies to `req.body`. automatically uses one of the following
parsers based on content type:
- `urlEncodedBodyParser(options)` - parses url encoded form bodies
- `jsonBodyParser(options)` - parses JSON POST bodies
- `multipartBodyParser(options)` - parses multipart form bodies
All bodyParsers support the following options:
- `options.mapParams` - default false. copies parsed post body values onto
req.params
- `options.overrideParams` - default false. only applies when if
mapParams true. when true, will stomp on req.params value when
existing value is found.
**Parameters**
- `options` **[Object][39]?** an option object
- `options.maxBodySize` **[Number][44]?** The maximum size in bytes allowed in
the HTTP body. Useful for limiting clients from hogging server memory.
- `options.mapParams` **[Boolean][46]?** if `req.params` should be filled with
parsed parameters from HTTP body.
- `options.mapFiles` **[Boolean][46]?** if `req.params` should be filled with
the contents of files sent through a multipart request.
[formidable][47] is used internally
for parsing, and a file is denoted as a multipart part with the `filename`
option set in its `Content-Disposition`. This will only be performed if
`mapParams` is true.
- `options.overrideParams` **[Boolean][46]?** if an entry in `req.params`
should be overwritten by the value in the body if the names are the same.
For instance, if you have the route `/:someval`,
and someone posts an `x-www-form-urlencoded`
Content-Type with the body `someval=happy` to `/sad`, the value will be
`happy` if `overrideParams` is `true`, `sad` otherwise.
- `options.multipartHandler` **[Function][35]?** a callback to handle any
multipart part which is not a file.
If this is omitted, the default handler is invoked which may
or may not map the parts into `req.params`, depending on
the `mapParams`-option.
- `options.multipartFileHandler` **[Function][35]?** a callback to handle any
multipart file.
It will be a file if the part has a `Content-Disposition` with the
`filename` parameter set. This typically happens when a browser sends a
form and there is a parameter similar to ``.
If this is not provided, the default behaviour is to map the contents
into `req.params`.
- `options.keepExtensions` **[Boolean][46]?** if you want the uploaded
files to include the extensions of the original files
(multipart uploads only).
Does nothing if `multipartFileHandler` is defined.
- `options.uploadDir` **[String][41]?** Where uploaded files are
intermediately stored during transfer before the contents is mapped
into `req.params`.
Does nothing if `multipartFileHandler` is defined.
- `options.multiples` **[Boolean][46]?** if you want to support html5 multiple
attribute in upload fields.
- `options.hash` **[String][41]?** If you want checksums calculated for
incoming files, set this to either `sha1` or `md5`.
- `options.rejectUnknown` **[Boolean][46]?** Set to `true` if you want to end
the request with a `UnsupportedMediaTypeError` when none of
the supported content types was given.
- `options.requestBodyOnGet` **[Boolean][46]** Parse body of a GET
request. (optional, default `false`)
- `options.reviver` **[Function][35]?** `jsonParser` only. If a function,
this prescribes how the value originally produced by parsing is transformed,
before being returned. For more information check out
`JSON.parse(text[, reviver])`.
- `options.maxFieldsSize` **[Number][44]** `multipartParser`
only.
Limits the amount of memory all fields together (except files)
can allocate in bytes.
The default size is `2 * 1024 * 1024` bytes _(2MB)_. (optional, default `2*1024*1024`)
**Examples**
```javascript
server.use(restify.plugins.bodyParser({
maxBodySize: 0,
mapParams: true,
mapFiles: false,
overrideParams: false,
multipartHandler: function(part) {
part.on('data', function(data) {
// do something with the multipart data
});
},
multipartFileHandler: function(part) {
part.on('data', function(data) {
// do something with the multipart file data
});
},
keepExtensions: false,
uploadDir: os.tmpdir(),
multiples: true,
hash: 'sha1',
rejectUnknown: true,
requestBodyOnGet: false,
reviver: undefined,
maxFieldsSize: 2 * 1024 * 1024
}));
```
- Throws **UnsupportedMediaTypeError**
Returns **[Function][35]** Handler
### requestLogger
Sets up a child [bunyan][48] logger with
the current request id filled in, along with any other parameters you define.
You can pass in no options to this, in which case only the request id will be
appended, and no serializers appended (this is also the most performant); the
logger created at server creation time will be used as the parent logger.
This logger can be used normally, with [req.log][49].
This plugin does _not_ log each individual request. Use the Audit Logging
plugin or a custom middleware for that use.
**Parameters**
- `options` **[Object][39]?** an options object
- `options.headers` **[Array][40]?** A list of headers to transfer from
the request to top level props on the log.
**Examples**
```javascript
server.use(restify.plugins.requestLogger({
properties: {
foo: 'bar'
},
serializers: {...}
}));
```
Returns **[Function][35]** Handler
### gzipResponse
If the client sends an `accept-encoding: gzip` header (or one with an
appropriate q-val), then the server will automatically gzip all
response data.
Note that only `gzip` is supported, as this is most widely supported by
clients in the wild.
This plugin will overwrite some of the internal streams, so any
calls to `res.send`, `res.write`, etc., will be compressed. A side effect is
that the `content-length` header cannot be known, and so
`transfer-encoding: chunked` will _always_ be set when this is in effect.
This plugin has no impact if the client does not send
`accept-encoding: gzip`.
[https://github.com/restify/node-restify/issues/284][50]
**Parameters**
- `opts` **[Object][39]?** an options object, see: zlib.createGzip
**Examples**
```javascript
server.use(restify.plugins.gzipResponse());
```
Returns **[Function][35]** Handler
### serveStatic
Serves static files.
**Parameters**
- `options` **[Object][39]** an options object
**Examples**
The serveStatic module is different than most of the other plugins, in that
it is expected that you are going to map it to a route, as below:
```javascript
server.get('/docs/current/*', restify.plugins.serveStatic({
directory: './documentation/v1',
default: 'index.html'
}));
```
The above `route` and `directory` combination will serve a file located in
`./documentation/v1/docs/current/index.html` when you attempt to hit
`http://localhost:8080/docs/current/`. If you want the serveStatic module to
serve files directly from the `/documentation/v1` directory
(and not append the request path `/docs/current/`),
you can set the `appendRequestPath` option to `false`, and the served file
would be `./documentation/v1/index.html`, in the previous example.
The plugin will enforce that all files under `directory` are served.
The `directory` served is relative to the process working directory.
You can also provide a `default` parameter such as index.html for any
directory that lacks a direct file match.
You can specify additional restrictions by passing in a `match` parameter,
which is just a `RegExp` to check against the requested file name.
Additionally, you may set the `charSet` parameter, which will append a
character set to the content-type detected by the plugin.
For example, `charSet: 'utf-8'` will result in HTML being served with a
`Content-Type` of `text/html; charset=utf-8`.
Lastly, you can pass in a `maxAge` numeric, which will set the
`Cache-Control` header. Default is `3600` (1 hour).
An additional option for serving a static file is to pass `file` in to the
serveStatic method as an option. The following will serve index.html from
the documentation/v1/ directory anytime a client requests `/home/`.
```javascript
server.get('/home/*', restify.plugins.serveStatic({
directory: './documentation/v1',
file: 'index.html'
}));
// or
server.get('/home/([a-z]+[.]html)', restify.plugins.serveStatic({
directory: './documentation/v1',
file: 'index.html'
}));
```
- Throws **MethodNotAllowedError** \|
- Throws **NotAuthorizedError**
- Throws **ResourceNotFoundError**
Returns **[Function][35]** Handler
### serveStaticFiles
Serves static files, with API similar to expressjs
**Parameters**
- `directory` **[String][41]** the directory to serve files from
- `opts` **[Object][39]** an options object, which is optional
- `opts.maxAge` **[Number][44]** specify max age in millisecs (optional, default `0`)
- `opts.etag` **[Boolean][46]** enable/disable etag, default = true (optional, default `true`)
- `opts.setHeaders` **[Function][35]?** set custom headers for the Files
(synchronously), The function is called as `fn(res, path, stat)`,
where the arguments are:
`res` the response object
`path` the file path that is being sent
`stat` the stat object of the file that is being sent
**Examples**
The serveStaticFiles plugin allows you to map a GET route to a
directory on the disk
```javascript
server.get('/public/*', // don't forget the `/*`
restify.plugins.serveStaticFiles('./documentation/v1')
);
```
The GET `route` and `directory` combination will serve a file
located in `./documentation/v1/index.html` when you attempt to hit
`http://localhost:8080/public/index.html`
The plugin uses [send][51] under the hood
which is also used by `expressjs` to serve static files. Most of the options
that work with `send` will work with this plugin.
The default file the plugin looks for is `index.html`
```javascript
server.get('/public/*',
restify.plugins.serveStaticFiles('./documentation/v1', {
maxAge: 3600000, // this is in millisecs
etag: false,
setHeaders: function setCustomHeaders(response, requestedPath, stat) {
response.setHeader('restify-plugin-x', 'awesome');
}
})
);
```
- Throws **MethodNotAllowedError**
- Throws **NotAuthorizedError**
- Throws **ResourceNotFoundError**
Returns **[Function][35]** Handler
### throttle
Creates an API rate limiter that can be plugged into the standard
restify request handling pipeline.
`restify` ships with a fairly comprehensive implementation of
[Token bucket][52], with the ability
to throttle on IP (or x-forwarded-for) and username (from `req.username`).
You define "global" request rate and burst rate, and you can define
overrides for specific keys.
Note that you can always place this on per-URL routes to enable
different request rates to different resources (if for example, one route,
like `/my/slow/database` is much easier to overwhlem
than `/my/fast/memcache`).
If a client has consumed all of their available rate/burst, an HTTP response
code of `429`
[Too Many Requests][53]
is returned.
This throttle gives you three options on which to throttle:
username, IP address and 'X-Forwarded-For'. IP/XFF is a /32 match,
so keep that in mind if using it. Username takes the user specified
on req.username (which gets automagically set for supported Authorization
types; otherwise set it yourself with a filter that runs before this).
In both cases, you can set a `burst` and a `rate` (in requests/seconds),
as an integer/float. Those really translate to the `TokenBucket`
algorithm, so read up on that (or see the comments above...).
In either case, the top level options burst/rate set a blanket throttling
rate, and then you can pass in an `overrides` object with rates for
specific users/IPs. You should use overrides sparingly, as we make a new
TokenBucket to track each.
On the `options` object ip and username are treated as an XOR.
**Parameters**
- `options` **[Object][39]** required options with:
- `options.burst` **[Number][44]** burst
- `options.rate` **[Number][44]** rate
- `options.ip` **[Boolean][46]?** ip
- `options.username` **[Boolean][46]?** username
- `options.xff` **[Boolean][46]?** xff
- `options.setHeaders` **[Boolean][46]** Set response headers for rate,
limit (burst) and remaining. (optional, default `false`)
- `options.overrides` **[Object][39]?** overrides
- `options.tokensTable` **[Object][39]** a storage engine this plugin will
use to store throttling keys -> bucket mappings.
If you don't specify this, the default is to
use an in-memory O(1) LRU, with 10k distinct
keys. Any implementation just needs to support
put/get.
- `options.maxKeys` **[Number][44]** If using the default
implementation, you can specify how large you
want the table to be. (optional, default `10000`)
**Examples**
An example options object with overrides:
```javascript
{
burst: 10, // Max 10 concurrent requests (if tokens)
rate: 0.5, // Steady state: 1 request / 2 seconds
ip: true, // throttle per IP
overrides: {
'192.168.1.1': {
burst: 0,
rate: 0 // unlimited
}
}
```
- Throws **TooManyRequestsError**
Returns **[Function][35]** Handler
### requestExpiry
Request Expiry can be used to throttle requests that have already exceeded
their client timeouts. Requests can be sent with a configurable client
timeout header, e.g. 'x-request-expiry-time', which gives in absolute ms
since epoch, when this request will be timed out by the client.
This plugin will throttle all incoming requests via a 504 where
'x-request-expiry-time' less than Date.now() -- since these incoming requests
have already been timed out by the client. This prevents the server from
processing unnecessary requests.
Request expiry will use headers to tell if the incoming request has expired.
There are two options for this plugin:
1\. Absolute Time
_ Time in Milliseconds since Epoch when this request should be
considered expired
2\. Timeout
_ The request start time is supplied
_ A timeout, in milliseconds, is given
_ The timeout is added to the request start time to arrive at the
absolute time in which the request is considered expired
#### Using an external storage mechanism for key/bucket mappings.
By default, the restify throttling plugin uses an in-memory LRU to store
mappings between throttling keys (i.e., IP address) to the actual bucket that
key is consuming. If this suits you, you can tune the maximum number of keys
to store in memory with `options.maxKeys`; the default is 10000.
In some circumstances, you want to offload this into a shared system, such as
Redis, if you have a fleet of API servers and you're not getting steady
and/or uniform request distribution. To enable this, you can pass in
`options.tokensTable`, which is simply any Object that supports `put` and
`get` with a `String` key, and an `Object` value.
**Parameters**
- `opts` **[Object][39]** an options object
- `opts.absoluteHeader` **[String][41]?** The header key to be used for
the expiry time of each request.
- `opts.startHeader` **[String][41]** The header key for the start time
of the request.
- `opts.timeoutHeader` **[String][41]** The header key for the time in
milliseconds that should ellapse before
the request is considered expired.
**Examples**
The only option provided is `header` which is the request header used
to specify the client timeout.
```javascript
server.use(restify.plugins.requestExpiry({
header: 'x-request-expiry-time'
});
```
Returns **[Function][35]** Handler
### inflightRequestThrottle
The `inflightRequestThrottle` module allows you to specify an upper limit to
the maximum number of inflight requests your server is able to handle. This
is a simple heuristic for protecting against event loop contention between
requests causing unacceptable latencies.
The custom error is optional, and allows you to specify your own response
and status code when rejecting incoming requests due to too many inflight
requests. It defaults to `503 ServiceUnavailableError`.
This plugin should be registered as early as possibly in the middleware stack
using `pre` to avoid performing unnecessary work.
**Parameters**
- `opts` **[Object][39]** configure this plugin
- `opts.limit` **[Number][44]** maximum number of inflight requests the server
will handle before returning an error
- `opts.err` **[Error][54]** A restify error used as a response when the
inflight request limit is exceeded
- `opts.server` **[Function][35]** the instance of the restify server this
plugin will throttle.
**Examples**
```javascript
var errors = require('restify-errors');
var restify = require('restify');
var server = restify.createServer();
const options = { limit: 600, server: server };
options.res = new errors.InternalServerError();
server.pre(restify.plugins.inflightRequestThrottle(options));
```
Returns **[Function][35]** middleware to be registered on server.pre
### cpuUsageThrottle
cpuUsageThrottle is a middleware that rejects a variable number of requests
(between 0% and 100%) based on a historical view of CPU utilization of a
Node.js process. Essentially, this plugin allows you to define what
constitutes a saturated Node.js process via CPU utilization and it will
handle dropping a % of requests based on that definiton. This is useful when
you would like to keep CPU bound tasks from piling up causing an increased
per-request latency.
The algorithm asks you for a maximum CPU utilization rate, which it uses to
determine at what point it should be rejecting 100% of traffic. For a normal
Node.js service, this is 1 since Node is single threaded. It uses this,
paired with a limit that you provide to determine the total % of traffic it
should be rejecting. For example, if you specify a limit of .5 and a max of
1, and the current EWMA (next paragraph) value reads .75, this plugin will
reject approximately 50% of all requests.
When looking at the process' CPU usage, this algorithm will take a load
average over a user specified interval. example, if given an interval of
250ms, this plugin will attempt to record the average CPU utilization over
250ms intervals. Due to contention for resources, the duration of each
average may be wider or narrower than 250ms. To compensate for this, we use
an exponentially weighted moving average. The EWMA algorithm is provided by
the ewma module. The parameter for configuring the EWMA is halfLife. This
value controls how quickly each load average measurment decays to half it's
value when being represented in the current average. For example, if you
have an interval of 250, and a halfLife of 250, you will take the previous
ewma value multiplied by 0.5 and add it to the new CPU utilization average
measurement multiplied by 0.5. The previous value and the new measurement
would each represent 50% of the new value. A good way of thinking about the
halfLife is in terms of how responsive this plugin will be to spikes in CPU
utilization. The higher the halfLife, the longer CPU utilization will have
to remain above your defined limit before this plugin begins rejecting
requests and, converserly, the longer it will have to drop below your limit
before the plugin begins accepting requests again. This is a knob you will
want to with play when trying to determine the ideal value for your use
case.
For a better understanding of the EWMA algorithn, refer to the documentation
for the ewma module.
**Parameters**
- `opts` **[Object][39]** Configure this plugin.
- `opts.limit` **[Number][44]?** The point at which restify will begin
rejecting a % of all requests at the front door.
This value is a percentage.
For example 0.8 === 80% average CPU utilization. Defaults to 0.75.
- `opts.max` **[Number][44]?** The point at which restify will reject 100% of
all requests at the front door. This is used in conjunction with limit to
determine what % of traffic restify needs to reject when attempting to
bring the average load back to the user requested values. Since Node.js is
single threaded, the default for this is 1. In some rare cases, a Node.js
process can exceed 100% CPU usage and you will want to update this value.
- `opts.interval` **[Number][44]?** How frequently we calculate the average CPU
utilization. When we calculate an average CPU utilization, we calculate it
over this interval, and this drives whether or not we should be shedding
load. This can be thought of as a "resolution" where the lower this value,
the higher the resolution our load average will be and the more frequently
we will recalculate the % of traffic we should be shedding. This check
is rather lightweight, while the default is 250ms, you should be able to
decrease this value without seeing a significant impact to performance.
- `opts.halfLife` **[Number][44]?** When we sample the CPU usage on an
interval, we create a series of data points.
We take these points and calculate a
moving average. The halfLife indicates how quickly a point "decays" to
half it's value in the moving average. The lower the halfLife, the more
impact newer data points have on the average. If you want to be extremely
responsive to spikes in CPU usage, set this to a lower value. If you want
your process to put more emphasis on recent historical CPU usage when
determininng whether it should shed load, set this to a higher value. The
unit is in ms. Defaults to 250.
**Examples**
```javascript
var restify = require('restify');
var server = restify.createServer();
const options = {
limit: .75,
max: 1,
interval: 250,
halfLife: 500,
}
server.pre(restify.plugins.cpuUsageThrottle(options));
```
You can also update the plugin during runtime using the `.update()` function.
This function accepts the same `opts` object as a constructor.
```javascript
var plugin = restify.plugins.cpuUsageThrottle(options);
server.pre(plugin);
plugin.update({ limit: .4, halfLife: 5000 });
```
Returns **[Function][35]** middleware to be registered on server.pre
### conditionalHandler
Runs first handler that matches to the condition
**Parameters**
- `candidates` **([Object][39] \| [Array][40]<[Object][39]>)** candidates
- `candidates.handler` **([Function][35] \| [Array][40]<[Function][35]>)** handler(s)
- `candidates.version` **([String][41] \| [Array][40]<[String][41]>)?** '1.1.0', ['1.1.0', '1.2.0']
- `candidates.contentType` **[String][41]?** accepted content type, '\*\\/json'
**Examples**
```javascript
server.use(restify.plugins.conditionalHandler({
contentType: 'application/json',
version: '1.0.0',
handler: function (req, res, next) {
next();
})
});
server.get('/hello/:name', restify.plugins.conditionalHandler([
{
version: '1.0.0',
handler: function(req, res, next) { res.send('1.x'); }
},
{
version: ['1.5.0', '2.0.0'],
handler: function(req, res, next) { res.send('1.5.x, 2.x'); }
},
{
version: '3.0.0',
contentType: ['text/html', 'text/html']
handler: function(req, res, next) { res.send('3.x, text'); }
},
{
version: '3.0.0',
contentType: 'application/json'
handler: function(req, res, next) { res.send('3.x, json'); }
},
// Array of handlers
{
version: '4.0.0',
handler: [
function(req, res, next) { next(); },
function(req, res, next) { next(); },
function(req, res, next) { res.send('4.x') }
]
},
]);
// 'accept-version': '^1.1.0' => 1.5.x, 2.x'
// 'accept-version': '3.x', accept: 'application/json' => '3.x, json'
```
- Throws **InvalidVersionError**
- Throws **UnsupportedMediaTypeError**
Returns **[Function][35]** Handler
### conditionalRequest
Returns a set of plugins that will compare an already set `ETag` header with
the client's `If-Match` and `If-None-Match` header, and an already set
Last-Modified header with the client's `If-Modified-Since` and
`If-Unmodified-Since` header.
You can use this handler to let clients do nice HTTP semantics with the
"match" headers. Specifically, with this plugin in place, you would set
`res.etag=$yourhashhere`, and then this plugin will do one of:
- return `304` (Not Modified) [and stop the handler chain]
- return `412` (Precondition Failed) [and stop the handler chain]
- Allow the request to go through the handler chain.
The specific headers this plugin looks at are:
- `Last-Modified`
- `If-Match`
- `If-None-Match`
- `If-Modified-Since`
- `If-Unmodified-Since`
**Examples**
```javascript
server.use(restify.plugins.conditionalRequest());
```
```javascript
server.use(function setETag(req, res, next) {
res.header('ETag', 'myETag');
res.header('Last-Modified', new Date());
});
server.use(restify.plugins.conditionalRequest());
server.get('/hello/:name', function(req, res, next) {
res.send('hello ' + req.params.name);
});
```
- Throws **BadRequestError**
- Throws **PreconditionFailedError**
Returns **[Array][40]<[Function][35]>** Handlers
### auditLogger
**Parameters**
- `opts` **[Object][39]** The options object.
- `opts.log` **[Object][39]** The logger.
- `opts.event` **[String][41]** The event from the server which initiates the
log, one of 'pre', 'routed', or 'after'
- `opts.context` **[Function][35]?** The optional context function of signature
f(req, res, route, err). Invoked each time an audit log is generated. This
function can return an object that customizes the format of anything off the
req, res, route, and err objects. The output of this function will be
available on the `context` key in the audit object.
- `opts.server` **[Object][39]?** The restify server, used to emit
the audit log object programmatically
- `opts.printLog` **[boolean][46]** Whether to print the log
via the logger. (optional, default `true`)
- `opts.serializers` **[Object][39]?** Override the default logger serializers
for err, req and res
**Examples**
Audit logging is a special plugin, as you don't use it with `.use()`
but with the `after` event:
```javascript
server.on('after', restify.plugins.auditLogger({
log: bunyan.createLogger({
name: 'audit',
stream: process.stdout
}),
event: 'after',
server: SERVER,
logMetrics : logBuffer,
printLog : true
}));
```
You pass in the auditor a bunyan logger, optionally server object,
Ringbuffer and a flag printLog indicate if log needs to be print out at info
level or not. By default, without specify printLog flag, it will write out
record lookling like this:
```javascript
{
"name": "audit",
"hostname": "your.host.name",
"audit": true,
"remoteAddress": "127.0.0.1",
"remotePort": 57692,
"req_id": "ed634c3e-1af0-40e4-ad1e-68c2fb67c8e1",
"req": {
"method": "GET",
"url": "/foo",
"headers": {
"authorization": "Basic YWRtaW46am95cGFzczEyMw==",
"user-agent": "curl/7.19.7 (universal-apple-darwin10.0)
libcurl/7.19.7 OpenSSL/0.9.8r zlib/1.2.3",
"host": "localhost:8080",
"accept": "application/json"
},
"httpVersion": "1.1",
"query": {
"foo": "bar"
},
"trailers": {},
"version": "*",
"timers": {
"bunyan": 52,
"saveAction": 8,
"reqResTracker": 213,
"addContext": 8,
"addModels": 4,
"resNamespaces": 5,
"parseQueryString": 11,
"instanceHeaders": 20,
"xForwardedProto": 7,
"httpsRedirector": 14,
"readBody": 21,
"parseBody": 6,
"xframe": 7,
"restifyCookieParser": 15,
"fooHandler": 23,
"barHandler": 14,
"carHandler": 14
}
},
"res": {
"statusCode": 200,
"headers": {
"access-control-allow-origin": "*",
"access-control-allow-headers": "Accept, Accept-Version,
Content-Length, Content-MD5, Content-Type, Date, Api-Version",
"access-control-expose-headers": "Api-Version, Request-Id,
Response-Time",
"server": "Joyent SmartDataCenter 7.0.0",
"x-request-id": "ed634c3e-1af0-40e4-ad1e-68c2fb67c8e1",
"access-control-allow-methods": "GET",
"x-api-version": "1.0.0",
"connection": "close",
"content-length": 158,
"content-md5": "zkiRn2/k3saflPhxXI7aXA==",
"content-type": "application/json",
"date": "Tue, 07 Feb 2012 20:30:31 GMT",
"x-response-time": 1639
},
"trailer": false
},
"route": {
"name": "GetFoo",
"version": ["1.0.0"]
},
"secure": false,
"level": 30,
"msg": "GetFoo handled: 200",
"time": "2012-02-07T20:30:31.896Z",
"v": 0
}
```
The `timers` field shows the time each handler took to run in microseconds.
Restify by default will record this information for every handler for each
route. However, if you decide to include nested handlers, you can track the
timing yourself by utilizing the Request
[startHandlerTimer][55] and
[endHandlerTimer][56] API.
You can also listen to auditlog event and get same above log object when
log event emits. For example
```javascript
SERVER.on('auditlog', function (data) {
//do some process with log
});
```
Returns **[Function][35]** Handler
### metrics
The module includes the following plugins to be used with restify's `after`
event, e.g., `server.on('after', restify.plugins.metrics());`:
A plugin that listens to the server's after event and emits information
about that request.
**Parameters**
- `opts` **[Object][39]** an options obj
- `opts.server` **Server** restify server
- `callback` **createMetrics~callback** a callback fn
**Examples**
```javascript
server.on('after', restify.plugins.metrics({ server: server },
function (err, metrics, req, res, route) {
// metrics is an object containing information about the request
}));
```
Returns **[Function][35]** returns a function suitable to be used
with restify server's `after` event
## Types
### metrics~callback
Callback used by metrics plugin
Type: [Function][35]
**Parameters**
- `err` **[Error][54]**
- `metrics` **[Object][39]** metrics about the request
- `metrics.statusCode` **[Number][44]** status code of the response. can be
undefined in the case of an uncaughtException
- `metrics.method` **[String][41]** http request verb
- `metrics.totalLatency` **[Number][44]** latency includes both request is flushed
and all handlers finished
- `metrics.latency` **[Number][44]** latency when request is flushed
- `metrics.preLatency` **([Number][44] | null)** pre handlers latency
- `metrics.useLatency` **([Number][44] | null)** use handlers latency
- `metrics.routeLatency` **([Number][44] | null)** route handlers latency
- `metrics.path` **[String][41]** `req.path()` value
- `metrics.inflightRequests` **[Number][44]** Number of inflight requests pending
in restify.
- `metrics.unifinishedRequests` **[Number][44]** Same as `inflightRequests`
- `metrics.connectionState` **[String][41]** can be either `'close'` or
`undefined`. If this value is set, err will be a
corresponding `RequestCloseError`.
If connectionState is either
`'close'`, then the `statusCode` is not applicable since the
connection was severed before a response was written.
- `req` **[Request][57]** the request obj
- `res` **[Response][58]** the response obj
- `route` **Route** the route obj that serviced the request
## req.set
Set context value by key
Requires the context plugin.
**Parameters**
- `key` **[String][41]** key
- `value` **any** value
Returns **[undefined][59]** no return value
## req.get
Get context value by key.
Requires the context plugin.
**Parameters**
- `key` **[String][41]** key
Returns **any** value stored in context
## req.getAll
Get all context
Requires the context plugin.
Returns **any** value stored in context
[1]: #usage
[2]: #serverpre-plugins
[3]: #context
[4]: #dedupeslashes
[5]: #pause
[6]: #sanitizepath
[7]: #reqidheaders
[8]: #strictqueryparams
[9]: #useragentconnection
[10]: #serveruse-plugins
[11]: #acceptparser
[12]: #authorizationparser
[13]: #dateparser
[14]: #queryparser
[15]: #jsonp
[16]: #bodyparser
[17]: #requestlogger
[18]: #gzipresponse
[19]: #servestatic
[20]: #servestaticfiles
[21]: #throttle
[22]: #requestexpiry
[23]: #using-an-external-storage-mechanism-for-keybucket-mappings
[24]: #inflightrequestthrottle
[25]: #cpuusagethrottle
[26]: #conditionalhandler
[27]: #conditionalrequest
[28]: #auditlogger
[29]: #metrics
[30]: #types
[31]: #metricscallback
[32]: #reqset
[33]: #reqget
[34]: #reqgetall
[35]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
[36]: https://github.com/restify/node-restify/issues/287
[37]: https://github.com/restify/node-restify/issues/409
[38]: https://github.com/restify/node-restify/wiki/1.4-to-2.0-Migration-Tips
[39]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[40]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[41]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[42]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp
[43]: https://github.com/joyent/node-http-signature
[44]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[45]: https://github.com/ljharb/qs
[46]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[47]: https://github.com/felixge/node-formidable
[48]: https://github.com/trentm/node-bunyan
[49]: #request-api
[50]: https://github.com/restify/node-restify/issues/284
[51]: https://github.com/pillarjs/send
[52]: http://en.wikipedia.org/wiki/Token_bucket
[53]: http://tools.ietf.org/html/draft-nottingham-http-new-status-03#section-4
[54]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error
[55]: #starthandlertimerhandlername
[56]: #endhandlertimerhandlername
[57]: https://developer.mozilla.org/Add-ons/SDK/High-Level_APIs/request
[58]: https://developer.mozilla.org/docs/Web/Guide/HTML/HTML5
[59]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined
================================================
FILE: docs/_api/request.md
================================================
---
title: Request API
permalink: /docs/request-api/
---
### Table of Contents
- [Request][1]
- [accepts][2]
- [acceptsEncoding][3]
- [contentLength][4]
- [getContentType][5]
- [date][6]
- [href][7]
- [id][8]
- [getPath][9]
- [getQuery][10]
- [time][11]
- [version][12]
- [header][13]
- [trailer][14]
- [is][15]
- [isChunked][16]
- [isKeepAlive][17]
- [isSecure][18]
- [isUpgradeRequest][19]
- [isUpload][20]
- [toString][21]
- [userAgent][22]
- [startHandlerTimer][23]
- [endHandlerTimer][24]
- [connectionState][25]
- [getRoute][26]
- [Events][27]
- [Log][28]
## Request
**Extends http.IncomingMessage**
Wraps all of the node
[http.IncomingMessage][29]
APIs, events and properties, plus the following.
### accepts
Check if the Accept header is present, and includes the given type.
When the Accept header is not present true is returned.
Otherwise the given type is matched by an exact match, and then subtypes.
**Parameters**
- `types` **([String][30] \| [Array][31]<[String][30]>)** an array of accept type headers
**Examples**
You may pass the subtype such as html which is then converted internally
to text/html using the mime lookup table:
```javascript
// Accept: text/html
req.accepts('html');
// => true
// Accept: text/*; application/json
req.accepts('html');
req.accepts('text/html');
req.accepts('text/plain');
req.accepts('application/json');
// => true
req.accepts('image/png');
req.accepts('png');
// => false
```
Returns **[Boolean][32]** is accepteed
### acceptsEncoding
Checks if the request accepts the encoding type(s) specified.
**Parameters**
- `types` **([String][30] \| [Array][31]<[String][30]>)** an array of accept type headers
Returns **[Boolean][32]** is accepted encoding
### contentLength
Returns the value of the content-length header.
Returns **[Number][33]**
### getContentType
Returns the value of the content-type header. If a content-type is not
set, this will return a default value of `application/octet-stream`
Returns **[String][30]** content type
### date
Returns a Date object representing when the request was setup.
Like `time()`, but returns a Date object.
Returns **[Date][34]** date when request began being processed
### href
Returns the full requested URL.
**Examples**
```javascript
// incoming request is http://localhost:3000/foo/bar?a=1
server.get('/:x/bar', function(req, res, next) {
console.warn(req.href());
// => /foo/bar/?a=1
});
```
Returns **[String][30]**
### id
Returns the request id. If a `reqId` value is passed in,
this will become the request’s new id. The request id is immutable,
and can only be set once. Attempting to set the request id more than
once will cause restify to throw.
**Parameters**
- `reqId` **[String][30]** request id
Returns **[String][30]** id
### getPath
Returns the cleaned up requested URL.
**Examples**
```javascript
// incoming request is http://localhost:3000/foo/bar?a=1
server.get('/:x/bar', function(req, res, next) {
console.warn(req.path());
// => /foo/bar
});
```
Returns **[String][30]**
### getQuery
Returns the raw query string. Returns empty string
if no query string is found.
**Examples**
```javascript
// incoming request is /foo?a=1
req.getQuery();
// => 'a=1'
```
If the queryParser plugin is used, the parsed query string is
available under the req.query:
```javascript
// incoming request is /foo?a=1
server.use(restify.plugins.queryParser());
req.query;
// => { a: 1 }
```
Returns **[String][30]** query
### time
The number of ms since epoch of when this request began being processed.
Like date(), but returns a number.
Returns **[Number][33]** time when request began being processed in epoch:
ellapsed milliseconds since
January 1, 1970, 00:00:00 UTC
### version
Returns the accept-version header.
Returns **[String][30]**
### header
Get the case-insensitive request header key,
and optionally provide a default value (express-compliant).
Returns any header off the request. also, 'correct' any
correctly spelled 'referrer' header to the actual spelling used.
**Parameters**
- `key` **[String][30]** the key of the header
- `defaultValue` **[String][30]?** default value if header isn't
found on the req
**Examples**
```javascript
req.header('Host');
req.header('HOST');
req.header('Accept', '*\/*');
```
Returns **[String][30]** header value
### trailer
Returns any trailer header off the request. Also, 'correct' any
correctly spelled 'referrer' header to the actual spelling used.
**Parameters**
- `name` **[String][30]** the name of the header
- `value` **[String][30]** default value if header isn't found on the req
Returns **[String][30]** trailer value
### is
Check if the incoming request contains the `Content-Type` header field,
and if it contains the given mime type.
**Parameters**
- `type` **[String][30]** a content-type header value
**Examples**
```javascript
// With Content-Type: text/html; charset=utf-8
req.is('html');
req.is('text/html');
// => true
// When Content-Type is application/json
req.is('json');
req.is('application/json');
// => true
req.is('html');
// => false
```
Returns **[Boolean][32]** is content-type header
### isChunked
Check if the incoming request is chunked.
Returns **[Boolean][32]** is chunked
### isKeepAlive
Check if the incoming request is kept alive.
Returns **[Boolean][32]** is keep alive
### isSecure
Check if the incoming request is encrypted.
Returns **[Boolean][32]** is secure
### isUpgradeRequest
Check if the incoming request has been upgraded.
Returns **[Boolean][32]** is upgraded
### isUpload
Check if the incoming request is an upload verb.
Returns **[Boolean][32]** is upload
### toString
toString serialization
Returns **[String][30]** serialized request
### userAgent
Returns the user-agent header.
Returns **[String][30]** user agent
### startHandlerTimer
Start the timer for a request handler.
By default, restify uses calls this automatically for all handlers
registered in your handler chain.
However, this can be called manually for nested functions inside the
handler chain to record timing information.
**Parameters**
- `handlerName` **[String][30]** The name of the handler.
**Examples**
You must explicitly invoke
endHandlerTimer() after invoking this function. Otherwise timing
information will be inaccurate.
```javascript
server.get('/', function fooHandler(req, res, next) {
vasync.pipeline({
funcs: [
function nestedHandler1(req, res, next) {
req.startHandlerTimer('nestedHandler1');
// do something
req.endHandlerTimer('nestedHandler1');
return next();
},
function nestedHandler1(req, res, next) {
req.startHandlerTimer('nestedHandler2');
// do something
req.endHandlerTimer('nestedHandler2');
return next();
}...
]...
}, next);
});
```
Returns **[undefined][35]** no return value
### endHandlerTimer
End the timer for a request handler.
You must invoke this function if you called `startRequestHandler` on a
handler. Otherwise the time recorded will be incorrect.
**Parameters**
- `handlerName` **[String][30]** The name of the handler.
Returns **[undefined][35]** no return value
### connectionState
Returns the connection state of the request. Current possible values are:
- `close` - when the request has been closed by the clien
Returns **[String][30]** connection state (`"close"`)
### getRoute
Returns the route object to which the current request was matched to.
**Examples**
Route info object structure:
```javascript
{
path: '/ping/:name',
method: 'GET',
versions: [],
name: 'getpingname'
}
```
Returns **[Object][36]** route
## Events
In additional to emitting all the events from node's
[http.Server][37],
restify servers also emit a number of additional events that make building REST
and web applications much easier.
### restifyError
This event is emitted following all error events as a generic catch all. It is
recommended to use specific error events to handle specific errors, but this
event can be useful for metrics or logging. If you use this in conjunction with
other error events, the most specific event will be fired first, followed by
this one:
```js
server.get('/', function(req, res, next) {
return next(new InternalServerError('boom'));
});
server.on('InternalServer', function(req, res, err, callback) {
// this will get fired first, as it's the most relevant listener
return callback();
});
server.on('restifyError', function(req, res, err, callback) {
// this is fired second.
return callback();
});
```
### after
After each request has been fully serviced, an `after` event is fired. This
event can be hooked into to handle audit logs and other metrics. Note that
flushing a response does not necessarily correspond with an `after` event.
restify considers a request to be fully serviced when either:
1) The handler chain for a route has been fully completed
2) An error was returned to `next()`, and the corresponding error events have
been fired for that error type
The signature is for the after event is as follows:
```js
function(req, res, route, error) { }
```
- `req` - the request object
- `res` - the response object
- `route` - the route object that serviced the request
- `error` - the error passed to `next()`, if applicable
Note that when the server automatically responds with a
NotFound/MethodNotAllowed/VersionNotAllowed, this event will still be fired.
### pre
Before each request has been routed, a `pre` event is fired. This event can be
hooked into handle audit logs and other metrics. Since this event fires
_before_ routing has occured, it will fire regardless of whether the route is
supported or not, e.g. requests that result in a `404`.
The signature for the `pre` event is as follows:
```js
function(req, res) {}
```
- `req` - the request object
- `res` - the response object
Note that when the server automatically responds with a
NotFound/MethodNotAllowed/VersionNotAllowed, this event will still be fired.
### routed
A `routed` event is fired after a request has been routed by the router, but
before handlers specific to that route has run.
The signature for the `routed` event is as follows:
```js
function(req, res, route) {}
```
- `req` - the request object
- `res` - the response object
- `route` - the route object that serviced the request
Note that this event will _not_ fire if a requests comes in that are not
routable, i.e. one that would result in a `404`.
### uncaughtException
If the restify server was created with `handleUncaughtExceptions: true`,
restify will leverage [domains][38] to handle
thrown errors in the handler chain. Thrown errors are a result of an explicit
`throw` statement, or as a result of programmer errors like a typo or a null
ref. These thrown errors are caught by the domain, and will be emitted via this
event. For example:
```js
server.get('/', function(req, res, next) {
res.send(x); // this will cause a ReferenceError
return next();
});
server.on('uncaughtException', function(req, res, route, err) {
// this event will be fired, with the error object from above:
// ReferenceError: x is not defined
});
```
If you listen to this event, you **must** send a response to the client. This
behavior is different from the standard error events. If you do not listen to
this event, restify's default behavior is to call `res.send()` with the error
that was thrown.
The signature is for the after event is as follows:
```js
function(req, res, route, error) { }
```
- `req` - the request object
- `res` - the response object
- `route` - the route object that serviced the request
- `error` - the error passed to `next()`, if applicable
### close
Emitted when the server closes.
## Log
If you are using the [RequestLogger][39] plugin, the child logger
will be available on `req.log`:
```js
function myHandler(req, res, next) {
var log = req.log;
log.debug({params: req.params}, 'Hello there %s', 'foo');
}
```
The child logger will inject the request's UUID in the `req._id` attribute of
each log statement. Since the logger lasts for the life of the request, you can
use this to correlate statements for an individual request across any number of
separate handlers.
[1]: #request
[2]: #accepts
[3]: #acceptsencoding
[4]: #contentlength
[5]: #getcontenttype
[6]: #date
[7]: #href
[8]: #id
[9]: #getpath
[10]: #getquery
[11]: #time
[12]: #version
[13]: #header
[14]: #trailer
[15]: #is
[16]: #ischunked
[17]: #iskeepalive
[18]: #issecure
[19]: #isupgraderequest
[20]: #isupload
[21]: #tostring
[22]: #useragent
[23]: #starthandlertimer
[24]: #endhandlertimer
[25]: #connectionstate
[26]: #getroute
[27]: #events
[28]: #log
[29]: https://nodejs.org/api/http.html
[30]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[31]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[32]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[33]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[34]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Date
[35]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined
[36]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[37]: http://nodejs.org/docs/latest/api/http.html#http_class_http_server
[38]: https://nodejs.org/api/domain.html
[39]: #bundled-plugins
================================================
FILE: docs/_api/response.md
================================================
---
title: Response API
permalink: /docs/response-api/
---
### Table of Contents
- [Response][1]
- [cache][2]
- [noCache][3]
- [charSet][4]
- [header][5]
- [json][6]
- [link][7]
- [send][8]
- [sendRaw][9]
- [set][10]
- [status][11]
- [redirect][12]
- [redirect][13]
- [redirect][14]
## Response
**Extends http.ServerResponse**
Wraps all of the node
[http.ServerResponse][15]
APIs, events and properties, plus the following.
### cache
Sets the `cache-control` header.
**Parameters**
- `type` **[String][16]** value of the header
(`"public"` or `"private"`) (optional, default `"public"`)
- `options` **[Object][17]?** an options object
- `options.maxAge` **[Number][18]** max-age in seconds
Returns **[String][16]** the value set to the header
### noCache
Turns off all cache related headers.
Returns **[Response][19]** self, the response object
### charSet
Appends the provided character set to the response's `Content-Type`.
**Parameters**
- `type` **[String][16]** char-set value
**Examples**
```javascript
res.charSet('utf-8');
```
Returns **[Response][19]** self, the response object
### header
Sets headers on the response.
**Parameters**
- `key` **[String][16]** the name of the header
- `value` **[String][16]** the value of the header
**Examples**
If only key is specified, return the value of the header.
If both key and value are specified, set the response header.
```javascript
res.header('Content-Length');
// => undefined
res.header('Content-Length', 123);
// => 123
res.header('Content-Length');
// => 123
res.header('foo', new Date());
// => Fri, 03 Feb 2012 20:09:58 GMT
```
`header()` can also be used to automatically chain header values
when applicable:
```javascript
res.header('x-foo', 'a');
res.header('x-foo', 'b');
// => { 'x-foo': ['a', 'b'] }
```
Returns **[Object][17]** the retrieved value or the value that was set
### json
Syntatic sugar for:
```js
res.contentType = 'json';
res.send({hello: 'world'});
```
**Parameters**
- `code` **[Number][18]?** http status code
- `body` **[Object][17]?** value to json.stringify
- `headers` **[Object][17]?** headers to set on the response
**Examples**
```javascript
res.header('content-type', 'json');
res.send({hello: 'world'});
```
Returns **[Object][17]** the response object
### link
Sets the link header.
**Parameters**
- `key` **[String][16]** the link key
- `value` **[String][16]** the link value
Returns **[String][16]** the header value set to res
### send
Sends the response object. pass through to internal `__send` that uses a
formatter based on the `content-type` header.
**Parameters**
- `code` **[Number][18]?** http status code
- `body` **([Object][17] \| [Buffer][20] \| [Error][21])?** the content to send
- `headers` **[Object][17]?** any add'l headers to set
**Examples**
You can use send() to wrap up all the usual writeHead(), write(), end()
calls on the HTTP API of node.
You can pass send either a `code` and `body`, or just a body. body can be
an `Object`, a `Buffer`, or an `Error`.
When you call `send()`, restify figures out how to format the response
based on the `content-type`.
```javascript
res.send({hello: 'world'});
res.send(201, {hello: 'world'});
res.send(new BadRequestError('meh'));
```
Returns **[Object][17]** the response object
### sendRaw
Like `res.send()`, but skips formatting. This can be useful when the
payload has already been preformatted.
Sends the response object. pass through to internal `__send` that skips
formatters entirely and sends the content as is.
**Parameters**
- `code` **[Number][18]?** http status code
- `body` **([Object][17] \| [Buffer][20] \| [Error][21])?** the content to send
- `headers` **[Object][17]?** any add'l headers to set
Returns **[Object][17]** the response object
### set
Sets multiple header(s) on the response.
Uses `header()` underneath the hood, enabling multi-value headers.
**Parameters**
- `name` **([String][16] \| [Object][17])** name of the header or
`Object` of headers
- `val` **[String][16]** value of the header
**Examples**
```javascript
res.header('x-foo', 'a');
res.set({
'x-foo', 'b',
'content-type': 'application/json'
});
// =>
// {
// 'x-foo': [ 'a', 'b' ],
// 'content-type': 'application/json'
// }
```
Returns **[Object][17]** self, the response object
### status
Sets the http status code on the response.
**Parameters**
- `code` **[Number][18]** http status code
**Examples**
```javascript
res.status(201);
```
Returns **[Number][18]** the status code passed in
### redirect
Redirect is sugar method for redirecting.
**Parameters**
- `options` **[Object][17]** url or an options object to configure a redirect
- `options.secure` **[Boolean][22]?** whether to redirect to http or https
- `options.hostname` **[String][16]?** redirect location's hostname
- `options.pathname` **[String][16]?** redirect location's pathname
- `options.port` **[String][16]?** redirect location's port number
- `options.query` **[String][16]?** redirect location's query string
parameters
- `options.overrideQuery` **[Boolean][22]?** if true, `options.query`
stomps over any existing query
parameters on current URL.
by default, will merge the two.
- `options.permanent` **[Boolean][22]?** if true, sets 301. defaults to 302.
- `next` **[Function][23]** mandatory, to complete the response and trigger
audit logger.
**Examples**
```javascript
res.redirect({...}, next);
```
A convenience method for 301/302 redirects. Using this method will tell
restify to stop execution of your handler chain.
You can also use an options object. `next` is required.
```javascript
res.redirect({
hostname: 'www.foo.com',
pathname: '/bar',
port: 80, // defaults to 80
secure: true, // sets https
permanent: true,
query: {
a: 1
}
}, next); // => redirects to 301 https://www.foo.com/bar?a=1
```
Returns **[undefined][24]**
### redirect
Redirect with code and url.
**Parameters**
- `code` **[Number][18]** http redirect status code
- `url` **[String][16]** redirect url
- `next` **[Function][23]** mandatory, to complete the response and trigger
audit logger.
**Examples**
```javascript
res.redirect(301, 'www.foo.com', next);
```
Returns **[undefined][24]**
### redirect
Redirect with url.
**Parameters**
- `url` **[String][16]** redirect url
- `next` **[Function][23]** mandatory, to complete the response and trigger
audit logger.
**Examples**
```javascript
res.redirect('www.foo.com', next);
res.redirect('/foo', next);
```
Returns **[undefined][24]**
[1]: #response
[2]: #cache
[3]: #nocache
[4]: #charset
[5]: #header
[6]: #json
[7]: #link
[8]: #send
[9]: #sendraw
[10]: #set
[11]: #status
[12]: #redirect
[13]: #redirect-1
[14]: #redirect-2
[15]: https://nodejs.org/docs/latest/api/http.html
[16]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[17]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[18]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[19]: #response
[20]: https://nodejs.org/api/buffer.html
[21]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error
[22]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[23]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
[24]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined
================================================
FILE: docs/_api/server.md
================================================
---
title: Server API
permalink: /docs/server-api/
---
### Table of Contents
- [createServer][1]
- [Server][2]
- [listen][3]
- [close][4]
- [get][5]
- [head][6]
- [post][7]
- [put][8]
- [patch][9]
- [del][10]
- [opts][11]
- [pre][12]
- [use][13]
- [param][14]
- [rm][15]
- [address][16]
- [inflightRequests][17]
- [debugInfo][18]
- [toString][19]
- [Events][20]
- [Errors][21]
- [Types][22]
- [Server~methodOpts][23]
## createServer
A restify server object is the main interface through which you will register
routes and handlers for incoming requests.
**Parameters**
- `options` **[Object][24]?** an options object
- `options.name` **[String][25]** Name of the server. (optional, default `"restify"`)
- `options.dtrace` **[Boolean][26]** enable DTrace support (optional, default `false`)
- `options.router` **Router** Router (optional, default `newRouter(opts)`)
- `options.log` **[Object][24]** [bunyan][27] instance. (optional, default `bunyan.createLogger(options.name||"restify")`)
- `options.url` **[String][25]?** Once listen() is called, this will be filled
in with where the server is running.
- `options.certificate` **([String][25] \| [Buffer][28])?** If you want to create an HTTPS
server, pass in a PEM-encoded certificate and key.
- `options.key` **([String][25] \| [Buffer][28])?** If you want to create an HTTPS server,
pass in a PEM-encoded certificate and key.
- `options.formatters` **[Object][24]?** Custom response formatters for
`res.send()`.
- `options.handleUncaughtExceptions` **[Boolean][26]** When true restify
will use a domain to catch and respond to any uncaught
exceptions that occur in its handler stack.
Comes with significant negative performance impact. (optional, default `false`)
- `options.spdy` **[Object][24]?** Any options accepted by
[node-spdy][29].
- `options.http2` **[Object][24]?** Any options accepted by
[http2.createSecureServer][30].
- `options.handleUpgrades` **[Boolean][26]** Hook the `upgrade` event
from the node HTTP server, pushing `Connection: Upgrade` requests through the
regular request handling chain. (optional, default `false`)
- `options.onceNext` **[Boolean][26]** Prevents calling next multiple
times (optional, default `false`)
- `options.strictNext` **[Boolean][26]** Throws error when next() is
called more than once, enabled onceNext option (optional, default `false`)
- `options.httpsServerOptions` **[Object][24]?** Any options accepted by
[node-https Server][31].
If provided the following restify server options will be ignored:
spdy, ca, certificate, key, passphrase, rejectUnauthorized, requestCert and
ciphers; however these can all be specified on httpsServerOptions.
- `options.noWriteContinue` **[Boolean][26]** prevents
`res.writeContinue()` in `server.on('checkContinue')` when proxing (optional, default `false`)
- `options.ignoreTrailingSlash` **[Boolean][26]** ignore trailing slash
on paths (optional, default `false`)
- `options.strictFormatters` **[Boolean][26]** enables strict formatters
behavior: a formatter matching the response's content-type is required. If
not found, the response's content-type is automatically set to
'application/octet-stream'. If a formatter for that content-type is not
found, sending the response errors. (optional, default `true`)
**Examples**
```javascript
var restify = require('restify');
var server = restify.createServer();
server.listen(8080, function () {
console.log('ready on %s', server.url);
});
```
Returns **[Server][32]** server
## Server
Creates a new Server.
**Parameters**
- `options` **[Object][24]** an options object
- `options.name` **[String][25]** Name of the server.
- `options.dtrace` **[Boolean][26]** enable DTrace support (optional, default `false`)
- `options.router` **Router** Router
- `options.log` **[Object][24]** [bunyan][27]
instance.
- `options.url` **[String][25]?** Once listen() is called, this will be filled
in with where the server is running.
- `options.certificate` **([String][25] \| [Buffer][28])?** If you want to create an HTTPS
server, pass in a PEM-encoded certificate and key.
- `options.key` **([String][25] \| [Buffer][28])?** If you want to create an HTTPS server,
pass in a PEM-encoded certificate and key.
- `options.formatters` **[Object][24]?** Custom response formatters for
`res.send()`.
- `options.handleUncaughtExceptions` **[Boolean][26]** When true restify
will use a domain to catch and respond to any uncaught
exceptions that occur in its handler stack.
Comes with significant negative performance impact.
- `options.spdy` **[Object][24]?** Any options accepted by
[node-spdy][29].
- `options.http2` **[Object][24]?** Any options accepted by
[http2.createSecureServer][30].
- `options.handleUpgrades` **[Boolean][26]** Hook the `upgrade` event
from the node HTTP server, pushing `Connection: Upgrade` requests through the
regular request handling chain. (optional, default `false`)
- `options.onceNext` **[Boolean][26]** Prevents calling next multiple
times (optional, default `false`)
- `options.strictNext` **[Boolean][26]** Throws error when next() is
called more than once, enabled onceNext option (optional, default `false`)
- `options.httpsServerOptions` **[Object][24]?** Any options accepted by
[node-https Server][31].
If provided the following restify server options will be ignored:
spdy, ca, certificate, key, passphrase, rejectUnauthorized, requestCert and
ciphers; however these can all be specified on httpsServerOptions.
- `options.noWriteContinue` **[Boolean][26]** prevents
`res.writeContinue()` in `server.on('checkContinue')` when proxing (optional, default `false`)
- `options.ignoreTrailingSlash` **[Boolean][26]** ignore trailing slash
on paths (optional, default `false`)
- `options.strictFormatters` **[Boolean][26]** enables strict formatters
behavior: a formatter matching the response's content-type is required. If
not found, the response's content-type is automatically set to
'application/octet-stream'. If a formatter for that content-type is not
found, sending the response errors. (optional, default `true`)
**Examples**
```javascript
var restify = require('restify');
var server = restify.createServer();
server.listen(8080, function () {
console.log('ready on %s', server.url);
});
```
### listen
Gets the server up and listening.
Wraps node's
[listen()][33].
**Parameters**
- `port` **[Number][34]** Port
- `host` **[Number][34]?** Host
- `callback` **[Function][35]?** optionally get notified when listening.
**Examples**
You can call like:
```javascript
server.listen(80)
server.listen(80, '127.0.0.1')
server.listen('/tmp/server.sock')
```
- Throws **[TypeError][36]**
Returns **[undefined][37]** no return value
### close
Shuts down this server, and invokes callback (optionally) when done.
Wraps node's
[close()][38].
**Parameters**
- `callback` **[Function][35]?** callback to invoke when done
Returns **[undefined][37]** no return value
### get
Mounts a chain on the given path against this HTTP verb
**Parameters**
- `opts` **[Server~methodOpts][39]** if string, the URL to handle.
if options, the URL to handle, at minimum.
**Examples**
```javascript
server.get('/', function (req, res, next) {
res.send({ hello: 'world' });
next();
});
```
Returns **Route** the newly created route.
### head
Mounts a chain on the given path against this HTTP verb
**Parameters**
- `opts` **[Server~methodOpts][39]** if string, the URL to handle.
if options, the URL to handle, at minimum.
Returns **Route** the newly created route.
### post
Mounts a chain on the given path against this HTTP verb
**Parameters**
- `post` **[Server~methodOpts][39]** if string, the URL to handle.
if options, the URL to handle, at minimum.
Returns **Route** the newly created route.
### put
Mounts a chain on the given path against this HTTP verb
**Parameters**
- `put` **[Server~methodOpts][39]** if string, the URL to handle.
if options, the URL to handle, at minimum.
Returns **Route** the newly created route.
### patch
Mounts a chain on the given path against this HTTP verb
**Parameters**
- `patch` **[Server~methodOpts][39]** if string, the URL to handle.
if options, the URL to handle, at minimum.
Returns **Route** the newly created route.
### del
Mounts a chain on the given path against this HTTP verb
**Parameters**
- `opts` **[Server~methodOpts][39]** if string, the URL to handle.
if options, the URL to handle, at minimum.
Returns **Route** the newly created route.
### opts
Mounts a chain on the given path against this HTTP verb
**Parameters**
- `opts` **[Server~methodOpts][39]** if string, the URL to handle.
if options, the URL to handle, at minimum.
Returns **Route** the newly created route.
### pre
Gives you hooks to run _before_ any routes are located. This gives you
a chance to intercept the request and change headers, etc., that routing
depends on. Note that req.params will _not_ be set yet.
**Parameters**
- `handler` **...([Function][35] \| [Array][40])** Allows you to add handlers that
run for all routes. _before_ routing occurs.
This gives you a hook to change request headers and the like if you need to.
Note that `req.params` will be undefined, as that's filled in _after_
routing.
Takes a function, or an array of functions.
variable number of nested arrays of handler functions
**Examples**
```javascript
server.pre(function(req, res, next) {
req.headers.accept = 'application/json';
return next();
});
```
For example, `pre()` can be used to deduplicate slashes in
URLs
```javascript
server.pre(restify.pre.dedupeSlashes());
```
Returns **[Object][24]** returns self
### use
Allows you to add in handlers that run for all routes. Note that handlers
added
via `use()` will run only after the router has found a matching route. If no
match is found, these handlers will never run. Takes a function, or an array
of functions.
You can pass in any combination of functions or array of functions.
**Parameters**
- `handler` **...([Function][35] \| [Array][40])** A variable number of handler functions- and/or a
variable number of nested arrays of handler functions
Returns **[Object][24]** returns self
### param
- **See: [http://expressjs.com/guide.html#route-param%20pre-conditions][41]**
Minimal port of the functionality offered by Express.js Route Param
Pre-conditions
This basically piggy-backs on the `server.use` method. It attaches a
new middleware function that only fires if the specified parameter exists
in req.params
Exposes an API:
server.param("user", function (req, res, next) {
// load the user's information here, always making sure to call next()
});
**Parameters**
- `name` **[String][25]** The name of the URL param to respond to
- `fn` **[Function][35]** The middleware function to execute
Returns **[Object][24]** returns self
### rm
Removes a route from the server.
You pass in the route 'blob' you got from a mount call.
**Parameters**
- `routeName` **[String][25]** the route name.
- Throws **[TypeError][36]** on bad input.
Returns **[Boolean][26]** true if route was removed, false if not.
### address
Returns the server address.
Wraps node's
[address()][42].
**Examples**
```javascript
server.address()
```
Output:
```javascript
{ address: '::', family: 'IPv6', port: 8080 }
```
Returns **[Object][24]** Address of server
### inflightRequests
Returns the number of inflight requests currently being handled by the server
Returns **[number][34]** number of inflight requests
### debugInfo
Return debug information about the server.
**Examples**
```javascript
server.getDebugInfo()
```
Output:
```javascript
{
routes: [
{
name: 'get',
method: 'get',
input: '/',
compiledRegex: /^[\/]*$/,
compiledUrlParams: null,
handlers: [Array]
}
],
server: {
formatters: {
'application/javascript': [Function: formatJSONP],
'application/json': [Function: formatJSON],
'text/plain': [Function: formatText],
'application/octet-stream': [Function: formatBinary]
},
address: '::',
port: 8080,
inflightRequests: 0,
pre: [],
use: [ 'parseQueryString', '_jsonp' ],
after: []
}
}
```
Returns **[Object][24]** debug info
### toString
toString() the server for easy reading/output.
**Examples**
```javascript
server.toString()
```
Output:
```javascript
Accepts: application/json, text/plain, application/octet-stream,
application/javascript
Name: restify
Pre: []
Router: RestifyRouter:
DELETE: []
GET: [get]
HEAD: []
OPTIONS: []
PATCH: []
POST: []
PUT: []
Routes:
get: [parseQueryString, _jsonp, function]
Secure: false
Url: http://[::]:8080
Version:
```
Returns **[String][25]** stringified server
## Events
In additional to emitting all the events from node's
[http.Server][43],
restify servers also emit a number of additional events that make building REST
and web applications much easier.
### restifyError
This event is emitted following all error events as a generic catch all. It is
recommended to use specific error events to handle specific errors, but this
event can be useful for metrics or logging. If you use this in conjunction with
other error events, the most specific event will be fired first, followed by
this one:
```js
server.get('/', function(req, res, next) {
return next(new InternalServerError('boom'));
});
server.on('InternalServer', function(req, res, err, callback) {
// this will get fired first, as it's the most relevant listener
return callback();
});
server.on('restifyError', function(req, res, err, callback) {
// this is fired second.
return callback();
});
```
### after
After each request has been fully serviced, an `after` event is fired. This
event can be hooked into to handle audit logs and other metrics. Note that
flushing a response does not necessarily correspond with an `after` event.
restify considers a request to be fully serviced when either:
1) The handler chain for a route has been fully completed
2) An error was returned to `next()`, and the corresponding error events have
been fired for that error type
The signature is for the after event is as follows:
```js
function(req, res, route, error) { }
```
- `req` - the request object
- `res` - the response object
- `route` - the route object that serviced the request
- `error` - the error passed to `next()`, if applicable
Note that when the server automatically responds with a
NotFound/MethodNotAllowed/VersionNotAllowed, this event will still be fired.
### pre
Before each request has been routed, a `pre` event is fired. This event can be
hooked into handle audit logs and other metrics. Since this event fires
_before_ routing has occured, it will fire regardless of whether the route is
supported or not, e.g. requests that result in a `404`.
The signature for the `pre` event is as follows:
```js
function(req, res) {}
```
- `req` - the request object
- `res` - the response object
Note that when the server automatically responds with a
NotFound/MethodNotAllowed/VersionNotAllowed, this event will still be fired.
### routed
A `routed` event is fired after a request has been routed by the router, but
before handlers specific to that route has run.
The signature for the `routed` event is as follows:
```js
function(req, res, route) {}
```
- `req` - the request object
- `res` - the response object
- `route` - the route object that serviced the request
Note that this event will _not_ fire if a requests comes in that are not
routable, i.e. one that would result in a `404`.
### uncaughtException
If the restify server was created with `handleUncaughtExceptions: true`,
restify will leverage [domains][44] to handle
thrown errors in the handler chain. Thrown errors are a result of an explicit
`throw` statement, or as a result of programmer errors like a typo or a null
ref. These thrown errors are caught by the domain, and will be emitted via this
event. For example:
```js
server.get('/', function(req, res, next) {
res.send(x); // this will cause a ReferenceError
return next();
});
server.on('uncaughtException', function(req, res, route, err) {
// this event will be fired, with the error object from above:
// ReferenceError: x is not defined
});
```
If you listen to this event, you **must** send a response to the client. This
behavior is different from the standard error events. If you do not listen to
this event, restify's default behavior is to call `res.send()` with the error
that was thrown.
The signature is for the after event is as follows:
```js
function(req, res, route, error) { }
```
- `req` - the request object
- `res` - the response object
- `route` - the route object that serviced the request
- `error` - the error passed to `next()`, if applicable
### close
Emitted when the server closes.
## Errors
Restify handles errors as first class citizens. When an error object is passed
to the `next()` function, an event is emitted on the server object, and the
error object will be serialized and sent to the client. An error object is any
object that passes an `instanceof Error` check.
Before the error object is sent to the client, the server will fire an event
using the name of the error, without the `Error` part of the name. For example,
given an `InternalServerError`, the server will emit an `InternalServer` event.
This creates opportunities to do logging, metrics, or payload mutation based on
the type of error. For example:
```js
var errs = require('restify-errors');
server.get('/', function(req, res, next) {
return next(new errs.InternalServerError('boom!'));
});
server.on('InternalServer', function(req, res, err, callback) {
// before the response is sent, this listener will be invoked, allowing
// opportunities to do metrics capturing or logging.
myMetrics.capture(err);
// invoke the callback to complete your work, and the server will send out
// a response.
return callback();
});
```
Inside the error event listener, it is also possible to change the serialization
method of the error if desired. To do so, simply implement a custom
`toString()` or `toJSON()`. Depending on the content-type and formatter being
used for the response, one of the two serializers will be used. For example,
given the folllwing example:
```js
server.on('restifyError', function(req, res, err, callback) {
err.toJSON = function customToJSON() {
return {
name: err.name,
message: err.message
};
};
err.toString = function customToString() {
return 'i just want a string';
};
return callback();
});
```
A request with an `accept: application/json` will trigger the `toJSON()`
serializer, while a request with `accept: text/plain` will trigger the
`toString()` serializer.
Note that the function signature for the error listener is identical for all
emitted error events. The signature is as follows:
```js
function(req, res, err, callback) { }
```
- `req` - the request object
- `res` - the response object
- `err` - the error object
- `callback` - a callback function to invoke
When using this feature in conjunction with
[restify-errors][45], restify will emit events
for all of the basic http errors:
- `400` - `BadRequestError`
- `401` - `UnauthorizedError`
- `402` - `PaymentRequiredError`
- `403` - `ForbiddenError`
- `404` - `NotFoundError`
- `405` - `MethodNotAllowedError`
- `406` - `NotAcceptableError`
- `407` - `ProxyAuthenticationRequiredError`
- `408` - `RequestTimeoutError`
- `409` - `ConflictError`
- `410` - `GoneError`
- `411` - `LengthRequiredError`
- `412` - `PreconditionFailedError`
- `413` - `RequestEntityTooLargeError`
- `414` - `RequesturiTooLargeError`
- `415` - `UnsupportedMediaTypeError`
- `416` - `RangeNotSatisfiableError` (node >= 4)
- `416` - `RequestedRangeNotSatisfiableError` (node 0.x)
- `417` - `ExpectationFailedError`
- `418` - `ImATeapotError`
- `422` - `UnprocessableEntityError`
- `423` - `LockedError`
- `424` - `FailedDependencyError`
- `425` - `UnorderedCollectionError`
- `426` - `UpgradeRequiredError`
- `428` - `PreconditionRequiredError`
- `429` - `TooManyRequestsError`
- `431` - `RequestHeaderFieldsTooLargeError`
- `500` - `InternalServerError`
- `501` - `NotImplementedError`
- `502` - `BadGatewayError`
- `503` - `ServiceUnavailableError`
- `504` - `GatewayTimeoutError`
- `505` - `HttpVersionNotSupportedError`
- `506` - `VariantAlsoNegotiatesError`
- `507` - `InsufficientStorageError`
- `509` - `BandwidthLimitExceededError`
- `510` - `NotExtendedError`
- `511` - `NetworkAuthenticationRequiredError`
Restify will also emit the following events:
### NotFound
When a client request is sent for a URL that does not exist, restify
will emit this event. Note that restify checks for listeners on this
event, and if there are none, responds with a default 404 handler.
### MethodNotAllowed
When a client request is sent for a URL that exists, but not for the requested
HTTP verb, restify will emit this event. Note that restify checks for listeners
on this event, and if there are none, responds with a default 405 handler.
### VersionNotAllowed
When a client request is sent for a route that exists, but does not
match the version(s) on those routes, restify will emit this
event. Note that restify checks for listeners on this event, and if
there are none, responds with a default 400 handler.
### UnsupportedMediaType
When a client request is sent for a route that exist, but has a `content-type`
mismatch, restify will emit this event. Note that restify checks for listeners
on this event, and if there are none, responds with a default 415 handler.
## Types
### Server~methodOpts
Server method opts
Type: ([String][25] \| [Regexp][46] \| [Object][24])
**Properties**
- `name` **[String][25]** a name for the route
- `path` **[String][25]** can be any String accepted by
[find-my-way][47]
**Examples**
```javascript
// a static route
server.get('/foo', function(req, res, next) {});
// a parameterized route
server.get('/foo/:bar', function(req, res, next) {});
// a regular expression
server.get('/example/:file(^\\d+).png', function(req, res, next) {});
// an options object
server.get({
path: '/foo',
}, function(req, res, next) {});
```
[1]: #createserver
[2]: #server
[3]: #listen
[4]: #close
[5]: #get
[6]: #head
[7]: #post
[8]: #put
[9]: #patch
[10]: #del
[11]: #opts
[12]: #pre
[13]: #use
[14]: #param
[15]: #rm
[16]: #address
[17]: #inflightrequests
[18]: #debuginfo
[19]: #tostring
[20]: #events
[21]: #errors
[22]: #types
[23]: #servermethodopts
[24]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[25]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[26]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[27]: https://github.com/trentm/node-bunyan
[28]: https://nodejs.org/api/buffer.html
[29]: https://github.com/indutny/node-spdy
[30]: https://nodejs.org/api/http2.html
[31]: http://nodejs.org/api/https.html#https_https
[32]: #server
[33]: http://nodejs.org/docs/latest/api/net.html#net_server_listen_path_callback
[34]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[35]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
[36]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/TypeError
[37]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined
[38]: http://nodejs.org/docs/latest/api/net.html#net_event_close
[39]: #servermethodopts
[40]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[41]: http://expressjs.com/guide.html#route-param%20pre-conditions
[42]: http://nodejs.org/docs/latest/api/net.html#net_server_address
[43]: http://nodejs.org/docs/latest/api/http.html#http_class_http_server
[44]: https://nodejs.org/api/domain.html
[45]: https://github.com/restify/errors
[46]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp
[47]: https://github.com/delvedor/find-my-way
================================================
FILE: docs/api/formatters-usage.md
================================================
Restify comes bundled with a selection of useful formatters that prepare your
responses for being sent over the wire, but you are free to include your own!
```js
function formatGraphQL(req, res, body) {
var data = body;
/* Do a thing to data */
res.setHeader('Content-Length', Buffer.byteLength(data));
return data;
}
var server = restify.createServer({
formatters: {
'application/graphql': formatGraphQL
}
});
// Your application now supports content-type 'application/graphql'
```
================================================
FILE: docs/api/plugins-usage.md
================================================
Restify comes bundled with a selection of useful plugins. These are accessible
off of `restify.plugins` and `restify.pre`.
```js
var server = restify.createServer();
server.use(restify.plugins.acceptParser(server.acceptable));
server.use(restify.plugins.authorizationParser());
server.use(restify.plugins.dateParser());
server.use(restify.plugins.queryParser());
server.use(restify.plugins.jsonp());
server.use(restify.plugins.gzipResponse());
server.use(restify.plugins.bodyParser());
server.use(restify.plugins.requestExpiry());
server.use(restify.plugins.throttle({
burst: 100,
rate: 50,
ip: true,
overrides: {
'192.168.1.1': {
rate: 0, // unlimited
burst: 0
}
}
}));
server.use(restify.plugins.conditionalRequest());
================================================
FILE: docs/api/request-events.md
================================================
### restifyDone
After request has been fully serviced, an `restifyDone` event is fired.
restify considers a request to be fully serviced when either:
1) The handler chain for a route has been fully completed
2) An error was returned to `next()`, and the corresponding error events have
been fired for that error type
The signature for the `restifyDone` event is as follows:
```js
function(route, error) { }
```
* `route` - the route object that serviced the request
* `error` - the error passed to `next()`, if applicable
Note that when the server automatically responds with a
`NotFound`/`MethodNotAllowed`/`VersionNotAllowed`, this event will still be
fired.
================================================
FILE: docs/api/request-log.md
================================================
If you are using the [RequestLogger](#bundled-plugins) plugin, the child logger
will be available on `req.log`:
```js
function myHandler(req, res, next) {
var log = req.log;
log.debug({params: req.params}, 'Hello there %s', 'foo');
}
```
The child logger will inject the request's UUID in the `req._id` attribute of
each log statement. Since the logger lasts for the life of the request, you can
use this to correlate statements for an individual request across any number of
separate handlers.
================================================
FILE: docs/api/server-errors.md
================================================
Restify handles errors as first class citizens. When an error object is passed
to the `next()` function, an event is emitted on the server object, and the
error object will be serialized and sent to the client. An error object is any
object that passes an `instanceof Error` check.
Before the error object is sent to the client, the server will fire an event
using the name of the error, without the `Error` part of the name. For example,
given an `InternalServerError`, the server will emit an `InternalServer` event.
This creates opportunities to do logging, metrics, or payload mutation based on
the type of error. For example:
```js
var errs = require('restify-errors');
server.get('/', function(req, res, next) {
return next(new errs.InternalServerError('boom!'));
});
server.on('InternalServer', function(req, res, err, callback) {
// before the response is sent, this listener will be invoked, allowing
// opportunities to do metrics capturing or logging.
myMetrics.capture(err);
// invoke the callback to complete your work, and the server will send out
// a response.
return callback();
});
```
Inside the error event listener, it is also possible to change the serialization
method of the error if desired. To do so, simply implement a custom
`toString()` or `toJSON()`. Depending on the content-type and formatter being
used for the response, one of the two serializers will be used. For example,
given the folllwing example:
```js
server.on('restifyError', function(req, res, err, callback) {
err.toJSON = function customToJSON() {
return {
name: err.name,
message: err.message
};
};
err.toString = function customToString() {
return 'i just want a string';
};
return callback();
});
```
A request with an `accept: application/json` will trigger the `toJSON()`
serializer, while a request with `accept: text/plain` will trigger the
`toString()` serializer.
Note that the function signature for the error listener is identical for all
emitted error events. The signature is as follows:
```js
function(req, res, err, callback) { }
```
* `req` - the request object
* `res` - the response object
* `err` - the error object
* `callback` - a callback function to invoke
When using this feature in conjunction with
[restify-errors](https://github.com/restify/errors), restify will emit events
for all of the basic http errors:
* `400` - `BadRequestError`
* `401` - `UnauthorizedError`
* `402` - `PaymentRequiredError`
* `403` - `ForbiddenError`
* `404` - `NotFoundError`
* `405` - `MethodNotAllowedError`
* `406` - `NotAcceptableError`
* `407` - `ProxyAuthenticationRequiredError`
* `408` - `RequestTimeoutError`
* `409` - `ConflictError`
* `410` - `GoneError`
* `411` - `LengthRequiredError`
* `412` - `PreconditionFailedError`
* `413` - `RequestEntityTooLargeError`
* `414` - `RequesturiTooLargeError`
* `415` - `UnsupportedMediaTypeError`
* `416` - `RangeNotSatisfiableError` (node >= 4)
* `416` - `RequestedRangeNotSatisfiableError` (node 0.x)
* `417` - `ExpectationFailedError`
* `418` - `ImATeapotError`
* `422` - `UnprocessableEntityError`
* `423` - `LockedError`
* `424` - `FailedDependencyError`
* `425` - `UnorderedCollectionError`
* `426` - `UpgradeRequiredError`
* `428` - `PreconditionRequiredError`
* `429` - `TooManyRequestsError`
* `431` - `RequestHeaderFieldsTooLargeError`
* `500` - `InternalServerError`
* `501` - `NotImplementedError`
* `502` - `BadGatewayError`
* `503` - `ServiceUnavailableError`
* `504` - `GatewayTimeoutError`
* `505` - `HttpVersionNotSupportedError`
* `506` - `VariantAlsoNegotiatesError`
* `507` - `InsufficientStorageError`
* `509` - `BandwidthLimitExceededError`
* `510` - `NotExtendedError`
* `511` - `NetworkAuthenticationRequiredError`
Restify will also emit the following events:
### NotFound
When a client request is sent for a URL that does not exist, restify
will emit this event. Note that restify checks for listeners on this
event, and if there are none, responds with a default 404 handler.
### MethodNotAllowed
When a client request is sent for a URL that exists, but not for the requested
HTTP verb, restify will emit this event. Note that restify checks for listeners
on this event, and if there are none, responds with a default 405 handler.
### VersionNotAllowed
When a client request is sent for a route that exists, but does not
match the version(s) on those routes, restify will emit this
event. Note that restify checks for listeners on this event, and if
there are none, responds with a default 400 handler.
### UnsupportedMediaType
When a client request is sent for a route that exist, but has a `content-type`
mismatch, restify will emit this event. Note that restify checks for listeners
on this event, and if there are none, responds with a default 415 handler.
================================================
FILE: docs/api/server-events.md
================================================
In additional to emitting all the events from node's
[http.Server](http://nodejs.org/docs/latest/api/http.html#http_class_http_server),
restify servers also emit a number of additional events that make building REST
and web applications much easier.
### restifyError
This event is emitted following all error events as a generic catch all. It is
recommended to use specific error events to handle specific errors, but this
event can be useful for metrics or logging. If you use this in conjunction with
other error events, the most specific event will be fired first, followed by
this one:
```js
server.get('/', function(req, res, next) {
return next(new InternalServerError('boom'));
});
server.on('InternalServer', function(req, res, err, callback) {
// this will get fired first, as it's the most relevant listener
return callback();
});
server.on('restifyError', function(req, res, err, callback) {
// this is fired second.
return callback();
});
```
### after
After each request has been fully serviced, an `after` event is fired. This
event can be hooked into to handle audit logs and other metrics. Note that
flushing a response does not necessarily correspond with an `after` event.
restify considers a request to be fully serviced when either:
1) The handler chain for a route has been fully completed
2) An error was returned to `next()`, and the corresponding error events have
been fired for that error type
The signature is for the after event is as follows:
```js
function(req, res, route, error) { }
```
* `req` - the request object
* `res` - the response object
* `route` - the route object that serviced the request
* `error` - the error passed to `next()`, if applicable
Note that when the server automatically responds with a
NotFound/MethodNotAllowed/VersionNotAllowed, this event will still be fired.
### pre
Before each request has been routed, a `pre` event is fired. This event can be
hooked into handle audit logs and other metrics. Since this event fires
*before* routing has occured, it will fire regardless of whether the route is
supported or not, e.g. requests that result in a `404`.
The signature for the `pre` event is as follows:
```js
function(req, res) {}
```
* `req` - the request object
* `res` - the response object
Note that when the server automatically responds with a
NotFound/MethodNotAllowed/VersionNotAllowed, this event will still be fired.
### routed
A `routed` event is fired after a request has been routed by the router, but
before handlers specific to that route has run.
The signature for the `routed` event is as follows:
```js
function(req, res, route) {}
```
* `req` - the request object
* `res` - the response object
* `route` - the route object that serviced the request
Note that this event will *not* fire if a requests comes in that are not
routable, i.e. one that would result in a `404`.
### uncaughtException
If the restify server was created with `handleUncaughtExceptions: true`,
restify will leverage [domains](https://nodejs.org/api/domain.html) to handle
thrown errors in the handler chain. Thrown errors are a result of an explicit
`throw` statement, or as a result of programmer errors like a typo or a null
ref. These thrown errors are caught by the domain, and will be emitted via this
event. For example:
```js
server.get('/', function(req, res, next) {
res.send(x); // this will cause a ReferenceError
return next();
});
server.on('uncaughtException', function(req, res, route, err, callback) {
// this event will be fired, with the error object from above:
// ReferenceError: x is not defined
res.send(504, 'boom');
callback();
});
```
If you listen to this event, you __must__:
1. send a response to the client _and_
2. call the callback function passed as the fourth argument of the event listener
This behavior is different from the standard error events. If you do not listen
to this event, restify's default behavior is to call `res.send()` with the error
that was thrown.
The signature is for the after event is as follows:
```js
function(req, res, route, error) { }
```
* `req` - the request object
* `res` - the response object
* `route` - the route object that serviced the request
* `error` - the error passed to `next()`, if applicable
### close
Emitted when the server closes.
================================================
FILE: docs/config/formatters.yaml
================================================
toc:
- name: Usage
file: ../api/formatters-usage.md
- name: Types
children:
- formatter
- name: Included formatters
description: |
restify comes pre-loaded with a standard set of formatters for common
use cases.
children:
- formatText
- formatJSON
- formatJSONP
- formatBinary
================================================
FILE: docs/config/plugins.yaml
================================================
toc:
- name: Usage
file: ../api/plugins-usage.md
- name: server.pre() plugins
description: |
This module includes various pre plugins, which are intended to be used prior
to routing of the URL. To use a plugin before routing, use the `server.pre()`
method.
children:
- context
- dedupeSlashes
- pause
- sanitizePath
- reqIdHeaders
- strictQueryParams
- userAgentConnection
- name: server.use() plugins
children:
- acceptParser
- authorizationParser
- dateParser
- queryParser
- jsonp
- bodyParser
- requestLogger
- gzipResponse
- serveStatic
- serveStaticFiles
- throttle
- requestExpiry
- inflightRequestThrottle
- cpuUsageThrottle
- conditionalHandler
- conditionalRequest
- auditLogger
- metrics
- name: Types
children:
- metrics~callback
================================================
FILE: docs/config/request.yaml
================================================
toc:
- Request
- name: Events
file: ../api/server-events.md
- name: Log
file: ../api/request-log.md
================================================
FILE: docs/config/server.yaml
================================================
toc:
- createServer
- Server
- name: Events
file: ../api/server-events.md
- name: Errors
file: ../api/server-errors.md
- name: Types
children:
- Server~methodOpts
================================================
FILE: docs/guides/4TO5GUIDE.md
================================================
---
title: restify 4.x to 5.x migration guide
permalink: /docs/4to5/
---
## Introduction
restify 5.0 is finally here! And a great big thank you to all of our
contributors. 5.x fixes a ton of bugs, adds some new features, and introduces
some breaking changes. This guide helps make sense of all the major changes
that have happened since the last 4.x release. A more detailed change log can
be found in CHANGES.md.
#### queryParser() and bodyParser()
By default, queryParser and bodyParser no longer map req.query and req.body to
req.params. To get the old behavior, please enable the `mapParams` behavior
with these plugins.
### restify-errors
Errors, which used to be available on the `restify.errors` namespace, now live
in their own [repository](https://github.com/restify/errors) and are published
[independently on npm](https://www.npmjs.com/package/restify-errors).
`restify-errors` can be used independently of restify in any of your other
projects for customizable error classes and chained errors.
### restify-clients
All restify clients have been broken out into their own
[repository](https://github.com/restify/clients), and are published
[independently on npm](https://www.npmjs.com/package/restify-clients).
### server.on('restifyError', ...)
restify now emits a generic error event. This error event will be fired for all
errors passed to `next()`. If you have specific listeners attached for a class
of error, the most specific one will be fired first, with the generic one being
fired last.
```js
// in some route, create a 500
server.get('/', function(req, res, next) {
return next(new InternalServerError('oh noes!'));
// this will hit the InternalServerError FIRST, allowing you to handle it some fashion,
// before firing restifyError event. the semantics of the generic handler means it should
// always be fired, but doesn't mean we shouldn't allow you to handle it first within
// the error handler they care about. this is only possible if we fire events in serial.
});
// handle 500s
server.on('InternalServer', function(req, res, err, cb) {
// this event is fired first. you can annotate errors here by saying
// err.handled = true, because we must ALWAYS fire the generic handler after.
err.handled = true;
return cb();
});
// generic error handler
server.on('restifyError', function(req, res, err, cb) {
// this event is fired last. do some generic metrics/logging
if (!err.handled) {
// do something
}
return cb();
});
```
### server.on('redirect', ...)
restify now emits a redirect event when `res.redirect()` is used. The event is
fired with the new location of the redirect.
```js
SERVER.on('redirect', function (newLocation) {
// newLocation is the new url we redirected to.
});
```
### server.on('NotFound', ...)
### server.on('MethodNotAllowed', ...)
### server.on('VersionNotAllowed', ...)
### server.on('UnsupportedMediaType', ...)
restify's error events for these four types of errors have now been normalized
to act like other error events. Previously, listening to these events would
require you to send a response. It has now been normalized to work like the
other error events:
```js
server.on('NotFound', function(req, res, err, cb) {
// do some logging or metrics collection here. if you want to send a custom
// response, you can do so here by setting the response on the body of the
// error object.
err.body = "whoops! can't find your stuff!"; // the body of the error becomes the response
return cb();
});
```
### CORS
CORS has been removed from restify core. For CORS support, please use
[TabDigital's](https://github.com/TabDigital/restify-cors-middleware) plugin.
### strict routing
Strict routing is now supported via the `strictRouting` option. This allows
differentiation of routes with trailing slashes. The default value is `false`,
which mimics the behavior in 4.x which is to strip trailing slashes.
```js
var server = restify.createServer({
strictRouting: true
});
// these two routes are distinct with strictRouting option
server.get('/foo/', function(req, res, next) { });
server.get('/foo', function(req, res, next) { });
```
### res.sendRaw()
restify has a concept of formatters, where each formatter is executed to format
a the content of a response before sending it out. A new method,
`res.sendRaw()`, has been added which allows bypassing of the formatters in
scenarios where you have preformatted content (pre-gzipped, pre-JSON
stringified, etc.). `sendRaw` has the same signature as `send`.
### Removal of undocumented APIs
Previous versions of restify had some undocumented exports on the main object.
These have been removed as of 5.x. These include:
* `restify.CORS` - due to removal of CORS from core
* `restify.httpDate` - undocumented
* `restify.realizeUrl` - undocumented
### next(err) & res.send(err)
To help reduce unintentional exposure of errors to the client, restify no
longer does special JSON serialization for Error objects. For example:
```js
server.get('/sendErr', function(req, res, next) {
res.send(new Error('where is my msg?'));
return next();
});
server.get('/nextErr', function(req, res, next) {
return next(new Error('where is my msg?'));
});
```
```sh
$ curl -is localhost:8080/sendErr
HTTP/1.1 410 Gone
Content-Type: application/json
Content-Length: 37
Date: Fri, 03 Jun 2016 20:17:48 GMT
Connection: keep-alive
{}
$ curl -is localhost:8080/nextErr
HTTP/1.1 410 Gone
Content-Type: application/json
Content-Length: 37
Date: Fri, 03 Jun 2016 20:17:48 GMT
Connection: keep-alive
{}
```
The response is an empty object because `JSON.stringify(err)` returns an empty
object. In order to get properly serialized Errors, the preferred method is to
use restify-errors, which will have defined `toJSON` methods. Alternatively,
if you have custom Error classes, you can define a `toJSON` method which is
invoked when your Error is being stringified. If you have many custom error
types, consider using restify-errors to help you create and manage them easily.
Lastly, you can use restify-errors to opt-in to automatic `toJSON`
serialization:
```js
var errs = require('restify-errors');
server.get('/', function(req, res, next) {
res.send(new errs.GoneError('gone girl'));
return next();
});
```
```sh
$ curl -is localhost:8080/
HTTP/1.1 410 Gone
Content-Type: application/json
Content-Length: 37
Date: Fri, 03 Jun 2016 20:17:48 GMT
Connection: keep-alive
{"code":"Gone","message":"gone girl"}
```
## Deprecations
The following are still currently supported, but are on life support and may be
removed in future versions. Usage of these features will cause restify to spit
out deprecation warnings in the logs.
### domains
In 4.x, restify utilized domains by default. Any errors captured by the domain
could be handled to via the `server.on('uncaughtException', ...)` event.
However, it was not immediately obvious that this behavior was happening by
default, and many errors often went unhandled or unnoticed by end users.
With domains being deprecated, we've opted to turn domains off by default. If
you want to use domains, you can turn them back on via the
`handleUncaughtExceptions` option when you create the server:
```js
var server = restify.createServer({
handleUncaughtExceptions: true
});
```
### next.ifError()
The `next.ifError()` feature leveraged domains under the hood. This feature is
also deprecated, and will only be available to you if the
`handleUncaughtExceptions` flag is set to true.
================================================
FILE: docs/guides/6to7guide.md
================================================
---
title: restify 6.x to 7.x migration guide
permalink: /docs/6to7/
---
## Introduction
restify `7.x` comes with a completely new router and middleware logic that
brings significant performance improvement to your application.
From `v7.0.0` restify uses the Radix Tree based
[find-my-way](https://github.com/delvedor/find-my-way) package as a router
backend.
## Breaking Changes
### Server returns `RequestCloseError` instead of `RequestAbortedError`
Server returns `RequestCloseError` instead of `RequestAbortedError` in the case
of the request was terminated by the client for some reason.
The new version of restify never returns `RequestAbortedError`.
### Non-strict routing is gone
Option `strictRouting` is removed `createServer({ strictRouting: false })`.
Strict routing is the new default.
### Path trailing slash at the end
`/path` and `/path/` are not the same thing in restify `v7.x`.
Use `ignoreTrailingSlash: true` server option if you don't want to differentiate
them from each other.
### Path must start with `/`
In restify 7.x path must start with a `/`.
For example `server.get('foo')` is invalid, change it to `server.get('/foo')`.
If you use [enroute](https://github.com/restify/enroute) be sure
that you updated it to the latest version.
### Different `RegExp` usage in router path and wildcards
restify's new router backend
[find-my-way](https://github.com/delvedor/find-my-way) has limited RegExp
support.
#### Guide to define paths
To register a **parametric** path, use the *colon* before the parameter name.
For **wildcard** use the *star*.
*Remember that static routes are always inserted before parametric and wildcard.*
```js
// parametric
server.get('GET', '/example/:userId', (req, res, next) => {}))
server.get('GET', '/example/:userId/:secretToken', (req, res, next) => {}))
// wildcard
server.get('GET', '/example/*', (req, res, next) => {}))
```
Regular expression routes are supported as well, but pay attention, RegExp are
very expensive in term of performance!
```js
// parametric with RegExp
server.get('GET', '/example/:file(^\\d+).png', () => {}))
```
RegExp path chunks needs to be between parentheses.
It's possible to define more than one parameter within the same couple of slash
("/"). Such as:
```js
server.get('/example/near/:lat-:lng/radius/:r', (req, res, next) => {}))
```
*Remember in this case to use the dash ("-") as parameters separator.*
Finally it's possible to have multiple parameters with RegExp.
```js
server.get('/example/at/:hour(^\\d{2})h:minute(^\\d{2})m', (req, res, next) => {
// req.params => { hour: 12, minute: 15 }
}))
```
In this case as parameter separator it's possible to use whatever character is
not matched by the regular expression.
Having a route with multiple parameters may affect negatively the performance,
so prefer single parameter approach whenever possible, especially on routes
which are on the hot path of your application.
Fore more info see: https://github.com/delvedor/find-my-way
### Remove already deprecated `next.ifError`
`next.ifError(err)` is not available anymore.
### Disable DTrace probes by default
DTrace probes come with some performance impact that's fine for the sake of
observability but you don't have to use them: they are disabled by default.
### Change in calling `next` multiple times
Earlier `restify` automatically prevented calling the `next()` more than once.
In the new version this behaviour is disabled by default, but you can activate
it with the `onceNext` property.
The behaviour of the `strictNext` option is unchanged.
Which means `strictNext` enforces `onceNext` option.
```js
var server = restify.createServer({ onceNext: true })
server.use(function (req, req, next) {
next();
next();
});
// -> fine
var server = restify.createServer({ strictNext: true })
server.use(function (req, req, next) {
next();
next();
});
// -> throws an Error
```
### Router versioning and content type
`accept-version` and `accept` based conditional routing moved to the
`conditionalHandler` plugin, see docs or example:
```js
var server = restify.createServer()
server.use(restify.plugins.conditionalHandler({
contentType: 'application/json',
version: '1.0.0'
handler: function (req, res, next) {
next();
})
});
server.get('/hello/:name', restify.plugins.conditionalHandler([
{
version: '1.0.0',
handler: function(req, res, next) { res.send('1.x') }
},
{
version: ['1.5.0', '2.0.0'],
handler: function(req, res, next) { res.send('1.5.x, 2.x') }
},
{
version: '3.0.0',
contentType: ['text/html', 'text/html']
handler: function(req, res, next) { res.send('3.x, text') }
},
{
version: '3.0.0',
contentType: 'application/json'
handler: function(req, res, next) { res.send('3.x, json') }
}
]);
// 'accept-version': '^1.1.0' => 1.5.x, 2.x'
// 'accept-version': '3.x', accept: 'application/json' => '3.x, json'
```
### After event fires when both request is flushed and last handler is finished
In 7.x `after` event fires after both request is flushed
and last handler is finished.
### Metrics plugin latency
In 7.x Metrics plugin's `latency` is calculated when the request is
fully flushed. Earlier it was calculated when the last handler finished.
To address the previous use-cases, new timings were added to the metrics plugin:
- `metrics.totalLatency` both request is flushed and all handlers finished
- `metrics.preLatency` pre handlers latency
- `metrics.useLatency` use handlers latency
- `metrics.routeLatency` route handlers latency
================================================
FILE: docs/guides/8to9guide.md
================================================
---
title: restify 8.x to 9.x migration guide
permalink: /docs/8to9/
---
## Introduction
Restify `9.x` comes with `async/await` support, `pino` and more!
## Breaking Changes
### Drops support for Node.js `8.x`
Restify requires Node.js version `>=10.0.0`.
### Async/await support
`async/await` basic support for `.pre()`, `.use()` and route handlers.
#### Example
```js
const restify = require('restify');
const server = restify.createServer({});
server.use(async (req, res) => {
req.something = await doSomethingAsync();
});
server.get('/params', async (req, res) => {
const value = await asyncOperation(req.something);
res.send(value);
});
```
#### Middleware API (`.pre()` and `.use()`)
```js
server.use(async (req, res) => {
req.something = await doSomethingAsync();
});
```
- `fn.length === 2` (arity 2);
- `fn instanceof AsyncFunction`;
- if the async function resolves, it calls `next()`;
- any value returned by the async function will be discarded;
- if it rejects with an `Error` instance it calls `next(err)`;
- if it rejects with anything else it wraps in a `AsyncError` and calls `next(err)`;
#### Route handler API
```js
server.get('/something', async (req, res) => {
const someData = await fetchSomeDataAsync();
res.send({ data: someData });
});
```
- `fn.length === 2` (arity 2);
- `fn instanceof AsyncFunction`;
- if the async function resolves without a value, it calls `next()`;
- if the async function resolves with a string value, it calls `next(string)` (re-routes*);
- if the async function resolves with a value other than string, it calls `next(any)`;
- if it rejects with an `Error` instance it calls `next(err)`;
- if it rejects with anything else it wraps in a `AsyncError` and calls `next(err)` (error-handing**);
##### (*) Note about re-routing:
The `8.x` API allows re-routing when calling `next()` with a string value. If the string matches a valid route,
it will re-route to the given handler. The same is valid for resolving a async function. If the value returned by
the async function is a string, it will try to re-route to the given handler.
##### (**) Note about error handling:
Although it is recommended to always reject with an instance of Error, in a async function it is possible to
throw or reject without returning an `Error` instance or even anything at all. In such cases, the value rejected
will be wrapped on a `AsyncError`.
### Handler arity check
Handlers expecting 2 or fewer parameters added to a `.pre()`, `.use()` or route chain must be async functions, as:
```js
server.use(async (req, res) => {
req.something = await doSomethingAsync();
});
```
Handlers expecting more than 2 parameters shouldn't be async functions, as:
````js
// This middleware will be rejected and restify will throw
server.use(async (req, res, next) => {
doSomethingAsync(function callback(val) {
req.something = val;
next();
});
});
````
### Remove `RequestCaptureStream`
Removes `RequestCaptureStream` from Restify core.
### Use `Pino` as default logger (removes dependency on `Bunyan`)
[Pino](https://github.com/pinojs/pino) is well maintained, performance-focused,
compatible API. It does have a few key differences from `Bunyan`:
- As a performance optimization, it stores bindings a single serialized `string`,
while `Bunyan` stores them as object keys;
- It uses a `setter` to set the log level, `Bunyan` uses a method;
- It only accepts one stream. If you need the multi-stream functionality, you
must use [pino-multistream](https://github.com/pinojs/pino-multi-stream).
================================================
FILE: docs/guides/client.md
================================================
---
title: Client Guide
permalink: /docs/client-guide/
---
There are actually three separate clients shipped in restify:
* **JsonClient:** sends and expects application/json
* **StringClient:** sends url-encoded request and expects text/plain
* **HttpClient:** thin wrapper over node's http/https libraries
The idea being that if you want to support "typical" control-plane
REST APIs, you probably want the `JsonClient`, or if you're using some
other serialization (like XML) you'd write your own client that
extends the `StringClient`. If you need streaming support, you'll need
to do some work on top of the `HttpClient`, as `StringClient` and
friends buffer requests/responses.
All clients support retry with exponential backoff for getting a TCP
connection; they do not perform retries on 5xx error codes like
previous versions of the restify client. You can set `retry` to `false` to
disable this logic altogether. Also, all clients support a `connectTimeout`
field, which is use *on each retry*. The default is not to set a
`connectTimeout`, so you end up with the node.js socket defaults.
Here's an example of hitting the
[Joyent CloudAPI](https://apidocs.joyent.com/cloudapi/):
```js
var restify = require('restify-clients');
// Creates a JSON client
var client = restify.createJsonClient({
url: 'https://us-east-1.api.joyent.com'
});
client.basicAuth('$login', '$password');
client.get('/my/machines', function(err, req, res, obj) {
assert.ifError(err);
console.log(JSON.stringify(obj, null, 2));
});
```
As a short-hand, a client can be initialized with a string-URL rather than
an options object:
```js
var restify = require('restify-clients');
var client = restify.createJsonClient('https://us-east-1.api.joyent.com');
```
Note that all further documentation refers to the "short-hand" form of
methods like `get/put/del` which take a string path. You can also
pass in an object to any of those methods with extra params (notably
headers):
```js
var options = {
path: '/foo/bar',
headers: {
'x-foo': 'bar'
},
retry: {
'retries': 0
},
agent: false
};
client.get(options, function(err, req, res) { .. });
```
If you need to interpose additional headers in the request before it is sent on
to the server, you can provide a synchronous callback function as the
`signRequest` option when creating a client. This is particularly useful with
[node-http-signature](https://github.com/joyent/node-http-signature), which
needs to attach a cryptographic signature of selected outgoing headers. If
provided, this callback will be invoked with a single parameter: the outgoing
`http.ClientRequest` object.
## JsonClient
The JSON Client is the highest-level client bundled with restify-clients; it
exports a set of methods that map directly to HTTP verbs. All
callbacks look like `function(err, req, res, [obj])`, where `obj` is
optional, depending on if content was returned. HTTP status codes are
not interpreted, so if the server returned 4xx or something with a
JSON payload, `obj` will be the JSON payload. `err` however will be
set if the server returned a status code >= 400 (it will be one of the
restify HTTP errors). If `obj` looks like a `RestError`:
```js
{
"code": "FooError",
"message": "some foo happened"
}
```
then `err` gets "upconverted" into a `RestError` for you. Otherwise
it will be an `HttpError`.
### createJsonClient(options)
```js
var client = restify.createJsonClient({
url: 'https://api.us-east-1.joyent.com',
version: '*'
});
```
Options:
|Name|Type|Description|
|----|----|-----------|
|accept|String|Accept header to send|
|connectTimeout|Number|Amount of time to wait for a socket|
|requestTimeout|Number|Amount of time to wait for the request to finish|
|dtrace|Object|node-dtrace-provider handle|
|gzip|Object|Will compress data when sent using `content-encoding: gzip`|
|headers|Object|HTTP headers to set in all requests|
|log|Object|[pino](https://github.com/pinojs/pino) instance|
|retry|Object|options to provide to node-retry;"false" disables retry; defaults to 4 retries|
|signRequest|Function|synchronous callback for interposing headers before request is sent|
|url|String|Fully-qualified URL to connect to|
|userAgent|String|user-agent string to use; restify inserts one, but you can override it|
|version|String|semver string to set the accept-version|
### get(path, callback)
Performs an HTTP get; if no payload was returned, `obj` defaults to
`{}` for you (so you don't get a bunch of null pointer errors).
```js
client.get('/foo/bar', function(err, req, res, obj) {
assert.ifError(err);
console.log('%j', obj);
});
```
### head(path, callback)
Just like `get`, but without `obj`:
```js
client.head('/foo/bar', function(err, req, res) {
assert.ifError(err);
console.log('%d -> %j', res.statusCode, res.headers);
});
```
### post(path, object, callback)
Takes a complete object to serialize and send to the server.
```js
client.post('/foo', { hello: 'world' }, function(err, req, res, obj) {
assert.ifError(err);
console.log('%d -> %j', res.statusCode, res.headers);
console.log('%j', obj);
});
```
### put(path, object, callback)
Just like `post`:
```js
client.put('/foo', { hello: 'world' }, function(err, req, res, obj) {
assert.ifError(err);
console.log('%d -> %j', res.statusCode, res.headers);
console.log('%j', obj);
});
```
### del(path, callback)
`del` doesn't take content, since you know, it should't:
```js
client.del('/foo/bar', function(err, req, res) {
assert.ifError(err);
console.log('%d -> %j', res.statusCode, res.headers);
});
```
## StringClient
`StringClient` is what `JsonClient` is built on, and provides a base
for you to write other buffering/parsing clients (like say an XML
client). If you need to talk to some "raw" HTTP server, then
`StringClient` is what you want, as it by default will provide you
with content uploads in `application/x-www-form-url-encoded` and
downloads as `text/plain`. To extend a `StringClient`, take a look at
the source for `JsonClient`. Effectively, you extend it, and set the
appropriate options in the constructor and implement a `write` (for
put/post) and `parse` method (for all HTTP bodies), and that's it.
### createStringClient(options)
```js
var client = restify.createStringClient({
url: 'https://example.com'
})
```
### get(path, callback)
Performs an HTTP get; if no payload was returned, `data` defaults to
`''` for you (so you don't get a bunch of null pointer errors).
```js
client.get('/foo/bar', function(err, req, res, data) {
assert.ifError(err);
console.log('%s', data);
});
```
### head(path, callback)
Just like `get`, but without `data`:
```js
client.head('/foo/bar', function(err, req, res) {
assert.ifError(err);
console.log('%d -> %j', res.statusCode, res.headers);
});
```
### post(path, object, callback)
Takes a complete object to serialize and send to the server.
```js
client.post('/foo', { hello: 'world' }, function(err, req, res, data) {
assert.ifError(err);
console.log('%d -> %j', res.statusCode, res.headers);
console.log('%s', data);
});
```
### put(path, object, callback)
Just like `post`:
```js
client.put('/foo', { hello: 'world' }, function(err, req, res, data) {
assert.ifError(err);
console.log('%d -> %j', res.statusCode, res.headers);
console.log('%s', data);
});
```
### del(path, callback)
`del` doesn't take content, since you know, it should't:
```js
client.del('/foo/bar', function(err, req, res) {
assert.ifError(err);
console.log('%d -> %j', res.statusCode, res.headers);
});
```
## HttpClient
`HttpClient` is the lowest-level client shipped in restify, and is
basically just some sugar over the top of node's http/https modules
(with HTTP methods like the other clients). It is useful if you want
to stream with restify. Note that the event below is unfortunately
named `result` and not `response` (because
[Event 'response'](http://nodejs.org/docs/latest/api/all.html#event_response_)
is already used).
```js
client = restify.createClient({
url: 'http://127.0.0.1'
});
client.get('/str/mcavage', function(err, req) {
assert.ifError(err); // connection error
req.on('result', function(err, res) {
assert.ifError(err); // HTTP status code >= 400
res.body = '';
res.setEncoding('utf8');
res.on('data', function(chunk) {
res.body += chunk;
});
res.on('end', function() {
console.log(res.body);
});
});
});
```
Or a write:
```js
client.post(opts, function(err, req) {
assert.ifError(connectErr);
req.on('result', function(err, res) {
assert.ifError(err);
res.body = '';
res.setEncoding('utf8');
res.on('data', function(chunk) {
res.body += chunk;
});
res.on('end', function() {
console.log(res.body);
});
});
req.write('hello world');
req.end();
});
```
Note that get/head/del all call `req.end()` for you, so you can't
write data over those. Otherwise, all the same methods exist as
`JsonClient/StringClient`.
One wishing to extend the `HttpClient` should look at the internals
and note that `read` and `write` probably need to be overridden.
### Proxy
There are several options for enabling a proxy for the
http client. The following options are available to set a proxy url:
```js
// Set the proxy option in the client configuration
restify.createClient({
proxy: 'http://127.0.0.1'
});
```
From environment variables:
```sh
$ export HTTPS_PROXY = 'https://127.0.0.1'
$ export HTTP_PROXY = 'http://127.0.0.1'
```
There is an option to disable the use of a proxy on a url basis or for
all urls. This can be enabled by setting an environment variable.
Don't proxy requests to any urls
```sh
$ export NO_PROXY='*'
```
Don't proxy requests to localhost
```sh
$ export NO_PROXY='127.0.0.1'
```
Don't proxy requests to localhost on port 8000
```sh
$ export NO_PROXY='localhost:8000'
```
Don't proxy requests to multiple IPs
```sh
$ export NO_PROXY='127.0.0.1, 8.8.8.8'
```
**Note**: The url being requested must match the full hostname in
the proxy configuration or NO_PROXY environment variable. DNS
lookups are not performed to determine the IP address of a hostname.
### basicAuth(username, password)
Since it hasn't been mentioned yet, this convenience method (available
on all clients), just sets the `Authorization` header for all HTTP requests:
```js
client.basicAuth('mark', 'mysupersecretpassword');
```
### Upgrades
If you successfully negotiate an Upgrade with the HTTP server, an
`upgradeResult` event will be emitted with the arguments `err`, `res`, `socket`
and `head`. You can use this functionality to establish a WebSockets
connection with a server. For example, using the
[watershed](https://github.com/jclulow/node-watershed) library:
```js
var ws = new Watershed();
var wskey = ws.generateKey();
var options = {
path: '/websockets/attach',
headers: {
connection: 'upgrade',
upgrade: 'websocket',
'sec-websocket-key': wskey,
}
};
client.get(options, function(err, res, socket, head) {
res.once('upgradeResult', function(err2, res2, socket2, head2) {
var shed = ws.connect(res2, socket2, head2, wskey);
shed.on('text', function(msg) {
console.log('message from server: ' + msg);
shed.end();
});
shed.send('greetings program');
});
});
```
================================================
FILE: docs/guides/dtrace.md
================================================
---
title: Dtrace
permalink: /docs/dtrace/
---
One of the coolest features of restify is that it automatically
creates DTrace probes for you whenever you add a new route/handler.
To use DTrace you need to pass `dtrace` option to the server
`restify.createServer({ dtrace: true })`.
The easiest way to explain this is with an example:
```js
var restify = require('restify');
var server = restify.createServer({
name: 'helloworld',
dtrace: true
});
server.use(restify.plugins.acceptParser(server.acceptable));
server.use(restify.plugins.authorizationParser());
server.use(restify.plugins.dateParser());
server.use(restify.plugins.queryParser());
server.use(restify.plugins.urlEncodedBodyParser());
server.use(function slowHandler(req, res, next) {
setTimeout(function() {
return next();
}, 250);
});
server.get({path: '/hello/:name', name: 'GetFoo'}, function respond(req, res, next) {
res.send({
hello: req.params.name
});
return next();
});
server.listen(8080, function() {
console.log('listening: %s', server.url);
});
```
So we've got our typical "hello world" server now, with a slight twist; we
introduced an artificial 250ms lag. Also, note that we named our server, our
routes, and all of our handlers (functions); while that's optional, it
does make DTrace much more usable. So, if you started that server,
then looked for DTrace probes, you'd see something like this:
```sh
$ dtrace -l -P restify*
ID PROVIDER MODULE FUNCTION NAME
24 restify38789 mod-88f3f88 route-start route-start
25 restify38789 mod-88f3f88 handler-start handler-start
26 restify38789 mod-88f3f88 handler-done handler-done
27 restify38789 mod-88f3f88 route-done route-done
```
## route-start
|Field|Type|Description|
|-----|----|-----------|
|server name|char *|name of the restify server that fired|
|route name|char *|name of the route that fired|
|id|int|unique id for this request|
|method|char *|HTTP request method|
|url|char *|(full) HTTP URL|
|headers|char *|JSON encoded map of all request headers|
## handler-start
|Field|Type|Description|
|-----|----|-----------|
|server name|char *|name of the restify server that fired|
|route name|char *|name of the route that fired|
|handler name|char *|name of the function that just entered|
|id|int|unique id for this request|
## route-done
|Field|Type|Description|
|-----|----|-----------|
|server name|char *|name of the restify server that fired|
|route name|char *|name of the route that fired|
|id|int|unique id for this request|
|statusCode|int|HTTP response code|
|headers|char *|JSON encoded map of response headers|
## handler-done
|Field|Type|Description|
|-----|----|-----------|
|server name|char *|name of the restify server that fired|
|route name|char *|name of the route that fired|
|handler name|char *|name of the function that just entered|
|id|int|unique id for this request|
## Example D Script
Now, if you wanted to say get a breakdown of latency by handler, you
could do something like this:
```
#!/usr/sbin/dtrace -s
#pragma D option quiet
restify*:::route-start
{
track[arg2] = timestamp;
}
restify*:::handler-start
/track[arg3]/
{
h[arg3, copyinstr(arg2)] = timestamp;
}
restify*:::handler-done
/track[arg3] && h[arg3, copyinstr(arg2)]/
{
@[copyinstr(arg2)] = quantize((timestamp - h[arg3, copyinstr(arg2)]) / 1000000);
h[arg3, copyinstr(arg2)] = 0;
}
restify*:::route-done
/track[arg2]/
{
@[copyinstr(arg1)] = quantize((timestamp - track[arg2]) / 1000000);
track[arg2] = 0;
}
```
So running the server in one terminal:
```sh
$ node helloworld.js
```
The D script in another:
```sh
$ ./helloworld.d
```
Hit the server a few times with curl:
```sh
$ for i in {1..10} ; do curl -is http://127.0.0.1:8080/hello/mark ; done
```
Then Ctrl-C the D script, and you'll see the "slowHandler" at the
bottom of the stack, bucketized that it's the vast majority of latency
in this pipeline
```sh
handler-6
value ------------- Distribution ------------- count
-1 | 0
0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
1 | 0
parseAccept
value ------------- Distribution ------------- count
-1 | 0
0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
1 | 0
parseAuthorization
value ------------- Distribution ------------- count
-1 | 0
0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
1 | 0
parseDate
value ------------- Distribution ------------- count
-1 | 0
0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
1 | 0
parseQueryString
value ------------- Distribution ------------- count
-1 | 0
0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
1 | 0
parseUrlEncodedBody
value ------------- Distribution ------------- count
-1 | 0
0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
1 | 0
respond
value ------------- Distribution ------------- count
1 | 0
2 |@@@@ 1
4 | 0
8 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 9
16 | 0
slowHandler
value ------------- Distribution ------------- count
64 | 0
128 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 9
256 |@@@@ 1
512 | 0
getfoo
value ------------- Distribution ------------- count
64 | 0
128 |@@@@ 1
256 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 9
512 |
```
================================================
FILE: docs/guides/server.md
================================================
# Server Guide
Setting up a server is quick and easy. Here is a barebones echo server:
```js
var restify = require('restify');
function respond(req, res, next) {
res.send('hello ' + req.params.name);
next();
}
var server = restify.createServer();
server.get('/hello/:name', respond);
server.head('/hello/:name', respond);
server.listen(8080, function() {
console.log('%s listening at %s', server.name, server.url);
});
```
Try hitting that with the following curl commands to get a feel for what
restify is going to turn that into:
```sh
$ curl -is http://localhost:8080/hello/mark -H 'accept: text/plain'
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 10
Date: Mon, 31 Dec 2012 01:32:44 GMT
Connection: keep-alive
hello mark
$ curl -is http://localhost:8080/hello/mark
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 12
Date: Mon, 31 Dec 2012 01:33:33 GMT
Connection: keep-alive
"hello mark"
$ curl -is http://localhost:8080/hello/mark -X HEAD -H 'connection: close'
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 12
Date: Mon, 31 Dec 2012 01:42:07 GMT
Connection: close
```
Note that by default, curl uses `Connection: keep-alive`. In order to make the
HEAD method return right away, you'll need to pass `Connection: close`.
Since curl is often used with REST APIs, restify's plugins include a plugin to
work around this idiosyncrasy in curl. The plugin checks whether the user agent
is curl. If it is, it sets the Connection header to "close" and removes the
"Content-Length" header.
```js
server.pre(restify.plugins.pre.userAgentConnection());
```
## Sinatra style handler chains
Like many other Node.js based REST frameworks, restify leverages a Sinatra
style syntax for defining routes and the function handlers that service those
routes:
```js
server.get('/', function(req, res, next) {
res.send('home')
return next();
});
server.post('/foo',
function(req, res, next) {
req.someData = 'foo';
return next();
},
function(req, res, next) {
res.send(req.someData);
return next();
}
);
```
In a restify server, there are three distinct handler chains:
* `pre` - a handler chain executed prior to routing
* `use` - a handler chain executed post routing
* `{httpVerb}` - a handler chain executed specific to a route
All three handler chains accept either a single function, multiple functions,
or an array of functions.
## Universal pre-handlers: server.pre()
The `pre` handler chain is executed before routing. That means these handlers
will execute for an incoming request even if it's for a route that you did not
register. This can be useful for logging metrics or for cleaning up the
incoming request before routing it.
```js
// dedupe slashes in URL before routing
server.pre(restify.plugins.dedupeSlashes());
```
## Universal handlers: server.use()
The `use` handler chains is executed after a route has been chosen to service
the request. Function handlers that are attached via the `use()` method will be
run for all routes. Since restify runs handlers in the order they are
registered, make sure that all your `use()` calls happen before defining any
routes.
```js
server.use(function(req, res, next) {
console.warn('run for all routes!');
return next();
});
```
## Using next()
Upon completion of each function in the handler chain, you are responsible for
calling `next()`. Calling `next()` will move to the next function in the chain.
Unlike other REST frameworks, calling `res.send()` does not trigger `next()`
automatically. In many applications, work can continue to happen after
`res.send()`, so flushing the response is not synonmyous with completion of a
request.
In the normal case, `next()` does not typically take any parameters. If for
some reason you want to stop processing the request, you can call `next(false)`
to stop processing the request:
```js
server.use([
function(req, res, next) {
if (someCondition) {
res.send('done!');
return next(false);
}
return next();
},
function(req, res, next) {
// if someCondition is true, this handler is never executed
}
]);
```
`next()` also accepts any object for which `instanceof Error` is true, which
will cause restify to send that Error object as a response to the client. The
status code for the response will be inferred from the Error object's
`statusCode` property. If no `statusCode` is found, it will default to 500.
So the snippet below will send a serialized error to the client with an http
500:
```js
server.use(function(req, res, next) {
return next(new Error('boom!'));
});
```
And this will send a 404, since the `NotFoundError` constructor provides a
value of 404 for `statusCode`:
```js
server.use(function(req, res, next) {
return next(new NotFoundError('not here!'));
});
```
Calling `res.send()` with an Error object produces similar results, with this
snippet sending an http 500 with a serialized error the client:
```js
server.use(function(req, res, next) {
res.send(new Error('boom!'));
return next();
});
```
The difference between the two is that invoking `next()` with an Error object
allows you to leverage the server's event emitter.md#errors). This enables you
to handle all occurrences of an error type using a common handler. See the
Server API for more details.
## Routing
restify routing, in 'basic' mode, is pretty much identical to express/sinatra,
in that HTTP verbs are used with a parameterized resource to determine what
chain of handlers to run. Values associated with named placeholders are
available in `req.params`. Those values will be URL-decoded before being
passed to you.
```js
function send(req, res, next) {
res.send('hello ' + req.params.name);
return next();
}
server.post('/hello', function create(req, res, next) {
res.send(201, Math.random().toString(36).substr(3, 8));
return next();
});
server.put('/hello', send);
server.get('/hello/:name', send);
server.head('/hello/:name', send);
server.del('hello/:name', function rm(req, res, next) {
res.send(204);
return next();
});
```
You can also pass in a [RegExp](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/RegExp)
object and access the capture group with `req.params` (which will not
be interpreted in any way):
```js
server.get(/^\/([a-zA-Z0-9_\.~-]+)\/(.*)/, function(req, res, next) {
console.log(req.params[0]);
console.log(req.params[1]);
res.send(200);
return next();
});
```
Here any request like:
```sh
$ curl localhost:8080/foo/my/cats/name/is/gandalf
```
Would result in `req.params[0]` being `foo` and `req.params[1]` being
`my/cats/name/is/gandalf`.
Routes can be specified by any of the following http verbs - `del`, `get`,
`head`, `opts`, `post`, `put`, and `patch`.
```js
server.get(
'/foo/:id',
function(req, res, next) {
console.log('Authenticate');
return next();
},
function(req, res, next) {
res.send(200);
return next();
}
);
```
### Hypermedia
If a parameterized route was defined with a string (not a regex), you can
render it from other places in the server. This is useful to have HTTP
responses that link to other resources, without having to hardcode URLs
throughout the codebase. Both path and query strings parameters get URL encoded
appropriately.
```js
server.get({name: 'city', path: '/cities/:slug'}, /* ... */);
// in another route
res.send({
country: 'Australia',
// render a URL by specifying the route name and parameters
capital: server.router.render('city', {slug: 'canberra'}, {details: true})
});
```
Which returns:
```js
{
"country": "Australia",
"capital": "/cities/canberra?details=true"
}
```
### Versioned Routes
Most REST APIs tend to need versioning, and restify ships with support
for [semver](http://semver.org/) versioning in an `Accept-Version`
header, the same way you specify NPM version dependencies:
```js
var restify = require('restify');
var server = restify.createServer();
function sendV1(req, res, next) {
res.send('hello: ' + req.params.name);
return next();
}
function sendV2(req, res, next) {
res.send({ hello: req.params.name });
return next();
}
server.get('/hello/:name', restify.plugins.conditionalHandler([
{ version: '1.1.3', handler: sendV1 },
{ version: '2.0.0', handler: sendV2 }
]));
server.listen(8080);
```
Try hitting with:
```sh
$ curl -s localhost:8080/hello/mark
{"hello":"mark"}
$ curl -s -H 'accept-version: ~1' localhost:8080/hello/mark
"hello: mark"
$ curl -s -H 'accept-version: ~2' localhost:8080/hello/mark
{"hello":"mark"}
$ curl -s -H 'accept-version: ~3' localhost:8080/hello/mark | json
{
"code": "InvalidVersion",
"message": "~3 is not supported by GET /hello/mark"
}
```
In the first case, we didn't specify an `Accept-Version` header at all, so
restify treats that like sending a `*`. Much as not sending an `Accept` header
means the client gets the server's choice. Restify will choose this highest
matching route. In the second case, we explicitly asked for for V1, which got
us response a response from the version 1 handler function, but then we asked
for V2 and got back JSON. Finally, we asked for a version that doesn't exist
and got an error.
You can default the versions on routes by passing in a version field at server
creation time. Lastly, you can support multiple versions in the API by using
an array:
```js
server.get('/hello/:name' restify.plugins.conditionalHandler([
{ version: ['2.0.0', '2.1.0', '2.2.0'], handler: sendV2 }
]));
```
In this case you may need to know more information such as what the original
requested version string was, and what the matching version from the routes
supported version array was. Two methods make this info available:
```js
server.get('/version/test', restify.plugins.conditionalHandler([
{
version: ['2.0.0', '2.1.0', '2.2.0'],
handler: function (req, res, next) {
res.send(200, {
requestedVersion: req.version(),
matchedVersion: req.matchedVersion()
});
return next();
}
}
]));
```
Hitting this route will respond as below:
```sh
$ curl -s -H 'accept-version: <2.2.0' localhost:8080/version/test | json
{
"requestedVersion": "<2.2.0",
"matchedVersion": "2.1.0"
}
```
## Upgrade Requests
Incoming HTTP requests that contain a `Connection: Upgrade` header are treated
somewhat differently by the node HTTP server. If you want restify to push
Upgrade requests through the regular routing chain, you need to enable
`handleUpgrades` when creating the server.
To determine if a request is eligible for Upgrade, check for the existence of
`res.claimUpgrade()`. This method will return an object with two properties:
the `socket` of the underlying connection, and the first received data `Buffer`
as `head` (may be zero-length).
Once `res.claimUpgrade()` is called, `res` itself is marked unusable for
further HTTP responses; any later attempt to `send()` or `end()`, etc, will
throw an `Error`. Likewise if `res` has already been used to send at least
part of a response to the client, `res.claimUpgrade()` will throw an `Error`.
Upgrades and regular HTTP Response behaviour are mutually exclusive on any
particular connection.
Using the Upgrade mechanism, you can use a library like
[watershed](https://github.com/jclulow/node-watershed) to negotiate WebSockets
connections. For example:
```js
var ws = new Watershed();
server.get('/websocket/attach', function upgradeRoute(req, res, next) {
if (!res.claimUpgrade) {
next(new Error('Connection Must Upgrade For WebSockets'));
return;
}
var upgrade = res.claimUpgrade();
var shed = ws.accept(req, upgrade.socket, upgrade.head);
shed.on('text', function(msg) {
console.log('Received message from websocket client: ' + msg);
});
shed.send('hello there!');
next(false);
});
```
## Responses' Content Negotiation And Formatting
If you're using `res.send()` restify will determine the content-type to respond
with by, from highest priority to lowest priority:
1. using the value of `res.contentType` if present
1. otherwise, using the value of the `Content-Type` response header if set
1. otherwise, using `application/json` if the body is an object that is not a
Buffer instance
1. otherwise, negotiating the content-type by matching available formatters with
the request's `accept` header
If a content-type can't be determined, then restify will respond with an error.
If a content-type can be negotiated, restify then determines what formatter to
use to format the response's content.
If no formatter matching the content-type can be found, restify will by default
override the response's content-type to `'application/octet-stream'` and then
error if no formatter is found for that content-type.
This default behavior can be changed by passing `strictFormatters: false`
(default is true) when creating the restify server instance. In that case, if no
formatter is found for the negotiated content-type, the response is flushed
without applying any formatter.
Note in the examples above we've not defined any formatters, so we've been
leveraging the fact that restify ships with `application/json`, `text/plain` and
`application/octet-stream` formatters. You can add additional formatters to
restify by passing in a hash of content-type -> parser at server creation time:
```js
var server = restify.createServer({
formatters: {
'application/foo': function formatFoo(req, res, body) {
if (body instanceof Error)
return body.stack;
if (Buffer.isBuffer(body))
return body.toString('base64');
return util.inspect(body);
}
}
});
```
For example, attempting to send a content-type that does not have a defined
formatter:
```js
server.get('/foo', function(req, res, next) {
res.setHeader('content-type', 'text/css');
res.send('hi');
return next();
});
```
Will result in a response with a content-type of `application/octet-stream`:
```sh
$ curl -i localhost:3000/
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Length: 2
Date: Thu, 02 Jun 2016 06:50:54 GMT
Connection: keep-alive
```
However, if the server instance is created with `strictFormatters: false`:
```js
var server = restify.createServer({
strictFormatters: false
});
```
The response has a content-type of `text/css` even though no `'text/css'`
formatter is present:
```sh
$ curl -i localhost:3000/
HTTP/1.1 200 OK
Content-Type: text/css
Content-Length: 2
Date: Thu, 02 Jun 2016 06:50:54 GMT
Connection: keep-alive
```
As previously noted, restify ships with built-in formatters for json, text,
and binary. When you override or append to this, the "priority" might change;
to ensure that the priority is set to what you want, you should set a `q-value`
on your formatter definitions, which will ensure sorting happens the way you
want:
```js
restify.createServer({
formatters: {
'application/foo; q=0.9': function formatFoo(req, res, body) {
if (body instanceof Error)
return body.stack;
if (Buffer.isBuffer(body))
return body.toString('base64');
return util.inspect(body);
}
}
});
```
Restify ships with the following default formatters, which can be overridden
when passing a formatters options to `createServer()`:
* application/javascript
* application/json
* text/plain
* application/octet-stream
The restify response object retains has all the "raw" methods of a node
[ServerResponse](http://nodejs.org/docs/latest/api/http.html#http.ServerResponse)
on it as well.
```js
var body = 'hello world';
res.writeHead(200, {
'Content-Length': Buffer.byteLength(body),
'Content-Type': 'text/plain'
});
res.write(body);
res.end();
```
## Socket.IO
To use [socket.io](http://socket.io/) with restify, just treat your restify
server as if it were a "raw" node server:
```js
var server = restify.createServer();
var io = socketio.listen(server.server);
server.get('/', function indexHTML(req, res, next) {
fs.readFile(__dirname + '/index.html', function (err, data) {
if (err) {
next(err);
return;
}
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
res.end(data);
next();
});
});
io.sockets.on('connection', function (socket) {
socket.emit('news', { hello: 'world' });
socket.on('my other event', function (data) {
console.log(data);
});
});
server.listen(8080, function () {
console.log('socket.io server listening at %s', server.url);
});
```
================================================
FILE: docs/index.md
================================================
---
title: Quick Start
permalink: /docs/home/
redirect_from: /docs/
---
Setting up a server is quick and easy. Here is a barebones echo server:
```js
var restify = require('restify');
function respond(req, res, next) {
res.send('hello ' + req.params.name);
next();
}
var server = restify.createServer();
server.get('/hello/:name', respond);
server.head('/hello/:name', respond);
server.listen(8080, function() {
console.log('%s listening at %s', server.name, server.url);
});
```
Try hitting that with the following curl commands to get a feel for what
restify is going to turn that into:
```sh
$ curl -is http://localhost:8080/hello/mark -H 'accept: text/plain'
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 10
Date: Mon, 31 Dec 2012 01:32:44 GMT
Connection: keep-alive
hello mark
$ curl -is http://localhost:8080/hello/mark
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 12
Date: Mon, 31 Dec 2012 01:33:33 GMT
Connection: keep-alive
"hello mark"
$ curl -is http://localhost:8080/hello/mark -X HEAD -H 'connection: close'
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 12
Date: Mon, 31 Dec 2012 01:42:07 GMT
Connection: close
```
Note that by default, curl uses `Connection: keep-alive`. In order to make the
HEAD method return right away, you'll need to pass `Connection: close`.
Since curl is often used with REST APIs, restify's plugins include a plugin to
work around this idiosyncrasy in curl. The plugin checks whether the user agent
is curl. If it is, it sets the Connection header to "close" and removes the
"Content-Length" header.
```js
server.pre(restify.plugins.pre.userAgentConnection());
```
## Sinatra style handler chains
Like many other Node.js based REST frameworks, restify leverages a Sinatra
style syntax for defining routes and the function handlers that service those
routes:
```js
server.get('/', function(req, res, next) {
res.send('home')
return next();
});
server.post('/foo',
function(req, res, next) {
req.someData = 'foo';
return next();
},
function(req, res, next) {
res.send(req.someData);
return next();
}
);
```
In a restify server, there are three distinct handler chains:
* `pre` - a handler chain executed prior to routing
* `use` - a handler chain executed post routing
* `{httpVerb}` - a handler chain executed specific to a route
All three handler chains accept either a single function, multiple functions,
or an array of functions.
## Universal pre-handlers: server.pre()
The `pre` handler chain is executed before routing. That means these handlers
will execute for an incoming request even if it's for a route that you did not
register. This can be useful for logging metrics or for cleaning up the
incoming request before routing it.
```js
// dedupe slashes in URL before routing
server.pre(restify.plugins.pre.dedupeSlashes());
```
## Universal handlers: server.use()
The `use` handler chains is executed after a route has been chosen to service
the request. Function handlers that are attached via the `use()` method will be
run for all routes. Since restify runs handlers in the order they are
registered, make sure that all your `use()` calls happen before defining any
routes.
```js
server.use(function(req, res, next) {
console.warn('run for all routes!');
return next();
});
```
## Using next()
Upon completion of each function in the handler chain, you are responsible for
calling `next()`. Calling `next()` will move to the next function in the chain.
Unlike other REST frameworks, calling `res.send()` does not trigger `next()`
automatically. In many applications, work can continue to happen after
`res.send()`, so flushing the response is not synonymous with completion of a
request.
In the normal case, `next()` does not typically take any parameters. If for
some reason you want to stop processing the request, you can call `next(false)`
to stop processing the request:
```js
server.use([
function(req, res, next) {
if (someCondition) {
res.send('done!');
return next(false);
}
return next();
},
function(req, res, next) {
// if someCondition is true, this handler is never executed
}
]);
```
`next()` also accepts any object for which `instanceof Error` is true, which
will cause restify to send that Error object as a response to the client. The
status code for the response will be inferred from the Error object's
`statusCode` property. If no `statusCode` is found, it will default to 500.
So the snippet below will send a serialized error to the client with an http
500:
```js
server.use(function(req, res, next) {
return next(new Error('boom!'));
});
```
And this will send a 404, since the `NotFoundError` constructor provides a
value of 404 for `statusCode`:
```js
server.use(function(req, res, next) {
return next(new NotFoundError('not here!'));
});
```
Calling `res.send()` with an Error object produces similar results, with this
snippet sending an http 500 with a serialized error the client:
```js
server.use(function(req, res, next) {
res.send(new Error('boom!'));
return next();
});
```
The difference between the two is that invoking `next()` with an Error object
allows you to leverage the server's [event
emitter](/components/server.md#errors). This enables you to handle all
occurrences of an error type using a common handler. See the [error
handling](#error-handling) section for more details.
Lastly, you can call `next.ifError(err)` with an Error object to cause restify
to throw, bringing down the process. This can be useful if you an Error is
surfaced that cannot be handled, requiring you to kill the process.
## Routing
restify routing, in 'basic' mode, is pretty much identical to express/sinatra,
in that HTTP verbs are used with a parameterized resource to determine what
chain of handlers to run. Values associated with named placeholders are
available in `req.params`. Those values will be URL-decoded before being
passed to you.
```js
function send(req, res, next) {
res.send('hello ' + req.params.name);
return next();
}
server.post('/hello', function create(req, res, next) {
res.send(201, Math.random().toString(36).substr(3, 8));
return next();
});
server.put('/hello', send);
server.get('/hello/:name', send);
server.head('/hello/:name', send);
server.del('hello/:name', function rm(req, res, next) {
res.send(204);
return next();
});
```
You can also pass in a [RegExp](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/RegExp)
object and access the capture group with `req.params` (which will not
be interpreted in any way):
```js
server.get(/^\/([a-zA-Z0-9_\.~-]+)\/(.*)/, function(req, res, next) {
console.log(req.params[0]);
console.log(req.params[1]);
res.send(200);
return next();
});
```
Here any request like:
```sh
$ curl localhost:8080/foo/my/cats/name/is/gandalf
```
Would result in `req.params[0]` being `foo` and `req.params[1]` being
`my/cats/name/is/gandalf`.
Routes can be specified by any of the following http verbs - `del`, `get`,
`head`, `opts`, `post`, `put`, and `patch`.
```js
server.get(
'/foo/:id',
function(req, res, next) {
console.log('Authenticate');
return next();
},
function(req, res, next) {
res.send(200);
return next();
}
);
```
### Hypermedia
If a parameterized route was defined with a string (not a regex), you can
render it from other places in the server. This is useful to have HTTP
responses that link to other resources, without having to hardcode URLs
throughout the codebase. Both path and query strings parameters get URL encoded
appropriately.
```js
server.get({name: 'city', path: '/cities/:slug'}, /* ... */);
// in another route
res.send({
country: 'Australia',
// render a URL by specifying the route name and parameters
capital: server.router.render('city', {slug: 'canberra'}, {details: true})
});
```
Which returns:
```js
{
"country": "Australia",
"capital": "/cities/canberra?details=true"
}
```
### Versioned Routes
Most REST APIs tend to need versioning, and restify ships with support
for [semver](http://semver.org/) versioning in an `Accept-Version`
header, the same way you specify NPM version dependencies:
```js
var restify = require('restify');
var server = restify.createServer();
function sendV1(req, res, next) {
res.send('hello: ' + req.params.name);
return next();
}
function sendV2(req, res, next) {
res.send({ hello: req.params.name });
return next();
}
server.get('/hello/:name', restify.plugins.conditionalHandler([
{ version: '1.1.3', handler: sendV1 },
{ version: '2.0.0', handler: sendV2 }
]));
server.listen(8080);
```
Try hitting with:
```sh
$ curl -s localhost:8080/hello/mark
{"hello":"mark"}
$ curl -s -H 'accept-version: ~1' localhost:8080/hello/mark
"hello: mark"
$ curl -s -H 'accept-version: ~2' localhost:8080/hello/mark
{"hello":"mark"}
$ curl -s -H 'accept-version: ~3' localhost:8080/hello/mark | json
{
"code": "InvalidVersion",
"message": "~3 is not supported by GET /hello/mark"
}
```
In the first case, we didn't specify an `Accept-Version` header at all, so
restify treats that like sending a `*`. Much as not sending an `Accept` header
means the client gets the server's choice. Restify will choose this highest
matching route. In the second case, we explicitly asked for for V1, which got
us response a response from the version 1 handler function, but then we asked
for V2 and got back JSON. Finally, we asked for a version that doesn't exist
and got an error.
You can default the versions on routes by passing in a version field at server
creation time. Lastly, you can support multiple versions in the API by using
an array:
```js
server.get('/hello/:name', restify.plugins.conditionalHandler([
{ version: ['2.0.0', '2.1.0', '2.2.0'], handler: sendV2 }
]));
```
In this case you may need to know more information such as what the original
requested version string was, and what the matching version from the routes
supported version array was. Two methods make this info available:
```js
server.get('/version/test', restify.plugins.conditionalHandler([
{
version: ['2.0.0', '2.1.0', '2.2.0'],
handler: function (req, res, next) {
res.send(200, {
requestedVersion: req.version(),
matchedVersion: req.matchedVersion()
});
return next();
}
}
]));
```
Hitting this route will respond as below:
```sh
$ curl -s -H 'accept-version: <2.2.0' localhost:8080/version/test | json
{
"requestedVersion": "<2.2.0",
"matchedVersion": "2.1.0"
}
```
## Upgrade Requests
Incoming HTTP requests that contain a `Connection: Upgrade` header are treated
somewhat differently by the node HTTP server. If you want restify to push
Upgrade requests through the regular routing chain, you need to enable
`handleUpgrades` when creating the server.
To determine if a request is eligible for Upgrade, check for the existence of
`res.claimUpgrade()`. This method will return an object with two properties:
the `socket` of the underlying connection, and the first received data `Buffer`
as `head` (may be zero-length).
Once `res.claimUpgrade()` is called, `res` itself is marked unusable for
further HTTP responses; any later attempt to `send()` or `end()`, etc, will
throw an `Error`. Likewise if `res` has already been used to send at least
part of a response to the client, `res.claimUpgrade()` will throw an `Error`.
Upgrades and regular HTTP Response behaviour are mutually exclusive on any
particular connection.
Using the Upgrade mechanism, you can use a library like
[watershed](https://github.com/jclulow/node-watershed) to negotiate WebSockets
connections. For example:
```js
var ws = new Watershed();
server.get('/websocket/attach', function upgradeRoute(req, res, next) {
if (!res.claimUpgrade) {
next(new Error('Connection Must Upgrade For WebSockets'));
return;
}
var upgrade = res.claimUpgrade();
var shed = ws.accept(req, upgrade.socket, upgrade.head);
shed.on('text', function(msg) {
console.log('Received message from websocket client: ' + msg);
});
shed.send('hello there!');
next(false);
});
```
## Responses' Content Negotiation And Formatting
If you're using `res.send()` restify will determine the content-type to respond
with by, from highest priority to lowest priority:
1. using the value of `res.contentType` if present
1. otherwise, using the value of the `Content-Type` response header if set
1. otherwise, using `application/json` if the body is an object that is not a
Buffer instance
1. otherwise, negotiating the content-type by matching available formatters with
the request's `accept` header
If a content-type can't be determined, then restify will respond with an error.
If a content-type can be negotiated, restify then determines what formatter to
use to format the response's content.
If no formatter matching the content-type can be found, restify will by default
override the response's content-type to `'application/octet-stream'` and then
error if no formatter is found for that content-type.
This default behavior can be changed by passing `strictFormatters: false`
(default is false) when creating the restify server instance. In that case, if
no formatter is found for the negotiated content-type, the response is flushed
without applying any formatter.
Note in the examples above we've not defined any formatters, so we've been
leveraging the fact that restify ships with `application/json`, `text/plain` and
`application/octet-stream` formatters. You can add additional formatters to
restify by passing in a hash of content-type -> parser at server creation time:
```js
var server = restify.createServer({
formatters: {
'application/foo': function formatFoo(req, res, body) {
if (body instanceof Error)
return body.stack;
if (Buffer.isBuffer(body))
return body.toString('base64');
return util.inspect(body);
}
}
});
```
For example, attempting to send a content-type that does not have a defined
formatter:
```js
server.get('/foo', function(req, res, next) {
res.setHeader('content-type', 'text/css');
res.send('hi');
return next();
});
```
Will result in a response with a content-type of `application/octet-stream`:
```sh
$ curl -i localhost:3000/
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Length: 2
Date: Thu, 02 Jun 2016 06:50:54 GMT
Connection: keep-alive
```
However, if the server instance is created with `strictFormatters:false`:
```js
var server = restify.createServer({
strictFormatters: false
});
```
The response would has a content-type of `text/css` even though no `'text/css'`
formatter is present:
```sh
$ curl -i localhost:3000/
HTTP/1.1 200 OK
Content-Type: text/css
Content-Length: 2
Date: Thu, 02 Jun 2016 06:50:54 GMT
Connection: keep-alive
```
As previously noted, restify ships with built-in formatters for json, text,
and binary. When you override or append to this, the "priority" might change;
to ensure that the priority is set to what you want, you should set a `q-value`
on your formatter definitions, which will ensure sorting happens the way you
want:
```js
restify.createServer({
formatters: {
'application/foo; q=0.9': function formatFoo(req, res, body) {
if (body instanceof Error)
return body.stack;
if (Buffer.isBuffer(body))
return body.toString('base64');
return util.inspect(body);
}
}
});
```
Restify ships with the following default formatters, which can be overridden
when passing a formatters options to `createServer()`:
* application/javascript
* application/json
* text/plain
* application/octet-stream
The restify response object retains has all the "raw" methods of a node
[ServerResponse](http://nodejs.org/docs/latest/api/http.html#http.ServerResponse)
on it as well.
```js
var body = 'hello world';
res.writeHead(200, {
'Content-Length': Buffer.byteLength(body),
'Content-Type': 'text/plain'
});
res.write(body);
res.end();
```
## Error handling
It is common to want to handle an error conditions the same way. As an example,
you may want to serve a 500 page on all `InternalServerErrors`. In this case,
you can add a listener for the `InternalServer` error event that is always
fired when this Error is encountered by restify as part of a `next(error)`
statement. This gives you a way to handle all errors of the same class
identically across the server. You can also use a generic `restifyError` event
which will catch errors of all types.
An example of sending a 404:
```js
var errs = require('restify-errors');
server.get('/hello/:foo', function(req, res, next) {
// resource not found error
var err = new errs.NotFoundError('oh noes!');
return next(err);
});
server.on('NotFound', function (req, res, err, cb) {
// do not call res.send! you are now in an error context and are outside
// of the normal next chain. you can log or do metrics here, and invoke
// the callback when you're done. restify will automtically render the
// NotFoundError depending on the content-type header you have set in your
// response.
return cb();
});
```
For customizing the error being sent back to the client:
```js
var errs = require('restify-errors');
server.get('/hello/:name', function(req, res, next) {
// some internal unrecoverable error
var err = new errs.InternalServerError('oh noes!');
return next(err);
});
server.on('InternalServer', function (req, res, err, cb) {
// by default, restify will usually render the Error object as plaintext or
// JSON depending on content negotiation. the default text formatter and JSON
// formatter are pretty simple, they just call toString() and toJSON() on the
// object being passed to res.send, which in this case, is the error object.
// so to customize what it sent back to the client when this error occurs,
// you would implement as follows:
// for any response that is text/plain
err.toString = function toString() {
return 'an internal server error occurred!';
};
// for any response that is application/json
err.toJSON = function toJSON() {
return {
message: 'an internal server error occurred!',
code: 'boom!'
}
};
return cb();
});
server.on('restifyError', function (req, res, err, cb) {
// this listener will fire after both events above!
// `err` here is the same as the error that was passed to the above
// error handlers.
return cb();
});
```
Here is another example of `InternalServerError`, but this time with a custom
formatter:
```js
const errs = require('restify-errors');
const server = restify.createServer({
formatters: {
'text/html': function(req, res, body) {
if (body instanceof Error) {
// body here is an instance of InternalServerError
return '' + body.message + '';
}
}
}
});
server.get('/', function(req, res, next) {
res.header('content-type', 'text/html');
return next(new errs.InternalServerError('oh noes!'));
});
```
### restify-errors
A module called restify-errors exposes a suite of error constructors for many
common http and REST related errors. These constructors can be used in
conjunction with the `next(err)` pattern to easily leverage the server's event
emitter. The full list of constructors can be viewed over at the
[restify-errors](https://github.com/restify/errors) repository. Here are some
examples:
```js
var errs = require('restify-errors');
server.get('/', function(req, res, next) {
return next(new errs.ConflictError("I just don't like you"));
});
```
```sh
$ curl -is localhost:3000
HTTP/1.1 409 Conflict
Content-Type: application/json
Content-Length: 53
Date: Fri, 03 Jun 2016 20:29:45 GMT
Connection: keep-alive
{"code":"Conflict","message":"I just don't like you"}
```
When using restify-errors, you can also directly call `res.send(err)`, and
restify will automatically serialize your error for you:
```js
var errs = require('restify-errors');
server.get('/', function(req, res, next) {
res.send(new errs.GoneError('gone girl'));
return next();
});
```
```sh
$ curl -is localhost:8080/
HTTP/1.1 410 Gone
Content-Type: application/json
Content-Length: 37
Date: Fri, 03 Jun 2016 20:17:48 GMT
Connection: keep-alive
{"code":"Gone","message":"gone girl"}
```
This automatic serialization happens because the JSON formatter will call
`JSON.stringify()` on the Error object, and all restify-errors have a `toJSON`
method defined. Compare this to a standard Error object which does not have
`toJSON` defined:
```js
server.get('/sendErr', function(req, res, next) {
res.send(new Error('where is my msg?'));
return next();
});
server.get('/nextErr', function(req, res, next) {
return next(new Error('where is my msg?'));
});
```
```sh
$ curl -is localhost:8080/sendErr
HTTP/1.1 410 Gone
Content-Type: application/json
Content-Length: 37
Date: Fri, 03 Jun 2016 20:17:48 GMT
Connection: keep-alive
{}
$ curl -is localhost:8080/nextErr
HTTP/1.1 410 Gone
Content-Type: application/json
Content-Length: 37
Date: Fri, 03 Jun 2016 20:17:48 GMT
Connection: keep-alive
{}
```
If you want to use custom errors, make sure you have `toJSON` defined, or use
restify-error's `makeConstructor()` method to automatically create Errors that
are supported with with `toJSON`.
#### HttpError
restify-errors provides constructors that inherit from either HttpError or
RestError. All HttpErrors have a numeric http `statusCode` and `body`
properties. The statusCode will automatically set the HTTP response status
code, and the body attribute by default will be the message.
All status codes between 400 and 5xx are automatically converted into
an HttpError with the name being 'PascalCase' and spaces removed. For
the complete list, take a look at the
[node source](https://github.com/nodejs/node/blob/master/lib/_http_server.js#L17).
From that code above `418: I'm a teapot` would be `ImATeapotError`, as
an example.
#### RestError
A common problem with REST APIs and HTTP is that they often end
up needing to overload 400 and 409 to mean a bunch of different
things. There's no real standard on what to do in these cases, but in
general you want machines to be able to (safely) parse these things
out, and so restify defines a convention of a `RestError`. A
`RestError` is a subclass of one of the particular `HttpError` types,
and additionally sets the body attribute to be a JS object with the
attributes `code` and `message`. For example, here's a built-in RestError:
```js
var errs = require('restify-errors');
var server = restify.createServer();
server.get('/hello/:name', function(req, res, next) {
return next(new errs.InvalidArgumentError("I just don't like you"));
});
$ curl -is localhost:8080/hello/mark | json
HTTP/1.1 409 Conflict
Content-Type: application/json
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET
Access-Control-Allow-Headers: Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, Api-Version
Access-Control-Expose-Headers: Api-Version, Request-Id, Response-Time
Connection: close
Content-Length: 60
Content-MD5: MpEcO5EQFUZ2MNeUB2VaZg==
Date: Tue, 03 Jan 2012 00:50:21 GMT
Server: restify
Request-Id: bda456dd-2fe4-478d-809c-7d159d58d579
Response-Time: 3
{
"code": "InvalidArgument",
"message": "I just don't like you"
}
```
The built-in HttpErrors are:
* BadRequestError (400 Bad Request)
* UnauthorizedError (401 Unauthorized)
* PaymentRequiredError (402 Payment Required)
* ForbiddenError (403 Forbidden)
* NotFoundError (404 Not Found)
* MethodNotAllowedError (405 Method Not Allowed)
* NotAcceptableError (406 Not Acceptable)
* ProxyAuthenticationRequiredError (407 Proxy Authentication Required)
* RequestTimeoutError (408 Request Time-out)
* ConflictError (409 Conflict)
* GoneError (410 Gone)
* LengthRequiredError (411 Length Required)
* PreconditionFailedError (412 Precondition Failed)
* RequestEntityTooLargeError (413 Request Entity Too Large)
* RequesturiTooLargeError (414 Request-URI Too Large)
* UnsupportedMediaTypeError (415 Unsupported Media Type)
* RequestedRangeNotSatisfiableError (416 Requested Range Not Satisfiable)
* ExpectationFailedError (417 Expectation Failed)
* ImATeapotError (418 I'm a teapot)
* UnprocessableEntityError (422 Unprocessable Entity)
* LockedError (423 Locked)
* FailedDependencyError (424 Failed Dependency)
* UnorderedCollectionError (425 Unordered Collection)
* UpgradeRequiredError (426 Upgrade Required)
* PreconditionRequiredError (428 Precondition Required)
* TooManyRequestsError (429 Too Many Requests)
* RequestHeaderFieldsTooLargeError (431 Request Header Fields Too Large)
* InternalServerError (500 Internal Server Error)
* NotImplementedError (501 Not Implemented)
* BadGatewayError (502 Bad Gateway)
* ServiceUnavailableError (503 Service Unavailable)
* GatewayTimeoutError (504 Gateway Time-out)
* HttpVersionNotSupportedError (505 HTTP Version Not Supported)
* VariantAlsoNegotiatesError (506 Variant Also Negotiates)
* InsufficientStorageError (507 Insufficient Storage)
* BandwidthLimitExceededError (509 Bandwidth Limit Exceeded)
* NotExtendedError (510 Not Extended)
* NetworkAuthenticationRequiredError (511 Network Authentication Required)
* BadDigestError (400 Bad Request)
* BadMethodError (405 Method Not Allowed)
* InternalError (500 Internal Server Error)
* InvalidArgumentError (409 Conflict)
* InvalidContentError (400 Bad Request)
* InvalidCredentialsError (401 Unauthorized)
* InvalidHeaderError (400 Bad Request)
* InvalidVersionError (400 Bad Request)
* MissingParameterError (409 Conflict)
* NotAuthorizedError (403 Forbidden)
* RequestExpiredError (400 Bad Request)
* RequestThrottledError (429 Too Many Requests)
* ResourceNotFoundError (404 Not Found)
* WrongAcceptError (406 Not Acceptable)
And the built in RestErrors are:
* 400 BadDigestError
* 405 BadMethodError
* 500 InternalError
* 409 InvalidArgumentError
* 400 InvalidContentError
* 401 InvalidCredentialsError
* 400 InvalidHeaderError
* 400 InvalidVersionError
* 409 MissingParameterError
* 403 NotAuthorizedError
* 412 PreconditionFailedError
* 400 RequestExpiredError
* 429 RequestThrottledError
* 404 ResourceNotFoundError
* 406 WrongAcceptError
You can also create your own subclasses using the `makeConstructor` method:
```js
var errs = require('restify-errors');
var restify = require('restify');
errs.makeConstructor('ZombieApocalypseError');
var myErr = new errs.ZombieApocalypseError('zomg!');
```
The constructor takes `message`, `statusCode`, `restCode`, and `context`
options. Please check out the restify-errors repo for more information.
## Socket.IO
To use [socket.io](http://socket.io/) with restify, just treat your restify
server as if it were a "raw" node server:
```js
var server = restify.createServer();
var io = socketio.listen(server.server);
server.get('/', function indexHTML(req, res, next) {
fs.readFile(__dirname + '/index.html', function (err, data) {
if (err) {
next(err);
return;
}
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
res.end(data);
next();
});
});
io.sockets.on('connection', function (socket) {
socket.emit('news', { hello: 'world' });
socket.on('my other event', function (data) {
console.log(data);
});
});
server.listen(8080, function () {
console.log('socket.io server listening at %s', server.url);
});
```
================================================
FILE: examples/dtrace/demo.js
================================================
'use strict';
// There's an example D script here to showcase a "slow" handler where it's
// wildcard'd by the route name. In "real life" you'd probably start with a
// d script that breaks down the route -start and -done, and then you'd want
// to see which handler is taking longest from there.
//
// $ node demo.js
// $ curl localhost:9080/foo/bar
// $ sudo ./handler-timing.d
// ^C
//
// handler-6
// value ------------- Distribution ------------- count
// -1 | 0
// 0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
// 1 | 0
//
// parseAccept
// value ------------- Distribution ------------- count
// -1 | 0
// 0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
// 1 | 0
//
// parseAuthorization
// value ------------- Distribution ------------- count
// -1 | 0
// 0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
// 1 | 0
//
// parseDate
// value ------------- Distribution ------------- count
// -1 | 0
// 0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
// 1 | 0
//
// parseQueryString
// value ------------- Distribution ------------- count
// -1 | 0
// 0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
// 1 | 0
//
// parseUrlEncodedBody
// value ------------- Distribution ------------- count
// -1 | 0
// 0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 10
// 1 | 0
//
// sendResult
// value ------------- Distribution ------------- count
// 1 | 0
// 2 |@@@@ 1
// 4 | 0
// 8 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 9
// 16 | 0
//
// slowHandler
// value ------------- Distribution ------------- count
// 64 | 0
// 128 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 9
// 256 |@@@@ 1
// 512 | 0
//
// getfoo
// value ------------- Distribution ------------- count
// 64 | 0
// 128 |@@@@ 1
// 256 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 9
// 512 | 0
var restify = require('../../lib');
var Logger = require('pino');
///--- Globals
var NAME = 'exampleapp';
///--- Mainline
var log = new Logger({
name: NAME,
level: 'trace',
base: { service: NAME }
});
var server = restify.createServer({
name: NAME,
Logger: log,
dtrace: true,
formatters: {
'application/foo': function(req, res, body) {
if (body instanceof Error) {
body = body.stack;
} else if (Buffer.isBuffer(body)) {
body = body.toString('base64');
} else {
switch (typeof body) {
case 'boolean':
case 'number':
case 'string':
body = body.toString();
break;
case 'undefined':
body = '';
break;
default:
body =
body === null
? ''
: 'Demoing application/foo formatter; ' +
JSON.stringify(body);
break;
}
}
return body;
}
}
});
server.use(restify.plugins.acceptParser(server.acceptable));
server.use(restify.plugins.authorizationParser());
server.use(restify.plugins.dateParser());
server.use(restify.plugins.queryParser());
server.use(restify.plugins.urlEncodedBodyParser());
server.use(function slowHandler(req, res, next) {
setTimeout(function() {
next();
}, 250);
});
server.get(
{ url: '/foo/:id', name: 'GetFoo' },
function(req, res, next) {
next();
},
function sendResult(req, res, next) {
res.contentType = 'application/foo';
res.send({
hello: req.params.id
});
next();
}
);
server.head('/foo/:id', function(req, res, next) {
res.send({
hello: req.params.id
});
next();
});
server.put('/foo/:id', function(req, res, next) {
res.send({
hello: req.params.id
});
next();
});
server.post('/foo/:id', function(req, res, next) {
res.json(201, req.params);
next();
});
server.del('/foo/:id', function(req, res, next) {
res.send(204);
next();
});
server.on('after', function(req, res, name) {
req.log.info('%s just finished: %d.', name, res.code);
});
server.on('NotFound', function(req, res) {
res.send(404, req.url + ' was not found');
});
server.listen(9080, function() {
log.info('listening: %s', server.url);
});
================================================
FILE: examples/dtrace/handler-timing.d
================================================
#!/usr/sbin/dtrace -s
#pragma D option quiet
restify*:::route-start
{
track[arg2] = timestamp;
}
restify*:::handler-start
/track[arg3]/
{
h[arg3, copyinstr(arg2)] = timestamp;
}
restify*:::handler-done
/track[arg3] && h[arg3, copyinstr(arg2)]/
{
@[copyinstr(arg2)] = quantize((timestamp - h[arg3, copyinstr(arg2)]) / 1000000);
h[arg3, copyinstr(arg2)] = 0;
}
restify*:::route-done
/track[arg2]/
{
@[copyinstr(arg1)] = quantize((timestamp - track[arg2]) / 1000000);
track[arg2] = 0;
}
================================================
FILE: examples/dtrace/hello.js
================================================
var restify = require('../../lib');
var server = restify.createServer({
name: 'helloworld',
dtrace: true
});
server.use(restify.plugins.acceptParser(server.acceptable));
server.use(restify.plugins.authorizationParser());
server.use(restify.plugins.dateParser());
server.use(restify.plugins.queryParser());
server.use(restify.plugins.urlEncodedBodyParser());
server.use(function slowHandler(req, res, next) {
setTimeout(function() {
next();
}, 250);
});
server.get(
{
path: '/hello/:name',
name: 'GetFoo'
},
function respond(req, res, next) {
res.send({
hello: req.params.name
});
next();
}
);
server.listen(8080, function() {
console.log('listening: %s', server.url);
});
================================================
FILE: examples/example.js
================================================
'use strict';
var restify = require('../lib');
var server = restify.createServer();
server.pre(function pre(req, res, next) {
console.log('pre');
next();
});
server.use(function use(req, res, next) {
console.log('use');
next();
});
server.on('after', function(req, res, route, err) {
console.log('after');
});
server.get(
'/:userId',
function onRequest(req, res, next) {
console.log(req.url, '1');
next();
},
function onRequest(req, res, next) {
console.log(req.url, '2');
res.send({ hello: 'world' });
next();
}
);
server.listen(3001);
================================================
FILE: examples/http2/http2.js
================================================
var path = require('path');
var fs = require('fs');
var pino = require('pino');
var restify = require('../../lib');
var srv = restify.createServer({
http2: {
cert: fs.readFileSync(path.join(__dirname, './keys/http2-cert.pem')),
key: fs.readFileSync(path.join(__dirname, './keys/http2-key.pem')),
ca: fs.readFileSync(path.join(__dirname, 'keys/http2-csr.pem')),
allowHTTP1: true //allow incoming connections that do not support HTTP/2 to be downgraded to HTTP/1.x
}
});
srv.get('/', function(req, res, next) {
res.send({ hello: 'world' });
next();
});
srv.on(
'after',
restify.plugins.auditLogger({
event: 'after',
body: true,
log: pino(
{ name: 'audit' },
process.stdout
)
})
);
srv.listen(8080, function() {
console.log('ready on %s', srv.url);
});
================================================
FILE: examples/http2/keys/http2-cert.pem
================================================
-----BEGIN CERTIFICATE-----
MIICHzCCAYgCCQCPPSUAa8QZojANBgkqhkiG9w0BAQUFADBUMQswCQYDVQQGEwJS
VTETMBEGA1UECBMKU29tZS1TdGF0ZTENMAsGA1UEBxMET21zazEhMB8GA1UEChMY
SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTExMDQwOTEwMDY0NVoXDTExMDUw
OTEwMDY0NVowVDELMAkGA1UEBhMCUlUxEzARBgNVBAgTClNvbWUtU3RhdGUxDTAL
BgNVBAcTBE9tc2sxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCB
nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1bn25sPkv46wl70BffxradlkRd/x
p5Xf8HDhPSfzNNctERYslXT2fX7Dmfd5w1XTVqqGqJ4izp5VewoVOHA8uavo3ovp
gNWasil5zADWaM1T0nnV0RsFbZWzOTmm1U3D48K8rW3F5kOZ6f4yRq9QT1gF/gN7
5Pt494YyYyJu/a8CAwEAATANBgkqhkiG9w0BAQUFAAOBgQBuRZisIViI2G/R+w79
vk21TzC/cJ+O7tKsseDqotXYTH8SuimEH5IWcXNgnWhNzczwN8s2362NixyvCipV
yd4wzMpPbjIhnWGM0hluWZiK2RxfcqimIBjDParTv6CMUIuwGQ257THKY8hXGg7j
Uws6Lif3P9UbsuRiYPxMgg98wg==
-----END CERTIFICATE-----
================================================
FILE: examples/http2/keys/http2-csr.pem
================================================
-----BEGIN CERTIFICATE REQUEST-----
MIIBkzCB/QIBADBUMQswCQYDVQQGEwJSVTETMBEGA1UECBMKU29tZS1TdGF0ZTEN
MAsGA1UEBxMET21zazEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDVufbmw+S/jrCXvQF9/Gtp2WRF
3/Gnld/wcOE9J/M01y0RFiyVdPZ9fsOZ93nDVdNWqoaoniLOnlV7ChU4cDy5q+je
i+mA1ZqyKXnMANZozVPSedXRGwVtlbM5OabVTcPjwrytbcXmQ5np/jJGr1BPWAX+
A3vk+3j3hjJjIm79rwIDAQABoAAwDQYJKoZIhvcNAQEFBQADgYEAiNWhz6EppIVa
FfUaB3sLeqfamb9tg9kBHtvqj/FJni0snqms0kPWaTySEPHZF0irIb7VVdq/sVCb
3gseMVSyoDvPJ4lHC3PXqGQ7kM1mIPhDnR/4HDA3BhlGhTXSDIHgZnvI+HMBdsyC
hC3dz5odyKqe4nmoofomALkBL9t4H8s=
-----END CERTIFICATE REQUEST-----
================================================
FILE: examples/http2/keys/http2-key.pem
================================================
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDVufbmw+S/jrCXvQF9/Gtp2WRF3/Gnld/wcOE9J/M01y0RFiyV
dPZ9fsOZ93nDVdNWqoaoniLOnlV7ChU4cDy5q+jei+mA1ZqyKXnMANZozVPSedXR
GwVtlbM5OabVTcPjwrytbcXmQ5np/jJGr1BPWAX+A3vk+3j3hjJjIm79rwIDAQAB
AoGAAv2QI9h32epQND9TxwSCKD//dC7W/cZOFNovfKCTeZjNK6EIzKqPTGA6smvR
C1enFl5adf+IcyWqAoe4lkqTvurIj+2EhtXdQ8DBlVuXKr3xvEFdYxXPautdTCF6
KbXEyS/s1TZCRFjYftvCrXxc3pK45AQX/wg7z1K+YB5pyIECQQD0OJvLoxLYoXAc
FZraIOZiDsEbGuSHqoCReFXH75EC3+XGYkH2bQ/nSIZ0h1buuwQ/ylKXOlTPT3Qt
Xm1OQEBvAkEA4AjWsIO/rRpOm/Q2aCrynWMpoUXTZSbL2yGf8pxp/+8r2br5ier0
M1LeBb/OPY1+k39NWLXxQoo64xoSFYk2wQJAd2wDCwX4HkR7HNCXw1hZL9QFK6rv
20NN0VSlpboJD/3KT0MW/FiCcVduoCbaJK0Au+zEjDyy4hj5N4I4Mw6KMwJAXVAx
I+psTsxzS4/njXG+BgIEl/C+gRYsuMQDnAi8OebDq/et8l0Tg8ETSu++FnM18neG
ntmBeMacinUUbTXuwQJBAJp/onZdsMzeVulsGrqR1uS+Lpjc5Q1gt5ttt2cxj91D
rio48C/ZvWuKNE8EYj2ALtghcVKRvgaWfOxt2GPguGg=
-----END RSA PRIVATE KEY-----
================================================
FILE: examples/jsonp/jsonp.js
================================================
var restify = require('../../lib');
var srv = restify.createServer();
srv.use(restify.plugins.queryParser());
srv.use(restify.plugins.jsonp());
srv.get('/', function(req, res, next) {
res.send({ hello: 'world' });
next();
});
srv.listen(8080, function() {
console.log('ready on %s', srv.url);
});
================================================
FILE: examples/sockio/package.json
================================================
{
"name": "restify-example",
"version": "0.0.0",
"description": "Socket.io example",
"main": "sockio.js",
"dependencies": {
"socket.io": "^4.5.0"
},
"scripts": {
"start": "node sockio.js"
}
}
================================================
FILE: examples/sockio/sockio.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var socketio = require('socket.io');
var restify = require('../../lib');
///--- Globals
var HTML =
'\n' +
'';
///--- Mainline
var server = restify.createServer();
var io = socketio(server.server);
server.get('/', function indexHTML(req, res, next) {
res.setHeader('Content-Type', 'text/html');
res.setHeader('Content-Length', Buffer.byteLength(HTML));
res.writeHead(200);
res.write(HTML);
res.end();
next();
});
io.on('connection', function(socket) {
socket.emit('news', { hello: 'world' });
socket.on('my other event', function(data) {
console.log(data);
});
});
server.listen(8080, function() {
console.log('socket.io server listening at %s', server.url);
});
================================================
FILE: examples/spdy/keys/spdy-cert.pem
================================================
-----BEGIN CERTIFICATE-----
MIICHzCCAYgCCQCPPSUAa8QZojANBgkqhkiG9w0BAQUFADBUMQswCQYDVQQGEwJS
VTETMBEGA1UECBMKU29tZS1TdGF0ZTENMAsGA1UEBxMET21zazEhMB8GA1UEChMY
SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTExMDQwOTEwMDY0NVoXDTExMDUw
OTEwMDY0NVowVDELMAkGA1UEBhMCUlUxEzARBgNVBAgTClNvbWUtU3RhdGUxDTAL
BgNVBAcTBE9tc2sxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCB
nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1bn25sPkv46wl70BffxradlkRd/x
p5Xf8HDhPSfzNNctERYslXT2fX7Dmfd5w1XTVqqGqJ4izp5VewoVOHA8uavo3ovp
gNWasil5zADWaM1T0nnV0RsFbZWzOTmm1U3D48K8rW3F5kOZ6f4yRq9QT1gF/gN7
5Pt494YyYyJu/a8CAwEAATANBgkqhkiG9w0BAQUFAAOBgQBuRZisIViI2G/R+w79
vk21TzC/cJ+O7tKsseDqotXYTH8SuimEH5IWcXNgnWhNzczwN8s2362NixyvCipV
yd4wzMpPbjIhnWGM0hluWZiK2RxfcqimIBjDParTv6CMUIuwGQ257THKY8hXGg7j
Uws6Lif3P9UbsuRiYPxMgg98wg==
-----END CERTIFICATE-----
================================================
FILE: examples/spdy/keys/spdy-csr.pem
================================================
-----BEGIN CERTIFICATE REQUEST-----
MIIBkzCB/QIBADBUMQswCQYDVQQGEwJSVTETMBEGA1UECBMKU29tZS1TdGF0ZTEN
MAsGA1UEBxMET21zazEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDVufbmw+S/jrCXvQF9/Gtp2WRF
3/Gnld/wcOE9J/M01y0RFiyVdPZ9fsOZ93nDVdNWqoaoniLOnlV7ChU4cDy5q+je
i+mA1ZqyKXnMANZozVPSedXRGwVtlbM5OabVTcPjwrytbcXmQ5np/jJGr1BPWAX+
A3vk+3j3hjJjIm79rwIDAQABoAAwDQYJKoZIhvcNAQEFBQADgYEAiNWhz6EppIVa
FfUaB3sLeqfamb9tg9kBHtvqj/FJni0snqms0kPWaTySEPHZF0irIb7VVdq/sVCb
3gseMVSyoDvPJ4lHC3PXqGQ7kM1mIPhDnR/4HDA3BhlGhTXSDIHgZnvI+HMBdsyC
hC3dz5odyKqe4nmoofomALkBL9t4H8s=
-----END CERTIFICATE REQUEST-----
================================================
FILE: examples/spdy/keys/spdy-key.pem
================================================
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDVufbmw+S/jrCXvQF9/Gtp2WRF3/Gnld/wcOE9J/M01y0RFiyV
dPZ9fsOZ93nDVdNWqoaoniLOnlV7ChU4cDy5q+jei+mA1ZqyKXnMANZozVPSedXR
GwVtlbM5OabVTcPjwrytbcXmQ5np/jJGr1BPWAX+A3vk+3j3hjJjIm79rwIDAQAB
AoGAAv2QI9h32epQND9TxwSCKD//dC7W/cZOFNovfKCTeZjNK6EIzKqPTGA6smvR
C1enFl5adf+IcyWqAoe4lkqTvurIj+2EhtXdQ8DBlVuXKr3xvEFdYxXPautdTCF6
KbXEyS/s1TZCRFjYftvCrXxc3pK45AQX/wg7z1K+YB5pyIECQQD0OJvLoxLYoXAc
FZraIOZiDsEbGuSHqoCReFXH75EC3+XGYkH2bQ/nSIZ0h1buuwQ/ylKXOlTPT3Qt
Xm1OQEBvAkEA4AjWsIO/rRpOm/Q2aCrynWMpoUXTZSbL2yGf8pxp/+8r2br5ier0
M1LeBb/OPY1+k39NWLXxQoo64xoSFYk2wQJAd2wDCwX4HkR7HNCXw1hZL9QFK6rv
20NN0VSlpboJD/3KT0MW/FiCcVduoCbaJK0Au+zEjDyy4hj5N4I4Mw6KMwJAXVAx
I+psTsxzS4/njXG+BgIEl/C+gRYsuMQDnAi8OebDq/et8l0Tg8ETSu++FnM18neG
ntmBeMacinUUbTXuwQJBAJp/onZdsMzeVulsGrqR1uS+Lpjc5Q1gt5ttt2cxj91D
rio48C/ZvWuKNE8EYj2ALtghcVKRvgaWfOxt2GPguGg=
-----END RSA PRIVATE KEY-----
================================================
FILE: examples/spdy/spdy.js
================================================
var path = require('path');
var fs = require('fs');
var pino = require('pino');
var restify = require('../../lib');
var srv = restify.createServer({
spdy: {
cert: fs.readFileSync(path.join(__dirname, './keys/spdy-cert.pem')),
key: fs.readFileSync(path.join(__dirname, './keys/spdy-key.pem')),
ca: fs.readFileSync(path.join(__dirname, 'keys/spdy-csr.pem'))
}
});
srv.get('/', function(req, res, next) {
res.send({ hello: 'world' });
next();
});
srv.on(
'after',
restify.plugins.auditLogger({
event: 'after',
body: true,
log: pino({name: 'audit'})
})
);
srv.listen(8080, function() {
console.log('ready on %s', srv.url);
});
================================================
FILE: examples/todoapp/README.md
================================================
# tl;dr
This is a (small) sample app using a reasonable subset of restify components to
illustrate how you go about structuring a restify application. This is pretty
minimal, and most logic is contained in `server.js`; in reality, you'd probably
break up your logic into a set of files that are easier to maintain. But for
the purpose of this app, that suffices.
# What is it?
I just cooked up a small "TODO" REST API. You get a simple CRUD interface over
JSON on managing TODOs, and TODOs are stored locally on the file system. The
code should be commented enough to help you make sense of it.
# What's included?
I cooked up a small server, and a small client wrapper to illustrate how I
usually use restify in my own projects; typically, I have a server application
that does whatever API thing I need it to do, and I "wrap" the restify client(s)
as appropriate to deliver a "high-level" SDK that users can code against; note
I tend to try to hide HTTP when you're at that level so the system is easier to
work with.
I also cooked up a small set of unit tests using
[nodeunit](https://github.com/caolan/nodeunit), as several times questions have
come up as to how to mock, or unit test a restify service. I typically
structure my app so that I can either:
- Run it on a UNIX Domain Socket as part of the unit test
- Just require an endpoint to be running, and pass it in as an env var
Here I chose the former option; run with `npm test`.
# How do I run this?
First, this has a `package.json`, so you'll need to run `npm install` in the
directory. Once you've done that, to get started _and_ see audit logs on your
terminal, run it like this:
$ node main.js 2>&1 | npx pino-pretty
If you want to see all the built in restify tracing:
$ node main.js -vv 2>&1 | npx pino-pretty
By default, this program writes to a directory in `/tmp`, but you can override
with a `-d` option. Additionally, by default it does not require
authentication, but you can require that with:
$ node main.js -u admin -z secret 2>&1 | npx pino-pretty
Lastly, re: the `2>&1 | npx pino-pretty` bit. In production, you assuredly would *not*
want to pipe to the [pino-pretty](https://github.com/pinojs/pino-pretty) CLI, but
rather capture the audit records in their raw form, so they would be easy to
post process and perform analytics on. Like all UNIX programs should, this
example writes "informational" messages to `stderr`, and `audit` records to
stdout. It's up to you to redirect them as appropriate.
# Some sample curl requests
Let's get the full magilla (i.e., with auth) running:
$ node main.js -u admin -z secret 2>&1 | npx pino-pretty
Also, before we go any further, install the
[json](https://github.com/trentm/json) tool as all the examples below use that.
## List Routes
$ curl -isS http://127.0.0.1:8080 | json
HTTP/1.1 200 OK
Content-Type: application/todo
Content-Length: 127
Date: Sat, 29 Dec 2012 23:05:05 GMT
Connection: keep-alive
[
"GET /",
"POST /todo",
"GET /todo",
"DELETE /todo",
"PUT /todo/:name",
"GET /todo/:name",
"DELETE /todo/:name"
]
## List TODOs (empty)
$ curl -isS http://127.0.0.1:8080/todo | json
HTTP/1.1 200 OK
Content-Type: application/todo
Content-Length: 2
Date: Sat, 29 Dec 2012 23:07:05 GMT
Connection: keep-alive
[]
## Create TODO
$ curl -isS http://127.0.0.1:8080/todo -X POST -d name=demo -d task="buy milk"
HTTP/1.1 201 Created
Content-Type: application/todo
Content-Length: 8
Date: Sat, 29 Dec 2012 23:08:04 GMT
Connection: keep-alive
buy milk
Aha! Note that here the `content-type` was `application/foo` because our server
set the `q-val` highest for that, and curl sets the `accept` header to `*/*`.
## List
$ curl -isS http://127.0.0.1:8080/todo | json
HTTP/1.1 200 OK
Content-Type: application/todo
Content-Length: 8
Date: Sat, 29 Dec 2012 23:09:45 GMT
Connection: keep-alive
[
"demo"
]
## Get TODO
Note here our server was setup to use streaming, and we explicitly opted for
JSON:
$ curl -isS http://127.0.0.1:8080/todo/demo | json
HTTP/1.1 200 OK
Content-Type: application/json
Date: Sat, 29 Dec 2012 23:11:19 GMT
Connection: keep-alive
Transfer-Encoding: chunked
{
"name": "demo",
"task": "buy milk"
}
However, we still supported the full negotiation via another means:
$ curl -isS -H accept:application/todo http://127.0.0.1:8080/todo/demo
HTTP/1.1 200 OK
Content-Type: application/todo
Content-Length: 8
Date: Sat, 29 Dec 2012 23:14:31 GMT
Connection: keep-alive
buy milk
## Delete all
$ curl -isS -X DELETE http://127.0.0.1:8080/todo/demo
HTTP/1.1 204 No Content
Date: Sat, 29 Dec 2012 23:15:50 GMT
Connection: keep-alive
================================================
FILE: examples/todoapp/lib/client.js
================================================
// Copyright (c) 2012 Mark Cavage. All rights reserved.
var util = require('util');
var assert = require('assert-plus');
var clients = require('restify-clients');
///--- Globals
var sprintf = util.format;
///--- API
function TodoClient(options) {
assert.object(options, 'options');
assert.object(options.log, 'options.log');
assert.optionalString(options.socketPath, 'options.socketPath');
assert.optionalString(options.url, 'options.url');
assert.optionalString(options.version, 'options.version');
var ver = options.version || '~1.0';
this.client = clients.createJSONClient({
log: options.log,
name: 'TodoClient',
socketPath: options.socketPath,
url: options.url,
version: ver
});
this.log = options.log.child({ component: 'TodoClient' }, true);
this.url = options.url;
this.version = ver;
if (options.username && options.password) {
this.username = options.username;
this.client.basicAuth(options.username, options.password);
}
}
TodoClient.prototype.create = function create(task, cb) {
assert.string(task, 'task');
assert.func(cb, 'callback');
this.client.post('/todo', { task: task }, function(err, req, res, obj) {
if (err) {
cb(err);
} else {
cb(null, obj);
}
});
};
TodoClient.prototype.list = function list(cb) {
assert.func(cb, 'callback');
this.client.get('/todo', function(err, req, res, obj) {
if (err) {
cb(err);
} else {
cb(null, obj);
}
});
};
TodoClient.prototype.get = function get(name, cb) {
assert.string(name, 'name');
assert.func(cb, 'callback');
this.client.get('/todo/' + name, function(err, req, res, obj) {
if (err) {
cb(err);
} else {
cb(null, obj);
}
});
};
TodoClient.prototype.update = function update(todo, cb) {
assert.object(todo, 'todo');
assert.func(cb, 'callback');
this.client.put('/todo/' + todo.name, todo, function(err) {
if (err) {
cb(err);
} else {
cb(null);
}
});
};
TodoClient.prototype.del = function del(name, cb) {
if (typeof name === 'function') {
cb = name;
name = '';
}
assert.string(name, 'name');
assert.func(cb, 'callback');
var p = '/todo' + (name.length > 0 ? '/' + name : '');
this.client.del(p, function(err) {
if (err) {
cb(err);
} else {
cb(null);
}
});
};
TodoClient.prototype.toString = function toString() {
var str = sprintf(
'[object TodoClient&1 | npx pino-pretty
*
* And the log level will be set to TRACE.
*/
function parseOptions() {
var option;
var opts = {};
var parser = new getopt.BasicParser('hvd:p:u:z:', process.argv);
while ((option = parser.getopt()) !== undefined) {
switch (option.option) {
case 'd':
opts.directory = path.normalize(option.optarg);
break;
case 'h':
usage();
break;
case 'p':
opts.port = parseInt(option.optarg, 10);
break;
case 'u':
opts.user = option.optarg;
break;
case 'v':
// Allows us to set -vvv -> this little hackery
// just ensures that we're never < TRACE
LOG.level(Math.max(pino.levels.values.trace, LOG.level - 10));
if (LOG.level <= pino.levels.values.debug) {
LOG = LOG.child({ src: true });
}
break;
case 'z':
opts.password = option.optarg;
break;
default:
usage('invalid option: ' + option.option);
break;
}
}
return opts;
}
function usage(msg) {
if (msg) {
console.error(msg);
}
var str =
'usage: ' + NAME + ' [-v] [-d dir] [-p port] [-u user] [-z password]';
console.error(str);
process.exit(msg ? 1 : 0);
}
///--- Mainline
(function main() {
var options = parseOptions();
LOG.debug(options, 'command line arguments parsed');
// First setup our 'database'
var dir = path.normalize((options.directory || '/tmp') + '/todos');
try {
fs.mkdirSync(dir);
} catch (e) {
if (e.code !== 'EEXIST') {
LOG.fatal(e, 'unable to create "database" %s', dir);
process.exit(1);
}
}
var server = todo.createServer({
directory: dir,
log: LOG
});
// At last, let's rock and roll
server.listen(options.port || 8080, function onListening() {
LOG.info('listening at %s', server.url);
});
})();
================================================
FILE: examples/todoapp/package.json
================================================
{
"name": "restify-example",
"version": "0.0.0",
"description": "Kitchen Sink App of restify",
"main": "main.js",
"dependencies": {
"assert-plus": "1.0.0",
"pino": "^7.11.0",
"posix-getopt": "^1.2.1",
"restify-errors": "^8.0.2",
"restify-clients": "^3.1.0",
"restify": "^8.6.1"
},
"devDependencies": {
"mocha": "^10.0.0",
"chai": "^4.3.6",
"pino-pretty": "^7.6.1"
},
"scripts": {
"start": "node main.js 2>&1 | pino-pretty",
"test": "mocha test"
},
"author": "Mark Cavage",
"license": "MIT"
}
================================================
FILE: examples/todoapp/test/todo.test.js
================================================
// Copyright (c) 2012 Mark Cavage. All rights reserved.
var fs = require('fs');
var pino = require('pino');
var restify = require('restify');
var assert = require('chai').assert;
var todo = require('../lib');
///--- Globals
var DIR = '/tmp/.todo_unit_test';
var SOCK = '/tmp/.todo_sock';
///--- Tests
describe('todoapp', function () {
var CLIENT;
var SERVER;
before(function (done) {
var log = pino({
name: 'todo_unit_test',
level: process.env.LOG_LEVEL || 'info'
});
fs.mkdir(DIR, function(err) {
if (err && err.code !== 'EEXIST') {
console.error('unable to mkdir: ' + err.stack);
process.exit(1);
}
SERVER = todo.createServer({
directory: DIR,
log: log.child({ component: 'server' }, true),
noAudit: true
});
assert.ok(SERVER);
SERVER.listen(SOCK, function() {
CLIENT = todo.createClient({
log: log.child({ component: 'client' }, true),
socketPath: SOCK
});
assert.ok(CLIENT);
done();
});
});
});
it('should return an empty list', function (done) {
CLIENT.list(function(err, todos) {
assert.ifError(err);
assert.ok(todos);
assert.ok(Array.isArray(todos));
if (todos) {
assert.equal(todos.length, 0);
}
done();
});
});
it('should create a new task', function (done) {
var task = 'check that unit test works';
CLIENT.create(task, function(err, todo) {
assert.ifError(err);
assert.ok(todo);
if (todo) {
assert.ok(todo.name);
assert.equal(todo.task, task);
}
done();
});
});
it('should list and get', function (done) {
CLIENT.list(function(err, todos) {
assert.ifError(err);
assert.ok(todos);
assert.ok(Array.isArray(todos));
if (todos) {
assert.equal(todos.length, 1);
CLIENT.get(todos[0], function(err2, todo) {
assert.ifError(err2);
assert.ok(todo);
done();
});
} else {
done();
}
});
});
it('should update', function (done) {
CLIENT.list(function(err, todos) {
assert.ifError(err);
assert.ok(todos);
assert.ok(Array.isArray(todos));
if (todos) {
assert.equal(todos.length, 1);
var todo = {
name: todos[0],
task: 'something else'
};
CLIENT.update(todo, function(err2) {
assert.ifError(err2);
done();
});
} else {
done();
}
});
});
after(function(done) {
CLIENT.del(function(err) {
assert.ifError(err);
CLIENT.client.close();
SERVER.close(function () {
fs.rmdir(DIR, function(err) {
assert.ifError(err);
done();
});
});
});
});
});
================================================
FILE: lib/chain.js
================================================
'use strict';
var assert = require('assert-plus');
var once = require('once');
var customErrorTypes = require('./errorTypes');
module.exports = Chain;
/**
* Create a new middleware chain
*
* @public
* @class Chain
* @param {Object} [options] - options
* @param {Boolean} [options.onceNext=false] - Prevents calling next multiple
* times
* @param {Boolean} [options.strictNext=false] - Throws error when next() is
* called more than once, enables onceNext option
* @example
* var chain = new Chain();
* chain.add(function (req, res, next) { next(); })
* // chain.add(function (req, res, next) { next(new Error('Foo')); })
* // chain.add(function (req, res, next) { next(false); })
*
* http.createServer((req, res) => {
* chain.run(req, res, function done(err) {
* res.end(err ? err.message : 'hello world');
* });
* })
*/
function Chain(options) {
assert.optionalObject(options, 'options');
options = options || {};
assert.optionalBool(options.onceNext, 'options.onceNext');
assert.optionalBool(options.strictNext, 'options.strictNext');
this.onceNext = !!options.onceNext;
this.strictNext = !!options.strictNext;
// strictNext next enforces onceNext
if (this.strictNext) {
this.onceNext = true;
}
this._stack = [];
this._once = this.strictNext === false ? once : once.strict;
}
/**
* Public methods.
* @private
*/
/**
* Get handlers of a chain instance
*
* @memberof Chain
* @instance
* @returns {Function[]} handlers
*/
Chain.prototype.getHandlers = function getHandlers() {
return this._stack;
};
/**
* Utilize the given middleware `handler`
*
* @public
* @memberof Chain
* @instance
* @param {Function} handler - handler
* @returns {undefined} no return value
*/
Chain.prototype.add = function add(handler) {
assert.func(handler);
var handlerId = handler._identifier || handler._name || handler.name;
if (handler.length <= 2) {
// arity <= 2, must be AsyncFunction
assert.equal(
handler.constructor.name,
'AsyncFunction',
`Handler [${handlerId}] is missing a third argument (the ` +
'"next" callback) but is not an async function. Middleware ' +
'handlers can be either async/await or callback-based.' +
'Callback-based (non-async) handlers should accept three ' +
'arguments: (req, res, next). Async handler functions should ' +
'accept maximum of 2 arguments: (req, res).'
);
} else {
// otherwise shouldn't be AsyncFunction
assert.notEqual(
handler.constructor.name,
'AsyncFunction',
`Handler [${handlerId}] accepts a third argument (the 'next" ` +
'callback) but is also an async function. Middleware ' +
'handlers can be either async/await or callback-based. Async ' +
'handler functions should accept maximum of 2 arguments: ' +
'(req, res). Non-async handlers should accept three ' +
'arguments: (req, res, next).'
);
}
// _name is assigned in the server and router
handler._name = handler._name || handler.name;
// add the middleware
this._stack.push(handler);
};
/**
* Returns the number of handlers
*
* @public
* @memberof Chain
* @instance
* @returns {Number} number of handlers in the stack
*/
Chain.prototype.count = function count() {
return this._stack.length;
};
/**
* Handle server requests, punting them down
* the middleware stack.
*
* @public
* @memberof Chain
* @instance
* @param {Request} req - request
* @param {Response} res - response
* @param {Function} done - final handler
* @returns {undefined} no return value
*/
Chain.prototype.run = function run(req, res, done) {
var self = this;
var index = 0;
function next(err) {
// next callback
var handler = self._stack[index++];
// all done or request closed
if (!handler || req.connectionState() === 'close') {
process.nextTick(function nextTick() {
return done(err, req, res);
});
return;
}
// call the handler
call(handler, err, req, res, self.onceNext ? self._once(next) : next);
}
next();
return;
};
/**
* Helper functions
* @private
*/
/**
* Invoke a handler.
*
* @private
* @param {Function} handler - handler function
* @param {Error|false|*} err - error, abort when true value or false
* @param {Request} req - request
* @param {Response} res - response
* @param {Function} _next - next handler
* @returns {undefined} no return value
*/
function call(handler, err, req, res, _next) {
var arity = handler.length;
var hasError = err === false || Boolean(err);
// Meassure handler timings
// _name is assigned in the server and router
req._currentHandler = handler._name;
req.startHandlerTimer(handler._name);
function next(nextErr) {
req.endHandlerTimer(handler._name);
_next(nextErr, req, res);
}
function resolve(value) {
if (value && req.log) {
// logs resolved value
req.log.warn(
{ value },
'Discarded returned value from async handler'
);
}
return next();
}
function reject(error) {
if (!(error instanceof Error)) {
error = new customErrorTypes.AsyncError(
{
info: {
cause: error,
handler: handler._name,
method: req.method,
path: req.path ? req.path() : undefined
}
},
'Async middleware rejected without an error'
);
}
return next(error);
}
if (hasError && arity === 4) {
// error-handling middleware
handler(err, req, res, next);
return;
} else if (!hasError && arity < 4) {
// request-handling middleware
process.nextTick(function nextTick() {
const result = handler(req, res, next);
if (result && typeof result.then === 'function') {
result.then(resolve, reject);
}
});
return;
}
// continue
next(err);
}
================================================
FILE: lib/deprecationWarnings.js
================================================
'use strict';
function deprecationWarnings(server) {
// deprecation for domains and next.ifError
if (server.handleUncaughtExceptions === true) {
server.log.warn(
[
'DEPRECATION WARNING: Due to deprecation of the domain module',
'in node.js, all features in restify that depend on it have',
'been deprecated as well.',
'This includes `handleUncaughtExceptions` and',
'`next.ifError()`. They will continue to work in 5.x, but',
'consider them unsupported and likely to be removed',
'from future versions of restify.'
].join(' ')
);
}
}
module.exports = deprecationWarnings;
================================================
FILE: lib/dtrace.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
///--- Globals
'use strict';
var ID = 0;
var MAX_INT = Math.pow(2, 32) - 1;
var PROBES = {
// server_name, route_name, id, method, url, headers (json)
'route-start': ['char *', 'char *', 'int', 'char *', 'char *', 'json'],
// server_name, route_name, handler_name, id
'handler-start': ['char *', 'char *', 'char *', 'int'],
// server_name, route_name, handler_name, id
'handler-done': ['char *', 'char *', 'char *', 'int'],
// server_name, route_name, id, statusCode, headers (json)
'route-done': ['char *', 'char *', 'int', 'int', 'json'],
// Client probes
// method, url, headers, id
'client-request': ['char *', 'char *', 'json', 'int'],
// id, statusCode, headers
'client-response': ['int', 'int', 'json'],
// id, Error.toString()
'client-error': ['id', 'char *']
};
var PROVIDER;
///--- API
// eslint-disable-next-line wrap-iife
module.exports = (function exportStaticProvider() {
if (!PROVIDER) {
try {
var dtrace = require('dtrace-provider');
PROVIDER = dtrace.createDTraceProvider('restify');
} catch (e) {
PROVIDER = {
fire: function fire() {},
enable: function enable() {},
addProbe: function addProbe() {
var p = {
fire: function fire() {}
};
return p;
},
removeProbe: function removeProbe() {},
disable: function disable() {}
};
}
PROVIDER._rstfy_probes = {};
Object.keys(PROBES).forEach(function forEach(p) {
var args = PROBES[p].splice(0);
args.unshift(p);
var probe = PROVIDER.addProbe.apply(PROVIDER, args);
PROVIDER._rstfy_probes[p] = probe;
});
PROVIDER.enable();
PROVIDER.nextId = function nextId() {
if (++ID >= MAX_INT) {
ID = 1;
}
return ID;
};
}
return PROVIDER;
})();
================================================
FILE: lib/errorTypes.js
================================================
'use strict';
var errors = require('restify-errors');
// This allows Restify to work with restify-errors v6+
module.exports = {
RequestCloseError: errors.makeConstructor('RequestCloseError'),
RouteMissingError: errors.makeConstructor('RouteMissingError'),
AsyncError: errors.makeConstructor('AsyncError')
};
================================================
FILE: lib/formatters/binary.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
///--- Exports
/**
* Binary formatter.
*
* @public
* @function formatBinary
* @param {Object} req - the request object
* @param {Object} res - the response object
* @param {Object} body - response body
* @returns {Buffer} body
*/
function formatBinary(req, res, body) {
if (!Buffer.isBuffer(body)) {
body = new Buffer(body.toString());
}
res.setHeader('Content-Length', body.length);
return body;
}
module.exports = formatBinary;
================================================
FILE: lib/formatters/index.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
///--- Exports
/**
* Format a response for being sent over the wire
*
* @public
* @typedef {Function} formatter
* @param {Object} req - the request object (not used)
* @param {Object} res - the response object
* @param {Object} body - response body to format
* @returns {String} formatted response data
*/
module.exports = {
'application/javascript; q=0.1': require('./jsonp'),
'application/json; q=0.4': require('./json'),
'text/plain; q=0.3': require('./text'),
'application/octet-stream; q=0.2': require('./binary')
};
================================================
FILE: lib/formatters/json.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var errors = require('restify-errors');
///--- Exports
/**
* JSON formatter. Will look for a toJson() method on the body. If one does not
* exist then a JSON.stringify will be attempted.
*
* @public
* @function formatJSON
* @param {Object} req - the request object (not used)
* @param {Object} res - the response object
* @param {Object} body - response body
* @returns {String} data
*/
function formatJSON(req, res, body) {
var data = 'null';
if (body !== undefined) {
try {
data = JSON.stringify(body);
} catch (e) {
throw new errors.InternalServerError(
{ cause: e, info: { formatter: 'json' } },
'could not format response body'
);
}
}
// Setting the content-length header is not a formatting feature and should
// be separated into another module
res.setHeader('Content-Length', Buffer.byteLength(data));
return data;
}
module.exports = formatJSON;
================================================
FILE: lib/formatters/jsonp.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
///--- Exports
/**
* JSONP formatter. like JSON, but with a callback invocation.
* Unicode escapes line and paragraph separators.
*
* @public
* @function formatJSONP
* @param {Object} req - the request object
* @param {Object} res - the response object
* @param {Object} body - response body
* @returns {String} data
*/
function formatJSONP(req, res, body) {
if (!body) {
res.setHeader('Content-Length', 0);
return null;
}
if (Buffer.isBuffer(body)) {
body = body.toString('base64');
}
var _cb = req.query.callback || req.query.jsonp;
var data;
if (_cb) {
data =
'typeof ' +
_cb +
" === 'function' && " +
_cb +
'(' +
JSON.stringify(body) +
');';
} else {
data = JSON.stringify(body);
}
data = data.replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
res.setHeader('Content-Length', Buffer.byteLength(data));
return data;
}
module.exports = formatJSONP;
================================================
FILE: lib/formatters/text.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
/**
* Formats the body to 'text' by invoking a toString() on the body if it
* exists. If it doesn't, then the response is a zero-length string.
*
* @public
* @function formatText
* @param {Object} req - the request object (not used)
* @param {Object} res - the response object
* @param {Object} body - response body. If it has a toString() method this
* will be used to make the string representation
* @returns {String} data
*/
function formatText(req, res, body) {
// if body is null, default to empty string
var data = '';
data = body.toString();
// TODO: setting the content-length header is not a formatting
// feature and should be separated into another module
res.setHeader('Content-Length', Buffer.byteLength(data));
return data;
}
module.exports = formatText;
================================================
FILE: lib/helpers/chainComposer.js
================================================
'use strict';
/* eslint-disable func-names */
var Chain = require('../chain');
var _ = require('lodash');
module.exports = composeHandlerChain;
/**
* Builds a function with the signature of a handler
* function(req,resp,callback).
* which internally executes the passed in array of handler function as a chain.
*
* @param {Array} [handlers] - handlers Array of
* function(req,resp,callback) handlers.
* @param {Object} [options] - options Optional option object that is
* passed to Chain.
* @returns {Function} Handler function that executes the handler chain when run
*/
function composeHandlerChain(handlers, options) {
var chain = new Chain(options);
if (_.isArray(handlers)) {
handlers = _.flattenDeep(handlers);
handlers.forEach(function(handler) {
chain.add(handler);
});
} else {
chain.add(handlers);
}
return function handlerChain(req, resp, callback) {
chain.run(req, resp, callback);
};
}
================================================
FILE: lib/http_date.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
/**
* Takes an instance of a date object, formats it UTC
* e.g., Wed, 17 Jun 2015 01:30:26 GMT
*
* @public
* @function httpDate
* @param {Object} now - a date object
* @returns {String} formatted dated object
*/
module.exports = function httpDate(now) {
if (!now) {
now = new Date();
}
return now.toUTCString();
};
================================================
FILE: lib/index.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var assert = require('assert-plus');
var errors = require('restify-errors');
var pino = require('pino');
var Router = require('./router');
var Server = require('./server');
var shallowCopy = require('./utils').shallowCopy;
var InternalError = errors.InternalError;
/**
* A restify server object is the main interface through which you will register
* routes and handlers for incoming requests.
*
* @public
* @function createServer
* @param {Object} [options] - an options object
* @param {String} [options.name="restify"] - Name of the server.
* @param {Boolean} [options.dtrace=false] - enable DTrace support
* @param {Router} [options.router=new Router(opts)] - Router
* @param {Object} [options.log=pino({name:options.name || "restify"})]
* - [pino](https://github.com/pinojs/pino) instance.
* @param {String} [options.url] - Once listen() is called, this will be filled
* in with where the server is running.
* @param {String|Buffer} [options.certificate] - If you want to create an HTTPS
* server, pass in a PEM-encoded certificate and key.
* @param {String|Buffer} [options.key] - If you want to create an HTTPS server,
* pass in a PEM-encoded certificate and key.
* @param {Object} [options.formatters] - Custom response formatters for
* `res.send()`.
* @param {Boolean} [options.handleUncaughtExceptions=false] - When true restify
* will use a domain to catch and respond to any uncaught
* exceptions that occur in its handler stack.
* Comes with significant negative performance impact.
* @param {Object} [options.spdy] - Any options accepted by
* [node-spdy](https://github.com/indutny/node-spdy).
* @param {Object} [options.http2] - Any options accepted by
* [http2.createSecureServer](https://nodejs.org/api/http2.html).
* @param {Boolean} [options.handleUpgrades=false] - Hook the `upgrade` event
* from the node HTTP server, pushing `Connection: Upgrade` requests through the
* regular request handling chain.
* @param {Boolean} [options.onceNext=false] - Prevents calling next multiple
* times
* @param {Boolean} [options.strictNext=false] - Throws error when next() is
* called more than once, enabled onceNext option
* @param {Object} [options.httpsServerOptions] - Any options accepted by
* [node-https Server](http://nodejs.org/api/https.html#https_https).
* If provided the following restify server options will be ignored:
* spdy, ca, certificate, key, passphrase, rejectUnauthorized, requestCert and
* ciphers; however these can all be specified on httpsServerOptions.
* @param {Boolean} [options.noWriteContinue=false] - prevents
* `res.writeContinue()` in `server.on('checkContinue')` when proxing
* @param {Boolean} [options.ignoreTrailingSlash=false] - ignore trailing slash
* on paths
* @param {Boolean} [options.strictFormatters=true] - enables strict formatters
* behavior: a formatter matching the response's content-type is required. If
* not found, the response's content-type is automatically set to
* 'application/octet-stream'. If a formatter for that content-type is not
* found, sending the response errors.
* @example
* var restify = require('restify');
* var server = restify.createServer();
*
* server.listen(8080, function () {
* console.log('ready on %s', server.url);
* });
* @returns {Server} server
*/
function createServer(options) {
assert.optionalObject(options, 'options');
var opts = shallowCopy(options || {});
var server;
// empty string should override default value.
opts.name = opts.hasOwnProperty('name') ? opts.name : 'restify';
opts.log = opts.log || pino({ name: opts.name || 'restify' });
opts.router = opts.router || new Router(opts);
server = new Server(opts);
if (opts.handleUncaughtExceptions) {
server.on('uncaughtException', function onUncaughtException(
req,
res,
route,
e
) {
if (
this.listeners('uncaughtException').length > 1 ||
res.headersSent
) {
return false;
}
res.send(new InternalError(e, e.message || 'unexpected error'));
return true;
});
}
return server;
}
///--- Exports
module.exports.logger = pino;
module.exports.createServer = createServer;
module.exports.formatters = require('./formatters');
module.exports.plugins = require('./plugins');
module.exports.pre = require('./plugins').pre;
module.exports.helpers = { compose: require('./helpers/chainComposer') };
================================================
FILE: lib/plugins/accept.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var assert = require('assert-plus');
var mime = require('mime');
var NotAcceptableError = require('restify-errors').NotAcceptableError;
/**
* Parses the `Accept` header, and ensures that the server can respond to what
* the client asked for. In almost all cases passing in `server.acceptable` is
* all that's required, as that's an array of content types the server knows
* how to respond to (with the formatters you've registered). If the request is
* for a non-handled type, this plugin will return a `NotAcceptableError` (406).
*
* Note you can get the set of types allowed from a restify server by doing
* `server.acceptable`.
*
* @public
* @function acceptParser
* @throws {NotAcceptableError}
* @param {String[]} accepts - array of accept types.
* @returns {Function} restify handler.
* @example
* server.use(restify.plugins.acceptParser(server.acceptable));
*/
function acceptParser(accepts) {
var acceptable = accepts;
if (!Array.isArray(acceptable)) {
acceptable = [acceptable];
}
assert.arrayOfString(acceptable, 'acceptable');
acceptable = acceptable
.filter(function filter(a) {
return a;
})
.map(function map(a) {
return a.indexOf('/') === -1 ? mime.getType(a) : a;
})
.filter(function filter(a) {
return a;
});
var e = new NotAcceptableError('Server accepts: ' + acceptable.join());
function parseAccept(req, res, next) {
if (req.accepts(acceptable)) {
return next();
}
return next(e);
}
return parseAccept;
}
module.exports = acceptParser;
================================================
FILE: lib/plugins/audit.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var assert = require('assert-plus');
var pino = require('pino');
var HttpError = require('restify-errors').HttpError;
var errors = require('restify-errors');
var hrTimeDurationInMs = require('./utils/hrTimeDurationInMs');
/**
* Utility to get response headers from a given response.
* Manually generates a POJO from `res.getHeaderNames` and `res.getHeader`,
* if available, falling back to deprecated `res._headers`, otherwise.
* Intentionally does not use `res.getHeaders` to avoid deserialization
* issues with object returned by that method.
*
* @param {http.ServerResponse} res - the OutgoingMessage
* @private
* @function getResponseHeaders
* @returns {object} map from header name to header value
* @see https://github.com/restify/node-restify/issues/1370
*/
function getResponseHeaders(res) {
if (res.getHeaderNames && res.getHeader) {
return res.getHeaderNames().reduce(function reduce(prev, curr) {
var header = {};
header[curr] = res.getHeader(curr);
return Object.assign({}, prev, header);
}, {});
}
return res._headers;
}
///--- API
/**
* @public
* @function auditLogger
* @param {Object} opts - The options object.
* @param {Object} opts.log - The logger.
* @param {String} opts.event - The event from the server which initiates the
* log, one of 'pre', 'routed', or 'after'
* @param {Function} [opts.context] - The optional context function of signature
* f(req, res, route, err). Invoked each time an audit log is generated. This
* function can return an object that customizes the format of anything off the
* req, res, route, and err objects. The output of this function will be
* available on the `context` key in the audit object.
* @param {Object} [opts.server] - The restify server, used to emit
* the audit log object programmatically
* @param {boolean} [opts.printLog=true] - Whether to print the log
* via the logger.
* @param {Object} [opts.serializers] - Override the default logger serializers
* for err, req and res
* @param {String} [opts.requestIdFieldName] - The name of the request id property attached
* to log lines. Defaults to "req_id".
* @returns {Function} Handler
* @fires audit when an audit log has been generated
* @example
*
* Audit logging is a special plugin, as you don't use it with `.use()`
* but with the `after` event:
*
* You pass in the auditor a pino logger, optionally server object,
* Ringbuffer and a flag printLog indicate if log needs to be print out at info
* level or not. By default, without specify printLog flag, it will write out
* record lookling like this:
*
* The `timers` field shows the time each handler took to run in microseconds.
* Restify by default will record this information for every handler for each
* route. However, if you decide to include nested handlers, you can track the
* timing yourself by utilizing the Request
* [startHandlerTimer](#starthandlertimerhandlername) and
* [endHandlerTimer](#endhandlertimerhandlername) API.
* You can also listen to auditlog event and get same above log object when
* log event emits. For example
*
* SERVER.on('auditlog', function (data) {
* //do some process with log
* });
*
*/
function auditLogger(opts) {
assert.object(opts, 'opts');
assert.object(opts.log, 'opts.log');
assert.string(opts.event, 'opts.event');
assert.optionalFunc(opts.context, 'opts.context');
assert.optionalObject(opts.server, 'opts.server');
assert.optionalBool(opts.printLog, 'opts.printLog');
assert.optionalObject(opts.serializers, 'opts.serializers');
if (
opts.event !== 'after' &&
opts.event !== 'pre' &&
opts.event !== 'routed'
) {
throw new errors.VError(
'opts.event must be %s, %s, or %s, but is %s',
'pre',
'routed',
'after',
opts.event
);
}
var server = opts.server;
var printLog = opts.printLog;
const requestIdFieldName = opts.requestIdFieldName || 'req_id';
if (typeof printLog === 'undefined') {
printLog = true;
}
var errSerializer = pino.stdSerializers.err;
// don't break legacy use, where there was no top level opts.serializer
if (opts.log.serializers && opts.log.serializers.err) {
errSerializer = opts.log.serializers.err;
}
var DEFAULT_SERIALIZERS = {
err: errSerializer,
req: function auditRequestSerializer(req) {
if (!req) {
return false;
}
var timers = {};
(req.timers || []).forEach(function forEach(time) {
var t = time.time;
var _t = Math.floor(1000000 * t[0] + t[1] / 1000);
timers[time.name] = (timers[time.name] || 0) + _t;
});
return {
// account for native and queryParser plugin usage
query:
typeof req.query === 'function' ? req.query() : req.query,
method: req.method,
url: req.url,
headers: req.headers,
httpVersion: req.httpVersion,
trailers: req.trailers,
version: req.version(),
body: opts.body === true ? req.body : undefined,
timers: timers,
connectionState: req.connectionState && req.connectionState()
};
},
res: function auditResponseSerializer(res) {
if (!res) {
return false;
}
var body;
if (opts.body === true) {
if (res._body instanceof HttpError) {
body = res._body.body;
} else {
body = res._body;
}
}
return {
statusCode: res.statusCode,
headers: getResponseHeaders(res),
trailer: res._trailer || false,
body: body
};
}
};
var serializers = Object.assign({}, DEFAULT_SERIALIZERS, opts.serializers);
function audit(req, res, route, err) {
var latency = res.get('Response-Time');
if (typeof latency !== 'number') {
latency = hrTimeDurationInMs(req._timeStart, req._timeFlushed);
}
var obj = {
remoteAddress: req.connection.remoteAddress,
remotePort: req.connection.remotePort,
[requestIdFieldName]: req.getId(),
req: req,
res: res,
err: err,
latency: latency,
secure: req.secure,
_audit: true,
event: opts.event
};
// run through the custom context function
if (opts.context) {
obj.context = opts.context(req, res, route, err);
}
const origLog = (req && req.log) || opts.log;
if (printLog && origLog) {
const log = origLog.child(
{
audit: true,
component: opts.event
},
{
serializers: serializers
}
);
switch (opts.event) {
case 'after':
log.info(obj, 'handled: %d', res.statusCode);
break;
case 'pre':
log.info(obj, 'pre');
break;
case 'routed':
log.info(obj, 'routed');
break;
default:
throw new Error('Unexpected audit event: ' + opts.event);
}
}
if (server) {
server.emit('audit', obj);
}
return true;
}
return audit;
}
///-- Exports
module.exports = auditLogger;
================================================
FILE: lib/plugins/authorization.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var httpSignature = require('http-signature');
var errors = require('restify-errors');
///--- Globals
var InvalidHeaderError = errors.InvalidHeaderError;
var OPTIONS = {
algorithms: [
'rsa-sha1',
'rsa-sha256',
'rsa-sha512',
'dsa-sha1',
'hmac-sha1',
'hmac-sha256',
'hmac-sha512'
]
};
///--- Helpers
function parseBasic(string) {
var decoded;
var index;
var pieces;
decoded = new Buffer(string, 'base64').toString('utf8');
if (!decoded) {
throw new InvalidHeaderError('Authorization header invalid');
}
index = decoded.indexOf(':');
if (index === -1) {
pieces = [decoded];
} else {
pieces = [decoded.slice(0, index), decoded.slice(index + 1)];
}
if (!pieces || typeof pieces[0] !== 'string') {
throw new InvalidHeaderError('Authorization header invalid');
}
// Allows for usernameless authentication
if (!pieces[0]) {
pieces[0] = null;
}
// Allows for passwordless authentication
if (!pieces[1]) {
pieces[1] = null;
}
return {
username: pieces[0],
password: pieces[1]
};
}
function parseSignature(request, options) {
var opts = options || {};
opts.algorithms = OPTIONS.algorithms;
try {
return httpSignature.parseRequest(request, options);
} catch (e) {
throw new InvalidHeaderError(
'Authorization header invalid: ' + e.message
);
}
}
/**
* Parses out the `Authorization` header as best restify can.
* Currently only HTTP Basic Auth and
* [HTTP Signature](https://github.com/joyent/node-http-signature)
* schemes are supported.
*
* @public
* @function authorizationParser
* @throws {InvalidArgumentError}
* @param {Object} [options] - an optional options object that is
* passed to http-signature
* @returns {Function} Handler
* @example
*
* Subsequent handlers will see `req.authorization`, which looks like above.
*
* `req.username` will also be set, and defaults to 'anonymous'. If the scheme
* is unrecognized, the only thing available in `req.authorization` will be
* `scheme` and `credentials` - it will be up to you to parse out the rest.
*
* {
* scheme: "",
* credentials: "",
* basic: {
* username: $user
* password: $password
* }
* }
*/
function authorizationParser(options) {
function parseAuthorization(req, res, next) {
req.authorization = {};
req.username = 'anonymous';
if (!req.headers.authorization) {
return next();
}
var pieces = req.headers.authorization.split(' ', 2);
if (!pieces || pieces.length !== 2) {
var e = new InvalidHeaderError('BasicAuth content is invalid.');
return next(e);
}
req.authorization.scheme = pieces[0];
req.authorization.credentials = pieces[1];
try {
switch (pieces[0].toLowerCase()) {
case 'basic':
req.authorization.basic = parseBasic(pieces[1]);
req.username = req.authorization.basic.username;
break;
case 'signature':
req.authorization.signature = parseSignature(req, options);
req.username = req.authorization.signature.keyId;
break;
default:
break;
}
} catch (e2) {
return next(e2);
}
return next();
}
return parseAuthorization;
}
module.exports = authorizationParser;
================================================
FILE: lib/plugins/bodyParser.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var assert = require('assert-plus');
var errors = require('restify-errors');
var bodyReader = require('./bodyReader');
var jsonParser = require('./jsonBodyParser');
var formParser = require('./formBodyParser');
var multipartParser = require('./multipartBodyParser');
var fieldedTextParser = require('./fieldedTextBodyParser.js');
var regex = require('./utils/regex');
///--- Globals
var UnsupportedMediaTypeError = errors.UnsupportedMediaTypeError;
///--- API
/**
* Blocks your chain on reading and parsing the HTTP request body. Switches on
* `Content-Type` and does the appropriate logic. `application/json`,
* `application/x-www-form-urlencoded` and `multipart/form-data` are currently
* supported.
*
* Parses `POST` bodies to `req.body`. automatically uses one of the following
* parsers based on content type:
* - `urlEncodedBodyParser(options)` - parses url encoded form bodies
* - `jsonBodyParser(options)` - parses JSON POST bodies
* - `multipartBodyParser(options)` - parses multipart form bodies
*
* All bodyParsers support the following options:
* - `options.mapParams` - default false. copies parsed post body values onto
* req.params
* - `options.overrideParams` - default false. only applies when if
* mapParams true. when true, will stomp on req.params value when
* existing value is found.
*
* @public
* @function bodyParser
* @throws {UnsupportedMediaTypeError}
* @param {Object} [options] - an option object
* @param {Number} [options.maxBodySize] - The maximum size in bytes allowed in
* the HTTP body. Useful for limiting clients from hogging server memory.
* @param {Boolean} [options.mapParams] - if `req.params` should be filled with
* parsed parameters from HTTP body.
* @param {Boolean} [options.mapFiles] - if `req.params` should be filled with
* the contents of files sent through a multipart request.
* [formidable](https://github.com/felixge/node-formidable) is used internally
* for parsing, and a file is denoted as a multipart part with the `filename`
* option set in its `Content-Disposition`. This will only be performed if
* `mapParams` is true.
* @param {Boolean} [options.overrideParams] - if an entry in `req.params`
* should be overwritten by the value in the body if the names are the same.
* For instance, if you have the route `/:someval`,
* and someone posts an `x-www-form-urlencoded`
* Content-Type with the body `someval=happy` to `/sad`, the value will be
* `happy` if `overrideParams` is `true`, `sad` otherwise.
* @param {Function} [options.multipartHandler] - a callback to handle any
* multipart part which is not a file.
* If this is omitted, the default handler is invoked which may
* or may not map the parts into `req.params`, depending on
* the `mapParams`-option.
* @param {Function} [options.multipartFileHandler] - a callback to handle any
* multipart file.
* It will be a file if the part has a `Content-Disposition` with the
* `filename` parameter set. This typically happens when a browser sends a
* form and there is a parameter similar to ``.
* If this is not provided, the default behaviour is to map the contents
* into `req.params`.
* @param {Boolean} [options.keepExtensions] - if you want the uploaded
* files to include the extensions of the original files
* (multipart uploads only).
* Does nothing if `multipartFileHandler` is defined.
* @param {String} [options.uploadDir] - Where uploaded files are
* intermediately stored during transfer before the contents is mapped
* into `req.params`.
* Does nothing if `multipartFileHandler` is defined.
* @param {Boolean} [options.multiples] - if you want to support html5 multiple
* attribute in upload fields.
* @param {String} [options.hash] - If you want checksums calculated for
* incoming files, set this to either `sha1` or `md5`.
* @param {Boolean} [options.rejectUnknown] - Set to `true` if you want to end
* the request with a `UnsupportedMediaTypeError` when none of
* the supported content types was given.
* @param {Boolean} [options.requestBodyOnGet=false] - Parse body of a GET
* request.
* @param {Function} [options.reviver] - `jsonParser` only. If a function,
* this prescribes how the value originally produced by parsing is transformed,
* before being returned. For more information check out
* `JSON.parse(text[, reviver])`.
* @param {Number} [options.maxFieldsSize=2 * 1024 * 1024] - `multipartParser`
* only.
* Limits the amount of memory all fields together (except files)
* can allocate in bytes.
* The default size is `2 * 1024 * 1024` bytes *(2MB)*.
* @returns {Function} Handler
* @example
* server.use(restify.plugins.bodyParser({
* maxBodySize: 0,
* mapParams: true,
* mapFiles: false,
* overrideParams: false,
* multipartHandler: function(part) {
* part.on('data', function(data) {
* // do something with the multipart data
* });
* },
* multipartFileHandler: function(part) {
* part.on('data', function(data) {
* // do something with the multipart file data
* });
* },
* keepExtensions: false,
* uploadDir: os.tmpdir(),
* multiples: true,
* hash: 'sha1',
* rejectUnknown: true,
* requestBodyOnGet: false,
* reviver: undefined,
* maxFieldsSize: 2 * 1024 * 1024
* }));
*/
function bodyParser(options) {
assert.optionalObject(options, 'options');
var opts = options || {};
opts.bodyReader = true;
var read = bodyReader(opts);
var parseForm = formParser(opts);
var parseJson = jsonParser(opts);
var parseMultipart = multipartParser(opts);
var parseFieldedText = fieldedTextParser(opts);
function parseBody(req, res, next) {
// #100 don't parse the body again if we've read it once
if (req._parsedBody) {
next();
return;
} else {
req._parsedBody = true;
}
// Allow use of 'requestBodyOnGet' flag to allow for merging of
// the request body of a GET request into req.params
if (req.method === 'HEAD') {
next();
return;
}
if (req.method === 'GET') {
if (!opts.requestBodyOnGet) {
next();
return;
}
}
if (req.contentLength() === 0 && !req.isChunked()) {
next();
return;
}
var parser;
var type = req.contentType().toLowerCase();
switch (type) {
case 'application/json':
parser = parseJson[0];
break;
case 'application/x-www-form-urlencoded':
parser = parseForm[0];
break;
case 'multipart/form-data':
parser = parseMultipart;
break;
case 'text/tsv':
parser = parseFieldedText;
break;
case 'text/tab-separated-values':
parser = parseFieldedText;
break;
case 'text/csv':
parser = parseFieldedText;
break;
default:
break;
}
// if we find no matches from the direct string comparisons, perform
// more expensive regex matches. map any +json to application/json.
// theoretically these could be mapped to application/json prior to the
// switch statement, but putting it here allows us to skip the regex
// entirely unless absolutely necessary. additional types could be
// added later at some point.
if (!parser) {
if (regex.jsonContentType.test(type)) {
parser = parseJson[0];
}
}
if (parser) {
parser(req, res, next);
} else if (opts && opts.rejectUnknown) {
next(new UnsupportedMediaTypeError(type));
} else {
next();
}
}
return [read, parseBody];
}
module.exports = bodyParser;
================================================
FILE: lib/plugins/bodyReader.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var crypto = require('crypto');
var zlib = require('zlib');
var assert = require('assert-plus');
var once = require('once');
var errors = require('restify-errors');
///--- Globals
var BadDigestError = errors.BadDigestError;
var RequestEntityTooLargeError = errors.RequestEntityTooLargeError;
var PayloadTooLargeError = errors.PayloadTooLargeError;
var UnsupportedMediaTypeError = errors.UnsupportedMediaTypeError;
var MD5_MSG = "Content-MD5 '%s' didn't match '%s'";
///--- Helpers
function createBodyWriter(req) {
var buffers = [];
var contentType = req.contentType();
var isText = false;
if (
!contentType ||
contentType === 'application/json' ||
contentType === 'application/x-www-form-urlencoded' ||
contentType === 'multipart/form-data' ||
contentType.substr(0, 5) === 'text/'
) {
isText = true;
}
req.body = new Buffer(0);
return {
write: function write(chunk) {
buffers.push(chunk);
},
end: function end() {
req.body = Buffer.concat(buffers);
if (isText) {
req.body = req.body.toString('utf8');
}
}
};
}
///--- API
/**
* Reads the body of the request.
*
* @public
* @function bodyReader
* @throws {BadDigestError | PayloadTooLargeError}
* @param {Object} options - an options object
* @returns {Function} Handler
*/
function bodyReader(options) {
var opts = options || {};
assert.object(opts, 'opts');
var maxBodySize = opts.maxBodySize || 0;
function readBody(req, res, originalNext) {
var next = once(originalNext);
// #100 don't read the body again if we've read it once
if (req._readBody) {
next();
return;
} else {
req._readBody = true;
}
if (
(req.getContentLength() === 0 && !req.isChunked()) ||
req.contentType() === 'multipart/form-data' ||
req.contentType() === 'application/octet-stream'
) {
next();
return;
}
var bodyWriter = createBodyWriter(req);
var bytesReceived = 0;
var digest;
var gz;
var hash;
var md5;
var unsupportedContentEncoding;
if ((md5 = req.headers['content-md5'])) {
hash = crypto.createHash('md5');
}
function done() {
bodyWriter.end();
if (unsupportedContentEncoding) {
next(
new UnsupportedMediaTypeError(
{
info: {
contentEncoding: unsupportedContentEncoding
}
},
'content encoding not supported'
)
);
return;
}
if (maxBodySize && bytesReceived > maxBodySize) {
var msg = 'Request body size exceeds ' + maxBodySize;
var err;
// Between Node 0.12 and 4 http status code messages changed
// RequestEntityTooLarge was changed to PayloadTooLarge
// this check is to maintain backwards compatibility
if (PayloadTooLargeError !== undefined) {
err = new PayloadTooLargeError(msg);
} else {
err = new RequestEntityTooLargeError(msg);
}
next(err);
return;
}
if (!req.body.length) {
next();
return;
}
if (hash && md5 !== (digest = hash.digest('base64'))) {
next(new BadDigestError(MD5_MSG, md5, digest));
return;
}
next();
}
if (req.headers['content-encoding'] === undefined) {
// This handles the original else branch
req.once('end', done);
} else if (req.headers['content-encoding'] === 'gzip') {
gz = zlib.createGunzip();
gz.on('data', bodyWriter.write);
gz.once('end', done);
req.once('end', gz.end.bind(gz));
} else {
unsupportedContentEncoding = req.headers['content-encoding'];
res.setHeader('Accept-Encoding', 'gzip');
req.once('end', done);
}
req.on('data', function onRequestData(chunk) {
if (maxBodySize) {
bytesReceived += chunk.length;
if (bytesReceived > maxBodySize) {
return;
}
}
if (hash) {
hash.update(chunk, 'binary');
}
if (gz) {
gz.write(chunk);
} else {
bodyWriter.write(chunk);
}
});
req.once('error', next);
// add 'close and 'aborted' event handlers so that requests (and their
// corresponding memory) don't leak if client stops sending data half
// way through a POST request
res.once('close', next);
req.once('aborted', next);
req.resume();
}
return readBody;
}
module.exports = bodyReader;
================================================
FILE: lib/plugins/conditionalHandler.js
================================================
'use strict';
var errors = require('restify-errors');
var _ = require('lodash');
var assert = require('assert-plus');
var semver = require('semver');
var Negotiator = require('negotiator');
var Chain = require('../chain');
///--- Globals
var InvalidVersionError = errors.InvalidVersionError;
var UnsupportedMediaTypeError = errors.UnsupportedMediaTypeError;
var DEF_CT = 'application/octet-stream';
///--- Exports
/**
* Runs first handler that matches to the condition
*
* @public
* @function conditionalHandler
* @param {Object|Object[]} candidates - candidates
* @param {Function|Function[]} candidates.handler - handler(s)
* @param {String|String[]} [candidates.version] - '1.1.0', ['1.1.0', '1.2.0']
* @param {String} [candidates.contentType] - accepted content type, '*\/json'
* @returns {Function} Handler
* @throws {InvalidVersionError}
* @throws {UnsupportedMediaTypeError}
* @example
* server.use(restify.plugins.conditionalHandler({
* contentType: 'application/json',
* version: '1.0.0',
* handler: function (req, res, next) {
* next();
* })
* });
*
* server.get('/hello/:name', restify.plugins.conditionalHandler([
* {
* version: '1.0.0',
* handler: function(req, res, next) { res.send('1.x'); }
* },
* {
* version: ['1.5.0', '2.0.0'],
* handler: function(req, res, next) { res.send('1.5.x, 2.x'); }
* },
* {
* version: '3.0.0',
* contentType: ['text/html', 'text/html']
* handler: function(req, res, next) { res.send('3.x, text'); }
* },
* {
* version: '3.0.0',
* contentType: 'application/json'
* handler: function(req, res, next) { res.send('3.x, json'); }
* },
* // Array of handlers
* {
* version: '4.0.0',
* handler: [
* function(req, res, next) { next(); },
* function(req, res, next) { next(); },
* function(req, res, next) { res.send('4.x') }
* ]
* },
* ]);
* // 'accept-version': '^1.1.0' => 1.5.x, 2.x'
* // 'accept-version': '3.x', accept: 'application/json' => '3.x, json'
*/
function conditionalHandler(candidates) {
var isVersioned = false;
var isContentTyped = false;
if (!_.isArray(candidates)) {
candidates = [candidates];
}
// Assert
assert.arrayOfObject(candidates, 'candidates');
candidates = candidates.map(function map(candidate) {
// Array of handlers, convert to chain
if (_.isArray(candidate.handler)) {
var chain = new Chain();
candidate.handler.forEach(function forEach(_handler) {
assert.func(_handler);
chain.add(_handler);
});
candidate.handler = chain.run.bind(chain);
}
assert.func(candidate.handler);
if (_.isString(candidate.version)) {
candidate.version = [candidate.version];
}
if (_.isString(candidate.contentType)) {
candidate.contentType = [candidate.contentType];
}
assert.optionalArrayOfString(candidate.version);
assert.optionalArrayOfString(candidate.contentType);
isVersioned = isVersioned || !!candidate.version;
isContentTyped = isContentTyped || !!candidate.contentType;
return candidate;
});
/**
* Conditional Handler
*
* @private
* @param {Request} req - request
* @param {Response} res - response
* @param {Function} next - next
* @returns {undefined} no return value
*/
return function _conditionalHandlerFactory(req, res, next) {
var contentType = req.headers.accept || DEF_CT;
var reqCandidates = candidates;
// Content Type
if (isContentTyped) {
var contentTypes = contentType.split(/\s*,\s*/);
reqCandidates = candidates.filter(function filter(candidate) {
var neg = new Negotiator({
headers: {
accept: candidate.contentType.join(', ')
}
});
var tmp = neg.preferredMediaType(contentTypes);
return tmp && tmp.length;
});
if (!reqCandidates.length) {
next(new UnsupportedMediaTypeError(contentType));
return;
}
}
// Accept Version
if (isVersioned) {
var reqVersion = req.version();
var maxVersion;
var maxVersionIndex;
reqCandidates.forEach(function forEach(candidate, idx) {
var version = semver.maxSatisfying(
candidate.version,
reqVersion
);
if (version) {
if (!maxVersion || semver.gt(version, maxVersion)) {
maxVersion = version;
maxVersionIndex = idx;
}
}
});
// No version find
if (_.isUndefined(maxVersionIndex)) {
next(
new InvalidVersionError(
'%s is not supported by %s %s',
req.version() || '?',
req.method,
req.path()
)
);
return;
}
// Add api-version response header
res.header('api-version', maxVersion);
// Store matched version on request internal
req._matchedVersion = maxVersion;
// Run handler
reqCandidates[maxVersionIndex].handler(req, res, next);
return;
}
// When not versioned
reqCandidates[0].handler(req, res, next);
};
}
module.exports = conditionalHandler;
================================================
FILE: lib/plugins/conditionalRequest.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var errors = require('restify-errors');
///--- Globals
var BadRequestError = errors.BadRequestError;
var PreconditionFailedError = errors.PreconditionFailedError;
var IF_MATCH_FAIL = "if-match '%s' didn't match etag '%s'";
var IF_NO_MATCH_FAIL = "if-none-match '%s' matched etag '%s'";
var IF_MOD_FAIL = "object was modified at '%s'; if-modified-since '%s'";
var IF_UNMOD_FAIL = "object was modified at '%s'; if-unmodified-since '%s'";
///--- API
// Reference RFC2616 section 14 for an explanation of what this all does.
function checkIfMatch(req, res, next) {
var clientETags;
var cur;
var etag = res.etag || res.getHeader('etag') || '';
var ifMatch;
var matched = false;
if ((ifMatch = req.headers['if-match'])) {
clientETags = ifMatch.split(/\s*,\s*/);
for (var i = 0; i < clientETags.length; i++) {
cur = clientETags[i];
// only strong comparison
cur = cur.replace(/^W\//, '');
cur = cur.replace(/^"(\w*)"$/, '$1');
if (cur === '*' || cur === etag) {
matched = true;
break;
}
}
if (!matched) {
var err = new PreconditionFailedError(IF_MATCH_FAIL, ifMatch, etag);
return next(err);
}
}
return next();
}
function checkIfNoneMatch(req, res, next) {
var clientETags;
var cur;
var etag = res.etag || res.getHeader('etag') || '';
var ifNoneMatch;
var matched = false;
if ((ifNoneMatch = req.headers['if-none-match'])) {
clientETags = ifNoneMatch.split(/\s*,\s*/);
for (var i = 0; i < clientETags.length; i++) {
cur = clientETags[i];
// ignore weak validation
cur = cur.replace(/^W\//, '');
cur = cur.replace(/^"(\w*)"$/, '$1');
if (cur === '*' || cur === etag) {
matched = true;
break;
}
}
if (!matched) {
return next();
}
if (req.method !== 'GET' && req.method !== 'HEAD') {
var err = new PreconditionFailedError(
IF_NO_MATCH_FAIL,
ifNoneMatch,
etag
);
return next(err);
}
res.send(304);
return next(false);
}
return next();
}
function checkIfModified(req, res, next) {
var code;
var err;
var ctime = req.header('if-modified-since');
var mtime = res.mtime || res.header('Last-Modified') || '';
if (!mtime || !ctime) {
next();
return;
}
try {
//
// TODO handle Range header modifications
//
// Note: this is not technically correct as per 2616 -
// 2616 only specifies semantics for GET requests, not
// any other method - but using if-modified-since with a
// PUT or DELETE seems like returning 412 is sane
//
if (Date.parse(mtime) <= Date.parse(ctime)) {
switch (req.method) {
case 'GET':
case 'HEAD':
code = 304;
break;
default:
err = new PreconditionFailedError(
IF_MOD_FAIL,
mtime,
ctime
);
break;
}
}
} catch (e) {
next(new BadRequestError(e.message));
return;
}
if (code !== undefined) {
res.send(code);
next(false);
return;
}
next(err);
}
function checkIfUnmodified(req, res, next) {
var err;
var ctime = req.headers['if-unmodified-since'];
var mtime = res.mtime || res.header('Last-Modified') || '';
if (!mtime || !ctime) {
next();
return;
}
try {
if (Date.parse(mtime) > Date.parse(ctime)) {
err = new PreconditionFailedError(IF_UNMOD_FAIL, mtime, ctime);
}
} catch (e) {
next(new BadRequestError(e.message));
return;
}
next(err);
}
///--- Exports
/**
* Returns a set of plugins that will compare an already set `ETag` header with
* the client's `If-Match` and `If-None-Match` header, and an already set
* Last-Modified header with the client's `If-Modified-Since` and
* `If-Unmodified-Since` header.
*
* You can use this handler to let clients do nice HTTP semantics with the
* "match" headers. Specifically, with this plugin in place, you would set
* `res.etag=$yourhashhere`, and then this plugin will do one of:
*
* - return `304` (Not Modified) [and stop the handler chain]
* - return `412` (Precondition Failed) [and stop the handler chain]
* - Allow the request to go through the handler chain.
*
* The specific headers this plugin looks at are:
*
* - `Last-Modified`
* - `If-Match`
* - `If-None-Match`
* - `If-Modified-Since`
* - `If-Unmodified-Since`
*
* @public
* @throws {BadRequestError}
* @throws {PreconditionFailedError}
* @function conditionalRequest
* @returns {Function[]} Handlers
* @example
* server.use(restify.plugins.conditionalRequest());
* @example
* server.use(function setETag(req, res, next) {
* res.header('ETag', 'myETag');
* res.header('Last-Modified', new Date());
* });
*
* server.use(restify.plugins.conditionalRequest());
*
* server.get('/hello/:name', function(req, res, next) {
* res.send('hello ' + req.params.name);
* });
*/
function conditionalRequest() {
var chain = [
checkIfMatch,
checkIfNoneMatch,
checkIfModified,
checkIfUnmodified
];
return chain;
}
module.exports = conditionalRequest;
================================================
FILE: lib/plugins/cpuUsageThrottle.js
================================================
'use strict';
var assert = require('assert-plus');
var pidusage = require('pidusage');
var errors = require('restify-errors');
var EWMA = require('ewma');
/**
* cpuUsageThrottle is a middleware that rejects a variable number of requests
* (between 0% and 100%) based on a historical view of CPU utilization of a
* Node.js process. Essentially, this plugin allows you to define what
* constitutes a saturated Node.js process via CPU utilization and it will
* handle dropping a % of requests based on that definiton. This is useful when
* you would like to keep CPU bound tasks from piling up causing an increased
* per-request latency.
*
* The algorithm asks you for a maximum CPU utilization rate, which it uses to
* determine at what point it should be rejecting 100% of traffic. For a normal
* Node.js service, this is 1 since Node is single threaded. It uses this,
* paired with a limit that you provide to determine the total % of traffic it
* should be rejecting. For example, if you specify a limit of .5 and a max of
* 1, and the current EWMA (next paragraph) value reads .75, this plugin will
* reject approximately 50% of all requests.
*
* When looking at the process' CPU usage, this algorithm will take a load
* average over a user specified interval. example, if given an interval of
* 250ms, this plugin will attempt to record the average CPU utilization over
* 250ms intervals. Due to contention for resources, the duration of each
* average may be wider or narrower than 250ms. To compensate for this, we use
* an exponentially weighted moving average. The EWMA algorithm is provided by
* the ewma module. The parameter for configuring the EWMA is halfLife. This
* value controls how quickly each load average measurment decays to half it's
* value when being represented in the current average. For example, if you
* have an interval of 250, and a halfLife of 250, you will take the previous
* ewma value multiplied by 0.5 and add it to the new CPU utilization average
* measurement multiplied by 0.5. The previous value and the new measurement
* would each represent 50% of the new value. A good way of thinking about the
* halfLife is in terms of how responsive this plugin will be to spikes in CPU
* utilization. The higher the halfLife, the longer CPU utilization will have
* to remain above your defined limit before this plugin begins rejecting
* requests and, converserly, the longer it will have to drop below your limit
* before the plugin begins accepting requests again. This is a knob you will
* want to with play when trying to determine the ideal value for your use
* case.
*
* For a better understanding of the EWMA algorithn, refer to the documentation
* for the ewma module.
*
* @public
* @function cpuUsageThrottle
* @param {Object} opts - Configure this plugin.
* @param {Number} [opts.limit] - The point at which restify will begin
* rejecting a % of all requests at the front door.
* This value is a percentage.
* For example 0.8 === 80% average CPU utilization. Defaults to 0.75.
* @param {Number} [opts.max] - The point at which restify will reject 100% of
* all requests at the front door. This is used in conjunction with limit to
* determine what % of traffic restify needs to reject when attempting to
* bring the average load back to the user requested values. Since Node.js is
* single threaded, the default for this is 1. In some rare cases, a Node.js
* process can exceed 100% CPU usage and you will want to update this value.
* @param {Number} [opts.interval] - How frequently we calculate the average CPU
* utilization. When we calculate an average CPU utilization, we calculate it
* over this interval, and this drives whether or not we should be shedding
* load. This can be thought of as a "resolution" where the lower this value,
* the higher the resolution our load average will be and the more frequently
* we will recalculate the % of traffic we should be shedding. This check
* is rather lightweight, while the default is 250ms, you should be able to
* decrease this value without seeing a significant impact to performance.
* @param {Number} [opts.halfLife] - When we sample the CPU usage on an
* interval, we create a series of data points.
* We take these points and calculate a
* moving average. The halfLife indicates how quickly a point "decays" to
* half it's value in the moving average. The lower the halfLife, the more
* impact newer data points have on the average. If you want to be extremely
* responsive to spikes in CPU usage, set this to a lower value. If you want
* your process to put more emphasis on recent historical CPU usage when
* determininng whether it should shed load, set this to a higher value. The
* unit is in ms. Defaults to 250.
* @returns {Function} middleware to be registered on server.pre
* @example
* var restify = require('restify');
*
* var server = restify.createServer();
* const options = {
* limit: .75,
* max: 1,
* interval: 250,
* halfLife: 500,
* }
*
* server.pre(restify.plugins.cpuUsageThrottle(options));
* @example
*
* You can also update the plugin during runtime using the `.update()` function.
* This function accepts the same `opts` object as a constructor.
*
* var plugin = restify.plugins.cpuUsageThrottle(options);
* server.pre(plugin);
*
* plugin.update({ limit: .4, halfLife: 5000 });
*/
function cpuUsageThrottlePlugin(opts) {
// Scrub input and populate our configuration
assert.object(opts, 'opts');
assert.optionalNumber(opts.limit, 'opts.limit');
assert.optionalNumber(opts.max, 'opts.max');
assert.optionalNumber(opts.interval, 'opts.interval');
assert.optionalNumber(opts.halfLife, 'opts.halfLife');
var plugin = {};
plugin._limit = typeof opts.limit === 'number' ? opts.limit : 0.75;
plugin._max = opts.max || 1;
plugin._interval = opts.interval || 250;
plugin._halfLife = typeof opts.halfLife === 'number' ? opts.halfLife : 250;
assert.ok(plugin._max > plugin._limit, 'limit must be less than max');
plugin._ewma = new EWMA(plugin._halfLife);
// plugin._reject represents the % of traffic that we should reject at the
// current point in time based on how much over our limit we are. This is
// updated on an interval by updateReject().
plugin._reject = 0;
// plugin._timeout keeps track of the current handle for the setTimeout we
// use to gather CPU load averages, this allows us to cancel the timeout
// when shutting down restify.
plugin._timeout = null;
// plugin._timeoutDelta represents the amount of time between when we
// _should_ have run updateReject and the actual time it was invoked.
// This allows us to monitor lag caused by both the event loop
// and pidusage
plugin._timeoutDelta = 0;
plugin._timeoutStart = Date.now();
// updateReject should be called on an interval, it checks the average CPU
// usage between two invocations of updateReject.
function updateReject() {
pidusage(process.pid, function pidusageStat(e, stat) {
// Requeue an updateReject irrespective of whether or not pidusage
// encountered an error
plugin._timeout = setTimeout(updateReject, plugin._interval);
// If we were unable to get cpu usage, don't make any new decisions.
if (
!stat ||
typeof stat.cpu !== 'number' ||
Number.isNaN(stat.cpu)
) {
return;
}
// Divide by 100 to match Linux's `top` format
plugin._ewma.insert(stat.cpu / 100);
plugin._cpu = plugin._ewma.value();
// Update reject with the % of traffic we should be rejecting. This
// is safe since max > limit so the denominator can never be 0. If
// the current cpu usage is less that the limit, _reject will be
// negative and we will never shed load
plugin._reject =
(plugin._cpu - plugin._limit) / (plugin._max - plugin._limit);
// Calculate how long it took between when our interval should have
// updated the _reject value and how long it actually took. This
// metric accounts for the misbehaviour of pidusage
var now = Date.now();
plugin._timeoutDelta =
now - plugin._timeoutStart - plugin._interval;
plugin._timeoutStart = now;
});
}
// Kick off updating our _reject value
updateReject();
function cpuUsageThrottle(req, res, next) {
// Check to see if this request gets rejected. Since, in updateReject,
// we calculate a percentage of traffic we are planning to reject, we
// can use Math.random() (which picks from a uniform distribution in
// [0,1)) to give us a `plugin._reject`% chance of dropping any given
// request. This is a stateless was to drop approximatly
// `plugin._reject`% of traffic.
var probabilityDraw = Math.random();
if (probabilityDraw >= plugin._reject) {
return next(); // Don't reject this request
}
var err = new errors.ServiceUnavailableError({
context: {
plugin: 'cpuUsageThrottle',
cpuUsage: plugin._cpu,
limit: plugin._limit,
max: plugin._max,
reject: plugin._reject,
halfLife: plugin._halfLife,
interval: plugin._interval,
probabilityDraw: probabilityDraw,
lag: plugin._timeoutDelta
}
});
return next(err);
}
// Allow the app to clear the timeout for this plugin if necessary, without
// this we would never be able to clear the event loop when letting Node
// shut down gracefully
function close() {
clearTimeout(plugin._timeout);
}
cpuUsageThrottle.close = close;
// Expose internal plugin state for introspection
Object.defineProperty(cpuUsageThrottle, 'state', {
get: function get() {
// We intentionally do not expose ewma since we don't want the user
// to be able to update it's configuration, the current state of
// ewma is represented in plugin._cpu
return {
limit: plugin._limit,
max: plugin._max,
interval: plugin._interval,
halfLife: plugin._halfLife,
cpuUsage: plugin._cpu,
reject: plugin._reject,
lag: plugin._timeoutDelta
};
}
});
/**
* cpuUsageThrottle.update
*
* Allow the plugin's configuration to be updated during runtime.
*
* @private
* @param {Object} newOpts - The opts object for reconfiguring this plugin,
* it follows the same format as the constructor for this plugin.
* @returns {undefined} no return value
*/
cpuUsageThrottle.update = function update(newOpts) {
assert.object(newOpts, 'newOpts');
assert.optionalNumber(newOpts.limit, 'newOpts.limit');
assert.optionalNumber(newOpts.max, 'newOpts.max');
assert.optionalNumber(newOpts.interval, 'newOpts.interval');
assert.optionalNumber(newOpts.halfLife, 'newOpts.halfLife');
if (newOpts.limit !== undefined) {
plugin._limit = newOpts.limit;
}
if (newOpts.max !== undefined) {
plugin._max = newOpts.max;
}
if (newOpts.interval !== undefined) {
plugin._interval = newOpts.interval;
}
if (newOpts.halfLife !== undefined) {
plugin._halfLife = newOpts.halfLife;
// update our ewma with the new halfLife, we use the previous known
// state as the initial state for our new halfLife in lieu of
// having access to true historical data.
plugin._ewma = new EWMA(plugin._halfLife, plugin._cpu);
}
// Ensure new values are still valid
assert.ok(plugin._max > plugin._limit, 'limit must be less than max');
// Update _reject with the new settings
plugin._reject =
(plugin._cpu - plugin._limit) / (plugin._max - plugin._limit);
};
return cpuUsageThrottle;
}
module.exports = cpuUsageThrottlePlugin;
================================================
FILE: lib/plugins/date.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var assert = require('assert-plus');
var errors = require('restify-errors');
///--- Globals
var InvalidHeaderError = errors.InvalidHeaderError;
var RequestExpiredError = errors.RequestExpiredError;
var BAD_MSG = 'Date header is invalid';
var OLD_MSG = 'Date header %s is too old';
///--- API
/**
* Parses out the HTTP Date header (if present) and checks for clock skew.
* If the header is invalid, a `InvalidHeaderError` (`400`) is returned.
* If the clock skew exceeds the specified value,
* a `RequestExpiredError` (`400`) is returned.
* Where expired means the request originated at a time
* before (`$now - $clockSkew`).
* The default clockSkew allowance is 5m (thanks
* Kerberos!)
*
* @public
* @function dateParser
* @throws {RequestExpiredError}
* @throws {InvalidHeaderError}
* @param {Number} [clockSkew=300] - allowed clock skew in seconds.
* @returns {Function} restify handler.
* @example
* // Allows clock skew of 1m
* server.use(restify.plugins.dateParser(60));
*/
function dateParser(clockSkew) {
var normalizedClockSkew = clockSkew || 300;
assert.number(normalizedClockSkew, 'normalizedClockSkew');
normalizedClockSkew = normalizedClockSkew * 1000;
function parseDate(req, res, next) {
if (!req.headers.date) {
return next();
}
var e;
var date = req.headers.date;
var log = req.log;
try {
var now = Date.now();
var sent = new Date(date).getTime();
if (log.trace()) {
log.trace(
{
allowedSkew: normalizedClockSkew,
now: now,
sent: sent
},
'Checking clock skew'
);
}
if (now - sent > normalizedClockSkew) {
e = new RequestExpiredError(OLD_MSG, date);
return next(e);
}
} catch (err) {
log.trace(
{
err: err
},
'Bad Date header: %s',
date
);
e = new InvalidHeaderError(BAD_MSG, date);
return next(e);
}
return next();
}
return parseDate;
}
module.exports = dateParser;
================================================
FILE: lib/plugins/fieldedTextBodyParser.js
================================================
/**
* Dependencies
*/
'use strict';
var csv = require('csv');
var assert = require('assert-plus');
///--- API
/**
* Returns a plugin that will parse the HTTP request body if the
* contentType is `text/csv` or `text/tsv`.
*
* @public
* @function fieldedTextParser
* @param {Object} options - an options object
* @returns {Function} Handler
*/
function fieldedTextParser(options) {
assert.optionalObject(options, 'options');
function parseFieldedText(req, res, next) {
// save original body on req.rawBody and req._body
req.rawBody = req._body = req.body;
var contentType = req.getContentType();
if (
(contentType !== 'text/csv' &&
contentType !== 'text/tsv' &&
contentType !== 'text/tab-separated-values') ||
!req.body
) {
next();
return;
}
var hDelimiter = req.headers['x-content-delimiter'];
var hEscape = req.headers['x-content-escape'];
var hQuote = req.headers['x-content-quote'];
var hColumns = req.headers['x-content-columns'];
var delimiter = contentType === 'text/tsv' ? '\t' : ',';
delimiter = hDelimiter ? hDelimiter : delimiter;
var escape = hEscape ? hEscape : '\\';
var quote = hQuote ? hQuote : '"';
var columns = hColumns ? hColumns : true;
var parserOptions = {
delimiter: delimiter,
quote: quote,
escape: escape,
columns: columns
};
csv.parse(req.body, parserOptions, function parse(err, parsedBody) {
if (err) {
return next(err);
}
// Add an "index" property to every row
parsedBody.forEach(function forEach(row, index) {
row.index = index;
});
req.body = parsedBody;
return next();
});
}
return parseFieldedText;
}
module.exports = fieldedTextParser;
================================================
FILE: lib/plugins/formBodyParser.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var assert = require('assert-plus');
var querystring = require('qs');
var bodyReader = require('./bodyReader');
var errors = require('restify-errors');
///--- Globals
var MIME_TYPE = 'application/x-www-form-urlencoded';
///--- API
/**
* Returns a plugin that will parse the HTTP request body IFF the
* contentType is application/x-www-form-urlencoded.
*
* If req.params already contains a given key, that key is skipped and an
* error is logged.
*
* @public
* @function urlEncodedBodyParser
* @param {Object} options - an option sobject
* @returns {Function} Handler
*/
function urlEncodedBodyParser(options) {
var opts = options || {};
assert.object(opts, 'opts');
var override = opts.overrideParams;
function parseUrlEncodedBody(req, res, next) {
// save original body on req.rawBody and req._body
req.rawBody = req._body = req.body;
if (req.getContentType() !== MIME_TYPE || !req.body) {
next();
return;
}
try {
var params = querystring.parse(req.body);
if (opts.mapParams === true) {
var keys = Object.keys(params);
keys.forEach(function forEach(k) {
var p = req.params[k];
if (p && !override) {
return;
}
req.params[k] = params[k];
});
}
req.body = params;
} catch (e) {
next(new errors.InvalidContentError(e.message));
return;
}
req.log.trace('req.params now: %j', req.params);
next();
}
var chain = [];
if (!opts.bodyReader) {
chain.push(bodyReader(opts));
}
chain.push(parseUrlEncodedBody);
return chain;
}
module.exports = urlEncodedBodyParser;
================================================
FILE: lib/plugins/fullResponse.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var crypto = require('crypto');
var httpDate = require('./utils/httpDate');
var hrTimeDurationInMs = require('./utils/hrTimeDurationInMs');
///--- API
function setHeaders(req, res) {
var hash;
var now = new Date();
if (!res.getHeader('Connection')) {
res.setHeader('Connection', req.isKeepAlive() ? 'Keep-Alive' : 'close');
}
if (res._data && !res.getHeader('Content-MD5')) {
hash = crypto.createHash('md5');
hash.update(res._data);
res.setHeader('Content-MD5', hash.digest('base64'));
}
if (!res.getHeader('Date')) {
res.setHeader('Date', httpDate(now));
}
if (res.etag && !res.getHeader('Etag')) {
res.setHeader('Etag', res.etag);
}
if (!res.getHeader('Server')) {
res.setHeader('Server', res.serverName);
}
if (res.version && !res.getHeader('Api-Version')) {
res.setHeader('Api-Version', res.version);
}
if (!res.getHeader('Request-Id')) {
res.setHeader('Request-Id', req.getId());
}
if (!res.getHeader('Response-Time')) {
// we cannot use req._timeFlushed here as
// the response is not flushed yet
res.setHeader(
'Response-Time',
hrTimeDurationInMs(req._timeStart, process.hrtime())
);
}
}
/**
* handles disappeared CORS headers.
* https://github.com/restify/node-restify/issues/284
*
* @public
* @function fullResponse
* @returns {Function} Handler
*/
function fullResponse() {
function restifyResponseHeaders(req, res, next) {
res.once('header', function onceHeader() {
// Restify 1.0 compatibility
if (res.defaultResponseFormatters) {
res.defaultResponseFormatters(res._data);
}
res.emit('beforeSend', res._data, res._body);
// end backwards-compatibility
return setHeaders(req, res);
});
return next();
}
return restifyResponseHeaders;
}
///--- Exports
module.exports = fullResponse;
================================================
FILE: lib/plugins/gzip.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var zlib = require('zlib');
var assert = require('assert-plus');
/**
* @private
* @function _writeHead
* @param {Function} originalFunction - originalFunction
* @returns {undefined} no return value
*/
function _writeHead(originalFunction) {
this.removeHeader('Content-Length');
var argsLength = arguments.length;
var args = new Array(argsLength - 1);
for (var i = 1; i < argsLength; i++) {
args[i - 1] = arguments[i];
}
originalFunction.apply(this, args);
}
///--- API
/**
* If the client sends an `accept-encoding: gzip` header (or one with an
* appropriate q-val), then the server will automatically gzip all
* response data.
* Note that only `gzip` is supported, as this is most widely supported by
* clients in the wild.
* This plugin will overwrite some of the internal streams, so any
* calls to `res.send`, `res.write`, etc., will be compressed. A side effect is
* that the `content-length` header cannot be known, and so
* `transfer-encoding: chunked` will *always* be set when this is in effect.
* This plugin has no impact if the client does not send
* `accept-encoding: gzip`.
*
* https://github.com/restify/node-restify/issues/284
*
* @public
* @function gzipResponse
* @param {Object} [opts] - an options object, see: zlib.createGzip
* @returns {Function} Handler
* @example
* server.use(restify.plugins.gzipResponse());
*/
function gzipResponse(opts) {
assert.optionalObject(opts, 'options');
function gzip(req, res, next) {
if (!req.acceptsEncoding('gzip')) {
next();
return;
}
var gz = zlib.createGzip(opts);
gz.on('data', res.write.bind(res));
gz.once('end', res.end.bind(res));
gz.on('drain', res.emit.bind(res, 'drain'));
var origWrite = res.write;
var origEnd = res.end;
var origWriteHead = res.writeHead;
res.handledGzip = function _handledGzip() {
res.write = origWrite;
res.end = origEnd;
res.writeHead = origWriteHead;
};
res.write = gz.write.bind(gz);
res.end = gz.end.bind(gz);
res.writeHead = _writeHead.bind(res, res.writeHead);
res.setHeader('Content-Encoding', 'gzip');
next();
}
return gzip;
}
///--- Exports
module.exports = gzipResponse;
================================================
FILE: lib/plugins/index.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
///--- Exports
module.exports = {
acceptParser: require('./accept'),
auditLogger: require('./audit'),
authorizationParser: require('./authorization'),
bodyParser: require('./bodyParser'),
bodyReader: require('./bodyReader'),
conditionalHandler: require('./conditionalHandler'),
conditionalRequest: require('./conditionalRequest'),
cpuUsageThrottle: require('./cpuUsageThrottle.js'),
dateParser: require('./date'),
fullResponse: require('./fullResponse'),
gzipResponse: require('./gzip'),
inflightRequestThrottle: require('./inflightRequestThrottle'),
jsonBodyParser: require('./jsonBodyParser'),
jsonp: require('./jsonp'),
multipartBodyParser: require('./multipartBodyParser'),
oauth2TokenParser: require('./oauth2TokenParser'),
queryParser: require('./query'),
metrics: require('./metrics'),
requestExpiry: require('./requestExpiry'),
requestLogger: require('./requestLogger'),
serveStatic: require('./static'),
serveStaticFiles: require('./staticFiles'),
throttle: require('./throttle'),
urlEncodedBodyParser: require('./formBodyParser'),
pre: {
context: require('./pre/context'),
dedupeSlashes: require('./pre/dedupeSlashes'),
pause: require('./pre/pause'),
reqIdHeaders: require('./pre/reqIdHeaders'),
sanitizePath: require('./pre/prePath'),
strictQueryParams: require('./pre/strictQueryParams'),
userAgentConnection: require('./pre/userAgent')
}
};
================================================
FILE: lib/plugins/inflightRequestThrottle.js
================================================
'use strict';
var assert = require('assert-plus');
var ServiceUnavailableError = require('restify-errors').ServiceUnavailableError;
/**
* The `inflightRequestThrottle` module allows you to specify an upper limit to
* the maximum number of inflight requests your server is able to handle. This
* is a simple heuristic for protecting against event loop contention between
* requests causing unacceptable latencies.
*
* The custom error is optional, and allows you to specify your own response
* and status code when rejecting incoming requests due to too many inflight
* requests. It defaults to `503 ServiceUnavailableError`.
*
* This plugin should be registered as early as possibly in the middleware stack
* using `pre` to avoid performing unnecessary work.
*
* @public
* @function inflightRequestThrottle
* @param {Object} opts - configure this plugin
* @param {Number} opts.limit - maximum number of inflight requests the server
* will handle before returning an error
* @param {Error} opts.err - A restify error used as a response when the
* inflight request limit is exceeded
* @param {Function} opts.server - the instance of the restify server this
* plugin will throttle.
* @returns {Function} middleware to be registered on server.pre
* @example
* var errors = require('restify-errors');
* var restify = require('restify');
*
* var server = restify.createServer();
* const options = { limit: 600, server: server };
* options.res = new errors.InternalServerError();
* server.pre(restify.plugins.inflightRequestThrottle(options));
*/
function inflightRequestThrottle(opts) {
// Scrub input and populate our configuration
assert.object(opts, 'opts');
assert.number(opts.limit, 'opts.limit');
assert.object(opts.server, 'opts.server');
assert.func(opts.server.inflightRequests, 'opts.server.inflightRequests');
if (opts.err !== undefined && opts.err !== null) {
assert.ok(opts.err instanceof Error, 'opts.err must be an error');
assert.optionalNumber(opts.err.statusCode, 'opts.err.statusCode');
}
var plugin = {};
plugin._err = opts.err || new ServiceUnavailableError('resource exhausted');
plugin._limit = opts.limit;
plugin._server = opts.server;
function onRequest(req, res, next) {
var inflightRequests = plugin._server.inflightRequests();
if (inflightRequests > plugin._limit) {
req.log.trace(
{
plugin: 'inflightRequestThrottle',
inflightRequests: inflightRequests,
limit: plugin._limit
},
'maximum inflight requests exceeded, rejecting request'
);
return next(plugin._err);
}
return next();
}
return onRequest;
}
module.exports = inflightRequestThrottle;
================================================
FILE: lib/plugins/jsonBodyParser.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var assert = require('assert-plus');
var errors = require('restify-errors');
var bodyReader = require('./bodyReader');
var regex = require('./utils/regex');
///--- API
/**
* Parses json body from the request.
*
* @public
* @function jsonBodyParser
* @param {Object} options - an options object
* @throws {InvalidContentError} on bad input
* @returns {Function} Handler
*/
function jsonBodyParser(options) {
assert.optionalObject(options, 'options');
var opts = options || {};
var override = opts.overrideParams;
function parseJson(req, res, next) {
// save original body on req.rawBody and req._body
req.rawBody = req._body = req.body;
var contentType = req.getContentType();
// check for empty body first, don't pay regex tax unless necessary.
// for content type, check for exact match and any of the *+json types
if (
!req.body ||
(contentType !== 'application/json' &&
!regex.jsonContentType.test(contentType))
) {
return next();
}
var params;
try {
params = JSON.parse(req.body, opts.reviver);
} catch (e) {
return next(
new errors.InvalidContentError(
'%s',
'Invalid JSON: ' + e.message
)
);
}
if (opts.mapParams === true) {
if (Array.isArray(params)) {
// if req.params exists, we have url params. we can't map an
// array safely onto req.params, throw an error.
if (
req.params &&
Object.keys(req.params).length > 0 &&
!(req.params instanceof Array)
) {
return next(
new errors.InternalServerError(
'Cannot map POST body of [Array array] onto ' +
'req.params'
)
);
}
req.params = params;
} else if (typeof params === 'object' && params !== null) {
// else, try to merge the objects
Object.keys(params).forEach(function forEach(k) {
var p = req.params[k];
if (p && !override) {
return;
}
req.params[k] = params[k];
});
} else {
// otherwise, do a wholesale stomp, no need to merge one by one.
req.params = params || req.params;
}
}
req.body = params;
return next();
}
var chain = [];
if (!opts.bodyReader) {
chain.push(bodyReader(opts));
}
chain.push(parseJson);
return chain;
}
module.exports = jsonBodyParser;
================================================
FILE: lib/plugins/jsonp.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var qs = require('qs');
///--- API
/**
* Parses the jsonp callback out of the request.
* Supports checking the query string for `callback` or `jsonp` and ensuring
* that the content-type is appropriately set if JSONP params are in place.
* There is also a default `application/javascript` formatter to handle this.
*
* You *should* set the `queryParser` plugin to run before this, but if you
* don't this plugin will still parse the query string properly.
*
* @public
* @function jsonp
* @returns {Function} Handler
* @example
* var server = restify.createServer();
* server.use(restify.plugins.jsonp());
*/
function jsonp() {
function _jsonp(req, res, next) {
var q = req.getQuery();
// If the query plugin wasn't used, we need to hack it in now
if (typeof q === 'string') {
req.query = qs.parse(q);
}
if (req.query.callback || req.query.jsonp) {
res.setHeader('Content-Type', 'application/javascript');
}
next();
}
return _jsonp;
}
module.exports = jsonp;
================================================
FILE: lib/plugins/metrics.js
================================================
'use strict';
var assert = require('assert-plus');
var hrTimeDurationInMs = require('./utils/hrTimeDurationInMs');
/**
* Timing internals
*
* Timings are also saved when there is no handler in the given category.
* Some handler categories are optional, for example there is no
* `use` and `route` for 404.
*
* @private
*
* req._timeStart - request lifecycle started in restify
* req._timePreStart - pre handlers started
* req._timePreEnd - all pre handlers finished
* req._timeUseStart - use handlers started
* req._timeUseEnd - all use handlers finished
* req._timeRouteStart - route handlers started
* req._timeRouteEnd - all route handlers finished
* req._timeFlushed - request flushed, may happens before handlers finished
* req._timeFinished - both all handlers finished and request flushed
*/
///--- API
/**
* The module includes the following plugins to be used with restify's `after`
* event, e.g., `server.on('after', restify.plugins.metrics());`:
*
* A plugin that listens to the server's after event and emits information
* about that request.
*
* @public
* @function metrics
* @param {Object} opts - an options obj
* @param {Server} opts.server - restify server
* @param {createMetrics~callback} callback - a callback fn
* @returns {Function} returns a function suitable to be used
* with restify server's `after` event
* @example
* server.on('after', restify.plugins.metrics({ server: server },
* function (err, metrics, req, res, route) {
* // metrics is an object containing information about the request
* }));
*/
function createMetrics(opts, callback) {
assert.object(opts, 'opts');
assert.object(opts.server, 'opts.server');
assert.func(callback, 'callback');
return function metrics(req, res, route, err) {
var data = {
// response status code. in most cases this should be a proper
// http status code, but in the case of an uncaughtException it can
// be undefined. otherwise, in most normal scenarios, even calling
// res.send() or res.end() should result in a 200 by default.
statusCode: res.statusCode,
// REST verb
method: req.method,
// overall request latency
totalLatency: hrTimeDurationInMs(req._timeStart, req._timeFinished),
latency: hrTimeDurationInMs(req._timeStart, req._timeFlushed),
preLatency: hrTimeDurationInMs(req._timePreStart, req._timePreEnd),
useLatency: hrTimeDurationInMs(req._timeUseStart, req._timeUseEnd),
routeLatency: hrTimeDurationInMs(
req._timeRouteStart,
req._timeRouteEnd
),
// the cleaned up url path
// e.g., /foo?a=1 => /foo
path: req.path(),
// connection state can currently only have the following values:
// 'close' | undefined.
//
// if the connection state is 'close'
// the status code will be set to 444
// it is possible to get a 200 statusCode with a connectionState
// value of 'close'. i.e., the client timed out,
// but restify thinks it "sent" a response. connectionState should
// always be the primary source of truth here, and check it first
// before consuming statusCode. otherwise, it may result in skewed
// metrics.
connectionState: req.connectionState && req.connectionState(),
unfinishedRequests:
opts.server.inflightRequests && opts.server.inflightRequests(),
inflightRequests:
opts.server.inflightRequests && opts.server.inflightRequests()
};
return callback(err, data, req, res, route);
};
}
/**
* Callback used by metrics plugin
* @callback metrics~callback
* @param {Error} err
* @param {Object} metrics - metrics about the request
* @param {Number} metrics.statusCode status code of the response. can be
* undefined in the case of an uncaughtException
* @param {String} metrics.method http request verb
* @param {Number} metrics.totalLatency latency includes both request is flushed
* and all handlers finished
* @param {Number} metrics.latency latency when request is flushed
* @param {Number|null} metrics.preLatency pre handlers latency
* @param {Number|null} metrics.useLatency use handlers latency
* @param {Number|null} metrics.routeLatency route handlers latency
* @param {String} metrics.path `req.path()` value
* @param {Number} metrics.inflightRequests Number of inflight requests pending
* in restify.
* @param {Number} metrics.unifinishedRequests Same as `inflightRequests`
* @param {String} metrics.connectionState can be either `'close'` or
* `undefined`. If this value is set, err will be a
* corresponding `RequestCloseError`.
* If connectionState is either
* `'close'`, then the `statusCode` is not applicable since the
* connection was severed before a response was written.
* @param {Request} req the request obj
* @param {Response} res the response obj
* @param {Route} route the route obj that serviced the request
*/
///-- Exports
module.exports = createMetrics;
================================================
FILE: lib/plugins/multipartBodyParser.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var fs = require('fs');
var assert = require('assert-plus');
var formidable = require('formidable');
var once = require('once');
var errors = require('restify-errors');
var vasync = require('vasync');
///--- API
/**
* Returns a plugin that will parse the HTTP request body IFF the
* contentType is multipart/form-data
*
* If req.params already contains a given key, that key is skipped and an
* error is logged.
*
* @public
* @function multipartBodyParser
* @param {Object} options - an options object
* @throws {BadRequestError}
* @returns {Function} Handler
*/
function multipartBodyParser(options) {
var opts = options || {};
assert.object(opts, 'opts');
assert.optionalBool(opts.overrideParams, 'opts.overrideParams');
assert.optionalBool(opts.multiples, 'opts.multiples');
assert.optionalBool(opts.keepExtensions, 'opts.keepExtensions');
assert.optionalString(opts.uploadDir, 'opts.uploadDir');
assert.optionalNumber(opts.maxFieldsSize, 'opts.maxFieldsSize');
assert.optionalString(opts.hash, 'opts.hash');
assert.optionalFunc(opts.multipartFileHandler, 'opts.multipartFileHandler');
assert.optionalFunc(opts.multipartHandler, 'opts.multipartHandler');
assert.optionalBool(opts.mapParams, 'opts.mapParams');
assert.optionalBool(opts.mapFiles, 'opts.mapFiles');
assert.optionalNumber(opts.maxFileSize, 'opts.maxFileSize');
var override = opts.overrideParams;
function parseMultipartBody(req, res, originalNext) {
// save original body on req.rawBody and req._body
req.rawBody = req._body = undefined;
var next = once(originalNext);
if (
req.getContentType() !== 'multipart/form-data' ||
(req.getContentLength() === 0 && !req.isChunked())
) {
return next();
}
var form = new formidable.IncomingForm();
// enable multiple files on a single upload field
// (html5 multiple attribute)
form.multiples = opts.multiples || false;
form.keepExtensions = opts.keepExtensions ? true : false;
if (opts.uploadDir) {
form.uploadDir = opts.uploadDir;
}
if (opts.maxFieldsSize) {
form.maxFieldsSize = opts.maxFieldsSize;
}
if (opts.maxFileSize) {
form.maxFileSize = opts.maxFileSize;
}
if (opts.hash) {
form.hash = opts.hash;
}
form.onPart = function onPart(part) {
if (part.filename && opts.multipartFileHandler) {
opts.multipartFileHandler(part, req);
} else if (!part.filename && opts.multipartHandler) {
opts.multipartHandler(part, req);
} else {
form.handlePart(part);
}
};
return form.parse(req, function parse(err, fields, files) {
if (err) {
return next(new errors.BadRequestError(err.message));
}
req.body = fields;
req.files = files;
if (opts.mapParams !== false) {
Object.keys(fields).forEach(function forEach(k) {
if (req.params[k] && !override) {
return;
}
req.params[k] = fields[k];
});
if (opts.mapFiles) {
var barrier = vasync.barrier();
barrier.on('drain', function onDrain() {
return next();
});
barrier.start('fs');
Object.keys(files).forEach(function forEach(f) {
if (req.params[f] && !override) {
return;
}
barrier.start('fs' + f);
fs.readFile(files[f].path, function readFile(ex, data) {
barrier.done('fs' + f);
/*
* We want to stop the request here, if there's
* an error trying to read the file from disk.
* Ideally we'd like to stop the other oustanding
* file reads too, but there's no way to cancel
* in flight fs reads. So we just return an
* error, and be grudgingly let the other file
* reads finish.
*/
if (ex) {
return next(
new errors.InternalError(
ex,
'unable to read file' + f
)
);
}
req.params[f] = data;
return true;
});
});
barrier.done('fs');
return false;
} else {
return next();
}
} else {
return next();
}
});
}
return parseMultipartBody;
}
module.exports = multipartBodyParser;
================================================
FILE: lib/plugins/oauth2TokenParser.js
================================================
/*
oauth2TokenParser - Parser oauth2 tokens from the authorization header
or BODY of the request
If parsing from the BODY there is adependency on the bodyParser plugin:
server.use(plugins.bodyParser());
server.use(plugins.oauth2TokenParser());
*/
'use strict';
var errors = require('restify-errors');
/*
Parses the header for the authorization: bearer
*/
function parseHeader(req) {
if (req.headers && req.headers.authorization) {
var credentialsIndex = 1;
var parts = req.headers.authorization.split(' ');
var partsExpectedLength = 2;
var schemeIndex = 0;
if (parts.length === partsExpectedLength) {
var credentials = parts[credentialsIndex];
var scheme = parts[schemeIndex];
if (/^Bearer$/i.test(scheme)) {
return credentials;
}
}
}
return null;
}
/**
* Returns a plugin that will parse the client's request for an OAUTH2
access token
*
* Subsequent handlers will see `req.oauth2`, which looks like:
*
* ```js
* {
* oauth2: {
accessToken: 'mF_9.B5f-4.1JqM&p=q'
}
* }
* ```
*
* @public
* @function oauth2TokenParser
* @throws {InvalidArgumentError}
* @param {Object} options - an options object
* @returns {Function} Handler
*/
function oauth2TokenParser(options) {
function parseOauth2Token(req, res, next) {
req.oauth2 = { accessToken: null };
var tokenFromHeader = parseHeader(req);
if (tokenFromHeader) {
req.oauth2.accessToken = tokenFromHeader;
}
var tokenFromBody = null;
if (typeof req.body === 'object') {
tokenFromBody = req.body.access_token;
}
// more than one method to transmit the token in each request
// is not allowed - return 400
if (tokenFromBody && tokenFromHeader) {
// eslint-disable-next-line new-cap
return next(
new errors.makeErrFromCode(400, 'multiple tokens disallowed')
);
}
if (
tokenFromBody &&
req.contentType().toLowerCase() ===
'application/x-www-form-urlencoded'
) {
req.oauth2.accessToken = tokenFromBody;
}
return next();
}
return parseOauth2Token;
}
module.exports = oauth2TokenParser;
================================================
FILE: lib/plugins/pre/context.js
================================================
// Copyright 2018 Restify. All rights reserved.
'use strict';
var assert = require('assert-plus');
///--- API
/**
* This plugin creates `req.set(key, val)` and `req.get(key)` methods for
* setting and retrieving request specific data.
*
* @public
* @function context
* @returns {Function} Handler
* @example
* server.pre(restify.plugins.pre.context());
* server.get('/', [
* function(req, res, next) {
* req.set(myMessage, 'hello world');
* return next();
* },
* function two(req, res, next) {
* res.send(req.get(myMessage)); // => sends 'hello world'
* return next();
* }
* ]);
*/
function ctx() {
return function context(req, res, next) {
var data = {};
/**
* Set context value by key
* Requires the context plugin.
*
* @public
* @memberof Request
* @instance
* @function req.set
* @param {String} key - key
* @param {*} value - value
* @returns {undefined} no return value
*/
req.set = function set(key, value) {
assert.string(key, 'key must be string');
if (key === '') {
assert.fail('key must not be empty string');
}
data[key] = value;
};
/**
* Get context value by key.
* Requires the context plugin.
*
* @public
* @memberof Request
* @instance
* @function req.get
* @param {String} key - key
* @returns {*} value stored in context
*/
req.get = function get(key) {
assert.string(key, 'key must be string');
if (key === '') {
assert.fail('key must not be empty string');
}
return data[key];
};
/**
* Get all context
* Requires the context plugin.
*
* @public
* @memberof Request
* @instance
* @function req.getAll
* @returns {*} value stored in context
*/
req.getAll = function getAll() {
return data;
};
return next();
};
}
///--- Exports
module.exports = ctx;
================================================
FILE: lib/plugins/pre/dedupeSlashes.js
================================================
'use strict';
/**
* This plugin deduplicates extra slashes found in the URL. This can help with
* malformed URLs that might otherwise get misrouted.
*
* @public
* @function dedupeSlashes
* @returns {Function} Handler
* @example
* server.pre(restify.plugins.pre.dedupeSlashes());
* server.get('/hello/:one', function(req, res, next) {
* res.send(200);
* return next();
* });
*
* // the server will now convert requests to /hello//jake => /hello/jake
*/
function createDedupeSlashes() {
return function dedupeSlashes(req, res, next) {
req.url = req.url.replace(/(\/)\/+/g, '$1');
return next();
};
}
module.exports = createDedupeSlashes;
================================================
FILE: lib/plugins/pre/pause.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
///--- Helpers
/**
* @private
* @function pauseStream
* @param {Stream} stream - the stream to pause
* @returns {undefined} no return value
*/
function pauseStream(stream) {
function _buffer(chunk) {
stream.__buffered.push(chunk);
}
function _catchEnd(chunk) {
stream.__rstfyEnded = true;
}
stream.__rstfyEnded = false;
stream.__rstfyPaused = true;
stream.__buffered = [];
stream.on('data', _buffer);
stream.once('end', _catchEnd);
stream.pause();
stream._resume = stream.resume;
stream.resume = function _rstfy_resume() {
if (!stream.__rstfyPaused) {
return;
}
stream.removeListener('data', _buffer);
stream.removeListener('end', _catchEnd);
stream.__buffered.forEach(stream.emit.bind(stream, 'data'));
stream.__buffered.length = 0;
stream._resume();
stream.resume = stream._resume;
if (stream.__rstfyEnded) {
stream.emit('end');
}
};
}
/**
* This pre handler fixes issues with node hanging when an `asyncHandler` is
* used prior to `bodyParser`.
* https://github.com/restify/node-restify/issues/287
* https://github.com/restify/node-restify/issues/409
* https://github.com/restify/node-restify/wiki/1.4-to-2.0-Migration-Tips
*
* @public
* @function pause
* @returns {Function} Handler
*/
function pause() {
function prePause(req, res, next) {
pauseStream(req);
next();
}
return prePause;
}
///--- Exports
module.exports = pause;
================================================
FILE: lib/plugins/pre/prePath.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
///--- Helpers
/**
* Cleans up sloppy URLs on the request object, like /foo////bar/// to /foo/bar.
*
* @private
* @function strip
* @param {Object} path - a url path to clean up
* @returns {String} cleaned path
*/
function strip(path) {
var cur;
var next;
var str = '';
for (var i = 0; i < path.length; i++) {
cur = path.charAt(i);
if (i !== path.length - 1) {
next = path.charAt(i + 1);
}
if (cur === '/' && (next === '/' || (next === '?' && i > 0))) {
continue;
}
str += cur;
}
return str;
}
/**
* Cleans up sloppy URLs on the request object,
* like `/foo////bar///` to `/foo/bar`.
*
* @public
* @function sanitizePath
* @returns {Function} Handler
*/
function sanitizePath() {
function _sanitizePath(req, res, next) {
req.url = strip(req.url);
next();
}
return _sanitizePath;
}
///--- Exports
module.exports = sanitizePath;
================================================
FILE: lib/plugins/pre/reqIdHeaders.js
================================================
'use strict';
var assert = require('assert-plus');
var DEFAULT_HEADERS = ['request-id', 'x-request-id'];
/**
* This plugin pulls the value from an incoming request header and uses it
* as the value of the request id. Subsequent calls to `req.id()`
* will return the header values.
*
* @public
* @function reqIdHeaders
* @param {Object} opts - an options object
* @param {String[]} opts.headers - array of headers from where to pull existing
* request id headers. Lookup precedence
* is left to right (lowest index first)
* @returns {Function} Handler
*/
function createReqIdHeaders(opts) {
assert.object(opts, 'opts');
assert.arrayOfString(opts.headers, 'opts.headers');
var headers = opts.headers.concat(DEFAULT_HEADERS);
return function reqIdHeaders(req, res, next) {
for (var i = 0; i < headers.length; i++) {
var val = req.header(headers[i]);
if (val) {
req.id(val);
break;
}
}
return next();
};
}
///--- Exports
module.exports = createReqIdHeaders;
================================================
FILE: lib/plugins/pre/strictQueryParams.js
================================================
'use strict';
var BadRequestError = require('restify-errors').BadRequestError;
var assert = require('assert-plus');
///--- API
/**
* Prevents `req.urls` non-strict key-value query params
*
* The Request-URI is transmitted in the format specified in section 3.2.1.
* If the Request-URI is encoded using the "% HEX HEX" encoding [42],
* the origin server MUST decode the Request-URI
* in order to properly interpret the request.
* Servers SHOULD respond to invalid Request-URIs
* with an appropriate status code.
*
* part of Hypertext Transfer Protocol -- HTTP/1.1 | 5.1.2 Request-URI
* RFC 2616 Fielding, et al.
*
* @public
* @function strictQueryParams
* @param {Object} [options] - an options object
* @param {String} [options.message] - a custom error message
* default value:
* "Url query params does not meet strict format"
* @returns {Function} Handler
*/
function strictQueryParams(options) {
var opts = options || {};
assert.optionalObject(opts, 'options');
assert.optionalString(opts.message, 'options.message');
function _strictQueryParams(req, res, next) {
var keyValQParams = !/(\&(?!(\w+=\w+)))/.test(req.url);
if (!keyValQParams) {
var msg = opts.message
? opts.message
: 'Url query params does not meet strict format';
return next(new BadRequestError(msg));
}
return next();
}
return _strictQueryParams;
}
///--- Exports
module.exports = strictQueryParams;
================================================
FILE: lib/plugins/pre/userAgent.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var assert = require('assert-plus');
///--- API
/**
* This basically exists for `curl`. `curl` on `HEAD` requests usually
* just sits there and hangs, unless you explicitly set
* Connection:close. And in general, you probably want to set
* Connection: close to curl anyway.
*
* Also, because curl spits out an annoying message to stderr about
* remaining bytes if content-length is set, this plugin also drops
* the `content-length` header (some user agents handle it and want it,
* curl does not).
*
* To be slightly more generic, the options block takes a user
* agent regexp, however.
*
* @public
* @function userAgentConnection
* @param {Object} [options] - an options object
* @param {RegExp} [options.userAgentRegExp=/^curl.+/] - matching any
* user-agents applicable
* @returns {Function} Handler
*/
function userAgentConnection(options) {
var opts = options || {};
assert.optionalObject(opts, 'options');
assert.optionalObject(opts.userAgentRegExp, 'options.userAgentRegExp');
var re = opts.userAgentRegExp;
if (!re) {
re = /^curl.+/;
}
function handleUserAgent(req, res, next) {
var ua = req.headers['user-agent'];
if (ua && re.test(ua)) {
res.setHeader('Connection', 'close');
if (req.method === 'HEAD') {
res.once(
'header',
res.removeHeader.bind(res, 'content-length')
);
}
}
next();
}
return handleUserAgent;
}
module.exports = userAgentConnection;
================================================
FILE: lib/plugins/query.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var qs = require('qs');
var assert = require('assert-plus');
/**
* Parses the HTTP query string (i.e., `/foo?id=bar&name=mark`).
* If you use this, the parsed content will always be available in `req.query`,
* additionally params are merged into `req.params`.
* You can disable by passing in `mapParams: false` in the options object.
*
* Many options correspond directly to option defined for the underlying
* [`qs.parse`](https://github.com/ljharb/qs).
*
* @public
* @function queryParser
* @param {Object} [options] - an options object
* @param {Object} [options.mapParams=true] - disable passing
* @param {Boolean} [options.mapParams=false] - Copies parsed query parameters
* into`req.params`.
* @param {Boolean} [options.overrideParams=false] - Only applies when if
* mapParams true.
* When true, will stomp on req.params field when existing value is found.
* @param {Boolean} [options.allowDots=false] - Transform `?foo.bar=baz` to a
* nested object: `{foo: {bar: 'baz'}}`.
* @param {Number} [options.arrayLimit=20] - Only transform `?a[$index]=b`
* to an array if `$index` is less than `arrayLimit`.
* @param {Number} [options.depth=5] - The depth limit for parsing
* nested objects, e.g. `?a[b][c][d][e][f][g][h][i]=j`.
* @param {Number} [options.parameterLimit=1000] - Maximum number of query
* params parsed. Additional params are silently dropped.
* @param {Boolean} [options.parseArrays=true] - Whether to parse
* `?a[]=b&a[1]=c` to an array, e.g. `{a: ['b', 'c']}`.
* @param {Boolean} [options.plainObjects=false] - Whether `req.query` is a
* "plain" object -- does not inherit from `Object`.
* This can be used to allow query params whose names collide with Object
* methods, e.g. `?hasOwnProperty=blah`.
* @param {Boolean} [options.strictNullHandling=false] - If true, `?a&b=`
* results in `{a: null, b: ''}`. Otherwise, `{a: '', b: ''}`.
* @returns {Function} Handler
* @example
* server.use(restify.plugins.queryParser({ mapParams: false }));
*/
function queryParser(options) {
var opts = options || {};
assert.object(opts, 'opts');
function parseQueryString(req, res, next) {
if (!req.getQuery()) {
req.query = {};
return next();
}
req.query = qs.parse(req.getQuery(), opts);
if (opts.mapParams === true) {
Object.keys(req.query).forEach(function forEach(k) {
if (req.params[k] && !opts.overrideParams) {
return;
}
req.params[k] = req.query[k];
});
}
return next();
}
return parseQueryString;
}
module.exports = queryParser;
================================================
FILE: lib/plugins/requestExpiry.js
================================================
'use strict';
var assert = require('assert-plus');
var GatewayTimeoutError = require('restify-errors').GatewayTimeoutError;
/**
* Request Expiry can be used to throttle requests that have already exceeded
* their client timeouts. Requests can be sent with a configurable client
* timeout header, e.g. 'x-request-expiry-time', which gives in absolute ms
* since epoch, when this request will be timed out by the client.
*
* This plugin will throttle all incoming requests via a 504 where
* 'x-request-expiry-time' less than Date.now() -- since these incoming requests
* have already been timed out by the client. This prevents the server from
* processing unnecessary requests.
*
* Request expiry will use headers to tell if the incoming request has expired.
* There are two options for this plugin:
* 1. Absolute Time
* * Time in Milliseconds since Epoch when this request should be
* considered expired
* 2. Timeout
* * The request start time is supplied
* * A timeout, in milliseconds, is given
* * The timeout is added to the request start time to arrive at the
* absolute time in which the request is considered expired
*
* #### Using an external storage mechanism for key/bucket mappings.
*
* By default, the restify throttling plugin uses an in-memory LRU to store
* mappings between throttling keys (i.e., IP address) to the actual bucket that
* key is consuming. If this suits you, you can tune the maximum number of keys
* to store in memory with `options.maxKeys`; the default is 10000.
*
* In some circumstances, you want to offload this into a shared system, such as
* Redis, if you have a fleet of API servers and you're not getting steady
* and/or uniform request distribution. To enable this, you can pass in
* `options.tokensTable`, which is simply any Object that supports `put` and
* `get` with a `String` key, and an `Object` value.
*
* @public
* @function requestExpiry
* @param {Object} opts - an options object
* @param {String} [opts.absoluteHeader] - The header key to be used for
* the expiry time of each request.
* @param {String} opts.startHeader - The header key for the start time
* of the request.
* @param {String} opts.timeoutHeader - The header key for the time in
* milliseconds that should ellapse before
* the request is considered expired.
* @returns {Function} Handler
* @example
*
* The only option provided is `header` which is the request header used
* to specify the client timeout.
*
* server.use(restify.plugins.requestExpiry({
* header: 'x-request-expiry-time'
* });
*/
function requestExpiry(opts) {
assert.object(opts, 'opts');
assert.optionalString(opts.absoluteHeader, 'opts.absoluteHeader');
if (!opts.absoluteHeader) {
assert.string(opts.startHeader, 'opts.startHeader');
assert.string(opts.timeoutHeader, 'opts.timeoutHeader');
}
var useAbsolute = opts.absoluteHeader !== undefined;
var absoluteHeaderKey = opts.absoluteHeader;
var startHeaderKey = opts.startHeader;
var timeoutHeaderKey = opts.timeoutHeader;
return function requestExpirationCheck(req, res, next) {
/*
* Add check expiry API to to req if it doesn't already exist. We only
* add this the first time this handler is run, since this handler
* could be used in multiple places in the handler chain.
*/
if (!req._expiryTime) {
// if the headers don't exist, then the request will never expire.
req._expiryTime = Infinity;
if (useAbsolute) {
var expiryTime = parseInt(req.headers[absoluteHeaderKey], 10);
if (!isNaN(expiryTime)) {
req._expiryTime = expiryTime;
}
} else {
// Use the start time header and add the timeout header to it
// to arrive at the expiration time
var startTime = parseInt(req.headers[startHeaderKey], 10);
var timeout = parseInt(req.headers[timeoutHeaderKey], 10);
if (!isNaN(startTime) && !isNaN(timeout)) {
req._expiryTime = startTime + timeout;
}
}
req.isExpired = function isExpired() {
return Date.now() > req._expiryTime;
};
}
if (req.isExpired()) {
// The request has expired
return next(
new GatewayTimeoutError({
message: 'Request has expired',
context: {
expiryTime: req._expiryTime,
mode: opts.absoluteHeader ? 'absolute' : 'relative'
}
})
);
} else {
// Happy case
return next();
}
};
}
module.exports = requestExpiry;
================================================
FILE: lib/plugins/requestLogger.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var assert = require('assert-plus');
var shallowCopy = require('./utils/shallowCopy');
///--- API
/**
* Sets up a child [logger](https://github.com/pinojs/pino) logger with
* the current request id filled in, along with any other parameters you define.
*
* You can pass in no options to this, in which case only the request id will be
* appended, and no serializers appended (this is also the most performant); the
* logger created at server creation time will be used as the parent logger.
* This logger can be used normally, with [req.log](#request-api).
*
* This plugin does _not_ log each individual request. Use the Audit Logging
* plugin or a custom middleware for that use.
*
* @public
* @function requestLogger
* @param {Object} [options] - an options object
* @param {Array} [options.headers] - A list of headers to transfer from
* the request to top level props on the log.
* @param {Object} [options.properties] - A set of key-values to pass to the child logger
* @param {Object} [options.serializers] - Override serializers to use in the child logger
* @param {Object} [options.log] - A logger to use as a fallback if req.log is missing
* @param {String} [options.requestIdFieldName] - The name of the request id property attached
* to log lines. Defaults to "req_id".
* @returns {Function} Handler
* @example
* server.use(restify.plugins.requestLogger({
* properties: {
* foo: 'bar'
* },
* serializers: {...}
* }));
*/
function requestLogger(options) {
assert.optionalObject(options);
var opts = options || {};
var props;
if (opts.properties) {
props = shallowCopy(opts.properties);
} else {
props = {};
}
if (opts.serializers) {
props.serializers = opts.serializers;
}
var headersToCopy = opts.headers || [];
const requestIdFieldName = opts.requestIdFieldName || 'req_id';
return function logger(req, res, next) {
if (!req.log && !opts.log) {
next();
return;
}
var log = req.log || opts.log;
props[requestIdFieldName] = req.getId();
headersToCopy.forEach(function forEach(k) {
if (req.headers[k]) {
props[k] = req.headers[k];
}
});
const childOptions = {};
if (props.serializers) {
childOptions.serializers = props.serializers;
}
req.log = log.child(props, childOptions);
if (props[requestIdFieldName]) {
delete props[requestIdFieldName];
}
next();
};
}
///--- Exports
module.exports = requestLogger;
================================================
FILE: lib/plugins/static.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var fs = require('fs');
var path = require('path');
var escapeRE = require('escape-regexp-component');
var assert = require('assert-plus');
var mime = require('mime');
var errors = require('restify-errors');
///--- Globals
var MethodNotAllowedError = errors.MethodNotAllowedError;
var NotAuthorizedError = errors.NotAuthorizedError;
var ResourceNotFoundError = errors.ResourceNotFoundError;
///--- Functions
/**
* Serves static files.
*
* @public
* @function serveStatic
* @param {Object} options - an options object
* @throws {MethodNotAllowedError} |
* @throws {NotAuthorizedError}
* @throws {ResourceNotFoundError}
* @returns {Function} Handler
* @example
*
* The serveStatic module is different than most of the other plugins, in that
* it is expected that you are going to map it to a route, as below:
*
* The above `route` and `directory` combination will serve a file located in
* `./documentation/v1/docs/current/index.html` when you attempt to hit
* `http://localhost:8080/docs/current/`. If you want the serveStatic module to
* serve files directly from the `/documentation/v1` directory
* (and not append the request path `/docs/current/`),
* you can set the `appendRequestPath` option to `false`, and the served file
* would be `./documentation/v1/index.html`, in the previous example.
*
* The plugin will enforce that all files under `directory` are served.
* The `directory` served is relative to the process working directory.
* You can also provide a `default` parameter such as index.html for any
* directory that lacks a direct file match.
* You can specify additional restrictions by passing in a `match` parameter,
* which is just a `RegExp` to check against the requested file name.
* Additionally, you may set the `charSet` parameter, which will append a
* character set to the content-type detected by the plugin.
* For example, `charSet: 'utf-8'` will result in HTML being served with a
* `Content-Type` of `text/html; charset=utf-8`.
* Lastly, you can pass in a `maxAge` numeric, which will set the
* `Cache-Control` header. Default is `3600` (1 hour).
*
* An additional option for serving a static file is to pass `file` in to the
* serveStatic method as an option. The following will serve index.html from
* the documentation/v1/ directory anytime a client requests `/home/`.
*
* server.get('/home/*', restify.plugins.serveStatic({
* directory: './documentation/v1',
* file: 'index.html'
* }));
* // or
* server.get('/home/([a-z]+[.]html)', restify.plugins.serveStatic({
* directory: './documentation/v1',
* file: 'index.html'
* }));
*/
function serveStatic(options) {
var opts = options || {};
if (typeof opts.appendRequestPath === 'undefined') {
opts.appendRequestPath = true;
}
assert.object(opts, 'options');
assert.string(opts.directory, 'options.directory');
assert.optionalNumber(opts.maxAge, 'options.maxAge');
assert.optionalObject(opts.match, 'options.match');
assert.optionalString(opts.charSet, 'options.charSet');
assert.optionalString(opts.file, 'options.file');
assert.bool(opts.appendRequestPath, 'options.appendRequestPath');
var p = path.normalize(opts.directory).replace(/\\/g, '/');
var re = new RegExp('^' + escapeRE(p) + '/?.*');
function serveFileFromStats(file, err, stats, isGzip, req, res, next) {
if (
typeof req.connectionState === 'function' &&
req.connectionState() === 'close'
) {
next(false);
return;
}
if (err) {
next(new ResourceNotFoundError(err, '%s', req.path()));
return;
} else if (!stats.isFile()) {
next(new ResourceNotFoundError('%s does not exist', req.path()));
return;
}
if (res.handledGzip && isGzip) {
res.handledGzip();
}
var fstream = fs.createReadStream(file + (isGzip ? '.gz' : ''));
var maxAge = opts.maxAge === undefined ? 3600 : opts.maxAge;
fstream.once('open', function onceOpen(fd) {
res.cache({ maxAge: maxAge });
res.set('Content-Length', stats.size);
res.set('Content-Type', mime.getType(file));
res.set('Last-Modified', stats.mtime);
if (opts.charSet) {
var type =
res.getHeader('Content-Type') + '; charset=' + opts.charSet;
res.setHeader('Content-Type', type);
}
if (opts.etag) {
res.set('ETag', opts.etag(stats, opts));
}
res.writeHead(200);
fstream.pipe(res);
fstream.once('close', function onceClose() {
next(false);
});
});
res.once('close', function onceClose() {
fstream.close();
});
}
function serveNormal(file, req, res, next) {
fs.stat(file, function fileStat(err, stats) {
if (!err && stats.isDirectory() && opts.default) {
// Serve an index.html page or similar
var filePath = path.join(file, opts.default);
fs.stat(filePath, function dirStat(dirErr, dirStats) {
serveFileFromStats(
filePath,
dirErr,
dirStats,
false,
req,
res,
next
);
});
} else {
serveFileFromStats(file, err, stats, false, req, res, next);
}
});
}
function serve(req, res, next) {
var file;
if (opts.file) {
//serves a direct file
file = path.join(opts.directory, decodeURIComponent(opts.file));
} else if (opts.appendRequestPath) {
file = path.join(opts.directory, decodeURIComponent(req.path()));
} else {
var dirBasename = path.basename(opts.directory);
var reqpathBasename = path.basename(req.path());
if (
path.extname(req.path()) === '' &&
dirBasename === reqpathBasename
) {
file = opts.directory;
} else {
file = path.join(
opts.directory,
decodeURIComponent(path.basename(req.path()))
);
}
}
if (req.method !== 'GET' && req.method !== 'HEAD') {
next(new MethodNotAllowedError('%s', req.method));
return;
}
if (!re.test(file.replace(/\\/g, '/'))) {
next(new NotAuthorizedError('%s', req.path()));
return;
}
if (opts.match && !opts.match.test(file)) {
next(new NotAuthorizedError('%s', req.path()));
return;
}
if (opts.gzip && req.acceptsEncoding('gzip')) {
fs.stat(file + '.gz', function stat(err, stats) {
if (!err) {
res.setHeader('Content-Encoding', 'gzip');
serveFileFromStats(file, err, stats, true, req, res, next);
} else {
serveNormal(file, req, res, next);
}
});
} else {
serveNormal(file, req, res, next);
}
}
return serve;
}
module.exports = serveStatic;
================================================
FILE: lib/plugins/staticFiles.js
================================================
'use strict';
var assert = require('assert-plus');
var errors = require('restify-errors');
var path = require('path');
var send = require('send');
var shallowCopy = require('./utils/shallowCopy');
///--- Globals
var MethodNotAllowedError = errors.MethodNotAllowedError;
var NotAuthorizedError = errors.NotAuthorizedError;
var ResourceNotFoundError = errors.ResourceNotFoundError;
/**
* Serves static files, with API similar to expressjs
*
* @public
* @function serveStaticFiles
* @param {String} directory - the directory to serve files from
* @param {Object} opts - an options object, which is optional
* @param {Number} [opts.maxAge=0] - specify max age in millisecs
* @param {Boolean} [opts.etag=true] - enable/disable etag, default = true
* @param {Function} [opts.setHeaders] - set custom headers for the Files
* (synchronously), The function is called as `fn(res, path, stat)`,
* where the arguments are:
* `res` the response object
* `path` the file path that is being sent
* `stat` the stat object of the file that is being sent
* @throws {MethodNotAllowedError}
* @throws {NotAuthorizedError}
* @throws {ResourceNotFoundError}
* @returns {Function} Handler
* @example
*
* The serveStaticFiles plugin allows you to map a GET route to a
* directory on the disk
*
* The GET `route` and `directory` combination will serve a file
* located in `./documentation/v1/index.html` when you attempt to hit
* `http://localhost:8080/public/index.html`
*
* The plugin uses [send](https://github.com/pillarjs/send) under the hood
* which is also used by `expressjs` to serve static files. Most of the options
* that work with `send` will work with this plugin.
*
* The default file the plugin looks for is `index.html`
*
* server.get('/public/*',
* restify.plugins.serveStaticFiles('./documentation/v1', {
* maxAge: 3600000, // this is in millisecs
* etag: false,
* setHeaders: function setCustomHeaders(response, requestedPath, stat) {
* response.setHeader('restify-plugin-x', 'awesome');
* }
* })
* );
*/
function serveStaticFiles(directory, opts) {
// make a copy of the options that will be passed to send
var optionsPlugin = shallowCopy(opts || {});
var optionsSend = shallowCopy(opts || {});
// lets assert some options
assert.object(optionsSend, 'options');
assert.object(optionsPlugin, 'options');
assert.string(directory, 'directory');
// `send` library relies on `root` to specify the root folder
// to look for files
optionsSend.root = path.resolve(directory);
// `setHeaders` is only understood by our plugin
if (optionsSend.setHeaders) {
delete optionsSend.setHeaders;
}
return function handleServeStaticFiles(req, res, next) {
// Check to make sure that this was either a GET or a HEAD request
if (req.method !== 'GET' && req.method !== 'HEAD') {
return next(new MethodNotAllowedError('%s', req.method));
}
// we expect the params to have `*`:
// This allows the router to accept any file path
var requestedFile = req.params['*'] || 'index.html';
// This is used only for sending back correct error message text
var requestedFullPath = req.url;
// Rely on `send` library to create a stream
var stream = send(req, requestedFile, optionsSend);
// Lets handle the various events being emitted by send module
// stream has ended, must call `next()`
stream.on('end', function handleEnd() {
return next();
});
// when `send` encounters any `error`, we have the opportunity
// to handle the errors here
stream.on('error', function handleError(err) {
var respondWithError = null;
// When file does not exist
if (err.statusCode === 404) {
respondWithError = new ResourceNotFoundError(requestedFullPath);
} else {
// or action is forbidden (like requesting a directory)
respondWithError = new NotAuthorizedError(requestedFullPath);
}
return next(respondWithError);
});
// If the request was for directory and that directory did not
// have index.html, this will be called
stream.on('directory', function handleDirectoryRequest() {
next(new NotAuthorizedError('%s', requestedFullPath));
return;
});
// stream is about to send headers, and custom headers must be
// set now
stream.on('headers', function handleCustomHeaders(
response,
requestedPath,
stat
) {
if (
optionsPlugin.setHeaders &&
typeof optionsPlugin.setHeaders === 'function'
) {
optionsPlugin.setHeaders(response, requestedPath, stat);
}
});
// pipe the stream into response
return stream.pipe(res);
};
}
module.exports = serveStaticFiles;
================================================
FILE: lib/plugins/throttle.js
================================================
// Copyright 2012 Mark Cavage All rights reserved.
'use strict';
var sprintf = require('util').format;
var assert = require('assert-plus');
var LRU = require('lru-cache');
var errors = require('restify-errors');
///--- Globals
var TooManyRequestsError = errors.TooManyRequestsError;
var MESSAGE = 'You have exceeded your request rate of %s r/s.';
///--- Helpers
/**
* @private
* @function xor
* @returns {undefined} no return value
*/
function xor() {
var x = false;
for (var i = 0; i < arguments.length; i++) {
if (arguments[i] && !x) {
x = true;
} else if (arguments[i] && x) {
return false;
}
}
return x;
}
///--- Internal Class (TokenBucket)
/**
* An implementation of the Token Bucket algorithm.
*
* Basically, in network throttling, there are two "mainstream"
* algorithms for throttling requests, Token Bucket and Leaky Bucket.
* For restify, I went with Token Bucket. For a good description of the
* algorithm, see: http://en.wikipedia.org/wiki/Token_bucket
*
* In the options object, you pass in the total tokens and the fill rate.
* Practically speaking, this means "allow `fill rate` requests/second,
* with bursts up to `total tokens`". Note that the bucket is initialized
* to full.
*
* Also, in googling, I came across a concise python implementation, so this
* is just a port of that. Thanks http://code.activestate.com/recipes/511490 !
*
* @private
* @class TokenBucket
* @param {Object} options - contains the parameters:
* - {Number} capacity the maximum burst.
* - {Number} fillRate the rate to refill tokens.
*/
function TokenBucket(options) {
assert.object(options, 'options');
assert.number(options.capacity, 'options.capacity');
assert.number(options.fillRate, 'options.fillRate');
this.tokens = this.capacity = options.capacity;
this.fillRate = options.fillRate;
this.time = Date.now();
}
/**
* Consume N tokens from the bucket.
*
* If there is not capacity, the tokens are not pulled from the bucket.
*
* @private
* @memberof TokenBucket
* @instance
* @function consume
* @param {Number} tokens - the number of tokens to pull out.
* @returns {Boolean} true if capacity, false otherwise.
*/
TokenBucket.prototype.consume = function consume(tokens) {
if (tokens <= this._fill()) {
this.tokens -= tokens;
return true;
}
return false;
};
/**
* Fills the bucket with more tokens.
*
* Rather than do some whacky setTimeout() deal, we just approximate refilling
* the bucket by tracking elapsed time from the last time we touched the bucket.
*
* Simply, we set the bucket size to min(totalTokens,
* current + (fillRate * elapsed time)).
*
* @private
* @memberof TokenBucket
* @instance
* @function _fill
* @returns {Number} the current number of tokens in the bucket.
*/
TokenBucket.prototype._fill = function _fill() {
var now = Date.now();
// reset account for clock drift (like DST)
if (now < this.time) {
this.time = now - 1000;
}
if (this.tokens < this.capacity) {
var delta = this.fillRate * ((now - this.time) / 1000);
this.tokens = Math.min(this.capacity, this.tokens + delta);
}
this.time = now;
return this.tokens;
};
///--- Internal Class (TokenTable)
/**
* Just a wrapper over LRU that supports put/get to store token -> bucket
* mappings.
*
* @private
* @class TokenTable
* @param {Object} options - an options object
* @param {Number} options.size - size of the LRU
*/
function TokenTable(options) {
assert.object(options, 'options');
this.table = new LRU({ max: options.size || 10000 });
}
/**
* Puts a value in the token table
*
* @private
* @memberof TokenTable
* @instance
* @function put
* @param {String} key - a name
* @param {TokenBucket} value - a TokenBucket
* @returns {undefined} no return value
*/
TokenTable.prototype.put = function put(key, value) {
this.table.set(key, value);
};
/**
* Puts a value in the token table
*
* @private
* @memberof TokenTable
* @instance
* @function get
* @param {String} key - a key
* @returns {TokenBucket} token bucket instance
*/
TokenTable.prototype.get = function get(key) {
return this.table.get(key);
};
///--- Exported API
/**
* Creates an API rate limiter that can be plugged into the standard
* restify request handling pipeline.
*
* `restify` ships with a fairly comprehensive implementation of
* [Token bucket](http://en.wikipedia.org/wiki/Token_bucket), with the ability
* to throttle on IP (or x-forwarded-for) and username (from `req.username`).
* You define "global" request rate and burst rate, and you can define
* overrides for specific keys.
* Note that you can always place this on per-URL routes to enable
* different request rates to different resources (if for example, one route,
* like `/my/slow/database` is much easier to overwhlem
* than `/my/fast/memcache`).
*
* If a client has consumed all of their available rate/burst, an HTTP response
* code of `429`
* [Too Many Requests]
* (http://tools.ietf.org/html/draft-nottingham-http-new-status-03#section-4)
* is returned.
*
* This throttle gives you three options on which to throttle:
* username, IP address and 'X-Forwarded-For'. IP/XFF is a /32 match,
* so keep that in mind if using it. Username takes the user specified
* on req.username (which gets automagically set for supported Authorization
* types; otherwise set it yourself with a filter that runs before this).
*
* In both cases, you can set a `burst` and a `rate` (in requests/seconds),
* as an integer/float. Those really translate to the `TokenBucket`
* algorithm, so read up on that (or see the comments above...).
*
* In either case, the top level options burst/rate set a blanket throttling
* rate, and then you can pass in an `overrides` object with rates for
* specific users/IPs. You should use overrides sparingly, as we make a new
* TokenBucket to track each.
*
* On the `options` object ip and username are treated as an XOR.
*
* @public
* @function throttle
* @throws {TooManyRequestsError}
* @param {Object} options - required options with:
* @param {Number} options.burst - burst
* @param {Number} options.rate - rate
* @param {Boolean} [options.ip] - ip
* @param {Boolean} [options.username] - username
* @param {Boolean} [options.xff] - xff
* @param {Boolean} [options.setHeaders=false] - Set response headers for rate,
* limit (burst) and remaining.
* @param {Object} [options.overrides] - overrides
* @param {Object} options.tokensTable - a storage engine this plugin will
* use to store throttling keys -> bucket mappings.
* If you don't specify this, the default is to
* use an in-memory O(1) LRU, with 10k distinct
* keys. Any implementation just needs to support
* put/get.
* @param {Number} [options.maxKeys=10000] - If using the default
* implementation, you can specify how large you
* want the table to be.
* @returns {Function} Handler
* @example
*
* An example options object with overrides:
*
* {
* burst: 10, // Max 10 concurrent requests (if tokens)
* rate: 0.5, // Steady state: 1 request / 2 seconds
* ip: true, // throttle per IP
* overrides: {
* '192.168.1.1': {
* burst: 0,
* rate: 0 // unlimited
* }
* }
*/
function throttle(options) {
assert.object(options, 'options');
assert.number(options.burst, 'options.burst');
assert.number(options.rate, 'options.rate');
assert.optionalBool(options.setHeaders, 'options.setHeaders');
if (!xor(options.ip, options.xff, options.username)) {
throw new Error('(ip ^ username ^ xff)');
}
var table =
options.tokensTable || new TokenTable({ size: options.maxKeys });
function rateLimit(req, res, next) {
var attr;
var burst = options.burst;
var rate = options.rate;
if (options.ip) {
attr = req.connection.remoteAddress;
} else if (options.xff) {
attr = req.headers['x-forwarded-for'];
} else if (options.username) {
attr = req.username;
} else {
req.log.warn({ config: options }, 'Invalid throttle configuration');
return next();
}
// Before bothering with overrides, see if this request
// even matches
if (!attr) {
return next();
}
// Check the overrides
if (
options.overrides &&
options.overrides[attr] &&
options.overrides[attr].burst !== undefined &&
options.overrides[attr].rate !== undefined
) {
burst = options.overrides[attr].burst;
rate = options.overrides[attr].rate;
}
if (!rate || !burst) {
return next();
}
var bucket = table.get(attr);
if (!bucket) {
bucket = new TokenBucket({
capacity: burst,
fillRate: rate
});
table.put(attr, bucket);
}
req.log.trace('Throttle(%s): num_tokens= %d', attr, bucket.tokens);
var tooManyRequests = !bucket.consume(1);
// set throttle headers after consume which changes the remaining tokens
if (options.setHeaders) {
res.header('X-RateLimit-Remaining', Math.floor(bucket.tokens));
res.header('X-RateLimit-Limit', burst);
res.header('X-RateLimit-Rate', rate);
}
if (tooManyRequests) {
req.log.info(
{
address: req.connection.remoteAddress || '?',
method: req.method,
url: req.url,
user: req.username || '?'
},
'Throttling'
);
var msg = sprintf(MESSAGE, rate);
return next(new TooManyRequestsError(msg));
}
return next();
}
return rateLimit;
}
module.exports = throttle;
================================================
FILE: lib/plugins/utils/hrTimeDurationInMs.js
================================================
'use strict';
var NS_PER_SEC = 1e9;
var MS_PER_NS = 1e6;
/**
* Get duration in milliseconds from two process.hrtime()
* @function hrTimeDurationInMs
* @param {Array} startTime - [seconds, nanoseconds]
* @param {Array} endTime - [seconds, nanoseconds]
* @returns {Number|null} durationInMs
*/
function hrTimeDurationInMs(startTime, endTime) {
if (!Array.isArray(startTime) || !Array.isArray(endTime)) {
return null;
}
var secondDiff = endTime[0] - startTime[0];
var nanoSecondDiff = endTime[1] - startTime[1];
var diffInNanoSecond = secondDiff * NS_PER_SEC + nanoSecondDiff;
return Math.round(diffInNanoSecond / MS_PER_NS);
}
module.exports = hrTimeDurationInMs;
================================================
FILE: lib/plugins/utils/httpDate.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
/**
* Takes an instance of a date object, formats it UTC
* e.g., Wed, 17 Jun 2015 01:30:26 GMT.
*
* @public
* @function httpDate
* @param {Object} now - a date object
* @returns {String} formatted dated object
*/
module.exports = function httpDate(now) {
return now.toUTCString();
};
================================================
FILE: lib/plugins/utils/regex.js
================================================
'use strict';
module.exports = {
jsonContentType: new RegExp('^application/[a-zA-Z.]+\\+json')
};
================================================
FILE: lib/plugins/utils/shallowCopy.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
/**
* Return a shallow copy of the given object
*
* @public
* @function shallowCopy
* @param {Object} obj - the object to copy
* @returns {Object} the new copy of the object
*/
function shallowCopy(obj) {
if (!obj) {
return obj;
}
var copy = {};
Object.keys(obj).forEach(function forEach(k) {
copy[k] = obj[k];
});
return copy;
}
///--- Exports
module.exports = shallowCopy;
================================================
FILE: lib/request.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var url = require('url');
var sprintf = require('util').format;
var assert = require('assert-plus');
var mime = require('mime');
var Negotiator = require('negotiator');
var uuid = require('uuid');
var dtrace = require('./dtrace');
///-- Helpers
/**
* Creates and sets negotiator on request if one doesn't already exist,
* then returns it.
*
* @private
* @function negotiator
* @param {Object} req - the request object
* @returns {Object} a negotiator
*/
function negotiator(req) {
var h = req.headers;
if (!req._negotiator) {
req._negotiator = new Negotiator({
headers: {
accept: h.accept || '*/*',
'accept-encoding': h['accept-encoding'] || 'identity'
}
});
}
return req._negotiator;
}
///--- API
/**
* Patch Request object and extends with extra functionalities
*
* @private
* @function patch
* @param {http.IncomingMessage|http2.Http2ServerRequest} Request -
* Server Request
* @returns {undefined} No return value
*/
function patch(Request) {
/**
* Wraps all of the node
* [http.IncomingMessage](https://nodejs.org/api/http.html)
* APIs, events and properties, plus the following.
* @class Request
* @extends http.IncomingMessage
*/
///--- Patches
/**
* Builds an absolute URI for the request.
*
* @private
* @memberof Request
* @instance
* @function absoluteUri
* @param {String} path - a url path
* @returns {String} uri
*/
Request.prototype.absoluteUri = function absoluteUri(path) {
assert.string(path, 'path');
var protocol = this.isSecure() ? 'https://' : 'http://';
var hostname = this.headers.host;
return url.resolve(protocol + hostname + this.path() + '/', path);
};
/**
* Check if the Accept header is present, and includes the given type.
* When the Accept header is not present true is returned.
* Otherwise the given type is matched by an exact match, and then subtypes.
*
* @public
* @memberof Request
* @instance
* @function accepts
* @param {String | String[]} types - an array of accept type headers
* @returns {Boolean} is accepteed
* @example
*
* You may pass the subtype such as html which is then converted internally
* to text/html using the mime lookup table:
*
* // Accept: text/html
* req.accepts('html');
* // => true
*
* // Accept: text/*; application/json
* req.accepts('html');
* req.accepts('text/html');
* req.accepts('text/plain');
* req.accepts('application/json');
* // => true
*
* req.accepts('image/png');
* req.accepts('png');
* // => false
*/
Request.prototype.accepts = function accepts(types) {
if (typeof types === 'string') {
types = [types];
}
types = types.map(function map(t) {
assert.string(t, 'type');
if (t.indexOf('/') === -1) {
t = mime.getType(t);
}
return t;
});
negotiator(this);
return this._negotiator.preferredMediaType(types);
};
/**
* Checks if the request accepts the encoding type(s) specified.
*
* @public
* @memberof Request
* @instance
* @function acceptsEncoding
* @param {String | String[]} types - an array of accept type headers
* @returns {Boolean} is accepted encoding
*/
Request.prototype.acceptsEncoding = function acceptsEncoding(types) {
if (typeof types === 'string') {
types = [types];
}
assert.arrayOfString(types, 'types');
negotiator(this);
return this._negotiator.preferredEncoding(types);
};
/**
* Returns the value of the content-length header.
*
* @private
* @memberof Request
* @instance
* @function getContentLength
* @returns {Number} content length
*/
Request.prototype.getContentLength = function getContentLength() {
if (this._clen !== undefined) {
return this._clen === false ? undefined : this._clen;
}
// We should not attempt to read and parse the body of an
// Upgrade request, so force Content-Length to zero:
if (this.isUpgradeRequest()) {
return 0;
}
var len = this.header('content-length');
if (!len) {
this._clen = false;
} else {
this._clen = parseInt(len, 10);
}
return this._clen === false ? undefined : this._clen;
};
/**
* Returns the value of the content-length header.
* @public
* @memberof Request
* @instance
* @function contentLength
* @returns {Number}
*/
Request.prototype.contentLength = Request.prototype.getContentLength;
/**
* Returns the value of the content-type header. If a content-type is not
* set, this will return a default value of `application/octet-stream`.
*
* @private
* @memberof Request
* @instance
* @function getContentType
* @returns {String} content type
*/
Request.prototype.getContentType = function getContentType() {
if (this._contentType !== undefined) {
return this._contentType;
}
var index;
var type = this.headers['content-type'];
if (!type) {
// RFC2616 section 7.2.1
this._contentType = 'application/octet-stream';
} else if ((index = type.indexOf(';')) === -1) {
this._contentType = type;
} else {
this._contentType = type.substring(0, index);
}
// #877 content-types need to be case insensitive.
this._contentType = this._contentType.toLowerCase();
return this._contentType;
};
/**
* Returns the value of the content-type header. If a content-type is not
* set, this will return a default value of `application/octet-stream`
* @public
* @memberof Request
* @instance
* @function getContentType
* @returns {String} content type
*/
Request.prototype.contentType = Request.prototype.getContentType;
/**
* Returns a Date object representing when the request was setup.
* Like `time()`, but returns a Date object.
*
* @public
* @memberof Request
* @instance
* @function date
* @returns {Date} date when request began being processed
*/
Request.prototype.date = function date() {
return this._date;
};
/**
* Retrieves the complete URI requested by the client.
*
* @private
* @memberof Request
* @instance
* @function getHref
* @returns {String} URI
*/
Request.prototype.getHref = function getHref() {
return this.getUrl().href;
};
/**
* Returns the full requested URL.
* @public
* @memberof Request
* @instance
* @function href
* @returns {String}
* @example
* // incoming request is http://localhost:3000/foo/bar?a=1
* server.get('/:x/bar', function(req, res, next) {
* console.warn(req.href());
* // => /foo/bar/?a=1
* });
*/
Request.prototype.href = Request.prototype.getHref;
/**
* Retrieves the request uuid. was created when the request was setup.
*
* @private
* @memberof Request
* @instance
* @function getId
* @returns {String} id
*/
Request.prototype.getId = function getId() {
if (this._id !== undefined) {
return this._id;
}
this._id = uuid.v4();
return this._id;
};
/**
* Returns the request id. If a `reqId` value is passed in,
* this will become the request’s new id. The request id is immutable,
* and can only be set once. Attempting to set the request id more than
* once will cause restify to throw.
*
* @public
* @memberof Request
* @instance
* @function id
* @param {String} reqId - request id
* @returns {String} id
*/
Request.prototype.id = function id(reqId) {
var self = this;
if (reqId) {
if (self._id) {
throw new Error(
'request id is immutable, cannot be set again!'
);
} else {
assert.string(reqId, 'reqId');
self._id = reqId;
return self._id;
}
}
return self.getId();
};
/**
* Retrieves the cleaned up url path.
* e.g., /foo?a=1 => /foo
*
* @private
* @memberof Request
* @instance
* @function getPath
* @returns {String} path
*/
Request.prototype.getPath = function getPath() {
return this.getUrl().pathname;
};
/**
* Returns the cleaned up requested URL.
* @public
* @memberof Request
* @instance
* @function getPath
* @returns {String}
* @example
* // incoming request is http://localhost:3000/foo/bar?a=1
* server.get('/:x/bar', function(req, res, next) {
* console.warn(req.path());
* // => /foo/bar
* });
*/
Request.prototype.path = Request.prototype.getPath;
/**
* Returns the raw query string. Returns empty string
* if no query string is found.
*
* @public
* @memberof Request
* @instance
* @function getQuery
* @returns {String} query
* @example
* // incoming request is /foo?a=1
* req.getQuery();
* // => 'a=1'
* @example
*
* If the queryParser plugin is used, the parsed query string is
* available under the req.query:
*
* // incoming request is /foo?a=1
* server.use(restify.plugins.queryParser());
* req.query;
* // => { a: 1 }
*/
Request.prototype.getQuery = function getQuery() {
// always return a string, because this is the raw query string.
// if the queryParser plugin is used, req.query will provide an empty
// object fallback.
return this.getUrl().query || '';
};
/**
* Returns the raw query string. Returns empty string
* if no query string is found
* @private
* @memberof Request
* @instance
* @function query
* @returns {String}
*/
Request.prototype.query = Request.prototype.getQuery;
/**
* The number of ms since epoch of when this request began being processed.
* Like date(), but returns a number.
*
* @public
* @memberof Request
* @instance
* @function time
* @returns {Number} time when request began being processed in epoch:
* ellapsed milliseconds since
* January 1, 1970, 00:00:00 UTC
*/
Request.prototype.time = function time() {
return this._date.getTime();
};
/**
* returns a parsed URL object.
*
* @private
* @memberof Request
* @instance
* @function getUrl
* @returns {Object} url
*/
Request.prototype.getUrl = function getUrl() {
if (this._cacheURL !== this.url) {
this._url = url.parse(this.url);
this._cacheURL = this.url;
}
return this._url;
};
/**
* Returns the accept-version header.
*
* @private
* @memberof Request
* @instance
* @function getVersion
* @returns {String} version
*/
Request.prototype.getVersion = function getVersion() {
if (this._version !== undefined) {
return this._version;
}
this._version =
this.headers['accept-version'] ||
this.headers['x-api-version'] ||
'*';
return this._version;
};
/**
* Returns the accept-version header.
* @public
* @memberof Request
* @instance
* @function version
* @returns {String}
*/
Request.prototype.version = Request.prototype.getVersion;
/**
* Returns the version of the route that matched.
*
* @private
* @memberof Request
* @instance
* @function matchedVersion
* @returns {String} version
*/
Request.prototype.matchedVersion = function matchedVersion() {
if (this._matchedVersion !== undefined) {
return this._matchedVersion;
} else {
return this.version();
}
};
/**
* Get the case-insensitive request header key,
* and optionally provide a default value (express-compliant).
* Returns any header off the request. also, 'correct' any
* correctly spelled 'referrer' header to the actual spelling used.
*
* @public
* @memberof Request
* @instance
* @function header
* @param {String} key - the key of the header
* @param {String} [defaultValue] - default value if header isn't
* found on the req
* @returns {String} header value
* @example
* req.header('Host');
* req.header('HOST');
* req.header('Accept', '*\/*');
*/
Request.prototype.header = function header(key, defaultValue) {
assert.string(key, 'key');
key = key.toLowerCase();
if (key === 'referer' || key === 'referrer') {
key = 'referer';
}
return this.headers[key] || defaultValue;
};
/**
* Returns any trailer header off the request. Also, 'correct' any
* correctly spelled 'referrer' header to the actual spelling used.
*
* @public
* @memberof Request
* @instance
* @function trailer
* @param {String} name - the name of the header
* @param {String} value - default value if header isn't found on the req
* @returns {String} trailer value
*/
Request.prototype.trailer = function trailer(name, value) {
assert.string(name, 'name');
name = name.toLowerCase();
if (name === 'referer' || name === 'referrer') {
name = 'referer';
}
return (this.trailers || {})[name] || value;
};
/**
* Check if the incoming request contains the `Content-Type` header field,
* and if it contains the given mime type.
*
* @public
* @memberof Request
* @instance
* @function is
* @param {String} type - a content-type header value
* @returns {Boolean} is content-type header
* @example
* // With Content-Type: text/html; charset=utf-8
* req.is('html');
* req.is('text/html');
* // => true
*
* // When Content-Type is application/json
* req.is('json');
* req.is('application/json');
* // => true
*
* req.is('html');
* // => false
*/
Request.prototype.is = function is(type) {
assert.string(type, 'type');
var contentType = this.getContentType();
var matches = true;
if (!contentType) {
return false;
}
if (type.indexOf('/') === -1) {
type = mime.getType(type);
}
if (type.indexOf('*') !== -1) {
type = type.split('/');
contentType = contentType.split('/');
matches &= type[0] === '*' || type[0] === contentType[0];
matches &= type[1] === '*' || type[1] === contentType[1];
} else {
matches = contentType === type;
}
return matches;
};
/**
* Check if the incoming request is chunked.
*
* @public
* @memberof Request
* @instance
* @function isChunked
* @returns {Boolean} is chunked
*/
Request.prototype.isChunked = function isChunked() {
return this.headers['transfer-encoding'] === 'chunked';
};
/**
* Check if the incoming request is kept alive.
*
* @public
* @memberof Request
* @instance
* @function isKeepAlive
* @returns {Boolean} is keep alive
*/
Request.prototype.isKeepAlive = function isKeepAlive() {
if (this._keepAlive !== undefined) {
return this._keepAlive;
}
if (this.headers.connection) {
this._keepAlive = /keep-alive/i.test(this.headers.connection);
} else {
this._keepAlive = this.httpVersion === '1.0' ? false : true;
}
return this._keepAlive;
};
/**
* Check if the incoming request is encrypted.
*
* @public
* @memberof Request
* @instance
* @function isSecure
* @returns {Boolean} is secure
*/
Request.prototype.isSecure = function isSecure() {
if (this._secure !== undefined) {
return this._secure;
}
this._secure = this.connection.encrypted ? true : false;
return this._secure;
};
/**
* Check if the incoming request has been upgraded.
*
* @public
* @memberof Request
* @instance
* @function isUpgradeRequest
* @returns {Boolean} is upgraded
*/
Request.prototype.isUpgradeRequest = function isUpgradeRequest() {
if (this._upgradeRequest !== undefined) {
return this._upgradeRequest;
} else {
return false;
}
};
/**
* Check if the incoming request is an upload verb.
*
* @public
* @memberof Request
* @instance
* @function isUpload
* @returns {Boolean} is upload
*/
Request.prototype.isUpload = function isUpload() {
var m = this.method;
return m === 'PATCH' || m === 'POST' || m === 'PUT';
};
/**
* toString serialization
*
* @public
* @memberof Request
* @instance
* @function toString
* @returns {String} serialized request
*/
Request.prototype.toString = function toString() {
var headers = '';
var self = this;
var str;
Object.keys(this.headers).forEach(function forEach(k) {
headers += sprintf('%s: %s\n', k, self.headers[k]);
});
str = sprintf(
'%s %s HTTP/%s\n%s',
this.method,
this.url,
this.httpVersion,
headers
);
return str;
};
/**
* Returns the user-agent header.
*
* @public
* @memberof Request
* @instance
* @function userAgent
* @returns {String} user agent
*/
Request.prototype.userAgent = function userAgent() {
return this.headers['user-agent'];
};
/**
* Start the timer for a request handler.
* By default, restify uses calls this automatically for all handlers
* registered in your handler chain.
* However, this can be called manually for nested functions inside the
* handler chain to record timing information.
*
* @public
* @memberof Request
* @instance
* @function startHandlerTimer
* @param {String} handlerName - The name of the handler.
* @returns {undefined} no return value
* @example
*
* You must explicitly invoke
* endHandlerTimer() after invoking this function. Otherwise timing
* information will be inaccurate.
*
* server.get('/', function fooHandler(req, res, next) {
* vasync.pipeline({
* funcs: [
* function nestedHandler1(req, res, next) {
* req.startHandlerTimer('nestedHandler1');
* // do something
* req.endHandlerTimer('nestedHandler1');
* return next();
* },
* function nestedHandler1(req, res, next) {
* req.startHandlerTimer('nestedHandler2');
* // do something
* req.endHandlerTimer('nestedHandler2');
* return next();
*
* }...
* ]...
* }, next);
* });
*/
Request.prototype.startHandlerTimer = function startHandlerTimer(
handlerName
) {
var self = this;
// For nested handlers, we prepend the top level handler func name
var name =
self._currentHandler === handlerName
? handlerName
: self._currentHandler + '-' + handlerName;
if (!self._timerMap) {
self._timerMap = {};
}
self._timerMap[name] = process.hrtime();
if (self.dtrace) {
dtrace._rstfy_probes['handler-start'].fire(function fire() {
return [
self.serverName,
self._currentRoute, // set in server._run
name,
self._dtraceId
];
});
}
};
/**
* End the timer for a request handler.
* You must invoke this function if you called `startRequestHandler` on a
* handler. Otherwise the time recorded will be incorrect.
*
* @public
* @memberof Request
* @instance
* @function endHandlerTimer
* @param {String} handlerName - The name of the handler.
* @returns {undefined} no return value
*/
Request.prototype.endHandlerTimer = function endHandlerTimer(handlerName) {
var self = this;
// For nested handlers, we prepend the top level handler func name
var name =
self._currentHandler === handlerName
? handlerName
: self._currentHandler + '-' + handlerName;
if (!self.timers) {
self.timers = [];
}
self._timerMap[name] = process.hrtime(self._timerMap[name]);
self.timers.push({
name: name,
time: self._timerMap[name]
});
if (self.dtrace) {
dtrace._rstfy_probes['handler-done'].fire(function fire() {
return [
self.serverName,
self._currentRoute, // set in server._run
name,
self._dtraceId
];
});
}
};
/**
* Returns the connection state of the request. Current possible values are:
* - `close` - when the request has been closed by the clien
*
* @public
* @memberof Request
* @instance
* @function connectionState
* @returns {String} connection state (`"close"`)
*/
Request.prototype.connectionState = function connectionState() {
var self = this;
return self._connectionState;
};
/**
* Returns the route object to which the current request was matched to.
*
* @public
* @memberof Request
* @instance
* @function getRoute
* @returns {Object} route
* @example
*
Route info object structure:
* {
* path: '/ping/:name',
* method: 'GET',
* versions: [],
* name: 'getpingname'
* }
*/
Request.prototype.getRoute = function getRoute() {
var self = this;
return self.route;
};
}
module.exports = patch;
================================================
FILE: lib/response.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var http = require('http');
var sprintf = require('util').format;
var url = require('url');
var assert = require('assert-plus');
var mime = require('mime');
var errors = require('restify-errors');
var httpDate = require('./http_date');
var utils = require('./utils');
///--- Globals
var InternalServerError = errors.InternalServerError;
/**
* @private
* Headers that cannot be multi-values.
* @see #779, multiple set-cookie values are allowed only as multiple headers.
* @see #986, multiple content-type values / headers disallowed.
*/
var HEADER_ARRAY_BLACKLIST = {
'content-type': true
};
///--- API
/**
* Patch Response object and extends with extra functionalities
*
* @private
* @function patch
* @param {http.ServerResponse|http2.Http2ServerResponse} Response -
* Server Response
* @returns {undefined} No return value
*/
function patch(Response) {
assert.func(Response, 'Response');
/**
* Wraps all of the node
* [http.ServerResponse](https://nodejs.org/docs/latest/api/http.html)
* APIs, events and properties, plus the following.
* @class Response
* @extends http.ServerResponse
*/
/**
* Sets the `cache-control` header.
*
* @public
* @memberof Response
* @instance
* @function cache
* @param {String} [type="public"] - value of the header
* (`"public"` or `"private"`)
* @param {Object} [options] - an options object
* @param {Number} options.maxAge - max-age in seconds
* @returns {String} the value set to the header
*/
Response.prototype.cache = function cache(type, options) {
if (typeof type !== 'string') {
options = type;
type = 'public';
}
if (options && options.maxAge !== undefined) {
assert.number(options.maxAge, 'options.maxAge');
type += ', max-age=' + options.maxAge;
}
return this.setHeader('Cache-Control', type);
};
/**
* Turns off all cache related headers.
*
* @public
* @memberof Response
* @instance
* @function noCache
* @returns {Response} self, the response object
*/
Response.prototype.noCache = function noCache() {
// HTTP 1.1
this.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
// HTTP 1.0
this.setHeader('Pragma', 'no-cache');
// Proxies
this.setHeader('Expires', '0');
return this;
};
/**
* Appends the provided character set to the response's `Content-Type`.
*
* @public
* @memberof Response
* @instance
* @function charSet
* @param {String} type - char-set value
* @returns {Response} self, the response object
* @example
* res.charSet('utf-8');
*/
Response.prototype.charSet = function charSet(type) {
assert.string(type, 'charset');
this._charSet = type;
return this;
};
/**
* Retrieves a header off the response.
*
* @private
* @memberof Response
* @instance
* @function get
* @param {Object} name - the header name
* @returns {String} header value
*/
Response.prototype.get = function get(name) {
assert.string(name, 'name');
return this.getHeader(name);
};
// If getHeaders is not provided by the Node platform, monkey patch our own.
// This is needed since versions of Node <7 did not come with a getHeaders.
// For more see GH-1408
if (typeof Response.prototype.getHeaders !== 'function') {
/**
* Retrieves all headers off the response.
*
* @private
* @memberof Response
* @instance
* @function getHeaders
* @returns {Object} headers
*/
Response.prototype.getHeaders = function getHeaders() {
return this._headers || {};
};
}
/**
* Sets headers on the response.
*
* @public
* @memberof Response
* @instance
* @function header
* @param {String} key - the name of the header
* @param {String} value - the value of the header
* @returns {Object} the retrieved value or the value that was set
* @example
*
* If only key is specified, return the value of the header.
* If both key and value are specified, set the response header.
*
* Note that certain headers like `content-type`
* do not support multiple values, so calling `header()`
* twice for those headers will
* overwrite the existing value.
*
*/
Response.prototype.header = function header(key, value) {
assert.string(key, 'name');
if (value === undefined) {
return this.getHeader(key);
}
if (value instanceof Date) {
value = httpDate(value);
} else if (arguments.length > 2) {
// Support res.header('foo', 'bar %s', 'baz');
var arg = Array.prototype.slice.call(arguments).slice(2);
value = sprintf(value, arg);
}
var current = this.getHeader(key);
// Check the header blacklist before changing a header to an array
var keyLc = key.toLowerCase();
if (current && !(keyLc in HEADER_ARRAY_BLACKLIST)) {
if (Array.isArray(current)) {
current.push(value);
value = current;
} else {
value = [current, value];
}
}
this.setHeader(key, value);
return value;
};
/**
* Syntatic sugar for:
* ```js
* res.contentType = 'json';
* res.send({hello: 'world'});
* ```
*
* @public
* @memberof Response
* @instance
* @function json
* @param {Number} [code] - http status code
* @param {Object} [body] - value to json.stringify
* @param {Object} [headers] - headers to set on the response
* @returns {Object} the response object
* @example
* res.header('content-type', 'json');
* res.send({hello: 'world'});
*/
Response.prototype.json = function json(code, body, headers) {
this.setHeader('Content-Type', 'application/json');
return this.send(code, body, headers);
};
/**
* Sets the link header.
*
* @public
* @memberof Response
* @instance
* @function link
* @param {String} key - the link key
* @param {String} value - the link value
* @returns {String} the header value set to res
*/
Response.prototype.link = function link(key, value) {
assert.string(key, 'key');
assert.string(value, 'value');
var _link = sprintf('<%s>; rel="%s"', key, value);
return this.header('Link', _link);
};
/**
* Sends the response object. pass through to internal `__send` that uses a
* formatter based on the `content-type` header.
*
* @public
* @memberof Response
* @instance
* @function send
* @param {Number} [code] - http status code
* @param {Object | Buffer | Error} [body] - the content to send
* @param {Object} [headers] - any add'l headers to set
* @returns {Object} the response object
* @example
*
* You can use send() to wrap up all the usual writeHead(), write(), end()
* calls on the HTTP API of node.
* You can pass send either a `code` and `body`, or just a body. body can be
* an `Object`, a `Buffer`, or an `Error`.
* When you call `send()`, restify figures out how to format the response
* based on the `content-type`.
*
* res.send({hello: 'world'});
* res.send(201, {hello: 'world'});
* res.send(new BadRequestError('meh'));
*/
Response.prototype.send = function send(code, body, headers) {
var self = this;
var sendArgs;
if (typeof code === 'number') {
sendArgs = {
code: code,
body: body,
headers: headers
};
} else {
sendArgs = {
body: code,
headers: body
};
}
sendArgs.format = true;
return self.__send(sendArgs);
};
/**
* Like `res.send()`, but skips formatting. This can be useful when the
* payload has already been preformatted.
* Sends the response object. pass through to internal `__send` that skips
* formatters entirely and sends the content as is.
*
* @public
* @memberof Response
* @instance
* @function sendRaw
* @param {Number} [code] - http status code
* @param {string | Buffer} [body] - the content to send
* @param {Object} [headers] - any add'l headers to set
* @returns {Object} the response object
*/
Response.prototype.sendRaw = function sendRaw(code, body, headers) {
var self = this;
var sendArgs;
if (typeof code === 'number') {
sendArgs = {
code: code,
body: body,
headers: headers
};
} else {
sendArgs = {
body: code,
headers: body
};
}
assert.ok(
typeof sendArgs.body === 'string' || Buffer.isBuffer(sendArgs.body),
'res.sendRaw() accepts only strings or buffers'
);
sendArgs.format = false;
return self.__send(sendArgs);
};
// eslint-disable-next-line jsdoc/check-param-names
/**
* Internal implementation of send. convenience method that handles:
* writeHead(), write(), end().
*
* Both body and headers are optional, but you MUST provide body if you are
* providing headers.
*
* @private
* @param {Object} opts - an option sobject
* @param {Object | Buffer | String | Error} opts.body - the content to send
* @param {Boolean} opts.format - When false, skip formatting
* @param {Number} [opts.code] - http status code
* @param {Object} [opts.headers] - any add'l headers to set
* @returns {Object} - returns the response object
*/
Response.prototype.__send = function __send(opts) {
var self = this;
var isHead = self.req.method === 'HEAD';
var log = self.log;
var code = opts.code;
var body = opts.body;
var headers = opts.headers || {};
self._sent = true;
// Now lets try to derive values for optional arguments that we were not
// provided, otherwise we choose sane defaults.
// If the body is an error object and we were not given a status code,
// try to derive it from the error object, otherwise default to 500
if (!code && body instanceof Error) {
code = body.statusCode || 500;
}
// Set sane defaults for optional arguments if they were not provided
// and we failed to derive their values
code = code || self.statusCode || 200;
// Populate our response object with the derived arguments
self.statusCode = code;
self._body = body;
Object.keys(headers).forEach(function forEach(k) {
self.setHeader(k, headers[k]);
});
// If log level is set to trace, output our constructed response object
if (log.trace()) {
var _props = {
code: self.statusCode,
headers: self._headers
};
if (body instanceof Error) {
_props.err = self._body;
} else {
_props.body = self._body;
}
log.trace(_props, 'response::send entered');
}
// 204 = No Content and 304 = Not Modified, we don't want to send the
// body in these cases. HEAD never provides a body.
if (isHead || code === 204 || code === 304) {
return flush(self);
}
if (opts.format === true) {
// if no body, then no need to format. if this was an error caught
// by a domain, don't send the domain error either.
if (body === undefined || (body instanceof Error && body.domain)) {
return flush(self);
}
// At this point we know we have a body that needs to be formatted,
// so lets derive the formatter based on the response object's
// properties
var formatter;
var type = self.contentType || self.getHeader('Content-Type');
// Set Content-Type to application/json when
// res.send is called with an Object instead of calling res.json
if (!type && typeof body === 'object' && !Buffer.isBuffer(body)) {
type = 'application/json';
}
// Derive type if not provided by the user
type = type || self.req.accepts(self.acceptable);
// Check to see if we could find a content type to use for the
// response.
if (!type) {
return formatterError(
self,
new errors.NotAcceptableError({
message:
'could not find suitable content-type to use ' +
'for the response'
})
);
}
type = type.split(';')[0];
if (!self.formatters[type] && type.indexOf('/') === -1) {
type = mime.getType(type);
}
// If finding a formatter matching the negotiated content-type is
// required, and we were unable to derive a valid type, default to
// treating it as arbitrary binary data per RFC 2046 Section 4.5.1
if (
this._strictFormatters &&
!self.formatters[type] &&
self.acceptable.indexOf(type) === -1
) {
type = 'application/octet-stream';
}
formatter = self.formatters[type] || self.formatters['*/*'];
// If after the above attempts we were still unable to derive a
// formatter, provide a meaningful error message
if (this._strictFormatters && !formatter) {
return formatterError(
self,
new errors.InternalServerError({
message:
'could not find formatter for response ' +
'content-type "' +
type +
'"'
})
);
}
var formatterType = type;
if (self._charSet) {
type = type + '; charset=' + self._charSet;
}
// Update Content-Type header to the one originally set or to the
// type inferred from the most relevant formatter found.
self.setHeader('Content-Type', type);
if (formatter) {
// Finally, invoke the formatter and flush the request with it's
// results
var formattedBody;
try {
formattedBody = formatter(self.req, self, body);
} catch (e) {
if (
e instanceof errors.RestError ||
e instanceof errors.HttpError
) {
var res = formatterError(
self,
e,
'error in formatter (' +
formatterType +
') formatting response body'
);
return res;
}
throw e;
}
return flush(self, formattedBody);
}
}
return flush(self, body);
};
/**
* Sets multiple header(s) on the response.
* Uses `header()` underneath the hood, enabling multi-value headers.
*
* @public
* @memberof Response
* @instance
* @function set
* @param {String|Object} name - name of the header or
* `Object` of headers
* @param {String} val - value of the header
* @returns {Object} self, the response object
* @example
* res.header('x-foo', 'a');
* res.set({
* 'x-foo', 'b',
* 'content-type': 'application/json'
* });
* // =>
* // {
* // 'x-foo': [ 'a', 'b' ],
* // 'content-type': 'application/json'
* // }
*/
Response.prototype.set = function set(name, val) {
var self = this;
if (arguments.length === 2) {
assert.string(
name,
'res.set(name, val) requires name to be a string'
);
this.header(name, val);
} else {
assert.object(
name,
'res.set(headers) requires headers to be an object'
);
Object.keys(name).forEach(function forEach(k) {
self.set(k, name[k]);
});
}
return this;
};
/**
* Sets the http status code on the response.
*
* @public
* @memberof Response
* @instance
* @function status
* @param {Number} code - http status code
* @returns {Number} the status code passed in
* @example
* res.status(201);
*/
Response.prototype.status = function status(code) {
assert.number(code, 'code');
this.statusCode = code;
return code;
};
/**
* toString() serialization.
*
* @private
* @memberof Response
* @instance
* @function toString
* @returns {String} stringified response
*/
Response.prototype.toString = function toString() {
var headers = this.getHeaders();
var headerString = '';
var str;
Object.keys(headers).forEach(function forEach(k) {
headerString += k + ': ' + headers[k] + '\n';
});
str = sprintf(
'HTTP/1.1 %s %s\n%s',
this.statusCode,
http.STATUS_CODES[this.statusCode],
headerString
);
return str;
};
if (!Response.prototype.hasOwnProperty('_writeHead')) {
Response.prototype._writeHead = Response.prototype.writeHead;
}
/**
* Pass through to native response.writeHead()
*
* @private
* @memberof Response
* @instance
* @function writeHead
* @fires header
* @returns {undefined} no return value
*/
Response.prototype.writeHead = function restifyWriteHead() {
this.emit('header');
if (this.statusCode === 204 || this.statusCode === 304) {
this.removeHeader('Content-Length');
this.removeHeader('Content-MD5');
this.removeHeader('Content-Type');
this.removeHeader('Content-Encoding');
}
this._writeHead.apply(this, arguments);
};
/**
* Redirect is sugar method for redirecting.
* @public
* @memberof Response
* @instance
* @param {Object} options url or an options object to configure a redirect
* @param {Boolean} [options.secure] whether to redirect to http or https
* @param {String} [options.hostname] redirect location's hostname
* @param {String} [options.pathname] redirect location's pathname
* @param {String} [options.port] redirect location's port number
* @param {String} [options.query] redirect location's query string
* parameters
* @param {Boolean} [options.overrideQuery] if true, `options.query`
* stomps over any existing query
* parameters on current URL.
* by default, will merge the two.
* @param {Boolean} [options.permanent] if true, sets 301. defaults to 302.
* @param {Function} next mandatory, to complete the response and trigger
* audit logger.
* @fires redirect
* @function redirect
* @returns {undefined}
* @example
* res.redirect({...}, next);
* @example
*
* A convenience method for 301/302 redirects. Using this method will tell
* restify to stop execution of your handler chain.
* You can also use an options object. `next` is required.
*
* res.redirect({
* hostname: 'www.foo.com',
* pathname: '/bar',
* port: 80, // defaults to 80
* secure: true, // sets https
* permanent: true,
* query: {
* a: 1
* }
* }, next); // => redirects to 301 https://www.foo.com/bar?a=1
*/
/**
* Redirect with code and url.
* @memberof Response
* @instance
* @param {Number} code http redirect status code
* @param {String} url redirect url
* @param {Function} next mandatory, to complete the response and trigger
* audit logger.
* @fires redirect
* @function redirect
* @returns {undefined}
* @example
* res.redirect(301, 'www.foo.com', next);
*/
/**
* Redirect with url.
* @public
* @memberof Response
* @instance
* @param {String} url redirect url
* @param {Function} next mandatory, to complete the response and trigger
* audit logger.
* @fires redirect
* @function redirect
* @returns {undefined}
* @example
* res.redirect('www.foo.com', next);
* res.redirect('/foo', next);
*/
Response.prototype.redirect = redirect;
/**
* @private
* @param {*} arg1 - arg1
* @param {*} arg2 - arg2
* @param {*} arg3 - arg3
* @fires redirect
* @function redirect
* @returns {undefined} no return value
*/
function redirect(arg1, arg2, arg3) {
var self = this;
var statusCode = 302;
var finalUri;
var redirectLocation;
var next;
// 1) this is signature 1, where an explicit status code is passed in.
// MUST guard against null here, passing null is likely indicative
// of an attempt to call res.redirect(null, next);
// as a way to do a reload of the current page.
if (arg1 && !isNaN(arg1)) {
statusCode = arg1;
finalUri = arg2;
next = arg3;
} else if (typeof arg1 === 'string') {
// 2) this is signaure number 2
// otherwise, it's a string, and use it directly
finalUri = arg1;
next = arg2;
} else if (typeof arg1 === 'object') {
// 3) signature number 3, using an options object.
// set next, then go to work.
next = arg2;
var req = self.req;
var opt = arg1 || {};
var currentFullPath = req.href();
var secure = opt.hasOwnProperty('secure')
? opt.secure
: req.isSecure();
// if hostname is passed in, use that as the base,
// otherwise fall back on current url.
var parsedUri = url.parse(opt.hostname || currentFullPath, true);
// create the object we'll use to format for the final uri.
// this object will eventually get passed to url.format().
// can't use parsedUri to seed it, as it confuses the url module
// with some existing parsed state. instead, we'll pick the things
// we want and use that as a starting point.
finalUri = {
port: parsedUri.port,
hostname: parsedUri.hostname,
query: parsedUri.query,
pathname: parsedUri.pathname
};
// start building url based on options.
// start with the host
if (opt.hostname) {
finalUri.hostname = opt.hostname;
}
// then set protocol IFF hostname is set - otherwise we end up with
// malformed URL.
if (finalUri.hostname) {
finalUri.protocol = secure === true ? 'https' : 'http';
}
// then set current path after the host
if (opt.pathname) {
finalUri.pathname = opt.pathname;
}
// then set port
if (opt.port) {
finalUri.port = opt.port;
}
// then add query params
if (opt.query) {
if (opt.overrideQuery === true) {
finalUri.query = opt.query;
} else {
finalUri.query = utils.mergeQs(opt.query, finalUri.query);
}
}
// change status code to 301 permanent if specified
if (opt.permanent) {
statusCode = 301;
}
}
// if we're missing a next we should probably throw. if user wanted
// to redirect but we were unable to do so, we should not continue
// down the handler stack.
assert.func(next, 'res.redirect() requires a next param');
// if we are missing a finalized uri
// by this point, pass an error to next.
if (!finalUri) {
return next(new InternalServerError('could not construct url'));
}
redirectLocation = url.format(finalUri);
self.emit('redirect', redirectLocation);
// now we're done constructing url, send the res
self.send(statusCode, null, {
Location: redirectLocation
});
// tell server to stop processing the handler stack.
return next(false);
}
}
/**
* Flush takes our constructed response object and sends it to the client
*
* @private
* @function flush
* @param {Response} res - response
* @param {String|Buffer} body - response body
* @returns {Response} response
*/
function flush(res, body) {
assert.ok(
body === null ||
body === undefined ||
typeof body === 'string' ||
Buffer.isBuffer(body),
'body must be a string or a Buffer instance'
);
res._data = body;
// Flush headers
res.writeHead(res.statusCode);
// Send body if it was provided
if (res._data) {
res.write(res._data);
}
// Finish request
res.end();
// If log level is set to trace, log the entire response object
if (res.log.trace()) {
res.log.trace({ res: res }, 'response sent');
}
// Return the response object back out to the caller of __send
return res;
}
/**
* formatterError is used to handle any case where we were unable to
* properly format the provided body
*
* @private
* @function formatterError
* @param {Response} res - response
* @param {Error} err - error
* @param {String} [msg] - custom log message
* @returns {Response} response
*/
function formatterError(res, err, msg) {
// If the user provided a non-success error code, we don't want to
// mess with it since their error is probably more important than
// our inability to format their message.
if (res.statusCode >= 200 && res.statusCode < 300) {
res.statusCode = err.statusCode;
}
if (typeof msg !== 'string') {
msg = 'error retrieving formatter';
}
res.log.warn(
{
req: res.req,
err: err
},
msg
);
return flush(res);
}
module.exports = patch;
================================================
FILE: lib/router.js
================================================
'use strict';
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var http = require('http');
var _ = require('lodash');
var assert = require('assert-plus');
var errors = require('restify-errors');
var uuid = require('uuid');
var Chain = require('./chain');
var RouterRegistryRadix = require('./routerRegistryRadix');
///--- Globals
var MethodNotAllowedError = errors.MethodNotAllowedError;
var ResourceNotFoundError = errors.ResourceNotFoundError;
///--- API
/**
* Router class handles mapping of http verbs and a regexp path,
* to an array of handler functions.
*
* @class
* @public
* @param {Object} options - an options object
* @param {Bunyan} options.log - Bunyan logger instance
* @param {Boolean} [options.onceNext=false] - Prevents calling next multiple
* times
* @param {Boolean} [options.strictNext=false] - Throws error when next() is
* called more than once, enabled onceNext option
* @param {Object} [options.registry] - route registry
* @param {Boolean} [options.ignoreTrailingSlash=false] - ignore trailing slash
* on paths
*/
function Router(options) {
assert.object(options, 'options');
assert.object(options.log, 'options.log');
assert.optionalBool(options.onceNext, 'options.onceNext');
assert.optionalBool(options.strictNext, 'options.strictNext');
assert.optionalBool(
options.ignoreTrailingSlash,
'options.ignoreTrailingSlash'
);
EventEmitter.call(this);
this.log = options.log;
this.onceNext = !!options.onceNext;
this.strictNext = !!options.strictNext;
this.name = 'RestifyRouter';
// Internals
this._anonymousHandlerCounter = 0;
this._registry = options.registry || new RouterRegistryRadix(options);
}
util.inherits(Router, EventEmitter);
/**
* Lookup for route
*
* @public
* @memberof Router
* @instance
* @function lookup
* @param {Request} req - request
* @param {Response} res - response
* @returns {Chain|undefined} handler or undefined
*/
Router.prototype.lookup = function lookup(req, res) {
var pathname = req.getUrl().pathname;
// Find route
var registryRoute = this._registry.lookup(req.method, pathname);
// Not found
if (!registryRoute) {
return undefined;
}
// Decorate req
req.params = Object.assign(req.params, registryRoute.params);
req.route = registryRoute.route;
// Call handler chain
return registryRoute.handler;
};
/**
* Lookup by name
*
* @public
* @memberof Router
* @instance
* @function lookupByName
* @param {String} name - route name
* @param {Request} req - request
* @param {Response} res - response
* @returns {Chain|undefined} handler or undefined
*/
Router.prototype.lookupByName = function lookupByName(name, req, res) {
var self = this;
var route = self._registry.get()[name];
if (!route) {
return undefined;
}
// Decorate req
req.route = route;
return route.chain.run.bind(route.chain);
};
/**
* Takes an object of route params and query params, and 'renders' a URL.
*
* @public
* @function render
* @param {String} routeName - the route name
* @param {Object} params - an object of route params
* @param {Object} query - an object of query params
* @returns {String} URL
* @example
* server.get({
* name: 'cities',
* path: '/countries/:name/states/:state/cities'
* }, (req, res, next) => ...));
* let cities = server.router.render('cities', {
* name: 'Australia',
* state: 'New South Wales'
* });
* // cities: '/countries/Australia/states/New%20South%20Wales/cities'
*/
Router.prototype.render = function render(routeName, params, query) {
var self = this;
function pathItem(match, key) {
if (params.hasOwnProperty(key) === false) {
throw new Error(
'Route <' + routeName + '> is missing parameter <' + key + '>'
);
}
return '/' + encodeURIComponent(params[key]);
}
function queryItem(key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(query[key]);
}
var route = self._registry.get()[routeName];
if (!route) {
return null;
}
var _path = route.spec.path;
var _url = _path.replace(/\/:([A-Za-z0-9_]+)(\([^\\]+?\))?/g, pathItem);
var items = Object.keys(query || {}).map(queryItem);
var queryString = items.length > 0 ? '?' + items.join('&') : '';
return _url + queryString;
};
/**
* Adds a route.
*
* @public
* @memberof Router
* @instance
* @function mount
* @param {Object} opts - an options object
* @param {String} opts.name - name
* @param {String} opts.method - method
* @param {String} opts.path - path can be any String
* @param {Function[]} handlers - handlers
* @returns {String} returns the route name if creation is successful.
* @fires ...String#mount
*/
Router.prototype.mount = function mount(opts, handlers) {
var self = this;
assert.object(opts, 'opts');
assert.string(opts.method, 'opts.method');
assert.arrayOfFunc(handlers, 'handlers');
assert.optionalString(opts.name, 'opts.name');
var chain = new Chain({
onceNext: self.onceNext,
strictNext: self.strictNext
});
// Route
var route = {
name: self._getRouteName(opts.name, opts.method, opts.path),
method: opts.method,
path: opts.path,
spec: opts,
chain: chain
};
handlers.forEach(function forEach(handler) {
// Assign name to anonymous functions
handler._name =
handler.name || 'handler-' + self._anonymousHandlerCounter++;
handler._identifier = `${handler._name} on ${opts.method} ${opts.path}`;
// Attach to middleware chain
chain.add(handler);
});
self._registry.add(route);
self.emit('mount', route.method, route.path);
return route;
};
/**
* Unmounts a route.
*
* @public
* @memberof Router
* @instance
* @function unmount
* @param {String} name - the route name
* @returns {Object|undefined} removed route if found
*/
Router.prototype.unmount = function unmount(name) {
assert.string(name, 'name');
var route = this._registry.remove(name);
return route;
};
/**
* toString() serialization.
*
* @public
* @memberof Router
* @instance
* @function toString
* @returns {String} stringified router
*/
Router.prototype.toString = function toString() {
return this._registry.toString();
};
/**
* Return information about the routes registered in the router.
*
* @public
* @memberof Router
* @instance
* @returns {object} The routes in the router.
*/
Router.prototype.getDebugInfo = function getDebugInfo() {
var routes = this._registry.get();
return _.mapValues(routes, function mapValues(route, routeName) {
return {
name: route.name,
method: route.method.toLowerCase(),
path: route.path,
handlers: route.chain.getHandlers()
};
});
};
/**
* Return mounted routes
*
* @public
* @memberof Router
* @instance
* @returns {object} The routes in the router.
*/
Router.prototype.getRoutes = function getRoutes() {
return this._registry.get();
};
/**
* Returns true if the router generated a 404 for an options request.
*
* TODO: this is relevant for CORS only. Should move this out eventually to a
* userland middleware? This also seems a little like overreach, as there is no
* option to opt out of this behavior today.
*
* @private
* @static
* @function _optionsError
* @param {Object} req - the request object
* @param {Object} res - the response object
* @returns {Boolean} is options error
*/
Router._optionsError = function _optionsError(req, res) {
var pathname = req.getUrl().pathname;
return req.method === 'OPTIONS' && pathname === '*';
};
/**
* Default route, when no route found
* Responds with a ResourceNotFoundError error.
*
* @private
* @memberof Router
* @instance
* @function defaultRoute
* @param {Request} req - request
* @param {Response} res - response
* @param {Function} next - next
* @returns {undefined} no return value
*/
Router.prototype.defaultRoute = function defaultRoute(req, res, next) {
var self = this;
var pathname = req.getUrl().pathname;
// Allow CORS
if (Router._optionsError(req, res, pathname)) {
res.send(200);
next(null, req, res);
return;
}
// Check for 405 instead of 404
var allowedMethods = http.METHODS.filter(function some(method) {
return method !== req.method && self._registry.lookup(method, pathname);
});
if (allowedMethods.length) {
res.methods = allowedMethods;
res.setHeader('Allow', allowedMethods.join(', '));
var methodErr = new MethodNotAllowedError(
'%s is not allowed',
req.method
);
next(methodErr, req, res);
return;
}
// clean up the url in case of potential xss
// https://github.com/restify/node-restify/issues/1018
var err = new ResourceNotFoundError('%s does not exist', pathname);
next(err, req, res);
};
/**
* Generate route name
*
* @private
* @memberof Router
* @instance
* @function _getRouteName
* @param {String|undefined} name - Name of the route
* @param {String} method - HTTP method
* @param {String} path - path
* @returns {String} name of the route
*/
Router.prototype._getRouteName = function _getRouteName(name, method, path) {
// Generate name
if (!name) {
name = method + '-' + path;
name = name.replace(/\W/g, '').toLowerCase();
}
// Avoid name conflict: GH-401
if (this._registry.get()[name]) {
name += uuid.v4().substr(0, 7);
}
return name;
};
module.exports = Router;
================================================
FILE: lib/routerRegistryRadix.js
================================================
'use strict';
var assert = require('assert-plus');
var FindMyWay = require('find-my-way');
var Chain = require('./chain');
/**
* Radix tree based router registry backed by `find-my-way`
*
* @class RouterRegistryRadix
* @public
* @param {Object} options - an options object
* @param {Object} [options.ignoreTrailingSlash] - ignore trailing slash on
* paths
*/
function RouterRegistryRadix(options) {
this._findMyWay = new FindMyWay(options);
this._routes = {};
}
/**
* Adds a route.
*
* @public
* @memberof Router
* @instance
* @function add
* @param {Object} route - an route object
* @param {String} route.name - name of the route
* @param {String} route.method - HTTP method
* @param {String} route.path - any String accepted by
* [find-my-way](https://github.com/delvedor/find-my-way)
* @param {Chain} route.chain - Chain instance
* @returns {Boolean} true
*/
RouterRegistryRadix.prototype.add = function add(route) {
assert.object(route, 'route');
assert.string(route.method, 'route.method');
assert.string(route.path, 'path');
assert.ok(route.chain instanceof Chain, 'route.chain');
this._findMyWay.on(
route.method,
route.path,
function onRoute(req, res, next) {
route.chain.run(req, res, next);
},
{
route: route
}
);
this._routes[route.name] = route;
return route;
};
/**
* Removes a route.
*
* @public
* @memberof RouterRegistryRadix
* @instance
* @function remove
* @param {String} name - the route name
* @returns {Object|undefined} removed route if found
*/
RouterRegistryRadix.prototype.remove = function remove(name) {
assert.string(name, 'name');
// check for route
var route = this._routes[name];
if (!route) {
return undefined;
}
// remove from registry
this._findMyWay.off(route.method, route.path);
delete this._routes[name];
return route;
};
/**
* Registry for route
*
* @public
* @memberof RouterRegistryRadix
* @instance
* @function Registry
* @param {String} method - method
* @param {String} pathname - pathname
* @returns {Chain|undefined} handler or undefined
*/
RouterRegistryRadix.prototype.lookup = function lookup(method, pathname) {
assert.string(method, 'method');
assert.string(pathname, 'pathname');
var fmwRoute = this._findMyWay.find(method, pathname);
// Not found
if (!fmwRoute) {
return undefined;
}
// Call handler chain
return {
route: fmwRoute.store.route,
params: fmwRoute.params,
handler: fmwRoute.handler
};
};
/**
* Get registry
*
* @public
* @memberof RouterRegistryRadix
* @instance
* @function toString
* @returns {String} stringified RouterRegistryRadix
*/
RouterRegistryRadix.prototype.get = function get() {
return this._routes;
};
/**
* toString() serialization.
*
* @public
* @memberof RouterRegistryRadix
* @instance
* @function toString
* @returns {String} stringified RouterRegistryRadix
*/
RouterRegistryRadix.prototype.toString = function toString() {
return this._findMyWay.prettyPrint();
};
module.exports = RouterRegistryRadix;
================================================
FILE: lib/server.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var EventEmitter = require('events').EventEmitter;
var http = require('http');
var https = require('https');
var util = require('util');
var _ = require('lodash');
var assert = require('assert-plus');
var errors = require('restify-errors');
var mime = require('mime');
var vasync = require('vasync');
var Chain = require('./chain');
var dtrace = require('./dtrace');
var formatters = require('./formatters');
var shallowCopy = require('./utils').shallowCopy;
var upgrade = require('./upgrade');
var deprecationWarnings = require('./deprecationWarnings');
var customErrorTypes = require('./errorTypes');
// Ensure these are loaded
var patchRequest = require('./request');
var patchResponse = require('./response');
var domain;
var http2;
var spdy;
patchResponse(http.ServerResponse);
patchRequest(http.IncomingMessage);
///--- Globals
var sprintf = util.format;
// Run in domain to catch async errors
// It has significant negative performance impact
// Warning: this feature depends on the deprecated domains module
function handleWithDomain(req, res, onError, next) {
// In Node v12.x requiring the domain module has a negative performance
// impact. As using domains in restify is optional and only required
// with the `handleUncaughtExceptions` options, we apply a singleton
// pattern to avoid any performance regression in the default scenario.
if (!domain) {
domain = require('domain');
}
var handlerDomain = domain.create();
handlerDomain.add(req);
handlerDomain.add(res);
handlerDomain.on('error', onError);
handlerDomain.run(function run() {
next(req, res);
});
}
///--- API
/**
* Creates a new Server.
*
* @public
* @class
* @param {Object} options - an options object
* @param {String} options.name - Name of the server.
* @param {Boolean} [options.dtrace=false] - enable DTrace support
* @param {Router} options.router - Router
* @param {Object} options.log - [pino](https://github.com/pinojs/pino)
* instance.
* @param {String} [options.url] - Once listen() is called, this will be filled
* in with where the server is running.
* @param {String|Buffer} [options.certificate] - If you want to create an HTTPS
* server, pass in a PEM-encoded certificate and key.
* @param {String|Buffer} [options.key] - If you want to create an HTTPS server,
* pass in a PEM-encoded certificate and key.
* @param {Object} [options.formatters] - Custom response formatters for
* `res.send()`.
* @param {Boolean|Function} [options.handleUncaughtExceptions=false] - When
* true restify will use a domain to catch and respond to any uncaught
* exceptions that occur in its handler stack. Comes with significant negative
* performance impact.
* Can also receive a function with signature (req, res, onError, next),
* allowing for domains alternatives. onError should be called by the custom
* error handler, and next must be called at the end of this function. THIS
* FUNCTION IS NOT INTENDED TO BE USED TO HANDLE ERRORS DIRECTLY, IT IS ONLY
* INTENDED AS AN ALTERNATIVE TO `domains`.
* onError signature: (err, req, res)
* next signature: (res, res)
* @param {Object} [options.spdy] - Any options accepted by
* [node-spdy](https://github.com/indutny/node-spdy).
* @param {Object} [options.http2] - Any options accepted by
* [http2.createSecureServer](https://nodejs.org/api/http2.html).
* @param {Boolean} [options.handleUpgrades=false] - Hook the `upgrade` event
* from the node HTTP server, pushing `Connection: Upgrade` requests through the
* regular request handling chain.
* @param {Boolean} [options.onceNext=false] - Prevents calling next multiple
* times
* @param {Boolean} [options.strictNext=false] - Throws error when next() is
* called more than once, enabled onceNext option
* @param {Object} [options.httpsServerOptions] - Any options accepted by
* [node-https Server](http://nodejs.org/api/https.html#https_https).
* If provided the following restify server options will be ignored:
* spdy, ca, certificate, key, passphrase, rejectUnauthorized, requestCert and
* ciphers; however these can all be specified on httpsServerOptions.
* @param {Boolean} [options.noWriteContinue=false] - prevents
* `res.writeContinue()` in `server.on('checkContinue')` when proxing
* @param {Boolean} [options.ignoreTrailingSlash=false] - ignore trailing slash
* on paths
* @param {Boolean} [options.strictFormatters=true] - enables strict formatters
* behavior: a formatter matching the response's content-type is required. If
* not found, the response's content-type is automatically set to
* 'application/octet-stream'. If a formatter for that content-type is not
* found, sending the response errors.
* @example
* var restify = require('restify');
* var server = restify.createServer();
*
* server.listen(8080, function () {
* console.log('ready on %s', server.url);
* });
*/
function Server(options) {
assert.object(options, 'options');
assert.object(options.log, 'options.log');
assert.object(options.router, 'options.router');
assert.string(options.name, 'options.name');
assert.optionalBool(options.dtrace, 'options.dtrace');
assert.optionalBool(options.socketio, 'options.socketio');
assert.optionalBool(options.onceNext, 'options.onceNext');
assert.optionalBool(options.strictNext, 'options.strictNext');
assert.optionalBool(options.strictFormatters, 'options.strictFormatters');
var self = this;
EventEmitter.call(this);
this.onceNext = !!options.onceNext;
this.strictNext = !!options.strictNext;
this.firstChain = [];
this.preChain = new Chain({
onceNext: this.onceNext,
strictNext: this.strictNext
});
this.useChain = new Chain({
onceNext: this.onceNext,
strictNext: this.strictNext
});
this.log = options.log;
this.name = options.name;
this.handleUncaughtExceptions = false;
const { handleUncaughtExceptions } = options;
if (options.handleUncaughtExceptions) {
const handleType = typeof handleUncaughtExceptions;
if (handleType === 'boolean') {
this.handleUncaughtExceptions = handleWithDomain;
} else if (handleType === 'function') {
this.handleUncaughtExceptions = handleUncaughtExceptions;
} else {
// prettier-ignore
// eslint-disable-next-line max-len
assert.fail(`typeof opts.handleUncaughtException (${handleType}) should be boolean OR function`);
}
}
this.router = options.router;
this.secure = false;
this.socketio = options.socketio || false;
this.dtrace = options.dtrace || false;
this._inflightRequests = 0;
this.strictFormatters = true;
if (options.strictFormatters !== undefined) {
this.strictFormatters = options.strictFormatters;
}
var fmt = mergeFormatters(options.formatters);
this.acceptable = fmt.acceptable;
this.formatters = fmt.formatters;
this.proxyEvents = [
'close',
'connection',
'error',
'listening',
'secureConnection'
];
if (options.spdy) {
spdy = require('spdy');
this.spdy = true;
this.server = spdy.createServer(options.spdy);
} else if (options.http2) {
// http2 module is not available < v8.4.0 (only with flag <= 8.8.0)
// load http2 module here to avoid experimental warning in other cases
if (!http2) {
try {
http2 = require('http2');
patchResponse(http2.Http2ServerResponse);
patchRequest(http2.Http2ServerRequest);
// eslint-disable-next-line no-empty
} catch (err) {}
}
assert(
http2,
'http2 module is not available, ' +
'upgrade your Node.js version to >= 8.8.0'
);
this.http2 = true;
this.server = http2.createSecureServer(options.http2);
} else if ((options.cert || options.certificate) && options.key) {
this.ca = options.ca;
this.certificate = options.certificate || options.cert;
this.key = options.key;
this.passphrase = options.passphrase || null;
this.secure = true;
this.server = https.createServer({
ca: self.ca,
cert: self.certificate,
key: self.key,
passphrase: self.passphrase,
rejectUnauthorized: options.rejectUnauthorized,
requestCert: options.requestCert,
ciphers: options.ciphers,
secureOptions: options.secureOptions
});
} else if (options.httpsServerOptions) {
this.server = https.createServer(options.httpsServerOptions);
} else {
this.server = http.createServer();
}
this.router.on('mount', this.emit.bind(this, 'mount'));
if (!options.handleUpgrades) {
this.proxyEvents.push('upgrade');
}
this.proxyEvents.forEach(function forEach(e) {
self.server.on(e, self.emit.bind(self, e));
});
// Now the things we can't blindly proxy
proxyEventWhenListenerAdded('clientError', this, this.server);
this.server.on('checkContinue', function onCheckContinue(req, res) {
if (self.listeners('checkContinue').length > 0) {
self.emit('checkContinue', req, res);
return;
}
if (!options.noWriteContinue) {
res.writeContinue();
}
self._onRequest(req, res);
});
if (options.handleUpgrades) {
this.server.on('upgrade', function onUpgrade(req, socket, head) {
req._upgradeRequest = true;
var res = upgrade.createResponse(req, socket, head);
self._onRequest(req, res);
});
}
this.server.on('request', this._onRequest.bind(this));
this.__defineGetter__('maxHeadersCount', function getMaxHeadersCount() {
return self.server.maxHeadersCount;
});
this.__defineSetter__('maxHeadersCount', function setMaxHeadersCount(c) {
self.server.maxHeadersCount = c;
return c;
});
this.__defineGetter__('url', function getUrl() {
if (self.socketPath) {
return 'http://' + self.socketPath;
}
var addr = self.address();
var str = '';
if (self.spdy) {
str += 'spdy://';
} else if (self.secure) {
str += 'https://';
} else {
str += 'http://';
}
if (addr) {
str +=
addr.family === 'IPv6' || addr.family === 6
? '[' + addr.address + ']'
: addr.address;
str += ':';
str += addr.port;
} else {
str += '169.254.0.1:0000';
}
return str;
});
// print deprecation messages based on server configuration
deprecationWarnings(self);
}
util.inherits(Server, EventEmitter);
module.exports = Server;
/**
* Only add a listener on the wrappedEmitter when a listener for the event is
* added to the wrapperEmitter. This is useful when just adding a listener to
* the wrappedEmittter overrides/disables a default behavior.
*
* @param {string} eventName - The name of the event to proxy
* @param {EventEmitter} wrapperEmitter - The emitter that proxies events from the wrappedEmitter
* @param {EventEmitter} wrappedEmitter - The proxied emitter
* @returns {undefined} NA
*/
function proxyEventWhenListenerAdded(
eventName,
wrapperEmitter,
wrappedEmitter
) {
var isEventProxied = false;
wrapperEmitter.on('newListener', function onNewListener(handledEventName) {
if (handledEventName === eventName && !isEventProxied) {
isEventProxied = true;
wrappedEmitter.on(
eventName,
wrapperEmitter.emit.bind(wrapperEmitter, eventName)
);
}
});
}
///--- Server lifecycle methods
// eslint-disable-next-line jsdoc/check-param-names
/**
* Gets the server up and listening.
* Wraps node's
* [listen()](
* http://nodejs.org/docs/latest/api/net.html#net_server_listen_path_callback).
*
* @public
* @memberof Server
* @instance
* @function listen
* @throws {TypeError}
* @param {Number} port - Port
* @param {Number} [host] - Host
* @param {Function} [callback] - optionally get notified when listening.
* @returns {undefined} no return value
* @example
*
You can call like:
* server.listen(80)
* server.listen(80, '127.0.0.1')
* server.listen('/tmp/server.sock')
*/
Server.prototype.listen = function listen() {
var args = Array.prototype.slice.call(arguments);
return this.server.listen.apply(this.server, args);
};
/**
* Shuts down this server, and invokes callback (optionally) when done.
* Wraps node's
* [close()](http://nodejs.org/docs/latest/api/net.html#net_event_close).
*
* @public
* @memberof Server
* @instance
* @function close
* @param {Function} [callback] - callback to invoke when done
* @returns {undefined} no return value
*/
Server.prototype.close = function close(callback) {
if (callback) {
assert.func(callback, 'callback');
}
this.server.once('close', function onClose() {
return callback ? callback() : false;
});
return this.server.close();
};
///--- Routing methods
/**
* Server method opts
* @typedef {String|Regexp |Object} Server~methodOpts
* @type {Object}
* @property {String} name a name for the route
* @property {String} path can be any String accepted by
* [find-my-way](https://github.com/delvedor/find-my-way)
* @example
* // a static route
* server.get('/foo', function(req, res, next) {});
* // a parameterized route
* server.get('/foo/:bar', function(req, res, next) {});
* // a regular expression
* server.get('/example/:file(^\\d+).png', function(req, res, next) {});
* // an options object
* server.get({
* path: '/foo',
* }, function(req, res, next) {});
*/
/**
* Mounts a chain on the given path against this HTTP verb
*
* @public
* @memberof Server
* @instance
* @function get
* @param {Server~methodOpts} opts - if string, the URL to handle.
* if options, the URL to handle, at minimum.
* @returns {Route} the newly created route.
* @example
* server.get('/', function (req, res, next) {
* res.send({ hello: 'world' });
* next();
* });
* @example
*
using with async/await
* server.get('/', function (req, res) {
* await somethingAsync();
* res.send({ hello: 'world' });
* next();
* }
*/
Server.prototype.get = serverMethodFactory('GET');
/**
* Mounts a chain on the given path against this HTTP verb
*
* @public
* @memberof Server
* @instance
* @function head
* @param {Server~methodOpts} opts - if string, the URL to handle.
* if options, the URL to handle, at minimum.
* @returns {Route} the newly created route.
*/
Server.prototype.head = serverMethodFactory('HEAD');
/**
* Mounts a chain on the given path against this HTTP verb
*
* @public
* @memberof Server
* @instance
* @function post
* @param {Server~methodOpts} post - if string, the URL to handle.
* if options, the URL to handle, at minimum.
* @returns {Route} the newly created route.
*/
Server.prototype.post = serverMethodFactory('POST');
/**
* Mounts a chain on the given path against this HTTP verb
*
* @public
* @memberof Server
* @instance
* @function put
* @param {Server~methodOpts} put - if string, the URL to handle.
* if options, the URL to handle, at minimum.
* @returns {Route} the newly created route.
*/
Server.prototype.put = serverMethodFactory('PUT');
/**
* Mounts a chain on the given path against this HTTP verb
*
* @public
* @memberof Server
* @instance
* @function patch
* @param {Server~methodOpts} patch - if string, the URL to handle.
* if options, the URL to handle, at minimum.
* @returns {Route} the newly created route.
*/
Server.prototype.patch = serverMethodFactory('PATCH');
/**
* Mounts a chain on the given path against this HTTP verb
*
* @public
* @memberof Server
* @instance
* @function del
* @param {Server~methodOpts} opts - if string, the URL to handle.
* if options, the URL to handle, at minimum.
* @returns {Route} the newly created route.
*/
Server.prototype.del = serverMethodFactory('DELETE');
/**
* Mounts a chain on the given path against this HTTP verb
*
* @public
* @memberof Server
* @instance
* @function opts
* @param {Server~methodOpts} opts - if string, the URL to handle.
* if options, the URL to handle, at minimum.
* @returns {Route} the newly created route.
*/
Server.prototype.opts = serverMethodFactory('OPTIONS');
///--- Request lifecycle and middleware methods
// eslint-disable-next-line jsdoc/check-param-names
/**
* Gives you hooks to run _before_ any routes are located. This gives you
* a chance to intercept the request and change headers, etc., that routing
* depends on. Note that req.params will _not_ be set yet.
*
* @public
* @memberof Server
* @instance
* @function pre
* @param {...Function|Array} handler - Allows you to add handlers that
* run for all routes. *before* routing occurs.
* This gives you a hook to change request headers and the like if you need to.
* Note that `req.params` will be undefined, as that's filled in *after*
* routing.
* Takes a function, or an array of functions.
* variable number of nested arrays of handler functions
* @returns {Object} returns self
* @example
* server.pre(function(req, res, next) {
* req.headers.accept = 'application/json';
* return next();
* });
* @example
*
For example, `pre()` can be used to deduplicate slashes in
* URLs
* server.pre(restify.pre.dedupeSlashes());
* @see {@link http://restify.com/docs/plugins-api/#serverpre-plugins|Restify pre() plugins}
*/
Server.prototype.pre = function pre() {
var self = this;
var handlers = Array.prototype.slice.call(arguments);
argumentsToChain(handlers).forEach(function forEach(handler) {
handler._name = handler.name || 'pre-' + self.preChain.count();
self.preChain.add(handler);
});
return this;
};
// eslint-disable-next-line jsdoc/check-param-names
/**
* Gives you hooks that run before restify touches a request. These hooks
* allow you to do processing early in the request/response life cycle without
* the overhead of the restify framework. You can not yield the event loop in
* this handler.
*
* The function handler accepts two parameters: req, res. If you want restify
* to ignore this request, return false from your handler. Return true or
* undefined to let restify continue handling the request.
*
* When false is returned, restify immediately stops handling the request. This
* means that no further middleware will be executed for any chain and routing
* will not occure. All request/response handling for an incoming request must
* be done inside the first handler if you intend to return false. This
* includes things like closing the response and returning a status code.
*
* The only work restify does for a first handler is to increment the number of
* inflightRequests prior to calling the chain, and decrement that value if the
* handler returns false. Returning anything other than true, false, undefined,
* or null will cause an exception to be thrown.
*
* Since server.first is designed to bypass the restify framework, there are
* naturally trade-offs you make when using this API:
* * Standard restify lifecycle events such as 'after' are not triggered for
* any request that you return false from a handler for
* * Invoking any of the restify req/res APIs from within a first handler is
* unspecified behavior, as the restify framework hasn't built up state for
* the request yet.
* * There are no request timers available at the time that the first chain
* runs.
* * And more! Beware doing anything with restify in these handlers. They are
* designed to give you similar access to the req/res as you would have if
* you were directly using node.js' http module, they are outside of the
* restify framework!
*
* @public
* @memberof Server
* @instance
* @function first
* @param {...Function} handler - Allows you to add handlers that
* run for all requests, *before* restify touches the request.
* This gives you a hook to change request headers and the like if you need to.
* Note that `req.params` will be undefined, as that's filled in *after*
* routing.
* Takes one or more functions.
* @returns {Object} returns self
* @example
* server.first(function(req, res) {
* if(server.inflightRequests() > 100) {
* res.statusCode = 503;
* res.end();
* return false
* }
* return true;
* })
*/
Server.prototype.first = function first() {
var args = Array.prototype.slice.call(arguments);
for (var i = 0; i < args.length; i++) {
assert.func(args[i]);
this.firstChain.push(args[i]);
}
return this;
};
// eslint-disable-next-line jsdoc/check-param-names
/**
* Allows you to add in handlers that run for all routes. Note that handlers
* added
* via `use()` will run only after the router has found a matching route. If no
* match is found, these handlers will never run. Takes a function, or an array
* of functions.
*
* You can pass in any combination of functions or array of functions.
*
* @public
* @memberof Server
* @instance
* @function use
* @param {...Function|Array} handler - A variable number of handler functions
* * and/or a
* variable number of nested arrays of handler functions
* @returns {Object} returns self
* @example
* server.use(function(req, res, next) {
* // do something...
* return next();
* });
* @example
*
For example, `use()` can be used to attach a request logger
*
* server.pre(restify.plugins.requestLogger());
* @see {@link http://restify.com/docs/plugins-api/#serveruse-plugins|Restify use() plugins}
*/
Server.prototype.use = function use() {
var self = this;
var handlers = Array.prototype.slice.call(arguments);
argumentsToChain(handlers).forEach(function forEach(handler) {
handler._name = handler.name || 'use-' + self.useChain.count();
self.useChain.add(handler);
});
return this;
};
/**
* Minimal port of the functionality offered by Express.js Route Param
* Pre-conditions
*
* This basically piggy-backs on the `server.use` method. It attaches a
* new middleware function that only fires if the specified parameter exists
* in req.params
*
* @example
* server.param("user", function (req, res, next) {
* // load the user's information here, always making sure to call next()
* fetchUserInformation(req, function callback(user) {
* req.user = user;
* next();
* });
* });
* @example
*
using with async/await
* server.param("user", async function(req, res) {
* req.user = await fetchUserInformation(req);
* somethingSync();
* }
*
* @see {@link http://expressjs.com/guide.html#route-param%20pre-conditions| Express route param pre-conditions}
* @public
* @memberof Server
* @instance
* @function param
* @param {String} name - The name of the URL param to respond to
* @param {Function} fn - The middleware function to execute
* @returns {Object} returns self
*/
Server.prototype.param = function param(name, fn) {
this.use(function _param(req, res, next) {
if (req.params && req.params.hasOwnProperty(name)) {
fn.call(this, req, res, next, req.params[name], name);
} else {
next();
}
});
return this;
};
/**
* Removes a route from the server.
* You pass in the route 'blob' you got from a mount call.
*
* @public
* @memberof Server
* @instance
* @function rm
* @throws {TypeError} on bad input.
* @param {String} routeName - the route name.
* @returns {Boolean} true if route was removed, false if not.
*/
Server.prototype.rm = function rm(routeName) {
var route = this.router.unmount(routeName);
return !!route;
};
///--- Info and debug methods
/**
* Returns the server address.
* Wraps node's
* [address()](http://nodejs.org/docs/latest/api/net.html#net_server_address).
*
* @public
* @memberof Server
* @instance
* @function address
* @returns {Object} Address of server
* @example
* server.address()
* @example
*
Output:
* { address: '::', family: 'IPv6', port: 8080 }
*/
Server.prototype.address = function address() {
return this.server.address();
};
/**
* Returns the number of inflight requests currently being handled by the server
*
* @public
* @memberof Server
* @instance
* @function inflightRequests
* @returns {number} number of inflight requests
*/
Server.prototype.inflightRequests = function inflightRequests() {
var self = this;
return self._inflightRequests;
};
/**
* Return debug information about the server.
*
* @public
* @memberof Server
* @instance
* @function debugInfo
* @returns {Object} debug info
* @example
* server.getDebugInfo()
* @example
*
Output:
* {
* routes: [
* {
* name: 'get',
* method: 'get',
* input: '/',
* compiledRegex: /^[\/]*$/,
* compiledUrlParams: null,
* handlers: [Array]
* }
* ],
* server: {
* formatters: {
* 'application/javascript': [Function: formatJSONP],
* 'application/json': [Function: formatJSON],
* 'text/plain': [Function: formatText],
* 'application/octet-stream': [Function: formatBinary]
* },
* address: '::',
* port: 8080,
* inflightRequests: 0,
* pre: [],
* use: [ 'parseQueryString', '_jsonp' ],
* after: []
* }
* }
*/
Server.prototype.getDebugInfo = function getDebugInfo() {
var self = this;
// map an array of function to an array of function names
var funcNameMapper = function funcNameMapper(handler) {
if (handler.name === '') {
return 'anonymous';
} else {
return handler.name;
}
};
if (!self._debugInfo) {
var addressInfo = self.server.address();
// output the actual routes registered with restify
var routeInfo = self.router.getDebugInfo();
var preHandlers = self.preChain.getHandlers().map(funcNameMapper);
var useHandlers = self.useChain.getHandlers().map(funcNameMapper);
// get each route's handler chain
var routes = _.map(routeInfo, function mapValues(route) {
route.handlers = Array.prototype.concat.call(
// TODO: should it contain use handlers?
useHandlers,
route.handlers.map(funcNameMapper)
);
return route;
});
self._debugInfo = {
routes: routes,
server: {
formatters: self.formatters,
// if server is not yet listening, addressInfo may be null
address: addressInfo && addressInfo.address,
port: addressInfo && addressInfo.port,
inflightRequests: self.inflightRequests(),
pre: preHandlers,
use: useHandlers,
after: self.listeners('after').map(funcNameMapper)
}
};
}
return self._debugInfo;
};
/**
* toString() the server for easy reading/output.
*
* @public
* @memberof Server
* @instance
* @function toString
* @returns {String} stringified server
* @example
* server.toString()
* @example
*
Output:
* Accepts: application/json, text/plain, application/octet-stream,
* application/javascript
* Name: restify
* Pre: []
* Router: RestifyRouter:
* DELETE: []
* GET: [get]
* HEAD: []
* OPTIONS: []
* PATCH: []
* POST: []
* PUT: []
*
* Routes:
* get: [parseQueryString, _jsonp, function]
* Secure: false
* Url: http://[::]:8080
* Version:
*/
Server.prototype.toString = function toString() {
var LINE_FMT = '\t%s: %s\n';
var SUB_LINE_FMT = '\t\t%s: %s\n';
var str = '';
function handlersToString(arr) {
var s =
'[' +
arr
.map(function map(b) {
return b.name || 'function';
})
.join(', ') +
']';
return s;
}
str += sprintf(LINE_FMT, 'Accepts', this.acceptable.join(', '));
str += sprintf(LINE_FMT, 'Name', this.name);
str += sprintf(
LINE_FMT,
'Pre',
handlersToString(this.preChain.getHandlers())
);
str += sprintf(LINE_FMT, 'Router', '');
this.router
.toString()
.split('\n')
.forEach(function forEach(line) {
str += sprintf('\t\t%s\n', line);
});
str += sprintf(LINE_FMT, 'Routes', '');
_.forEach(this.router.getRoutes(), function forEach(route, routeName) {
var handlers = handlersToString(route.chain.getHandlers());
str += sprintf(SUB_LINE_FMT, routeName, handlers);
});
str += sprintf(LINE_FMT, 'Secure', this.secure);
str += sprintf(LINE_FMT, 'Url', this.url);
return str;
};
///--- Private methods
// Lifecycle:
//
// 1. _onRequest (handle new request, setup request and triggers pre)
// 2. _runPre
// 3. _afterPre (runs after pre handlers are finisehd, triggers route)
// 4. _runRoute (route lookup)
// 5. _runUse (runs use handlers, if route exists)
// 6. Runs route handlers
// 7. _afterRoute (runs after route handlers are finised,
// triggers use)
// 8. _finishReqResCycle (on response "finish" and "error" events)
//
// Events:
// e.1 after (triggered when response is flushed)
//
// Errors:
// e.1 _onHandlerError (runs when next was called with an Error)
// e.2 _routeErrorResponse
// e.1 _onHandlerError (when, next('string') called, trigger route by name)
// e.2 _afterRoute
/**
* Setup request and calls _onRequest to run middlewares and call router
*
* @private
* @memberof Server
* @instance
* @function _onRequest
* @param {Object} req - the request object
* @param {Object} res - the response object
* @returns {undefined} no return value
* @fires Request,Response#request
*/
Server.prototype._onRequest = function _onRequest(req, res) {
var self = this;
// Increment the number of inflight requests prior to calling the firstChain
// handlers. This accomplishes two things. First, it gives earliest an
// accurate count of how many inflight requests there would be including
// this new request. Second, it intentionally winds up the inflight request
// accounting with the implementation of firstChain. Note how we increment
// here, but decrement down inside the for loop below. It's easy to end up
// with race conditions betwen inflight request accounting and inflight
// request load shedding, causing load shedding to reject/allow too many
// requests. The current implementation of firstChain is designed to
// remove those race conditions. By winding these implementations up with
// one another, it makes it clear that moving around the inflight request
// accounting will change the behavior of earliest.
self._inflightRequests++;
// Give the first chain the earliest possible opportunity to process
// this request before we do any work on it.
var firstChain = self.firstChain;
for (var i = 0; i < firstChain.length; i++) {
var handle = firstChain[i](req, res);
// Limit the range of values we will accept as return results of
// first handlers. This helps us maintain forward compatibility by
// ensuring users don't rely on undocumented/unspecified behavior.
assert.ok(
handle === true ||
handle === false ||
handle === undefined ||
handle === null,
'Return value of first[' +
i +
'] must be: ' +
'boolean, undefined, or null'
);
// If the first handler returns false, stop handling the request
// immediately.
if (handle === false) {
self._inflightRequests--;
return;
}
}
this.emit('request', req, res);
// Skip Socket.io endpoints
if (this.socketio && /^\/socket\.io.*/.test(req.url)) {
self._inflightRequests--;
return;
}
// Decorate req and res objects
self._setupRequest(req, res);
if (self.handleUncaughtExceptions) {
self.handleUncaughtExceptions(
req,
res,
err => this._onHandlerError(err, req, res, true),
self._runPre.bind(self)
);
} else {
self._runPre(req, res);
}
};
/**
* Run pre handlers
*
* @private
* @memberof Server
* @instance
* @function _runPre
* @param {Object} req - the request object
* @param {Object} res - the response object
* @returns {undefined} no return value
* @fires Request,Response#request
*/
Server.prototype._runPre = function _runPre(req, res) {
var self = this;
// emit 'pre' event before we run the pre handlers
self.emit('pre', req, res);
// Run "pre"
req._currentHandler = 'pre';
req._timePreStart = process.hrtime();
self.preChain.run(req, res, function preChainDone(err) {
// Execution time of a handler with error can be significantly lower
req._timePreEnd = process.hrtime();
self._afterPre(err, req, res);
});
};
/**
* After pre handlers finished
*
* @private
* @memberof Server
* @instance
* @function _afterPre
* @param {Error|false|undefined} err - pre handler error
* @param {Request} req - request
* @param {Response} res - response
* @returns {undefined} no return value
*/
Server.prototype._afterPre = function _afterPre(err, req, res) {
var self = this;
// Handle error
if (err) {
self._onHandlerError(err, req, res);
self._finishReqResCycle(req, res, err);
return;
}
// Stop
if (err === false) {
self._onHandlerStop(req, res);
return;
}
self._runRoute(req, res);
};
/**
* Find route and run handlers
*
* @private
* @memberof Server
* @instance
* @function _runRoute
* @param {Object} req - the request object
* @param {Object} res - the response object
* @returns {undefined} no return value
* @fires Request,Response#request
*/
Server.prototype._runRoute = function _runRoute(req, res) {
var self = this;
var routeHandler = self.router.lookup(req, res);
if (!routeHandler) {
self.router.defaultRoute(req, res, function afterRouter(err) {
self._afterRoute(err, req, res);
});
return;
}
// Emit routed
self.emit('routed', req, res, req.route);
self._runUse(req, res, function afterUse() {
// DTrace
if (self.dtrace) {
dtrace._rstfy_probes['route-start'].fire(function fire() {
return [
self.name,
req.route.name,
req._dtraceId,
req.method,
req.href(),
req.headers
];
});
}
req._timeRouteStart = process.hrtime();
routeHandler(req, res, function afterRouter(err) {
// Execution time of a handler with error can be significantly lower
req._timeRouteEnd = process.hrtime();
// DTrace
if (self.dtrace) {
dtrace._rstfy_probes['route-done'].fire(function fire() {
return [
self.name,
req.route.name,
req._dtraceId,
res.statusCode || 200,
res.headers
];
});
}
self._afterRoute(err, req, res);
});
});
};
/**
* After use handlers finished
*
* @private
* @memberof Server
* @instance
* @function _afterRoute
* @param {Error|false|undefined} err - use handler error
* @param {Request} req - request
* @param {Response} res - response
* @returns {undefined} no return value
*/
Server.prototype._afterRoute = function _afterRoute(err, req, res) {
var self = this;
res._handlersFinished = true;
// Handle error
if (err) {
self._onHandlerError(err, req, res);
self._finishReqResCycle(req, res, err);
return;
}
// Trigger finish
self._finishReqResCycle(req, res, err);
};
/**
* Run use handlers
*
* @private
* @memberof Server
* @instance
* @function _runUse
* @param {Object} req - the request object
* @param {Object} res - the response object
* @param {Function} next - next
* @returns {undefined} no return value
* @fires Request,Response#request
*/
Server.prototype._runUse = function _runUse(req, res, next) {
var self = this;
// Run "use"
req._currentHandler = 'use';
req._timeUseStart = process.hrtime();
self.useChain.run(req, res, function useChainDone(err) {
// Execution time of a handler with error can be significantly lower
req._timeUseEnd = process.hrtime();
self._afterUse(err, req, res, next);
});
};
/**
* After use handlers finished
*
* @private
* @memberof Server
* @instance
* @function _afterUse
* @param {Error|false|undefined} err - use handler error
* @param {Request} req - request
* @param {Response} res - response
* @param {Function} next - next
* @returns {undefined} no return value
*/
Server.prototype._afterUse = function _afterUse(err, req, res, next) {
var self = this;
// Handle error
if (err) {
self._onHandlerError(err, req, res);
self._finishReqResCycle(req, res, err);
return;
}
// Stop
if (err === false) {
self._onHandlerStop(req, res);
return;
}
next();
};
/**
* Runs after next(false) is called
*
* @private
* @memberof Server
* @instance
* @function _onHandlerStop
* @param {Request} req - request
* @param {Response} res - response
* @returns {undefined} no return value
*/
Server.prototype._onHandlerStop = function _onHandlerStop(req, res) {
res._handlersFinished = true;
this._finishReqResCycle(req, res);
};
/**
* After route handlers finished
* NOTE: only called when last handler calls next([err])
*
* @private
* @memberof Server
* @instance
* @function _onHandlerError
* @param {Error|String|undefined} err - router handler error or route name
* @param {Request} req - request
* @param {Response} res - response
* @param {boolean} isUncaught - whether the error is uncaught
* @returns {undefined} no return value
*/
Server.prototype._onHandlerError = function _onHandlerError(
err,
req,
res,
isUncaught
) {
var self = this;
// Handlers don't continue when error happen
res._handlersFinished = true;
// Preserve handler err for finish event
res.err = res.err || err;
// Error happened in router handlers
self._routeErrorResponse(req, res, err, isUncaught);
};
/**
* Set up the request before routing and execution of handler chain functions.
*
* @private
* @memberof Server
* @instance
* @function _setupRequest
* @param {Object} req - the request object
* @param {Object} res - the response object
* @returns {undefined} no return value
*/
Server.prototype._setupRequest = function _setupRequest(req, res) {
var self = this;
// Extend request
req._dtraceId = dtrace.nextId();
// if log is set on .first, don't override it
if (req.log === undefined) {
req.log = self.log;
}
if (res.log === undefined) {
res.log = req.log;
}
req._date = new Date();
req._timeStart = process.hrtime();
req.serverName = self.name;
req.params = {};
req.timers = [];
req.dtrace = self.dtrace;
// Extend response
res.acceptable = self.acceptable;
res.formatters = self.formatters;
res.req = req;
res.serverName = self.name;
res._handlersFinished = false;
res._flushed = false;
res._strictFormatters = this.strictFormatters;
// set header only if name isn't empty string
if (self.name !== '') {
res.setHeader('Server', self.name);
}
// Request lifecycle events
// attach a listener for 'aborted' events, this will let us set
// a flag so that we can stop processing the request if the client aborts
// the connection (or we lose the connection).
// we consider a closed request as flushed from metrics point of view
function onReqAborted() {
// Request was aborted, override the status code
var err = new customErrorTypes.RequestCloseError();
err.statusCode = 444;
// For backward compatibility we only set connection state to "close"
// for RequestCloseError, also aborted is always immediatly followed
// by a "close" event.
// We don't set _connectionState to "close" in the happy path
req._connectionState = 'close';
// Set status code and err for audit as req is already closed connection
res.statusCode = err.statusCode;
res.err = err;
}
// Response lifecycle events
function onResFinish() {
var processHrTime = process.hrtime();
res._flushed = true;
req._timeFlushed = processHrTime;
// Response may get flushed before handler callback is triggered
req._timeFlushed = processHrTime;
req._timePreEnd = req._timePreEnd || processHrTime;
req._timeUseEnd = req._timeUseEnd || processHrTime;
req._timeRouteEnd = req._timeRouteEnd || processHrTime;
// In Node < 10 "close" event dont fire always
// https://github.com/nodejs/node/pull/20611
self._finishReqResCycle(req, res);
}
// We are handling when connection is being closed prematurely outside of
// restify. It's not because the req is aborted.
function onResClose() {
res._flushed = true;
// Finish may already set the req._timeFlushed
req._timeFlushed = req._timeFlushed || process.hrtime();
self._finishReqResCycle(req, res, res.err);
}
// Request events
req.once('aborted', onReqAborted);
// Response events
res.once('finish', onResFinish);
res.once('close', onResClose);
// attach a listener for the response's 'redirect' event
res.on('redirect', function onRedirect(redirectLocation) {
self.emit('redirect', redirectLocation);
});
};
/**
* Maintaining the end of the request-response cycle:
* - emitting after event
* - updating inflight requests metrics
* Check if the response is finished, and if not, wait for it before firing the
* response object.
*
* @private
* @memberof Server
* @instance
* @function _finishReqResCycle
* @param {Object} req - the request object
* @param {Object} res - the response object
* @param {Object} [err] - a possible error as a result of failed route matching
* or failed execution of the handler array.
* @returns {undefined} no return value
*/
Server.prototype._finishReqResCycle = function _finishReqResCycle(
req,
res,
err
) {
var self = this;
var route = req.route; // can be undefined when 404 or error
// if the returned err value was a string, then we're handling the
// next('foo') case where we redirect to another middleware stack. don't
// do anything here because we're not done yet.
if (res._finished || _.isString(err)) {
return;
}
if (res._flushed && res._handlersFinished) {
// decrement number of requests
self._inflightRequests--;
res._finished = true;
req._timeFinished = process.hrtime();
// after event has signature of function(req, res, route, err) {...}
var finalErr = err || res.err;
req.emit('restifyDone', route, finalErr);
self.emit('after', req, res, route, finalErr);
} else if (
res._handlersFinished === true &&
res.headersSent === false &&
!res.err
) {
// if we reached the end of the handler chain and headers haven't been
// sent AND there isn't an existing res.err (e.g., req abort/close),
// it's possible it's a user error and a response was never written.
// send a 500.
res.send(
new errors.InternalServerError(
'reached the end of the handler chain without ' +
'writing a response!'
)
);
return;
} else {
// Store error for when the response is flushed and we actually emit the
// 'after' event. The "err" object passed to this method takes
// precedence, but in case it's not set, "res.err" may have been already
// set by another code path and we want to preserve it. The caveat thus
// is that the 'after' event will be emitted with the latest error that
// was set before the response is fully flushed. While not ideal, this
// is on purpose and accepted as a reasonable trade-off for now.
res.err = err || res.err;
}
};
/**
* Helper function to, when on router error, emit error events and then
* flush the err.
*
* @private
* @memberof Server
* @instance
* @function _routeErrorResponse
* @param {Request} req - the request object
* @param {Response} res - the response object
* @param {Error} err - error
* @param {boolean} isUncaught - whether the error is uncaught
* @returns {undefined} no return value
*/
Server.prototype._routeErrorResponse = function _routeErrorResponse(
req,
res,
err,
isUncaught
) {
var self = this;
if (
isUncaught &&
self.handleUncaughtExceptions &&
self.listenerCount('uncaughtException') > 1
) {
self.emit(
'uncaughtException',
req,
res,
req.route,
err,
function uncaughtExceptionCompleted() {
// We provide a callback to listeners of the 'uncaughtException'
// event and we call _finishReqResCycle when that callback is
// called so that, in case the actual request/response lifecycle
// was completed _before_ the error was thrown or emitted, and
// thus _before_ route handlers were marked as "finished", we
// can still mark the req/res lifecycle as complete.
// This edge case can occur when e.g. a client aborts a request
// and the route handler that handles that request throws an
// uncaught exception _after_ the request was aborted and the
// response was closed.
self._finishReqResCycle(req, res, err);
}
);
return;
}
self._emitErrorEvents(req, res, null, err, function emitError() {
// Prevent double handling
if (res._sent) {
return;
}
// only automatically send errors that are known (e.g., restify-errors)
if (err instanceof Error && _.isNumber(err.statusCode)) {
res.send(err);
return;
}
// if the thrown exception is not really an Error object, e.g.,
// "throw 'foo';"
// try to do best effort here to pass on that value by casting it to a
// string. This should work even for falsy values like 0, false, null,
// or undefined.
res.send(new errors.InternalError(String(err)));
});
};
/**
* Emit error events when errors are encountered either while attempting to
* route the request (via router) or while executing the handler chain.
*
* @private
* @memberof Server
* @instance
* @function _emitErrorEvents
* @param {Object} req - the request object
* @param {Object} res - the response object
* @param {Object} route - the current route, if applicable
* @param {Object} err - an error object
* @param {Object} cb - callback function
* @returns {undefined} no return value
* @fires Error#restifyError
*/
Server.prototype._emitErrorEvents = function _emitErrorEvents(
req,
res,
route,
err,
cb
) {
var self = this;
// Error can be of any type: undefined, Error, Number, etc. so we need
// to protect ourselves from trying to resolve names from non Error objects
var errName = err && err.name;
var normalizedErrName = errName && errEvtNameFromError(err);
req.log.trace(
{
err: err,
errName: normalizedErrName
},
'entering emitErrorEvents',
errName
);
var errEvtNames = [];
// if we have listeners for the specific error, fire those first.
// if there's no error name, we should not emit an event
if (normalizedErrName && self.listeners(normalizedErrName).length > 0) {
errEvtNames.push(normalizedErrName);
}
// or if we have a generic error listener. always fire generic error event
// listener afterwards.
if (self.listeners('restifyError').length > 0) {
errEvtNames.push('restifyError');
}
// kick off the async listeners
return vasync.forEachPipeline(
{
inputs: errEvtNames,
func: function emitError(errEvtName, vasyncCb) {
self.emit(errEvtName, req, res, err, function emitErrDone() {
// the error listener may return arbitrary objects, throw
// them away and continue on. don't want vasync to take
// that error and stop, we want to emit every single event.
return vasyncCb();
});
}
},
// eslint-disable-next-line handle-callback-err
function onResult(__, results) {
// vasync will never return error here since we throw them away.
return cb();
}
);
};
///--- Helpers
/**
* Verify and flatten a nested array of request handlers.
*
* @private
* @function argumentsToChain
* @throws {TypeError}
* @param {Function[]} handlers - pass through of funcs from server.[method]
* @returns {Array} request handlers
*/
function argumentsToChain(handlers) {
assert.array(handlers, 'handlers');
var chain = [];
// A recursive function for unwinding a nested array of handlers into a
// single chain.
function process(array) {
for (var i = 0; i < array.length; i++) {
if (Array.isArray(array[i])) {
// Recursively call on nested arrays
process(array[i]);
continue;
}
// If an element of the array isn't an array, ensure it is a
// handler function and then push it onto the chain of handlers
assert.func(array[i], 'handler');
chain.push(array[i]);
}
return chain;
}
// Return the chain, note that if `handlers` is an empty array, this will
// return an empty array.
return process(handlers);
}
/**
* merge optional formatters with the default formatters to create a single
* formatters object. the passed in optional formatters object looks like:
* formatters: {
* 'application/foo': function formatFoo(req, res, body) {...}
* }
* @private
* @function mergeFormatters
* @param {Object} fmt user specified formatters object
* @returns {Object}
*/
function mergeFormatters(fmt) {
var arr = [];
var obj = {};
function addFormatter(src, k) {
assert.func(src[k], 'formatter');
var q = 1.0; // RFC 2616 sec14 - The default value is q=1
var t = k;
if (k.indexOf(';') !== -1) {
var tmp = k.split(/\s*;\s*/);
t = tmp[0];
if (tmp[1].indexOf('q=') !== -1) {
q = parseFloat(tmp[1].split('=')[1]);
}
}
if (k.indexOf('/') === -1) {
k = mime.getType(k);
}
obj[t] = src[k];
arr.push({
q: q,
t: t
});
}
Object.keys(formatters).forEach(addFormatter.bind(this, formatters));
Object.keys(fmt || {}).forEach(addFormatter.bind(this, fmt || {}));
arr = arr
.sort(function sort(a, b) {
return b.q - a.q;
})
.map(function map(a) {
return a.t;
});
return {
formatters: obj,
acceptable: arr
};
}
/**
* Map an Error's .name property into the actual event name that is emitted
* by the restify server object.
*
* @function
* @private errEvtNameFromError
* @param {Object} err - an error object
* @returns {String} an event name to emit
*/
function errEvtNameFromError(err) {
if (err.name === 'ResourceNotFoundError') {
// remap the name for router errors
return 'NotFound';
} else if (err.name === 'InvalidVersionError') {
// remap the name for router errors
return 'VersionNotAllowed';
} else if (err.name) {
return err.name.replace(/Error$/, '');
}
// If the err is not an Error, then just return an empty string
return '';
}
/**
* Mounts a chain on the given path against this HTTP verb
*
* @private
* @function serverMethodFactory
* @param {String} method - name of the HTTP method
* @returns {Function} factory
*/
function serverMethodFactory(method) {
return function serverMethod(opts) {
if (opts instanceof RegExp || typeof opts === 'string') {
opts = {
path: opts
};
} else if (typeof opts === 'object') {
opts = shallowCopy(opts);
} else {
throw new TypeError('path (string) required');
}
if (arguments.length < 2) {
throw new TypeError('handler (function) required');
}
opts.method = method;
opts.path = opts.path || opts.url;
// We accept both a variable number of handler functions, a
// variable number of nested arrays of handler functions, or a mix
// of both
var handlers = Array.prototype.slice.call(arguments, 1);
var chain = argumentsToChain(handlers);
var route = this.router.mount(opts, chain);
return route.name;
};
}
================================================
FILE: lib/upgrade.js
================================================
// Copyright (c) 2013, Joyent, Inc. All rights reserved.
'use strict';
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var assert = require('assert-plus');
/**
* An custom error for capturing an invalid upgrade state.
*
* @public
* @class
* @param {String} msg - an error message
*/
function InvalidUpgradeStateError(msg) {
if (Error.captureStackTrace) {
Error.captureStackTrace(this, InvalidUpgradeStateError);
}
this.message = msg;
this.name = 'InvalidUpgradeStateError';
}
util.inherits(InvalidUpgradeStateError, Error);
//
// The Node HTTP Server will, if we handle the 'upgrade' event, swallow any
// Request with the 'Connection: upgrade' header set. While doing this it
// detaches from the 'data' events on the Socket and passes the socket to
// us, so that we may take over handling for the connection.
//
// Unfortunately, the API does not presently provide a http.ServerResponse
// for us to use in the event that we do not wish to upgrade the connection.
// This factory method provides a skeletal implementation of a
// restify-compatible response that is sufficient to allow the existing
// request handling path to work, while allowing us to perform _at most_ one
// of either:
//
// - Return a basic HTTP Response with a provided Status Code and
// close the socket.
// - Upgrade the connection and stop further processing.
//
// To determine if an upgrade is requested, a route handler would check for
// the 'claimUpgrade' method on the Response. The object this method
// returns will have the 'socket' and 'head' Buffer emitted with the
// 'upgrade' event by the http.Server. If the upgrade is not possible, such
// as when the HTTP head (or a full request) has already been sent by some
// other handler, this method will throw.
//
/**
* Create a new upgraded response.
*
* @public
* @function createServerUpgradeResponse
* @param {Object} req - the request object
* @param {Object} socket - the network socket
* @param {Object} head - a buffer, the first packet of the upgraded stream
* @returns {Object} an upgraded reponses
*/
function createServerUpgradeResponse(req, socket, head) {
return new ServerUpgradeResponse(socket, head);
}
/**
* Upgrade the http response
*
* @private
* @class
* @param {Object} socket - the network socket
* @param {Object} head - a buffer, the first packet of
* the upgraded stream
* @returns {undefined} no return value
*/
function ServerUpgradeResponse(socket, head) {
assert.object(socket, 'socket');
assert.buffer(head, 'head');
EventEmitter.call(this);
this.sendDate = true;
this.statusCode = 400;
this._upgrade = {
socket: socket,
head: head
};
this._headWritten = false;
this._upgradeClaimed = false;
}
util.inherits(ServerUpgradeResponse, EventEmitter);
/**
* A function generator for all programatically attaching methods on to
* the ServerUpgradeResponse class.
*
* @private
* @function notImplemented
* @param {Object} method - an object containing configuration
* @returns {Function} function
*/
function notImplemented(method) {
if (!method.throws) {
return function returns() {
return method.returns;
};
} else {
return function throws() {
throw new Error('Method ' + method.name + ' is not implemented!');
};
}
}
var NOT_IMPLEMENTED = [
{ name: 'writeContinue', throws: true },
{ name: 'setHeader', throws: false, returns: null },
{ name: 'getHeader', throws: false, returns: null },
{ name: 'getHeaders', throws: false, returns: {} },
{ name: 'removeHeader', throws: false, returns: null },
{ name: 'addTrailer', throws: false, returns: null },
{ name: 'cache', throws: false, returns: 'public' },
{ name: 'format', throws: true },
{ name: 'set', throws: false, returns: null },
{ name: 'get', throws: false, returns: null },
{ name: 'headers', throws: false, returns: {} },
{ name: 'header', throws: false, returns: null },
{ name: 'json', throws: false, returns: null },
{ name: 'link', throws: false, returns: null }
];
// programatically add a bunch of methods to the ServerUpgradeResponse proto
NOT_IMPLEMENTED.forEach(function forEach(method) {
ServerUpgradeResponse.prototype[method.name] = notImplemented(method);
});
/**
* Internal implementation of `writeHead`
*
* @private
* @function _writeHeadImpl
* @param {Number} statusCode - the http status code
* @param {String} reason - a message
* @returns {undefined} no return value
*/
ServerUpgradeResponse.prototype._writeHeadImpl = function _writeHeadImpl(
statusCode,
reason
) {
if (this._headWritten) {
return;
}
this._headWritten = true;
if (this._upgradeClaimed) {
throw new InvalidUpgradeStateError('Upgrade already claimed!');
}
var head = ['HTTP/1.1 ' + statusCode + ' ' + reason, 'Connection: close'];
if (this.sendDate) {
head.push('Date: ' + new Date().toUTCString());
}
this._upgrade.socket.write(head.join('\r\n') + '\r\n');
};
/**
* Set the status code of the response.
*
* @public
* @function status
* @param {Number} code - the http status code
* @returns {undefined} no return value
*/
ServerUpgradeResponse.prototype.status = function status(code) {
assert.number(code, 'code');
this.statusCode = code;
return code;
};
/**
* Sends the response.
*
* @public
* @function send
* @param {Number} code - the http status code
* @param {Object | String} body - the response to send out
* @returns {undefined} no return value
*/
ServerUpgradeResponse.prototype.send = function send(code, body) {
if (typeof code === 'number') {
this.statusCode = code;
} else {
body = code;
}
if (typeof body === 'object') {
if (typeof body.statusCode === 'number') {
this.statusCode = body.statusCode;
}
if (typeof body.message === 'string') {
this.statusReason = body.message;
}
}
return this.end();
};
/**
* End the response.
*
* @public
* @function end
* @returns {Boolean} always returns true
*/
ServerUpgradeResponse.prototype.end = function end() {
this._writeHeadImpl(this.statusCode, 'Connection Not Upgraded');
this._upgrade.socket.end('\r\n');
return true;
};
/**
* Write to the response.
*
* @public
* @function write
* @returns {Boolean} always returns true
*/
ServerUpgradeResponse.prototype.write = function write() {
this._writeHeadImpl(this.statusCode, 'Connection Not Upgraded');
return true;
};
/**
* Write to the head of the response.
*
* @public
* @function writeHead
* @param {Number} statusCode - the http status code
* @param {String} reason - a message
* @returns {undefined} no return value
*/
ServerUpgradeResponse.prototype.writeHead = function writeHead(
statusCode,
reason
) {
assert.number(statusCode, 'statusCode');
assert.optionalString(reason, 'reason');
this.statusCode = statusCode;
if (!reason) {
reason = 'Connection Not Upgraded';
}
if (this._headWritten) {
throw new Error('Head already written!');
}
return this._writeHeadImpl(statusCode, reason);
};
/**
* Attempt to upgrade.
*
* @public
* @function claimUpgrade
* @returns {Object} an object containing the socket and head
*/
ServerUpgradeResponse.prototype.claimUpgrade = function claimUpgrade() {
if (this._upgradeClaimed) {
throw new InvalidUpgradeStateError('Upgrade already claimed!');
}
if (this._headWritten) {
throw new InvalidUpgradeStateError('Upgrade already aborted!');
}
this._upgradeClaimed = true;
return this._upgrade;
};
module.exports = {
createResponse: createServerUpgradeResponse,
InvalidUpgradeStateError: InvalidUpgradeStateError
};
================================================
FILE: lib/utils.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
/**
* Return a shallow copy of the given object;
*
* @public
* @function shallowCopy
* @param {Object} obj - the object to copy
* @returns {Object} the new copy of the object
*/
function shallowCopy(obj) {
if (!obj) {
return obj;
}
var copy = {};
Object.keys(obj).forEach(function forEach(k) {
copy[k] = obj[k];
});
return copy;
}
/**
* Merges two query parameter objects. Merges to array
* if the same key is encountered.
*
* @public
* @function mergeQs
* @param {Object} obj1 - first qs object
* @param {Object} obj2 - second qs object
* @returns {Object} the merged object
*/
function mergeQs(obj1, obj2) {
var merged = shallowCopy(obj1) || {};
// defend against null cause null is an object. yay js.
if (obj2 && typeof obj2 === 'object') {
Object.keys(obj2).forEach(function forEach(key) {
// if we already have this key and it isn't an array,
// make it one array of the same element.
if (merged.hasOwnProperty(key) && !(merged[key] instanceof Array)) {
merged[key] = [merged[key]];
// push the new value down
merged[key].push(obj2[key]);
} else {
// otherwise just set it
merged[key] = obj2[key];
}
});
}
return merged;
}
///--- Exports
module.exports = {
shallowCopy: shallowCopy,
mergeQs: mergeQs
};
================================================
FILE: package.json
================================================
{
"author": "Mark Cavage ",
"contributors": [
"Adam Argo",
"Alex Liu",
"Alexander Olsson",
"Andrew Robinson",
"Andrew Sliwinski",
"Anro Robinson",
"Armin Tamzarian",
"Asa Ayers",
"Bastiaan Marinus van de Weerd",
"Ben Doerr",
"Ben Hale",
"Ben Howes",
"Ben Hutchison",
"Benjamine Coe",
"Benjamin Urban",
"Blake VanLandingham",
"Brian Pin",
"Bryan Donovan",
"Bryce Kahle",
"Christopher Cannell",
"Clément Désiles",
"Colin O'Brien",
"Corbin Uselton",
"Diego Torres",
"Domenic Denicola",
"Domikik Lessel",
"Dominic Barnes",
"Erik Kristensen",
"Falco Nogatz",
"Gergely Nemeth",
"Guillaume Chauvet",
"Ifiok Idiang",
"Isaac Schlueter",
"Jacob Quatier",
"James O'Cull",
"James Womack",
"Jonathan Dahan",
"Josh Clulow",
"Jorge Serrano",
"Jason Ghent",
"Khaja Naquiuddin",
"Lou Sacco",
"Matt Smillie",
"Mattijs Spierings",
"Micah Ransdell",
"Michal Moskal",
"Michael Paulson",
"Mike Williams",
"Nathanael Anderson",
"Patrick Mooney",
"Paul Bouzakis",
"Pedro Palazón",
"Quentin Buathier",
"Richardo Stuven",
"Scott Turnquest",
"Shaun Berryman",
"Steve Mason",
"Tim Kuijsten",
"Trent Mick",
"Tuure Kanuisto",
"Will Prater",
"Yunong Xiao",
"Zachary Snow"
],
"name": "restify",
"homepage": "http://restify.com",
"description": "REST framework",
"keywords": [
"REST",
"framework",
"express",
"DTrace"
],
"version": "11.2.0",
"repository": {
"type": "git",
"url": "git://github.com/restify/node-restify.git"
},
"bugs": {
"url": "https://github.com/restify/node-restify/issues"
},
"main": "lib/index.js",
"directories": {
"lib": "./lib"
},
"bin": {
"report-latency": "./bin/report-latency"
},
"engines": {
"node": ">=10.0.0"
},
"dependencies": {
"assert-plus": "^1.0.0",
"csv": "^6.2.2",
"escape-regexp-component": "^1.0.2",
"ewma": "^2.0.1",
"find-my-way": "^7.6.0",
"formidable": "^1.2.1",
"http-signature": "^1.3.6",
"lodash": "^4.17.11",
"lru-cache": "^7.14.1",
"mime": "^3.0.0",
"negotiator": "^0.6.2",
"once": "^1.4.0",
"pidusage": "^3.0.2",
"pino": "^8.7.0",
"qs": "^6.7.0",
"restify-errors": "^8.0.2",
"semver": "^7.3.8",
"send": "^0.18.0",
"spdy": "^4.0.0",
"uuid": "^9.0.0",
"vasync": "^2.2.0"
},
"optionalDependencies": {
"dtrace-provider": "~0.8"
},
"devDependencies": {
"autocannon": "^4.0.0",
"autocannon-compare": "^0.4.0",
"chai": "^4.2.0",
"coveralls": "^3.0.3",
"documentation": "^11.0.0",
"eslint": "^5.16.0",
"eslint-config-prettier": "^4.3.0",
"eslint-plugin-jsdoc": "^3.15.1",
"eslint-plugin-prettier": "^3.1.0",
"glob": "^7.1.4",
"inquirer": "^3.3.0",
"mkdirp": "^0.5.1",
"mocha": "^7.1.1",
"nodeunit": "^0.11.3",
"nyc": "^15.0.0",
"ora": "^1.3.0",
"pre-commit": "^1.2.2",
"prettier": "^1.17.1",
"proxyquire": "^1.8.0",
"restify-clients": "^2.6.6",
"rimraf": "^2.6.3",
"sinon": "^7.5.0",
"validator": "^7.2.0",
"watershed": "^0.4.0"
},
"license": "MIT",
"scripts": {
"test": "make prepush"
}
}
================================================
FILE: test/.eslintrc
================================================
{
"rules": {
"handle-callback-err": [ 0 ]
}
}
================================================
FILE: test/chain.test.js
================================================
'use strict';
/* eslint-disable func-names */
var domain = require('domain');
var Chain = require('../lib/chain');
if (require.cache[__dirname + '/lib/helper.js']) {
delete require.cache[__dirname + '/lib/helper.js'];
}
var helper = require('./lib/helper.js');
///--- Globals
var test = helper.test;
test('calls all the handlers', function(t) {
var chain = new Chain();
var counter = 0;
chain.add(function(req, res, next) {
counter++;
next();
});
chain.add(function(req, res, next) {
counter++;
next();
});
chain.run(
{
startHandlerTimer: function() {},
endHandlerTimer: function() {},
connectionState: function() {
return '';
}
},
{},
function() {
t.equal(counter, 2);
t.done();
}
);
});
test('abort with Error in next', function(t) {
var chain = new Chain();
var counter = 0;
var myError = new Error('Foo');
chain.add(function(req, res, next) {
counter++;
next(myError);
});
chain.add(function(req, res, next) {
counter++;
next();
});
chain.run(
{
startHandlerTimer: function() {},
endHandlerTimer: function() {},
connectionState: function() {
return '';
}
},
{},
function(err) {
t.deepEqual(err, myError);
t.equal(counter, 1);
t.done();
}
);
});
test('abort with false in next', function(t) {
var chain = new Chain();
chain.add(function(req, res, next) {
next(false);
});
chain.add(function(req, res, next) {
t.fail('Should not be here');
next();
});
chain.run(
{
startHandlerTimer: function() {},
endHandlerTimer: function() {},
connectionState: function() {
return false;
}
},
{},
function(err) {
t.equal(err, false);
t.done();
}
);
});
test('abort with closed request', function(t) {
var chain = new Chain();
var closed = false;
chain.add(function(req, res, next) {
closed = true;
next();
});
chain.add(function(req, res, next) {
t.fail('Should not be here');
});
chain.run(
{
startHandlerTimer: function() {},
endHandlerTimer: function() {},
connectionState: function() {
return closed ? 'close' : '';
}
},
{},
function(err) {
t.ifError(err);
t.done();
}
);
});
test('cals error middleware', function(t) {
t.expect(2);
var chain = new Chain();
var myError = new Error('Foo');
chain.add(function(req, res, next) {
next(myError);
});
chain.add(function(err, req, res, next) {
t.deepEqual(err, myError);
next(err);
});
chain.add(function(req, res, next) {
t.fail('Should not be here');
});
chain.run(
{
startHandlerTimer: function() {},
endHandlerTimer: function() {},
connectionState: function() {
return '';
}
},
{},
function(err) {
t.deepEqual(err, myError);
t.done();
}
);
});
test('onceNext prevents double next calls', function(t) {
var doneCalled = 0;
var chain = new Chain({
onceNext: true
});
chain.add(function foo(req, res, next) {
next();
next();
});
chain.run(
{
startHandlerTimer: function() {},
endHandlerTimer: function() {},
connectionState: function() {
return '';
}
},
{},
function(err) {
t.ifError(err);
doneCalled++;
t.equal(doneCalled, 1);
t.done();
}
);
});
test('throws error for double next calls in strictNext mode', function(t) {
t.expect(1);
var chain = new Chain({
strictNext: true
});
chain.add(function foo(req, res, next) {
next();
next();
});
var testDomain = domain.create();
testDomain.on('error', function onError(err) {
t.equal(err.message, "next shouldn't be called more than once");
testDomain.exit();
t.done();
});
testDomain.run(function run() {
chain.run(
{
startHandlerTimer: function() {},
endHandlerTimer: function() {},
connectionState: function() {
return '';
}
},
{},
function(err) {
t.ifError(err);
}
);
});
});
test('calls req.startHandlerTimer', function(t) {
var chain = new Chain();
chain.add(function foo(req, res, next) {
next();
});
chain.run(
{
startHandlerTimer: function(handleName) {
t.equal(handleName, 'foo');
t.done();
},
endHandlerTimer: function() {},
connectionState: function() {
return '';
}
},
{},
function() {}
);
});
test('calls req.endHandlerTimer', function(t) {
var chain = new Chain();
chain.add(function foo(req, res, next) {
next();
});
chain.run(
{
startHandlerTimer: function() {},
endHandlerTimer: function(handleName) {
t.equal(handleName, 'foo');
t.done();
},
connectionState: function() {
return '';
}
},
{},
function() {}
);
});
test('count returns with the number of registered handlers', function(t) {
var chain = new Chain();
chain.add(function(req, res, next) {});
chain.add(function(req, res, next) {});
t.equal(chain.count(), 2);
t.end();
});
test('getHandlers returns with the array of handlers', function(t) {
var chain = new Chain();
var handlers = [function(req, res, next) {}, function(req, res, next) {}];
chain.add(handlers[0]);
chain.add(handlers[1]);
t.deepEqual(chain.getHandlers(), handlers);
t.end();
});
test('waits async handlers', function(t) {
const chain = new Chain();
let counter = 0;
chain.add(async function(req, res) {
await helper.sleep(50);
counter++;
});
chain.add(function(req, res, next) {
counter++;
next();
});
chain.run(
{
startHandlerTimer: function() {},
endHandlerTimer: function() {},
connectionState: function() {
return '';
}
},
{},
function() {
t.equal(counter, 2);
t.done();
}
);
});
test('abort with rejected promise', function(t) {
const myError = new Error('Foo');
const chain = new Chain();
let counter = 0;
chain.add(async function(req, res) {
counter++;
await helper.sleep(10);
return Promise.reject(myError);
});
chain.add(function(req, res, next) {
counter++;
next();
});
chain.run(
{
startHandlerTimer: function() {},
endHandlerTimer: function() {},
connectionState: function() {
return '';
}
},
{},
function(err) {
t.deepEqual(err, myError);
t.equal(counter, 1);
t.done();
}
);
});
test('abort with rejected promise without error', function(t) {
const chain = new Chain();
let counter = 0;
chain.add(async function(req, res) {
counter++;
await helper.sleep(10);
return Promise.reject();
});
chain.add(function(req, res, next) {
counter++;
next();
});
chain.run(
{
startHandlerTimer: function() {},
endHandlerTimer: function() {},
connectionState: function() {
return '';
},
path: function() {
return '/';
}
},
{},
function(err) {
t.ok(typeof err === 'object');
t.equal(err.name, 'AsyncError');
t.equal(err.jse_info.cause, undefined);
t.equal(counter, 1);
t.done();
}
);
});
test('abort with throw inside async function', function(t) {
const myError = new Error('Foo');
const chain = new Chain();
let counter = 0;
chain.add(async function(req, res) {
counter++;
await helper.sleep(10);
throw myError;
});
chain.add(function(req, res, next) {
counter++;
next();
});
chain.run(
{
startHandlerTimer: function() {},
endHandlerTimer: function() {},
connectionState: function() {
return '';
}
},
{},
function(err) {
t.deepEqual(err, myError);
t.equal(counter, 1);
t.done();
}
);
});
test('fails to add non async function with arity 2', function(t) {
var handler = function getLunch(req, res) {
res.send('ok');
};
var chain = new Chain();
t.throws(function() {
chain.add(handler);
}, /getLunch/);
t.end();
});
test('fails to add async function with arity 3', function(t) {
var handler = async function getBreakfast(req, res, next) {
res.send('ok');
};
var chain = new Chain();
t.throws(function() {
chain.add(handler);
}, /getBreakfast/);
t.end();
});
================================================
FILE: test/chainComposer.test.js
================================================
'use strict';
/* eslint-disable func-names */
if (require.cache[__dirname + '/lib/helper.js']) {
delete require.cache[__dirname + '/lib/helper.js'];
}
var helper = require('./lib/helper.js');
///--- Globals
var test = helper.test;
var composer = require('../lib/helpers/chainComposer');
test('chainComposer creates a valid chain for a handler array ', function(t) {
var counter = 0;
var handlers = [];
handlers.push(function(req, res, next) {
counter++;
next();
});
handlers.push(function(req, res, next) {
counter++;
next();
});
var chain = composer(handlers);
chain(
{
startHandlerTimer: function() {},
endHandlerTimer: function() {},
connectionState: function() {
return '';
}
},
{},
function() {
t.equal(counter, 2);
t.done();
}
);
});
test('chainComposer creates a valid chain for a single handler', function(t) {
var counter = 0;
var handlers = function(req, res, next) {
counter++;
next();
};
var chain = composer(handlers);
chain(
{
startHandlerTimer: function() {},
endHandlerTimer: function() {},
connectionState: function() {
return '';
}
},
{},
function() {
t.equal(counter, 1);
t.done();
}
);
});
================================================
FILE: test/formatter-optional.test.js
================================================
'use strict';
/* eslint-disable func-names */
var restifyClients = require('restify-clients');
var restify = require('../lib');
if (require.cache[__dirname + '/lib/helper.js']) {
delete require.cache[__dirname + '/lib/helper.js'];
}
var helper = require('./lib/helper.js');
///--- Globals
var after = helper.after;
var before = helper.before;
var test = helper.test;
var CLIENT;
var LOCALHOST;
var PORT = process.env.UNIT_TEST_PORT || 0;
var SERVER;
///--- Tests
before(function(callback) {
try {
SERVER = restify.createServer({
handleUncaughtExceptions: true,
log: helper.getLog('server'),
strictFormatters: false
});
SERVER.listen(PORT, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
LOCALHOST = 'http://' + '127.0.0.1:' + PORT;
callback();
});
} catch (e) {
console.error(e.stack);
process.exit(1);
}
});
after(function(callback) {
try {
SERVER.close(callback);
CLIENT.close();
} catch (e) {
console.error(e.stack);
process.exit(1);
}
});
test('send 200 on formatter missing and strictFormatters false', function(t) {
// When server is passed "strictFormatters: false" at creation time,
// res.send still sends a successful response even when a formatter is
// not set up for a specific content-type.
SERVER.get('/11', function handle(req, res, next) {
res.header('content-type', 'application/hal+json');
res.send(200, JSON.stringify({ hello: 'world' }));
return next();
});
CLIENT.get(LOCALHOST + '/11', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.equal(res.headers['content-type'], 'application/hal+json');
t.end();
});
});
================================================
FILE: test/formatter.test.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
/* eslint-disable func-names */
var restifyClients = require('restify-clients');
var errors = require('restify-errors');
var sinon = require('sinon');
var restify = require('../lib');
var jsonFormatter = require('../lib/formatters/json');
if (require.cache[__dirname + '/lib/helper.js']) {
delete require.cache[__dirname + '/lib/helper.js'];
}
var helper = require('./lib/helper.js');
///--- Globals
var after = helper.after;
var before = helper.before;
var test = helper.test;
var PORT = process.env.UNIT_TEST_PORT || 0;
var CLIENT;
var SERVER;
///--- Tests
before(function(callback) {
try {
SERVER = restify.createServer({
handleUncaughtExceptions: true,
formatters: {
'text/sync': function(req, res, body) {
return 'sync fmt';
},
'text/syncerror': function(req, res, body) {
// this is a bad formatter, on purpose.
return x.toString(); // eslint-disable-line no-undef
},
'text/syncerror_expected': function(req, res, body) {
throw new errors.InternalServerError('Errors happen');
},
'application/foo; q=0.9': function(req, res, body) {
return 'foo!';
},
'application/bar; q=0.1': function(req, res, body) {
return 'bar!';
}
},
dtrace: helper.dtrace,
log: helper.getLog('server'),
version: ['2.0.0', '0.5.4', '1.4.3']
});
SERVER.listen(PORT, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createStringClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false,
agent: false
});
SERVER.get('/sync', function(req, res, next) {
res.send('dummy response');
return next();
});
SERVER.get('/missingFormatter', function(req, res, next) {
delete res.formatters['application/octet-stream'];
res.setHeader('content-type', 'text/html');
res.send('dummy response');
return next();
});
SERVER.get('/jsonpSeparators', function(req, res, next) {
res.setHeader('content-type', 'application/javascript');
res.send(
String.fromCharCode(0x2028) + String.fromCharCode(0x2029)
);
return next();
});
process.nextTick(callback);
});
} catch (e) {
console.error(e.stack);
process.exit(1);
}
});
after(function(callback) {
try {
SERVER.close(callback);
} catch (e) {
console.error(e.stack);
process.exit(1);
}
});
test('GH-845: sync formatter', function(t) {
CLIENT.get(
{
path: '/sync',
headers: {
accept: 'text/sync'
}
},
function(err, req, res, data) {
t.ifError(err);
t.ok(req);
t.ok(res);
t.equal(data, 'sync fmt');
t.end();
}
);
});
test('GH-845: sync formatter should blow up', function(t) {
SERVER.once('uncaughtException', function(req, res, route, err) {
t.ok(err);
t.equal(err.name, 'ReferenceError');
t.equal(err.message, 'x is not defined');
res.write('uncaughtException');
res.end();
});
CLIENT.get(
{
path: '/sync',
headers: {
accept: 'text/syncerror'
}
},
function(err, req, res, data) {
t.equal(data, 'uncaughtException');
t.end();
}
);
});
test('sync formatter should handle expected errors gracefully', function(t) {
SERVER.once('uncaughtException', function(req, res, route, err) {
throw new Error('Should not reach');
});
CLIENT.get(
{
path: '/sync',
headers: {
accept: 'text/syncerror_expected'
}
},
function(err, req, res, data) {
t.ok(err);
t.ok(req);
t.ok(res);
t.equal(res.statusCode, 500);
SERVER.removeAllListeners('uncaughtException');
t.end();
}
);
});
test('q-val priority', function(t) {
var opts = {
path: '/sync',
headers: {
accept: 'application/*'
}
};
CLIENT.get(opts, function(err, req, res, data) {
t.ifError(err);
t.ok(req);
t.ok(res);
t.equal(data, 'foo!');
t.end();
});
});
test('GH-771 q-val priority on */*', function(t) {
var opts = {
path: '/sync',
headers: {
accept: '*/*'
}
};
// this test is a little flaky - it will look for first formatter that
// satisfies q-val but in this test we have a bunch of bad formatters.
// it appears V8 will use the first found formatter (this case, text/sync).
CLIENT.get(opts, function(err, req, res, data) {
t.ifError(err);
t.ok(req);
t.ok(res);
t.equal(data, 'sync fmt');
t.end();
});
});
test(
'GH-937 should return 406 when no content-type header set on response ' +
'matching an acceptable type found by matching client',
function(t) {
// ensure client accepts only a type not specified by server
var opts = {
path: '/sync',
headers: {
accept: 'text/html'
}
};
CLIENT.get(opts, function(err, req, res, data) {
t.ok(err);
t.ok(req);
t.ok(res);
t.equal(res.statusCode, 406);
t.end();
});
}
);
test(
'GH-937 should return 500 when no default formatter found ' +
'and octet-stream is not available',
function(t) {
// ensure client accepts only a type not specified by server
var opts = {
path: '/missingFormatter',
headers: {
accept: 'text/html'
}
};
CLIENT.get(opts, function(err, req, res, data) {
t.ok(err);
t.ok(req);
t.ok(res);
t.equal(res.statusCode, 500);
t.end();
});
}
);
// eslint-disable-next-line
test('default jsonp formatter should escape line and paragraph separators', function(t) {
// ensure client accepts only a type not specified by server
var opts = {
path: '/jsonpSeparators',
headers: {
accept: 'application/javascript'
}
};
CLIENT.get(opts, function(err, req, res, data) {
t.ifError(err);
t.ok(req);
t.ok(res);
t.equal(data, '"\\u2028\\u2029"');
t.end();
});
});
// eslint-disable-next-line
test('default json formatter should wrap & throw InternalServer error on unserializable bodies', function(t) {
t.expect(2);
sinon.spy(JSON, 'stringify');
SERVER.once('uncaughtException', function(req, res, route, err) {
console.log(err.stack); // For convenience
throw new Error('Should not reach');
});
var opts = {
path: '/badJSON',
name: 'badJSON'
};
SERVER.get(opts, function(req, res, next) {
var body = {};
// Add unserializable circular reference
body.body = body;
try {
jsonFormatter(req, res, body);
throw new Error('Should not reach');
} catch (e) {
t.ok(e instanceof errors.InternalServerError);
t.ok(JSON.stringify.threw(e.cause()));
}
res.send();
});
CLIENT.get('/badJSON', function(err, req, res, data) {
SERVER.rm('badJSON');
SERVER.removeAllListeners('uncaughtException');
JSON.stringify.restore();
t.end();
});
});
================================================
FILE: test/index.test.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
/* eslint-disable func-names */
var httpDate = require('../lib/http_date');
if (require.cache[__dirname + '/lib/helper.js']) {
delete require.cache[__dirname + '/lib/helper.js'];
}
var helper = require('./lib/helper.js');
///--- Globals
var test = helper.test;
///--- Tests
test('httpDate', function(t) {
var d = httpDate();
var regex = /\w{3}, \d{1,2} \w{3} \d{4} \d{2}:\d{2}:\d{2} GMT/;
t.ok(regex.test(d));
t.end();
});
================================================
FILE: test/keys/http2-cert.pem
================================================
-----BEGIN CERTIFICATE-----
MIICHzCCAYgCCQCPPSUAa8QZojANBgkqhkiG9w0BAQUFADBUMQswCQYDVQQGEwJS
VTETMBEGA1UECBMKU29tZS1TdGF0ZTENMAsGA1UEBxMET21zazEhMB8GA1UEChMY
SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTExMDQwOTEwMDY0NVoXDTExMDUw
OTEwMDY0NVowVDELMAkGA1UEBhMCUlUxEzARBgNVBAgTClNvbWUtU3RhdGUxDTAL
BgNVBAcTBE9tc2sxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCB
nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1bn25sPkv46wl70BffxradlkRd/x
p5Xf8HDhPSfzNNctERYslXT2fX7Dmfd5w1XTVqqGqJ4izp5VewoVOHA8uavo3ovp
gNWasil5zADWaM1T0nnV0RsFbZWzOTmm1U3D48K8rW3F5kOZ6f4yRq9QT1gF/gN7
5Pt494YyYyJu/a8CAwEAATANBgkqhkiG9w0BAQUFAAOBgQBuRZisIViI2G/R+w79
vk21TzC/cJ+O7tKsseDqotXYTH8SuimEH5IWcXNgnWhNzczwN8s2362NixyvCipV
yd4wzMpPbjIhnWGM0hluWZiK2RxfcqimIBjDParTv6CMUIuwGQ257THKY8hXGg7j
Uws6Lif3P9UbsuRiYPxMgg98wg==
-----END CERTIFICATE-----
================================================
FILE: test/keys/http2-csr.pem
================================================
-----BEGIN CERTIFICATE REQUEST-----
MIIBkzCB/QIBADBUMQswCQYDVQQGEwJSVTETMBEGA1UECBMKU29tZS1TdGF0ZTEN
MAsGA1UEBxMET21zazEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDVufbmw+S/jrCXvQF9/Gtp2WRF
3/Gnld/wcOE9J/M01y0RFiyVdPZ9fsOZ93nDVdNWqoaoniLOnlV7ChU4cDy5q+je
i+mA1ZqyKXnMANZozVPSedXRGwVtlbM5OabVTcPjwrytbcXmQ5np/jJGr1BPWAX+
A3vk+3j3hjJjIm79rwIDAQABoAAwDQYJKoZIhvcNAQEFBQADgYEAiNWhz6EppIVa
FfUaB3sLeqfamb9tg9kBHtvqj/FJni0snqms0kPWaTySEPHZF0irIb7VVdq/sVCb
3gseMVSyoDvPJ4lHC3PXqGQ7kM1mIPhDnR/4HDA3BhlGhTXSDIHgZnvI+HMBdsyC
hC3dz5odyKqe4nmoofomALkBL9t4H8s=
-----END CERTIFICATE REQUEST-----
================================================
FILE: test/keys/http2-key.pem
================================================
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDVufbmw+S/jrCXvQF9/Gtp2WRF3/Gnld/wcOE9J/M01y0RFiyV
dPZ9fsOZ93nDVdNWqoaoniLOnlV7ChU4cDy5q+jei+mA1ZqyKXnMANZozVPSedXR
GwVtlbM5OabVTcPjwrytbcXmQ5np/jJGr1BPWAX+A3vk+3j3hjJjIm79rwIDAQAB
AoGAAv2QI9h32epQND9TxwSCKD//dC7W/cZOFNovfKCTeZjNK6EIzKqPTGA6smvR
C1enFl5adf+IcyWqAoe4lkqTvurIj+2EhtXdQ8DBlVuXKr3xvEFdYxXPautdTCF6
KbXEyS/s1TZCRFjYftvCrXxc3pK45AQX/wg7z1K+YB5pyIECQQD0OJvLoxLYoXAc
FZraIOZiDsEbGuSHqoCReFXH75EC3+XGYkH2bQ/nSIZ0h1buuwQ/ylKXOlTPT3Qt
Xm1OQEBvAkEA4AjWsIO/rRpOm/Q2aCrynWMpoUXTZSbL2yGf8pxp/+8r2br5ier0
M1LeBb/OPY1+k39NWLXxQoo64xoSFYk2wQJAd2wDCwX4HkR7HNCXw1hZL9QFK6rv
20NN0VSlpboJD/3KT0MW/FiCcVduoCbaJK0Au+zEjDyy4hj5N4I4Mw6KMwJAXVAx
I+psTsxzS4/njXG+BgIEl/C+gRYsuMQDnAi8OebDq/et8l0Tg8ETSu++FnM18neG
ntmBeMacinUUbTXuwQJBAJp/onZdsMzeVulsGrqR1uS+Lpjc5Q1gt5ttt2cxj91D
rio48C/ZvWuKNE8EYj2ALtghcVKRvgaWfOxt2GPguGg=
-----END RSA PRIVATE KEY-----
================================================
FILE: test/lib/helper.js
================================================
// Copyright 2012 Mark Cavage. All rights reserved.
//
// Just a simple wrapper over nodeunit's exports syntax. Also exposes
// a common logger for all tests.
//
'use strict';
/* eslint-disable func-names */
var domain = require('domain');
var pino = require('pino');
var once = require('once');
///--- Exports
module.exports = {
after: function after(teardown) {
module.parent.exports.tearDown = function _teardown(callback) {
var d = domain.create();
var self = this;
d.once('error', function(err) {
console.error('after: uncaught error\n', err.stack);
process.exit(1);
});
d.run(function() {
teardown.call(self, once(callback));
});
};
},
before: function before(setup) {
module.parent.exports.setUp = function _setup(callback) {
var d = domain.create();
var self = this;
d.once('error', function(err) {
console.error('before: uncaught error\n' + err.stack);
process.exit(1);
});
d.run(function() {
setup.call(self, once(callback));
});
};
},
test: function test(name, tester) {
module.parent.exports[name] = function _(t) {
var d = domain.create();
var self = this;
d.once('error', function(err) {
t.ifError(err);
t.end();
});
d.add(t);
d.run(function() {
t.end = once(function() {
t.done();
});
t.notOk = function notOk(ok, message) {
return t.ok(!ok, message);
};
tester.call(self, t);
});
};
},
getLog: function(name, streams, level) {
return pino(
{
level: process.env.LOG_LEVEL || level || 'fatal',
name: name || process.argv[1],
serializers: pino.stdSerializers
},
streams || process.stdout
);
},
get dtrace() {
return true;
},
sleep: function sleep(timeInMs) {
return new Promise(function sleepPromise(resolve) {
setTimeout(function timeout() {
resolve();
}, timeInMs);
});
}
};
================================================
FILE: test/lib/server-withDisableUncaughtException.js
================================================
// A simple node process that will start a restify server with the
// uncaughtException handler disabled. Responds to a 'serverPortRequest' message
// and sends back the server's bound port number.
'use strict';
/* eslint-disable func-names */
var restify = require('../../lib');
function main() {
var port = process.env.UNIT_TEST_PORT || 0;
var server = restify.createServer({ handleUncaughtExceptions: false });
server.get('/', function(req, res, next) {
throw new Error('Catch me!');
});
server.listen(0, function() {
port = server.address().port;
console.log('port: ', port);
process.on('message', function(msg) {
if (msg.task !== 'serverPortRequest') {
process.send({ error: 'Unexpected message: ' + msg });
return;
}
process.send({ task: 'serverPortResponse', port: port });
});
});
}
if (require.main === module) {
main();
}
================================================
FILE: test/lib/streamRecorder.js
================================================
'use strict';
const stream = require('stream');
class StreamRecorder extends stream.Writable {
constructor(options) {
options = options || {};
super(options);
this.flushRecords();
}
_write(chunk, encoding, callback) {
const record = JSON.parse(chunk.toString());
this.records.push(record);
callback();
}
flushRecords() {
this.records = [];
}
}
module.exports = StreamRecorder;
================================================
FILE: test/plugins/.eslintrc
================================================
{
env: {
mocha: true
}
}
================================================
FILE: test/plugins/accept.test.js
================================================
'use strict';
/* eslint-disable func-names */
// external requires
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
// local files
var helper = require('../lib/helper');
// local globals
var SERVER;
var CLIENT;
var PORT;
describe('accept parser', function() {
before(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.use(restify.plugins.acceptParser(SERVER.acceptable));
SERVER.get('/', function respond(req, res, next) {
res.send();
next();
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
done();
});
});
after(function(done) {
CLIENT.close();
SERVER.close(done);
});
it('accept ok', function(done) {
CLIENT.get('/', function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('accept not ok (406)', function(done) {
var opts = {
path: '/',
headers: {
accept: 'foo/bar'
}
};
CLIENT.get(opts, function(err, _, res) {
assert.ok(err);
assert.equal(err.name, 'NotAcceptableError');
assert.equal(res.statusCode, 406);
done();
});
});
it('GH-1619: should fire NotAcceptable event on server', function(done) {
var opts = {
path: '/',
headers: {
accept: 'foo/bar'
}
};
var evtFired = false;
SERVER.on('NotAcceptable', function(req, res, err, cb) {
evtFired = true;
return cb();
});
CLIENT.get(opts, function(err, _, res) {
assert.ok(err);
assert.equal(err.name, 'NotAcceptableError');
assert.equal(res.statusCode, 406);
assert.isTrue(evtFired);
return done();
});
});
});
================================================
FILE: test/plugins/audit.test.js
================================================
'use strict';
/* eslint-disable func-names */
// runtime modules
var PassThrough = require('stream').PassThrough;
// external requires
var assert = require('chai').assert;
var pino = require('pino');
var lodash = require('lodash');
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
// local files
var helper = require('../lib/helper');
var StreamRecorder = require('../lib/streamRecorder');
var vasync = require('vasync');
// local globals
var MILLISECOND_IN_MICROSECONDS = 1000;
var TOLERATED_MICROSECONDS = MILLISECOND_IN_MICROSECONDS;
var SERVER;
var CLIENT;
var PORT;
let LOG_BUFFER;
function assertIsAtLeastWithTolerate(num1, num2, tolerate, msg) {
assert.isAtLeast(num1, num2 - tolerate, msg + 'should be >= ' + num2);
}
describe('audit logger', function() {
beforeEach(function(done) {
LOG_BUFFER = new StreamRecorder();
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server', LOG_BUFFER, 'info')
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
done();
});
});
afterEach(function(done) {
CLIENT.close();
SERVER.close(done);
});
it('audit logger should print log by default', function(done) {
var collectLog;
SERVER.on(
'after',
restify.plugins.auditLogger({
log: pino({ name: 'audit' }),
server: SERVER,
event: 'after'
})
);
SERVER.get('/foo', function(req, res, next) {
res.send(200, { testdata: 'foo' });
next();
});
SERVER.get('/bar', function(req, res, next) {
res.send(200, { testdata: 'bar' });
next();
});
SERVER.get('/auditrecords', function(req, res, next) {
// strip log records of req/res as they will cause
// serialization issues.
var data = LOG_BUFFER.records.map(function(record) {
return lodash.omit(record, 'req', 'res');
}, []);
res.send(200, data);
next();
});
collectLog = function() {
CLIENT.get('/auditrecords', function(err, req, res) {
assert.ifError(err);
var data = JSON.parse(res.body);
assert.ok(data);
data.forEach(function(d) {
assert.isNumber(d.latency);
});
done();
});
};
vasync.forEachParallel(
{
func: function clientRequest(urlPath, callback) {
CLIENT.get(urlPath, function(err, req, res) {
assert.ifError(err);
assert.ok(JSON.parse(res.body));
return callback(err, JSON.parse(res.body));
});
},
inputs: ['/foo', '/bar']
},
function(err, results) {
assert.ifError(err);
collectLog();
}
);
});
it('test audit logger emit', function(done) {
SERVER.once(
'after',
restify.plugins.auditLogger({
log: pino({ name: 'audit' }),
server: SERVER,
event: 'after'
})
);
SERVER.once('audit', function(data) {
assert.ok(data);
assert.ok(data.req_id);
assert.equal(
data.req.url,
'/audit',
'request url should be /audit'
);
assert.isNumber(data.latency);
done();
});
SERVER.get('/audit', [
restify.plugins.queryParser(),
function(req, res, next) {
res.send();
next();
}
]);
CLIENT.get('/audit', function(err, req, res) {
assert.ifError(err);
});
});
it('test custom serializers', function(done) {
SERVER.once(
'after',
restify.plugins.auditLogger({
log: pino({ name: 'audit' }),
event: 'after',
serializers: {
req: function(req) {
return { fooReq: 'barReq' };
},
res: function(res) {
return { fooRes: 'barRes' };
}
}
})
);
SERVER.get('/audit', function aTestHandler(req, res, next) {
res.send('');
return next();
});
SERVER.on('after', function() {
var record = LOG_BUFFER.records && LOG_BUFFER.records[0];
assert.equal(record.req.fooReq, 'barReq');
assert.equal(record.res.fooRes, 'barRes');
done();
});
CLIENT.get('/audit', function(err, req, res) {
assert.ifError(err);
});
});
it('should log handler timers', function(done) {
var WAIT_IN_MILLISECONDS = 1100;
SERVER.once(
'after',
restify.plugins.auditLogger({
log: pino({ name: 'audit' }),
event: 'after'
})
);
SERVER.get('/audit', function aTestHandler(req, res, next) {
req.startHandlerTimer('audit-sub');
setTimeout(function() {
req.endHandlerTimer('audit-sub');
res.send('');
return next();
}, WAIT_IN_MILLISECONDS);
// this really should be 1000 but make it 1100 so that the tests
// don't sporadically fail due to timing issues.
});
SERVER.on('after', function() {
var record = LOG_BUFFER.records && LOG_BUFFER.records[0];
// check timers
assert.ok(record, 'no log records');
assert.equal(
LOG_BUFFER.records.length,
1,
'should only have 1 log record'
);
assertIsAtLeastWithTolerate(
record.req.timers.aTestHandler,
WAIT_IN_MILLISECONDS * MILLISECOND_IN_MICROSECONDS,
TOLERATED_MICROSECONDS,
'atestHandler'
);
assertIsAtLeastWithTolerate(
record.req.timers['aTestHandler-audit-sub'],
WAIT_IN_MILLISECONDS * MILLISECOND_IN_MICROSECONDS,
TOLERATED_MICROSECONDS,
'aTestHandler-audit-sub'
);
var handlers = Object.keys(record.req.timers);
assert.equal(
handlers[handlers.length - 2],
'aTestHandler-audit-sub',
'sub handler timer not in order'
);
assert.equal(
handlers[handlers.length - 1],
'aTestHandler',
'aTestHandler not last'
);
done();
});
CLIENT.get('/audit', function(err, req, res) {
assert.ifError(err);
});
});
it('should log anonymous handler timers', function(done) {
this.timeout(5000);
var WAIT_IN_MILLISECONDS = 1000;
SERVER.once(
'after',
restify.plugins.auditLogger({
log: pino({ name: 'audit' }),
event: 'after'
})
);
SERVER.pre(function(req, res, next) {
next();
});
SERVER.pre(function(req, res, next) {
next();
});
SERVER.use(function(req, res, next) {
next();
});
SERVER.use(function(req, res, next) {
next();
});
SERVER.get(
'/audit',
function(req, res, next) {
setTimeout(function() {
return next();
}, WAIT_IN_MILLISECONDS);
},
function(req, res, next) {
req.startHandlerTimer('audit-sub');
setTimeout(function() {
req.endHandlerTimer('audit-sub');
res.send('');
return next();
}, WAIT_IN_MILLISECONDS);
}
);
SERVER.on('after', function() {
// check timers
var record = LOG_BUFFER.records && LOG_BUFFER.records[0];
assert.ok(record, 'no log records');
assert.equal(
LOG_BUFFER.records.length,
1,
'should only have 1 log record'
);
assertIsAtLeastWithTolerate(
record.req.timers['pre-0'],
0,
TOLERATED_MICROSECONDS,
'pre-0'
);
assertIsAtLeastWithTolerate(
record.req.timers['pre-1'],
0,
TOLERATED_MICROSECONDS,
'pre-1'
);
assertIsAtLeastWithTolerate(
record.req.timers['use-0'],
0,
TOLERATED_MICROSECONDS,
'use-0'
);
assertIsAtLeastWithTolerate(
record.req.timers['use-1'],
0,
TOLERATED_MICROSECONDS,
'use-1'
);
assertIsAtLeastWithTolerate(
record.req.timers['handler-0'],
WAIT_IN_MILLISECONDS * MILLISECOND_IN_MICROSECONDS,
TOLERATED_MICROSECONDS,
'handler-0'
);
assertIsAtLeastWithTolerate(
record.req.timers['handler-1'],
WAIT_IN_MILLISECONDS * MILLISECOND_IN_MICROSECONDS,
TOLERATED_MICROSECONDS,
'handler-1'
);
assertIsAtLeastWithTolerate(
record.req.timers['handler-1-audit-sub'],
WAIT_IN_MILLISECONDS * MILLISECOND_IN_MICROSECONDS,
TOLERATED_MICROSECONDS,
'handler-0-audit-sub'
);
var handlers = Object.keys(record.req.timers);
assert.equal(
handlers[handlers.length - 2],
'handler-1-audit-sub',
'sub handler timer not in order'
);
assert.equal(
handlers[handlers.length - 1],
'handler-1',
'handler-1 not last'
);
done();
});
CLIENT.get('/audit', function(err, req, res) {
assert.ifError(err);
});
});
it('restify-GH-1435 should accumulate log handler timers', function(done) {
// capture the log record
var WAIT_IN_MILLISECONDS = 1100;
SERVER.once(
'after',
restify.plugins.auditLogger({
log: pino({ name: 'audit' }),
event: 'after'
})
);
SERVER.get('/audit', function aTestHandler(req, res, next) {
req.startHandlerTimer('audit-acc');
setTimeout(function() {
req.endHandlerTimer('audit-acc');
// Very brief timing for same name
req.startHandlerTimer('audit-acc');
req.endHandlerTimer('audit-acc');
res.send('');
return next();
}, WAIT_IN_MILLISECONDS);
// this really should be 1000 but make it 1100 so that the tests
// don't sporadically fail due to timing issues.
});
SERVER.on('after', function() {
var record = LOG_BUFFER.records && LOG_BUFFER.records[0];
// check timers
assert.ok(record, 'no log records');
assert.equal(
LOG_BUFFER.records.length,
1,
'should only have 1 log record'
);
assertIsAtLeastWithTolerate(
record.req.timers.aTestHandler,
WAIT_IN_MILLISECONDS * MILLISECOND_IN_MICROSECONDS,
TOLERATED_MICROSECONDS,
'atestHandler'
);
assertIsAtLeastWithTolerate(
record.req.timers['aTestHandler-audit-acc'],
WAIT_IN_MILLISECONDS * MILLISECOND_IN_MICROSECONDS,
TOLERATED_MICROSECONDS,
'aTestHandler-audit-acc'
);
done();
});
CLIENT.get('/audit', function(err, req, res) {
assert.ifError(err);
});
});
it('restify-GH-812 audit logger has query params string', function(done) {
SERVER.once(
'after',
restify.plugins.auditLogger({
log: pino({ name: 'audit' }),
event: 'after'
})
);
SERVER.get('/audit', function(req, res, next) {
res.send();
next();
});
SERVER.on('after', function() {
// check timers
assert.ok(LOG_BUFFER.records[0], 'no log records');
assert.equal(
LOG_BUFFER.records.length,
1,
'should only have 1 log record'
);
assert.ok(LOG_BUFFER.records[0].req.query, 'a=1&b=2');
done();
});
CLIENT.get('/audit?a=1&b=2', function(err, req, res) {
assert.ifError(err);
});
});
it('restify-GH-812 audit logger has query params obj', function(done) {
SERVER.once(
'after',
restify.plugins.auditLogger({
log: pino({ name: 'audit' }),
event: 'after'
})
);
SERVER.get('/audit', [
restify.plugins.queryParser(),
function(req, res, next) {
res.send();
next();
}
]);
SERVER.on('after', function() {
// check timers
assert.ok(LOG_BUFFER.records[0], 'no log records');
assert.equal(
LOG_BUFFER.records.length,
1,
'should only have 1 log record'
);
assert.deepEqual(LOG_BUFFER.records[0].req.query, {
a: '1',
b: '2'
});
done();
});
CLIENT.get('/audit?a=1&b=2', function(err, req, res) {
assert.ifError(err);
});
});
it('should work with pre events', function(done) {
var ptStream = new PassThrough();
SERVER.once(
'pre',
restify.plugins.auditLogger({
log: pino({ name: 'audit' }, ptStream),
event: 'pre'
})
);
SERVER.get('/audit', [
restify.plugins.queryParser(),
function(req, res, next) {
res.send();
next();
}
]);
ptStream.on('data', function(data) {
var log = JSON.parse(data);
assert.equal('pre', log.component);
assert.ok(log.req_id);
assert.ok(log.req);
assert.ok(log.res);
});
CLIENT.get('/audit?a=1&b=2', function(err, req, res) {
assert.ifError(err);
done();
});
});
it('should work with routed events', function(done) {
var ptStream = new PassThrough();
SERVER.once(
'routed',
restify.plugins.auditLogger({
log: pino({ name: 'audit' }, ptStream),
event: 'routed'
})
);
SERVER.get('/audit', [
restify.plugins.queryParser(),
function(req, res, next) {
res.send();
next();
}
]);
ptStream.on('data', function(data) {
var log = JSON.parse(data);
assert.equal('routed', log.component);
assert.ok(log.req_id);
assert.ok(log.req);
assert.ok(log.res);
});
CLIENT.get('/audit?a=1&b=2', function(err, req, res) {
assert.ifError(err);
done();
});
});
it('should work with custom context functions', function(done) {
SERVER.once(
'after',
restify.plugins.auditLogger({
log: pino({ name: 'audit' }),
context: function(req, res, route, err) {
return {
qs: req.getQuery()
};
},
server: SERVER,
event: 'after'
})
);
SERVER.once('audit', function(data) {
assert.ok(data);
assert.ok(data.req_id);
assert.isNumber(data.latency);
assert.ok(data.context);
assert.equal(data.context.qs, 'foo=bar');
done();
});
SERVER.get('/audit', [
restify.plugins.queryParser(),
function(req, res, next) {
res.send();
next();
}
]);
CLIENT.get('/audit?foo=bar', function(err, req, res) {
assert.ifError(err);
});
});
it('should log 444 for closed request', function(done) {
SERVER.once(
'after',
restify.plugins.auditLogger({
log: pino({ name: 'audit' }),
server: SERVER,
event: 'after'
})
);
SERVER.once('audit', function(data) {
assert.ok(data);
assert.ok(data.req_id);
assert.isNumber(data.latency);
assert.equal(data.res.statusCode, 444);
done();
});
SERVER.get('/audit', function(req, res, next) {
setTimeout(function() {
res.send();
next();
}, 150);
});
CLIENT.get(
{
path: '/audit',
requestTimeout: 50
},
function(err, req, res) {}
);
});
it('should set request id using supplied field name', function(done) {
SERVER.once(
'after',
restify.plugins.auditLogger({
log: pino({ name: 'audit' }),
server: SERVER,
event: 'after',
requestIdFieldName: 'traceId'
})
);
SERVER.once('audit', function(data) {
assert.ok(data);
assert.ok(data.traceId);
assert.notOk(data.req_id);
done();
});
SERVER.get('/audit', function(req, res, next) {
setTimeout(function() {
res.send();
next();
}, 150);
});
CLIENT.get(
{
path: '/audit',
requestTimeout: 50
},
function(err, req, res) {}
);
});
});
================================================
FILE: test/plugins/authorization.test.js
================================================
'use strict';
/* eslint-disable func-names */
// external requires
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
// local files
var helper = require('../lib/helper');
// local globals
var SERVER;
var CLIENT;
var PORT;
describe('authorization parser', function() {
before(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.use(restify.plugins.authorizationParser());
SERVER.get('/', function respond(req, res, next) {
res.send();
next();
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
done();
});
});
after(function(done) {
CLIENT.close();
SERVER.close(done);
});
it('should accept basic authorization', function(done) {
var authz = 'Basic ' + new Buffer('user:secret').toString('base64');
var opts = {
path: '/',
headers: {
authorization: authz
}
};
CLIENT.get(opts, function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('should reject basic authorization', function(done) {
var opts = {
path: '/',
headers: {
authorization: 'Basic '
}
};
CLIENT.get(opts, function(err, _, res) {
assert.ok(err);
assert.equal(err.name, 'InvalidHeaderError');
assert.equal(res.statusCode, 400);
done();
});
});
});
================================================
FILE: test/plugins/bodyReader.test.js
================================================
'use strict';
/* eslint-disable func-names */
// core requires
var http = require('http');
// external requires
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
// local files
var helper = require('../lib/helper');
// local globals
var SERVER;
var CLIENT;
var PORT;
describe('body reader', function() {
beforeEach(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
done();
});
});
afterEach(function(done) {
CLIENT.close();
SERVER.close(done);
});
describe('gzip content encoding', function() {
it('should parse gzip encoded content', function(done) {
SERVER.use(restify.plugins.bodyParser());
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
retry: false,
gzip: {}
});
SERVER.post('/compressed', function(req, res, next) {
assert.equal(req.body.apple, 'red');
res.send();
next();
});
CLIENT.post(
'/compressed',
{
apple: 'red'
},
function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
}
);
});
it('should not accept unsupported content encoding', function(done) {
SERVER.use(restify.plugins.bodyParser());
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
retry: false,
headers: {
'content-encoding': 'unsupported'
}
});
SERVER.post('/compressed', function(req, res, next) {
assert.equal(req.body.apple, 'red');
res.send();
next();
});
CLIENT.post(
'/compressed',
{
apple: 'red'
},
function(err, _, res) {
assert.isOk(err, 'should fail');
assert.equal(res.statusCode, 415);
assert.equal(res.headers['accept-encoding'], 'gzip');
done();
}
);
});
it('should parse unencoded content', function(done) {
SERVER.use(restify.plugins.bodyParser());
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
retry: false
});
SERVER.post('/compressed', function(req, res, next) {
assert.equal(req.body.apple, 'red');
res.send();
next();
});
CLIENT.post(
'/compressed',
{
apple: 'red'
},
function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
}
);
});
it('should handle client timeout', function(done) {
SERVER.use(restify.plugins.bodyParser());
SERVER.post('/compressed', function(req, res, next) {
res.send('ok');
next();
});
// set timeout to 100ms so test runs faster, when client stops
// sending POST data
SERVER.on('connection', function(socket) {
socket.setTimeout(100);
});
var postData = 'hello world';
var options = {
hostname: '127.0.0.1',
port: PORT,
path: '/compressed?v=1',
method: 'POST',
headers: {
'Content-Type': 'application/json',
// report postData + 1 so that request isn't sent
'Content-Length': Buffer.byteLength(postData) + 1
}
};
var req = http.request(options, function(res) {
// should never receive a response
assert.isNotOk(res);
});
SERVER.on('after', function(req2) {
if (req2.href() === '/compressed?v=2') {
assert.equal(SERVER.inflightRequests(), 0);
done();
}
});
// will get a req error after 100ms timeout
req.on('error', function(e) {
// make another request to verify in flight request is only 1
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
retry: false
});
CLIENT.post(
'/compressed?v=2',
{
apple: 'red'
},
function(err, _, res, obj) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
}
);
});
// write data to request body, but don't req.send()
req.write(postData);
});
});
it('should not add a listener for each call on same socket', done => {
SERVER.use(restify.plugins.bodyParser());
let serverReq, serverRes, serverReqSocket;
SERVER.post('/meals', function(req, res, next) {
serverReq = req;
serverRes = res;
serverReqSocket = req.socket;
res.send();
next();
});
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
retry: false,
agent: new http.Agent({ keepAlive: true })
});
CLIENT.post('/meals', { breakfast: 'pancakes' }, (err, _, res) => {
assert.ifError(err);
assert.equal(res.statusCode, 200);
const firstReqSocket = serverReqSocket;
const numReqListeners = listenerCount(serverReq);
const numResListeners = listenerCount(serverRes);
const numReqSocketListeners = listenerCount(serverReq.socket);
// Without setImmediate, the second request will not reuse the socket.
setImmediate(() => {
CLIENT.post('/meals', { lunch: 'salad' }, (err2, __, res2) => {
assert.ifError(err2);
assert.equal(res2.statusCode, 200);
assert.equal(
serverReqSocket,
firstReqSocket,
'This test should issue two requests that share the ' +
'same socket.'
);
// The number of listeners on each emitter should not have
// increased since the first request.
assert.equal(listenerCount(serverReq), numReqListeners);
assert.equal(listenerCount(serverRes), numResListeners);
assert.equal(
listenerCount(serverReq.socket),
numReqSocketListeners
);
done();
});
});
});
});
it('should call next for each successful request on same socket', done => {
let nextCallCount = 0;
SERVER.use(restify.plugins.bodyParser());
SERVER.use((req, res, next) => {
nextCallCount += 1;
next();
});
let serverReqSocket;
SERVER.post('/meals', function(req, res, next) {
res.send();
next();
});
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
retry: false,
agent: new http.Agent({ keepAlive: true })
});
CLIENT.post('/meals', { breakfast: 'waffles' }, (err, _, res) => {
assert.ifError(err);
assert.equal(res.statusCode, 200);
const firstReqSocket = serverReqSocket;
assert.equal(nextCallCount, 1);
// Without setImmediate, the second request will not reuse the socket.
setImmediate(() => {
CLIENT.post('/meals', { lunch: 'candy' }, (err2, __, res2) => {
assert.ifError(err2);
assert.equal(res2.statusCode, 200);
assert.equal(
serverReqSocket,
firstReqSocket,
'This test should issue two requests that share the ' +
'same socket.'
);
assert.equal(nextCallCount, 2);
done();
});
});
});
});
});
/**
* @param {EventEmitter} emitter - An emitter
* @returns {number} - The total number of listeners across all events
*/
function listenerCount(emitter) {
let numListeners = 0;
for (const eventName of emitter.eventNames()) {
numListeners += emitter.listenerCount(eventName);
}
return numListeners;
}
================================================
FILE: test/plugins/conditionalHandler.test.js
================================================
'use strict';
/* eslint-disable func-names */
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
var parallel = require('vasync').parallel;
// local files
var helper = require('../lib/helper');
// local globals
var SERVER;
var CLIENT;
var PORT;
function handlerFactory(response, version) {
return function handler(req, res, next) {
res.send(response);
if (version) {
assert.equal(req.matchedVersion(), version);
}
next();
};
}
describe('conditional request', function() {
describe('version', function() {
beforeEach(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
done();
});
});
afterEach(function(done) {
CLIENT.close();
SERVER.close(done);
});
it('should find handler by string version', function(done) {
SERVER.get(
'/',
restify.plugins.conditionalHandler([
{
handler: handlerFactory('v1.1.0', 'v1.1.0'),
version: 'v1.1.0'
},
{
handler: handlerFactory('v1.2.0', 'v1.2.0'),
version: 'v1.2.0'
}
])
);
parallel(
{
funcs: [
function v1(callback) {
var opts = {
path: '/',
headers: {
'accept-version': '1.1.0'
}
};
CLIENT.get(opts, function(err, _, res, response) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.equal(response, 'v1.1.0');
callback();
});
},
function v2(callback) {
var opts = {
path: '/',
headers: {
'accept-version': '1.2.0'
}
};
CLIENT.get(opts, function(err, _, res, response) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.equal(
res.headers['api-version'],
'v1.2.0'
);
assert.equal(response, 'v1.2.0');
callback();
});
}
]
},
function parallelDone(err) {
assert.ifError(err);
done();
}
);
});
it('should find handler by array of versions', function(done) {
SERVER.get(
'/',
restify.plugins.conditionalHandler([
{
handler: handlerFactory('v1.x, v2.x'),
version: ['v1.1.0', 'v2.0.0']
},
{
handler: handlerFactory('v3.x'),
version: 'v3.0.0'
}
])
);
parallel(
{
funcs: [
function v1(callback) {
var opts = {
path: '/',
headers: {
'accept-version': '2.x'
}
};
CLIENT.get(opts, function(err, _, res, response) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.equal(response, 'v1.x, v2.x');
callback();
});
},
function v2(callback) {
var opts = {
path: '/',
headers: {
'accept-version': '3.x'
}
};
CLIENT.get(opts, function(err, _, res, response) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.equal(response, 'v3.x');
callback();
});
}
]
},
function parallelDone(err) {
assert.ifError(err);
done();
}
);
});
it('should find handler with 1.x', function(done) {
SERVER.get(
'/',
restify.plugins.conditionalHandler([
{
handler: handlerFactory('v1.1.0'),
version: 'v1.1.0'
},
{
handler: handlerFactory('v1.2.0'),
version: 'v1.2.0'
}
])
);
var opts = {
path: '/',
headers: {
'accept-version': '1.x'
}
};
CLIENT.get(opts, function(err, _, res, response) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.equal(response, 'v1.2.0');
done();
});
});
it('should find handler with ~1.1.0', function(done) {
SERVER.get(
'/',
restify.plugins.conditionalHandler([
{
handler: handlerFactory('v1.1.1'),
version: 'v1.1.1'
},
{
handler: handlerFactory('v1.2.0'),
version: 'v1.2.0'
}
])
);
var opts = {
path: '/',
headers: {
'accept-version': '~1.1.0'
}
};
CLIENT.get(opts, function(err, _, res, response) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.equal(response, 'v1.1.1');
done();
});
});
it('should find handler with ^1.1.0', function(done) {
SERVER.get(
'/',
restify.plugins.conditionalHandler([
{
handler: handlerFactory('v1.1.1'),
version: 'v1.1.1'
},
{
handler: handlerFactory('v1.2.0'),
version: 'v1.2.0'
}
])
);
var opts = {
path: '/',
headers: {
'accept-version': '^1.1.0'
}
};
CLIENT.get(opts, function(err, _, res, response) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.equal(response, 'v1.2.0');
done();
});
});
it('should find largest version with missing header', function(done) {
SERVER.get(
'/',
restify.plugins.conditionalHandler([
{
handler: handlerFactory('v1.1.0'),
version: 'v1.1.0'
},
{
handler: handlerFactory('v1.2.0'),
version: 'v1.2.0'
}
])
);
var opts = {
path: '/',
headers: {}
};
CLIENT.get(opts, function(err, _, res, response) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.equal(response, 'v1.2.0');
done();
});
});
it('should throw invalid version error', function(done) {
SERVER.get(
'/',
restify.plugins.conditionalHandler([
{
handler: handlerFactory('v1.1.0'),
version: 'v1.1.0'
},
{
handler: handlerFactory('v1.2.0'),
version: 'v1.2.0'
}
])
);
var opts = {
path: '/',
headers: {
'accept-version': '1.3.0'
}
};
CLIENT.get(opts, function(err, _, res, response) {
assert.equal(err.name, 'InvalidVersionError');
assert.equal(err.message, '1.3.0 is not supported by GET /');
assert.equal(res.statusCode, 400);
done();
});
});
});
describe('content type', function() {
beforeEach(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createStringClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
done();
});
});
afterEach(function(done) {
CLIENT.close();
SERVER.close(done);
});
it('should find handler by content type by string', function(done) {
SERVER.get(
'/',
restify.plugins.conditionalHandler([
{
handler: handlerFactory('application/json'),
contentType: 'application/json'
},
{
handler: handlerFactory('text/plain'),
contentType: 'text/plain'
}
])
);
parallel(
{
funcs: [
function v1(callback) {
var opts = {
path: '/',
headers: {
accept: 'application/json'
}
};
CLIENT.get(opts, function(err, _, res, response) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.equal(response, '"application/json"');
callback();
});
},
function v2(callback) {
var opts = {
path: '/',
headers: {
accept: 'text/plain'
}
};
CLIENT.get(opts, function(err, _, res, response) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.equal(response, 'text/plain');
callback();
});
}
]
},
function parallelDone(err) {
assert.ifError(err);
done();
}
);
});
it('should find handler by array of content types', function(done) {
SERVER.get(
'/',
restify.plugins.conditionalHandler([
{
handler: handlerFactory('application/*'),
contentType: [
'application/json',
'application/javascript'
]
},
{
handler: handlerFactory('text/plain'),
contentType: 'text/plain'
}
])
);
var opts = {
path: '/',
headers: {
accept: 'application/javascript'
}
};
CLIENT.get(opts, function(err, _, res, response) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.equal(response, '"application/*"');
done();
});
});
it('should find handler with multiple accept', function(done) {
SERVER.get(
'/',
restify.plugins.conditionalHandler([
{
handler: handlerFactory('application/*'),
contentType: 'application/json'
},
{
handler: handlerFactory('text/plain'),
contentType: 'text/plain'
}
])
);
var opts = {
path: '/',
headers: {
accept: 'text/html,text/plain'
}
};
CLIENT.get(opts, function(err, _, res, response) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.equal(response, 'text/plain');
done();
});
});
it('should find handler with application/*', function(done) {
SERVER.get(
'/',
restify.plugins.conditionalHandler([
{
handler: handlerFactory('application/*'),
contentType: 'application/json'
},
{
handler: handlerFactory('text/plain'),
contentType: 'text/plain'
}
])
);
var opts = {
path: '/',
headers: {
accept: 'application/json'
}
};
CLIENT.get(opts, function(err, _, res, response) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.equal(response, '"application/*"');
done();
});
});
it('should find handler with content type and version', function(done) {
SERVER.get(
'/',
restify.plugins.conditionalHandler([
{
handler: handlerFactory('application/json, 1.1.0'),
contentType: 'application/json',
version: '1.1.0'
},
{
handler: handlerFactory('application/json, 1.2.0'),
contentType: 'application/json',
version: '1.2.0'
},
{
handler: handlerFactory('text/plain'),
contentType: 'text/plain'
}
])
);
var opts = {
path: '/',
headers: {
accept: 'application/json',
'accept-version': '1.2.0'
}
};
CLIENT.get(opts, function(err, _, res, response) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.equal(response, '"application/json, 1.2.0"');
done();
});
});
it('should throw invalid media type error', function(done) {
SERVER.get(
'/',
restify.plugins.conditionalHandler([
{
handler: handlerFactory('application/json'),
contentType: 'application/json'
},
{
handler: handlerFactory('text/plain'),
contentType: 'text/plain'
}
])
);
var opts = {
path: '/',
headers: {
accept: 'text/html'
}
};
CLIENT.get(opts, function(err, _, res, response) {
assert.equal(err.name, 'UnsupportedMediaTypeError');
assert.equal(
err.message,
'{"code":"UnsupportedMediaType","message":"text/html"}'
);
assert.equal(res.statusCode, 415);
done();
});
});
});
describe('multiple handlers', function() {
beforeEach(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
done();
});
});
afterEach(function(done) {
CLIENT.close();
SERVER.close(done);
});
it('should run each of the handlers', function(done) {
var counter = 0;
SERVER.get(
'/',
restify.plugins.conditionalHandler([
{
handler: [
function handler1(req, res, next) {
counter += 1;
next();
},
function handler2(req, res, next) {
counter += 1;
next();
},
function handler3(req, res, next) {
counter += 1;
res.send('v1.2.0');
}
],
version: 'v1.2.0'
}
])
);
var opts = {
path: '/',
headers: {
'accept-version': '1.2.0'
}
};
CLIENT.get(opts, function(err, _, res, response) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.equal(counter, 3, 'calls all of the handlers');
assert.equal(response, 'v1.2.0');
done();
});
});
});
});
================================================
FILE: test/plugins/conditionalRequest.test.js
================================================
'use strict';
/* eslint-disable func-names */
// external requires
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
// local files
var helper = require('../lib/helper');
// local globals
var SERVER;
var CLIENT;
var PORT;
describe('conditional request', function() {
beforeEach(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
done();
});
});
afterEach(function(done) {
CLIENT.close();
SERVER.close(done);
});
it('Correct Etag and headers', function(done) {
SERVER.get(
'/etag/:id',
function(req, res, next) {
res.etag = 'testETag';
next();
},
restify.plugins.conditionalRequest(),
function(req, res, next) {
res.body = 'testing 304';
res.send();
next();
}
);
var opts = {
path: '/etag/foo',
headers: {
'If-Match': 'testETag',
'If-None-Match': 'testETag'
}
};
CLIENT.get(opts, function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 304);
done();
});
});
it('mismatched Etag and If-Match', function(done) {
SERVER.get(
'/etag/:id',
function setEtag(req, res, next) {
res.etag = 'testEtag';
next();
},
restify.plugins.conditionalRequest(),
function respond(req, res, next) {
res.send();
next();
}
);
var opts = {
path: '/etag/foo',
headers: {
'If-Match': 'testETag2'
}
};
CLIENT.get(opts, function(err, _, res) {
assert.ok(err);
assert.equal(res.statusCode, 412);
done();
});
});
it('If-Modified header & !modified content', function(done) {
var now = new Date();
var yesterday = new Date(now.setDate(now.getDate() - 1));
SERVER.get(
'/etag/:id',
function(req, res, next) {
res.header('Last-Modified', yesterday);
next();
},
restify.plugins.conditionalRequest(),
function(req, res, next) {
res.send('testing 304');
next();
}
);
var opts = {
path: '/etag/foo',
headers: {
'If-Modified-Since': new Date()
}
};
CLIENT.get(opts, function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 304);
done();
});
});
it('If-Unmodified-Since header,modified content', function(done) {
var now = new Date();
var yesterday = new Date(now.setDate(now.getDate() - 1));
SERVER.get(
'/etag/:id',
function(req, res, next) {
res.header('Last-Modified', new Date());
next();
},
restify.plugins.conditionalRequest(),
function(req, res, next) {
res.send('testing 412');
next();
}
);
var opts = {
path: '/etag/foo',
headers: {
'If-Unmodified-Since': yesterday
}
};
CLIENT.get(opts, function(err, _, res) {
assert.ok(err);
assert.equal(res.statusCode, 412);
done();
});
});
it('valid headers, ahead time, unmodified OK', function(done) {
var now = new Date();
var ahead = new Date(now.getTime() + 1000);
SERVER.get(
'/etag/:id',
function(req, res, next) {
res.header('Last-Modified', now);
next();
},
restify.plugins.conditionalRequest(),
function(req, res, next) {
res.send();
next();
}
);
var opts = {
path: '/etag/foo',
headers: {
'If-Modified-Since': ahead
}
};
CLIENT.get(opts, function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 304);
done();
});
});
it('valid headers, ahead Timezone, modified content', function(done) {
var now = new Date();
var ahead = new Date(now.setHours(now.getHours() + 5));
SERVER.get(
'/etag/:id',
function(req, res, next) {
res.header('Last-Modified', now);
next();
},
restify.plugins.conditionalRequest(),
function(req, res, next) {
res.send();
next();
}
);
var opts = {
path: '/etag/foo',
headers: {
'If-Unmodified-Since': ahead
}
};
CLIENT.get(opts, function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('PUT with matched Etag and headers', function(done) {
SERVER.put(
'/etag/:id',
function(req, res, next) {
res.etag = 'testETag';
next();
},
restify.plugins.conditionalRequest(),
function(req, res, next) {
res.send();
next();
}
);
var opts = {
path: '/etag/foo',
headers: {
'If-Match': 'testETag',
'If-None-Match': 'testETag'
}
};
CLIENT.put(opts, {}, function(err, _, res) {
assert.ok(err);
assert.equal(res.statusCode, 412);
done();
});
});
});
================================================
FILE: test/plugins/context.test.js
================================================
'use strict';
/* eslint-disable func-names */
// external requires
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
// local files
var helper = require('../lib/helper');
// local globals
var SERVER;
var CLIENT;
var PORT;
describe('accept parser', function() {
before(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.use(restify.plugins.pre.context());
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
done();
});
});
after(function(done) {
CLIENT.close();
SERVER.close(done);
});
it('should use context', function(done) {
SERVER.get('/', [
function one(req, res, next) {
req.set('foo', {
a: 1
});
return next();
},
function two(req, res, next) {
assert.deepEqual(req.get('foo'), {
a: 1
});
req.get('foo').b = 2;
req.set('bar', [1]);
return next();
},
function three(req, res, next) {
assert.deepEqual(req.get('foo'), {
a: 1,
b: 2
});
assert.deepEqual(req.get('bar'), [1]);
assert.deepEqual(req.getAll(), {
foo: {
a: 1,
b: 2
},
bar: [1]
});
res.send();
return next();
}
]);
CLIENT.get('/', function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
return done();
});
});
it('should not share context', function(done) {
SERVER.get('/1', function one(req, res, next) {
// ensure we don't get context from previous request
assert.equal(req.get('foo', null));
res.end();
return next();
});
CLIENT.get('/1', function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
return done();
});
});
});
================================================
FILE: test/plugins/cpuUsageThrottle.test.js
================================================
'use strict';
/* eslint-disable func-names */
var assert = require('chai').assert;
var proxyquire = require('proxyquire');
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
var pidusage = require('pidusage');
// Allow tests to set the CPU usage returned by pidUsage
var CPU = 50;
var cpuUsageThrottle = proxyquire('../../lib/plugins/cpuUsageThrottle.js', {
pidusage: function(pid, cb) {
return cb(null, { cpu: CPU });
}
});
var MR = Math.random;
describe('cpuUsageThrottle', function() {
var plugin;
before('Setup: stub math.random', function(done) {
Math.random = function() {
return 0;
};
done();
});
it('Unit: Should shed load', function(done) {
var opts = { limit: 0, interval: 500 };
plugin = cpuUsageThrottle(opts);
function next(cont) {
assert(cont instanceof Error, 'Should call next with error');
assert.equal(cont.statusCode, 503, 'Defaults to 503 status');
done();
}
plugin({}, {}, next);
});
it('Unit: Should let request through when not under load', function(done) {
var opts = { interval: 500, limit: 0.9 };
plugin = cpuUsageThrottle(opts);
function next(cont) {
assert.isUndefined(cont, 'Should call next');
done();
}
plugin({}, {}, next);
});
it('Unit: Update should update state', function(done) {
var opts = {
max: 1,
limit: 0.9,
halfLife: 50,
interval: 50
};
plugin = cpuUsageThrottle(opts);
opts = {
max: 0.5,
limit: 0.1,
halfLife: 1000,
interval: 1000
};
plugin.update(opts);
assert.equal(plugin.state.limit, opts.limit, 'opts.limit');
assert.equal(plugin.state.max, opts.max, 'opts.max');
assert.equal(plugin.state.halfLife, opts.halfLife, 'opts.halfLife');
assert.equal(plugin.state.interval, opts.interval, 'opts.interval');
done();
});
it('Unit: Should have proper name', function(done) {
var opts = {
max: 1,
limit: 0.9,
halfLife: 50,
interval: 50
};
plugin = cpuUsageThrottle(opts);
assert.equal(plugin.name, 'cpuUsageThrottle');
done();
});
it('Unit: Should report proper lag', function(done) {
var opts = { max: 1, limit: 0.9, halfLife: 50, interval: 50 };
var dn = Date.now;
var now = 0;
// First timer will be 0, all future timers will be interval
Date.now = function() {
return (now++ > 0) * opts.interval;
};
plugin = cpuUsageThrottle(opts);
Date.now = dn;
assert.equal(plugin.state.lag, 0);
done();
});
it('Integration: Should shed load', function(done) {
var server = restify.createServer();
var client = {
close: function() {}
};
var opts = { interval: 500, limit: 0 };
plugin = cpuUsageThrottle(opts);
server.pre(plugin);
server.get('/foo', function(req, res, next) {
res.send(200);
next();
});
server.listen(0, '127.0.0.1', function() {
client = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + server.address().port,
retry: false
});
client.get({ path: '/foo' }, function(e, _, res) {
assert(e, 'Second request is shed');
assert.equal(
res.statusCode,
503,
'Default shed status code returned'
);
clearTimeout(plugin._timeout);
// we should close the server else mocha wont exit
server.close();
done();
});
});
});
it('Integration: pidusage should report CPU usage', function(done) {
assert.isFunction(pidusage, 'pidusage can be invoked');
pidusage(process.pid, function(e, stat) {
assert.ifError(e);
assert.isObject(stat);
assert.isNumber(stat.cpu);
pidusage.clear();
done();
});
});
afterEach(function(done) {
if (plugin) {
plugin.close();
}
plugin = undefined;
done();
});
after('Teardown: Reset Math.random', function(done) {
Math.random = MR;
done();
});
});
================================================
FILE: test/plugins/dedupeSlashes.test.js
================================================
'use strict';
/* eslint-disable func-names */
// external requires
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
// local files
var helper = require('../lib/helper');
// local globals
var SERVER;
var CLIENT;
var PORT;
describe('dedupe forward slashes in URL', function() {
before(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.pre(restify.plugins.pre.dedupeSlashes());
SERVER.get('/foo/bar/', function respond(req, res, next) {
res.send(req.url);
next();
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
done();
});
});
after(function(done) {
CLIENT.close();
SERVER.close(done);
});
it('should not remove single slashes', function(done) {
CLIENT.get('/foo/bar/', function(err, _, res, data) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.equal(data, '/foo/bar/');
done();
});
});
it('should remove duplicate slashes', function(done) {
CLIENT.get('//////foo///bar///////', function(err, _, res, data) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.equal(data, '/foo/bar/');
done();
});
});
// eslint-disable-next-line
it('should remove duplicate slashes including trailing slashes', function(done) {
CLIENT.get('//foo//bar//', function(err, _, res, data) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.equal(data, '/foo/bar/');
done();
});
});
});
================================================
FILE: test/plugins/fieldedTextParser.test.js
================================================
'use strict';
/* eslint-disable func-names */
// core requires
var fs = require('fs');
var path = require('path');
// external requires
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
// local files
var helper = require('../lib/helper');
var fsOptions = { encoding: 'utf8' };
var PORT = process.env.UNIT_TEST_PORT || 3333;
var CLIENT;
var SERVER;
var DATA_CSV = fs.readFileSync(
path.join(__dirname, '/files/data-csv.txt'),
fsOptions
);
var DATA_TSV = fs.readFileSync(
path.join(__dirname, '/files/data-tsv.txt'),
fsOptions
);
var OBJECT_CSV = require(path.join(__dirname, '/files/object-csv.json'));
var OBJECT_TSV = require(path.join(__dirname, '/files/object-tsv.json'));
/**
* Tests
*/
describe('fielded text parser', function() {
beforeEach(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.use(restify.plugins.bodyParser());
SERVER.listen(PORT, '127.0.0.1', function() {
CLIENT = restifyClients.createClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
done();
});
});
afterEach(function(done) {
CLIENT.close();
SERVER.close(done);
});
it('should parse CSV body', function(done) {
var options = {
path: '/data',
headers: {
'Content-Type': 'text/csv'
}
};
SERVER.post('/data', function respond(req, res, next) {
res.send({
status: 'okay',
parsedReq: req.body
});
return next();
});
CLIENT.post(options, function(err, req) {
assert.ifError(err);
req.on('result', function(errReq, res) {
assert.ifError(errReq);
res.body = '';
res.setEncoding('utf8');
res.on('data', function(chunk) {
res.body += chunk;
});
res.on('end', function() {
res.body = JSON.parse(res.body);
var parsedReqStr = JSON.stringify(res.body.parsedReq);
var objectStr = JSON.stringify(OBJECT_CSV);
assert.equal(parsedReqStr, objectStr);
done();
});
});
req.write(DATA_CSV);
req.end();
});
});
// eslint-disable-next-line
it('#100 should parse CSV body even if bodyparser declared twice', function(done) {
SERVER.use(restify.plugins.bodyParser());
var options = {
path: '/data',
headers: {
'Content-Type': 'text/csv'
}
};
SERVER.post('/data', function respond(req, res, next) {
res.send({
status: 'okay',
parsedReq: req.body
});
return next();
});
CLIENT.post(options, function(err, req) {
assert.ifError(err);
req.on('result', function(errReq, res) {
assert.ifError(errReq);
res.body = '';
res.setEncoding('utf8');
res.on('data', function(chunk) {
res.body += chunk;
});
res.on('end', function() {
res.body = JSON.parse(res.body);
var parsedReqStr = JSON.stringify(res.body.parsedReq);
var objectStr = JSON.stringify(OBJECT_CSV);
assert.equal(parsedReqStr, objectStr);
done();
});
});
req.write(DATA_CSV);
req.end();
});
});
it('should parse TSV body', function(done) {
var options = {
path: '/data',
headers: {
'Content-Type': 'text/tsv'
}
};
SERVER.post('/data', function respond(req, res, next) {
res.send({
status: 'okay',
parsedReq: req.body
});
return next();
});
CLIENT.post(options, function(err, req) {
assert.ifError(err);
req.on('result', function(errReq, res) {
assert.ifError(errReq);
res.body = '';
res.setEncoding('utf8');
res.on('data', function(chunk) {
res.body += chunk;
});
res.on('end', function() {
res.body = JSON.parse(res.body);
var parsedReqStr = JSON.stringify(res.body.parsedReq);
var objectStr = JSON.stringify(OBJECT_TSV);
assert.equal(parsedReqStr, objectStr);
done();
});
});
req.write(DATA_TSV);
req.end();
});
});
it('plugins-GH-6: should expose rawBody on request', function(done) {
var options = {
path: '/data',
headers: {
'Content-Type': 'text/csv'
}
};
SERVER.post('/data', function respond(req, res, next) {
assert.ok(req.rawBody);
res.send();
return next();
});
CLIENT.post(options, function(err, req) {
assert.ifError(err);
req.on('result', function(errReq, res) {
assert.ifError(errReq);
res.body = '';
res.setEncoding('utf8');
res.on('data', function(chunk) {
res.body += chunk;
});
res.on('end', done);
});
req.write(DATA_TSV);
req.end();
});
});
});
================================================
FILE: test/plugins/files/data-csv.txt
================================================
field1,field2,field3
1,2,3
3,2,1
"a","b","c"
"\"c","b","a"
================================================
FILE: test/plugins/files/data-tsv.txt
================================================
field1 field2 field3
1 2 3
3 2 1
================================================
FILE: test/plugins/files/object-csv.json
================================================
[
{ "field1": "1", "field2": "2", "field3": "3", "index": 0 },
{ "field1": "3", "field2": "2", "field3": "1", "index": 1 },
{ "field1": "a", "field2": "b", "field3": "c", "index": 2 },
{ "field1": "\"c", "field2": "b", "field3": "a", "index": 3 }
]
================================================
FILE: test/plugins/files/object-tsv.json
================================================
[
{ "field1": "1", "field2": "2", "field3": "3", "index": 0 },
{ "field1": "3", "field2": "2", "field3": "1", "index": 1 }
]
================================================
FILE: test/plugins/formBodyParser.test.js
================================================
'use strict';
/* eslint-disable func-names */
// core requires
var http = require('http');
// external requires
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
// local files
var helper = require('../lib/helper');
// local globals
var SERVER;
var PORT;
describe('form body parser', function() {
beforeEach(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
done();
});
});
afterEach(function(done) {
SERVER.close(done);
});
it('should parse req.body, req.query, req.params', function(done) {
SERVER.use(restify.plugins.queryParser());
SERVER.use(restify.plugins.bodyParser());
SERVER.post('/bodyurl2/:id', function(req, res, next) {
assert.equal(req.query.name, 'markc');
assert.equal(req.params.id, 'foo');
assert.equal(req.body.name, 'somethingelse');
assert.equal(req.body.phone, '(206) 555-1212');
res.send();
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/bodyurl2/foo?name=markc',
agent: false,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
};
var client = http.request(opts, function(res) {
assert.equal(res.statusCode, 200);
done();
});
client.write('phone=(206)%20555-1212&name=somethingelse');
client.end();
});
it('should map req.body & req.query onto req.params', function(done) {
SERVER.use(
restify.plugins.queryParser({
mapParams: true
})
);
SERVER.use(
restify.plugins.bodyParser({
mapParams: true
})
);
SERVER.post('/bodyurl2/:id', function(req, res, next) {
assert.equal(req.query.name, 'markc');
assert.equal(req.body.phone, '(206) 555-1212');
assert.equal(req.body.name, 'somethingelse');
assert.equal(req.params.id, 'foo');
assert.equal(req.params.name, 'markc');
assert.equal(req.params.phone, '(206) 555-1212');
res.send();
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/bodyurl2/foo?name=markc',
agent: false,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
};
var client = http.request(opts, function(res) {
assert.equal(res.statusCode, 200);
done();
});
client.write('phone=(206)%20555-1212&name=somethingelse');
client.end();
});
it('should take req.body and stomp on req.params', function(done) {
SERVER.use(
restify.plugins.queryParser({
mapParams: true
})
);
SERVER.use(
restify.plugins.bodyParser({
mapParams: true,
overrideParams: true
})
);
SERVER.post('/bodyurl2/:id', function(req, res, next) {
assert.equal(req.query.name, 'markc');
assert.equal(req.body.phone, '(206) 555-1212');
assert.equal(req.body.name, 'somethingelse');
assert.equal(req.params.id, 'foo');
assert.equal(req.params.name, 'somethingelse');
assert.equal(req.params.phone, '(206) 555-1212');
res.send();
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/bodyurl2/foo?name=markc',
agent: false,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
};
var client = http.request(opts, function(res) {
assert.equal(res.statusCode, 200);
done();
});
client.write('phone=(206)%20555-1212&name=somethingelse');
client.end();
});
it('should parse associative array syntax', function(done) {
SERVER.use(restify.plugins.bodyParser());
SERVER.post('/bodyurl2/:id', function(req, res, next) {
assert.isObject(req.body.name);
assert.equal(req.body.name.first, 'alex');
assert.equal(req.body.name.last, 'liu');
res.send();
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/bodyurl2/foo',
agent: false,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
};
var client = http.request(opts, function(res) {
assert.equal(res.statusCode, 200);
done();
});
client.write('name[first]=alex&name[last]=liu');
client.end();
});
it('should parse array syntax', function(done) {
SERVER.use(restify.plugins.bodyParser());
SERVER.post('/bodyurl2/:id', function(req, res, next) {
assert.isArray(req.body.meat);
assert.deepEqual(req.body.meat, ['ham', 'bacon']);
res.send();
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/bodyurl2/foo',
agent: false,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
};
var client = http.request(opts, function(res) {
assert.equal(res.statusCode, 200);
done();
});
client.write('meat[]=ham&meat[]=bacon');
client.end();
});
it('should parse nested array syntax', function(done) {
SERVER.use(restify.plugins.bodyParser());
SERVER.post('/bodyurl2/:id', function(req, res, next) {
assert.isObject(req.body.pizza);
assert.isArray(req.body.pizza.left);
assert.isArray(req.body.pizza.right);
assert.deepEqual(req.body.pizza.left, ['ham', 'bacon']);
assert.deepEqual(req.body.pizza.right, ['pineapple']);
res.send();
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/bodyurl2/foo',
agent: false,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
};
var client = http.request(opts, function(res) {
assert.equal(res.statusCode, 200);
done();
});
var p =
'pizza[left][]=ham&pizza[left][]=bacon&' +
'pizza[right][]=pineapple';
client.write(p);
client.end();
});
it('plugins-GH-6: should expose rawBody', function(done) {
var input = 'name[first]=alex&name[last]=liu';
SERVER.use(restify.plugins.bodyParser());
SERVER.post('/bodyurl2/:id', function(req, res, next) {
assert.equal(req.rawBody, input);
res.send();
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/bodyurl2/foo',
agent: false,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
};
var client = http.request(opts, function(res) {
assert.equal(res.statusCode, 200);
done();
});
client.write(input);
client.end();
});
});
================================================
FILE: test/plugins/gzip.test.js
================================================
'use strict';
/* eslint-disable func-names */
// external requires
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
// local files
var helper = require('../lib/helper');
// local globals
var SERVER;
var CLIENT;
var PORT;
describe('gzip parser', function() {
beforeEach(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
done();
});
});
afterEach(function(done) {
CLIENT.close();
SERVER.close(done);
});
it('should gzip response', function(done) {
SERVER.use(restify.plugins.gzipResponse());
SERVER.get('/gzip/:id', function(req, res, next) {
res.send({
hello: 'world'
});
next();
});
var opts = {
path: '/gzip/foo',
headers: {
'Accept-Encoding': 'gzip'
}
};
CLIENT.get(opts, function(err, _, res, obj) {
assert.ifError(err);
assert.deepEqual({ hello: 'world' }, obj);
done();
});
});
it('gzip large response', function(done) {
var testResponseSize = 65536 * 3;
var TestStream = function() {
this.readable = true;
this.sentSize = 0;
this.totalSize = testResponseSize;
this.interval = null;
};
require('util').inherits(TestStream, require('stream'));
TestStream.prototype.resume = function() {
var self = this;
if (!this.interval) {
this.interval = setInterval(function() {
var chunkSize = Math.min(
self.totalSize - self.sentSize,
65536
);
if (chunkSize > 0) {
var chunk = new Array(chunkSize + 1);
chunk = chunk.join('a');
self.emit('data', chunk);
self.sentSize += chunkSize;
} else {
self.emit('data', '"}');
self.emit('end');
self.pause();
}
}, 1);
}
};
TestStream.prototype.pause = function() {
clearInterval(this.interval);
this.interval = null;
};
var bodyStream = new TestStream();
SERVER.use(restify.plugins.gzipResponse());
SERVER.get('/gzip/:id', function(req, res, next) {
bodyStream.resume();
res.write('{"foo":"');
bodyStream.pipe(res);
bodyStream.on('end', function() {
next();
});
});
var opts = {
path: '/gzip/foo',
headers: {
'Accept-Encoding': 'gzip'
}
};
CLIENT.get(opts, function(err, _, res, obj) {
assert.ifError(err);
var expectedResponse = {
foo: new Array(testResponseSize + 1).join('a')
};
assert.deepEqual(expectedResponse, obj);
done();
});
});
it('gzip body json ok', function(done) {
SERVER.use(restify.plugins.gzipResponse());
SERVER.use(
restify.plugins.queryParser({
mapParams: true
})
);
SERVER.use(
restify.plugins.bodyParser({
mapParams: true
})
);
SERVER.post('/body/:id', function(req, res, next) {
assert.equal(req.params.id, 'foo');
assert.equal(req.params.name, 'markc');
assert.equal(req.params.phone, '(206) 555-1212');
res.send();
next();
});
var obj = {
phone: '(206) 555-1212',
name: 'somethingelse'
};
CLIENT.gzip = {};
CLIENT.post('/body/foo?name=markc', obj, function(err, _, res) {
assert.ifError(err);
assert.ok(res);
if (res) {
assert.equal(res.statusCode, 200);
}
done();
});
});
});
================================================
FILE: test/plugins/inflightRequestThrottle.test.js
================================================
'use strict';
/* eslint-disable func-names */
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
var inflightRequestThrottle = restify.plugins.inflightRequestThrottle;
function fakeServer(count) {
return {
inflightRequests: function() {
return count;
}
};
}
describe('inlfightRequestThrottle', function() {
it('Unit: Should shed load', function(done) {
var logged = false;
var opts = { server: fakeServer(10), limit: 1 };
var plugin = inflightRequestThrottle(opts);
function send(body) {
assert(logged, 'Should have emitted a log');
assert.equal(body.statusCode, 503, 'Defaults to 503 status');
assert(body instanceof Error, 'Defaults to error body');
done();
}
function next(err) {
assert.equal(err.name, 'ServiceUnavailableError');
done();
}
function trace() {
logged = true;
}
var log = { trace: trace };
var fakeReq = { log: log };
plugin(fakeReq, { send: send }, next);
});
it('Unit: Should support custom response', function(done) {
var server = fakeServer(10);
var err = new Error('foo');
var opts = { server: server, limit: 1, err: err };
var plugin = inflightRequestThrottle(opts);
function send(body) {
assert.equal(body, err, 'Overrides body');
}
function next(nextErr) {
assert.equal(err, nextErr);
done();
}
var fakeReq = { log: { trace: function() {} } };
plugin(fakeReq, { send: send }, next);
});
it('Unit: Should let request through when not under load', function(done) {
var opts = { server: fakeServer(1), limit: 2 };
var plugin = inflightRequestThrottle(opts);
function send() {
assert(false, 'Should not call send');
}
function next(cont) {
assert.isUndefined(cont, 'Should call next');
done();
}
var fakeReq = { log: { trace: function() {} } };
plugin(fakeReq, { send: send }, next);
});
it('Integration: Should shed load', function(done) {
var server = restify.createServer();
var client = {
close: function() {}
};
var isDone = false;
var to;
function finish() {
if (isDone) {
return null;
}
clearTimeout(to);
isDone = true;
client.close();
server.close();
return done();
}
to = setTimeout(finish, 2000);
var err = new Error('foo');
err.statusCode = 555;
var opts = { server: server, limit: 1, err: err };
server.pre(inflightRequestThrottle(opts));
var RES;
server.get('/foo', function(req, res, next) {
if (RES) {
res.send(999);
} else {
RES = res;
}
});
server.listen(0, '127.0.0.1', function() {
client = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + server.address().port,
retry: false
});
client.get({ path: '/foo' }, function(e, _, res) {
assert(
e === null || e === undefined,
'First request isnt shed'
);
assert.equal(res.statusCode, 200, '200 returned on success');
finish();
});
client.get({ path: '/foo' }, function(e, _, res) {
assert(e, 'Second request is shed');
assert.equal(
e.name,
'InternalServerError',
'Default err returned'
);
assert.equal(
res.statusCode,
555,
'Default shed status code returned'
);
if (RES) {
RES.send(200);
}
});
});
});
});
================================================
FILE: test/plugins/jsonBodyParser.test.js
================================================
'use strict';
/* eslint-disable func-names */
// core requires
var net = require('net');
var http = require('http');
// external requires
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
// local files
var helper = require('../lib/helper');
// local globals
var SERVER;
var CLIENT;
var STRING_CLIENT;
var PORT;
describe('JSON body parser', function() {
beforeEach(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
STRING_CLIENT = restifyClients.createStringClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false,
agent: false,
contentType: 'application/json',
accept: 'application/json'
});
done();
});
});
afterEach(function(done) {
CLIENT.close();
STRING_CLIENT.close();
SERVER.close(done);
});
it('should parse null JSON body', function(done) {
SERVER.use(
restify.plugins.jsonBodyParser({
mapParams: true
})
);
SERVER.post('/body/:id', function(req, res, next) {
assert.equal(req.params.id, 'foo');
assert.equal(req.body, null);
res.send();
next();
});
STRING_CLIENT.post('/body/foo?name=markc', 'null', function(
err,
_,
res
) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('should parse empty JSON body', function(done) {
SERVER.use(restify.plugins.jsonBodyParser());
SERVER.post('/body/:id', function(req, res, next) {
assert.equal(req.params.id, 'foo');
assert.deepEqual(req.body, {});
res.send();
next();
});
CLIENT.post('/body/foo', null, function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('should parse req.body and req.params independently', function(done) {
SERVER.use(restify.plugins.jsonBodyParser());
SERVER.post('/body/:id', function(req, res, next) {
assert.equal(req.params.id, 'foo');
assert.equal(req.body.id, 'bar');
assert.equal(req.body.name, 'alex');
assert.notDeepEqual(req.body, req.params);
res.send();
next();
});
CLIENT.post(
'/body/foo',
{
id: 'bar',
name: 'alex'
},
function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
}
);
});
it('should fail to map array req.body onto req.params', function(done) {
SERVER.use(
restify.plugins.jsonBodyParser({
mapParams: true
})
);
SERVER.post('/body/:id', function(req, res, next) {
// this handler should never be reached
res.send();
next();
});
CLIENT.post('/body/foo', [1, 2, 3], function(err, _, res) {
assert.ok(err);
assert.equal(err.name, 'InternalServerError');
assert.equal(res.statusCode, 500);
done();
});
});
// TODO: router param mapping runs later
it('should map req.body onto req.params', function(done) {
SERVER.use(
restify.plugins.jsonBodyParser({
mapParams: true
})
);
SERVER.post('/body/:id', function(req, res, next) {
assert.equal(req.params.id, 'foo');
assert.equal(req.params.name, 'alex');
assert.notDeepEqual(req.body, req.params);
res.send();
next();
});
CLIENT.post(
'/body/foo',
{
id: 'bar',
name: 'alex'
},
function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
}
);
});
it('should take req.body and stomp on req.params', function(done) {
SERVER.use(
restify.plugins.jsonBodyParser({
mapParams: true,
overrideParams: true
})
);
SERVER.post('/body/:id', function(req, res, next) {
assert.equal(req.params.id, 'bar');
assert.equal(req.params.name, 'alex');
assert.deepEqual(req.body, req.params);
res.send();
next();
});
CLIENT.post(
'/body/foo',
{
id: 'bar',
name: 'alex'
},
function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
}
);
});
it('should parse JSON body with reviver', function(done) {
SERVER.use(
restify.plugins.jsonBodyParser({
reviver: function reviver(key, value) {
if (key === '') {
return value;
}
return value + value;
}
})
);
SERVER.post('/body/:id', function(req, res, next) {
assert.equal(req.params.id, 'foo');
assert.equal(req.body.apple, 'redred');
assert.equal(req.body.orange, 'orangeorange');
assert.equal(req.body.banana, 'yellowyellow');
res.send();
next();
});
CLIENT.post(
'/body/foo',
{
apple: 'red',
orange: 'orange',
banana: 'yellow'
},
function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
}
);
});
it('restify-GH-318 get request with body (default)', function(done) {
SERVER.use(
restify.plugins.bodyParser({
mapParams: true
})
);
SERVER.get('/getWithoutBody', function(req, res, next) {
assert.notEqual(req.params.foo, 'bar');
res.send();
next();
});
var request =
'GET /getWithoutBody HTTP/1.1\r\n' +
'Content-Type: application/json\r\n' +
'Content-Length: 13\r\n' +
'\r\n' +
'{"foo":"bar"}';
var client = net.connect({ host: '127.0.0.1', port: PORT }, function() {
client.write(request);
});
client.once('data', function(data) {
client.end();
});
client.once('end', function() {
done();
});
});
// eslint-disable-next-line
it('restify-GH-318 get request with body (requestBodyOnGet=true)', function(done) {
SERVER.use(
restify.plugins.bodyParser({
mapParams: true,
requestBodyOnGet: true
})
);
SERVER.get('/getWithBody', function(req, res, next) {
assert.equal(req.params.foo, 'bar');
res.send();
next();
});
var request =
'GET /getWithBody HTTP/1.1\r\n' +
'Content-Type: application/json\r\n' +
'Content-Length: 13\r\n' +
'\r\n' +
'{"foo":"bar"}';
var client = net.connect({ host: '127.0.0.1', port: PORT }, function() {
client.write(request);
});
client.once('data', function(data) {
client.end();
});
client.once('end', function() {
done();
});
});
it('restify-GH-111 JSON Parser not right for arrays', function(done) {
SERVER.use(
restify.plugins.bodyParser({
mapParams: true
})
);
SERVER.post('/gh111', function(req, res, next) {
assert.ok(Array.isArray(req.params));
assert.equal(req.params[0], 'foo');
assert.equal(req.params[1], 'bar');
res.send();
next();
});
var obj = ['foo', 'bar'];
CLIENT.post('/gh111', obj, function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('restify-GH-279 more JSON Arrays', function(done) {
SERVER.use(
restify.plugins.jsonBodyParser({
mapParams: true
})
);
SERVER.post('/gh279', function respond(req, res, next) {
assert.ok(Array.isArray(req.params));
assert.equal(req.params[0].id, '123654');
assert.ok(req.params[0].name, 'mimi');
assert.ok(req.params[1].id, '987654');
assert.ok(req.params[1].name, 'pijama');
res.send(200);
next();
});
var obj = [
{
id: '123654',
name: 'mimi'
},
{
id: '987654',
name: 'pijama'
}
];
CLIENT.post('/gh279', obj, function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('restify-GH-774 utf8 corruption in body parser', function(done) {
var slen = 100000;
SERVER.use(restify.plugins.bodyParser());
SERVER.post('/utf8', function(req, res, next) {
assert.notOk(/\ufffd/.test(req.body.text));
assert.equal(req.body.text.length, slen);
res.send({ len: req.body.text.length });
next();
});
// create a long string of unicode characters
var tx = '';
for (var i = 0; i < slen; ++i) {
tx += '\u2661';
}
CLIENT.post('/utf8', { text: tx }, function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('restify-GH-149 limit request body size', function(done) {
SERVER.use(restify.plugins.bodyParser({ maxBodySize: 1024 }));
SERVER.post('/', function(req, res, next) {
res.send(200, { length: req.body.length });
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/',
method: 'POST',
agent: false,
headers: {
accept: 'application/json',
'content-type': 'application/x-www-form-urlencoded',
'transfer-encoding': 'chunked'
}
};
var client = http.request(opts, function(res) {
assert.equal(res.statusCode, 413);
res.once('end', done);
res.resume();
});
client.write(new Array(1028).join('x'));
client.end();
});
it('restify-GH-149 limit request body size (json)', function(done) {
SERVER.use(restify.plugins.bodyParser({ maxBodySize: 1024 }));
SERVER.post('/', function(req, res, next) {
res.send(200, { length: req.body.length });
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/',
method: 'POST',
agent: false,
headers: {
accept: 'application/json',
'content-type': 'application/json',
'transfer-encoding': 'chunked'
}
};
var client = http.request(opts, function(res) {
assert.equal(res.statusCode, 413);
res.once('end', done);
res.resume();
});
client.write('{"a":[' + new Array(512).join('1,') + '0]}');
client.end();
});
it('plugins-GH-6: should expose rawBody', function(done) {
var payload = {
id: 'bar',
name: 'alex'
};
SERVER.use(restify.plugins.jsonBodyParser());
SERVER.post('/body/:id', function(req, res, next) {
assert.equal(req.rawBody, JSON.stringify(payload));
assert.equal(req.body.id, 'bar');
assert.equal(req.body.name, 'alex');
res.send();
next();
});
CLIENT.post('/body/foo', payload, done);
});
it('should not throw uncaught "too few args to sprintf"', function(done) {
// https://github.com/restify/node-restify/issues/1411
SERVER.use(restify.plugins.bodyParser());
SERVER.post('/', function(req, res, next) {
res.send();
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/',
method: 'POST',
agent: false,
headers: {
accept: 'application/json',
'content-type': 'application/json'
}
};
var client = http.request(opts, function(res) {
assert.equal(res.statusCode, 400);
res.once('end', done);
res.resume();
});
client.write('{"malformedJsonWithPercentSign":30%}');
client.end();
});
it('should handle application/*+json as application/json', function(done) {
SERVER.use(restify.plugins.bodyParser({ maxBodySize: 1024 }));
SERVER.post('/', function(req, res, next) {
res.send(200, { length: req.body.length });
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/',
method: 'POST',
agent: false,
headers: {
accept: 'application/json',
'content-type': 'application/hal+json',
'transfer-encoding': 'chunked'
}
};
var client = http.request(opts, function(res) {
assert.equal(res.statusCode, 413);
res.once('end', done);
res.resume();
});
client.write('{"a":[' + new Array(512).join('1,') + '0]}');
client.end();
});
});
================================================
FILE: test/plugins/metrics.test.js
================================================
'use strict';
/* eslint-disable func-names */
// external requires
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
// local files
var helper = require('../lib/helper');
// local globals
var SERVER;
var CLIENT;
var PORT;
function adjustExpectedLatency(expectedLatency, nbTimers) {
// Expected latencies are adjusted to substract 1 ms per timer, because each
// timer may have fired 1ms earlier for Node.js versions < 11.0. See
// https://github.com/nodejs/node/issues/10154 for more info.
return expectedLatency - nbTimers;
}
describe('request metrics plugin', function() {
beforeEach(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server'),
handleUncaughtExceptions: true
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
done();
});
});
afterEach(function(done) {
CLIENT.close();
SERVER.removeAllListeners();
SERVER.close(done);
});
it('should return metrics for a given request', function(done) {
SERVER.on('uncaughtException', function(req, res, route, err) {
assert.ifError(err);
});
SERVER.on(
'after',
restify.plugins.metrics(
{
server: SERVER
},
function(err, metrics, req, res, route) {
assert.ifError(err);
assert.isObject(metrics, 'metrics');
assert.equal(metrics.statusCode, 202);
assert.isAtLeast(
metrics.preLatency,
adjustExpectedLatency(50, 1)
);
assert.isAtLeast(
metrics.useLatency,
adjustExpectedLatency(50, 1)
);
assert.isAtLeast(
metrics.routeLatency,
adjustExpectedLatency(50, 1)
);
assert.isAtLeast(
metrics.latency,
adjustExpectedLatency(150, 3)
);
assert.isAtLeast(
metrics.totalLatency,
adjustExpectedLatency(150, 3)
);
assert.equal(metrics.path, '/foo');
assert.equal(metrics.connectionState, undefined);
assert.equal(metrics.method, 'GET');
assert.isNumber(metrics.inflightRequests);
assert.isObject(req, 'req');
assert.isObject(res, 'res');
assert.isObject(route, 'route');
}
)
);
SERVER.pre(function(req, res, next) {
setTimeout(function() {
return next();
}, 50);
});
SERVER.use(function(req, res, next) {
setTimeout(function() {
return next();
}, 50);
});
SERVER.get('/foo', function(req, res, next) {
setTimeout(function() {
res.send(202, 'hello world');
return next();
}, 50);
});
CLIENT.get('/foo?a=1', function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 202);
return done();
});
});
it('should return metrics with pre error', function(done) {
SERVER.on('uncaughtException', function(req, res, route, err) {
assert.ok(err);
res.send(err);
});
SERVER.on(
'after',
restify.plugins.metrics(
{
server: SERVER
},
function(err, metrics, req, res, route) {
assert.ok(err);
assert.isObject(metrics, 'metrics');
assert.isAtLeast(
metrics.preLatency,
adjustExpectedLatency(50, 1)
);
assert.equal(metrics.useLatency, null);
assert.equal(metrics.routeLatency, null);
assert.isAtLeast(
metrics.latency,
adjustExpectedLatency(50, 1)
);
return done();
}
)
);
SERVER.pre(function(req, res, next) {
setTimeout(function() {
return next(new Error('My Error'));
}, 50);
});
CLIENT.get('/foo?a=1', function(err, _, res) {
assert.ok(err);
});
});
it('should return metrics with use error', function(done) {
SERVER.on('uncaughtException', function(req, res, route, err) {
assert.ok(err);
res.send(err);
});
SERVER.on(
'after',
restify.plugins.metrics(
{
server: SERVER
},
function(err, metrics, req, res, route) {
assert.ok(err);
assert.isObject(metrics, 'metrics');
assert.isAtLeast(metrics.preLatency, 0);
assert.isAtLeast(
metrics.useLatency,
adjustExpectedLatency(50, 1)
);
assert.equal(metrics.routeLatency, null);
assert.isAtLeast(
metrics.latency,
adjustExpectedLatency(50, 1)
);
return done();
}
)
);
SERVER.use(function(req, res, next) {
setTimeout(function() {
return next(new Error('My Error'));
}, 50);
});
SERVER.get('/foo', function(req, res, next) {
res.send(202, 'hello world');
return next();
});
CLIENT.get('/foo?a=1', function(err, _, res) {
assert.ok(err);
});
});
it("should return 'RequestCloseError' err", function(done) {
// we test that the client times out and closes the request. server
// flushes request responsibly but connectionState should indicate it
// was closed by the server.
SERVER.on('uncaughtException', function(req, res, route, err) {
assert.ifError(err);
});
SERVER.on(
'after',
restify.plugins.metrics(
{
server: SERVER
},
function(err, metrics, req, res, route) {
assert.ok(err);
assert.equal(err.name, 'RequestCloseError');
assert.isObject(metrics, 'metrics');
// router doesn't run
assert.equal(metrics.statusCode, 444);
assert.isAtLeast(
metrics.preLatency,
adjustExpectedLatency(50, 1)
);
assert.isAtLeast(
metrics.useLatency,
adjustExpectedLatency(50, 1)
);
assert.isAtLeast(
metrics.routeLatency,
adjustExpectedLatency(250, 1)
);
// The request timeout value is 200 client side, but the
// overall latency is computed on the server, so we're
// tolerating a 10ms difference. This is inherently flaky.
assert.isAtLeast(metrics.latency, 200 - 10);
// latency should be lower as request timeouts
assert.isAbove(metrics.routeLatency, metrics.latency);
assert.equal(metrics.path, '/foo');
assert.equal(metrics.method, 'GET');
assert.equal(metrics.connectionState, 'close');
assert.isNumber(metrics.inflightRequests);
return done();
}
)
);
SERVER.pre(function(req, res, next) {
setTimeout(function() {
return next();
}, 50);
});
SERVER.use(function(req, res, next) {
setTimeout(function() {
return next();
}, 50);
});
SERVER.get(
'/foo',
function(req, res, next) {
setTimeout(function() {
return next();
}, 250);
},
function(req, res, next) {
assert.fail('Client has already closed request');
res.send(202, 'hello world');
return next();
}
);
CLIENT.get(
{
path: '/foo?a=1',
requestTimeout: 200
},
function(err, _, res) {
// request should timeout
assert.ok(err);
assert.equal(err.name, 'RequestTimeoutError');
}
);
});
it('should handle uncaught exceptions', function(done) {
// we test that the client times out and closes the request. server
// flushes request responsibly but connectionState should indicate it
// was closed by the server.
SERVER.on(
'after',
restify.plugins.metrics(
{
server: SERVER
},
// TODO: test timeouts if any of the following asserts fails
function(err, metrics, req, res, route) {
assert.ok(err);
assert.equal(err.name, 'Error');
assert.equal(err.message, 'boom');
assert.isObject(err.domain);
assert.isObject(metrics, 'metrics');
assert.equal(metrics.statusCode, 500);
assert.isNumber(metrics.latency);
assert.equal(metrics.path, '/foo');
assert.equal(metrics.method, 'GET');
assert.equal(metrics.connectionState, undefined);
assert.isNumber(metrics.inflightRequests);
return done();
}
)
);
SERVER.get('/foo', function(req, res, next) {
throw new Error('boom');
});
CLIENT.get('/foo?a=1', function(err, _, res) {
assert.ok(err);
});
});
});
================================================
FILE: test/plugins/multipart.test.js
================================================
'use strict';
/* eslint-disable func-names */
// core requires
var http = require('http');
// external requires
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
// local files
var helper = require('../lib/helper');
// local globals
var SERVER;
var PORT;
describe('multipart parser', function() {
beforeEach(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.get('/', function respond(req, res, next) {
res.send();
next();
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
done();
});
});
afterEach(function(done) {
SERVER.close(done);
});
it('body multipart ok', function(done) {
SERVER.use(
restify.plugins.queryParser({
mapParams: true
})
);
SERVER.use(
restify.plugins.bodyParser({
mapParams: true
})
);
SERVER.post('/multipart/:id', function(req, res, next) {
assert.equal(req.params.id, 'foo');
assert.equal(req.params.mood, 'happy');
assert.equal(req.params.endorphins, '9000');
res.send();
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/multipart/foo?mood=happy',
agent: false,
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data; boundary=huff'
}
};
var client = http.request(opts, function(res) {
assert.equal(res.statusCode, 200);
done();
});
client.write(
'--huff\r\nContent-Disposition: form-data; ' +
'name="endorphins"\r\n\r\n9000\r\n--huff--'
);
client.end();
});
it('gh-847 body multipart no files ok', function(done) {
SERVER.use(
restify.plugins.queryParser({
mapParams: true
})
);
SERVER.use(
restify.plugins.bodyParser({
mapFiles: true,
mapParams: true,
keepExtensions: true,
uploadDir: '/tmp/',
override: true
})
);
SERVER.post('/multipart/:id', function(req, res, next) {
assert.equal(req.params.id, 'foo');
assert.equal(req.params.mood, 'happy');
assert.equal(req.params.endorphins, '9000');
res.send();
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/multipart/foo?mood=happy',
agent: false,
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data; boundary=huff'
}
};
var client = http.request(opts, function(res) {
assert.equal(res.statusCode, 200);
done();
});
// don't actually upload a file
client.write(
'--huff\r\nContent-Disposition: form-data; ' +
'name="endorphins"\r\n\r\n9000\r\n--huff--'
);
client.end();
});
it('gh-847 body multipart files ok', function(done) {
var shine =
'Well you wore out your welcome with random precision, ' +
'rode on the steel breeze. Come on you raver, you seer of ' +
'visions, come on you painter, you piper, you prisoner, and shine!';
var echoes =
'Overhead the albatross hangs motionless upon the air ' +
'And deep beneath the rolling waves in labyrinths of coral ' +
'caves The echo of a distant tide Comes willowing across the ' +
'sand And everything is green and submarine';
SERVER.use(
restify.plugins.queryParser({
mapParams: true
})
);
SERVER.use(
restify.plugins.bodyParser({
mapFiles: true,
mapParams: true,
keepExtensions: true,
uploadDir: '/tmp/',
override: true
})
);
SERVER.post('/multipart/:id', function(req, res, next) {
assert.equal(req.params.id, 'foo');
assert.equal(req.params.mood, 'happy');
assert.equal(req.params.endorphins, '12');
assert.ok(req.params.shine);
assert.ok(req.params.echoes);
assert.equal(req.params.shine.toString('utf8'), shine);
assert.equal(req.params.echoes.toString('utf8'), echoes);
res.send();
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/multipart/foo?mood=happy',
agent: false,
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data; boundary=huff'
}
};
var client = http.request(opts, function(res) {
assert.equal(res.statusCode, 200);
done();
});
client.write('--huff\r\n');
client.write(
'Content-Disposition: form-data; name="endorphins"\r\n\r\n'
);
client.write('12\r\n');
client.write('--huff\r\n');
client.write(
'Content-Disposition: form-data; name="shine"; ' +
'filename="shine.txt"\r\n'
);
client.write('Content-Type: text/plain\r\n\r\n');
client.write(shine + '\r\n');
client.write('--huff\r\n');
client.write(
'Content-Disposition: form-data; name="echoes"; ' +
'filename="echoes.txt"\r\n'
);
client.write('Content-Type: text/plain\r\n\r\n');
client.write(echoes + '\r\n');
client.write('--huff--');
client.end();
});
it('body multipart ok custom handling', function(done) {
var detailsString =
'High endorphin levels make you happy. ' +
'Mostly... I guess. Whatever.';
SERVER.post(
'/multipart/:id',
restify.plugins.bodyParser({
multipartHandler: function(part) {
var buffer = new Buffer(0);
part.on('data', function(data) {
buffer = Buffer.concat([data]);
});
part.on('end', function() {
assert.equal(part.name, 'endorphins');
assert.equal(buffer.toString('ascii'), '12');
});
},
multipartFileHandler: function(part) {
var buffer = new Buffer(0);
part.on('data', function(data) {
buffer = Buffer.concat([data]);
});
part.on('end', function() {
assert.equal(part.name, 'details');
assert.equal(part.filename, 'mood_details.txt');
assert.equal(buffer.toString('ascii'), detailsString);
});
},
mapParams: false
}),
function(req, res, next) {
res.send();
next();
}
);
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/multipart/foo?mood=sad',
agent: false,
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data; boundary=huff'
}
};
var client = http.request(opts, function(res) {
assert.equal(res.statusCode, 200);
done();
});
client.write('--huff\r\n');
client.write(
'Content-Disposition: form-data; name="endorphins"\r\n\r\n'
);
client.write('12\r\n');
client.write('--huff\r\n');
// jscs:disable maximumLineLength
client.write(
'Content-Disposition: form-data; name="details"; ' +
'filename="mood_details.txt"\r\n'
);
// jscs:enable maximumLineLength
client.write('Content-Type: text/plain\r\n\r\n');
client.write(detailsString + '\r\n');
client.write('--huff--');
client.end();
});
it('restify-GH-694 pass hash option through to Formidable', function(done) {
var content = 'Hello World!';
var hash = '2ef7bde608ce5404e97d5f042f95f89f1c232871';
SERVER.post(
'/multipart',
restify.plugins.bodyParser({ hash: 'sha1' }),
function(req, res, next) {
assert.equal(req.files.details.hash, hash);
res.send();
next();
}
);
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/multipart',
agent: false,
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data; boundary=huff'
}
};
var client = http.request(opts, function(res) {
assert.equal(res.statusCode, 200);
done();
});
client.write('--huff\r\n');
// jscs:disable maximumLineLength
client.write(
'Content-Disposition: form-data; name="details"; ' +
'filename="mood_details.txt"\r\n'
);
// jscs:enable maximumLineLength
client.write('Content-Type: text/plain\r\n\r\n');
client.write(content + '\r\n');
client.write('--huff--');
client.end();
});
it('Ensure maxFileSize change is enforced', function(done) {
var shine =
'Well you wore out your welcome with random precision, ' +
'rode on the steel breeze. Come on you raver, you seer of ' +
'visions, come on you painter, you piper, you prisoner, and shine!';
var echoes =
'Overhead the albatross hangs motionless upon the air ' +
'And deep beneath the rolling waves in labyrinths of coral ' +
'caves The echo of a distant tide Comes willowing across the ' +
'sand And everything is green and submarine';
var shortest = Math.min(shine.length, echoes.length);
SERVER.use(
restify.plugins.queryParser({
mapParams: true
})
);
SERVER.use(
restify.plugins.bodyParser({
mapFiles: true,
mapParams: true,
keepExtensions: true,
uploadDir: '/tmp/',
override: true,
// Set limit to shortest of the 'files',
// longer will trigger an error.
maxFileSize: shortest
})
);
SERVER.post('/multipart/:id', function(req, res, next) {
assert.equal(req.params.id, 'foo');
assert.equal(req.params.mood, 'happy');
assert.equal(req.params.endorphins, '12');
assert.ok(req.params.shine);
assert.ok(req.params.echoes);
assert.equal(req.params.shine.toString('utf8'), shine);
assert.equal(req.params.echoes.toString('utf8'), echoes);
res.send();
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/multipart/foo?mood=happy',
agent: false,
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data; boundary=huff'
}
};
var client = http.request(opts, function(res) {
assert.equal(res.statusCode, 400);
var body = '';
res.on('data', function(d) {
body += d;
});
res.on('end', function() {
var rsp = JSON.parse(body);
assert.equal(rsp.code, 'BadRequest');
assert.equal(
rsp.message.substring(0, 30),
'maxFileSize exceeded, received'
);
done();
});
});
client.write('--huff\r\n');
client.write(
'Content-Disposition: form-data; name="endorphins"\r\n\r\n'
);
client.write('12\r\n');
client.write('--huff\r\n');
client.write(
'Content-Disposition: form-data; name="shine"; ' +
'filename="shine.txt"\r\n'
);
client.write('Content-Type: text/plain\r\n\r\n');
client.write(shine + '\r\n');
client.write('--huff\r\n');
client.write(
'Content-Disposition: form-data; name="echoes"; ' +
'filename="echoes.txt"\r\n'
);
client.write('Content-Type: text/plain\r\n\r\n');
client.write(echoes + '\r\n');
client.write('--huff--');
client.end();
});
});
================================================
FILE: test/plugins/oauth2.test.js
================================================
// Copyright 2016 Brian Aabel, Inc. All rights reserved.
'use strict';
/* eslint-disable func-names */
var http = require('http');
// external requires
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
// local files
var helper = require('../lib/helper');
// local globals
var SERVER;
var CLIENT;
var PORT;
var TEST_TOKEN = '18926970-A-nMnSHDqg8Fsunm6Qx1cF1APp';
describe('oauth2 token parser', function() {
before(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.use(restify.plugins.bodyParser());
SERVER.use(restify.plugins.oauth2TokenParser());
SERVER.get('/', function respond(req, res, next) {
res.send();
next();
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
done();
});
});
after(function(done) {
CLIENT.close();
SERVER.close(done);
});
it('should parse oauth2 token from authorization header', function(done) {
var opts = {
path: '/test1/auth-header',
headers: {
Authorization: 'Bearer ' + TEST_TOKEN
}
};
SERVER.get('/test1/auth-header', function(req, res, next) {
assert.isNotNull(req.oauth2.accessToken);
assert.equal(req.oauth2.accessToken, TEST_TOKEN);
res.send();
next();
});
CLIENT.get(opts, function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
// eslint-disable-next-line
it('should do nothing (token is null) if there is no oauth2 token set', function(done) {
var opts = {
path: '/test2/do/nothing'
};
SERVER.get(opts, function(req, res, next) {
assert.isNull(req.oauth2.accessToken);
assert.equal(res.statusCode, 200);
res.send();
next();
});
CLIENT.get(opts, function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('should parse from request body', function(done) {
var test3Url = '/test3/contenttype/ok';
SERVER.post(test3Url, function(req, res, next) {
assert.isNotNull(req.oauth2.accessToken);
assert.equal(req.oauth2.accessToken, TEST_TOKEN);
res.send();
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: test3Url,
agent: false,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
};
var client = http.request(opts, function(res) {
assert.equal(res.statusCode, 200);
done();
});
client.write('access_token=' + TEST_TOKEN);
client.end();
});
// eslint-disable-next-line
it('should parse oauth2 token from request body(case-insensitive)', function(done) {
var test4Url = '/test4/contenttype/mixedcase';
SERVER.post(test4Url, function(req, res, next) {
assert.isNotNull(req.oauth2.accessToken);
assert.equal(req.oauth2.accessToken, TEST_TOKEN);
res.send();
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: test4Url,
agent: false,
method: 'POST',
headers: {
'Content-Type': 'APPLICATION/x-www-form-urlencoded'
}
};
var client = http.request(opts, function(res) {
assert.equal(res.statusCode, 200);
done();
});
client.write('access_token=' + TEST_TOKEN);
client.end();
});
it('should ignore token from request body', function(done) {
var test5Url = '/test5/contenttype/missing/1';
SERVER.post(test5Url, function(req, res, next) {
assert.isNull(req.oauth2.accessToken);
res.send(200);
next();
});
var opts5 = {
hostname: '127.0.0.1',
port: PORT,
path: test5Url,
agent: false,
method: 'POST',
headers: {
'Content-Type': 'text/xml'
}
};
var client = http.request(opts5, function(res) {
assert.equal(res.statusCode, 200);
done();
});
client.write('access_token=' + TEST_TOKEN);
client.end();
});
// eslint-disable-next-line
it('should fail if more than one method is used to set the oauth2 token', function(done) {
SERVER.post('/test6/multi/method/fail', function(req, res, next) {
assert.isNull(req.oauth2.accessToken);
res.send();
next();
});
var opts = {
path: '/test6/multi/method/fail',
headers: {
Authorization: 'Bearer ' + TEST_TOKEN,
'Content-Type': 'application/x-www-form-urlencoded'
}
};
CLIENT.post(opts, { access_token: TEST_TOKEN }, function(err, _, res) {
assert.ok(err);
assert.equal(res.statusCode, 400);
done();
});
});
});
================================================
FILE: test/plugins/plugins.test.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
/* eslint-disable func-names */
// external requires
var pino = require('pino');
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
var sanitizePath = require('../../lib/plugins/pre/prePath.js');
// local files
var helper = require('../lib/helper');
// local globals
var PORT = process.env.UNIT_TEST_PORT || 0;
var CLIENT;
var SERVER;
describe('all other plugins', function() {
beforeEach(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server'),
version: ['2.0.0', '0.5.4', '1.4.3']
});
SERVER.listen(PORT, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
done();
});
});
afterEach(function(done) {
CLIENT.close();
SERVER.close(done);
});
describe('date parser', function() {
it('should reject expired request', function(done) {
SERVER.use(restify.plugins.dateParser());
SERVER.get('/', function respond(req, res, next) {
res.send();
next();
});
var opts = {
path: '/',
headers: {
date: 'Tue, 15 Nov 1994 08:12:31 GMT'
}
};
CLIENT.get(opts, function(err, _, res) {
assert.ok(err);
assert.ok(/Date header .+ is too old/.test(err.message));
assert.equal(res.statusCode, 400);
done();
});
});
});
describe('request logger', function() {
it('tests the requestLoggers extra header properties', function(done) {
var key = 'x-request-uuid';
var badKey = 'x-foo-bar';
var getPath = '/requestLogger/extraHeaders';
var headers = [key, badKey];
SERVER.use(restify.plugins.requestLogger({ headers: headers }));
SERVER.get(getPath, function(req, res, next) {
var childings = req.log[pino.symbols.chindingsSym];
assert.match(childings, /"x-request-uuid":"foo-for-eva"/);
assert.notMatch(childings, /x-foo-bar/);
res.send();
next();
});
var obj = {
path: getPath,
headers: {}
};
obj.headers[key] = 'foo-for-eva';
CLIENT.get(obj, function(err, _, res) {
assert.equal(res.statusCode, 200);
assert.ifError(err);
done();
});
});
it('adds the request id to logs', function(done) {
SERVER.use(restify.plugins.requestLogger());
SERVER.get('/requestLogger/test', function(req, res, next) {
var childings = req.log[pino.symbols.chindingsSym];
assert.match(childings, /"req_id":"[0-9A-F-]+"/i);
res.send();
next();
});
CLIENT.get('/requestLogger/test', function(err, _, res) {
assert.equal(res.statusCode, 200);
assert.ifError(err);
done();
});
});
it('adds the request id with a custom field name', function(done) {
SERVER.use(
restify.plugins.requestLogger({ requestIdFieldName: 'traceId' })
);
SERVER.get('/requestLogger/test', function(req, res, next) {
var childings = req.log[pino.symbols.chindingsSym];
assert.match(childings, /"traceId":"[0-9A-F-]+"/i);
assert.notMatch(childings, /"req_id"/);
res.send();
next();
});
CLIENT.get('/requestLogger/test', function(err, _, res) {
assert.equal(res.statusCode, 200);
assert.ifError(err);
done();
});
});
});
describe('full response', function() {
it('full response', function(done) {
SERVER.use(restify.plugins.fullResponse());
SERVER.get('/bar/:id', function tester2(req, res, next) {
assert.ok(req.params);
assert.equal(req.params.id, 'bar');
res.send();
next();
});
CLIENT.get('/bar/bar', function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
var headers = res.headers;
assert.ok(headers, 'headers ok');
assert.ok(headers.date);
assert.ok(headers['request-id']);
assert.ok(headers['response-time'] >= 0);
assert.equal(headers.server, 'restify');
assert.equal(headers.connection, 'Keep-Alive');
done();
});
});
});
describe('context', function() {
it('set and get request context', function(done) {
SERVER.pre(restify.plugins.pre.context());
var asserted = false;
var expectedData = {
pink: 'floyd'
};
SERVER.get('/context', [
function(req, res, next) {
req.set('pink', 'floyd');
return next();
},
function(req, res, next) {
assert.equal('floyd', req.get('pink'));
assert.deepEqual(expectedData, req.getAll());
asserted = true;
res.send(200);
return next();
}
]);
CLIENT.get('/context', function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.ok(asserted);
done();
});
});
it('should throw if set key is not string', function(done) {
SERVER.pre(restify.plugins.pre.context());
var asserted = false;
SERVER.get('/context', [
function(req, res, next) {
try {
req.set({}, 'floyd');
} catch (e) {
asserted = true;
res.send(200);
}
return next();
}
]);
CLIENT.get('/context', function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.ok(asserted);
done();
});
});
it('should throw if set key is empty string', function(done) {
SERVER.pre(restify.plugins.pre.context());
var asserted = false;
SERVER.get('/context', [
function(req, res, next) {
try {
req.set('', 'floyd');
} catch (e) {
asserted = true;
res.send(200);
}
return next();
}
]);
CLIENT.get('/context', function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.ok(asserted);
done();
});
});
it('should throw if get key is not string', function(done) {
SERVER.pre(restify.plugins.pre.context());
var asserted = false;
SERVER.get('/context', [
function(req, res, next) {
try {
req.get({});
} catch (e) {
asserted = true;
res.send(200);
}
return next();
}
]);
CLIENT.get('/context', function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.ok(asserted);
done();
});
});
it('should throw if get key is empty string', function(done) {
SERVER.pre(restify.plugins.pre.context());
var asserted = false;
SERVER.get('/context', [
function(req, res, next) {
try {
req.get('');
} catch (e) {
asserted = true;
res.send(200);
}
return next();
}
]);
CLIENT.get('/context', function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.ok(asserted);
done();
});
});
});
describe('sanitizePath', function() {
// Ensure it santizies potential edge cases correctly
var tests = {
input: [
'////foo////', //excess padding on both ends
'bar/foo/', // trailing slash
'bar/foo/////', // multiple trailing slashes
'foo////bar', // multiple slashes inbetween
'////foo', // multiple at beginning
'/foo/bar' // don't mutate
],
output: [
'/foo',
'bar/foo',
'bar/foo',
'foo/bar',
'/foo',
'/foo/bar'
],
description: [
'should clean excess padding on both ends',
'should clean trailing slash',
'should clean multiple trailing slashes',
'should clean multiple slashes inbetween',
'should clean multiple at beginning',
'dont mutate correct urls'
]
};
for (var i = 0; i < tests.input.length; i++) {
// eslint-disable-next-line wrap-iife
(function() {
var index = i;
it(tests.description[index], function(done) {
var req = { url: tests.input[index] };
sanitizePath()(req, null, function() {
assert.equal(req.url, tests.output[index]);
done();
});
});
})();
}
});
});
================================================
FILE: test/plugins/query.test.js
================================================
'use strict';
/* eslint-disable func-names */
// external requires
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
// local files
var helper = require('../lib/helper');
// local globals
var SERVER;
var CLIENT;
var PORT;
describe('query parser', function() {
beforeEach(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
done();
});
});
afterEach(function(done) {
CLIENT.close();
SERVER.close(done);
});
it('restify-GH-124 should return empty query', function(done) {
SERVER.use(restify.plugins.queryParser());
SERVER.get('/query/:id', function(req, res, next) {
assert.equal(req.getQuery(), '');
assert.deepEqual(req.query, {});
res.send();
next();
});
CLIENT.get('/query/foo', function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('req.getQuery() should return with raw query string', function(done) {
SERVER.use(restify.plugins.queryParser());
SERVER.get('/query/:id', function(req, res, next) {
assert.equal(req.getQuery(), 'a=1');
assert.deepEqual(req.query, { a: '1' });
res.send();
next();
});
CLIENT.get('/query/foo?a=1', function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('should parse req.query and req.params independently', function(done) {
SERVER.use(restify.plugins.queryParser());
SERVER.get('/query/:id', function(req, res, next) {
assert.equal(req.query.id, 'bar');
assert.equal(req.query.name, 'markc');
assert.equal(req.params.id, 'foo');
assert.notDeepEqual(req.query, req.params);
res.send();
next();
});
CLIENT.get('/query/foo?id=bar&name=markc', function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('should map req.query onto req.params', function(done) {
SERVER.use(
restify.plugins.queryParser({
mapParams: true
})
);
SERVER.get('/query/:id', function(req, res, next) {
assert.equal(req.params.id, 'foo');
assert.equal(req.params.name, 'markc');
res.send();
next();
});
CLIENT.get('/query/foo?id=bar&name=markc', function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('should take req.query and stomp on req.params', function(done) {
SERVER.use(
restify.plugins.queryParser({
mapParams: true,
overrideParams: true
})
);
SERVER.get('/query/:id', function(req, res, next) {
assert.equal(req.params.id, 'bar');
assert.equal(req.params.name, 'markc');
assert.deepEqual(req.query, req.params);
res.send();
next();
});
CLIENT.get('/query/foo?id=bar&name=markc', function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('should parse associative array syntax', function(done) {
SERVER.use(restify.plugins.queryParser());
SERVER.get('/query/:id', function(req, res, next) {
assert.equal(req.params.id, 'foo');
assert.isObject(req.query.name);
assert.equal(req.query.name.first, 'mark');
assert.equal(req.query.name.last, 'cavage');
res.send();
next();
});
var p = '/query/foo?name[first]=mark&name[last]=cavage';
CLIENT.get(p, function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('should parse array syntax', function(done) {
SERVER.use(restify.plugins.queryParser());
SERVER.get('/query/:id', function(req, res, next) {
assert.equal(req.params.id, 'foo');
assert.isArray(req.query.char);
assert.deepEqual(req.query.char, ['a', 'b', 'c']);
res.send();
next();
});
var p = '/query/foo?char[]=a&char[]=b&char[]=c';
CLIENT.get(p, function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('should parse nested array syntax', function(done) {
SERVER.use(restify.plugins.queryParser());
SERVER.get('/query/:id', function(req, res, next) {
assert.equal(req.params.id, 'foo');
assert.isObject(req.query.pizza);
assert.isArray(req.query.pizza.left);
assert.isArray(req.query.pizza.right);
assert.deepEqual(req.query.pizza.left, ['ham', 'bacon']);
assert.deepEqual(req.query.pizza.right, ['pineapple']);
res.send();
next();
});
var p =
'/query/foo?pizza[left][]=ham&pizza[left][]=bacon&' +
'pizza[right][]=pineapple';
CLIENT.get(p, function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('restify-GH-59 Query params with / result in a 404', function(done) {
SERVER.use(restify.plugins.queryParser());
SERVER.get('/', function tester(req, res, next) {
res.send('hello world');
next();
});
CLIENT.get('/?foo=bar/foo', function(err, _, res, obj) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert.equal(obj, 'hello world');
done();
});
});
it('restify-GH-323: //? broken', function(done) {
SERVER.pre(restify.plugins.pre.sanitizePath());
SERVER.use(
restify.plugins.queryParser({
mapParams: true
})
);
SERVER.get('/hello/:name', function(req, res, next) {
res.send(req.params);
});
CLIENT.get('/hello/foo/?bar=baz', function(err, _, __, obj) {
assert.ifError(err);
assert.deepEqual(obj, { name: 'foo', bar: 'baz' });
done();
});
});
it('/? broken', function(done) {
SERVER.pre(restify.plugins.pre.sanitizePath());
SERVER.use(
restify.plugins.queryParser({
mapParams: true
})
);
SERVER.get('/', function(req, res, next) {
res.send(req.params);
});
CLIENT.get('/?bar=baz', function(err, _, __, obj) {
assert.ifError(err);
assert.deepEqual(obj, { bar: 'baz' });
done();
});
});
});
================================================
FILE: test/plugins/reqIdHeaders.test.js
================================================
'use strict';
/* eslint-disable func-names */
// external modules
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
var validator = require('validator');
// internal files
var helper = require('../lib/helper');
describe('request id headers', function() {
var SERVER;
var CLIENT;
var PORT;
beforeEach(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.pre(
restify.plugins.pre.reqIdHeaders({
headers: ['x-req-id-a', 'x-req-id-b']
})
);
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
return done();
});
});
afterEach(function(done) {
CLIENT.close();
SERVER.close(function() {
CLIENT = null;
SERVER = null;
return done();
});
});
it('GH-1086: should reuse request id when available', function(done) {
SERVER.get('/1', function(req, res, next) {
// the 12345 value is set when the client is created.
assert.ok(req.headers.hasOwnProperty('x-req-id-a'));
assert.equal(req.getId(), req.headers['x-req-id-a']);
res.send('hello world');
return next();
});
// create new client since we new specific headers
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
headers: {
'x-req-id-a': 12345
}
});
CLIENT.get('/1', function(err, req, res, data) {
assert.ifError(err);
assert.equal(data, 'hello world');
return done();
});
});
it('GH-1086: should use second request id when available', function(done) {
SERVER.get('/1', function(req, res, next) {
assert.ok(req.headers.hasOwnProperty('x-req-id-b'));
assert.equal(req.getId(), req.headers['x-req-id-b']);
res.send('hello world');
return next();
});
// create new client since we new specific headers
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
headers: {
'x-req-id-b': 678910
}
});
CLIENT.get('/1', function(err, req, res, data) {
assert.ifError(err);
assert.equal(data, 'hello world');
return done();
});
});
// eslint-disable-next-line
it('GH-1086: should use default uuid request id if none provided', function(done) {
SERVER.get('/1', function(req, res, next) {
assert.ok(req.getId());
assert.ok(validator.isUUID(req.getId()));
res.send('hello world');
return next();
});
// create new client since we new specific headers
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT
});
CLIENT.get('/1', function(err, req, res, data) {
assert.ifError(err);
assert.equal(data, 'hello world');
return done();
});
});
it('GH-1086: empty request id should be ignored', function(done) {
SERVER.get('/1', function(req, res, next) {
assert.ok(req.headers.hasOwnProperty('x-req-id-b'));
assert.equal(req.getId(), req.headers['x-req-id-b']);
res.send('hello world');
return next();
});
// create new client since we new specific headers
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
headers: {
'x-req-id-a': '',
'x-req-id-b': 12345
}
});
CLIENT.get('/1', function(err, req, res, data) {
assert.ifError(err);
assert.equal(data, 'hello world');
return done();
});
});
});
================================================
FILE: test/plugins/requestExpiry.test.js
================================================
'use strict';
/* eslint-disable func-names */
// external modules
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
// local modules
var helper = require('../lib/helper');
// globals
var SERVER;
var CLIENT;
var PORT;
describe('request expiry parser', function() {
beforeEach(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
done();
});
});
afterEach(function(done) {
CLIENT.close();
SERVER.close(done);
});
describe('constructor', function() {
it('should throw if no headers passed in', function(done) {
try {
SERVER.use(restify.plugins.requestExpiry({}));
} catch (e) {
done();
}
});
it('should throw if only timeout header passed in', function(done) {
try {
SERVER.use(
restify.plugins.requestExpiry({
timeoutHeader: 'foo'
})
);
} catch (e) {
done();
}
});
it('should throw if only timeout header passed in', function(done) {
try {
SERVER.use(
restify.plugins.requestExpiry({
startHeader: 'foo'
})
);
} catch (e) {
done();
}
});
});
describe('absolute header', function() {
it('should timeout due to request expiry', function(done) {
var key = 'x-request-expiry';
var getPath = '/request/expiry';
var called = false;
SERVER.use(restify.plugins.requestExpiry({ absoluteHeader: key }));
SERVER.get(getPath, function(req, res, next) {
called = true;
res.send();
next();
});
var obj = {
path: getPath,
headers: {
'x-request-expiry': Date.now() - 100
}
};
CLIENT.get(obj, function(err, _, res) {
assert.ok(err);
assert.equal(res.statusCode, 504);
assert.equal(called, false);
done();
});
});
it('should not timeout due to request expiry', function(done) {
var key = 'x-request-expiry';
var getPath = '/request/expiry';
var called = false;
SERVER.use(restify.plugins.requestExpiry({ absoluteHeader: key }));
SERVER.get(getPath, function(req, res, next) {
assert.isFalse(req.isExpired());
called = true;
res.send();
next();
});
var obj = {
path: getPath,
headers: {
'x-request-expiry': Date.now() + 100
}
};
CLIENT.get(obj, function(err, _, res) {
assert.equal(res.statusCode, 200);
assert.equal(called, true);
assert.ifError(err);
done();
});
});
it('should be ok without request expiry header', function(done) {
var key = 'x-request-expiry';
var getPath = '/request/expiry';
var called = false;
SERVER.use(restify.plugins.requestExpiry({ absoluteHeader: key }));
SERVER.get(getPath, function(req, res, next) {
// requests never expire if the header is not set
assert.isFalse(req.isExpired());
called = true;
res.send();
next();
});
var obj = {
path: getPath,
headers: {
'x-request-expiry': Date.now() + 100
}
};
CLIENT.get(obj, function(err, _, res) {
assert.equal(res.statusCode, 200);
assert.equal(called, true);
assert.ifError(err);
done();
});
});
it('should be ok if request expiry header is NaN', function(done) {
var key = 'x-request-expiry';
var getPath = '/request/expiry';
var called = false;
SERVER.use(restify.plugins.requestExpiry({ absoluteHeader: key }));
SERVER.get(getPath, function(req, res, next) {
// requests never expire if the header is not set
assert.isFalse(req.isExpired());
called = true;
res.send();
next();
});
var obj = {
path: getPath,
headers: {
'x-request-expiry':
'I am just a poor boy with my story seldom told'
}
};
CLIENT.get(obj, function(err, _, res) {
assert.equal(res.statusCode, 200);
assert.equal(called, true);
assert.ifError(err);
done();
});
});
});
describe('timeout header', function() {
it('should timeout due to request expiry', function(done) {
var startKey = 'x-request-starttime';
var timeoutKey = 'x-request-timeout';
var getPath = '/request/expiry';
var called = false;
SERVER.use(
restify.plugins.requestExpiry({
startHeader: startKey,
timeoutHeader: timeoutKey
})
);
SERVER.get(getPath, function(req, res, next) {
assert.isFalse(req.isExpired());
called = true;
res.send();
next();
});
var obj = {
path: getPath,
headers: {
'x-request-starttime': Date.now() - 200,
'x-request-timeout': 100
}
};
CLIENT.get(obj, function(err, _, res) {
assert.ok(err);
assert.equal(res.statusCode, 504);
assert.equal(called, false);
done();
});
});
it('should not timeout due to request expiry', function(done) {
var startKey = 'x-request-starttime';
var timeoutKey = 'x-request-timeout';
var getPath = '/request/expiry';
var called = false;
SERVER.use(
restify.plugins.requestExpiry({
startHeader: startKey,
timeoutHeader: timeoutKey
})
);
SERVER.get(getPath, function(req, res, next) {
called = true;
res.send();
next();
});
var obj = {
path: getPath,
headers: {
'x-request-starttime': Date.now(),
'x-request-timeout': 100
}
};
CLIENT.get(obj, function(err, _, res) {
assert.equal(res.statusCode, 200);
assert.equal(called, true);
assert.ifError(err);
done();
});
});
it('should be ok without request expiry header', function(done) {
var startKey = 'x-request-starttime';
var timeoutKey = 'x-request-timeout';
var getPath = '/request/expiry';
var called = false;
SERVER.use(
restify.plugins.requestExpiry({
startHeader: startKey,
timeoutHeader: timeoutKey
})
);
SERVER.get(getPath, function(req, res, next) {
// requests never expire if the header is not set
assert.isFalse(req.isExpired());
called = true;
res.send();
next();
});
var obj = {
path: getPath,
headers: {}
};
CLIENT.get(obj, function(err, _, res) {
assert.equal(res.statusCode, 200);
assert.equal(called, true);
assert.ifError(err);
done();
});
});
it('should be ok if start header is NaN', function(done) {
var startKey = 'x-request-starttime';
var timeoutKey = 'x-request-timeout';
var getPath = '/request/expiry';
var called = false;
SERVER.use(
restify.plugins.requestExpiry({
startHeader: startKey,
timeoutHeader: timeoutKey
})
);
SERVER.get(getPath, function(req, res, next) {
// requests never expire if the header is not set
assert.isFalse(req.isExpired());
called = true;
res.send();
next();
});
var obj = {
path: getPath,
headers: {
'x-request-starttime': 'I have squandered my resistance',
'x-request-timeout': 100
}
};
CLIENT.get(obj, function(err, _, res) {
assert.equal(res.statusCode, 200);
assert.equal(called, true);
assert.ifError(err);
done();
});
});
it('should be ok if timeout header is NaN', function(done) {
var startKey = 'x-request-starttime';
var timeoutKey = 'x-request-timeout';
var getPath = '/request/expiry';
var called = false;
SERVER.use(
restify.plugins.requestExpiry({
startHeader: startKey,
timeoutHeader: timeoutKey
})
);
SERVER.get(getPath, function(req, res, next) {
// requests never expire if the header is not set
assert.isFalse(req.isExpired());
called = true;
res.send();
next();
});
var obj = {
path: getPath,
headers: {
'x-request-starttime': Date.now(),
'x-request-timeout': 'For a pocked full of mumbles'
}
};
CLIENT.get(obj, function(err, _, res) {
assert.equal(res.statusCode, 200);
assert.equal(called, true);
assert.ifError(err);
done();
});
});
it('should be ok if both headers are NaN', function(done) {
var startKey = 'x-request-starttime';
var timeoutKey = 'x-request-timeout';
var getPath = '/request/expiry';
var called = false;
SERVER.use(
restify.plugins.requestExpiry({
startHeader: startKey,
timeoutHeader: timeoutKey
})
);
SERVER.get(getPath, function(req, res, next) {
// requests never expire if the header is not set
assert.isFalse(req.isExpired());
called = true;
res.send();
next();
});
var obj = {
path: getPath,
headers: {
'x-request-starttime': 'Such are promises',
'x-request-timeout': 'All lies and jests'
}
};
CLIENT.get(obj, function(err, _, res) {
assert.equal(res.statusCode, 200);
assert.equal(called, true);
assert.ifError(err);
done();
});
});
});
});
================================================
FILE: test/plugins/static.test.js
================================================
'use strict';
/* eslint-disable func-names */
// core requires
var fs = require('fs');
var path = require('path');
var net = require('net');
// external requires
var assert = require('chai').assert;
var mkdirp = require('mkdirp');
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
var rimraf = require('rimraf');
// local files
var helper = require('../lib/helper');
// local globals
var SERVER;
var CLIENT;
var PORT;
var FILES_TO_DELETE = [];
var DIRS_TO_DELETE = [];
describe('static resource plugin', function() {
beforeEach(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
done();
});
});
afterEach(function(done) {
var i;
for (i = 0; i < FILES_TO_DELETE.length; ++i) {
try {
fs.unlinkSync(FILES_TO_DELETE[i]);
} catch (err) {
/* normal */
}
}
for (i = 0; i < DIRS_TO_DELETE.length; ++i) {
try {
rimraf.sync(DIRS_TO_DELETE[i]);
} catch (err) {
/* normal */
}
}
CLIENT.close();
SERVER.close(done);
});
function serveStaticTest(done, testDefault, tmpDir, regex, staticFile) {
var staticContent = '{"content": "abcdefg"}';
var staticObj = JSON.parse(staticContent);
var testDir = 'public';
var testFileName = 'index.json';
var routeName = 'GET wildcard';
var tmpPath = path.join(__dirname, '../', tmpDir);
mkdirp(tmpPath, function(err) {
assert.ifError(err);
DIRS_TO_DELETE.push(tmpPath);
var folderPath = path.join(tmpPath, testDir);
mkdirp(folderPath, function(err2) {
assert.ifError(err2);
DIRS_TO_DELETE.push(folderPath);
var file = path.join(folderPath, testFileName);
fs.writeFile(file, staticContent, function(err3) {
assert.ifError(err3);
FILES_TO_DELETE.push(file);
var p = '/' + testDir + '/' + testFileName;
var opts = { directory: tmpPath };
if (staticFile) {
opts.file = p;
}
if (testDefault) {
p = '/' + testDir + '/';
opts.default = testFileName;
routeName += ' with default';
}
SERVER.get(
{
path: '/' + testDir + '/*',
name: routeName
},
restify.plugins.serveStatic(opts)
);
CLIENT.get(p, function(err4, req, res, obj) {
assert.ifError(err4);
assert.equal(
res.headers['cache-control'],
'public, max-age=3600'
);
assert.deepEqual(obj, staticObj);
done();
});
});
});
});
}
function testNoAppendPath(done, testDefault, tmpDir, regex, staticFile) {
var staticContent = '{"content": "abcdefg"}';
var staticObj = JSON.parse(staticContent);
var testDir = 'public';
var testFileName = 'index.json';
var routeName = 'GET wildcard';
var tmpPath = path.join(__dirname, '../', tmpDir);
mkdirp(tmpPath, function(err) {
assert.ifError(err);
DIRS_TO_DELETE.push(tmpPath);
var folderPath = path.join(tmpPath, testDir);
mkdirp(folderPath, function(err2) {
assert.ifError(err2);
DIRS_TO_DELETE.push(folderPath);
var file = path.join(folderPath, testFileName);
fs.writeFile(file, staticContent, function(err3) {
assert.ifError(err3);
FILES_TO_DELETE.push(file);
var p = '/' + testDir + '/' + testFileName;
var opts = { directory: folderPath };
opts.appendRequestPath = false;
if (staticFile) {
opts.file = testFileName;
}
if (testDefault) {
p = '/' + testDir + '/';
opts.default = testFileName;
routeName += ' with default';
}
SERVER.get(
{
path: '/' + testDir + '/*',
name: routeName
},
restify.plugins.serveStatic(opts)
);
CLIENT.get(p, function(err4, req, res, obj) {
assert.ifError(err4);
assert.equal(
res.headers['cache-control'],
'public, max-age=3600'
);
assert.deepEqual(obj, staticObj);
done();
});
});
});
});
}
it('static serves static files', function(done) {
serveStaticTest(done, false, '.tmp');
});
it('static serves static files in nested folders', function(done) {
serveStaticTest(done, false, '.tmp/folder');
});
it('static serves static files in with a root regex', function(done) {
serveStaticTest(done, false, '.tmp', '/.*');
});
// eslint-disable-next-line
it('static serves static files with a root, !greedy, regex', function(done) {
serveStaticTest(done, false, '.tmp', '/?.*');
});
it('static serves default file', function(done) {
serveStaticTest(done, true, '.tmp');
});
// eslint-disable-next-line
it('restify-GH-379 static serves file with parentheses in path', function(done) {
serveStaticTest(done, false, '.(tmp)');
});
it('restify-GH-719 serve a specific static file', function(done) {
// serve the same default file .tmp/public/index.json
// but get it from opts.file
serveStaticTest(done, false, '.tmp', null, true);
});
// eslint-disable-next-line
it('static serves static file with appendRequestPath = false', function(done) {
testNoAppendPath(done, false, '.tmp');
});
// eslint-disable-next-line
it('static serves default file with appendRequestPath = false', function(done) {
testNoAppendPath(done, true, '.tmp');
});
// eslint-disable-next-line
it('restify serve a specific static file with appendRequestPath = false', function(done) {
testNoAppendPath(done, false, '.tmp', null, true);
});
it('static responds 404 for missing file', function(done) {
var p = '/public/no-such-file.json';
var tmpPath = path.join(process.cwd(), '.tmp');
SERVER.get(
'/public/.*',
restify.plugins.serveStatic({ directory: tmpPath })
);
CLIENT.get(p, function(err, req, res, obj) {
assert.ok(err);
assert.strictEqual(err.statusCode, 404);
assert.strictEqual(err.restCode, 'ResourceNotFound');
done();
});
});
// eslint-disable-next-line
it('GH-1382 static responds 404 for missing file with percent-codes', function(done) {
var p = '/public/no-%22such-file.json';
var tmpPath = path.join(process.cwd(), '.tmp');
SERVER.get(
'/public/.*',
restify.plugins.serveStatic({ directory: tmpPath })
);
CLIENT.get(p, function(err, req, res, obj) {
assert.ok(err);
assert.equal(err.statusCode, 404);
assert.equal(err.restCode, 'ResourceNotFound');
done();
});
});
// To ensure this will always get properly restored (even in case of a test
// failure) we do it here.
var originalCreateReadStream = fs.createReadStream;
afterEach(function() {
fs.createReadStream = originalCreateReadStream;
});
var TMP_PATH = path.join(__dirname, '../', '.tmp');
var RAW_REQUEST =
'GET /index.html HTTP/1.1\r\n' +
'Host: 127.0.0.1:' +
PORT +
'\r\n' +
'User-Agent: curl/7.48.0\r\n' +
'Accept: */*\r\n' +
'\r\n';
it(
'static does not leak the file stream and next() is properly called ' +
'when the client disconnects before receiving a reply',
function(done) {
var streamWasAlreadyCreated = false;
var streamWasClosed = false;
var socket = new net.Socket();
fs.createReadStream = function() {
// Just in case the code would open more streams in the future.
assert.strictEqual(streamWasAlreadyCreated, false);
streamWasAlreadyCreated = true;
var stream = originalCreateReadStream.apply(this, arguments);
stream.once('close', function() {
streamWasClosed = true;
});
socket.end();
return stream;
};
mkdirp(TMP_PATH, function(err) {
assert.ifError(err);
DIRS_TO_DELETE.push(TMP_PATH);
fs.writeFileSync(
path.join(TMP_PATH, 'index.html'),
'Hello world!'
);
var serve = restify.plugins.serveStatic({
directory: TMP_PATH
});
SERVER.get('/index.html', function(req, res, next) {
serve(req, res, function(nextRoute) {
assert.strictEqual(streamWasClosed, true);
assert.strictEqual(nextRoute, false);
done();
});
});
socket.connect({ host: '127.0.0.1', port: PORT }, function() {
socket.write(RAW_REQUEST, 'utf-8', function(err2, data) {
assert.ifError(err2);
});
});
});
}
);
it(
'static does not open a file stream and next() is properly called ' +
'when the client disconnects immediately after sending a request',
function(done) {
fs.createReadStream = function() {
assert(false);
};
mkdirp(TMP_PATH, function(err) {
assert.ifError(err);
DIRS_TO_DELETE.push(TMP_PATH);
fs.writeFileSync(
path.join(TMP_PATH, 'index.html'),
'Hello world!'
);
var serve = restify.plugins.serveStatic({
directory: TMP_PATH
});
var socket = new net.Socket();
SERVER.get('/index.html', function(req, res, next) {
// closed before serve
socket.on('end', () => {
serve(req, res, function(nextRoute) {
assert.strictEqual(nextRoute, false);
done();
});
});
});
SERVER.on('after', function(req, res, route, afterErr) {
assert(afterErr.name, 'RequestCloseError');
done();
});
socket.connect({ host: '127.0.0.1', port: PORT }, function() {
socket.write(RAW_REQUEST, 'utf-8', function(err2, data) {
assert.ifError(err2);
socket.end();
});
});
});
}
);
it('static responds 404 for missing file', function(done) {
var p = '/public/no-such-file.json';
var tmpPath = path.join(process.cwd(), '.tmp');
SERVER.get(
'/public/.*',
restify.plugins.serveStatic({ directory: tmpPath })
);
CLIENT.get(p, function(err, req, res, obj) {
assert.ok(err);
assert.equal(err.statusCode, 404);
assert.equal(err.restCode, 'ResourceNotFound');
return done();
});
});
// eslint-disable-next-line
it('GH-1382 static responds 404 for missing file with percent-codes', function(done) {
var p = '/public/no-%22such-file.json';
var tmpPath = path.join(process.cwd(), '.tmp');
SERVER.get(
'/public/.*',
restify.plugins.serveStatic({ directory: tmpPath })
);
CLIENT.get(p, function(err, req, res, obj) {
assert.ok(err);
assert.equal(err.statusCode, 404);
assert.equal(err.restCode, 'ResourceNotFound');
return done();
});
});
});
================================================
FILE: test/plugins/staticFiles.test.js
================================================
'use strict';
/* eslint-disable func-names */
// core requires
var fs = require('fs');
var path = require('path');
var net = require('net');
// external requires
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
// local files
var helper = require('../lib/helper');
// local globals
var SERVER;
var CLIENT;
var PORT;
describe('staticFiles plugin - no options', function() {
beforeEach(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createStringClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
done();
});
});
afterEach(function(done) {
CLIENT.close();
SERVER.close(done);
});
var STATIC_FILES_PATH = __dirname + '/testStaticFiles';
function simpleTests(endpoint, filePath, contentType, done) {
var ENDPOINT = endpoint;
var fileSuffixPath = filePath;
var requestPath = path.join(ENDPOINT, fileSuffixPath);
var fileOnDisk = path.join(STATIC_FILES_PATH, fileSuffixPath);
if (fileSuffixPath.endsWith('/')) {
fileOnDisk = path.join(fileOnDisk, 'index.html');
}
var fileContent = fs.readFileSync(fileOnDisk, 'utf8');
var fileStat = fs.statSync(fileOnDisk);
SERVER.get(
ENDPOINT + '/*',
restify.plugins.serveStaticFiles(path.resolve(STATIC_FILES_PATH))
);
CLIENT.get(encodeURI(requestPath), function(err, req, res, obj) {
assert.ifError(err);
assert.equal(fileContent, obj);
// Verify headers
assert.equal(res.headers['cache-control'], 'public, max-age=0');
assert.equal(
res.headers['content-type'],
contentType //'text/html; charset=UTF-8'
);
assert.exists(res.headers.etag);
assert.equal(
res.headers['last-modified'],
fileStat.mtime.toUTCString()
);
done();
});
}
it('serve static file', function(done) {
simpleTests('/public', 'index.html', 'text/html; charset=UTF-8', done);
});
it('serve default static file(index.html)', function(done) {
simpleTests('/public', '/', 'text/html; charset=UTF-8', done);
});
it('serve static file(file1.txt)', function(done) {
simpleTests('/public', 'file1.txt', 'text/plain; charset=UTF-8', done);
});
it('serve nested static files in request', function(done) {
simpleTests(
'/public',
'docs/doc.md',
'text/markdown; charset=UTF-8',
done
);
});
it('serve default nested static file(index.html)', function(done) {
simpleTests('/public', 'docs/', 'text/html; charset=UTF-8', done);
});
it('serve file paths with special chars', function(done) {
simpleTests(
'/public',
'special/$_$/bad (file).txt',
'text/plain; charset=UTF-8',
done
);
});
});
describe('staticFiles plugin - with options', function() {
beforeEach(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createStringClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
done();
});
});
afterEach(function(done) {
CLIENT.close();
SERVER.close(done);
});
var STATIC_FILES_PATH = __dirname + '/testStaticFiles';
var OPTIONS = {
maxAge: 3600000, // this is in millisecs
etag: false,
setHeaders: function setCustomHeaders(response, requestedPath, stat) {
response.setHeader('restify-plugin-x', 'awesome');
}
};
function testsWithOptions(endpoint, filePath, options, contentType, done) {
var ENDPOINT = endpoint;
var fileSuffixPath = filePath;
var requestPath = path.join(ENDPOINT, fileSuffixPath);
var fileOnDisk = path.join(STATIC_FILES_PATH, fileSuffixPath);
if (fileSuffixPath.endsWith('/')) {
fileOnDisk = path.join(fileOnDisk, 'index.html');
}
var fileContent = fs.readFileSync(fileOnDisk, 'utf8');
var fileStat = fs.statSync(fileOnDisk);
SERVER.get(
ENDPOINT + '/*',
restify.plugins.serveStaticFiles(
path.resolve(STATIC_FILES_PATH),
options
)
);
CLIENT.get(encodeURI(requestPath), function(err, req, res, obj) {
assert.ifError(err);
assert.equal(fileContent, obj);
// Verify headers
assert.equal(res.headers['cache-control'], 'public, max-age=3600');
assert.equal(
res.headers['content-type'],
contentType //'text/html; charset=UTF-8'
);
assert.notExists(res.headers.etag);
assert.equal(
res.headers['last-modified'],
fileStat.mtime.toUTCString()
);
assert.equal(res.headers['restify-plugin-x'], 'awesome');
done();
});
}
it('serve static file', function(done) {
testsWithOptions(
'/public',
'index.html',
OPTIONS,
'text/html; charset=UTF-8',
done
);
});
it('serve default static file(index.html)', function(done) {
testsWithOptions(
'/public',
'/',
OPTIONS,
'text/html; charset=UTF-8',
done
);
});
it('serve static file(file1.txt)', function(done) {
testsWithOptions(
'/public',
'file1.txt',
OPTIONS,
'text/plain; charset=UTF-8',
done
);
});
it('serve nested static files in request', function(done) {
testsWithOptions(
'/public',
'docs/doc.md',
OPTIONS,
'text/markdown; charset=UTF-8',
done
);
});
it('serve default nested static file(index.html)', function(done) {
testsWithOptions(
'/public',
'docs/',
OPTIONS,
'text/html; charset=UTF-8',
done
);
});
it('serve file paths with special chars', function(done) {
testsWithOptions(
'/public',
'special/$_$/bad (file).txt',
OPTIONS,
'text/plain; charset=UTF-8',
done
);
});
});
describe('staticFiles plugin - negative cases', function() {
beforeEach(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createStringClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
done();
});
});
afterEach(function(done) {
CLIENT.close();
SERVER.close(done);
});
var STATIC_FILES_PATH = __dirname + '/testStaticFiles';
var OPTIONS = {
maxAge: 3600000, // this is in millisecs
etag: false,
setHeaders: function setCustomHeaders(response, requestedPath, stat) {
response.setHeader('restify-plugin-x', 'awesome');
}
};
function negativeTests(
endpoint,
filePath,
expectedStatusCode,
expectedStatusMsg,
done
) {
var ENDPOINT = endpoint;
var fileSuffixPath = filePath;
var requestPath = path.join(ENDPOINT, fileSuffixPath);
SERVER.get(
ENDPOINT + '/*',
restify.plugins.serveStaticFiles(
path.resolve(STATIC_FILES_PATH),
OPTIONS
)
);
CLIENT.get(encodeURI(requestPath), function(err, req, res, obj) {
assert.exists(err);
assert.equal(res.statusCode, expectedStatusCode);
assert.equal(res.statusMessage, expectedStatusMsg);
done();
});
}
it('fail to serve root directory', function(done) {
negativeTests('/public', '', 404, 'Not Found', done);
});
it('fail to serve nested directory', function(done) {
negativeTests('/public', 'docs', 403, 'Forbidden', done);
});
it('fail on file not found', function(done) {
negativeTests('/public', 'file2.txt', 404, 'Not Found', done);
});
it('fail on nested file not found', function(done) {
negativeTests(
'/public',
'docs/doc_not_there.md',
404,
'Not Found',
done
);
});
it('fail on missing file special characters', function(done) {
negativeTests(
'/public',
'special/$_$/bad (file)~notExists.txt',
404,
'Not Found',
done
);
});
it('fail on POST', function(done) {
var ENDPOINT = '/public';
var fileSuffixPath = 'docs/';
var requestPath = path.join(ENDPOINT, fileSuffixPath);
SERVER.post(
ENDPOINT + '/*',
restify.plugins.serveStaticFiles(
path.resolve(STATIC_FILES_PATH),
OPTIONS
)
);
CLIENT.get(encodeURI(requestPath), function(err, req, res, obj) {
assert.exists(err);
assert.equal(res.statusCode, 405);
assert.equal(res.statusMessage, 'Method Not Allowed');
done();
});
});
});
describe('staticFiles plugin - with sockets', function() {
// for some reason the server.close with socket
// takes longer to close
this.timeout(15000);
beforeEach(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
done();
});
});
afterEach(function(done) {
SERVER.close(done);
});
var STATIC_FILES_PATH = __dirname + '/testStaticFiles';
var OPTIONS = {
maxAge: 3600000, // this is in millisecs
etag: false,
setHeaders: function setCustomHeaders(response, requestedPath, stat) {
response.setHeader('restify-plugin-x', 'awesome');
}
};
function testsWithOptions(endpoint, filePath, done) {
var ENDPOINT = endpoint;
var fileSuffixPath = filePath;
var requestPath = path.join(ENDPOINT, fileSuffixPath);
var socket = new net.Socket();
SERVER.get(
ENDPOINT + '/*',
restify.plugins.serveStaticFiles(
path.resolve(STATIC_FILES_PATH),
OPTIONS
)
);
var RAW_REQUEST =
'GET ' +
requestPath +
' HTTP/1.1\r\n' +
'Host: 127.0.0.1:' +
PORT +
'\r\n' +
'User-Agent: curl/7.48.0\r\n' +
'Accept: */*\r\n' +
'\r\n';
socket.connect({ host: '127.0.0.1', port: PORT }, function() {
socket.write(RAW_REQUEST, 'utf8', function(err2, data) {
assert.ifError(err2);
});
});
socket.on('data', function(data) {
var stringData = data.toString('utf8');
assert.isTrue(stringData.indexOf('restify-plugin-x') > 0);
done();
});
}
it('serve static file', function(done) {
testsWithOptions('/public', 'index.html', done);
});
});
================================================
FILE: test/plugins/strictQueryParams.test.js
================================================
'use strict';
/* eslint-disable func-names */
// external requires
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
// local files
var helper = require('../lib/helper');
// local globals
var SERVER;
var CLIENT;
var PORT;
var MESSAGE = 'Malformed request syntax';
describe('strictQueryParams', function() {
beforeEach(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server'),
handleUncaughtExceptions: true
});
SERVER.listen(0, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
done();
});
});
afterEach(function(done) {
CLIENT.close();
SERVER.close(done);
});
it('should respond 200 without plugin', function(done) {
SERVER.use(
restify.plugins.queryParser({
mapParams: true,
overrideParams: true
})
);
SERVER.get('/query/:id', function(req, res, next) {
assert.equal(req.params.id, 'bar');
assert.notEqual(req.params.name, 'josep&jorge');
assert.deepEqual(req.query, req.params);
res.send();
next();
});
CLIENT.get('/query/foo?id=bar&name=josep&jorge', function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('should respond 400 to non-strict key/val query param', function(done) {
SERVER.pre(
restify.plugins.pre.strictQueryParams({
message: MESSAGE
})
);
SERVER.use(
restify.plugins.queryParser({
mapParams: true,
overrideParams: true
})
);
SERVER.get('/query/:id', function(req, res, next) {
res.send();
next();
});
CLIENT.get('/query/foo?id=bar&name="josep&jorge', function(
err,
_,
res
) {
assert.equal(typeof err, 'object');
assert.equal(res.statusCode, 400);
assert.deepEqual(JSON.parse(res.body), {
code: 'BadRequest',
message: MESSAGE
});
done();
});
});
it('should respond 400 without message opt', function(done) {
SERVER.pre(restify.plugins.pre.strictQueryParams());
SERVER.use(
restify.plugins.queryParser({
mapParams: true,
overrideParams: true
})
);
SERVER.get('/query/:id', function(req, res, next) {
res.send();
next();
});
CLIENT.get('/query/foo?id=bar&name="josep&jorge', function(
err,
_,
res
) {
assert.equal(typeof err, 'object');
assert.equal(res.statusCode, 400);
assert.deepEqual(JSON.parse(res.body), {
code: 'BadRequest',
message: 'Url query params does not meet strict format'
});
done();
});
});
it('should respond 400 to query param with amp and plus', function(done) {
SERVER.pre(
restify.plugins.pre.strictQueryParams({
message: MESSAGE
})
);
SERVER.use(
restify.plugins.queryParser({
mapParams: true,
overrideParams: true
})
);
SERVER.get('/query/:id', function(req, res, next) {
assert.equal(req.params.id, 'bar');
assert.equal(req.params.name, 'josep & jorge');
assert.deepEqual(req.query, req.params);
res.send();
next();
});
CLIENT.get('/query/foo?id=bar&name=josep+&+jorge', function(
err,
_,
res
) {
assert.equal(typeof err, 'object');
assert.equal(res.statusCode, 400);
assert.deepEqual(JSON.parse(res.body), {
code: 'BadRequest',
message: MESSAGE
});
done();
});
});
// eslint-disable-next-line
it('should respond to non-strict key/val query param value with 400', function(done) {
SERVER.pre(
restify.plugins.pre.strictQueryParams({
message: MESSAGE
})
);
SERVER.use(
restify.plugins.queryParser({
mapParams: true,
overrideParams: true
})
);
SERVER.get('/query/:id', function(req, res, next) {
res.send();
next();
});
CLIENT.get('/query/foo?id=bar&name=josep&jorge', function(err, _, res) {
assert.equal(typeof err, 'object');
assert.equal(res.statusCode, 400);
assert.deepEqual(JSON.parse(res.body), {
code: 'BadRequest',
message: MESSAGE
});
done();
});
});
it('should respond to valid query param value with 200', function(done) {
SERVER.pre(
restify.plugins.pre.strictQueryParams({
message: MESSAGE
})
);
SERVER.use(
restify.plugins.queryParser({
mapParams: true,
overrideParams: true
})
);
SERVER.get('/query/:id', function(req, res, next) {
assert.equal(req.params.id, 'bar');
assert.equal(req.params.name, 'josep & jorge');
assert.deepEqual(req.query, req.params);
res.send();
next();
});
CLIENT.get('/query/foo?id=bar&name=josep+%26+jorge', function(
err,
_,
res
) {
assert.equal(typeof err, 'object');
assert.equal(res.statusCode, 200);
done();
});
});
it('should respond 200 with scaped amp and s', function(done) {
SERVER.pre(
restify.plugins.pre.strictQueryParams({
message: MESSAGE
})
);
SERVER.use(
restify.plugins.queryParser({
mapParams: true,
overrideParams: true
})
);
SERVER.get('/query', function(req, res, next) {
assert.equal(req.params.id, 'bar');
assert.equal(req.params.name, 'josep & jorge');
assert.deepEqual(req.query, req.params);
res.send();
next();
});
CLIENT.get('/query?id=bar&name=josep%20%26%20jorge', function(
err,
_,
res
) {
assert.equal(typeof err, 'object');
assert.equal(res.statusCode, 200);
done();
});
});
});
================================================
FILE: test/plugins/testStaticFiles/docs/doc.md
================================================
#This is doc.md
================================================
FILE: test/plugins/testStaticFiles/docs/index.html
================================================
Document
testStaticFiles/docs/index.html
================================================
FILE: test/plugins/testStaticFiles/file1.txt
================================================
This is file1.txt
================================================
FILE: test/plugins/testStaticFiles/index.html
================================================
Document
testStaticFiles/index.html
================================================
FILE: test/plugins/testStaticFiles/special/$_$/bad (file).txt
================================================
This is a very badly named file.
================================================
FILE: test/plugins/throttle.test.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
/* eslint-disable func-names */
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
var restifyClients = require('restify-clients');
///--- Globals
var helper = require('../lib/helper');
var PORT = process.env.UNIT_TEST_PORT || 0;
var CLIENT;
var SERVER;
var errorMessage = 'Error message should include rate 0.5 r/s. Received: ';
function setupClientServer(ip, throttleOptions, done) {
var server = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
server.use(function ghettoAuthenticate(req, res, next) {
var username = req.url.match(/test\/([a-z]+)/)[1];
if (username) {
req.username = username;
}
next();
});
server.use(restify.plugins.throttle(throttleOptions));
server.get('/test/:name', function(req, res, next) {
res.send();
next();
});
server.listen(PORT, ip, function() {
PORT = server.address().port;
var client = restifyClients.createJsonClient({
url: 'http://' + ip + ':' + PORT,
dtrace: helper.dtrace,
retry: false
});
done(client, server);
});
}
///--- Tests
describe('throttle plugin', function() {
before(function setup(done) {
setupClientServer(
'127.0.0.1',
{
burst: 1,
rate: 0.5,
username: true,
overrides: {
admin: {
burst: 0,
rate: 0
},
special: {
burst: 3,
rate: 1
}
}
},
function setupGlobal(client, server) {
CLIENT = client;
SERVER = server;
done();
}
);
});
after(function teardown(done) {
CLIENT.close();
SERVER.close(done);
});
it('ok', function(done) {
CLIENT.get('/test/throttleMe', function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('throttled', function(done) {
this.timeout(3000);
CLIENT.get('/test/throttleMe', function(err, _, res) {
assert.ok(err);
assert.equal(err.statusCode, 429);
assert.ok(
err.message.indexOf('0.5 r/s') !== -1,
errorMessage + (err && err.message)
);
assert.equal(res.statusCode, 429);
setTimeout(function() {
done();
}, 2100);
});
});
it('ok after tokens', function(done) {
CLIENT.get('/test/throttleMe', function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('override limited', function(done) {
CLIENT.get('/test/special', function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('override limited (not throttled)', function(done) {
CLIENT.get('/test/special', function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('throttled after limited override', function(done) {
CLIENT.get('/test/throttleMe', function() {
CLIENT.get('/test/throttleMe', function(err, _, res) {
assert.ok(err);
assert.equal(res.statusCode, 429);
assert.ok(
err.message.indexOf('0.5 r/s') !== -1,
errorMessage + (err && err.message)
);
done();
});
});
});
it('override unlimited', function(done) {
CLIENT.get('/test/admin', function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('override unlimited (not throttled)', function(done) {
CLIENT.get('/test/admin', function(err, _, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
done();
});
});
it('throttled after unlimited override', function(done) {
CLIENT.get('/test/throttleMe', function() {
CLIENT.get('/test/throttleMe', function(err, _, res) {
assert.ok(err);
assert.equal(res.statusCode, 429);
assert.ok(
err.message.indexOf('0.5 r/s') !== -1,
errorMessage + (err && err.message)
);
done();
});
});
});
it('should not expose rate limit headers per default', function(done) {
CLIENT.get('/test/throttleMe', function(err, _, res) {
assert.isUndefined(res.headers['x-ratelimit-limit']);
assert.isUndefined(res.headers['x-ratelimit-rate']);
assert.isUndefined(res.headers['x-ratelimit-rate']);
done();
});
});
describe('expose headers', function() {
before(function(done) {
// close global server before creating a new to avoid port conflicts
CLIENT.close();
SERVER.close(done);
});
it('should expose headers on options set', function(done) {
// setup a new server with headers set to true since we cant
// change throttle options after init
setupClientServer(
'127.0.0.1',
{
burst: 17,
rate: 0.1,
username: true,
setHeaders: true
},
function setupWithHeaders(client, server) {
client.get('/test/throttleMe', function(err, req, res) {
assert.equal(res.headers['x-ratelimit-limit'], '17');
assert.equal(res.headers['x-ratelimit-rate'], '0.1');
assert.equal(
res.headers['x-ratelimit-remaining'],
'16'
);
// it should count down
client.get('/test/throttleMe', function(
nextErr,
nextReq,
nextRes
) {
assert.equal(
nextRes.headers['x-ratelimit-limit'],
'17'
);
assert.equal(
nextRes.headers['x-ratelimit-rate'],
'0.1'
);
assert.equal(
nextRes.headers['x-ratelimit-remaining'],
'15'
);
client.close();
server.close(done);
});
});
}
);
});
});
});
================================================
FILE: test/plugins/userAgent.test.js
================================================
'use strict';
/* eslint-disable func-names */
// core requires
var child_process = require('child_process');
var http = require('http');
// external requires
var assert = require('chai').assert;
var restify = require('../../lib/index.js');
// local files
var helper = require('../lib/helper');
// local globals
var SERVER;
var SERVER_PORT;
var SERVER_ADDRESS = '127.0.0.1';
var SERVER_ENDPOINT;
var TEST_ENDPOINT;
var TEST_RESPONSE_DATA = 'foobar';
var TEST_RESPONSE_DATA_LENGTH = TEST_RESPONSE_DATA.length;
var TEST_PATH = '/test/userAgent';
describe('userAgent pre-route handler', function() {
beforeEach(function(done) {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
// Enable the user agent pre-route handler, since this is the component
// under test.
SERVER.use(restify.plugins.pre.userAgentConnection());
SERVER.head('/test/:name', function(req, res, next) {
// Explicitly set Content-Length response header so that we can test
// for its removal (or lack thereof) by the userAgentConnection
// pre-route handler in tests below.
res.setHeader('Content-Length', TEST_RESPONSE_DATA_LENGTH);
res.send(200, TEST_RESPONSE_DATA);
next();
});
SERVER.listen(0, SERVER_ADDRESS, function() {
SERVER_PORT = SERVER.address().port;
SERVER_ENDPOINT = SERVER_ADDRESS + ':' + SERVER_PORT;
TEST_ENDPOINT = SERVER_ENDPOINT + TEST_PATH;
done();
});
});
afterEach(function(done) {
SERVER.close(done);
});
// By default, the userAgentConnection pre-route handler must:
//
// 1. set the 'connection' header to 'close'
//
// 2. remove the content-length header from the response
//
// when a HEAD request is handled and the client's user agent is curl.
it('sets proper headers for HEAD requests from curl', function(done) {
var CURL_CMD = ['curl', '-sS', '-i', TEST_ENDPOINT, '-X', 'HEAD'].join(
' '
);
child_process.exec(CURL_CMD, function onExec(err, stdout, stderr) {
assert.ifError(err);
var lines = stdout.split(/\n/);
var contentLengthHeaderNotPresent = lines.every(
function checkContentLengthNotPresent(line) {
return /Content-Length:.*/.test(line) === false;
}
);
var connectionCloseHeaderPresent = lines.some(
function checkConnectionClosePresent(line) {
return /Connection: close/.test(line);
}
);
assert.ok(contentLengthHeaderNotPresent);
assert.ok(connectionCloseHeaderPresent);
done();
});
});
// When handling a HEAD request, and if the client's user agent is not curl,
// the userAgentConnection should not remove the content-length header from
// the response, and it should not replace the value of the 'connection'
// header by 'close'.
// eslint-disable-next-line
it('sets proper headers for HEAD requests from non-curl clients', function(done) {
var req = http.request(
{
hostname: SERVER_ADDRESS,
port: SERVER_PORT,
path: TEST_PATH,
method: 'HEAD',
headers: {
'user-agent': 'foobar',
connection: 'keep-alive'
}
},
function onResponse(res) {
var responseHeaders = res.headers;
assert.ok(responseHeaders.hasOwnProperty('content-length'));
assert.equal(responseHeaders.connection, 'keep-alive');
// destroy the socket explicitly now since the request was
// explicitly requesting to not destroy the socket by setting
// its connection header to 'keep-alive'.
req.abort();
done();
}
);
req.on('error', function onReqError(err) {
assert.ifError(err);
done();
});
req.end();
});
});
================================================
FILE: test/plugins/utilsHrTimeDurationInMs.test.js
================================================
'use strict';
/* eslint-disable func-names */
var assert = require('chai').assert;
var hrTimeDurationInMs = require('../../lib/plugins/utils/hrTimeDurationInMs');
describe('utils #hrTimeDurationInMs', function() {
it('should return with duration', function() {
var startTime = [0, 0];
var endTime = [1, 1e6];
var duration = hrTimeDurationInMs(startTime, endTime);
assert.equal(duration, 1001);
});
});
================================================
FILE: test/request.test.js
================================================
'use strict';
/* eslint-disable func-names */
var restifyClients = require('restify-clients');
var validator = require('validator');
var restify = require('../lib');
if (require.cache[__dirname + '/lib/helper.js']) {
delete require.cache[__dirname + '/lib/helper.js'];
}
var helper = require('./lib/helper.js');
///--- Globals
var after = helper.after;
var before = helper.before;
var test = helper.test;
var PORT = process.env.UNIT_TEST_PORT || 0;
var CLIENT;
var SERVER;
before(function(cb) {
try {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});
SERVER.listen(PORT, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
cb();
});
} catch (e) {
console.error(e.stack);
process.exit(1);
}
});
after(function(cb) {
try {
CLIENT.close();
SERVER.close(function() {
CLIENT = null;
SERVER = null;
cb();
});
} catch (e) {
console.error(e.stack);
process.exit(1);
}
});
test('query should return empty string', function(t) {
SERVER.get('/emptyQs', function(req, res, next) {
t.equal(req.query(), '');
t.equal(req.getQuery(), '');
res.send();
next();
});
CLIENT.get('/emptyQs', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.end();
});
});
test('query should return raw query string string', function(t) {
SERVER.get('/qs', function(req, res, next) {
t.equal(req.query(), 'a=1&b=2');
t.equal(req.getQuery(), 'a=1&b=2');
res.send();
next();
});
CLIENT.get('/qs?a=1&b=2', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.end();
});
});
test('should generate request id on first req.id() call', function(t) {
SERVER.get('/ping', function(req, res, next) {
t.equal(typeof req.id(), 'string');
t.equal(validator.isUUID(req.id(), 4), true);
res.send();
return next();
});
CLIENT.get('/ping', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.end();
});
});
test('should set request id', function(t) {
SERVER.pre(function setId(req, res, next) {
var newId = req.id('lagavulin');
t.equal(newId, 'lagavulin');
return next();
});
SERVER.get('/ping', function(req, res, next) {
t.equal(typeof req.id(), 'string');
t.equal(req.id(), 'lagavulin');
res.send();
return next();
});
CLIENT.get('/ping', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.end();
});
});
test('should throw when setting request id after autogeneration', function(t) {
SERVER.get('/ping', function(req, res, next) {
t.equal(typeof req.id(), 'string');
t.equal(validator.isUUID(req.id(), 4), true);
t.throws(
function() {
req.id('blowup');
},
Error,
'request id is immutable, cannot be set again!'
);
res.send();
return next();
});
CLIENT.get('/ping', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.end();
});
});
test('should throw when setting request id twice', function(t) {
SERVER.get('/ping', function(req, res, next) {
req.id('lagavulin');
t.throws(
function() {
req.id('blowup');
},
Error,
'request id is immutable, cannot be set again!'
);
res.send();
return next();
});
CLIENT.get('/ping', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.end();
});
});
test('should provide route object', function(t) {
SERVER.get('/ping/:name', function(req, res, next) {
/*
req.getRoute() should return something like this :
{
path: '/ping/:name',
method: 'GET',
versions: [],
name: 'getpingname'
}
*/
var routeInfo = req.getRoute();
t.equal(routeInfo.path, '/ping/:name');
t.equal(routeInfo.method, 'GET');
res.send({ name: req.params.name });
return next();
});
CLIENT.get('/ping/lagavulin', function(err, _, res, parsedBody) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.deepEqual(parsedBody, { name: 'lagavulin' });
t.end();
});
});
test('should provide time when request started', function(t) {
SERVER.get('/ping/:name', function(req, res, next) {
t.equal(typeof req.time(), 'number');
t.ok(req.time() > Date.now() - 1000);
t.ok(req.time() <= Date.now());
res.send('ok');
return next();
});
CLIENT.get('/ping/lagavulin', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.end();
});
});
test('should provide date when request started', function(t) {
SERVER.get('/ping/:name', function(req, res, next) {
t.ok(req.date() instanceof Date);
t.ok(req.date().getTime() > Date.now() - 1000);
t.ok(req.date().getTime() <= Date.now());
res.send('ok');
return next();
});
CLIENT.get('/ping/lagavulin', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.end();
});
});
// restifyDone is emitted at the same time when server's after event is emitted,
// you can find more comprehensive testing for `after` lives in server tests.
test('should emit restifyDone event when request is fully served', function(t) {
var restifyDoneCalled = false;
SERVER.get('/', function(req, res, next) {
req.on('restifyDone', function(route, err) {
t.ifError(err);
t.ok(route);
setImmediate(function() {
restifyDoneCalled = true;
});
});
res.send('hello');
return next();
});
CLIENT.get('/', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.ok(restifyDoneCalled);
t.end();
});
});
// eslint-disable-next-line max-len
test('should emit restifyDone event when request is fully served with error', function(t) {
var clientDone = false;
SERVER.get('/', function(req, res, next) {
var myErr = new Error('My Error');
req.on('restifyDone', function(route, err) {
t.ok(route);
t.deepEqual(err, myErr);
setImmediate(function() {
t.ok(clientDone);
t.end();
});
});
return next(myErr);
});
CLIENT.get('/', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 500);
clientDone = true;
});
});
================================================
FILE: test/response.test.js
================================================
'use strict';
/* eslint-disable func-names */
var url = require('url');
var restifyClients = require('restify-clients');
var errs = require('restify-errors');
var restify = require('../lib');
if (require.cache[__dirname + '/lib/helper.js']) {
delete require.cache[__dirname + '/lib/helper.js'];
}
var helper = require('./lib/helper.js');
///--- Globals
var after = helper.after;
var before = helper.before;
var test = helper.test;
var PORT = process.env.UNIT_TEST_PORT || 0;
var CLIENT;
var STRING_CLIENT;
var SERVER;
var LOCALHOST;
var SLOCALHOST;
before(function(cb) {
try {
SERVER = restify.createServer({
dtrace: helper.dtrace,
handleUncaughtExceptions: true,
log: helper.getLog('server'),
version: ['2.0.0', '0.5.4', '1.4.3']
});
SERVER.use(restify.plugins.queryParser());
SERVER.listen(PORT, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
STRING_CLIENT = restifyClients.createStringClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
LOCALHOST = 'http://' + '127.0.0.1:' + PORT;
SLOCALHOST = 'https://' + '127.0.0.1:' + PORT;
cb();
});
} catch (e) {
console.error(e.stack);
process.exit(1);
}
});
after(function(cb) {
try {
CLIENT.close();
STRING_CLIENT.close();
SERVER.close(function() {
CLIENT = null;
SERVER = null;
cb();
});
} catch (e) {
console.error(e.stack);
process.exit(1);
}
});
// helper for joining array into strings
function join() {
var args = [].slice.call(arguments, 0);
return args.join('');
}
test('redirect to new string url as-is', function(t) {
SERVER.get('/1', function(req, res, next) {
res.redirect('www.foo.com', next);
});
CLIENT.get(join(LOCALHOST, '/1'), function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 302);
t.equal(res.headers.location, 'www.foo.com');
t.end();
});
});
test('redirect to new relative string url as-is', function(t) {
SERVER.get('/20', function(req, res, next) {
res.redirect('/1', next);
});
CLIENT.get(join(LOCALHOST, '/20?a=1'), function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 302);
t.equal(res.headers.location, '/1');
t.end();
});
});
test('redirect to current url (reload)', function(t) {
SERVER.get('/2', function(req, res, next) {
res.redirect(
{
reload: true
},
next
);
});
CLIENT.get(join(LOCALHOST, '/2'), function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 302);
t.equal(res.headers.location, join(LOCALHOST, '/2'));
t.end();
});
});
test('redirect to current url from http -> https', function(t) {
SERVER.get('/3', function(req, res, next) {
res.redirect(
{
secure: true
},
next
);
});
CLIENT.get(join(LOCALHOST, '/3'), function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 302);
t.equal(res.headers.location, join(SLOCALHOST, '/3'));
t.end();
});
});
test('redirect to current url from https -> http', function(t) {
SERVER.get('/3', function(req, res, next) {
res.redirect(
{
reload: true,
secure: false
},
next
);
});
CLIENT.get(join(LOCALHOST, '/3'), function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 302);
t.equal(res.headers.location, join(LOCALHOST, '/3'));
t.end();
});
});
test('redirect by changing path', function(t) {
SERVER.get('/4', function(req, res, next) {
res.redirect(
{
pathname: '1'
},
next
);
});
CLIENT.get(join(LOCALHOST, '/4'), function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 302);
t.equal(res.headers.location, join(LOCALHOST, '/1'));
t.end();
});
});
test(
'GH-1494: redirect should succeed even if req.url does not specify host' +
' or protocol',
function(t) {
SERVER.get('/5', function(req, res, next) {
res.redirect(
{
pathname: '/'
},
next
);
});
// use a relative URL here instead of request with full
// protocol and host.
// this causes node to receive different values for req.url,
// which affects
// how reconstruction of the redirect URL is done. for example including
// full host will result in a req.url value of:
// http://127.0.0.1:57824/5
// using relative URL results in a req.url value of:
// /5
// this causes a bug as documented in GH-1494
CLIENT.get('/5', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 302);
t.equal(res.headers.location, '/');
t.end();
});
}
);
test('redirect should add query params', function(t) {
SERVER.get('/5', function(req, res, next) {
res.redirect(
{
query: {
a: 1
}
},
next
);
});
CLIENT.get(join(LOCALHOST, '/5'), function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 302);
t.equal(res.headers.location, join(LOCALHOST, '/5?a=1'));
t.end();
});
});
test('redirect should extend existing query params', function(t) {
SERVER.get('/6', function(req, res, next) {
res.redirect(
{
query: {
b: 2
}
},
next
);
});
CLIENT.get(join(LOCALHOST, '/6?a=1'), function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 302);
var parsedUrl = url.parse(res.headers.location, true);
t.deepEqual(parsedUrl.query, {
a: 1,
b: 2
});
t.equal(parsedUrl.query.b, 2);
t.equal(parsedUrl.pathname, '/6');
// t.equal(res.headers.location, join(LOCALHOST, '/6?a=1&b=2'));
t.end();
});
});
test('redirect should stomp over existing query params', function(t) {
SERVER.get('/7', function(req, res, next) {
res.redirect(
{
overrideQuery: true,
query: {
b: 2
}
},
next
);
});
CLIENT.get(join(LOCALHOST, '/7?a=1'), function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 302);
t.equal(res.headers.location, join(LOCALHOST, '/7?b=2'));
t.end();
});
});
test('redirect with 301 status code', function(t) {
SERVER.get('/8', function(req, res, next) {
res.redirect(
{
permanent: true
},
next
);
});
CLIENT.get(join(LOCALHOST, '/8'), function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 301);
t.equal(res.headers.location, join(LOCALHOST, '/8'));
t.end();
});
});
test('redirect with 301 status code ising string url', function(t) {
SERVER.get('/30', function(req, res, next) {
res.redirect(301, '/foo', next);
});
CLIENT.get(join(LOCALHOST, '/30'), function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 301);
t.equal(res.headers.location, '/foo');
t.end();
});
});
test('redirect using options.url', function(t) {
SERVER.get('/8', function(req, res, next) {
res.redirect(
{
hostname: 'www.foo.com',
pathname: '/8',
query: {
a: 1
}
},
next
);
});
CLIENT.get(join(LOCALHOST, '/8'), function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 302);
t.equal(res.headers.location, 'http://www.foo.com/8?a=1');
t.end();
});
});
test('redirect using opts.port', function(t) {
SERVER.get('/9', function(req, res, next) {
res.redirect(
{
port: 3000
},
next
);
});
CLIENT.get(join(LOCALHOST, '/9'), function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 302);
var parsedUrl = url.parse(res.headers.location, true);
t.equal(parsedUrl.port, 3000);
t.end();
});
});
test('redirect using external url and custom port', function(t) {
SERVER.get('/9', function(req, res, next) {
res.redirect(
{
hostname: 'www.foo.com',
pathname: '/99',
port: 3000
},
next
);
});
CLIENT.get(join(LOCALHOST, '/9'), function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 302);
var parsedUrl = url.parse(res.headers.location, true);
t.equal(parsedUrl.port, 3000);
t.equal(parsedUrl.hostname, 'www.foo.com');
t.equal(parsedUrl.pathname, '/99');
t.end();
});
});
test('redirect using default hostname with custom port', function(t) {
SERVER.get('/9', function(req, res, next) {
res.redirect(
{
pathname: '/99',
port: 3000
},
next
);
});
CLIENT.get(join(LOCALHOST, '/9'), function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 302);
var parsedUrl = url.parse(res.headers.location, true);
t.equal(parsedUrl.port, 3000);
t.equal(parsedUrl.pathname, '/99');
t.equal(res.headers.location, 'http://127.0.0.1:3000/99');
t.end();
});
});
// eslint-disable-next-line
test('redirect should cause InternalError when invoked without next', function(t) {
SERVER.get('/9', function(req, res, next) {
res.redirect();
});
CLIENT.get(join(LOCALHOST, '/9'), function(err, _, res, body) {
t.equal(res.statusCode, 500);
// json parse the response
t.equal(body.code, 'Internal');
t.end();
});
});
// eslint-disable-next-line
test('redirect should call next with false to stop handler stack execution', function(t) {
var wasRun = false;
function A(req, res, next) {
req.a = 1;
next();
}
function B(req, res, next) {
req.b = 2;
wasRun = true;
next();
}
function redirect(req, res, next) {
res.redirect('/10', next);
}
SERVER.get('/10', [A, redirect, B]);
CLIENT.get(join(LOCALHOST, '/10'), function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 302);
t.equal(res.headers.location, '/10');
// handler B should not be executed
t.equal(wasRun, false);
t.end();
});
});
test('redirect should emit a redirect event', function(t) {
var wasEmitted = false;
var redirectLocation;
function preRedirectHandler(req, res, next) {
res.on('redirect', function(payload) {
wasEmitted = true;
redirectLocation = payload;
});
next();
}
function redirect(req, res, next) {
res.redirect('/10', next);
}
SERVER.get('/10', [preRedirectHandler, redirect]);
CLIENT.get(join(LOCALHOST, '/10'), function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 302);
t.equal(res.headers.location, '/10');
// event 'redirect' should have been emitted
t.equal(wasEmitted, true);
t.equal(redirectLocation, '/10');
t.end();
});
});
test('writeHead should emit a header event', function(t) {
var wasEmitted = false;
var payloadPlaceholder;
// writeHead is called on each request
function handler(req, res, next) {
res.on('header', function(payload) {
wasEmitted = true;
payloadPlaceholder = payload;
});
res.send(302);
next();
}
SERVER.get('/10', [handler]);
CLIENT.get(join(LOCALHOST, '/10'), function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 302);
// event 'header' should have been emitted
t.equal(wasEmitted, true);
t.equal(payloadPlaceholder, undefined);
t.end();
});
});
test('should fail to set header due to missing formatter', function(t) {
// when a formatter is not set up for a specific content-type, restify will
// default to octet-stream.
SERVER.get('/11', function handle(req, res, next) {
res.header('content-type', 'application/hal+json');
res.send(200, JSON.stringify({ hello: 'world' }));
return next();
});
CLIENT.get(join(LOCALHOST, '/11'), function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.equal(res.headers['content-type'], 'application/octet-stream');
t.end();
});
});
test('should not fail to send null as body', function(t) {
SERVER.get('/12', function handle(req, res, next) {
res.send(200, null);
return next();
});
CLIENT.get(join(LOCALHOST, '/12'), function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.end();
});
});
test('should not fail to send null as body without status code', function(t) {
SERVER.get('/13', function handle(req, res, next) {
res.send(null);
return next();
});
CLIENT.get(join(LOCALHOST, '/13'), function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.end();
});
});
test('should prefer explicit status code over error status code', function(t) {
SERVER.get('/14', function handle(req, res, next) {
res.send(200, new errs.InternalServerError('boom'));
return next();
});
CLIENT.get(join(LOCALHOST, '/14'), function(err, _, res, body) {
t.ifError(err);
t.equal(res.statusCode, 200);
// ensure error body was still sent
t.equal(body.code, 'InternalServer');
t.equal(body.message, 'boom');
t.end();
});
});
test('GH-951: should send without formatting', function(t) {
SERVER.get('/15', function handle(req, res, next) {
res.header('content-type', 'application/json');
res.sendRaw(
200,
JSON.stringify({
hello: 'world'
})
);
return next();
});
STRING_CLIENT.get(join(LOCALHOST, '/15'), function(err, _, res, body) {
t.ifError(err);
t.equal(
body,
JSON.stringify({
hello: 'world'
})
);
t.end();
});
});
test('GH-951: sendRaw accepts only strings or buffers', function(t) {
SERVER.on('uncaughtException', function(req, res, route, err) {
t.ok(err);
// Node v8 uses static error codes
// and `name` includes the error name and error code as well which
// caused this test to break. Just changing the logic to check for
// string instead
t.equal(err.name.indexOf('AssertionError') >= 0, true);
t.equal(err.message, 'res.sendRaw() accepts only strings or buffers');
t.end();
});
SERVER.get('/16', function handle(req, res, next) {
res.header('content-type', 'application/json');
res.sendRaw(200, {
hello: 'world'
});
return next();
});
// throw away response, we don't need it.
STRING_CLIENT.get(join(LOCALHOST, '/16'));
});
test('GH-1429: setting code with res.status not respected', function(t) {
SERVER.get('/404', function(req, res, next) {
res.status(404);
res.send(null);
});
CLIENT.get(join(LOCALHOST, '/404'), function(err, _, res) {
t.equal(res.statusCode, 404);
t.end();
});
});
test('should support multiple set-cookie headers', function(t) {
SERVER.get('/set-cookie', function(req, res, next) {
res.header('Set-Cookie', 'a=1');
res.header('Set-Cookie', 'b=2');
res.send(null);
});
CLIENT.get(join(LOCALHOST, '/set-cookie'), function(err, _, res) {
t.equal(res.headers['set-cookie'].length, 2);
t.end();
});
});
test('GH-1607: should send bools with explicit status code', function(t) {
SERVER.get('/bool/:value', function(req, res, next) {
res.send(200, req.params.value === 'true' ? true : false);
return next();
});
STRING_CLIENT.get(join(LOCALHOST, '/bool/false'), function(
err,
req,
res,
data
) {
t.equal(data, 'false');
STRING_CLIENT.get(join(LOCALHOST, '/bool/true'), function(
err2,
req2,
res2,
data2
) {
t.equal(data2, 'true');
t.end();
});
});
});
test('GH-1607: should send numbers with explicit status code', function(t) {
SERVER.get('/zero', function(req, res, next) {
res.send(200, 0);
return next();
});
SERVER.get('/one', function(req, res, next) {
res.send(200, 1);
return next();
});
STRING_CLIENT.get(join(LOCALHOST, '/zero'), function(err, req, res, data) {
t.equal(data, '0');
STRING_CLIENT.get(join(LOCALHOST, '/one'), function(
err2,
req2,
res2,
data2
) {
t.equal(data2, '1');
t.end();
});
});
});
test('GH-1791: should send 0 as 0 with application/json', function(t) {
SERVER.get('/zero', function(req, res, next) {
res.contentType = 'application/json';
res.send(200, 0);
return next();
});
STRING_CLIENT.get(join(LOCALHOST, '/zero'), function(err, req, res, data) {
t.equal(data, '0');
t.end();
});
});
test('GH-1791: should send false as false with application/json', function(t) {
SERVER.get('/false', function(req, res, next) {
res.contentType = 'application/json';
res.send(200, false);
return next();
});
STRING_CLIENT.get(join(LOCALHOST, '/false'), function(err, req, res, data) {
t.equal(data, 'false');
t.end();
});
});
// eslint-disable-next-line
test('GH-1791: should send empty string as "" with application/json', function(t) {
SERVER.get('/empty', function(req, res, next) {
res.contentType = 'application/json';
res.send(200, '');
return next();
});
STRING_CLIENT.get(join(LOCALHOST, '/empty'), function(err, req, res, data) {
t.equal(data, '""');
t.end();
});
});
test('GH-1791: should send null as null with application/json', function(t) {
SERVER.get('/null', function(req, res, next) {
res.contentType = 'application/json';
res.send(200, null);
return next();
});
STRING_CLIENT.get(join(LOCALHOST, '/null'), function(err, req, res, data) {
t.equal(data, 'null');
t.end();
});
});
// eslint-disable-next-line
test('GH-1791: should send undefined as empty with application/json', function(t) {
SERVER.get('/undef', function(req, res, next) {
res.contentType = 'application/json';
res.send(200, undefined);
return next();
});
STRING_CLIENT.get(join(LOCALHOST, '/undef'), function(err, req, res, data) {
t.equal(data, '');
t.end();
});
});
test('GH-1791: should send NaN as null with application/json', function(t) {
SERVER.get('/nan', function(req, res, next) {
res.contentType = 'application/json';
res.send(200, NaN);
return next();
});
STRING_CLIENT.get(join(LOCALHOST, '/nan'), function(err, req, res, data) {
t.equal(data, 'null');
t.end();
});
});
================================================
FILE: test/router.test.js
================================================
'use strict';
/* eslint-disable func-names */
var restify = require('../lib');
var Router = require('../lib/router');
var clients = require('restify-clients');
var _ = require('lodash');
if (require.cache[__dirname + '/lib/helper.js']) {
delete require.cache[__dirname + '/lib/helper.js'];
}
var helper = require('./lib/helper.js');
///--- Globals
var test = helper.test;
var mockReq = {
params: {},
connectionState: function() {
return '';
},
startHandlerTimer: function() {},
endHandlerTimer: function() {}
};
var mockRes = {
setHeader: function() {},
send: function() {}
};
///--- Tests
test('mounts a route', function(t) {
function handler(req, res, next) {
res.send('Hello world');
}
var router = new Router({
log: {}
});
router.mount({ method: 'GET', path: '/' }, [handler]);
router.mount({ method: 'POST', path: '/' }, [handler]);
router.mount({ method: 'GET', path: '/ab' }, [handler]);
t.deepEqual(Object.keys(router.getRoutes()), ['get', 'post', 'getab']);
// Route names are unique
router.mount({ name: 'get', method: 'GET', path: '/get' }, [handler]);
router.mount({ method: 'GET', path: '/a/b' }, [handler]);
t.deepEqual(
_.uniq(Object.keys(router.getRoutes())),
Object.keys(router.getRoutes())
);
t.done();
});
test('unmounts a route', function(t) {
function handler(req, res, next) {
res.send('Hello world');
}
var router = new Router({
log: {}
});
// Mount
router.mount({ method: 'GET', path: '/a' }, [handler]);
router.mount({ method: 'POST', path: '/b' }, [handler]);
t.deepEqual(Object.keys(router.getRoutes()), ['geta', 'postb']);
// Unmount
var route = router.unmount('geta');
t.ok(route);
t.equal(route.name, 'geta');
// Removes from mounted routes
t.deepEqual(Object.keys(router.getRoutes()), ['postb']);
// 404
var handlerFound = router.lookup(
Object.assign(
{
getUrl: function() {
return { pathname: '/a' };
},
method: 'GET'
},
mockReq
),
mockRes
);
t.notOk(handlerFound);
t.end();
});
test('unmounts a route that does not exist', function(t) {
function handler(req, res, next) {
res.send('Hello world');
}
var router = new Router({
log: {}
});
// Mount
router.mount({ method: 'GET', path: '/a' }, [handler]);
t.notOk(router.unmount('non-existing'));
t.end();
});
test('clean up xss for 404', function(t) {
var server = restify.createServer();
server.listen(3000, function(listenErr) {
t.ifError(listenErr);
var client = clients.createStringClient({
url: 'http://127.0.0.1:3000/'
});
client.get(
{
path:
'/no5_such3_file7.pl?%22%3E%3Cscript%3Ealert(73541);%3C/' +
'script%3E',
headers: {
connection: 'close'
}
},
function(clientErr, req, res, data) {
t.ok(clientErr);
t.ok(
data.indexOf('%22%3E%3Cscript%3Ealert(73541)') === -1,
'should not reflect raw url'
);
server.close(function() {
t.end();
});
}
);
});
});
test('lookupByName runs a route by name and calls next', function(t) {
var router = new Router({
log: {}
});
function handler(req, res, next) {
res.send('hello world');
next();
}
router.mount({ method: 'GET', path: '/', name: 'my-route' }, [handler]);
var handlerFound = router.lookupByName('my-route', mockReq, mockRes);
t.ok(handlerFound);
handlerFound(mockReq, mockRes, function next(err) {
t.ifError(err);
t.end();
});
});
test('lookupByName calls next with err', function(t) {
var router = new Router({
log: {}
});
var myErr = new Error('My Error');
router.mount({ method: 'GET', path: '/', name: 'my-route' }, [
function(req, res, next) {
next(myErr);
}
]);
var handlerFound = router.lookupByName('my-route', mockReq, mockRes);
t.ok(handlerFound);
handlerFound(mockReq, mockRes, function next(err) {
t.deepEqual(err, myErr);
t.end();
});
});
test('lookup runs a route chain by path and calls next', function(t) {
var router = new Router({
log: {}
});
router.mount({ method: 'GET', path: '/', name: 'my-route' }, [
function(req, res, next) {
res.send('Hello world');
next(); // no _afterRoute without next()
}
]);
var handlerFound = router.lookup(
Object.assign(
{
getUrl: function() {
return { pathname: '/' };
},
method: 'GET'
},
mockReq
),
mockRes
);
t.ok(handlerFound);
handlerFound(mockReq, mockRes, function next(err) {
t.ifError(err);
t.end();
});
});
test('lookup calls next with err', function(t) {
var router = new Router({
log: {}
});
var myErr = new Error('My Error');
router.mount({ method: 'GET', path: '/', name: 'my-route' }, [
function(req, res, next) {
next(myErr);
}
]);
var handlerFound = router.lookup(
Object.assign(
{
getUrl: function() {
return { pathname: '/' };
},
method: 'GET'
},
mockReq
),
mockRes
);
t.ok(handlerFound);
handlerFound(mockReq, mockRes, function next(err) {
t.deepEqual(err, myErr);
t.end();
});
});
test('route handles 404', function(t) {
var router = new Router({
log: {}
});
router.defaultRoute(
Object.assign(
{
getUrl: function() {
return { pathname: '/' };
},
method: 'GET'
},
mockReq
),
mockRes,
function next(err) {
t.equal(err.statusCode, 404);
t.end();
}
);
});
test('route handles method not allowed (405)', function(t) {
var router = new Router({
log: {}
});
router.mount({ method: 'GET', path: '/', name: 'my-route' }, [
function(req, res, next) {
res.send('Hello world');
}
]);
router.defaultRoute(
Object.assign(
{
getUrl: function() {
return { pathname: '/' };
},
method: 'POST'
},
mockReq
),
mockRes,
function next(err) {
t.equal(err.statusCode, 405);
t.end();
}
);
});
test('prints debug info', function(t) {
function handler1(req, res, next) {
res.send('Hello world');
}
function handler2(req, res, next) {
res.send('Hello world');
}
var router = new Router({
log: {}
});
router.mount({ method: 'GET', path: '/' }, [handler1]);
router.mount({ method: 'POST', path: '/' }, [handler1, handler2]);
t.deepEqual(router.getDebugInfo(), {
get: {
name: 'get',
method: 'get',
path: '/',
handlers: [handler1]
},
post: {
name: 'post',
method: 'post',
path: '/',
handlers: [handler1, handler2]
}
});
t.end();
});
test('toString()', function(t) {
function handler(req, res, next) {
res.send('Hello world');
}
var router = new Router({
log: {}
});
router.mount({ method: 'GET', path: '/' }, [handler]);
router.mount({ method: 'GET', path: '/a' }, [handler]);
router.mount({ method: 'GET', path: '/a/b' }, [handler]);
router.mount({ method: 'POST', path: '/' }, [handler]);
t.deepEqual(
router.toString(),
// prettier-ignore
'└── / (GET, POST)\n' +
' └── a (GET)\n' +
' └── /b (GET)\n'
);
t.end();
});
test('toString() with ignoreTrailingSlash', function(t) {
function handler(req, res, next) {
res.send('Hello world');
}
var router = new Router({
log: {},
ignoreTrailingSlash: true
});
router.mount({ method: 'GET', path: '/' }, [handler]);
router.mount({ method: 'GET', path: '/a' }, [handler]);
router.mount({ method: 'GET', path: '/a/b' }, [handler]);
router.mount({ method: 'POST', path: '/' }, [handler]);
t.deepEqual(
router.toString(),
// prettier-ignore
'└── / (GET, POST)\n' +
' └── a (GET)\n' +
' └── /b (GET)\n'
);
t.end();
});
// Tests router.render()
var mockResponse = function respond(req, res, next) {
res.send(200);
};
test('render route', function(t) {
var server = restify.createServer();
server.get({ name: 'countries', path: '/countries' }, mockResponse);
server.get({ name: 'country', path: '/countries/:name' }, mockResponse);
server.get(
{ name: 'cities', path: '/countries/:name/states/:state/cities' },
mockResponse
);
var countries = server.router.render('countries', {});
t.equal(countries, '/countries');
var country = server.router.render('country', { name: 'Australia' });
t.equal(country, '/countries/Australia');
var cities = server.router.render('cities', {
name: 'Australia',
state: 'New South Wales'
});
t.equal(cities, '/countries/Australia/states/New%20South%20Wales/cities');
t.end();
});
test('render route (missing params)', function(t) {
var server = restify.createServer();
server.get(
{ name: 'cities', path: '/countries/:name/states/:state/cities' },
mockResponse
);
try {
server.router.render('cities', { name: 'Australia' });
} catch (ex) {
// server is expected to throw an error
// hence catching it here
t.equal(ex, 'Error: Route is missing parameter ');
}
t.end();
});
test('GH #704: render route (special charaters)', function(t) {
var server = restify.createServer();
server.get({ name: 'my-route', path: '/countries/:name' }, mockResponse);
var link = server.router.render('my-route', { name: 'AustraliaIsC@@!' });
// special charaacters are URI encoded
t.equal(link, '/countries/AustraliaIsC%40%40!');
t.end();
});
test('GH #704: render route (with sub-regex param)', function(t) {
var server = restify.createServer();
server.get(
{
name: 'my-route',
path: '/countries/:code([A-Z]{2,3})'
},
mockResponse
);
var link = server.router.render('my-route', { code: 'FR' });
t.equal(link, '/countries/FR');
link = server.router.render('my-route', { code: '111' });
t.equal(link, '/countries/111');
t.end();
});
test('GH-796: render route (with multiple sub-regex param)', function(t) {
var server = restify.createServer();
server.get(
{
name: 'my-route',
path: '/countries/:code([A-Z]{2,3})/:area([0-9]+)'
},
mockResponse
);
var link = server.router.render('my-route', { code: '111', area: 42 });
t.equal(link, '/countries/111/42');
t.end();
});
test('render route (with encode)', function(t) {
var server = restify.createServer();
server.get({ name: 'my-route', path: '/countries/:name' }, mockResponse);
var link = server.router.render('my-route', { name: 'Trinidad & Tobago' });
t.equal(link, '/countries/Trinidad%20%26%20Tobago');
t.end();
});
test('render route (query string)', function(t) {
var server = restify.createServer();
server.get({ name: 'country', path: '/countries/:name' }, mockResponse);
var country1 = server.router.render(
'country',
{
name: 'Australia'
},
{
state: 'New South Wales',
'cities/towns': 5
}
);
t.equal(
country1,
'/countries/Australia?state=New%20South%20Wales&cities%2Ftowns=5'
);
var country2 = server.router.render(
'country',
{
name: 'Australia'
},
{
state: 'NSW & VIC',
'cities&towns': 5
}
);
t.equal(
country2,
'/countries/Australia?state=NSW%20%26%20VIC&cities%26towns=5'
);
t.end();
});
================================================
FILE: test/routerRegistryRadix.test.js
================================================
'use strict';
/* eslint-disable func-names */
var RouterRegistryRadix = require('../lib/routerRegistryRadix');
var Chain = require('../lib/chain');
if (require.cache[__dirname + '/lib/helper.js']) {
delete require.cache[__dirname + '/lib/helper.js'];
}
var helper = require('./lib/helper.js');
///--- Globals
var test = helper.test;
function getTestRoute(opts) {
var chain = new Chain();
var name = opts.method + '-' + opts.path;
name = name.replace(/\W/g, '').toLowerCase();
return {
name: name,
method: opts.method,
path: opts.path,
spec: opts,
chain: chain
};
}
///--- Tests
test('adds a route', function(t) {
var registry = new RouterRegistryRadix();
registry.add(getTestRoute({ method: 'GET', path: '/' }));
registry.add(getTestRoute({ method: 'POST', path: '/' }));
registry.add(getTestRoute({ method: 'GET', path: '/ab' }));
t.deepEqual(Object.keys(registry.get()), ['get', 'post', 'getab']);
t.done();
});
test('removes a route', function(t) {
var registry = new RouterRegistryRadix();
// Mount
registry.add(getTestRoute({ method: 'GET', path: '/a' }));
registry.add(getTestRoute({ method: 'POST', path: '/b' }));
t.deepEqual(Object.keys(registry.get()), ['geta', 'postb']);
// Unmount
var route = registry.remove('geta');
t.ok(route);
t.equal(route.name, 'geta');
// Removes from registry
t.deepEqual(Object.keys(registry.get()), ['postb']);
t.end();
});
test('lookups a route', function(t) {
var registry = new RouterRegistryRadix();
var route = getTestRoute({ method: 'GET', path: '/a/:b' });
registry.add(route);
var result = registry.lookup('GET', '/a/b');
t.deepEqual(result, {
route: route,
params: { b: 'b' },
handler: result.handler
});
t.done();
});
test('get registered routes', function(t) {
var registry = new RouterRegistryRadix();
registry.add(getTestRoute({ method: 'GET', path: '/' }));
registry.add(getTestRoute({ method: 'GET', path: '/a' }));
registry.add(getTestRoute({ method: 'GET', path: '/a/b' }));
registry.add(getTestRoute({ method: 'POST', path: '/' }));
t.deepEqual(Object.keys(registry.get()), ['get', 'geta', 'getab', 'post']);
t.end();
});
test('toString()', function(t) {
var registry = new RouterRegistryRadix();
registry.add(getTestRoute({ method: 'GET', path: '/' }));
registry.add(getTestRoute({ method: 'GET', path: '/a' }));
registry.add(getTestRoute({ method: 'GET', path: '/a/b' }));
registry.add(getTestRoute({ method: 'POST', path: '/' }));
t.deepEqual(
registry.toString(),
// prettier-ignore
'└── / (GET, POST)\n' +
' └── a (GET)\n' +
' └── /b (GET)\n'
);
t.end();
});
test('toString() with ignoreTrailingSlash', function(t) {
var registry = new RouterRegistryRadix({ ignoreTrailingSlash: true });
registry.add(getTestRoute({ method: 'GET', path: '/' }));
registry.add(getTestRoute({ method: 'GET', path: '/a' }));
registry.add(getTestRoute({ method: 'GET', path: '/a/b' }));
registry.add(getTestRoute({ method: 'POST', path: '/' }));
t.deepEqual(
registry.toString(),
// prettier-ignore
'└── / (GET, POST)\n' +
' └── a (GET)\n' +
' └── /b (GET)\n'
);
t.end();
});
================================================
FILE: test/server.test.js
================================================
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
/* eslint-disable func-names */
const { AsyncLocalStorage } = require('async_hooks');
var assert = require('assert-plus');
var childprocess = require('child_process');
var http = require('http');
var pino = require('pino');
var errors = require('restify-errors');
var restifyClients = require('restify-clients');
var uuid = require('uuid');
var RestError = errors.RestError;
var restify = require('../lib');
if (require.cache[__dirname + '/lib/helper.js']) {
delete require.cache[__dirname + '/lib/helper.js'];
}
var helper = require('./lib/helper.js');
var StreamRecorder = require('./lib/streamRecorder');
///--- Globals
var after = helper.after;
var before = helper.before;
var test = helper.test;
var SKIP_IP_V6 = !!process.env.TEST_SKIP_IP_V6;
var PORT = process.env.UNIT_TEST_PORT || 0;
var CLIENT;
var FAST_CLIENT;
var SERVER;
let LOG_BUFFER;
var NODE_MAJOR_VERSION = process.versions.node.split('.')[0];
if (SKIP_IP_V6) {
console.warn('IPv6 tests are skipped: No IPv6 network is available');
}
///--- Tests
before(function(cb) {
try {
LOG_BUFFER = new StreamRecorder();
SERVER = restify.createServer({
dtrace: helper.dtrace,
handleUncaughtExceptions: true,
log: helper.getLog('server', LOG_BUFFER, 'info'),
version: ['2.0.0', '0.5.4', '1.4.3'],
ignoreTrailingSlash: true
});
SERVER.listen(PORT, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
FAST_CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false,
requestTimeout: 500
});
cb();
});
} catch (e) {
console.error(e.stack);
process.exit(1);
}
});
after(function(cb) {
try {
CLIENT.close();
FAST_CLIENT.close();
SERVER.close(function() {
CLIENT = null;
FAST_CLIENT = null;
SERVER = null;
cb();
});
} catch (e) {
console.error(e.stack);
process.exit(1);
}
});
test('listen and close (port only)', function(t) {
var server = restify.createServer();
server.listen(0, function() {
server.close(function() {
t.end();
});
});
});
test('listen and close (port only) w/ port number as string', function(t) {
var server = restify.createServer();
server.listen(String(0), function() {
server.close(function() {
t.end();
});
});
});
test('listen and close (socketPath)', function(t) {
var server = restify.createServer();
server.listen('/tmp/.' + uuid.v4(), function() {
server.close(function() {
t.end();
});
});
});
// Run IPv6 tests only if IPv6 network is available
if (!SKIP_IP_V6) {
test('gh-751 IPv4/IPv6 server URL', function(t) {
t.equal(SERVER.url, 'http://127.0.0.1:' + PORT, 'ipv4 url');
var server = restify.createServer();
server.listen(PORT + 1, '::1', function() {
t.equal(server.url, 'http://[::1]:' + (PORT + 1), 'ipv6 url');
server.close(function() {
t.end();
});
});
});
}
test('get (path only)', function(t) {
var r = SERVER.get('/foo/:id', function echoId(req, res, next) {
t.ok(req.params);
t.equal(req.params.id, 'bar');
t.equal(req.isUpload(), false);
res.send();
next();
});
var count = 0;
SERVER.once('after', function(req, res, route) {
t.ok(req);
t.ok(res);
t.equal(r, route.name);
if (++count === 2) {
t.end();
}
});
CLIENT.get('/foo/bar', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
if (++count === 2) {
t.end();
}
});
});
test('get (path only - with trailing slash)', function(t) {
SERVER.get('/foo/', function echoId(req, res, next) {
res.send();
next();
});
var count = 0;
CLIENT.get('/foo/', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
if (++count === 2) {
t.end();
}
});
CLIENT.get('/foo', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
if (++count === 2) {
t.end();
}
});
});
test('get (path only - with trailing slash and nested route)', function(t) {
SERVER.get('/foo/', function echoId(req, res, next) {
res.statusCode = 200;
res.send();
next();
});
SERVER.get('/foo/bar', function echoId(req, res, next) {
res.statusCode = 201;
res.send();
next();
});
var count = 0;
CLIENT.get('/foo/', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
if (++count === 4) {
t.end();
}
});
CLIENT.get('/foo', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
if (++count === 4) {
t.end();
}
});
CLIENT.get('/foo/bar/', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 201);
if (++count === 4) {
t.end();
}
});
CLIENT.get('/foo/bar', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 201);
if (++count === 4) {
t.end();
}
});
});
test('use + get (path only)', function(t) {
SERVER.use(function(req, res, next) {
next();
});
SERVER.get('/foo/:id', function tester(req, res, next) {
t.ok(req.params);
t.equal(req.params.id, 'bar');
res.send();
next();
});
CLIENT.get('/foo/bar', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.end();
});
});
test('rm', function(t) {
var routeName = SERVER.get('/foo/:id', function foosy(req, res, next) {
next();
});
SERVER.get('/bar/:id', function barsy(req, res, next) {
t.ok(req.params);
t.equal(req.params.id, 'foo');
res.send();
next();
});
t.ok(SERVER.rm(routeName));
CLIENT.get('/foo/bar', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 404);
CLIENT.get('/bar/foo', function(err2, __, res2) {
t.ifError(err2);
t.equal(res2.statusCode, 200);
t.end();
});
});
});
test(
'_routeErrorResponse does not cause uncaughtException when called when' +
'header has already been sent',
function(t) {
SERVER.on('MethodNotAllowed', function(req, res, error, next) {
res.json(405, { status: 'MethodNotAllowed' });
try {
next();
} catch (err) {
t.fail(
'next() should not throw error' +
'when header has already been sent'
);
}
t.end();
});
SERVER.post('/routePostOnly', function tester(req, res, next) {
next();
});
CLIENT.get('/routePostOnly', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 405);
});
}
);
test('use - throws TypeError on non function as argument', function(t) {
var errMsg = 'handler (function) is required';
t.throws(
function() {
SERVER.use('/nonfn');
},
assert.AssertionError,
errMsg
);
t.throws(
function() {
SERVER.use({ an: 'object' });
},
assert.AssertionError,
errMsg
);
t.throws(
function() {
SERVER.use(
function good(req, res, next) {
next();
},
'/bad',
{
really: 'bad'
}
);
},
assert.AssertionError,
errMsg
);
t.end();
});
test('405', function(t) {
SERVER.post('/foo/:id', function posty(req, res, next) {
t.ok(req.params);
t.equal(req.params.id, 'bar');
res.send();
next();
});
CLIENT.get('/foo/bar', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 405);
t.equal(res.headers.allow, 'POST');
t.end();
});
});
test('PUT ok', function(t) {
SERVER.put('/foo/:id', function tester(req, res, next) {
t.ok(req.params);
t.equal(req.params.id, 'bar');
t.equal(req.isUpload(), true);
res.send();
next();
});
CLIENT.put('/foo/bar', {}, function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.end();
});
});
test('PATCH ok', function(t) {
SERVER.patch('/foo/:id', function tester(req, res, next) {
t.ok(req.params);
t.equal(req.params.id, 'bar');
t.equal(req.isUpload(), true);
res.send();
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/foo/bar',
method: 'PATCH',
agent: false
};
http.request(opts, function(res) {
t.equal(res.statusCode, 200);
res.on('end', function() {
t.end();
});
res.resume();
}).end();
});
test('HEAD ok', function(t) {
SERVER.head('/foo/:id', function tester(req, res, next) {
t.ok(req.params);
t.equal(req.params.id, 'bar');
t.equal(req.isUpload(), false);
res.send('hi there');
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/foo/bar',
method: 'HEAD',
agent: false
};
http.request(opts, function(res) {
t.equal(res.statusCode, 200);
res.on('data', function(chunk) {
t.fail('Data was sent on HEAD');
});
res.on('end', function() {
t.end();
});
}).end();
});
test('DELETE ok', function(t) {
SERVER.del('/foo/:id', function tester(req, res, next) {
t.ok(req.params);
t.equal(req.params.id, 'bar');
t.equal(req.isUpload(), false);
res.send(204, 'hi there');
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/foo/bar',
method: 'DELETE',
agent: false
};
http.request(opts, function(res) {
t.equal(res.statusCode, 204);
res.on('data', function(chunk) {
t.fail('Data was sent on 204');
});
t.end();
}).end();
});
test('OPTIONS', function(t) {
['get', 'post', 'put', 'del'].forEach(function(method) {
SERVER[method]('/foo/:id', function tester(req, res, next) {
t.ok(req.params);
t.equal(req.params.id, 'bar');
res.send();
next();
});
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '*',
method: 'OPTIONS',
agent: false
};
http.request(opts, function(res) {
t.equal(res.statusCode, 200);
t.end();
}).end();
});
test('RegExp ok', function(t) {
SERVER.get('/example/:file(^\\d+).png', function tester(req, res, next) {
t.deepEqual(req.params, {
file: '12'
});
res.send('hi there');
next();
});
CLIENT.get('/example/12.png', function(err, _, res, obj) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.equal(obj, 'hi there');
t.end();
});
});
test('get (path and version ok)', function(t) {
SERVER.get(
{
url: '/foo/:id',
version: '1.2.3'
},
function tester(req, res, next) {
t.ok(req.params);
t.equal(req.params.id, 'bar');
res.send();
next();
}
);
var opts = {
path: '/foo/bar',
headers: {
'accept-version': '~1.2'
}
};
CLIENT.get(opts, function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.end();
});
});
test('GH-63 res.send 204 is sending a body', function(t) {
SERVER.del('/hello/:name', function tester(req, res, next) {
res.send(204);
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/hello/mark',
method: 'DELETE',
agent: false,
headers: {
accept: 'text/plain'
}
};
http.request(opts, function(res) {
t.equal(res.statusCode, 204);
var body = '';
res.setEncoding('utf8');
res.on('data', function(chunk) {
body += chunk;
});
res.on('end', function() {
t.notOk(body);
t.end();
});
}).end();
});
test('GH-64 prerouting chain', function(t) {
SERVER.pre(function(req, res, next) {
req.log.debug('testing log is set');
req.headers.accept = 'application/json';
next();
});
SERVER.get('/hello/:name', function tester(req, res, next) {
res.send(req.params.name);
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/hello/mark',
method: 'GET',
agent: false,
headers: {
accept: 'text/plain'
}
};
http.request(opts, function(res) {
t.equal(res.statusCode, 200);
var body = '';
res.setEncoding('utf8');
res.on('data', function(chunk) {
body += chunk;
});
res.on('end', function() {
t.equal(body, '"mark"');
t.end();
});
}).end();
});
test('GH-64 prerouting chain with error', function(t) {
SERVER.pre(function(req, res, next) {
next(
new RestError(
{
statusCode: 400,
restCode: 'BadRequest'
},
'screw you client'
)
);
});
SERVER.get('/hello/:name', function tester(req, res, next) {
res.send(req.params.name);
next();
});
CLIENT.get('/hello/mark', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 400);
t.end();
});
});
test('GH-67 extend access-control headers', function(t) {
SERVER.get('/hello/:name', function tester(req, res, next) {
res.header(
'Access-Control-Allow-Headers',
res.header('Access-Control-Allow-Headers') +
', If-Match, If-None-Match'
);
res.send(req.params.name);
next();
});
CLIENT.get('/hello/mark', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.ok(res.headers['access-control-allow-headers'].indexOf('If-Match'));
t.end();
});
});
test('GH-77 uncaughtException (default behavior)', function(t) {
SERVER.get('/', function(req, res, next) {
throw new Error('Catch me!');
});
CLIENT.get('/', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 500);
t.end();
});
});
// eslint-disable-next-line
test('handleUncaughtExceptions should not call handler for internal errors', function(t) {
SERVER.get('/', function(req, res, next) {
// This route is not used for the test but at least one route needs to
// be registered to Restify in order for routing logic to be run
assert.fail('should not run');
});
SERVER.on('uncaughtException', function throwError(err) {
t.ifError(err);
t.end();
});
CLIENT.head('/', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 405);
t.end();
});
});
// eslint-disable-next-line
test('handleUncaughtExceptions should not call handler for next(new Error())', function(t) {
SERVER.get('/', function(req, res, next) {
next(new Error('I am not fatal'));
});
SERVER.on('uncaughtException', function throwError(err) {
t.ifError(err);
t.end();
});
CLIENT.get('/', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 500);
t.end();
});
});
test('GH-77 uncaughtException (with custom handler)', function(t) {
SERVER.on('uncaughtException', function(req, res, route, err) {
res.send(204);
});
SERVER.get('/', function(req, res, next) {
throw new Error('Catch me!');
});
CLIENT.get('/', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 204);
t.end();
});
});
test('GH-180 can parse DELETE body', function(t) {
SERVER.use(restify.plugins.bodyParser({ mapParams: false }));
SERVER.del('/', function(req, res, next) {
res.send(200, req.body);
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/',
method: 'DELETE',
agent: false,
headers: {
accept: 'application/json',
'content-type': 'application/json',
'transfer-encoding': 'chunked'
}
};
http.request(opts, function(res) {
t.equal(res.statusCode, 200);
res.setEncoding('utf8');
res.body = '';
res.on('data', function(chunk) {
res.body += chunk;
});
res.on('end', function() {
t.equal(res.body, '{"param1":1234}');
t.end();
});
}).end('{"param1": 1234}');
});
test('returning error from a handler (with domains)', function(t) {
SERVER.get('/', function(req, res, next) {
next(new errors.InternalError('bah!'));
});
CLIENT.get('/', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 500);
t.end();
});
});
test('emitting error from a handler (with domains)', function(t) {
SERVER.get('/', function(req, res, next) {
req.emit('error', new Error('bah!'));
});
CLIENT.get('/', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 500);
t.end();
});
});
test('re-emitting redirect from a response', function(t) {
var redirectLocation;
SERVER.on('redirect', function(payload) {
redirectLocation = payload;
});
SERVER.get('/', function(req, res, next) {
res.redirect('/10', next);
});
CLIENT.get('/', function(err, _, res) {
t.equal(redirectLocation, '/10');
t.end();
});
});
test('throwing error from a handler (with domains)', function(t) {
SERVER.get('/', function(req, res, next) {
process.nextTick(function() {
throw new Error('bah!');
});
});
CLIENT.get('/', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 500);
t.end();
});
});
test('gh-278 missing router error events (404)', function(t) {
SERVER.once('NotFound', function(req, res) {
res.send(404, 'foo');
});
CLIENT.get('/' + uuid.v4(), function(err, _, res) {
t.ok(err);
t.equal(err.message, '"foo"');
t.equal(res.statusCode, 404);
t.end();
});
});
test('gh-278 missing router error events (405)', function(t) {
var p = '/' + uuid.v4();
SERVER.post(p, function(req, res, next) {
res.send(201);
next();
});
SERVER.once('MethodNotAllowed', function(req, res) {
res.send(405, 'foo');
});
CLIENT.get(p, function(err, _, res) {
t.ok(err);
t.equal(err.message, '"foo"');
t.equal(res.statusCode, 405);
t.end();
});
});
test('gh-329 wrong values in res.methods', function(t) {
function route(req, res, next) {
res.send(200);
next();
}
SERVER.get('/stuff', route);
SERVER.post('/stuff', route);
SERVER.get('/stuff/:id', route);
SERVER.put('/stuff/:id', route);
SERVER.del('/stuff/:id', route);
SERVER.once('MethodNotAllowed', function(req, res, cb) {
t.ok(res.methods);
t.deepEqual(res.methods, ['DELETE', 'GET', 'PUT']);
res.send(405);
});
CLIENT.post('/stuff/foo', {}, function(err, _, res) {
t.ok(err);
t.end();
});
});
test('GH #704: Route with a valid RegExp params', function(t) {
SERVER.get(
{
name: 'regexp_param1',
path: '/foo/:id([0-9]+)'
},
function(req, res, next) {
t.equal(req.params.id, '0123456789');
res.send();
next();
}
);
CLIENT.get('/foo/0123456789', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.end();
});
});
test('GH #704: Route with an invalid RegExp params', function(t) {
SERVER.get(
{
name: 'regexp_param2',
path: '/foo/:id([0-9]+)'
},
function(req, res, next) {
t.equal(req.params.id, 'A__M');
res.send();
next();
}
);
CLIENT.get('/foo/A__M', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 404);
t.end();
});
});
test('run param only with existing req.params', function(t) {
var count = 0;
SERVER.param('name', function(req, res, next) {
count++;
next();
});
SERVER.param('userId', function(req, res, next) {
count++;
next();
});
SERVER.get('/users/:userId', function(req, res, next) {
res.send(200);
});
CLIENT.get('/users/1', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.equal(count, 1);
t.end();
});
});
test('run param only with existing req.params', function(t) {
var count = 0;
SERVER.param('name', function(req, res, next) {
count++;
next();
});
SERVER.param('userId', function(req, res, next, param, name) {
t.equal(param, '1');
t.equal(name, 'userId');
count++;
next();
});
SERVER.get('/users/:userId', function(req, res, next) {
res.send(200);
});
CLIENT.get('/users/1', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.equal(count, 1);
t.end();
});
});
test('next("string") returns InternalServer', function(t) {
var count = 0;
SERVER.use(function(req, res, next) {
count++;
next();
});
SERVER.get(
{
name: 'foo',
path: '/foo/:id'
},
function(req, res, next) {
t.equal(req.params.id, 'blah');
next('bar');
}
);
CLIENT.get('/foo/blah', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 500);
t.equal(count, 1);
t.end();
});
});
test('next("string") from a use plugin returns InternalServer', function(t) {
var count = 0;
SERVER.use(function plugin(req, res, next) {
count++;
next('bar');
});
SERVER.get(
{
name: 'foo',
path: '/foo'
},
function getFoo(req, res, next) {
res.send(200);
next();
}
);
CLIENT.get('/foo', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 500);
t.equal(count, 1);
t.end();
});
});
test('res.charSet', function(t) {
SERVER.get('/foo', function getFoo(req, res, next) {
res.charSet('ISO-8859-1');
res.set('Content-Type', 'text/plain');
// send a string instead of JSON
res.send(200, JSON.stringify({ foo: 'bar' }));
next();
});
CLIENT.get('/foo', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.equal(res.headers['content-type'], 'text/plain; charset=ISO-8859-1');
t.end();
});
});
test('res.charSet override', function(t) {
SERVER.get('/foo', function getFoo(req, res, next) {
res.charSet('ISO-8859-1');
res.set('Content-Type', 'text/plain;charset=utf-8');
// send a string instead of JSON
res.send(200, JSON.stringify({ foo: 'bar' }));
next();
});
CLIENT.get('/foo', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.equal(res.headers['content-type'], 'text/plain; charset=ISO-8859-1');
t.end();
});
});
test('GH-384 res.json(200, {}) broken', function(t) {
SERVER.get('/foo', function(req, res, next) {
res.json(200, { foo: 'bar' });
next();
});
CLIENT.get('/foo', function(err, _, res, obj) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.ok(obj);
t.equal((obj || {}).foo, 'bar');
t.end();
});
});
test('explicitly sending a 403 with custom error', function(t) {
function MyCustomError() {}
MyCustomError.prototype = Object.create(Error.prototype);
SERVER.get('/', function(req, res, next) {
res.send(403, new MyCustomError('bah!'));
});
CLIENT.get('/', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 403);
t.end();
});
});
test('explicitly sending a 403 on error', function(t) {
SERVER.get('/', function(req, res, next) {
res.send(403, new Error('bah!'));
});
CLIENT.get('/', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 403);
t.end();
});
});
test('fire event on error', function(t) {
SERVER.once('InternalServer', function(req, res, err, cb) {
t.ok(req);
t.ok(res);
t.ok(err);
t.ok(cb);
t.equal(typeof cb, 'function');
return cb();
});
SERVER.get('/', function(req, res, next) {
return next(new errors.InternalServerError('bah!'));
});
CLIENT.get('/', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 500);
t.expect(7);
t.end();
});
});
test('error handler defers "after" event', async function(t) {
let afterResolve;
let clientResolve;
t.expect(9);
SERVER.once('NotFound', function(req, res, err, cb) {
t.ok(req);
t.ok(res);
t.ok(cb);
t.equal(typeof cb, 'function');
t.ok(err);
SERVER.removeAllListeners('after');
SERVER.once('after', function(req2, res2) {
t.ok(req2);
t.ok(res2);
afterResolve();
});
return cb();
});
SERVER.once('after', function() {
// do not fire prematurely
t.notOk(true);
});
CLIENT.get('/' + uuid.v4(), function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 404);
clientResolve();
});
await Promise.all([
new Promise(resolve => {
afterResolve = resolve;
}),
new Promise(resolve => {
clientResolve = resolve;
})
]);
t.end();
});
// eslint-disable-next-line
test('gh-757 req.absoluteUri() defaults path segment to req.path()', function(t) {
SERVER.get('/the-original-path', function(req, res, next) {
var prefix = 'http://127.0.0.1:' + PORT;
t.equal(
req.absoluteUri('?key=value'),
prefix + '/the-original-path/?key=value'
);
t.equal(
req.absoluteUri('#fragment'),
prefix + '/the-original-path/#fragment'
);
t.equal(
req.absoluteUri('?key=value#fragment'),
prefix + '/the-original-path/?key=value#fragment'
);
res.send();
next();
});
CLIENT.get('/the-original-path', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.end();
});
});
test('GH-693 sending multiple response header values', function(t) {
SERVER.get('/', function(req, res, next) {
res.link('/', 'self');
res.link('/foo', 'foo');
res.link('/bar', 'bar');
res.send(200, 'root');
});
CLIENT.get('/', function(err, _, res) {
t.equal(res.statusCode, 200);
t.equal(res.headers.link.split(',').length, 3);
t.end();
});
});
test('gh-762 res.noCache()', function(t) {
SERVER.get('/some-path', function(req, res, next) {
res.noCache();
res.send('data');
});
CLIENT.get('/some-path', function(err, _, res) {
t.equal(
res.headers['cache-control'],
'no-cache, no-store, must-revalidate'
);
t.equal(res.headers.pragma, 'no-cache');
t.equal(res.headers.expires, '0');
t.end();
});
});
test('gh-779 set-cookie fields should never have commas', function(t) {
SERVER.get('/set-cookie', function(req, res, next) {
res.header('set-cookie', 'foo');
res.header('set-cookie', 'bar');
res.send(200);
});
CLIENT.get('/set-cookie', function(err, _, res) {
t.ifError(err);
t.equal(
res.rawHeaders.filter(function(keyOrValue) {
return keyOrValue === 'set-cookie';
}).length,
2,
'multiple set-cookie headers should not be merged'
);
t.equal(res.headers['set-cookie'][0], 'foo');
t.equal(res.headers['set-cookie'][1], 'bar');
t.end();
});
});
test(
'gh-986 content-type fields should never have commas' +
' (via `res.header(...)`)',
function(t) {
SERVER.get('/content-type', function(req, res, next) {
res.header('content-type', 'foo');
res.header('content-type', 'bar');
res.send(200);
});
CLIENT.get('/content-type', function(err, _, res) {
t.ifError(err);
t.equal(
Array.isArray(res.headers['content-type']),
false,
'content-type header should not be an array'
);
t.equal(res.headers['content-type'], 'bar');
t.end();
});
}
);
test(
'gh-986 content-type fields should never have commas' +
' (via `res.setHeader(...)`)',
function(t) {
SERVER.get('/content-type', function(req, res, next) {
res.setHeader('content-type', 'foo');
res.setHeader('content-type', 'bar');
res.send(200);
});
CLIENT.get('/content-type', function(err, _, res) {
t.ifError(err);
t.equal(
Array.isArray(res.headers['content-type']),
false,
'content-type header should not be an array'
);
t.equal(res.headers['content-type'], 'bar');
t.end();
});
}
);
test('GH-877 content-type should be case insensitive', function(t) {
SERVER.use(restify.plugins.bodyParser({ maxBodySize: 1024 }));
SERVER.get('/cl', function(req, res, next) {
t.equal(req.getContentType(), 'application/json');
res.send(200);
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/cl',
method: 'GET',
agent: false,
headers: {
accept: 'application/json',
'content-type': 'APPLicatioN/JSon',
'transfer-encoding': 'chunked'
}
};
var client = http.request(opts, function(res) {
t.equal(res.statusCode, 200);
t.end();
});
client.end();
});
test('GH-882: route name is same as specified', function(t) {
SERVER.get(
{
name: 'my-r$-%-x',
path: '/m1'
},
function(req, res, next) {
res.send({ name: req.route.name });
}
);
CLIENT.get('/m1', function(err, _, res) {
t.ifError(err);
t.equal(res.body, '{"name":"my-r$-%-x"}');
t.end();
});
});
test(
'GH-733 if request closed early, stop processing. ensure only ' +
'relevant audit logs output.',
function(t) {
// Dirty hack to capture the log record using a ring buffer.
var numCount = 0;
// FAST_CLIENT times out at 500ms, should capture two records then close
// the request.
SERVER.get('/audit', [
function first(req, res, next) {
req.startHandlerTimer('first');
setTimeout(function() {
numCount++;
req.endHandlerTimer('first');
return next();
}, 300);
},
function second(req, res, next) {
req.startHandlerTimer('second');
numCount++;
req.endHandlerTimer('second');
setTimeout(function() {
return next();
}, 300);
},
function third(req, res, next) {
req.endHandlerTimer('third');
numCount++;
res.send({ hello: 'world' });
return next();
}
]);
// set up audit logs
SERVER.on(
'after',
restify.plugins.auditLogger({
log: pino({ name: 'audit' }),
event: 'after'
})
);
SERVER.on('after', function(req, res, route, err) {
if (req.href() === '/audit?v=2') {
// should request timeout error
t.ok(err);
t.equal(err.name, 'RequestCloseError');
// check records
t.ok(LOG_BUFFER.records[0], 'no log records');
t.equal(
LOG_BUFFER.records.length,
1,
'should only have 1 log record'
);
// check timers
var handlers = Object.keys(LOG_BUFFER.records[0].req.timers);
t.equal(handlers.length, 2, 'should only have 2 req timers');
t.equal(
handlers[0],
'first',
'first handler timer not in order'
);
t.equal(
handlers[handlers.length - 1],
'second',
'second handler not last'
);
t.end();
// ensure third handler never ran
t.equal(numCount, 2);
t.end();
}
});
CLIENT.get('/audit?v=1', function(err, req, res, data) {
t.ifError(err);
t.deepEqual(data, { hello: 'world' });
t.equal(numCount, 3);
// reset numCount
numCount = 0;
//reset stream-recorder
LOG_BUFFER.flushRecords();
FAST_CLIENT.get('/audit?v=2', function(err2, req2, res2, data2) {
t.ok(err2);
t.equal(err2.name, 'RequestTimeoutError');
});
});
}
);
test('GH-667 emit error event for generic Errors', function(t) {
var restifyErrorFired = 0;
var notFoundFired = 0;
var myErr = new errors.NotFoundError('foobar');
SERVER.get('/1', function(req, res, next) {
return next(new Error('foobar'));
});
SERVER.get('/2', function(req, res, next) {
return next(myErr);
});
SERVER.get('/3', function(req, res, next) {
SERVER.on('NotFound', function(req2, res2, err, cb) {
notFoundFired++;
t.ok(err);
t.equal(err, myErr);
t.end();
return cb();
});
return next(myErr);
});
SERVER.on('restifyError', function(req, res, err, cb) {
restifyErrorFired++;
t.ok(err);
t.equal(err instanceof Error, true);
if (err instanceof errors.NotFoundError) {
t.equal(err, myErr);
}
return cb();
});
/*eslint-disable no-shadow*/
CLIENT.get('/1', function(err, req, res, data) {
// should get regular error
// fail here. But why?
t.ok(err);
t.equal(restifyErrorFired, 1);
CLIENT.get('/2', function(err, req, res, data) {
// should get not found error
t.ok(err);
t.equal(restifyErrorFired, 2);
CLIENT.get('/3', function(err, req, res, data) {
// should get notfounderror
t.ok(err);
t.equal(restifyErrorFired, 3);
t.equal(notFoundFired, 1);
});
});
});
/*eslint-enable no-shadow*/
});
// eslint-disable-next-line
test('GH-667 returning error in error handler should not do anything', function(t) {
SERVER.on('ImATeapot', function(req, res, err, cb) {
// attempt to pass a new error back
return cb(new errors.LockedError('oh noes'));
});
SERVER.get('/1', function(req, res, next) {
return next(new errors.ImATeapotError('foobar'));
});
CLIENT.get('/1', function(err, req, res, data) {
t.ok(err);
// should still get the original error
t.equal(err.name, 'ImATeapotError');
t.end();
});
});
test('GH-1024 disable uncaughtException handler', function(t) {
// With uncaughtException handling disabled, the node process will abort,
// so testing of this feature must occur in a separate node process.
var allStderr = '';
var serverPath = __dirname + '/lib/server-withDisableUncaughtException.js';
var serverProc = childprocess.fork(serverPath, { silent: true });
// Record stderr, to check for the correct exception stack.
serverProc.stderr.on('data', function(data) {
allStderr += String(data);
});
// Handle serverPortResponse and then make the client request - the request
// should receive a connection closed error (because the server aborts).
serverProc.on('message', function(msg) {
if (msg.task !== 'serverPortResponse') {
serverProc.kill();
t.end();
return;
}
var port = msg.port;
var client = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + port,
dtrace: helper.dtrace,
retry: false
});
client.get('/', function(err, _, res) {
// Should get a connection closed error, but no response object.
t.ok(err);
t.equal(err.code, 'ECONNRESET');
t.equal(res, undefined);
serverProc.kill(); // Ensure it's dead.
t.ok(allStderr.indexOf('Error: Catch me!') > 0);
t.end();
});
});
serverProc.send({ task: 'serverPortRequest' });
});
test('GH-999 Custom 404 handler does not send response', function(t) {
// make the 404 handler act like other error handlers - must modify
// err.body to send a custom response.
SERVER.on('NotFound', function(req, res, err, cb) {
err.body = {
message: 'my custom not found'
};
return cb();
});
CLIENT.get('/notfound', function(err, _, res) {
t.ok(err);
t.deepEqual(
res.body,
JSON.stringify({
message: 'my custom not found'
})
);
t.end();
});
});
test('calling next(false) should early exit from pre handlers', function(t) {
var afterFired = false;
SERVER.pre(function(req, res, next) {
res.send('early exit');
return next(false);
});
SERVER.get('/1', function(req, res, next) {
res.send('hello world');
return next();
});
SERVER.on('after', function() {
afterFired = true;
});
CLIENT.get('/1', function(err, req, res, data) {
t.ifError(err);
t.equal(data, 'early exit');
// ensure after event fired
t.ok(afterFired);
t.end();
});
});
test('calling next(false) should early exit from use handlers', function(t) {
var steps = 0;
SERVER.use(function(req, res, next) {
res.send('early exit');
return next(false);
});
SERVER.get('/1', function(req, res, next) {
res.send('hello world');
return next();
});
SERVER.on('after', function() {
steps++;
t.equal(steps, 1);
t.end();
});
CLIENT.get('/1', function(err, req, res, data) {
t.ifError(err);
t.equal(data, 'early exit');
steps++;
});
});
test('calling next(err) from pre should still emit after event', function(t) {
setTimeout(function() {
t.fail('Timed out');
t.end();
}, 2000);
var error = new Error();
SERVER.pre(function(req, res, next) {
next(error);
});
SERVER.get('/', function(req, res, next) {
t.fail('should have aborted stack before routing');
});
SERVER.on('after', function(req, res, route, err) {
t.equal(err, error);
t.end();
});
CLIENT.get('/', function() {});
});
test('GH-1078: server name should default to restify', function(t) {
var myServer = restify.createServer();
var port = 3000;
myServer.get('/', function(req, res, next) {
res.send('hi');
return next();
});
var myClient = restifyClients.createStringClient({
url: 'http://127.0.0.1:' + port,
headers: {
connection: 'close'
}
});
myServer.listen(port, function() {
myClient.get('/', function(err, req, res, data) {
t.ifError(err);
t.equal(res.headers.server, 'restify');
myServer.close(t.end);
});
});
});
test('GH-1078: server name should be customizable', function(t) {
var myServer = restify.createServer({
name: 'foo'
});
var port = 3000;
myServer.get('/', function(req, res, next) {
res.send('hi');
return next();
});
var myClient = restifyClients.createStringClient({
url: 'http://127.0.0.1:' + port,
headers: {
connection: 'close'
}
});
myServer.listen(port, function() {
myClient.get('/', function(err, req, res, data) {
t.ifError(err);
t.equal(res.headers.server, 'foo');
myServer.close(t.end);
});
});
});
// eslint-disable-next-line
test('GH-1078: server name should be overridable and not sent down', function(t) {
var myServer = restify.createServer({
name: ''
});
var port = 3000;
myServer.get('/', function(req, res, next) {
res.send('hi');
return next();
});
var myClient = restifyClients.createStringClient({
url: 'http://127.0.0.1:' + port,
headers: {
connection: 'close'
}
});
myServer.listen(port, function() {
myClient.get('/', function(err, req, res, data) {
t.ifError(err);
t.equal(res.headers.hasOwnProperty('server'), false);
myServer.close(t.end);
});
});
});
test("should emit 'after' on successful request", function(t) {
SERVER.on('after', function(req, res, route, err) {
t.ifError(err);
t.end();
});
SERVER.get('/foobar', function(req, res, next) {
res.send('hello world');
next();
});
CLIENT.get('/foobar', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
});
});
test("should emit 'after' on successful request with work", function(t) {
SERVER.on('after', function(req, res, route, err) {
t.ifError(err);
t.end();
});
SERVER.get('/foobar', function(req, res, next) {
// with timeouts we are testing that request lifecycle
// events are firing in the correct order
setTimeout(function() {
res.send('hello world');
setTimeout(function() {
next();
}, 500);
}, 500);
});
CLIENT.get('/foobar', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
});
});
test("should emit 'after' on errored request", function(t) {
SERVER.on('after', function(req, res, route, err) {
t.ok(err);
t.end();
});
SERVER.get('/foobar', function(req, res, next) {
next(new Error('oh noes'));
});
CLIENT.get('/foobar', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 500);
});
});
test("should emit 'after' on uncaughtException", function(t) {
SERVER.on('after', function(req, res, route, err) {
t.ok(err);
t.equal(err.message, 'oh noes');
});
SERVER.get('/foobar', function(req, res, next) {
throw new Error('oh noes');
});
CLIENT.get('/foobar', function(err, _, res) {
t.ok(err);
t.equal(err.name, 'InternalError');
t.end();
});
});
test("should emit 'after' when sending res on uncaughtException", function(t) {
SERVER.on('after', function(req, res, route, err) {
t.ok(err);
t.equal(err.message, 'oh noes');
});
SERVER.on('uncaughtException', function(req, res, route, err) {
res.send(504, 'boom');
});
SERVER.get('/foobar', function(req, res, next) {
throw new Error('oh noes');
});
CLIENT.get('/foobar', function(err, _, res) {
t.ok(err);
t.equal(err.name, 'GatewayTimeoutError');
t.end();
});
});
test(
"should emit 'after' on client closed request " +
"(req.connectionState(): 'close')",
function(t) {
SERVER.on('after', function(req, res, route, err) {
t.ok(err);
t.equal(req.connectionState(), 'close');
t.equal(res.statusCode, 444);
t.equal(err.name, 'RequestCloseError');
t.end();
});
SERVER.get('/foobar', function(req, res, next) {
// fast client times out at 500ms, wait for 800ms which should cause
// client to timeout
setTimeout(function() {
return next();
}, 800);
});
FAST_CLIENT.get('/foobar', function(err, _, res) {
t.ok(err);
t.equal(err.name, 'RequestTimeoutError');
});
}
);
// This test reproduces https://github.com/restify/node-restify/issues/1765. It
// specifically tests the edge case of an exception being thrown from a route
// handler _after_ the response is considered to be "flushed" (for instance when
// the request is aborted before a response is sent and an exception is thrown).
// eslint-disable-next-line max-len
test("should emit 'after' on uncaughtException after response closed with custom uncaughtException listener", function(t) {
var ERR_MSG = 'foo';
var gotAfter = false;
var gotReqCallback = false;
SERVER.on('after', function(req, res, route, err) {
gotAfter = true;
t.ok(err);
t.equal(req.connectionState(), 'close');
t.equal(res.statusCode, 444);
t.equal(err.name, 'Error');
t.equal(err.message, ERR_MSG);
if (gotReqCallback) {
t.end();
}
});
SERVER.on('uncaughtException', function(req, res, route, err, callback) {
callback();
});
SERVER.get('/foobar', function(req, res, next) {
res.on('close', function onResClose() {
// We throw this error in the response's close event handler on
// purpose to exercise the code path where we mark the route
// handlers as finished _after_ the response is marked as flushed.
throw new Error(ERR_MSG);
});
});
FAST_CLIENT.get('/foobar', function(err, _, res) {
gotReqCallback = true;
t.ok(err);
t.equal(err.name, 'RequestTimeoutError');
if (gotAfter) {
t.end();
}
});
});
test('should increment/decrement inflight request count', function(t) {
SERVER.get('/foo', function(req, res, next) {
t.equal(SERVER.inflightRequests(), 1);
res.send();
return next();
});
SERVER.on('after', function() {
t.equal(SERVER.inflightRequests(), 0);
t.end();
});
CLIENT.get('/foo', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.equal(SERVER.inflightRequests(), 0);
});
});
// eslint-disable-next-line
test('should increment/decrement inflight request count for concurrent reqs', function(t) {
SERVER.get('/foo1', function(req, res, next) {
// other request is already sent
t.equal(SERVER.inflightRequests() >= 1, true);
setTimeout(function() {
res.send();
return next();
}, 250);
});
SERVER.get('/foo2', function(req, res, next) {
t.equal(SERVER.inflightRequests(), 2);
res.send();
return next();
});
CLIENT.get('/foo1', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.equal(SERVER.inflightRequests(), 0);
t.end();
});
CLIENT.get('/foo2', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.equal(SERVER.inflightRequests(), 1);
});
});
test("should emit 'close' on server close", function(t) {
var server = restify.createServer();
server.listen(PORT + 1, '127.0.0.1', function() {
server.on('close', function() {
t.end();
});
server.close();
});
});
test('should cleanup inflight requests count for 404s', async function(t) {
let afterResolve;
let clientResolve;
SERVER.get('/foo1', function(req, res, next) {
t.equal(SERVER.inflightRequests(), 1);
res.send();
return next();
});
SERVER.on('after', function(req) {
if (req.path() === '/doesnotexist') {
t.equal(SERVER.inflightRequests(), 0);
afterResolve();
}
});
CLIENT.get('/foo1', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.equal(SERVER.inflightRequests(), 0);
CLIENT.get('/doesnotexist', function(err2, _2, res2) {
t.ok(err2);
t.equal(res2.statusCode, 404);
t.equal(SERVER.inflightRequests(), 0);
clientResolve();
});
});
await Promise.all([
new Promise(resolve => {
afterResolve = resolve;
}),
new Promise(resolve => {
clientResolve = resolve;
})
]);
t.end();
});
test('should cleanup inflight requests count for timeouts', function(t) {
t.equal(SERVER.inflightRequests(), 0);
SERVER.get('/foo1', function(req, res, next) {
// othr request is already sent
t.equal(SERVER.inflightRequests() >= 1, true);
setTimeout(function() {
res.send();
return next();
}, 1000);
});
SERVER.get('/foo2', function(req, res, next) {
t.equal(SERVER.inflightRequests(), 2);
res.send();
return next();
});
SERVER.on('after', function(req) {
if (req.path() === '/foo1') {
t.equal(SERVER.inflightRequests(), 0);
t.end();
} else if (req.path() === '/foo2') {
t.equal(SERVER.inflightRequests(), 1);
}
});
FAST_CLIENT.get('/foo1', function(err, _, res) {
t.ok(err);
t.equal(SERVER.inflightRequests(), 1);
});
CLIENT.get('/foo2', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.equal(SERVER.inflightRequests(), 1);
});
});
// eslint-disable-next-line
test('should cleanup inflight requests count on uncaughtExceptions', function(t) {
SERVER.on('uncaughtException', function(req, res, route, err) {
res.send(500, 'asplode');
});
SERVER.get('/foo1', function(req, res, next) {
t.equal(SERVER.inflightRequests(), 1);
throw new Error('oh noes');
});
CLIENT.get('/foo1', function(err, _, res) {
t.ok(err);
t.equal(SERVER.inflightRequests(), 0);
t.end();
});
});
test('should show debug information', function(t) {
SERVER.pre(function pre(req, res, next) {
return next();
});
SERVER.pre(function pre2(req, res, next) {
return next();
});
SERVER.use(function use(req, res, next) {
return next();
});
SERVER.use(function use2(req, res, next) {
return next();
});
SERVER.on('after', function aft() {});
SERVER.on('after', function aft2() {});
SERVER.get(
'/foo',
function(req, res, next) {
return next();
},
function foo(req, res, next) {
res.end();
return next();
}
);
SERVER.get('/bar/:a/:b', function bar(req, res, next) {
res.end();
return next();
});
SERVER.get('/example/:file(^\\d+).png', function freeform(req, res, next) {
res.end();
return next();
});
var debugInfo = SERVER.getDebugInfo();
t.ok(debugInfo);
t.ok(debugInfo.routes);
debugInfo.routes.forEach(function(route) {
t.ok(route);
t.equal(typeof route.name, 'string');
t.equal(typeof route.method, 'string');
t.equal(route.handlers instanceof Array, true);
route.handlers.forEach(function(handlerFn) {
t.equal(typeof handlerFn, 'string');
});
});
// // check /foo
// TODO: should it contain use handlers?
t.equal(debugInfo.routes[0].handlers[0], 'use');
t.equal(debugInfo.routes[0].handlers[1], 'use2');
t.equal(debugInfo.routes[0].handlers[2], 'anonymous');
t.equal(debugInfo.routes[0].handlers[3], 'foo');
// check /bar
t.equal(debugInfo.routes[0].handlers[0], 'use');
t.equal(debugInfo.routes[0].handlers[1], 'use2');
t.equal(debugInfo.routes[1].handlers[2], 'bar');
// check use, pre, and after handlers
t.ok(debugInfo.server.use);
t.equal(debugInfo.server.use[0], 'use');
t.equal(debugInfo.server.use[1], 'use2');
t.ok(debugInfo.server.pre);
t.equal(debugInfo.server.pre[0], 'pre');
t.equal(debugInfo.server.pre[1], 'pre2');
t.ok(debugInfo.server.after);
t.equal(debugInfo.server.after[0], 'aft');
t.equal(debugInfo.server.after[1], 'aft2');
// detailed test for compiled regex
// verify url parameter regex
t.deepEqual(debugInfo.routes[1].name, 'getbarab');
t.deepEqual(debugInfo.routes[1].method, 'get');
// verify freeform regex
t.deepEqual(debugInfo.routes[2].name, 'getexamplefiledpng');
t.deepEqual(debugInfo.routes[2].method, 'get');
// verify other server details
t.deepEqual(Object.keys(debugInfo.server.formatters), [
'application/javascript',
'application/json',
'text/plain',
'application/octet-stream'
]);
t.equal(debugInfo.server.address, '127.0.0.1');
t.equal(typeof debugInfo.server.port, 'number');
t.equal(typeof debugInfo.server.inflightRequests, 'number');
t.end();
});
test("should emit 'pre' event on a 200", function(t) {
SERVER.get('/foo/:id', function echoId(req, res, next) {
t.ok(req.params);
t.equal(req.params.id, 'bar');
t.equal(req.isUpload(), false);
res.send();
next();
});
SERVER.once('pre', function(req, res) {
t.ok(req);
t.ok(res);
});
CLIENT.get('/foo/bar', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.end();
});
});
test("should emit 'pre' event on 404", function(t) {
SERVER.get('/foo/:id', function echoId(req, res, next) {
t.ok(req.params);
t.equal(req.params.id, 'bar');
t.equal(req.isUpload(), false);
res.send();
next();
});
SERVER.once('pre', function(req, res) {
t.ok(req);
t.ok(res);
});
CLIENT.get('/badroute', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 404);
t.end();
});
});
test("should emit 'routed' event on a 200", function(t) {
SERVER.get('/foo/:id', function echoId(req, res, next) {
t.ok(req.params);
t.equal(req.params.id, 'bar');
t.equal(req.isUpload(), false);
res.send();
next();
});
SERVER.once('routed', function(req, res, route) {
t.ok(req);
t.ok(res);
t.ok(route);
});
CLIENT.get('/foo/bar', function(err, _, res) {
t.ifError(err);
t.equal(res.statusCode, 200);
t.end();
});
});
test("should not emit 'routed' event on 404", function(t) {
SERVER.get('/foo/:id', function echoId(req, res, next) {
t.ok(req.params);
t.equal(req.params.id, 'bar');
t.equal(req.isUpload(), false);
res.send();
next();
});
SERVER.once('routed', function(req, res, route) {
t.fail();
});
CLIENT.get('/badroute', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 404);
t.end();
});
});
test('should emit restifyError even for router errors', function(t) {
var notFoundFired = false;
var restifyErrFired = false;
SERVER.once('NotFound', function(req, res, err, cb) {
notFoundFired = true;
t.ok(err);
t.equal(err instanceof Error, true);
t.equal(err.name, 'ResourceNotFoundError');
return cb();
});
SERVER.once('restifyError', function(req, res, err, cb) {
restifyErrFired = true;
t.ok(err);
t.equal(err instanceof Error, true);
t.equal(err.name, 'ResourceNotFoundError');
return cb();
});
/*eslint-disable no-shadow*/
CLIENT.get('/dne', function(err, req, res, data) {
t.ok(err);
t.equal(err.name, 'ResourceNotFoundError');
t.equal(notFoundFired, true);
t.equal(restifyErrFired, true);
t.done();
});
});
test('should emit error with multiple next calls with strictNext', function(t) {
var server = restify.createServer({
dtrace: helper.dtrace,
strictNext: true,
handleUncaughtExceptions: true,
log: helper.getLog('server')
});
var client;
var port;
server.listen(PORT + 1, '127.0.0.1', function() {
port = server.address().port;
client = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + port,
dtrace: helper.dtrace,
retry: false
});
server.get('/strict-next', function(req, res, next) {
next();
next();
});
server.on('uncaughtException', function(req, res, route, err) {
t.ok(err);
t.equal(err.message, "next shouldn't be called more than once");
res.send(err);
});
client.get('/strict-next', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 500);
client.close();
server.close(function() {
t.end();
});
});
});
});
test(
'should send 500 if we reached the end of handler chain w/o sending ' +
'headers',
function(t) {
var server = restify.createServer({
dtrace: helper.dtrace,
strictNext: true,
log: helper.getLog('server')
});
var client;
var port;
server.listen(PORT + 1, '127.0.0.1', function() {
port = server.address().port;
client = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + port,
dtrace: helper.dtrace,
retry: false
});
server.get('/noResponse', function(req, res, next) {
next();
});
client.get('/noResponse', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 500);
t.equal(err.name, 'InternalServerError');
t.equal(
err.message,
'reached the end of the handler chain without ' +
'writing a response!'
);
client.close();
server.close(function() {
t.end();
});
});
});
}
);
test('uncaughtException should not trigger named routeHandler', function(t) {
SERVER.get(
{
name: 'foo',
path: '/foo'
},
function(req, res, next) {
throw 'bar'; //eslint-disable-line no-throw-literal
}
);
SERVER.get(
{
name: 'bar',
path: '/bar'
},
function(req, res, next) {
// This code should not run, but we can test against the status code
res.send(200);
next();
}
);
CLIENT.get('/foo', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 500);
t.end();
});
});
test('uncaughtException should handle thrown null', function(t) {
SERVER.get(
{
name: 'foo',
path: '/foo'
},
function(req, res, next) {
throw null; //eslint-disable-line no-throw-literal
}
);
SERVER.get(
{
name: 'bar',
path: '/bar'
},
function(req, res, next) {
// This code should not run, but we can test against the status code
res.send(200);
next();
}
);
CLIENT.get('/foo', function(err, _, res, data) {
t.ok(err);
t.equal(res.statusCode, 500);
t.equal(data.message, 'null');
t.end();
});
});
test('uncaughtException should handle thrown undefined literal', function(t) {
SERVER.get(
{
name: 'foo',
path: '/foo'
},
function(req, res, next) {
throw undefined; //eslint-disable-line no-throw-literal
}
);
SERVER.get(
{
name: 'bar',
path: '/bar'
},
function(req, res, next) {
// This code should not run, but we can test against the status code
res.send(200);
next();
}
);
CLIENT.get('/foo', function(err, _, res, data) {
t.ok(err);
t.equal(res.statusCode, 500);
t.equal(data.message, 'undefined');
t.end();
});
});
test('uncaughtException should handle thrown falsy number', function(t) {
SERVER.get(
{
name: 'foo',
path: '/foo'
},
function(req, res, next) {
throw 0; //eslint-disable-line no-throw-literal
}
);
SERVER.get(
{
name: 'bar',
path: '/bar'
},
function(req, res, next) {
// This code should not run, but we can test against the status code
res.send(200);
next();
}
);
CLIENT.get('/foo', function(err, _, res, data) {
t.ok(err);
t.equal(data.message, '0');
t.equal(res.statusCode, 500);
t.end();
});
});
test('uncaughtException should handle thrown non falsy number', function(t) {
SERVER.get(
{
name: 'foo',
path: '/foo'
},
function(req, res, next) {
throw 1; //eslint-disable-line no-throw-literal
}
);
SERVER.get(
{
name: 'bar',
path: '/bar'
},
function(req, res, next) {
// This code should not run, but we can test against the status code
res.send(200);
next();
}
);
CLIENT.get('/foo', function(err, _, res, data) {
t.ok(err);
t.equal(data.message, '1');
t.equal(res.statusCode, 500);
t.end();
});
});
test('uncaughtException should handle thrown boolean', function(t) {
SERVER.get(
{
name: 'foo',
path: '/foo'
},
function(req, res, next) {
throw true; //eslint-disable-line no-throw-literal
}
);
SERVER.get(
{
name: 'bar',
path: '/bar'
},
function(req, res, next) {
// This code should not run, but we can test against the status code
res.send(200);
next();
}
);
CLIENT.get('/foo', function(err, _, res, data) {
t.ok(err);
t.equal(data.message, 'true');
t.equal(res.statusCode, 500);
t.end();
});
});
test('uncaughtException should handle thrown falsy boolean', function(t) {
SERVER.get(
{
name: 'foo',
path: '/foo'
},
function(req, res, next) {
throw false; //eslint-disable-line no-throw-literal
}
);
SERVER.get(
{
name: 'bar',
path: '/bar'
},
function(req, res, next) {
// This code should not run, but we can test against the status code
res.send(200);
next();
}
);
CLIENT.get('/foo', function(err, _, res, data) {
t.ok(err);
t.equal(data.message, 'false');
t.equal(res.statusCode, 500);
t.end();
});
});
test('should have proxy event handlers as instance', function(t) {
var server = restify.createServer({
handleUpgrades: false
});
t.equal(server.proxyEvents.length, 6);
server = restify.createServer({
handleUpgrades: true
});
t.equal(server.proxyEvents.length, 5);
server.close(function() {
t.end();
});
});
test('first chain should get to reject requests', function(t) {
SERVER.get('/foobar', function(req, res, next) {
t.fail('should not call handler');
});
SERVER.first(function(req, res) {
res.statusCode = 413; // I'm a teapot!
res.end();
return false;
});
CLIENT.get('/foobar', function(_, __, res) {
t.equal(res.statusCode, 413);
t.end();
});
});
test('first chain should get to allow requests', function(t) {
SERVER.get('/foobar', function(req, res, next) {
res.send(413, 'Im a teapot');
return next();
});
SERVER.first(function(req, res) {
return true;
});
CLIENT.get('/foobar', function(_, __, res) {
t.equal(res.statusCode, 413);
t.end();
});
});
test('first chain should allow multiple handlers', function(t) {
SERVER.get('/foobar', function(req, res, next) {
res.send(413, 'Im a teapot');
return next();
});
var count = 0;
var handler = function() {
count++;
};
SERVER.first(handler, handler, handler);
SERVER.first(handler, handler, handler);
CLIENT.get('/foobar', function(_, __, res) {
t.equal(res.statusCode, 413);
t.equal(count, 6, 'invoked 6 handlers');
t.end();
});
});
test('first chain should allow any handler to reject', function(t) {
SERVER.get('/foobar', function(req, res, next) {
res.send(200, 'Handled');
return next();
});
var count = 0;
var handler = function() {
count++;
};
var handlerAbort = function(req, res) {
count++;
res.statusCode = 413;
res.end();
return false;
};
SERVER.first(handler, handler, handler);
// Should append these handlers and abort the chain on the second
SERVER.first(handler, handlerAbort, handler);
// These should never run
SERVER.first(handler, handlerAbort);
CLIENT.get('/foobar', function(_, __, res) {
t.equal(res.statusCode, 413);
t.equal(count, 5, 'invoked 5 handlers');
t.end();
});
});
test('inflightRequest accounting stable with firstChain', function(t) {
// Make 3 requests, shed the second, and ensure inflightRequest accounting
// for all the requests
var request = 0;
SERVER.first(function(req, res) {
request++;
if (request === 1) {
t.equal(SERVER._inflightRequests, 1);
return true;
}
if (request === 2) {
t.equal(SERVER._inflightRequests, 2);
res.statusCode = 413;
res.end();
return false;
}
if (request === 3) {
// Since the second request was shed, and inflightRequest accounting
// should be happening synchronously, this should still be 2 for
// the third request
t.equal(SERVER._inflightRequests, 2);
return true;
}
t.fail('Too many requests for test');
return false;
});
var nexts = [];
SERVER.get('/foobar', function(req, res, next) {
res.send(200, 'success');
nexts.push(next);
if (nexts.length === 2) {
nexts.forEach(function(finishRequest) {
finishRequest();
});
}
});
var results = [];
function getDone(_, __, res) {
results.push(res);
if (results.length < 3) {
return;
}
for (var i = 0; i < results.length; i++) {
// The shed request should always be returned first, since it isn't
// handled by SERVER.get
if (i === 1) {
t.equal(
results[i].statusCode,
413,
'results[' + i + '] === 413'
);
} else {
t.equal(
results[i].statusCode,
200,
'results[' + i + '] === 200'
);
}
}
t.end();
}
// kick off all 3 at the same time to see if we can trigger a race condition
CLIENT.get('/foobar', getDone);
CLIENT.get('/foobar', getDone);
CLIENT.get('/foobar', getDone);
});
test('async prerouting chain with error', function(t) {
SERVER.pre(async function(req, res) {
await helper.sleep(10);
throw new RestError({ statusCode: 400, restCode: 'BadRequest' }, 'bum');
});
SERVER.get('/hello/:name', function tester(req, res, next) {
res.send(req.params.name);
next();
});
CLIENT.get('/hello/mark', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 400);
t.end();
});
});
test('async prerouting chain with empty rejection', function(t) {
SERVER.pre(async function(req, res) {
await helper.sleep(10);
return Promise.reject();
});
SERVER.get('/hello/:name', function tester(req, res, next) {
res.send(req.params.name);
next();
});
SERVER.on('Async', function(req, res, err, callback) {
t.equal(err.jse_info.cause, undefined);
t.equal(err.jse_info.method, 'GET');
t.equal(err.jse_info.path, '/hello/mark');
callback();
});
CLIENT.get('/hello/mark', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 500);
t.end();
});
});
test('async use chain with error', function(t) {
SERVER.use(async function(req, res) {
await helper.sleep(10);
throw new RestError({ statusCode: 400, restCode: 'BadRequest' }, 'bum');
});
SERVER.get('/hello/:name', function tester(req, res, next) {
res.send(req.params.name);
next();
});
CLIENT.get('/hello/mark', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 400);
t.end();
});
});
test('async handler with error', function(t) {
SERVER.get('/hello/:name', async function tester(req, res) {
await helper.sleep(10);
throw new RestError({ statusCode: 400, restCode: 'BadRequest' }, 'bum');
});
CLIENT.get('/hello/mark', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 400);
t.end();
});
});
test('async handler with error after send succeeds', function(t) {
SERVER.get('/hello/:name', async function tester(req, res) {
await helper.sleep(10);
res.send(req.params.name);
throw new RestError({ statusCode: 400, restCode: 'BadRequest' }, 'bum');
});
CLIENT.get('/hello/mark', function(err, _, res) {
t.ok(!err);
t.equal(res.statusCode, 200);
t.end();
});
});
test('async handler with error after send succeeds', function(t) {
SERVER.get('/hello/:name', async function tester(req, res) {
res.send(req.params.name);
await helper.sleep(20);
throw new RestError({ statusCode: 400, restCode: 'BadRequest' }, 'bum');
});
SERVER.on('after', function(req, res, route, error) {
t.ok(error);
t.end();
});
CLIENT.get('/hello/mark', function(err, _, res) {
t.ok(!err);
t.equal(res.statusCode, 200);
});
});
test('async handler without next', function(t) {
SERVER.get('/hello/:name', async function tester(req, res) {
await helper.sleep(10);
res.send(req.params.name);
});
SERVER.on('after', function(req, res, route, error) {
t.ok(!error);
t.equal(res.statusCode, 200);
t.end();
});
CLIENT.get('/hello/mark', function(err, _, res) {
t.ok(!err);
t.equal(res.statusCode, 200);
});
});
test('async handler should discard value', function(t) {
SERVER.get('/hello/:name', async function tester(req, res) {
await helper.sleep(10);
res.send(req.params.name);
return 'foo';
});
CLIENT.get('/hello/mark', function(err, _, res) {
t.ok(!err);
t.equal(res.statusCode, 200);
t.equal(res.body, '"mark"');
t.end();
});
});
test('Server returns 400 on invalid method', function(t) {
SERVER.get('/snickers/bar', function echoId(req, res, next) {
res.send();
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/snickers/bar',
method: 'CANDYBARS',
agent: false
};
http.request(opts, function(res) {
t.equal(res.statusCode, 400);
t.equal(res.statusMessage, 'Bad Request');
res.on('data', function() {
t.fail('Data was sent on 400 error');
});
res.on('end', function() {
t.end();
});
}).end();
});
test('Server returns 4xx when header size is too large', function(t) {
SERVER.get('/jellybeans', function echoId(req, res, next) {
res.send();
next();
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/jellybeans',
method: 'GET',
agent: false,
headers: {
'jellybean-colors': 'purple,green,red,black,pink,'.repeat(1000)
}
};
http.request(opts, function(res) {
if (NODE_MAJOR_VERSION > '10') {
t.equal(res.statusCode, 431);
t.equal(res.statusMessage, 'Request Header Fields Too Large');
} else {
t.equal(res.statusCode, 400);
t.equal(res.statusMessage, 'Bad Request');
}
res.on('data', function() {
t.fail('Data was sent on 431 error');
});
res.on('end', function() {
t.end();
});
}).end();
});
test('Server supports adding custom clientError listener', function(t) {
SERVER.get('/popcorn', function echoId(req, res, next) {
res.send();
next();
});
SERVER.on('clientError', function(err, socket) {
if (err.code !== 'HPE_HEADER_OVERFLOW') {
t.fail('Expected HPE_HEADER_OVERFLOW but err.code was ' + err.code);
}
socket.write("HTTP/1.1 418 I'm a teapot\r\nConnection: close\r\n\r\n");
socket.destroy(err);
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/popcorn',
method: 'GET',
agent: false,
headers: {
'jellybean-colors': 'purple,green,red,black,pink,'.repeat(1000)
}
};
http.request(opts, function(res) {
t.equal(res.statusCode, 418);
t.equal(res.statusMessage, "I'm a teapot");
res.on('data', function() {});
res.on('end', function() {
t.end();
});
}).end();
});
test('Server correctly handles multiple clientError listeners', function(t) {
SERVER.get('/popcorn', function echoId(req, res, next) {
res.send();
next();
});
let numListenerCalls = 0;
SERVER.on('clientError', function(err, socket) {
socket.write("HTTP/1.1 418 I'm a teapot\r\nConnection: close\r\n\r\n");
numListenerCalls += 1;
});
SERVER.on('clientError', function(err, socket) {
if (numListenerCalls !== 1) {
t.fail('listener was called ' + numListenerCalls + ' times');
}
socket.destroy(err);
});
var opts = {
hostname: '127.0.0.1',
port: PORT,
path: '/popcorn',
method: 'GET',
agent: false,
headers: {
'jellybean-colors': 'purple,green,red,black,pink,'.repeat(1000)
}
};
http.request(opts, function(res) {
t.equal(res.statusCode, 418);
t.equal(res.statusMessage, "I'm a teapot");
res.on('data', function() {});
res.on('end', function() {
t.end();
});
}).end();
});
test('req and res should use server logger by default', function(t) {
SERVER.get('/ping', function echoId(req, res, next) {
t.ok(req.log);
t.strictEqual(req.log, SERVER.log);
req.log.info('foo');
t.equal(LOG_BUFFER.records[LOG_BUFFER.length - 1].msg, 'foo');
res.log.info('bar');
t.equal(LOG_BUFFER.records[LOG_BUFFER.length - 1].msg, 'bar');
res.send();
next();
});
CLIENT.get('/ping', function() {
t.end();
});
});
test('req and res should use own logger by if set during .first', function(t) {
const buffer = new StreamRecorder();
SERVER.first(function first(req, res) {
req.log = helper.getLog('server', buffer, 'info');
});
SERVER.get('/ping', function echoId(req, res, next) {
LOG_BUFFER.flushRecords();
t.ok(req.log);
t.notStrictEqual(req.log, SERVER.log);
req.log.info('foo');
t.equal(buffer.records[buffer.length - 1].msg, 'foo');
res.log.info('bar');
t.equal(buffer.records[buffer.length - 1].msg, 'bar');
t.equal(LOG_BUFFER.records.length, 0);
res.send();
next();
});
CLIENT.get('/ping', function() {
t.end();
});
});
test('should throw if handleUncaughtExceptions is invalid', function(t) {
t.throws(() => {
restify.createServer({
handleUncaughtExceptions: 'this is invalid'
});
});
t.end();
});
test('should use custom function for error handling', function(t) {
const asl = new AsyncLocalStorage();
let callOnError;
var server = restify.createServer({
dtrace: helper.dtrace,
strictNext: true,
handleUncaughtExceptions: (req, res, onError, next) => {
callOnError = err => {
const newErr = new Error('new error');
newErr.orig = err;
onError(newErr);
};
asl.run({}, next, req, res);
},
log: helper.getLog('server')
});
var client;
var port;
server.listen(PORT + 1, '127.0.0.1', function() {
port = server.address().port;
client = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + port,
dtrace: helper.dtrace,
retry: false
});
const expectedErr = new Error('foo');
server.get('/throw', function(req, res, next) {
// We don't really need to throw to test, and catching the throw is
// hard because nodeunit messes with uncaughtException event
callOnError(expectedErr);
});
server.on('uncaughtException', function(req, res, route, err) {
t.ok(err);
t.strictEqual(err.orig, expectedErr);
t.equal(err.message, 'new error');
res.send(err);
});
client.get('/throw', function(err, _, res) {
t.ok(err);
t.equal(res.statusCode, 500);
client.close();
server.close(function() {
t.end();
});
});
});
});
================================================
FILE: test/serverHttp2.test.js
================================================
'use strict';
/* eslint-disable func-names */
var path = require('path');
var fs = require('fs');
var http2;
// http2 module is not available < v8.4.0 (only with flag <= 8.8.0)
try {
http2 = require('http2');
} catch (err) {
console.log('HTTP2 module is not available');
console.log(
'Node.js version >= v8.8.8 required, current: ' + process.versions.node
);
return;
}
var restify = require('../lib');
if (require.cache[__dirname + '/lib/helper.js']) {
delete require.cache[__dirname + '/lib/helper.js'];
}
var helper = require('./lib/helper.js');
///--- Globals
var after = helper.after;
var before = helper.before;
var test = helper.test;
var CERT = fs.readFileSync(path.join(__dirname, './keys/http2-cert.pem'));
var KEY = fs.readFileSync(path.join(__dirname, './keys/http2-key.pem'));
var CA = fs.readFileSync(path.join(__dirname, 'keys/http2-csr.pem'));
var PORT = process.env.UNIT_TEST_PORT || 0;
var CLIENT;
var SERVER;
///--- Tests
before(function(cb) {
try {
SERVER = restify.createServer({
dtrace: helper.dtrace,
handleUncaughtExceptions: true,
http2: {
cert: CERT,
key: KEY,
ca: CA
},
log: helper.getLog('server')
});
SERVER.listen(PORT, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = http2.connect('https://127.0.0.1:' + PORT, {
rejectUnauthorized: false
});
cb();
});
} catch (e) {
console.error(e.stack);
process.exit(1);
}
});
after(function(cb) {
try {
CLIENT.destroy();
SERVER.close(function() {
CLIENT = null;
SERVER = null;
cb();
});
} catch (e) {
console.error(e.stack);
process.exit(1);
}
});
test('get (path only)', function(t) {
SERVER.get('/foo/:id', function echoId(req, res, next) {
t.ok(req.params);
t.equal(req.params.id, 'bar');
t.equal(req.isUpload(), false);
res.json({ hello: 'world' });
next();
});
var req = CLIENT.request({
':path': '/foo/bar',
':method': 'GET'
});
req.on('response', function(headers, flags) {
var data = '';
t.equal(headers[':status'], 200);
req.on('data', function(chunk) {
data += chunk;
});
req.on('end', function() {
t.deepEqual(JSON.parse(data), { hello: 'world' });
t.end();
});
});
req.on('error', function(err) {
t.ifError(err);
});
});
================================================
FILE: test/upgrade.test.js
================================================
// Copyright (c) 2013, Joyent, Inc. All rights reserved.
// vim: set ts=8 sts=8 sw=8 et:
'use strict';
/* eslint-disable func-names */
var restifyClients = require('restify-clients');
var Watershed = require('watershed').Watershed;
var restify = require('../lib');
if (require.cache[__dirname + '/lib/helper.js']) {
delete require.cache[__dirname + '/lib/helper.js'];
}
var helper = require('./lib/helper.js');
///--- Globals
var after = helper.after;
var before = helper.before;
var test = helper.test;
var PORT = process.env.UNIT_TEST_PORT || 0;
var CLIENT;
var SERVER;
var WATERSHED = new Watershed();
var SHEDLIST = [];
var TIMEOUT = 15000;
///--- Test Helper
function finish_latch(_test, _names) {
var complete = false;
var t = _test;
var names = _names;
var iv = setTimeout(function() {
if (complete) {
return;
}
complete = true;
t.ok(false, 'timeout after ' + TIMEOUT + 'ms');
t.ok(false, 'remaining latches: ' + Object.keys(names).join(', '));
t.done();
}, TIMEOUT);
return function(name, err) {
if (complete) {
return;
}
if (names[name] === undefined) {
complete = true;
t.ok(false, 'latch name "' + name + '" not expected');
t.ok(false, 'remaining latches: ' + Object.keys(names).join(', '));
t.done();
return;
}
if (--names[name] === 0) {
delete names[name];
}
/*
* Check that all latch names are done, and if so,
* end the test:
*/
if (Object.keys(names).length === 0) {
complete = true;
clearTimeout(iv);
iv = null;
t.done();
}
};
}
///--- Tests
before(function(cb) {
try {
SERVER = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server'),
version: ['2.0.0', '0.5.4', '1.4.3'],
handleUpgrades: true
});
SERVER.listen(PORT, '127.0.0.1', function() {
PORT = SERVER.address().port;
CLIENT = restifyClients.createHttpClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});
cb();
});
} catch (e) {
console.error(e.stack);
process.exit(1);
}
});
after(function(cb) {
try {
CLIENT.close();
SERVER.close(function() {
CLIENT = null;
SERVER = null;
cb();
});
while (SHEDLIST.length > 0) {
SHEDLIST.pop().destroy();
}
} catch (e) {
console.error(e.stack);
process.exit(1);
}
});
test('GET without upgrade headers', function(t) {
var done = finish_latch(t, {
'client response': 1,
'server response': 1
});
SERVER.get('/attach', function(req, res, next) {
t.ok(!res.claimUpgrade, 'res.claimUpgrade not present');
res.send(400);
next();
done('server response');
});
var options = {
headers: {
uprgade: 'ebfrockets' // this is intentional misspelling of upgrade
},
path: '/attach'
};
CLIENT.get(options, function(err, req) {
t.ifError(err);
req.on('error', function(err2) {
t.ifError(err2);
done('client error');
});
req.on('result', function(err2, res) {
if (err2 && err2.name !== 'BadRequestError') {
t.ifError(err2);
}
t.equal(res.statusCode, 400);
res.on('end', function() {
done('client response');
});
res.resume();
});
req.on('upgradeResult', function(err2, res) {
done('server upgraded unexpectedly');
});
});
});
test('Dueling upgrade and response handling 1', function(t) {
var done = finish_latch(t, {
'expected requestUpgrade error': 1,
'client response': 1
});
SERVER.get('/attach', function(req, res, next) {
try {
res.send(400);
} catch (ex) {
t.ifError(ex);
done('unxpected res.send error');
return;
}
try {
res.claimUpgrade();
} catch (ex) {
done('expected requestUpgrade error');
}
next(false);
});
var wskey = WATERSHED.generateKey();
var options = {
headers: {
connection: 'upgrade',
upgrade: 'websocket',
'sec-websocket-key': wskey
},
path: '/attach'
};
CLIENT.get(options, function(err, req) {
t.ifError(err);
req.on('error', function(err2) {
t.ifError(err2);
done('client error');
});
req.on('result', function(err2, res) {
if (err2 && err2.name !== 'BadRequestError') {
t.ifError(err2);
}
t.equal(res.statusCode, 400);
res.on('end', function() {
done('client response');
});
// noop data listener required for resume to take effect
res.on('data', function() {});
res.resume();
});
req.on('upgradeResult', function(err2, res) {
done('server upgraded unexpectedly');
});
});
});
test('Dueling upgrade and response handling 2', function(t) {
var done = finish_latch(t, {
'expected res.send error': 1,
'expected server to reset': 1
});
SERVER.get('/attach', function(req, res, next) {
try {
var upg = res.claimUpgrade();
upg.socket.destroy();
} catch (ex) {
t.ifError(ex);
done('unexpected requestUpgrade error');
}
try {
res.send(400);
} catch (ex) {
if (ex.name !== 'InvalidUpgradeStateError') {
t.ifError(ex);
}
done('expected res.send error');
return;
}
next(false);
});
var wskey = WATERSHED.generateKey();
var options = {
headers: {
connection: 'upgrade',
upgrade: 'websocket',
'sec-websocket-key': wskey
},
path: '/attach'
};
CLIENT.get(options, function(err, req) {
t.ifError(err);
done('expected server to reset');
return;
});
});
test('GET with upgrade headers', function(t) {
var done = finish_latch(t, {
'client shed end': 1,
'server shed end': 1
});
SERVER.get('/attach', function(req, res, next) {
t.ok(res.claimUpgrade, 'res.claimUpgrade present');
t.doesNotThrow(function() {
var upgrade = res.claimUpgrade();
var shed = WATERSHED.accept(req, upgrade.socket, upgrade.head);
SHEDLIST.push(shed);
shed.end("ok we're done here");
shed.on('error', function(err) {
t.ifError(err);
done('server shed error');
});
shed.on('end', function() {
done('server shed end');
});
next(false);
});
});
var wskey = WATERSHED.generateKey();
var options = {
headers: {
connection: 'upgrade',
upgrade: 'websocket',
'sec-websocket-key': wskey
},
path: '/attach'
};
CLIENT.get(options, function(err, req) {
t.ifError(err);
req.on('result', function(err2, res) {
t.ifError(err2);
t.ok(false, 'server did not upgrade');
done(true);
});
req.on('upgradeResult', function(err2, res, socket, head) {
t.ifError(err2);
t.ok(true, 'server upgraded');
t.equal(res.statusCode, 101);
t.equal(typeof socket, 'object');
t.ok(Buffer.isBuffer(head), 'head is Buffer');
t.doesNotThrow(function() {
var shed = WATERSHED.connect(res, socket, head, wskey);
SHEDLIST.push(shed);
shed.end('ok, done');
shed.on('error', function(err3) {
t.ifError(err3);
done('client shed error');
});
shed.on('end', function() {
done('client shed end');
});
});
});
});
});
test('GET with some websocket traffic', function(t) {
var done = finish_latch(t, {
'client shed end': 1,
'server shed end': 1,
'server receive message': 5,
'client receive message': 3
});
SERVER.get('/attach', function(req, res, next) {
t.ok(res.claimUpgrade, 'res.claimUpgrade present');
t.doesNotThrow(function() {
var upgrade = res.claimUpgrade();
var shed = WATERSHED.accept(req, upgrade.socket, upgrade.head);
SHEDLIST.push(shed);
shed.on('error', function(err) {
t.ifError(err);
done('server shed error');
});
shed.on('text', function(msg) {
if (msg === 'to server') {
done('server receive message');
}
});
shed.on('end', function() {
done('server shed end');
});
shed.send('to client');
shed.send('to client');
shed.send('to client');
next(false);
});
});
var wskey = WATERSHED.generateKey();
var options = {
headers: {
connection: 'upgrade',
upgrade: 'websocket',
'sec-websocket-key': wskey
},
path: '/attach'
};
CLIENT.get(options, function(err, req) {
t.ifError(err);
req.on('result', function(err2, res) {
t.ifError(err2);
t.ok(false, 'server did not upgrade');
done(true);
});
req.on('upgradeResult', function(err2, res, socket, head) {
t.ifError(err2);
t.ok(true, 'server upgraded');
t.equal(res.statusCode, 101);
t.equal(typeof socket, 'object');
t.ok(Buffer.isBuffer(head), 'head is Buffer');
t.doesNotThrow(function() {
var shed = WATERSHED.connect(res, socket, head, wskey);
SHEDLIST.push(shed);
shed.on('error', function(err3) {
t.ifError(err3);
done('client shed error');
});
shed.on('end', function() {
done('client shed end');
});
shed.on('text', function(msg) {
if (msg === 'to client') {
done('client receive message');
}
});
var count = 5;
var iv = setInterval(function() {
if (--count < 0) {
clearInterval(iv);
shed.end();
} else {
shed.send('to server');
}
}, 100);
});
});
});
});
================================================
FILE: test/utils.test.js
================================================
'use strict';
/* eslint-disable func-names */
var mergeQs = require('../lib/utils').mergeQs;
if (require.cache[__dirname + '/lib/helper.js']) {
delete require.cache[__dirname + '/lib/helper.js'];
}
var helper = require('./lib/helper.js');
///--- Globals
var test = helper.test;
test('merge qs', function(t) {
var qs1 = mergeQs(undefined, { a: 1 });
t.deepEqual(qs1, { a: 1 });
var qs2 = mergeQs({ a: 1 }, null);
t.deepEqual(qs2, { a: 1 });
var qs3 = mergeQs({ a: 1 }, { a: 2 });
t.deepEqual(qs3, { a: [1, 2] });
var qs4 = mergeQs({ a: 1 }, { b: 2 });
t.deepEqual(qs4, { a: 1, b: 2 });
var qs5 = mergeQs(null, null);
t.deepEqual(qs5, {});
t.done();
});
================================================
FILE: tools/docsBuild.js
================================================
'use strict';
var glob = require('glob');
var path = require('path');
var util = require('util');
var fs = require('fs');
var documentation = require('documentation');
var DOCS_PATH = '../docs';
var OUTPUT_PATH = '../docs/_api';
var LIB_PATH = '../lib';
var JEKYLL_HEADER_TEMPLATE = '---\ntitle: %s\npermalink: %s\n---\n\n%s';
var docsConfig = [
{
title: 'Server API',
permalink: '/docs/server-api/',
output: path.join(__dirname, OUTPUT_PATH, 'server.md'),
files: [
path.join(__dirname, LIB_PATH, 'index.js'),
path.join(__dirname, LIB_PATH, 'server.js')
],
config: path.join(__dirname, DOCS_PATH, 'config/server.yaml')
},
{
title: 'Request API',
permalink: '/docs/request-api/',
output: path.join(__dirname, OUTPUT_PATH, 'request.md'),
files: [path.join(__dirname, LIB_PATH, 'request.js')],
config: path.join(__dirname, DOCS_PATH, 'config/request.yaml')
},
{
title: 'Response API',
permalink: '/docs/response-api/',
output: path.join(__dirname, OUTPUT_PATH, 'response.md'),
files: [path.join(__dirname, LIB_PATH, 'response.js')]
},
{
title: 'Plugins API',
permalink: '/docs/plugins-api/',
output: path.join(__dirname, OUTPUT_PATH, 'plugins.md'),
files: [
// Pre plugins
path.join(__dirname, LIB_PATH, 'plugins/pre/context.js'),
path.join(__dirname, LIB_PATH, 'plugins/pre/dedupeSlashes.js'),
path.join(__dirname, LIB_PATH, 'plugins/pre/pause.js'),
path.join(__dirname, LIB_PATH, 'plugins/pre/prePath.js'),
path.join(__dirname, LIB_PATH, 'plugins/pre/reqIdHeaders.js'),
path.join(__dirname, LIB_PATH, 'plugins/pre/strictQueryParams.js'),
path.join(__dirname, LIB_PATH, 'plugins/pre/userAgent.js'),
// Use plugins
path.join(__dirname, LIB_PATH, 'plugins/accept.js'),
path.join(__dirname, LIB_PATH, 'plugins/authorization.js'),
path.join(__dirname, LIB_PATH, 'plugins/date.js'),
path.join(__dirname, LIB_PATH, 'plugins/query.js'),
path.join(__dirname, LIB_PATH, 'plugins/jsonp.js'),
path.join(__dirname, LIB_PATH, 'plugins/bodyParser.js'),
path.join(__dirname, LIB_PATH, 'plugins/requestLogger.js'),
path.join(__dirname, LIB_PATH, 'plugins/gzip.js'),
path.join(__dirname, LIB_PATH, 'plugins/static.js'),
path.join(__dirname, LIB_PATH, 'plugins/staticFiles.js'),
path.join(__dirname, LIB_PATH, 'plugins/throttle.js'),
path.join(__dirname, LIB_PATH, 'plugins/requestExpiry.js'),
path.join(
__dirname,
LIB_PATH,
'plugins/inflightRequestThrottle.js'
),
path.join(__dirname, LIB_PATH, 'plugins/cpuUsageThrottle.js'),
path.join(__dirname, LIB_PATH, 'plugins/conditionalHandler.js'),
path.join(__dirname, LIB_PATH, 'plugins/conditionalRequest.js'),
path.join(__dirname, LIB_PATH, 'plugins/audit.js'),
path.join(__dirname, LIB_PATH, 'plugins/metrics.js')
],
config: path.join(__dirname, DOCS_PATH, 'config/plugins.yaml')
},
{
title: 'Formatters API',
permalink: '/docs/formatters-api/',
output: path.join(__dirname, OUTPUT_PATH, 'formatters.md'),
files: glob.sync(path.join(__dirname, LIB_PATH, 'formatters', '*')),
config: path.join(__dirname, DOCS_PATH, 'config/formatters.yaml')
}
];
/**
* @function build
* @param {Object} options - Options
* @param {Array} options.files - Array of file paths ["./foo.js"]
* @param {String} options.config - Path to "config.yaml"
* @param {String} options.output - Path to output dir
* @param {String} options.title - Jekyll title
* @param {String} options.permalink - Jekyll permalink
* @returns {Promise} - Promise
*/
function build(options) {
return documentation
.build(options.files, {
shallow: true,
config: options.config
})
.then(function docsFormat(docs) {
return documentation.formats.md(docs, {
markdownToc: true
});
})
.then(function docsWrite(docsContent) {
var output = util.format(
JEKYLL_HEADER_TEMPLATE,
options.title,
options.permalink,
docsContent
);
fs.writeFileSync(options.output, output);
});
}
// eslint-disable-next-line
Promise.all(docsConfig.map(build))
.then(function onSucceed(res) {
console.log('Docs built');
process.exit(0);
})
.catch(function onError(err) {
console.error(err);
process.exit(1);
});
================================================
FILE: tools/mk/Makefile.defs
================================================
# -*- mode: makefile -*-
#
# Copyright (c) 2012, Joyent, Inc. All rights reserved.
#
# Makefile.defs: common defines.
#
# NOTE: This makefile comes from the "eng" repo. It's designed to be dropped
# into other repos as-is without requiring any modifications. If you find
# yourself changing this file, you should instead update the original copy in
# eng.git and then update your repo to use the new version.
#
# This makefile defines some useful defines. Include it at the top of
# your Makefile.
#
# Definitions in this Makefile:
#
# TOP The absolute path to the project directory. The top dir.
# BRANCH The current git branch.
# TIMESTAMP The timestamp for the build. This can be set via
# the TIMESTAMP envvar (used by MG-based builds).
# STAMP A build stamp to use in built package names.
#
TOP := $(shell pwd)
#
# Mountain Gorilla-spec'd versioning.
# See "Package Versioning" in MG's README.md:
#
#
# Need GNU awk for multi-char arg to "-F".
_AWK := $(shell (which gawk >/dev/null && echo gawk) \
|| (which nawk >/dev/null && echo nawk) \
|| echo awk)
BRANCH := $(shell git log -n 1 --pretty=%d HEAD | $(_AWK) '{print $$NF}' | sed -e 's/.$$//' | cut -d/ -f2)
ifeq ($(TIMESTAMP),)
TIMESTAMP := $(shell date -u "+%Y%m%dT%H%M%SZ")
endif
_GITDESCRIBE := g$(shell git describe --all --long --dirty | $(_AWK) -F'-g' '{print $$NF}')
STAMP := $(BRANCH)-$(TIMESTAMP)-$(_GITDESCRIBE)
================================================
FILE: tools/mk/Makefile.deps
================================================
# -*- mode: makefile -*-
#
# Copyright (c) 2012, Joyent, Inc. All rights reserved.
#
# Makefile.deps: Makefile for including common tools as dependencies
#
# NOTE: This makefile comes from the "eng" repo. It's designed to be dropped
# into other repos as-is without requiring any modifications. If you find
# yourself changing this file, you should instead update the original copy in
# eng.git and then update your repo to use the new version.
#
# This file is separate from Makefile.targ so that teams can choose
# independently whether to use the common targets in Makefile.targ and the
# common tools here.
#
#
# restdown
#
RESTDOWN_EXEC ?= deps/restdown/bin/restdown
RESTDOWN ?= python $(RESTDOWN_EXEC)
$(RESTDOWN_EXEC): | deps/restdown/.git
================================================
FILE: tools/mk/Makefile.targ
================================================
# -*- mode: makefile -*-
#
# Copyright (c) 2012, Joyent, Inc. All rights reserved.
#
# Makefile.targ: common targets.
#
# NOTE: This makefile comes from the "eng" repo. It's designed to be dropped
# into other repos as-is without requiring any modifications. If you find
# yourself changing this file, you should instead update the original copy in
# eng.git and then update your repo to use the new version.
#
# This Makefile defines several useful targets and rules. You can use it by
# including it from a Makefile that specifies some of the variables below.
#
# Targets defined in this Makefile:
#
# check Checks JavaScript files for lint and style
# Checks bash scripts for syntax
# Checks SMF manifests for validity against the SMF DTD
#
# clean Removes built files
#
# docs Builds restdown documentation in docs/
#
# prepush Depends on "check" and "test"
#
# test Does nothing (you should override this)
#
# xref Generates cscope (source cross-reference index)
#
# For details on what these targets are supposed to do, see the Joyent
# Engineering Guide.
#
# To make use of these targets, you'll need to set some of these variables. Any
# variables left unset will simply not be used.
#
# BASH_FILES Bash scripts to check for syntax
# (paths relative to top-level Makefile)
#
# CLEAN_FILES Files to remove as part of the "clean" target. Note
# that files generated by targets in this Makefile are
# automatically included in CLEAN_FILES. These include
# restdown-generated HTML and JSON files.
#
# DOC_FILES Restdown (documentation source) files. These are
# assumed to be contained in "docs/", and must NOT
# contain the "docs/" prefix.
##
# You can also override these variables:
#
# BASH Path to bash (default: bash)
#
# CSCOPE_DIRS Directories to search for source files for the cscope
# index. (default: ".")
#
#
# Defaults for the various tools we use.
#
BASH ?= bash
BASHSTYLE ?= tools/bashstyle
CP ?= cp
CSCOPE ?= cscope
CSCOPE_DIRS ?= .
MKDIR ?= mkdir -p
MV ?= mv
RESTDOWN_FLAGS ?= -b docs/branding
RMTREE ?= rm -rf
ifeq ($(shell uname -s),SunOS)
TAR ?= gtar
else
TAR ?= tar
endif
#
# Defaults for other fixed values.
#
BUILD = build
DISTCLEAN_FILES += $(BUILD)
DOC_BUILD = $(BUILD)/docs/public
#
# Targets. For descriptions on what these are supposed to do, see the
# Joyent Engineering Guide.
#
#
# Instruct make to keep around temporary files. We have rules below that
# automatically update git submodules as needed, but they employ a deps/*/.git
# temporary file. Without this directive, make tries to remove these .git
# directories after the build has completed.
#
.SECONDARY: $($(wildcard deps/*):%=%/.git)
#
# This rule enables other rules that use files from a git submodule to have
# those files depend on deps/module/.git and have "make" automatically check
# out the submodule as needed.
#
deps/%/.git:
git submodule update --init deps/$*
#
# These recipes make heavy use of dynamically-created phony targets. The parent
# Makefile defines a list of input files like BASH_FILES. We then say that each
# of these files depends on a fake target called filename.bashchk, and then we
# define a pattern rule for those targets that runs bash in check-syntax-only
# mode. This mechanism has the nice properties that if you specify zero files,
# the rule becomes a noop (unlike a single rule to check all bash files, which
# would invoke bash with zero files), and you can check individual files from
# the command line with "make filename.bashchk".
#
.PHONY: check-bash
check-bash: $(BASH_FILES:%=%.bashchk) $(BASH_FILES:%=%.bashstyle)
%.bashchk: %
$(BASH) -n $^
%.bashstyle: %
$(BASHSTYLE) $^
.PHONY: check-eslint
check-eslint::
$(ESLINT) $(JS_FILES)
.PHONY: check-lint
check-lint::
@(echo 'Running "make check-lint"'; NO_STYLE=true $(ESLINT) $(JS_FILES))
.PHONY: check-style
check-style::
@(echo 'Running "make check-style"'; NO_LINT=true $(ESLINT) $(JS_FILES) || (echo 'Run "make fix-style" to auto-fix styling issues'; exit 1))
.PHONY: fix-style
fix-style::
$(PRETTIER) --write "**/*.js"
.PHONY: check
check: check-lint check-style check-bash
@echo check ok
.PHONY: clean
clean::
-$(RMTREE) $(CLEAN_FILES)
.PHONY: distclean
distclean:: clean
-$(RMTREE) $(DISTCLEAN_FILES)
CSCOPE_FILES = cscope.in.out cscope.out cscope.po.out
CLEAN_FILES += $(CSCOPE_FILES)
.PHONY: xref
xref: cscope.files
$(CSCOPE) -bqR
.PHONY: cscope.files
cscope.files:
find $(CSCOPE_DIRS) -name '*.c' -o -name '*.h' -o -name '*.cc' \
-o -name '*.js' -o -name '*.s' -o -name '*.cpp' > $@
#
# The "docs" target is complicated because we do several things here:
#
# (1) Use restdown to build HTML and JSON files from each of DOC_FILES.
#
# (2) Copy these files into $(DOC_BUILD) (build/docs/public), which
# functions as a complete copy of the documentation that could be
# mirrored or served over HTTP.
#
# (3) Then copy any directories and media from docs/media into
# $(DOC_BUILD)/media. This allows projects to include their own media,
# including files that will override same-named files provided by
# restdown.
#
# Step (3) is the surprisingly complex part: in order to do this, we need to
# identify the subdirectories in docs/media, recreate them in
# $(DOC_BUILD)/media, then do the same with the files.
#
DOC_MEDIA_DIRS := $(shell find docs/media -type d 2>/dev/null | grep -v "^docs/media$$")
DOC_MEDIA_DIRS := $(DOC_MEDIA_DIRS:docs/media/%=%)
DOC_MEDIA_DIRS_BUILD := $(DOC_MEDIA_DIRS:%=$(DOC_BUILD)/media/%)
DOC_MEDIA_FILES := $(shell find docs/media -type f 2>/dev/null)
DOC_MEDIA_FILES := $(DOC_MEDIA_FILES:docs/media/%=%)
DOC_MEDIA_FILES_BUILD := $(DOC_MEDIA_FILES:%=$(DOC_BUILD)/media/%)
#
# Like the other targets, "docs" just depends on the final files we want to
# create in $(DOC_BUILD), leveraging other targets and recipes to define how
# to get there.
#
.PHONY: docs
docs: \
$(DOC_FILES:%.restdown=$(DOC_BUILD)/%.html) \
$(DOC_FILES:%.restdown=$(DOC_BUILD)/%.json) \
$(DOC_MEDIA_FILES_BUILD)
#
# We keep the intermediate files so that the next build can see whether the
# files in DOC_BUILD are up to date.
#
.PRECIOUS: \
$(DOC_FILES:%.restdown=docs/%.html) \
$(DOC_FILES:%.restdown=docs/%json)
#
# We do clean those intermediate files, as well as all of DOC_BUILD.
#
CLEAN_FILES += \
$(DOC_BUILD) \
$(DOC_FILES:%.restdown=docs/%.html) \
$(DOC_FILES:%.restdown=docs/%.json)
#
# Before installing the files, we must make sure the directories exist. The |
# syntax tells make that the dependency need only exist, not be up to date.
# Otherwise, it might try to rebuild spuriously because the directory itself
# appears out of date.
#
$(DOC_MEDIA_FILES_BUILD): | $(DOC_MEDIA_DIRS_BUILD)
$(DOC_BUILD)/%: docs/% | $(DOC_BUILD)
$(CP) $< $@
docs/%.json docs/%.html: docs/%.restdown | $(DOC_BUILD) $(RESTDOWN_EXEC)
$(RESTDOWN) $(RESTDOWN_FLAGS) -m $(DOC_BUILD) $<
$(DOC_BUILD):
$(MKDIR) $@
$(DOC_MEDIA_DIRS_BUILD):
$(MKDIR) $@
#
# The default "test" target does nothing. This should usually be overridden by
# the parent Makefile. It's included here so we can define "prepush" without
# requiring the repo to define "test".
#
.PHONY: test
test:
.PHONY: prepush
prepush: check test