Summary
WireHttp's three send types handle HTTP redirects differently, so getHttpCode(), getResponseHeaders() and the returned body for the same URL depend on which transport happened to be used (or silently fallen back to). Measured on 3.0.269 dev against a local test server for a 301 → 200 chain (302 behaves identically; GET and HEAD identical):
| send type |
follows redirects? |
getHttpCode() |
location header |
body |
| curl |
yes (unless open_basedir set → then no) |
200 (final) |
stale first-hop value |
final |
| fopen |
yes (PHP wrapper default) |
301 (first hop) |
first-hop value |
final (disagrees with code) |
| socket |
301/302 only, not 303/307/308 |
200 (final), but 307 stays 307 |
none / first-hop |
final / empty |
Individual bugs behind the matrix:
- fopen reports the first response's status with the final response's body.
$http_response_header contains headers from every response in the chain, and setResponseHeader() parses the status from element [0] — the first hop. A 301 → 200 URL reports code 301 while returning the 200 page's markup.
- curl retains stale headers across hops. The
CURLOPT_HEADERFUNCTION callback accumulates headers from every response, so a followed 301 → 200 reports code 200 with the first hop's location header still present in getResponseHeaders().
- socket permanently stops following redirects after 5 hops per process.
sendSocket() uses static $level as its recursion guard and never resets it, so the count accumulates across all requests in the process. In long-running CLI/queue processes, redirect following silently dies after the 5th followed hop (reproduced: after 5 followed redirects, subsequent requests report the 301 instead of following).
- socket ignores 303/307/308, following only 301/302 — curl and fopen follow all of them.
send() never passes $options to sendSocket(), so a per-call timeout option (and anything else) is lost on the socket path.
There is also no way to opt out of redirect following. Link-checking code (e.g. FieldtypeVerifiedURL's useRedirect/fixRedirect features) needs to see the 301 itself — and since curl is first in the default use order and follows, such code currently never sees a redirect status on a typical install.
Reproduction
Serve a redirect locally (?redir=301 → Location: /?landed=1 → 200 "LANDED"), then:
foreach(['curl', 'fopen', 'socket'] as $use) {
$http = new WireHttp();
$body = $http->send('http://127.0.0.1:8899/?redir=301', [], 'GET', ['use' => $use]);
echo "$use: code=" . $http->getHttpCode() .
" location=" . $http->getResponseHeader('location') .
" body=" . substr((string) $body, 0, 10) . "\n";
}
Output: curl: code=200 location=http://…/?landed=1 body=LANDED, fopen: code=301 location=http://…/?landed=1 body=LANDED, socket: code=200 location= body=LANDED. For bug 3, run 6+ socket requests against the redirecting URL in one process — following stops partway through.
Expected
The same URL should produce the same reported status/headers/body regardless of send type, and there should be a way to ask WireHttp not to follow redirects so the redirect status itself can be inspected.
Environment
ProcessWire 3.0.269 dev (also verified on 3.0.253); PHP 8.5; macOS.
A PR with a fix (new followRedirects option + all three transports normalized, with measured before/after matrices) follows shortly.
Summary
WireHttp's three send types handle HTTP redirects differently, sogetHttpCode(),getResponseHeaders()and the returned body for the same URL depend on which transport happened to be used (or silently fallen back to). Measured on 3.0.269 dev against a local test server for a301 → 200chain (302 behaves identically; GET and HEAD identical):getHttpCode()locationheaderopen_basedirset → then no)Individual bugs behind the matrix:
$http_response_headercontains headers from every response in the chain, andsetResponseHeader()parses the status from element[0]— the first hop. A301 → 200URL reports code 301 while returning the 200 page's markup.CURLOPT_HEADERFUNCTIONcallback accumulates headers from every response, so a followed301 → 200reports code 200 with the first hop'slocationheader still present ingetResponseHeaders().sendSocket()usesstatic $levelas its recursion guard and never resets it, so the count accumulates across all requests in the process. In long-running CLI/queue processes, redirect following silently dies after the 5th followed hop (reproduced: after 5 followed redirects, subsequent requests report the 301 instead of following).send()never passes$optionstosendSocket(), so a per-calltimeoutoption (and anything else) is lost on the socket path.There is also no way to opt out of redirect following. Link-checking code (e.g. FieldtypeVerifiedURL's
useRedirect/fixRedirectfeatures) needs to see the 301 itself — and since curl is first in the defaultuseorder and follows, such code currently never sees a redirect status on a typical install.Reproduction
Serve a redirect locally (
?redir=301→Location: /?landed=1→ 200 "LANDED"), then:Output:
curl: code=200 location=http://…/?landed=1 body=LANDED,fopen: code=301 location=http://…/?landed=1 body=LANDED,socket: code=200 location= body=LANDED. For bug 3, run 6+ socket requests against the redirecting URL in one process — following stops partway through.Expected
The same URL should produce the same reported status/headers/body regardless of send type, and there should be a way to ask WireHttp not to follow redirects so the redirect status itself can be inspected.
Environment
ProcessWire 3.0.269 dev (also verified on 3.0.253); PHP 8.5; macOS.
A PR with a fix (new
followRedirectsoption + all three transports normalized, with measured before/after matrices) follows shortly.