|
| 1 | +# 🚨 Elysia HTTP Exception |
| 2 | + |
| 3 | +[](https://www.npmjs.com/package/elysia-http-exception) |
| 4 | +[](https://www.npmjs.com/package/elysia-http-exception) |
| 5 | +[](https://opensource.org/licenses/MIT) |
| 6 | +[](https://www.typescriptlang.org/) |
| 7 | + |
| 8 | +A comprehensive Elysia plugin for handling HTTP 4xx and 5xx errors with structured exception classes and automatic error responses. This plugin provides a clean, type-safe way to handle HTTP exceptions in your Elysia applications. |
| 9 | + |
| 10 | +## ✨ Features |
| 11 | + |
| 12 | +- 🎯 **Complete HTTP Status Coverage** - Support for all standard 4xx and 5xx HTTP status codes |
| 13 | +- 🔧 **Type-Safe** - Full TypeScript support with proper type definitions |
| 14 | +- 🚀 **Easy Integration** - Simple plugin installation with zero configuration required |
| 15 | +- 🎨 **Flexible Error Data** - Support for string messages, objects, and Error instances |
| 16 | +- 🔄 **Two Usage Patterns** - Use either `throw` statements or the `httpException` decorator |
| 17 | +- 🛡️ **Automatic Error Handling** - Built-in error handler for common Elysia errors (PARSE, VALIDATION, NOT_FOUND, etc.) |
| 18 | +- 📦 **Lightweight** - Minimal overhead with tree-shakable exports |
| 19 | +- 🧪 **Well Tested** - Comprehensive test coverage for reliability |
| 20 | + |
| 21 | +## 📦 Installation |
| 22 | + |
| 23 | +```bash |
| 24 | +bun add elysia-http-exception |
| 25 | +``` |
| 26 | + |
| 27 | +## 🚀 Quick Start |
| 28 | + |
| 29 | +### Basic Setup |
| 30 | + |
| 31 | +```typescript |
| 32 | +import { Elysia } from 'elysia'; |
| 33 | +import { httpExceptionPlugin, NotFoundException, BadRequestException } from 'elysia-http-exception'; |
| 34 | + |
| 35 | +const app = new Elysia() |
| 36 | + .use(httpExceptionPlugin()) |
| 37 | + .get('/users/:id', ({ params }) => { |
| 38 | + const userId = parseInt(params.id); |
| 39 | + |
| 40 | + if (isNaN(userId)) { |
| 41 | + throw new BadRequestException('User ID must be a valid number'); |
| 42 | + } |
| 43 | + |
| 44 | + if (userId === 404) { |
| 45 | + throw new NotFoundException(`User with ID ${userId} not found`); |
| 46 | + } |
| 47 | + |
| 48 | + return { id: userId, name: `User ${userId}`, email: `user${userId}@example.com` }; |
| 49 | + }) |
| 50 | + .listen(3000); |
| 51 | +``` |
| 52 | + |
| 53 | +### Using the Decorator Pattern |
| 54 | + |
| 55 | +```typescript |
| 56 | +import { Elysia } from 'elysia'; |
| 57 | +import { httpExceptionPlugin, NotFoundException, BadRequestException } from 'elysia-http-exception'; |
| 58 | + |
| 59 | +const app = new Elysia() |
| 60 | + .use(httpExceptionPlugin()) |
| 61 | + .get('/users/:id', ({ params, httpException }) => { |
| 62 | + const userId = parseInt(params.id); |
| 63 | + |
| 64 | + if (isNaN(userId)) { |
| 65 | + return httpException(new BadRequestException('User ID must be a valid number')); |
| 66 | + } |
| 67 | + |
| 68 | + if (userId === 404) { |
| 69 | + return httpException(new NotFoundException(`User with ID ${userId} not found`)); |
| 70 | + } |
| 71 | + |
| 72 | + return { id: userId, name: `User ${userId}`, email: `user${userId}@example.com` }; |
| 73 | + }) |
| 74 | + .listen(3000); |
| 75 | +``` |
| 76 | + |
| 77 | +## 📚 Available Exception Classes |
| 78 | + |
| 79 | +### 4xx Client Error Exceptions |
| 80 | + |
| 81 | +| Exception Class | Status Code | Description | |
| 82 | +|---|---|---| |
| 83 | +| `BadRequestException` | 400 | Bad Request | |
| 84 | +| `UnauthorizedException` | 401 | Unauthorized | |
| 85 | +| `PaymentRequiredException` | 402 | Payment Required | |
| 86 | +| `ForbiddenException` | 403 | Forbidden | |
| 87 | +| `NotFoundException` | 404 | Not Found | |
| 88 | +| `MethodNotAllowedException` | 405 | Method Not Allowed | |
| 89 | +| `NotAcceptableException` | 406 | Not Acceptable | |
| 90 | +| `RequestTimeoutException` | 408 | Request Timeout | |
| 91 | +| `ConflictException` | 409 | Conflict | |
| 92 | +| `GoneException` | 410 | Gone | |
| 93 | +| `LengthRequiredException` | 411 | Length Required | |
| 94 | +| `PreconditionFailedException` | 412 | Precondition Failed | |
| 95 | +| `PayloadTooLargeException` | 413 | Payload Too Large | |
| 96 | +| `UriTooLongException` | 414 | URI Too Long | |
| 97 | +| `UnsupportedMediaTypeException` | 415 | Unsupported Media Type | |
| 98 | +| `RangeNotSatisfiableException` | 416 | Range Not Satisfiable | |
| 99 | +| `ExpectationFailedException` | 417 | Expectation Failed | |
| 100 | +| `ImATeapotException` | 418 | I'm a teapot | |
| 101 | +| `MisdirectedRequestException` | 421 | Misdirected Request | |
| 102 | +| `UnprocessableEntityException` | 422 | Unprocessable Entity | |
| 103 | +| `LockedException` | 423 | Locked | |
| 104 | +| `FailedDependencyException` | 424 | Failed Dependency | |
| 105 | +| `TooEarlyException` | 425 | Too Early | |
| 106 | +| `UpgradeRequiredException` | 426 | Upgrade Required | |
| 107 | +| `PreconditionRequiredException` | 428 | Precondition Required | |
| 108 | +| `TooManyRequestsException` | 429 | Too Many Requests | |
| 109 | +| `RequestHeaderFieldsTooLargeException` | 431 | Request Header Fields Too Large | |
| 110 | +| `UnavailableForLegalReasonsException` | 451 | Unavailable For Legal Reasons | |
| 111 | + |
| 112 | +### 5xx Server Error Exceptions |
| 113 | + |
| 114 | +| Exception Class | Status Code | Description | |
| 115 | +|---|---|---| |
| 116 | +| `InternalServerErrorException` | 500 | Internal Server Error | |
| 117 | +| `NotImplementedException` | 501 | Not Implemented | |
| 118 | +| `BadGatewayException` | 502 | Bad Gateway | |
| 119 | +| `ServiceUnavailableException` | 503 | Service Unavailable | |
| 120 | +| `GatewayTimeoutException` | 504 | Gateway Timeout | |
| 121 | +| `HttpVersionNotSupportedException` | 505 | HTTP Version Not Supported | |
| 122 | +| `VariantAlsoNegotiatesException` | 506 | Variant Also Negotiates | |
| 123 | +| `InsufficientStorageException` | 507 | Insufficient Storage | |
| 124 | +| `LoopDetectedException` | 508 | Loop Detected | |
| 125 | +| `NotExtendedException` | 510 | Not Extended | |
| 126 | +| `NetworkAuthenticationRequiredException` | 511 | Network Authentication Required | |
| 127 | + |
| 128 | +## 💡 Usage Examples |
| 129 | + |
| 130 | +### 1. Simple String Messages |
| 131 | + |
| 132 | +```typescript |
| 133 | +app.get('/simple', () => { |
| 134 | + throw new BadRequestException('Invalid input provided'); |
| 135 | +}); |
| 136 | +``` |
| 137 | + |
| 138 | +**Response:** |
| 139 | +```json |
| 140 | +{ |
| 141 | + "statusCode": 400, |
| 142 | + "message": "Invalid input provided" |
| 143 | +} |
| 144 | +``` |
| 145 | + |
| 146 | +### 2. Custom Object Data |
| 147 | + |
| 148 | +```typescript |
| 149 | +app.post('/validate', ({ body }) => { |
| 150 | + throw new UnprocessableEntityException({ |
| 151 | + error: 'VALIDATION_FAILED', |
| 152 | + details: { |
| 153 | + field: 'email', |
| 154 | + message: 'Invalid email format' |
| 155 | + }, |
| 156 | + timestamp: new Date().toISOString() |
| 157 | + }); |
| 158 | +}); |
| 159 | +``` |
| 160 | + |
| 161 | +**Response:** |
| 162 | +```json |
| 163 | +{ |
| 164 | + "error": "VALIDATION_FAILED", |
| 165 | + "details": { |
| 166 | + "field": "email", |
| 167 | + "message": "Invalid email format" |
| 168 | + }, |
| 169 | + "timestamp": "2024-01-15T10:30:00.000Z" |
| 170 | +} |
| 171 | +``` |
| 172 | + |
| 173 | +### 3. Error Object Handling |
| 174 | + |
| 175 | +```typescript |
| 176 | +app.get('/error-object', () => { |
| 177 | + const validationError = new Error('Database validation failed'); |
| 178 | + throw new InternalServerErrorException(validationError); |
| 179 | +}); |
| 180 | +``` |
| 181 | + |
| 182 | +**Response:** |
| 183 | +```json |
| 184 | +{ |
| 185 | + "statusCode": 500, |
| 186 | + "message": "Database validation failed" |
| 187 | +} |
| 188 | +``` |
| 189 | + |
| 190 | +### 4. Rate Limiting Example |
| 191 | + |
| 192 | +```typescript |
| 193 | +app.get('/api/data', () => { |
| 194 | + const rateLimitExceeded = checkRateLimit(); // Your rate limit logic |
| 195 | + |
| 196 | + if (rateLimitExceeded) { |
| 197 | + throw new TooManyRequestsException({ |
| 198 | + message: 'Rate limit exceeded', |
| 199 | + retryAfter: 60, |
| 200 | + limit: 100, |
| 201 | + remaining: 0 |
| 202 | + }); |
| 203 | + } |
| 204 | + |
| 205 | + return { data: 'Your API data' }; |
| 206 | +}); |
| 207 | +``` |
| 208 | + |
| 209 | +### 5. Authentication Example |
| 210 | + |
| 211 | +```typescript |
| 212 | +app.get('/profile', ({ headers }) => { |
| 213 | + const authHeader = headers['authorization']; |
| 214 | + |
| 215 | + if (!authHeader) { |
| 216 | + throw new UnauthorizedException({ |
| 217 | + error: 'MISSING_AUTH_HEADER', |
| 218 | + message: 'Authorization header is required', |
| 219 | + requiredFormat: 'Bearer <token>' |
| 220 | + }); |
| 221 | + } |
| 222 | + |
| 223 | + if (!authHeader.startsWith('Bearer ')) { |
| 224 | + throw new UnauthorizedException('Invalid authorization format'); |
| 225 | + } |
| 226 | + |
| 227 | + return { user: { id: 1, name: 'John Doe' } }; |
| 228 | +}); |
| 229 | +``` |
| 230 | + |
| 231 | +### 6. File Upload Example |
| 232 | + |
| 233 | +```typescript |
| 234 | +app.post('/upload', ({ request }) => { |
| 235 | + const contentLength = parseInt(request.headers.get('content-length') || '0'); |
| 236 | + const maxSize = 5 * 1024 * 1024; // 5MB |
| 237 | + |
| 238 | + if (contentLength > maxSize) { |
| 239 | + throw new PayloadTooLargeException({ |
| 240 | + error: 'FILE_TOO_LARGE', |
| 241 | + message: 'File size exceeds maximum allowed size', |
| 242 | + maxSize: `${maxSize / 1024 / 1024}MB`, |
| 243 | + receivedSize: `${(contentLength / 1024 / 1024).toFixed(2)}MB` |
| 244 | + }); |
| 245 | + } |
| 246 | + |
| 247 | + return { message: 'Upload successful' }; |
| 248 | +}); |
| 249 | +``` |
| 250 | + |
| 251 | +### 7. E-commerce Example |
| 252 | + |
| 253 | +```typescript |
| 254 | +app.post('/checkout', ({ body }) => { |
| 255 | + const data = body as any; |
| 256 | + |
| 257 | + if (!data.items?.length) { |
| 258 | + throw new BadRequestException({ |
| 259 | + error: 'EMPTY_CART', |
| 260 | + message: 'Cart cannot be empty for checkout' |
| 261 | + }); |
| 262 | + } |
| 263 | + |
| 264 | + const outOfStockItem = data.items.find((item: any) => !item.inStock); |
| 265 | + if (outOfStockItem) { |
| 266 | + throw new ConflictException({ |
| 267 | + error: 'ITEM_OUT_OF_STOCK', |
| 268 | + message: 'Some items in your cart are no longer available', |
| 269 | + unavailableItems: [outOfStockItem] |
| 270 | + }); |
| 271 | + } |
| 272 | + |
| 273 | + return { message: 'Checkout successful', orderId: 'order-123' }; |
| 274 | +}); |
| 275 | +``` |
| 276 | + |
| 277 | +## 🔧 Built-in Error Handling |
| 278 | + |
| 279 | +The plugin automatically handles common Elysia errors: |
| 280 | + |
| 281 | +- **PARSE**: JSON parsing errors → 400 Bad Request |
| 282 | +- **VALIDATION**: Schema validation errors → 400 Bad Request |
| 283 | +- **NOT_FOUND**: Route not found → 404 Not Found |
| 284 | +- **INVALID_COOKIE_SIGNATURE**: Invalid cookies → 400 Bad Request |
| 285 | +- **INVALID_FILE_TYPE**: Unsupported file types → 415 Unsupported Media Type |
| 286 | + |
| 287 | +## 🏗️ API Reference |
| 288 | + |
| 289 | +### `httpExceptionPlugin()` |
| 290 | + |
| 291 | +The main plugin function that adds HTTP exception handling to your Elysia app. |
| 292 | + |
| 293 | +```typescript |
| 294 | +import { httpExceptionPlugin } from 'elysia-http-exception'; |
| 295 | + |
| 296 | +const app = new Elysia().use(httpExceptionPlugin()); |
| 297 | +``` |
| 298 | + |
| 299 | +### `HttpException` Base Class |
| 300 | + |
| 301 | +All exception classes extend from the base `HttpException` class: |
| 302 | + |
| 303 | +```typescript |
| 304 | +class HttpException extends Error { |
| 305 | + public readonly statusCode: number; |
| 306 | + public readonly code: string; |
| 307 | + public readonly data?: unknown; |
| 308 | + public readonly isHttpException = true; |
| 309 | + |
| 310 | + constructor(httpError: HttpError, message?: string | object | Error | unknown); |
| 311 | + toBody(): unknown; |
| 312 | +} |
| 313 | +``` |
| 314 | + |
| 315 | +### Exception Constructor Parameters |
| 316 | + |
| 317 | +Each exception class accepts an optional message parameter: |
| 318 | + |
| 319 | +- **string**: Simple error message |
| 320 | +- **object**: Custom error data (returned as-is in response) |
| 321 | +- **Error**: Error instance (uses error.message) |
| 322 | +- **undefined**: Uses default message for the status code |
| 323 | + |
| 324 | +## 🧪 Testing |
| 325 | + |
| 326 | +The plugin includes comprehensive tests. Run them with: |
| 327 | + |
| 328 | +```bash |
| 329 | +# Run all tests |
| 330 | +bun test |
| 331 | + |
| 332 | +# Run unit tests only |
| 333 | +bun test:unit |
| 334 | + |
| 335 | +# Run e2e tests only |
| 336 | +bun test:e2e |
| 337 | + |
| 338 | +# Run tests with coverage |
| 339 | +bun test:coverage |
| 340 | + |
| 341 | +# Watch mode |
| 342 | +bun test:watch |
| 343 | +``` |
| 344 | + |
| 345 | +## 📋 Examples |
| 346 | + |
| 347 | +Check out the comprehensive examples in the `/example` directory: |
| 348 | + |
| 349 | +- **example-throw.ts**: Demonstrates using `throw` statements |
| 350 | +- **example-decorator.ts**: Demonstrates using the `httpException` decorator |
| 351 | + |
| 352 | +Run the examples: |
| 353 | + |
| 354 | +```bash |
| 355 | +cd example |
| 356 | +bun run example-throw.ts # Server on http://localhost:3000 |
| 357 | +bun run example-decorator.ts # Server on http://localhost:3001 |
| 358 | +``` |
| 359 | + |
| 360 | +## 🤝 Contributing |
| 361 | + |
| 362 | +Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. |
| 363 | + |
| 364 | +1. Fork the repository |
| 365 | +2. Create your feature branch (`git checkout -b feature/amazing-feature`) |
| 366 | +3. Commit your changes (`git commit -m 'Add some amazing feature'`) |
| 367 | +4. Push to the branch (`git push origin feature/amazing-feature`) |
| 368 | +5. Open a Pull Request |
| 369 | + |
| 370 | +## 📄 License |
| 371 | + |
| 372 | +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. |
| 373 | + |
| 374 | +## 🙏 Acknowledgments |
| 375 | + |
| 376 | +- [Elysia](https://elysiajs.com/) - The fast and friendly Bun web framework |
| 377 | +- [HTTP Status Codes](https://httpstatuses.com/) - For comprehensive status code reference |
| 378 | +- The Bun and TypeScript communities for their excellent tooling |
| 379 | + |
| 380 | +## 🔗 Links |
| 381 | + |
| 382 | +- [GitHub Repository](https://github.com/codev911/elysia-http-exception) |
| 383 | +- [npm Package](https://www.npmjs.com/package/elysia-http-exception) |
| 384 | +- [Elysia Documentation](https://elysiajs.com/introduction.html) |
| 385 | +- [TypeScript Documentation](https://www.typescriptlang.org/docs/) |
| 386 | + |
| 387 | +--- |
0 commit comments