Skip to content

fix(router): detect Docker Desktop connection resets on router ports after alias updates - #9026

Merged
rfay merged 4 commits into
ddev:mainfrom
rfay:20261028_rfay_docker_desktop_router_reset
Oct 30, 2026
Merged

rfay merged 4 commits into
ddev:mainfrom
rfay:20261028_rfay_docker_desktop_router_reset

Conversation

@rfay

@rfay rfay commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Short Summary (TL;DR)

After DDEV reconnects ddev-router to update its network aliases, Docker Desktop on macOS can keep forwarding a router port to a stale target, so requests get connection reset by peer. DDEV now warns about it, and TestGetWebContainerDirectURLsWithGenericWebserver logs better diagnostics and tolerates the delay on Docker Desktop.

The Issue

Docker Desktop on macOS has reset connections to ddev-router ports 80/443 for years. It happens after the router's network endpoint changes, either because DDEV recreates the router or because it disconnects and reconnects it to ddev_default.

History:

Since 2026-10-08, TestGetWebContainerDirectURLsWithGenericWebserver failed 15 times on ddev-macos-arm64-mutagen, counting retried jobs. That is about 7% of test jobs on both agents, and it also failed on main. Every failure came right after Updating router network aliases in place. Traefik had the route loaded, and the router log showed no request. Port 443 or 80 still reset the connection 5s later, while the other port and the direct web container port worked. TestCmdLogs shows the same reset.

Examples:

How This PR Solves The Issue

  • netutil.IsPortUnreachable (formerly IsPortRefused) sends a minimal HTTP request. A refused connection, or one that closes or resets before any response, counts as unreachable.
  • warnUnreachableRouterPorts also runs after the in-place alias update.
  • logRouterDiagnostics logs the router's published ports. It curls the URL from a container on ddev_default, which reaches the router directly. It also curls from a container on the host network, which skips Docker Desktop's host-side forwarding. Then it polls the URL for up to 60s and logs how long recovery takes.
  • On Docker Desktop, the test passes when the URL recovers within that time. Otherwise it fails as before.

This PR does not fix Docker Desktop. It makes the problem visible to users and gives us the recovery time we need to decide between polling and skipping.

Manual Testing Instructions

ddev utility download-ddev --pr 9026
export PATH="$HOME/tmp/ddev-download-ddev/pr-9026:$PATH"
ddev start   # in a first project
ddev start   # in a second project with a new hostname

Expect no nothing answers on host port(s) warning on a healthy Docker provider.

Automated Testing Overview

TestIsPortUnreachable covers answering, refused, reset, and closed-without-answer listeners. In the Docker Desktop CI logs, look for recovered after Ns from logRouterDiagnostics. It shows whether polling is enough here and in TestCmdLogs, or whether these tests need the #8627 skip.

🤖 Developed with assistance from Claude Code

@rfay
rfay requested a review from a team as a code owner October 28, 2026 15:25
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Download this PR's build for your OS and architecture with:

ddev utility download-ddev --pr 9026

Or download the artifact for your OS and architecture directly:

See Testing a PR.

Open in GitHub Codespaces

@rfay
rfay requested a review from stasadev October 28, 2026 18:36

@tyler36 tyler36 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have access to a Mac so I can't confirm this resolves the issue.

However, I did spin up multiple PHP sites and a Laravel 13 site without issue.

Tested on WSL3 (Ubuntu 24.04.5) Win10

@stasadev stasadev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing to fix in the test and a small one, both inline.

Claude Code ran two ddev starts with a new hostname on Docker 29 on Linux. The alias update path ran the new check without a warning, and every router port, 443 included, answered the plain HTTP probe. TestIsPortUnreachable fails at the reset case with the old dial-only check.

🤖 Reviewed with assistance from Claude Code

Comment thread pkg/ddevapp/ddevapp_test.go Outdated
t.Logf("%s failed at first (err=%v) and recovered; a known Docker Desktop port-forwarding delay", u, err)
continue
}
testcommon.AssertLocalHTTPContent(t, u+"/testfile.html", want,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This IsDockerDesktop() gate doesn't do anything. On any other provider, once logRouterDiagnostics returns recovered, the code falls through to AssertLocalHTTPContent, which makes one more request after the URL already recovered, so it passes. A URL that fails and recovers within 60s now passes everywhere, not only on Docker Desktop. Maybe fail on the first error instead:

t.Errorf("every reported URL should serve the project: %s err=%v body=%.200s", u, err, body)

Comment thread pkg/ddevapp/ddevapp_test.go Outdated
t.Logf("router diagnostics: last router log lines (err=%v):\n%s", err, logs)
ports, err := exec.RunHostCommand("docker", "port", router.ID)
t.Logf("router diagnostics: router published ports (err=%v):\n%s", err, ports)
for _, netMode := range []string{dockerutil.NetName, "host"} {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small thing: this curl from ddev_default always fails for the direct http://127.0.0.1:<port> URL, since 127.0.0.1 is the curl container itself there. Maybe skip that network for loopback URLs so the log doesn't mislead.

rfay and others added 4 commits October 30, 2026 09:01
…after alias updates

## Short Summary (TL;DR)

After DDEV reconnects `ddev-router` to update its aliases, Docker Desktop on macOS can forward a router port to a stale target, so requests get `connection reset by peer`. DDEV now warns about it, and `TestGetWebContainerDirectURLsWithGenericWebserver` logs better diagnostics and tolerates the delay on Docker Desktop.

## The Issue

`TestGetWebContainerDirectURLsWithGenericWebserver` failed 15 times on `ddev-macos-arm64-mutagen` since 2026-10-08, counting retried jobs. That is about 7% of test jobs. Every failure came right after `Updating router network aliases in place`. Traefik had the route, but `127.0.0.1:443` or `:80` reset the connection and was still resetting 5s later.

The reset is a long-standing Docker Desktop problem. ddev#8627 skipped about 18 tests for it. This test started requesting router URLs in ddev#8903, after that skip.

`warnUnreachableRouterPorts` could not see it: it only looked for refused connections, and it only ran when the router was recreated.

## How This PR Solves The Issue

- `netutil.IsPortUnreachable` (formerly `IsPortRefused`) sends a minimal HTTP request. A refused connection, or one that closes or resets before any response, counts as unreachable.
- `warnUnreachableRouterPorts` also runs after the in-place alias update.
- `logRouterDiagnostics` logs the router's published ports and curls the URL from containers on `ddev_default` and the host network. It then polls the URL for up to 60s and logs how long recovery takes.
- On Docker Desktop, the test passes when the URL recovers within that time. Otherwise it fails as before.

## Manual Testing Instructions

```bash
ddev utility download-ddev --pr 9026
export PATH="$HOME/tmp/ddev-download-ddev/pr-9026:$PATH"
ddev start   # in a first project
ddev start   # in a second project with a new hostname
# Expect no "nothing answers on host port(s)" warning on a healthy provider
```

## Automated Testing Overview

`TestIsPortUnreachable` covers answering, refused, reset, and closed-without-answer listeners. In the Docker Desktop CI logs, look for `recovered after Ns` from `logRouterDiagnostics`. It shows whether polling is enough here and in `TestCmdLogs`, or whether these tests need the ddev#8627 skip.

🤖 Developed with assistance from [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Linux, a reset that arrives right after the handshake can fail the dial with `connect: connection reset by peer`. `IsPortUnreachable` only counted a refused dial, so it reported the port as reachable and `TestIsPortUnreachable` failed on the Linux GitHub runners. A write after a reset can also fail with `EPIPE`, so that now counts as dropped too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p the loopback curl on ddev_default

Once logRouterDiagnostics reported recovery, the test fell through to
AssertLocalHTTPContent, which passed because the URL now worked, so a
failing-then-recovering URL passed on every provider. Fail on the first
error instead, unless the URL recovered on Docker Desktop.

A curl from ddev_default to 127.0.0.1 reaches the curl container itself,
so skip that network for loopback URLs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rfay
rfay force-pushed the 20261028_rfay_docker_desktop_router_reset branch from 2114cda to a736517 Compare October 30, 2026 15:40
@rfay

rfay commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

Suggested changes done.

I had Claude attempt to recreate the problem we see in tests on macOS DD, it wasn't able to.

The only real risk in this IMO is catering too much to DD, and it's probably not doing too much.

@stasadev stasadev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me.

@rfay
rfay merged commit cab7095 into ddev:main Oct 30, 2026
43 checks passed
@rfay
rfay deleted the 20261028_rfay_docker_desktop_router_reset branch October 30, 2026 20:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants