Repository navigation
Description
Problem
Across flutter_tools, package:dwds, dev/bots, package:web_benchmarks, and flutter/devtools, several code paths dynamically locate a "free" TCP port by probing a port (ServerSocket.bind(..., 0) or RawSocket.connect), closing the socket, and passing that port number to an HTTP server or child process (Chrome, chromedriver) to bind later. Other test harnesses hardcode fixed ports (9999, 10000, 10001), pushing downstream callers to add their own port probes.
Probing for a free port before binding has two flaws that cause recurring CI flakes:
- TOCTOU race condition: Once the probe socket closes (or
RawSocket.connectfails), the port is unreserved. Under parallel CI load, another test shard or process can claim the port before the target server or browser binds it. Worse,RawSocket.connectprobes only detect sockets actively listening onlocalhost, missing sockets that are bound but not yet listening. - Address-family and dual-stack mismatch: The probe socket rarely matches the target bind's address family or socket flags. On macOS and Linux, an IPv4 socket bound to
127.0.0.1:Por0.0.0.0:Pdoes not conflict with an IPv6-only (v6Only: true) probe on[::1]:P.- For example,
dwds'sfindUnusedPort()probes[::1]withv6Only: true, thenHttpMultiServer.bind('any', port)binds[::]dual-stack (v6Only: false). If the kernel hands out a port held by an IPv4 socket, the probe succeeds on[::1]and the subsequent[::]bind fails withEADDRINUSE(errno = 48on macOS,98on Linux). Under congested ephemeral port ranges, all 5 retry attempts fail in ~500ms.
- For example,
The Flutter Style Guide already prohibits this pattern ("Never check if a port is available before using it... Instead, have the code that opens the port pick an available port and return it"), and dev/bots/utils.dart even names its helper findAvailablePortAndPossiblyCauseFlakyTests().
Past Flakes & Failures
| Date | Subsystem | Failure Mode & Impact | Links |
|---|---|---|---|
| 2026-10 | flutter/packages DriveExamplesCommand |
Hardcoded chromedriver --port=4444 inside the per-example loop combined with chromedriver.kill() without awaiting exitCode. When a package has multiple web examples (google_maps_flutter_web's example/latest and example/3-64), the dying chromedriver from the first example still holds port 4444 when the second example spawns a new chromedriver --port=4444, so the second chromedriver fails to bind and exits immediately. |
flutter/packages#13355, build 1, build 2, build 3 |
| 2026-10 | dwds startHttpServer |
findUnusedPort() ([::1] v6Only) -> HttpMultiServer.bind('any') ([::] dual-stack) fails with Address already in use (errno = 48). Caused 8 of 15 recent post-submit failures on Mac web_tool_tests. |
#185452, CL 563621, build 1, build 2, build 3 |
| 2026-08 | flutter_tools chrome.dart |
os.findFreePort() (127.0.0.1) -> chrome --remote-debugging-port=<port>. When the port is taken before Chrome binds, Chrome logs Cannot start http server for devtools. and stays alive, hanging flutter test --platform chrome for 30m. |
#192013, #193461 |
| 2025-07 | package:web_benchmarks & flutter/devtools |
Hardcoded ports 9999 and 10000 collide when a prior Chrome holds 127.0.0.1:10000; the new Chrome falls back to [::1]:10000 while the runner connects to the stale IPv4 tab. Worked around in DevTools by copying dev/bots's RawSocket.connect scanner. |
flutter/devtools#9314, flutter/devtools#9315 |
| 2022-08 | dev/bots (run_web_tests.dart, service_worker_test.dart) |
findAvailablePortAndPossiblyCauseFlakyTests() scans upward from 8080 via RawSocket.connect, missing bound-but-not-listening sockets and racing across shards. |
#109474 |
| 2022-02 | dwds TestServer.start |
Inter-process race on findUnusedPort() -> errno = 98, address = 127.0.0.1. |
dart-lang/webdev#1514, dart-lang/webdev#1515 |
| 2021-08 | dwds DebugService.start |
findUnusedPort() -> HttpMultiServer.bind threw EADDRINUSE (errno = 98). |
#87659, dart-lang/webdev#1378, dart-lang/webdev#1379 |
| 2020-10 | flutter_tools WebAssetServer |
Pre-probed port collided on HttpMultiServer bind (address = ::1). |
#69348, #69351 |
Plan
Instead of probing for a free port and closing it, pass 0 directly to the bind() call (or --remote-debugging-port=0 / --port=0 flag) so the OS allocates an ephemeral port atomically for the target address family, then read back the assigned port from server.port or child stdout/stderr. Keep a bounded retry loop around multi-socket binds (HttpMultiServer.loopback).
-
package:dwds: Passport ?? 0directly toHttpMultiServer.bind(hostname, requestedPort)insidestartHttpServerinstead of callingfindUnusedPort().- Fix in
pkg/dwds: https://dart-review.googlesource.com/c/sdk/+/563621 - Publish
dwds27.2.0: https://dart-review.googlesource.com/c/sdk/+/564020 - Roll
dwds27.2.0intoflutter_tools: Roll pub packages #194398
- Fix in
-
package:http_multi_server: RetryHttpMultiServer._loopbackwhenport == 0even on in-processListeningSocketRegistrycollisions (errorCode == -1), and keepv4Serveropen during the retry.- [VM] Intermittent failure to bind to an ephemeral port dart-lang/sdk#47806
- Fix in
pkgs/http_multi_server: [http_multi_server] Retryloopback(0)when IPv6 port is already bound dart-lang/http#2025 - Publish
http_multi_server3.2.3: [http_multi_server] Prepare to release 3.2.3 dart-lang/http#2028 - Roll
http_multi_server3.2.3intoflutter_tools: Roll pub packages #194984
-
flutter_toolsChrome launcher: Pass--remote-debugging-port=0whendebugPort == nullin_spawnChromiumProcessinstead of callingfindFreePort(), and parse the bound host and port from Chrome'sDevTools listening on ws://...stderr line. -
dev/botsweb runner and service worker test: BindHttpServer.bind('localhost', 0)before constructingappUrlindev/bots/browser.dart, supportdebugPort = 0indev/devicelab/lib/framework/browser.dart, and deletefindAvailablePortAndPossiblyCauseFlakyTests(). -
package:web_benchmarks,devicelab, andflutter/devtools: BindHttpServer.bind('localhost', 0)first, readserver.port, and launch Chrome with--remote-debugging-port=0inpackage:web_benchmarksandweb_benchmarks.dartinstead of hardcoding9999/10000/10001, then remove the_findAvailablePortworkaround indevtools_benchmarks_test.dart. -
flutter_toolsresolvePortcleanup: Delete the unreachableif (port == null) return os.findFreePort();branch indevfs_config.dart(WebDevServerConfig.portis a non-nullintdefaulting to0). -
chromedrivertest harness: Launchchromedriver --port=0and parsestarted successfully on port (\d+)from stdout inweb_server_test_common.dartbefore passing--driver-porttoflutter drive, then delete the localfindFreePort()helper. -
flutter/packagesDriveExamplesCommand: Launchchromedriver --port=0, parsestarted successfully on port (\d+)from stdout before passing--driver-porttoflutter drive, and awaitchromedriver.exitCodeafterchromedriver.kill()indrive_examples_command.dartinstead of hardcoding port4444.
Out of Scope
- External binaries that do not report an OS-allocated port:
iproxy(ios/devices.dart) and user-defined custom-device forwarding commands (custom_devices.dart) require a concrete host port on the CLI and do not emit a dynamically bound port. They must retainfindFreePort()with retry re-probing. - Explicit user-supplied port flags (
--web-port,--dds-port,--host-vmservice-port): When omitted,flutter_toolsalready defaults all three to port0. When a user or test explicitly passes a port number (e.g.,observatory_port_test.darttesting that--dds-port=Nis honored), the tool must bindN.
Activity
Replace dynamic free-port probing with OS port-0 binding (flutter#194419
Replace dynamic free-port probing with OS port-0 binding (flutter#194419
[tool] Let the OS pick a port for chromedriver and wait for exit (#13355
[flutter_tools] Retry chromedriver startup when its port is taken (fl…