@web-ts-toolkit/http-errors 0.1.0 → 0.3.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/LICENSE CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  Apache License
3
2
  Version 2.0, January 2004
4
3
  http://www.apache.org/licenses/
@@ -187,7 +186,7 @@
187
186
  same "printed page" as the copyright notice for easier
188
187
  identification within third-party archives.
189
188
 
190
- Copyright © 2017, Province of British Columbia, Canada.
189
+ Copyright Junmin Ahn
191
190
 
192
191
  Licensed under the Apache License, Version 2.0 (the "License");
193
192
  you may not use this file except in compliance with the License.
package/README.md CHANGED
@@ -8,9 +8,12 @@ HTTP error classes for backend APIs, including 4xx client errors and 5xx server
8
8
  npm install @web-ts-toolkit/http-errors
9
9
  ```
10
10
 
11
- ## Usage
11
+ ## Documentation
12
12
 
13
- ### Basic TypeScript usage
13
+ - Full package documentation lives in `website/docs/packages/http-errors.md`.
14
+ - Use the Docusaurus site in `website` for the complete hierarchy, payload helpers, and Express examples.
15
+
16
+ ## Minimal Example
14
17
 
15
18
  ```ts
16
19
  import { HttpError, UnauthorizedError, ServiceUnavailableError } from '@web-ts-toolkit/http-errors';
@@ -23,233 +26,3 @@ throw new HttpError(503, 'please try again later');
23
26
 
24
27
  throw new ServiceUnavailableError();
25
28
  ```
26
-
27
- ### Use a specific 4xx error
28
-
29
- ```ts
30
- import { BadRequestError } from '@web-ts-toolkit/http-errors';
31
-
32
- function parseLimit(value: string | undefined): number {
33
- const limit = Number(value);
34
-
35
- if (!Number.isInteger(limit) || limit <= 0) {
36
- throw new BadRequestError('limit must be a positive integer');
37
- }
38
-
39
- return limit;
40
- }
41
- ```
42
-
43
- ### Use the category base classes
44
-
45
- ```ts
46
- import { ClientError, ServerError } from '@web-ts-toolkit/http-errors';
47
-
48
- throw new ClientError(403, 'forbidden project access');
49
- throw new ServerError(503, 'search index is rebuilding');
50
- ```
51
-
52
- ### Attach a cause
53
-
54
- ```ts
55
- import { ServiceUnavailableError } from '@web-ts-toolkit/http-errors';
56
-
57
- try {
58
- await fetch('https://api.example.com/health');
59
- } catch (cause) {
60
- throw new ServiceUnavailableError('upstream API is unavailable', { cause });
61
- }
62
- ```
63
-
64
- ### Add machine-readable error metadata
65
-
66
- ```ts
67
- import { BadRequestError } from '@web-ts-toolkit/http-errors';
68
-
69
- throw new BadRequestError('invalid email', {
70
- reason: 'INVALID_EMAIL',
71
- domain: 'api.example.com',
72
- metadata: {
73
- field: 'email',
74
- },
75
- details: [
76
- {
77
- type: 'help',
78
- links: [
79
- {
80
- description: 'Validation guide',
81
- url: 'https://api.example.com/docs/errors/invalid-email',
82
- },
83
- ],
84
- },
85
- ],
86
- errors: [
87
- {
88
- field: 'email',
89
- description: 'Email must be a valid address.',
90
- },
91
- ],
92
- });
93
- ```
94
-
95
- The base `HttpError` now carries optional structured fields that are useful when building AIP-193-style error payloads:
96
-
97
- - `statusCode`: HTTP status code
98
- - `status`: canonical status string for common HTTP codes, otherwise `UNKNOWN`
99
- - `reason`: application-specific machine-readable identifier
100
- - `domain`: logical error domain such as `api.example.com`
101
- - `metadata`: stringified key-value metadata
102
- - `details`: structured detail entries
103
- - `errors`: validation or field-level error payloads
104
-
105
- ### Convert an error to an AIP-193-style payload
106
-
107
- ```ts
108
- import { BadRequestError, toAip193ErrorPayload } from '@web-ts-toolkit/http-errors';
109
-
110
- const error = new BadRequestError('invalid email', {
111
- reason: 'INVALID_EMAIL',
112
- domain: 'api.example.com',
113
- metadata: {
114
- field: 'email',
115
- },
116
- });
117
-
118
- const payload = toAip193ErrorPayload(error);
119
- ```
120
-
121
- ### Express route example
122
-
123
- ```ts
124
- import express from 'express';
125
- import { BadRequestError, ForbiddenError, NotFoundError } from '@web-ts-toolkit/http-errors';
126
-
127
- type User = {
128
- id: string;
129
- role: 'admin' | 'member';
130
- };
131
-
132
- type Project = {
133
- id: string;
134
- ownerId: string;
135
- name: string;
136
- };
137
-
138
- const app = express();
139
-
140
- const projects = new Map<string, Project>([['project-1', { id: 'project-1', ownerId: 'user-1', name: 'Toolkit' }]]);
141
-
142
- app.use(express.json());
143
-
144
- app.put('/projects/:id', (req, res) => {
145
- const user = req.user as User | undefined;
146
- const project = projects.get(req.params.id);
147
-
148
- if (!user) {
149
- throw new ForbiddenError('authentication required');
150
- }
151
-
152
- if (!project) {
153
- throw new NotFoundError('project was not found');
154
- }
155
-
156
- if (project.ownerId !== user.id && user.role !== 'admin') {
157
- throw new ForbiddenError('you cannot update this project');
158
- }
159
-
160
- if (typeof req.body.name !== 'string' || req.body.name.trim() === '') {
161
- throw new BadRequestError('name is required');
162
- }
163
-
164
- project.name = req.body.name.trim();
165
- res.json(project);
166
- });
167
- ```
168
-
169
- ### Express error middleware example
170
-
171
- ```ts
172
- import type { NextFunction, Request, Response } from 'express';
173
- import express from 'express';
174
- import { HttpError, InternalServerError } from '@web-ts-toolkit/http-errors';
175
-
176
- const app = express();
177
-
178
- app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => {
179
- if (err instanceof HttpError) {
180
- return res.status(err.statusCode).json({
181
- error: {
182
- name: err.name,
183
- message: err.message,
184
- statusCode: err.statusCode,
185
- },
186
- });
187
- }
188
-
189
- const fallback = new InternalServerError('unexpected server error');
190
-
191
- return res.status(fallback.statusCode).json({
192
- error: {
193
- name: fallback.name,
194
- message: fallback.message,
195
- statusCode: fallback.statusCode,
196
- },
197
- });
198
- });
199
- ```
200
-
201
- ## Error Hierarchy
202
-
203
- `HttpError` is the neutral base class for HTTP responses.
204
-
205
- `ClientError` is the base class for 4xx responses.
206
-
207
- `ServerError` is the base class for 5xx responses.
208
-
209
- ## Client Errors
210
-
211
- | Code | Description | Class Name | Default Message |
212
- | ---- | ------------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------- |
213
- | 400 | Bad Request | BadRequestError | The server cannot process the request due to a client error |
214
- | 401 | Unauthorized | UnauthorizedError | The user is not authorized |
215
- | 403 | Forbidden | ForbiddenError | The server refused to authorize the request |
216
- | 404 | Not Found | NotFoundError | The server did not find a current representation for the target resource |
217
- | 405 | Method Not Allowed | MethodNotAllowedError | The method received is not allowed |
218
- | 406 | Not Acceptable | NotAcceptableError | The request is not acceptable to the user agent |
219
- | 407 | Proxy Authentication Required | ProxyAuthRequiredError | The client needs to authenticate itself in order to use a proxy |
220
- | 408 | Request Timeout | RequestTimeoutError | The request was not completed in the expected time |
221
- | 409 | Conflict | ConflictError | The request was not completed due to a conflict with the target resource |
222
- | 410 | Gone | GoneError | The target resource is no longer available at the origin server |
223
- | 411 | Length Required | LengthRequiredError | The server refuses to accept the request without a defined Content-Length |
224
- | 412 | Precondition Failed | PreconditionFailedError | One or more conditions given in the request header fields evaluated to false |
225
- | 413 | Payload Too Large | PayloadTooLargeError | The request payload is too large |
226
- | 414 | URI Too Long | UriTooLongError | The request target is too long |
227
- | 415 | Unsupported Media Type | UnsupportedMediaTypeError | The payload is in a format not supported |
228
- | 416 | Requested Range Not Satisfiable | RequestedRangeNotSatisfiableError | None of the ranges in the request's Range header field overlap the current extent of the selected resource |
229
- | 417 | Expectation Failed | ExpectationFailedError | The expectation given in the request's Expect header field could not be met |
230
- | 418 | I'm a teapot | TeapotError | I'm a teapot |
231
- | 421 | Misdirected Request | MisdirectedRequestError | The request was directed at a server that is not able to produce a response |
232
- | 422 | Unprocessable Entity | UnprocessableEntityError | The server is unable to process the request |
233
- | 423 | Locked | LockedError | The source or destination resource of a method is locked |
234
- | 424 | Failed Dependency | FailedDependencyError | The requested action depended on another action |
235
- | 426 | Upgrade Required | UpgradeRequiredError | This service requires use of a different protocol |
236
- | 428 | Precondition Required | PreconditionRequiredError | This request is required to be conditional |
237
- | 429 | Too Many Requests | TooManyRequestsError | The user has sent too many requests in a given amount of time |
238
- | 431 | Request Header Fields Too Large | RequestHeaderFieldsTooLargeError | Request header fields too large |
239
- | 451 | Unavailable For Legal Reasons | UnavailableForLegalReasonsError | Denied access due to a consequence of a legal demand |
240
-
241
- ## Server Errors
242
-
243
- | Code | Description | Class Name | Default Message |
244
- | ---- | ------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------- |
245
- | 500 | Internal Server Error | InternalServerError | The server encountered an unexpected condition |
246
- | 501 | Not Implemented | NotImplementedError | The server does not support the functionality required to fulfill the request |
247
- | 502 | Bad Gateway | BadGatewayError | The server received an invalid response from an upstream server |
248
- | 503 | Service Unavailable | ServiceUnavailableError | The server is temporarily unable to handle the request |
249
- | 504 | Gateway Timeout | GatewayTimeoutError | The server did not receive a timely response from an upstream server |
250
- | 505 | HTTP Version Not Supported | HttpVersionNotSupportedError | The server does not support the HTTP protocol version used in the request |
251
- | 506 | Variant Also Negotiates | VariantAlsoNegotiatesError | The server has an internal configuration error |
252
- | 507 | Insufficient Storage | InsufficientStorageError | The server is unable to store the representation needed to complete the request |
253
- | 508 | Loop Detected | LoopDetectedError | The server detected an infinite loop while processing the request |
254
- | 510 | Not Extended | NotExtendedError | Further extensions to the request are required for the server to fulfill it |
255
- | 511 | Network Authentication Required | NetworkAuthenticationRequiredError | The client needs to authenticate to gain network access |
package/index.d.mts CHANGED
@@ -1,5 +1,10 @@
1
1
  type HttpErrorMetadataValue = string | number | boolean | bigint | null | undefined;
2
- type HttpErrorOptions = ErrorOptions & {
2
+ type HttpErrorProblemFields = {
3
+ type?: string;
4
+ title?: string;
5
+ instance?: string;
6
+ };
7
+ type HttpErrorOptions = ErrorOptions & HttpErrorProblemFields & {
3
8
  status?: string;
4
9
  reason?: string;
5
10
  domain?: string;
@@ -7,7 +12,7 @@ type HttpErrorOptions = ErrorOptions & {
7
12
  details?: unknown[];
8
13
  errors?: unknown;
9
14
  };
10
- type HttpErrorShape = {
15
+ type HttpErrorShape = HttpErrorProblemFields & {
11
16
  statusCode: number;
12
17
  status?: string;
13
18
  message: string;
@@ -31,6 +36,20 @@ type Aip193ErrorPayload = {
31
36
  details?: unknown[];
32
37
  };
33
38
  };
39
+ type Rfc9457ErrorPayload<TError = unknown> = {
40
+ type?: string;
41
+ title?: string;
42
+ status?: number;
43
+ detail?: string;
44
+ instance?: string;
45
+ errors?: TError[];
46
+ };
47
+ type Rfc9457ValidationError = {
48
+ detail: string;
49
+ pointer?: string;
50
+ parameter?: string;
51
+ header?: string;
52
+ };
34
53
 
35
54
  declare const canonicalStatusByHttpStatus: {
36
55
  readonly 400: "INVALID_ARGUMENT";
@@ -46,9 +65,12 @@ declare const canonicalStatusByHttpStatus: {
46
65
  readonly 504: "DEADLINE_EXCEEDED";
47
66
  };
48
67
  declare const getCanonicalStatus: (statusCode: number) => string;
68
+ declare const getStatusTitle: (statusCode: number) => string;
49
69
 
50
70
  declare const createAip193ErrorInfoDetail: (reason: string, domain: string, metadata?: Record<string, string>) => Aip193ErrorInfoDetail;
51
71
  declare const toAip193ErrorPayload: (error: HttpErrorShape, fallbackDomain?: string) => Aip193ErrorPayload;
72
+ declare const toRfc9457ErrorPayload: <TError = unknown>(error: HttpErrorShape) => Rfc9457ErrorPayload<TError>;
73
+ declare const toRfc9457ValidationErrorPayload: (error: HttpErrorShape) => Rfc9457ErrorPayload<Rfc9457ValidationError>;
52
74
 
53
75
  declare class HttpError extends Error {
54
76
  readonly statusCode: number;
@@ -59,6 +81,9 @@ declare class HttpError extends Error {
59
81
  readonly metadata?: Record<string, string>;
60
82
  readonly details?: unknown[];
61
83
  readonly errors?: unknown;
84
+ readonly type?: string;
85
+ readonly title?: string;
86
+ readonly instance?: string;
62
87
  constructor(statusCode?: number, message?: string, options?: HttpErrorOptions);
63
88
  }
64
89
  declare class ClientError extends HttpError {
@@ -184,4 +209,4 @@ declare class NetworkAuthenticationRequiredError extends ServerError {
184
209
  constructor(message?: string, options?: HttpErrorOptions);
185
210
  }
186
211
 
187
- export { type Aip193ErrorInfoDetail, type Aip193ErrorPayload, BadGatewayError, BadRequestError, ClientError, ConflictError, ExpectationFailedError, FailedDependencyError, ForbiddenError, GatewayTimeoutError, GoneError, HttpError, type HttpErrorMetadataValue, type HttpErrorOptions, type HttpErrorShape, HttpVersionNotSupportedError, InsufficientStorageError, InternalServerError, LengthRequiredError, LockedError, LoopDetectedError, MethodNotAllowedError, MisdirectedRequestError, NetworkAuthenticationRequiredError, NotAcceptableError, NotExtendedError, NotFoundError, NotImplementedError, PayloadTooLargeError, PreconditionFailedError, PreconditionRequiredError, ProxyAuthRequiredError, RequestHeaderFieldsTooLargeError, RequestTimeoutError, RequestedRangeNotSatisfiableError, ServerError, ServiceUnavailableError, TeapotError, TooManyRequestsError, UnauthorizedError, UnavailableForLegalReasonsError, UnprocessableEntityError, UnsupportedMediaTypeError, UpgradeRequiredError, UriTooLongError, VariantAlsoNegotiatesError, canonicalStatusByHttpStatus, createAip193ErrorInfoDetail, getCanonicalStatus, toAip193ErrorPayload };
212
+ export { type Aip193ErrorInfoDetail, type Aip193ErrorPayload, BadGatewayError, BadRequestError, ClientError, ConflictError, ExpectationFailedError, FailedDependencyError, ForbiddenError, GatewayTimeoutError, GoneError, HttpError, type HttpErrorMetadataValue, type HttpErrorOptions, type HttpErrorProblemFields, type HttpErrorShape, HttpVersionNotSupportedError, InsufficientStorageError, InternalServerError, LengthRequiredError, LockedError, LoopDetectedError, MethodNotAllowedError, MisdirectedRequestError, NetworkAuthenticationRequiredError, NotAcceptableError, NotExtendedError, NotFoundError, NotImplementedError, PayloadTooLargeError, PreconditionFailedError, PreconditionRequiredError, ProxyAuthRequiredError, RequestHeaderFieldsTooLargeError, RequestTimeoutError, RequestedRangeNotSatisfiableError, type Rfc9457ErrorPayload, type Rfc9457ValidationError, ServerError, ServiceUnavailableError, TeapotError, TooManyRequestsError, UnauthorizedError, UnavailableForLegalReasonsError, UnprocessableEntityError, UnsupportedMediaTypeError, UpgradeRequiredError, UriTooLongError, VariantAlsoNegotiatesError, canonicalStatusByHttpStatus, createAip193ErrorInfoDetail, getCanonicalStatus, getStatusTitle, toAip193ErrorPayload, toRfc9457ErrorPayload, toRfc9457ValidationErrorPayload };
package/index.d.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  type HttpErrorMetadataValue = string | number | boolean | bigint | null | undefined;
2
- type HttpErrorOptions = ErrorOptions & {
2
+ type HttpErrorProblemFields = {
3
+ type?: string;
4
+ title?: string;
5
+ instance?: string;
6
+ };
7
+ type HttpErrorOptions = ErrorOptions & HttpErrorProblemFields & {
3
8
  status?: string;
4
9
  reason?: string;
5
10
  domain?: string;
@@ -7,7 +12,7 @@ type HttpErrorOptions = ErrorOptions & {
7
12
  details?: unknown[];
8
13
  errors?: unknown;
9
14
  };
10
- type HttpErrorShape = {
15
+ type HttpErrorShape = HttpErrorProblemFields & {
11
16
  statusCode: number;
12
17
  status?: string;
13
18
  message: string;
@@ -31,6 +36,20 @@ type Aip193ErrorPayload = {
31
36
  details?: unknown[];
32
37
  };
33
38
  };
39
+ type Rfc9457ErrorPayload<TError = unknown> = {
40
+ type?: string;
41
+ title?: string;
42
+ status?: number;
43
+ detail?: string;
44
+ instance?: string;
45
+ errors?: TError[];
46
+ };
47
+ type Rfc9457ValidationError = {
48
+ detail: string;
49
+ pointer?: string;
50
+ parameter?: string;
51
+ header?: string;
52
+ };
34
53
 
35
54
  declare const canonicalStatusByHttpStatus: {
36
55
  readonly 400: "INVALID_ARGUMENT";
@@ -46,9 +65,12 @@ declare const canonicalStatusByHttpStatus: {
46
65
  readonly 504: "DEADLINE_EXCEEDED";
47
66
  };
48
67
  declare const getCanonicalStatus: (statusCode: number) => string;
68
+ declare const getStatusTitle: (statusCode: number) => string;
49
69
 
50
70
  declare const createAip193ErrorInfoDetail: (reason: string, domain: string, metadata?: Record<string, string>) => Aip193ErrorInfoDetail;
51
71
  declare const toAip193ErrorPayload: (error: HttpErrorShape, fallbackDomain?: string) => Aip193ErrorPayload;
72
+ declare const toRfc9457ErrorPayload: <TError = unknown>(error: HttpErrorShape) => Rfc9457ErrorPayload<TError>;
73
+ declare const toRfc9457ValidationErrorPayload: (error: HttpErrorShape) => Rfc9457ErrorPayload<Rfc9457ValidationError>;
52
74
 
53
75
  declare class HttpError extends Error {
54
76
  readonly statusCode: number;
@@ -59,6 +81,9 @@ declare class HttpError extends Error {
59
81
  readonly metadata?: Record<string, string>;
60
82
  readonly details?: unknown[];
61
83
  readonly errors?: unknown;
84
+ readonly type?: string;
85
+ readonly title?: string;
86
+ readonly instance?: string;
62
87
  constructor(statusCode?: number, message?: string, options?: HttpErrorOptions);
63
88
  }
64
89
  declare class ClientError extends HttpError {
@@ -184,4 +209,4 @@ declare class NetworkAuthenticationRequiredError extends ServerError {
184
209
  constructor(message?: string, options?: HttpErrorOptions);
185
210
  }
186
211
 
187
- export { type Aip193ErrorInfoDetail, type Aip193ErrorPayload, BadGatewayError, BadRequestError, ClientError, ConflictError, ExpectationFailedError, FailedDependencyError, ForbiddenError, GatewayTimeoutError, GoneError, HttpError, type HttpErrorMetadataValue, type HttpErrorOptions, type HttpErrorShape, HttpVersionNotSupportedError, InsufficientStorageError, InternalServerError, LengthRequiredError, LockedError, LoopDetectedError, MethodNotAllowedError, MisdirectedRequestError, NetworkAuthenticationRequiredError, NotAcceptableError, NotExtendedError, NotFoundError, NotImplementedError, PayloadTooLargeError, PreconditionFailedError, PreconditionRequiredError, ProxyAuthRequiredError, RequestHeaderFieldsTooLargeError, RequestTimeoutError, RequestedRangeNotSatisfiableError, ServerError, ServiceUnavailableError, TeapotError, TooManyRequestsError, UnauthorizedError, UnavailableForLegalReasonsError, UnprocessableEntityError, UnsupportedMediaTypeError, UpgradeRequiredError, UriTooLongError, VariantAlsoNegotiatesError, canonicalStatusByHttpStatus, createAip193ErrorInfoDetail, getCanonicalStatus, toAip193ErrorPayload };
212
+ export { type Aip193ErrorInfoDetail, type Aip193ErrorPayload, BadGatewayError, BadRequestError, ClientError, ConflictError, ExpectationFailedError, FailedDependencyError, ForbiddenError, GatewayTimeoutError, GoneError, HttpError, type HttpErrorMetadataValue, type HttpErrorOptions, type HttpErrorProblemFields, type HttpErrorShape, HttpVersionNotSupportedError, InsufficientStorageError, InternalServerError, LengthRequiredError, LockedError, LoopDetectedError, MethodNotAllowedError, MisdirectedRequestError, NetworkAuthenticationRequiredError, NotAcceptableError, NotExtendedError, NotFoundError, NotImplementedError, PayloadTooLargeError, PreconditionFailedError, PreconditionRequiredError, ProxyAuthRequiredError, RequestHeaderFieldsTooLargeError, RequestTimeoutError, RequestedRangeNotSatisfiableError, type Rfc9457ErrorPayload, type Rfc9457ValidationError, ServerError, ServiceUnavailableError, TeapotError, TooManyRequestsError, UnauthorizedError, UnavailableForLegalReasonsError, UnprocessableEntityError, UnsupportedMediaTypeError, UpgradeRequiredError, UriTooLongError, VariantAlsoNegotiatesError, canonicalStatusByHttpStatus, createAip193ErrorInfoDetail, getCanonicalStatus, getStatusTitle, toAip193ErrorPayload, toRfc9457ErrorPayload, toRfc9457ValidationErrorPayload };
package/index.js CHANGED
@@ -64,11 +64,15 @@ __export(index_exports, {
64
64
  canonicalStatusByHttpStatus: () => canonicalStatusByHttpStatus,
65
65
  createAip193ErrorInfoDetail: () => createAip193ErrorInfoDetail,
66
66
  getCanonicalStatus: () => getCanonicalStatus,
67
- toAip193ErrorPayload: () => toAip193ErrorPayload
67
+ getStatusTitle: () => getStatusTitle,
68
+ toAip193ErrorPayload: () => toAip193ErrorPayload,
69
+ toRfc9457ErrorPayload: () => toRfc9457ErrorPayload,
70
+ toRfc9457ValidationErrorPayload: () => toRfc9457ValidationErrorPayload
68
71
  });
69
72
  module.exports = __toCommonJS(index_exports);
70
73
 
71
74
  // src/status.ts
75
+ var import_node_http = require("http");
72
76
  var canonicalStatusByHttpStatus = {
73
77
  400: "INVALID_ARGUMENT",
74
78
  401: "UNAUTHENTICATED",
@@ -83,8 +87,19 @@ var canonicalStatusByHttpStatus = {
83
87
  504: "DEADLINE_EXCEEDED"
84
88
  };
85
89
  var getCanonicalStatus = (statusCode) => canonicalStatusByHttpStatus[statusCode] || "UNKNOWN";
90
+ var getStatusTitle = (statusCode) => import_node_http.STATUS_CODES[statusCode] || "Unknown Error";
86
91
 
87
92
  // src/serialize.ts
93
+ var RFC_9457_DEFAULT_TYPE = "about:blank";
94
+ var toAip193ErrorDetails = (error, fallbackDomain) => {
95
+ const status = error.status ?? getCanonicalStatus(error.statusCode);
96
+ return [
97
+ createAip193ErrorInfoDetail(error.reason ?? status, error.domain ?? fallbackDomain, error.metadata),
98
+ ...error.errors !== void 0 ? [{ type: "bad_request", errors: error.errors }] : [],
99
+ ...error.details ?? []
100
+ ];
101
+ };
102
+ var toRfc9457Errors = (errors) => Array.isArray(errors) ? errors : void 0;
88
103
  var createAip193ErrorInfoDetail = (reason, domain, metadata) => ({
89
104
  type: "error_info",
90
105
  reason,
@@ -92,21 +107,32 @@ var createAip193ErrorInfoDetail = (reason, domain, metadata) => ({
92
107
  ...metadata ? { metadata } : {}
93
108
  });
94
109
  var toAip193ErrorPayload = (error, fallbackDomain = "http-errors") => {
95
- const status = error.status || getCanonicalStatus(error.statusCode);
96
- const details = [
97
- createAip193ErrorInfoDetail(error.reason || status, error.domain || fallbackDomain, error.metadata),
98
- ...error.errors !== void 0 ? [{ type: "bad_request", errors: error.errors }] : [],
99
- ...error.details || []
100
- ];
110
+ const status = error.status ?? getCanonicalStatus(error.statusCode);
111
+ const details = toAip193ErrorDetails(error, fallbackDomain);
101
112
  return {
102
113
  error: {
103
114
  code: error.statusCode,
104
115
  status,
105
116
  message: error.message,
106
- ...details.length > 0 ? { details } : {}
117
+ details
107
118
  }
108
119
  };
109
120
  };
121
+ var toRfc9457ErrorPayload = (error) => {
122
+ const errors = toRfc9457Errors(error.errors);
123
+ return {
124
+ type: error.type ?? RFC_9457_DEFAULT_TYPE,
125
+ title: error.title ?? getStatusTitle(error.statusCode),
126
+ status: error.statusCode,
127
+ detail: error.message,
128
+ ...error.instance ? { instance: error.instance } : {},
129
+ ...errors ? { errors } : {}
130
+ };
131
+ };
132
+ var toRfc9457ValidationErrorPayload = (error) => toRfc9457ErrorPayload(error);
133
+
134
+ // src/base.ts
135
+ var import_utils = require("@web-ts-toolkit/utils");
110
136
 
111
137
  // src/messages.ts
112
138
  var messages = {
@@ -153,39 +179,41 @@ var getDefaultMessage = (statusCode) => messages[statusCode] || messages[500];
153
179
 
154
180
  // src/base.ts
155
181
  var ErrorCtor = Error;
156
- var toMetadata = (metadata) => {
157
- if (!metadata) {
158
- return void 0;
159
- }
160
- return Object.entries(metadata).reduce((result, [key, value]) => {
161
- result[key] = String(value);
162
- return result;
163
- }, {});
164
- };
165
182
  var HttpError = class extends Error {
166
183
  constructor(statusCode = 500, message, options = {}) {
167
- super(message || getDefaultMessage(statusCode), options);
184
+ super(message ?? getDefaultMessage(statusCode), options);
185
+ const { status, reason, domain, metadata, details, errors, type, title, instance } = options;
168
186
  if (ErrorCtor.captureStackTrace) {
169
187
  ErrorCtor.captureStackTrace(this, this.constructor);
170
188
  }
171
189
  this.name = this.constructor.name;
172
190
  this.statusCode = statusCode;
173
- this.status = options.status || getCanonicalStatus(statusCode);
191
+ this.status = status ?? getCanonicalStatus(statusCode);
174
192
  this.date = /* @__PURE__ */ new Date();
175
- if (options.reason) {
176
- this.reason = options.reason;
193
+ if (reason !== void 0) {
194
+ this.reason = reason;
195
+ }
196
+ if (domain !== void 0) {
197
+ this.domain = domain;
198
+ }
199
+ const normalizedMetadata = (0, import_utils.toStringRecord)(metadata);
200
+ if (normalizedMetadata !== void 0) {
201
+ this.metadata = normalizedMetadata;
202
+ }
203
+ if (details !== void 0) {
204
+ this.details = details;
177
205
  }
178
- if (options.domain) {
179
- this.domain = options.domain;
206
+ if (errors !== void 0) {
207
+ this.errors = errors;
180
208
  }
181
- if (options.metadata) {
182
- this.metadata = toMetadata(options.metadata);
209
+ if (type !== void 0) {
210
+ this.type = type;
183
211
  }
184
- if (options.details) {
185
- this.details = options.details;
212
+ if (title !== void 0) {
213
+ this.title = title;
186
214
  }
187
- if (options.errors !== void 0) {
188
- this.errors = options.errors;
215
+ if (instance !== void 0) {
216
+ this.instance = instance;
189
217
  }
190
218
  }
191
219
  };
@@ -439,5 +467,8 @@ var NetworkAuthenticationRequiredError = class extends ServerError {
439
467
  canonicalStatusByHttpStatus,
440
468
  createAip193ErrorInfoDetail,
441
469
  getCanonicalStatus,
442
- toAip193ErrorPayload
470
+ getStatusTitle,
471
+ toAip193ErrorPayload,
472
+ toRfc9457ErrorPayload,
473
+ toRfc9457ValidationErrorPayload
443
474
  });
package/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/status.ts
2
+ import { STATUS_CODES } from "http";
2
3
  var canonicalStatusByHttpStatus = {
3
4
  400: "INVALID_ARGUMENT",
4
5
  401: "UNAUTHENTICATED",
@@ -13,8 +14,19 @@ var canonicalStatusByHttpStatus = {
13
14
  504: "DEADLINE_EXCEEDED"
14
15
  };
15
16
  var getCanonicalStatus = (statusCode) => canonicalStatusByHttpStatus[statusCode] || "UNKNOWN";
17
+ var getStatusTitle = (statusCode) => STATUS_CODES[statusCode] || "Unknown Error";
16
18
 
17
19
  // src/serialize.ts
20
+ var RFC_9457_DEFAULT_TYPE = "about:blank";
21
+ var toAip193ErrorDetails = (error, fallbackDomain) => {
22
+ const status = error.status ?? getCanonicalStatus(error.statusCode);
23
+ return [
24
+ createAip193ErrorInfoDetail(error.reason ?? status, error.domain ?? fallbackDomain, error.metadata),
25
+ ...error.errors !== void 0 ? [{ type: "bad_request", errors: error.errors }] : [],
26
+ ...error.details ?? []
27
+ ];
28
+ };
29
+ var toRfc9457Errors = (errors) => Array.isArray(errors) ? errors : void 0;
18
30
  var createAip193ErrorInfoDetail = (reason, domain, metadata) => ({
19
31
  type: "error_info",
20
32
  reason,
@@ -22,21 +34,32 @@ var createAip193ErrorInfoDetail = (reason, domain, metadata) => ({
22
34
  ...metadata ? { metadata } : {}
23
35
  });
24
36
  var toAip193ErrorPayload = (error, fallbackDomain = "http-errors") => {
25
- const status = error.status || getCanonicalStatus(error.statusCode);
26
- const details = [
27
- createAip193ErrorInfoDetail(error.reason || status, error.domain || fallbackDomain, error.metadata),
28
- ...error.errors !== void 0 ? [{ type: "bad_request", errors: error.errors }] : [],
29
- ...error.details || []
30
- ];
37
+ const status = error.status ?? getCanonicalStatus(error.statusCode);
38
+ const details = toAip193ErrorDetails(error, fallbackDomain);
31
39
  return {
32
40
  error: {
33
41
  code: error.statusCode,
34
42
  status,
35
43
  message: error.message,
36
- ...details.length > 0 ? { details } : {}
44
+ details
37
45
  }
38
46
  };
39
47
  };
48
+ var toRfc9457ErrorPayload = (error) => {
49
+ const errors = toRfc9457Errors(error.errors);
50
+ return {
51
+ type: error.type ?? RFC_9457_DEFAULT_TYPE,
52
+ title: error.title ?? getStatusTitle(error.statusCode),
53
+ status: error.statusCode,
54
+ detail: error.message,
55
+ ...error.instance ? { instance: error.instance } : {},
56
+ ...errors ? { errors } : {}
57
+ };
58
+ };
59
+ var toRfc9457ValidationErrorPayload = (error) => toRfc9457ErrorPayload(error);
60
+
61
+ // src/base.ts
62
+ import { toStringRecord } from "@web-ts-toolkit/utils";
40
63
 
41
64
  // src/messages.ts
42
65
  var messages = {
@@ -83,39 +106,41 @@ var getDefaultMessage = (statusCode) => messages[statusCode] || messages[500];
83
106
 
84
107
  // src/base.ts
85
108
  var ErrorCtor = Error;
86
- var toMetadata = (metadata) => {
87
- if (!metadata) {
88
- return void 0;
89
- }
90
- return Object.entries(metadata).reduce((result, [key, value]) => {
91
- result[key] = String(value);
92
- return result;
93
- }, {});
94
- };
95
109
  var HttpError = class extends Error {
96
110
  constructor(statusCode = 500, message, options = {}) {
97
- super(message || getDefaultMessage(statusCode), options);
111
+ super(message ?? getDefaultMessage(statusCode), options);
112
+ const { status, reason, domain, metadata, details, errors, type, title, instance } = options;
98
113
  if (ErrorCtor.captureStackTrace) {
99
114
  ErrorCtor.captureStackTrace(this, this.constructor);
100
115
  }
101
116
  this.name = this.constructor.name;
102
117
  this.statusCode = statusCode;
103
- this.status = options.status || getCanonicalStatus(statusCode);
118
+ this.status = status ?? getCanonicalStatus(statusCode);
104
119
  this.date = /* @__PURE__ */ new Date();
105
- if (options.reason) {
106
- this.reason = options.reason;
120
+ if (reason !== void 0) {
121
+ this.reason = reason;
122
+ }
123
+ if (domain !== void 0) {
124
+ this.domain = domain;
125
+ }
126
+ const normalizedMetadata = toStringRecord(metadata);
127
+ if (normalizedMetadata !== void 0) {
128
+ this.metadata = normalizedMetadata;
129
+ }
130
+ if (details !== void 0) {
131
+ this.details = details;
107
132
  }
108
- if (options.domain) {
109
- this.domain = options.domain;
133
+ if (errors !== void 0) {
134
+ this.errors = errors;
110
135
  }
111
- if (options.metadata) {
112
- this.metadata = toMetadata(options.metadata);
136
+ if (type !== void 0) {
137
+ this.type = type;
113
138
  }
114
- if (options.details) {
115
- this.details = options.details;
139
+ if (title !== void 0) {
140
+ this.title = title;
116
141
  }
117
- if (options.errors !== void 0) {
118
- this.errors = options.errors;
142
+ if (instance !== void 0) {
143
+ this.instance = instance;
119
144
  }
120
145
  }
121
146
  };
@@ -368,5 +393,8 @@ export {
368
393
  canonicalStatusByHttpStatus,
369
394
  createAip193ErrorInfoDetail,
370
395
  getCanonicalStatus,
371
- toAip193ErrorPayload
396
+ getStatusTitle,
397
+ toAip193ErrorPayload,
398
+ toRfc9457ErrorPayload,
399
+ toRfc9457ValidationErrorPayload
372
400
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@web-ts-toolkit/http-errors",
3
3
  "description": "HTTP error classes for backend APIs",
4
- "version": "0.1.0",
4
+ "version": "0.3.0",
5
5
  "keywords": [
6
6
  "http-errors",
7
7
  "api-errors"
@@ -17,7 +17,10 @@
17
17
  "default": "./index.js"
18
18
  }
19
19
  },
20
- "author": "Junmin Dev",
20
+ "dependencies": {
21
+ "@web-ts-toolkit/utils": "0.3.0"
22
+ },
23
+ "author": "Junmin Ahn",
21
24
  "bugs": {
22
25
  "url": "https://github.com/egose/web-ts-toolkit/issues"
23
26
  },