@@ -85,9 +85,17 @@ for pair in NGINX_HTTP_PORT:NGINX_WEBROOT APACHE_HTTP_PORT:APACHE_DOCUMENT_ROOT
8585 fi
8686done
8787
88+ # The same images serve TLS on their HTTPS port once SSL_MODE is on, which is the only
89+ # way to reach them over HTTP/2.
90+ https_port=" "
91+ for variable in NGINX_HTTPS_PORT APACHE_HTTPS_PORT CADDY_HTTPS_PORT; do
92+ https_port=$( image_env " $variable " )
93+ [ -z " $https_port " ] || break
94+ done
95+
8896# Web images run with OPcache enabled so the health check and the served page
8997# cover the FPM and FrankenPHP SAPIs starting with the tuned defaults.
90- run_args=(--detach --rm -- env PHP_OPCACHE_ENABLE=1)
98+ run_args=(--detach --env PHP_OPCACHE_ENABLE=1)
9199if [ -n " $http_port " ]; then
92100 # The container runs unprivileged, so the mounted document root must be world readable.
93101 web_dir=$( mktemp -d)
@@ -98,7 +106,18 @@ if [ -n "$http_port" ]; then
98106 chmod 755 " $web_dir /storage"
99107 echo ' <?php echo "storage-php-executed";' > " $web_dir /storage/uploaded.php"
100108 chmod 644 " $web_dir /storage/uploaded.php"
109+ # Symfony's Response::send() calls fastcgi_finish_request() and Laravel keeps running
110+ # terminate callbacks after it, so the web server has to survive a request that finishes
111+ # before PHP does.
112+ cat > " $web_dir /finish-request.php" << 'PHP '
113+ <?php
114+ echo "finish-request-ok:" . PHP_VERSION;
115+ fastcgi_finish_request();
116+ usleep(50000);
117+ PHP
118+ chmod 644 " $web_dir /finish-request.php"
101119 run_args+=(--publish " 127.0.0.1::${http_port} " --volume " $web_dir :$web_root :ro" )
120+ [ -z " $https_port " ] || run_args+=(--publish " 127.0.0.1::${https_port} " )
102121fi
103122
104123containers=()
@@ -136,6 +155,7 @@ start_container() {
136155 fail " Container did not become healthy within ${health_timeout_seconds} s (status: $status )"
137156 fi
138157 [ -z " $http_port " ] || host_port=$( docker port " $container " " $http_port " | head -n1 | sed ' s/.*://' )
158+ [ -z " $https_port " ] || https_host_port=$( docker port " $container " " $https_port " | head -n1 | sed ' s/.*://' )
139159}
140160
141161# Retries until the response body matches, since the web server may still be warming up.
@@ -177,6 +197,32 @@ expect_authorization_redacted() {
177197 fail " Access log does not redact the authorization query parameter"
178198}
179199
200+ # PHP carrying on after fastcgi_finish_request() has to leave the server able to take the
201+ # next request. Caddy 2.11.6 segfaulted the whole FrankenPHP process here, but only over
202+ # HTTP/2, so both protocol versions are worth checking.
203+ expect_work_after_finish_request () {
204+ for protocol in --http1.1 --http2; do
205+ body=" "
206+ for _ in $( seq 1 " $http_timeout_seconds " ) ; do
207+ body=$( curl --silent --insecure " $protocol " --max-time 5 " https://127.0.0.1:${https_host_port} /finish-request.php" 2> /dev/null || true)
208+ [ " $body " = " finish-request-ok:${php_version} " ] && break
209+ sleep 1
210+ done
211+ if [ " $body " != " finish-request-ok:${php_version} " ]; then
212+ dump_container_state " $container "
213+ fail " Web server did not serve /finish-request.php over ${protocol# --} . Response: ${body:- <empty>} "
214+ fi
215+
216+ # The crash lands after the response, so give the process a moment to fall over.
217+ sleep 2
218+ status=$( docker inspect --format ' {{.State.Status}}' " $container " 2> /dev/null || echo gone)
219+ if [ " $status " != " running" ]; then
220+ dump_container_state " $container "
221+ fail " Web server stopped after a ${protocol# --} request finished before PHP did (status: $status )"
222+ fi
223+ done
224+ }
225+
180226# A detached container has no terminal, so Caddy's default format is json and its default
181227# stream is stderr. Octane depends on both: it only relays stderr and only parses JSON.
182228expect_json_logs_on_stderr () {
@@ -202,6 +248,19 @@ pass "Web server serves PHP on port ${http_port}"
202248expect_storage_blocked
203249pass " Web server blocks PHP execution under /storage"
204250
251+ # HTTP/2 needs TLS, so this runs against its own container with SSL_MODE=full, which
252+ # generates a self-signed certificate and stops serving the plain HTTP port. The checks
253+ # after it still expect the container the rest of the suite has been using.
254+ if [ -n " $https_port " ]; then
255+ default_container=" $container "
256+ default_host_port=" $host_port "
257+ start_container --env SSL_MODE=full
258+ expect_work_after_finish_request
259+ pass " Web server survives a request that finishes before PHP does, over HTTP/1.1 and HTTP/2"
260+ container=" $default_container "
261+ host_port=" $default_host_port "
262+ fi
263+
205264# The rest applies to FrankenPHP only: Caddy's log defaults and Laravel Octane.
206265[ -n " $( image_env CADDY_HTTP_PORT) " ] || exit 0
207266
0 commit comments