Skip to content

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

Description

@zsait-clearstreet

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

BugFor general bugs on Jetty side

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions