@dunx/http 2.3.1 → 2.4.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.
@@ -33,6 +33,14 @@ export interface StandardSchemaIssue {
33
33
  }
34
34
  /** The validated output of a schema - `InferOutput<typeof CreateNote>` is `Note`. */
35
35
  export type InferOutput<S> = S extends StandardSchemaV1<unknown, infer Out> ? Out : never;
36
+ /**
37
+ * A JSON Schema, as JSON. OpenAPI 3.1 embeds draft 2020-12 verbatim.
38
+ *
39
+ * Declared here rather than in `@dunx/openapi` because {@link RouteSchemas} names
40
+ * it and that package depends on this one, so this is the lowest common owner.
41
+ * `@dunx/openapi` re-exports it.
42
+ */
43
+ export type JsonSchema = Readonly<Record<string, unknown>>;
36
44
  /**
37
45
  * The second argument to `@Get`/`@Post`/... Declaring a schema is what makes the
38
46
  * matching `input` field appear, get parsed, and get validated; omitting one
@@ -62,8 +70,20 @@ export interface RouteSchemas {
62
70
  * cost paid for a documentation feature, which is the wrong trade - the
63
71
  * handler's own return type is what checks the answer, at compile time and for
64
72
  * free. Nothing in the request path reads this key.
73
+ *
74
+ * A plain {@link JsonSchema} is accepted here too, and only here: a JSON Schema
75
+ * needs no conversion, so documenting a response costs no validator. `$id` names
76
+ * it, hoisting it into `components/schemas` the way `.meta({ id })` does for a
77
+ * zod schema. `body`, `query` and `params` still take a Standard Schema, because
78
+ * those are parsed.
79
+ *
80
+ * ```ts
81
+ * response: {
82
+ * 200: Object.freeze({ $id: 'Pong', type: 'object' }),
83
+ * }
84
+ * ```
65
85
  */
66
- readonly response?: Readonly<Record<number, StandardSchemaV1>>;
86
+ readonly response?: Readonly<Record<number, StandardSchemaV1 | JsonSchema>>;
67
87
  }
68
88
  /**
69
89
  * The handler's parameter type, derived from its own options object. It has to be
@@ -11,6 +11,16 @@ export interface RouteContext {
11
11
  readonly handler: string;
12
12
  readonly method: HttpMethod;
13
13
  readonly path: string;
14
+ /**
15
+ * Whether this route declares a `body` schema, and therefore whether the input
16
+ * reader will parse the body and record it for anything else that wants it.
17
+ *
18
+ * Resolved here because it is a property of the route, known when the table is
19
+ * built. `RequestLoggingMiddleware` reads it to decide once - not per request -
20
+ * whether logging the body needs a `Request.clone()`, which is the single most
21
+ * expensive thing it can do. See `raw-body.ts`.
22
+ */
23
+ readonly parsesBody: boolean;
14
24
  get<T>(key: MetaKey<T>): T | undefined;
15
25
  }
16
26
  /**
@@ -0,0 +1,21 @@
1
+ import type { BunRequest } from 'bun';
2
+ export declare class RawBody {
3
+ /**
4
+ * Called by `RequestLoggingMiddleware` before the chain runs, when it intends to
5
+ * log this body and the route is going to parse it anyway.
6
+ */
7
+ static want(req: BunRequest): void;
8
+ /** Whether the body reader should buffer the text on the way past. */
9
+ static wanted(req: BunRequest): boolean;
10
+ /**
11
+ * Called by the body reader with the text it buffered, **before** validating it.
12
+ *
13
+ * Before, on purpose, and for two reasons. Validation applies defaults, coerces
14
+ * and strips unknown keys, so the validated object is not what the caller sent.
15
+ * And a body that fails to parse at all still has text - which is the case the
16
+ * clone used to cover and the reason this holds text rather than a value.
17
+ */
18
+ static record(req: BunRequest, text: string): void;
19
+ /** The buffered text, or `undefined` when nothing read one. */
20
+ static read(req: BunRequest): string | undefined;
21
+ }
@@ -8,15 +8,35 @@ export interface RequestLoggingOptions {
8
8
  /**
9
9
  * Log the request body. Default **`false`**.
10
10
  *
11
- * Reading it means `req.clone().text()` - a second copy of every payload,
12
- * buffered and parsed, on the hot path. Measured on the `validate` scenario in
13
- * `internal/bench`, turning both body options on costs roughly two thirds of the
14
- * throughput. It is also the field most likely to contain a password.
11
+ * **What it costs depends on whether the route declares a `body` schema**, and by
12
+ * a factor of fifteen. `bun run logging:bodies` in `internal/bench`, round-robin
13
+ * over 3 runs against `POST /validate`:
15
14
  *
16
- * Turn it on in development, where seeing the payload is the point.
15
+ * | setting | us/req | vs the default |
16
+ * | ---------------------------------------- | -----: | -------------: |
17
+ * | `requestLogging: false` | 12.80 | -4.45 us |
18
+ * | the shipped default, both bodies off | 17.25 | - |
19
+ * | **`requestBody: true`, schema route** | **19.12** | **+1.87 us** |
20
+ * | `responseBody: true` | 19.80 | +2.55 us |
21
+ * | both bodies, schema route | 20.03 | +2.78 us |
22
+ * | **`requestBody: true`, no schema** | **46.06** | **+28.81 us** |
23
+ *
24
+ * A route with a schema has already had its body buffered by the input reader, so
25
+ * the logger reads that text and nothing is cloned. A route without one leaves the
26
+ * logger to `req.clone()`, and cloning a request whose body is an unread network
27
+ * stream is the entire cost - not the second `JSON.parse`, which is 0.32 us.
28
+ * `raw-body.ts` has that decomposition.
29
+ *
30
+ * It is the field most likely to contain a password. Turn it on in development,
31
+ * where seeing the payload is the point.
17
32
  */
18
33
  readonly requestBody?: boolean;
19
- /** Log the response body. Default **`false`** - same clone-and-buffer cost. */
34
+ /**
35
+ * Log the response body. Default **`false`**, +2.55 us - see the table above.
36
+ *
37
+ * No equivalent trick here and none needed: a response is already a materialised
38
+ * string by the time this clones it, which is why it was never the expensive half.
39
+ */
20
40
  readonly responseBody?: boolean;
21
41
  /**
22
42
  * Paths to skip entirely - a health check polled every second, say.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dunx/http",
3
- "version": "2.3.1",
3
+ "version": "2.4.0",
4
4
  "description": "Bun.serve adapter for the dunx framework: controllers, middleware and WebSocket gateways",
5
5
  "keywords": [
6
6
  "bun",
@@ -58,7 +58,7 @@
58
58
  "@dunx/core": "workspace:*"
59
59
  },
60
60
  "peerDependencies": {
61
- "@dunx/core": "^2.3.1",
61
+ "@dunx/core": "^2.4.0",
62
62
  "@types/bun": ">=1.3.0"
63
63
  },
64
64
  "peerDependenciesMeta": {