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 ![restify](/../gh-images/logo/png/restify_logo_black_transp_288x288.png?raw=true "restify") [![Build Status](https://travis-ci.org/restify/node-restify.svg?branch=master)](https://travis-ci.org/restify/node-restify) [![Dependency Status](https://david-dm.org/restify/node-restify.svg)](https://david-dm.org/restify/node-restify) [![devDependency Status](https://david-dm.org/restify/node-restify/dev-status.svg)](https://david-dm.org/restify/node-restify#info=devDependencies) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](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: * * * server.on('after', restify.plugins.auditLogger({ * log: pino( * {name: 'audit'}, * process.stdout * ), * event: 'after', * server: SERVER, * logMetrics : logBuffer, * printLog : true * })); * * @example * * 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: * * * { * "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": { * "requestLogger": 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 * } * * @example * * 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: * * server.get('/docs/current/*', restify.plugins.serveStatic({ * directory: './documentation/v1', * default: 'index.html' * })); * @example * * 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 * * server.get('/public/*', // don't forget the `/*` * restify.plugins.serveStaticFiles('./documentation/v1') * ); * @example * * 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. * * 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 * @example * * `header()` can also be used to automatically chain header values * when applicable: * * res.header('x-foo', 'a'); * res.header('x-foo', 'b'); * // => { 'x-foo': ['a', 'b'] } * @example * * 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 * using with async/await * server.pre(async function(req, res) { * await somethingAsync(); * somethingSync(); * } * @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 * using with async/await * server.use(async function(req, res) { * await somethingAsync(); * somethingSync(); * } * @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