Repository navigation
Releases: antrixy/reqtrail
Release list
v0.5.0
The request reqtrail sends is now the request it built. 0.4.1's transport re-read the URL it had just shown you and rebuilt what to send from that text, and two things were lost on the way. 0.5.0 builds the endpoint and target once, in the core, and the transport sends them without parsing anything.
Every example below is captured output from the 0.5.0 binary; the wire lines are what a raw receiver captured.
A bare ? is sent as shown
$ reqtrail run bare.json
GET http://127.0.0.1:PORT/health?
Host: 127.0.0.1:PORT (derived)
200
The receiver's request line, 0.4.1 then 0.5.0:
GET /health HTTP/1.1
GET /health? HTTP/1.1
IPv6 literals can be sent
$ reqtrail resolve ipv6.json
GET http://[::1]:8080/health?
Host: [::1]:8080 (derived)
0.4.1 resolved this correctly and then failed ENOTFOUND: it handed [::1] — brackets included — to Node as a hostname. 0.5.0 connects to ::1.
HTTPS to an IPv6 literal fails on some Node versions, and reqtrail does not work around it. Node's security releases for CVE-2026-48618 stopped matching IPv6 addresses against a certificate's IP entries (nodejs/node#64144), so a correctly certified https://[::1]/… fails with ERR_TLS_CERT_ALTNAME_INVALID, exit 3. Measured on Node 22.23.2; Node 24.4.1 verifies it; fixed upstream in Node 26.6.0. The failure is closed, and reqtrail does not check certificates itself.
Newly refused: framing headers, and most Connection values
reqtrail sends GET without a body. A workspace that sets Content-Length, Transfer-Encoding, Expect, Upgrade or Trailer is now refused, whatever the value:
$ reqtrail run framing.json
reqtrail: headers[0]: a workspace may not set "Transfer-Encoding"; reqtrail sends GET without a body and does not model message framing, so this header would change what is sent in a way the request shown would not [header.framing]
On 0.4.1 each of these changed what went out, or made the send wait, in a way the shown request did not: Transfer-Encoding: chunked made Node write a body, 0\r\n\r\n, that nothing displayed; Content-Length: 5 left a real server waiting for five bytes forever; Upgrade answered with 101 never returned; Trailer exited 3, "send attempted", when nothing had been attempted — it is now exit 1.
Connection may appear once, as close or keep-alive. Anything else is refused, and the refusal does not show the value, which may be a secret:
$ reqtrail run conn.json
reqtrail: headers[0]: Connection may appear once, with the value close or keep-alive; reqtrail does not model other connection options, so it does not send them. The value is not shown [header.connection]
This is why 0.5.0 is not a patch release. Some of these refusals remove false output; others refuse a header that 0.4.1 sent as written — Expect: 100-continue, Connection: Upgrade. If a workspace of yours sets one, remove it. TE, Keep-Alive and Proxy-Connection are not refused.
Evidence
Pre-registered before the change: EXACT-TRANSPORT-PREREGISTRATION.md. What happened, including four predictions that were wrong and a Node regression found along the way: EXACT-TRANSPORT-EVIDENCE.md.
v0.4.1
0.4.1 fixes places where reqtrail's output said something untrue or unsafe. No transport code changed: what run sends is byte-for-byte what 0.4.0 sent.
Every sample below was captured from the release commit 2a85d12, not written by hand.
Two workspaces that worked in 0.4.0 now refuse
Both were being turned into output the file did not express.
A URL with userinfo. 0.4.0 displayed user:pw@ and sent neither the userinfo nor an Authorization header.
$ reqtrail resolve userinfo.json
reqtrail: url: the URL contains userinfo (user:password@); reqtrail does not send it, so it refuses to show it. Put credentials in a header instead [url.userinfo]
[exit 1]
A file that is not valid UTF-8. 0.4.0 replaced a bad byte with U+FFFD and resolved a raw 0x80 to %EF%BF%BD, exit 0.
$ reqtrail resolve bad-utf8.json
reqtrail: bad-utf8.json: file is not valid UTF-8; reqtrail reads it byte for byte and will not repair it [workspace.encoding]
[exit 1]
Output that is now true
A mask stands for bytes that are sent. An empty secret showed ?k=•••• in 0.4.0; the wire carried ?k=.
$ EMPTY= reqtrail resolve empty.json
GET https://api.example.com/x?k=
Host: api.example.com (derived)
Substitutions
url {{$env.EMPTY}} environment •••• (masked)
warning: url: environment variable EMPTY is set but empty
[exit 0]
A secret is never named by its code point. For a one-character secret Ā, 0.4.0's refusal contained U+0100 — the whole value in another notation.
$ reqtrail resolve charset.json
reqtrail: headers[0]: the value of "{{$env.KEY}}" contains a character the transport will not accept; it is not shown because the value is secret. Header values may contain tab, U+0020-U+007E and U+0080-U+00FF [header.charset]
[exit 1]
A workspace cannot rewrite your terminal line. A carriage return in a key used to return the cursor and overwrite reqtrail:. CR, bidi controls and line separators are now escaped, in workspace content and in echoed arguments.
$ reqtrail resolve cr.json
reqtrail: x\u000dFAKE: all good: unknown key "x\u000dFAKE: all good"; allowed here: "version, variables, requests" [schema.unknown-key]
[exit 1]
--help and the README no longer say run shows the request before sending it. It never did: run sends, then shows the request with the status. To see a request without sending it, use resolve.
reqtrail ui says it is a snapshot, in its startup banner and on the page: it shows the file as it was when it started, and needs a restart after an edit.
Also: root-level refusal paths lose a stray leading dot (oops, not .oops), and the README's "not here" list no longer says run does not exist.
Still true in 0.4.1
runsends before it displays, and has no timeout.- Closing stdout early (
reqtrail --help | head -n 0) prints a Node stack trace. - An IPv6 literal URL fails as a DNS error, and a URL ending in a bare
?is sent without the?.
These are the next releases' work.
Evidence, with every prediction resolved and the one that was wrong kept as written: HONESTY-PATCH-EVIDENCE.md.
reqtrail v0.4.0 — the verified send
reqtrail 0.4.0 — run sends https
run refused every https:// URL with transport.unsupported, exit 1, while
every example in the README is HTTPS. It sends them now.
Sending
$ API_TOKEN=... reqtrail resolve example.reqtrail.json
GET https://api.example.com/users?q=a%20b
Host: api.example.com (derived)
Authorization: Bearer ••••
Substitutions
url {{baseUrl}} variables.baseUrl https://api.example.com
headers[0] {{$env.API_TOKEN}} environment •••• (masked)
resolve still sends nothing, ever. run prints the same request and then
sends it.
Certificate verification cannot be turned off
A certificate reqtrail cannot verify is exit 3 — bytes were attempted — and the
request is still printed, because the diagnosis is the product:
$ API_TOKEN=... reqtrail run workspace.json
GET https://localhost:42607/users
Host: localhost:42607 (derived)
Authorization: Bearer ••••
Substitutions
headers[0] {{$env.API_TOKEN}} environment •••• (masked)
not sent: DEPTH_ZERO_SELF_SIGNED_CERT
$ echo $?
3
There is no flag to skip verification, and the environment cannot skip it
either. Node reads NODE_TLS_REJECT_UNAUTHORIZED process-wide, so =0
disables certificate checking for everything else that process sends. reqtrail
passes rejectUnauthorized: true explicitly, which wins over it. With that
variable set, Node prints its own stderr warning saying validation is disabled;
under reqtrail that warning is false, and test/run-tls.mjs proves the send
still fails.
When verification fails, no request bytes reach the server. The test
receiver captures zero bytes on every refused certificate, so a secret in a
header never leaves the machine.
Exit codes, unchanged in meaning
0 resolved, or sent and answered
1 refused or unresolved — the fix is in your file
2 usage
3 send attempted and failed
An untrusted certificate, a certificate issued for a different host, and a DNS
failure are all 3.
What this release does NOT do
sendhas no timeout. TLS adds a way to hang: a handshake against a
silent host waits indefinitely. Known, and not fixed here.- No proxy support, no HTTP/2, no client certificates. None of these worked
over plain HTTP either. - No test sends to port 443. The default-port
Hostderivation is confirmed
by the capturedresolveoutput above and by readingsrc/core/url.js, not
by an automated test. - The
--jsondocument still carries no version of its own.
Verification
npm test on a clean checkout of the tagged commit:
refusals 40/40 leak-audit 0 of 31 fixtures leak
selftest 201/201 ui 27/27
parity 7/7 server 51/51
server-slow 2/2 wire 22/22
wire-tls 16/16 run-tls 12/12
Sitting A, real Chrome: 14 of 16 predictions held, unchanged from 0.3.0.
HTTPS-EVIDENCE.md records all ten predictions, three of them falsified: the
line-count ceiling, the expectation that GitHub would flag the committed
test-only private key, and the estimate of how much the suite would grow. It
also records that main was broken for nine days before this work started, and
that --help claimed run refused HTTPS for one commit after it stopped
doing so.
reqtrail v0.3.0 — the transport
reqtrail v0.3.0 — the transport
reqtrail run sends the request it just showed you. Real output, not a sketch:
$ API_TOKEN=... reqtrail run example.reqtrail.json --request get-user
GET http://api.internal/users/42?q=a%20b
Host: api.internal (derived)
Authorization: Bearer ••••
X-Tag: alpha
X-Tag: beta
X-Empty:
Substitutions
url {{baseUrl}} variables.baseUrl http://api.internal
url {{userId}} variables.userId 42
url {{query}} variables.query "a b" → a%20b
headers[0] {{$env.API_TOKEN}} environment •••• (masked)
200
What changed
run is a second consumer of the same pair resolve reads. It builds no
request of its own — that was the point of shipping 0.2.0 early. What it sends
is what resolve displays, and test/wire.mjs checks that on every npm test
by comparing bytes a raw socket recorded against the request the core built.
Host is derived and shown. It used to be supplied invisibly by node:http
from an object of headers. reqtrail now hands the transport an ordered array,
the runtime supplies no Host, and reqtrail derives its own from the URL — with
the same secret-masking the URL gets, so a secret hostname is masked in Host
too. Connection is now the only header reaching a receiver without appearing
in the display.
Exit code 3 is reachable. Documented since 0.1.0 and impossible until now.
run exits 3 when bytes were attempted and the transport failed. A non-2xx
response is exit 0 — the send worked and the server answered.
A failed send reports a code and nothing else. Node puts request values in
its error prose (getaddrinfo ENOTFOUND your-host), and a hostname can come
from a secret. Only the code is surfaced.
Breaking
--json header arrays changed shape.
- Every entry gains
"origin", either"workspace"or"derived". - A
Hostentry is prepended when the URL resolves. Anything indexing
projection.headers[0]now getsHost.
A workspace that sets its own Host is refused (header.host). Two Host
headers on the wire is a request-smuggling shape, so this is a refusal rather
than a merge.
Scope
Plain HTTP only. resolve will show an https:// request; run refuses it
with exit 1, because the fix is in your file rather than in the network.
node:https is imported by nothing. Response bodies, redirects and timeouts are
not in this release. reqtrail ui stays read-only with no send button.
Evidence
TRANSPORT-EVIDENCE.md records what was found, including the predictions that
were wrong and a defect found after the release was called complete.
TRANSPORT-PREREGISTRATION.md holds the predictions, written before the code
and unedited.
Suite at this tag: refusals 41/41, selftest 196/196, leak-audit 0 of 28 fixtures
leaking, ui 27/27, parity 7/7 byte-identical, server 51/51, server-slow 2/2,
wire 24/24.
reqtrail 0.2.0 — the boundary
One construction site, now enforced rather than intended: the exact request is a
private value and the public view is derived from it by a single function.
inspect consumes that pair; run will be the second consumer. Nothing is sent
— there is still no transport.
reqtrail resolve <file> [--request <id>] [--json]
reqtrail ui <file> [--request <id>]
Breaking, if you parse --json: prepared is now projection. The
document is otherwise byte-identical to 0.1.0. schemaVersion stays 1 — it
versions the workspace file, not the output.
Node 22+. Zero runtime dependencies.
Pre-registered in BOUNDARY-PREREGISTRATION.md, results in
BOUNDARY-EVIDENCE.md, including two predictions that were wrong. The
derivation is proven; that the exact request matches the wire is not, and cannot
be until something sends. Sending arrives in 0.3.0.
reqtrail 0.1.0 — the inspector
Shows the request it would hand to the transport — method, URL, headers —
and where every substituted value came from. Nothing is sent: there is no
transport in this release.
reqtrail resolve <file> [--request <id>] [--json]
reqtrail ui <file> [--request <id>]
Node 22+. Zero runtime dependencies.
What is verified, and how, is in EVIDENCE-0.1.0.md. The claim that the
prepared request matches what a receiver records comes from slice 0, a
frozen harness in slice0/ that ships with the package — not from anything
this release does on your machine. Sending arrives in 0.3.0.