├── bun.lockb ├── tsconfig.json ├── package.json ├── README.md ├── .gitignore └── index.ts /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yfrans/elysia-http-error/HEAD/bun.lockb -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "module": "esnext", 5 | "target": "esnext", 6 | "moduleResolution": "bundler", 7 | "moduleDetection": "force", 8 | "allowImportingTsExtensions": true, 9 | "noEmit": true, 10 | "composite": true, 11 | "strict": true, 12 | "downlevelIteration": true, 13 | "skipLibCheck": true, 14 | "jsx": "preserve", 15 | "allowSyntheticDefaultImports": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "allowJs": true, 18 | "types": [ 19 | "bun-types" // add Bun global 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elysia-http-error", 3 | "version": "1.3.0", 4 | "description": "Easy http error handling in Elysia.", 5 | "author": { 6 | "name": "Yossi Frances" 7 | }, 8 | "module": "index.ts", 9 | "type": "module", 10 | "devDependencies": { 11 | "bun-types": "latest" 12 | }, 13 | "peerDependencies": { 14 | "typescript": "^5.0.0" 15 | }, 16 | "dependencies": { 17 | "elysia": "latest" 18 | }, 19 | "repository": { 20 | "url": "https://github.com/yfrans/elysia-http-error.git" 21 | }, 22 | "keywords": [ 23 | "bun", 24 | "elysia", 25 | "http", 26 | "error" 27 | ] 28 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elysia HTTP Error 2 | 3 | Throw HTTP errors from Elysia handlers. 4 | 5 | # Usage 6 | 7 | Install the package: 8 | 9 | `bun add elysia-http-error` 10 | 11 | Then use `httpError()` in Elysia pipeline as follow: 12 | 13 | ```ts 14 | const app = new Elysia().use(httpError()); 15 | ``` 16 | 17 | By default, this module will return a structured message with the error details. 18 | 19 | If you need the message to be a simple string, use: 20 | 21 | ```ts 22 | returnStringOnly: true; 23 | ``` 24 | 25 | when initializing `httpError()`. 26 | 27 | Another option is to write a custom error message formatter using: 28 | 29 | ```ts 30 | customFormatter: (err: HttpError) => any; 31 | ``` 32 | 33 | which allows a full control over the returned error object. 34 | 35 | For example: 36 | 37 | ```ts 38 | customFormatter: (err) => `Oh no... we got an error ${err.statusCode}!`; 39 | ``` 40 | 41 | In order to use the HttpError class and throw an error, the `HttpError` decorator must be added to the pipeline: 42 | 43 | ```ts 44 | const app = new Elysia().use(httpErrorDecorator).get("/", ({ HttpError }) => { 45 | throw HttpError.BadRequest("Something went wrong..."); 46 | }); 47 | ``` 48 | 49 | The HttpError constructor receive the following parameters: 50 | 51 | ```ts 52 | error: string (error message, optional) 53 | errorData: any (more error details, optional) 54 | ``` 55 | 56 | For example, when using OTP validation and the user is asking for a new OTP while the OTP is still valid, the error can contain the message and the time left for the OTP so the client can display a proper message and sync the time displayed to the user: 57 | 58 | ```ts 59 | throw HttpError.Conflict("OTP is still valid", { 60 | timeLeft: otpExpireTime - Date.now(), 61 | }); 62 | ``` 63 | 64 | # Some important notes 65 | 66 | The plugin initializer must be called when initializing Elysia and before any other handler that use this plugin. 67 | 68 | When using dependecy injection and sub-modules, the decorator must be used in the sub-module definition in order to get full type support (see https://elysiajs.com/patterns/dependency-injection.html#dependency-injection) 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | 15 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 16 | 17 | # Runtime data 18 | 19 | pids 20 | _.pid 21 | _.seed 22 | \*.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | 30 | coverage 31 | \*.lcov 32 | 33 | # nyc test coverage 34 | 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | 39 | .grunt 40 | 41 | # Bower dependency directory (https://bower.io/) 42 | 43 | bower_components 44 | 45 | # node-waf configuration 46 | 47 | .lock-wscript 48 | 49 | # Compiled binary addons (https://nodejs.org/api/addons.html) 50 | 51 | build/Release 52 | 53 | # Dependency directories 54 | 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # Snowpack dependency directory (https://snowpack.dev/) 59 | 60 | web_modules/ 61 | 62 | # TypeScript cache 63 | 64 | \*.tsbuildinfo 65 | 66 | # Optional npm cache directory 67 | 68 | .npm 69 | 70 | # Optional eslint cache 71 | 72 | .eslintcache 73 | 74 | # Optional stylelint cache 75 | 76 | .stylelintcache 77 | 78 | # Microbundle cache 79 | 80 | .rpt2_cache/ 81 | .rts2_cache_cjs/ 82 | .rts2_cache_es/ 83 | .rts2_cache_umd/ 84 | 85 | # Optional REPL history 86 | 87 | .node_repl_history 88 | 89 | # Output of 'npm pack' 90 | 91 | \*.tgz 92 | 93 | # Yarn Integrity file 94 | 95 | .yarn-integrity 96 | 97 | # dotenv environment variable files 98 | 99 | .env 100 | .env.development.local 101 | .env.test.local 102 | .env.production.local 103 | .env.local 104 | 105 | # parcel-bundler cache (https://parceljs.org/) 106 | 107 | .cache 108 | .parcel-cache 109 | 110 | # Next.js build output 111 | 112 | .next 113 | out 114 | 115 | # Nuxt.js build / generate output 116 | 117 | .nuxt 118 | dist 119 | 120 | # Gatsby files 121 | 122 | .cache/ 123 | 124 | # Comment in the public line in if your project uses Gatsby and not Next.js 125 | 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | 128 | # public 129 | 130 | # vuepress build output 131 | 132 | .vuepress/dist 133 | 134 | # vuepress v2.x temp and cache directory 135 | 136 | .temp 137 | .cache 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.\* 170 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import Elysia from "elysia"; 2 | 3 | export class HttpError extends Error { 4 | public constructor( 5 | public message: string, 6 | public statusCode: number, 7 | public errorData: any = undefined 8 | ) { 9 | super(message); 10 | } 11 | 12 | public static BadRequest(message?: string, errorData?: any) { 13 | return new HttpError(message || "Bad Request", 400, errorData); 14 | } 15 | 16 | public static Unauthorized(message?: string, errorData?: any) { 17 | return new HttpError(message || "Unauthorized", 401, errorData); 18 | } 19 | 20 | public static PaymentRequired(message?: string, errorData?: any) { 21 | return new HttpError(message || "Payment Required", 402, errorData); 22 | } 23 | 24 | public static Forbidden(message?: string, errorData?: any) { 25 | return new HttpError(message || "Forbidden", 403, errorData); 26 | } 27 | 28 | public static NotFound(message?: string, errorData?: any) { 29 | return new HttpError(message || "Not Found", 404, errorData); 30 | } 31 | 32 | public static MethodNotAllowed(message?: string, errorData?: any) { 33 | return new HttpError(message || "Method Not Allowed", 405, errorData); 34 | } 35 | 36 | public static Conflict(message?: string, errorData?: any) { 37 | return new HttpError(message || "Conflict", 409, errorData); 38 | } 39 | 40 | public static UnsupportedMediaType(message?: string, errorData?: any) { 41 | return new HttpError(message || "UnsupportedMediaType", 415, errorData); 42 | } 43 | 44 | public static IAmATeapot(message?: string, errorData?: any) { 45 | return new HttpError(message || "IAmATeapot", 418, errorData); 46 | } 47 | 48 | public static TooManyRequests(message?: string, errorData?: any) { 49 | return new HttpError(message || "Too Many Requests", 429, errorData); 50 | } 51 | 52 | public static Internal(message?: string, errorData?: any) { 53 | return new HttpError(message || "Internal Server Error", 500, errorData); 54 | } 55 | 56 | public static NotImplemented(message?: string, errorData?: any) { 57 | return new HttpError(message || "Not Implemented", 501, errorData); 58 | } 59 | 60 | public static BadGateway(message?: string, errorData?: any) { 61 | return new HttpError(message || "Bad Gateway", 502, errorData); 62 | } 63 | 64 | public static ServiceUnavailable(message?: string, errorData?: any) { 65 | return new HttpError(message || "Service Unavailable", 503, errorData); 66 | } 67 | 68 | public static GatewayTimeout(message?: string, errorData?: any) { 69 | return new HttpError(message || "Gateway Timeout", 504, errorData); 70 | } 71 | } 72 | 73 | export const httpErrorDecorator = new Elysia({ 74 | name: "elysia-http-error-decorator", 75 | }).decorate("HttpError", HttpError); 76 | 77 | interface HttpErrorConstructor { 78 | customFormatter?: (error: HttpError) => any; 79 | returnStringOnly?: boolean; 80 | } 81 | 82 | export const httpError = ( 83 | params: HttpErrorConstructor = { 84 | customFormatter: undefined, 85 | returnStringOnly: false, 86 | } 87 | ) => 88 | new Elysia({ name: "elysia-http-error" }) 89 | .error({ 90 | ELYSIA_HTTP_ERROR: HttpError, 91 | }) 92 | .onError({ as: "global" }, ({ code, error, set }) => { 93 | if (code === "ELYSIA_HTTP_ERROR") { 94 | set.status = error.statusCode; 95 | if (params.customFormatter) { 96 | return params.customFormatter(error); 97 | } 98 | if (params.returnStringOnly) { 99 | return error.message; 100 | } 101 | return { 102 | error: true, 103 | code: error.statusCode, 104 | message: error.message, 105 | data: error.errorData, 106 | }; 107 | } 108 | }); 109 | --------------------------------------------------------------------------------