@lovrozagar/honey 0.1.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 +51 -0
- package/docs/api-ref/go.md +399 -0
- package/docs/api-ref/python.md +399 -0
- package/docs/api-ref/rust.md +399 -0
- package/docs/api-ref/typescript.md +399 -0
- package/docs/invalidate-spec.md +303 -0
- package/docs/sdk.md +722 -0
- package/package.json +120 -0
- package/src/accepts.ts +78 -0
- package/src/body-limit.ts +100 -0
- package/src/build/index.ts +174 -0
- package/src/cli-go/auth.go +15 -0
- package/src/cli-go/config.go +165 -0
- package/src/cli-go/errors.go +57 -0
- package/src/cli-go/multipart.go +55 -0
- package/src/cli-go/output.go +109 -0
- package/src/cli-go/root.go +30 -0
- package/src/cli-go/stream.go +88 -0
- package/src/cli-go/version.go +5 -0
- package/src/cli.ts +204 -0
- package/src/client/error.ts +27 -0
- package/src/client/http.ts +502 -0
- package/src/client/index.ts +127 -0
- package/src/client/sdk.ts +265 -0
- package/src/client/sse.ts +102 -0
- package/src/client/types.ts +211 -0
- package/src/client/ws.ts +115 -0
- package/src/client-go/errors.go +207 -0
- package/src/client-go/invalidation.go +373 -0
- package/src/client-go/realtime.go +317 -0
- package/src/client-go/result.go +20 -0
- package/src/client-go/runtime.go +413 -0
- package/src/client-go/sse.go +149 -0
- package/src/client-go/transport.go +452 -0
- package/src/client-go/ws.go +104 -0
- package/src/client-mcp/README.md +10 -0
- package/src/client-mcp/server.ts +94 -0
- package/src/client-mcp/types.ts +19 -0
- package/src/client-python/_errors.py +115 -0
- package/src/client-python/_invalidation.py +578 -0
- package/src/client-python/_realtime.py +156 -0
- package/src/client-python/_runtime.py +469 -0
- package/src/client-python/_sse.py +99 -0
- package/src/client-python/_transport.py +335 -0
- package/src/client-python/_ws.py +131 -0
- package/src/client-python/py.typed +0 -0
- package/src/client-rust/errors.rs +200 -0
- package/src/client-rust/invalidation.rs +441 -0
- package/src/client-rust/invalidation_sync.rs +306 -0
- package/src/client-rust/realtime.rs +695 -0
- package/src/client-rust/result.rs +22 -0
- package/src/client-rust/runtime.rs +547 -0
- package/src/client-rust/runtime_sync.rs +452 -0
- package/src/client-rust/sse.rs +167 -0
- package/src/client-rust/ws.rs +143 -0
- package/src/cloudflare/realtime.ts +236 -0
- package/src/codegen-go-cli.ts +1180 -0
- package/src/codegen-go.ts +1010 -0
- package/src/codegen-hash.ts +8 -0
- package/src/codegen-ir.ts +517 -0
- package/src/codegen-loaders.ts +53 -0
- package/src/codegen-mcp.ts +364 -0
- package/src/codegen-python.ts +1028 -0
- package/src/codegen-rust.ts +1652 -0
- package/src/codegen-sanitize.ts +67 -0
- package/src/codegen-schema-naming.ts +146 -0
- package/src/codegen.ts +5015 -0
- package/src/context.ts +210 -0
- package/src/cookie-sign.ts +58 -0
- package/src/cookie.ts +89 -0
- package/src/cors.ts +96 -0
- package/src/crypto.ts +19 -0
- package/src/csrf.ts +54 -0
- package/src/curl-logger.ts +235 -0
- package/src/detect-runtime.ts +19 -0
- package/src/error.ts +52 -0
- package/src/errors.ts +87 -0
- package/src/etag.ts +74 -0
- package/src/feature-load.ts +23 -0
- package/src/go-type-emitter.ts +404 -0
- package/src/honey-response.ts +354 -0
- package/src/i18n-slot.ts +20 -0
- package/src/i18n.ts +234 -0
- package/src/index.ts +3843 -0
- package/src/init.ts +149 -0
- package/src/input.ts +8 -0
- package/src/ip-restrict.ts +192 -0
- package/src/logger.ts +149 -0
- package/src/middleware.ts +199 -0
- package/src/node-request.ts +240 -0
- package/src/node.ts +212 -0
- package/src/openapi/register.ts +19 -0
- package/src/openapi/scalar.ts +33 -0
- package/src/openapi/spec-factory.ts +46 -0
- package/src/openapi/spec.ts +31 -0
- package/src/openapi/swagger.ts +34 -0
- package/src/plugin.ts +771 -0
- package/src/powered-by.ts +17 -0
- package/src/pretty-json.ts +39 -0
- package/src/proxy.ts +155 -0
- package/src/python-type-emitter.ts +144 -0
- package/src/realtime/buffer.ts +101 -0
- package/src/realtime/bus.ts +93 -0
- package/src/realtime/longpoll.ts +148 -0
- package/src/realtime/protocol.ts +128 -0
- package/src/realtime/route.ts +104 -0
- package/src/realtime/runtime.ts +430 -0
- package/src/request-id.ts +23 -0
- package/src/request-to-curl.ts +34 -0
- package/src/response.ts +414 -0
- package/src/rust-type-emitter.ts +412 -0
- package/src/secure-headers.ts +69 -0
- package/src/serve-register.ts +8 -0
- package/src/serve-slot.ts +20 -0
- package/src/serve.ts +141 -0
- package/src/server-timing.ts +66 -0
- package/src/static.ts +60 -0
- package/src/telemetry/otel.ts +139 -0
- package/src/testing.ts +114 -0
- package/src/timeout.ts +36 -0
- package/src/tree.ts +464 -0
- package/src/ts-type-emitter.ts +106 -0
- package/src/type-emitter.ts +524 -0
- package/src/type-extractor.ts +749 -0
- package/src/types.ts +638 -0
- package/src/validation.ts +300 -0
- package/src/ws/bun.ts +89 -0
- package/src/ws/cloudflare.ts +133 -0
- package/src/ws/deno.ts +109 -0
- package/src/ws/node.ts +161 -0
- package/src/yaml.ts +80 -0
package/src/response.ts
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import type { CookieOptions } from "./cookie.ts";
|
|
2
|
+
import { serializeCookie } from "./cookie.ts";
|
|
3
|
+
import type { HoneyError } from "./error.ts";
|
|
4
|
+
import { createHoneyResponse } from "./honey-response.ts";
|
|
5
|
+
import type { StatusKey } from "./types.ts";
|
|
6
|
+
import { statusKeyToCode } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
export type { CookieOptions } from "./cookie.ts";
|
|
9
|
+
export { serializeCookie } from "./cookie.ts";
|
|
10
|
+
|
|
11
|
+
export type ResponseOptions = {
|
|
12
|
+
cookies?: Record<string, CookieOptions>;
|
|
13
|
+
headers?: Record<string, string>;
|
|
14
|
+
status?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type SSEEvent = {
|
|
18
|
+
data: object | string;
|
|
19
|
+
event: string;
|
|
20
|
+
id?: string;
|
|
21
|
+
retry?: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type SSEOptions = ResponseOptions & {
|
|
25
|
+
defaultRetry?: number;
|
|
26
|
+
keepalive?: number;
|
|
27
|
+
lastEventId?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type SSEStream = {
|
|
31
|
+
close(): void;
|
|
32
|
+
lastEventId: string | undefined;
|
|
33
|
+
send(event: SSEEvent): Promise<void>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type ErrorFormatter = (
|
|
37
|
+
error: HoneyError,
|
|
38
|
+
defaultShape: Record<string, unknown>,
|
|
39
|
+
) => Record<string, unknown>;
|
|
40
|
+
|
|
41
|
+
function applyResponseOptions(headers: Headers, opts?: ResponseOptions): void {
|
|
42
|
+
if (opts?.headers) {
|
|
43
|
+
for (const [k, v] of Object.entries(opts.headers)) {
|
|
44
|
+
headers.set(k, v);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (opts?.cookies) {
|
|
48
|
+
for (const [name, cookieOpts] of Object.entries(opts.cookies)) {
|
|
49
|
+
headers.append("set-cookie", serializeCookie(name, cookieOpts));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function applyPlainOptions(
|
|
55
|
+
headers: Record<string, string | string[]>,
|
|
56
|
+
opts?: ResponseOptions,
|
|
57
|
+
): void {
|
|
58
|
+
if (opts?.headers) {
|
|
59
|
+
for (const [k, v] of Object.entries(opts.headers)) {
|
|
60
|
+
headers[k.toLowerCase()] = v;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (opts?.cookies) {
|
|
64
|
+
const cookies: string[] = [];
|
|
65
|
+
for (const [name, cookieOpts] of Object.entries(opts.cookies)) {
|
|
66
|
+
cookies.push(serializeCookie(name, cookieOpts));
|
|
67
|
+
}
|
|
68
|
+
headers["set-cookie"] = cookies;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* phantom brands — exist only at type level, never assigned at runtime */
|
|
73
|
+
declare const CONTENT_TYPE: unique symbol;
|
|
74
|
+
declare const STATUS_KEY: unique symbol;
|
|
75
|
+
|
|
76
|
+
/** Branded Response — phantom content-type + status-key at type level, native Response at runtime */
|
|
77
|
+
export type TypedResponse<
|
|
78
|
+
CT extends string = string,
|
|
79
|
+
SK extends string = string,
|
|
80
|
+
> = Response & {
|
|
81
|
+
readonly [CONTENT_TYPE]: CT;
|
|
82
|
+
readonly [STATUS_KEY]: SK;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Cast a native Response to TypedResponse — zero runtime cost */
|
|
86
|
+
function typed<CT extends string, SK extends string>(
|
|
87
|
+
response: Response,
|
|
88
|
+
): TypedResponse<CT, SK> {
|
|
89
|
+
return response as TypedResponse<CT, SK>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Node adapter reads this to `res.end(payload)` without draining the Fetch body. */
|
|
93
|
+
const RAW_BODY = Symbol.for("honey.rawBody");
|
|
94
|
+
|
|
95
|
+
function withRawBody(response: Response, body: string | Uint8Array): Response {
|
|
96
|
+
Object.defineProperty(response, RAW_BODY, { value: body });
|
|
97
|
+
return response;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* pre-allocated header objects — Bun optimizes plain objects better than Headers instances */
|
|
101
|
+
const JSON_HEADERS = { "content-type": "application/json" };
|
|
102
|
+
const TEXT_HEADERS = { "content-type": "text/plain; charset=utf-8" };
|
|
103
|
+
const HTML_HEADERS = { "content-type": "text/html; charset=utf-8" };
|
|
104
|
+
const CSV_HEADERS = { "content-type": "text/csv; charset=utf-8" };
|
|
105
|
+
const BINARY_HEADERS = { "content-type": "application/octet-stream" };
|
|
106
|
+
|
|
107
|
+
export class HoneyRes {
|
|
108
|
+
protected readonly _nodeOut: boolean;
|
|
109
|
+
|
|
110
|
+
constructor(nodeOut = false) {
|
|
111
|
+
this._nodeOut = nodeOut;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private _known<CT extends string, SK extends string>(
|
|
115
|
+
status: number,
|
|
116
|
+
headers: Record<string, string>,
|
|
117
|
+
raw: string | Uint8Array,
|
|
118
|
+
opts?: ResponseOptions,
|
|
119
|
+
): TypedResponse<CT, SK> {
|
|
120
|
+
if (this._nodeOut) {
|
|
121
|
+
if (!opts?.headers && !opts?.cookies) {
|
|
122
|
+
return typed(createHoneyResponse({ headers, raw, status }));
|
|
123
|
+
}
|
|
124
|
+
const plain: Record<string, string | string[]> = { ...headers };
|
|
125
|
+
applyPlainOptions(plain, opts);
|
|
126
|
+
return typed(createHoneyResponse({ headers: plain, raw, status }));
|
|
127
|
+
}
|
|
128
|
+
if (!opts?.headers && !opts?.cookies) {
|
|
129
|
+
return typed(withRawBody(new Response(raw as BodyInit, { headers, status }), raw));
|
|
130
|
+
}
|
|
131
|
+
const native = new Headers(headers);
|
|
132
|
+
applyResponseOptions(native, opts);
|
|
133
|
+
return typed(withRawBody(new Response(raw as BodyInit, { headers: native, status }), raw));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
binary<SK extends StatusKey>(
|
|
137
|
+
statusKey: SK,
|
|
138
|
+
body: ArrayBuffer | Uint8Array<ArrayBuffer>,
|
|
139
|
+
opts?: ResponseOptions,
|
|
140
|
+
): TypedResponse<"application/octet-stream", SK> {
|
|
141
|
+
const raw = body instanceof Uint8Array ? body : new Uint8Array(body);
|
|
142
|
+
return this._known(statusKeyToCode[statusKey], BINARY_HEADERS, raw, opts);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
csv<SK extends StatusKey>(
|
|
146
|
+
statusKey: SK,
|
|
147
|
+
body: string,
|
|
148
|
+
opts?: ResponseOptions,
|
|
149
|
+
): TypedResponse<"text/csv", SK> {
|
|
150
|
+
return this._known(statusKeyToCode[statusKey], CSV_HEADERS, body, opts);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
html<SK extends StatusKey>(
|
|
154
|
+
statusKey: SK,
|
|
155
|
+
body: string,
|
|
156
|
+
opts?: ResponseOptions,
|
|
157
|
+
): TypedResponse<"text/html", SK> {
|
|
158
|
+
return this._known(statusKeyToCode[statusKey], HTML_HEADERS, body, opts);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
json<SK extends StatusKey>(
|
|
162
|
+
statusKey: SK,
|
|
163
|
+
data: unknown,
|
|
164
|
+
opts?: ResponseOptions,
|
|
165
|
+
): TypedResponse<"application/json", SK> {
|
|
166
|
+
return this._known(statusKeyToCode[statusKey], JSON_HEADERS, JSON.stringify(data), opts);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
noContent(opts?: ResponseOptions): TypedResponse<"none", "no_content"> {
|
|
170
|
+
if (this._nodeOut) {
|
|
171
|
+
const headers: Record<string, string | string[]> = {};
|
|
172
|
+
applyPlainOptions(headers, opts);
|
|
173
|
+
return typed(createHoneyResponse({ headers, raw: null, status: 204 }));
|
|
174
|
+
}
|
|
175
|
+
const headers = new Headers();
|
|
176
|
+
applyResponseOptions(headers, opts);
|
|
177
|
+
return typed(new Response(null, { headers, status: 204 }));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
raw(response: Response): TypedResponse {
|
|
181
|
+
return typed(response);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
redirect(
|
|
185
|
+
url: string,
|
|
186
|
+
opts?: ResponseOptions,
|
|
187
|
+
): TypedResponse<"none", "found"> {
|
|
188
|
+
if (this._nodeOut) {
|
|
189
|
+
const headers: Record<string, string | string[]> = { location: url };
|
|
190
|
+
applyPlainOptions(headers, opts);
|
|
191
|
+
return typed(createHoneyResponse({ headers, raw: null, status: opts?.status ?? 302 }));
|
|
192
|
+
}
|
|
193
|
+
const headers = new Headers({ location: url });
|
|
194
|
+
applyResponseOptions(headers, opts);
|
|
195
|
+
return typed(new Response(null, { headers, status: opts?.status ?? 302 }));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
sse(
|
|
199
|
+
callback: (stream: SSEStream) => Promise<void>,
|
|
200
|
+
opts?: SSEOptions,
|
|
201
|
+
): TypedResponse<"text/event-stream", "ok"> {
|
|
202
|
+
const { readable, writable } = new TransformStream();
|
|
203
|
+
const writer = writable.getWriter();
|
|
204
|
+
const encoder = new TextEncoder();
|
|
205
|
+
let keepaliveTimer: ReturnType<typeof setInterval> | undefined;
|
|
206
|
+
|
|
207
|
+
let closed = false;
|
|
208
|
+
const sseStream: SSEStream = {
|
|
209
|
+
close() {
|
|
210
|
+
if (closed) return;
|
|
211
|
+
closed = true;
|
|
212
|
+
if (keepaliveTimer !== undefined) clearInterval(keepaliveTimer);
|
|
213
|
+
writer.close();
|
|
214
|
+
},
|
|
215
|
+
lastEventId: opts?.lastEventId,
|
|
216
|
+
send(event) {
|
|
217
|
+
if (/[\r\n]/.test(event.event)) {
|
|
218
|
+
throw new Error("SSE event name must not contain newlines");
|
|
219
|
+
}
|
|
220
|
+
if (event.id && /[\r\n]/.test(event.id)) {
|
|
221
|
+
throw new Error("SSE id must not contain newlines");
|
|
222
|
+
}
|
|
223
|
+
const dataStr =
|
|
224
|
+
typeof event.data === "string"
|
|
225
|
+
? event.data
|
|
226
|
+
: JSON.stringify(event.data);
|
|
227
|
+
const dataLines = dataStr
|
|
228
|
+
.split(/\r\n|\r|\n/)
|
|
229
|
+
.map((line) => `data: ${line}`)
|
|
230
|
+
.join("\n");
|
|
231
|
+
let msg = `event: ${event.event}\n${dataLines}\n`;
|
|
232
|
+
if (event.id) msg += `id: ${event.id}\n`;
|
|
233
|
+
/** SSE retry must be a non-negative integer per spec — skip if invalid */
|
|
234
|
+
if (
|
|
235
|
+
event.retry !== undefined &&
|
|
236
|
+
Number.isFinite(event.retry) &&
|
|
237
|
+
event.retry >= 0
|
|
238
|
+
) {
|
|
239
|
+
msg += `retry: ${Math.floor(event.retry)}\n`;
|
|
240
|
+
}
|
|
241
|
+
msg += "\n";
|
|
242
|
+
return writer.write(encoder.encode(msg));
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
if (opts?.defaultRetry !== undefined) {
|
|
247
|
+
writer.write(encoder.encode(`retry: ${opts.defaultRetry}\n\n`));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (opts?.keepalive !== undefined && opts.keepalive > 0) {
|
|
251
|
+
keepaliveTimer = setInterval(() => {
|
|
252
|
+
writer.write(encoder.encode(": heartbeat\n\n")).catch(() => {
|
|
253
|
+
if (keepaliveTimer !== undefined) clearInterval(keepaliveTimer);
|
|
254
|
+
});
|
|
255
|
+
}, opts.keepalive);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
void callback(sseStream).then(
|
|
259
|
+
async () => {
|
|
260
|
+
if (closed) return;
|
|
261
|
+
closed = true;
|
|
262
|
+
if (keepaliveTimer !== undefined) clearInterval(keepaliveTimer);
|
|
263
|
+
try {
|
|
264
|
+
await writer.close();
|
|
265
|
+
} catch {}
|
|
266
|
+
},
|
|
267
|
+
async () => {
|
|
268
|
+
if (closed) return;
|
|
269
|
+
closed = true;
|
|
270
|
+
if (keepaliveTimer !== undefined) clearInterval(keepaliveTimer);
|
|
271
|
+
try {
|
|
272
|
+
await writer.close();
|
|
273
|
+
} catch {}
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const headers = new Headers({
|
|
278
|
+
"cache-control": "no-cache",
|
|
279
|
+
connection: "keep-alive",
|
|
280
|
+
"content-type": "text/event-stream",
|
|
281
|
+
});
|
|
282
|
+
applyResponseOptions(headers, opts);
|
|
283
|
+
return typed(new Response(readable, { headers, status: 200 }));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
generate(
|
|
287
|
+
generator:
|
|
288
|
+
| AsyncGenerator<string, void, unknown>
|
|
289
|
+
| Generator<string, void, unknown>,
|
|
290
|
+
opts?: { contentType?: string; status?: number },
|
|
291
|
+
): TypedResponse<string, "ok"> {
|
|
292
|
+
const encoder = new TextEncoder();
|
|
293
|
+
const gen = generator;
|
|
294
|
+
const readable = new ReadableStream({
|
|
295
|
+
async pull(controller) {
|
|
296
|
+
try {
|
|
297
|
+
const { done, value } = await gen.next();
|
|
298
|
+
if (done) {
|
|
299
|
+
controller.close();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
controller.enqueue(encoder.encode(value));
|
|
303
|
+
} catch {
|
|
304
|
+
controller.close();
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
const headers = new Headers({
|
|
309
|
+
"content-type": opts?.contentType ?? "application/octet-stream",
|
|
310
|
+
});
|
|
311
|
+
return typed(
|
|
312
|
+
new Response(readable, { headers, status: opts?.status ?? 200 }),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
stream(
|
|
317
|
+
callback: (writable: WritableStream) => Promise<void>,
|
|
318
|
+
opts?: ResponseOptions,
|
|
319
|
+
): TypedResponse<"application/octet-stream", "ok"> {
|
|
320
|
+
const { readable, writable } = new TransformStream();
|
|
321
|
+
void callback(writable).then(
|
|
322
|
+
async () => {
|
|
323
|
+
try {
|
|
324
|
+
await writable.close();
|
|
325
|
+
} catch {}
|
|
326
|
+
},
|
|
327
|
+
async () => {
|
|
328
|
+
try {
|
|
329
|
+
await writable.close();
|
|
330
|
+
} catch {}
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
const headers = new Headers();
|
|
334
|
+
applyResponseOptions(headers, opts);
|
|
335
|
+
return typed(
|
|
336
|
+
new Response(readable, { headers, status: opts?.status ?? 200 }),
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
text<SK extends StatusKey>(
|
|
341
|
+
statusKey: SK,
|
|
342
|
+
body: string,
|
|
343
|
+
opts?: ResponseOptions,
|
|
344
|
+
): TypedResponse<"text/plain", SK> {
|
|
345
|
+
return this._known(statusKeyToCode[statusKey], TEXT_HEADERS, body, opts);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
xml<SK extends StatusKey>(
|
|
349
|
+
statusKey: SK,
|
|
350
|
+
body: string,
|
|
351
|
+
opts?: ResponseOptions,
|
|
352
|
+
): TypedResponse<"application/xml", SK> {
|
|
353
|
+
return this._known(statusKeyToCode[statusKey], { "content-type": "application/xml" }, body, opts);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export type CustomErrorFormatter = (
|
|
358
|
+
error: HoneyError,
|
|
359
|
+
data: Record<string, unknown>,
|
|
360
|
+
) => Record<string, unknown>;
|
|
361
|
+
|
|
362
|
+
export function createErrorResponse(
|
|
363
|
+
error: HoneyError,
|
|
364
|
+
defaultFormatter: ErrorFormatter,
|
|
365
|
+
customFormatter?: CustomErrorFormatter | null,
|
|
366
|
+
): Response {
|
|
367
|
+
let body: Record<string, unknown>;
|
|
368
|
+
|
|
369
|
+
if (error.data !== undefined) {
|
|
370
|
+
/* custom schema error — apply customErrorFormatter if set, else use data as-is */
|
|
371
|
+
const rawData = error.data as Record<string, unknown>;
|
|
372
|
+
if (customFormatter) {
|
|
373
|
+
try {
|
|
374
|
+
body = customFormatter(error, rawData);
|
|
375
|
+
} catch {
|
|
376
|
+
body = rawData;
|
|
377
|
+
}
|
|
378
|
+
} else {
|
|
379
|
+
body = rawData;
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
/* standard error — build default shape, apply defaultErrorFormatter */
|
|
383
|
+
const defaultShape: Record<string, unknown> = {
|
|
384
|
+
error_key: error.errorKey,
|
|
385
|
+
fields: error.fields,
|
|
386
|
+
message: error.message,
|
|
387
|
+
status: error.status,
|
|
388
|
+
status_key: error.statusKey,
|
|
389
|
+
success: false,
|
|
390
|
+
...(error.vars ? { vars: error.vars } : {}),
|
|
391
|
+
};
|
|
392
|
+
try {
|
|
393
|
+
body = defaultFormatter(error, defaultShape);
|
|
394
|
+
} catch {
|
|
395
|
+
/* formatter crashed — fall back to default shape */
|
|
396
|
+
body = defaultShape;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const headers: Record<string, string> = { "content-type": "application/json" }
|
|
401
|
+
if (error.headers) {
|
|
402
|
+
for (const [k, v] of Object.entries(error.headers)) {
|
|
403
|
+
headers[k] = v
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const payload = JSON.stringify(body);
|
|
407
|
+
return withRawBody(
|
|
408
|
+
new Response(payload, {
|
|
409
|
+
headers,
|
|
410
|
+
status: error.status,
|
|
411
|
+
}),
|
|
412
|
+
payload,
|
|
413
|
+
);
|
|
414
|
+
}
|