@nifrajs/middleware 2.1.0 → 2.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.
@@ -1 +1 @@
1
- {"version":3,"file":"pretty-json.d.ts","sourceRoot":"","sources":["../src/pretty-json.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,iBAAiB;IAChC,4CAA4C;IAC5C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,6FAA6F;IAC7F,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,gGAAgG;IAChG,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;IAC/B,2DAA2D;IAC3D,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,CAAA;CAC7D;AAqCD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,kIA0CzD"}
1
+ {"version":3,"file":"pretty-json.d.ts","sourceRoot":"","sources":["../src/pretty-json.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,iBAAiB;IAChC,4CAA4C;IAC5C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,6FAA6F;IAC7F,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,gGAAgG;IAChG,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;IAC/B,2DAA2D;IAC3D,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,CAAA;CAC7D;AA6FD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,kIAgDzD"}
@@ -10,29 +10,75 @@ function parseLength(value) {
10
10
  return undefined;
11
11
  return Number(value);
12
12
  }
13
- async function readTextCapped(res, maxBytes) {
14
- const body = res.clone().body;
13
+ /** Re-emit the bytes already pulled, then the untouched remainder of the same reader. */
14
+ function replayFrom(res, chunks, reader) {
15
+ const stream = new ReadableStream({
16
+ start(controller) {
17
+ for (const chunk of chunks)
18
+ controller.enqueue(chunk);
19
+ },
20
+ async pull(controller) {
21
+ try {
22
+ const { done, value } = await reader.read();
23
+ // No value and not done cannot happen for a byte stream, but it means the same thing here:
24
+ // there is nothing further to emit.
25
+ if (done || value === undefined)
26
+ controller.close();
27
+ else
28
+ controller.enqueue(value);
29
+ }
30
+ catch (error) {
31
+ // The upstream body failed; surface that to the client rather than truncating silently.
32
+ controller.error(error);
33
+ }
34
+ },
35
+ cancel: (reason) => reader.cancel(reason),
36
+ });
37
+ return new Response(stream, {
38
+ status: res.status,
39
+ statusText: res.statusText,
40
+ headers: res.headers,
41
+ });
42
+ }
43
+ /**
44
+ * Read up to `maxBytes` of a response, without disturbing it when the answer is "pass through".
45
+ *
46
+ * `Response.clone()` is the obvious way to peek and is a trap here. Cancelling the clone's reader once
47
+ * the cap is exceeded also stalls the ORIGINAL body in Bun, so an oversized streamed response hung the
48
+ * client instead of passing through - a hang, from a middleware whose entire job is cosmetic. (A clone
49
+ * read to completion is fine; only the cancel breaks it, which is why every buffered case passed and
50
+ * only a streamed one failed.) Abandoning the clone rather than cancelling it trades the hang for
51
+ * buffering the whole oversized body, which is the exact cost the cap exists to avoid.
52
+ *
53
+ * So the body is read directly and, when it proves too large, replayed: the bytes already pulled are
54
+ * re-emitted ahead of the rest of the same reader. No tee, nothing buffered past the cap, and the
55
+ * response the client receives is byte-for-byte the one the handler produced.
56
+ */
57
+ async function peekText(res, maxBytes) {
58
+ const body = res.body;
15
59
  if (body === null)
16
- return null;
60
+ return { response: res };
17
61
  const reader = body.getReader();
18
- const decoder = new TextDecoder();
62
+ const chunks = [];
19
63
  let total = 0;
20
- let text = "";
21
64
  try {
22
65
  while (true) {
23
66
  const { done, value } = await reader.read();
24
- if (done)
25
- return text + decoder.decode();
26
- total += value.byteLength;
27
- if (total > maxBytes) {
28
- await reader.cancel();
29
- return null;
67
+ if (done) {
68
+ const decoder = new TextDecoder();
69
+ let text = "";
70
+ for (const chunk of chunks)
71
+ text += decoder.decode(chunk, { stream: true });
72
+ return { text: text + decoder.decode() };
30
73
  }
31
- text += decoder.decode(value, { stream: true });
74
+ chunks.push(value);
75
+ total += value.byteLength;
76
+ if (total > maxBytes)
77
+ return { response: replayFrom(res, chunks, reader) };
32
78
  }
33
79
  }
34
80
  catch {
35
- return null;
81
+ return { response: replayFrom(res, chunks, reader) };
36
82
  }
37
83
  }
38
84
  /**
@@ -66,15 +112,21 @@ export function prettyJson(options = {}) {
66
112
  const declared = parseLength(res.headers.get("content-length"));
67
113
  if (declared !== undefined && declared > maxBytes)
68
114
  return res;
69
- const text = await readTextCapped(res, maxBytes);
70
- if (text === null)
71
- return res;
115
+ const peeked = await peekText(res, maxBytes);
116
+ if ("response" in peeked)
117
+ return peeked.response;
72
118
  let parsed;
73
119
  try {
74
- parsed = JSON.parse(text);
120
+ parsed = JSON.parse(peeked.text);
75
121
  }
76
122
  catch {
77
- return res;
123
+ // The body was read to decide that, so `res` can no longer be returned as-is. Rebuild it from
124
+ // the text: re-encoding UTF-8 reproduces the original bytes, so `content-length` still holds.
125
+ return new Response(peeked.text, {
126
+ status: res.status,
127
+ statusText: res.statusText,
128
+ headers: res.headers,
129
+ });
78
130
  }
79
131
  const headers = new Headers(res.headers);
80
132
  headers.delete("content-length");
@@ -1 +1 @@
1
- {"version":3,"file":"pretty-json.js","sourceRoot":"","sources":["../src/pretty-json.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pretty-json.js","sourceRoot":"","sources":["../src/pretty-json.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/middleware",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Composable middleware for nifra — auth, CSRF, JWT/JWKS, IP restriction, CORS, body limits, response cache, timing, and operational helpers.",
5
5
  "keywords": [
6
6
  "bun",
@@ -46,10 +46,10 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@nifrajs/schema": "2.1.0"
49
+ "@nifrajs/schema": "2.2.0"
50
50
  },
51
51
  "peerDependencies": {
52
- "@nifrajs/core": "^2.1.0"
52
+ "@nifrajs/core": "^2.2.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@nifrajs/core": "workspace:*",
@@ -25,26 +25,82 @@ function parseLength(value: string | null): number | undefined {
25
25
  return Number(value)
26
26
  }
27
27
 
28
- async function readTextCapped(res: Response, maxBytes: number): Promise<string | null> {
29
- const body = res.clone().body
30
- if (body === null) return null
28
+ /**
29
+ * The two reader methods this file uses, structurally.
30
+ *
31
+ * Named by use rather than imported: `lib.dom`'s `ReadableStreamDefaultReader` and Bun's augmented one
32
+ * are not mutually assignable (Bun adds `readMany`), and `ReturnType<…getReader>` resolves to the BYOB
33
+ * overload, whose `read` requires an argument. A structural shape sidesteps all of it without a cast.
34
+ */
35
+ interface ChunkReader {
36
+ read(): Promise<{ readonly done: boolean; readonly value?: Uint8Array }>
37
+ cancel(reason?: unknown): Promise<void>
38
+ }
39
+
40
+ /** Re-emit the bytes already pulled, then the untouched remainder of the same reader. */
41
+ function replayFrom(res: Response, chunks: readonly Uint8Array[], reader: ChunkReader): Response {
42
+ const stream = new ReadableStream<Uint8Array>({
43
+ start(controller) {
44
+ for (const chunk of chunks) controller.enqueue(chunk)
45
+ },
46
+ async pull(controller) {
47
+ try {
48
+ const { done, value } = await reader.read()
49
+ // No value and not done cannot happen for a byte stream, but it means the same thing here:
50
+ // there is nothing further to emit.
51
+ if (done || value === undefined) controller.close()
52
+ else controller.enqueue(value)
53
+ } catch (error) {
54
+ // The upstream body failed; surface that to the client rather than truncating silently.
55
+ controller.error(error)
56
+ }
57
+ },
58
+ cancel: (reason) => reader.cancel(reason),
59
+ })
60
+ return new Response(stream, {
61
+ status: res.status,
62
+ statusText: res.statusText,
63
+ headers: res.headers,
64
+ })
65
+ }
66
+
67
+ type Peeked = { readonly text: string } | { readonly response: Response }
68
+
69
+ /**
70
+ * Read up to `maxBytes` of a response, without disturbing it when the answer is "pass through".
71
+ *
72
+ * `Response.clone()` is the obvious way to peek and is a trap here. Cancelling the clone's reader once
73
+ * the cap is exceeded also stalls the ORIGINAL body in Bun, so an oversized streamed response hung the
74
+ * client instead of passing through - a hang, from a middleware whose entire job is cosmetic. (A clone
75
+ * read to completion is fine; only the cancel breaks it, which is why every buffered case passed and
76
+ * only a streamed one failed.) Abandoning the clone rather than cancelling it trades the hang for
77
+ * buffering the whole oversized body, which is the exact cost the cap exists to avoid.
78
+ *
79
+ * So the body is read directly and, when it proves too large, replayed: the bytes already pulled are
80
+ * re-emitted ahead of the rest of the same reader. No tee, nothing buffered past the cap, and the
81
+ * response the client receives is byte-for-byte the one the handler produced.
82
+ */
83
+ async function peekText(res: Response, maxBytes: number): Promise<Peeked> {
84
+ const body = res.body
85
+ if (body === null) return { response: res }
31
86
  const reader = body.getReader()
32
- const decoder = new TextDecoder()
87
+ const chunks: Uint8Array[] = []
33
88
  let total = 0
34
- let text = ""
35
89
  try {
36
90
  while (true) {
37
91
  const { done, value } = await reader.read()
38
- if (done) return text + decoder.decode()
39
- total += value.byteLength
40
- if (total > maxBytes) {
41
- await reader.cancel()
42
- return null
92
+ if (done) {
93
+ const decoder = new TextDecoder()
94
+ let text = ""
95
+ for (const chunk of chunks) text += decoder.decode(chunk, { stream: true })
96
+ return { text: text + decoder.decode() }
43
97
  }
44
- text += decoder.decode(value, { stream: true })
98
+ chunks.push(value)
99
+ total += value.byteLength
100
+ if (total > maxBytes) return { response: replayFrom(res, chunks, reader) }
45
101
  }
46
102
  } catch {
47
- return null
103
+ return { response: replayFrom(res, chunks, reader) }
48
104
  }
49
105
  }
50
106
 
@@ -76,13 +132,19 @@ export function prettyJson(options: PrettyJsonOptions = {}) {
76
132
  const declared = parseLength(res.headers.get("content-length"))
77
133
  if (declared !== undefined && declared > maxBytes) return res
78
134
 
79
- const text = await readTextCapped(res, maxBytes)
80
- if (text === null) return res
135
+ const peeked = await peekText(res, maxBytes)
136
+ if ("response" in peeked) return peeked.response
81
137
  let parsed: unknown
82
138
  try {
83
- parsed = JSON.parse(text)
139
+ parsed = JSON.parse(peeked.text)
84
140
  } catch {
85
- return res
141
+ // The body was read to decide that, so `res` can no longer be returned as-is. Rebuild it from
142
+ // the text: re-encoding UTF-8 reproduces the original bytes, so `content-length` still holds.
143
+ return new Response(peeked.text, {
144
+ status: res.status,
145
+ statusText: res.statusText,
146
+ headers: res.headers,
147
+ })
86
148
  }
87
149
 
88
150
  const headers = new Headers(res.headers)