https://bz.apache.org/bugzilla/show_bug.cgi?id=65677
Bug ID: 65677 Summary: InvalidMarkException in Http11InputBuffer.fill when socket read throws AsynchronousCloseException Product: Tomcat 9 Version: 9.0.54 Hardware: PC OS: Linux Status: NEW Severity: normal Priority: P2 Component: Connectors Assignee: dev@tomcat.apache.org Reporter: andreas.g...@qaware.de Target Milestone: ----- When reading a CoyoteInputStream during a chunked Http/1.1 POST request, I've encountered a java.nio.InvalidMarkException under certain conditions. Here's the stacktrace I observe: java.nio.InvalidMarkException: null at java.nio.Buffer.reset(Unknown Source) at java.nio.ByteBuffer.reset(Unknown Source) at java.nio.ByteBuffer.reset(Unknown Source) at o.a.coyote.http11.Http11InputBuffer.fill(Http11InputBuffer.java:813) at o.a.coyote.http11.Http11InputBuffer.access$400(Http11InputBuffer.java:42) at o.a.c.h.Http11InputBuffer$SocketInputBuffer.doRead(Http11InputBuffer.java:1172) at o.a.c.h.filters.ChunkedInputFilter.readBytes(ChunkedInputFilter.java:310) at o.a.c.h.filters.ChunkedInputFilter.parseChunkHeader(ChunkedInputFilter.java:338) at o.a.c.h.filters.ChunkedInputFilter.doRead(ChunkedInputFilter.java:164) at o.a.coyote.http11.Http11InputBuffer.doRead(Http11InputBuffer.java:249) at org.apache.coyote.Request.doRead(Request.java:640) at o.a.catalina.connector.InputBuffer.realReadBytes(InputBuffer.java:317) at o.a.catalina.connector.InputBuffer.checkByteBufferEof(InputBuffer.java:600) at o.a.catalina.connector.InputBuffer.read(InputBuffer.java:340) at o.a.c.connector.CoyoteInputStream.read(CoyoteInputStream.java:132) I currently assume that the thread reading the CoyoteInputStream is interrupted because it's running within a Resilience4j TimeLimiter and the POST request takes too long. This should be ok though and I'd expect an InterruptedException or IOException in that case from the read call, but instead I get an InvalidMarkException. This InvalidMarkException is thrown in the "finally" block of this part in Http11InputBuffer.fill: byteBuffer.mark(); try { if (byteBuffer.position() < byteBuffer.limit()) { byteBuffer.position(byteBuffer.limit()); } byteBuffer.limit(byteBuffer.capacity()); SocketWrapperBase<?> socketWrapper = this.wrapper; if (socketWrapper != null) { nRead = socketWrapper.read(block, byteBuffer); } else { throw new CloseNowException(sm.getString("iib.eof.error")); } } finally { // Ensure that the buffer limit and position are returned to a // consistent "ready for read" state if an error occurs during in // the above code block. byteBuffer.limit(byteBuffer.position()).reset(); } I've instrumented that part with more debug logs as follows (in particular, for upcoming Tomcat versions, it might be good to add at least the catch block to enhance debugging of such issues in the future): byteBuffer.mark(); if (log.isDebugEnabled()) { log.debug("Set mark at position " + byteBuffer.position()); } try { if (byteBuffer.position() < byteBuffer.limit()) { if (log.isDebugEnabled()) { log.debug("Setting position to limit " + byteBuffer.limit()); } byteBuffer.position(byteBuffer.limit()); } byteBuffer.limit(byteBuffer.capacity()); if (log.isDebugEnabled()) { log.debug("Position before read " + byteBuffer.position()); } SocketWrapperBase<?> socketWrapper = this.wrapper; if (socketWrapper != null) { nRead = socketWrapper.read(block, byteBuffer); } else { throw new CloseNowException(sm.getString("iib.eof.error")); } } catch (Throwable e) { if (log.isDebugEnabled()) { log.debug("Encountered exception in read during fill, position=" + byteBuffer.position(), e); } throw e; } finally { // Ensure that the buffer limit and position are returned to a // consistent "ready for read" state if an error occurs during in // the above code block. if (log.isDebugEnabled()) { log.debug("Calling limit on byteBuffer with position " + byteBuffer.position()); } byteBuffer.limit(byteBuffer.position()).reset(); } When that exception occurs, I see the following logs (before that, the POST request was already running for about 30 seconds until the TimeLimiter kicked in and interrupted the thread): 10:03:18.899 Before fill(): parsingHeader: [false], parsingRequestLine: [false], parsingRequestLinePhase: [0], parsingRequestLineStart: [0], byteBuffer.position(): [3313], byteBuffer.limit(): [3313], end: [2019] 10:03:18.899 Set mark at position 2019 10:03:18.899 Position before read 2019 10:03:18.942 Calling limit on byteBuffer with position 3306 10:03:18.942 Before fill(): parsingHeader: [false], parsingRequestLine: [false], parsingRequestLinePhase: [0], parsingRequestLineStart: [0], byteBuffer.position(): [3306], byteBuffer.limit(): [3306], end: [2019] 10:03:18.942 Set mark at position 2019 10:03:18.942 Position before read 2019 10:03:19.007 Calling limit on byteBuffer with position 3306 10:03:19.007 Received [500 <binary output>] 10:03:19.007 Before fill(): parsingHeader: [false], parsingRequestLine: [false], parsingRequestLinePhase: [0], parsingRequestLineStart: [0], byteBuffer.position(): [3306], byteBuffer.limit(): [3306], end: [2019] 10:03:19.007 Set mark at position 2019 10:03:19.007 Position before read 2019 10:03:19.062 Calling limit on byteBuffer with position 3306 10:03:19.062 Received [500 <binary output>] 10:03:19.063 Before fill(): parsingHeader: [false], parsingRequestLine: [false], parsingRequestLinePhase: [0], parsingRequestLineStart: [0], byteBuffer.position(): [3306], byteBuffer.limit(): [3306], end: [2019] 10:03:19.063 Set mark at position 2019 10:03:19.063 Position before read 2019 10:03:19.131 Calling limit on byteBuffer with position 3306 10:03:19.132 Before fill(): parsingHeader: [false], parsingRequestLine: [false], parsingRequestLinePhase: [0], parsingRequestLineStart: [0], byteBuffer.position(): [3306], byteBuffer.limit(): [3306], end: [2019] 10:03:19.132 Set mark at position 2019 10:03:19.132 Position before read 2019 10:03:19.244 Calling limit on byteBuffer with position 3306 10:03:19.245 Before fill(): parsingHeader: [false], parsingRequestLine: [false], parsingRequestLinePhase: [0], parsingRequestLineStart: [0], byteBuffer.position(): [3306], byteBuffer.limit(): [3306], end: [2019] 10:03:19.245 Set mark at position 2019 10:03:19.245 Position before read 2019 10:03:19.854 Encountered exception in read during fill, position=0 j.n.c.AsynchronousCloseException: null at s.n.c.UnixAsynchronousSocketChannelImpl.finishRead(Unknown Source) at s.n.c.UnixAsynchronousSocketChannelImpl.finish(Unknown Source) ... 2 frames excluded at o.apache.tomcat.util.net.Nio2Channel.close(Nio2Channel.java:81) at o.apache.tomcat.util.net.Nio2Channel.close(Nio2Channel.java:94) at o.a.t.u.n.Nio2Endpoint$Nio2SocketWrapper.doClose(Nio2Endpoint.java:935) at o.a.t.util.net.SocketWrapperBase.close(SocketWrapperBase.java:422) at o.a.t.u.n.Nio2Endpoint$SocketProcessor.doRun(Nio2Endpoint.java:1685) at o.a.t.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) at o.a.tomcat.util.net.AbstractEndpoint.processSocket(AbstractEndpoint.java:1171) at o.a.t.u.n.Nio2Endpoint$Nio2SocketWrapper$2.completed(Nio2Endpoint.java:613) at o.a.t.u.n.Nio2Endpoint$Nio2SocketWrapper$2.completed(Nio2Endpoint.java:591) ... 3 frames excluded at o.a.t.u.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) at o.a.t.u.t.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) at o.a.t.u.t.TaskThread$WrappingRunnable.run(TaskThread.java:61) ... 1 frames excluded 10:03:19.854 Calling limit on byteBuffer with position 0 So, the java.nio.channels.AsynchronousCloseException thrown presumably from socketWrapper.read leaves the byteBuffer with position=0. So the limit is set to 0 in the finally block, which sets the mark to -1, as its larger than the limit (it was 2019 according to logs). Here's the snippet for Buffer.limit: public Buffer limit(int newLimit) { if (newLimit > capacity | newLimit < 0) throw createLimitException(newLimit); limit = newLimit; if (position > newLimit) position = newLimit; if (mark > newLimit) mark = -1; return this; } This in turn makes the reset() call fail on the byte buffer as the mark is invalid. I've also observed that the above logs are sometimes missing the "Received" log. I assume that this is swallowed by my logging consumer as either the message is too large or the "raw bytes" make it choke. But I think that shouldn't worry us now! Now the question is: Is this really a bug in Tomcat? Do you agree that the limit(0) call above should not happen? Is it reasonable that a thread interruption could lead to the observed AsynchronousCloseException above? I've tried to reproduce it locally, but currently I can only observe it on a production system. I appreciate hints what could be going on or how one could try to reproduce this within a more isolated environment. Let me know if you need further information. -- You are receiving this mail because: You are the assignee for the bug. --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org