@b9g/http-errors 0.1.4 → 0.2.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # @b9g/http-errors
2
-
3
- **Standard HTTP error responses for ServiceWorker applications. Returns proper Response objects with status codes, not thrown exceptions.**
2
+ **Standard HTTP error classes with native cause support and automatic serialization**
4
3
 
5
4
  ## Features
6
5
 
7
- - **ServiceWorker Compatible**: Returns Response objects perfect for `event.respondWith()`
8
- - **Standard HTTP Status Codes**: Pre-defined functions for all common HTTP errors
9
- - **No Exceptions**: Functional approach - return errors, don't throw them
10
- - **TypeScript Support**: Full type definitions for all error classes
11
6
  - **Universal**: Works in browsers, Node.js, Bun, and edge platforms
7
+ - **Response Protocol**: Implements `toResponse()` for automatic HTTP response conversion
8
+ - **Structured Logging**: Built-in `toJSON()` for clean error serialization
9
+ - **Standard HTTP Status Codes**: Pre-defined classes for all common HTTP errors
10
+ - **TypeScript Support**: Full type definitions for all error classes
11
+ - **Error Chaining**: Native `cause` support for error context
12
12
 
13
13
  ## Installation
14
14
 
@@ -18,336 +18,162 @@ npm install @b9g/http-errors
18
18
 
19
19
  ## Quick Start
20
20
 
21
- ```javascript
22
- import {
23
- NotFound,
24
- BadRequest,
25
- Unauthorized,
26
- InternalServerError
27
- } from '@b9g/http-errors';
28
-
29
- // Create error responses
30
- const notFound = NotFound('Page not found');
31
- const badRequest = BadRequest('Invalid input data');
32
- const unauthorized = Unauthorized('Authentication required');
33
- const serverError = InternalServerError('Database connection failed');
34
-
35
- // All return Response objects
36
- console.log(notFound instanceof Response); // true
37
- console.log(notFound.status); // 404
38
- ```
39
-
40
- ## Available Error Classes
41
-
42
- ### Client Errors (4xx)
43
-
44
21
  ```javascript
45
22
  import {
46
- BadRequest, // 400
47
- Unauthorized, // 401
48
- PaymentRequired, // 402
49
- Forbidden, // 403
50
- NotFound, // 404
51
- MethodNotAllowed, // 405
52
- NotAcceptable, // 406
53
- RequestTimeout, // 408
54
- Conflict, // 409
55
- Gone, // 410
56
- LengthRequired, // 411
57
- PreconditionFailed, // 412
58
- PayloadTooLarge, // 413
59
- URITooLong, // 414
60
- UnsupportedMediaType, // 415
61
- RangeNotSatisfiable, // 416
62
- ExpectationFailed, // 417
63
- ImATeapot, // 418
64
- UnprocessableEntity, // 422
65
- TooManyRequests, // 429
23
+ NotFound,
24
+ BadRequest,
25
+ Unauthorized,
26
+ InternalServerError
66
27
  } from '@b9g/http-errors';
67
- ```
68
-
69
- ### Server Errors (5xx)
70
-
71
- ```javascript
72
- import {
73
- InternalServerError, // 500
74
- NotImplemented, // 501
75
- BadGateway, // 502
76
- ServiceUnavailable, // 503
77
- GatewayTimeout, // 504
78
- HTTPVersionNotSupported, // 505
79
- } from '@b9g/http-errors';
80
- ```
81
-
82
- ## Usage Examples
83
-
84
- ### Basic Error Responses
85
-
86
- ```javascript
87
- import { NotFound, BadRequest } from '@b9g/http-errors';
88
-
89
- // Simple message
90
- const error1 = NotFound('User not found');
91
-
92
- // With additional details
93
- const error2 = BadRequest('Invalid email format', {
94
- field: 'email',
95
- code: 'INVALID_FORMAT'
96
- });
97
- ```
98
-
99
- ### Router Integration
100
28
 
101
- ```javascript
102
- import { Router } from '@b9g/router';
103
- import { NotFound, BadRequest, Unauthorized } from '@b9g/http-errors';
29
+ // Throw as exceptions
30
+ throw new NotFound('Page not found');
31
+ throw new BadRequest('Invalid input data');
104
32
 
105
- const router = new Router();
33
+ // Or convert to Response objects
34
+ const error = new NotFound('Page not found');
35
+ return error.toResponse(); // Response with status 404
106
36
 
107
- router.get('/users/:id', async (request, context) => {
108
- const { id } = context.params;
109
-
110
- // Validate input
111
- if (!id || isNaN(Number(id))) {
112
- return BadRequest('Invalid user ID');
113
- }
114
-
115
- // Check authentication
116
- if (!request.headers.get('authorization')) {
117
- return Unauthorized('Authentication required');
118
- }
119
-
120
- // Find user
121
- const user = await db.users.find(id);
122
- if (!user) {
123
- return NotFound('User not found');
124
- }
125
-
126
- return Response.json(user);
127
- });
37
+ // In development, get detailed error pages
38
+ return error.toResponse(true); // HTML page with stack trace
128
39
  ```
129
40
 
130
- ### Middleware Error Handling
41
+ ## API
131
42
 
132
- ```javascript
133
- router.use(async function* (request, context) {
134
- try {
135
- return yield request;
136
- } catch (error) {
137
- console.error('Request failed:', error);
138
- return InternalServerError('Something went wrong');
139
- }
140
- });
141
- ```
43
+ ### HTTPError Class
142
44
 
143
- ### Custom Error Details
45
+ Base class for all HTTP errors. Extends `Error`.
144
46
 
145
47
  ```javascript
146
- import { BadRequest, Conflict } from '@b9g/http-errors';
147
-
148
- // With error code
149
- const validationError = BadRequest('Validation failed', {
150
- code: 'VALIDATION_ERROR',
151
- fields: ['email', 'password']
152
- });
153
-
154
- // With retry information
155
- const rateLimitError = TooManyRequests('Rate limit exceeded', {
156
- retryAfter: 60,
157
- limit: 100,
158
- window: 3600
48
+ import { HTTPError } from '@b9g/http-errors';
49
+
50
+ const error = new HTTPError(404, 'Resource not found', {
51
+ cause: originalError, // Error that caused this
52
+ headers: { // Custom headers for response
53
+ 'Cache-Control': 'no-store'
54
+ },
55
+ expose: true // Whether to expose message to client
159
56
  });
160
57
 
161
- // With conflict details
162
- const duplicateError = Conflict('Email already exists', {
163
- field: 'email',
164
- value: 'user@example.com'
165
- });
58
+ error.status; // 404
59
+ error.message; // 'Resource not found'
60
+ error.expose; // true (client errors default to true, server errors to false)
61
+ error.headers; // { 'Cache-Control': 'no-store' }
166
62
  ```
167
63
 
168
- ## Exports
169
-
170
- ### Classes
171
-
172
- - `HTTPError` - Base HTTP error class (extends Error)
173
- - `NotHandled` - Special error for unhandled requests
174
-
175
- ### Client Error Classes (4xx)
176
-
177
- - `BadRequest` (400)
178
- - `Unauthorized` (401)
179
- - `Forbidden` (403)
180
- - `NotFound` (404)
181
- - `MethodNotAllowed` (405)
182
- - `Conflict` (409)
183
- - `UnprocessableEntity` (422)
184
- - `TooManyRequests` (429)
185
-
186
- ### Server Error Classes (5xx)
187
-
188
- - `InternalServerError` (500)
189
- - `NotImplemented` (501)
190
- - `BadGateway` (502)
191
- - `ServiceUnavailable` (503)
192
- - `GatewayTimeout` (504)
193
-
194
- ### Functions
195
-
196
- - `createHTTPError(status, message?, options?)` - Create an HTTPError with a specific status code
197
- - `isHTTPError(value)` - Type guard to check if a value is an HTTPError
198
-
199
- ### Types
200
-
201
- - `HTTPErrorOptions` - Options for HTTPError constructor
64
+ ### Methods
202
65
 
203
- ### Default Export
66
+ #### `toResponse(isDev?: boolean): Response`
204
67
 
205
- - `createHTTPError` - Factory function for creating HTTP errors
68
+ Converts the error to an HTTP Response object.
206
69
 
207
- ## API Reference
70
+ - In development mode (`isDev = true`): Returns HTML page with stack trace
71
+ - In production mode: Returns plain text with minimal information
208
72
 
209
- ### Error Classes
73
+ ```javascript
74
+ const error = new NotFound('Page not found');
210
75
 
211
- All error functions follow the same signature:
76
+ // Production response
77
+ error.toResponse(); // Response { status: 404, body: 'Page not found' }
212
78
 
213
- ```typescript
214
- function ErrorName(message?: string, details?: any): Response
79
+ // Development response with stack trace
80
+ error.toResponse(true); // Response { status: 404, body: '<html>...</html>' }
215
81
  ```
216
82
 
217
- #### Parameters
83
+ #### `toJSON(): object`
218
84
 
219
- - `message` (optional): Human-readable error message
220
- - `details` (optional): Additional error details (serialized as JSON)
221
-
222
- #### Returns
223
-
224
- Returns a `Response` object with:
225
- - Appropriate HTTP status code
226
- - `Content-Type: application/json`
227
- - JSON body containing error information
228
-
229
- ### Response Format
85
+ Converts the error to a plain object for logging and serialization.
230
86
 
231
87
  ```javascript
232
- {
233
- "error": {
234
- "type": "NotFound",
235
- "message": "User not found",
236
- "status": 404,
237
- "details": {
238
- // Any additional details provided
239
- }
240
- }
241
- }
242
- ```
88
+ const error = new BadRequest('Invalid email', {
89
+ headers: { 'X-Custom': 'value' }
90
+ });
243
91
 
244
- ## TypeScript Support
92
+ JSON.stringify(error);
93
+ // {
94
+ // "name": "BadRequest",
95
+ // "message": "Invalid email",
96
+ // "status": 400,
97
+ // "statusCode": 400,
98
+ // "expose": true,
99
+ // "headers": { "X-Custom": "value" }
100
+ // }
245
101
 
246
- Full TypeScript definitions included:
102
+ ### Client Error Classes (4xx)
247
103
 
248
- ```typescript
249
- import type { ErrorResponse } from '@b9g/http-errors';
104
+ ```javascript
105
+ import {
106
+ BadRequest, // 400
107
+ Unauthorized, // 401
108
+ Forbidden, // 403
109
+ NotFound, // 404
110
+ MethodNotAllowed, // 405
111
+ Conflict, // 409
112
+ UnprocessableEntity, // 422
113
+ TooManyRequests // 429
114
+ } from '@b9g/http-errors';
250
115
 
251
- function handleError(): ErrorResponse {
252
- return NotFound('Resource not found');
253
- }
116
+ // All accept message and options
117
+ throw new Unauthorized('Invalid credentials', {
118
+ headers: { 'WWW-Authenticate': 'Bearer realm="api"' }
119
+ });
254
120
 
255
- // ErrorResponse extends Response
256
- const response: Response = handleError();
121
+ throw new TooManyRequests('Rate limit exceeded', {
122
+ headers: { 'Retry-After': '60' }
123
+ });
257
124
  ```
258
125
 
259
- ## Integration Examples
260
-
261
- ### API Error Handling
126
+ ### Server Error Classes (5xx)
262
127
 
263
128
  ```javascript
264
- import {
265
- BadRequest,
266
- NotFound,
267
- Conflict,
268
- InternalServerError
129
+ import {
130
+ InternalServerError, // 500
131
+ NotImplemented, // 501
132
+ BadGateway, // 502
133
+ ServiceUnavailable, // 503
134
+ GatewayTimeout // 504
269
135
  } from '@b9g/http-errors';
270
136
 
271
- router.post('/api/users', async (request) => {
272
- try {
273
- const data = await request.json();
274
-
275
- // Validation
276
- if (!data.email) {
277
- return BadRequest('Email is required');
278
- }
279
-
280
- // Check for existing user
281
- const existing = await db.users.findByEmail(data.email);
282
- if (existing) {
283
- return Conflict('Email already registered');
284
- }
285
-
286
- // Create user
287
- const user = await db.users.create(data);
288
- return Response.json(user, { status: 201 });
289
-
290
- } catch (error) {
291
- return InternalServerError('Failed to create user');
292
- }
137
+ // Server errors default to expose: false
138
+ throw new InternalServerError('Database connection failed', {
139
+ cause: dbError // Chain the original error
293
140
  });
294
141
  ```
295
142
 
296
- ### Auth Middleware
143
+ ### Functions
144
+
145
+ #### `isHTTPError(value): value is HTTPError`
146
+
147
+ Type guard to check if a value is an HTTPError.
297
148
 
298
149
  ```javascript
299
- import { Unauthorized, Forbidden } from '@b9g/http-errors';
150
+ import {isHTTPError} from '@b9g/http-errors';
300
151
 
301
- const authMiddleware = async function* (request, context) {
302
- const token = request.headers.get('authorization');
303
-
304
- if (!token) {
305
- return Unauthorized('Authentication required');
306
- }
307
-
308
- try {
309
- const user = await verifyToken(token);
310
- context.user = user;
311
- return yield request;
312
- } catch (error) {
313
- return Forbidden('Invalid or expired token');
152
+ try {
153
+ // ...
154
+ } catch (err) {
155
+ if (isHTTPError(err)) {
156
+ return err.toResponse();
314
157
  }
315
- };
316
-
317
- router.use('/api/admin/*', authMiddleware);
158
+ throw err;
159
+ }
318
160
  ```
319
161
 
320
- ### Input Validation
162
+ ### Types
321
163
 
322
- ```javascript
323
- import { BadRequest } from '@b9g/http-errors';
324
-
325
- function validateUser(data) {
326
- const errors = [];
327
-
328
- if (!data.email) errors.push('email is required');
329
- if (!data.password) errors.push('password is required');
330
- if (data.password && data.password.length < 8) {
331
- errors.push('password must be at least 8 characters');
332
- }
333
-
334
- if (errors.length > 0) {
335
- return BadRequest('Validation failed', { errors });
336
- }
337
-
338
- return null; // Valid
164
+ ```typescript
165
+ interface HTTPErrorOptions {
166
+ /** Original error that caused this HTTP error */
167
+ cause?: Error;
168
+ /** Custom headers to include in the error response */
169
+ headers?: Record<string, string>;
170
+ /** Whether error details should be exposed to clients (defaults based on status) */
171
+ expose?: boolean;
172
+ /** Additional properties to attach to the error */
173
+ [key: string]: any;
339
174
  }
340
-
341
- router.post('/register', async (request) => {
342
- const data = await request.json();
343
-
344
- const validationError = validateUser(data);
345
- if (validationError) return validationError;
346
-
347
- // Process valid data...
348
- });
349
175
  ```
350
176
 
351
177
  ## License
352
178
 
353
- MIT
179
+ MIT
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@b9g/http-errors",
3
- "version": "0.1.4",
4
- "description": "Standard HTTP error responses for ServiceWorker applications. Returns proper Response objects with status codes, not thrown exceptions.",
3
+ "version": "0.2.0-beta.0",
4
+ "description": "Standard HTTP error classes with native cause support and automatic serialization",
5
5
  "keywords": [
6
6
  "http",
7
7
  "errors",
@@ -14,8 +14,7 @@
14
14
  ],
15
15
  "dependencies": {},
16
16
  "devDependencies": {
17
- "@b9g/libuild": "^0.1.11",
18
- "bun-types": "latest"
17
+ "@b9g/libuild": "^0.1.18"
19
18
  },
20
19
  "type": "module",
21
20
  "types": "src/index.d.ts",
package/src/index.d.ts CHANGED
@@ -1,10 +1,7 @@
1
1
  /**
2
- * Modern HTTP error classes for Shovel
3
- * Lightweight alternative to http-errors with native Error cause support
4
- */
5
- /**
6
- * Options for creating HTTP errors
2
+ * Standard HTTP error classes with native cause support and automatic serialization
7
3
  */
4
+ /** Options for creating HTTP errors */
8
5
  export interface HTTPErrorOptions {
9
6
  /** Original error that caused this HTTP error */
10
7
  cause?: Error;
@@ -12,18 +9,14 @@ export interface HTTPErrorOptions {
12
9
  headers?: Record<string, string>;
13
10
  /** Whether the error details should be exposed to clients (defaults based on status) */
14
11
  expose?: boolean;
15
- /** Additional properties to attach to the error */
16
- [key: string]: any;
17
12
  }
18
- /**
19
- * Base HTTP error class
20
- */
13
+ /** Base HTTP error class */
21
14
  export declare class HTTPError extends Error {
22
15
  readonly status: number;
23
- readonly statusCode: number;
24
16
  readonly expose: boolean;
25
17
  readonly headers?: Record<string, string>;
26
18
  constructor(status: number, message?: string, options?: HTTPErrorOptions);
19
+ get statusCode(): number;
27
20
  /**
28
21
  * Convert error to a plain object for serialization
29
22
  */
@@ -35,21 +28,17 @@ export declare class HTTPError extends Error {
35
28
  expose: boolean;
36
29
  headers: Record<string, string>;
37
30
  };
38
- }
39
- /**
40
- * Special error for middleware fallthrough (not an HTTP error)
41
- */
42
- export declare class NotHandled extends Error {
43
- constructor(message?: string);
31
+ /**
32
+ * Convert error to an HTTP Response
33
+ * In development mode, shows detailed error page with stack trace
34
+ * In production mode, shows minimal error message
35
+ */
36
+ toResponse(isDev?: boolean): Response;
44
37
  }
45
38
  /**
46
39
  * Check if a value is an HTTP error
47
40
  */
48
41
  export declare function isHTTPError(value: any): value is HTTPError;
49
- /**
50
- * Create an HTTP error with the given status code
51
- */
52
- export declare function createHTTPError(status: number, message?: string, options?: HTTPErrorOptions): HTTPError;
53
42
  export declare class BadRequest extends HTTPError {
54
43
  constructor(message?: string, options?: HTTPErrorOptions);
55
44
  }
@@ -89,4 +78,3 @@ export declare class ServiceUnavailable extends HTTPError {
89
78
  export declare class GatewayTimeout extends HTTPError {
90
79
  constructor(message?: string, options?: HTTPErrorOptions);
91
80
  }
92
- export default createHTTPError;
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /// <reference types="./index.d.ts" />
2
2
  // src/index.ts
3
- var STATUS_CODES = {
3
+ var STATUS_CODE_DEFAULTS = {
4
4
  // 4xx Client Errors
5
5
  400: "Bad Request",
6
6
  401: "Unauthorized",
@@ -44,20 +44,23 @@ var STATUS_CODES = {
44
44
  510: "Not Extended",
45
45
  511: "Network Authentication Required"
46
46
  };
47
+ var HTTP_ERROR = /* @__PURE__ */ Symbol.for("shovel.http-error");
47
48
  var HTTPError = class extends Error {
48
49
  status;
49
- statusCode;
50
50
  expose;
51
51
  headers;
52
52
  constructor(status, message, options = {}) {
53
- const defaultMessage = STATUS_CODES[status] || "Unknown Error";
53
+ const defaultMessage = STATUS_CODE_DEFAULTS[status] || "Unknown Error";
54
54
  super(message || defaultMessage, { cause: options.cause });
55
55
  this.name = this.constructor.name;
56
- this.status = this.statusCode = status;
56
+ this.status = status;
57
57
  this.expose = options.expose ?? status < 500;
58
58
  this.headers = options.headers;
59
59
  Object.assign(this, options);
60
60
  }
61
+ get statusCode() {
62
+ return this.status;
63
+ }
61
64
  /**
62
65
  * Convert error to a plain object for serialization
63
66
  */
@@ -71,24 +74,54 @@ var HTTPError = class extends Error {
71
74
  headers: this.headers
72
75
  };
73
76
  }
74
- };
75
- var NotHandled = class extends Error {
76
- constructor(message = "Request not handled by middleware") {
77
- super(message);
78
- this.name = "NotHandled";
77
+ /**
78
+ * Convert error to an HTTP Response
79
+ * In development mode, shows detailed error page with stack trace
80
+ * In production mode, shows minimal error message
81
+ */
82
+ toResponse(isDev) {
83
+ const headers = new Headers(this.headers);
84
+ if (isDev && this.expose) {
85
+ headers.set("Content-Type", "text/html; charset=utf-8");
86
+ const statusText = STATUS_CODE_DEFAULTS[this.status] || "Unknown Error";
87
+ const html = `<!DOCTYPE html>
88
+ <html>
89
+ <head>
90
+ <title>${this.status} ${escapeHTML(statusText)}</title>
91
+ <style>
92
+ body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
93
+ h1 { color: ${this.status >= 500 ? "#c00" : "#e67700"}; }
94
+ .message { font-size: 1.2em; color: #333; }
95
+ pre { background: #f5f5f5; padding: 1rem; overflow-x: auto; border-radius: 4px; }
96
+ </style>
97
+ </head>
98
+ <body>
99
+ <h1>${this.status} ${escapeHTML(statusText)}</h1>
100
+ <p class="message">${escapeHTML(this.message)}</p>
101
+ <pre>${escapeHTML(this.stack || "No stack trace available")}</pre>
102
+ </body>
103
+ </html>`;
104
+ return new Response(html, {
105
+ status: this.status,
106
+ statusText,
107
+ headers
108
+ });
109
+ }
110
+ headers.set("Content-Type", "text/plain; charset=utf-8");
111
+ const body = this.expose ? this.message : STATUS_CODE_DEFAULTS[this.status] || "Unknown Error";
112
+ return new Response(body, {
113
+ status: this.status,
114
+ statusText: STATUS_CODE_DEFAULTS[this.status],
115
+ headers
116
+ });
79
117
  }
80
118
  };
81
- function isHTTPError(value) {
82
- if (value instanceof HTTPError)
83
- return true;
84
- if (!(value instanceof Error))
85
- return false;
86
- const hasStatus = "status" in value && typeof value.status === "number";
87
- const hasStatusCode = "statusCode" in value && typeof value.statusCode === "number";
88
- return hasStatus && hasStatusCode && value.status === value.statusCode;
119
+ HTTPError.prototype[HTTP_ERROR] = true;
120
+ function escapeHTML(str) {
121
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
89
122
  }
90
- function createHTTPError(status, message, options) {
91
- return new HTTPError(status, message, options);
123
+ function isHTTPError(value) {
124
+ return !!(value && value[HTTP_ERROR]);
92
125
  }
93
126
  var BadRequest = class extends HTTPError {
94
127
  constructor(message, options) {
@@ -155,7 +188,6 @@ var GatewayTimeout = class extends HTTPError {
155
188
  super(504, message, options);
156
189
  }
157
190
  };
158
- var src_default = createHTTPError;
159
191
  export {
160
192
  BadGateway,
161
193
  BadRequest,
@@ -166,13 +198,10 @@ export {
166
198
  InternalServerError,
167
199
  MethodNotAllowed,
168
200
  NotFound,
169
- NotHandled,
170
201
  NotImplemented,
171
202
  ServiceUnavailable,
172
203
  TooManyRequests,
173
204
  Unauthorized,
174
205
  UnprocessableEntity,
175
- createHTTPError,
176
- src_default as default,
177
206
  isHTTPError
178
207
  };