@ultimat3/http 24.0.0 → 25.0.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/src/stages.ts CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  import {
8
8
  anonymousActor,
9
+ BUILD_ID_HEADER,
9
10
  inflightCount,
10
11
  isAnonymous,
11
12
  isDraining,
@@ -48,7 +49,7 @@ import type { UltimateRequest } from './request';
48
49
  import { addVary, problem, redirect } from './response';
49
50
  import { matchRoute, type Route, type RouteHandler, type RouteTable } from './router';
50
51
  import { responseSecurityHeaders } from './security-headers';
51
- import { validate } from './validate';
52
+ import { validateBody } from './validate';
52
53
 
53
54
  export type StageName =
54
55
  | 'request-id'
@@ -173,12 +174,12 @@ export const stageRunners = (input: StageRunnersInput): Record<StageName, StageR
173
174
  const answered = preflight(request.raw, config.cors);
174
175
  if (answered !== undefined) return answered;
175
176
 
176
- ctx.clientBuildId = request.header(config.buildIdHeader);
177
+ ctx.clientBuildId = request.header(BUILD_ID_HEADER);
177
178
  // Stamped BEFORE the assertion, so the refusal carries it too. A skew answer that
178
179
  // withholds the server's own id tells a client it is stale without telling it what to
179
180
  // become — and the one caller that can act on that, the service worker holding the
180
181
  // stale id, then has no way to tell this 409 from any other.
181
- if (config.buildId !== null) ctx.headers.set(config.buildIdHeader, config.buildId);
182
+ if (config.buildId !== null) ctx.headers.set(BUILD_ID_HEADER, config.buildId);
182
183
  request.assertBuild();
183
184
 
184
185
  // A `/<locale>/` prefix is stripped BEFORE the match, never by middleware: middleware wraps
@@ -273,7 +274,7 @@ export const stageRunners = (input: StageRunnersInput): Record<StageName, StageR
273
274
  body: async (request, ctx) => {
274
275
  const schema = ctx.route?.meta.input;
275
276
  if (schema === undefined) return undefined;
276
- const outcome = await validate(schema, await request.bodyRaw());
277
+ const outcome = await validateBody(schema, await request.bodyRaw());
277
278
  if (!outcome.ok) throw bodyInvalid(ctx.url.pathname, outcome.issues);
278
279
  ctx.input = outcome.value;
279
280
  return undefined;
@@ -375,7 +376,15 @@ export const stageRunners = (input: StageRunnersInput): Record<StageName, StageR
375
376
  // never handed a page. Which of the two a browser gets is the ENVIRONMENT — the overlay
376
377
  // prints the cause, the fix and the stack, which is what a visitor may never see.
377
378
  if (acceptsHtml(request.raw)) {
378
- if (config.dev) {
379
+ // A 4xx is not a defect to debug, it is a page a visitor will see, so dev asks for the
380
+ // app's own file first (#492, O-492). A 5xx keeps the overlay, and so does a 4xx the app
381
+ // has no file for: in dev the diagnostic beats the framework's generic page.
382
+ // A throw from the hook is caught by `recoverWith` and degrades to the problem document.
383
+ const override =
384
+ config.dev && facts.status >= 500
385
+ ? undefined
386
+ : await hooks.errorPage?.(facts.status, ctx);
387
+ if (config.dev && override === undefined) {
379
388
  // Asked for inside the branch, never above it: the overlay is the only surface a notice
380
389
  // has, so a production process — or an agent that asked for json — must not pay a
381
390
  // diagnostic's per-request cost to produce findings nothing will render.
@@ -401,10 +410,7 @@ export const stageRunners = (input: StageRunnersInput): Record<StageName, StageR
401
410
  ...(seconds === undefined ? {} : { retryAfterSeconds: seconds }),
402
411
  signInPath: config.signInPath,
403
412
  },
404
- // The app's own file, read per request by whoever mounted the hook. A throw here is
405
- // caught by `recoverWith` and degrades to the problem document, which is the answer a
406
- // page whose renderer failed can still give.
407
- { override: await hooks.errorPage?.(facts.status, ctx), headers: retryAfter },
413
+ { override, headers: retryAfter },
408
414
  );
409
415
  }
410
416
  return problem(error, {
package/src/validate.ts CHANGED
@@ -55,12 +55,12 @@ const outcome = <Out>(result: {
55
55
  export const validateSync = <Out>(schema: Schema<Out>, value: unknown): ValidationOutcome<Out> => {
56
56
  const result = schema['~standard'].validate(value);
57
57
  if (result instanceof Promise) {
58
- return { ok: false, issues: ['schema is async; use validate() for request bodies'] };
58
+ return { ok: false, issues: ['schema is async; use validateBody() for request bodies'] };
59
59
  }
60
60
  return outcome<Out>(result);
61
61
  };
62
62
 
63
- export const validate = async <Out>(
63
+ export const validateBody = async <Out>(
64
64
  schema: Schema<Out>,
65
65
  value: unknown,
66
66
  ): Promise<ValidationOutcome<Out>> => outcome<Out>(await schema['~standard'].validate(value));