WebMVC: upstream aborting the response body is logged by Tomcat as an unhandled ERROR and dispatched to `/error`
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 25/100
- Tipo di issue
- Bug
- Chiarezza
- Abbastanza chiara
- Stato di attività
- Attiva
- Stack tecnologico
- java, spring-boot
- Ambito
- backend
Direzione di ricerca
Start in AbstractProxyExchange.copyResponseBodyWithFlushing and copyResponseBody, where the upstream read failure is rethrown, and RestClientProxyExchange.doExchange, where the copy is invoked. Distinguish an upstream read failure from a client write failure, then decide how to abort the downstream response without propagating. The reproducer is a raw-socket test that sends a 200 text/event-stream, one event, then a TCP RST; done when the truncated stream still reaches the client and neither the Tomcat ERROR nor the /error dispatch is logged.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Describe the bug
Spring Cloud 2025.1.4 / spring-cloud-gateway-server-webmvc 5.0.4 (Spring Boot 4.0.9, Tomcat 11.0.27). The same behaviour occurs on 5.1.0-RC1 and on 4.1.x.
We run a WebMVC gateway in front of SSE (LLM streaming) backends. When a backend hits its own read timeout, it gives up on a stream it has already started: the status, headers and some events are already sent, and then it closes the connection without the terminating chunk. That's an everyday condition for us and nothing the gateway can fix. Even so, every occurrence shows up as an unhandled servlet exception:
ERROR o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
java.io.IOException: closed
at o.s.c.g.server.mvc.common.AbstractProxyExchange.copyResponseBodyWithFlushing(AbstractProxyExchange.java:97)
at o.s.c.g.server.mvc.common.AbstractProxyExchange.copyResponseBody(AbstractProxyExchange.java:50)
at o.s.c.g.server.mvc.handler.RestClientProxyExchange.lambda$doExchange$3(RestClientProxyExchange.java:82)
at o.s.c.g.server.mvc.handler.GatewayServerResponseBuilder$WriteFunctionResponse.writeToInternal(GatewayServerResponseBuilder.java:232)
at o.s.c.g.server.mvc.handler.AbstractGatewayServerResponse.writeTo(AbstractGatewayServerResponse.java:97)
at o.s.web.servlet.function.support.HandlerFunctionAdapter.handle(HandlerFunctionAdapter.java:110)
at o.s.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:963)
...
Caused by: java.io.IOException: chunked transfer encoding, state: READING_LENGTH
Caused by: java.net.SocketException: Connection reset
DEBUG o.s.web.servlet.DispatcherServlet : "INCLUDE" dispatch for POST "/error", parameters={}
WARN .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotWritableException: No converter for [class java.util.LinkedHashMap] with preset Content-Type 'text/event-stream']
What the client sees is already correct: a truncated chunked stream with no terminating chunk, so it can tell the stream didn't complete. The problem is the server side. An expected upstream condition produces one ERROR with a full stack trace and two more lines from an /error dispatch that can't write anything to a committed response. In our case this is the main source of noise in our error alerting.
As far as we can tell, there's no clean place to handle this in an application:
- The upstream body is read inside
ServerResponse.writeTo, after everyHandlerFilterFunctionhas returned, so a route filter'stry/catcharoundnext.handle(request)never sees the exception. - Replacing
MvcUtils.CLIENT_RESPONSE_INPUT_STREAM_ATTRfrom an outer filter doesn't work either, because the request is rebuilt with a copied attributes map. - Spring Framework's
DisconnectedClientHelperhandles the opposite case, where the downstream client went away. It matches on messages such as "connection reset", so it would also misclassify this upstream reset as a client disconnect.
Sample
A self-contained reproducer (single test, mvn test, JDK 21+): https://gist.github.com/Shawyeok/8bb7604a03b63598d0946a6a05dcd9f2
The test's upstream is a raw socket that sends 200 text/event-stream, one event, and then a TCP RST. The test asserts that the downstream stream is truncated, which passes, and that neither the Tomcat ERROR nor the /error dispatch appears, which fails.
What we do today (workaround)
We wrap the route's ServerResponse in a delegating GatewayServerResponse. Its writeTo hands SCG an HttpServletResponseWrapper that records whether a write or flush to the client failed:
- If the client write failed, we rethrow unchanged, as today.
- Otherwise the upstream read failed. We log one INFO line, increment a metric, and rethrow as
org.apache.coyote.CloseNowException. Tomcat logs that only at DEBUG and still closes the connection without the terminating chunk.
We also exclude ErrorMvcAutoConfiguration, since a gateway has no error page of its own to serve.
This works, but it depends on a Tomcat-internal exception type and on wrapping every route, so we'd rather not keep it.
Questions for the maintainers
- Is it intended that an upstream I/O failure during the body copy propagates to the servlet container as an unhandled exception? Or would you consider handling it inside the gateway, for example by telling an upstream read failure apart from a client write failure in
copyResponseBody, logging the former at a lower level, and aborting the downstream response without rethrowing? - If you'd rather keep it propagating, could it become an exception type or hook that applications can recognise and handle? A dedicated exception, a callback, or a property would all work for us. Is there an approach you'd recommend instead of ours?
- For the
/errordispatch on an already-committed proxied response: is that something the gateway should avoid, or is it Spring Boot / Tomcat behaviour that we should raise there?
Related: #3999 / #4217 changed how the upstream response is closed when the copy fails because the client disconnected. The upstream-side failure described here still rethrows on 5.0.4 and 5.1.0-RC1.
Happy to contribute a PR if you can point us at the direction you'd prefer.
- Lingua principale
- Java
- Stelle
- 4.9k
- Fork
- 3.5k
- Merge medio
- 2g 4h
- PR unite (30g)
- 9
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Nessun modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di spring-cloud/spring-cloud-gateway
-
RequestRateLimiterGatewayFilterFactory commits 429 response instead of throwingForse già presa @seonwooj0810 l’ha presa 155 giorni fa. Apertawaiting-for-triage
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
spring-cloud/spring-cloud-gateway#4175 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Add @Order annotation to gatewayRestClientCustomizer BeanForse già presa @CheHyeonYeong l’ha presa 227 giorni fa. Apertaenhancement help wanted
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
spring-cloud/spring-cloud-gateway#3626 · 3 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
Ambiguity in section 19 of the documentation - Spring Cloud Gateway - Custom Route PredicateForse già presa @won-seoop l’ha presa 154 giorni fa. Apertadocumentation
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
spring-cloud/spring-cloud-gateway#3463 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Improve TokenRelay filter documentation when Gateway acts as a BFFForse già presa Una pull request collegata a questa issue è aperta o già unita. Apertawaiting-for-triage
Difficoltà 2/5 Mezza giornata Idoneità per principianti 58/100
spring-cloud/spring-cloud-gateway#4355 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
waiting-for-triage
Difficoltà 3/5 1-2 giorni Idoneità per principianti 35/100
spring-cloud/spring-cloud-gateway#4341 ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di spring-cloud/spring-cloud-gateway
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
langchain4j/langchain4j#7068 ·
I maintainer di solito rispondono entro 1 giorno
-
Mend: dependency security vulnerability untriaged
Difficoltà 2/5 1-3 ore Idoneità per principianti 73/100
opensearch-project/k-NN#3693 ·
I maintainer di solito rispondono entro 1 giorno
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 67/100
I maintainer di solito rispondono entro 1 giorno
-
[Bug]: Encrypted PDF portfolios show an empty portfolio panelForse già presa @christophmertins l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
Stirling-Tools/Stirling-PDF#8864 · 1 reazione ·
I maintainer di solito rispondono entro 2 giorni
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 85/100
CodingDocs/awesome-java#41 · 1 commento ·