From 944b9e8943c896fc8bed1def1dd844bfa64034c0 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Tue, 30 Jul 2024 19:05:52 +0200 Subject: [PATCH 1/5] Improvements to HttpSender. * Changed ContentSender demand from iterate()+IDLE to succeeded()+SCHEDULED. This ensures that there is no re-iteration in case a 100 Continue response arrives. This, in turn, avoids that the demand is performed multiple times, causing ISE to be thrown. * Changed the 100 Continue action of the proxy Servlet/Handler, that provides the request content, to be executed by the HttpSender, rather than by the HttpReceiver. Signed-off-by: Simone Bordet --- .../jetty/client/ContinueProtocolHandler.java | 11 ++- .../jetty/client/transport/HttpChannel.java | 4 +- .../jetty/client/transport/HttpExchange.java | 4 +- .../jetty/client/transport/HttpSender.java | 93 ++++++++++++------- .../org/eclipse/jetty/proxy/ProxyHandler.java | 10 +- .../ee10/proxy/AbstractProxyServlet.java | 9 +- .../ee10/proxy/AsyncMiddleManServlet.java | 8 +- .../jetty/ee10/proxy/ProxyServlet.java | 10 +- .../jetty/ee9/proxy/AbstractProxyServlet.java | 9 +- .../ee9/proxy/AsyncMiddleManServlet.java | 8 +- .../eclipse/jetty/ee9/proxy/ProxyServlet.java | 10 +- 11 files changed, 98 insertions(+), 78 deletions(-) diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/ContinueProtocolHandler.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/ContinueProtocolHandler.java index 146b241e6440..213e0ef2586d 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/ContinueProtocolHandler.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/ContinueProtocolHandler.java @@ -52,8 +52,9 @@ public Response.Listener getResponseListener() return new ContinueListener(); } - protected void onContinue(Request request) + protected Runnable onContinue(Request request) { + return null; } protected class ContinueListener extends BufferingResponseListener @@ -79,8 +80,10 @@ public void onSuccess(Response response) { // All good, continue. exchange.resetResponse(); - exchange.proceed(null); - onContinue(request); + Runnable proceedAction = onContinue(request); + // Pass the proceed action to be executed + // by the sender, not here by the receiver. + exchange.proceed(proceedAction, null); } else { @@ -90,7 +93,7 @@ public void onSuccess(Response response) ResponseListeners listeners = exchange.getResponseListeners(); HttpContentResponse contentResponse = new HttpContentResponse(response, getContent(), getMediaType(), getEncoding()); listeners.emitSuccess(contentResponse); - exchange.proceed(new HttpRequestException("Expectation failed", request)); + exchange.proceed(null, new HttpRequestException("Expectation failed", request)); } } diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpChannel.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpChannel.java index b524ede09f9f..6545d31d07ff 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpChannel.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpChannel.java @@ -146,9 +146,9 @@ public void send() public abstract void release(); - public void proceed(HttpExchange exchange, Throwable failure) + public void proceed(HttpExchange exchange, Runnable proceedAction, Throwable failure) { - getHttpSender().proceed(exchange, failure); + getHttpSender().proceed(exchange, proceedAction, failure); } public void abort(HttpExchange exchange, Throwable requestFailure, Throwable responseFailure, Promise promise) diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpExchange.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpExchange.java index b68815bea285..0028e6c3a4bd 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpExchange.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpExchange.java @@ -317,11 +317,11 @@ public void resetResponse() } } - public void proceed(Throwable failure) + public void proceed(Runnable proceedAction, Throwable failure) { HttpChannel channel = getHttpChannel(); if (channel != null) - channel.proceed(this, failure); + channel.proceed(this, proceedAction, failure); } @Override diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpSender.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpSender.java index 551a210ae0e7..43903c1437a7 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpSender.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpSender.java @@ -317,12 +317,15 @@ protected void dispose() { } - public void proceed(HttpExchange exchange, Throwable failure) + public void proceed(HttpExchange exchange, Runnable proceedAction, Throwable failure) { - // Received a 100 Continue, although Expect header was not sent. + // Received a 100 Continue, although the Expect header was not sent. if (!contentSender.expect100) return; + // Write the fields in this order, since the reader of + // these fields will read them in the opposite order. + contentSender.proceedAction = proceedAction; contentSender.expect100 = false; if (failure == null) { @@ -462,32 +465,39 @@ private enum RequestState private class ContentSender extends IteratingCallback { - private HttpExchange exchange; + // Fields that are set externally. + private volatile HttpExchange exchange; + private volatile Runnable proceedAction; + private volatile boolean expect100; + // Fields only used internally. private Content.Chunk chunk; private ByteBuffer contentBuffer; - private boolean expect100; private boolean committed; private boolean success; private boolean complete; private Promise abort; + private boolean demanded; @Override public boolean reset() { exchange = null; + proceedAction = null; + expect100 = false; chunk = null; contentBuffer = null; - expect100 = false; committed = false; success = false; complete = false; abort = null; + demanded = false; return super.reset(); } @Override protected Action process() throws Throwable { + HttpExchange exchange = this.exchange; if (complete) { if (success) @@ -498,15 +508,26 @@ protected Action process() throws Throwable HttpRequest request = exchange.getRequest(); Content.Source content = request.getBody(); + boolean expect100 = this.expect100; if (expect100) { + // If the request was sent already, wait for + // the 100 response before sending the content. if (committed) return Action.IDLE; - else - chunk = null; + // Do not send any content yet. + chunk = null; } else { + // Run the proceed action first, which likely will provide + // content after having received the 100 Continue response. + Runnable action = proceedAction; + proceedAction = null; + if (action != null) + action.run(); + + // Read the request content. chunk = content.read(); } if (LOG.isDebugEnabled()) @@ -516,11 +537,14 @@ protected Action process() throws Throwable { if (committed) { - content.demand(this::iterate); - return Action.IDLE; + // No content after the headers, demand. + demanded = true; + content.demand(this::succeeded); + return Action.SCHEDULED; } else { + // Normalize to avoid null checks. chunk = Content.Chunk.EMPTY; } } @@ -545,49 +569,50 @@ protected Action process() throws Throwable @Override protected void onSuccess() { - boolean proceed = true; - if (committed) + if (demanded) { - if (contentBuffer.hasRemaining()) - proceed = someToContent(exchange, contentBuffer); + // Content is now available, reset + // the demand and iterate again. + demanded = false; } else { - committed = true; - if (headersToCommit(exchange)) + boolean proceed = true; + if (committed) { - // Was any content sent while committing? if (contentBuffer.hasRemaining()) proceed = someToContent(exchange, contentBuffer); } else { - proceed = false; + committed = true; + proceed = headersToCommit(exchange); + if (proceed) + { + // Was any content sent while committing? + if (contentBuffer.hasRemaining()) + proceed = someToContent(exchange, contentBuffer); + } } - } - boolean last = chunk.isLast(); - chunk.release(); - chunk = null; + boolean last = chunk.isLast(); + chunk.release(); + chunk = null; - if (proceed) - { - if (last) + if (proceed) { - success = true; - complete = true; + if (last) + { + success = true; + complete = true; + } } - else if (expect100) + else { - if (LOG.isDebugEnabled()) - LOG.debug("Expecting 100 Continue for {}", exchange.getRequest()); + // There was some concurrent error, terminate. + complete = true; } } - else - { - // There was some concurrent error, terminate. - complete = true; - } } @Override diff --git a/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java b/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java index 99baae9f5910..f72236241881 100644 --- a/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java +++ b/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java @@ -444,12 +444,11 @@ protected void onServerToProxyResponseFailure(Request clientToProxyRequest, org. Response.writeError(clientToProxyRequest, proxyToClientResponse, callback, status); } - protected void onServerToProxyResponse100Continue(Request clientToProxyRequest, org.eclipse.jetty.client.Request proxyToServerRequest) + protected Runnable onServerToProxyResponse100Continue(Request clientToProxyRequest, org.eclipse.jetty.client.Request proxyToServerRequest) { if (LOG.isDebugEnabled()) LOG.debug("{} P2C 100 continue response", requestId(clientToProxyRequest)); - Runnable action = (Runnable)proxyToServerRequest.getAttributes().get(PROXY_TO_SERVER_CONTINUE_ATTRIBUTE); - action.run(); + return (Runnable)proxyToServerRequest.getAttributes().get(PROXY_TO_SERVER_CONTINUE_ATTRIBUTE); } protected void onServerToProxyResponse102Processing(Request clientToProxyRequest, org.eclipse.jetty.client.Request proxyToServerRequest, HttpFields serverToProxyResponseHeaders, Response proxyToClientResponse) @@ -776,13 +775,12 @@ public InvocationType getInvocationType() private class ProxyContinueProtocolHandler extends ContinueProtocolHandler { @Override - protected void onContinue(org.eclipse.jetty.client.Request proxyToServerRequest) + protected Runnable onContinue(org.eclipse.jetty.client.Request proxyToServerRequest) { - super.onContinue(proxyToServerRequest); var clientToProxyRequest = (Request)proxyToServerRequest.getAttributes().get(CLIENT_TO_PROXY_REQUEST_ATTRIBUTE); if (LOG.isDebugEnabled()) LOG.debug("{} S2P received 100 Continue", requestId(clientToProxyRequest)); - onServerToProxyResponse100Continue(clientToProxyRequest, proxyToServerRequest); + return onServerToProxyResponse100Continue(clientToProxyRequest, proxyToServerRequest); } } diff --git a/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AbstractProxyServlet.java b/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AbstractProxyServlet.java index 8cad022eec05..dd7db3f33a05 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AbstractProxyServlet.java +++ b/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AbstractProxyServlet.java @@ -763,10 +763,9 @@ protected void sendProxyResponseError(HttpServletRequest clientRequest, HttpServ } } - protected void onContinue(HttpServletRequest clientRequest, Request proxyRequest) + protected Runnable onContinue(HttpServletRequest clientRequest, Request proxyRequest) { - if (_log.isDebugEnabled()) - _log.debug("{} handling 100 Continue", getRequestId(clientRequest)); + return null; } /** @@ -851,10 +850,10 @@ protected String rewriteTarget(HttpServletRequest request) class ProxyContinueProtocolHandler extends ContinueProtocolHandler { @Override - protected void onContinue(Request request) + protected Runnable onContinue(Request request) { HttpServletRequest clientRequest = (HttpServletRequest)request.getAttributes().get(CLIENT_REQUEST_ATTRIBUTE); - AbstractProxyServlet.this.onContinue(clientRequest, request); + return AbstractProxyServlet.this.onContinue(clientRequest, request); } } } diff --git a/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AsyncMiddleManServlet.java b/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AsyncMiddleManServlet.java index 765495e64add..a03be8bdb542 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AsyncMiddleManServlet.java +++ b/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AsyncMiddleManServlet.java @@ -171,11 +171,11 @@ protected ContentTransformer newServerResponseContentTransformer(HttpServletRequ } @Override - protected void onContinue(HttpServletRequest clientRequest, Request proxyRequest) + protected Runnable onContinue(HttpServletRequest clientRequest, Request proxyRequest) { - super.onContinue(clientRequest, proxyRequest); - Runnable action = (Runnable)proxyRequest.getAttributes().get(CONTINUE_ACTION_ATTRIBUTE); - action.run(); + if (_log.isDebugEnabled()) + _log.debug("{} handling 100 Continue", getRequestId(clientRequest)); + return (Runnable)proxyRequest.getAttributes().get(CONTINUE_ACTION_ATTRIBUTE); } private void transform(ContentTransformer transformer, ByteBuffer input, boolean finished, List output) throws IOException diff --git a/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/ProxyServlet.java b/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/ProxyServlet.java index c8e0f532b97f..bfb8c26c783e 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/ProxyServlet.java +++ b/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/ProxyServlet.java @@ -16,7 +16,6 @@ import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; -import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import jakarta.servlet.AsyncContext; @@ -144,12 +143,11 @@ protected void onResponseContent(HttpServletRequest request, HttpServletResponse } @Override - protected void onContinue(HttpServletRequest clientRequest, Request proxyRequest) + protected Runnable onContinue(HttpServletRequest clientRequest, Request proxyRequest) { - super.onContinue(clientRequest, proxyRequest); - Runnable action = (Runnable)proxyRequest.getAttributes().get(CONTINUE_ACTION_ATTRIBUTE); - Executor executor = getHttpClient().getExecutor(); - executor.execute(action); + if (_log.isDebugEnabled()) + _log.debug("{} handling 100 Continue", getRequestId(clientRequest)); + return (Runnable)proxyRequest.getAttributes().get(CONTINUE_ACTION_ATTRIBUTE); } /** diff --git a/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AbstractProxyServlet.java b/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AbstractProxyServlet.java index 0b3acdacd530..f021c65f5b8e 100644 --- a/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AbstractProxyServlet.java +++ b/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AbstractProxyServlet.java @@ -768,10 +768,9 @@ protected void sendProxyResponseError(HttpServletRequest clientRequest, HttpServ } } - protected void onContinue(HttpServletRequest clientRequest, Request proxyRequest) + protected Runnable onContinue(HttpServletRequest clientRequest, Request proxyRequest) { - if (_log.isDebugEnabled()) - _log.debug("{} handling 100 Continue", getRequestId(clientRequest)); + return null; } /** @@ -856,10 +855,10 @@ protected String rewriteTarget(HttpServletRequest request) class ProxyContinueProtocolHandler extends ContinueProtocolHandler { @Override - protected void onContinue(Request request) + protected Runnable onContinue(Request request) { HttpServletRequest clientRequest = (HttpServletRequest)request.getAttributes().get(CLIENT_REQUEST_ATTRIBUTE); - AbstractProxyServlet.this.onContinue(clientRequest, request); + return AbstractProxyServlet.this.onContinue(clientRequest, request); } } } diff --git a/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AsyncMiddleManServlet.java b/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AsyncMiddleManServlet.java index b70bfab77475..e929a724a96f 100644 --- a/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AsyncMiddleManServlet.java +++ b/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AsyncMiddleManServlet.java @@ -171,11 +171,11 @@ protected ContentTransformer newServerResponseContentTransformer(HttpServletRequ } @Override - protected void onContinue(HttpServletRequest clientRequest, Request proxyRequest) + protected Runnable onContinue(HttpServletRequest clientRequest, Request proxyRequest) { - super.onContinue(clientRequest, proxyRequest); - Runnable action = (Runnable)proxyRequest.getAttributes().get(CONTINUE_ACTION_ATTRIBUTE); - action.run(); + if (_log.isDebugEnabled()) + _log.debug("{} handling 100 Continue", getRequestId(clientRequest)); + return (Runnable)proxyRequest.getAttributes().get(CONTINUE_ACTION_ATTRIBUTE); } private void transform(ContentTransformer transformer, ByteBuffer input, boolean finished, List output) throws IOException diff --git a/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/ProxyServlet.java b/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/ProxyServlet.java index f9520096bcf5..9691bcb96282 100644 --- a/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/ProxyServlet.java +++ b/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/ProxyServlet.java @@ -16,7 +16,6 @@ import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; -import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import jakarta.servlet.AsyncContext; @@ -144,12 +143,11 @@ protected void onResponseContent(HttpServletRequest request, HttpServletResponse } @Override - protected void onContinue(HttpServletRequest clientRequest, Request proxyRequest) + protected Runnable onContinue(HttpServletRequest clientRequest, Request proxyRequest) { - super.onContinue(clientRequest, proxyRequest); - Runnable action = (Runnable)proxyRequest.getAttributes().get(CONTINUE_ACTION_ATTRIBUTE); - Executor executor = getHttpClient().getExecutor(); - executor.execute(action); + if (_log.isDebugEnabled()) + _log.debug("{} handling 100 Continue", getRequestId(clientRequest)); + return (Runnable)proxyRequest.getAttributes().get(CONTINUE_ACTION_ATTRIBUTE); } /** From 7964f40bf265b1d8ec88a0bb6cd035948f75653f Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Wed, 31 Jul 2024 15:21:56 +0200 Subject: [PATCH 2/5] Improved handling of 100 Continue. * Now `HttpClient` removed the `Expect` header if there is no request content. * Changed AbstractProxyServlet and ProxyHandler check for request content: now the Content-Type header is not taken into consideration. * Now the server avoids sending the 100 Continue response if there is no request content. Signed-off-by: Simone Bordet --- .../jetty/client/ContinueProtocolHandler.java | 10 ++- .../client/transport/HttpConnection.java | 38 ++++----- .../org/eclipse/jetty/proxy/ProxyHandler.java | 9 ++- .../server/internal/HttpChannelState.java | 8 +- .../ee10/proxy/AbstractProxyServlet.java | 9 ++- .../jetty/ee10/proxy/ProxyServletTest.java | 79 +++++++++++++++++++ .../transport/HttpClientContinueTest.java | 66 +++++++++++++++- .../jetty/ee9/proxy/AbstractProxyServlet.java | 9 ++- 8 files changed, 187 insertions(+), 41 deletions(-) diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/ContinueProtocolHandler.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/ContinueProtocolHandler.java index 213e0ef2586d..86111bc11b89 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/ContinueProtocolHandler.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/ContinueProtocolHandler.java @@ -39,10 +39,14 @@ public String getName() @Override public boolean accept(Request request, Response response) { - boolean is100 = response.getStatus() == HttpStatus.CONTINUE_100; - boolean expect100 = request.getHeaders().contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()); boolean handled100 = request.getAttributes().containsKey(ATTRIBUTE); - return (is100 || expect100) && !handled100; + if (handled100) + return false; + boolean is100 = response.getStatus() == HttpStatus.CONTINUE_100; + if (is100) + return true; + // Also handle non-100 responses, because we need to complete the request to complete the whole exchange. + return request.getHeaders().contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()); } @Override diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpConnection.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpConnection.java index 160ca2649ffe..3c47e39b764e 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpConnection.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpConnection.java @@ -146,7 +146,7 @@ protected void normalizeRequest(HttpRequest request) // Make sure the path is there String path = request.getPath(); - if (path.trim().length() == 0) + if (path.trim().isEmpty()) { path = "/"; request.path(path); @@ -192,29 +192,25 @@ protected void normalizeRequest(HttpRequest request) // Add content headers. Request.Content content = request.getBody(); if (content == null) + request.body(content = new BytesRequestContent()); + + if (!headers.contains(HttpHeader.CONTENT_TYPE)) { - request.body(new BytesRequestContent()); + String contentType = content.getContentType(); + if (contentType == null) + contentType = getHttpClient().getDefaultRequestContentType(); + if (contentType != null) + request.addHeader(new HttpField(HttpHeader.CONTENT_TYPE, contentType)); } - else + long contentLength = content.getLength(); + if (contentLength >= 0) { - if (!headers.contains(HttpHeader.CONTENT_TYPE)) - { - String contentType = content.getContentType(); - if (contentType == null) - contentType = getHttpClient().getDefaultRequestContentType(); - if (contentType != null) - { - HttpField field = new HttpField(HttpHeader.CONTENT_TYPE, contentType); - request.addHeader(field); - } - } - long contentLength = content.getLength(); - if (contentLength >= 0) - { - if (!headers.contains(HttpHeader.CONTENT_LENGTH)) - request.addHeader(new HttpField.LongValueHttpField(HttpHeader.CONTENT_LENGTH, contentLength)); - } + if (!headers.contains(HttpHeader.CONTENT_LENGTH)) + request.addHeader(new HttpField.LongValueHttpField(HttpHeader.CONTENT_LENGTH, contentLength)); } + // RFC 9110, section 10.1.1. + if (contentLength == 0) + request.headers(h -> h.remove(HttpHeader.EXPECT)); // Cookies. StringBuilder cookies = convertCookies(request.getCookies(), null); @@ -243,7 +239,7 @@ private StringBuilder convertCookies(List cookies, StringBuilder bui { if (builder == null) builder = new StringBuilder(); - if (builder.length() > 0) + if (!builder.isEmpty()) builder.append("; "); builder.append(cookie.getName()).append("=").append(cookie.getValue()); } diff --git a/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java b/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java index f72236241881..ae4540ca7107 100644 --- a/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java +++ b/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java @@ -393,11 +393,12 @@ protected void addForwardedHeader(Request clientToProxyRequest, org.eclipse.jett private boolean hasContent(Request clientToProxyRequest) { - if (clientToProxyRequest.getLength() > 0) + long contentLength = clientToProxyRequest.getLength(); + if (contentLength == 0) + return false; + if (contentLength > 0) return true; - HttpFields headers = clientToProxyRequest.getHeaders(); - return headers.get(HttpHeader.CONTENT_TYPE) != null || - headers.get(HttpHeader.TRANSFER_ENCODING) != null; + return clientToProxyRequest.getHeaders().get(HttpHeader.TRANSFER_ENCODING) != null; } private boolean expects100Continue(Request clientToProxyRequest) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java index e97331b573ae..5aa3287355e6 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java @@ -1117,8 +1117,6 @@ public String toString() */ public static class ChannelResponse implements Response, Callback { - private static final CompletableFuture UNEXPECTED_100_CONTINUE = CompletableFuture.failedFuture(new IllegalStateException("100 not expected")); - private static final CompletableFuture COMMITTED_100_CONTINUE = CompletableFuture.failedFuture(new IllegalStateException("Committed")); private final ChannelRequest _request; private final ResponseHttpFields _httpFields; protected int _status; @@ -1408,12 +1406,14 @@ public CompletableFuture writeInterim(int status, HttpFields headers) if (status == HttpStatus.CONTINUE_100) { if (!httpChannelState._expects100Continue) - return UNEXPECTED_100_CONTINUE; + return CompletableFuture.failedFuture(new IllegalStateException("100 not expected")); + if (_request.getLength() == 0) + return CompletableFuture.completedFuture(null); httpChannelState._expects100Continue = false; } if (_httpFields.isCommitted()) - return status == HttpStatus.CONTINUE_100 ? COMMITTED_100_CONTINUE : CompletableFuture.failedFuture(new IllegalStateException("Committed")); + return CompletableFuture.failedFuture(new IllegalStateException("Committed")); if (_writeCallback != null) return CompletableFuture.failedFuture(new WritePendingException()); diff --git a/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AbstractProxyServlet.java b/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AbstractProxyServlet.java index dd7db3f33a05..3bebd249a616 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AbstractProxyServlet.java +++ b/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AbstractProxyServlet.java @@ -456,9 +456,12 @@ protected void onProxyRewriteFailed(HttpServletRequest clientRequest, HttpServle protected boolean hasContent(HttpServletRequest clientRequest) { - return clientRequest.getContentLength() > 0 || - clientRequest.getContentType() != null || - clientRequest.getHeader(HttpHeader.TRANSFER_ENCODING.asString()) != null; + long contentLength = clientRequest.getContentLengthLong(); + if (contentLength == 0) + return false; + if (contentLength > 0) + return true; + return clientRequest.getHeader(HttpHeader.TRANSFER_ENCODING.asString()) != null; } protected boolean expects100Continue(HttpServletRequest request) diff --git a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServletTest.java b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServletTest.java index ccd90c8a2753..f98ca0d4a350 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServletTest.java +++ b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServletTest.java @@ -20,7 +20,9 @@ import java.io.PrintWriter; import java.io.Writer; import java.net.ConnectException; +import java.net.InetSocketAddress; import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -80,6 +82,7 @@ import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; @@ -1703,4 +1706,80 @@ protected void service(HttpServletRequest request, HttpServletResponse response) assertEquals(HttpStatus.OK_200, response.getStatus()); } } + + @ParameterizedTest + @MethodSource("impls") + public void testExpect100ContinueContentLengthZero(Class proxyServletClass) throws Exception + { + testExpect100ContinueNoRequestContent(proxyServletClass, false); + } + + @ParameterizedTest + @MethodSource("impls") + public void testExpect100ContinueEmptyChunkedContent(Class proxyServletClass) throws Exception + { + testExpect100ContinueNoRequestContent(proxyServletClass, true); + } + + private void testExpect100ContinueNoRequestContent(Class proxyServletClass, boolean chunked) throws Exception + { + startServer(new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException + { + // Send the 100 Continue. + ServletInputStream input = request.getInputStream(); + // Echo the content. + IO.copy(input, response.getOutputStream()); + } + }); + startProxy(proxyServletClass); + + String authority = "localhost:" + serverConnector.getLocalPort(); + for (int i = 0; i < 50; i++) + { + try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", proxyConnector.getLocalPort()))) + { + String request; + if (chunked) + { + request = """ + POST http://$A/ HTTP/1.1 + Host: $A + Expect: 100-Continue + Transfer-Encoding: chunked + + 0 + + """; + } + else + { + request = """ + POST http://$A/ HTTP/1.1 + Host: $A + Expect: 100-Continue + Content-Length: 0 + + """; + } + request = request.replace("$A", authority); + client.write(StandardCharsets.UTF_8.encode(request)); + + HttpTester.Input input = HttpTester.from(client); + HttpTester.Response response1 = HttpTester.parseResponse(input); + if (chunked) + { + assertEquals(HttpStatus.CONTINUE_100, response1.getStatus()); + HttpTester.Response response2 = HttpTester.parseResponse(input); + assertEquals(HttpStatus.OK_200, response2.getStatus()); + } + else + { + assertEquals(HttpStatus.OK_200, response1.getStatus()); + } + } + } + } } diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/HttpClientContinueTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/HttpClientContinueTest.java index 11b33f6cc8b8..4626f0a7f5dc 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/HttpClientContinueTest.java +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/HttpClientContinueTest.java @@ -22,6 +22,7 @@ import java.net.ServerSocket; import java.net.Socket; import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; import java.util.EnumSet; import java.util.Random; @@ -45,11 +46,13 @@ import org.eclipse.jetty.client.Request; import org.eclipse.jetty.client.Response; import org.eclipse.jetty.client.Result; +import org.eclipse.jetty.client.StringRequestContent; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpTester; +import org.eclipse.jetty.server.NetworkConnector; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.IO; import org.junit.jupiter.api.Test; @@ -163,7 +166,6 @@ protected void service(HttpServletRequest request, HttpServletResponse response) .body(content) .timeout(5, TimeUnit.SECONDS) .send(); - } assertNotNull(response); @@ -233,14 +235,14 @@ public long getLength() @MethodSource("transportsNoFCGI") public void testExpect100ContinueWithContentRespond417ExpectationFailed(Transport transport) throws Exception { - testExpect100ContinueWithContentRespondError(transport, 417); + testExpect100ContinueWithContentRespondError(transport, HttpStatus.EXPECTATION_FAILED_417); } @ParameterizedTest @MethodSource("transportsNoFCGI") public void testExpect100ContinueWithContentRespond413RequestEntityTooLarge(Transport transport) throws Exception { - testExpect100ContinueWithContentRespondError(transport, 413); + testExpect100ContinueWithContentRespondError(transport, HttpStatus.PAYLOAD_TOO_LARGE_413); } private void testExpect100ContinueWithContentRespondError(Transport transport, int error) throws Exception @@ -796,6 +798,64 @@ protected void service(HttpServletRequest request, HttpServletResponse response) assertTrue(clientLatch.await(5, TimeUnit.SECONDS)); } + @ParameterizedTest + @MethodSource("transportsNoFCGI") + public void testExpect100ContinueWithContentLengthZeroExpectIsRemoved(Transport transport) throws Exception + { + start(transport, new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + { + assertEquals(0, request.getContentLengthLong()); + // The Expect header must have been removed by the client. + assertNull(request.getHeader(HttpHeader.EXPECT.asString())); + } + }); + + ContentResponse response = client.newRequest(newURI(transport)) + .headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString())) + .body(new StringRequestContent("")) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertEquals(HttpStatus.OK_200, response.getStatus()); + } + + @Test + public void testExpect100ContinueWithContentLengthZero() throws Exception + { + startServer(Transport.HTTP, new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + assertEquals(0, request.getContentLengthLong()); + assertNotNull(request.getHeader(HttpHeader.EXPECT.asString())); + + // Trigger the 100-Continue logic. + // The 100 continue will not be sent, since there is no request content. + ServletInputStream input = request.getInputStream(); + assertEquals(-1, input.read()); + } + }); + + try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", ((NetworkConnector)connector).getLocalPort()))) + { + String request = """ + GET / HTTP/1.1 + Host: localhost + Expect: 100-Continue + Content-Length: 0 + + """; + client.write(StandardCharsets.UTF_8.encode(request)); + + HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(client)); + assertEquals(HttpStatus.OK_200, response.getStatus()); + } + } + @Test public void testExpect100ContinueWithTwoResponsesInOneRead() throws Exception { diff --git a/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AbstractProxyServlet.java b/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AbstractProxyServlet.java index f021c65f5b8e..2b368c7844e8 100644 --- a/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AbstractProxyServlet.java +++ b/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AbstractProxyServlet.java @@ -459,9 +459,12 @@ protected void onProxyRewriteFailed(HttpServletRequest clientRequest, HttpServle protected boolean hasContent(HttpServletRequest clientRequest) { - return clientRequest.getContentLength() > 0 || - clientRequest.getContentType() != null || - clientRequest.getHeader(HttpHeader.TRANSFER_ENCODING.asString()) != null; + long contentLength = clientRequest.getContentLengthLong(); + if (contentLength == 0) + return false; + if (contentLength > 0) + return true; + return clientRequest.getHeader(HttpHeader.TRANSFER_ENCODING.asString()) != null; } protected boolean expects100Continue(HttpServletRequest request) From 4e9755d6f8082a364f68140fa5d96439977aabee Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Thu, 1 Aug 2024 17:20:46 +0200 Subject: [PATCH 3/5] Fixes after test failure. Now the request body is not defaulted if missing, but just kept null. Signed-off-by: Simone Bordet --- .../client/transport/HttpConnection.java | 33 +++++++++---------- .../jetty/client/transport/HttpSender.java | 3 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpConnection.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpConnection.java index 3c47e39b764e..17dee5cab017 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpConnection.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpConnection.java @@ -20,7 +20,6 @@ import org.eclipse.jetty.client.Authentication; import org.eclipse.jetty.client.AuthenticationStore; -import org.eclipse.jetty.client.BytesRequestContent; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpProxy; import org.eclipse.jetty.client.HttpRequestException; @@ -191,25 +190,25 @@ protected void normalizeRequest(HttpRequest request) // Add content headers. Request.Content content = request.getBody(); - if (content == null) - request.body(content = new BytesRequestContent()); - - if (!headers.contains(HttpHeader.CONTENT_TYPE)) - { - String contentType = content.getContentType(); - if (contentType == null) - contentType = getHttpClient().getDefaultRequestContentType(); - if (contentType != null) - request.addHeader(new HttpField(HttpHeader.CONTENT_TYPE, contentType)); - } - long contentLength = content.getLength(); - if (contentLength >= 0) + if (content != null) { - if (!headers.contains(HttpHeader.CONTENT_LENGTH)) - request.addHeader(new HttpField.LongValueHttpField(HttpHeader.CONTENT_LENGTH, contentLength)); + if (!headers.contains(HttpHeader.CONTENT_TYPE)) + { + String contentType = content.getContentType(); + if (contentType == null) + contentType = getHttpClient().getDefaultRequestContentType(); + if (contentType != null) + request.addHeader(new HttpField(HttpHeader.CONTENT_TYPE, contentType)); + } + long contentLength = content.getLength(); + if (contentLength >= 0) + { + if (!headers.contains(HttpHeader.CONTENT_LENGTH)) + request.addHeader(new HttpField.LongValueHttpField(HttpHeader.CONTENT_LENGTH, contentLength)); + } } // RFC 9110, section 10.1.1. - if (contentLength == 0) + if (content == null || content.getLength() == 0) request.headers(h -> h.remove(HttpHeader.EXPECT)); // Cookies. diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpSender.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpSender.java index 43903c1437a7..55476b7f48ce 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpSender.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpSender.java @@ -528,7 +528,7 @@ protected Action process() throws Throwable action.run(); // Read the request content. - chunk = content.read(); + chunk = content != null ? content.read() : Content.Chunk.EOF; } if (LOG.isDebugEnabled()) LOG.debug("Content {} for {}", chunk, request); @@ -539,6 +539,7 @@ protected Action process() throws Throwable { // No content after the headers, demand. demanded = true; + assert content != null; content.demand(this::succeeded); return Action.SCHEDULED; } From e11ab97295f8b9ec5ad834d998a132f0d146b9e8 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Tue, 13 Aug 2024 11:23:50 +0200 Subject: [PATCH 4/5] Updates from review. Signed-off-by: Simone Bordet --- .../client/transport/HttpConnection.java | 3 +- .../org/eclipse/jetty/util/StringUtil.java | 32 ++----------------- 2 files changed, 4 insertions(+), 31 deletions(-) diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpConnection.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpConnection.java index 17dee5cab017..38723867bb2e 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpConnection.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpConnection.java @@ -34,6 +34,7 @@ import org.eclipse.jetty.io.CyclicTimeouts; import org.eclipse.jetty.util.Attachable; import org.eclipse.jetty.util.NanoTime; +import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.thread.AutoLock; import org.eclipse.jetty.util.thread.Scheduler; import org.slf4j.Logger; @@ -145,7 +146,7 @@ protected void normalizeRequest(HttpRequest request) // Make sure the path is there String path = request.getPath(); - if (path.trim().isEmpty()) + if (StringUtil.isBlank(path)) { path = "/"; request.path(path); diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java index 7d30336279e4..393500f4001c 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java @@ -657,21 +657,7 @@ public static int indexOfControlChars(String str) */ public static boolean isBlank(String str) { - if (str == null) - { - return true; - } - int len = str.length(); - for (int i = 0; i < len; i++) - { - if (!Character.isWhitespace(str.codePointAt(i))) - { - // found a non-whitespace, we can stop searching now - return false; - } - } - // only whitespace - return true; + return str == null || str.isBlank(); } /** @@ -727,21 +713,7 @@ public static int getLength(String s) */ public static boolean isNotBlank(String str) { - if (str == null) - { - return false; - } - int len = str.length(); - for (int i = 0; i < len; i++) - { - if (!Character.isWhitespace(str.codePointAt(i))) - { - // found a non-whitespace, we can stop searching now - return true; - } - } - // only whitespace - return false; + return !isBlank(str); } public static boolean isHex(String str, int offset, int length) From cd50711ffdc5c654ca1b82ab89c3ac18fab96040 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Tue, 13 Aug 2024 12:30:56 +0200 Subject: [PATCH 5/5] Updates after review. Signed-off-by: Simone Bordet --- .../jetty/ee9/proxy/ProxyServletTest.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ProxyServletTest.java b/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ProxyServletTest.java index f1d5513df298..e8c48e7ca4f0 100644 --- a/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ProxyServletTest.java +++ b/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ProxyServletTest.java @@ -20,7 +20,9 @@ import java.io.PrintWriter; import java.io.Writer; import java.net.ConnectException; +import java.net.InetSocketAddress; import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -79,6 +81,7 @@ import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; @@ -1701,4 +1704,80 @@ protected void service(HttpServletRequest request, HttpServletResponse response) assertEquals(HttpStatus.OK_200, response.getStatus()); } } + + @ParameterizedTest + @MethodSource("impls") + public void testExpect100ContinueContentLengthZero(Class proxyServletClass) throws Exception + { + testExpect100ContinueNoRequestContent(proxyServletClass, false); + } + + @ParameterizedTest + @MethodSource("impls") + public void testExpect100ContinueEmptyChunkedContent(Class proxyServletClass) throws Exception + { + testExpect100ContinueNoRequestContent(proxyServletClass, true); + } + + private void testExpect100ContinueNoRequestContent(Class proxyServletClass, boolean chunked) throws Exception + { + startServer(new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException + { + // Send the 100 Continue. + ServletInputStream input = request.getInputStream(); + // Echo the content. + IO.copy(input, response.getOutputStream()); + } + }); + startProxy(proxyServletClass); + + String authority = "localhost:" + serverConnector.getLocalPort(); + for (int i = 0; i < 50; i++) + { + try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", proxyConnector.getLocalPort()))) + { + String request; + if (chunked) + { + request = """ + POST http://$A/ HTTP/1.1 + Host: $A + Expect: 100-Continue + Transfer-Encoding: chunked + + 0 + + """; + } + else + { + request = """ + POST http://$A/ HTTP/1.1 + Host: $A + Expect: 100-Continue + Content-Length: 0 + + """; + } + request = request.replace("$A", authority); + client.write(StandardCharsets.UTF_8.encode(request)); + + HttpTester.Input input = HttpTester.from(client); + HttpTester.Response response1 = HttpTester.parseResponse(input); + if (chunked) + { + assertEquals(HttpStatus.CONTINUE_100, response1.getStatus()); + HttpTester.Response response2 = HttpTester.parseResponse(input); + assertEquals(HttpStatus.OK_200, response2.getStatus()); + } + else + { + assertEquals(HttpStatus.OK_200, response1.getStatus()); + } + } + } + } }