Skip to content

Fix GH-8832: Honor protocol_version for proxy CONNECT requests - #25048

Open
glbessa wants to merge 1 commit into
php:masterfrom
glbessa:gh8832-proxy-connect-protocol-version
Open

glbessa wants to merge 1 commit into
php:masterfrom
glbessa:gh8832-proxy-connect-protocol-version

Conversation

@glbessa

@glbessa glbessa commented Oct 30, 2026

Copy link
Copy Markdown

Closes GH-8832

The http stream wrapper always sends the CONNECT request used to tunnel HTTPS through a proxy as HTTP/1.0, regardless of the protocol_version context option. Proxies that only accept HTTP/1.1 (e.g. Envoy, as in the issue) answer with 426 Upgrade Required, making file_get_contents() over HTTPS through such a proxy impossible.

This makes the CONNECT request honor the existing protocol_version context option, and sends the Host header (the target authority) that HTTP/1.1 requires:

CONNECT www.php.net:443 HTTP/1.1
Host: www.php.net:443

When the option is not set, nothing changes: CONNECT is still sent as HTTP/1.0, without Host.

Notes:

  • I reused protocol_version rather than adding a new option such as proxy_protocol_version, since users setting it most likely expect it to apply to the whole exchange, and it keeps the API surface unchanged. Happy to switch if a separate option is preferred.
  • Handling 426 Upgrade Required automatically (as discussed in the issue) does not help here, since the proxy in the report closes the connection and sends no Upgrade header.
  • The version is formatted exactly like the main request line (zval_get_double() + %.1F), so both lines always agree.
  • Added a test based on the existing http_proxy_ssl_connect_0x.phpt tests; the HTTP/1.0 default is already covered by http_proxy_ssl_connect_01.phpt. Added an UPGRADING entry under "Changed Functions".

🤖 Generated with Claude Code

The http stream wrapper always sent the CONNECT request used to tunnel
HTTPS through a proxy as HTTP/1.0, ignoring the "protocol_version"
context option. Proxies that only accept HTTP/1.1 reject it with
426 Upgrade Required.

Use the "protocol_version" context option for the CONNECT request as
well, and send the Host header required by HTTP/1.1 in that case.
Without the option, CONNECT still uses HTTP/1.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@arshidkv12

Copy link
Copy Markdown
Contributor

I wonder whether deprecating HTTP/HTTPS stream wrapper support would be a better long-term solution.

Developers could use ext/curl for HTTP requests, gaining HTTP/2 and HTTP/3 support without duplicating protocol implementations in PHP.

@anun333

anun333 commented Oct 30, 2026

Copy link
Copy Markdown

Quoted below: written with Claude, an LLM, as CONTRIBUTING.md asks us to mark. The builds and test runs behind it are on my machine; the commands to repeat them are under the fold.

Tested end to end on today's master (1a72e5f2542) with and without this PR, debug builds with openssl, through a local proxy that behaves like the Envoy in the issue (426 for a CONNECT sent as HTTP/1.0, 400 for HTTP/1.1 without Host) to a local TLS server. For each context, the CONNECT head the proxy received and what file_get_contents() returned:

http context master this PR
no protocol_version CONNECT 127.0.0.1:34443 HTTP/1.0: fails the same
protocol_version 1.1 … HTTP/1.0: fails CONNECT 127.0.0.1:34443 HTTP/1.1, Host: 127.0.0.1:34443: body
protocol_version 1.0 … HTTP/1.0: fails … HTTP/1.0, Host: 127.0.0.1:34443: fails (426)
1.1, with header holding Proxy-Authorization: Basic … and Host: other.example … HTTP/1.0, Proxy-Authorization: fails … HTTP/1.1, Host: 127.0.0.1:34443, Proxy-Authorization: body
1.1, https://[::1]:34443/ CONNECT [::1]:34443 HTTP/1.0: fails CONNECT [::1]:34443 HTTP/1.1, Host: [::1]:34443: body

So the default is unchanged, Host is the target authority as RFC 9112 section 3.2.3 shows for authority-form, the user's own Host header isn't carried into the CONNECT (only Proxy-Authorization is, as before), and IPv6 targets keep their brackets in both places. With protocol_version 1.0 the CONNECT now carries a Host header too, which HTTP/1.0 proxies accept. ext/standard/tests/http passes on both (91; 92 with the new test), and the new test fails on master without the fix.

How this was tested

Linux x86_64, gcc 13.3, OpenSSL 3.0.13, debug builds of origin/master and of it with this PR's commit cherry-picked:

./buildconf --force
./configure --enable-debug --disable-cgi --disable-phpdbg --disable-fileinfo --enable-pcntl --enable-zend-test --enable-fpm --with-curl --enable-mbstring --disable-mbregex --with-mysqli --with-pdo-mysql --with-openssl --with-bz2
make -j4

Python's ssl module served "hello" on 127.0.0.1 and ::1 with a self-signed certificate; the proxy, on 127.0.0.1, logged each request head, answered as above and otherwise tunnelled with 200 Connection established. The client: file_get_contents($url, false, stream_context_create(['http' => ['proxy' => 'tcp://127.0.0.1:33128', 'protocol_version' => 1.1, 'header' => [...]], 'ssl' => ['verify_peer' => false, 'verify_peer_name' => false]])). Suites: TEST_PHP_EXECUTABLE=$PWD/sapi/cli/php sapi/cli/php run-tests.php -n -q -j4 ext/standard/tests/http.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support "protocol_version" stream context option for proxy connections

3 participants