jetty / jetty/jetty.project

GzipResponseAndCallback.commit() NPE on null HttpField array entry with Spring MVC ResponseBodyEmitter (12.0.34)

Open
#15,732 3 comments 0 reactions 1 assignee Claimed by @lorban View on GitHub
Bug
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.