From 8b06eb0c4c18c852c921caa2e99f143cb831a4ad Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 26 May 2022 10:02:14 +1000 Subject: [PATCH 01/11] Implement #8057 103 Early Hint Added test harness for intermediary responses. --- .../http/client/IntermediateResponseTest.java | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/IntermediateResponseTest.java diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/IntermediateResponseTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/IntermediateResponseTest.java new file mode 100644 index 000000000000..c2225e0d1166 --- /dev/null +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/IntermediateResponseTest.java @@ -0,0 +1,215 @@ +// +// ======================================================================== +// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.http.client; + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.client.ContinueProtocolHandler; +import org.eclipse.jetty.client.HttpConversation; +import org.eclipse.jetty.client.HttpExchange; +import org.eclipse.jetty.client.HttpRequest; +import org.eclipse.jetty.client.ProtocolHandler; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.client.util.AsyncRequestContent; +import org.eclipse.jetty.client.util.BufferingResponseListener; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpHeaderValue; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.IO; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import static org.eclipse.jetty.http.client.Transport.FCGI; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class IntermediateResponseTest extends AbstractTest +{ + @Override + public void init(Transport transport) throws IOException + { + // Skip FCGI for now, not much interested in its server-side behavior. + Assumptions.assumeTrue(transport != FCGI); + setScenario(new TransportScenario(transport)); + } + + @ParameterizedTest + @ArgumentsSource(TransportProvider.class) + public void test100Continue(Transport transport) throws Exception + { + init(transport); + scenario.start(new AbstractHandler() + { + @Override + public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + jettyRequest.setHandled(true); + String body = IO.toString(request.getInputStream()); + response.getOutputStream().print("read " + body.length()); + } + }); + long idleTimeout = 10000; + scenario.setRequestIdleTimeout(idleTimeout); + + AsyncRequestContent content = new AsyncRequestContent() + { + @Override + public void demand() + { + super.demand(); + } + }; + + scenario.client.getProtocolHandlers().put(new ContinueProtocolHandler() + { + @Override + protected void onContinue(org.eclipse.jetty.client.api.Request request) + { + super.onContinue(request); + content.offer(BufferUtil.toBuffer("Some content!"), Callback.from(content::close)); + } + }); + + CountDownLatch complete = new CountDownLatch(1); + AtomicReference response = new AtomicReference<>(); + BufferingResponseListener listener = new BufferingResponseListener() + { + @Override + public void onComplete(Result result) + { + response.set(result.getResponse()); + complete.countDown(); + } + }; + scenario.client.POST(scenario.newURI()) + .headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE)) + .body(content) + .timeout(10, TimeUnit.SECONDS) + .send(listener); + + assertTrue(complete.await(10, TimeUnit.SECONDS)); + assertThat(response.get().getStatus(), is(200)); + assertThat(listener.getContentAsString(), is("read 13")); + } + + @ParameterizedTest + @ArgumentsSource(TransportProvider.class) + public void test102Processing(Transport transport) throws Exception + { + init(transport); + scenario.start(new AbstractHandler() + { + @Override + public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + jettyRequest.setHandled(true); + response.sendError(102); + response.sendError(102); + response.setStatus(200); + response.getOutputStream().print("OK"); + } + }); + long idleTimeout = 10000; + scenario.setRequestIdleTimeout(idleTimeout); + + scenario.client.getProtocolHandlers().put(new ProtocolHandler() + { + @Override + public String getName() + { + return "Processing"; + } + + @Override + public boolean accept(org.eclipse.jetty.client.api.Request request, Response response) + { + System.err.println("accept: " + request); + System.err.println(response.getStatus()); + + return response.getStatus() == 102; + } + + @Override + public Response.Listener getResponseListener() + { + return new Response.Listener() + { + @Override + public void onBegin(Response response) + { + System.err.println("onBegin " + response); + Response.Listener.super.onBegin(response); + } + @Override + public void onSuccess(Response response) + { + org.eclipse.jetty.client.api.Request request = response.getRequest(); + HttpConversation conversation = ((HttpRequest)request).getConversation(); + // Reset the conversation listeners, since we are going to receive another response code + conversation.updateResponseListeners(null); + + HttpExchange exchange = conversation.getExchanges().peekLast(); + if (exchange != null && response.getStatus() == HttpStatus.PROCESSING_102) + { + // All good, continue. + exchange.resetResponse(); + exchange.proceed(null); + } + else + { + throw new IllegalStateException("should not have accepted"); + } + } + }; + } + }); + + CountDownLatch complete = new CountDownLatch(1); + AtomicReference response = new AtomicReference<>(); + BufferingResponseListener listener = new BufferingResponseListener() + { + @Override + public void onComplete(Result result) + { + System.err.println("COMPLETE " + result.getResponse()); + response.set(result.getResponse()); + complete.countDown(); + } + }; + scenario.client.newRequest(scenario.newURI()) + .method("GET") + .headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.PROCESSING)) + .timeout(10, TimeUnit.SECONDS) + .send(listener); + + assertTrue(complete.await(10, TimeUnit.SECONDS)); + assertThat(response.get().getStatus(), is(200)); + assertThat(listener.getContentAsString(), is("OK")); + + + } +} From a3bbbc009f387df5cb17cce5d57f5df10bfc9997 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 26 May 2022 13:17:16 +1000 Subject: [PATCH 02/11] Implement #8057 103 Early Hint Implemented 103 early hint, but is failing for HTTP>=2 --- .../eclipse/jetty/client/HttpExchange.java | 1 + .../eclipse/jetty/client/HttpResponse.java | 5 + .../org/eclipse/jetty/http/HttpGenerator.java | 16 ++- .../org/eclipse/jetty/http/HttpStatus.java | 2 + .../org/eclipse/jetty/server/Response.java | 20 ++++ .../http/client/IntermediateResponseTest.java | 108 +++++++++++++++--- 6 files changed, 134 insertions(+), 18 deletions(-) diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpExchange.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpExchange.java index b0ac2e1679b4..859b92c70159 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpExchange.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpExchange.java @@ -291,6 +291,7 @@ public void resetResponse() { responseState = State.PENDING; responseFailure = null; + response.clearHeaders(); } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java index 3f01a0aa3752..a8576679390e 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java @@ -87,6 +87,11 @@ public HttpFields getHeaders() return headers.asImmutable(); } + public void clearHeaders() + { + headers.clear(); + } + public HttpResponse addHeader(HttpField header) { headers.add(header); diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java index 8a4b3ebab798..2a75f7c7cf3e 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java @@ -388,12 +388,18 @@ public Result generateResponse(MetaData.Response info, boolean head, ByteBuffer if (status >= 100 && status < 200) { _noContentResponse = true; - - if (status != HttpStatus.SWITCHING_PROTOCOLS_101) + switch(status) { - header.put(HttpTokens.CRLF); - _state = State.COMPLETING_1XX; - return Result.FLUSH; + case HttpStatus.SWITCHING_PROTOCOLS_101: + break; + case HttpStatus.EARLY_HINT_103: + generateHeaders(header, content, last); + _state = State.COMPLETING_1XX; + return Result.FLUSH; + default: + header.put(HttpTokens.CRLF); + _state = State.COMPLETING_1XX; + return Result.FLUSH; } } else if (status == HttpStatus.NO_CONTENT_204 || status == HttpStatus.NOT_MODIFIED_304) diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java index 5c807e4128bb..63bfcccce1fc 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java @@ -25,6 +25,7 @@ public class HttpStatus public static final int CONTINUE_100 = 100; public static final int SWITCHING_PROTOCOLS_101 = 101; public static final int PROCESSING_102 = 102; + public static final int EARLY_HINT_103 = 103; public static final int OK_200 = 200; public static final int CREATED_201 = 201; @@ -103,6 +104,7 @@ public enum Code CONTINUE(CONTINUE_100, "Continue"), SWITCHING_PROTOCOLS(SWITCHING_PROTOCOLS_101, "Switching Protocols"), PROCESSING(PROCESSING_102, "Processing"), + EARLY_HINT(EARLY_HINT_103, "Early Hint"), OK(OK_200, "OK"), CREATED(CREATED_201, "Created"), diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 62eb329b0c85..21748ad6f5c1 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -490,6 +490,9 @@ public void sendError(int code, String message) throws IOException case HttpStatus.PROCESSING_102: sendProcessing(); break; + case HttpStatus.EARLY_HINT_103: + sendEarlyHint(); + break; default: _channel.getState().sendError(code, message); break; @@ -514,6 +517,23 @@ public void sendProcessing() throws IOException } } + /** + * Sends a 102-Processing response. + * If the connection is an HTTP connection, the version is 1.1 and the + * request has a Expect header starting with 102, then a 102 response is + * sent. This indicates that the request still be processed and real response + * can still be sent. This method is called by sendError if it is passed 102. + * + * @throws IOException if unable to send the 102 response + * @see javax.servlet.http.HttpServletResponse#sendError(int) + */ + public void sendEarlyHint() throws IOException + { + if (!isCommitted()) + _channel.sendResponse(new MetaData.Response(_channel.getRequest().getHttpVersion(), HttpStatus.EARLY_HINT_103, + _channel.getResponse()._fields.asImmutable()), null, true); + } + /** * Sends a response with one of the 300 series redirection codes. * diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/IntermediateResponseTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/IntermediateResponseTest.java index c2225e0d1166..263eb5d2d946 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/IntermediateResponseTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/IntermediateResponseTest.java @@ -14,6 +14,8 @@ package org.eclipse.jetty.http.client; import java.io.IOException; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -44,6 +46,7 @@ import static org.eclipse.jetty.http.client.Transport.FCGI; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -127,8 +130,8 @@ public void test102Processing(Transport transport) throws Exception public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { jettyRequest.setHandled(true); - response.sendError(102); - response.sendError(102); + response.sendError(HttpStatus.PROCESSING_102); + response.sendError(HttpStatus.PROCESSING_102); response.setStatus(200); response.getOutputStream().print("OK"); } @@ -147,10 +150,7 @@ public String getName() @Override public boolean accept(org.eclipse.jetty.client.api.Request request, Response response) { - System.err.println("accept: " + request); - System.err.println(response.getStatus()); - - return response.getStatus() == 102; + return response.getStatus() == HttpStatus.PROCESSING_102; } @Override @@ -158,12 +158,6 @@ public Response.Listener getResponseListener() { return new Response.Listener() { - @Override - public void onBegin(Response response) - { - System.err.println("onBegin " + response); - Response.Listener.super.onBegin(response); - } @Override public void onSuccess(Response response) { @@ -195,7 +189,6 @@ public void onSuccess(Response response) @Override public void onComplete(Result result) { - System.err.println("COMPLETE " + result.getResponse()); response.set(result.getResponse()); complete.countDown(); } @@ -209,7 +202,96 @@ public void onComplete(Result result) assertTrue(complete.await(10, TimeUnit.SECONDS)); assertThat(response.get().getStatus(), is(200)); assertThat(listener.getContentAsString(), is("OK")); + } + + @ParameterizedTest + @ArgumentsSource(TransportProvider.class) + public void test103EarlyHint(Transport transport) throws Exception + { + init(transport); + scenario.start(new AbstractHandler() + { + @Override + public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + jettyRequest.setHandled(true); + response.setHeader("Hint", "one"); + response.sendError(HttpStatus.EARLY_HINT_103); + response.setHeader("Hint", "two"); + response.sendError(HttpStatus.EARLY_HINT_103); + response.setHeader("Hint", "three"); + response.setStatus(200); + response.getOutputStream().print("OK"); + } + }); + long idleTimeout = 10000; + scenario.setRequestIdleTimeout(idleTimeout); + + List hints = new CopyOnWriteArrayList<>(); + scenario.client.getProtocolHandlers().put(new ProtocolHandler() + { + @Override + public String getName() + { + return "EarlyHint"; + } + + @Override + public boolean accept(org.eclipse.jetty.client.api.Request request, Response response) + { + return response.getStatus() == HttpStatus.EARLY_HINT_103; + } + + @Override + public Response.Listener getResponseListener() + { + return new Response.Listener() + { + @Override + public void onSuccess(Response response) + { + org.eclipse.jetty.client.api.Request request = response.getRequest(); + HttpConversation conversation = ((HttpRequest)request).getConversation(); + // Reset the conversation listeners, since we are going to receive another response code + conversation.updateResponseListeners(null); + + HttpExchange exchange = conversation.getExchanges().peekLast(); + if (exchange != null && response.getStatus() == HttpStatus.EARLY_HINT_103) + { + // All good, continue. + hints.add(response.getHeaders().get("Hint")); + exchange.resetResponse(); + exchange.proceed(null); + } + else + { + throw new IllegalStateException("should not have accepted"); + } + } + }; + } + }); + CountDownLatch complete = new CountDownLatch(1); + AtomicReference response = new AtomicReference<>(); + BufferingResponseListener listener = new BufferingResponseListener() + { + @Override + public void onComplete(Result result) + { + hints.add(result.getResponse().getHeaders().get("Hint")); + response.set(result.getResponse()); + complete.countDown(); + } + }; + scenario.client.newRequest(scenario.newURI()) + .method("GET") + .timeout(10, TimeUnit.SECONDS) + .send(listener); + assertTrue(complete.await(10, TimeUnit.SECONDS)); + assertThat(response.get().getStatus(), is(200)); + assertThat(listener.getContentAsString(), is("OK")); + assertThat(hints, contains("one", "two", "three")); } } From 52a68944fda6b627cc9eb0b3a943d039d9387443 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 26 May 2022 13:24:10 +1000 Subject: [PATCH 03/11] Implement #8057 103 Early Hint Fixed H2 Attempted fix H3 --- .../org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java | 2 +- .../jetty/http3/server/internal/HttpTransportOverHTTP3.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java index b1cc39b88a37..bda79fc8e33c 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java @@ -98,7 +98,7 @@ public void sendHeaders(MetaData.Request request, MetaData.Response response, By boolean isHeadRequest = HttpMethod.HEAD.is(request.getMethod()); boolean hasContent = BufferUtil.hasContent(content) && !isHeadRequest; int status = response.getStatus(); - boolean interimResponse = status == HttpStatus.CONTINUE_100 || status == HttpStatus.PROCESSING_102; + boolean interimResponse = status >= HttpStatus.CONTINUE_100 && status < HttpStatus.OK_200 && status != HttpStatus.SWITCHING_PROTOCOLS_101; if (interimResponse) { // Must not commit interim responses. diff --git a/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/HttpTransportOverHTTP3.java b/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/HttpTransportOverHTTP3.java index b4da3245e637..80c50cadd1ba 100644 --- a/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/HttpTransportOverHTTP3.java +++ b/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/HttpTransportOverHTTP3.java @@ -71,7 +71,7 @@ private void sendHeaders(MetaData.Request request, MetaData.Response response, B boolean isHeadRequest = HttpMethod.HEAD.is(request.getMethod()); boolean hasContent = BufferUtil.hasContent(content) && !isHeadRequest; int status = response.getStatus(); - boolean interimResponse = status == HttpStatus.CONTINUE_100 || status == HttpStatus.PROCESSING_102; + boolean interimResponse = status >= HttpStatus.CONTINUE_100 && status < HttpStatus.OK_200 && status != HttpStatus.SWITCHING_PROTOCOLS_101; if (interimResponse) { // Must not commit interim responses. From 29741a27603d8898d2110370b887aa926257be73 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 26 May 2022 13:28:45 +1000 Subject: [PATCH 04/11] Implement #8057 103 Early Hint Fixed H2 Attempted fix H3 --- .../org/eclipse/jetty/http/client/IntermediateResponseTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/IntermediateResponseTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/IntermediateResponseTest.java index 263eb5d2d946..e6e597ec586d 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/IntermediateResponseTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/IntermediateResponseTest.java @@ -259,6 +259,7 @@ public void onSuccess(Response response) if (exchange != null && response.getStatus() == HttpStatus.EARLY_HINT_103) { // All good, continue. + System.err.println("onSuccess\n" + response.getHeaders()); hints.add(response.getHeaders().get("Hint")); exchange.resetResponse(); exchange.proceed(null); From 12a0049774d3f5929c48fb2b98ca4ad4ac1b4f09 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 26 May 2022 16:46:16 +1000 Subject: [PATCH 05/11] Implement #8057 103 Early Hint fixed checkstyle --- .../src/main/java/org/eclipse/jetty/http/HttpGenerator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java index 2a75f7c7cf3e..f3676ca7e1db 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java @@ -388,7 +388,7 @@ public Result generateResponse(MetaData.Response info, boolean head, ByteBuffer if (status >= 100 && status < 200) { _noContentResponse = true; - switch(status) + switch (status) { case HttpStatus.SWITCHING_PROTOCOLS_101: break; From 8470f4922a925630eff5b3f4985482f49189af85 Mon Sep 17 00:00:00 2001 From: Ludovic Orban Date: Thu, 26 May 2022 16:19:00 +0200 Subject: [PATCH 06/11] add missing H3 support for status 103 Signed-off-by: Ludovic Orban --- .../eclipse/jetty/http3/client/internal/HTTP3StreamClient.java | 2 ++ .../org/eclipse/jetty/http3/internal/HTTP3StreamConnection.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/jetty-http3/http3-client/src/main/java/org/eclipse/jetty/http3/client/internal/HTTP3StreamClient.java b/jetty-http3/http3-client/src/main/java/org/eclipse/jetty/http3/client/internal/HTTP3StreamClient.java index b62226aebdba..8539d078ec25 100644 --- a/jetty-http3/http3-client/src/main/java/org/eclipse/jetty/http3/client/internal/HTTP3StreamClient.java +++ b/jetty-http3/http3-client/src/main/java/org/eclipse/jetty/http3/client/internal/HTTP3StreamClient.java @@ -53,6 +53,8 @@ public void onResponse(HeadersFrame frame) boolean valid; if (response.getStatus() == HttpStatus.CONTINUE_100) valid = validateAndUpdate(EnumSet.of(FrameState.INITIAL), FrameState.CONTINUE); + else if (response.getStatus() == HttpStatus.EARLY_HINT_103) + valid = validateAndUpdate(EnumSet.of(FrameState.INITIAL, FrameState.HEADER, FrameState.CONTINUE), FrameState.CONTINUE); else valid = validateAndUpdate(EnumSet.of(FrameState.INITIAL, FrameState.CONTINUE), FrameState.HEADER); if (valid) diff --git a/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/HTTP3StreamConnection.java b/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/HTTP3StreamConnection.java index 83e2aefad73a..e15895e4503d 100644 --- a/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/HTTP3StreamConnection.java +++ b/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/HTTP3StreamConnection.java @@ -486,7 +486,7 @@ public void onHeaders(long streamId, HeadersFrame frame) else if (metaData.isResponse()) { MetaData.Response response = (MetaData.Response)metaData; - if (response.getStatus() != HttpStatus.CONTINUE_100) + if (response.getStatus() != HttpStatus.CONTINUE_100 && response.getStatus() != HttpStatus.EARLY_HINT_103) { // Expect DATA frames now. parserDataMode = true; From 41532edf0993e1c5933dc3856912c5b7b2298b90 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 27 May 2022 09:48:48 +1000 Subject: [PATCH 07/11] Implement #8057 103 Early Hint updates from review --- .../org/eclipse/jetty/http/HttpGenerator.java | 2 +- .../http2/server/HttpTransportOverHTTP2.java | 2 +- .../internal/HttpTransportOverHTTP3.java | 2 +- .../org/eclipse/jetty/server/HttpChannel.java | 2 +- .../org/eclipse/jetty/server/Response.java | 15 ++-- ...st.java => InformationalResponseTest.java} | 77 ++----------------- 6 files changed, 17 insertions(+), 83 deletions(-) rename tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/{IntermediateResponseTest.java => InformationalResponseTest.java} (75%) diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java index f3676ca7e1db..16f608357f17 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java @@ -385,7 +385,7 @@ public Result generateResponse(MetaData.Response info, boolean head, ByteBuffer // Handle 1xx and no content responses int status = info.getStatus(); - if (status >= 100 && status < 200) + if (HttpStatus.isInformational(status)) { _noContentResponse = true; switch (status) diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java index bda79fc8e33c..f62eb6ed280f 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java @@ -98,7 +98,7 @@ public void sendHeaders(MetaData.Request request, MetaData.Response response, By boolean isHeadRequest = HttpMethod.HEAD.is(request.getMethod()); boolean hasContent = BufferUtil.hasContent(content) && !isHeadRequest; int status = response.getStatus(); - boolean interimResponse = status >= HttpStatus.CONTINUE_100 && status < HttpStatus.OK_200 && status != HttpStatus.SWITCHING_PROTOCOLS_101; + boolean interimResponse = HttpStatus.isInformational(status) && status != HttpStatus.SWITCHING_PROTOCOLS_101; if (interimResponse) { // Must not commit interim responses. diff --git a/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/HttpTransportOverHTTP3.java b/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/HttpTransportOverHTTP3.java index 80c50cadd1ba..bb01b68e7e49 100644 --- a/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/HttpTransportOverHTTP3.java +++ b/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/HttpTransportOverHTTP3.java @@ -71,7 +71,7 @@ private void sendHeaders(MetaData.Request request, MetaData.Response response, B boolean isHeadRequest = HttpMethod.HEAD.is(request.getMethod()); boolean hasContent = BufferUtil.hasContent(content) && !isHeadRequest; int status = response.getStatus(); - boolean interimResponse = status >= HttpStatus.CONTINUE_100 && status < HttpStatus.OK_200 && status != HttpStatus.SWITCHING_PROTOCOLS_101; + boolean interimResponse = HttpStatus.isInformational(status) && status != HttpStatus.SWITCHING_PROTOCOLS_101; if (interimResponse) { // Must not commit interim responses. diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 6e13f885faf1..f8a4b1cf4676 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -1046,7 +1046,7 @@ protected boolean sendResponse(MetaData.Response response, ByteBuffer content, b // wrap callback to process 100 responses final int status = response.getStatus(); - final Callback committed = (status < HttpStatus.OK_200 && status >= HttpStatus.CONTINUE_100) + final Callback committed = HttpStatus.isInformational(status) ? new Send100Callback(callback) : new SendCallback(callback, content, true, complete); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 21748ad6f5c1..620b319ef28f 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -470,6 +470,7 @@ public void sendError(int sc) throws IOException *

In addition to the servlet standard handling, this method supports some additional codes:

*
*
102
Send a partial PROCESSING response and allow additional responses
+ *
103
Send a partial EARLY_HINT response as per RFC8297
*
-1
Abort the HttpChannel and close the connection/stream
*
* @param code The error code @@ -501,9 +502,8 @@ public void sendError(int code, String message) throws IOException /** * Sends a 102-Processing response. - * If the connection is an HTTP connection, the version is 1.1 and the - * request has a Expect header starting with 102, then a 102 response is - * sent. This indicates that the request still be processed and real response + * If the request had an Expect header starting with 102, then + * a 102 response is sent. This indicates that the request still be processed and real response * can still be sent. This method is called by sendError if it is passed 102. * * @throws IOException if unable to send the 102 response @@ -518,11 +518,10 @@ public void sendProcessing() throws IOException } /** - * Sends a 102-Processing response. - * If the connection is an HTTP connection, the version is 1.1 and the - * request has a Expect header starting with 102, then a 102 response is - * sent. This indicates that the request still be processed and real response - * can still be sent. This method is called by sendError if it is passed 102. + * Sends a 103 Early Hint response. + * + * Send a 103 response as per RFC8297 + * This method is called by sendError if it is passed 103. * * @throws IOException if unable to send the 102 response * @see javax.servlet.http.HttpServletResponse#sendError(int) diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/IntermediateResponseTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/InformationalResponseTest.java similarity index 75% rename from tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/IntermediateResponseTest.java rename to tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/InformationalResponseTest.java index e6e597ec586d..55879e408dae 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/IntermediateResponseTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/InformationalResponseTest.java @@ -23,23 +23,18 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.eclipse.jetty.client.ContinueProtocolHandler; import org.eclipse.jetty.client.HttpConversation; import org.eclipse.jetty.client.HttpExchange; import org.eclipse.jetty.client.HttpRequest; import org.eclipse.jetty.client.ProtocolHandler; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Result; -import org.eclipse.jetty.client.util.AsyncRequestContent; import org.eclipse.jetty.client.util.BufferingResponseListener; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.AbstractHandler; -import org.eclipse.jetty.util.BufferUtil; -import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.util.IO; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; @@ -50,7 +45,7 @@ import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertTrue; -public class IntermediateResponseTest extends AbstractTest +public class InformationalResponseTest extends AbstractTest { @Override public void init(Transport transport) throws IOException @@ -60,65 +55,6 @@ public void init(Transport transport) throws IOException setScenario(new TransportScenario(transport)); } - @ParameterizedTest - @ArgumentsSource(TransportProvider.class) - public void test100Continue(Transport transport) throws Exception - { - init(transport); - scenario.start(new AbstractHandler() - { - @Override - public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException - { - jettyRequest.setHandled(true); - String body = IO.toString(request.getInputStream()); - response.getOutputStream().print("read " + body.length()); - } - }); - long idleTimeout = 10000; - scenario.setRequestIdleTimeout(idleTimeout); - - AsyncRequestContent content = new AsyncRequestContent() - { - @Override - public void demand() - { - super.demand(); - } - }; - - scenario.client.getProtocolHandlers().put(new ContinueProtocolHandler() - { - @Override - protected void onContinue(org.eclipse.jetty.client.api.Request request) - { - super.onContinue(request); - content.offer(BufferUtil.toBuffer("Some content!"), Callback.from(content::close)); - } - }); - - CountDownLatch complete = new CountDownLatch(1); - AtomicReference response = new AtomicReference<>(); - BufferingResponseListener listener = new BufferingResponseListener() - { - @Override - public void onComplete(Result result) - { - response.set(result.getResponse()); - complete.countDown(); - } - }; - scenario.client.POST(scenario.newURI()) - .headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE)) - .body(content) - .timeout(10, TimeUnit.SECONDS) - .send(listener); - - assertTrue(complete.await(10, TimeUnit.SECONDS)); - assertThat(response.get().getStatus(), is(200)); - assertThat(listener.getContentAsString(), is("read 13")); - } - @ParameterizedTest @ArgumentsSource(TransportProvider.class) public void test102Processing(Transport transport) throws Exception @@ -127,7 +63,7 @@ public void test102Processing(Transport transport) throws Exception scenario.start(new AbstractHandler() { @Override - public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { jettyRequest.setHandled(true); response.sendError(HttpStatus.PROCESSING_102); @@ -161,7 +97,7 @@ public Response.Listener getResponseListener() @Override public void onSuccess(Response response) { - org.eclipse.jetty.client.api.Request request = response.getRequest(); + var request = response.getRequest(); HttpConversation conversation = ((HttpRequest)request).getConversation(); // Reset the conversation listeners, since we are going to receive another response code conversation.updateResponseListeners(null); @@ -175,7 +111,7 @@ public void onSuccess(Response response) } else { - throw new IllegalStateException("should not have accepted"); + response.abort(new IllegalStateException("should not have accepted")); } } }; @@ -250,7 +186,7 @@ public Response.Listener getResponseListener() @Override public void onSuccess(Response response) { - org.eclipse.jetty.client.api.Request request = response.getRequest(); + var request = response.getRequest(); HttpConversation conversation = ((HttpRequest)request).getConversation(); // Reset the conversation listeners, since we are going to receive another response code conversation.updateResponseListeners(null); @@ -259,14 +195,13 @@ public void onSuccess(Response response) if (exchange != null && response.getStatus() == HttpStatus.EARLY_HINT_103) { // All good, continue. - System.err.println("onSuccess\n" + response.getHeaders()); hints.add(response.getHeaders().get("Hint")); exchange.resetResponse(); exchange.proceed(null); } else { - throw new IllegalStateException("should not have accepted"); + response.abort(new IllegalStateException("should not have accepted")); } } }; From 9d40cd4740e903af6cc179a6778135f331f04201 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Mon, 30 May 2022 11:26:13 +1000 Subject: [PATCH 08/11] Implement #8057 103 Early Hint updates from review --- .../http3/client/internal/HTTP3StreamClient.java | 6 +++--- .../eclipse/jetty/http3/internal/HTTP3Stream.java | 2 +- .../jetty/http3/internal/HTTP3StreamConnection.java | 12 ++++++------ .../main/java/org/eclipse/jetty/server/Response.java | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/jetty-http3/http3-client/src/main/java/org/eclipse/jetty/http3/client/internal/HTTP3StreamClient.java b/jetty-http3/http3-client/src/main/java/org/eclipse/jetty/http3/client/internal/HTTP3StreamClient.java index 8539d078ec25..b2d31bb71869 100644 --- a/jetty-http3/http3-client/src/main/java/org/eclipse/jetty/http3/client/internal/HTTP3StreamClient.java +++ b/jetty-http3/http3-client/src/main/java/org/eclipse/jetty/http3/client/internal/HTTP3StreamClient.java @@ -52,11 +52,11 @@ public void onResponse(HeadersFrame frame) MetaData.Response response = (MetaData.Response)frame.getMetaData(); boolean valid; if (response.getStatus() == HttpStatus.CONTINUE_100) - valid = validateAndUpdate(EnumSet.of(FrameState.INITIAL), FrameState.CONTINUE); + valid = validateAndUpdate(EnumSet.of(FrameState.INITIAL), FrameState.INFORMATIONAL); else if (response.getStatus() == HttpStatus.EARLY_HINT_103) - valid = validateAndUpdate(EnumSet.of(FrameState.INITIAL, FrameState.HEADER, FrameState.CONTINUE), FrameState.CONTINUE); + valid = validateAndUpdate(EnumSet.of(FrameState.INITIAL, FrameState.HEADER, FrameState.INFORMATIONAL), FrameState.INFORMATIONAL); else - valid = validateAndUpdate(EnumSet.of(FrameState.INITIAL, FrameState.CONTINUE), FrameState.HEADER); + valid = validateAndUpdate(EnumSet.of(FrameState.INITIAL, FrameState.INFORMATIONAL), FrameState.HEADER); if (valid) { notIdle(); diff --git a/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/HTTP3Stream.java b/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/HTTP3Stream.java index 3cdafba2a699..ed6dbd3857dd 100644 --- a/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/HTTP3Stream.java +++ b/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/HTTP3Stream.java @@ -315,7 +315,7 @@ public String toString() protected enum FrameState { - INITIAL, CONTINUE, HEADER, DATA, TRAILER, FAILED + INITIAL, INFORMATIONAL, HEADER, DATA, TRAILER, FAILED } private enum CloseState diff --git a/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/HTTP3StreamConnection.java b/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/HTTP3StreamConnection.java index e15895e4503d..d7d3de5e3df7 100644 --- a/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/HTTP3StreamConnection.java +++ b/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/HTTP3StreamConnection.java @@ -486,18 +486,18 @@ public void onHeaders(long streamId, HeadersFrame frame) else if (metaData.isResponse()) { MetaData.Response response = (MetaData.Response)metaData; - if (response.getStatus() != HttpStatus.CONTINUE_100 && response.getStatus() != HttpStatus.EARLY_HINT_103) + if (HttpStatus.isInformational(response.getStatus())) { - // Expect DATA frames now. - parserDataMode = true; - parser.setDataMode(true); if (LOG.isDebugEnabled()) - LOG.debug("switching to parserDataMode=true for response {} on {}", metaData, this); + LOG.debug("staying in parserDataMode=false for response {} on {}", metaData, this); } else { + // Expect DATA frames now. + parserDataMode = true; + parser.setDataMode(true); if (LOG.isDebugEnabled()) - LOG.debug("staying in parserDataMode=false for response {} on {}", metaData, this); + LOG.debug("switching to parserDataMode=true for response {} on {}", metaData, this); } } else diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 620b319ef28f..7d327cbfb92e 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -523,7 +523,7 @@ public void sendProcessing() throws IOException * Send a 103 response as per RFC8297 * This method is called by sendError if it is passed 103. * - * @throws IOException if unable to send the 102 response + * @throws IOException if unable to send the 103 response * @see javax.servlet.http.HttpServletResponse#sendError(int) */ public void sendEarlyHint() throws IOException From 94517235f64d5e8fe770ff5285e038f25dd5fc0c Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Mon, 30 May 2022 12:16:37 +1000 Subject: [PATCH 09/11] Implement #8057 103 Early Hint Found (but not fixed) flake. --- .../java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java | 1 + 1 file changed, 1 insertion(+) diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java index e9d1b6aaeb33..8260ca5cb6ac 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java @@ -252,6 +252,7 @@ private boolean parse() if (complete) { + // TODO this can clean a pipelined response after a 1xx if (LOG.isDebugEnabled()) LOG.debug("Discarding unexpected content after response: {}", networkBuffer); networkBuffer.clear(); From 01bf0f4af2cfab3cdff8639a804a5e45ee36fcc0 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Mon, 30 May 2022 13:14:03 +1000 Subject: [PATCH 10/11] Implement #8057 103 Early Hint Fixed flake. @sbordet please review this commit! --- .../client/http/HttpReceiverOverHTTP.java | 30 +++++++++++-------- .../client/InformationalResponseTest.java | 4 +-- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java index 8260ca5cb6ac..64703060d365 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java @@ -187,9 +187,12 @@ private void process() } else if (read == 0) { - releaseNetworkBuffer(); - fillInterested(); - return; + if (networkBuffer.isEmpty()) + { + releaseNetworkBuffer(); + fillInterested(); + return; + } } else { @@ -245,19 +248,22 @@ private boolean parse() this.method = null; if (getHttpChannel().isTunnel(method, status)) return true; - } - if (networkBuffer.isEmpty()) - return false; + if (networkBuffer.isEmpty()) + return false; - if (complete) - { - // TODO this can clean a pipelined response after a 1xx - if (LOG.isDebugEnabled()) - LOG.debug("Discarding unexpected content after response: {}", networkBuffer); - networkBuffer.clear(); + if (!HttpStatus.isInformational(status)) + { + if (LOG.isDebugEnabled()) + LOG.debug("Discarding unexpected content after response {}: {}", status, networkBuffer); + LOG.info("Discarding unexpected content after response {}: {}", status, networkBuffer); + networkBuffer.clear(); + } return false; } + + if (networkBuffer.isEmpty()) + return false; } } diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/InformationalResponseTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/InformationalResponseTest.java index 55879e408dae..d0cdf4ec9e6c 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/InformationalResponseTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/InformationalResponseTest.java @@ -222,10 +222,10 @@ public void onComplete(Result result) }; scenario.client.newRequest(scenario.newURI()) .method("GET") - .timeout(10, TimeUnit.SECONDS) + .timeout(5, TimeUnit.SECONDS) .send(listener); - assertTrue(complete.await(10, TimeUnit.SECONDS)); + assertTrue(complete.await(5, TimeUnit.SECONDS)); assertThat(response.get().getStatus(), is(200)); assertThat(listener.getContentAsString(), is("OK")); assertThat(hints, contains("one", "two", "three")); From d0cfd7ca1aad7d2f04295ef654a66d59742dd37f Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Mon, 30 May 2022 14:42:15 +0200 Subject: [PATCH 11/11] Issue #8057 - Support Http Response 103 (Early Hints) Improved implementation on the client-side. Introduced HttpStatus.isInterim() for 1XX codes that are not 101. Signed-off-by: Simone Bordet --- .../java/org/eclipse/jetty/client/HttpReceiver.java | 5 ++--- .../jetty/client/http/HttpReceiverOverHTTP.java | 13 +++++-------- .../java/org/eclipse/jetty/http/HttpHeader.java | 1 + .../java/org/eclipse/jetty/http/HttpStatus.java | 11 +++++++++++ .../http2/client/http/HttpReceiverOverHTTP2.java | 3 +-- .../jetty/http2/server/HttpTransportOverHTTP2.java | 3 +-- .../client/http/internal/HttpReceiverOverHTTP3.java | 3 +-- .../server/internal/HttpTransportOverHTTP3.java | 3 +-- .../java/org/eclipse/jetty/server/HttpChannel.java | 8 ++++---- .../http/client/InformationalResponseTest.java | 2 -- 10 files changed, 27 insertions(+), 25 deletions(-) diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java index 01e662b106aa..8315e6b48cd3 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java @@ -411,9 +411,8 @@ protected boolean responseSuccess(HttpExchange exchange) ResponseNotifier notifier = getHttpDestination().getResponseNotifier(); notifier.notifySuccess(listeners, response); - // Special case for 100 Continue that cannot - // be handled by the ContinueProtocolHandler. - if (exchange.getResponse().getStatus() == HttpStatus.CONTINUE_100) + // Interim responses do not terminate the exchange. + if (HttpStatus.isInterim(exchange.getResponse().getStatus())) return true; // Mark atomically the response as terminated, with diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java index 64703060d365..b54115ea365d 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java @@ -187,12 +187,10 @@ private void process() } else if (read == 0) { - if (networkBuffer.isEmpty()) - { - releaseNetworkBuffer(); - fillInterested(); - return; - } + assert networkBuffer.isEmpty(); + releaseNetworkBuffer(); + fillInterested(); + return; } else { @@ -256,7 +254,6 @@ private boolean parse() { if (LOG.isDebugEnabled()) LOG.debug("Discarding unexpected content after response {}: {}", status, networkBuffer); - LOG.info("Discarding unexpected content after response {}: {}", status, networkBuffer); networkBuffer.clear(); } return false; @@ -379,7 +376,7 @@ public boolean messageComplete() } int status = exchange.getResponse().getStatus(); - if (status != HttpStatus.CONTINUE_100) + if (!HttpStatus.isInterim(status)) { inMessages.increment(); complete = true; diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java index 555fee7335ad..bc56e005aaef 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java @@ -88,6 +88,7 @@ public enum HttpHeader AGE("Age"), ALT_SVC("Alt-Svc"), ETAG("ETag"), + LINK("Link"), LOCATION("Location"), PROXY_AUTHENTICATE("Proxy-Authenticate"), RETRY_AFTER("Retry-After"), diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java index 63bfcccce1fc..dbfdb016fcb9 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java @@ -341,6 +341,17 @@ public static boolean isInformational(int code) return ((100 <= code) && (code <= 199)); } + /** + * Tests whether the status code is informational but not {@code 101 Switching Protocols}. + * + * @param code the code to test + * @return whether the status code is informational but not {@code 101 Switching Protocols} + */ + public static boolean isInterim(int code) + { + return isInformational(code) && code != HttpStatus.SWITCHING_PROTOCOLS_101; + } + /** * Simple test against an code to determine if it falls into the * Success message category as defined in the