Skip to content

Vulcain is not working with gzip #1873

@mislavjakopovic

Description

@mislavjakopovic

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.

Image

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}

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions