A Bug in a Java Servlet
We have a legacy system, which is a web service, receives HTTP POST from clients, parses the data, then stores them in a file.
The function of the system is simple, and people already done functional and performance test, it's stable. As time drifted away, the system was copy and paste to some projects by only changing the data parsing logic.
I had a similar requirement recently, then I delved into the legacy code to check if it works in order to not reinventing the wheel.
WTF
At first, I noticed below code in a HttpServlet
class,
it allocates more than 1M memory for each HTTP POST request.
1 | private static final long MAX_CONTENT_LENGTH = 1024 * 1024; |
It's insane, I believe the memory should be the same as each HTTP POST body size. Then I changed the code.
1 | int requestContentBufSize = request.getContentLength(); |
Deployed the service and sent one HTTP POST request to it.
curl -d 'Hello, World' http://my.server.com:9000/log
An Exception
occurred.
The BufferOverflowException
After reducing the memory allocated for ByteBuffer
, it
overflows.
1 | java.nio.BufferOverflowException |
I thought I'd better dig into how does the servlet do to make
ByteBuffer
get its data?
- It creates a small buffer occupied
BUFFER_SIZE
(4096) bytes. - It iterates the HTTP request input stream, to put the data into the small buffer.
- It puts the small buffer to
ByteBuffer
and loop back to1
.
Well, in the last loop, the data read from the HTTP request input
stream might smaller than the BUFFER_SIZE
, but the servlet
still puts BUFFER_SIZE
bytes to
ByteBuffer
.
Then, to fix the ExceptionBufferOverflowException
, I
increased the capacity of previous ByteBuffer
by
BUFFER_SIZE
.
1 | int requestContentBufSize = request.getContentLength() + BUFFER_SIZE; |
Deployed again, and
curl -d 'Hello, World' http://my.server.com:9000/log
The bug was fixed.
Did I?
The ServletInputStream
When client posts huge data, what could happen?
I created a String which is 7516 bytes, and sent to server.
curl -d 'very very long string' http://my.server.com:9000/log
Sometimes, the java.nio.BufferOverflowException
occurred, and sometimes it didn't.
What went wrong?
To find the root cause, I added some logs to trace the
ByteBuffer
.
1 | int requestContentBufSize = request.getContentLength() + BUFFER_SIZE; |
The log printed when no exception,
1 | - 1: ByteBuffer position: 0, buffer capacity: 11612, buffer remaining: 11612 |
The log printed when exception occurred,
1 | - 1: ByteBuffer position: 0, buffer capacity: 11612, buffer remaining: 11612 |
Now, it is easy to find out the root cause is in these lines of code.
1 | while ((readBytes = requestInputStream.read(buffer)) > 0) { |
The read
method call won't put data to the
buffer
fully which was specified as 4096 bytes even when
the input stream still has data.
And to fix it, just specify the offset and length of the small
buffer
.
1 | while ((readBytes = requestInputStream.read(buffer)) > 0) { |
I had increased the capacity of the ByteBuffer
by
BUFFER_SIZE
, this change should also be reverted.
Now, the bug is fixed, and this is network programming.
Questions
"The system works a long time, and it shouldn't have this problem or we knew it long ago"
This is because the client seldom posts data more than 4096 bytes to server.
"I have read the Javadoc of DataInputStream
, the
read
method will put data fully to the specified
buffer"
It didn't, please read it again.
"I have tested the read
method of
DataInputStream
on a file, it reads fully 4096 bytes in
every iteration"
This is a web service, deploy it to a server and test.
"I have tested it on my local machine as a web service, and it reads fully 4096 bytes in every iteration"
This is a web service, it should be in a network.
At Last
When a potential bug was reported, we do tests to make it happen again and find the root cause.
We do not stop listening and just look for reasons to reject it.
When we find a bug, we do help others to make it reappear to collect information.
We do not sit there and just blame on others for their mistakes.