Skip to content

0.8 cleanup: fd-cleanup unification, LocalAPI error codes, API consistency - #80

Merged
danReynolds merged 4 commits into
mainfrom
danreynolds/0.8-cleanup
Jul 22, 2026
Merged

0.8 cleanup: fd-cleanup unification, LocalAPI error codes, API consistency#80
danReynolds merged 4 commits into
mainfrom
danreynolds/0.8-cleanup

Conversation

@danReynolds

Copy link
Copy Markdown
Owner

Targeted cleanup from the three-way architecture-health review (task #33). The review found the codebase healthy (2/5 debt) with a short list of real-but-cheap items — this is that list, minus the ones inspection showed weren't worth it (see Skipped).

Genuine (close a bug-class / fix a live defect)

  • Unify HTTP send() fd cleanup into one path (_sendOverFds, ownership tracked by a flag not by code position — mirrors the cohesive inbound accept path). Documents PosixFdTransport.adopt's failure-ownership contract on adopt itself. New fault-injection test (http_send_cleanup_test.dart) proves both fds are released when finalize() throws — verified discriminating.
  • DuneStatus/DunePeers now emit error codes: they returned post-call LocalAPI failures via jsonError instead of localAPIError, so a classifiable 403/404 read back as TailscaleErrorCode.unknown on the Dart side. Routed through localAPIError.

Tidy-ups

  • Validation exception consistency (behavior change): UDP caller-argument validation (bad remote port/address, oversize payload) now throws ArgumentError/RangeError like serve/funnel/http, not TailscaleUdpException. Wire-parsing errors (bytes from the native side) stay TailscaleUdpException.
  • Dedupe serve/funnel validators: the byte-identical port/address/path validators (previously in serve.dart, funnel.dart, and Go) now share serve_validation.dart.
  • Pruned 2 dead classifyLocalAPIError substrings; the classifier test now uses the real upstream NodeCanFunnel/CheckFunnelAccess strings so it catches drift.
  • CI gates Go tests on -race; marked the shipped shared-reactor item done in pr-readiness.md.

Skipped (inspection showed poor value/risk) — with reasons

  • Generic withBoundedLocalClient chokepoint: per-site rendering is genuinely bespoke (pre-formatted strings, nested maps, not-found early-returns), and all contexts are already bounded since 0.7.1 — a generic wrapper is a poor fit for marginal enforcement value against real churn. The one live drift it would've prevented (DuneStatus/DunePeers) is fixed directly above.
  • localPort ?? 0 → require-field: Go always sends these fields (verified), so it's a theoretical drift-mask, not a live bug; converting silent-0 to a throw across 7 sites in different error-handling contexts risks turning an impossible case into a stream-breaking throw.
  • profiles/taildrop stub sync-throw: UnimplementedError is an Error — a synchronous throw is the correct loud "not implemented" signal for a stub; you don't want .catchError swallowing it.

Verification

Darwin go vet + -race, dart analyze clean, unit + fd/ffi +118. Linux container + Headscale e2e running. This is 0.8 work — no version bump; changelog written when 0.8 is cut.

🤖 Generated with Claude Code

danReynolds and others added 4 commits July 22, 2026 11:14
…ubstrings

- DuneStatus/DunePeers returned post-call LocalAPI failures via jsonError
  instead of localAPIError, so they emitted no code/statusCode field and a
  classifiable 403/404 read back as TailscaleErrorCode.unknown on the Dart
  side. Route both through localAPIError like every other wrapper.
- Removed two dead classifyLocalAPIError substrings ('no funnel attribute' /
  'does not have funnel attribute') — the real upstream funnel errors both
  begin 'Funnel not available; ...' and already classify via 'not available'.
- Replaced the classifier test's fabricated strings with the exact upstream
  NodeCanFunnel/CheckFunnelAccess text so it catches drift on the live path.

(Considered a generic withBoundedLocalClient chokepoint for the LocalAPI
wrappers but skipped it: the per-site rendering is genuinely bespoke, all
contexts are already bounded since 0.7.1, so a generic wrapper is a poor fit
for marginal enforcement value against real churn.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tion test

send() tracked fd ownership across 3 separate try/catch blocks by code
position, so a future inserted step could silently leak a fd (and pin the Go
request goroutine). Extracted the fd-owning core into _sendOverFds with a
single cleanup path: ownership is tracked by a requestFdTransferred flag, and
one catch closes whatever is still owned — mirroring the cohesive inbound
accept path in api/http.dart.

- Documented PosixFdTransport.adopt's failure-ownership contract on adopt
  itself (registration failure closes the fd; only the up-front arg checks
  leave it untouched) — previously stated only at call sites.
- Added test/integration/fd/http_send_cleanup_test.dart: injects a throwing
  finalize() and asserts BOTH fds are released (peers observe EOF). Verified
  discriminating — reverting the raw-fd close fails it with 'request fd
  leaked'. This closes the fault-injection coverage gap the review flagged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…alidators

- UDP caller-argument validation (invalid remote port/address, oversize
  payload) now throws ArgumentError/RangeError like serve/funnel/http, instead
  of TailscaleUdpException. Wire-parsing errors in _decodeDatagramEnvelope
  (bytes from the native side) stay TailscaleUdpException — those are
  operational, not caller mistakes. BEHAVIOR CHANGE: a bad argument to
  udp.send now surfaces as ArgumentError/RangeError.
- Extracted the byte-identical serve/funnel mount validators (port/address/
  path) into lib/src/api/serve_validation.dart — the rules existed in three
  places (serve.dart, funnel.dart, and Go); now one Dart copy. No behavior
  change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- CI ran 'go test' without -race even though the teardown/reactor harness was
  written to stress register-vs-teardown under the detector. Add -race.
- pr-readiness.md still listed the shared-fd-reactor as a 1.0 launch blocker;
  the sharded reactor shipped. Marked done.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@danReynolds
danReynolds merged commit e214b96 into main Jul 22, 2026
5 of 6 checks passed
@danReynolds
danReynolds deleted the danreynolds/0.8-cleanup branch July 22, 2026 16:58
@danReynolds danReynolds mentioned this pull request Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant