├── .eslintrc ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .markdownlintrc ├── .node-version ├── .nvmrc ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── CODEOWNERS ├── LICENSE ├── README.md ├── commitlint.config.js ├── jest.config.js ├── package.json ├── src ├── errors │ ├── ClientError.ts │ ├── HTTPError.ts │ ├── ServerError.ts │ ├── client │ │ ├── BadRequestError.ts │ │ ├── ConflictError.ts │ │ ├── ExpectationFailedError.ts │ │ ├── FailedDependencyError.ts │ │ ├── ForbiddenError.ts │ │ ├── GoneError.ts │ │ ├── ImATeapotError.ts │ │ ├── LengthRequiredError.ts │ │ ├── LockedError.ts │ │ ├── MethodNotAllowedError.ts │ │ ├── MisdirectedRequestError.ts │ │ ├── NotAcceptableError.ts │ │ ├── NotFoundError.ts │ │ ├── PayloadTooLargeError.ts │ │ ├── PaymentRequiredError.ts │ │ ├── PreconditionFailedError.ts │ │ ├── PreconditionRequiredError.ts │ │ ├── ProxyAuthenticationRequiredError.ts │ │ ├── RangeNotSatisfiableError.ts │ │ ├── RequestHeaderFieldsTooLargeError.ts │ │ ├── RequestTimeoutError.ts │ │ ├── TooManyRequestsError.ts │ │ ├── URITooLongError.ts │ │ ├── UnauthorizedError.ts │ │ ├── UnavailableForLegalReasonsError.ts │ │ ├── UnorderedCollectionError.ts │ │ ├── UnprocessableEntityError.ts │ │ ├── UnsupportedMediaTypeError.ts │ │ ├── UpgradeRequiredError.ts │ │ └── index.ts │ ├── index.ts │ └── server │ │ ├── BadGatewayError.ts │ │ ├── BandwidthLimitExceededError.ts │ │ ├── GatewayTimeoutError.ts │ │ ├── HTTPVersionNotSupportedError.ts │ │ ├── InsufficientStorageError.ts │ │ ├── InternalServerError.ts │ │ ├── LoopDetectedError.ts │ │ ├── NetworkAuthenticationRequiredError.ts │ │ ├── NotExtendedError.ts │ │ ├── NotImplementedError.ts │ │ ├── ServiceUnavailableError.ts │ │ ├── VariantAlsoNegotiatesError.ts │ │ └── index.ts ├── index.ts └── status │ └── index.ts ├── test ├── fixtures │ ├── express.ts │ └── koa.ts ├── functional │ ├── express.spec.ts │ └── koa.spec.ts ├── unit │ ├── error.spec.ts │ └── status.spec.ts └── utils │ └── index.ts ├── tsconfig.json └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@greylocklabs" 3 | } 4 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Follow these guidelines if you'd like to contribute to the project! 4 | 5 | --- 6 | 7 | ## Table of contents 8 | 9 | Read through these guidelines before you get started: 10 | 11 | 1. [Questions & Concerns](#questions--concerns) 12 | 2. [Issues & Bugs](#issues--bugs) 13 | 3. [Feature Requests](#feature-requests) 14 | 4. [Submitting Pull Requests](#submitting-pull-requests) 15 | 5. [Code Style](#code-style) 16 | 17 | ## Questions & concerns 18 | 19 | If you have any questions about using or developing for this project, reach out 20 | to @tylucaskelley or send an [email][1]. 21 | 22 | ## Issues & bugs 23 | 24 | Submit an [issue][2] or [pull request][3] with a fix if you find any bugs in 25 | the project. See [below](#submitting-pull-requests) for instructions on sending 26 | in pull requests, and be sure to reference the [code style guide](#code-style) 27 | first! 28 | 29 | When submitting an issue or pull request, make sure you're as detailed as possible 30 | and fill in all answers to questions asked in the templates. For example, an issue 31 | that simply states "X/Y/Z isn't working!" will be closed. 32 | 33 | ## Feature requests 34 | 35 | Submit an [issue][2] to request a new feature. Features fall into one of two 36 | categories: 37 | 38 | 1. **Major**: Major changes should be discussed via [email][1]. I'm 39 | always open to suggestions and will get back to you as soon as I can! 40 | 2. **Minor**: A minor feature can simply be added via a [pull request][3]. 41 | 42 | ## Submitting pull requests 43 | 44 | Before you do anything, make sure you check the current list of [pull requests][4] 45 | to ensure you aren't duplicating anyone's work. Then, do the following: 46 | 47 | 1. Fork the repository and make your changes in a git branch: `git checkout -b my-branch base-branch` 48 | 2. Read and follow the [code style guidelines](#code-style). 49 | 3. Make sure your feature or fix doesn't break the project! Test thoroughly. 50 | 4. Commit your changes, and be sure to leave a detailed commit message. 51 | 5. Push your branch to your forked repo on GitHub: `git push origin my-branch` 52 | 6. [Submit a pull request][3] and hold tight! 53 | 7. If any changes are requested by the project maintainers, make them and follow 54 | this process again until the changes are merged in. 55 | 56 | ## Code style 57 | 58 | Please follow the coding style conventions detailed below: 59 | 60 | - Ensure that `yarn lint` always passes 61 | 62 | [1]: mailto:ty@greylocklabs.com 63 | [2]: https://github.com/greylocklabs/teapot/issues/new 64 | [3]: https://github.com/greylocklabs/teapot/compare 65 | [4]: https://github.com/greylocklabs/teapot/pulls 66 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Submit a feature request or bug report 2 | 3 | Before you submit an issue, check to see if it has [already been reported][1]. 4 | Any questions should be directed to @tylucaskelley. 5 | 6 | --- 7 | 8 | Replace any "X" below with your information. 9 | 10 | ## Development environment 11 | 12 | - Operating system: X 13 | - Browser: X 14 | - Software version: X 15 | 16 | ## Current behavior 17 | 18 | Include screenshots if possible. 19 | 20 | X 21 | 22 | ## Expected behavior 23 | 24 | X 25 | 26 | ## Steps to reproduce 27 | 28 | Only fill this in if you are filing a bug report. 29 | 30 | 1. X 31 | 32 | ## Other relevant information 33 | 34 | X 35 | 36 | [1]: https://github.com/greylocklabs/teapot/issues 37 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Submit a pull request 2 | 3 | Thank you for submitting a pull request! To speed up the review process, please ensure that everything below 4 | is true: 5 | 6 | 1. This is not a duplicate of an [existing pull request][1]. 7 | 2. No existing features have been broken without good reason. 8 | 3. Your commit messages are detailed. 9 | 4. The code style [guidelines][2] have been followed. 10 | 5. Documentation has been updated to reflect your changes. 11 | 6. Tests have been added or updated to reflect your changes. 12 | 7. All tests have passed. 13 | 14 | Any questions should be directed to @tylucaskelley. 15 | 16 | --- 17 | 18 | Replace any "X" below with information about your pull request. 19 | 20 | ## Pull request details 21 | 22 | Provide details about your pull request and what it adds, fixes, or changes. 23 | 24 | X 25 | 26 | ## Breaking changes 27 | 28 | Describe what features are broken by this pull request and why, if any. 29 | 30 | X 31 | 32 | ## Issues fixed 33 | 34 | Enter the issue numbers resolved by this pull request below, if any. 35 | 36 | 1. X 37 | 38 | ## Other relevant information 39 | 40 | Provide any other important details below. 41 | 42 | X 43 | 44 | [1]: https://github.com/greylocklabs/teapot/pulls 45 | [2]: https://github.com/greylocklabs/teapot/blob/master/.github/CONTRIBUTING.md#code-style 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .rts2_cache_* 3 | 4 | # Created by https://www.gitignore.io/api/node 5 | # Edit at https://www.gitignore.io/?templates=node 6 | 7 | ### Node ### 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | 16 | # Diagnostic reports (https://nodejs.org/api/report.html) 17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | .env.test 71 | 72 | # parcel-bundler cache (https://parceljs.org/) 73 | .cache 74 | 75 | # next.js build output 76 | .next 77 | 78 | # nuxt.js build output 79 | .nuxt 80 | 81 | # vuepress build output 82 | .vuepress/dist 83 | 84 | # Serverless directories 85 | .serverless/ 86 | 87 | # FuseBox cache 88 | .fusebox/ 89 | 90 | # DynamoDB Local files 91 | .dynamodb/ 92 | 93 | # End of https://www.gitignore.io/api/node 94 | -------------------------------------------------------------------------------- /.markdownlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | 4 | "MD001": true, 5 | "MD002": true, 6 | "MD003": { 7 | "headers": "atx" 8 | }, 9 | "MD004": { 10 | "style": "dash" 11 | }, 12 | "MD005": true, 13 | "MD006": true, 14 | "MD007": { 15 | "indent": 2 16 | }, 17 | "MD009": true, 18 | "MD010": true, 19 | "MD011": true, 20 | "MD012": true, 21 | "MD013": { 22 | "line_length": 120, 23 | "code_blocks": false, 24 | "tables": false, 25 | "headers": true 26 | }, 27 | "MD014": false, 28 | "MD018": true, 29 | "MD019": true, 30 | "MD020": true, 31 | "MD021": true, 32 | "MD022": true, 33 | "MD023": true, 34 | "MD024": false, 35 | "MD025": true, 36 | "MD026": { 37 | "punctuation": ".,;:!" 38 | }, 39 | "MD027": true, 40 | "MD028": true, 41 | "MD029": { 42 | "style": "ordered" 43 | }, 44 | "MD030": true, 45 | "MD031": true, 46 | "MD032": true, 47 | "MD033": { 48 | "allowed_elements": [ "script", "div" ] 49 | }, 50 | "MD034": true, 51 | "MD035": { 52 | "style": "---" 53 | }, 54 | "MD036": { 55 | "punctuation": ".,;:!" 56 | }, 57 | "MD037": true, 58 | "MD038": true, 59 | "MD039": true, 60 | "MD040": true, 61 | "MD041": true, 62 | "MD042": true, 63 | "MD043": false, 64 | "MD044": false 65 | } 66 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 12.9.1 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.9.1 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | arrowParens: 'always' 2 | bracketSpacing: true 3 | printWidth: 100 4 | semiColon: true 5 | singleQuote: true 6 | tabs: false 7 | tabWidth: 2 8 | trailingComma: 'es5' 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | - 11 5 | - 10 6 | - 8 7 | after_success: yarn test:coverage 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. The format is based on 4 | [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to 5 | [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | --- 8 | 9 | ## [Unreleased](https://github.com/greylocklabs/teapot/compare/3.1.1...HEAD) 10 | 11 | - N/A 12 | 13 | ## [3.1.1](https://github.com/greylocklabs/teapot/compare/3.1.0...3.1.1) - 2020-02-24 14 | 15 | ### Changed 16 | 17 | - Update dependencies, fix linting errors 18 | 19 | ## [3.1.0](https://github.com/greylocklabs/teapot/compare/3.0.5...3.1.0) - 2019-09-04 20 | 21 | ### Fixed 22 | 23 | - TypeScript type definitions 24 | 25 | ### Changed 26 | 27 | - Dependency upgrades 28 | - Minor config file changes 29 | 30 | ## [3.0.5](https://github.com/greylocklabs/teapot/compare/3.0.4...3.0.5) - 2019-04-22 31 | 32 | ### Fixed 33 | 34 | - Check for existence of `Error.captureStackTrace` before calling 35 | 36 | ## [3.0.4](https://github.com/greylocklabs/teapot/compare/3.0.2...3.0.4) - 2019-04-22 37 | 38 | ### Added 39 | 40 | - Ability to set `data` property on errors, using `teapot.error` function. 41 | 42 | ## [3.0.2](https://github.com/greylocklabs/teapot/compare/3.0.1...3.0.2) - 2019-04-19 43 | 44 | ### Fixed 45 | 46 | - Adds TypeScript declaration file 47 | 48 | ## [3.0.1](https://github.com/greylocklabs/teapot/compare/3.0.0...3.0.1) - 2019-04-19 49 | 50 | ### Fixed 51 | 52 | - Correct package name in README + .yarnrc 53 | 54 | ## [3.0.0](https://github.com/greylocklabs/teapot/compare/2.0.1...3.0.0) - 2019-04-18 55 | 56 | ### Changed 57 | 58 | - Moves to TypeScript 59 | - New APIs - `teapot.status` function replaced with `teapot.status.code` 60 | - Uses Jest for testing 61 | 62 | ## [2.0.1](https://github.com/greylocklabs/teapot/compare/1.2.0...2.0.1) - 2018-02-06 63 | 64 | ### Changed 65 | 66 | A whole lot! The package has been renamed to `teapot` to make room for the new default export and avoid confusion 67 | between it and Node's native `http` module. Some other changes: 68 | 69 | - `createError` is now `teapot.error`; functionality is the same 70 | - `status` is now `teapot.status`; functionality is the same 71 | - Errors can be accessed via the default export `teapot.errors`, or by named import 72 | - The package bundle now only includes the `dist` folder to shrink size 73 | - Minor changes to examples 74 | - Far more extensive test suite 75 | 76 | ## [1.2.0](https://github.com/greylocklabs/teapot/compare/1.1.0...1.2.0) - 2018-02-01 77 | 78 | ### Added 79 | 80 | - A new `createError` function! It lets you take a status code and create the proper `ClientError` 81 | or `ServerError` that goes with it. Examples: 82 | - `const err = clientError(404, 'My not found message');` 83 | - Update dependencies 84 | - Examples updated to use `createError` 85 | - More tests 86 | 87 | ### Changed 88 | 89 | - Classes that inherit `ClientError` or `ServerError` now have a static `code` method to get the 90 | HTTP status code associated with the error. For example, `NotFoundError.code() === 404` 91 | 92 | ## [1.1.0](https://github.com/greylocklabs/teapot/compare/1.0.1...1.1.0) - 2018-01-17 93 | 94 | ### Changed 95 | 96 | - Switch to Node.js Markdown linting tool 97 | - Update packages 98 | 99 | ## [1.0.1](https://github.com/greylocklabs/teapot/compare/1.0.0...1.0.1) - 2017-11-18 100 | 101 | ### Changed 102 | 103 | - Minor changes to NPM scripts 104 | 105 | ### Removed 106 | 107 | - No longer generating JSDoc files, using Doclets instead 108 | 109 | ## [1.0.0](https://github.com/greylocklabs/teapot/releases/tag/1.0.0) - 2017-11-17 110 | 111 | ### Added 112 | 113 | - Status code and error utility modules 114 | - Tests for all modules 115 | - Full code coverage 116 | - JSDoc documentation 117 | - Examples using Koa and Express 118 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | @tylucaskelley 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2019 Greylock Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Teapot 2 | 3 | > Utilities for working with HTTP status codes, errors, and more. 4 | 5 | [![npm version](https://badge.fury.io/js/node-teapot.svg)](https://badge.fury.io/js/node-teapot) 6 | [![build status](https://travis-ci.org/greylocklabs/teapot.svg?branch=master)](https://travis-ci.org/greylocklabs/teapot) 7 | [![coverage status](https://coveralls.io/repos/github/greylocklabs/teapot/badge.svg?branch=master)](https://coveralls.io/github/greylocklabs/teapot?branch=master) 8 | 9 | --- 10 | 11 | Teapot is an HTTP utility library for JavaScript, which leverages the 12 | [Node.js HTTP library](https://nodejs.org/api/http.html). It provides the following: 13 | 14 | 1. The ability to get an HTTP status code: `teapot.status(404)` and `teapot.status('not found')` would both 15 | return `404`. 16 | 2. Useful error classes to represent HTTP error codes: 17 | - `HTTPError`: Base class to represent an HTTP error 18 | - `ClientError` and `ServerError`: Classes to represent 4xx and 5xx errors 19 | - Classes for every unique HTTP error status code, ranging from `NotImplementedError` to `PaymentRequiredError` 20 | 3. A function to generate one of the HTTP error classes from a status code: `teapot.error(404)` would return an 21 | instance of `NotFoundError`. Great when handling responses from third-party APIs, when you might not know what 22 | status codes to expect all the time. 23 | 24 | TypeScript definitions are included as well. 25 | 26 | ## Installation 27 | 28 | With `yarn`: 29 | 30 | ```bash 31 | $ yarn add node-teapot 32 | ``` 33 | 34 | With `npm`: 35 | 36 | ```bash 37 | $ npm install node-teapot 38 | ``` 39 | 40 | ## Usage 41 | 42 | ### Get a status code 43 | 44 | There are a variety of ways to get a status code from a number or string message: 45 | 46 | ```js 47 | teapot.status.code(404); // 404 48 | teapot.status.code('not implemented'); // 405 49 | 50 | teapot.status.codes['BAD GATEWAY']; // 502 51 | 52 | teapot.status.MOVED_PERMANENTLY; // 301 53 | ``` 54 | 55 | ### Get a canned status message 56 | 57 | ```js 58 | teapot.status[200]; // "OK" 59 | ``` 60 | 61 | ### Create an HTTP error 62 | 63 | Teapot's errors are compatible with Koa and Express: 64 | 65 | ```js 66 | throw new teapot.InternalServerError('Oops! Something went wrong.'); 67 | ``` 68 | 69 | ### Generate an error from a status code 70 | 71 | ```js 72 | teapot.error(500) // returns instance of InternalServerError 73 | teapot.error(204) // throws error because 204 is not an error code 74 | 75 | teapot.error(404, 'My custom message', { // custom message w/ misc. additional properties 76 | expose: true, 77 | data: { 78 | misc: 'blah', 79 | }, 80 | }) 81 | ``` 82 | 83 | ## Contributing 84 | 85 | See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. 86 | 87 | ## License 88 | 89 | MIT License. See [LICENSE](LICENSE) file for details. 90 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ '@commitlint/config-conventional' ], 3 | }; 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverageFrom: [ 3 | 'src/**/*', 4 | ], 5 | coverageThreshold: { 6 | global: { 7 | branches: 50, 8 | functions: 95, 9 | lines: 95, 10 | statements: 95, 11 | }, 12 | }, 13 | testMatch: [ 14 | '**/*.spec.ts', 15 | ], 16 | preset: 'ts-jest', 17 | testEnvironment: 'node', 18 | globals: { 19 | 'ts-jest': { 20 | diagnostics: false, 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-teapot", 3 | "version": "3.1.1", 4 | "description": "Utilities for working with HTTP status codes, errors, and more", 5 | "author": { 6 | "name": "Ty-Lucas Kelley", 7 | "email": "ty@greylocklabs.com", 8 | "url": "https://greylocklabs.com" 9 | }, 10 | "homepage": "https://github.com/greylocklabs/teapot#readme", 11 | "bugs": "https://github.com/greylocklabs/teapot/issues", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/greylocklabs/teapot" 15 | }, 16 | "license": "MIT", 17 | "keywords": [ 18 | "http", 19 | "rest", 20 | "api", 21 | "error", 22 | "status", 23 | "koa", 24 | "express" 25 | ], 26 | "files": [ 27 | "dist" 28 | ], 29 | "source": "src/index.ts", 30 | "main": "dist/index.js", 31 | "module": "dist/index.mjs", 32 | "umd:main": "dist/index.umd.js", 33 | "types": "dist/index.d.ts", 34 | "scripts": { 35 | "clean": "rm -rf dist node_modules", 36 | "postclean": "yarn install", 37 | "prebuild": "rm -rf dist", 38 | "build": "microbundle --external http --no-compress --target node", 39 | "lint": "yarn lint:eslint && yarn lint:markdownlint", 40 | "lint:eslint": "eslint --ignore-path .gitignore --ext .js,.jsx,.ts,.tsx src", 41 | "lint:markdownlint": "markdownlint README.md CHANGELOG.md .github", 42 | "test": "jest", 43 | "test:coverage": "jest --coverage && cat coverage/lcov.info | coveralls", 44 | "prepublishOnly": "yarn build && yarn test" 45 | }, 46 | "engines": { 47 | "node": ">= 8" 48 | }, 49 | "husky": { 50 | "hooks": { 51 | "pre-commit": "yarn test", 52 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 53 | } 54 | }, 55 | "devDependencies": { 56 | "@babel/cli": "^7.8.4", 57 | "@babel/core": "^7.8.4", 58 | "@babel/preset-env": "^7.8.4", 59 | "@commitlint/cli": "^8.3.5", 60 | "@commitlint/config-conventional": "^8.3.4", 61 | "@greylocklabs/eslint-config": "3.1.2", 62 | "@types/express": "^4.17.2", 63 | "@types/jest": "^25.1.3", 64 | "@types/koa": "^2.11.1", 65 | "@types/koa-router": "^7.4.0", 66 | "@types/supertest": "^2.0.8", 67 | "@typescript-eslint/eslint-plugin": "^2.20.0", 68 | "@typescript-eslint/parser": "^2.20.0", 69 | "@vue/eslint-config-typescript": "^5.0.1", 70 | "coveralls": "^3.0.9", 71 | "eslint": "^6.8.0", 72 | "eslint-plugin-import": "^2.20.1", 73 | "eslint-plugin-jest": "^23.8.0", 74 | "eslint-plugin-promise": "^4.2.1", 75 | "eslint-plugin-react": "^7.18.3", 76 | "eslint-plugin-security": "^1.4.0", 77 | "eslint-plugin-vue": "^6.2.1", 78 | "express": "^4.17.1", 79 | "husky": "^4.2.3", 80 | "jest": "^25.1.0", 81 | "koa": "^2.11.0", 82 | "koa-router": "^8.0.8", 83 | "markdownlint-cli": "^0.22.0", 84 | "microbundle": "^0.11.0", 85 | "shelljs": "0.8.3", 86 | "supertest": "^4.0.2", 87 | "ts-jest": "^25.2.1", 88 | "typescript": "^3.8.2" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/errors/ClientError.ts: -------------------------------------------------------------------------------- 1 | import status from '../status'; 2 | 3 | import HTTPError, { ErrorOptions } from './HTTPError'; 4 | 5 | class ClientError extends HTTPError { 6 | constructor(code: number, message: string, options: ErrorOptions = { expose: true }) { 7 | super(code, message, options); 8 | 9 | if (!status.isClientError(code)) throw new Error(`Invalid status code ${code} - must be 4xx`); 10 | } 11 | } 12 | 13 | export default ClientError; 14 | -------------------------------------------------------------------------------- /src/errors/HTTPError.ts: -------------------------------------------------------------------------------- 1 | import status from '../status'; 2 | 3 | class HTTPError extends Error { 4 | name: string; 5 | 6 | code: number; 7 | 8 | status: number; 9 | 10 | expose: boolean; 11 | 12 | data?: any; 13 | 14 | reservedKeys: string[] = [ 'expose', 'status', 'statusCode', 'name', 'message' ]; 15 | 16 | constructor(code: number, message: string, options: ErrorOptions = { expose: false }) { 17 | super(message); 18 | this.name = this.constructor.name; 19 | 20 | if (typeof Error.captureStackTrace === 'function') { 21 | Error.captureStackTrace(this, this.constructor); 22 | } 23 | 24 | this.code = code; 25 | this.status = code; 26 | this.expose = options.expose; 27 | 28 | if (!status.isError(code)) throw new Error(`Invalid status code ${code} = must be 4xx or 5xx`); 29 | 30 | if (options.data) this.data = options.data; 31 | } 32 | } 33 | 34 | export interface ErrorOptions { 35 | expose: boolean; 36 | data?: any; 37 | } 38 | 39 | export default HTTPError; 40 | -------------------------------------------------------------------------------- /src/errors/ServerError.ts: -------------------------------------------------------------------------------- 1 | import status from '../status'; 2 | 3 | import HTTPError, { ErrorOptions } from './HTTPError'; 4 | 5 | class ServerError extends HTTPError { 6 | constructor(code: number, message: string, options: ErrorOptions) { 7 | super(code, message, options); 8 | 9 | if (!status.isServerError(code)) throw new Error(`Invalid status code ${code} - must be 5xx`); 10 | } 11 | } 12 | 13 | export default ServerError; 14 | -------------------------------------------------------------------------------- /src/errors/client/BadRequestError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class BadRequestError extends ClientError { 6 | constructor(message: string = status.codes[status.BAD_REQUEST], options: ErrorOptions = { expose: true }) { 7 | super(status.BAD_REQUEST, message, options); 8 | } 9 | } 10 | 11 | export default BadRequestError; 12 | -------------------------------------------------------------------------------- /src/errors/client/ConflictError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class ConflictError extends ClientError { 6 | constructor(message: string = status.codes[status.CONFLICT], options: ErrorOptions = { expose: true }) { 7 | super(status.CONFLICT, message, options); 8 | } 9 | } 10 | 11 | export default ConflictError; 12 | -------------------------------------------------------------------------------- /src/errors/client/ExpectationFailedError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class ExpectationFailedError extends ClientError { 6 | constructor(message: string = status.codes[status.EXPECTATION_FAILED], options: ErrorOptions = { expose: true }) { 7 | super(status.EXPECTATION_FAILED, message, options); 8 | } 9 | } 10 | 11 | export default ExpectationFailedError; 12 | -------------------------------------------------------------------------------- /src/errors/client/FailedDependencyError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class FailedDependencyError extends ClientError { 6 | constructor(message: string = status.codes[status.FAILED_DEPENDENCY], options: ErrorOptions = { expose: true }) { 7 | super(status.FAILED_DEPENDENCY, message, options); 8 | } 9 | } 10 | 11 | export default FailedDependencyError; 12 | -------------------------------------------------------------------------------- /src/errors/client/ForbiddenError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class ForbiddenError extends ClientError { 6 | constructor(message: string = status.codes[status.FORBIDDEN], options: ErrorOptions = { expose: true }) { 7 | super(status.FORBIDDEN, message, options); 8 | } 9 | } 10 | 11 | export default ForbiddenError; 12 | -------------------------------------------------------------------------------- /src/errors/client/GoneError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class GoneError extends ClientError { 6 | constructor(message: string = status.codes[status.GONE], options: ErrorOptions = { expose: true }) { 7 | super(status.GONE, message, options); 8 | } 9 | } 10 | 11 | export default GoneError; 12 | -------------------------------------------------------------------------------- /src/errors/client/ImATeapotError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class ImATeapotError extends ClientError { 6 | constructor(message: string = status.codes[status.IM_A_TEAPOT], options: ErrorOptions = { expose: true }) { 7 | super(status.IM_A_TEAPOT, message, options); 8 | } 9 | } 10 | 11 | export default ImATeapotError; 12 | -------------------------------------------------------------------------------- /src/errors/client/LengthRequiredError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class LengthRequiredError extends ClientError { 6 | constructor(message: string = status.codes[status.LENGTH_REQUIRED], options: ErrorOptions = { expose: true }) { 7 | super(status.LENGTH_REQUIRED, message, options); 8 | } 9 | } 10 | 11 | export default LengthRequiredError; 12 | -------------------------------------------------------------------------------- /src/errors/client/LockedError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class LockedError extends ClientError { 6 | constructor(message: string = status.codes[status.LOCKED], options: ErrorOptions = { expose: true }) { 7 | super(status.LOCKED, message, options); 8 | } 9 | } 10 | 11 | export default LockedError; 12 | -------------------------------------------------------------------------------- /src/errors/client/MethodNotAllowedError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class MethodNotAllowedError extends ClientError { 6 | constructor(message: string = status.codes[status.METHOD_NOT_ALLOWED], options: ErrorOptions = { expose: true }) { 7 | super(status.METHOD_NOT_ALLOWED, message, options); 8 | } 9 | } 10 | 11 | export default MethodNotAllowedError; 12 | -------------------------------------------------------------------------------- /src/errors/client/MisdirectedRequestError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class MisdirectedRequestError extends ClientError { 6 | constructor(message: string = status.codes[status.MISDIRECTED_REQUEST], options: ErrorOptions = { expose: true }) { 7 | super(status.MISDIRECTED_REQUEST, message, options); 8 | } 9 | } 10 | 11 | export default MisdirectedRequestError; 12 | -------------------------------------------------------------------------------- /src/errors/client/NotAcceptableError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class NotAcceptableError extends ClientError { 6 | constructor(message: string = status.codes[status.NOT_ACCEPTABLE], options: ErrorOptions = { expose: true }) { 7 | super(status.NOT_ACCEPTABLE, message, options); 8 | } 9 | } 10 | 11 | export default NotAcceptableError; 12 | -------------------------------------------------------------------------------- /src/errors/client/NotFoundError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class NotFoundError extends ClientError { 6 | constructor(message: string = status.codes[status.NOT_FOUND], options: ErrorOptions = { expose: true }) { 7 | super(status.NOT_FOUND, message, options); 8 | } 9 | } 10 | 11 | export default NotFoundError; 12 | -------------------------------------------------------------------------------- /src/errors/client/PayloadTooLargeError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class PayloadTooLargeError extends ClientError { 6 | constructor(message: string = status.codes[status.PAYLOAD_TOO_LARGE], options: ErrorOptions = { expose: true }) { 7 | super(status.PAYLOAD_TOO_LARGE, message, options); 8 | } 9 | } 10 | 11 | export default PayloadTooLargeError; 12 | -------------------------------------------------------------------------------- /src/errors/client/PaymentRequiredError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class PaymentRequiredError extends ClientError { 6 | constructor(message: string = status.codes[status.PAYMENT_REQUIRED], options: ErrorOptions = { expose: true }) { 7 | super(status.PAYMENT_REQUIRED, message, options); 8 | } 9 | } 10 | 11 | export default PaymentRequiredError; 12 | -------------------------------------------------------------------------------- /src/errors/client/PreconditionFailedError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class PreconditionFailedError extends ClientError { 6 | constructor(message: string = status.codes[status.PRECONDITION_FAILED], options: ErrorOptions = { expose: true }) { 7 | super(status.PRECONDITION_FAILED, message, options); 8 | } 9 | } 10 | 11 | export default PreconditionFailedError; 12 | -------------------------------------------------------------------------------- /src/errors/client/PreconditionRequiredError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class PreconditionRequiredError extends ClientError { 6 | constructor(message: string = status.codes[status.PRECONDITION_REQUIRED], options: ErrorOptions = { expose: true }) { 7 | super(status.PRECONDITION_REQUIRED, message, options); 8 | } 9 | } 10 | 11 | export default PreconditionRequiredError; 12 | -------------------------------------------------------------------------------- /src/errors/client/ProxyAuthenticationRequiredError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class ProxyAuthenticationRequiredError extends ClientError { 6 | constructor( 7 | message: string = status.codes[status.PROXY_AUTHENTICATION_REQUIRED], 8 | options: ErrorOptions = { expose: true }, 9 | ) { 10 | super(status.PROXY_AUTHENTICATION_REQUIRED, message, options); 11 | } 12 | } 13 | 14 | export default ProxyAuthenticationRequiredError; 15 | -------------------------------------------------------------------------------- /src/errors/client/RangeNotSatisfiableError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class RangeNotSatisfiableError extends ClientError { 6 | constructor(message: string = status.codes[status.RANGE_NOT_SATISFIABLE], options: ErrorOptions = { expose: true }) { 7 | super(status.RANGE_NOT_SATISFIABLE, message, options); 8 | } 9 | } 10 | 11 | export default RangeNotSatisfiableError; 12 | -------------------------------------------------------------------------------- /src/errors/client/RequestHeaderFieldsTooLargeError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class RequestHeaderFieldsTooLargeError extends ClientError { 6 | constructor( 7 | message: string = status.codes[status.REQUEST_HEADER_FIELDS_TOO_LARGE], 8 | options: ErrorOptions = { expose: true }, 9 | ) { 10 | super(status.REQUEST_HEADER_FIELDS_TOO_LARGE, message, options); 11 | } 12 | } 13 | 14 | export default RequestHeaderFieldsTooLargeError; 15 | -------------------------------------------------------------------------------- /src/errors/client/RequestTimeoutError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class RequestTimeoutError extends ClientError { 6 | constructor(message: string = status.codes[status.REQUEST_TIMEOUT], options: ErrorOptions = { expose: true }) { 7 | super(status.REQUEST_TIMEOUT, message, options); 8 | } 9 | } 10 | 11 | export default RequestTimeoutError; 12 | -------------------------------------------------------------------------------- /src/errors/client/TooManyRequestsError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class TooManyRequestsError extends ClientError { 6 | constructor(message: string = status.codes[status.TOO_MANY_REQUESTS], options: ErrorOptions = { expose: true }) { 7 | super(status.TOO_MANY_REQUESTS, message, options); 8 | } 9 | } 10 | 11 | export default TooManyRequestsError; 12 | -------------------------------------------------------------------------------- /src/errors/client/URITooLongError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class URITooLargeError extends ClientError { 6 | constructor(message: string = status.codes[status.URI_TOO_LONG], options: ErrorOptions = { expose: true }) { 7 | super(status.URI_TOO_LONG, message, options); 8 | } 9 | } 10 | 11 | export default URITooLargeError; 12 | -------------------------------------------------------------------------------- /src/errors/client/UnauthorizedError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class UnauthorizedError extends ClientError { 6 | constructor(message: string = status.codes[status.UNAUTHORIZED], options: ErrorOptions = { expose: true }) { 7 | super(status.UNAUTHORIZED, message, options); 8 | } 9 | } 10 | 11 | export default UnauthorizedError; 12 | -------------------------------------------------------------------------------- /src/errors/client/UnavailableForLegalReasonsError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class UnavailableForLegalReasonsError extends ClientError { 6 | constructor( 7 | message: string = status.codes[status.UNAVAILABLE_FOR_LEGAL_REASONS], 8 | options: ErrorOptions = { expose: true }, 9 | ) { 10 | super(status.UNAVAILABLE_FOR_LEGAL_REASONS, message, options); 11 | } 12 | } 13 | 14 | export default UnavailableForLegalReasonsError; 15 | -------------------------------------------------------------------------------- /src/errors/client/UnorderedCollectionError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class UnorderedCollectionError extends ClientError { 6 | constructor(message: string = status.codes[status.UNORDERED_COLLECTION], options: ErrorOptions = { expose: true }) { 7 | super(status.UNORDERED_COLLECTION, message, options); 8 | } 9 | } 10 | 11 | export default UnorderedCollectionError; 12 | -------------------------------------------------------------------------------- /src/errors/client/UnprocessableEntityError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class UnprocessableEntityError extends ClientError { 6 | constructor(message: string = status.codes[status.UNPROCESSABLE_ENTITY], options: ErrorOptions = { expose: true }) { 7 | super(status.UNPROCESSABLE_ENTITY, message, options); 8 | } 9 | } 10 | 11 | export default UnprocessableEntityError; 12 | -------------------------------------------------------------------------------- /src/errors/client/UnsupportedMediaTypeError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class UnsupportedMediaTypeError extends ClientError { 6 | constructor(message: string = status.codes[status.UNSUPPORTED_MEDIA_TYPE], options: ErrorOptions = { expose: true }) { 7 | super(status.UNSUPPORTED_MEDIA_TYPE, message, options); 8 | } 9 | } 10 | 11 | export default UnsupportedMediaTypeError; 12 | -------------------------------------------------------------------------------- /src/errors/client/UpgradeRequiredError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ClientError from '../ClientError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class UpgradeRequiredError extends ClientError { 6 | constructor(message: string = status.codes[status.UPGRADE_REQUIRED], options: ErrorOptions = { expose: true }) { 7 | super(status.UPGRADE_REQUIRED, message, options); 8 | } 9 | } 10 | 11 | export default UpgradeRequiredError; 12 | -------------------------------------------------------------------------------- /src/errors/client/index.ts: -------------------------------------------------------------------------------- 1 | export { default as BadRequestError } from './BadRequestError'; 2 | export { default as UnauthorizedError } from './UnauthorizedError'; 3 | export { default as PaymentRequiredError } from './PaymentRequiredError'; 4 | export { default as ForbiddenError } from './ForbiddenError'; 5 | export { default as NotFoundError } from './NotFoundError'; 6 | export { default as MethodNotAllowedError } from './MethodNotAllowedError'; 7 | export { default as NotAcceptableError } from './NotAcceptableError'; 8 | export { default as ProxyAuthenticationRequiredError } from './ProxyAuthenticationRequiredError'; 9 | export { default as RequestTimeoutError } from './RequestTimeoutError'; 10 | export { default as ConflictError } from './ConflictError'; 11 | export { default as GoneError } from './GoneError'; 12 | export { default as LengthRequiredError } from './LengthRequiredError'; 13 | export { default as PreconditionFailedError } from './PreconditionFailedError'; 14 | export { default as PayloadTooLargeError } from './PayloadTooLargeError'; 15 | export { default as URITooLongError } from './URITooLongError'; 16 | export { default as UnsupportedMediaTypeError } from './UnsupportedMediaTypeError'; 17 | export { default as RangeNotSatisfiableError } from './RangeNotSatisfiableError'; 18 | export { default as ExpectationFailedError } from './ExpectationFailedError'; 19 | export { default as ImATeapotError } from './ImATeapotError'; 20 | export { default as MisdirectedRequestError } from './MisdirectedRequestError'; 21 | export { default as UnprocessableEntityError } from './UnprocessableEntityError'; 22 | export { default as LockedError } from './LockedError'; 23 | export { default as FailedDependencyError } from './FailedDependencyError'; 24 | export { default as UnorderedCollectionError } from './UnorderedCollectionError'; 25 | export { default as UpgradeRequiredError } from './UpgradeRequiredError'; 26 | export { default as PreconditionRequiredError } from './PreconditionRequiredError'; 27 | export { default as TooManyRequestsError } from './TooManyRequestsError'; 28 | export { default as RequestHeaderFieldsTooLargeError } from './RequestHeaderFieldsTooLargeError'; 29 | export { default as UnavailableForLegalReasonsError } from './UnavailableForLegalReasonsError'; 30 | -------------------------------------------------------------------------------- /src/errors/index.ts: -------------------------------------------------------------------------------- 1 | export { default as HTTPError } from './HTTPError'; 2 | 3 | export { default as ClientError } from './ClientError'; 4 | export { default as ServerError } from './ServerError'; 5 | 6 | export * from './client'; 7 | export * from './server'; 8 | -------------------------------------------------------------------------------- /src/errors/server/BadGatewayError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ServerError from '../ServerError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class BadGatewayError extends ServerError { 6 | constructor(message: string = status.codes[status.BAD_GATEWAY], options: ErrorOptions = { expose: false }) { 7 | super(status.BAD_GATEWAY, message, options); 8 | } 9 | } 10 | 11 | export default BadGatewayError; 12 | -------------------------------------------------------------------------------- /src/errors/server/BandwidthLimitExceededError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ServerError from '../ServerError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class BandwidthLimitExceededError extends ServerError { 6 | constructor( 7 | message: string = status.codes[status.BANDWIDTH_LIMIT_EXCEEDED], 8 | options: ErrorOptions = { expose: false }, 9 | ) { 10 | super(status.BANDWIDTH_LIMIT_EXCEEDED, message, options); 11 | } 12 | } 13 | 14 | export default BandwidthLimitExceededError; 15 | -------------------------------------------------------------------------------- /src/errors/server/GatewayTimeoutError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ServerError from '../ServerError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class GatewayTimeoutError extends ServerError { 6 | constructor(message: string = status.codes[status.GATEWAY_TIMEOUT], options: ErrorOptions = { expose: false }) { 7 | super(status.GATEWAY_TIMEOUT, message, options); 8 | } 9 | } 10 | 11 | export default GatewayTimeoutError; 12 | -------------------------------------------------------------------------------- /src/errors/server/HTTPVersionNotSupportedError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ServerError from '../ServerError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class HTTPVersionNotSupportedError extends ServerError { 6 | constructor( 7 | message: string = status.codes[status.HTTP_VERSION_NOT_SUPPORTED], 8 | options: ErrorOptions = { expose: false }, 9 | ) { 10 | super(status.HTTP_VERSION_NOT_SUPPORTED, message, options); 11 | } 12 | } 13 | 14 | export default HTTPVersionNotSupportedError; 15 | -------------------------------------------------------------------------------- /src/errors/server/InsufficientStorageError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ServerError from '../ServerError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class InsufficientStorageError extends ServerError { 6 | constructor(message: string = status.codes[status.INSUFFICIENT_STORAGE], options: ErrorOptions = { expose: false }) { 7 | super(status.INSUFFICIENT_STORAGE, message, options); 8 | } 9 | } 10 | 11 | export default InsufficientStorageError; 12 | -------------------------------------------------------------------------------- /src/errors/server/InternalServerError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ServerError from '../ServerError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class InternalServerError extends ServerError { 6 | constructor(message: string = status.codes[status.INTERNAL_SERVER_ERROR], options: ErrorOptions = { expose: false }) { 7 | super(status.INTERNAL_SERVER_ERROR, message, options); 8 | } 9 | } 10 | 11 | export default InternalServerError; 12 | -------------------------------------------------------------------------------- /src/errors/server/LoopDetectedError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ServerError from '../ServerError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class LoopDetectedError extends ServerError { 6 | constructor(message: string = status.codes[status.LOOP_DETECTED], options: ErrorOptions = { expose: false }) { 7 | super(status.LOOP_DETECTED, message, options); 8 | } 9 | } 10 | 11 | export default LoopDetectedError; 12 | -------------------------------------------------------------------------------- /src/errors/server/NetworkAuthenticationRequiredError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ServerError from '../ServerError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class NetworkAuthenticationRequiredError extends ServerError { 6 | constructor( 7 | message: string = status.codes[status.NETWORK_AUTHENTICATION_REQUIRED], 8 | options: ErrorOptions = { expose: false }, 9 | ) { 10 | super(status.NETWORK_AUTHENTICATION_REQUIRED, message, options); 11 | } 12 | } 13 | 14 | export default NetworkAuthenticationRequiredError; 15 | -------------------------------------------------------------------------------- /src/errors/server/NotExtendedError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ServerError from '../ServerError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class NotExtendedError extends ServerError { 6 | constructor(message: string = status.codes[status.NOT_EXTENDED], options: ErrorOptions = { expose: false }) { 7 | super(status.NOT_EXTENDED, message, options); 8 | } 9 | } 10 | 11 | export default NotExtendedError; 12 | -------------------------------------------------------------------------------- /src/errors/server/NotImplementedError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ServerError from '../ServerError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class NotImplementedError extends ServerError { 6 | constructor(message: string = status.codes[status.NOT_IMPLEMENTED], options: ErrorOptions = { expose: false }) { 7 | super(status.NOT_IMPLEMENTED, message, options); 8 | } 9 | } 10 | 11 | export default NotImplementedError; 12 | -------------------------------------------------------------------------------- /src/errors/server/ServiceUnavailableError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ServerError from '../ServerError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class ServiceUnavailableError extends ServerError { 6 | constructor(message: string = status.codes[status.SERVICE_UNAVAILABLE], options: ErrorOptions = { expose: false }) { 7 | super(status.SERVICE_UNAVAILABLE, message, options); 8 | } 9 | } 10 | 11 | export default ServiceUnavailableError; 12 | -------------------------------------------------------------------------------- /src/errors/server/VariantAlsoNegotiatesError.ts: -------------------------------------------------------------------------------- 1 | import status from '../../status'; 2 | import ServerError from '../ServerError'; 3 | import { ErrorOptions } from '../HTTPError'; 4 | 5 | class VariantAlsoNegotiatesError extends ServerError { 6 | constructor( 7 | message: string = status.codes[status.VARIANT_ALSO_NEGOTIATES], 8 | options: ErrorOptions = { expose: false }, 9 | ) { 10 | super(status.VARIANT_ALSO_NEGOTIATES, message, options); 11 | } 12 | } 13 | 14 | export default VariantAlsoNegotiatesError; 15 | -------------------------------------------------------------------------------- /src/errors/server/index.ts: -------------------------------------------------------------------------------- 1 | export { default as InternalServerError } from './InternalServerError'; 2 | export { default as NotImplementedError } from './NotImplementedError'; 3 | export { default as BadGatewayError } from './BadGatewayError'; 4 | export { default as ServiceUnavailableError } from './ServiceUnavailableError'; 5 | export { default as GatewayTimeoutError } from './GatewayTimeoutError'; 6 | export { default as HTTPVersionNotSupportedError } from './HTTPVersionNotSupportedError'; 7 | export { default as VariantAlsoNegotiatesError } from './VariantAlsoNegotiatesError'; 8 | export { default as InsufficientStorageError } from './InsufficientStorageError'; 9 | export { default as LoopDetectedError } from './LoopDetectedError'; 10 | export { default as BandwidthLimitExceededError } from './BandwidthLimitExceededError'; 11 | export { default as NotExtendedError } from './NotExtendedError'; 12 | export { default as NetworkAuthenticationRequiredError } from './NetworkAuthenticationRequiredError'; 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import status, { Status } from './status'; 2 | import * as errors from './errors'; 3 | import { ErrorOptions } from './errors/HTTPError'; 4 | 5 | const map = new Map(); 6 | 7 | for (const error in errors) { 8 | const Err = (errors as any)[error]; 9 | const proto = Err.prototype; 10 | 11 | if (proto instanceof errors.ClientError || proto instanceof errors.ServerError) { 12 | const err = new Err(); 13 | map.set(err.code, Err); 14 | } 15 | } 16 | 17 | const teapot = { 18 | ...errors, 19 | status: status as Status, 20 | error: (code: string | number, message?: string, options?: ErrorOptions): errors.HTTPError => { 21 | if (!status.isError(code)) throw new Error(`Invalid status code ${code} - must be 4xx or 5xx`); 22 | 23 | const ErrorClass = map.get(status.code(code)); 24 | return new ErrorClass(message, options); 25 | }, 26 | }; 27 | 28 | export default teapot; 29 | -------------------------------------------------------------------------------- /src/status/index.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | 3 | enum StatusType { 4 | INFO = '1', 5 | SUCCESS = '2', 6 | REDIRECT = '3', 7 | CLIENTERROR = '4', 8 | SERVERERROR = '5', 9 | } 10 | 11 | export type StatusCode = string | number; 12 | 13 | export interface StatusCodeMap { 14 | [key: number]: string; 15 | [key: string]: StatusCode; 16 | } 17 | 18 | export class Status { 19 | CONTINUE = 100; 20 | 21 | SWITCHING_PROTOCOLS = 101; 22 | 23 | PROCESSING = 102; 24 | 25 | EARLY_HINTS = 103; 26 | 27 | OK = 200; 28 | 29 | CREATED = 201; 30 | 31 | ACCEPTED = 202; 32 | 33 | NON_AUTHORITATIVE_INFORMATION = 203; 34 | 35 | NO_CONTENT = 204; 36 | 37 | RESET_CONTENT = 205; 38 | 39 | PARTIAL_CONTENT = 206; 40 | 41 | MULTI_STATUS= 207; 42 | 43 | ALREADY_REPORTED = 208; 44 | 45 | IM_USED = 226; 46 | 47 | MULTIPLE_CHOICES = 300; 48 | 49 | MOVED_PERMANENTLY = 301; 50 | 51 | FOUND = 302; 52 | 53 | SEE_OTHER = 303; 54 | 55 | NOT_MODIFIED = 304; 56 | 57 | USE_PROXY = 305; 58 | 59 | TEMPORARY_REDIRECT = 307; 60 | 61 | PERMANENT_REDIRECT = 308; 62 | 63 | BAD_REQUEST = 400; 64 | 65 | UNAUTHORIZED = 401; 66 | 67 | PAYMENT_REQUIRED = 402; 68 | 69 | FORBIDDEN = 403; 70 | 71 | NOT_FOUND = 404; 72 | 73 | METHOD_NOT_ALLOWED = 405; 74 | 75 | NOT_ACCEPTABLE = 406; 76 | 77 | PROXY_AUTHENTICATION_REQUIRED = 407; 78 | 79 | REQUEST_TIMEOUT = 408; 80 | 81 | CONFLICT = 409; 82 | 83 | GONE = 410; 84 | 85 | LENGTH_REQUIRED = 411; 86 | 87 | PRECONDITION_FAILED = 412; 88 | 89 | PAYLOAD_TOO_LARGE = 413; 90 | 91 | URI_TOO_LONG = 414; 92 | 93 | UNSUPPORTED_MEDIA_TYPE = 415; 94 | 95 | RANGE_NOT_SATISFIABLE = 416; 96 | 97 | EXPECTATION_FAILED = 417; 98 | 99 | IM_A_TEAPOT = 418; 100 | 101 | MISDIRECTED_REQUEST = 421; 102 | 103 | UNPROCESSABLE_ENTITY = 422; 104 | 105 | LOCKED = 423; 106 | 107 | FAILED_DEPENDENCY = 424; 108 | 109 | UNORDERED_COLLECTION = 425; 110 | 111 | UPGRADE_REQUIRED = 426; 112 | 113 | PRECONDITION_REQUIRED = 428; 114 | 115 | TOO_MANY_REQUESTS = 429; 116 | 117 | REQUEST_HEADER_FIELDS_TOO_LARGE = 431; 118 | 119 | UNAVAILABLE_FOR_LEGAL_REASONS = 451; 120 | 121 | INTERNAL_SERVER_ERROR = 500; 122 | 123 | NOT_IMPLEMENTED = 501; 124 | 125 | BAD_GATEWAY = 502; 126 | 127 | SERVICE_UNAVAILABLE = 503; 128 | 129 | GATEWAY_TIMEOUT = 504; 130 | 131 | HTTP_VERSION_NOT_SUPPORTED = 505; 132 | 133 | VARIANT_ALSO_NEGOTIATES = 506; 134 | 135 | INSUFFICIENT_STORAGE = 507; 136 | 137 | LOOP_DETECTED = 508; 138 | 139 | BANDWIDTH_LIMIT_EXCEEDED = 509; 140 | 141 | NOT_EXTENDED = 510; 142 | 143 | NETWORK_AUTHENTICATION_REQUIRED = 511; 144 | 145 | STATUS_CODES = http.STATUS_CODES; 146 | 147 | codes: StatusCodeMap; 148 | 149 | statusCodes: number[]; 150 | 151 | constructor() { 152 | const map = {}; 153 | 154 | this.statusCodes = ((obj: StatusCodeMap): number[] => { 155 | const list: number[] = []; 156 | 157 | Object.keys(http.STATUS_CODES).forEach((code: string): void => { 158 | const message = http.STATUS_CODES[code]; 159 | const num = Number.parseInt(code); 160 | 161 | if (message) { 162 | obj[message] = num; 163 | obj[message.toLowerCase()] = num; 164 | obj[message.toUpperCase()] = num; 165 | obj[num] = message; 166 | obj[code] = message; 167 | } 168 | 169 | list.push(num); 170 | }); 171 | 172 | return list; 173 | })(map); 174 | 175 | this.codes = map; 176 | } 177 | 178 | code(input: StatusCode): StatusCode { 179 | if (typeof input === 'number') { 180 | if (this.codes[input]) return input; 181 | 182 | throw new Error(`Invalid status code ${input}`); 183 | } else if (typeof input === 'string') { 184 | if (isNaN(Number.parseInt(input))) { 185 | const code = this.codes[input.toLowerCase()]; 186 | 187 | if (code) return code; 188 | 189 | throw new Error(`Invalid status message ${input}`); 190 | } 191 | 192 | return this.code(Number.parseInt(input)); 193 | } 194 | 195 | throw new Error('Input must be a number or string'); 196 | } 197 | 198 | isInfo(input: StatusCode): boolean { 199 | return this.is(StatusType.INFO, input); 200 | } 201 | 202 | isSuccess(input: StatusCode): boolean { 203 | return this.is(StatusType.SUCCESS, input); 204 | } 205 | 206 | isRedirect(input: StatusCode): boolean { 207 | return this.is(StatusType.REDIRECT, input); 208 | } 209 | 210 | isClientError(input: StatusCode): boolean { 211 | return this.is(StatusType.CLIENTERROR, input); 212 | } 213 | 214 | isServerError(input: StatusCode): boolean { 215 | return this.is(StatusType.SERVERERROR, input); 216 | } 217 | 218 | isError(input: StatusCode): boolean { 219 | return this.isClientError(input) || this.isServerError(input); 220 | } 221 | 222 | private is(type: StatusType, input: StatusCode): boolean { 223 | return this.statusCodes.includes(Number.parseInt(input.toString())) && String(input).charAt(0) === type; 224 | } 225 | } 226 | 227 | export default new Status(); 228 | -------------------------------------------------------------------------------- /test/fixtures/express.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express'; 2 | 3 | import teapot from '../../src'; 4 | 5 | const app = express(); 6 | 7 | app.get('/errors/:code', (req: Request, res: Response): void => { 8 | throw teapot.error(req.params.code); 9 | }); 10 | 11 | export default app; 12 | -------------------------------------------------------------------------------- /test/fixtures/koa.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | 3 | import Koa, { Context } from 'koa'; 4 | import Router from 'koa-router'; 5 | 6 | import teapot from '../../src'; 7 | 8 | const app = new Koa(); 9 | const router = new Router(); 10 | 11 | router.get('/errors/:code', (ctx: Context): void => { 12 | throw teapot.error(ctx.params.code); 13 | }); 14 | 15 | app.use(router.routes()); 16 | 17 | const server = http.createServer(app.callback()); 18 | 19 | export default server; 20 | -------------------------------------------------------------------------------- /test/functional/express.spec.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | 3 | import teapot from '../../src'; 4 | import app from '../fixtures/express'; 5 | import { errorClassNames } from '../utils'; 6 | 7 | describe('express server', () => { 8 | errorClassNames().forEach((className: string): void => { 9 | it('gets the correct set of codes and messages from each error', async () => { 10 | const { code } = teapot[className](); 11 | 12 | const res = await request(app).get(`/errors/${code}`); 13 | 14 | expect(res.status).toBe(code); 15 | expect(res.status).toBe(res.statusCode); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/functional/koa.spec.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | 3 | import teapot from '../../src'; 4 | import app from '../fixtures/koa'; 5 | import { errorClassNames } from '../utils'; 6 | 7 | describe('koa server', () => { 8 | errorClassNames().forEach((className: string): void => { 9 | it('gets the correct set of codes and messages from each error', async () => { 10 | const { code } = teapot[className](); 11 | 12 | const res = await request(app).get(`/errors/${code}`); 13 | 14 | expect(res.status).toBe(code); 15 | expect(res.status).toBe(res.statusCode); 16 | expect(res.error.text).toBe(new teapot[className]().message); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/unit/error.spec.ts: -------------------------------------------------------------------------------- 1 | import teapot from '../../src'; 2 | import { randomErrorCode } from '../utils'; 3 | 4 | describe('teapot.error', () => { 5 | let errorCode: number; 6 | 7 | beforeEach(() => { 8 | errorCode = randomErrorCode(); 9 | }); 10 | 11 | it('creates an error from a status code', () => { 12 | const err = teapot.error(errorCode); 13 | 14 | expect(err.code).toBe(errorCode); 15 | expect(err.status).toBe(errorCode); 16 | expect(err.message).toBe(teapot.status.codes[errorCode]); 17 | }); 18 | 19 | it('creates an error with a custom message', () => { 20 | const err = teapot.error(errorCode, 'Oops'); 21 | 22 | expect(err.message).toBe('Oops'); 23 | }); 24 | 25 | it('creates an error with custom data', () => { 26 | const err = teapot.error(errorCode, 'My message', { 27 | expose: false, 28 | data: { success: false }, 29 | }); 30 | 31 | expect(err.data).toMatchObject({ success: false }); 32 | }); 33 | 34 | it('creates the correct HTTPError subclass (ClientError or ServerError) for all valid status codes', () => { 35 | let err; 36 | 37 | teapot.status.statusCodes.forEach((code: number): void => { 38 | if (teapot.status.isError(code)) { 39 | err = teapot.error(code); 40 | 41 | if (code.toString().charAt(0) === '4') { 42 | expect(err.expose).toBe(true); 43 | } else { 44 | expect(err.expose).toBe(false); 45 | } 46 | } 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/unit/status.spec.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | 3 | import teapot from '../../src'; 4 | 5 | describe('teapot.status', () => { 6 | it('provides the correct status code on the status.codes object', () => { 7 | Object.values(http.STATUS_CODES).forEach((msg: string): void => { 8 | const key = msg 9 | .replace(/\s|-/gu, '_') 10 | .replace('\'', '') 11 | .toUpperCase(); 12 | 13 | expect(Number.isInteger(teapot.status[key])).toBe(true); 14 | }); 15 | }); 16 | 17 | it('contains the http.STATUS_CODES object from the Node.js standard library', () => { 18 | expect(teapot.status.STATUS_CODES).toStrictEqual(http.STATUS_CODES); 19 | }); 20 | 21 | describe('invalid status codes', () => { 22 | const codes = [ 600, '108', 'Hello', 'Not foundd', 33 ]; 23 | 24 | for (const invalidCode of codes) { 25 | it('throws an error when the code() method is not provided a valid status code', () => { 26 | const fn = (): void => { 27 | teapot.status.code(invalidCode); 28 | }; 29 | 30 | expect(fn).toThrow(/Invalid status (code|message) */u); 31 | }); 32 | } 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/utils/index.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | 3 | import teapot from '../../src'; 4 | 5 | export const statusCodes = Object.keys(http.STATUS_CODES).map(Number); 6 | 7 | export const randomErrorCode = (type?: 'client' | 'server'): number => { 8 | let pattern: RegExp; 9 | 10 | switch (type) { 11 | case 'server': 12 | pattern = /5/u; 13 | break; 14 | case 'client': 15 | pattern = /4/u; 16 | break; 17 | default: 18 | pattern = /4|5/u; 19 | } 20 | 21 | const codes = statusCodes.filter((c: number) => pattern.test(c.toString().charAt(0))); 22 | 23 | return codes[Math.floor(Math.random() * codes.length)]; 24 | }; 25 | 26 | export const errorClassNames = (): string[] => { 27 | const formatted = Object.values(http.STATUS_CODES).map((msg: string) => msg.replace(/\s|-|'/gu, '')); 28 | const names = []; 29 | 30 | for (let f of formatted) { 31 | if (f === 'ImaTeapot') f = 'ImATeapot'; 32 | if (!(f.slice(-5) === 'Error')) f = `${f}Error`; 33 | 34 | if (teapot[f]) names.push(f); 35 | } 36 | 37 | return names; 38 | }; 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "module": "esnext", 5 | "target": "es5", 6 | "strict": true, 7 | "jsx": "react", 8 | "emitDecoratorMetadata": true, 9 | "esModuleInterop": true, 10 | "experimentalDecorators": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "importHelpers": true, 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "sourceMap": true 16 | }, 17 | "exclude": [ 18 | "src/__tests__/*", 19 | "test" 20 | ] 21 | } 22 | --------------------------------------------------------------------------------