Background
EndpointChecker::check() (src/Service/EndpointChecker.php) decides whether rl.php is reachable by POSTing action=ping to the URL Drupal builds for the current request and accepting any HTTP 200 as success. In its current form the check produces false negatives in surprisingly common environments and the resulting hook_requirements() warning ("rl.php is not accessible") doesn't tell the operator why.
Concrete failure we hit
Drupal multisite behind an nginx → Apache reverse proxy. Symptom: editor-facing pages display the rl-php-warning.js toast ("A/B test results are not being collected…"), even though hitting https://<site>/modules/contrib/rl/rl.php directly returns pong.
What the check actually does:
$request->getSchemeAndHttpHost() returns http://<host> because $settings['reverse_proxy'] isn't set, so Drupal doesn't trust X-Forwarded-Proto.
- Guzzle POSTs to that
http:// URL.
- nginx 301-redirects to HTTPS using
$server_name (which collapses wildcards), losing the subdomain.
- Guzzle auto-follows the 3xx and lands on a different host returning a 200 HTML page (or 404).
- Either way,
getStatusCode() === 200 is the only condition checked → result depends on the redirect target, not on whether rl.php is actually serving.
The site config is buggy, but the check should detect that, not paper over it (or worse, silently approve based on an unrelated 200).
Proposed changes
Ranked by impact / blast-radius:
1. Validate the response body, not just the status code
return $response->getStatusCode() === 200
&& trim((string) $response->getBody()) === 'pong';
Smallest possible change; rejects "200 from a redirected-to wrong host."
2. Disable auto-follow redirects
'allow_redirects' => false,
A redirect on a same-origin self-probe is a misconfiguration signal, not a transparent path to success. Combined with #1, the only way to get TRUE is "rl.php served at the URL we constructed, returned 'pong'."
3. Differentiate failure modes and surface them in hook_requirements
Right now every failure collapses to the same generic "rl.php is not accessible" string. Proposed: have check() return a small struct (e.g. ['status' => 'ok'|'redirected'|'http_error'|'body_mismatch'|'connection_error', 'detail' => ...]) and translate each into a specific description in rl_requirements(). Examples:
- 3xx →
"rl.php redirected to <Location>. This usually means scheme/host detection in Drupal is wrong. Check $settings['reverse_proxy'] and X-Forwarded-Proto handling."
- non-200 →
"rl.php returned HTTP <code>. The web server is not serving the file at <url>."
- body mismatch →
"rl.php served, but returned unexpected content. Another module or rewrite rule may be intercepting the request."
- exception →
"Connection failed: <message>. The web server may not be running, or DNS may be misconfigured for the public hostname."
This converts the warning from "something's wrong, look elsewhere" into a fix-this-specific-thing pointer, which is the bigger user-experience win.
4. Loopback fallback probe
After the public-URL probe fails, retry against http://127.0.0.1[:port]/... with Host: header set to $request->getHost(). If that returns "pong", the file is being served correctly by the local web server and the failure is in the proxy / scheme / redirect layer — not in Apache or PHP. Reporting that distinction is much more actionable. Also resilient against TLS-cert and DNS-from-inside-the-container classes of failure.
5. Trust X-Forwarded-Proto for this probe even if reverse_proxy isn't set
For a self-probe specifically, the module knows it's the same site — it can safely upgrade the URL to HTTPS based on X-Forwarded-Proto: https regardless of Drupal's global trusted-proxy config. That guard is too coarse for absolute-URL generation generally, but for the self-check it's fine.
$forwarded_proto = $request?->headers->get('X-Forwarded-Proto');
$scheme = in_array($forwarded_proto, ['http', 'https'], true)
? $forwarded_proto
: $request?->getScheme();
6. Asymmetric cache TTLs
1h cache on success is fine; 1h on failure pins the warning long after a fix. Drop failure TTL to ~5 min (or invalidate on cache:rebuild).
7. Tighten the connection-error fallback
The current catch (\Exception $e) { return TRUE; } block (lines 78–82) is right in spirit (don't false-alarm in Docker isolation) but a bit broad. Narrow it to the cURL "couldn't resolve / couldn't connect" cases (errno 6/7); SSL errors and timeouts are real problems worth surfacing.
Suggested first PR
#1 + #2 + #3 are a small, self-contained patch with no infrastructure dependencies and they collectively turn the most common false-positive into a precise diagnostic. #4 is the next-best add but warrants its own change.
Repro on a stock setup
- nginx in front of Apache,
proxy_set_header X-Forwarded-Proto https
- Drupal
$settings['reverse_proxy'] not set
- Install
rl module, visit /admin/reports/status
- Observe "rl.php is not accessible" even though
curl https://<host>/modules/contrib/rl/rl.php -d action=ping returns pong
Files
src/Service/EndpointChecker.php (the probe — main change site)
rl.install lines 450–462 (hook_requirements — needs the structured result and richer descriptions)
Background
EndpointChecker::check()(src/Service/EndpointChecker.php) decides whetherrl.phpis reachable by POSTingaction=pingto the URL Drupal builds for the current request and accepting any HTTP 200 as success. In its current form the check produces false negatives in surprisingly common environments and the resultinghook_requirements()warning ("rl.php is not accessible") doesn't tell the operator why.Concrete failure we hit
Drupal multisite behind an nginx → Apache reverse proxy. Symptom: editor-facing pages display the
rl-php-warning.jstoast ("A/B test results are not being collected…"), even though hittinghttps://<site>/modules/contrib/rl/rl.phpdirectly returnspong.What the check actually does:
$request->getSchemeAndHttpHost()returnshttp://<host>because$settings['reverse_proxy']isn't set, so Drupal doesn't trustX-Forwarded-Proto.http://URL.$server_name(which collapses wildcards), losing the subdomain.getStatusCode() === 200is the only condition checked → result depends on the redirect target, not on whetherrl.phpis actually serving.The site config is buggy, but the check should detect that, not paper over it (or worse, silently approve based on an unrelated 200).
Proposed changes
Ranked by impact / blast-radius:
1. Validate the response body, not just the status code
Smallest possible change; rejects "200 from a redirected-to wrong host."
2. Disable auto-follow redirects
'allow_redirects' => false,A redirect on a same-origin self-probe is a misconfiguration signal, not a transparent path to success. Combined with #1, the only way to get TRUE is "rl.php served at the URL we constructed, returned 'pong'."
3. Differentiate failure modes and surface them in hook_requirements
Right now every failure collapses to the same generic "rl.php is not accessible" string. Proposed: have
check()return a small struct (e.g.['status' => 'ok'|'redirected'|'http_error'|'body_mismatch'|'connection_error', 'detail' => ...]) and translate each into a specific description inrl_requirements(). Examples:"rl.php redirected to <Location>. This usually means scheme/host detection in Drupal is wrong. Check $settings['reverse_proxy'] and X-Forwarded-Proto handling.""rl.php returned HTTP <code>. The web server is not serving the file at <url>.""rl.php served, but returned unexpected content. Another module or rewrite rule may be intercepting the request.""Connection failed: <message>. The web server may not be running, or DNS may be misconfigured for the public hostname."This converts the warning from "something's wrong, look elsewhere" into a fix-this-specific-thing pointer, which is the bigger user-experience win.
4. Loopback fallback probe
After the public-URL probe fails, retry against
http://127.0.0.1[:port]/...withHost:header set to$request->getHost(). If that returns "pong", the file is being served correctly by the local web server and the failure is in the proxy / scheme / redirect layer — not in Apache or PHP. Reporting that distinction is much more actionable. Also resilient against TLS-cert and DNS-from-inside-the-container classes of failure.5. Trust X-Forwarded-Proto for this probe even if reverse_proxy isn't set
For a self-probe specifically, the module knows it's the same site — it can safely upgrade the URL to HTTPS based on
X-Forwarded-Proto: httpsregardless of Drupal's global trusted-proxy config. That guard is too coarse for absolute-URL generation generally, but for the self-check it's fine.6. Asymmetric cache TTLs
1h cache on success is fine; 1h on failure pins the warning long after a fix. Drop failure TTL to ~5 min (or invalidate on
cache:rebuild).7. Tighten the connection-error fallback
The current
catch (\Exception $e) { return TRUE; }block (lines 78–82) is right in spirit (don't false-alarm in Docker isolation) but a bit broad. Narrow it to the cURL "couldn't resolve / couldn't connect" cases (errno 6/7); SSL errors and timeouts are real problems worth surfacing.Suggested first PR
#1 + #2 + #3 are a small, self-contained patch with no infrastructure dependencies and they collectively turn the most common false-positive into a precise diagnostic. #4 is the next-best add but warrants its own change.
Repro on a stock setup
proxy_set_header X-Forwarded-Proto https$settings['reverse_proxy']not setrlmodule, visit/admin/reports/statuscurl https://<host>/modules/contrib/rl/rl.php -d action=pingreturnspongFiles
src/Service/EndpointChecker.php(the probe — main change site)rl.installlines 450–462 (hook_requirements— needs the structured result and richer descriptions)