On Mon, 9 Feb 2026 08:36:02 GMT, Volkan Yazici <[email protected]> wrote:
>> When a HttpHandler::handle method throws an unexpected exception, the
>> HttpServer rightfully closes the associated connection. However, the
>> exchange is still discounted as pending, which causes HttpServer::stop to
>> wait for the full timeout duration, even though all connections have been
>> closed.
>
> src/jdk.httpserver/share/classes/sun/net/httpserver/ExchangeImpl.java line 91:
>
>> 89:
>> 90: final AtomicBoolean ended = new AtomicBoolean();
>> 91: final AtomicBoolean finished = new AtomicBoolean();
>
> *Nit:* I'd suggest preferring the minimum visibility, that is,
>
> - `ended` and `finished` can be `private`
> - `endExchange`, `postWriteFinished`, and `postExchangeFinished` can be
> package private
Done
> src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java line 440:
>
>> 438:
>> 439: try {
>> 440: if (r instanceof Event.WriteFinished || r instanceof
>> Event.ExchangeFinished) {
>
> This line is the only place in the code where `WriteFinished` and
> `ExchangeFinished` are read. Instead of creating two separate events with a
> big overlap, have you considered changing `WriteFinished` as follows:
>
> static final class ExchangeFinished extends Event {
> ExchangeFinished(ExchangeImpl t, boolean writeFinished) {
> super(Objects.requireNonNull(t));
> if (writeFinished) {
> assert !t.writefinished;
> t.writefinished = true;
> }
> }
> }
Well I had considered modifying WriteFinished and rejected it. I hadn't thought
about replacing it with ExchangeFinished. Thanks for the suggestion.
> src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java line 442:
>
>> 440: if (r instanceof Event.WriteFinished || r instanceof
>> Event.ExchangeFinished) {
>> 441:
>> 442: logger.log(Level.TRACE, "Write Finished");
>
> This log information is not accurate anymore — event can be
> `ExchangeFinished` too. I see a similar log line a couple of lines above
> right after the `r instanceof Event.StopRequested` check. Shall we instead
> remove both and add a `logger.log(Level.TRACE, "Handling {}",
> r.getClass().getSimpleName())` line at the beginning of the
> `handleEvent(Event)` method?
Done
> src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java line 905:
>
>> 903: uc.doFilter(new HttpExchangeImpl(tx));
>> 904: }
>> 905: } catch (Throwable t) {
>
> 1. I'm in favor of catching `Exception` instead of `Throwable` here, since
> the difference between the two are mostly irrecoverable `Error`s.
> 2. A couple of lines below, we have a `catch (Exception e)` block containing
> `closeConnection()`. Why can't we have `tx.postExchangeFinished()` there and
> remove the need for this new `try/catch`?
I would prefer to keep it logically ordered. That is - the ref count is
incremented at the point where we create the exchange, so the try-catch that
ensures it's correctly decremented in case of any exception should begin at the
line after that. We're invoking user code there (the handler) and we really
want to catch things like AssertionError too. These things have a tendency to
be just swallowed and disappear, leaving everything in an undefined state when
they are fired from an executor thread. In addition - we do need to close the
connection before firing the event - otherwise the connection state might get
inconsistent by the time the event is handled, and new assertion errors will be
fired down the line.
> test/jdk/com/sun/net/httpserver/FailAndStopTest.java line 84:
>
>> 82: default -> shouldFail && !"HEAD".equals(method);
>> 83: };
>> 84: }
>
> We have a `shouldFail` flag, but we're introducing further logic to it in
> `shouldFail()` method. I _personally_ find it difficult to wrap my mind
> around it. Can we instead change the type of `shouldFail` from `boolean` to
> `Predicate<String>`? Or simplify this design some other way?
HEAD is special because the body output stream is closed by
sendResponseHeaders. So by the time the exception is thrown the body is already
closed and the exchange is already finished. The expected behaviour is thus
different depending on which HTTP method is used.
> test/jdk/com/sun/net/httpserver/FailAndStopTest.java line 160:
>
>> 158: public static void main(String[] args) throws Exception {
>> 159: LOGGER.setLevel(Level.ALL);
>> 160: Logger.getLogger("").getHandlers()[0].setLevel(Level.ALL);
>
> 1. The `LOGGER` class constant is only used here. Can we move it here as a
> local variable?
> 2. Would you mind documenting these two lines for mere mortals who did not
> author JUL, please? 😅
1. No - it might get GC'ed and the level configuration will be lost.
2. will do :-)
> test/jdk/com/sun/net/httpserver/FailAndStopTest.java line 165:
>
>> 163: test(test, Optional.empty());
>> 164: }
>> 165: // test with HEAD
>
> What is the reason we repeat the test twice, once for `GET`, and once for
> `HEAD`?
As I said, HEAD is special so I wanted to test that path too.
> test/jdk/com/sun/net/httpserver/FailAndStopTest.java line 175:
>
>> 173: new InetSocketAddress(InetAddress.getLoopbackAddress(),
>> 0), 0);
>> 174:
>> 175: System.out.println("Test: " + method.orElse("GET") + " " +
>> test.query);
>
> AFAICT, there are time-sensitive asynchronous processing involved in the
> test. You might consider replacing `println("foo")` lines with `println(now()
> + " foo")` to help with troubleshooting. `now` can be statically imported
> from `java.util.time.Instant`, or you can roll-out your own `log(String
> format, Object... args)` method doing that.
Not sure having a time stamp would help much for diagnosability here.
> test/jdk/com/sun/net/httpserver/FailAndStopTest.java line 178:
>
>> 176: System.out.println("Server listening at: " +
>> server.getAddress());
>> 177: try {
>> 178: server.createContext("/FailAndStopTest/", new
>> FailAndStopTest());
>
> *Nit:* Rename resiliency?
>
> Suggestion:
>
> server.createContext('/' + FailAndStopTest.class.getSimpleName()
> + '/', new FailAndStopTest());
>
>
> Note that `.path("/FailAndStopTest/")` line below needs to be adapted too.
I don't see a reason to use `FailAndStopTest.class.getSimpleName()` rather than
hardcoded "FailAndStopTest". In my experience when using copy/paste on the
test this doesn't get handled by the IDE either way.
> test/jdk/com/sun/net/httpserver/FailAndStopTest.java line 182:
>
>> 180:
>> 181: URL url = URIBuilder.newBuilder()
>> 182: .scheme("http")
>
> Would testing `HttpsServer` (i.e., the `https` scheme) be useful too?
Ah! Interestingly enough there are some discrepencies in behavior with https:
- some SocketException are transformed into IOException
- if there are bytes buffered in the output stream before the connection is
closed, they are sent before closing the connection. This means that there are
two cases where getInputStream().readAllBytes() throws with http but doesn't
with https.
Seems like https was worth testing after all...
> test/jdk/com/sun/net/httpserver/FailAndStopTest.java line 200:
>
>> 198: .formatted(test.query));
>> 199: }
>> 200: System.out.println("Client: read body:
>> \"%s\"".formatted(body));
>
> You might want to simplify `println("...".formatted(...))` with `printf`, in
> particular, if this needs to be backported.
Simplified
-------------
PR Review Comment: https://git.openjdk.org/jdk/pull/29606#discussion_r2783568436
PR Review Comment: https://git.openjdk.org/jdk/pull/29606#discussion_r2782850684
PR Review Comment: https://git.openjdk.org/jdk/pull/29606#discussion_r2783569110
PR Review Comment: https://git.openjdk.org/jdk/pull/29606#discussion_r2781956401
PR Review Comment: https://git.openjdk.org/jdk/pull/29606#discussion_r2781984847
PR Review Comment: https://git.openjdk.org/jdk/pull/29606#discussion_r2781964666
PR Review Comment: https://git.openjdk.org/jdk/pull/29606#discussion_r2781988450
PR Review Comment: https://git.openjdk.org/jdk/pull/29606#discussion_r2782968292
PR Review Comment: https://git.openjdk.org/jdk/pull/29606#discussion_r2783585969
PR Review Comment: https://git.openjdk.org/jdk/pull/29606#discussion_r2783322890
PR Review Comment: https://git.openjdk.org/jdk/pull/29606#discussion_r2783574620