@johnhenry/letterpress 0.2.0 → 0.2.2
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/.github/workflows/ci.yml +3 -0
- package/.github/workflows/publish.yml +10 -13
- package/CHANGELOG.md +42 -4
- package/README.md +1 -8
- package/create-route.mjs +33 -41
- package/package.json +1 -1
- package/rewrite.mjs +1 -1
- package/tag-request.mjs +1 -1
- package/tag-response.mjs +1 -1
- package/test/route.mjs +21 -0
- package/test/tag-req-res.mjs +32 -1
- package/utility/parse-http-text.mjs +31 -8
package/.github/workflows/ci.yml
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
name: Publish to npm
|
|
2
2
|
|
|
3
|
+
# Publish model: main is the release branch. A push to main publishes only if
|
|
4
|
+
# package.json's version isn't on npm yet (otherwise a clean no-op), then tags
|
|
5
|
+
# v<version> and creates a GitHub Release. See johnhenry/workflows' README.
|
|
3
6
|
on:
|
|
4
|
-
release:
|
|
5
|
-
types: [published]
|
|
6
7
|
push:
|
|
7
|
-
|
|
8
|
-
# publish.yml / johnhenry/workflows' README for the full incident
|
|
9
|
-
# writeup: creating a release very soon after a push/merge to the
|
|
10
|
-
# same commit can silently drop the release event for a job whose
|
|
11
|
-
# body is uses: <external-repo>/...@v1. This gives a second,
|
|
12
|
-
# idempotency-safe chance to fire.
|
|
13
|
-
tags:
|
|
14
|
-
- "v*"
|
|
8
|
+
branches: [main]
|
|
15
9
|
workflow_dispatch: {}
|
|
16
10
|
|
|
11
|
+
concurrency:
|
|
12
|
+
group: publish-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: false
|
|
14
|
+
|
|
17
15
|
jobs:
|
|
18
16
|
publish:
|
|
19
17
|
permissions:
|
|
20
|
-
contents:
|
|
21
|
-
id-token: write #
|
|
22
|
-
# see johnhenry/workflows' README for why this must be declared here too.
|
|
18
|
+
contents: write # tag + GitHub Release by-product of npm-publish.yml
|
|
19
|
+
id-token: write # npm provenance
|
|
23
20
|
uses: johnhenry/workflows/.github/workflows/npm-publish.yml@v1
|
|
24
21
|
with:
|
|
25
22
|
node-version: "26"
|
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,44 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|
|
6
6
|
|
|
7
7
|
This project will adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) once it reaches 1.0.0.
|
|
8
8
|
|
|
9
|
+
## [0.2.2] - 2026-10-14
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- 🐛 **Multi-line bodies duplicated earlier lines** (#20). In
|
|
14
|
+
`utility/parse-http-text.mjs`, the line buffer was not cleared after each
|
|
15
|
+
committed body line, so a body of `a\nb\nc` came out as `a\nab\nabc`.
|
|
16
|
+
This affected `createResponse`/`createRoute`/`tagRequest`/`tagResponse`
|
|
17
|
+
(a regression since `0.2.0`; `0.0.3` was correct).
|
|
18
|
+
|
|
19
|
+
## [0.2.1] - 2026-10-01
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- 🐛 **A later function substitution's `context.setHeader()` stopped
|
|
24
|
+
overwriting an earlier template header line**, reversing `0.2.0`'s own
|
|
25
|
+
create-route.mjs backport. That backport resolved every function-valued
|
|
26
|
+
substitution in a pre-pass, before any template header line was
|
|
27
|
+
applied -- so a template header line always won, regardless of whether
|
|
28
|
+
a function substitution calling `setHeader` for the same name appeared
|
|
29
|
+
*after* it (originally, whichever ran later always won, matching plain
|
|
30
|
+
left-to-right template evaluation). `utility/parse-http-text.mjs` now
|
|
31
|
+
takes a `resolveSubstitution` callback invoked at the exact point the
|
|
32
|
+
scan reaches each substitution, and an `onHeaderLine` callback invoked
|
|
33
|
+
immediately per header line, instead of batching either until the whole
|
|
34
|
+
parse finishes -- restoring the original ordering while still sharing
|
|
35
|
+
one parser implementation. `tagRequest`/`tagResponse` are unaffected
|
|
36
|
+
(they have no function-substitution/side-effect concept to reorder).
|
|
37
|
+
Found by deliberately re-testing `0.2.0`'s backport against the
|
|
38
|
+
pre-refactor code's actual behavior, not by inspection.
|
|
39
|
+
- A function-valued substitution returning a `Headers` object is now
|
|
40
|
+
merged directly, like a directly-substituted `Headers` object already
|
|
41
|
+
was -- previously it fell through to `.toString()`, producing the
|
|
42
|
+
literal text `"[object Headers]"` in the response body and silently
|
|
43
|
+
dropping the headers. Not separately tested before (no prior test
|
|
44
|
+
covered this combination); noted here since it's a user-visible
|
|
45
|
+
behavior difference from `0.2.0` and earlier, not just a `0.2.0` revert.
|
|
46
|
+
|
|
9
47
|
## [0.2.0] - 2026-09-30
|
|
10
48
|
|
|
11
49
|
### Added
|
|
@@ -89,7 +127,7 @@ already behaved) is the one `create-route.mjs` was always meant to have.
|
|
|
89
127
|
carried over during the leroute -> `@johnhenry/letterpress` rename --
|
|
90
128
|
it simply wasn't in the initial `@johnhenry/letterpress` source, a gap
|
|
91
129
|
in that migration rather than an intentional removal. It was discovered
|
|
92
|
-
while porting a downstream consumer
|
|
130
|
+
while porting a private downstream consumer (a live HTTP request
|
|
93
131
|
inspector/proxy), whose proxy mode depends on it for rewriting proxied
|
|
94
132
|
requests/responses. Ported unchanged in behavior as `rewrite.mjs`
|
|
95
133
|
(`createRewriter`, plus `rewriteRequest`/`rewriteResponse`/`addRule`/
|
|
@@ -105,7 +143,7 @@ already behaved) is the one `create-route.mjs` was always meant to have.
|
|
|
105
143
|
|
|
106
144
|
- ✨ **`router.mount(prefix, subHandler)`**: another `leroute` capability
|
|
107
145
|
(`create-lerouter.mjs`'s `router.mount`) dropped during the same rename,
|
|
108
|
-
found via the same
|
|
146
|
+
found via the same port -- that app's own top-level router uses it
|
|
109
147
|
to mount its dashboard sub-router under `/inspect`. Delegates every
|
|
110
148
|
request under `prefix` to `subHandler` with the prefix stripped from the
|
|
111
149
|
forwarded request's path; `subHandler` may be a plain `(Request) =>
|
|
@@ -127,9 +165,9 @@ already behaved) is the one `create-route.mjs` was always meant to have.
|
|
|
127
165
|
and forwarded to `defaultHandler` too. This was also present in `leroute`
|
|
128
166
|
(`router(request, ctx = {})`) and dropped during the rename; brought back
|
|
129
167
|
specifically so `mount()` can hand a mounted sub-router its `mountPrefix`
|
|
130
|
-
(used by
|
|
168
|
+
(used by that app to reconstruct the full request path for display in its
|
|
131
169
|
captured-request feed, purely cosmetic but a real fidelity gap otherwise)
|
|
132
|
-
-- caught by testing `mount()` end-to-end against
|
|
170
|
+
-- caught by testing `mount()` end-to-end against that app itself, not by
|
|
133
171
|
a direct comparison against the old source this time. Existing single-arg
|
|
134
172
|
`router(request)` callers are unaffected (`ctx` defaults to `{}`).
|
|
135
173
|
|
package/README.md
CHANGED
|
@@ -265,7 +265,7 @@ rewriter.addRule({ match: { path: "/teapot" }, action: { setStatus: 418 } });
|
|
|
265
265
|
> This existed in `leroute` (the package letterpress was renamed from --
|
|
266
266
|
> see the provenance note near the top of this README) but was dropped
|
|
267
267
|
> during that rename. It's ported here, discovered missing while porting a
|
|
268
|
-
> downstream consumer (
|
|
268
|
+
> downstream consumer (a private request inspector/proxy) that relies on
|
|
269
269
|
> it for proxy-mode rewriting -- see CHANGELOG.md.
|
|
270
270
|
|
|
271
271
|
### `deconstruct`, `cook`
|
|
@@ -318,13 +318,6 @@ is designed to pair with a sibling package that does.
|
|
|
318
318
|
`Route`-shaped function works too; `leserve` is the tested, documented
|
|
319
319
|
pairing, not a hard dependency (it's a `devDependency` here, used only in
|
|
320
320
|
the demo scripts).
|
|
321
|
-
- **[`@johnhenry/prism`](https://github.com/johnhenry/prism)** -- a live
|
|
322
|
-
HTTP request inspector/proxy that uses `createRouter()` as its top-level
|
|
323
|
-
router. Porting it surfaced two real gaps left over from letterpress's
|
|
324
|
-
own rename from `leroute`: `router.mount()` (with the `ctx`-forwarding
|
|
325
|
-
its `mountPrefix` depends on) and `createRewriter`
|
|
326
|
-
(`@johnhenry/letterpress/rewrite`) -- both closed as part of that port,
|
|
327
|
-
see CHANGELOG.md.
|
|
328
321
|
|
|
329
322
|
## 🤝 Contributing
|
|
330
323
|
|
package/create-route.mjs
CHANGED
|
@@ -2,24 +2,6 @@ import { parseHttpText } from "./utility/parse-http-text.mjs";
|
|
|
2
2
|
|
|
3
3
|
const DEFAULT_REQUEST = () => new Request("http://.");
|
|
4
4
|
|
|
5
|
-
// Same rule tag-request.mjs/tag-response.mjs use: the first occurrence of
|
|
6
|
-
// a header name overwrites whatever was already set (e.g. from `init`),
|
|
7
|
-
// a second-or-later occurrence of the *same name* accumulates instead of
|
|
8
|
-
// overwriting it. Fixes the Set-Cookie-overwrite bug this file used to
|
|
9
|
-
// have (headers.set() for every line, unconditionally).
|
|
10
|
-
const mergeHeaderEntries = (headers, headerEntries) => {
|
|
11
|
-
const seen = new Set();
|
|
12
|
-
for (const { name, value } of headerEntries) {
|
|
13
|
-
const key = name.toLowerCase();
|
|
14
|
-
if (seen.has(key)) {
|
|
15
|
-
headers.append(name, value);
|
|
16
|
-
} else {
|
|
17
|
-
headers.set(name, value);
|
|
18
|
-
seen.add(key);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
|
|
23
5
|
export const createRoute = (initOrMiddleware) => {
|
|
24
6
|
const getInit =
|
|
25
7
|
typeof initOrMiddleware === "function"
|
|
@@ -49,28 +31,40 @@ export const createRoute = (initOrMiddleware) => {
|
|
|
49
31
|
...additionalContext,
|
|
50
32
|
};
|
|
51
33
|
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
{
|
|
70
|
-
isStartLine: (line) => line.startsWith("HTTP/"),
|
|
71
|
-
trimBodyLines: true,
|
|
34
|
+
// Same rule tag-request.mjs/tag-response.mjs use: the first
|
|
35
|
+
// occurrence of a header name overwrites whatever was already set
|
|
36
|
+
// (e.g. from `init`), a second-or-later occurrence of the *same
|
|
37
|
+
// name* accumulates instead of overwriting it. Applied immediately
|
|
38
|
+
// per header line (via onHeaderLine below), not batched until the
|
|
39
|
+
// whole parse finishes -- a *later* function substitution's
|
|
40
|
+
// context.setHeader() call must still be able to overwrite an
|
|
41
|
+
// earlier template header line, exactly like the original
|
|
42
|
+
// chunk-by-chunk implementation did.
|
|
43
|
+
const seenHeaderNames = new Set();
|
|
44
|
+
const applyHeaderLine = (name, value) => {
|
|
45
|
+
const key = name.toLowerCase();
|
|
46
|
+
if (seenHeaderNames.has(key)) {
|
|
47
|
+
headers.append(name, value);
|
|
48
|
+
} else {
|
|
49
|
+
headers.set(name, value);
|
|
50
|
+
seenHeaderNames.add(key);
|
|
72
51
|
}
|
|
73
|
-
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const { startLine, body } = await parseHttpText(strings, substitutions, {
|
|
55
|
+
isStartLine: (line) => line.startsWith("HTTP/"),
|
|
56
|
+
trimBodyLines: true,
|
|
57
|
+
onHeaderLine: applyHeaderLine,
|
|
58
|
+
// Function-valued substitutions are create-route's one feature
|
|
59
|
+
// the shared parser doesn't (and shouldn't) know about on its
|
|
60
|
+
// own: a substitution can run arbitrary async work and mutate
|
|
61
|
+
// `context` (headers/status) as a side effect, in addition to
|
|
62
|
+
// contributing a value to the template. Resolved inline, at the
|
|
63
|
+
// exact point the scan reaches it, so its side effects land in
|
|
64
|
+
// the same left-to-right order as the template text itself.
|
|
65
|
+
resolveSubstitution: (sub) =>
|
|
66
|
+
typeof sub === "function" ? sub(request, context) : sub,
|
|
67
|
+
});
|
|
74
68
|
|
|
75
69
|
if (startLine !== null) {
|
|
76
70
|
const [, statusCodeText, ...statusTextParts] = startLine.split(" ");
|
|
@@ -81,8 +75,6 @@ export const createRoute = (initOrMiddleware) => {
|
|
|
81
75
|
}
|
|
82
76
|
}
|
|
83
77
|
|
|
84
|
-
mergeHeaderEntries(headers, headerEntries);
|
|
85
|
-
|
|
86
78
|
let responseBody = body;
|
|
87
79
|
const isBinaryBody = typeof responseBody !== "string";
|
|
88
80
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnhenry/letterpress",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "A tagged-template-string HTTP routing library for defining and matching request handlers with plain JavaScript syntax.",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"types": "types.d.ts",
|
package/rewrite.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* see the README's provenance note) as `createRewriter`, but was dropped
|
|
9
9
|
* during the leroute -> @johnhenry/letterpress rename. It's ported here
|
|
10
10
|
* unchanged in behavior, discovered missing while porting a downstream
|
|
11
|
-
* consumer (
|
|
11
|
+
* consumer (a private request inspector/proxy) that relies on it for
|
|
12
12
|
* proxy-mode rewriting.
|
|
13
13
|
*
|
|
14
14
|
* Rules format:
|
package/tag-request.mjs
CHANGED
|
@@ -45,7 +45,7 @@ export const tagRequest = (defaults = {}) => {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
return async function (strings, ...substitutions) {
|
|
48
|
-
const { startLine, headerEntries, body } = parseHttpText(
|
|
48
|
+
const { startLine, headerEntries, body } = await parseHttpText(
|
|
49
49
|
strings,
|
|
50
50
|
substitutions
|
|
51
51
|
);
|
package/tag-response.mjs
CHANGED
|
@@ -26,7 +26,7 @@ const mergeHeaderEntries = (headerEntries) => {
|
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
export const tagResponse = async function (strings, ...substitutions) {
|
|
29
|
-
const { startLine, headerEntries, body } = parseHttpText(
|
|
29
|
+
const { startLine, headerEntries, body } = await parseHttpText(
|
|
30
30
|
strings,
|
|
31
31
|
substitutions
|
|
32
32
|
);
|
package/test/route.mjs
CHANGED
|
@@ -124,6 +124,27 @@ ${"body text"}`;
|
|
|
124
124
|
assert.strictEqual(await response.text(), "body text");
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
+
it("should apply a later function substitution's setHeader() after an earlier template header line (ordering regression)", async () => {
|
|
128
|
+
// Regression test: the first version of the create-route.mjs/
|
|
129
|
+
// parse-http-text.mjs backport resolved every function-valued
|
|
130
|
+
// substitution in a pre-pass, before any template header line was
|
|
131
|
+
// parsed -- so a context.setHeader() call from a function appearing
|
|
132
|
+
// *later* in the template always lost to an earlier template header
|
|
133
|
+
// line, reversing the original left-to-right "last write wins"
|
|
134
|
+
// ordering. Substitutions must now be resolved (and header lines
|
|
135
|
+
// applied) at the exact point the scan reaches them.
|
|
136
|
+
const route = createRoute()`HTTP/1.1 200 OK
|
|
137
|
+
X-Foo: from-template-line
|
|
138
|
+
Content-Type: text/plain
|
|
139
|
+
|
|
140
|
+
${async (_, { response }) => {
|
|
141
|
+
response.headers.set("X-Foo", "from-function-side-effect");
|
|
142
|
+
return "body";
|
|
143
|
+
}}`;
|
|
144
|
+
const response = await route(new Request("https://example.com"));
|
|
145
|
+
assert.strictEqual(response.headers.get("X-Foo"), "from-function-side-effect");
|
|
146
|
+
});
|
|
147
|
+
|
|
127
148
|
it("should handle streaming responses", async () => {
|
|
128
149
|
const route = createRoute({ streaming: true })`
|
|
129
150
|
${async (_, { response }) => {
|
package/test/tag-req-res.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test } from "node:test";
|
|
2
2
|
import { strict as assert } from "node:assert";
|
|
3
|
-
import { tagRequest, tagResponse } from "../index.mjs";
|
|
3
|
+
import { tagRequest, tagResponse, createResponse } from "../index.mjs";
|
|
4
4
|
|
|
5
5
|
const createReadableStream = (data) =>
|
|
6
6
|
new ReadableStream({
|
|
@@ -251,3 +251,34 @@ hello`;
|
|
|
251
251
|
assert.equal(auto.headers.get("content-length"), "5");
|
|
252
252
|
});
|
|
253
253
|
});
|
|
254
|
+
|
|
255
|
+
test("multi-line bodies are not duplicated (issue #20)", async (t) => {
|
|
256
|
+
await t.test("tagResponse", async () => {
|
|
257
|
+
const response = await tagResponse`HTTP/1.1 200 OK
|
|
258
|
+
|
|
259
|
+
a
|
|
260
|
+
b
|
|
261
|
+
c`;
|
|
262
|
+
assert.equal(await response.text(), "a\nb\nc");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await t.test("tagRequest", async () => {
|
|
266
|
+
const request = await tagRequest()`POST /x HTTP/1.1
|
|
267
|
+
|
|
268
|
+
a
|
|
269
|
+
b
|
|
270
|
+
c`;
|
|
271
|
+
assert.equal(await request.text(), "a\nb\nc");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
await t.test("createResponse with substitution in a later line", async () => {
|
|
275
|
+
const name = "x";
|
|
276
|
+
const response = await createResponse`HTTP/1.1 200 OK
|
|
277
|
+
Content-Type: text/plain
|
|
278
|
+
|
|
279
|
+
line1
|
|
280
|
+
line2 ${name}
|
|
281
|
+
line3`;
|
|
282
|
+
assert.equal(await response.text(), "line1\nline2 x\nline3");
|
|
283
|
+
});
|
|
284
|
+
});
|
|
@@ -39,8 +39,27 @@ const BINARY_BODY_TYPES = (value) =>
|
|
|
39
39
|
// this -- body content there is literal, preserved exactly as written,
|
|
40
40
|
// since trimming could corrupt meaningful whitespace in a text body.
|
|
41
41
|
// Defaults to false (the strict behavior); create-route.mjs passes true.
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
// options.resolveSubstitution(value, index): called (and awaited) for
|
|
43
|
+
// each substitution at the exact point the scan reaches it -- not
|
|
44
|
+
// pre-resolved up front -- so a caller like create-route.mjs can run a
|
|
45
|
+
// function-valued substitution's side effects (context.setHeader, etc.)
|
|
46
|
+
// in the same left-to-right order the original chunk-by-chunk processor
|
|
47
|
+
// used. Defaults to the identity function.
|
|
48
|
+
// options.onHeaderLine(name, value): called immediately as each header
|
|
49
|
+
// line (or Headers-object substitution) is parsed, interleaved with
|
|
50
|
+
// resolveSubstitution calls in the same left-to-right order -- lets
|
|
51
|
+
// create-route.mjs apply a template header line to its live `headers`
|
|
52
|
+
// object right away, so a *later* function substitution's
|
|
53
|
+
// context.setHeader() call still overwrites it (matching the original
|
|
54
|
+
// implementation's ordering), rather than only being visible once the
|
|
55
|
+
// whole parse finishes.
|
|
56
|
+
export const parseHttpText = async (strings, substitutions, options = {}) => {
|
|
57
|
+
const {
|
|
58
|
+
isStartLine = () => true,
|
|
59
|
+
trimBodyLines = false,
|
|
60
|
+
resolveSubstitution = (value) => value,
|
|
61
|
+
onHeaderLine = null,
|
|
62
|
+
} = options;
|
|
44
63
|
|
|
45
64
|
const stringsArr = [...strings];
|
|
46
65
|
stringsArr[0] = stringsArr[0].replace(/^[ \t\r\n]+/, "");
|
|
@@ -107,10 +126,10 @@ export const parseHttpText = (strings, substitutions, options = {}) => {
|
|
|
107
126
|
} else {
|
|
108
127
|
const colonIndex = lineBuffer.indexOf(":");
|
|
109
128
|
if (colonIndex > 0) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
129
|
+
const name = lineBuffer.slice(0, colonIndex).trim();
|
|
130
|
+
const value = lineBuffer.slice(colonIndex + 1).trim();
|
|
131
|
+
headerEntries.push({ name, value });
|
|
132
|
+
if (onHeaderLine) onHeaderLine(name, value);
|
|
114
133
|
}
|
|
115
134
|
}
|
|
116
135
|
}
|
|
@@ -123,6 +142,7 @@ export const parseHttpText = (strings, substitutions, options = {}) => {
|
|
|
123
142
|
if (p > 0) {
|
|
124
143
|
if (zone === "body") {
|
|
125
144
|
commitBodyLine(lineBuffer, false);
|
|
145
|
+
lineBuffer = "";
|
|
126
146
|
} else {
|
|
127
147
|
flushLine();
|
|
128
148
|
}
|
|
@@ -131,9 +151,12 @@ export const parseHttpText = (strings, substitutions, options = {}) => {
|
|
|
131
151
|
}
|
|
132
152
|
|
|
133
153
|
if (i < substitutions.length) {
|
|
134
|
-
const sub = substitutions[i];
|
|
154
|
+
const sub = await resolveSubstitution(substitutions[i], i);
|
|
135
155
|
if (sub instanceof Headers) {
|
|
136
|
-
for (const [name, value] of sub)
|
|
156
|
+
for (const [name, value] of sub) {
|
|
157
|
+
headerEntries.push({ name, value });
|
|
158
|
+
if (onHeaderLine) onHeaderLine(name, value);
|
|
159
|
+
}
|
|
137
160
|
} else if (zone !== "headers" && BINARY_BODY_TYPES(sub)) {
|
|
138
161
|
if (pendingBinary) {
|
|
139
162
|
throw new TypeError(
|