GzipResponseAndCallback.commit() NPE on null HttpField array entry with Spring MVC ResponseBodyEmitter (12.0.34)
- Dominant language
- Java
- Stars
- 4.1k
- Forks
- 2k
- Avg merge
- 3d 56m
- Merged PRs (30d)
- 48
Description
Jetty version(s)
Jetty: 12.0.34 (via Spring Boot BOM)
HTTP version
HTTP 1.1 and HTTP/2
Java version/vendor
openjdk version "21.0.10" 2026-01-20 LTS
OpenJDK Runtime Environment Corretto-21.0.10.7.1 (build 21.0.10+7-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.10.7.1 (build 21.0.10+7-LTS, mixed mode, sharing)
OS type/version
Linux (Kubernetes pod, Amazon Linux 2)
Description
When GzipHandler is enabled and a Spring MVC endpoint returns a ResponseBodyEmitter that streams a response ≥ the min-response-size threshold, GzipResponseAndCallback.commit() throws a NullPointerException on a null entry in the internal HttpField[] array. The exception is swallowed by the async infrastructure, leaving the stream permanently stalled — the opening bytes are sent to the client but no further data arrives and the connection eventually times out.
The bug does not reproduce when the response is below the threshold (GzipHandler skips compression and passes through). This makes it environment-dependent — e.g. sparse dev data never crosses the threshold while large prod result sets always do.
Stack trace:
```
java.lang.IllegalStateException: Failed to send [
at org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter.send(ResponseBodyEmitter.java:217)
at ..
at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1804)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: java.lang.NullPointerException: Cannot invoke "org.eclipse.jetty.http.HttpField.is(String)" because "f" is null
at org.eclipse.jetty.http.HttpFields.contains(HttpFields.java:371)
at org.eclipse.jetty.ee10.servlet.ServletApiResponse.containsHeader(ServletApiResponse.java:117)
at jakarta.servlet.http.HttpServletResponseWrapper.containsHeader(HttpServletResponseWrapper.java:68)
at jakarta.servlet.http.HttpServletResponseWrapper.containsHeader(HttpServletResponseWrapper.java:68)
at org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.writeHeaders(ReferrerPolicyHeaderWriter.java:96)
at org.springframework.security.web.header.HeaderWriterFilter.writeHeaders(HeaderWriterFilter.java:99)
at org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterResponse.writeHeaders(HeaderWriterFilter.java:132)
at org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterResponse.onResponseCommitted(HeaderWriterFilter.java:124)
at org.springframework.security.web.util.OnCommittedResponseWrapper.doOnResponseCommitted(OnCommittedResponseWrapper.java:279)
at org.springframework.security.web.util.OnCommittedResponseWrapper$SaveContextServletOutputStream.flush(OnCommittedResponseWrapper.java:551)
at org.springframework.web.context.request.async.StandardServletAsyncWebRequest$LifecycleServletOutputStream.flush(StandardServletAsyncWebRequest.java:415)
at org.springframework.util.StreamUtils.copy(StreamUtils.java:137)
at org.springframework.http.converter.StringHttpMessageConverter.writeInternal(StringHttpMessageConverter.java:128)
at org.springframework.http.converter.StringHttpMessageConverter.writeInternal(StringHttpMessageConverter.java:44)
at org.springframework.http.converter.AbstractHttpMessageConverter.write(AbstractHttpMessageConverter.java:234)
at org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler$DefaultSseEmitterHandler.sendInternal(ResponseBodyEmitterReturnValueHandler.java:315)
at org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler$DefaultSseEmitterHandler.send(ResponseBodyEmitterReturnValueHandler.java:295)
at org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter.send(ResponseBodyEmitter.java:211)
... 5 common frames omitted
```
How to reproduce?
1. Enable GzipHandler with a min-response-size threshold (in Spring Boot: `server.compression.enabled=true`,
`server.compression.min-response-size=10240.`
2. Implement a @RestController endpoint returning Response>.
3. In a background thread, write chunks via emitter.send(...) until the total response exceeds the threshold.
4. Send a request to the endpoint.
Expected: response streams to completion with gzip compres
Actual: first chunk is delivered, then the stream stalls ireaches the caller. Client times out with 1 byte received.
Workaround: exclude the affected endpoint(s) from GzipHand in a JettyServerCustomizer bean.
Contributor guide
Assessment
This issue has not been assessed yet.