-
Notifications
You must be signed in to change notification settings - Fork 396
Description
Hello,
I've found a serious bug when working with Vulcain under FrankenPHP which renders it unusable in even a standard and recommended setup. I'm opening an issue here since it's strongly encouraged to use Vulcain with FrankenPHP and that's how I'm running it.
The Setup
We have two simple entities which are linked with OneToMany
relation and several items populated. Let's say Greeting
and GreetingItem
.
I've prepared a simple test case from official api-platform/api-platform
repository so you can try it for yourself: https://github.com/mislavjakopovic/frankenphp-issue-1873/tree/feat/add-test-case
The Problem
When sending a GET
request to Greeting
with Fields
header and with Accept-Encoding: gzip
header the request just gets killed and returns {}
. Same thing happens with URL fields
parameter instead of a header.
I'm mentioning "it gets killed" as I'm getting "Error: Decompression failed"
in Postman, so I suspect it doesn't even stream the response until the end.
Sending the request with Preload
header instead will return the proper response, but with no expected Link
preload headers.

As soon as I disable gzip
, for example "Accept-Encoding: gzip-disabled"
, the preload and fields works as expected and everything is returned, but of course not compressed.
Examples
I will be focusing on Fields
header as it's more easily visible. We can test this with a following cURL:
Scenario #1 (gzip
enabled) - doesn't work as expected
$ curl -k -vvv -H 'Fields: "name"' -H 'Accept-Encoding: gzip' https://localhost/greetings/1
* Trying 127.0.0.1:443...
* Connected to localhost (127.0.0.1) port 443 (#0)
* ALPN: offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN: server accepted h2
* Server certificate:
* subject: [NONE]
* start date: Sep 13 07:43:40 2025 GMT
* expire date: Sep 13 19:43:40 2025 GMT
* issuer: CN=Caddy Local Authority - ECC Intermediate
* SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
* using HTTP/2
* h2h3 [:method: GET]
* h2h3 [:path: /greetings/1]
* h2h3 [:scheme: https]
* h2h3 [:authority: localhost]
* h2h3 [user-agent: curl/7.88.1]
* h2h3 [accept: */*]
* h2h3 [fields: "name"]
* h2h3 [accept-encoding: gzip]
* Using Stream ID: 1 (easy handle 0x55f94cffbc80)
> GET /greetings/1 HTTP/2
> Host: localhost
> user-agent: curl/7.88.1
> accept: */*
> fields: "name"
> accept-encoding: gzip
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/2 200
< accept-patch: application/merge-patch+json
< alt-svc: h3=":443"; ma=2592000
< cache-control: no-cache, private
< content-encoding: gzip
< content-type: application/ld+json; charset=utf-8
< date: Sat, 13 Sep 2025 10:08:41 GMT
< etag: "dd8020af14ac57f1-gzip"
< link: <https://localhost/docs.jsonld>; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"
< permissions-policy: browsing-topics=()
< server: Caddy
< set-cookie: main_deauth_profile_token=ab1346; path=/; httponly; samesite=lax
< set-cookie: main_auth_profile_token=deleted; expires=Fri, 13 Sep 2024 10:08:40 GMT; Max-Age=0; path=/; httponly
< vary: Accept
< vary: Content-Type
< vary: Authorization
< vary: Origin
< vary: Accept-Encoding
< vary: Fields
< x-content-type-options: nosniff
< x-debug-token: ab1346
< x-debug-token-link: https://localhost/_profiler/ab1346
< x-frame-options: deny
< x-robots-tag: noindex
< content-length: 2
<
* Connection #0 to host localhost left intact
{}
Scenario #2 (gzip
disabled) - works as expected
$ curl -k -vvv -H 'Fields: "name"' -H 'Accept-Encoding: gzip-disabled' https://localhost/greetings/1
* Trying 127.0.0.1:443...
* Connected to localhost (127.0.0.1) port 443 (#0)
* ALPN: offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN: server accepted h2
* Server certificate:
* subject: [NONE]
* start date: Sep 13 07:43:40 2025 GMT
* expire date: Sep 13 19:43:40 2025 GMT
* issuer: CN=Caddy Local Authority - ECC Intermediate
* SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
* using HTTP/2
* h2h3 [:method: GET]
* h2h3 [:path: /greetings/1]
* h2h3 [:scheme: https]
* h2h3 [:authority: localhost]
* h2h3 [user-agent: curl/7.88.1]
* h2h3 [accept: */*]
* h2h3 [fields: "name"]
* h2h3 [accept-encoding: gzip-disabled]
* Using Stream ID: 1 (easy handle 0x55d0d99bfc80)
> GET /greetings/1 HTTP/2
> Host: localhost
> user-agent: curl/7.88.1
> accept: */*
> fields: "name"
> accept-encoding: gzip-disabled
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/2 200
< accept-patch: application/merge-patch+json
< alt-svc: h3=":443"; ma=2592000
< cache-control: no-cache, private
< content-type: application/ld+json; charset=utf-8
< date: Sat, 13 Sep 2025 10:07:59 GMT
< etag: "506f3acd7edc34f3"
< link: <https://localhost/docs.jsonld>; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"
< permissions-policy: browsing-topics=()
< server: Caddy
< set-cookie: main_deauth_profile_token=099a44; path=/; httponly; samesite=lax
< set-cookie: main_auth_profile_token=deleted; expires=Fri, 13 Sep 2024 10:07:58 GMT; Max-Age=0; path=/; httponly
< vary: Accept
< vary: Content-Type
< vary: Authorization
< vary: Origin
< vary: Fields
< x-content-type-options: nosniff
< x-debug-token: 099a44
< x-debug-token-link: https://localhost/_profiler/099a44
< x-frame-options: deny
< x-robots-tag: noindex
< content-length: 22
<
* Connection #0 to host localhost left intact
{"name":"hello world"}
Additional details
Also what I have discovered, if you lower the number of GreetingItems
that are linked to a /greeting/1
it will start working properly.
However since in above examples we're selecting a name
field, this should not cause an issue.
I've enabled debug mode in FrankenPHP and attached relevant logs here, but it seems to me that everything there looks fine.
TLDR;
There is a gzip
issue while running the Vulcain, just clone the forked api-platform
test case repository and run above cURL commands to recreate it.
Given that every major browser today automatically adds "Accept-Encoding: gzip deflate br"
header and it can not be overriden, Vulcain in its current state is not usable in any endpoints which have any OneToMany
relations.
Hope this can get attention it needs and get fixed.
Thanks,
Mislav
Build Type
Docker (Debian Bookworm)
Worker Mode
Tested only in worker mode
Operating System
GNU/Linux (Debian 12)
CPU Architecture
x86_64
PHP configuration
default from api-platform/api-platform
Relevant log output
php-1 | 2025/09/13 09:22:52.606 DEBUG http.handlers.rewrite rewrote request {"request": {"remote_ip": "172.23.0.1", "remote_port": "40164", "client_ip": "172.23.0.1", "proto": "HTTP/1.1", "method": "GET", "host": "localhost", "uri": "index.php", "headers": {"Accept": ["application/ld+json"], "Cache-Control": ["no-cache"], "Accept-Encoding": ["gzip, deflate, br"], "Cookie": ["REDACTED"], "Fields": ["\"name\""], "User-Agent": ["PostmanRuntime/7.46.0"], "Postman-Token": ["667ba6a3-ca2d-476c-9213-5696dd4954ec"], "Connection": ["keep-alive"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "", "server_name": "localhost"}}, "method": "GET", "uri": "index.php"}
php-1 | 2025/09/13 09:22:52.606 DEBUG frankenphp request handling started {"worker": "/app/public/index.php", "thread": 1, "url": "index.php"}
php-1 | 2025/09/13 09:22:52.612 DEBUG frankenphp request handling finished {"thread": 1, "url": "index.php"}
php-1 | 2025/09/13 09:22:52.612 DEBUG frankenphp request handling finished {"worker": "/app/public/index.php", "thread": 1, "url": "index.php"}
php-1 | 2025/09/13 09:22:52.612 INFO http.log.access.log0 handled request {"request": {"remote_ip": "172.23.0.1", "remote_port": "40164", "client_ip": "172.23.0.1", "proto": "HTTP/1.1", "method": "GET", "host": "localhost", "uri": "/greetings/1", "headers": {"Accept": ["application/ld+json"], "Cache-Control": ["no-cache"], "Accept-Encoding": ["gzip, deflate, br"], "Cookie": ["REDACTED"], "Fields": ["\"name\""], "User-Agent": ["PostmanRuntime/7.46.0"], "Postman-Token": ["667ba6a3-ca2d-476c-9213-5696dd4954ec"], "Connection": ["keep-alive"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "", "server_name": "localhost"}}, "bytes_read": 0, "user_id": "", "duration": 0.006403603, "size": 2, "status": 200, "resp_headers": {"Alt-Svc": ["h3=\":443\"; ma=2592000"], "Vary": ["Accept", "Content-Type", "Authorization", "Origin", "Accept-Encoding", "Fields"], "X-Content-Type-Options": ["nosniff"], "Cache-Control": ["no-cache, private"], "Date": ["Sat, 13 Sep 2025 09:22:52 GMT"], "Link": ["<https://localhost/docs.jsonld>; rel=\"http://www.w3.org/ns/hydra/core#apiDocumentation\""], "X-Robots-Tag": ["noindex"], "Server": ["Caddy"], "Accept-Patch": ["application/merge-patch+json"], "Etag": ["\"40022186737629e7-br\""], "X-Debug-Token": ["a66f1f"], "Content-Encoding": ["br"], "Permissions-Policy": ["browsing-topics=()"], "Content-Type": ["application/ld+json; charset=utf-8"], "X-Frame-Options": ["deny"], "X-Debug-Token-Link": ["https://localhost/_profiler/a66f1f"], "Content-Length": ["2"]}}
php-1 | 2025/09/13 09:22:52.617 DEBUG frankenphp waiting for request {"worker": "/app/public/index.php", "thread": 1}