如何在超时时使用 Spring SseEmitter 避免 HttpMediaTypeNotAcceptableException

How to avoid HttpMediaTypeNotAcceptableException with Spring SseEmitter on timeout

当 SseEmitter 超时时,我收到错误 HttpMediaTypeNotAcceptableException

在 SO 中搜索此错误代码时,我只找到与为 ResponseBody 注册转换器相关的答案。但我看不出这怎么可能是我面临的同一个问题,因为内容类型是 text/event-stream 并且由 Spring 自动处理。

但是,除此之外,通信似乎还不错;消息正在正确发送到客户端。

@RequestMapping(path = "/channel")
@CrossOrigin
public SseEmitter channel() throws IOException {

    SseEmitter emitter = new SseEmitter();

    // Storing the emitter
    // ...
    player.setEmitter(emitter);

    return emitter;
}

在播放器中,我这样做:

public void setEmitter(SseEmitter emitter) {
    if (this.emitter != null) {
        // Old emitter must be completed for user
        this.emitter.complete();
    }

    this.emitter = emitter;
    this.emitter.onTimeout(() -> {
        if (this.emitter != null) this.emitter.complete();
    });
    this.emitter.onCompletion(() -> this.emitter = null);
}

public void sendMessage(String message) {
    if (this.emitter != null) {
        emitter.send(message);
    }
}

(顺便说一下,这是处理发射器的好方法吗?即 onTimeout 和 onCompletion 的注册?这个想法是在每次重新连接时创建一个新的发射器,这意味着需要更换旧的发射器.)

发送的消息是序列化为 JSON 文本的数据结构。在 JavaScript 客户端上,它被正确反序列化为 JSON 对象。我没有在任何地方指定 JSON 内容,因为我认为从 SseEmitter/SSE 的角度来看,它只是文本。

在建立连接期间,一切正常。在每次超时时,客户端都会按预期重新连接。 但是,我似乎总是遇到以下与超时和重新连接过程有关的异常:

org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation
    at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:191) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor.handleReturnValue(HttpEntityMethodProcessor.java:183) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:81) ~[spring-web-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:126) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:832) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:743) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:961) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:895) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:967) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:858) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:687) ~[javax.servlet-api-3.1.0.jar:3.1.0]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:843) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:790) ~[javax.servlet-api-3.1.0.jar:3.1.0]
    at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:812) ~[jetty-servlet-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:587) ~[jetty-servlet-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143) ~[jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:595) ~[jetty-security-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:223) ~[jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.handler.ContextHandler.__doHandle(ContextHandler.java:1127) ~[jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java) ~[jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:515) ~[jetty-servlet-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:185) ~[jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1061) ~[jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141) ~[jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:97) ~[jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.Server.handleAsync(Server.java:549) ~[jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:348) [jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.HttpChannel.run(HttpChannel.java:262) [jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:635) [jetty-util-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.util.thread.QueuedThreadPool.run(QueuedThreadPool.java:555) [jetty-util-9.2.14.v20151106.jar:9.2.14.v20151106]
    at java.lang.Thread.run(Thread.java:745) [na:na]

我正在使用 Spring Boot with Jetty。我也遇到了这个错误,但我认为这是第一个错误的结果:

java.lang.IllegalStateException: Committed
    at org.eclipse.jetty.server.Response.resetBuffer(Response.java:1243) ~[jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.Response.sendError(Response.java:567) ~[jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.Response.sendError(Response.java:544) ~[jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at javax.servlet.http.HttpServletResponseWrapper.sendError(HttpServletResponseWrapper.java:167) ~[javax.servlet-api-3.1.0.jar:3.1.0]
    at javax.servlet.http.HttpServletResponseWrapper.sendError(HttpServletResponseWrapper.java:167) ~[javax.servlet-api-3.1.0.jar:3.1.0]
    at org.springframework.security.web.context.OnCommittedResponseWrapper.sendError(OnCommittedResponseWrapper.java:95) ~[spring-security-web-4.0.3.RELEASE.jar:4.0.3.RELEASE]
    at org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.handleHttpMediaTypeNotAcceptable(DefaultHandlerExceptionResolver.java:257) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.doResolveException(DefaultHandlerExceptionResolver.java:121) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver.resolveException(AbstractHandlerExceptionResolver.java:137) [spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.handler.HandlerExceptionResolverComposite.resolveException(HandlerExceptionResolverComposite.java:74) [spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.processHandlerException(DispatcherServlet.java:1185) [spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1022) [spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:973) [spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:895) [spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:967) [spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:858) [spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:687) [javax.servlet-api-3.1.0.jar:3.1.0]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:843) [spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:790) [javax.servlet-api-3.1.0.jar:3.1.0]
    at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:812) [jetty-servlet-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:587) [jetty-servlet-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143) [jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:595) [jetty-security-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:223) [jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.handler.ContextHandler.__doHandle(ContextHandler.java:1127) [jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java) [jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:515) [jetty-servlet-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:185) [jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1061) [jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141) [jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:97) [jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.Server.handleAsync(Server.java:549) [jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:348) [jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.server.HttpChannel.run(HttpChannel.java:262) [jetty-server-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:635) [jetty-util-9.2.14.v20151106.jar:9.2.14.v20151106]
    at org.eclipse.jetty.util.thread.QueuedThreadPool.run(QueuedThreadPool.java:555) [jetty-util-9.2.14.v20151106.jar:9.2.14.v20151106]
    at java.lang.Thread.run(Thread.java:745) [na:na]

错误不在服务器端。在客户端上点击几次后,突然有两个客户端 EventSource 对象。这就是导致错误的原因。

此错误发生在基于 servlet 的服务器上,例如 jetty 或 tomcat,因为 servlet api 不会在客户端连接关闭时发出通知。查明连接是否已关闭的唯一方法是发送消息,并在触发 IOException 时停止 sseemitter。

问题是 IOException 也被分派给 spring mvc,它找不到合适的 httpmessageconverter,因此抛出 HttpMediaTypeNotAcceptableException.

最好的选择是将 sse 发射器迁移到 netty 支持的微服务。就我而言,我不得不坚持 tomcat。这是我在 spring 启动应用程序中解决它的方法:

  • 我添加了一个自定义的 httpmessagecomverter 来处理媒体类型的地图 text/event-stream
  • 我使用 spring web @ExceptionHandler 添加了一个错误处理程序来静音 ClientAbortException