spring-projects / spring-boot

Spring Boot
https://spring.io/projects/spring-boot
Apache License 2.0
74.7k stars 40.58k forks source link

Facing getWriter() has already been called error in flux controller #9655

Closed sangtraceur closed 7 years ago

sangtraceur commented 7 years ago

Hello! Id like to ask if I'am making something wrong during adding reactive support to my application: My controller is:

@RestController
public class ReportController {

    @GetMapping(value = "/v2/reports/efficiency")
    public Flux<String> report1(Writer responseWriter) throws IOException {
        Flux<String> flux = Flux.just("red", "white", "blue");
        return flux;

    }
}

I'am facing an error during invocation:


java.lang.IllegalStateException: getWriter() has already been called for this response
    at org.apache.catalina.connector.Response.getOutputStream(Response.java:592) ~[tomcat-embed-core-8.5.15.jar:8.5.15]
    at org.apache.catalina.connector.ResponseFacade.getOutputStream(ResponseFacade.java:194) ~[tomcat-embed-core-8.5.15.jar:8.5.15]
    at javax.servlet.ServletResponseWrapper.getOutputStream(ServletResponseWrapper.java:100) ~[tomcat-embed-core-8.5.15.jar:8.5.15]
    at javax.servlet.ServletResponseWrapper.getOutputStream(ServletResponseWrapper.java:100) ~[tomcat-embed-core-8.5.15.jar:8.5.15]
    at org.springframework.http.server.ServletServerHttpResponse.getBody(ServletServerHttpResponse.java:83) ~[spring-web
...
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:904) ~[spring-webmvc-5.0.0.RC2.jar:5.0.0.RC2]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:971) ~[spring-webmvc-5.0.0.RC2.jar:5.0.0.RC2]
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:863) ~[spring-webmvc-5.0.0.RC2.jar:5.0.0.RC2]

As you see request is handled by webmvc, not webflux. Maby I should configure something more to make it work or it is really a bug?

My gradle dependencies:

compile('org.springframework.boot:spring-boot-starter')
compile("org.springframework.boot:spring-boot-starter-hateoas")
compile("org.springframework.boot:spring-boot-starter-security")
compile("org.springframework.boot:spring-boot-starter-data-jpa")
compile('org.springframework.boot:spring-boot-starter-actuator')
compile('org.springframework.boot:spring-boot-starter-log4j2');
compile('org.springframework.boot:spring-boot-starter-webflux')
compile("org.springframework.boot:spring-boot-starter-batch")
compile("org.springframework.security.oauth:spring-security-oauth2")
compile("io.reactivex:rxjava-reactive-streams:1.2.1")
wilkinsona commented 7 years ago

Can you please provide a complete, yet minimal, sample that illustrates the problem?

sangtraceur commented 7 years ago

Please see a repo with the example

bclozel commented 7 years ago

Hi @sangtraceur

I'm not sure this was intentional, but adding spring-boot-starter-hateoas as a dependency also brings spring-boot-starter-web transitively (at least for now, we're in the process of revisiting a few of those choices). Having both Spring MVC and Spring WebFlux on the classpath, Spring Boot assumes you want to use Spring MVC and you've brought WebFlux in your classpath to use its new HTTP client WebClient. Also, HATEAOS does not support (yet) WebFlux. Now this may have been a deliberate choice, but I just wanted to point that out.

Spring MVC now supports reactive endpoints as well. You won't have the full reactive experience since the Servlet support won't be fully non-blocking, but you can express your endpoints using Flux and Mono and still get some benefits out of it.

In order to do that, Spring MVC switches to async mode, just as if you were using one of the async return types like ResponseBodyEmitter or SseEmitter. In a nutshell, doing so will ask Spring MVC to take full control of the servlet response lifecycle (i.e. you should never write to it "manually").

In your project, injecting Writer responseWriter indeed asks for the Servlet response writer, which is resolved by ServletResponseMethodArgumentResolver. This class calls response.getWriter() and this has been done already by Sprinb MVC; this is why you're getting that error.

Removing that method argument solves the issue completely; in fact, getting the Servlet response or any related item in a controller handler should always be the last choice, since you're asking for more control (and more responsibilities).