
io.micronaut.http.client.jdk.AbstractJdkHttpClient Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of micronaut-http-client-jdk Show documentation
Show all versions of micronaut-http-client-jdk Show documentation
Core components supporting the Micronaut Framework
The newest version!
/*
* Copyright 2017-2023 original 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
*
* https://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.micronaut.http.client.jdk;
import io.micronaut.context.exceptions.ConfigurationException;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.Experimental;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.execution.ExecutionFlow;
import io.micronaut.core.propagation.PropagatedContext;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.BasicHttpAttributes;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MutableHttpRequest;
import io.micronaut.http.bind.RequestBinderRegistry;
import io.micronaut.http.body.MessageBodyHandlerRegistry;
import io.micronaut.http.client.ClientAttributes;
import io.micronaut.http.client.HttpClientConfiguration;
import io.micronaut.http.client.HttpVersionSelection;
import io.micronaut.http.client.LoadBalancer;
import io.micronaut.http.client.exceptions.HttpClientException;
import io.micronaut.http.client.exceptions.HttpClientExceptionUtils;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.http.client.exceptions.NoHostException;
import io.micronaut.http.client.filter.ClientFilterResolutionContext;
import io.micronaut.http.client.jdk.cookie.CookieDecoder;
import io.micronaut.http.codec.MediaTypeCodecRegistry;
import io.micronaut.http.context.ContextPathUtils;
import io.micronaut.http.cookie.Cookie;
import io.micronaut.http.filter.FilterRunner;
import io.micronaut.http.filter.GenericHttpFilter;
import io.micronaut.http.filter.HttpClientFilterResolver;
import io.micronaut.http.filter.HttpFilterResolver;
import io.micronaut.http.reactive.execution.ReactiveExecutionFlow;
import io.micronaut.http.ssl.ClientAuthentication;
import io.micronaut.http.ssl.ClientSslConfiguration;
import io.micronaut.http.util.HttpHeadersUtil;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.net.ssl.SSLParameters;
import java.io.IOException;
import java.net.Authenticator;
import java.net.CookieManager;
import java.net.HttpCookie;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.ProxySelector;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import static io.micronaut.http.client.exceptions.HttpClientExceptionUtils.populateServiceId;
/**
* Abstract implementation of {@link DefaultJdkHttpClient} that provides common functionality.
*
* @author Sergio del Amo
* @author Tim Yates
* @since 4.0.0
*/
@Internal
@Experimental
abstract class AbstractJdkHttpClient {
public static final String H2C_ERROR_MESSAGE = "H2C is not supported by the JDK HTTP client";
public static final String H3_ERROR_MESSAGE = "HTTP/3 is not supported by the JDK HTTP client";
public static final String WEIRD_ALPN_ERROR_MESSAGE = "The only supported ALPN modes are [" + HttpVersionSelection.ALPN_HTTP_1 + "] or [" + HttpVersionSelection.ALPN_HTTP_1 + "," + HttpVersionSelection.ALPN_HTTP_2 + "]";
protected final LoadBalancer loadBalancer;
protected final HttpVersionSelection httpVersion;
protected final HttpClientConfiguration configuration;
protected final String contextPath;
protected final HttpClient client;
protected final CookieManager cookieManager;
protected final RequestBinderRegistry requestBinderRegistry;
protected final String clientId;
protected final ConversionService conversionService;
protected final JdkClientSslBuilder sslBuilder;
protected final Logger log;
protected final HttpClientFilterResolver filterResolver;
protected final List clientFilterEntries;
protected final CookieDecoder cookieDecoder;
protected MediaTypeCodecRegistry mediaTypeCodecRegistry;
protected MessageBodyHandlerRegistry messageBodyHandlerRegistry;
protected AbstractJdkHttpClient(AbstractJdkHttpClient prototype) {
this.loadBalancer = prototype.loadBalancer;
this.httpVersion = prototype.httpVersion;
this.configuration = prototype.configuration;
this.contextPath = prototype.contextPath;
this.client = prototype.client;
this.cookieManager = prototype.cookieManager;
this.requestBinderRegistry = prototype.requestBinderRegistry;
this.clientId = prototype.clientId;
this.conversionService = prototype.conversionService;
this.sslBuilder = prototype.sslBuilder;
this.log = prototype.log;
this.filterResolver = prototype.filterResolver;
this.clientFilterEntries = prototype.clientFilterEntries;
this.cookieDecoder = prototype.cookieDecoder;
this.mediaTypeCodecRegistry = prototype.mediaTypeCodecRegistry;
this.messageBodyHandlerRegistry = prototype.messageBodyHandlerRegistry;
}
/**
* @param log the logger to use
* @param loadBalancer The {@link LoadBalancer} to use for selecting servers
* @param httpVersion The {@link HttpVersionSelection} to prefer
* @param configuration The {@link HttpClientConfiguration} to use
* @param contextPath The base URI to prepend to request uris
* @param mediaTypeCodecRegistry The {@link MediaTypeCodecRegistry} to use for encoding and decoding objects
* @param messageBodyHandlerRegistry The {@link MessageBodyHandlerRegistry} to use for encoding and decoding objects
* @param requestBinderRegistry The request binder registry
* @param clientId The client id
* @param conversionService The {@link ConversionService}
* @param sslBuilder The {@link JdkClientSslBuilder} for creating an {@link javax.net.ssl.SSLContext}
*/
@SuppressWarnings({"java:S107", "checkstyle:parameternumber"}) // too many parameters
protected AbstractJdkHttpClient(
Logger log,
LoadBalancer loadBalancer,
HttpVersionSelection httpVersion,
HttpClientConfiguration configuration,
String contextPath,
@Nullable HttpClientFilterResolver filterResolver,
@Nullable List clientFilterEntries,
MediaTypeCodecRegistry mediaTypeCodecRegistry,
MessageBodyHandlerRegistry messageBodyHandlerRegistry,
RequestBinderRegistry requestBinderRegistry,
String clientId,
ConversionService conversionService,
JdkClientSslBuilder sslBuilder,
CookieDecoder cookieDecoder
) {
this.cookieDecoder = cookieDecoder;
this.log = configuration.getLoggerName().map(LoggerFactory::getLogger).orElse(log);
this.loadBalancer = loadBalancer;
this.httpVersion = httpVersion;
this.configuration = configuration;
this.mediaTypeCodecRegistry = mediaTypeCodecRegistry;
this.messageBodyHandlerRegistry = messageBodyHandlerRegistry;
this.requestBinderRegistry = requestBinderRegistry;
this.clientId = clientId;
this.conversionService = conversionService;
this.cookieManager = new CookieManager();
this.sslBuilder = sslBuilder;
this.filterResolver = filterResolver;
this.clientFilterEntries = clientFilterEntries(filterResolver, clientFilterEntries);
if (System.getProperty("jdk.internal.httpclient.disableHostnameVerification") != null && log.isWarnEnabled()) {
log.warn("The jdk.internal.httpclient.disableHostnameVerification system property is set. This is not recommended for production use as it prevents proper certificate validation and may allow man-in-the-middle attacks.");
}
if (StringUtils.isNotEmpty(contextPath)) {
if (contextPath.charAt(0) != '/') {
contextPath = '/' + contextPath;
}
this.contextPath = contextPath;
} else {
this.contextPath = null;
}
HttpClient.Builder builder = HttpClient.newBuilder();
configuration.getConnectTimeout().ifPresent(builder::connectTimeout);
HttpVersionSelection httpVersionSelection = HttpVersionSelection.forClientConfiguration(configuration);
if (httpVersionSelection.getPlaintextMode() == HttpVersionSelection.PlaintextMode.H2C) {
throw new ConfigurationException(H2C_ERROR_MESSAGE);
}
if (httpVersionSelection.isHttp3()) {
throw new ConfigurationException(H3_ERROR_MESSAGE);
}
if (httpVersionSelection.isAlpn()) {
List supportedProtocols = Arrays.asList(httpVersionSelection.getAlpnSupportedProtocols());
if (supportedProtocols.size() == 2 &&
supportedProtocols.contains(HttpVersionSelection.ALPN_HTTP_1) &&
supportedProtocols.contains(HttpVersionSelection.ALPN_HTTP_2)) {
builder.version(HttpClient.Version.HTTP_2);
} else if (supportedProtocols.size() == 1 &&
supportedProtocols.get(0).equals(HttpVersionSelection.ALPN_HTTP_1)) {
builder.version(HttpClient.Version.HTTP_1_1);
} else {
throw new ConfigurationException(WEIRD_ALPN_ERROR_MESSAGE);
}
} else {
builder.version(HttpClient.Version.HTTP_1_1);
}
builder
.followRedirects(configuration.isFollowRedirects() ? HttpClient.Redirect.NORMAL : HttpClient.Redirect.NEVER)
.cookieHandler(cookieManager);
Optional proxyAddress = configuration.getProxyAddress();
if (proxyAddress.isPresent()) {
SocketAddress socketAddress = proxyAddress.get();
builder = configureProxy(builder, socketAddress, configuration.getProxyUsername().orElse(null), configuration.getProxyPassword().orElse(null));
}
if (configuration.getSslConfiguration() instanceof ClientSslConfiguration clientSslConfiguration) {
configureSsl(builder, clientSslConfiguration);
}
this.client = builder.build();
}
@NonNull
private static List clientFilterEntries(@Nullable HttpClientFilterResolver filterResolver,
@Nullable List clientFilterEntries) {
if (clientFilterEntries != null) {
return clientFilterEntries;
}
if (filterResolver == null) {
return Collections.emptyList();
}
return filterResolver.resolveFilterEntries(
new ClientFilterResolutionContext(null, AnnotationMetadata.EMPTY_METADATA)
);
}
private static HttpCookie toJdkCookie(@NonNull Cookie cookie,
@NonNull io.micronaut.http.HttpRequest> request,
@NonNull String host) {
HttpCookie newCookie = new HttpCookie(cookie.getName(), cookie.getValue());
newCookie.setMaxAge(cookie.getMaxAge());
newCookie.setDomain(host);
newCookie.setHttpOnly(cookie.isHttpOnly());
newCookie.setSecure(cookie.isSecure());
newCookie.setPath(cookie.getPath() == null ? request.getPath() : cookie.getPath());
return newCookie;
}
private HttpClient.Builder configureProxy(
@NonNull HttpClient.Builder builder,
@NonNull SocketAddress address,
@Nullable String username,
@Nullable String password
) {
if (log.isDebugEnabled()) {
log.debug("Configuring proxy: {} with username: {}", address, username);
}
if (address instanceof InetSocketAddress inetSocketAddress) {
builder = builder.proxy(ProxySelector.of(inetSocketAddress));
if (username != null && password != null) {
builder = builder.authenticator(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password.toCharArray());
}
});
}
} else {
throw new IllegalArgumentException("Unsupported proxy address type: " + address.getClass().getName());
}
return builder;
}
private void configureSsl(HttpClient.Builder builder, ClientSslConfiguration clientSslConfiguration) {
sslBuilder.build(clientSslConfiguration).ifPresent(builder::sslContext);
SSLParameters sslParameters = new SSLParameters();
clientSslConfiguration.getClientAuthentication().ifPresent(a -> {
if (a == ClientAuthentication.WANT) {
sslParameters.setWantClientAuth(true);
} else if (a == ClientAuthentication.NEED) {
sslParameters.setNeedClientAuth(true);
}
});
clientSslConfiguration.getProtocols().ifPresent(sslParameters::setProtocols);
clientSslConfiguration.getCiphers().ifPresent(sslParameters::setCipherSuites);
builder.sslParameters(sslParameters);
}
/**
* @return The {@link MediaTypeCodecRegistry}
*/
public MediaTypeCodecRegistry getMediaTypeCodecRegistry() {
return mediaTypeCodecRegistry;
}
/**
* @param mediaTypeCodecRegistry The {@link MediaTypeCodecRegistry}
*/
public void setMediaTypeCodecRegistry(MediaTypeCodecRegistry mediaTypeCodecRegistry) {
this.mediaTypeCodecRegistry = mediaTypeCodecRegistry;
}
/**
* @return The {@link MessageBodyHandlerRegistry}
*/
public MessageBodyHandlerRegistry getMessageBodyHandlerRegistry() {
return messageBodyHandlerRegistry;
}
/**
* @param messageBodyHandlerRegistry The {@link MessageBodyHandlerRegistry}
*/
public void setMessageBodyHandlerRegistry(MessageBodyHandlerRegistry messageBodyHandlerRegistry) {
this.messageBodyHandlerRegistry = messageBodyHandlerRegistry;
}
/**
* Convert the Micronaut request to a JDK request.
*
* @param request The Micronaut request object
* @param bodyType The body type
* @param The body type
* @return A JDK request object
*/
protected Mono mapToHttpRequest(@NonNull io.micronaut.http.HttpRequest request, @Nullable Argument> bodyType) {
return resolveRequestUri(request)
.map(uri -> {
cookieDecoder.decode(request).ifPresent(cookies -> cookies.getAll().forEach(cookie -> {
HttpCookie newCookie = toJdkCookie(cookie, request, uri.getHost());
cookieManager.getCookieStore().add(uri, newCookie);
}));
return HttpRequestFactory.builder(uri, request, configuration, bodyType, mediaTypeCodecRegistry, messageBodyHandlerRegistry).build();
});
}
protected Mono resolveRequestUri(io.micronaut.http.HttpRequest> request) {
if (request.getUri().getScheme() != null) {
// Full request URI, so use that
return Mono.just(request.getUri());
}
// Otherwise, go and look it up via the LoadBalancer
return resolveURI(request);
}
private Mono resolveURI(io.micronaut.http.HttpRequest request) {
URI requestURI = request.getUri();
if (loadBalancer == null) {
return Mono.error(populateServiceId(new NoHostException("Request URI specifies no host to connect to"), clientId, configuration));
}
return Mono.from(loadBalancer.select(request)).map(server -> {
Optional authInfo = server.getMetadata().get(io.micronaut.http.HttpHeaders.AUTHORIZATION_INFO, String.class);
if (request instanceof MutableHttpRequest> mutableRequest && authInfo.isPresent()) {
mutableRequest.getHeaders().auth(authInfo.get());
}
try {
return server.resolve(ContextPathUtils.prepend(requestURI, contextPath));
} catch (URISyntaxException e) {
throw populateServiceId(new HttpClientException("Failed to construct the request URI", e), clientId, configuration);
}
}
);
}
/**
* Convert the JDK response to a Micronaut response.
*
* @param netResponse The JDK response
* @param bodyType The body type
* @param The body type
* @return A Micronaut response
*/
@NonNull
protected HttpResponse response(@NonNull java.net.http.HttpResponse netResponse, @NonNull Argument bodyType) {
return new HttpResponseAdapter<>(netResponse, bodyType, conversionService, mediaTypeCodecRegistry, messageBodyHandlerRegistry);
}
protected Flux> exchangeImpl(@NonNull io.micronaut.http.HttpRequest request, @Nullable Argument bodyType) {
var defaultPublisher = responsePublisher(request, bodyType);
return resolveRequestUri(request)
.flatMapMany(uri -> applyFilterToResponsePublisher(request, uri, defaultPublisher));
}
protected > Publisher applyFilterToResponsePublisher(
io.micronaut.http.HttpRequest request,
URI requestURI,
Publisher responsePublisher
) {
if (!(request instanceof MutableHttpRequest> mutRequest) || filterResolver == null) {
return responsePublisher;
}
mutRequest.uri(requestURI);
List filters =
filterResolver.resolveFilters(request, clientFilterEntries);
FilterRunner.sortReverse(filters);
FilterRunner runner = new FilterRunner(filters) {
@Override
protected ExecutionFlow> provideResponse(io.micronaut.http.HttpRequest> request, PropagatedContext propagatedContext) {
try {
try (PropagatedContext.Scope ignore = propagatedContext.propagate()) {
return ReactiveExecutionFlow.fromPublisher((Publisher>) responsePublisher);
}
} catch (Throwable e) {
return ExecutionFlow.error(e);
}
}
};
return (Publisher) Mono.from(ReactiveExecutionFlow.fromFlow(runner.run(request)).toPublisher());
}
protected Publisher> responsePublisher(
@NonNull io.micronaut.http.HttpRequest> request,
@Nullable Argument bodyType
) {
if (clientId != null && BasicHttpAttributes.getServiceId(request).isEmpty()) {
ClientAttributes.setServiceId(request, clientId);
}
return Flux.defer(() -> mapToHttpRequest(request, bodyType)) // defered so any client filter changes are used
.map(httpRequest -> {
if (log.isDebugEnabled()) {
log.debug("Client {} Sending HTTP Request: {}", clientId, httpRequest);
}
HttpHeadersUtil.trace(log,
() -> httpRequest.headers().map().keySet(),
headerName -> httpRequest.headers().allValues(headerName));
return client.sendAsync(httpRequest, java.net.http.HttpResponse.BodyHandlers.ofByteArray());
})
.flatMap(Mono::fromCompletionStage)
.onErrorMap(IOException.class, e -> new HttpClientException("Error sending request: " + e.getMessage(), e))
.onErrorMap(InterruptedException.class, e -> new HttpClientException("Error sending request: " + e.getMessage(), e))
.handle((netResponse, sink) -> {
if (log.isDebugEnabled()) {
log.debug("Client {} Received HTTP Response: {} {}", clientId, netResponse.statusCode(), netResponse.uri());
}
boolean errorStatus = netResponse.statusCode() >= 400;
if (errorStatus && configuration.isExceptionOnErrorStatus()) {
sink.error(HttpClientExceptionUtils.populateServiceId(
new HttpClientResponseException(HttpStatus.valueOf(netResponse.statusCode()).getReason(), response(netResponse, bodyType)),
clientId,
configuration
));
} else {
sink.next(response(netResponse, bodyType));
}
});
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy