@nifrajs/middleware 2.0.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.
- package/dist/pretty-json.d.ts.map +1 -1
- package/dist/pretty-json.js +70 -18
- package/dist/pretty-json.js.map +1 -1
- package/package.json +3 -3
- package/src/pretty-json.ts +78 -16
|
@@ -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;
|
|
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"}
|
package/dist/pretty-json.js
CHANGED
|
@@ -10,29 +10,75 @@ function parseLength(value) {
|
|
|
10
10
|
return undefined;
|
|
11
11
|
return Number(value);
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
60
|
+
return { response: res };
|
|
17
61
|
const reader = body.getReader();
|
|
18
|
-
const
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
|
70
|
-
if (
|
|
71
|
-
return
|
|
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
|
-
|
|
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");
|
package/dist/pretty-json.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pretty-json.js","sourceRoot":"","sources":["../src/pretty-json.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAenD,MAAM,SAAS,GAAG,sDAAsD,CAAA;AAExE,SAAS,MAAM,CAAC,WAAmB;IACjC,OAAO,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;AACpC,CAAC;AAED,SAAS,WAAW,CAAC,KAAoB;IACvC,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAA;IACpC,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IACrD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAA;AACtB,CAAC;
|
|
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.
|
|
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.
|
|
49
|
+
"@nifrajs/schema": "2.2.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@nifrajs/core": "^2.
|
|
52
|
+
"@nifrajs/core": "^2.2.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@nifrajs/core": "workspace:*",
|
package/src/pretty-json.ts
CHANGED
|
@@ -25,26 +25,82 @@ function parseLength(value: string | null): number | undefined {
|
|
|
25
25
|
return Number(value)
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
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)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
|
80
|
-
if (
|
|
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
|
-
|
|
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)
|