diff --git a/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnection.java b/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnection.java index 3b46fbf013e..295242a908f 100644 --- a/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnection.java +++ b/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnection.java @@ -44,9 +44,6 @@ public class ALPNClientConnection extends NegotiatingClientConnection public void selected(String protocol) { - if (protocol == null || !protocols.contains(protocol)) - close(); - else - completed(protocol); + completed(protocol); } } diff --git a/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnectionFactory.java b/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnectionFactory.java index 900013fa1f4..63c2ef63231 100644 --- a/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnectionFactory.java +++ b/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnectionFactory.java @@ -110,12 +110,4 @@ public class ALPNClientConnectionFactory extends NegotiatingClientConnectionFact } throw new IllegalStateException("No ALPNProcessor for " + engine); } - - public static class ALPN extends Info - { - public ALPN(Executor executor, ClientConnectionFactory factory, List protocols) - { - super(List.of("alpn"), new ALPNClientConnectionFactory(executor, factory, protocols)); - } - } } diff --git a/jetty-alpn/jetty-alpn-java-client/src/main/java/org/eclipse/jetty/alpn/java/client/JDK9ClientALPNProcessor.java b/jetty-alpn/jetty-alpn-java-client/src/main/java/org/eclipse/jetty/alpn/java/client/JDK9ClientALPNProcessor.java index 3a455ffb024..c1c392242fa 100644 --- a/jetty-alpn/jetty-alpn-java-client/src/main/java/org/eclipse/jetty/alpn/java/client/JDK9ClientALPNProcessor.java +++ b/jetty-alpn/jetty-alpn-java-client/src/main/java/org/eclipse/jetty/alpn/java/client/JDK9ClientALPNProcessor.java @@ -75,8 +75,11 @@ public class JDK9ClientALPNProcessor implements ALPNProcessor.Client { String protocol = alpnConnection.getSSLEngine().getApplicationProtocol(); if (LOG.isDebugEnabled()) - LOG.debug("selected protocol {}", protocol); - alpnConnection.selected(protocol); + LOG.debug("selected protocol '{}'", protocol); + if (protocol != null && !protocol.isEmpty()) + alpnConnection.selected(protocol); + else + alpnConnection.selected(null); } } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/dynamic/HttpClientTransportDynamic.java b/jetty-client/src/main/java/org/eclipse/jetty/client/dynamic/HttpClientTransportDynamic.java index 732d6d843ef..2ab93cdd6a8 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/dynamic/HttpClientTransportDynamic.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/dynamic/HttpClientTransportDynamic.java @@ -19,12 +19,14 @@ package org.eclipse.jetty.client.dynamic; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.eclipse.jetty.alpn.client.ALPNClientConnection; import org.eclipse.jetty.alpn.client.ALPNClientConnectionFactory; @@ -37,6 +39,7 @@ import org.eclipse.jetty.client.MultiplexConnectionPool; import org.eclipse.jetty.client.MultiplexHttpDestination; import org.eclipse.jetty.client.Origin; import org.eclipse.jetty.client.http.HttpClientConnectionFactory; +import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.ClientConnector; @@ -105,7 +108,7 @@ public class HttpClientTransportDynamic extends AbstractConnectorHttpClientTrans factoryInfos = new Info[]{HttpClientConnectionFactory.HTTP11}; this.factoryInfos = Arrays.asList(factoryInfos); this.protocols = Arrays.stream(factoryInfos) - .flatMap(info -> info.getProtocols().stream()) + .flatMap(info -> Stream.concat(info.getProtocols(false).stream(), info.getProtocols(true).stream())) .distinct() .map(p -> p.toLowerCase(Locale.ENGLISH)) .collect(Collectors.toList()); @@ -117,9 +120,9 @@ public class HttpClientTransportDynamic extends AbstractConnectorHttpClientTrans @Override public Origin newOrigin(HttpRequest request) { - boolean ssl = HttpClient.isSchemeSecure(request.getScheme()); + boolean secure = HttpClient.isSchemeSecure(request.getScheme()); String http1 = "http/1.1"; - String http2 = ssl ? "h2" : "h2c"; + String http2 = secure ? "h2" : "h2c"; List protocols = List.of(); if (request.isVersionExplicit()) { @@ -130,16 +133,23 @@ public class HttpClientTransportDynamic extends AbstractConnectorHttpClientTrans } else { - if (ssl) + if (secure) { // There may be protocol negotiation, so preserve the order // of protocols chosen by the application. // We need to keep multiple protocols in case the protocol // is negotiated: e.g. [http/1.1, h2] negotiates [h2], but // here we don't know yet what will be negotiated. + List http = List.of("http/1.1", "h2c", "h2"); protocols = this.protocols.stream() - .filter(p -> p.equals(http1) || p.equals(http2)) - .collect(Collectors.toList()); + .filter(http::contains) + .collect(Collectors.toCollection(ArrayList::new)); + + // The http/1.1 upgrade to http/2 over TLS implicitly + // "negotiates" [h2c], so we need to remove [h2] + // because we don't want to negotiate using ALPN. + if (request.getHeaders().contains(HttpHeader.UPGRADE, "h2c")) + protocols.remove("h2"); } else { @@ -149,7 +159,7 @@ public class HttpClientTransportDynamic extends AbstractConnectorHttpClientTrans } Origin.Protocol protocol = null; if (!protocols.isEmpty()) - protocol = new Origin.Protocol(protocols, ssl && protocols.contains(http2)); + protocol = new Origin.Protocol(protocols, secure && protocols.contains(http2)); return getHttpClient().createOrigin(request, protocol); } @@ -164,32 +174,33 @@ public class HttpClientTransportDynamic extends AbstractConnectorHttpClientTrans { HttpDestination destination = (HttpDestination)context.get(HTTP_DESTINATION_CONTEXT_KEY); Origin.Protocol protocol = destination.getOrigin().getProtocol(); - ClientConnectionFactory.Info factoryInfo; + ClientConnectionFactory factory; if (protocol == null) { // Use the default ClientConnectionFactory. - factoryInfo = factoryInfos.get(0); + factory = factoryInfos.get(0).getClientConnectionFactory(); } else { if (destination.isSecure() && protocol.isNegotiate()) { - factoryInfo = new ALPNClientConnectionFactory.ALPN(getClientConnector().getExecutor(), this::newNegotiatedConnection, protocol.getProtocols()); + factory = new ALPNClientConnectionFactory(getClientConnector().getExecutor(), this::newNegotiatedConnection, protocol.getProtocols()); } else { - factoryInfo = findClientConnectionFactoryInfo(protocol.getProtocols()) - .orElseThrow(() -> new IOException("Cannot find " + ClientConnectionFactory.class.getSimpleName() + " for " + protocol)); + factory = findClientConnectionFactoryInfo(protocol.getProtocols(), destination.isSecure()) + .orElseThrow(() -> new IOException("Cannot find " + ClientConnectionFactory.class.getSimpleName() + " for " + protocol)) + .getClientConnectionFactory(); } } - return factoryInfo.getClientConnectionFactory().newConnection(endPoint, context); + return factory.newConnection(endPoint, context); } public void upgrade(EndPoint endPoint, Map context) { HttpDestination destination = (HttpDestination)context.get(HTTP_DESTINATION_CONTEXT_KEY); Origin.Protocol protocol = destination.getOrigin().getProtocol(); - Info info = findClientConnectionFactoryInfo(protocol.getProtocols()) + Info info = findClientConnectionFactoryInfo(protocol.getProtocols(), destination.isSecure()) .orElseThrow(() -> new IllegalStateException("Cannot find " + ClientConnectionFactory.class.getSimpleName() + " to upgrade to " + protocol)); info.upgrade(endPoint, context); } @@ -200,13 +211,22 @@ public class HttpClientTransportDynamic extends AbstractConnectorHttpClientTrans { ALPNClientConnection alpnConnection = (ALPNClientConnection)endPoint.getConnection(); String protocol = alpnConnection.getProtocol(); - if (LOG.isDebugEnabled()) - LOG.debug("ALPN negotiated {} among {}", protocol, alpnConnection.getProtocols()); - if (protocol == null) - throw new IOException("Could not negotiate protocol among " + alpnConnection.getProtocols()); - List protocols = List.of(protocol); - Info factoryInfo = findClientConnectionFactoryInfo(protocols) + Info factoryInfo; + if (protocol != null) + { + if (LOG.isDebugEnabled()) + LOG.debug("ALPN negotiated {} among {}", protocol, alpnConnection.getProtocols()); + List protocols = List.of(protocol); + factoryInfo = findClientConnectionFactoryInfo(protocols, true) .orElseThrow(() -> new IOException("Cannot find " + ClientConnectionFactory.class.getSimpleName() + " for negotiated protocol " + protocol)); + } + else + { + // Server does not support ALPN, let's try the first protocol. + factoryInfo = factoryInfos.get(0); + if (LOG.isDebugEnabled()) + LOG.debug("No ALPN protocol, using {}", factoryInfo); + } return factoryInfo.getClientConnectionFactory().newConnection(endPoint, context); } catch (Throwable failure) @@ -216,10 +236,10 @@ public class HttpClientTransportDynamic extends AbstractConnectorHttpClientTrans } } - private Optional findClientConnectionFactoryInfo(List protocols) + private Optional findClientConnectionFactoryInfo(List protocols, boolean secure) { return factoryInfos.stream() - .filter(info -> info.matches(protocols)) + .filter(info -> info.matches(protocols, secure)) .findFirst(); } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpClientConnectionFactory.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpClientConnectionFactory.java index 418616175f6..2063596c359 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpClientConnectionFactory.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpClientConnectionFactory.java @@ -21,12 +21,16 @@ package org.eclipse.jetty.client.http; import java.util.List; import java.util.Map; +import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic; import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.EndPoint; public class HttpClientConnectionFactory implements ClientConnectionFactory { - public static final Info HTTP11 = new Info(List.of("http/1.1"), new HttpClientConnectionFactory()); + /** + *

Representation of the {@code HTTP/1.1} application protocol used by {@link HttpClientTransportDynamic}.

+ */ + public static final Info HTTP11 = new HTTP11(new HttpClientConnectionFactory()); @Override public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map context) @@ -34,4 +38,26 @@ public class HttpClientConnectionFactory implements ClientConnectionFactory HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, context); return customize(connection, context); } + + private static class HTTP11 extends Info + { + private static final List protocols = List.of("http/1.1"); + + private HTTP11(ClientConnectionFactory factory) + { + super(factory); + } + + @Override + public List getProtocols(boolean secure) + { + return protocols; + } + + @Override + public String toString() + { + return String.format("%s@%x%s", getClass().getSimpleName(), hashCode(), protocols); + } + } } diff --git a/jetty-documentation/pom.xml b/jetty-documentation/pom.xml index 2c5a472ec73..3ad33457458 100644 --- a/jetty-documentation/pom.xml +++ b/jetty-documentation/pom.xml @@ -59,8 +59,13 @@ ${project.version} - org.eclipse.jetty - jetty-io + org.eclipse.jetty.http2 + http2-http-client-transport + ${project.version} + + + org.eclipse.jetty.fcgi + fcgi-client ${project.version} diff --git a/jetty-documentation/src/main/asciidoc/distribution-guide/http2/introduction.adoc b/jetty-documentation/src/main/asciidoc/distribution-guide/http2/introduction.adoc index 92fa37c7e8c..2c81f182022 100644 --- a/jetty-documentation/src/main/asciidoc/distribution-guide/http2/introduction.adoc +++ b/jetty-documentation/src/main/asciidoc/distribution-guide/http2/introduction.adoc @@ -50,5 +50,5 @@ The Jetty HTTP/2 implementation consists of the following sub-projects (each pro 2. `http2-hpack`: Contains the HTTP/2 HPACK implementation for HTTP header compression. 3. `http2-server`: Provides the server-side implementation of HTTP/2. 4. `http2-client`: Provides the implementation of HTTP/2 client with a low level HTTP/2 API, dealing with HTTP/2 streams, frames, etc. -5. `http2-http-client-transport`: Provides the implementation of the HTTP/2 transport for `HttpClient` (see xref:http-client[]). +5. `http2-http-client-transport`: Provides the implementation of the HTTP/2 transport for `HttpClient` (see xref:client-http[this section]). Applications can use the higher level API provided by `HttpClient` to send HTTP requests and receive HTTP responses, and the HTTP/2 transport will take care of converting them in HTTP/2 format (see also https://webtide.com/http2-support-for-httpclient/[this blog entry]). diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-api.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-api.adoc index 4765fb47a18..b369bb33e91 100644 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-api.adoc +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-api.adoc @@ -34,7 +34,7 @@ include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=simpleBl The method `HttpClient.GET(...)` performs a HTTP `GET` request to the given URI and returns a `ContentResponse` when the request/response conversation completes successfully. The `ContentResponse` object contains the HTTP response information: status code, headers and possibly content. -The content length is limited by default to 2 MiB; for larger content see xref:http-client-response-content[]. +The content length is limited by default to 2 MiB; for larger content see xref:client-http-content-response[]. If you want to customize the request, for example by issuing a `HEAD` request instead of a `GET`, and simulating a browser user agent, you can do it in this way: @@ -190,7 +190,7 @@ which allows applications to write request content when it is available to the ` include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tags=outputStreamRequestContent] ---- -[[http-client-response-content]] +[[client-http-content-response]] ===== Response Content Handling Jetty's `HttpClient` allows applications to handle response content in different ways. diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-transport.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-transport.adoc index 21870cc9460..518483296b4 100644 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-transport.adoc +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/client/http/client-http-transport.adoc @@ -16,21 +16,25 @@ // ======================================================================== // -[[http-client-transport]] -=== Pluggable Transports +[[client-http-transport]] +=== HttpClient Pluggable Transports -Jetty's HTTP client can be configured to use different transports to carry the semantic of HTTP requests and responses. +Jetty's `HttpClient` can be configured to use different transports to carry the +semantic of HTTP requests and responses. -This means that the intention of a client to request resource `/index.html` using the `GET` method can be carried over the network in different formats. +This means that the intention of a client to request resource `/index.html` +using the `GET` method can be carried over the network in different formats. -A HTTP client transport is the component that is in charge of converting a high-level, semantic, HTTP requests such as "GET resource /index.html" into the specific format understood by the server (for example, HTTP/2), and to convert the server response from the specific format (HTTP/2) into high-level, semantic objects that can be used by applications. +A `HttpClient` transport is the component that is in charge of converting a +high-level, semantic, HTTP requests such as "`GET` resource ``/index.html``" +into the specific format understood by the server (for example, HTTP/2), and to +convert the server response from the specific format (HTTP/2) into high-level, +semantic objects that can be used by applications. -In this way, applications are not aware of the actual protocol being used. -This allows them to write their logic against a high-level API that hides the details of the specific protocol being used over the network. +The most common protocol format is HTTP/1.1, a textual protocol with lines +separated by `\r\n`: -The most common protocol format is HTTP/1.1, a text-based protocol with lines separated by `\r\n`: - -[source, screen, subs="{sub-order}"] +[source,screen,subs="{sub-order}"] ---- GET /index.html HTTP/1.1\r\n Host: domain.com\r\n @@ -40,7 +44,7 @@ Host: domain.com\r\n However, the same request can be made using FastCGI, a binary protocol: -[source, screen, subs="{sub-order}"] +[source,screen,subs="{sub-order}"] ---- x01 x01 x00 x01 x00 x08 x00 x00 x00 x01 x01 x00 x00 x00 x00 x00 @@ -52,64 +56,153 @@ x0C x0B D O C U M E ... ---- -Similarly, HTTP/2 is a binary protocol that transports the same information in a yet different format. +Similarly, HTTP/2 is a binary protocol that transports the same information +in a yet different format. +A protocol may be _negotiated_ between client and server. A request for a +resource may be sent using one protocol (for example, HTTP/1.1), but the +response may arrive in a different protocol (for example, HTTP/2). + +`HttpClient` supports 3 static transports, each speaking only one protocol: +link:#client-http-transport-http11[HTTP/1.1], +link:#client-http-transport-http2[HTTP/2] and +link:#client-http-transport-fcgi[FastCGI], +all of them with 2 variants: clear-text and TLS encrypted. + +`HttpClient` also supports one +link:#client-http-transport-dynamic[dynamic transport], +that can speak different protocols and can select the right protocol by +negotiating it with the server or by explicit indication from applications. + +Applications are typically not aware of the actual protocol being used. +This allows them to write their logic against a high-level API that hides the +details of the specific protocol being used over the network. + +[[client-http-transport-http11]] ==== HTTP/1.1 Transport HTTP/1.1 is the default transport. -[source, java, subs="{sub-order}"] +[source,java,indent=0] ---- -// No transport specified, using default. -HttpClient client = new HttpClient(); -client.start(); +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tag=defaultTransport] ---- -If you want to customize the HTTP/1.1 transport, you can explicitly configure `HttpClient` in this way: +If you want to customize the HTTP/1.1 transport, you can explicitly configure +it in this way: -[source, java, subs="{sub-order}"] +[source,java,indent=0] ---- -int selectors = 1; -HttpClientTransportOverHTTP transport = new HttpClientTransportOverHTTP(selectors); - -HttpClient client = new HttpClient(transport, null); -client.start(); +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tag=http11Transport] ---- -The example above allows you to customize the number of NIO selectors that `HttpClient` will be using. - +[[client-http-transport-http2]] ==== HTTP/2 Transport The HTTP/2 transport can be configured in this way: -[source, java, subs="{sub-order}"] +[source,java,indent=0] ---- -HTTP2Client h2Client = new HTTP2Client(); -h2Client.setSelectors(1); -HttpClientTransportOverHTTP2 transport = new HttpClientTransportOverHTTP2(h2Client); - -HttpClient client = new HttpClient(transport, null); -client.start(); +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tag=http2Transport] ---- -`HTTP2Client` is the lower-level client that provides an API based on HTTP/2 concepts such as _sessions_, _streams_ and _frames_ that are specific to HTTP/2. +`HTTP2Client` is the lower-level client that provides an API based on HTTP/2 +concepts such as _sessions_, _streams_ and _frames_ that are specific to HTTP/2. +See link:#client-http2[the HTTP/2 client section] for more information. -`HttpClientTransportOverHTTP2` uses `HTTP2Client` to format high-level semantic HTTP requests ("GET resource /index.html") into the HTTP/2 specific format. +`HttpClientTransportOverHTTP2` uses `HTTP2Client` to format high-level semantic +HTTP requests (like "GET resource /index.html") into the HTTP/2 specific format. +[[client-http-transport-fcgi]] ==== FastCGI Transport The FastCGI transport can be configured in this way: -[source, java, subs="{sub-order}"] +[source,java,indent=0] ---- -int selectors = 1; -String scriptRoot = "/var/www/wordpress"; -HttpClientTransportOverFCGI transport = new HttpClientTransportOverFCGI(selectors, false, scriptRoot); - -HttpClient client = new HttpClient(transport, null); -client.start(); +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tag=fcgiTransport] ---- -In order to make requests using the FastCGI transport, you need to have a FastCGI server such as https://en.wikipedia.org/wiki/PHP#PHPFPM[PHP-FPM] (see also http://php.net/manual/en/install.fpm.php). +In order to make requests using the FastCGI transport, you need to have a +FastCGI server such as https://en.wikipedia.org/wiki/PHP#PHPFPM[PHP-FPM] +(see also http://php.net/manual/en/install.fpm.php). -The FastCGI transport is primarily used by Jetty's link:#fastcgi[FastCGI support] to serve PHP pages (WordPress for example). +The FastCGI transport is primarily used by Jetty's link:#fastcgi[FastCGI support] +to serve PHP pages (WordPress for example). + +[[client-http-transport-dynamic]] +==== Dynamic Transport + +The static transports work well if you know in advance the protocol you want +to speak with the server, or if the server only supports one protocol (such +as FastCGI). + +With the advent of HTTP/2, however, servers are now able to support multiple +protocols, at least both HTTP/1.1 and HTTP/2. + +The HTTP/2 protocol is typically negotiated between client and server. +This negotiation can happen via ALPN, a TLS extension that allows the client +to tell the server the list of protocol that the client supports, so that the +server can pick one of the client supported protocols that also the server +supports; or via HTTP/1.1 upgrade by means of the `Upgrade` header. + +Applications can configure the dynamic transport with one or more +_application_ protocols such as HTTP/1.1 or HTTP/2. The implementation will +take care of using TLS for HTTPS URIs, using ALPN, negotiating protocols, +upgrading from one protocol to another, etc. + +By default, the dynamic transport only speaks HTTP/1.1: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tag=dynamicDefault] +---- + +The dynamic transport can be configured with just one protocol, making it +equivalent to the corresponding static transport: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tag=dynamicOneProtocol] +---- + +The dynamic transport, however, has been implemented to support multiple +transports, in particular both HTTP/1.1 and HTTP/2: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tag=dynamicH1H2] +---- + +IMPORTANT: The order in which the protocols are specified to +`HttpClientTransportDynamic` indicates what is the client preference. +If the protocol is negotiated via ALPN, it is the server that decides what is +the protocol to use for the communication, regardless of the client preference. +If the protocol is not negotiated, the client preference is honored. + +Provided that the server supports both HTTP/1.1 and HTTP/2 clear-text, client +applications can explicitly hint the version they want to use: + +[source,java,indent=0] +---- +include::../../{doc_code}/embedded/client/http/HTTPClientDocs.java[tag=dynamicClearText] +---- + +In case of TLS encrypted communication using the HTTPS scheme, things are a +little more complicated. + +If the client application explicitly specifies the HTTP version, then ALPN +is not used on the client. By specifying the HTTP version explicitly, the +client application has prior-knowledge of what HTTP version the server +supports, and therefore ALPN is not needed. +If the server does not support the HTTP version chosen by the client, then +the communication will fail. + +If the client application does not explicitly specify the HTTP version, +then ALPN will be used on the client. +If the server also supports ALPN, then the protocol will be negotiated via +ALPN and the server will choose the protocol to use. +If the server does not support ALPN, the client will try to use the first +protocol configured in `HttpClientTransportDynamic`, and the communication +may succeed or fail depending on whether the server supports the protocol +chosen by the client. diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/server/embedding/examples/embedded-minimal-servlet.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/server/embedding/examples/embedded-minimal-servlet.adoc index 6d440c591fd..068f62faa1d 100644 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/server/embedding/examples/embedded-minimal-servlet.adoc +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/server/embedding/examples/embedded-minimal-servlet.adoc @@ -22,7 +22,7 @@ This example shows the bare minimum required for deploying a servlet into Jetty. Note that this is strictly a servlet, not a servlet in the context of a web application, that example comes later. This is purely just a servlet deployed and mounted on a context and able to process requests. -This example is excellent for situations where you have a simple servlet that you need to unit test, just mount it on a context and issue requests using your favorite http client library (like our Jetty client found in xref:http-client[]). +This example is excellent for situations where you have a simple servlet that you need to unit test, just mount it on a context and issue requests using your favorite http client library (like our Jetty client found in xref:client-http[]). [source, java, subs="{sub-order}"] ---- diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/server/embedding/examples/embedded-one-webapp.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/server/embedding/examples/embedded-one-webapp.adoc index 048d5ddd4d3..61c06a4d145 100644 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/server/embedding/examples/embedded-one-webapp.adoc +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/server/embedding/examples/embedded-one-webapp.adoc @@ -21,7 +21,7 @@ This example shows how to deploy a simple webapp with an embedded instance of Jetty. This is useful when you want to manage the lifecycle of a server programmatically, either within a production application or as a simple way to deploying and debugging a full scale application deployment. -In many ways it is easier then traditional deployment since you control the classpath yourself, making this easy to wire up in a test case in Maven and issue requests using your favorite http client library (like our Jetty client found in xref:http-client[]). +In many ways it is easier then traditional deployment since you control the classpath yourself, making this easy to wire up in a test case in Maven and issue requests using your favorite http client library (like our Jetty client found in xref:client-http[]). [source, java, subs="{sub-order}"] ---- diff --git a/jetty-documentation/src/main/java/embedded/client/http/HTTPClientDocs.java b/jetty-documentation/src/main/java/embedded/client/http/HTTPClientDocs.java index decd799c0d4..f72655f916f 100644 --- a/jetty-documentation/src/main/java/embedded/client/http/HTTPClientDocs.java +++ b/jetty-documentation/src/main/java/embedded/client/http/HTTPClientDocs.java @@ -41,6 +41,8 @@ import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic; +import org.eclipse.jetty.client.http.HttpClientConnectionFactory; +import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; import org.eclipse.jetty.client.util.AsyncRequestContent; import org.eclipse.jetty.client.util.BasicAuthentication; import org.eclipse.jetty.client.util.BufferingResponseListener; @@ -52,9 +54,16 @@ import org.eclipse.jetty.client.util.InputStreamResponseListener; import org.eclipse.jetty.client.util.OutputStreamRequestContent; import org.eclipse.jetty.client.util.PathRequestContent; import org.eclipse.jetty.client.util.StringRequestContent; +import org.eclipse.jetty.fcgi.client.http.HttpClientTransportOverFCGI; +import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.client.http.ClientConnectionFactoryOverHTTP2; +import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2; import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.HttpCookieStore; @@ -666,4 +675,129 @@ public class HTTPClientDocs ContentResponse response = httpClient.newRequest(serverURI).send(); // end::proxyAuthentication[] } + + public void defaultTransport() throws Exception + { + // tag::defaultTransport[] + // No transport specified, using default. + HttpClient httpClient = new HttpClient(); + httpClient.start(); + // end::defaultTransport[] + } + + public void http11Transport() throws Exception + { + // tag::http11Transport[] + // Configure HTTP/1.1 transport. + HttpClientTransportOverHTTP transport = new HttpClientTransportOverHTTP(); + transport.setHeaderCacheSize(16384); + + HttpClient client = new HttpClient(transport); + client.start(); + // end::http11Transport[] + } + + public void http2Transport() throws Exception + { + // tag::http2Transport[] + // The HTTP2Client powers the HTTP/2 transport. + HTTP2Client h2Client = new HTTP2Client(); + h2Client.setInitialSessionRecvWindow(64 * 1024 * 1024); + + // Create and configure the HTTP/2 transport. + HttpClientTransportOverHTTP2 transport = new HttpClientTransportOverHTTP2(h2Client); + transport.setUseALPN(true); + + HttpClient client = new HttpClient(transport); + client.start(); + // end::http2Transport[] + } + + public void fcgiTransport() throws Exception + { + // tag::fcgiTransport[] + String scriptRoot = "/var/www/wordpress"; + HttpClientTransportOverFCGI transport = new HttpClientTransportOverFCGI(scriptRoot); + + HttpClient client = new HttpClient(transport); + client.start(); + // end::fcgiTransport[] + } + + public void dynamicDefault() throws Exception + { + // tag::dynamicDefault[] + // Dynamic transport speaks HTTP/1.1 by default. + HttpClientTransportDynamic transport = new HttpClientTransportDynamic(); + + HttpClient client = new HttpClient(transport); + client.start(); + // end::dynamicDefault[] + } + + public void dynamicOneProtocol() throws Exception + { + // tag::dynamicOneProtocol[] + ClientConnector connector = new ClientConnector(); + + // Equivalent to HttpClientTransportOverHTTP. + HttpClientTransportDynamic http11Transport = new HttpClientTransportDynamic(connector, HttpClientConnectionFactory.HTTP11); + + // Equivalent to HttpClientTransportOverHTTP2. + HTTP2Client http2Client = new HTTP2Client(connector); + HttpClientTransportDynamic http2Transport = new HttpClientTransportDynamic(connector, new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client)); + // end::dynamicOneProtocol[] + } + + public void dynamicH1H2() throws Exception + { + // tag::dynamicH1H2[] + ClientConnector connector = new ClientConnector(); + + ClientConnectionFactory.Info http1 = HttpClientConnectionFactory.HTTP11; + + HTTP2Client http2Client = new HTTP2Client(connector); + ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + + HttpClientTransportDynamic transport = new HttpClientTransportDynamic(connector, http1, http2); + + HttpClient client = new HttpClient(transport); + client.start(); + // end::dynamicH1H2[] + } + + public void dynamicClearText() throws Exception + { + // tag::dynamicClearText[] + ClientConnector connector = new ClientConnector(); + ClientConnectionFactory.Info http1 = HttpClientConnectionFactory.HTTP11; + HTTP2Client http2Client = new HTTP2Client(connector); + ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + HttpClientTransportDynamic transport = new HttpClientTransportDynamic(connector, http1, http2); + HttpClient client = new HttpClient(transport); + client.start(); + + // The server supports both HTTP/1.1 and HTTP/2 clear-text on port 8080. + + // Make a clear-text request without explicit version. + // The first protocol specified to HttpClientTransportDynamic + // is picked, in this example will be HTTP/1.1. + ContentResponse http1Response = client.newRequest("host", 8080).send(); + + // Make a clear-text request with explicit version. + // Clear-text HTTP/2 is used for this request. + ContentResponse http2Response = client.newRequest("host", 8080) + // Specify the version explicitly. + .version(HttpVersion.HTTP_2) + .send(); + + // Make a clear-text upgrade request from HTTP/1.1 to HTTP/2. + // The request will start as HTTP/1.1, but the response will be HTTP/2. + ContentResponse upgradedResponse = client.newRequest("host", 8080) + .header(HttpHeader.UPGRADE, "h2c") + .header(HttpHeader.HTTP2_SETTINGS, "") + .header(HttpHeader.CONNECTION, "Upgrade, HTTP2-Settings") + .send(); + // end::dynamicClearText[] + } } diff --git a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/ClientConnectionFactoryOverHTTP2.java b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/ClientConnectionFactoryOverHTTP2.java index 1f17ae4bfcd..2493fbf6678 100644 --- a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/ClientConnectionFactoryOverHTTP2.java +++ b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/ClientConnectionFactoryOverHTTP2.java @@ -26,6 +26,8 @@ import java.util.Map; import org.eclipse.jetty.client.HttpClientTransport; import org.eclipse.jetty.client.HttpDestination; import org.eclipse.jetty.client.api.Connection; +import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic; +import org.eclipse.jetty.client.http.HttpClientConnectionFactory; import org.eclipse.jetty.http2.client.HTTP2Client; import org.eclipse.jetty.http2.client.HTTP2ClientConnectionFactory; import org.eclipse.jetty.io.ClientConnectionFactory; @@ -56,19 +58,25 @@ public class ClientConnectionFactoryOverHTTP2 extends ContainerLifeCycle impleme return factory.newConnection(endPoint, context); } - public static class H2 extends Info + /** + *

Representation of the {@code HTTP/2} application protocol used by {@link HttpClientTransportDynamic}.

+ * + * @see HttpClientConnectionFactory#HTTP11 + */ + public static class HTTP2 extends Info { - public H2(HTTP2Client client) - { - super(List.of("h2"), new ClientConnectionFactoryOverHTTP2(client)); - } - } + private static final List protocols = List.of("h2", "h2c"); + private static final List h2c = List.of("h2c"); - public static class H2C extends Info - { - public H2C(HTTP2Client client) + public HTTP2(HTTP2Client client) { - super(List.of("h2c"), new ClientConnectionFactoryOverHTTP2(client)); + super(new ClientConnectionFactoryOverHTTP2(client)); + } + + @Override + public List getProtocols(boolean secure) + { + return secure ? protocols : h2c; } @Override @@ -119,5 +127,11 @@ public class ClientConnectionFactoryOverHTTP2 extends ContainerLifeCycle impleme throw new UncheckedIOException(x); } } + + @Override + public String toString() + { + return String.format("%s@%x%s", getClass().getSimpleName(), hashCode(), protocols); + } } } diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java index 4e555e7878e..d66a5901b38 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java @@ -66,26 +66,21 @@ public interface ClientConnectionFactory } /** - *

A holder for a list of protocol strings identifying a network protocol + *

A holder for a list of protocol strings identifying an application protocol * (for example {@code ["h2", "h2-17", "h2-16"]}) and a {@link ClientConnectionFactory} * that creates connections that speak that network protocol.

*/ - public static class Info extends ContainerLifeCycle + public abstract static class Info extends ContainerLifeCycle { - private final List protocols; private final ClientConnectionFactory factory; - public Info(List protocols, ClientConnectionFactory factory) + public Info(ClientConnectionFactory factory) { - this.protocols = protocols; this.factory = factory; addBean(factory); } - public List getProtocols() - { - return protocols; - } + public abstract List getProtocols(boolean secure); public ClientConnectionFactory getClientConnectionFactory() { @@ -98,20 +93,14 @@ public interface ClientConnectionFactory * @param candidates the candidates to match against * @return whether one of the protocols of this class is present in the candidates */ - public boolean matches(List candidates) + public boolean matches(List candidates, boolean secure) { - return protocols.stream().anyMatch(p -> candidates.stream().anyMatch(c -> c.equalsIgnoreCase(p))); + return getProtocols(secure).stream().anyMatch(p -> candidates.stream().anyMatch(c -> c.equalsIgnoreCase(p))); } public void upgrade(EndPoint endPoint, Map context) { throw new UnsupportedOperationException(this + " does not support upgrade to another protocol"); } - - @Override - public String toString() - { - return String.format("%s@%x%s", getClass().getSimpleName(), hashCode(), protocols); - } } } diff --git a/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketOverHTTP2Test.java b/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketOverHTTP2Test.java index 1cd8a05caa4..cd590d1507f 100644 --- a/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketOverHTTP2Test.java +++ b/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketOverHTTP2Test.java @@ -152,7 +152,7 @@ public class WebSocketOverHTTP2Test @Test public void testWebSocketOverDynamicHTTP2() throws Exception { - testWebSocketOverDynamicTransport(clientConnector -> new ClientConnectionFactoryOverHTTP2.H2C(new HTTP2Client(clientConnector))); + testWebSocketOverDynamicTransport(clientConnector -> new ClientConnectionFactoryOverHTTP2.HTTP2(new HTTP2Client(clientConnector))); } private void testWebSocketOverDynamicTransport(Function protocolFn) throws Exception @@ -184,7 +184,7 @@ public class WebSocketOverHTTP2Test AbstractHTTP2ServerConnectionFactory h2c = connector.getBean(AbstractHTTP2ServerConnectionFactory.class); h2c.setConnectProtocolEnabled(false); - startClient(clientConnector -> new ClientConnectionFactoryOverHTTP2.H2C(new HTTP2Client(clientConnector))); + startClient(clientConnector -> new ClientConnectionFactoryOverHTTP2.HTTP2(new HTTP2Client(clientConnector))); EventSocket wsEndPoint = new EventSocket(); URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/ws/echo"); @@ -220,7 +220,7 @@ public class WebSocketOverHTTP2Test } }); - startClient(clientConnector -> new ClientConnectionFactoryOverHTTP2.H2(new HTTP2Client(clientConnector))); + startClient(clientConnector -> new ClientConnectionFactoryOverHTTP2.HTTP2(new HTTP2Client(clientConnector))); // Connect and send immediately a message, so the message // arrives to the server while the server is still upgrading. @@ -242,7 +242,7 @@ public class WebSocketOverHTTP2Test public void testWebSocketConnectPortDoesNotExist() throws Exception { startServer(); - startClient(clientConnector -> new ClientConnectionFactoryOverHTTP2.H2(new HTTP2Client(clientConnector))); + startClient(clientConnector -> new ClientConnectionFactoryOverHTTP2.HTTP2(new HTTP2Client(clientConnector))); EventSocket wsEndPoint = new EventSocket(); URI uri = URI.create("ws://localhost:" + (connector.getLocalPort() + 1) + "/ws/echo"); @@ -259,7 +259,7 @@ public class WebSocketOverHTTP2Test public void testWebSocketNotFound() throws Exception { startServer(); - startClient(clientConnector -> new ClientConnectionFactoryOverHTTP2.H2(new HTTP2Client(clientConnector))); + startClient(clientConnector -> new ClientConnectionFactoryOverHTTP2.HTTP2(new HTTP2Client(clientConnector))); EventSocket wsEndPoint = new EventSocket(); URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/nothing"); @@ -276,7 +276,7 @@ public class WebSocketOverHTTP2Test public void testNotNegotiated() throws Exception { startServer(); - startClient(clientConnector -> new ClientConnectionFactoryOverHTTP2.H2(new HTTP2Client(clientConnector))); + startClient(clientConnector -> new ClientConnectionFactoryOverHTTP2.HTTP2(new HTTP2Client(clientConnector))); EventSocket wsEndPoint = new EventSocket(); URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/ws/null"); @@ -293,7 +293,7 @@ public class WebSocketOverHTTP2Test public void testThrowFromCreator() throws Exception { startServer(); - startClient(clientConnector -> new ClientConnectionFactoryOverHTTP2.H2(new HTTP2Client(clientConnector))); + startClient(clientConnector -> new ClientConnectionFactoryOverHTTP2.HTTP2(new HTTP2Client(clientConnector))); CountDownLatch latch = new CountDownLatch(1); connector.addBean(new HttpChannel.Listener() @@ -327,7 +327,7 @@ public class WebSocketOverHTTP2Test public void testServerConnectionClose() throws Exception { startServer(); - startClient(clientConnector -> new ClientConnectionFactoryOverHTTP2.H2(new HTTP2Client(clientConnector))); + startClient(clientConnector -> new ClientConnectionFactoryOverHTTP2.HTTP2(new HTTP2Client(clientConnector))); EventSocket wsEndPoint = new EventSocket(); URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/ws/connectionClose"); diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTransportDynamicTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTransportDynamicTest.java index 8649870c55a..4ce25b5eb2e 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTransportDynamicTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTransportDynamicTest.java @@ -42,7 +42,7 @@ import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic; import org.eclipse.jetty.client.http.HttpClientConnectionFactory; import org.eclipse.jetty.client.util.BufferingResponseListener; -import org.eclipse.jetty.client.util.BytesContentProvider; +import org.eclipse.jetty.client.util.BytesRequestContent; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpScheme; @@ -244,7 +244,7 @@ public class HttpClientTransportDynamicTest startServer(this::h1H2C, new EmptyServerHandler()); ClientConnector clientConnector = new ClientConnector(); HTTP2Client http2Client = new HTTP2Client(clientConnector); - ClientConnectionFactory.Info h2c = new ClientConnectionFactoryOverHTTP2.H2C(http2Client); + ClientConnectionFactory.Info h2c = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); startClient(clientConnector, h2c); ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) // .version(HttpVersion.HTTP_2) @@ -273,14 +273,15 @@ public class HttpClientTransportDynamicTest clientConnector.setSslContextFactory(newClientSslContextFactory()); HttpClientConnectionFactory.Info h1 = HttpClientConnectionFactory.HTTP11; HTTP2Client http2Client = new HTTP2Client(clientConnector); - ClientConnectionFactory.Info h2c = new ClientConnectionFactoryOverHTTP2.H2C(http2Client); - HttpClientTransportDynamic transport = new HttpClientTransportDynamic(clientConnector, h1, h2c) + ClientConnectionFactory.Info http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + HttpClientTransportDynamic transport = new HttpClientTransportDynamic(clientConnector, h1, http2) { @Override public Origin newOrigin(HttpRequest request) { // Use prior-knowledge, i.e. negotiate==false. - List protocols = HttpVersion.HTTP_2 == request.getVersion() ? h2c.getProtocols() : h1.getProtocols(); + boolean secure = HttpClient.isSchemeSecure(request.getScheme()); + List protocols = HttpVersion.HTTP_2 == request.getVersion() ? http2.getProtocols(secure) : h1.getProtocols(secure); return new Origin(request.getScheme(), request.getHost(), request.getPort(), request.getTag(), new Origin.Protocol(protocols, false)); } }; @@ -320,8 +321,8 @@ public class HttpClientTransportDynamicTest startServer(this::sslAlpnH1H2, new EmptyServerHandler()); ClientConnector clientConnector = new ClientConnector(); HTTP2Client http2Client = new HTTP2Client(clientConnector); - ClientConnectionFactory.Info h2 = new ClientConnectionFactoryOverHTTP2.H2(http2Client); - startClient(clientConnector, HttpClientConnectionFactory.HTTP11, h2); + ClientConnectionFactory.Info http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + startClient(clientConnector, HttpClientConnectionFactory.HTTP11, http2); // Make a request, should be HTTP/1.1 because of the order of protocols on server. ContentResponse h1cResponse = client.newRequest("localhost", connector.getLocalPort()) @@ -355,8 +356,8 @@ public class HttpClientTransportDynamicTest startServer(this::sslAlpnH1, new EmptyServerHandler()); ClientConnector clientConnector = new ClientConnector(); HTTP2Client http2Client = new HTTP2Client(clientConnector); - ClientConnectionFactory.Info h2 = new ClientConnectionFactoryOverHTTP2.H2(http2Client); - startClient(clientConnector, h2, HttpClientConnectionFactory.HTTP11); + ClientConnectionFactory.Info http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + startClient(clientConnector, http2, HttpClientConnectionFactory.HTTP11); // The client prefers h2 over h1, and use of TLS and ALPN will allow the fallback to h1. ContentResponse h1cResponse = client.newRequest("localhost", connector.getLocalPort()) @@ -372,8 +373,8 @@ public class HttpClientTransportDynamicTest startServer(this::h1, new EmptyServerHandler()); ClientConnector clientConnector = new ClientConnector(); HTTP2Client http2Client = new HTTP2Client(clientConnector); - ClientConnectionFactory.Info h2c = new ClientConnectionFactoryOverHTTP2.H2C(http2Client); - startClient(clientConnector, HttpClientConnectionFactory.HTTP11, h2c); + ClientConnectionFactory.Info http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + startClient(clientConnector, HttpClientConnectionFactory.HTTP11, http2); // The client forces HTTP/2, but the server cannot speak it, so the request fails. // There is no fallback to HTTP/1 because the protocol version is set explicitly. @@ -450,9 +451,8 @@ public class HttpClientTransportDynamicTest server.start(); ClientConnector clientConnector = new ClientConnector(); HTTP2Client http2Client = new HTTP2Client(clientConnector); - ClientConnectionFactory.Info h2c = new ClientConnectionFactoryOverHTTP2.H2C(http2Client); - ClientConnectionFactory.Info h2 = new ClientConnectionFactoryOverHTTP2.H2(http2Client); - startClient(clientConnector, h2, HttpClientConnectionFactory.HTTP11, h2c); + ClientConnectionFactory.Info http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + startClient(clientConnector, HttpClientConnectionFactory.HTTP11, http2); // Make a clear-text request using HTTP/1.1. ContentResponse h1cResponse = client.newRequest("localhost", clearConnector.getLocalPort()) @@ -510,8 +510,8 @@ public class HttpClientTransportDynamicTest }); ClientConnector clientConnector = new ClientConnector(); HTTP2Client http2Client = new HTTP2Client(clientConnector); - ClientConnectionFactory.Info h2c = new ClientConnectionFactoryOverHTTP2.H2C(http2Client); - startClient(clientConnector, HttpClientConnectionFactory.HTTP11, h2c); + ClientConnectionFactory.Info http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + startClient(clientConnector, HttpClientConnectionFactory.HTTP11, http2); // Make an upgrade request from HTTP/1.1 to H2C. ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) @@ -584,8 +584,8 @@ public class HttpClientTransportDynamicTest }); ClientConnector clientConnector = new ClientConnector(); HTTP2Client http2Client = new HTTP2Client(clientConnector); - ClientConnectionFactory.Info h2c = new ClientConnectionFactoryOverHTTP2.H2C(http2Client); - startClient(clientConnector, HttpClientConnectionFactory.HTTP11, h2c); + ClientConnectionFactory.Info http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + startClient(clientConnector, HttpClientConnectionFactory.HTTP11, http2); int proxyPort = connector.getLocalPort(); // The proxy speaks both http/1.1 and h2c. @@ -622,8 +622,8 @@ public class HttpClientTransportDynamicTest }); ClientConnector clientConnector = new ClientConnector(); HTTP2Client http2Client = new HTTP2Client(clientConnector); - ClientConnectionFactory.Info h2c = new ClientConnectionFactoryOverHTTP2.H2C(http2Client); - startClient(clientConnector, HttpClientConnectionFactory.HTTP11, h2c); + ClientConnectionFactory.Info http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + startClient(clientConnector, HttpClientConnectionFactory.HTTP11, http2); // Make an upgrade request from HTTP/1.1 to H2C. ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) @@ -653,8 +653,8 @@ public class HttpClientTransportDynamicTest }); ClientConnector clientConnector = new ClientConnector(); HTTP2Client http2Client = new HTTP2Client(clientConnector); - ClientConnectionFactory.Info h2c = new ClientConnectionFactoryOverHTTP2.H2C(http2Client); - startClient(clientConnector, HttpClientConnectionFactory.HTTP11, h2c); + ClientConnectionFactory.Info http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + startClient(clientConnector, HttpClientConnectionFactory.HTTP11, http2); // Make a POST upgrade request from HTTP/1.1 to H2C. // We don't support upgrades with request content because @@ -668,7 +668,7 @@ public class HttpClientTransportDynamicTest .header(HttpHeader.UPGRADE, "h2c") .header(HttpHeader.HTTP2_SETTINGS, "") .header(HttpHeader.CONNECTION, "Upgrade, HTTP2-Settings") - .content(new BytesContentProvider(bytes)) + .body(new BytesRequestContent(bytes)) .timeout(5, TimeUnit.SECONDS) .send(new BufferingResponseListener(bytes.length) { @@ -693,8 +693,8 @@ public class HttpClientTransportDynamicTest startServer(this::h1H2C, new EmptyServerHandler()); ClientConnector clientConnector = new ClientConnector(); HTTP2Client http2Client = new HTTP2Client(clientConnector); - ClientConnectionFactory.Info h2c = new ClientConnectionFactoryOverHTTP2.H2C(http2Client); - startClient(clientConnector, HttpClientConnectionFactory.HTTP11, h2c); + ClientConnectionFactory.Info http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + startClient(clientConnector, HttpClientConnectionFactory.HTTP11, http2); // The upgrade request is missing the required HTTP2-Settings header. ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) @@ -719,8 +719,8 @@ public class HttpClientTransportDynamicTest }); ClientConnector clientConnector = new ClientConnector(); HTTP2Client http2Client = new HTTP2Client(clientConnector); - ClientConnectionFactory.Info h2c = new ClientConnectionFactoryOverHTTP2.H2C(http2Client); - startClient(clientConnector, HttpClientConnectionFactory.HTTP11, h2c); + ClientConnectionFactory.Info http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + startClient(clientConnector, HttpClientConnectionFactory.HTTP11, http2); // Make an upgrade request from HTTP/1.1 to H2C. CountDownLatch latch = new CountDownLatch(1); @@ -736,4 +736,23 @@ public class HttpClientTransportDynamicTest assertTrue(latch.await(5, TimeUnit.SECONDS)); } + + @Test + public void testClientWithALPNServerWithoutALPN() throws Exception + { + startServer(this::sslH1H2C, new EmptyServerHandler()); + ClientConnector clientConnector = new ClientConnector(); + HTTP2Client http2Client = new HTTP2Client(clientConnector); + ClientConnectionFactory.Info http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + startClient(clientConnector, HttpClientConnectionFactory.HTTP11, http2); + + // Make a request without explicit version, so ALPN is used on the client. + // Since the server does not support ALPN, the first protocol is used. + ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) + .scheme(HttpScheme.HTTPS.asString()) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertEquals(HttpStatus.OK_200, response.getStatus()); + } } diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ProxyWithDynamicTransportTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ProxyWithDynamicTransportTest.java index 7dbd499b063..1988361ca0c 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ProxyWithDynamicTransportTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ProxyWithDynamicTransportTest.java @@ -180,9 +180,8 @@ public class ProxyWithDynamicTransportTest { ClientConnectionFactory.Info h1 = HttpClientConnectionFactory.HTTP11; HTTP2Client http2Client = new HTTP2Client(clientConnector); - ClientConnectionFactory.Info h2c = new ClientConnectionFactoryOverHTTP2.H2C(http2Client); - ClientConnectionFactory.Info h2 = new ClientConnectionFactoryOverHTTP2.H2(http2Client); - return new HttpClient(new HttpClientTransportDynamic(clientConnector, h1, h2c, h2)); + ClientConnectionFactory.Info http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + return new HttpClient(new HttpClientTransportDynamic(clientConnector, h1, http2)); } }); context.addServlet(holder, "/*"); @@ -200,9 +199,8 @@ public class ProxyWithDynamicTransportTest clientConnector.setSslContextFactory(new SslContextFactory.Client(true)); http2Client = new HTTP2Client(clientConnector); ClientConnectionFactory.Info h1 = HttpClientConnectionFactory.HTTP11; - ClientConnectionFactory.Info h2c = new ClientConnectionFactoryOverHTTP2.H2C(http2Client); - ClientConnectionFactory.Info h2 = new ClientConnectionFactoryOverHTTP2.H2(http2Client); - client = new HttpClient(new HttpClientTransportDynamic(clientConnector, h1, h2c, h2)); + ClientConnectionFactory.Info http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + client = new HttpClient(new HttpClientTransportDynamic(clientConnector, h1, http2)); client.start(); }