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

io.servicetalk.http.netty.DefaultMultiAddressUrlHttpClientBuilder Maven / Gradle / Ivy

The newest version!
/*
 * Copyright © 2018-2024 Apple Inc. and the ServiceTalk project authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.servicetalk.http.netty;

import io.servicetalk.buffer.api.BufferAllocator;
import io.servicetalk.client.api.ClientGroup;
import io.servicetalk.concurrent.api.Completable;
import io.servicetalk.concurrent.api.CompositeCloseable;
import io.servicetalk.concurrent.api.Executor;
import io.servicetalk.concurrent.api.Single;
import io.servicetalk.context.api.ContextMap;
import io.servicetalk.http.api.DefaultHttpHeadersFactory;
import io.servicetalk.http.api.DefaultStreamingHttpRequestResponseFactory;
import io.servicetalk.http.api.EmptyHttpHeaders;
import io.servicetalk.http.api.FilterableReservedStreamingHttpConnection;
import io.servicetalk.http.api.FilterableStreamingHttpClient;
import io.servicetalk.http.api.HttpContextKeys;
import io.servicetalk.http.api.HttpExecutionContext;
import io.servicetalk.http.api.HttpExecutionStrategies;
import io.servicetalk.http.api.HttpExecutionStrategy;
import io.servicetalk.http.api.HttpHeadersFactory;
import io.servicetalk.http.api.HttpRequestMetaData;
import io.servicetalk.http.api.HttpRequestMethod;
import io.servicetalk.http.api.MultiAddressHttpClientBuilder;
import io.servicetalk.http.api.RedirectConfig;
import io.servicetalk.http.api.SingleAddressHttpClientBuilder;
import io.servicetalk.http.api.StreamingHttpClient;
import io.servicetalk.http.api.StreamingHttpClientFilter;
import io.servicetalk.http.api.StreamingHttpClientFilterFactory;
import io.servicetalk.http.api.StreamingHttpRequest;
import io.servicetalk.http.api.StreamingHttpRequestResponseFactory;
import io.servicetalk.http.api.StreamingHttpRequester;
import io.servicetalk.http.api.StreamingHttpResponse;
import io.servicetalk.http.api.StreamingHttpResponseFactory;
import io.servicetalk.http.utils.RedirectingHttpRequesterFilter;
import io.servicetalk.transport.api.ClientSslConfig;
import io.servicetalk.transport.api.ClientSslConfigBuilder;
import io.servicetalk.transport.api.HostAndPort;
import io.servicetalk.transport.api.IoExecutor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.util.Locale;
import java.util.function.Function;
import javax.annotation.Nullable;

import static io.netty.handler.codec.http.HttpScheme.HTTP;
import static io.netty.handler.codec.http.HttpScheme.HTTPS;
import static io.servicetalk.buffer.api.CharSequences.caseInsensitiveHashCode;
import static io.servicetalk.concurrent.api.AsyncCloseables.newCompositeCloseable;
import static io.servicetalk.concurrent.api.Single.defer;
import static io.servicetalk.http.api.HttpContextKeys.HTTP_EXECUTION_STRATEGY_KEY;
import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy;
import static io.servicetalk.http.api.HttpExecutionStrategies.offloadAll;
import static io.servicetalk.http.api.HttpExecutionStrategies.offloadNone;
import static io.servicetalk.http.api.HttpHeaderNames.HOST;
import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1;
import static io.servicetalk.http.api.HttpRequestMetaDataFactory.newRequestMetaData;
import static io.servicetalk.http.api.HttpRequestMethod.GET;
import static io.servicetalk.http.netty.DefaultSingleAddressHttpClientBuilder.setExecutionContext;
import static java.util.Objects.requireNonNull;

/**
 * A builder of {@link StreamingHttpClient} instances which have a capacity to call any server based on the parsed
 * absolute-form URL address information from each {@link StreamingHttpRequest}.
 * 

* If {@link HttpRequestMetaData#requestTarget()} is not an absolute-form URL, a {@link MalformedURLException} will be * returned or thrown. *

* It also provides a good set of default settings and configurations, which could be used by most users as-is or * could be overridden to address specific use cases. * * @see absolute-form rfc7230#section-5.3.2 */ final class DefaultMultiAddressUrlHttpClientBuilder implements MultiAddressHttpClientBuilder { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultMultiAddressUrlHttpClientBuilder.class); // Use HttpRequestMetaData to access "https" constant used by Uri3986 class to optimize "equals" check to be a // trivial reference check. @SuppressWarnings("DataFlowIssue") private static final String HTTPS_SCHEME = newRequestMetaData(HTTP_1_1, GET, "https://invalid./", EmptyHttpHeaders.INSTANCE).scheme(); private final Function> builderFactory; private final HttpExecutionContextBuilder executionContextBuilder = new HttpExecutionContextBuilder(); @Nullable private HttpHeadersFactory headersFactory; @Nullable private RedirectConfig redirectConfig; private int defaultHttpPort = HTTP.port(); private int defaultHttpsPort = HTTPS.port(); @Nullable private SingleAddressInitializer singleAddressInitializer; DefaultMultiAddressUrlHttpClientBuilder( final Function> bFactory) { this.builderFactory = requireNonNull(bFactory); } @Override public StreamingHttpClient buildStreaming() { final CompositeCloseable closeables = newCompositeCloseable(); try { final HttpExecutionContext executionContext = executionContextBuilder.build(); final ClientFactory clientFactory = new ClientFactory(builderFactory, executionContext, singleAddressInitializer); final UrlKeyFactory keyFactory = new UrlKeyFactory(defaultHttpPort, defaultHttpsPort); final HttpHeadersFactory headersFactory = this.headersFactory; FilterableStreamingHttpClient urlClient = closeables.prepend( new StreamingUrlHttpClient(executionContext, keyFactory, clientFactory, new DefaultStreamingHttpRequestResponseFactory(executionContext.bufferAllocator(), headersFactory != null ? headersFactory : DefaultHttpHeadersFactory.INSTANCE, HTTP_1_1))); // Need to wrap the top level client (group) in order for non-relative redirects to work urlClient = redirectConfig == null ? urlClient : new RedirectingHttpRequesterFilter(redirectConfig).create(urlClient); LOGGER.debug("Multi-address client created with base strategy {}", executionContext.executionStrategy()); return new FilterableClientToClient(urlClient, executionContext); } catch (final Throwable t) { closeables.closeAsync().subscribe(); throw t; } } /** * Creates a {@link UrlKey} based on {@link HttpRequestMetaData} information and rewrites absolute-form URL into a * relative-form URL with a "host" header. */ private static final class UrlKeyFactory { private final int defaultHttpPort; private final int defaultHttpsPort; UrlKeyFactory(final int defaultHttpPort, final int defaultHttpsPort) { this.defaultHttpPort = defaultHttpPort; this.defaultHttpsPort = defaultHttpsPort; } UrlKey get(final HttpRequestMetaData metaData) throws MalformedURLException { final String scheme = ensureUrlComponentNonNull(metaData.scheme(), "scheme"); assert scheme.equals(scheme.toLowerCase(Locale.ENGLISH)) : "scheme must be in lowercase"; final String host = ensureUrlComponentNonNull(metaData.host(), "host"); final int parsedPort = metaData.port(); final int port = parsedPort >= 0 ? parsedPort : (HTTPS_SCHEME.equals(scheme) ? defaultHttpsPort : defaultHttpPort); setHostHeader(metaData, host, parsedPort); metaData.requestTarget(absoluteToRelativeFormRequestTarget(metaData.requestTarget(), scheme, host)); return new UrlKey(scheme, host, port); } private static String ensureUrlComponentNonNull(@Nullable final String value, final String name) throws MalformedURLException { if (value == null) { throw new MalformedURLException("Request-target does not contain " + name + ", expected absolute-form URL (scheme://host/path)"); } return value; } private static void setHostHeader(final HttpRequestMetaData metaData, final String host, final int port) { if (metaData.version().compareTo(HTTP_1_1) >= 0 && !metaData.headers().contains(HOST)) { // Host header must be identical to authority component of the target URI, // as described in https://datatracker.ietf.org/doc/html/rfc7230#section-5.4 String authority = host; if (port >= 0) { authority = authority + ':' + port; } metaData.headers().add(HOST, authority); } } // This code is similar to io.servicetalk.http.utils.RedirectSingle#absoluteToRelativeFormRequestTarget // but cannot be shared because we don't have an internal module for http private static String absoluteToRelativeFormRequestTarget(final String requestTarget, final String scheme, final String host) { final int fromIndex = scheme.length() + 3 + host.length(); // +3 because of "://" delimiter after scheme final int relativeReferenceIdx = requestTarget.indexOf('/', fromIndex); if (relativeReferenceIdx >= 0) { return requestTarget.substring(relativeReferenceIdx); } final int questionMarkIdx = requestTarget.indexOf('?', fromIndex); return questionMarkIdx < 0 ? "/" : '/' + requestTarget.substring(questionMarkIdx); } } private static final class UrlKey { final String scheme; final String host; final int port; private final int hashCode; UrlKey(final String scheme, final String host, final int port) { this.scheme = scheme; this.host = host; this.port = port; // hashCode is required at least once, but may be necessary multiple times for a single selectClient run this.hashCode = 31 * (caseInsensitiveHashCode(host) + port) + scheme.hashCode(); } @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } final UrlKey urlKey = (UrlKey) o; return port == urlKey.port && host.equalsIgnoreCase(urlKey.host) && scheme.equals(urlKey.scheme); } @Override public int hashCode() { return hashCode; } } private static final class ClientFactory implements Function { private static final ClientSslConfig DEFAULT_CLIENT_SSL_CONFIG = new ClientSslConfigBuilder().build(); private final Function> builderFactory; private final HttpExecutionContext executionContext; @Nullable private final SingleAddressInitializer singleAddressInitializer; ClientFactory(final Function> builderFactory, final HttpExecutionContext executionContext, @Nullable final SingleAddressInitializer singleAddressInitializer) { this.builderFactory = builderFactory; this.executionContext = executionContext; this.singleAddressInitializer = singleAddressInitializer; } @Override public StreamingHttpClient apply(final UrlKey urlKey) { final HostAndPort hostAndPort = HostAndPort.of(urlKey.host, urlKey.port); final SingleAddressHttpClientBuilder builder = requireNonNull(builderFactory.apply(hostAndPort)); setExecutionContext(builder, executionContext); if (HTTPS_SCHEME.equals(urlKey.scheme)) { builder.sslConfig(DEFAULT_CLIENT_SSL_CONFIG); } builder.appendClientFilter(HttpExecutionStrategyUpdater.INSTANCE); if (singleAddressInitializer != null) { singleAddressInitializer.initialize(urlKey.scheme, hostAndPort, builder); } return builder.buildStreaming(); } } private static void singleClientStrategyUpdate(ContextMap context, HttpExecutionStrategy singleStrategy) { HttpExecutionStrategy requestStrategy = context.getOrDefault(HTTP_EXECUTION_STRATEGY_KEY, defaultStrategy()); assert requestStrategy != null : "Request strategy unexpectedly null"; HttpExecutionStrategy useStrategy = defaultStrategy() == requestStrategy ? // For all apis except async streaming default conversion has already been done. // This is the default to required strategy resolution for the async streaming client. offloadAll() : defaultStrategy() == singleStrategy || !singleStrategy.hasOffloads() ? // single client is default or has no *additional* offloads requestStrategy : // add single client offloads to existing strategy requestStrategy.merge(singleStrategy); if (useStrategy != requestStrategy) { LOGGER.debug("Request strategy {} changes to {}. SingleAddressClient strategy: {}", requestStrategy, useStrategy, singleStrategy); context.put(HTTP_EXECUTION_STRATEGY_KEY, useStrategy); } } /** * When request transitions from the multi-address level to the single-address level, this filter will make sure * that any missing offloading required by the selected single-address client will be applied for the request * execution. This filter never reduces offloading, it can only add missing offloading flags. Users who want to * execute a request without offloading must specify {@link HttpExecutionStrategies#offloadNone()} strategy at the * {@link MultiAddressHttpClientBuilder} or explicitly set the required strategy at request context with * {@link HttpContextKeys#HTTP_EXECUTION_STRATEGY_KEY}. */ private static final class HttpExecutionStrategyUpdater implements StreamingHttpClientFilterFactory { static final StreamingHttpClientFilterFactory INSTANCE = new HttpExecutionStrategyUpdater(); private HttpExecutionStrategyUpdater() { // Singleton } @Override public StreamingHttpClientFilter create(final FilterableStreamingHttpClient client) { return new StreamingHttpClientFilter(client) { @Override protected Single request( final StreamingHttpRequester delegate, final StreamingHttpRequest request) { return defer(() -> { singleClientStrategyUpdate(request.context(), client.executionContext().executionStrategy()); return delegate.request(request); }); } }; } @Override public HttpExecutionStrategy requiredOffloads() { return offloadNone(); } } private static final class StreamingUrlHttpClient implements FilterableStreamingHttpClient { private final HttpExecutionContext executionContext; private final StreamingHttpRequestResponseFactory reqRespFactory; private final ClientGroup group; private final UrlKeyFactory keyFactory; StreamingUrlHttpClient(final HttpExecutionContext executionContext, final UrlKeyFactory keyFactory, final ClientFactory clientFactory, final StreamingHttpRequestResponseFactory reqRespFactory) { this.reqRespFactory = requireNonNull(reqRespFactory); this.group = ClientGroup.from(clientFactory); this.keyFactory = keyFactory; this.executionContext = requireNonNull(executionContext); } private FilterableStreamingHttpClient selectClient(HttpRequestMetaData metaData) throws MalformedURLException { return group.get(keyFactory.get(metaData)); } @Override public Single reserveConnection( final HttpRequestMetaData metaData) { return defer(() -> { try { FilterableStreamingHttpClient singleClient = selectClient(metaData); singleClientStrategyUpdate(metaData.context(), singleClient.executionContext().executionStrategy()); return singleClient.reserveConnection(metaData).shareContextOnSubscribe(); } catch (Throwable t) { return Single.failed(t).shareContextOnSubscribe(); } }); } @Override public Single request(final StreamingHttpRequest request) { return defer(() -> { try { return selectClient(request).request(request).shareContextOnSubscribe(); } catch (Throwable t) { return Single.failed(t).shareContextOnSubscribe(); } }); } @Override public HttpExecutionContext executionContext() { return executionContext; } @Override public StreamingHttpResponseFactory httpResponseFactory() { return reqRespFactory; } @Override public Completable onClose() { return group.onClose(); } @Override public Completable onClosing() { return group.onClosing(); } @Override public Completable closeAsync() { return group.closeAsync(); } @Override public Completable closeAsyncGracefully() { return group.closeAsyncGracefully(); } @Override public StreamingHttpRequest newRequest(final HttpRequestMethod method, final String requestTarget) { return reqRespFactory.newRequest(method, requestTarget); } } @Override public MultiAddressHttpClientBuilder ioExecutor(final IoExecutor ioExecutor) { executionContextBuilder.ioExecutor(ioExecutor); return this; } @Override public MultiAddressHttpClientBuilder executor(final Executor executor) { executionContextBuilder.executor(executor); return this; } @Override public MultiAddressHttpClientBuilder bufferAllocator( final BufferAllocator allocator) { executionContextBuilder.bufferAllocator(allocator); return this; } @Override public MultiAddressHttpClientBuilder executionStrategy( final HttpExecutionStrategy strategy) { executionContextBuilder.executionStrategy(strategy); return this; } @Override public MultiAddressHttpClientBuilder headersFactory( final HttpHeadersFactory headersFactory) { this.headersFactory = requireNonNull(headersFactory); return this; } @Override public MultiAddressHttpClientBuilder initializer( final SingleAddressInitializer initializer) { this.singleAddressInitializer = requireNonNull(initializer); return this; } @Override public MultiAddressHttpClientBuilder followRedirects( @Nullable final RedirectConfig config) { this.redirectConfig = config; return this; } @Override public MultiAddressHttpClientBuilder defaultHttpPort(final int port) { this.defaultHttpPort = verifyPortRange(port); return this; } @Override public MultiAddressHttpClientBuilder defaultHttpsPort(final int port) { this.defaultHttpsPort = verifyPortRange(port); return this; } /** * Verifies that the given port number is within the allowed range. * * @param port the port number to verify. * @return the port number if in range. */ private static int verifyPortRange(final int port) { if (port < 1 || port > 0xFFFF) { throw new IllegalArgumentException("Provided port number is out of range (between 1 and 65535): " + port); } return port; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy