@alxia/core 0.1.0 → 0.2.1

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.
Files changed (60) hide show
  1. package/README.md +104 -3
  2. package/dist/app/alxia.d.ts +53 -5
  3. package/dist/app/alxia.d.ts.map +1 -1
  4. package/dist/app/chain.d.ts +5 -8
  5. package/dist/app/chain.d.ts.map +1 -1
  6. package/dist/app/context.d.ts +12 -0
  7. package/dist/app/context.d.ts.map +1 -0
  8. package/dist/app/definition.d.ts +22 -1
  9. package/dist/app/definition.d.ts.map +1 -1
  10. package/dist/app/pipeline.d.ts.map +1 -1
  11. package/dist/app/refusal.d.ts +15 -0
  12. package/dist/app/refusal.d.ts.map +1 -0
  13. package/dist/app/send.d.ts +24 -1
  14. package/dist/app/send.d.ts.map +1 -1
  15. package/dist/app/socket.d.ts +1 -1
  16. package/dist/app/socket.d.ts.map +1 -1
  17. package/dist/app/types/index.d.ts +1 -0
  18. package/dist/app/types/index.d.ts.map +1 -1
  19. package/dist/app/types/refusal.d.ts +94 -0
  20. package/dist/app/types/refusal.d.ts.map +1 -0
  21. package/dist/app/types/route-table.d.ts +3 -2
  22. package/dist/app/types/route-table.d.ts.map +1 -1
  23. package/dist/app/types/schema.d.ts +7 -0
  24. package/dist/app/types/schema.d.ts.map +1 -1
  25. package/dist/app/types/valid-schema.d.ts +1 -1
  26. package/dist/app/types/valid-schema.d.ts.map +1 -1
  27. package/dist/errors/errors.d.ts +47 -1
  28. package/dist/errors/errors.d.ts.map +1 -1
  29. package/dist/index.d.ts +5 -3
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +413 -103
  32. package/dist/index.js.map +20 -13
  33. package/dist/reply/problem.d.ts +33 -0
  34. package/dist/reply/problem.d.ts.map +1 -0
  35. package/dist/reply/reply.d.ts.map +1 -1
  36. package/dist/request/limit.d.ts +13 -0
  37. package/dist/request/limit.d.ts.map +1 -0
  38. package/dist/request/read.d.ts +3 -2
  39. package/dist/request/read.d.ts.map +1 -1
  40. package/dist/sse/async-iterable.d.ts +6 -0
  41. package/dist/sse/async-iterable.d.ts.map +1 -0
  42. package/dist/sse/event-stream.d.ts +29 -6
  43. package/dist/sse/event-stream.d.ts.map +1 -1
  44. package/dist/sse/frame.d.ts +35 -0
  45. package/dist/sse/frame.d.ts.map +1 -0
  46. package/dist/sse/named-events.d.ts +70 -0
  47. package/dist/sse/named-events.d.ts.map +1 -0
  48. package/docs/README.md +3 -3
  49. package/docs/guide/groups-and-plugins.md +8 -1
  50. package/docs/guide/hooks.md +131 -4
  51. package/docs/guide/replies.md +47 -0
  52. package/docs/guide/routes.md +115 -1
  53. package/docs/guide/server-sent-events.md +177 -5
  54. package/docs/guide/serving.md +1 -1
  55. package/docs/guide/types.md +3 -1
  56. package/docs/guide/websockets.md +1 -1
  57. package/docs/guide/writing-a-plugin.md +1 -1
  58. package/docs/roadmap.md +35 -2
  59. package/docs/troubleshooting.md +331 -7
  60. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,3 +1,78 @@
1
+ // src/errors/errors.ts
2
+ class HttpError extends Error {
3
+ name = "HttpError";
4
+ status;
5
+ body;
6
+ constructor(status, body, message) {
7
+ super(message ?? `HTTP ${status}`);
8
+ this.status = status;
9
+ this.body = body;
10
+ }
11
+ }
12
+
13
+ class ContentTooLargeError extends HttpError {
14
+ name = "ContentTooLargeError";
15
+ limit;
16
+ constructor(limit) {
17
+ super(413, { error: "content_too_large", limit }, `The request body is larger than the route's limit of ${limit} bytes`);
18
+ this.limit = limit;
19
+ }
20
+ }
21
+
22
+ class ResponseValidationError extends Error {
23
+ name = "ResponseValidationError";
24
+ status;
25
+ issues;
26
+ constructor(method, path, status, issues, message) {
27
+ super(message ?? `${method} ${path}: the ${status} reply does not match its schema: ${issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ")}`);
28
+ this.status = status;
29
+ this.issues = issues;
30
+ }
31
+ static undeclared(method, path, status) {
32
+ return new ResponseValidationError(method, path, status, [
33
+ {
34
+ target: "body",
35
+ path: [],
36
+ code: "undeclared_status",
37
+ message: `the route declares no ${status} reply`
38
+ }
39
+ ], `${method} ${path} declares no ${status} reply`);
40
+ }
41
+ }
42
+
43
+ // src/request/limit.ts
44
+ function limitBody(request, limit) {
45
+ const source = request.body;
46
+ if (source === null || request.bodyUsed || source.locked)
47
+ return request;
48
+ const declared = Number(request.headers.get("content-length") ?? Number.NaN);
49
+ let body;
50
+ if (declared > limit) {
51
+ source.cancel().catch(() => {});
52
+ body = new ReadableStream({
53
+ start: (controller) => controller.error(new ContentTooLargeError(limit))
54
+ });
55
+ } else {
56
+ let count = 0;
57
+ body = source.pipeThrough(new TransformStream({
58
+ transform(chunk, controller) {
59
+ count += chunk.byteLength;
60
+ if (count > limit) {
61
+ controller.error(new ContentTooLargeError(limit));
62
+ } else
63
+ controller.enqueue(chunk);
64
+ }
65
+ }));
66
+ }
67
+ return new Request(request, { body, duplex: "half" });
68
+ }
69
+ function checkLimit(limit, where) {
70
+ if (!Number.isSafeInteger(limit) || limit < 0) {
71
+ throw new TypeError(`${where}: bodyLimit must be a whole number of bytes, 0 or more; got ${limit}`);
72
+ }
73
+ return limit;
74
+ }
75
+
1
76
  // src/router/compile.ts
2
77
  var LITERAL = 0;
3
78
  var PARAMETER = 1;
@@ -233,6 +308,11 @@ class Router {
233
308
  // src/static/serve.ts
234
309
  import { extname as extname2, resolve, sep } from "node:path";
235
310
 
311
+ // src/sse/async-iterable.ts
312
+ function isAsyncIterable(value) {
313
+ return value !== null && typeof value === "object" && Symbol.asyncIterator in value && !(value instanceof ReadableStream);
314
+ }
315
+
236
316
  // src/schema/standard-schema.ts
237
317
  async function check(schema, value, target) {
238
318
  let result = schema["~standard"].validate(value);
@@ -254,8 +334,143 @@ async function check(schema, value, target) {
254
334
  };
255
335
  }
256
336
 
337
+ // src/sse/frame.ts
338
+ class Frame {
339
+ event;
340
+ data;
341
+ id;
342
+ retry;
343
+ constructor(event, data, id, retry) {
344
+ this.event = event;
345
+ this.data = data;
346
+ this.id = id;
347
+ this.retry = retry;
348
+ }
349
+ }
350
+ function frameText(value) {
351
+ const lines = [];
352
+ let data = value;
353
+ if (value instanceof Frame) {
354
+ lines.push(`event: ${value.event}`);
355
+ if (value.id !== undefined)
356
+ lines.push(`id: ${value.id}`);
357
+ if (value.retry !== undefined)
358
+ lines.push(`retry: ${value.retry}`);
359
+ data = value.data;
360
+ }
361
+ const json = JSON.stringify(data) ?? "null";
362
+ for (const line of json.split(/\r\n|\r|\n/))
363
+ lines.push(`data: ${line}`);
364
+ return `${lines.join(`
365
+ `)}
366
+
367
+ `;
368
+ }
369
+ function mismatch(issues, name) {
370
+ return new TypeError(`An event does not match its schema: ${issues.map((issue) => {
371
+ const path = name === undefined ? issue.path : [name, ...issue.path];
372
+ return `${path.join(".") || "(root)"}: ${issue.message}`;
373
+ }).join("; ")}`);
374
+ }
375
+ var BREAK = /[\r\n\0]/;
376
+ function refuseName(name) {
377
+ if (name === "")
378
+ return "An event name must not be empty";
379
+ if (BREAK.test(name)) {
380
+ return `An event name must not hold a line break or a NUL: ${JSON.stringify(name)}`;
381
+ }
382
+ return;
383
+ }
384
+ function refuseId(id) {
385
+ if (id === undefined)
386
+ return;
387
+ if (typeof id !== "string")
388
+ return "An event id must be a string";
389
+ if (BREAK.test(id)) {
390
+ return `An event id must not hold a line break or a NUL: ${JSON.stringify(id)}`;
391
+ }
392
+ return;
393
+ }
394
+ function refuseRetry(retry) {
395
+ if (retry === undefined)
396
+ return;
397
+ if (typeof retry !== "number" || !Number.isSafeInteger(retry) || retry < 0) {
398
+ return `An event retry must be a whole number of milliseconds, 0 or more: ${String(retry)}`;
399
+ }
400
+ return;
401
+ }
402
+
403
+ // src/sse/named-events.ts
404
+ function namedEventStream(events) {
405
+ const names = Object.keys(events);
406
+ if (names.length === 0) {
407
+ throw new TypeError("A named event stream declares at least one event");
408
+ }
409
+ for (const name of names) {
410
+ const refused = refuseName(name);
411
+ if (refused !== undefined)
412
+ throw new TypeError(refused);
413
+ const schema = events[name];
414
+ if (schema === null || typeof schema !== "object" || !("~standard" in schema)) {
415
+ throw new TypeError(`The event ${JSON.stringify(name)} is not a Standard Schema`);
416
+ }
417
+ }
418
+ return {
419
+ "~events": events,
420
+ event: (event, data, fields) => ({
421
+ ...fields,
422
+ event,
423
+ data
424
+ }),
425
+ "~standard": {
426
+ version: 1,
427
+ vendor: "alxia",
428
+ validate: (value) => {
429
+ if (!isAsyncIterable(value)) {
430
+ return {
431
+ issues: [
432
+ { message: "An event stream replies with an async iterable" }
433
+ ]
434
+ };
435
+ }
436
+ return { value: toFrames(events, value, true) };
437
+ }
438
+ }
439
+ };
440
+ }
441
+ function isNamedEventStreamSchema(schema) {
442
+ return "~events" in schema;
443
+ }
444
+ async function* toFrames(events, values, validate) {
445
+ for await (const value of values) {
446
+ if (value === null || typeof value !== "object" || !("event" in value)) {
447
+ throw new TypeError("An event of a named stream is an object { event, data }");
448
+ }
449
+ const { event, data, id, retry } = value;
450
+ const schema = typeof event === "string" && Object.hasOwn(events, event) ? events[event] : undefined;
451
+ if (schema === undefined) {
452
+ throw new TypeError(`The event ${JSON.stringify(event)} is not declared: ${Object.keys(events).join(", ")}`);
453
+ }
454
+ const refused = refuseId(id) ?? refuseRetry(retry);
455
+ if (refused !== undefined)
456
+ throw new TypeError(refused);
457
+ let output = data;
458
+ if (validate) {
459
+ const checked = await check(schema, data, "body");
460
+ if (!checked.ok) {
461
+ throw mismatch(checked.issues, event);
462
+ }
463
+ output = checked.value;
464
+ }
465
+ yield new Frame(event, output, id, retry);
466
+ }
467
+ }
468
+
257
469
  // src/sse/event-stream.ts
258
- function eventStream(item) {
470
+ function eventStream(schema) {
471
+ if (!isStandardSchema(schema))
472
+ return namedEventStream(schema);
473
+ const item = schema;
259
474
  return {
260
475
  "~eventStream": item,
261
476
  "~standard": {
@@ -277,14 +492,14 @@ function eventStream(item) {
277
492
  async function* checkEach(item, values) {
278
493
  for await (const value of values) {
279
494
  const checked = await check(item, value, "body");
280
- if (!checked.ok) {
281
- throw new TypeError(`An event does not match its schema: ${checked.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ")}`);
282
- }
495
+ if (!checked.ok)
496
+ throw mismatch(checked.issues);
283
497
  yield checked.value;
284
498
  }
285
499
  }
286
- function isAsyncIterable(value) {
287
- return value !== null && typeof value === "object" && Symbol.asyncIterator in value && !(value instanceof ReadableStream);
500
+ function isStandardSchema(value) {
501
+ const standard = value["~standard"];
502
+ return standard?.version === 1 && typeof standard.vendor === "string";
288
503
  }
289
504
  function isEventStreamSchema(schema) {
290
505
  return "~eventStream" in schema;
@@ -327,12 +542,7 @@ function toEventStream(values, signal) {
327
542
  controller.close();
328
543
  return;
329
544
  }
330
- const data = JSON.stringify(next.value) ?? "null";
331
- controller.enqueue(encoder.encode(`${data.split(`
332
- `).map((line) => `data: ${line}`).join(`
333
- `)}
334
-
335
- `));
545
+ controller.enqueue(encoder.encode(frameText(next.value)));
336
546
  } catch (error) {
337
547
  stop();
338
548
  console.error(error);
@@ -661,39 +871,6 @@ function isBunFile(blob) {
661
871
  return typeof blob.exists === "function";
662
872
  }
663
873
 
664
- // src/errors/errors.ts
665
- class HttpError extends Error {
666
- name = "HttpError";
667
- status;
668
- body;
669
- constructor(status, body, message) {
670
- super(message ?? `HTTP ${status}`);
671
- this.status = status;
672
- this.body = body;
673
- }
674
- }
675
-
676
- class ResponseValidationError extends Error {
677
- name = "ResponseValidationError";
678
- status;
679
- issues;
680
- constructor(method, path, status, issues, message) {
681
- super(message ?? `${method} ${path}: the ${status} reply does not match its schema: ${issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ")}`);
682
- this.status = status;
683
- this.issues = issues;
684
- }
685
- static undeclared(method, path, status) {
686
- return new ResponseValidationError(method, path, status, [
687
- {
688
- target: "body",
689
- path: [],
690
- code: "undeclared_status",
691
- message: `the route declares no ${status} reply`
692
- }
693
- ], `${method} ${path} declares no ${status} reply`);
694
- }
695
- }
696
-
697
874
  // src/request/read.ts
698
875
  function readQuery(url) {
699
876
  const query = {};
@@ -730,6 +907,8 @@ async function readBody(request, parsers = []) {
730
907
  try {
731
908
  return { ok: true, value: await custom.parse(request) };
732
909
  } catch (error) {
910
+ if (error instanceof ContentTooLargeError)
911
+ throw error;
733
912
  return {
734
913
  ok: false,
735
914
  issue: {
@@ -780,11 +959,51 @@ async function readBody(request, parsers = []) {
780
959
  return { ok: true, value: await request.arrayBuffer() };
781
960
  }
782
961
 
962
+ // src/app/context.ts
963
+ var redirect = (location, status) => new Reply(status ?? 302, undefined, {
964
+ headers: { location: String(location) }
965
+ });
966
+ function routeContext(definition, request, pathParams) {
967
+ let cookies;
968
+ const set = {
969
+ headers: new Headers,
970
+ get cookies() {
971
+ cookies ??= new Bun.CookieMap;
972
+ return cookies;
973
+ },
974
+ touched: () => cookies !== undefined
975
+ };
976
+ const limit = "bodyLimit" in definition ? definition.bodyLimit : undefined;
977
+ const ctx = {
978
+ ...request,
979
+ ...limit === undefined ? {} : { request: limitBody(request.request, limit) },
980
+ route: definition.path,
981
+ pathParams,
982
+ set,
983
+ reply: createReply,
984
+ redirect
985
+ };
986
+ return { ctx, set };
987
+ }
988
+
783
989
  // src/app/send.ts
784
990
  var internal = { error: "internal" };
785
991
  function internalError() {
786
992
  return toResponse(500, internal, new Headers);
787
993
  }
994
+ function clientGone(error, request) {
995
+ if (!request.signal.aborted)
996
+ return false;
997
+ return error === request.signal.reason || error instanceof DOMException && error.name === "AbortError";
998
+ }
999
+ function failed(error, request) {
1000
+ if (clientGone(error, request)) {
1001
+ return new Response(null, { status: CLIENT_GONE });
1002
+ }
1003
+ console.error(error);
1004
+ return internalError();
1005
+ }
1006
+ var CLIENT_GONE = 499;
788
1007
  function send2(reply, set, signal) {
789
1008
  const headers = new Headers(set.headers);
790
1009
  if (reply.headers !== undefined) {
@@ -810,17 +1029,23 @@ async function sendDeclared(route, reply, set, signal, validateResponses) {
810
1029
  if (responses === undefined || isRedirect(reply)) {
811
1030
  return send2(reply, set, signal);
812
1031
  }
1032
+ const checked = await checkReply(route.method, route.path, responses, reply, validateResponses);
1033
+ return send2(checked, set, signal);
1034
+ }
1035
+ async function checkReply(method, path, responses, reply, validateResponses) {
813
1036
  const schema = responses[reply.status];
814
1037
  if (schema === undefined) {
815
- throw ResponseValidationError.undeclared(route.method, route.path, reply.status);
1038
+ throw ResponseValidationError.undeclared(method, path, reply.status);
816
1039
  }
817
1040
  if (!validateResponses)
818
- return send2(reply, set, signal);
1041
+ return framed(schema, reply);
819
1042
  const checked = await check(schema, reply.body, "body");
820
1043
  if (!checked.ok) {
821
- throw new ResponseValidationError(route.method, route.path, reply.status, checked.issues);
1044
+ throw new ResponseValidationError(method, path, reply.status, checked.issues);
822
1045
  }
823
- return send2(new Reply(reply.status, checked.value, { headers: reply.headers ?? {} }), set, signal);
1046
+ return new Reply(reply.status, checked.value, {
1047
+ headers: reply.headers ?? {}
1048
+ });
824
1049
  }
825
1050
  function routingError(status, error, allowed) {
826
1051
  const headers = new Headers;
@@ -829,42 +1054,73 @@ function routingError(status, error, allowed) {
829
1054
  const body = { error };
830
1055
  return toResponse(status, body, headers);
831
1056
  }
1057
+ function framed(schema, reply) {
1058
+ if (!isNamedEventStreamSchema(schema) || !isAsyncIterable(reply.body)) {
1059
+ return reply;
1060
+ }
1061
+ return new Reply(reply.status, toFrames(schema["~events"], reply.body, false), { headers: reply.headers ?? {} });
1062
+ }
832
1063
  function isRedirect(reply) {
833
1064
  return reply.status >= 300 && reply.status < 400 && reply.body === undefined;
834
1065
  }
835
1066
 
836
- // src/app/chain.ts
837
- var redirect = (location, status) => new Reply(status ?? 302, undefined, {
838
- headers: { location: String(location) }
839
- });
840
- function routeContext(definition, request, pathParams) {
841
- let cookies;
842
- const set = {
843
- headers: new Headers,
844
- get cookies() {
845
- cookies ??= new Bun.CookieMap;
846
- return cookies;
847
- },
848
- touched: () => cookies !== undefined
849
- };
850
- const ctx = {
851
- ...request,
852
- route: definition.path,
853
- pathParams,
854
- set,
855
- reply: createReply,
856
- redirect
857
- };
858
- return { ctx, set };
1067
+ // src/app/refusal.ts
1068
+ async function refuse(definition, refusal, set, ctx, validateResponses) {
1069
+ const handler = definition.refusal;
1070
+ if (handler !== undefined) {
1071
+ let reply = handler.hook(refusal, ctx);
1072
+ if (reply instanceof Promise)
1073
+ reply = await reply;
1074
+ const method = "method" in definition ? definition.method : "WS";
1075
+ if (reply instanceof Reply) {
1076
+ if (handler.response !== undefined) {
1077
+ reply = await checkReply(method, definition.path, handler.response, reply, validateResponses);
1078
+ }
1079
+ return send2(withContentType2(reply, handler.contentType), set);
1080
+ }
1081
+ if (reply !== undefined) {
1082
+ throw new TypeError(`${method} ${definition.path}: the onRefusal hook returned neither a reply nor nothing.`);
1083
+ }
1084
+ }
1085
+ return send2(byDefault(refusal), set);
1086
+ }
1087
+ function byDefault(refusal) {
1088
+ switch (refusal.kind) {
1089
+ case "validation": {
1090
+ const body = {
1091
+ error: "validation",
1092
+ issues: refusal.issues
1093
+ };
1094
+ return new Reply(400, body);
1095
+ }
1096
+ case "body_limit": {
1097
+ const body = {
1098
+ error: "content_too_large",
1099
+ limit: refusal.limit
1100
+ };
1101
+ return new Reply(413, body);
1102
+ }
1103
+ }
859
1104
  }
860
- async function chain(definition, request, rawParams, set, ctx, parsers, last) {
1105
+ function withContentType2(reply, type) {
1106
+ if (type === undefined)
1107
+ return reply;
1108
+ const headers = new Headers(reply.headers);
1109
+ if (headers.has("content-type"))
1110
+ return reply;
1111
+ headers.set("content-type", type);
1112
+ return new Reply(reply.status, reply.body, { headers });
1113
+ }
1114
+
1115
+ // src/app/chain.ts
1116
+ async function chain(definition, request, rawParams, set, ctx, parsers, validateResponses, last) {
861
1117
  const hooks = definition.derive;
862
1118
  const socket = !("method" in definition);
863
1119
  const signal = request.request.signal;
864
1120
  const step = async (index) => {
865
1121
  const hook = hooks[index];
866
1122
  if (hook === undefined) {
867
- const refused = await validate(definition, request, rawParams, set, ctx, parsers);
1123
+ const refused = await validate(definition, request, rawParams, set, ctx, parsers, validateResponses);
868
1124
  return refused ?? last();
869
1125
  }
870
1126
  if (hook.kind === "wrap") {
@@ -887,9 +1143,10 @@ async function chain(definition, request, rawParams, set, ctx, parsers, last) {
887
1143
  };
888
1144
  return step(0);
889
1145
  }
890
- async function validate(definition, request, rawParams, set, ctx, parsers) {
1146
+ async function validate(definition, request, rawParams, set, ctx, parsers, validateResponses) {
891
1147
  const { schema } = definition;
892
1148
  const issues = [];
1149
+ let part;
893
1150
  const parts = [
894
1151
  ["params", schema.params, () => rawParams],
895
1152
  ["query", schema.query, () => readQuery(request.url)],
@@ -905,33 +1162,36 @@ async function validate(definition, request, rawParams, set, ctx, parsers) {
905
1162
  const checked = await check(partSchema, raw, target);
906
1163
  if (checked.ok)
907
1164
  ctx[target] = checked.value;
908
- else
1165
+ else {
1166
+ part ??= target;
909
1167
  issues.push(...checked.issues);
1168
+ }
910
1169
  }
911
1170
  ctx["body"] = undefined;
912
1171
  const bodySchema = "body" in schema ? schema.body : undefined;
913
1172
  if (bodySchema !== undefined) {
914
- const body = await readBody(request.request, parsers);
915
- if (!body.ok)
1173
+ const body = await readBody(ctx.request, parsers);
1174
+ if (!body.ok) {
1175
+ part ??= "body";
916
1176
  issues.push(body.issue);
917
- else {
1177
+ } else {
918
1178
  const checked = await check(bodySchema, body.value, "body");
919
1179
  if (checked.ok)
920
1180
  ctx["body"] = checked.value;
921
- else
1181
+ else {
1182
+ part ??= "body";
922
1183
  issues.push(...checked.issues);
1184
+ }
923
1185
  }
924
1186
  }
925
- if (issues.length > 0) {
926
- const body = { error: "validation", issues };
927
- return send2(new Reply(400, body), set);
928
- }
929
- return;
1187
+ if (part === undefined)
1188
+ return;
1189
+ return refuse(definition, { kind: "validation", part, issues }, set, ctx, validateResponses);
930
1190
  }
931
1191
  async function handle(route, request, rawParams, parsers, validateResponses) {
932
1192
  const { ctx, set } = routeContext(route, request, rawParams);
933
1193
  try {
934
- return await chain(route, request, rawParams, set, ctx, parsers, async () => {
1194
+ return await chain(route, request, rawParams, set, ctx, parsers, validateResponses, async () => {
935
1195
  let reply = route.handler(ctx);
936
1196
  if (reply instanceof Promise)
937
1197
  reply = await reply;
@@ -942,10 +1202,20 @@ async function handle(route, request, rawParams, parsers, validateResponses) {
942
1202
  });
943
1203
  } catch (error) {
944
1204
  request.error = error;
945
- return fail(route, error, ctx);
1205
+ return fail(route, error, ctx, validateResponses);
946
1206
  }
947
1207
  }
948
- async function fail(definition, error, ctx) {
1208
+ async function fail(definition, error, ctx, validateResponses = true) {
1209
+ if (clientGone(error, ctx.request))
1210
+ return failed(error, ctx.request);
1211
+ if (error instanceof ContentTooLargeError) {
1212
+ try {
1213
+ return await refuse(definition, { kind: "body_limit", limit: error.limit }, ctx.set, ctx, validateResponses);
1214
+ } catch (thrown) {
1215
+ console.error(thrown);
1216
+ return internalError();
1217
+ }
1218
+ }
949
1219
  for (const hook of definition.onError) {
950
1220
  let handled = hook(error, ctx);
951
1221
  if (handled instanceof Promise)
@@ -956,17 +1226,16 @@ async function fail(definition, error, ctx) {
956
1226
  if (error instanceof HttpError) {
957
1227
  return send2(new Reply(error.status, error.body), ctx.set);
958
1228
  }
959
- console.error(error);
960
- return internalError();
1229
+ return failed(error, ctx.request);
961
1230
  }
962
1231
 
963
1232
  // src/app/socket.ts
964
1233
  var UPGRADED = Symbol("upgraded");
965
- async function upgradeSocket(definition, request, rawParams, parsers) {
1234
+ async function upgradeSocket(definition, request, rawParams, parsers, validateResponses) {
966
1235
  const { ctx, set } = routeContext(definition, request, rawParams);
967
1236
  const server = request.server;
968
1237
  try {
969
- return await chain(definition, request, rawParams, set, ctx, parsers, async () => {
1238
+ return await chain(definition, request, rawParams, set, ctx, parsers, validateResponses, async () => {
970
1239
  if (server === undefined) {
971
1240
  return routingError(426, "upgrade_required");
972
1241
  }
@@ -1100,8 +1369,7 @@ async function serve(runtime, request, server, path) {
1100
1369
  try {
1101
1370
  return await run(0);
1102
1371
  } catch (error) {
1103
- console.error(error);
1104
- return internalError();
1372
+ return failed(error, ctx.request);
1105
1373
  }
1106
1374
  }
1107
1375
  async function pipeline(runtime, ctx, path) {
@@ -1118,8 +1386,7 @@ async function pipeline(runtime, ctx, path) {
1118
1386
  }
1119
1387
  response ??= await route(runtime, ctx, path);
1120
1388
  } catch (error) {
1121
- console.error(error);
1122
- response = internalError();
1389
+ response = failed(error, ctx.request);
1123
1390
  }
1124
1391
  if (response === UPGRADED)
1125
1392
  return;
@@ -1158,7 +1425,7 @@ async function route(runtime, ctx, path) {
1158
1425
  const socket = find("WS");
1159
1426
  if (socket !== undefined && "value" in socket && socket.value.kind === "ws") {
1160
1427
  ctx.route = socket.value.path;
1161
- return upgradeSocket(socket.value, ctx, socket.params, globals.parsers);
1428
+ return upgradeSocket(socket.value, ctx, socket.params, globals.parsers, runtime.validateResponses);
1162
1429
  }
1163
1430
  }
1164
1431
  let match = find(request.method);
@@ -1197,6 +1464,8 @@ class Alxia {
1197
1464
  #sockets = [];
1198
1465
  #derive = [];
1199
1466
  #onError = [];
1467
+ #refusal;
1468
+ #bodyLimit;
1200
1469
  #server;
1201
1470
  #websocket;
1202
1471
  constructor(options = {}) {
@@ -1253,7 +1522,8 @@ class Alxia {
1253
1522
  schema,
1254
1523
  handlers,
1255
1524
  derive: [...this.#derive],
1256
- onError: [...this.#onError]
1525
+ onError: [...this.#onError],
1526
+ refusal: this.#refusal
1257
1527
  };
1258
1528
  this.#mount(definition);
1259
1529
  return this;
@@ -1270,10 +1540,29 @@ class Alxia {
1270
1540
  this.#derive.push({ kind: "wrap", run: hook });
1271
1541
  return this;
1272
1542
  }
1543
+ bodyLimit(bytes) {
1544
+ this.#bodyLimit = checkLimit(bytes, "bodyLimit()");
1545
+ return this;
1546
+ }
1273
1547
  onError(hook) {
1274
1548
  this.#onError.push(hook);
1275
1549
  return this;
1276
1550
  }
1551
+ onRefusal(schemaOrHook, maybeHook) {
1552
+ if (typeof schemaOrHook === "function") {
1553
+ this.#refusal = { hook: schemaOrHook };
1554
+ return this;
1555
+ }
1556
+ if (typeof maybeHook !== "function") {
1557
+ throw new TypeError("onRefusal(): the hook is missing");
1558
+ }
1559
+ this.#refusal = {
1560
+ hook: maybeHook,
1561
+ response: schemaOrHook.response,
1562
+ ...schemaOrHook.contentType === undefined ? {} : { contentType: schemaOrHook.contentType }
1563
+ };
1564
+ return this;
1565
+ }
1277
1566
  onRequest(hook) {
1278
1567
  this.#runtime.globals.onRequest.push(hook);
1279
1568
  return this;
@@ -1308,6 +1597,8 @@ class Alxia {
1308
1597
  });
1309
1598
  child.#derive = [...this.#derive];
1310
1599
  child.#onError = [...this.#onError];
1600
+ child.#refusal = this.#refusal;
1601
+ child.#bodyLimit = this.#bodyLimit;
1311
1602
  child.#runtime = { ...child.#runtime, globals: this.#runtime.globals };
1312
1603
  const before = new Set(this.#runtime.globals.pages.keys());
1313
1604
  const built = build(child);
@@ -1330,7 +1621,8 @@ class Alxia {
1330
1621
  ...route,
1331
1622
  path: joinPath(this.#prefix, route.path),
1332
1623
  derive: [...this.#derive, ...route.derive],
1333
- onError: [...route.onError, ...this.#onError]
1624
+ onError: [...route.onError, ...this.#onError],
1625
+ refusal: route.refusal ?? this.#refusal
1334
1626
  });
1335
1627
  }
1336
1628
  for (const socket of plugin.sockets) {
@@ -1338,11 +1630,14 @@ class Alxia {
1338
1630
  ...socket,
1339
1631
  path: joinPath(this.#prefix, socket.path),
1340
1632
  derive: [...this.#derive, ...socket.derive],
1341
- onError: [...socket.onError, ...this.#onError]
1633
+ onError: [...socket.onError, ...this.#onError],
1634
+ refusal: socket.refusal ?? this.#refusal
1342
1635
  });
1343
1636
  }
1344
1637
  this.#derive = [...this.#derive, ...plugin.#derive];
1345
1638
  this.#onError = [...plugin.#onError, ...this.#onError];
1639
+ this.#refusal = plugin.#refusal ?? this.#refusal;
1640
+ this.#bodyLimit = plugin.#bodyLimit ?? this.#bodyLimit;
1346
1641
  if (plugin.#runtime.globals !== this.#runtime.globals) {
1347
1642
  const globals = plugin.#runtime.globals;
1348
1643
  this.#runtime.globals.around.push(...globals.around);
@@ -1410,13 +1705,17 @@ class Alxia {
1410
1705
  if (typeof handler !== "function") {
1411
1706
  throw new TypeError(`${method} ${path}: the handler is missing`);
1412
1707
  }
1708
+ const full = joinPath(this.#prefix, path);
1709
+ const bodyLimit = schema.bodyLimit === undefined ? this.#bodyLimit : checkLimit(schema.bodyLimit, `${method} ${full}`);
1413
1710
  this.#register({
1414
1711
  method,
1415
- path: joinPath(this.#prefix, path),
1712
+ path: full,
1416
1713
  schema,
1714
+ ...bodyLimit === undefined ? {} : { bodyLimit },
1417
1715
  handler,
1418
1716
  derive: [...this.#derive],
1419
- onError: [...this.#onError]
1717
+ onError: [...this.#onError],
1718
+ refusal: this.#refusal
1420
1719
  });
1421
1720
  return this;
1422
1721
  };
@@ -1455,8 +1754,17 @@ function alxia(options = {}) {
1455
1754
  function definePlugin() {
1456
1755
  return (build) => build(alxia());
1457
1756
  }
1757
+ // src/reply/problem.ts
1758
+ function problem(body, init) {
1759
+ const headers = new Headers(init?.headers);
1760
+ if (!headers.has("content-type")) {
1761
+ headers.set("content-type", "application/problem+json");
1762
+ }
1763
+ return new Reply(body.status, body, { ...init, headers });
1764
+ }
1458
1765
  export {
1459
1766
  Alxia,
1767
+ ContentTooLargeError,
1460
1768
  HttpError,
1461
1769
  Reply,
1462
1770
  ResponseValidationError,
@@ -1466,12 +1774,14 @@ export {
1466
1774
  definePlugin,
1467
1775
  eventStream,
1468
1776
  isEventStreamSchema,
1777
+ isNamedEventStreamSchema,
1469
1778
  joinPath,
1470
1779
  parseRange,
1780
+ problem,
1471
1781
  shapeOf,
1472
1782
  vary,
1473
1783
  withHeaders
1474
1784
  };
1475
1785
 
1476
- //# debugId=F86708E1572B31A764756E2164756E21
1786
+ //# debugId=B38C455B7CE36AB464756E2164756E21
1477
1787
  //# sourceMappingURL=index.js.map