spring-projects / spring-framework

Spring Framework
https://spring.io/projects/spring-framework
Apache License 2.0
56.32k stars 38.01k forks source link

SseEmitter HandlerInterceptor Multiple execution #33333

Closed FPEsocrter closed 1 month ago

FPEsocrter commented 1 month ago

HandlerInterceptor Interception SseEmitter Mapping Multiple execution preHandle

AsyncContextImpl AsyncContextImpl.complete is not called

FPEsocrter commented 1 month ago

AsyncContextImpl AsyncContextImpl.complete is not called

snicoll commented 1 month ago

@FPEsocrter sorry but the description is not actionable. I am not sure if you're trying to report a bug or a request for enhancement. Can you please edit the original description and provide more details?

FPEsocrter commented 1 month ago
@Component
public class ComputingPowerCheckerInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

        System.out.println("preHandle");
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {

        System.out.println("afterCompletion");
    }
}
package cn.biz;

import jakarta.servlet.AsyncContext;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;

import jakarta.servlet.http.HttpServletRequest;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.RequestFacade;
import org.apache.coyote.ActionCode;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.Map;

@RestController
@RequestMapping("/language")
public class LanguageController {

    @GetMapping("/open")
    public Map<String, Object> open() {
        return Map.of("", "");
    }

    @GetMapping("/see")
    public SseEmitter See(HttpServletRequest request ) throws Exception {
        System.out.println("see");
        SseEmitter sseEmitter = new SseEmitter();

        new Thread(() -> {

            try {
                sseEmitter.send("tes", MediaType.TEXT_PLAIN);
                Thread.sleep(3*1000);
                sseEmitter.send("tes", MediaType.TEXT_PLAIN);
                sseEmitter.complete();

            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }).start();
        return sseEmitter;
    }

    @GetMapping("/response")
    public void testResp(ServletRequest request, ServletResponse response){
        AsyncContext asyncContext = request.startAsync();
        System.out.println("response");
        asyncContext.start(() -> {
            try {
                PrintWriter out = asyncContext.getResponse().getWriter();
                out.println("test");
                out.flush();
                Thread.sleep(3*1000);
                out.println("test");
                out.flush();
                out.close();
                asyncContext.complete();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });

    }
}

transfer /response preHandle one response Print Order preHandle response
afterCompletion

transfer / see preHandle twice
see Print Order preHandle see
afterCompletion preHandle

Normally it should only be called once preHandle

FPEsocrter commented 1 month ago

sseEmitter.complete(); AsyncContextImpl.complete is not called. is bug

snicoll commented 1 month ago

Sorry but dumping code in text like that is not very helpful. Someone might see what the problem could be, but I don't. If you want to speed up support, please move all that code into an actual small application we can run ourselves. You can attach a zip to this issue or push the code to a GitHub repository.

rstoyanchev commented 1 month ago

Generally Spring MVC executes async requests in two phases as described in the docs. The first covers controller method invocation and async handling. After that we perform an ASYNC dispatch to complete processing on a Servlet container thread, which is required in some cases like view rendering.

The same is true for the SseEmitter scenario as well, in which case it looks a bit more strange because there is no further handling once the stream is over, but it provides consistency in the Spring MVC request lifecycle (exception handling, interception, etc).

If you want to ignore the second preHandle you can check if request.getDispatcherType() is ASYNC.