Docker breaks PHP file_get_contents() and http(s) stream wrappers?

I’m still a bit new to Docker so forgive me if I don’t know the correct terminology for some of this.

I am in the process of switching multiple PHP projects over to Docker containers, all of which are written in PHP. I have noticed that Docker seems to break PHPs HTTP(S) stream wrappers.

For example, doing this: file_get_contents('http://www.google.com'); will cause execution to hang.

This is because prior to PHP8 by default, the HTTP stream wrapper uses HTTP/1.0 along with a “Connection: close” when making HTTP requests. I’ve noticed that Docker, for some reason, changes this to (or forwards as) HTTP/1.1, but doesn’t modify how the connection is handled. So the server thinks the request is a HTTP/1.1 request and responds with chunked transfer-encoding, which in-turn relies on the client to close the connection, but the client thinks the request was HTTP/1.0 and sits there waiting for the server to close the connection causing the program to hand either indefinitely or until timeout is reached.

For versions >= PHP8, there is a bug that triggers the same behavior despite HTTP/1.1 being requested. See: PHP :: Bug #80931 :: file_get_contents() hangs with HTTP/1.1 if server doesn't close connection

This seems pretty bad to me. I now have 3 PHP apps that just don’t work because Docker wants to force me to use HTTP/1.1.

I’ve confirmed this behavior with direct socket writes hard-coded to HTTP/1.0 along with Wireshark sniffing packets on the host to confirm the request protocol is being changed. I have also confirmed that this only happens on port 80 (and 443 i’m assuming, I can’t sniff SSL). If I change the port used then HTTP/1.0 is requested.

Is anyone else experiencing this issue? Is this a known problem that I’m just not finding the right search results for? Is there a setting that needs to be turned on/off somewhere? I’m feeling pretty stuck here and I can’t figure out why Docker would feel the need to interfere with HTTP requests, so any light someone can shine on this issue for me would be greatly appreciated.

Docker CE would not change the network traffic. Only a proxy server could. If you epxerience this in a Docker container it must be a difference in the application settings inside the container (maybe a different default setting) or you are not using Docker CE. Or a proxy that you don’t know about. How did you install Docker on which operating system?

Thanks for the response. I’m running on Windows using Docker Desktop 4.15 and WSL2. The base image I’m using is the vanilla PHP images from Dockerhub. I’ve tried PHP7.4/8.0/8.1 both debian and alpine variants, all with the same issue.

Pretty sure it’s not the image though, or any other software or settings as I tested this with a skeleton PHP script using low level socket functions and I can see the request protocol get changed.

This is now confirmed as a Docker issue as I am now testing rolling back versions of the docker engine. I just installed Docker Desktop 3.6.0 which includes Docker Engine 20.10.8 and the problem goes away using the exact same Dockerfile to build my image.

I’m going to slowly roll versions forward now until I can try and pinpoint which version of the Docker Engine this becomes a problem.

Ok, I’m at my wits end with this issue. I rolled back to Docker Desktop 3.6.0 and ran through all major versions back up to 4.15 to see which Docker Engine version may be at fault. Somehow by doing all that, I fixed it and I can no long re-create this issue on my Windows development host. I still have this problem in my private Kubernetes cluster though and I am still trying to track down what bit of software was at fault, but it looks like Docker is off the hook for this one.

Side note, I have another host (my laptop) with the exact same versions of Docker installed and using the same project to build an image and it is broken. So now I have one that works, and one that doesn’t and I can’t see what is different between them. Arrghh!!!

More updates. I’m having the same problem on my laptop so I tried to fix in a similar method to my desktop machine. Downgrading to Docker Desktop 4.14.1 fixes the problem. Version 4.15 absolutely does not work.

So what is Docker Desktop 4.15 installing that is making this break? Can’t be Docker Engine as I think it’s the same version on both. Is it containerd? Anyone?

1 Like

There were multiple proxy-related changes in Docker Desktop 4.15

I don’t know which one could cause your problem if it is caused by any of those.

1 Like

I can confirm the very similar behavior after upgrading to 4.15.0.

Using PHP 7.4.33, any calls (http:// and https://) to IIS servers with file_get_contents() and related functions hung up until they hit the timeout limit.

Connections to non-IIS servers worked fine. Also wget from the container shell worked even for the IIS hosts.

I also inspected the connections with Wireshark just as @jamiecarl and noticed the same phenomen. Connections were HTTP/1.1 even if they were specifically set to HTTP/1.0 in PHP stream context options.

This seems to confirm Jamie’s theory, that the underlying issue with PHP streams was brought into daylight because of the connection change from HTTP/1.0 to HTTP/1.1 caused by Docker Desktop v4.15.0.

Uninstalling 4.15.0 and re-installing 4.14.1 fixed the issue for me too.

1 Like

Further research shows that - after downgrading to 4.14.1, calls to IIS servers work with both HTTP/1.0 and HTTP/1.1. (depending on which protocol you define in PHP stream context).

So the problem isn’t merely that v4.15.0 changes connection from HTTP/1.0 to HTTP/1.1, but something else changes, too.

FWIW, with v4.15.0 the Request headers had User-Agent: Go-http-client/1.1. This disappeared after the downgrade to v4.14.1.

That header itself doesn’t break anything obviously, but probably gives a hint of what could be going on under the hood.

It’s the “Connection: close” header/function. When the client sends a HTTP/1.0 request it expects the server to close the connection. But by changing it to HTTP/1.1 where “Connection: close” is not supported, the server expects the client to close the connection once it has received the response.

So basically, by changing the protocol in the middle, each host is sitting there waiting for the other host to close the connection.

Seems logical at first, but if that was the case, then everything should work fine if 'protocol_version' => 1.1 was defined PHP stream context, because then PHP would also follow HTTP/1.1 protocol, right?

However, with v4.15.0 it didn’t help to change to 'protocol_version' => 1.1. The connection was still stuck.

With v4.14.1 in turn, the connection works well with both protocol versions, and one can see from the Wireshark capture, that the outgoing connection indeed respects whichever protocol version was set in the PHP stream context.

You would think. But PHP<8 does not support HTTP/1.1 and PHP>=8 has a bug. https://bugs.php.net/bug.php?id=80931

http:// streams in PHP<8 do support HTTP/1.1 as well, but they default to HTTP/1.0.

PHP>=8 defaults to HTTP/1.1, which under certain circumstances reveals the underlying bug you’re referring to.

Following should confirm this true. (I’m on PHP 7.4.32)

// The problematic URL in the PHP bug report
$url = 'http://ws.correios.com.br/calculador/CalcPrecoPrazo.aspx?nCdEmpresa=&sDsSenha=&sCepOrigem=11661690&sCepDestino=88070-480&nVlPeso=1&nCdFormato=1&nVlComprimento=25&nVlAltura=3&nVlLargura=25&sCdMaoPropria=N&nVlValorDeclarado=0&sCdAvisoRecebimento=N&nCdServico=04014&nVlDiametro=0&StrRetorno=xml';

file_get_contents( $url, false, stream_context_create(array( 'http' => array( 'protocol_version' => 1.1,),)));

Wireshark output confirms that the outgoing request indeed is HTTP/1.1 and works well.

@jamiecarl @jgangso I could finally try it my macOS and Windows. Both worked for me. I didn’ use wireshark, but a custom PHP script whit this content to show the SERVER variables to see the protocol version:

receive.php on the remote server

<?php
print_r($_SERVER);

send.php on my machine in Docker Desktop

<?php
$target = 'http://<ipaddress>/receive.php';


// Create a stream
$opts = [
  'http' => [
    'method' => "GET",
    'header' => "Accept-language: en\r\n" .
                "Cookie: foo=bar\r\n",
    'protocol_version' => '1.0',
  ],
];

$context = stream_context_create($opts);

// Open the file using the HTTP headers set above
$file = file_get_contents($target, false, $context);

echo $file;

The test is based on the documentation with the additional protocol version. This is the command I ran locally:

docker run --rm -it -v "$(pwd)/send.php:/app/send.php" --workdir /app php:8.2 php send.php
# and
docker run --rm -it -v "$(pwd)/send.php:/app/send.php" --workdir /app php:7.4 php send.php

On Windows, I ran it from PowerShell.

Well, my server was not IIS, but changing the protocol should not depend on the target server. Or maybe it does… I found this in the php documentation linked above

Warning

When using SSL, Microsoft IIS will violate the protocol by closing the connection without sending a close_notify indicator. PHP will report this as “SSL: Fatal Protocol Error” when you reach the end of the data. To work around this, the value of error_reporting should be lowered to a level that does not include warnings. PHP can detect buggy IIS server software when you open the stream using the https:// wrapper and will suppress the warning. When using fsockopen() to create an ssl:// socket, the developer is responsible for detecting and suppressing this warning.

If Docker Desktop uses a proxy to send the request and if the Go library that Docker Desktop uses for the proxy detects something automatically, it could change the protocol, but I don’t see why.

Running my test I couldn’t see any user agent when I sent the request from PHP in Docker Desktop, only when I sent the request using curl.

If Docker Desktop uses a proxy server that would not depend on the client if that client is always in the container. Unless of course you have a proxy server and wget ignores the proxy settings since it uses lowercase variables and the Docker documentation shows only upper case variables to set a proxy. I am sure you would realized that by now, so I don’t think this is the case.

  1. Can you share what is your settings in the “General” tab of Docker Desktop?
  2. And just to make sure there is no accidental proxy settings in Docker Desktop, the Resources / Proxies tab as well.
  3. If it happens only with PHP, could you the exact HTTP request you send from PHP and from curl? You can replace the IP addresses with fake IP just make sure you use different IP addresses if the original IP addresses were different.
  4. You could also compare the request sent from PHP and using wget and try to send the same request (including user agent) from wget.
  5. I am not sure I can help, so if you want to report this issue, you can on GitHub, but they will eed the same information I asked for or maybe more.
1 Like

I’ve the same issue. We have to query external API via HTTP in our app, and every call hangs waiting for connection for over two minutes. I’ve a tcpdump to prove it:

14:46:38.960909 IP 172.25.0.4.38058 > 185.49.239.67.80: Flags [S], seq 3754257314, win 64240, options [mss 1460,sackOK,TS val 1154885490 ecr 0,nop,wscale 7], length 0
14:46:38.962618 IP 185.49.239.67.80 > 172.25.0.4.38058: Flags [S.], seq 4097685477, ack 3754257315, win 65535, options [mss 1460,wscale 2,eol], length 0
14:46:38.962658 IP 172.25.0.4.38058 > 185.49.239.67.80: Flags [.], ack 1, win 502, length 0
14:46:38.962701 IP 172.25.0.4.38058 > 185.49.239.67.80: Flags [P.], seq 1:142, ack 1, win 502, length 141: HTTP: GET /lookup.asmx/getAddress?account=test&password=test&postcode=G71+7SF HTTP/1.0
14:46:38.963295 IP 185.49.239.67.80 > 172.25.0.4.38058: Flags [.], ack 142, win 65535, length 0
14:46:39.201693 IP 185.49.239.67.80 > 172.25.0.4.38058: Flags [P.], seq 1:873, ack 142, win 65535, length 872: HTTP: HTTP/1.1 200 OK
14:46:39.201731 IP 172.25.0.4.38058 > 185.49.239.67.80: Flags [.], ack 873, win 501, length 0
14:48:39.309181 IP 172.25.0.4.38058 > 185.49.239.67.80: Flags [F.], seq 142, ack 873, win 501, length 0
14:48:39.311030 IP 185.49.239.67.80 > 172.25.0.4.38058: Flags [.], ack 143, win 65535, length 0
type or paste code here

Is there any way to change settings of Docker to achieve same behavior as in 4.14.1?

1 Like

I haven’t had a chance to try any of the suggestions from @rimelek as yet, however this morning I was automatically updated to 4.16.2. Thinking it was going to ruin my day I decided to try it out anyway and it appears I am no longer experiencing the problem. I now have two workstations that had the problem now updated to 4.16.2 and both are working perfectly now.

So is anyone else able to update to Docker Desktop 4.16.2 and confirm that the problem is gone? I’m happy to write this one off as “one of those things” we will never know the true cause of, if it is no longer a problem.

I’ve been experiencing the exact same issue as you since attempting to upgrade from 4.14.1 to 4.15 so I was excited to try out 4.16.2. Unfortunately the problem still remains and calls to file_get_contents still hangs. I am running PHP 8.2.1. I updated directly from 4.14.1 to 4.16.2, bypassing 4.15 altogether. Were there any other changes to your configuration that you can recall that may have solved your issue?

No other changes as far as I can remember. I’m a developer, not a docker expert so I tend not to screw around with things unless they don’t work.

Using Wireshark I have been able to confirm that my requests are NOT being changed from HTTP/1.0 to HTTP/1.1, which is why it is working for me.

The only change I’ve made since hitting this problem was updating to 4.16.2.

Side note: I found this issue on Github, and this issue which discuss something similar and could be related and discusses a work-around.

Basically, turn off Automatic proxy detection in the Windows settings.

2 Likes

Ok, turning off Automatic proxy detection worked. Thank you!
Will have to look into this more deeply but for now I can at least keep up with the latest Docker version.