Skip to content

HttpInput#read rethrows already thrown exception, leading to possible self-suppressing exception issue #13613

@willemv

Description

@willemv

Jetty version(s)

  • 12.0.27
  • 12.1.1

Jetty Environment

  • ee10

Java version/vendor (use: java -version)
Not relevant, but reproduced on:

openjdk version "21.0.4" 2024-07-16 LTS
OpenJDK Runtime Environment Zulu21.36+17-CA (build 21.0.4+7-LTS)
OpenJDK 64-Bit Server VM Zulu21.36+17-CA (build 21.0.4+7-LTS, mixed mode, sharing)

OS type/version
Not relevant, but reproduced on
macOS 15.6.1

Description

HttpInput#read can rethrow the same exception if the current chunk is in a failed state. This can happen, for example, when the client hung up, and there was an early EOF.

In itself this seems fine, but when the HttpInput is wrapped to deal with, for example, multipart uploads it is easy to run into a situation where the close on the wrapper needs to read the underlying input stream to skip to the next part of the multipart upload, leading to self-suppression issues with the try-with-resources construct.

Seems related to #12029 and #11736

How to reproduce?

When you have a servlet with this sort of doPost implementation, you can run into the issue by starting an upload to the POST endpoint, and interrupting the upload before it finishes

This example servlet uses apache commons file-upload. This is important because it is this library that wraps the HttpInput of jetty with an implementation fit for multipart processing.

public class UploadServlet extends HttpServlet {

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/html");

        // Hello
        PrintWriter out = response.getWriter();
        out.println("<html><body>");
        out.println("<h1>Hello World</h1>");
        out.println("</body></html>");
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // use this with, for example, curl:
        // curl -v -F upload=@large_file.bin http://localhost:8080/upload
        try {
            if (JakartaServletFileUpload.isMultipartContent(request)) {
                JakartaServletFileUpload<?, ?> upload = new JakartaServletFileUpload<>();
                for (FileItemInputIterator it = upload.getItemIterator(request); it.hasNext(); ) {
                    var item = it.next();
                    if (!item.isFormField()) {
                        byte[] buffer = new byte[1024];
                        try (var is = item.getInputStream()) {
                            long total = 0;
                            int read;
                            while ((read = is.read(buffer)) != -1) {
                                total += read;
                            }
                            response.setHeader("X-Content-Length-Echo", Long.toString(total));
                            System.out.println("total read: " + total);
                        }
                    }
                }
                response.sendRedirect("/upload");
            }
        } catch (IOException e) {
            System.err.println("Regular IO Exception: " + e.getMessage());
            throw e;
        } catch (RuntimeException e) {
            // Because of the try-with-resources usage of the input stream,
            // an IllegalArgumentException is thrown regarding self-suppressing
            // exceptions.
            System.err.println("Unexpected error occurred. " + e.getMessage());
            e.printStackTrace();
        }
    }
}

You can find a ready-to-run maven project here: https://github.com/datadobi/jetty-self-suppression-issue

  • import the project in an IDE supporting maven
  • start the Main class in that project
  • use cURL to upload a big file : curl -v -F upload=@large_file.bin http://localhost:8080/upload
  • interrupt the cURL upload (using CTRL-C, for example) before the upload completes

This will result in the following output on the console of the Main application:

Unexpected error occurred. Self-suppression not permitted
java.lang.IllegalArgumentException: Self-suppression not permitted
	at java.base/java.lang.Throwable.addSuppressed(Throwable.java:1096)
	at com.datadobi.bugreports.uploaddemo.UploadServlet.doPost(UploadServlet.java:36)
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:653)
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:723)
	at org.eclipse.jetty.ee10.servlet.ServletHolder.handle(ServletHolder.java:751)
	at org.eclipse.jetty.ee10.servlet.ServletHandler$ChainEnd.doFilter(ServletHandler.java:1622)
	at org.eclipse.jetty.ee10.servlet.ServletHandler$MappedServlet.handle(ServletHandler.java:1555)
	at org.eclipse.jetty.ee10.servlet.ServletChannel.dispatch(ServletChannel.java:823)
	at org.eclipse.jetty.ee10.servlet.ServletChannel.handle(ServletChannel.java:440)
	at org.eclipse.jetty.ee10.servlet.ServletHandler.handle(ServletHandler.java:470)
	at org.eclipse.jetty.server.handler.ContextHandler.handle(ContextHandler.java:1071)
	at org.eclipse.jetty.server.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:151)
	at org.eclipse.jetty.server.Server.handle(Server.java:182)
	at org.eclipse.jetty.server.internal.HttpChannelState$HandlerInvoker.run(HttpChannelState.java:677)
	at org.eclipse.jetty.server.internal.HttpConnection.onFillable(HttpConnection.java:416)
	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:322)
	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:105)
	at org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:981)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1211)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1166)
	at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: org.eclipse.jetty.server.internal.HttpConnection$HttpEofException: Early EOF
	at org.eclipse.jetty.server.internal.HttpConnection$RequestHandler.earlyEOF(HttpConnection.java:1080)
	at org.eclipse.jetty.http.HttpParser.parseNext(HttpParser.java:1753)
	at org.eclipse.jetty.server.internal.HttpConnection.parseAndFillForContent(HttpConnection.java:539)
	at org.eclipse.jetty.server.internal.HttpConnection$HttpStreamOverHTTP1.read(HttpConnection.java:1371)
	at org.eclipse.jetty.server.HttpStream$Wrapper.read(HttpStream.java:167)
	at org.eclipse.jetty.server.internal.HttpChannelState$ChannelRequest.read(HttpChannelState.java:948)
	at org.eclipse.jetty.server.Request$Wrapper.read(Request.java:919)
	at org.eclipse.jetty.ee10.servlet.AsyncContentProducer.readChunk(AsyncContentProducer.java:324)
	at org.eclipse.jetty.ee10.servlet.AsyncContentProducer.produceChunk(AsyncContentProducer.java:304)
	at org.eclipse.jetty.ee10.servlet.AsyncContentProducer.nextChunk(AsyncContentProducer.java:208)
	at org.eclipse.jetty.ee10.servlet.BlockingContentProducer.nextChunk(BlockingContentProducer.java:100)
	at org.eclipse.jetty.ee10.servlet.HttpInput.read(HttpInput.java:259)
	at org.eclipse.jetty.ee10.servlet.HttpInput.read(HttpInput.java:240)
	at org.apache.commons.fileupload2.core.MultipartInput$ItemInputStream.makeAvailable(MultipartInput.java:356)
	at org.apache.commons.fileupload2.core.MultipartInput$ItemInputStream.read(MultipartInput.java:415)
	at java.base/java.io.InputStream.read(InputStream.java:220)
	at com.datadobi.bugreports.uploaddemo.UploadServlet.doPost(UploadServlet.java:39)
	... 20 more

Metadata

Metadata

Assignees

Labels

BugFor general bugs on Jetty side

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions