Opened 4 hours ago
Last modified 3 hours ago
#65484 accepted defect (bug)
Loopback requests fail in the wordpress-develop Docker environment
| Reported by: |
|
Owned by: |
|
|---|---|---|---|
| Milestone: | 7.1 | Priority: | normal |
| Severity: | normal | Version: | |
| Component: | Build/Test Tools | Keywords: | has-patch |
| Focuses: | Cc: |
Description
For the longest time, loopback requests have failed in the built-in Docker dev environment with an error:
cURL error 7: Failed to connect to localhost port 8889 after 4 ms: Could not connect to server
This is easily reproduced via WP-CLI:
npm run env:cli eval "var_dump(wp_remote_get(home_url()));"
But it also is reflected in Site Health, where the loopback_requests failure is surfaces as a critical issue. This also prevents the page_cache test from working.
In order to get WP Cron to work, I've had to resort to adding define( 'ALTERNATE_WP_CRON', true ) to my wp-config.php which is very annoying because whenever cron needs to run, accessing a URL results in a redirect with a ?doing_wp_cron=1773273481.4603710174560546875000 query parameter added to the URL.
Attachments (1)
Change History (4)
#1
@
4 hours ago
- Owner set to westonruter
- Status changed from assigned to accepted
I put Claude on the problem and it very helpfully came up with the following mu-plugin:
<?php /** * Plugin Name: Fix Docker Loopback Requests * Description: Routes WordPress loopback HTTP requests (Site Health, wp_remote_get(home_url()), cron, etc.) to the Docker host gateway. Inside the php/cli containers "localhost" is the container's own loopback where nothing listens on the published port, so requests to home_url() fail with "cURL error 7: Could not connect to server". The docker-compose `extra_hosts: localhost:host-gateway` mapping is shadowed by Docker's default `127.0.0.1 localhost` entry, so this forces the gateway resolution at the cURL layer instead. * * This is a development-environment-only shim and should never ship to production. */ // Only run in the local Docker dev environment. if ( ! function_exists( 'wp_get_environment_type' ) || 'local' !== wp_get_environment_type() ) { return; } add_action( 'http_api_curl', /** * Pins loopback requests to the Docker host gateway via CURLOPT_RESOLVE. * * @param resource|\CurlHandle $handle The cURL handle. * @param array<string, mixed> $args The HTTP request arguments. * @param string $url The request URL. */ static function ( $handle, array $args, string $url ) { $host = wp_parse_url( $url, PHP_URL_HOST ); if ( ! is_string( $host ) ) { return; } // Only rewrite requests aimed at this site (loopback), not arbitrary outbound requests. $home_host = wp_parse_url( home_url(), PHP_URL_HOST ); if ( $host !== $home_host ) { return; } // host.docker.internal resolves to the host gateway, which reaches the published web-server port. $gateway = gethostbyname( 'host.docker.internal' ); if ( 'host.docker.internal' === $gateway ) { return; // Not running under Docker Desktop / gateway unavailable. } $port = wp_parse_url( $url, PHP_URL_PORT ); if ( ! $port ) { $port = ( 'https' === wp_parse_url( $url, PHP_URL_SCHEME ) ) ? 443 : 80; } curl_setopt( $handle, CURLOPT_RESOLVE, array( "{$host}:{$port}:{$gateway}" ) ); }, 10, 3 );
Claude's full analysis:
Loopback requests fail in the Docker local-env (cURL error 7)
Summary: Loopback HTTP requests fail in the Docker-based local development environment. The Site Health "loopback request" test fails, and so does any in-PHP loopback, e.g.
npm run env:cli -- eval "var_dump(wp_remote_get(home_url()));", which returnscURL error 7: Failed to connect to localhost port 8000 after 4 ms: Could not connect to server.
Root cause: The web server (nginx) runs in a separate container (
wordpress-develop) that publishes8000:80on the host, whilephp-fpmand WP-CLI run in their own containers (php,cli).home_url()ishttp://localhost:8000, but inside thephp/clicontainerslocalhostis the container's own loopback, where nothing listens on port 8000, so the request is refused. [a629d1cc6c] attempted to address this by addingextra_hosts: localhost:host-gatewayso thatlocalhostwould resolve to the host gateway (which does reach the published port —192.168.65.254:8000returns200). However, that mapping is shadowed: Docker always writes127.0.0.1 localhostand::1 localhostfirst in/etc/hosts, so the resolver returns the loopback addresses ahead of the gateway entry. cURL connects to::1/127.0.0.1, gets an immediate connection-refused, and never falls through to the gateway. In short,extra_hostscannot overridelocalhostbecause Docker's own entries take precedence.
Diagnostic evidence: Inside the
clicontainer,getent ahosts localhostreturns127.0.0.1before192.168.65.254, and a verbose curl shows only::1and127.0.0.1being tried before failing. Reaching the gateway directly (192.168.65.254:8000orhost.docker.internal:8000) returns200, and pinning the loopback withCURLOPT_RESOLVE(localhost:8000:192.168.65.254) also returns200— confirming the route works and only name resolution oflocalhostis the problem.
Possible fix direction: Force gateway resolution at the cURL layer rather than via
/etc/hosts. For requests aimed at the site's own host, setCURLOPT_RESOLVEto the host gateway (resolved at runtime viahost.docker.internal), scoped to the local environment. Verified working as a proof of concept:
<?php add_action( 'http_api_curl', static function ( $handle, $args, $url ) { if ( 'local' !== wp_get_environment_type() ) { return; } $host = wp_parse_url( $url, PHP_URL_HOST ); if ( ! is_string( $host ) || $host !== wp_parse_url( home_url(), PHP_URL_HOST ) ) { return; } $gateway = gethostbyname( 'host.docker.internal' ); if ( 'host.docker.internal' === $gateway ) { return; // Gateway unavailable (not Docker Desktop). } $port = wp_parse_url( $url, PHP_URL_PORT ); if ( ! $port ) { $port = ( 'https' === wp_parse_url( $url, PHP_URL_SCHEME ) ) ? 443 : 80; } curl_setopt( $handle, CURLOPT_RESOLVE, array( "{$host}:{$port}:{$gateway}" ) ); }, 10, 3 );After applying this,
wp_remote_get(home_url())returnsHTTP 200andWP_Site_Health::can_perform_loopback()returnsstatus=good. This covers Site Health, cron spawning, and any other loopback request. Note the existingextra_hosts: localhost:host-gatewaylines indocker-compose.ymlare ineffective and could be removed if a different mechanism is adopted.
#2
@
4 hours ago
I think we can make this fix available to everyone by adding such an mu-plugin to tools/local-env and mounting it via docker-compose.yml.
This ticket was mentioned in PR #12219 on WordPress/wordpress-develop by @westonruter.
3 hours ago
#3
- Keywords has-patch added
## Problem
Loopback HTTP requests fail in the local Docker environment. The Site Health "loopback request" test reports a failure, and any in-PHP loopback fails too:
$ npm run env:cli -- eval "var_dump( wp_remote_get( home_url() ) );" cURL error 7: Failed to connect to localhost port 8000 after 4 ms: Could not connect to server
The web server (nginx) runs in a separate container from php/cli. home_url() is http://localhost:8000, but inside those containers localhost is the container's own loopback, where nothing listens on the published port — so the request is refused.
The existing extra_hosts: localhost:host-gateway mapping (added in [a629d1cc6c]) is meant to address this, but it has no effect when cURL resolves localhost via glibc's getaddrinfo(), which special-cases that name to loopback and never consults the /etc/hosts gateway entry. (The legacy gethostbyname() path *does* return the gateway, but the HTTP stack doesn't use it.) The host gateway itself is reachable — host.docker.internal:8000 returns 200 — so only name resolution of localhost is broken.
## Fix
Add a development-only mu-plugin (tools/local-env/mu-plugins/fix-docker-loopback.php) that, for requests aimed at the site's own host, pins the cURL handle to the Docker host gateway via CURLOPT_RESOLVE (resolved at runtime through host.docker.internal). It is gated on wp_get_environment_type() === 'local' and no-ops where the gateway is unavailable.
The php container copies the shim into the (gitignored) wp-content/mu-plugins directory on startup, mirroring the existing object-cache.php drop-in pattern, so it covers Site Health, cron spawning, and wp_remote_get( home_url() ) from both php and cli.
The extra_hosts lines are left in place — they are the intended mechanism and remain effective on resolvers that honor them (e.g. a c-ares-based libcurl); this shim backs them up where they do not. composer.json is also updated to suggest ext-curl, which the shim relies on.
## Testing
With the environment running:
wp_remote_get( home_url() )→HTTP 200WP_Site_Health::can_perform_loopback()→status=good
PHPCS and PHPStan (level 10, via phpstan-diff) both pass on the new file.
## Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 4.8
Used for: Diagnosing the loopback failure, drafting the mu-plugin and docker-compose.yml changes, and verifying the fix end-to-end. All changes were reviewed and edited by me.
Loopback test failure