├── .eslintrc ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── codeql │ └── codeql-config.yml └── workflows │ ├── codeql-analysis.yml │ └── continuous-integration.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── CHANGES.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── commitlint.config.js ├── index.d.ts ├── intl ├── cs │ └── messages.json ├── de │ └── messages.json ├── en │ └── messages.json ├── es │ └── messages.json ├── fr │ └── messages.json ├── it │ └── messages.json ├── ja │ └── messages.json ├── ko │ └── messages.json ├── nl │ └── messages.json ├── pl │ └── messages.json ├── pt │ └── messages.json ├── ru │ └── messages.json ├── tr │ └── messages.json ├── zh-Hans │ └── messages.json └── zh-Hant │ └── messages.json ├── lib ├── clone.js ├── content-negotiation.js ├── data-builder.js ├── handler.js ├── logger.js ├── send-html.js ├── send-json.js └── send-xml.js ├── package-lock.json ├── package.json ├── renovate.json ├── test └── handler.test.js └── views ├── default-error.ejs └── style.css /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "loopback" 3 | } 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Description/Steps to reproduce 8 | 9 | 13 | 14 | # Link to reproduction sandbox 15 | 16 | 21 | 22 | # Expected result 23 | 24 | 27 | 28 | # Additional information 29 | 30 | 35 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 4 | #### Related issues 5 | 6 | 12 | 13 | - connect to 14 | 15 | ### Checklist 16 | 17 | 22 | 23 | - [ ] New tests added or existing tests modified to cover all changes 24 | - [ ] Code conforms with the [style 25 | guide](http://loopback.io/doc/en/contrib/style-guide.html) 26 | -------------------------------------------------------------------------------- /.github/codeql/codeql-config.yml: -------------------------------------------------------------------------------- 1 | paths-ignore: 2 | - '**/test/**' 3 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 13 * * 6' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v3 20 | 21 | - name: Initialize CodeQL 22 | uses: github/codeql-action/init@v2 23 | with: 24 | languages: 'javascript' 25 | config-file: ./.github/codeql/codeql-config.yml 26 | 27 | - name: Perform CodeQL Analysis 28 | uses: github/codeql-action/analyze@v2 29 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 2 * * 1' # At 02:00 on Monday 11 | 12 | env: 13 | NODE_OPTIONS: --max-old-space-size=4096 14 | 15 | jobs: 16 | test: 17 | name: Test 18 | timeout-minutes: 15 19 | strategy: 20 | matrix: 21 | os: [ubuntu-latest] 22 | node-version: [10, 12, 14, 16] 23 | include: 24 | - os: macos-latest 25 | node-version: 14 # LTS 26 | fail-fast: false 27 | runs-on: ${{ matrix.os }} 28 | steps: 29 | - uses: actions/checkout@v3 30 | with: 31 | fetch-depth: 0 32 | - name: Use Node.js ${{ matrix.node-version }} 33 | uses: actions/setup-node@v3 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | - name: Bootstrap project 37 | run: | 38 | npm ci --ignore-scripts 39 | - uses: Yuri6037/Action-FakeTTY@v1.1 40 | - name: Run tests 41 | run: faketty npm test --ignore-scripts 42 | 43 | code-lint: 44 | name: Code Lint 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v3 48 | - name: Use Node.js 14 49 | uses: actions/setup-node@v3 50 | with: 51 | node-version: 14 52 | - name: Bootstrap project 53 | run: | 54 | npm ci --ignore-scripts 55 | - name: Verify code linting 56 | run: npm run lint 57 | 58 | commit-lint: 59 | name: Commit Lint 60 | runs-on: ubuntu-latest 61 | if: ${{ github.event.pull_request }} 62 | steps: 63 | - uses: actions/checkout@v3 64 | with: 65 | fetch-depth: 0 66 | - name: Use Node.js 14 67 | uses: actions/setup-node@v3 68 | with: 69 | node-version: 14 70 | - name: Bootstrap project 71 | run: | 72 | npm ci --ignore-scripts 73 | - name: Verify commit linting 74 | run: npx commitlint --from origin/master --to HEAD --verbose 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | test 35 | .travis.yml 36 | 37 | grafana*.tar.gz -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=true 2 | scripts-prepend-node-path=true 3 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 2023-07-11, Version 4.0.7 2 | ========================= 3 | 4 | * chore: lock file maintenance (renovate[bot]) 5 | 6 | * chore: update dependency eslint to ^8.44.0 (renovate[bot]) 7 | 8 | * chore: update dependency @commitlint/config-conventional to ^17.6.6 (renovate[bot]) 9 | 10 | * chore: update dependency eslint to ^8.43.0 (renovate[bot]) 11 | 12 | 13 | 2023-06-13, Version 4.0.6 14 | ========================= 15 | 16 | * chore: lock file maintenance (renovate[bot]) 17 | 18 | * fix: remove grafana from package (dhmlau) 19 | 20 | * chore: update dependency eslint to ^8.42.0 (renovate[bot]) 21 | 22 | 23 | 2023-06-02, Version 4.0.5 24 | ========================= 25 | 26 | * chore: update dependency @commitlint/config-conventional to ^17.6.5 (renovate[bot]) 27 | 28 | * chore: lock file maintenance (renovate[bot]) 29 | 30 | * chore: update dependency eslint to ^8.41.0 (renovate[bot]) 31 | 32 | 33 | 2023-05-15, Version 4.0.4 34 | ========================= 35 | 36 | * chore: lock file maintenance (renovate[bot]) 37 | 38 | * chore: update dependency eslint to ^8.40.0 (renovate[bot]) 39 | 40 | * chore: update dependency @commitlint/config-conventional to ^17.6.3 (renovate[bot]) 41 | 42 | * chore: update dependency eslint to ^8.39.0 (renovate[bot]) 43 | 44 | * chore: update dependency @commitlint/config-conventional to ^17.6.1 (renovate[bot]) 45 | 46 | 47 | 2023-04-13, Version 4.0.3 48 | ========================= 49 | 50 | * chore: update dependency @commitlint/config-conventional to ^17.6.0 (renovate[bot]) 51 | 52 | * chore: update dependency eslint to ^8.38.0 (renovate[bot]) 53 | 54 | * chore: lock file maintenance (renovate[bot]) 55 | 56 | * chore: update dependency eslint to ^8.37.0 (renovate[bot]) 57 | 58 | * chore: update dependency ejs to ^3.1.9 (renovate[bot]) 59 | 60 | * chore: update dependency eslint to ^8.36.0 (renovate[bot]) 61 | 62 | 63 | 2023-03-09, Version 4.0.2 64 | ========================= 65 | 66 | * chore: lock file maintenance (renovate[bot]) 67 | 68 | * chore: update dependency eslint to ^8.35.0 (renovate[bot]) 69 | 70 | * chore: update dependency http-status to ^1.6.2 (renovate[bot]) 71 | 72 | * chore: update dependency eslint to ^8.34.0 (renovate[bot]) 73 | 74 | * chore: update dependency @commitlint/config-conventional to ^17.4.4 (renovate[bot]) 75 | 76 | * chore: update dependency @types/express to ^4.17.17 (renovate[bot]) 77 | 78 | * chore: update dependency eslint to ^8.31.0 (renovate[bot]) 79 | 80 | * chore: update dependency eslint to ^8.30.0 (renovate[bot]) 81 | 82 | * chore: update dependency @types/express to ^4.17.15 (renovate[bot]) 83 | 84 | * chore: update dependency mocha to ^10.2.0 (renovate[bot]) 85 | 86 | * chore: update dependency supertest to ^6.3.3 (renovate[bot]) 87 | 88 | * chore: update dependency eslint to ^8.29.0 (renovate[bot]) 89 | 90 | * chore: update dependency supertest to ^6.3.2 (renovate[bot]) 91 | 92 | * chore: update dependency @commitlint/config-conventional to ^17.3.0 (renovate[bot]) 93 | 94 | * chore: update dependency eslint to ^8.28.0 (renovate[bot]) 95 | 96 | 97 | 2022-11-07, Version 4.0.1 98 | ========================= 99 | 100 | * chore: update dependency mocha to v10 (renovate[bot]) 101 | 102 | * chore: update dependency chai to ^4.3.7 (renovate[bot]) 103 | 104 | * chore: lock file maintenance (renovate[bot]) 105 | 106 | * chore: update dependency eslint to ^8.27.0 (renovate[bot]) 107 | 108 | * chore: update dependency eslint to v8 (renovate[bot]) 109 | 110 | * chore: update dependency @commitlint/config-conventional to ^17.2.0 (renovate[bot]) 111 | 112 | * chore: update dependency supertest to ^6.3.1 (renovate[bot]) 113 | 114 | * chore: update dependency express to ^4.18.2 (renovate[bot]) 115 | 116 | * chore: update dependency supertest to ^6.3.0 (renovate[bot]) 117 | 118 | * chore: update dependency @types/express to ^4.17.14 (renovate[bot]) 119 | 120 | * chore: update dependency @commitlint/config-conventional to v17 (renovate[bot]) 121 | 122 | * chore: update dependency http-status to ^1.5.3 (renovate[bot]) 123 | 124 | * chore: update dependency supertest to ^6.2.4 (renovate[bot]) 125 | 126 | * chore: lock file maintenance (Renovate Bot) 127 | 128 | * chore: update dependency ejs to ^3.1.8 (Renovate Bot) 129 | 130 | * chore: update dependency http-status to ^1.5.2 (Renovate Bot) 131 | 132 | * chore: update dependency supertest to ^6.2.3 (Renovate Bot) 133 | 134 | * chore: update dependency @commitlint/config-conventional to ^16.2.4 (Renovate Bot) 135 | 136 | * chore: update github/codeql-action action to v2 (Renovate Bot) 137 | 138 | * chore: update dependency express to ^4.18.1 (Renovate Bot) 139 | 140 | * chore: update dependency ejs to ^3.1.7 (Renovate Bot) 141 | 142 | * chore: update dependency http-status to ^1.5.1 (Renovate Bot) 143 | 144 | * chore: update dependency @commitlint/config-conventional to v16 (Renovate Bot) 145 | 146 | * chore: update actions/setup-node action to v3 (Renovate Bot) 147 | 148 | * chore: update actions/checkout action to v3 (Renovate Bot) 149 | 150 | * chore: update dependency supertest to ^6.2.2 (Renovate Bot) 151 | 152 | * chore: update dependency mocha to ^9.2.2 (Renovate Bot) 153 | 154 | * chore: update dependency fast-safe-stringify to ^2.1.1 (Renovate Bot) 155 | 156 | * chore: update dependency eslint to ^7.32.0 (Renovate Bot) 157 | 158 | * chore: update dependency js2xmlparser to ^4.0.2 (Renovate Bot) 159 | 160 | * chore: update dependency ejs to ^3.1.6 (Renovate Bot) 161 | 162 | * chore: update dependency strong-globalize to ^6.0.5 (Renovate Bot) 163 | 164 | * chore: update dependency http-status to ^1.5.0 (Renovate Bot) 165 | 166 | * chore: update dependency express to ^4.17.3 (Renovate Bot) 167 | 168 | * chore: update dependency debug to ^4.3.4 (Renovate Bot) 169 | 170 | * chore: update dependency accepts to ^1.3.8 (Renovate Bot) 171 | 172 | * chore: update dependency chai to ^4.3.6 (Renovate Bot) 173 | 174 | * ci: add renovate config (Rifa Achrinza) 175 | 176 | * docs: add SECURITY.md (Diana Lau) 177 | 178 | * docs: update coc (Diana Lau) 179 | 180 | * docs: add code of conduct (Diana Lau) 181 | 182 | * feat: update CI pipeline (Rifa Achrinza) 183 | 184 | * chore: move repo to loopbackio org (Diana Lau) 185 | 186 | 187 | 2020-10-13, Version 4.0.0 188 | ========================= 189 | 190 | * docs: update LTS versions in README (Miroslav Bajtoš) 191 | 192 | * [SEMVER-MAJOR] Reword log messages for clarity (Miroslav Bajtoš) 193 | 194 | 195 | 2020-06-23, Version 3.5.0 196 | ========================= 197 | 198 | * feat: add options.rootProperty for json/xml (Raymond Feng) 199 | 200 | * chore: update deps and drop Node 8.x support (Raymond Feng) 201 | 202 | 203 | 2019-10-12, Version 3.4.0 204 | ========================= 205 | 206 | * chore: js2xmlparser to ^4.0.0 (Miroslav Bajtoš) 207 | 208 | * chore: update dev-dependencies (mocha, supertest) (Miroslav Bajtoš) 209 | 210 | * chore: update eslint & config to latest (Miroslav Bajtoš) 211 | 212 | * chore: update strong-globalize to ^5.0.2 (Miroslav Bajtoš) 213 | 214 | * chore: update debug to ^4.1.1 (Miroslav Bajtoš) 215 | 216 | * feat: drop support for Node.js 6.x (Miroslav Bajtoš) 217 | 218 | 219 | 2019-09-30, Version 3.3.0 220 | ========================= 221 | 222 | * fix: handle Error objects with circular properties (dkrantsberg) 223 | 224 | * chore: update copyrights years (Agnes Lin) 225 | 226 | 227 | 2018-08-30, Version 3.2.0 228 | ========================= 229 | 230 | * Add type definition and writeErrorToResponse (shimks) 231 | 232 | 233 | 2018-07-16, Version 3.1.0 234 | ========================= 235 | 236 | * [WebFM] cs/pl/ru translation (candytangnb) 237 | 238 | 239 | 2018-06-11, Version 3.0.0 240 | ========================= 241 | 242 | * Allow safeFields to work with arrays (shimks) 243 | 244 | * run lint (shimks) 245 | 246 | * drop node 4 from travis and update dependencies (shimks) 247 | 248 | 249 | 2018-03-05, Version 2.3.2 250 | ========================= 251 | 252 | * Undefined safeFields revert to data #71 (Zak Barbuto) 253 | 254 | 255 | 2018-01-25, Version 2.3.1 256 | ========================= 257 | 258 | * Escape strings in HTML output (XSS fix) (Zachery Metcalf) 259 | 260 | * Update LICENSE.md (Diana Lau) 261 | 262 | 263 | 2017-10-13, Version 2.3.0 264 | ========================= 265 | 266 | * update strong-globalize to 3.1.0 (shimks) 267 | 268 | * CODEOWNERS: add zbarbuto (Miroslav Bajtoš) 269 | 270 | * Update Issue and PR Templates (#59) (Sakib Hasan) 271 | 272 | * fixed json typo of server/middleware.json (karanssj4) 273 | 274 | * Add CODEOWNER file (Diana Lau) 275 | 276 | 277 | 2017-07-20, Version 2.2.0 278 | ========================= 279 | 280 | * Add new option: negotiateContentType (Raj) 281 | 282 | 283 | 2017-04-18, Version 2.1.0 284 | ========================= 285 | 286 | * Bump js2xmlparser dependency to version 3.0.0 (Matthew O'Donoghue) 287 | 288 | 289 | 2017-03-22, Version 2.0.0 290 | ========================= 291 | 292 | * Fix markdown formatting in README (Miroslav Bajtoš) 293 | 294 | * Fix the order of arguments in the jsdoc comment. (Charlie Schliesser) 295 | 296 | * Update readme with added XML support (David Cheung) 297 | 298 | * Add a machine-readable "code" property (Zak Barbuto) 299 | 300 | * Upgrade dependencies to their latest versions (Miroslav Bajtoš) 301 | 302 | * Describe "safeFields" option in README (Zak Barbuto) 303 | 304 | * Drop support for Node v0.10/v0.12 (Miroslav Bajtoš) 305 | 306 | 307 | 2017-01-30, Version 1.2.1 308 | ========================= 309 | 310 | * Stop adding safeFields to original options arg (Miroslav Bajtoš) 311 | 312 | 313 | 2017-01-30, Version 1.2.0 314 | ========================= 315 | 316 | * Support options.safeFields (Zak Barbuto) 317 | 318 | * Readme cleanup (#36) (Rand McKinney) 319 | 320 | * xml support added (Ahmet Ozisik) 321 | 322 | * Update paid support URL (Siddhi Pai) 323 | 324 | * Downstream ignore dashboard-controller (Simon Ho) 325 | 326 | * Update pt translation file (Candy) 327 | 328 | * Make the examples more clear (Amir Jafarian) 329 | 330 | * Fix readme (Amir Jafarian) 331 | 332 | 333 | 2016-10-07, Version 1.1.1 334 | ========================= 335 | 336 | * Update pt translation file (Candy) 337 | 338 | * Update translation files - round#2 (Candy) 339 | 340 | * globalization: add translated strings (gunjpan) 341 | 342 | 343 | 2016-09-05, Version 1.1.0 344 | ========================= 345 | 346 | * Fix minor Syntax error (Loay) 347 | 348 | * Globalize strong-error-handler (David Cheung) 349 | 350 | * Update eslint infrastructure (Loay) 351 | 352 | * Add documentation (Loay) 353 | 354 | * Improve grammar in readme. (Richard Pringle) 355 | 356 | * Test with express instead of http server (David Cheung) 357 | 358 | * HTML response for accepted headers (David Cheung) 359 | 360 | 361 | 2016-05-26, Version 1.0.1 362 | ========================= 363 | 364 | * Remove statusCode from details in Array errors (David Cheung) 365 | 366 | 367 | 2016-05-20, Version 1.0.0 368 | ========================= 369 | 370 | * First release! 371 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners, 3 | # the last matching pattern has the most precendence. 4 | 5 | * @bajtos @zbarbuto 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | LoopBack, as member project of the OpenJS Foundation, use 4 | [Contributor Covenant v2.0](https://contributor-covenant.org/version/2/0/code_of_conduct) 5 | as their code of conduct. The full text is included 6 | [below](#contributor-covenant-code-of-conduct-v2.0) in English, and translations 7 | are available from the Contributor Covenant organisation: 8 | 9 | - [contributor-covenant.org/translations](https://www.contributor-covenant.org/translations) 10 | - [github.com/ContributorCovenant](https://github.com/ContributorCovenant/contributor_covenant/tree/release/content/version/2/0) 11 | 12 | Refer to the sections on reporting and escalation in this document for the 13 | specific emails that can be used to report and escalate issues. 14 | 15 | ## Reporting 16 | 17 | ### Project Spaces 18 | 19 | For reporting issues in spaces related to LoopBack, please use the email 20 | `tsc@loopback.io`. The LoopBack Technical Steering Committee (TSC) handles CoC issues related to the spaces that it 21 | maintains. The project TSC commits to: 22 | 23 | - maintain the confidentiality with regard to the reporter of an incident 24 | - to participate in the path for escalation as outlined in the section on 25 | Escalation when required. 26 | 27 | ### Foundation Spaces 28 | 29 | For reporting issues in spaces managed by the OpenJS Foundation, for example, 30 | repositories within the OpenJS organization, use the email 31 | `report@lists.openjsf.org`. The Cross Project Council (CPC) is responsible for 32 | managing these reports and commits to: 33 | 34 | - maintain the confidentiality with regard to the reporter of an incident 35 | - to participate in the path for escalation as outlined in the section on 36 | Escalation when required. 37 | 38 | ## Escalation 39 | 40 | The OpenJS Foundation maintains a Code of Conduct Panel (CoCP). This is a 41 | foundation-wide team established to manage escalation when a reporter believes 42 | that a report to a member project or the CPC has not been properly handled. In 43 | order to escalate to the CoCP send an email to 44 | `coc-escalation@lists.openjsf.org`. 45 | 46 | For more information, refer to the full 47 | [Code of Conduct governance document](https://github.com/openjs-foundation/cross-project-council/blob/HEAD/CODE_OF_CONDUCT.md). 48 | 49 | --- 50 | 51 | ## Contributor Covenant Code of Conduct v2.0 52 | 53 | ## Our Pledge 54 | 55 | We as members, contributors, and leaders pledge to make participation in our 56 | community a harassment-free experience for everyone, regardless of age, body 57 | size, visible or invisible disability, ethnicity, sex characteristics, gender 58 | identity and expression, level of experience, education, socio-economic status, 59 | nationality, personal appearance, race, religion, or sexual identity and 60 | orientation. 61 | 62 | We pledge to act and interact in ways that contribute to an open, welcoming, 63 | diverse, inclusive, and healthy community. 64 | 65 | ## Our Standards 66 | 67 | Examples of behavior that contributes to a positive environment for our 68 | community include: 69 | 70 | - Demonstrating empathy and kindness toward other people 71 | - Being respectful of differing opinions, viewpoints, and experiences 72 | - Giving and gracefully accepting constructive feedback 73 | - Accepting responsibility and apologizing to those affected by our mistakes, 74 | and learning from the experience 75 | - Focusing on what is best not just for us as individuals, but for the overall 76 | community 77 | 78 | Examples of unacceptable behavior include: 79 | 80 | - The use of sexualized language or imagery, and sexual attention or advances of 81 | any kind 82 | - Trolling, insulting or derogatory comments, and personal or political attacks 83 | - Public or private harassment 84 | - Publishing others' private information, such as a physical or email address, 85 | without their explicit permission 86 | - Other conduct which could reasonably be considered inappropriate in a 87 | professional setting 88 | 89 | ## Enforcement Responsibilities 90 | 91 | Community leaders are responsible for clarifying and enforcing our standards of 92 | acceptable behavior and will take appropriate and fair corrective action in 93 | response to any behavior that they deem inappropriate, threatening, offensive, 94 | or harmful. 95 | 96 | Community leaders have the right and responsibility to remove, edit, or reject 97 | comments, commits, code, wiki edits, issues, and other contributions that are 98 | not aligned to this Code of Conduct, and will communicate reasons for moderation 99 | decisions when appropriate. 100 | 101 | ## Scope 102 | 103 | This Code of Conduct applies within all community spaces, and also applies when 104 | an individual is officially representing the community in public spaces. 105 | Examples of representing our community include using an official e-mail address, 106 | posting via an official social media account, or acting as an appointed 107 | representative at an online or offline event. 108 | 109 | ## Enforcement 110 | 111 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 112 | reported to the community leaders responsible for enforcement at 113 | [tsc@loopback.io](mailto:tsc@loopback.io). All complaints will be reviewed and 114 | investigated promptly and fairly. 115 | 116 | All community leaders are obligated to respect the privacy and security of the 117 | reporter of any incident. 118 | 119 | ## Enforcement Guidelines 120 | 121 | Community leaders will follow these Community Impact Guidelines in determining 122 | the consequences for any action they deem in violation of this Code of Conduct: 123 | 124 | ### 1. Correction 125 | 126 | **Community Impact**: Use of inappropriate language or other behavior deemed 127 | unprofessional or unwelcome in the community. 128 | 129 | **Consequence**: A private, written warning from community leaders, providing 130 | clarity around the nature of the violation and an explanation of why the 131 | behavior was inappropriate. A public apology may be requested. 132 | 133 | ### 2. Warning 134 | 135 | **Community Impact**: A violation through a single incident or series of 136 | actions. 137 | 138 | **Consequence**: A warning with consequences for continued behavior. No 139 | interaction with the people involved, including unsolicited interaction with 140 | those enforcing the Code of Conduct, for a specified period of time. This 141 | includes avoiding interactions in community spaces as well as external channels 142 | like social media. Violating these terms may lead to a temporary or permanent 143 | ban. 144 | 145 | ### 3. Temporary Ban 146 | 147 | **Community Impact**: A serious violation of community standards, including 148 | sustained inappropriate behavior. 149 | 150 | **Consequence**: A temporary ban from any sort of interaction or public 151 | communication with the community for a specified period of time. No public or 152 | private interaction with the people involved, including unsolicited interaction 153 | with those enforcing the Code of Conduct, is allowed during this period. 154 | Violating these terms may lead to a permanent ban. 155 | 156 | ### 4. Permanent Ban 157 | 158 | **Community Impact**: Demonstrating a pattern of violation of community 159 | standards, including sustained inappropriate behavior, harassment of an 160 | individual, or aggression toward or disparagement of classes of individuals. 161 | 162 | **Consequence**: A permanent ban from any sort of public interaction within the 163 | community. 164 | 165 | ## Attribution 166 | 167 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 168 | version 2.0, available at 169 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 170 | 171 | Community Impact Guidelines were inspired by 172 | [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 173 | 174 | [homepage]: https://www.contributor-covenant.org 175 | 176 | For answers to common questions about this code of conduct, see the FAQ at 177 | https://www.contributor-covenant.org/faq. Translations are available at 178 | https://www.contributor-covenant.org/translations. 179 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Contributing ### 2 | 3 | Thank you for your interest in `strong-error-handler`, an open source project 4 | as part of LoopBack. 5 | 6 | Contributing to `strong-error-handler` is easy. In a few simple steps: 7 | 8 | * Ensure that your effort is aligned with the project's roadmap by 9 | talking to the maintainers, especially if you are going to spend a 10 | lot of time on it. 11 | 12 | * Make something better or fix a bug. 13 | 14 | * Adhere to code style outlined in the [Google C++ Style Guide][] and 15 | [Google Javascript Style Guide][]. 16 | 17 | * Sign the [Developer Certificate of Origin](#developer-certificate-of-origin) 18 | 19 | * Submit a pull request through Github. 20 | 21 | 22 | ### Developer Certificate of Origin 23 | 24 | This project uses [DCO](https://developercertificate.org/). Be sure to sign off 25 | your commits using the `-s` flag or adding `Signed-off-By: Name` in the 26 | commit message. 27 | 28 | **Example** 29 | 30 | ``` 31 | git commit -s -m "feat: my commit message" 32 | ``` 33 | 34 | [Google C++ Style Guide]: https://google.github.io/styleguide/cppguide.html 35 | [Google Javascript Style Guide]: https://google.github.io/styleguide/javascriptguide.xml 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) IBM Corp. 2016,2017. All Rights Reserved. 2 | Node module: strong-error-handler 3 | This project is licensed under the MIT License, full text below. 4 | 5 | -------- 6 | 7 | MIT license 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # strong-error-handler 2 | 3 | This package is an error handler for use in both development (debug) and production environments. 4 | 5 | In production mode, `strong-error-handler` omits details from error responses to prevent leaking sensitive information: 6 | 7 | - For 5xx errors, the output contains only the status code and the status name from the HTTP specification. 8 | - For 4xx errors, the output contains the full error message (`error.message`) and the contents of the `details` 9 | property (`error.details`) that `ValidationError` typically uses to provide machine-readable details 10 | about validation problems. It also includes `error.code` to allow a machine-readable error code to be passed 11 | through which could be used, for example, for translation. 12 | 13 | In debug mode, `strong-error-handler` returns full error stack traces and internal details of any error objects to the client in the HTTP responses. 14 | 15 | ## Supported versions 16 | 17 | This module adopts the [Module Long Term Support (LTS)](http://github.com/CloudNativeJS/ModuleLTS) policy, with the following End Of Life (EOL) dates: 18 | 19 | | Version | Status | Published | EOL | 20 | | ---------- | --------------- | --------- | -------------------- | 21 | | 4.x | Current | Oct 2020 | Apr 2023 _(minimum)_ | 22 | | 3.x | Active LTS | Jun 2018 | Dec 2022 | 23 | | 2.x | End-of-life | Mar 2017 | Oct 2020 | 24 | 25 | Learn more about our LTS plan in the [LoopBack documentation](http://loopback.io/doc/en/contrib/Long-term-support.html). 26 | 27 | ## Installation 28 | 29 | ```bash 30 | $ npm install --save strong-error-handler 31 | ``` 32 | 33 | ## Use 34 | 35 | In an Express-based application: 36 | 37 | ```js 38 | var express = require('express'); 39 | var errorHandler = require('strong-error-handler'); 40 | 41 | var app = express(); 42 | // setup your routes 43 | // `options` are set to default values. For more info, see `options` below. 44 | // app.use(errorHandler({ /* options, see below */ })); 45 | app.use(errorHandler({ 46 | debug: app.get('env') === 'development', 47 | log: true, 48 | })); 49 | 50 | app.listen(3000); 51 | ``` 52 | 53 | The module also exports `writeErrorToResponse`, a non-middleware flavor of the 54 | error handler: 55 | 56 | ```js 57 | const http = require('http'); 58 | const writeErrorToResponse = require('strong-error-handler') 59 | .writeErrorToResponse; 60 | const errHandlingOptions = {debug: process.env.NODE_ENV === 'development'} 61 | 62 | http 63 | .createServer((req, res) => { 64 | if (errShouldBeThrown) { 65 | writeErrorToResponse( 66 | new Error('something went wrong'), 67 | req, 68 | res, 69 | errHandlingOptions, 70 | ); 71 | } 72 | }) 73 | .listen(3000); 74 | ``` 75 | 76 | In LoopBack applications, add the following entry to `server/middleware.json`: 77 | 78 | ```json 79 | { 80 | "final:after": { 81 | "strong-error-handler": { 82 | "params": { 83 | "debug": false, 84 | "log": true 85 | } 86 | } 87 | } 88 | } 89 | ``` 90 | 91 | In general, `strong-error-handler` must be the last middleware function registered. 92 | 93 | The above configuration will log errors to the server console, but not return stack traces in HTTP responses. 94 | For details on configuration options, see below. 95 | 96 | ### Response format and content type 97 | 98 | The `strong-error-handler` package supports JSON, HTML and XML responses: 99 | 100 | - When the object is a standard Error object, it returns the string provided by the stack property in HTML/text 101 | responses. 102 | - When the object is a non-Error object, it returns the result of `util.inspect` in HTML/text responses. 103 | - For JSON responses, the result is an object with all enumerable properties from the object in the response. 104 | 105 | The content type of the response depends on the request's `Accepts` header. 106 | 107 | - For Accepts header `json` or `application/json`, the response content type is JSON. 108 | - For Accepts header `html` or `text/html`, the response content type is HTML. 109 | - For Accepts header `xml` or `text/xml`, the response content type is XML. 110 | 111 | *There are plans to support other formats such as Plain-text.* 112 | 113 | ## Options 114 | 115 | | Option | Type | Default | Description | 116 | | -------------------- | ------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 117 | | debug | Boolean    | `false` | If `true`, HTTP responses include all error properties, including sensitive data such as file paths, URLs and stack traces. See [Example output](#example) below. | 118 | | log | Boolean | `true` | If `true`, all errors are printed via `console.error`, including an array of fields (custom error properties) that are safe to include in response messages (both 4xx and 5xx).
If `false`, sends only the error back in the response. | 119 | | safeFields | [String] | `[]` | Specifies property names on errors that are allowed to be passed through in 4xx and 5xx responses. See [Safe error fields](#safe-error-fields) below. | 120 | | defaultType | String | `"json"` | Specifies the default response content type to use when the client does not provide any Accepts header. | 121 | | rootProperty | String or false | `"error"` | Specifies the root property name for json or xml. If the value is set to `false`, no wrapper will be added to the json object. The false value is ignored by XML as a root element is always required. | 122 | | negotiateContentType | Boolean | true | Negotiate the response content type via Accepts request header. When disabled, strong-error-handler will always use the default content type when producing responses. Disabling content type negotiation is useful if you want to see JSON-formatted error responses in browsers, because browsers usually prefer HTML and XML over other content types. | 123 | 124 | ### Customizing log format 125 | 126 | **Express** 127 | 128 | To use a different log format, add your own custom error-handling middleware then disable `errorHandler.log`. 129 | For example, in an Express application: 130 | 131 | ```js 132 | app.use(myErrorLogger()); 133 | app.use(errorHandler({log: false})); 134 | ``` 135 | 136 | In general, add `strong-error-handler` as the last middleware function, just before calling `app.listen()`. 137 | 138 | **LoopBack** 139 | 140 | For LoopBack applications, put custom error-logging middleware in a separate file; for example, `server/middleware/error-logger.js`: 141 | 142 | ``` 143 | module.exports = function(options) { 144 | return function logError(err, req, res, next) { 145 | console.log('unhandled error' ,err); 146 | next(err); 147 | }; 148 | }; 149 | ``` 150 | 151 | Then in `server/middleware.json`, specify your custom error logging function as follows: 152 | 153 | ``` 154 | { 155 | // ... 156 | "final:after": { 157 | "./middleware/error-logger": {}, 158 | "strong-error-handler": { 159 | "params": { 160 | "log": false 161 | } 162 | } 163 | } 164 | ``` 165 | 166 | The default `middleware.development.json` file explicitly enables logging in strong-error-handler params, so you will need to change that file too. 167 | 168 | ### Safe error fields 169 | 170 | By default, `strong-error-handler` will only pass through the `name`, `message` and `details` properties of an error. Additional error 171 | properties may be allowed through on 4xx and 5xx status code errors using the `safeFields` option to pass in an array of safe field names: 172 | 173 | ``` 174 | { 175 | "final:after": { 176 | "strong-error-handler": { 177 | "params": { 178 | "safeFields": ["errorCode"] 179 | } 180 | } 181 | } 182 | ``` 183 | 184 | Using the above configuration, an error containing an `errorCode` property will produce the following response: 185 | 186 | ``` 187 | { 188 | "error": { 189 | "statusCode": 500, 190 | "message": "Internal Server Error", 191 | "errorCode": "INTERNAL_SERVER_ERROR" 192 | } 193 | } 194 | ``` 195 | 196 | ## Migration from old LoopBack error handler 197 | 198 | NOTE: This is only required for applications scaffolded with old versions of the `slc loopback` tool. 199 | 200 | To migrate a LoopBack 2.x application to use `strong-error-handler`: 201 | 202 | 1. In `package.json` dependencies, remove `"errorhandler": "^x.x.x”,` 203 | 1. Install the new error handler by entering the command: 204 |
npm install --save strong-error-handler
205 | 1. In `server/config.json`, remove: 206 |
207 |     "remoting": {
208 |       ...
209 |       "errorHandler": {
210 |         "disableStackTrace": false
211 |       }
212 | and replace it with: 213 |
214 |     "remoting": {
215 |       ...,
216 |       "rest": {
217 |         "handleErrors": false
218 |       }
219 | 1. In `server/middleware.json`, remove: 220 |
221 |     "final:after": {
222 |       "loopback#errorHandler": {}
223 |     }
224 | and replace it with: 225 |
226 |     "final:after": {
227 |       "strong-error-handler": {}
228 |     }
229 | 1. Delete `server/middleware.production.json`. 230 | 1. Create `server/middleware.development.json` containing: 231 |
232 |     "final:after": {
233 |       "strong-error-handler": {
234 |         "params": {
235 |           "debug": true,
236 |           "log": true
237 |         }
238 |       }
239 |     }
240 | 
241 | 242 | For more information, see 243 | [Migrating apps to LoopBack 3.0](http://loopback.io/doc/en/lb3/Migrating-to-3.0.html#update-use-of-rest-error-handler). 244 | 245 | ## Example 246 | 247 | 5xx error generated when `debug: false` : 248 | 249 | ``` 250 | { error: { statusCode: 500, message: 'Internal Server Error' } } 251 | ``` 252 | 253 | The same error generated when `debug: true` : 254 | 255 | ``` 256 | { error: 257 | { statusCode: 500, 258 | name: 'Error', 259 | message: 'a test error message', 260 | stack: 'Error: a test error message 261 | at Context. (User/strong-error-handler/test/handler.test.js:220:21) 262 | at callFnAsync (User/strong-error-handler/node_modules/mocha/lib/runnable.js:349:8) 263 | at Test.Runnable.run (User/strong-error-handler/node_modules/mocha/lib/runnable.js:301:7) 264 | at Runner.runTest (User/strong-error-handler/node_modules/mocha/lib/runner.js:422:10) 265 | at User/strong-error-handler/node_modules/mocha/lib/runner.js:528:12 266 | at next (User/strong-error-handler/node_modules/mocha/lib/runner.js:342:14) 267 | at User/strong-error-handler/node_modules/mocha/lib/runner.js:352:7 268 | at next (User/strong-error-handler/node_modules/mocha/lib/runner.js:284:14) 269 | at Immediate._onImmediate (User/strong-error-handler/node_modules/mocha/lib/runner.js:320:5) 270 | at tryOnImmediate (timers.js:543:15) 271 | at processImmediate [as _immediateCallback] (timers.js:523:5)' }} 272 | ``` 273 | 274 | 4xx error generated when `debug: false` : 275 | 276 | ``` 277 | { error: 278 | { statusCode: 422, 279 | name: 'Unprocessable Entity', 280 | message: 'Missing required fields', 281 | code: 'MISSING_REQUIRED_FIELDS' }} 282 | ``` 283 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Security advisories 4 | 5 | Security advisories can be found on the 6 | [LoopBack website](https://loopback.io/doc/en/sec/index.html). 7 | 8 | ## Reporting a vulnerability 9 | 10 | If you think you have discovered a new security issue with any LoopBack package, 11 | **please do not report it on GitHub**. Instead, send an email to 12 | [security@loopback.io](mailto:security@loopback.io) with the following details: 13 | 14 | - Full description of the vulnerability. 15 | - Steps to reproduce the issue. 16 | - Possible solutions. 17 | 18 | If you are sending us any logs as part of the report, then make sure to redact 19 | any sensitive data from them. -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2018. All Rights Reserved. 2 | // Node module: loopback-next 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 'use strict'; 6 | 7 | const isCI = process.env.CI; 8 | module.exports = { 9 | extends: [ 10 | '@commitlint/config-conventional', 11 | ], 12 | rules: { 13 | 'header-max-length': [2, 'always', 100], 14 | 'body-leading-blank': [2, 'always'], 15 | 'footer-leading-blank': [0, 'always'], 16 | // Only enforce the rule if CI flag is not set. This is useful for release 17 | // commits to skip DCO 18 | 'signed-off-by': [isCI ? 0 : 2, 'always', 'Signed-off-by:'], 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2018. All Rights Reserved. 2 | // Node module: strong-error-handler 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | // Type definitions for strong-error-handler 3.x 7 | // Project: https://github.com/strongloop/strong-error-handler 8 | // Definitions by: Kyusung Shim 9 | // TypeScript Version: 3.0 10 | 11 | import * as Express from 'express'; 12 | 13 | export = errorHandlerFactory; 14 | 15 | /** 16 | * Creates a middleware function for error-handling 17 | * @param options Options for error handler settings 18 | */ 19 | declare function errorHandlerFactory( 20 | options?: errorHandlerFactory.ErrorHandlerOptions 21 | ): errorHandlerFactory.StrongErrorHandler; 22 | 23 | declare namespace errorHandlerFactory { 24 | /** 25 | * Writes thrown error to response 26 | * @param err Error to handle 27 | * @param req Incoming request 28 | * @param res Response 29 | * @param options Options for error handler settings 30 | */ 31 | function writeErrorToResponse( 32 | err: Error, 33 | req: Express.Request, 34 | res: Express.Response, 35 | options?: ErrorWriterOptions 36 | ): void; 37 | 38 | /** 39 | * Error-handling middleware function. Includes server-side logging 40 | */ 41 | type StrongErrorHandler = ( 42 | err: Error, 43 | req: Express.Request, 44 | res: Express.Response, 45 | next: (err?: any) => void 46 | ) => void; 47 | 48 | /** 49 | * Options for writing errors to the response 50 | */ 51 | interface ErrorWriterOptions { 52 | debug?: boolean; 53 | safeFields?: string[]; 54 | defaultType?: string; 55 | negotiateContentType?: boolean; 56 | rootProperty?: string | false; 57 | } 58 | 59 | /** 60 | * Options for error-handling 61 | */ 62 | interface ErrorHandlerOptions extends ErrorWriterOptions { 63 | log?: boolean; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /intl/cs/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "85312d2b987e2e44001db3d6963a1364": "Neošetřené pole chyb pro požadavek {0} {1}\n", 3 | "adcbd4c562b19f45fb5d3427619646d5": "Neošetřená chyba pro požadavek {0} {1}: {2}" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /intl/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "85312d2b987e2e44001db3d6963a1364": "Nicht verarbeitetes Array mit Fehlern für Anforderung {0} {1}\n", 3 | "adcbd4c562b19f45fb5d3427619646d5": "Nicht verarbeiteter Fehler für Anforderung {0} {1}: {2}" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /intl/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "85312d2b987e2e44001db3d6963a1364": "Unhandled array of errors for request {0} {1}\n", 3 | "adcbd4c562b19f45fb5d3427619646d5": "Unhandled error for request {0} {1}: {2}" 4 | } 5 | -------------------------------------------------------------------------------- /intl/es/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "85312d2b987e2e44001db3d6963a1364": "Matriz de errores no gestionada para la solicitud {0} {1}\n", 3 | "adcbd4c562b19f45fb5d3427619646d5": "Error no gestionado para la solicitud {0} {1}: {2}" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /intl/fr/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "85312d2b987e2e44001db3d6963a1364": "Tableau d'erreurs non traitées pour la demande {0} {1}\n", 3 | "adcbd4c562b19f45fb5d3427619646d5": "Erreur non traitée pour la demande {0} {1} : {2}" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /intl/it/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "85312d2b987e2e44001db3d6963a1364": "Array di errori non gestito per la richieste {0} {1}\n", 3 | "adcbd4c562b19f45fb5d3427619646d5": "Errore non gestito per la richiesta {0} {1}: {2}" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /intl/ja/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "85312d2b987e2e44001db3d6963a1364": "要求 {0} {1} に対する一連のエラーが処理されていません\n", 3 | "adcbd4c562b19f45fb5d3427619646d5": "要求 {0} {1} のエラーが処理されていません: {2}" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /intl/ko/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "85312d2b987e2e44001db3d6963a1364": "요청 {0} {1}에 대해 처리되지 않는 오류 배열\n", 3 | "adcbd4c562b19f45fb5d3427619646d5": "요청 {0} {1}에 대해 처리되지 않는 오류: {2}" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /intl/nl/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "85312d2b987e2e44001db3d6963a1364": "Onverwerkte array van fouten voor opdracht {0} {1}\n", 3 | "adcbd4c562b19f45fb5d3427619646d5": "Onverwerkte fout opgetreden voor opdracht {0} {1}: {2}" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /intl/pl/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "85312d2b987e2e44001db3d6963a1364": "Nieobsługiwana tablica błędów dla żądania {0} {1}\n", 3 | "adcbd4c562b19f45fb5d3427619646d5": "Nieobsługiwany błąd dla żądania {0} {1}: {2}" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /intl/pt/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "85312d2b987e2e44001db3d6963a1364": "Matriz não manipulada de erros para solicitação {0} {1}\n", 3 | "adcbd4c562b19f45fb5d3427619646d5": "Erro não manipulado para solicitação {0} {1}: {2}" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /intl/ru/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "85312d2b987e2e44001db3d6963a1364": "Необработанный массив ошибок для запроса {0} {1}\n", 3 | "adcbd4c562b19f45fb5d3427619646d5": "Необработанная ошибка для запроса {0} {1}: {2}" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /intl/tr/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "85312d2b987e2e44001db3d6963a1364": "İstek için işlenemeyen hata dizisi: {0} {1}\n", 3 | "adcbd4c562b19f45fb5d3427619646d5": "{0} {1} isteği için işlenemeyen hata: {2}" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /intl/zh-Hans/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "85312d2b987e2e44001db3d6963a1364": "未处理请求 {0} {1} 的错误数组\n", 3 | "adcbd4c562b19f45fb5d3427619646d5": "未处理请求 {0} {1} 的错误:{2}" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /intl/zh-Hant/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "85312d2b987e2e44001db3d6963a1364": "要求 {0} {1} 發生許多未處理的錯誤\n", 3 | "adcbd4c562b19f45fb5d3427619646d5": "要求 {0} {1} 發生未處理的錯誤:{2}" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /lib/clone.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016. All Rights Reserved. 2 | // Node module: strong-error-handler 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | module.exports = cloneAllProperties; 8 | 9 | /** 10 | * clone the error properties to the data objects 11 | * [err.name, err.message, err.stack] are not enumerable properties 12 | * @param data Object to be altered 13 | * @param err Error Object 14 | */ 15 | function cloneAllProperties(data, err) { 16 | data.name = err.name; 17 | data.message = err.message; 18 | for (const p in err) { 19 | if ((p in data)) continue; 20 | data[p] = err[p]; 21 | } 22 | // stack is appended last to ensure order is the same for response 23 | data.stack = err.stack; 24 | } 25 | -------------------------------------------------------------------------------- /lib/content-negotiation.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016,2018. All Rights Reserved. 2 | // Node module: strong-error-handler 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const accepts = require('accepts'); 8 | const debug = require('debug')('strong-error-handler:http-response'); 9 | const sendJson = require('./send-json'); 10 | const sendHtml = require('./send-html'); 11 | const sendXml = require('./send-xml'); 12 | const util = require('util'); 13 | 14 | module.exports = negotiateContentProducer; 15 | 16 | /** 17 | * Handles req.accepts and req.query._format and options.defaultType 18 | * to resolve the correct operation 19 | * 20 | * @param req request object 21 | * @param {Function} logWarning a logger function for reporting warnings 22 | * @param {Object} options options of strong-error-handler 23 | * @returns {Function} Operation function with signature `fn(res, data)` 24 | */ 25 | function negotiateContentProducer(req, logWarning, options) { 26 | const SUPPORTED_TYPES = [ 27 | 'application/json', 'json', 28 | 'text/html', 'html', 29 | 'text/xml', 'xml', 30 | ]; 31 | 32 | options = options || {}; 33 | let defaultType = 'json'; 34 | 35 | // checking if user provided defaultType is supported 36 | if (options.defaultType) { 37 | if (SUPPORTED_TYPES.indexOf(options.defaultType) > -1) { 38 | debug('Accepting options.defaultType `%s`', options.defaultType); 39 | defaultType = options.defaultType; 40 | } else { 41 | debug('defaultType: `%s` is not supported, ' + 42 | 'falling back to defaultType: `%s`', options.defaultType, defaultType); 43 | } 44 | } 45 | 46 | // decide to use resolvedType or defaultType 47 | // Please note that accepts assumes the order of content-type is provided 48 | // in the priority returned 49 | // example 50 | // Accepts: text/html, */*, application/json ---> will resolve as text/html 51 | // Accepts: application/json, */*, text/html ---> will resolve as application/json 52 | // Accepts: */*, application/json, text/html ---> will resolve as application/json 53 | // eg. Chrome accepts defaults to `text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*` 54 | // In this case `resolvedContentType` will result as: `text/html` due to the order given 55 | const resolvedContentType = accepts(req).types(SUPPORTED_TYPES); 56 | debug('Resolved content-type', resolvedContentType); 57 | let contentType = resolvedContentType || defaultType; 58 | 59 | if (options.negotiateContentType === false) { 60 | if (SUPPORTED_TYPES.indexOf(options.defaultType) > -1) { 61 | debug('Forcing options.defaultType `%s`', 62 | options.defaultType); 63 | contentType = options.defaultType; 64 | } else { 65 | debug('contentType: `%s` is not supported, ' + 66 | 'falling back to contentType: `%s`', 67 | options.defaultType, contentType); 68 | } 69 | } 70 | 71 | // to receive _format from user's url param to overide the content type 72 | // req.query (eg /api/Users/1?_format=json will overide content negotiation 73 | // https://github.com/strongloop/strong-remoting/blob/ac3093dcfbb787977ca0229b0f672703859e52e1/lib/http-context.js#L643-L645 74 | const query = req.query || {}; 75 | if (query._format) { 76 | if (SUPPORTED_TYPES.indexOf(query._format) > -1) { 77 | contentType = query._format; 78 | } else { 79 | // format passed through query but not supported 80 | const msg = util.format('Response _format "%s" is not supported' + 81 | 'used "%s" instead"', query._format, defaultType); 82 | logWarning(msg); 83 | } 84 | } 85 | 86 | debug('Content-negotiation: req.headers.accept: `%s` Resolved as: `%s`', 87 | req.headers.accept, contentType); 88 | return resolveOperation(contentType); 89 | } 90 | 91 | function resolveOperation(contentType) { 92 | switch (contentType) { 93 | case 'application/json': 94 | case 'json': 95 | return sendJson; 96 | case 'text/html': 97 | case 'html': 98 | return sendHtml; 99 | case 'text/xml': 100 | case 'xml': 101 | return sendXml; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/data-builder.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016,2018. All Rights Reserved. 2 | // Node module: strong-error-handler 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const cloneAllProperties = require('../lib/clone.js'); 9 | const httpStatus = require('http-status'); 10 | 11 | module.exports = buildResponseData; 12 | 13 | function buildResponseData(err, options) { 14 | // Debugging mode is disabled by default. When turned on (in dev), 15 | // all error properties (including) stack traces are sent in the response 16 | const isDebugMode = options.debug; 17 | 18 | if (Array.isArray(err)) { 19 | return serializeArrayOfErrors(err, options); 20 | } 21 | 22 | const data = Object.create(null); 23 | fillStatusCode(data, err); 24 | 25 | if (typeof err !== 'object') { 26 | err = { 27 | statusCode: 500, 28 | message: '' + err, 29 | }; 30 | } 31 | 32 | if (isDebugMode) { 33 | fillDebugData(data, err); 34 | return data; 35 | } 36 | 37 | if (data.statusCode >= 400 && data.statusCode <= 499) { 38 | fillBadRequestError(data, err); 39 | } else { 40 | fillInternalError(data, err); 41 | } 42 | 43 | const safeFields = options.safeFields || []; 44 | fillSafeFields(data, err, safeFields); 45 | 46 | return data; 47 | } 48 | 49 | function serializeArrayOfErrors(errors, options) { 50 | const details = errors.map(e => buildResponseData(e, options)); 51 | return { 52 | statusCode: 500, 53 | message: 'Failed with multiple errors, ' + 54 | 'see `details` for more information.', 55 | details: details, 56 | }; 57 | } 58 | 59 | function fillStatusCode(data, err) { 60 | data.statusCode = err.statusCode || err.status; 61 | if (!data.statusCode || data.statusCode < 400) 62 | data.statusCode = 500; 63 | } 64 | 65 | function fillDebugData(data, err) { 66 | cloneAllProperties(data, err); 67 | } 68 | 69 | function fillBadRequestError(data, err) { 70 | data.name = err.name; 71 | data.message = err.message; 72 | data.code = err.code; 73 | data.details = err.details; 74 | } 75 | 76 | function fillInternalError(data, err) { 77 | data.message = httpStatus[data.statusCode] || 'Unknown Error'; 78 | } 79 | 80 | function fillSafeFields(data, err, safeFields) { 81 | if (!Array.isArray(safeFields)) { 82 | safeFields = [safeFields]; 83 | } 84 | 85 | safeFields.forEach(function(field) { 86 | if (err[field] !== undefined) { 87 | data[field] = err[field]; 88 | } 89 | }); 90 | } 91 | -------------------------------------------------------------------------------- /lib/handler.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016,2018. All Rights Reserved. 2 | // Node module: strong-error-handler 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const path = require('path'); 9 | const SG = require('strong-globalize'); 10 | SG.SetRootDir(path.resolve(__dirname, '..')); 11 | const buildResponseData = require('./data-builder'); 12 | const debug = require('debug')('strong-error-handler'); 13 | const logToConsole = require('./logger'); 14 | const negotiateContentProducer = require('./content-negotiation'); 15 | 16 | function noop() { 17 | } 18 | 19 | /** 20 | * Create a middleware error handler function. 21 | * 22 | * @param {Object} options 23 | * @returns {Function} 24 | */ 25 | function createStrongErrorHandler(options) { 26 | options = options || {}; 27 | 28 | debug('Initializing with options %j', options); 29 | 30 | // Log all errors via console.error (enabled by default) 31 | const logError = options.log !== false ? logToConsole : noop; 32 | 33 | return function strongErrorHandler(err, req, res, next) { 34 | logError(req, err); 35 | writeErrorToResponse(err, req, res, options); 36 | }; 37 | } 38 | 39 | /** 40 | * Writes thrown error to response 41 | * 42 | * @param {Error} err 43 | * @param {Express.Request} req 44 | * @param {Express.Response} res 45 | * @param {Object} options 46 | */ 47 | function writeErrorToResponse(err, req, res, options) { 48 | debug('Handling %s', err.stack || err); 49 | 50 | options = options || {}; 51 | 52 | if (res.headersSent) { 53 | debug('Response was already sent, closing the underlying connection'); 54 | return req.socket.destroy(); 55 | } 56 | 57 | // this will alter the err object, to handle when res.statusCode is an error 58 | if (!err.status && !err.statusCode && res.statusCode >= 400) 59 | err.statusCode = res.statusCode; 60 | 61 | const data = buildResponseData(err, options); 62 | debug('Response status %s data %j', data.statusCode, data); 63 | 64 | res.setHeader('X-Content-Type-Options', 'nosniff'); 65 | res.statusCode = data.statusCode; 66 | 67 | const sendResponse = negotiateContentProducer(req, warn, options); 68 | sendResponse(res, data, options); 69 | 70 | function warn(msg) { 71 | res.header('X-Warning', msg); 72 | debug(msg); 73 | } 74 | } 75 | 76 | exports = module.exports = createStrongErrorHandler; 77 | exports.writeErrorToResponse = writeErrorToResponse; 78 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016. All Rights Reserved. 2 | // Node module: strong-error-handler 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const format = require('util').format; 9 | const g = require('strong-globalize')(); 10 | 11 | module.exports = function logToConsole(req, err) { 12 | if (!Array.isArray(err)) { 13 | g.error('Request %s %s failed: %s', 14 | req.method, req.url, err.stack || err); 15 | return; 16 | } 17 | 18 | const errMsg = g.f('Request %s %s failed with multiple errors:\n', 19 | req.method, req.url); 20 | const errors = err.map(formatError).join('\n'); 21 | console.error(errMsg, errors); 22 | }; 23 | 24 | function formatError(err) { 25 | return format('%s', err.stack || err); 26 | } 27 | -------------------------------------------------------------------------------- /lib/send-html.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016. All Rights Reserved. 2 | // Node module: strong-error-handler 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | const ejs = require('ejs'); 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | 11 | const assetDir = path.resolve(__dirname, '../views'); 12 | const compiledTemplates = { 13 | // loading default template and stylesheet 14 | default: loadDefaultTemplates(), 15 | }; 16 | 17 | module.exports = sendHtml; 18 | 19 | function sendHtml(res, data, options) { 20 | const toRender = {options, data}; 21 | // TODO: ability to call non-default template functions from options 22 | const body = compiledTemplates.default(toRender); 23 | sendResponse(res, body); 24 | } 25 | 26 | /** 27 | * Compile and cache the file with the `filename` key in options 28 | * 29 | * @param filepath (description) 30 | * @returns {Function} render function with signature fn(data); 31 | */ 32 | function compileTemplate(filepath) { 33 | const options = {cache: true, filename: filepath}; 34 | const fileContent = fs.readFileSync(filepath, 'utf8'); 35 | return ejs.compile(fileContent, options); 36 | } 37 | 38 | // loads and cache default error templates 39 | function loadDefaultTemplates() { 40 | const defaultTemplate = path.resolve(assetDir, 'default-error.ejs'); 41 | return compileTemplate(defaultTemplate); 42 | } 43 | 44 | function sendResponse(res, body) { 45 | res.setHeader('Content-Type', 'text/html; charset=utf-8'); 46 | res.end(body); 47 | } 48 | -------------------------------------------------------------------------------- /lib/send-json.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016. All Rights Reserved. 2 | // Node module: strong-error-handler 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const safeStringify = require('fast-safe-stringify'); 9 | 10 | module.exports = function sendJson(res, data, options) { 11 | options = options || {}; 12 | // Set `options.rootProperty` to not wrap the data into an `error` object 13 | const err = options.rootProperty === false ? data : { 14 | // Use `options.rootProperty`, if not set, default to `error` 15 | [options.rootProperty || 'error']: data, 16 | }; 17 | const content = safeStringify(err); 18 | res.setHeader('Content-Type', 'application/json; charset=utf-8'); 19 | res.end(content, 'utf-8'); 20 | }; 21 | -------------------------------------------------------------------------------- /lib/send-xml.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: strong-error-handler 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const js2xmlparser = require('js2xmlparser'); 9 | 10 | module.exports = function sendXml(res, data, options) { 11 | options = options || {}; 12 | // Xml always requires a root element. 13 | // `options.rootProperty === false` is not honored 14 | const root = options.rootProperty || 'error'; 15 | const content = js2xmlparser.parse(root, data); 16 | res.setHeader('Content-Type', 'text/xml; charset=utf-8'); 17 | res.end(content, 'utf-8'); 18 | }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "strong-error-handler", 3 | "description": "Error handler for use in development and production environments.", 4 | "license": "MIT", 5 | "version": "4.0.7", 6 | "engines": { 7 | "node": ">=10" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/loopbackio/strong-error-handler.git" 12 | }, 13 | "main": "lib/handler.js", 14 | "scripts": { 15 | "lint": "eslint .", 16 | "test": "mocha", 17 | "posttest": "npm run lint" 18 | }, 19 | "dependencies": { 20 | "accepts": "^1.3.8", 21 | "debug": "^4.3.4", 22 | "ejs": "^3.1.9", 23 | "fast-safe-stringify": "^2.1.1", 24 | "http-status": "^1.6.2", 25 | "js2xmlparser": "^4.0.2", 26 | "strong-globalize": "^6.0.5" 27 | }, 28 | "devDependencies": { 29 | "@commitlint/config-conventional": "^17.7.0", 30 | "@types/express": "^4.17.17", 31 | "chai": "^4.3.7", 32 | "eslint": "^8.46.0", 33 | "eslint-config-loopback": "^13.1.0", 34 | "express": "^4.18.2", 35 | "mocha": "^10.2.0", 36 | "supertest": "^6.3.3" 37 | }, 38 | "browser": { 39 | "strong-error-handler": false 40 | }, 41 | "author": "IBM Corp." 42 | } 43 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>loopbackio/cicd//shared-configs/renovate/base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/handler.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016,2018. All Rights Reserved. 2 | // Node module: strong-error-handler 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const cloneAllProperties = require('../lib/clone.js'); 9 | const debug = require('debug')('test'); 10 | const expect = require('chai').expect; 11 | const express = require('express'); 12 | const strongErrorHandler = require('..'); 13 | const supertest = require('supertest'); 14 | const util = require('util'); 15 | 16 | describe('strong-error-handler', function() { 17 | before(setupHttpServerAndClient); 18 | beforeEach(resetRequestHandler); 19 | after(stopHttpServerAndClient); 20 | 21 | it('sets nosniff header', function(done) { 22 | givenErrorHandlerForError(); 23 | request.get('/') 24 | .expect('X-Content-Type-Options', 'nosniff') 25 | .expect(500, done); 26 | }); 27 | 28 | it('handles response headers already sent', function(done) { 29 | givenErrorHandlerForError(); 30 | const handler = _requestHandler; 31 | _requestHandler = function(req, res, next) { 32 | res.end('empty'); 33 | process.nextTick(function() { 34 | handler(req, res, next); 35 | }); 36 | }; 37 | 38 | request.get('/').expect(200, 'empty', done); 39 | }); 40 | 41 | context('status code', function() { 42 | it('converts non-error "err.status" to 500', function(done) { 43 | givenErrorHandlerForError(new ErrorWithProps({status: 200})); 44 | request.get('/').expect(500, done); 45 | }); 46 | 47 | it('converts non-error "err.statusCode" to 500', function(done) { 48 | givenErrorHandlerForError(new ErrorWithProps({statusCode: 200})); 49 | request.get('/').expect(500, done); 50 | }); 51 | 52 | it('uses the value from "err.status"', function(done) { 53 | givenErrorHandlerForError(new ErrorWithProps({status: 404})); 54 | request.get('/').expect(404, done); 55 | }); 56 | 57 | it('uses the value from "err.statusCode"', function(done) { 58 | givenErrorHandlerForError(new ErrorWithProps({statusCode: 404})); 59 | request.get('/').expect(404, done); 60 | }); 61 | 62 | it('prefers "err.statusCode" over "err.status"', function(done) { 63 | givenErrorHandlerForError(new ErrorWithProps({ 64 | statusCode: 400, 65 | status: 404, 66 | })); 67 | 68 | request.get('/').expect(400, done); 69 | }); 70 | 71 | it('handles error from `res.statusCode`', function(done) { 72 | givenErrorHandlerForError(); 73 | const handler = _requestHandler; 74 | _requestHandler = function(req, res, next) { 75 | res.statusCode = 507; 76 | handler(req, res, next); 77 | }; 78 | request.get('/').expect( 79 | 507, 80 | {error: {statusCode: 507, message: 'Insufficient Storage'}}, 81 | done, 82 | ); 83 | }); 84 | }); 85 | 86 | context('logging', function() { 87 | let logs; 88 | 89 | beforeEach(redirectConsoleError); 90 | afterEach(restoreConsoleError); 91 | 92 | it('logs by default', function(done) { 93 | givenErrorHandlerForError(new Error(), { 94 | // explicitly set to undefined to prevent givenErrorHandlerForError 95 | // from disabling this option 96 | log: undefined, 97 | }); 98 | 99 | request.get('/').end(function(err) { 100 | if (err) return done(err); 101 | expect(logs).to.have.length(1); 102 | done(); 103 | }); 104 | }); 105 | 106 | it('honours options.log=false', function(done) { 107 | givenErrorHandlerForError(new Error(), {log: false}); 108 | 109 | request.get('/api').end(function(err) { 110 | if (err) return done(err); 111 | expect(logs).to.have.length(0); 112 | done(); 113 | }); 114 | }); 115 | 116 | it('honours options.log=true', function(done) { 117 | givenErrorHandlerForError(new Error(), {log: true}); 118 | 119 | request.get('/api').end(function(err) { 120 | if (err) return done(err); 121 | expect(logs).to.have.length(1); 122 | done(); 123 | }); 124 | }); 125 | 126 | it('includes relevant information in the log message', function(done) { 127 | givenErrorHandlerForError(new TypeError('ERROR-NAME'), {log: true}); 128 | 129 | request.get('/api').end(function(err) { 130 | if (err) return done(err); 131 | 132 | const msg = logs[0]; 133 | // the request method 134 | expect(msg).to.contain('GET'); 135 | // the request path 136 | expect(msg).to.contain('/api'); 137 | // the error name & message 138 | expect(msg).to.contain('TypeError: ERROR-NAME'); 139 | // the stack 140 | expect(msg).to.contain(__filename); 141 | 142 | done(); 143 | }); 144 | }); 145 | 146 | it('handles array argument', function(done) { 147 | givenErrorHandlerForError( 148 | [new TypeError('ERR1'), new Error('ERR2')], 149 | {log: true}, 150 | ); 151 | 152 | request.get('/api').end(function(err) { 153 | if (err) return done(err); 154 | 155 | const msg = logs[0]; 156 | // the request method 157 | expect(msg).to.contain('GET'); 158 | // the request path 159 | expect(msg).to.contain('/api'); 160 | // the error name & message for all errors 161 | expect(msg).to.contain('TypeError: ERR1'); 162 | expect(msg).to.contain('Error: ERR2'); 163 | // verify that stacks are included too 164 | expect(msg).to.contain(__filename); 165 | 166 | done(); 167 | }); 168 | }); 169 | 170 | it('handles non-Error argument', function(done) { 171 | givenErrorHandlerForError('STRING ERROR', {log: true}); 172 | request.get('/').end(function(err) { 173 | if (err) return done(err); 174 | const msg = logs[0]; 175 | expect(msg).to.contain('STRING ERROR'); 176 | done(); 177 | }); 178 | }); 179 | 180 | const _consoleError = console.error; 181 | function redirectConsoleError() { 182 | logs = []; 183 | console.error = function() { 184 | const msg = util.format.apply(util, arguments); 185 | logs.push(msg); 186 | }; 187 | } 188 | 189 | function restoreConsoleError() { 190 | console.error = _consoleError; 191 | logs = []; 192 | } 193 | }); 194 | 195 | context('JSON response', function() { 196 | it('contains all error properties when debug=true', function(done) { 197 | const error = new ErrorWithProps({ 198 | message: 'a test error message', 199 | code: 'MACHINE_READABLE_CODE', 200 | details: 'some details', 201 | extra: 'sensitive data', 202 | }); 203 | givenErrorHandlerForError(error, {debug: true}); 204 | 205 | requestJson().end(function(err, res) { 206 | if (err) return done(err); 207 | 208 | const expectedData = { 209 | statusCode: 500, 210 | message: 'a test error message', 211 | name: 'ErrorWithProps', 212 | code: 'MACHINE_READABLE_CODE', 213 | details: 'some details', 214 | extra: 'sensitive data', 215 | stack: error.stack, 216 | }; 217 | expect(res.body).to.have.property('error'); 218 | expect(res.body.error).to.eql(expectedData); 219 | done(); 220 | }); 221 | }); 222 | 223 | it('includes code property for 4xx status codes when debug=false', 224 | function(done) { 225 | const error = new ErrorWithProps({ 226 | statusCode: 400, 227 | message: 'error with code', 228 | name: 'ErrorWithCode', 229 | code: 'MACHINE_READABLE_CODE', 230 | }); 231 | givenErrorHandlerForError(error, {debug: false}); 232 | 233 | requestJson().end(function(err, res) { 234 | if (err) return done(err); 235 | 236 | const expectedData = { 237 | statusCode: 400, 238 | message: 'error with code', 239 | name: 'ErrorWithCode', 240 | code: 'MACHINE_READABLE_CODE', 241 | }; 242 | expect(res.body).to.have.property('error'); 243 | expect(res.body.error).to.eql(expectedData); 244 | done(); 245 | }); 246 | }); 247 | 248 | it('excludes code property for 5xx status codes when debug=false', 249 | function(done) { 250 | const error = new ErrorWithProps({ 251 | statusCode: 500, 252 | code: 'MACHINE_READABLE_CODE', 253 | }); 254 | givenErrorHandlerForError(error, {debug: false}); 255 | 256 | requestJson().end(function(err, res) { 257 | if (err) return done(err); 258 | 259 | const expectedData = { 260 | statusCode: 500, 261 | message: 'Internal Server Error', 262 | }; 263 | expect(res.body).to.have.property('error'); 264 | expect(res.body.error).to.eql(expectedData); 265 | done(); 266 | }); 267 | }); 268 | 269 | it('contains non-enumerable Error properties when debug=true', 270 | function(done) { 271 | const error = new Error('a test error message'); 272 | givenErrorHandlerForError(error, {debug: true}); 273 | requestJson().end(function(err, res) { 274 | if (err) return done(err); 275 | expect(res.body).to.have.property('error'); 276 | const resError = res.body.error; 277 | expect(resError).to.have.property('name', 'Error'); 278 | expect(resError).to.have.property('message', 279 | 'a test error message'); 280 | expect(resError).to.have.property('stack', error.stack); 281 | done(); 282 | }); 283 | }); 284 | 285 | it('should allow setting safe fields when status=5xx', function(done) { 286 | const error = new ErrorWithProps({ 287 | name: 'Error', 288 | safeField: 'SAFE', 289 | unsafeField: 'UNSAFE', 290 | }); 291 | givenErrorHandlerForError(error, { 292 | safeFields: ['safeField'], 293 | }); 294 | 295 | requestJson().end(function(err, res) { 296 | if (err) return done(err); 297 | 298 | expect(res.body).to.have.property('error'); 299 | expect(res.body.error).to.have.property('safeField', 'SAFE'); 300 | expect(res.body.error).not.to.have.property('unsafeField'); 301 | 302 | done(); 303 | }); 304 | }); 305 | 306 | it('safe fields falls back to existing data', function(done) { 307 | const error = new ErrorWithProps({ 308 | name: 'Error', 309 | isSafe: false, 310 | }); 311 | givenErrorHandlerForError(error, { 312 | safeFields: ['statusCode', 'isSafe'], 313 | }); 314 | 315 | requestJson().end(function(err, res) { 316 | if (err) return done(err); 317 | expect(res.body.error.statusCode).to.equal(500); 318 | expect(res.body.error.isSafe).to.equal(false); 319 | 320 | done(); 321 | }); 322 | }); 323 | 324 | it('should allow setting safe fields when status=4xx', function(done) { 325 | const error = new ErrorWithProps({ 326 | name: 'Error', 327 | statusCode: 422, 328 | safeField: 'SAFE', 329 | unsafeField: 'UNSAFE', 330 | }); 331 | givenErrorHandlerForError(error, { 332 | safeFields: ['safeField'], 333 | }); 334 | 335 | requestJson().end(function(err, res) { 336 | if (err) return done(err); 337 | 338 | expect(res.body).to.have.property('error'); 339 | expect(res.body.error).to.have.property('safeField', 'SAFE'); 340 | expect(res.body.error).not.to.have.property('unsafeField'); 341 | 342 | done(); 343 | }); 344 | }); 345 | 346 | it('contains subset of properties when status=4xx', function(done) { 347 | const error = new ErrorWithProps({ 348 | name: 'ValidationError', 349 | message: 'The model instance is not valid.', 350 | statusCode: 422, 351 | details: 'some details', 352 | extra: 'sensitive data', 353 | }); 354 | givenErrorHandlerForError(error); 355 | 356 | requestJson().end(function(err, res) { 357 | if (err) return done(err); 358 | 359 | expect(res.body).to.have.property('error'); 360 | expect(res.body.error).to.eql({ 361 | name: 'ValidationError', 362 | message: 'The model instance is not valid.', 363 | statusCode: 422, 364 | details: 'some details', 365 | // notice the property "extra" is not included 366 | }); 367 | done(); 368 | }); 369 | }); 370 | 371 | it('contains only safe info when status=5xx', function(done) { 372 | // Mock an error reported by fs.readFile 373 | const error = new ErrorWithProps({ 374 | name: 'Error', 375 | message: 'ENOENT: no such file or directory, open "/etc/passwd"', 376 | errno: -2, 377 | code: 'ENOENT', 378 | syscall: 'open', 379 | path: '/etc/password', 380 | }); 381 | givenErrorHandlerForError(error); 382 | 383 | requestJson().end(function(err, res) { 384 | if (err) return done(err); 385 | 386 | expect(res.body).to.have.property('error'); 387 | expect(res.body.error).to.eql({ 388 | statusCode: 500, 389 | message: 'Internal Server Error', 390 | }); 391 | 392 | done(); 393 | }); 394 | }); 395 | 396 | it('handles array argument as 500 when debug=false', function(done) { 397 | const errors = [new Error('ERR1'), new Error('ERR2'), 'ERR STRING']; 398 | givenErrorHandlerForError(errors); 399 | 400 | requestJson().expect(500).end(function(err, res) { 401 | if (err) return done(err); 402 | const data = res.body.error; 403 | expect(data).to.have.property('message').that.match(/multiple errors/); 404 | expect(data).to.have.property('details').eql([ 405 | {statusCode: 500, message: 'Internal Server Error'}, 406 | {statusCode: 500, message: 'Internal Server Error'}, 407 | {statusCode: 500, message: 'Internal Server Error'}, 408 | ]); 409 | done(); 410 | }); 411 | }); 412 | 413 | it('returns all array items when debug=true', function(done) { 414 | const testError = new ErrorWithProps({ 415 | message: 'expected test error', 416 | statusCode: 400, 417 | }); 418 | const anotherError = new ErrorWithProps({ 419 | message: 'another expected error', 420 | statusCode: 500, 421 | }); 422 | const errors = [testError, anotherError, 'ERR STRING']; 423 | givenErrorHandlerForError(errors, {debug: true}); 424 | 425 | requestJson().expect(500).end(function(err, res) { 426 | if (err) return done(err); 427 | 428 | const data = res.body.error; 429 | expect(data).to.have.property('message').that.match(/multiple errors/); 430 | 431 | const expectedDetails = [ 432 | getExpectedErrorData(testError), 433 | getExpectedErrorData(anotherError), 434 | {message: 'ERR STRING', statusCode: 500}, 435 | ]; 436 | expect(data).to.have.property('details').to.eql(expectedDetails); 437 | done(); 438 | }); 439 | }); 440 | 441 | it('includes safeFields of array items when debug=false', (done) => { 442 | const internalError = new ErrorWithProps({ 443 | message: 'a test error message', 444 | code: 'MACHINE_READABLE_CODE', 445 | details: 'some details', 446 | extra: 'sensitive data', 447 | }); 448 | const validationError = new ErrorWithProps({ 449 | name: 'ValidationError', 450 | message: 'The model instance is not valid.', 451 | statusCode: 422, 452 | code: 'VALIDATION_ERROR', 453 | details: 'some details', 454 | extra: 'sensitive data', 455 | }); 456 | 457 | const errors = [internalError, validationError, 'ERR STRING']; 458 | givenErrorHandlerForError(errors, { 459 | debug: false, 460 | safeFields: ['code'], 461 | }); 462 | 463 | requestJson().end(function(err, res) { 464 | if (err) return done(err); 465 | const data = res.body.error; 466 | 467 | const expectedInternalError = { 468 | statusCode: 500, 469 | message: 'Internal Server Error', 470 | code: 'MACHINE_READABLE_CODE', 471 | // notice the property "extra" is not included 472 | }; 473 | const expectedValidationError = { 474 | statusCode: 422, 475 | message: 'The model instance is not valid.', 476 | name: 'ValidationError', 477 | code: 'VALIDATION_ERROR', 478 | details: 'some details', 479 | // notice the property "extra" is not included 480 | }; 481 | const expectedErrorFromString = { 482 | message: 'Internal Server Error', 483 | statusCode: 500, 484 | }; 485 | const expectedDetails = [ 486 | expectedInternalError, 487 | expectedValidationError, 488 | expectedErrorFromString, 489 | ]; 490 | 491 | expect(data).to.have.property('message').that.match(/multiple errors/); 492 | expect(data).to.have.property('details').to.eql(expectedDetails); 493 | done(); 494 | }); 495 | }); 496 | 497 | it('handles non-Error argument as 500 when debug=false', function(done) { 498 | givenErrorHandlerForError('Error Message', {debug: false}); 499 | requestJson().expect(500).end(function(err, res) { 500 | if (err) return done(err); 501 | 502 | expect(res.body.error).to.eql({ 503 | statusCode: 500, 504 | message: 'Internal Server Error', 505 | }); 506 | done(); 507 | }); 508 | }); 509 | 510 | it('returns non-Error argument in message when debug=true', function(done) { 511 | givenErrorHandlerForError('Error Message', {debug: true}); 512 | requestJson().expect(500).end(function(err, res) { 513 | if (err) return done(err); 514 | 515 | expect(res.body.error).to.eql({ 516 | statusCode: 500, 517 | message: 'Error Message', 518 | }); 519 | done(); 520 | }); 521 | }); 522 | 523 | it('handles Error objects containing circular properties', function(done) { 524 | const circularObject = {}; 525 | circularObject.recursiveProp = circularObject; 526 | const error = new ErrorWithProps({ 527 | statusCode: 422, 528 | message: 'The model instance is not valid.', 529 | name: 'ValidationError', 530 | code: 'VALIDATION_ERROR', 531 | details: circularObject, 532 | }); 533 | givenErrorHandlerForError(error, {debug: true}); 534 | requestJson().end(function(err, res) { 535 | if (err) return done(err); 536 | expect(res.body).to.have.property('error'); 537 | expect(res.body.error).to.have.property('details'); 538 | expect(res.body.error.details).to.have.property('recursiveProp', 539 | '[Circular]'); 540 | done(); 541 | }); 542 | }); 543 | 544 | it('honors rootProperty', function(done) { 545 | givenErrorHandlerForError('Error Message', {rootProperty: 'data'}); 546 | requestJson().expect(500).end(function(err, res) { 547 | if (err) return done(err); 548 | 549 | expect(res.body.data).to.eql({ 550 | statusCode: 500, 551 | message: 'Internal Server Error', 552 | }); 553 | done(); 554 | }); 555 | }); 556 | 557 | it('honors rootProperty=false', function(done) { 558 | givenErrorHandlerForError('Error Message', {rootProperty: false}); 559 | requestJson().expect(500).end(function(err, res) { 560 | if (err) return done(err); 561 | 562 | expect(res.body).to.eql({ 563 | statusCode: 500, 564 | message: 'Internal Server Error', 565 | }); 566 | done(); 567 | }); 568 | }); 569 | 570 | function requestJson(url) { 571 | return request.get(url || '/') 572 | .set('Accept', 'text/plain') 573 | .expect('Content-Type', /^application\/json/); 574 | } 575 | }); 576 | 577 | context('HTML response', function() { 578 | it('contains all error properties when debug=true', function(done) { 579 | const error = new ErrorWithProps({ 580 | message: 'a test error message', 581 | details: 'some details', 582 | extra: 'sensitive data', 583 | }); 584 | error.statusCode = 500; 585 | givenErrorHandlerForError(error, {debug: true}); 586 | requestHTML() 587 | .expect(500) 588 | .expect(/ErrorWithProps<\/title>/) 589 | .expect(/500(.*?)a test error message/) 590 | .expect(/extra(.*?)sensitive data/) 591 | .expect(/details(.*?)some details/) 592 | .expect(/id="stacktrace"(.*?)ErrorWithProps: a test error message/, 593 | done); 594 | }); 595 | 596 | it('HTML-escapes all 4xx response properties in production mode', 597 | function(done) { 598 | const error = new ErrorWithProps({ 599 | name: 'Error<img onerror=alert(1) src=a>', 600 | message: 601 | 'No instance with id <img onerror=alert(1) src=a> found for Model', 602 | statusCode: 404, 603 | }); 604 | givenErrorHandlerForError(error, {debug: false}); 605 | requestHTML() 606 | .end(function(err, res) { 607 | expect(res.statusCode).to.eql(404); 608 | const body = res.error.text; 609 | expect(body).to.match( 610 | /<title>Error<img onerror=alert\(1\) src=a><\/title>/, 611 | ); 612 | expect(body).to.match( 613 | /with id <img onerror=alert\(1\) src=a> found for Model/, 614 | ); 615 | done(); 616 | }); 617 | }); 618 | 619 | it('HTML-escapes all 5xx response properties in development mode', 620 | function(done) { 621 | const error = new ErrorWithProps({ 622 | message: 'a test error message<img onerror=alert(1) src=a>', 623 | }); 624 | error.statusCode = 500; 625 | givenErrorHandlerForError(error, {debug: true}); 626 | requestHTML() 627 | .expect(500) 628 | .expect(/<title>ErrorWithProps<\/title>/) 629 | .expect( 630 | /500(.*?)a test error message<img onerror=alert\(1\) src=a>/, 631 | done, 632 | ); 633 | }); 634 | 635 | it('contains subset of properties when status=4xx', function(done) { 636 | const error = new ErrorWithProps({ 637 | name: 'ValidationError', 638 | message: 'The model instance is not valid.', 639 | statusCode: 422, 640 | details: 'some details', 641 | extra: 'sensitive data', 642 | }); 643 | givenErrorHandlerForError(error, {debug: false}); 644 | requestHTML() 645 | .end(function(err, res) { 646 | expect(res.statusCode).to.eql(422); 647 | const body = res.error.text; 648 | expect(body).to.match(/some details/); 649 | expect(body).to.not.match(/sensitive data/); 650 | expect(body).to.match(/<title>ValidationError<\/title>/); 651 | expect(body).to.match(/422(.*?)The model instance is not valid./); 652 | done(); 653 | }); 654 | }); 655 | 656 | it('contains only safe info when status=5xx', function(done) { 657 | // Mock an error reported by fs.readFile 658 | const error = new ErrorWithProps({ 659 | name: 'Error', 660 | message: 'ENOENT: no such file or directory, open "/etc/passwd"', 661 | errno: -2, 662 | code: 'ENOENT', 663 | syscall: 'open', 664 | path: '/etc/password', 665 | }); 666 | givenErrorHandlerForError(error); 667 | 668 | requestHTML() 669 | .end(function(err, res) { 670 | expect(res.statusCode).to.eql(500); 671 | const body = res.error.text; 672 | expect(body).to.not.match(/\/etc\/password/); 673 | expect(body).to.not.match(/-2/); 674 | expect(body).to.not.match(/ENOENT/); 675 | // only have the following 676 | expect(body).to.match(/<title>Internal Server Error<\/title>/); 677 | expect(body).to.match(/500(.*?)Internal Server Error/); 678 | done(); 679 | }); 680 | }); 681 | 682 | function requestHTML(url) { 683 | return request.get(url || '/') 684 | .set('Accept', 'text/html') 685 | .expect('Content-Type', /^text\/html/); 686 | } 687 | }); 688 | 689 | context('XML response', function() { 690 | it('contains all error properties when debug=true', function(done) { 691 | const error = new ErrorWithProps({ 692 | message: 'a test error message', 693 | details: 'some details', 694 | extra: 'sensitive data', 695 | }); 696 | error.statusCode = 500; 697 | givenErrorHandlerForError(error, {debug: true}); 698 | requestXML() 699 | .expect(500) 700 | .expect(/<statusCode>500<\/statusCode>/) 701 | .expect(/<name>ErrorWithProps<\/name>/) 702 | .expect(/<message>a test error message<\/message>/) 703 | .expect(/<details>some details<\/details>/) 704 | .expect(/<extra>sensitive data<\/extra>/) 705 | .expect(/<stack>ErrorWithProps: a test error message(.*?)/, done); 706 | }); 707 | 708 | it('contains subset of properties when status=4xx', function(done) { 709 | const error = new ErrorWithProps({ 710 | name: 'ValidationError', 711 | message: 'The model instance is not valid.', 712 | statusCode: 422, 713 | details: 'some details', 714 | extra: 'sensitive data', 715 | }); 716 | givenErrorHandlerForError(error, {debug: false}); 717 | requestXML() 718 | .end(function(err, res) { 719 | expect(res.statusCode).to.eql(422); 720 | const body = res.error.text; 721 | expect(body).to.match(/<details>some details<\/details>/); 722 | expect(body).to.not.match(/<extra>sensitive data<\/extra>/); 723 | expect(body).to.match(/<name>ValidationError<\/name>/); 724 | expect(body).to.match( 725 | /<message>The model instance is not valid.<\/message>/, 726 | ); 727 | done(); 728 | }); 729 | }); 730 | 731 | it('contains only safe info when status=5xx', function(done) { 732 | // Mock an error reported by fs.readFile 733 | const error = new ErrorWithProps({ 734 | name: 'Error', 735 | message: 'ENOENT: no such file or directory, open "/etc/passwd"', 736 | errno: -2, 737 | code: 'ENOENT', 738 | syscall: 'open', 739 | path: '/etc/password', 740 | }); 741 | givenErrorHandlerForError(error); 742 | 743 | requestXML() 744 | .end(function(err, res) { 745 | expect(res.statusCode).to.eql(500); 746 | const body = res.error.text; 747 | expect(body).to.not.match(/\/etc\/password/); 748 | expect(body).to.not.match(/-2/); 749 | expect(body).to.not.match(/ENOENT/); 750 | // only have the following 751 | expect(body).to.match(/<statusCode>500<\/statusCode>/); 752 | expect(body).to.match(/<message>Internal Server Error<\/message>/); 753 | done(); 754 | }); 755 | }); 756 | 757 | it('honors options.rootProperty', function(done) { 758 | const error = new ErrorWithProps({ 759 | name: 'ValidationError', 760 | message: 'The model instance is not valid.', 761 | statusCode: 422, 762 | details: 'some details', 763 | extra: 'sensitive data', 764 | }); 765 | givenErrorHandlerForError(error, {rootProperty: 'myRoot'}); 766 | requestXML() 767 | .end(function(err, res) { 768 | expect(res.statusCode).to.eql(422); 769 | const body = res.error.text; 770 | expect(body).to.match(/<myRoot>/); 771 | expect(body).to.match(/<details>some details<\/details>/); 772 | expect(body).to.not.match(/<extra>sensitive data<\/extra>/); 773 | expect(body).to.match(/<name>ValidationError<\/name>/); 774 | expect(body).to.match( 775 | /<message>The model instance is not valid.<\/message>/, 776 | ); 777 | done(); 778 | }); 779 | }); 780 | 781 | it('ignores options.rootProperty = false', function(done) { 782 | const error = new ErrorWithProps({ 783 | name: 'ValidationError', 784 | message: 'The model instance is not valid.', 785 | statusCode: 422, 786 | details: 'some details', 787 | extra: 'sensitive data', 788 | }); 789 | givenErrorHandlerForError(error, {rootProperty: false}); 790 | requestXML() 791 | .end(function(err, res) { 792 | expect(res.statusCode).to.eql(422); 793 | const body = res.error.text; 794 | expect(body).to.match(/<error>/); 795 | expect(body).to.match(/<details>some details<\/details>/); 796 | expect(body).to.not.match(/<extra>sensitive data<\/extra>/); 797 | expect(body).to.match(/<name>ValidationError<\/name>/); 798 | expect(body).to.match( 799 | /<message>The model instance is not valid.<\/message>/, 800 | ); 801 | done(); 802 | }); 803 | }); 804 | 805 | function requestXML(url) { 806 | return request.get(url || '/') 807 | .set('Accept', 'text/xml') 808 | .expect('Content-Type', /^text\/xml/); 809 | } 810 | }); 811 | 812 | context('Content Negotiation', function() { 813 | it('defaults to json without options', function(done) { 814 | givenErrorHandlerForError(new Error('Some error'), {}); 815 | request.get('/') 816 | .set('Accept', '*/*') 817 | .expect('Content-Type', /^application\/json/, done); 818 | }); 819 | 820 | it('honors accepted content-type', function(done) { 821 | givenErrorHandlerForError(new Error('Some error'), { 822 | defaultType: 'application/json', 823 | }); 824 | request.get('/') 825 | .set('Accept', 'text/html') 826 | .expect('Content-Type', /^text\/html/, done); 827 | }); 828 | 829 | it('honors order of accepted content-type', function(done) { 830 | givenErrorHandlerForError(new Error('Some error'), { 831 | defaultType: 'text/html', 832 | }); 833 | request.get('/') 834 | // `application/json` will be used because its provided first 835 | .set('Accept', 'application/json, text/html') 836 | .expect('Content-Type', /^application\/json/, done); 837 | }); 838 | 839 | it('disables content-type negotiation when negotiateContentType=false', 840 | function(done) { 841 | givenErrorHandlerForError(new Error('Some error'), { 842 | negotiateContentType: false, 843 | defaultType: 'application/json', 844 | }); 845 | request.get('/') 846 | .set('Accept', 'text/html') 847 | .expect('Content-Type', /^application\/json/, done); 848 | }); 849 | 850 | it('chooses resolved type when negotiateContentType=false + not-supported', 851 | function(done) { 852 | givenErrorHandlerForError(new Error('Some error'), { 853 | negotiateContentType: false, 854 | defaultType: 'unsupported/type', 855 | }); 856 | request.get('/') 857 | .set('Accept', 'text/html') 858 | .expect('Content-Type', /^text\/html/, done); 859 | }); 860 | 861 | it('chooses default type when negotiateContentType=false + not-supported ', 862 | function(done) { 863 | givenErrorHandlerForError(new Error('Some error'), { 864 | negotiateContentType: false, 865 | defaultType: 'unsupported/type', 866 | }); 867 | request.get('/') 868 | .expect('Content-Type', /^application\/json/, done); 869 | }); 870 | 871 | it('honors order of accepted content-types of text/html', function(done) { 872 | givenErrorHandlerForError(new Error('Some error'), { 873 | defaultType: 'application/json', 874 | }); 875 | request.get('/') 876 | // text/html will be used because its provided first 877 | .set('Accept', 'text/html, application/json') 878 | .expect('Content-Type', /^text\/html/, done); 879 | }); 880 | 881 | it('picks first supported type upon multiple accepted', function(done) { 882 | givenErrorHandlerForError(new Error('Some error'), { 883 | defaultType: 'application/json', 884 | }); 885 | request.get('/') 886 | .set('Accept', '*/*, not-supported, text/html, application/json') 887 | .expect('Content-Type', /^text\/html/, done); 888 | }); 889 | 890 | it('falls back for unsupported option.defaultType', function(done) { 891 | givenErrorHandlerForError(new Error('Some error'), { 892 | defaultType: 'unsupported', 893 | }); 894 | request.get('/') 895 | .set('Accept', '*/*') 896 | .expect('Content-Type', /^application\/json/, done); 897 | }); 898 | 899 | it('returns defaultType for unsupported type', function(done) { 900 | givenErrorHandlerForError(new Error('Some error'), { 901 | defaultType: 'text/html', 902 | }); 903 | request.get('/') 904 | .set('Accept', 'unsupported/type') 905 | .expect('Content-Type', /^text\/html/, done); 906 | }); 907 | 908 | it('supports query _format', function(done) { 909 | givenErrorHandlerForError(new Error('Some error'), { 910 | defaultType: 'text/html', 911 | }); 912 | request.get('/?_format=html') 913 | .set('Accept', 'application/json') 914 | .expect('Content-Type', /^text\/html/, done); 915 | }); 916 | 917 | it('handles unknown _format query', function() { 918 | givenErrorHandlerForError(); 919 | return request.get('/?_format=unknown') 920 | .expect('X-Warning', /_format.*not supported/); 921 | }); 922 | }); 923 | 924 | it('does not modify "options" argument', function(done) { 925 | const options = {log: false, debug: false}; 926 | givenErrorHandlerForError(new Error(), options); 927 | request.get('/').end(function(err) { 928 | if (err) return done(err); 929 | expect(options).to.eql({log: false, debug: false}); 930 | done(); 931 | }); 932 | }); 933 | }); 934 | 935 | let app, _requestHandler, request, server; 936 | function resetRequestHandler() { 937 | _requestHandler = null; 938 | } 939 | 940 | function givenErrorHandlerForError(error, options) { 941 | if (!error) error = new Error('an error'); 942 | 943 | if (!options) options = {}; 944 | if (!('log' in options)) { 945 | // Disable logging to console by default, so that we don't spam 946 | // console output. One can use "DEBUG=strong-error-handler" when 947 | // troubleshooting. 948 | options.log = false; 949 | } 950 | 951 | const handler = strongErrorHandler(options); 952 | _requestHandler = function(req, res, next) { 953 | debug('Invoking strong-error-handler'); 954 | handler(error, req, res, next); 955 | }; 956 | } 957 | 958 | function setupHttpServerAndClient(done) { 959 | app = express(); 960 | app.use(function(req, res, next) { 961 | if (!_requestHandler) { 962 | const msg = 'Error handler middleware was not setup in this test'; 963 | console.error(msg); 964 | res.statusCode = 500; 965 | res.setHeader('Content-Type', 'text/plain; charset=utf-8'); 966 | res.end(msg); 967 | return; 968 | } 969 | 970 | _requestHandler(req, res, warnUnhandledError); 971 | 972 | function warnUnhandledError(err) { 973 | console.log('unexpected: strong-error-handler called next with', 974 | (err && (err.stack || err)) || 'no error'); 975 | res.statusCode = 500; 976 | res.setHeader('Content-Type', 'text/plain; charset=utf-8'); 977 | res.end(err ? 978 | 'Unhandled strong-error-handler error:\n' + (err.stack || err) : 979 | 'The error was silently discared by strong-error-handler'); 980 | } 981 | }); 982 | 983 | server = app.listen(0, function() { 984 | const url = 'http://127.0.0.1:' + this.address().port; 985 | debug('Test server listening on %s', url); 986 | request = supertest(app); 987 | done(); 988 | }) 989 | .once('error', function(err) { 990 | debug('Cannot setup HTTP server: %s', err.stack); 991 | done(err); 992 | }); 993 | } 994 | 995 | function stopHttpServerAndClient() { 996 | server.close(); 997 | } 998 | 999 | function ErrorWithProps(props) { 1000 | this.name = props.name || 'ErrorWithProps'; 1001 | for (const p in props) { 1002 | this[p] = props[p]; 1003 | } 1004 | 1005 | if (Error.captureStackTrace) { 1006 | // V8 (Chrome, Opera, Node) 1007 | Error.captureStackTrace(this, this.constructor); 1008 | } 1009 | } 1010 | util.inherits(ErrorWithProps, Error); 1011 | 1012 | function getExpectedErrorData(err) { 1013 | const data = {}; 1014 | cloneAllProperties(data, err); 1015 | return data; 1016 | } 1017 | -------------------------------------------------------------------------------- /views/default-error.ejs: -------------------------------------------------------------------------------- 1 | <html> 2 | <head> 3 | <meta charset='utf-8'> 4 | <title><%= data.name || data.message %> 5 | 6 | 7 | 8 |
9 |

<%= data.name %>

10 |

<%= data.statusCode %> <%= data.message %>

11 | <% 12 | // display all the non-standard properties 13 | var standardProps = ['name', 'statusCode', 'message', 'stack']; 14 | for (var prop in data) { 15 | if (standardProps.indexOf(prop) == -1 && data[prop]) { %> 16 |
<%= prop %>: <%= data[prop] %>
17 | <% } 18 | } 19 | if (data.stack) { %> 20 |
<%- data.stack %>
21 | <% } 22 | %> 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /views/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | outline: 0; 5 | } 6 | body { 7 | padding: 80px 100px; 8 | font: 13px "Helvetica Neue", "Lucida Grande", "Arial"; 9 | background: #ECE9E9 -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fff), to(#ECE9E9)); 10 | background: #ECE9E9 -moz-linear-gradient(top, #fff, #ECE9E9); 11 | background-repeat: no-repeat; 12 | color: #555; 13 | -webkit-font-smoothing: antialiased; 14 | } 15 | h1, h2 { 16 | font-size: 22px; 17 | color: #343434; 18 | } 19 | h1 em, h2 em { 20 | padding: 0 5px; 21 | font-weight: normal; 22 | } 23 | h1 { 24 | font-size: 60px; 25 | } 26 | h2 { 27 | margin: 10px 0; 28 | } 29 | ul li { 30 | list-style: none; 31 | } 32 | --------------------------------------------------------------------------------