Affects: \
The problem occurs defining a rest service in a webflux application, the service generates a Flux, and if an error occurs when processing it in an element (except the first one) there is no response of the server.
I created this demo to reproduce the problem. This publishes 3 services:
- /srvError1 -> fails processing the second element of the Flux
- /srvError2 -> fails processing the fist element of the Flux
- /srvOk -> without errors
Running the following requests:
-
curl --location --request GET 'http://localhost:8080/srvError1'
->curl: (18) transfer closed with outstanding read data remaining
-
curl --location --request GET 'http://localhost:8080/srvError2'
->{"timestamp":1705509733343,"path":"/srvError2","status":500,"error":"Internal Server Error","requestId":"764d728a-5"}
-
curl --location --request GET 'http://localhost:8080/srvOk'
->[1,2,3]
The responses of the services srvError
should be the same.
Comment From: bclozel
Thanks for providing a sample application.
The exact output of the problematic endpoint is the following.
$ curl --location --request GET 'http://localhost:8080/srvError1'
curl: (18) transfer closed with outstanding read data remaining
[1%
As you can see, the first element of the Flux
is written to the response. curl complains rightfully because the response is using transfer-encoding: chunked
and the response body is not completed. The main difference between /srvError1
and /srvError2
is that the first one starts writing to the response, whereas the second one triggers the error before writing elements to the response. Once the response is commited, the response headers are immutable and bytes can be flushed to the network. This means that if the error handling would write to the response, you would get garbled content; for example, you would get:
curl --location --request GET 'http://localhost:8080/srvError1' -v
Note: Unnecessary use of -X or --request, GET is already inferred.
* Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /srvError1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.1.2
> Accept: */*
>
< HTTP/1.1 200 OK
< transfer-encoding: chunked
< Content-Type: application/json
<
[1{"timestamp":1705509733343,"path":"/srvError2","status":500,"error":"Internal Server Error","requestId":"764d728a-5"}
You can see this in action by putting break points where RuntimeException
instances are thrown in your controller and in the following method: org.springframework.http.server.reactive.AbstractServerHttpResponse#doCommit(java.util.function.Supplier<? extends reactor.core.publisher.Mono<java.lang.Void>>)
. In one case, the commit action is performed before the error is raised.
There is a significant behavior difference with Spring MVC, because the Servlet spec does allow to reset the response, if possible. We have refined this behavior with #31104. Still, Spring MVC applications can also run into the same problem: if it's not possible to reset the response, the error handler will not write to the response and just close it.
At this point, there is no consistent way to reset the response with Reactor Netty. As said above, even if it was the case this does not provide any guarantee and you can still run into this. Unfortunately, I don't see anything actionable right now in Spring Framework about this, so I'm closing this issue.
Thanks!