"A Swift engine behind a Serve twin must not listen through Network.framework."
When the Engine Is Swift
Most family engines are Python services. A few serve HTTP from Swift, among them the terminal daemon and the prose editor's sync engine on the office Mac that its iPad and iPhone replicas talk to. The kit gives them one shared listener, and it deliberately does very little: one bounded HTTP/1.1 request per connection, Connection: close, loopback by default, and a body limit every engine must state because no default could be right for both a 16 KiB command and a 64 MiB document.
The Respawn That Could Not Bind
The sync engine first listened with NWListener on 127.0.0.1:7200, and Tailscale Serve published its HTTP twin on port 7200 of the tailnet addresses. It worked until the engine restarted. From then on, every launchd respawn failed with NWError 48, Address already in use, and both Serve routes answered 502. Nothing held 127.0.0.1:7200. The Serve daemon held 7200 on the tailnet's IPv4 and IPv6 addresses, and it had only ever worked because the engine happened to start before the twin existed. A reboot with Tailscale up first orders it the same broken way.
Every NWListener spelling failed: a required local endpoint of IPv4 loopback, the same as a string, a loopback interface type, local-only acceptance, endpoint reuse on and off. A plain bind(2) of 127.0.0.1:7200 beside the Serve twin succeeded, with or without SO_REUSEADDR. The reproduction needs no tailnet: hold the port on [::1] with IPV6_V6ONLY, then ask both APIs for 127.0.0.1. Run again for this lesson with Swift 6.3.3 on macOS 26, the result was the same. So the kit listener now sits on a BSD socket and serves each accepted descriptor with dispatch sources, and the family rule is simply: bind(2) behind a Serve twin.
Two earlier NWListener findings are worth knowing even so. cancel() returns before the port is released, so an in-process restart on the same port needs to wait for the .cancelled state. And a reply written in parts over NWConnection must use one content context for every part, or the body waits behind a head that never completes until the client times out.
What the Listener Refuses, and In What Order
The order is pinned: parse the head, then check Host and Origin, then apply the body rules. A request with a foreign Host and a 4 GiB length gets 403, not 413, so an untrusted caller learns nothing about which routes accept bodies. Through the HTTP twin the Host header stays the Mac's tailnet name with the port, so the engine's allowed hosts must name it. Only GET and POST, no Transfer-Encoding, and no Expect header, which means curl -d @file with a body over 1 MiB is refused because curl sends Expect: 100-continue. After replying, the listener shuts down its write side and keeps reading briefly: closing a socket with unread input sends a reset that can destroy a 413 before the client reads it.
A handler that does synchronous disk work, like a sync engine scanning its data folder, goes through the listener's blocking handler door on a dispatch queue, not the async one, so it never occupies a thread of Swift's cooperative pool for the length of a scan.