-
Notifications
You must be signed in to change notification settings - Fork 6.1k
Description
Affects: 5.2.5.RELEASE
We have a Spring Web MVC + jetty application using SSE for notifications. We recently converted our SSE controller from returning a SseEmitter
to returning a Flux<ServerSentEvent<?>>
, whereupon we started seeing sporadic NullPointerException
s from jetty Response.setHeader
calls for something on the order of 0.02% of requests for these SSE endpoints:
2020-05-12 11:15:53.837|WARN|1|qtp453671855-27|||org.eclipse.jetty.server.HttpChannel|MAIN||||/...
java.lang.NullPointerException: null
at org.eclipse.jetty.http.HttpFields.put(HttpFields.java:630) ~[jetty-http-9.4.20.v20190813.jar!/:9.4.20.v20190813]
at org.eclipse.jetty.http.HttpFields.put(HttpFields.java:658) ~[jetty-http-9.4.20.v20190813.jar!/:9.4.20.v20190813]
at org.eclipse.jetty.server.Response.setHeader(Response.java:582) ~[jetty-server-9.4.20.v20190813.jar!/:9.4.20.v20190813]
at javax.servlet.http.HttpServletResponseWrapper.setHeader(HttpServletResponseWrapper.java:203) ~[javax.servlet-api-3.1.0.jar!/:3.1.0]
at org.springframework.security.web.firewall.FirewalledResponse.setHeader(FirewalledResponse.java:49) ~[spring-security-web-5.2.2.RELEASE.jar!/:5.2.2.RELEASE]
at org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter.writeHeaders(XFrameOptionsHeaderWriter.java:99) ~[spring-security-web-5.2.2.RELEASE.jar!/:5.2.2.RELEASE]
....
Looking at the jetty HttpFields
implementation, these NPEs seem to be caused by a corrupted internal _fields
state containing nulls
.
With jetty 9.4.20 these exceptions cause the connection to be aborted, but with newer version of jetty (at least 9.4.27) these corrupted HttpFields
put the jetty HttpChannel
into a busyloop that crashes the entire server: jetty/jetty.project#4860
Investigating the underlying NPE issue together with the jetty maintainers we were unable to find any direct calls to the jetty APIs that would introduce null
values in the HttpFields
state, but looking at the implementation of the jetty HttpFields
structure used in Response.setHeaders/addHeaders
etc, it is suspectible to internal state corruption if called concurrently from multiple threads, and is documented as non-threadsafe:
* <p>This class is not synchronized as it is expected that modifications will only be performed by a * single thread.
This kind of corruption caused by concurrent multi-threaded accesses also matches up with our observation of very rarely occuring state corruption/crashes. And indeed, using jdb
to trace calls to these header-related methods for a single HTTP request, I can see calls from multiple different threads!
Some of the calls are from the jetty server thread, via the servlet filter chain, which is the correct usage:
[1] org.eclipse.jetty.server.Response.addHeader (Response.java:627)
[2] javax.servlet.http.HttpServletResponseWrapper.addHeader (HttpServletResponseWrapper.java:212)
[3] org.springframework.security.web.firewall.FirewalledResponse.addHeader (FirewalledResponse.java:55)
[4] javax.servlet.http.HttpServletResponseWrapper.addHeader (HttpServletResponseWrapper.java:212)
[5] org.springframework.security.web.util.OnCommittedResponseWrapper.addHeader (OnCommittedResponseWrapper.java:63)
[6] org.springframework.web.cors.DefaultCorsProcessor.processRequest (DefaultCorsProcessor.java:63)
[7] org.springframework.web.servlet.handler.AbstractHandlerMapping$CorsInterceptor.preHandle (AbstractHandlerMapping.java:582)
[8] org.springframework.web.servlet.HandlerExecutionChain.applyPreHandle (HandlerExecutionChain.java:141)
[9] org.springframework.web.servlet.DispatcherServlet.doDispatch (DispatcherServlet.java:1,035)
[10] org.springframework.web.servlet.DispatcherServlet.doService (DispatcherServlet.java:943)
[11] org.springframework.web.servlet.FrameworkServlet.processRequest (FrameworkServlet.java:1,006)
[12] org.springframework.web.servlet.FrameworkServlet.doGet (FrameworkServlet.java:898)
[13] javax.servlet.http.HttpServlet.service (HttpServlet.java:687)
[14] org.springframework.web.servlet.FrameworkServlet.service (FrameworkServlet.java:883)
[15] javax.servlet.http.HttpServlet.service (HttpServlet.java:790)
[16] org.eclipse.jetty.servlet.ServletHolder.handle (ServletHolder.java:755)
[17] org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter (ServletHandler.java:1,617)
[18] com.example.common.audit.MDCLogFilter.doFilter (MDCLogFilter.java:67)
[19] org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter (ServletHandler.java:1,604)
[20] org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter (FilterChainProxy.java:320)
[21] org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke (FilterSecurityInterceptor.java:126)
[22] org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter (FilterSecurityInterceptor.java:90)
[23] org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter (FilterChainProxy.java:334)
[24] org.springframework.security.web.access.ExceptionTranslationFilter.doFilter (ExceptionTranslationFilter.java:118)
[25] org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter (FilterChainProxy.java:334)
[26] org.springframework.security.web.session.SessionManagementFilter.doFilter (SessionManagementFilter.java:137)
[27] org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter (FilterChainProxy.java:334)
[28] org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter (AnonymousAuthenticationFilter.java:111)
[29] org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter (FilterChainProxy.java:334)
[30] org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter (SecurityContextHolderAwareRequestFilter.java:158)
[31] org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter (FilterChainProxy.java:334)
[32] com.example.common.auth.WhitelistIpAuthenticationFilter.doFilter (WhitelistIpAuthenticationFilter.java:37)
[33] org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter (FilterChainProxy.java:334)
[34] org.springframework.security.web.authentication.www.BasicAuthenticationFilter.doFilterInternal (BasicAuthenticationFilter.java:204)
[35] org.springframework.web.filter.OncePerRequestFilter.doFilter (OncePerRequestFilter.java:119)
[36] org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter (FilterChainProxy.java:334)
[37] com.example.common.auth.StatelessAuthenticationFilter.doFilter (StatelessAuthenticationFilter.java:49)
[38] org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter (FilterChainProxy.java:334)
[39] org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter (HeaderWriterFilter.java:92)
[40] org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal (HeaderWriterFilter.java:77)
[41] org.springframework.web.filter.OncePerRequestFilter.doFilter (OncePerRequestFilter.java:119)
[42] org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter (FilterChainProxy.java:334)
[43] org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter (SecurityContextPersistenceFilter.java:105)
[44] org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter (FilterChainProxy.java:334)
[45] org.springframework.security.web.FilterChainProxy.doFilterInternal (FilterChainProxy.java:215)
[46] org.springframework.security.web.FilterChainProxy.doFilter (FilterChainProxy.java:178)
[47] org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate (DelegatingFilterProxy.java:358)
[48] org.springframework.web.filter.DelegatingFilterProxy.doFilter (DelegatingFilterProxy.java:271)
[49] org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter (ServletHandler.java:1,604)
[50] org.springframework.web.filter.RequestContextFilter.doFilterInternal (RequestContextFilter.java:100)
[51] org.springframework.web.filter.OncePerRequestFilter.doFilter (OncePerRequestFilter.java:119)
[52] org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter (ServletHandler.java:1,604)
[53] org.springframework.web.filter.FormContentFilter.doFilterInternal (FormContentFilter.java:93)
[54] org.springframework.web.filter.OncePerRequestFilter.doFilter (OncePerRequestFilter.java:119)
[55] org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter (ServletHandler.java:1,604)
[56] org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal (WebMvcMetricsFilter.java:109)
[57] org.springframework.web.filter.OncePerRequestFilter.doFilter (OncePerRequestFilter.java:119)
[58] org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter (ServletHandler.java:1,604)
[59] org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal (CharacterEncodingFilter.java:201)
[60] org.springframework.web.filter.OncePerRequestFilter.doFilter (OncePerRequestFilter.java:119)
[61] org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter (ServletHandler.java:1,604)
[62] org.eclipse.jetty.servlet.ServletHandler.doHandle (ServletHandler.java:545)
[63] org.eclipse.jetty.server.handler.ScopedHandler.handle (ScopedHandler.java:143)
[64] org.eclipse.jetty.security.SecurityHandler.handle (SecurityHandler.java:590)
[65] org.eclipse.jetty.server.handler.HandlerWrapper.handle (HandlerWrapper.java:127)
[66] org.eclipse.jetty.server.handler.ScopedHandler.nextHandle (ScopedHandler.java:235)
[67] org.eclipse.jetty.server.session.SessionHandler.doHandle (SessionHandler.java:1,610)
[68] org.eclipse.jetty.server.handler.ScopedHandler.nextHandle (ScopedHandler.java:233)
[69] org.eclipse.jetty.server.handler.ContextHandler.doHandle (ContextHandler.java:1,300)
[70] org.eclipse.jetty.server.handler.ScopedHandler.nextScope (ScopedHandler.java:188)
[71] org.eclipse.jetty.servlet.ServletHandler.doScope (ServletHandler.java:485)
[72] org.eclipse.jetty.server.session.SessionHandler.doScope (SessionHandler.java:1,580)
[73] org.eclipse.jetty.server.handler.ScopedHandler.nextScope (ScopedHandler.java:186)
[74] org.eclipse.jetty.server.handler.ContextHandler.doScope (ContextHandler.java:1,215)
[75] org.eclipse.jetty.server.handler.ScopedHandler.handle (ScopedHandler.java:141)
[76] org.eclipse.jetty.server.handler.StatisticsHandler.handle (StatisticsHandler.java:173)
[77] org.eclipse.jetty.server.handler.HandlerWrapper.handle (HandlerWrapper.java:127)
[78] org.eclipse.jetty.server.Server.handle (Server.java:500)
[79] org.eclipse.jetty.server.HttpChannel.lambda$handle$1 (HttpChannel.java:383)
[80] org.eclipse.jetty.server.HttpChannel$$Lambda$1167.1412565883.dispatch (null)
[81] org.eclipse.jetty.server.HttpChannel.dispatch (HttpChannel.java:547)
[82] org.eclipse.jetty.server.HttpChannel.handle (HttpChannel.java:375)
[83] org.eclipse.jetty.server.HttpConnection.onFillable (HttpConnection.java:273)
[84] org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded (AbstractConnection.java:311)
[85] org.eclipse.jetty.io.FillInterest.fillable (FillInterest.java:103)
[86] org.eclipse.jetty.io.ChannelEndPoint$2.run (ChannelEndPoint.java:117)
[87] org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask (EatWhatYouKill.java:336)
[88] org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce (EatWhatYouKill.java:313)
[89] org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce (EatWhatYouKill.java:171)
[90] org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.produce (EatWhatYouKill.java:135)
[91] org.eclipse.jetty.io.ManagedSelector$$Lambda$1140.431966204.run (null)
[92] org.eclipse.jetty.util.thread.QueuedThreadPool.runJob (QueuedThreadPool.java:806)
[93] org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run (QueuedThreadPool.java:938)
[94] java.lang.Thread.run (Thread.java:834)
However, there are also calls to addHeader
from the ThreadPoolExecutor
used in the spring framework ResponseBodyEmitterReturnValueHandler
implementation, via the org.springframework.web.servlet.mvc.method.annotation.ReactiveTypeHandler$SseEmitterSubscriber
:
[1] org.eclipse.jetty.server.Response.addHeader (Response.java:627)
[2] javax.servlet.http.HttpServletResponseWrapper.addHeader (HttpServletResponseWrapper.java:212)
[3] org.springframework.security.web.firewall.FirewalledResponse.addHeader (FirewalledResponse.java:55)
[4] javax.servlet.http.HttpServletResponseWrapper.addHeader (HttpServletResponseWrapper.java:212)
[5] org.springframework.security.web.util.OnCommittedResponseWrapper.addHeader (OnCommittedResponseWrapper.java:63)
[6] org.springframework.http.server.ServletServerHttpResponse.lambda$writeHeaders$0 (ServletServerHttpResponse.java:104)
[7] org.springframework.http.server.ServletServerHttpResponse$$Lambda$1241.488706756.accept (null)
[8] java.util.Map.forEach (Map.java:661)
[9] org.springframework.http.server.ServletServerHttpResponse.writeHeaders (ServletServerHttpResponse.java:102)
[10] org.springframework.http.server.ServletServerHttpResponse.getBody (ServletServerHttpResponse.java:83)
[11] org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler$StreamingServletServerHttpResponse.getBody (ResponseBodyEmitterReturnValueHandler.java:278)
[12] org.springframework.http.converter.StringHttpMessageConverter.writeInternal (StringHttpMessageConverter.java:122)
[13] org.springframework.http.converter.StringHttpMessageConverter.writeInternal (StringHttpMessageConverter.java:44)
[14] org.springframework.http.converter.AbstractHttpMessageConverter.write (AbstractHttpMessageConverter.java:227)
[15] org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler$HttpMessageConvertingHandler.sendInternal (ResponseBodyEmitterReturnValueHandler.java:210)
[16] org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler$HttpMessageConvertingHandler.send (ResponseBodyEmitterReturnValueHandler.java:203)
[17] org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter.sendInternal (ResponseBodyEmitter.java:189)
[18] org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter.send (ResponseBodyEmitter.java:183)
[19] org.springframework.web.servlet.mvc.method.annotation.SseEmitter.send (SseEmitter.java:126)
[20] org.springframework.web.servlet.mvc.method.annotation.ReactiveTypeHandler$SseEmitterSubscriber.send (ReactiveTypeHandler.java:365)
[21] org.springframework.web.servlet.mvc.method.annotation.ReactiveTypeHandler$AbstractEmitterSubscriber.run (ReactiveTypeHandler.java:308)
[22] java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1,128)
[23] java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:628)
[24] java.lang.Thread.run (Thread.java:834)
Looking at the code, the EDIT: see comment below on spring-security HeaderWriterFilter behavior insteadResponseBodyEmitter
created by the ResponseBodyEmitterReturnValueHandler
/ReactiveTypeHandler
should not be writing any HTTP response headers outside of the extendResponse
called in the servlet thread, but the ReactiveTypeHandler
subscription's SseEmitter.send
call in the TaskExecutor
thread is also invoking ServletServerHttpResponse.writeHeaders
via the HttpMessageConvertingHandler.sendInternal
.
Complete error log and jdb
traces atttached:
EDIT: edited stack traces to censor custom package names; use the first task-9
thread stack from the jdb traces as the example