hijack({ sendResponse: false }) flushes any pending write-buffer bytes
without sending the response headers/body. This is intended for use
with the upcoming 1xx support: a handler can send a 101 Switching
Protocols as an interim response via the 1xx mechanism, then call
hijack({ sendResponse: false }) to take ownership of the transport
without double-sending the response.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ServerResponse.hijack() sends the current response then returns the
underlying RawTransport so handlers can switch to a different protocol
(WebSocket, raw TCP, etc.) without the server loop taking over again.
ClientResponse.hijack() returns the raw transport after a 101 response
so the caller can take over the connection for the new protocol.
Both variants prepend any bytes the ReadBuffer had already read ahead,
ensuring no data is lost. The server's handle() loop exits cleanly on
hijack and leaves the transport open. The pool integration in fetch.ts
rejects hijacked connections rather than returning them to the pool.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers: opening PRs with tea and the WIP: prefix convention, and
updating the README AI disclosure at the end of every branch.
CLAUDE.md is symlinked to AGENTS.md for Claude Code compatibility.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous disclosure significantly understated Claude Code's
involvement. It contributed the package scaffolding, a major transport
layer review and bugfix pass, the timeout feature, parse error message
improvements, and the full test suite — not just the initial empty
package and light spec guidance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The packages/http test suite was mostly generated by Claude Code, which
also surfaced and fixed three bugs during that work.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The slot waiters and global waiters are Promise resolve/reject callbacks.
Promise callbacks cannot throw, so the try/catch around them was dead
code that also required c8 ignore annotations to suppress coverage gaps.
Removed both wrappers and moved the errors array to where it's actually
needed (connection close errors).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Excluding unit tests via an env flag doesn't make sense; the flag only
created confusion. RUN_INTEGRATION=false (via test:unit) is the only
useful gate. Removed the export from flags.ts and the { skip } options
from all unit test suites.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
These are API-boundary parameter checks, not internal invariant
assertions. TypeError is the correct error type and the original message
communicates the problem clearly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prettier expanded two compact try/catch blocks from 1 line to 5 lines
each, and split an if/throw across two lines — bumping next 3 → next 5
for the shutdown() catch bodies and next → next 2 for the setTimeout
invariant guard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New tests:
- UnpooledDialer: exercise dialTls success path
- PooledDialer.rejectConnection: cover timeout.has branch by rejecting a
previously-released connection; also fixes a bug where the connection
was not removed from slot.idle before decrementing slot.alive
- PooledDialer.shutdown: cover the global-waiter-races-shutdown path
(releaseConnection fires ok() then shutdown sets #shutdown before the
microtask continuation runs)
Broken Invariant annotations:
- Standardise all invariant error messages to "Broken Invariant: ..."
- Add /* c8 ignore next */ to guards that TypeScript enforces on callers
or that cannot fire under correct internal state:
pool.ts: !connection checks, alive<0, idle>alive, not-in-idle-after-
setTimeout, catch blocks around Promise resolvers, allSettled error
collection, #notify's !slot guard
buffer.ts: slice-returned-fewer-bytes guard
node/transport.ts: string-chunk guard, closed-with-buffers branch,
socket write error callback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cover previously missing branches:
- common/pair.ts: PairSync.close() idempotence and getA()-after-close
- common/reader.ts: _triggerFinishOk/Ko catch blocks (handler throws),
BasicBodyReader #chunks pre-buffer path (slice returns multiple arrays),
ChunkedBodyReader already-closed guard and truncated-chunk sanity check
- common/buffer.ts: WriteBuffer.flush() partial write of a second buffer
- client/objects.ts: method defaults to POST when body is truthy
- client/fetch.ts: rejectConnection called when conn.request() throws
- node/transport.ts: fast-fail read() path with null #readError (socket
destroyed without error event)
- server/connection.ts: console.warn for missing requireHeaders entry,
non-TimeoutError parse error propagates out of handle()
Result: 100% functions, 97.81% branches — remaining gaps are dead code
(defensive throws/catches that cannot fire under normal API usage).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds full test suites for all implementation files and fixes two transport bugs
discovered during testing:
- fix(loopback): `#maybeEnded` checked `this.closed` (requiring BOTH ends closed)
instead of just `#readClosed`; a pending read would hang after the peer closed if
the local write side was still open. Also add fast-fail in `read()` when
`#readClosed` is already set.
- fix(node): same `#maybeEnded` bug as loopback. `close()` used `socket.end()`
(FIN — waits for peer FIN) instead of `socket.destroy()` (RST — immediate), which
caused the test suite to hang. Add `#readError` field so errors seen before
`read()` is called are preserved for the fast-fail path.
New/extended test files:
- src/client/fetch.test.ts — fetch() and makeFetch() integration suite
- src/client/pool.test.ts — UnpooledDialer and PooledDialer (incl. TLS throw paths)
- src/client/connection.test.ts — extended with more parse/request edge cases
- src/loopback/transport.test.ts — loopback pair, listener, halfClose, error paths
- src/node/transport.test.ts — NodeDialer, NodeTransport, NodeListener (incl.
buffered-data path, server error event, dialTls ECONNREFUSED)
- src/server/objects.test.ts — ServerRequestImpl, ServerResponseImpl
- src/server/router.test.ts — extended with optional-param and global middleware
Coverage result: 100% functions, ~99.3% lines, ~97.1% branches (remaining branch
gaps are V8/tsx source-map desync artifacts, not missing test paths).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix vacuous `!clientTransport.closed || conn.ended` → `serverTransport.readEnded`
- Move HTTP/1.0 success-path tests out of the parse error suite
- Fix fire-and-forget `.then()` in loopback whenClosed test (now awaited)
- Add CRLF split across chunk boundary, 3-buffer slice, multi-buffer forward tests
- Add negative chunk size and post-close onFinish handler tests for readers
- Add non-null body with bodyAllowed=false writer test
- Add maxTargetLength boundary, HEAD wire-level, and body auto-drain server tests
- Add global middleware on 404/405 and HEAD-in-Allow router tests
- Add setOptions effect and 1xx-todo client connection tests
- Add TLS path and multiple concurrent global waiters pool tests
- Add new src/client/utils.test.ts covering combineClientConnectionOptions
One test marked { todo }: 1xx interim responses not yet implemented.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add src/test-helpers to tsconfig.json exclude so flags.ts is not
compiled into dist. Add a second --test-coverage-exclude glob to both
coverage scripts so the helper does not appear in the coverage report
or skew aggregate numbers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Change RUN_INTEGRATION default to true so npm test / test:coverage /
test:watch run the full 284-test suite (unit + integration) by default.
RUN_EXTERNAL remains independent and must be set explicitly.
Add test:unit, test:coverage:unit and test:watch:unit convenience scripts
(RUN_INTEGRATION=false) for when only the 226 unit tests are wanted.
Remove test:all, which is now redundant with the new defaults.
Default coverage: 90.9% lines / 92.8% branches / 94.1% functions.
Unit-only coverage: 77.5% lines / 94.8% branches / 89.6% functions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Uses Node's built-in --experimental-test-coverage with tsx as an import
hook — no extra dependencies required.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers common/{buffer,connection,headers,objects,reader,utils,writer},
loopback/transport, server/{connection,router}, and client/{connection,pool}.
All tests use loopbackTransportPair for in-memory transport — no network
access required. Extends the existing server/connection.test.ts with
send() and handle() suites.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the last chunk pushed bodyLength over the limit, the error was
thrown inside the try/catch and then caught — but because body.closed
was already true at that point, the catch block hit `continue` and
discarded the error silently.
Move the maxBodyLength check (and the yield) outside the try/catch so
only the awaited read is guarded, not the enforcement logic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bugs:
- ReadableHttpImpl.bytes() was copying data byte-by-byte; use set()
- NodeTransport and LoopbackTransportHalf were not clearing #errcallback
after a successful read(), causing errors between reads to be silently
swallowed; also gate #maybeEnded to only fire when a read() is pending
- ServerConnection.handle() was draining the request body after closing
the connection; skip the drain entirely on close (it only matters for
keep-alive reuse), and close after draining otherwise
- PooledDialer threw "Pool full" / "Per-origin full" instead of queuing
waiters; implement queuing via slot.waiting and a new #globalWaiting
list; also fix #shutdown never being set to true in shutdown(), and
reserve the slot before the async connect to prevent races
Code quality:
- shouldClose() was comparing Connection header values case-sensitively;
RFC 7230 requires case-insensitive comparison
- Headers._normalized was public (convention only); make it protected
- Headers.headers returned the original wire-case record while
MutableHeaders.headers returned lowercase keys; unify both to return
from _normalized so ServerRequest.headers and ServerResponse.headers
are consistent (both lowercase)
Enhancements:
- RawTransport: add optional halfClose(), readEnded, whenClosed,
remoteAddr, localAddr; NodeTransport and LoopbackTransportHalf
implement all applicable members; addresses cached at construction
before the socket handle can be invalidated
- RawListener: add optional addr; NodeListener exposes the bound
address (critical when listening on port 0)
- LoopbackTransportHalf: internalize _closeFn/_writeFn as constructor
parameters stored in private fields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ping: invalid type strings now reject immediately with an error
- queryDNS: SERVFAIL + no upstreams falls back to dialer then browser DNS
- whoIs: accepts bare IP addresses in addition to ip:port
- types: rename addrPort to addr in whoIs; update JSDoc
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add peerAPIURL field to IPNNetMapNode (shared by self and all peers).
Add localAPI(method, path, body?) method to IPN class and interface,
returning {status, body}. Bump tailscale submodule.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add TypeScript types (WhoIsResponse, PingType, PingResult, DNSQueryResult,
ExitNodeSuggestion) and validated IPN class methods for the four new Go
bindings. Bump tailscale submodule.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add TLSCertKeyPair type and TypeScript wrappers for getCert, listenTLS, and
setFunnel. Add Http example component that provisions a TLS cert, opens HTTP
and HTTPS listeners, serves "Hewwo, world!", and enables Tailscale Funnel on
port 443. Bump tailscale submodule to include the matching Go changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces `npm run build --workspaces --if-present` with `turbo run build`,
which resolves build order from each package's package.json dependencies
and parallelises independent packages automatically.
- turbo.json: declares build task with `^build` topological dependency
- package.json: switches build script to turbo, adds packageManager field
(required by turbo v2), adds turbo to devDependencies
- .gitignore: add .turbo/ cache directory
- tailscale: bump submodule for tsconfig types-pinning fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>