diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/ChunkedOutputStream.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/ChunkedOutputStream.java index fd1a940c0a5..0af59f1d1a7 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/ChunkedOutputStream.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/ChunkedOutputStream.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -154,9 +154,7 @@ class ChunkedOutputStream extends FilterOutputStream } finally { closed = true; } - - Event e = new Event.WriteFinished(t); - t.getHttpContext().getServerImpl().addEvent(e); + t.postExchangeFinished(true); } public void flush() throws IOException { diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/Event.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/Event.java index 18c2535492b..41ce83596b7 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/Event.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/Event.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -47,13 +47,15 @@ abstract sealed class Event { } /** - * Event indicating that writing is finished for a given exchange. + * Event indicating that the exchange is finished, + * without having necessarily read the complete + * request or sent the complete response. + * Typically, this event is posted when invoking + * the filter chain throws an exception. */ - static final class WriteFinished extends Event { - WriteFinished(ExchangeImpl t) { + static final class ExchangeFinished extends Event { + ExchangeFinished(ExchangeImpl t) { super(Objects.requireNonNull(t)); - assert !t.writefinished; - t.writefinished = true; } } } diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/ExchangeImpl.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/ExchangeImpl.java index ad6805938a2..57296842db2 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/ExchangeImpl.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/ExchangeImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -32,10 +32,10 @@ import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.lang.System.Logger; import java.lang.System.Logger.Level; -import java.text.*; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Stream; import com.sun.net.httpserver.*; import static com.sun.net.httpserver.HttpExchange.RSPBODY_EMPTY; @@ -46,7 +46,7 @@ class ExchangeImpl { Headers reqHdrs, rspHdrs; Request req; String method; - boolean writefinished; + private boolean writefinished; URI uri; HttpConnection connection; long reqContentLen; @@ -87,6 +87,19 @@ class ExchangeImpl { HttpPrincipal principal; ServerImpl server; + // Used to control that ServerImpl::endExchange is called + // exactly once for this exchange. ServerImpl::endExchange decrements + // the refcount that was incremented by calling ServerImpl::startExchange + // in this ExchangeImpl constructor. + private final AtomicBoolean ended = new AtomicBoolean(); + + // Used to ensure that the Event.ExchangeFinished is posted only + // once for this exchange. The Event.ExchangeFinished is what will + // eventually cause the ServerImpl::finishedLatch to be triggered, + // once the number of active exchanges reaches 0 and ServerImpl::stop + // has been requested. + private final AtomicBoolean finished = new AtomicBoolean(); + ExchangeImpl( String m, URI u, Request req, long len, HttpConnection connection ) throws IOException { @@ -107,6 +120,55 @@ class ExchangeImpl { server.startExchange(); } + /** + * When true, writefinished indicates that all bytes expected + * by the client have been written to the response body + * outputstream, and that the response body outputstream has + * been closed. When all bytes have also been pulled from + * the request body input stream, this makes it possible to + * reuse the connection for the next request. + */ + synchronized boolean writefinished() { + return writefinished; + } + + /** + * Calls ServerImpl::endExchange if not already called for this + * exchange. ServerImpl::endExchange must be called exactly once + * per exchange, and this method ensures that it is not called + * more than once for this exchange. + * @return the new (or current) value of the exchange count. + */ + int endExchange() { + // only call server.endExchange(); once per exchange + if (ended.compareAndSet(false, true)) { + return server.endExchange(); + } + return server.getExchangeCount(); + } + + /** + * Posts the ExchangeFinished event if not already posted. + * If `writefinished` is true, marks the exchange as {@link + * #writefinished()} so that the connection can be reused. + * @param writefinished whether all bytes expected by the + * client have been writen out to the + * response body output stream. + */ + void postExchangeFinished(boolean writefinished) { + // only post ExchangeFinished once per exchange + if (finished.compareAndSet(false, true)) { + if (writefinished) { + synchronized (this) { + assert this.writefinished == false; + this.writefinished = true; + } + } + Event e = new Event.ExchangeFinished(this); + getHttpContext().getServerImpl().addEvent(e); + } + } + public Headers getRequestHeaders() { return reqHdrs; } @@ -140,7 +202,7 @@ class ExchangeImpl { /* close the underlying connection if, * a) the streams not set up yet, no response can be sent, or * b) if the wrapper output stream is not set up, or - * c) if the close of the input/outpu stream fails + * c) if the close of the input/output stream fails */ try { if (uis_orig == null || uos == null) { @@ -157,6 +219,8 @@ class ExchangeImpl { uos.close(); } catch (IOException e) { connection.close(); + } finally { + postExchangeFinished(false); } } diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/FixedLengthOutputStream.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/FixedLengthOutputStream.java index 95de03d27fa..e9dbd93bb12 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/FixedLengthOutputStream.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/FixedLengthOutputStream.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -94,8 +94,7 @@ class FixedLengthOutputStream extends FilterOutputStream is.close(); } catch (IOException e) {} } - Event e = new Event.WriteFinished(t); - t.getHttpContext().getServerImpl().addEvent(e); + t.postExchangeFinished(true); } // flush is a pass-through diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java index e8c8d336e03..94fe78b9c64 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -419,7 +419,8 @@ class ServerImpl { // Stopping marking the state as finished if stop is requested, // termination is in progress and exchange count is 0 if (r instanceof Event.StopRequested) { - logger.log(Level.TRACE, "Handling Stop Requested Event"); + logger.log(Level.TRACE, "Handling {0} event", + r.getClass().getSimpleName()); // checking if terminating is set to true final boolean terminatingCopy = terminating; @@ -437,10 +438,11 @@ class ServerImpl { HttpConnection c = t.getConnection(); try { - if (r instanceof Event.WriteFinished) { + if (r instanceof Event.ExchangeFinished) { - logger.log(Level.TRACE, "Write Finished"); - int exchanges = endExchange(); + logger.log(Level.TRACE, "Handling {0} event", + r.getClass().getSimpleName()); + int exchanges = t.endExchange(); if (terminating && exchanges == 0 && reqConnections.isEmpty()) { finishedLatch.countDown(); } @@ -842,68 +844,77 @@ class ServerImpl { tx = new ExchangeImpl( method, uri, req, clen, connection ); - String chdr = headers.getFirst("Connection"); - Headers rheaders = tx.getResponseHeaders(); + try { - if (chdr != null && chdr.equalsIgnoreCase("close")) { - tx.close = true; - } - if (version.equalsIgnoreCase("http/1.0")) { - tx.http10 = true; - if (chdr == null) { + String chdr = headers.getFirst("Connection"); + Headers rheaders = tx.getResponseHeaders(); + + if (chdr != null && chdr.equalsIgnoreCase("close")) { tx.close = true; - rheaders.set("Connection", "close"); - } else if (chdr.equalsIgnoreCase("keep-alive")) { - rheaders.set("Connection", "keep-alive"); - int idleSeconds = (int) (ServerConfig.getIdleIntervalMillis() / 1000); - String val = "timeout=" + idleSeconds; - rheaders.set("Keep-Alive", val); } - } + if (version.equalsIgnoreCase("http/1.0")) { + tx.http10 = true; + if (chdr == null) { + tx.close = true; + rheaders.set("Connection", "close"); + } else if (chdr.equalsIgnoreCase("keep-alive")) { + rheaders.set("Connection", "keep-alive"); + int idleSeconds = (int) (ServerConfig.getIdleIntervalMillis() / 1000); + String val = "timeout=" + idleSeconds; + rheaders.set("Keep-Alive", val); + } + } - if (newconnection) { - connection.setParameters( - rawin, rawout, chan, engine, sslStreams, - sslContext, protocol, ctx, rawin - ); - } - /* check if client sent an Expect 100 Continue. - * In that case, need to send an interim response. - * In future API may be modified to allow app to - * be involved in this process. - */ - String exp = headers.getFirst("Expect"); - if (exp != null && exp.equalsIgnoreCase("100-continue")) { - logReply(100, requestLine, null); - sendReply( - Code.HTTP_CONTINUE, false, null - ); - } - /* uf is the list of filters seen/set by the user. - * sf is the list of filters established internally - * and which are not visible to the user. uc and sc - * are the corresponding Filter.Chains. - * They are linked together by a LinkHandler - * so that they can both be invoked in one call. - */ - final List sf = ctx.getSystemFilters(); - final List uf = ctx.getFilters(); + if (newconnection) { + connection.setParameters( + rawin, rawout, chan, engine, sslStreams, + sslContext, protocol, ctx, rawin + ); + } + /* check if client sent an Expect 100 Continue. + * In that case, need to send an interim response. + * In future API may be modified to allow app to + * be involved in this process. + */ + String exp = headers.getFirst("Expect"); + if (exp != null && exp.equalsIgnoreCase("100-continue")) { + logReply(100, requestLine, null); + sendReply( + Code.HTTP_CONTINUE, false, null + ); + } + /* uf is the list of filters seen/set by the user. + * sf is the list of filters established internally + * and which are not visible to the user. uc and sc + * are the corresponding Filter.Chains. + * They are linked together by a LinkHandler + * so that they can both be invoked in one call. + */ + final List sf = ctx.getSystemFilters(); + final List uf = ctx.getFilters(); - final Filter.Chain sc = new Filter.Chain(sf, ctx.getHandler()); - final Filter.Chain uc = new Filter.Chain(uf, new LinkHandler(sc)); + final Filter.Chain sc = new Filter.Chain(sf, ctx.getHandler()); + final Filter.Chain uc = new Filter.Chain(uf, new LinkHandler(sc)); - /* set up the two stream references */ - tx.getRequestBody(); - tx.getResponseBody(); - if (https) { - uc.doFilter(new HttpsExchangeImpl(tx)); - } else { - uc.doFilter(new HttpExchangeImpl(tx)); + /* set up the two stream references */ + tx.getRequestBody(); + tx.getResponseBody(); + if (https) { + uc.doFilter(new HttpsExchangeImpl(tx)); + } else { + uc.doFilter(new HttpExchangeImpl(tx)); + } + } catch (Throwable t) { + // release the exchange. + logger.log(Level.TRACE, "ServerImpl.Exchange", t); + if (!tx.writefinished()) { + closeConnection(connection); + } + tx.postExchangeFinished(false); } - } catch (Exception e) { logger.log(Level.TRACE, "ServerImpl.Exchange", e); - if (tx == null || !tx.writefinished) { + if (tx == null || !tx.writefinished()) { closeConnection(connection); } } catch (Throwable t) { diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/UndefLengthOutputStream.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/UndefLengthOutputStream.java index ecda32ecc31..9e1f949321a 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/UndefLengthOutputStream.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/UndefLengthOutputStream.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2007, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -75,8 +75,7 @@ class UndefLengthOutputStream extends FilterOutputStream is.close(); } catch (IOException e) {} } - Event e = new Event.WriteFinished(t); - t.getHttpContext().getServerImpl().addEvent(e); + t.postExchangeFinished(true); } // flush is a pass-through diff --git a/test/jdk/com/sun/net/httpserver/FailAndStopTest.java b/test/jdk/com/sun/net/httpserver/FailAndStopTest.java new file mode 100644 index 00000000000..005d78add18 --- /dev/null +++ b/test/jdk/com/sun/net/httpserver/FailAndStopTest.java @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * @test + * @bug 8377302 + * @summary HttpServer.stop() blocks indefinitely if handler throws + * @modules jdk.httpserver java.logging + * @library /test/lib + * @run main/othervm ${test.main.class} + */ + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.function.BiPredicate; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpsConfigurator; +import com.sun.net.httpserver.HttpsServer; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; +import jdk.test.lib.Utils; +import static com.sun.net.httpserver.HttpExchange.RSPBODY_CHUNKED; + +public class FailAndStopTest implements HttpHandler { + // Keep that logger in a static field to make sure it doesn't + // get GC'ed and recreated before the HttpServer is initialized. + private static final Logger LOGGER = Logger.getLogger("com.sun.net.httpserver"); + private static final String BODY = "OK"; + + static enum TestCases { + FAILNOW("failNow", TestCases::shouldAlwaysFail), + ASSERTNOW("assertNow", TestCases::shouldAlwaysFail), + RESPANDFAIL("failAfterResponseStatus", TestCases::shouldFailExceptForHead), + RESPANDASSERT("assertAfterResponseStatus", TestCases::shouldFailExceptForHead), + CLOSEAFTERRESP("closeExchangeAfterResponseStatus", TestCases::shouldFailExceptForHead), + BODYANDFAIL("failAfterBody", TestCases::shouldFailExceptForHeadOrHttps), + BODYANDASSERT("assertAfterBody", TestCases::shouldFailExceptForHeadOrHttps), + CLOSEBEFOREOS("closeExchangeBeforeOS", TestCases::shouldNeverFail), + CLOSEANDRETURN("closeAndReturn", TestCases::shouldNeverFail), + CLOSEANDFAIL("closeAndFail", TestCases::shouldNeverFail), + CLOSEANDASSERT("closeAndAssert", TestCases::shouldNeverFail); + + private final String query; + private BiPredicate shouldFail; + TestCases(String query, BiPredicate shouldFail) { + this.query = query; + this.shouldFail = shouldFail; + } + boolean shouldFail(String scheme, String method) { + // in case of HEAD method the client should not + // fail if we throw after sending response headers + return shouldFail.test(scheme, method); + } + private static boolean shouldAlwaysFail(String scheme, String method) { + return true; + } + private static boolean shouldNeverFail(String scheme, String method) { + return false; + } + private static boolean shouldFailExceptForHead(String scheme, String method) { + return !"HEAD".equals(method); + } + private static boolean shouldFailExceptForHeadOrHttps(String scheme, String method) { + // When using https, the buffered response bytes may be sent + // when the connection is closed, in which case the full body + // will be correctly received, and the client connection + // will not fail. With plain http, the bytes are not sent and + // the client fails with premature end of file. + return !"HEAD".equals(method) && !"https".equalsIgnoreCase(scheme); + } + + } + + @Override + public void handle(HttpExchange ex) throws IOException { + String query = ex.getRequestURI().getRawQuery(); + TestCases step = TestCases.FAILNOW; + if (query == null || query.equals(step.query)) { + System.out.println("Server: " + step); + throw new NullPointerException("Got you!"); + } + step = TestCases.ASSERTNOW; + if (query.equals(step.query)) { + System.out.println("Server: " + step); + throw new AssertionError("Got you!"); + } + byte[] body = BODY.getBytes(StandardCharsets.UTF_8); + ex.sendResponseHeaders(200, body.length); + step = TestCases.RESPANDFAIL; + if (query.equals(step.query)) { + System.out.println("Server: " + step); + throw new NullPointerException("Got you!"); + } + step = TestCases.RESPANDASSERT; + if (query.equals(step.query)) { + System.out.println("Server: " + step); + throw new AssertionError("Got you!"); + } + step = TestCases.CLOSEAFTERRESP; + if (query.equals(step.query)) { + System.out.println("Server: " + step); + ex.close(); + return; + } + if (!"HEAD".equals(ex.getRequestMethod())) { + ex.getResponseBody().write(body); + } + step = TestCases.BODYANDFAIL; + if (query.equals(step.query)) { + System.out.println("Server: " + step); + throw new NullPointerException("Got you!"); + } + step = TestCases.BODYANDASSERT; + if (query.equals(step.query)) { + System.out.println("Server: " + step); + throw new AssertionError("Got you!"); + } + step = TestCases.CLOSEBEFOREOS; + if (query.equals(step.query)) { + System.out.println("Server: " + step); + ex.close(); + return; + } + System.out.println("Server: closing response body"); + ex.getResponseBody().close(); + step = TestCases.CLOSEANDRETURN; + if (query.equals(step.query)) { + System.out.println("Server: " + step); + ex.close(); + return; + } + step = TestCases.CLOSEANDFAIL; + if (query.equals(step.query)) { + System.out.println("Server: " + step); + throw new NullPointerException("Got you!"); + } + step = TestCases.CLOSEANDASSERT; + if (query.equals(step.query)) { + System.out.println("Server: " + step); + throw new AssertionError("Got you!"); + } + } + + private static void enableHttpServerLogging() { + // set HttpServer's logger to ALL + LOGGER.setLevel(Level.ALL); + // get the root logger, get its first handler (by default + // it's a ConsoleHandler), and set its level to ALL (by + // default its level is INFO). + Logger.getLogger("").getHandlers()[0].setLevel(Level.ALL); + } + + + public static void main(String[] args) throws Exception { + + enableHttpServerLogging(); + + // test with GET + for (var test : TestCases.values()) { + test(test, Optional.empty(), "http"); + test(test, Optional.empty(), "https"); + } + // test with HEAD + for (var test : TestCases.values()) { + test(test, Optional.of("HEAD"), "http"); + test(test, Optional.of("HEAD"), "https"); + } + } + + private static SSLContext initSSLContext(boolean secure) { + SSLContext context = secure ? SimpleSSLContext.findSSLContext() : null; + if (secure) { + SSLContext.setDefault(context); + } + return context; + } + + private static HttpServer createHttpServer(SSLContext context) throws IOException { + var address = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); + if (context != null) { + var server = HttpsServer.create(address, 0); + server.setHttpsConfigurator(new HttpsConfigurator(context)); + return server; + } else { + return HttpServer.create(address, 0); + } + } + + private static HttpURLConnection createConnection(URL url, boolean secure) + throws IOException { + HttpURLConnection urlc = (HttpURLConnection) url.openConnection(Proxy.NO_PROXY); + if (secure) { + ((HttpsURLConnection)urlc).setHostnameVerifier(new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + }); + } + return urlc; + } + + private static void test(TestCases test, Optional method, String scheme) + throws Exception { + boolean secure = "https".equalsIgnoreCase(scheme); + SSLContext context = initSSLContext(secure); + HttpServer server = createHttpServer(context); + + System.out.println("Test: " + method.orElse("GET") + " " + test.query); + System.out.println("Server listening at: " + server.getAddress()); + try { + server.createContext("/FailAndStopTest/", new FailAndStopTest()); + server.start(); + + URL url = URIBuilder.newBuilder() + .scheme(scheme) + .loopback() + .port(server.getAddress().getPort()) + .path("/FailAndStopTest/") + .query(test.query) + .toURLUnchecked(); + System.out.println("Connecting to: " + url); + HttpURLConnection urlc = createConnection(url, secure); + if (method.isPresent()) urlc.setRequestMethod(method.get()); + try { + System.out.println("Client: Response code received: " + urlc.getResponseCode()); + InputStream is = urlc.getInputStream(); + String body = new String(is.readAllBytes(), StandardCharsets.UTF_8); + is.close(); + System.out.printf("Client: read body: \"%s\"%n", body); + if (test.shouldFail(scheme, urlc.getRequestMethod())) { + throw new AssertionError(test.query + ": test did not fail"); + } + if (!method.orElse("GET").equals("HEAD")) { + if (!BODY.equals(body)) { + throw new AssertionError( + String.format("\"%s\" != \"%s\"", body, BODY)); + } + } else if (!body.isEmpty()) { + throw new AssertionError("Body is not empty: " + body); + } + } catch (IOException so) { + if (test.shouldFail(scheme, urlc.getRequestMethod())) { + // expected + System.out.println(test.query + ": Got expected exception: " + so); + } else if (!test.shouldFail("http", urlc.getRequestMethod())) { + // When using https, the buffered response bytes may be sent + // when the connection is closed, in which case the full body + // will be correctly received, and the client connection + // will not fail. With plain http, the bytes are not sent and + // the client fails with premature end of file. + // So only fail here if the test should not fail with plain + // http - we want to accept possible exception for https... + throw new AssertionError( + String.format("%s: test failed with %s", test.query, so), so); + } else { + System.out.printf("%s: WARNING: unexpected exception: %s%n", test.query, so); + // should only happen in those two cases: + assert secure && !"HEAD".equals(method) && + (test == TestCases.BODYANDFAIL || test == TestCases.BODYANDASSERT); + } + } + } finally { + // if not fixed will cause the test to fail in jtreg timeout + server.stop((int)Utils.adjustTimeout(5000)); + System.out.println("Server stopped as expected"); + } + } +}