jetty.project icon indicating copy to clipboard operation
jetty.project copied to clipboard

Issue with Response OutputStream#close() rethrowing same EofException instance

Open cdivilly opened this issue 9 months ago • 7 comments

Jetty version(s)

Jetty 12.0.7

Jetty Environment

core

Java version/vendor (use: java -version)

java version "21.0.1" 2023-10-17 LTS Java(TM) SE Runtime Environment (build 21.0.1+12-LTS-29) Java HotSpot(TM) 64-Bit Server VM (build 21.0.1+12-LTS-29, mixed mode, sharing)

OS type/version

ProductName: macOS ProductVersion: 14.4.1 BuildVersion: 23E224

Description

In the case where a client hangs up before a Jetty Handler has written a response, an EofException is raised when attempting to write to the response output stream.

This is expected.

It is best practice to acquire and release the response output stream using with the try-with-resources idiom:

                   public boolean handle(Request request, Response response, Callback callback) {
                        try (final var body = Content.Sink.asOutputStream(response)) {
                            body.write("Hello World".getBytes(StandardCharsets.UTF_8));
                            callback.succeeded();
                        } catch (IOException e) {
                            callback.failed(e);
                        }
                        return true;
                    }

Note that the try with resources block is translated into a regular try block per the Java Language Specification, so when the block completes the OutputStream#close() method will be invoked.

The generated try block has the following form:

{
    final {VariableModifierNoFinal} R Identifier = Expression;
    Throwable #primaryExc = null;

    try ResourceSpecification_tail
        Block
    catch (Throwable #t) {
        #primaryExc = #t;
        throw #t;
    } finally {
        if (Identifier != null) {
            if (#primaryExc != null) {
                try {
                    Identifier.close();
                } catch (Throwable #suppressedExc) {
                    #primaryExc.addSuppressed(#suppressedExc);
                }
            } else {
                Identifier.close();
            }
        }
    }
}
  • Note the line: #primaryExc.addSuppressed(#suppressedExc);

The logic of Throwable#addSuppressed is:

public final synchronized void addSuppressed(Throwable exception) {
        if (exception == this)
            throw new IllegalArgumentException(SELF_SUPPRESSION_MESSAGE, exception);

        Objects.requireNonNull(exception, NULL_CAUSE_MESSAGE);

        if (suppressedExceptions == null) // Suppressed exceptions not recorded
            return;

        if (suppressedExceptions == SUPPRESSED_SENTINEL)
            suppressedExceptions = new ArrayList<>(1);

        suppressedExceptions.add(exception);
    }
  • If the suppressed exception is the same instance as this then raise an IlegalArgumentException (to prevent an exception referring to itself as suppressed)

In Jetty 12 (to the best of my recollection this was not the case in earlier Jetty versions, but have not confirmed this), the OutputStream#close() call rethrows the exact same instance of EofException as was raised when the call to OutputStream#write() was invoked.

This causes the generated try block to end up raising an IllegalArgumentException due to Throwable#addSuppressed() refusing to add itself as a suppressed exception.

Whether intentional or not, the generated try block excludes the possibility that the close() method may throw the exact same exception instance which originally caused the try with resources block to be exited.

I cannot think of a good reason for the close() method to rethrow the same exception instance. I think a failure in the close() method is semantically different to the failure in the write() method and should result in a different exception instance (if any) being thrown.

In this specific case, where the client has hung-up, I think the appropriate behaviour of close() is to complete without raising any error.

How to reproduce?

See the test case below:

package com.example.jetty;

import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.Callback;

import java.io.IOException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public class Main {

    private static void sleep(final int seconds) {
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public static void main(String... args) throws Exception {
        final var server = new Server();
        final var connector = new ServerConnector(server, 1, 1);
        server.addConnector(connector);
        final var context = new ContextHandler("/");

        final var clientResponse = new AtomicReference<CompletableFuture<HttpResponse<String>>>();

        context.setHandler(
                new Handler.Abstract() {

                    @Override
                    public boolean handle(Request request, Response response, Callback callback) {
                        try (final var body = Content.Sink.asOutputStream(response)) {
                            // simulate the client hanging up
                            if (clientResponse.get() != null) {
                                clientResponse.get().cancel(true);
                            }
                            sleep(2);

                            // Raises EofException because client hung up
                            body.write("Hello World".getBytes(StandardCharsets.UTF_8));
                            callback.succeeded();
                        } catch (IOException e) {
                            // Does NOT reach here, because the OutputStream#close()
                            // rethrows same EofException instance, which causes
                            // try with resources block call to: Throwable#addSuppresed() to
                            // fail with java.lang.IllegalArgumentException: Self-suppression not
                            // permitted
                            callback.failed(e);
                        }
                        return true;
                    }
                });
        server.setHandler(context);

        server.start();

        clientResponse.set(
                HttpClient.newHttpClient()
                        .sendAsync(
                                HttpRequest.newBuilder().GET().uri(server.getURI()).build(),
                                HttpResponse.BodyHandlers.ofString()));
        sleep(2);
        server.stop();
    }
}

cdivilly avatar May 02 '24 13:05 cdivilly

This is the output of running the above test case:

[main] INFO org.eclipse.jetty.server.Server - jetty-12.0.7; built: 2024-02-29T21:19:41.771Z; git: c89aca8fd34083befd79f328a3b8b6ffff04347e; jvm 21.0.1+12-LTS-29
[main] INFO org.eclipse.jetty.server.handler.ContextHandler - Started oejsh.ContextHandler@cb644e{ROOT,/,b=null,a=AVAILABLE,h=cej.Main$@56ef9176{STARTED}}
[main] INFO org.eclipse.jetty.server.AbstractConnector - Started ServerConnector@56235b8e{HTTP/1.1, (http/1.1)}{0.0.0.0:57211}
[main] INFO org.eclipse.jetty.server.Server - Started oejs.Server@7921b0a2{STARTING}[12.0.7,sto=0] @186ms
[main] INFO org.eclipse.jetty.server.Server - Stopped oejs.Server@7921b0a2{STOPPING}[12.0.7,sto=0]
[main] INFO org.eclipse.jetty.server.AbstractConnector - Stopped ServerConnector@56235b8e{HTTP/1.1, (http/1.1)}{0.0.0.0:0}
[qtp1282788025-25] WARN org.eclipse.jetty.server.Response - writeError: status=500, message=java.lang.IllegalArgumentException: Self-suppression not permitted, response=org.eclipse.jetty.server.handler.ContextResponse@7ad5cbae
java.lang.IllegalArgumentException: Self-suppression not permitted
	at java.base/java.lang.Throwable.addSuppressed(Throwable.java:1096)
	at com.example.jetty.rethrown.exception.issue/com.example.jetty.Main$1.handle(Main.java:44)
	at [email protected]/org.eclipse.jetty.server.handler.ContextHandler.handle(ContextHandler.java:765)
	at [email protected]/org.eclipse.jetty.server.Server.handle(Server.java:179)
	at [email protected]/org.eclipse.jetty.server.internal.HttpChannelState$HandlerInvoker.run(HttpChannelState.java:619)
	at [email protected]/org.eclipse.jetty.server.internal.HttpConnection.onFillable(HttpConnection.java:411)
	at [email protected]/org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:322)
	at [email protected]/org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:99)
	at [email protected]/org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)
	at [email protected]/org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:971)
	at [email protected]/org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1201)
	at [email protected]/org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1156)
	at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: org.eclipse.jetty.io.EofException
	at [email protected]/org.eclipse.jetty.server.internal.HttpConnection$SendCallback.reset(HttpConnection.java:739)
	at [email protected]/org.eclipse.jetty.server.internal.HttpConnection$HttpStreamOverHTTP1.send(HttpConnection.java:1418)
	at [email protected]/org.eclipse.jetty.server.internal.HttpChannelState$ChannelResponse.write(HttpChannelState.java:1246)
	at [email protected]/org.eclipse.jetty.server.Response$Wrapper.write(Response.java:751)
	at [email protected]/org.eclipse.jetty.server.handler.ContextResponse.write(ContextResponse.java:56)
	at [email protected]/org.eclipse.jetty.io.content.ContentSinkOutputStream.write(ContentSinkOutputStream.java:53)
	at java.base/java.io.OutputStream.write(OutputStream.java:124)
	at com.example.jetty.rethrown.exception.issue/com.example.jetty.Main$1.handle(Main.java:52)
	... 11 more
[qtp1282788025-25] WARN org.eclipse.jetty.server.Response - writeError: status=500, message=java.lang.IllegalArgumentException: Self-suppression not permitted, response=ErrorResponse@7481e31b{500,GET@0 http://10.20.1.40:57211/ HTTP/1.1}
java.lang.IllegalArgumentException: Self-suppression not permitted
	at java.base/java.lang.Throwable.addSuppressed(Throwable.java:1096)
	at com.example.jetty.rethrown.exception.issue/com.example.jetty.Main$1.handle(Main.java:44)
	at [email protected]/org.eclipse.jetty.server.handler.ContextHandler.handle(ContextHandler.java:765)
	at [email protected]/org.eclipse.jetty.server.Server.handle(Server.java:179)
	at [email protected]/org.eclipse.jetty.server.internal.HttpChannelState$HandlerInvoker.run(HttpChannelState.java:619)
	at [email protected]/org.eclipse.jetty.server.internal.HttpConnection.onFillable(HttpConnection.java:411)
	at [email protected]/org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:322)
	at [email protected]/org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:99)
	at [email protected]/org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)
	at [email protected]/org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:971)
	at [email protected]/org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1201)
	at [email protected]/org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1156)
	at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: org.eclipse.jetty.io.EofException
	at [email protected]/org.eclipse.jetty.server.internal.HttpConnection$SendCallback.reset(HttpConnection.java:739)
	at [email protected]/org.eclipse.jetty.server.internal.HttpConnection$HttpStreamOverHTTP1.send(HttpConnection.java:1418)
	at [email protected]/org.eclipse.jetty.server.internal.HttpChannelState$ChannelResponse.write(HttpChannelState.java:1246)
	at [email protected]/org.eclipse.jetty.server.Response$Wrapper.write(Response.java:751)
	at [email protected]/org.eclipse.jetty.server.handler.ContextResponse.write(ContextResponse.java:56)
	at [email protected]/org.eclipse.jetty.io.content.ContentSinkOutputStream.write(ContentSinkOutputStream.java:53)
	at java.base/java.io.OutputStream.write(OutputStream.java:124)
	at com.example.jetty.rethrown.exception.issue/com.example.jetty.Main$1.handle(Main.java:52)
	... 11 more

cdivilly avatar May 02 '24 14:05 cdivilly

This seems to overlap with another issue that is currently being worked on.

  • #11679

joakime avatar May 02 '24 15:05 joakime

I've reviewed that issue, I'm not sure I see an overlap other than they both relate to EOFException conditions, I think this may be a distinct scenario.

cdivilly avatar May 02 '24 15:05 cdivilly

While the initial description of the issue isn't the same, the root cause, and the fix, seem to be the same.

See PR

  • #11719

joakime avatar May 02 '24 15:05 joakime

I cloned fix/jetty-12/11679/early-eof-jersey-leak and added the following unit test (had to hack in requires java.net.http to org.eclipse.jetty.server's module-info.java):

package org.eclipse.jetty.server;

import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.Callback;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public class EofDuringResponseWriteTest {

    private static void sleep(final int seconds) {
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    @Test
    public void testClientAbortsRequest() throws Exception {
        final var server = new Server();
        final var connector = new ServerConnector(server, 1, 1);
        server.addConnector(connector);
        final var context = new ContextHandler("/");

        final var clientResponse = new AtomicReference<CompletableFuture<HttpResponse<String>>>();

        context.setHandler(
                new Handler.Abstract() {

                    @Override
                    public boolean handle(Request request, Response response, Callback callback) {
                        try (final var body = Content.Sink.asOutputStream(response)) {
                            // simulate the client hanging up
                            if (clientResponse.get() != null) {
                                clientResponse.get().cancel(true);
                            }
                            sleep(2);

                            // Raises EofException because client hung up
                            body.write("Hello World".getBytes(StandardCharsets.UTF_8));
                            callback.succeeded();
                        } catch (IOException e) {
                            // Does NOT reach here, because the OutputStream#close()
                            // rethrows same EofException instance, which causes
                            // try with resources block call to: Throwable#addSuppresed() to
                            // fail with java.lang.IllegalArgumentException: Self-suppression not
                            // permitted
                            callback.failed(e);
                        }
                        return true;
                    }
                });
        server.setHandler(context);

        server.start();

        clientResponse.set(
                HttpClient.newHttpClient()
                        .sendAsync(
                                HttpRequest.newBuilder().GET().uri(server.getURI()).build(),
                                HttpResponse.BodyHandlers.ofString()));
        sleep(2);
        server.stop();
    }
}

This is the output of running the test:

[2024-05-03T11:13:45.284+0100][info][gc] Using G1
WARNING: Unknown module: org.eclipse.jetty.logging specified to --add-reads
java version "17.0.5" 2022-10-18 LTS
Java(TM) SE Runtime Environment (build 17.0.5+9-LTS-191)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.5+9-LTS-191, mixed mode)
2024-05-03 11:13:45.707:INFO :oejs.Server:main: jetty-12.0.9-SNAPSHOT; built: unknown; git: ; jvm 17.0.5+9-LTS-191
2024-05-03 11:13:45.725:INFO :oejsh.ContextHandler:main: Started oejsh.ContextHandler@3e27aa33{ROOT,/,b=null,a=AVAILABLE,h=oejs.EofDuringResponseWriteTest$@2ddc9a9f{STARTED}}
2024-05-03 11:13:45.733:INFO :oejs.AbstractConnector:main: Started ServerConnector@7674f035{HTTP/1.1, (http/1.1)}{0.0.0.0:51255}
2024-05-03 11:13:45.738:INFO :oejs.Server:main: Started oejs.Server@6392827e{STARTING}[12.0.9-SNAPSHOT,sto=0] @459ms
[2024-05-03T11:13:46.052+0100][info][gc] GC(0) Pause Young (Concurrent Start) (Metadata GC Threshold) 62M->6M(4096M) 4.872ms
[2024-05-03T11:13:46.052+0100][info][gc] GC(1) Concurrent Mark Cycle
[2024-05-03T11:13:46.055+0100][info][gc] GC(1) Pause Remark 8M->8M(4096M) 0.765ms
[2024-05-03T11:13:46.055+0100][info][gc] GC(1) Pause Cleanup 8M->8M(4096M) 0.002ms
[2024-05-03T11:13:46.061+0100][info][gc] GC(1) Concurrent Mark Cycle 8.248ms
2024-05-03 11:13:48.066:INFO :oejs.Server:main: Stopped oejs.Server@6392827e{STOPPING}[12.0.9-SNAPSHOT,sto=0]
2024-05-03 11:13:48.075:INFO :oejs.AbstractConnector:main: Stopped ServerConnector@7674f035{HTTP/1.1, (http/1.1)}{0.0.0.0:0}
2024-05-03 11:13:48.117:WARN :oejs.Response:qtp341138954-20: writeError: status=500, message=java.lang.IllegalArgumentException: Self-suppression not permitted, response=org.eclipse.jetty.server.handler.ContextResponse@89800d3
java.lang.IllegalArgumentException: Self-suppression not permitted
	at java.base/java.lang.Throwable.addSuppressed(Throwable.java:1072)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.EofDuringResponseWriteTest$1.handle(EofDuringResponseWriteTest.java:41)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.handler.ContextHandler.handle(ContextHandler.java:851)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.Server.handle(Server.java:179)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.internal.HttpChannelState$HandlerInvoker.run(HttpChannelState.java:635)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.internal.HttpConnection.onFillable(HttpConnection.java:411)
	at org.eclipse.jetty.io/org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:322)
	at org.eclipse.jetty.io/org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:99)
	at org.eclipse.jetty.io/org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)
	at org.eclipse.jetty.util/org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:979)
	at org.eclipse.jetty.util/org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1209)
	at org.eclipse.jetty.util/org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1164)
	at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: 
org.eclipse.jetty.io.EofException
	at org.eclipse.jetty.server/org.eclipse.jetty.server.internal.HttpConnection$SendCallback.reset(HttpConnection.java:748)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.internal.HttpConnection$HttpStreamOverHTTP1.send(HttpConnection.java:1428)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.internal.HttpChannelState$ChannelResponse.write(HttpChannelState.java:1263)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.Response$Wrapper.write(Response.java:751)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.handler.ContextResponse.write(ContextResponse.java:56)
	at org.eclipse.jetty.io/org.eclipse.jetty.io.content.ContentSinkOutputStream.write(ContentSinkOutputStream.java:53)
	at java.base/java.io.OutputStream.write(OutputStream.java:127)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.EofDuringResponseWriteTest$1.handle(EofDuringResponseWriteTest.java:49)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.handler.ContextHandler.handle(ContextHandler.java:851)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.Server.handle(Server.java:179)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.internal.HttpChannelState$HandlerInvoker.run(HttpChannelState.java:635)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.internal.HttpConnection.onFillable(HttpConnection.java:411)
	at org.eclipse.jetty.io/org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:322)
	at org.eclipse.jetty.io/org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:99)
	at org.eclipse.jetty.io/org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)
	at org.eclipse.jetty.util/org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:979)
	at org.eclipse.jetty.util/org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1209)
	at org.eclipse.jetty.util/org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1164)
	at java.base/java.lang.Thread.run(Thread.java:833)
2024-05-03 11:13:48.120:WARN :oejs.Response:qtp341138954-20: writeError: status=500, message=java.lang.IllegalArgumentException: Self-suppression not permitted, response=ErrorResponse@77e4e62a{500,GET@0 http://10.23.93.246:51255/ HTTP/1.1}
java.lang.IllegalArgumentException: Self-suppression not permitted
	at java.base/java.lang.Throwable.addSuppressed(Throwable.java:1072)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.EofDuringResponseWriteTest$1.handle(EofDuringResponseWriteTest.java:41)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.handler.ContextHandler.handle(ContextHandler.java:851)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.Server.handle(Server.java:179)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.internal.HttpChannelState$HandlerInvoker.run(HttpChannelState.java:635)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.internal.HttpConnection.onFillable(HttpConnection.java:411)
	at org.eclipse.jetty.io/org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:322)
	at org.eclipse.jetty.io/org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:99)
	at org.eclipse.jetty.io/org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)
	at org.eclipse.jetty.util/org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:979)
	at org.eclipse.jetty.util/org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1209)
	at org.eclipse.jetty.util/org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1164)
	at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: 
org.eclipse.jetty.io.EofException
	at org.eclipse.jetty.server/org.eclipse.jetty.server.internal.HttpConnection$SendCallback.reset(HttpConnection.java:748)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.internal.HttpConnection$HttpStreamOverHTTP1.send(HttpConnection.java:1428)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.internal.HttpChannelState$ChannelResponse.write(HttpChannelState.java:1263)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.Response$Wrapper.write(Response.java:751)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.handler.ContextResponse.write(ContextResponse.java:56)
	at org.eclipse.jetty.io/org.eclipse.jetty.io.content.ContentSinkOutputStream.write(ContentSinkOutputStream.java:53)
	at java.base/java.io.OutputStream.write(OutputStream.java:127)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.EofDuringResponseWriteTest$1.handle(EofDuringResponseWriteTest.java:49)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.handler.ContextHandler.handle(ContextHandler.java:851)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.Server.handle(Server.java:179)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.internal.HttpChannelState$HandlerInvoker.run(HttpChannelState.java:635)
	at org.eclipse.jetty.server/org.eclipse.jetty.server.internal.HttpConnection.onFillable(HttpConnection.java:411)
	at org.eclipse.jetty.io/org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:322)
	at org.eclipse.jetty.io/org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:99)
	at org.eclipse.jetty.io/org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)
	at org.eclipse.jetty.util/org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:979)
	at org.eclipse.jetty.util/org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1209)
	at org.eclipse.jetty.util/org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1164)
	at java.base/java.lang.Thread.run(Thread.java:833)

In other words the issue reported above continues to occur, even with the changes in fix/jetty-12/11679/early-eof-jersey-leak

cdivilly avatar May 03 '24 10:05 cdivilly

I think the core issue is that ContentSinkOutputStream#close() blocks unconditionally. I think this should be conditional, it should only block if the Blocker.Shared._callback is not in a failed state.

I think ContentSinkOutputStream#flush() may have a similar issue, it should only attempt to flush if there has not been a prior failure

cdivilly avatar May 03 '24 12:05 cdivilly

I don't think I agree with your statement that ContentSinkOutputStream#close() and ContentSinkOutputStream#flush() should block conditionally.

But it seems that you have found a real issue with our implementation of ContentSinkOutputStream: once it has failed because of an exception, it will throw that same exception for all other method calls, and that is clearly a problem when using try-with-resource blocks.

lorban avatar May 03 '24 12:05 lorban