All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.jetty.client.transport.HttpClientTransportDynamic Maven / Gradle / Ivy

There is a newer version: 2.0.31
Show newest version
//
// ========================================================================
// Copyright (c) 1995 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.client.transport;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import org.eclipse.jetty.alpn.client.ALPNClientConnection;
import org.eclipse.jetty.alpn.client.ALPNClientConnectionFactory;
import org.eclipse.jetty.client.AbstractConnectorHttpClientTransport;
import org.eclipse.jetty.client.Destination;
import org.eclipse.jetty.client.HttpClientTransport;
import org.eclipse.jetty.client.MultiplexConnectionPool;
import org.eclipse.jetty.client.Origin;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.Transport;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 

A {@link HttpClientTransport} that can dynamically switch among different application protocols.

*

Applications create HttpClientTransportDynamic instances specifying all the application protocols * it supports, in order of preference. The typical case is when the server supports both HTTP/1.1 and * HTTP/2, but the client does not know that. In this case, the application will create a * HttpClientTransportDynamic in this way:

*
{@code
 * ClientConnector clientConnector = new ClientConnector();
 * // Configure the clientConnector.
 *
 * // Prepare the application protocols.
 * ClientConnectionFactory.Info h1 = HttpClientConnectionFactory.HTTP11;
 * HTTP2Client http2Client = new HTTP2Client(clientConnector);
 * ClientConnectionFactory.Info h2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client);
 *
 * // Create the HttpClientTransportDynamic, preferring h2 over h1.
 * HttpClientTransport transport = new HttpClientTransportDynamic(clientConnector, h2, h1);
 *
 * // Create the HttpClient.
 * client = new HttpClient(transport);
 * }
*

Note how in the code above the HttpClientTransportDynamic has been created with the application * protocols {@code h2} and {@code h1}, without the need to specify TLS (which is implied by the request * scheme) or ALPN (which is implied by HTTP/2 over TLS).

*

When a request is first sent, {@code (scheme, host, port)} are not enough to identify the destination * because the same origin may speak different protocols. * For example, the Jetty server supports speaking clear-text {@code http/1.1} and {@code h2c} on the same port. * Imagine a client sending a {@code h2c} request to that port; this will create a destination and connections * that speak {@code h2c}; it won't be possible to use the connections from that destination to send * {@code http/1.1} requests. * Therefore a destination is identified by a {@link org.eclipse.jetty.client.Origin} and * applications can customize the creation of the origin (for example depending on request protocol * version, or request headers, or request attributes, or even request path) by overriding * {@link HttpClientTransport#newOrigin(Request)}.

*/ @ManagedObject("The HTTP client transport that supports many HTTP versions") public class HttpClientTransportDynamic extends AbstractConnectorHttpClientTransport { private static final Logger LOG = LoggerFactory.getLogger(HttpClientTransportDynamic.class); private final List infos; /** * Creates a dynamic transport that speaks only HTTP/1.1. */ public HttpClientTransportDynamic() { this(new ClientConnector(), HttpClientConnectionFactory.HTTP11); } /** *

Creates a dynamic transport that speaks the given protocols, in order of preference * (first the most preferred).

* * @param infos the protocols this dynamic transport speaks * @deprecated use {@link #HttpClientTransportDynamic(ClientConnector, ClientConnectionFactory.Info...)} */ @Deprecated(since = "12.0.7", forRemoval = true) public HttpClientTransportDynamic(ClientConnectionFactory.Info... infos) { this(findClientConnector(infos), infos); } /** *

Creates a dynamic transport with the given {@link ClientConnector} and the given protocols, * in order of preference (first the most preferred).

* * @param connector the ClientConnector used by this transport * @param infos the application protocols that this transport can speak */ public HttpClientTransportDynamic(ClientConnector connector, ClientConnectionFactory.Info... infos) { super(connector); this.infos = infos.length == 0 ? List.of(HttpClientConnectionFactory.HTTP11) : List.of(infos); this.infos.forEach(this::installBean); setConnectionPoolFactory(destination -> new MultiplexConnectionPool(destination, destination.getHttpClient().getMaxConnectionsPerDestination(), 1) ); } private static ClientConnector findClientConnector(ClientConnectionFactory.Info[] infos) { return Arrays.stream(infos) .flatMap(info -> info.getContainedBeans(ClientConnector.class).stream()) .findFirst() .orElseGet(ClientConnector::new); } @Override public Origin newOrigin(Request request) { boolean secure = HttpScheme.isSecure(request.getScheme()); List matchingInfos = new ArrayList<>(); boolean negotiate = false; if (((HttpRequest)request).isVersionExplicit()) { HttpVersion version = request.getVersion(); List wanted = toProtocols(version); for (Info info : infos) { // Find the first protocol that matches the version. List protocols = info.getProtocols(secure); for (String p : protocols) { if (wanted.contains(p)) { matchingInfos.add(info); break; } } if (matchingInfos.isEmpty()) continue; if (secure && protocols.size() > 1) negotiate = true; break; } } else { Info preferredInfo = infos.get(0); if (secure) { if (preferredInfo.getProtocols(true).contains("h3")) { // HTTP/3 is not compatible with HTTP/2 and HTTP/1 // due to UDP vs TCP, so we can only try HTTP/3. matchingInfos.add(preferredInfo); } else { // If the preferred protocol is not HTTP/3, then // must be excluded since it won't be compatible // with the other HTTP versions due to UDP vs TCP. for (Info info : infos) { if (info.getProtocols(true).contains("h3")) continue; matchingInfos.add(info); } // We can only have HTTP/1 and HTTP/2 here, // decide whether negotiation is necessary. if (!request.getHeaders().contains(HttpHeader.UPGRADE, "h2c")) { int matches = matchingInfos.size(); if (matches > 1) negotiate = true; else if (matches == 1) negotiate = matchingInfos.get(0).getProtocols(true).size() > 1; } } } else { // Pick the first that allows non-secure. for (Info info : infos) { if (info.getProtocols(false).contains("h3")) continue; matchingInfos.add(info); break; } } } if (matchingInfos.isEmpty()) return getHttpClient().createOrigin(request, null); Transport transport = request.getTransport(); if (transport == null) { // Ask the ClientConnector for backwards compatibility // until ClientConnector.Configurator is removed. transport = getClientConnector().newTransport(); if (transport == null) transport = matchingInfos.get(0).newTransport(); request.transport(transport); } List protocols = matchingInfos.stream() .flatMap(info -> info.getProtocols(secure).stream()) .collect(Collectors.toCollection(ArrayList::new)); if (negotiate) protocols.remove("h2c"); Origin.Protocol protocol = new Origin.Protocol(protocols, negotiate); return getHttpClient().createOrigin(request, protocol); } @Override public Destination newDestination(Origin origin) { return new HttpDestination(getHttpClient(), origin); } @Override public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map context) throws IOException { HttpDestination destination = (HttpDestination)context.get(HTTP_DESTINATION_CONTEXT_KEY); Origin origin = destination.getOrigin(); Origin.Protocol protocol = origin.getProtocol(); ClientConnectionFactory factory; if (protocol == null) { // Use the default ClientConnectionFactory. factory = infos.get(0).getClientConnectionFactory(); } else { boolean intrinsicallySecure = origin.getTransport().isIntrinsicallySecure(); if (!intrinsicallySecure && destination.isSecure() && protocol.isNegotiate()) { factory = new ALPNClientConnectionFactory(getClientConnector().getExecutor(), this::newNegotiatedConnection, protocol.getProtocols()); } else { factory = findClientConnectionFactoryInfo(protocol.getProtocols(), destination.isSecure()) .orElseThrow(() -> new IOException("Cannot find " + ClientConnectionFactory.class.getSimpleName() + " for " + protocol)) .getClientConnectionFactory(); } } 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(), destination.isSecure()) .orElseThrow(() -> new IllegalStateException("Cannot find " + ClientConnectionFactory.class.getSimpleName() + " to upgrade to " + protocol)); info.upgrade(endPoint, context); } protected Connection newNegotiatedConnection(EndPoint endPoint, Map context) throws IOException { try { ALPNClientConnection alpnConnection = (ALPNClientConnection)endPoint.getConnection(); String protocol = alpnConnection.getProtocol(); 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 = infos.get(0); if (LOG.isDebugEnabled()) LOG.debug("No ALPN protocol, using {}", factoryInfo); } return factoryInfo.getClientConnectionFactory().newConnection(endPoint, context); } catch (Throwable failure) { this.connectFailed(context, failure); throw failure; } } private Optional findClientConnectionFactoryInfo(List protocols, boolean secure) { return infos.stream() .filter(info -> info.matches(protocols, secure)) .findFirst(); } private List toProtocols(HttpVersion version) { return switch (version) { case HTTP_0_9, HTTP_1_0, HTTP_1_1 -> List.of("http/1.1"); case HTTP_2 -> List.of("h2c", "h2"); case HTTP_3 -> List.of("h3"); }; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy