@znight/http 0.1.4 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@znight/http",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Znight HTTP plugin: controllers, routing, guards, interceptors, validation with Fastify, Express or Hono",
5
5
  "license": "MIT",
6
6
  "author": "Ugho STEPHAN",
@@ -79,7 +79,7 @@
79
79
  "access": "public"
80
80
  },
81
81
  "dependencies": {
82
- "@znight/core": "0.1.4"
82
+ "@znight/core": "0.2.0"
83
83
  },
84
84
  "peerDependencies": {
85
85
  "@hono/node-server": "^2.1.5",
@@ -87,7 +87,7 @@
87
87
  "fastify": "^5.12.7",
88
88
  "hono": "^4.13.14",
89
89
  "light-my-request": "^6.8.0",
90
- "@znight/compiler": "^0.1.4"
90
+ "@znight/compiler": "^0.2.0"
91
91
  },
92
92
  "peerDependenciesMeta": {
93
93
  "@hono/node-server": {
@@ -118,7 +118,7 @@
118
118
  "valibot": "1.5.0",
119
119
  "zod": "4.6.7",
120
120
  "light-my-request": "6.8.0",
121
- "@znight/compiler": "0.1.4"
121
+ "@znight/compiler": "0.2.0"
122
122
  },
123
123
  "znight": {
124
124
  "apiVersion": 1,
package/src/pipeline.ts CHANGED
@@ -334,6 +334,7 @@ export class Pipeline {
334
334
  // a HEAD reply has no body: JSON results of an explicit `@Head` route take the general pipeline
335
335
  jsonStatus: definition.method === 'HEAD' || isBodylessStatus(json) ? undefined : json,
336
336
  emptyStatus: definition.status ?? (post ? 201 : 204),
337
+ responses: definition.method !== 'HEAD',
337
338
  }
338
339
  }
339
340
 
@@ -500,36 +501,51 @@ export class Pipeline {
500
501
  }
501
502
 
502
503
  /**
503
- * The reply of a plain JSON value (see {@link isPlainJson}) or of `undefined`: what
504
- * `serializeResult` and {@link #complete} produce for it, the headers in their final order.
505
- * `undefined` when the result or the context needs the general pipeline. Throws like
506
- * `JSON.stringify`.
504
+ * The reply of a plain JSON value (see {@link isPlainJson}), of an `HttpResponse` carrying one
505
+ * (see {@link fastResponseType}) or of `undefined`: what `serializeResult` and {@link #complete}
506
+ * produce for it, the headers in their final order. `undefined` when the result or the context
507
+ * needs the general pipeline. Throws like `JSON.stringify`.
507
508
  */
508
509
  #fastReply(route: FastRoute, ctx: RequestContext, result: unknown): AdapterReply | undefined {
509
- const status = result === undefined ? route.emptyStatus : route.jsonStatus
510
+ let payload = result
511
+ let status = result === undefined ? route.emptyStatus : route.jsonStatus
512
+ let type: string | null = null
513
+ if (route.responses && result instanceof HttpResponse) {
514
+ const own = fastResponseType(result)
515
+ if (own === undefined) return undefined
516
+ type = own
517
+ payload = result.body
518
+ // `@HttpCode` > `HttpResponse.status` > the default of the route (see serializeResult)
519
+ if (route.plan.definition.status === undefined && result.status !== undefined) {
520
+ status = isBodylessStatus(result.status) ? undefined : result.status
521
+ }
522
+ // an HttpResponse without body takes the general pipeline
523
+ if (payload === undefined) return undefined
524
+ }
510
525
  // draining since the request started (`connection: close`, see #frameworkHeaders)
511
526
  if (status === undefined || ctx.hasResponseHeaders || ctx.hasCookies || this.draining) return undefined
512
527
  const requestId = ctx.requestId
513
528
  // a custom generator's id that cannot be sent is left out (see #frameworkHeaders)
514
529
  if (!this.#validIds && !isHeaderValue(requestId)) return undefined
515
- if (result === undefined) return this.#templateReply(status, null, undefined, requestId)
516
- if (!isPlainJson(result)) return undefined
517
- const json = JSON.stringify(result)
518
- if (json === undefined) throw new TypeError(`Cannot serialise a ${typeof result} as a response body`)
530
+ if (payload === undefined) return this.#templateReply(status, null, undefined, requestId, null)
531
+ if (!isPlainJson(payload)) return undefined
532
+ const json = JSON.stringify(payload)
533
+ if (json === undefined) throw new TypeError(`Cannot serialise a ${typeof payload} as a response body`)
519
534
  // encoded once, its length from the bytes (see serialize.ts encodeText)
520
- return this.#templateReply(status, utf8(json), json, requestId)
535
+ return this.#templateReply(status, utf8(json), json, requestId, type)
521
536
  }
522
537
 
523
538
  /**
524
539
  * The reply of a JSON body (`text` set) or of no body, its headers copied from the template of
525
540
  * the fast lane (see {@link #jsonHeaders}), completed with the length and the request id (one that
526
- * can be sent).
541
+ * can be sent). `type` replaces the JSON content type in place (the `HttpResponse`'s own).
527
542
  */
528
543
  #templateReply(
529
544
  status: number,
530
545
  body: Uint8Array | null,
531
546
  text: string | undefined,
532
547
  requestId: RequestId,
548
+ type: string | null,
533
549
  ): AdapterReply {
534
550
  if (text === undefined) {
535
551
  const headers: Record<string, HeaderValue> = { ...this.#emptyHeaders }
@@ -538,6 +554,8 @@ export class Pipeline {
538
554
  return { status, headers, body: null }
539
555
  }
540
556
  const headers: Record<string, HeaderValue> = { ...this.#jsonHeaders }
557
+ // assignHeaders keeps the position of a key: the content type stays in place
558
+ if (type !== null) headers['content-type'] = type
541
559
  headers['content-length'] = String((body as Uint8Array).byteLength)
542
560
  const idHeader = this.#jsonIdHeader
543
561
  if (idHeader !== undefined) headers[idHeader] = requestId
@@ -662,7 +680,13 @@ export class Pipeline {
662
680
  !this.draining &&
663
681
  (this.#validIds || isHeaderValue(x.requestId))
664
682
  ) {
665
- const reply = this.#templateReply(outcome.status, outcome.body as Uint8Array | null, outcome.text, x.requestId)
683
+ const reply = this.#templateReply(
684
+ outcome.status,
685
+ outcome.body as Uint8Array | null,
686
+ outcome.text,
687
+ x.requestId,
688
+ null,
689
+ )
666
690
  ctx.commit()
667
691
  ctx.releaseBody()
668
692
  x.answer(reply)
@@ -1471,6 +1495,8 @@ interface FastRoute {
1471
1495
  readonly jsonStatus: number | undefined
1472
1496
  /** The status of a reply without value (`undefined` returned). */
1473
1497
  readonly emptyStatus: number
1498
+ /** An `HttpResponse` carrying a plain JSON body may be served (not on a `@Head` route: no body). */
1499
+ readonly responses: boolean
1474
1500
  }
1475
1501
 
1476
1502
  /** `#fastAsync` reads the body and runs the handler. */
@@ -1622,6 +1648,23 @@ function isPlainJson(value: unknown): value is object {
1622
1648
  return typeof (value as { pipe?: unknown }).pipe !== 'function'
1623
1649
  }
1624
1650
 
1651
+ /**
1652
+ * The content type of an `HttpResponse` the fast lane can serve: `null` without headers, its
1653
+ * `content-type` when that is its only header (lower-case name, a value every adapter can send);
1654
+ * `undefined` when it needs the general pipeline (other headers, cookies).
1655
+ */
1656
+ function fastResponseType(response: HttpResponse): string | null | undefined {
1657
+ if (response.cookies.length > 0) return undefined
1658
+ const headers = response.headers
1659
+ let type: string | null = null
1660
+ for (const name of Object.keys(headers)) {
1661
+ const value = headers[name]
1662
+ if (name !== 'content-type' || typeof value !== 'string' || !isHeaderValue(value)) return undefined
1663
+ type = value
1664
+ }
1665
+ return type
1666
+ }
1667
+
1625
1668
  const ENCODER = new TextEncoder()
1626
1669
 
1627
1670
  /** UTF-8 encoding of a text body, like the serialiser (a pooled `Buffer` on Node.js, `TextEncoder` on Bun). */