io.micronaut.http.client.netty.DefaultHttpClient Maven / Gradle / Ivy
/*
* Copyright 2017-2022 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.netty;
import io.micronaut.buffer.netty.NettyByteBufferFactory;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.AnnotationMetadataResolver;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.async.propagation.ReactivePropagation;
import io.micronaut.core.async.publisher.Publishers;
import io.micronaut.core.beans.BeanMap;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.convert.ConversionServiceAware;
import io.micronaut.core.execution.DelayedExecutionFlow;
import io.micronaut.core.execution.ExecutionFlow;
import io.micronaut.core.io.ResourceResolver;
import io.micronaut.core.io.buffer.ByteBuffer;
import io.micronaut.core.io.buffer.ByteBufferFactory;
import io.micronaut.core.io.buffer.ReferenceCounted;
import io.micronaut.core.propagation.PropagatedContext;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.ArrayUtils;
import io.micronaut.core.util.ObjectUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.core.util.functional.ThrowingFunction;
import io.micronaut.discovery.ServiceInstance;
import io.micronaut.http.BasicHttpAttributes;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpResponseWrapper;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
import io.micronaut.http.MutableHttpHeaders;
import io.micronaut.http.MutableHttpRequest;
import io.micronaut.http.MutableHttpRequestWrapper;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.bind.DefaultRequestBinderRegistry;
import io.micronaut.http.bind.RequestBinderRegistry;
import io.micronaut.http.body.ByteBody;
import io.micronaut.http.body.CharSequenceBodyWriter;
import io.micronaut.http.body.ChunkedMessageBodyReader;
import io.micronaut.http.body.CloseableAvailableByteBody;
import io.micronaut.http.body.CloseableByteBody;
import io.micronaut.http.body.ContextlessMessageBodyHandlerRegistry;
import io.micronaut.http.body.InternalByteBody;
import io.micronaut.http.body.MessageBodyHandlerRegistry;
import io.micronaut.http.body.MessageBodyReader;
import io.micronaut.http.body.WritableBodyWriter;
import io.micronaut.http.body.stream.BodySizeLimits;
import io.micronaut.http.client.BlockingHttpClient;
import io.micronaut.http.client.ClientAttributes;
import io.micronaut.http.client.DefaultHttpClientConfiguration;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.HttpClientConfiguration;
import io.micronaut.http.client.HttpVersionSelection;
import io.micronaut.http.client.LoadBalancer;
import io.micronaut.http.client.ProxyHttpClient;
import io.micronaut.http.client.ProxyRequestOptions;
import io.micronaut.http.client.RawHttpClient;
import io.micronaut.http.client.StreamingHttpClient;
import io.micronaut.http.client.exceptions.ContentLengthExceededException;
import io.micronaut.http.client.exceptions.HttpClientErrorDecoder;
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.exceptions.ReadTimeoutException;
import io.micronaut.http.client.filter.ClientFilterResolutionContext;
import io.micronaut.http.client.loadbalance.FixedLoadBalancer;
import io.micronaut.http.client.multipart.MultipartBody;
import io.micronaut.http.client.multipart.MultipartDataFactory;
import io.micronaut.http.client.netty.ssl.ClientSslBuilder;
import io.micronaut.http.client.netty.ssl.NettyClientSslBuilder;
import io.micronaut.http.client.netty.websocket.NettyWebSocketClientHandler;
import io.micronaut.http.client.sse.SseClient;
import io.micronaut.http.codec.MediaTypeCodecRegistry;
import io.micronaut.http.context.ContextPathUtils;
import io.micronaut.http.filter.FilterRunner;
import io.micronaut.http.filter.GenericHttpFilter;
import io.micronaut.http.filter.HttpClientFilter;
import io.micronaut.http.filter.HttpClientFilterResolver;
import io.micronaut.http.filter.HttpFilterResolver;
import io.micronaut.http.multipart.MultipartException;
import io.micronaut.http.netty.NettyHttpHeaders;
import io.micronaut.http.netty.NettyHttpRequestBuilder;
import io.micronaut.http.netty.NettyHttpResponseBuilder;
import io.micronaut.http.netty.body.AvailableNettyByteBody;
import io.micronaut.http.netty.body.NettyBodyAdapter;
import io.micronaut.http.netty.body.NettyByteBody;
import io.micronaut.http.netty.body.NettyByteBufMessageBodyHandler;
import io.micronaut.http.netty.body.NettyJsonHandler;
import io.micronaut.http.netty.body.NettyJsonStreamHandler;
import io.micronaut.http.netty.body.StreamingNettyByteBody;
import io.micronaut.http.netty.channel.ChannelPipelineCustomizer;
import io.micronaut.http.netty.stream.DefaultStreamedHttpResponse;
import io.micronaut.http.netty.stream.JsonSubscriber;
import io.micronaut.http.netty.stream.StreamedHttpResponse;
import io.micronaut.http.reactive.execution.ReactiveExecutionFlow;
import io.micronaut.http.sse.Event;
import io.micronaut.http.uri.UriBuilder;
import io.micronaut.http.uri.UriTemplate;
import io.micronaut.http.util.HttpHeadersUtil;
import io.micronaut.json.JsonMapper;
import io.micronaut.json.codec.JsonMediaTypeCodec;
import io.micronaut.json.codec.JsonStreamMediaTypeCodec;
import io.micronaut.runtime.ApplicationConfiguration;
import io.micronaut.websocket.WebSocketClient;
import io.micronaut.websocket.annotation.ClientWebSocket;
import io.micronaut.websocket.annotation.OnMessage;
import io.micronaut.websocket.context.WebSocketBean;
import io.micronaut.websocket.context.WebSocketBeanRegistry;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufHolder;
import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.EmptyByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFactory;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoop;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.MultithreadEventLoopGroup;
import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpContent;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.DefaultHttpRequest;
import io.netty.handler.codec.http.DefaultLastHttpContent;
import io.netty.handler.codec.http.EmptyHttpHeaders;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;
import io.netty.handler.codec.http.multipart.FileUpload;
import io.netty.handler.codec.http.multipart.HttpData;
import io.netty.handler.codec.http.multipart.HttpDataFactory;
import io.netty.handler.codec.http.multipart.HttpPostRequestEncoder;
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
import io.netty.resolver.AddressResolverGroup;
import io.netty.util.AsciiString;
import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.concurrent.DefaultThreadFactory;
import io.netty.util.concurrent.FastThreadLocalThread;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.Disposable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.Function;
/**
* Default implementation of the {@link HttpClient} interface based on Netty.
*
* @author Graeme Rocher
* @since 1.0
*/
@Internal
public class DefaultHttpClient implements
WebSocketClient,
HttpClient,
StreamingHttpClient,
SseClient,
ProxyHttpClient,
RawHttpClient,
Closeable,
AutoCloseable {
/**
* Default logger, use {@link #log} where possible.
*/
private static final Logger DEFAULT_LOG = LoggerFactory.getLogger(DefaultHttpClient.class);
private static final int DEFAULT_HTTP_PORT = 80;
private static final int DEFAULT_HTTPS_PORT = 443;
/**
* Which headers not to copy from the first request when redirecting to a second request. There doesn't
* appear to be a spec for this. {@link HttpURLConnection} seems to drop all headers, but that would be a
* breaking change.
*
* Stored as a {@link HttpHeaders} with empty values because presumably someone thought about optimizing those
* already.
*/
private static final HttpHeaders REDIRECT_HEADER_BLOCKLIST;
static {
REDIRECT_HEADER_BLOCKLIST = new DefaultHttpHeaders();
// The host should be recalculated based on the location
REDIRECT_HEADER_BLOCKLIST.add(HttpHeaderNames.HOST, "");
// post body headers
REDIRECT_HEADER_BLOCKLIST.add(HttpHeaderNames.CONTENT_TYPE, "");
REDIRECT_HEADER_BLOCKLIST.add(HttpHeaderNames.CONTENT_LENGTH, "");
REDIRECT_HEADER_BLOCKLIST.add(HttpHeaderNames.TRANSFER_ENCODING, "");
REDIRECT_HEADER_BLOCKLIST.add(HttpHeaderNames.CONNECTION, "");
}
protected MediaTypeCodecRegistry mediaTypeCodecRegistry;
protected ByteBufferFactory byteBufferFactory = new NettyByteBufferFactory();
ConnectionManager connectionManager;
private MessageBodyHandlerRegistry handlerRegistry;
private final List clientFilterEntries;
private final LoadBalancer loadBalancer;
private final HttpClientConfiguration configuration;
private final String contextPath;
private final Charset defaultCharset;
private final Logger log;
private final HttpClientFilterResolver filterResolver;
private final WebSocketBeanRegistry webSocketRegistry;
private final RequestBinderRegistry requestBinderRegistry;
private final String informationalServiceId;
private final ConversionService conversionService;
@Nullable
private final ExecutorService blockingExecutor;
/**
* Construct a client for the given arguments.
*
* @param loadBalancer The {@link LoadBalancer} to use for selecting servers
* @param configuration The {@link HttpClientConfiguration} object
* @param contextPath The base URI to prepend to request uris
* @param threadFactory The thread factory to use for client threads
* @param nettyClientSslBuilder The SSL builder
* @param codecRegistry The {@link MediaTypeCodecRegistry} to use for encoding and decoding objects
* @param handlerRegistry The handler registry for encoding and decoding
* @param annotationMetadataResolver The annotation metadata resolver
* @param conversionService The conversion service
* @param filters The filters to use
* @deprecated Please go through the {@link #builder()} instead. If you need access to properties that are not public in the builder, make them public in core and document their usage.
*/
@Deprecated
public DefaultHttpClient(@Nullable LoadBalancer loadBalancer,
@NonNull HttpClientConfiguration configuration,
@Nullable String contextPath,
@Nullable ThreadFactory threadFactory,
ClientSslBuilder nettyClientSslBuilder,
@NonNull MediaTypeCodecRegistry codecRegistry,
@NonNull MessageBodyHandlerRegistry handlerRegistry,
@Nullable AnnotationMetadataResolver annotationMetadataResolver,
ConversionService conversionService,
HttpClientFilter... filters) {
this(
builder()
.loadBalancer(loadBalancer)
.configuration(configuration)
.contextPath(contextPath)
.threadFactory(threadFactory)
.nettyClientSslBuilder(nettyClientSslBuilder)
.codecRegistry(codecRegistry)
.handlerRegistry(handlerRegistry)
.conversionService(conversionService)
.annotationMetadataResolver(annotationMetadataResolver)
.filters(filters)
);
}
/**
* Construct a client for the given arguments.
* @param loadBalancer The {@link LoadBalancer} to use for selecting servers
* @param explicitHttpVersion The HTTP version to use. Can be null and defaults to {@link io.micronaut.http.HttpVersion#HTTP_1_1}
* @param configuration The {@link HttpClientConfiguration} object
* @param contextPath The base URI to prepend to request uris
* @param filterResolver The http client filter resolver
* @param clientFilterEntries The client filter entries
* @param threadFactory The thread factory to use for client threads
* @param nettyClientSslBuilder The SSL builder
* @param codecRegistry The {@link MediaTypeCodecRegistry} to use for encoding and decoding objects
* @param handlerRegistry The handler registry for encoding and decoding
* @param webSocketBeanRegistry The websocket bean registry
* @param requestBinderRegistry The request binder registry
* @param eventLoopGroup The event loop group to use
* @param socketChannelFactory The socket channel factory
* @param udpChannelFactory The UDP channel factory
* @param clientCustomizer The pipeline customizer
* @param informationalServiceId Optional service ID that will be passed to exceptions created by this client
* @param conversionService The conversion service
* @param resolverGroup Optional predefined resolver group
* @deprecated Please go through the {@link #builder()} instead. If you need access to properties that are not public in the builder, make them public in core and document their usage.
*/
@Deprecated
public DefaultHttpClient(@Nullable LoadBalancer loadBalancer,
@Nullable HttpVersionSelection explicitHttpVersion,
@NonNull HttpClientConfiguration configuration,
@Nullable String contextPath,
@NonNull HttpClientFilterResolver filterResolver,
@NonNull List clientFilterEntries,
@Nullable ThreadFactory threadFactory,
@NonNull ClientSslBuilder nettyClientSslBuilder,
@NonNull MediaTypeCodecRegistry codecRegistry,
@NonNull MessageBodyHandlerRegistry handlerRegistry,
@NonNull WebSocketBeanRegistry webSocketBeanRegistry,
@NonNull RequestBinderRegistry requestBinderRegistry,
@Nullable EventLoopGroup eventLoopGroup,
@NonNull ChannelFactory extends SocketChannel> socketChannelFactory,
@NonNull ChannelFactory extends DatagramChannel> udpChannelFactory,
NettyClientCustomizer clientCustomizer,
@Nullable String informationalServiceId,
ConversionService conversionService,
@Nullable AddressResolverGroup> resolverGroup
) {
this(
builder()
.loadBalancer(loadBalancer)
.explicitHttpVersion(explicitHttpVersion)
.configuration(configuration)
.contextPath(contextPath)
.filterResolver(filterResolver)
.clientFilterEntries(clientFilterEntries)
.threadFactory(threadFactory)
.nettyClientSslBuilder(nettyClientSslBuilder)
.codecRegistry(codecRegistry)
.handlerRegistry(handlerRegistry)
.webSocketBeanRegistry(webSocketBeanRegistry)
.requestBinderRegistry(requestBinderRegistry)
.eventLoopGroup(eventLoopGroup)
.socketChannelFactory(socketChannelFactory)
.udpChannelFactory(udpChannelFactory)
.clientCustomizer(clientCustomizer)
.informationalServiceId(informationalServiceId)
.conversionService(conversionService)
.resolverGroup(resolverGroup)
);
}
DefaultHttpClient(DefaultHttpClientBuilder builder) {
this.loadBalancer = builder.loadBalancer;
this.configuration = builder.configuration == null ? new DefaultHttpClientConfiguration() : builder.configuration;
this.defaultCharset = configuration.getDefaultCharset();
if (StringUtils.isNotEmpty(builder.contextPath)) {
if (builder.contextPath.charAt(0) != '/') {
builder.contextPath = '/' + builder.contextPath;
}
this.contextPath = builder.contextPath;
} else {
this.contextPath = null;
}
this.mediaTypeCodecRegistry = builder.codecRegistry == null ? createDefaultMediaTypeRegistry() : builder.codecRegistry;
this.handlerRegistry = builder.handlerRegistry == null ? createDefaultMessageBodyHandlerRegistry() : builder.handlerRegistry;
this.log = configuration.getLoggerName().map(LoggerFactory::getLogger).orElse(DEFAULT_LOG);
if (builder.filterResolver == null) {
builder.filters();
}
this.filterResolver = builder.filterResolver;
if (builder.clientFilterEntries != null) {
this.clientFilterEntries = builder.clientFilterEntries;
} else {
this.clientFilterEntries = builder.filterResolver.resolveFilterEntries(
new ClientFilterResolutionContext(null, AnnotationMetadata.EMPTY_METADATA)
);
}
this.webSocketRegistry = builder.webSocketBeanRegistry;
this.conversionService = builder.conversionService;
this.requestBinderRegistry = builder.requestBinderRegistry == null ? new DefaultRequestBinderRegistry(conversionService) : builder.requestBinderRegistry;
this.informationalServiceId = builder.informationalServiceId;
this.blockingExecutor = builder.blockingExecutor;
this.connectionManager = new ConnectionManager(
log,
builder.eventLoopGroup,
builder.threadFactory == null ? new DefaultThreadFactory(MultithreadEventLoopGroup.class) : builder.threadFactory,
configuration,
builder.explicitHttpVersion,
builder.socketChannelFactory,
builder.udpChannelFactory,
builder.nettyClientSslBuilder == null ? new NettyClientSslBuilder(new ResourceResolver()) : builder.nettyClientSslBuilder,
builder.clientCustomizer,
builder.informationalServiceId,
builder.resolverGroup);
}
/**
* @param uri The URL
* @deprecated Please go through the {@link #builder()} instead.
*/
@Deprecated
public DefaultHttpClient(@Nullable URI uri) {
this(builder().uri(uri));
}
/**
* @deprecated Please go through the {@link #builder()} instead.
*/
@Deprecated
public DefaultHttpClient() {
this(builder());
}
/**
* @param uri The URI
* @param configuration The {@link HttpClientConfiguration} object
* @deprecated Please go through the {@link #builder()} instead.
*/
@Deprecated
public DefaultHttpClient(@Nullable URI uri, @NonNull HttpClientConfiguration configuration) {
this(
builder()
.uri(uri)
.configuration(configuration)
);
}
/**
* Constructor used by micronaut-oracle-cloud.
*
* @param uri The URI
* @param configuration The {@link HttpClientConfiguration} object
* @param clientSslBuilder The SSL builder
* @deprecated Please go through the {@link #builder()} instead.
*/
@Deprecated
public DefaultHttpClient(@Nullable URI uri, @NonNull HttpClientConfiguration configuration, @NonNull ClientSslBuilder clientSslBuilder) {
this(
builder()
.uri(uri)
.configuration(configuration)
.nettyClientSslBuilder(clientSslBuilder)
);
}
/**
* @param loadBalancer The {@link LoadBalancer} to use for selecting servers
* @param configuration The {@link HttpClientConfiguration} object
* @deprecated Please go through the {@link #builder()} instead. If you need access to properties that are not public in the builder, make them public in core and document their usage.
*/
@Deprecated
public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, HttpClientConfiguration configuration) {
this(
builder()
.loadBalancer(loadBalancer)
.configuration(configuration)
);
}
/**
* Create a new builder for a {@link DefaultHttpClient}.
*
* @return The builder
* @since 4.7.0
*/
@NonNull
public static DefaultHttpClientBuilder builder() {
return new DefaultHttpClientBuilder();
}
static boolean isAcceptEvents(io.micronaut.http.HttpRequest> request) {
String acceptHeader = request.getHeaders().get(io.micronaut.http.HttpHeaders.ACCEPT);
return acceptHeader != null && acceptHeader.equalsIgnoreCase(MediaType.TEXT_EVENT_STREAM);
}
/**
* @return The configuration used by this client
*/
public HttpClientConfiguration getConfiguration() {
return configuration;
}
/**
* @return The client-specific logger name
*/
public Logger getLog() {
return log;
}
/**
* Access to the connection manager, for micronaut-oracle-cloud.
*
* @return The connection manager of this client
*/
public ConnectionManager connectionManager() {
return connectionManager;
}
@Override
public HttpClient start() {
if (!isRunning()) {
connectionManager.start();
}
return this;
}
@Override
public boolean isRunning() {
return connectionManager.isRunning();
}
@Override
public HttpClient stop() {
if (isRunning()) {
connectionManager.shutdown();
}
return this;
}
/**
* @return The {@link MediaTypeCodecRegistry} used by this client
* @deprecated Use body handlers instead
*/
@Deprecated
public MediaTypeCodecRegistry getMediaTypeCodecRegistry() {
return mediaTypeCodecRegistry;
}
/**
* Sets the {@link MediaTypeCodecRegistry} used by this client.
*
* @param mediaTypeCodecRegistry The registry to use. Should not be null
* @deprecated Use builder instead
*/
@Deprecated(forRemoval = true)
public void setMediaTypeCodecRegistry(MediaTypeCodecRegistry mediaTypeCodecRegistry) {
if (mediaTypeCodecRegistry != null) {
this.mediaTypeCodecRegistry = mediaTypeCodecRegistry;
}
}
/**
* Get the handler registry for this client.
*
* @return The handler registry
*/
@NonNull
public final MessageBodyHandlerRegistry getHandlerRegistry() {
return handlerRegistry;
}
/**
* Set the handler registry for this client.
*
* @param handlerRegistry The handler registry
* @deprecated Use builder instead
*/
@Deprecated(forRemoval = true)
public final void setHandlerRegistry(@NonNull MessageBodyHandlerRegistry handlerRegistry) {
this.handlerRegistry = handlerRegistry;
}
@Override
public BlockingHttpClient toBlocking() {
return new BlockingHttpClient() {
@Override
public void close() {
DefaultHttpClient.this.close();
}
@Override
public HttpResponse exchange(io.micronaut.http.HttpRequest request, Argument bodyType, Argument errorType) {
if (!configuration.isAllowBlockEventLoop() && Thread.currentThread() instanceof FastThreadLocalThread) {
throw new HttpClientException("""
You are trying to run a BlockingHttpClient operation on a netty event \
loop thread. This is a common cause for bugs: Event loops should \
never be blocked. You can either mark your controller as \
@ExecuteOn(TaskExecutors.BLOCKING), or use the reactive HTTP client \
to resolve this bug. There is also a configuration option to \
disable this check if you are certain a blocking operation is fine \
here.""");
}
BlockHint blockHint = BlockHint.willBlockThisThread();
return DefaultHttpClient.this.exchange(request, bodyType, errorType, blockHint).block();
// We don't have to release client response buffer
}
@Override
public O retrieve(io.micronaut.http.HttpRequest request, Argument bodyType, Argument errorType) {
// mostly copied from super method, but with customizeException
HttpResponse response = exchange(request, bodyType, errorType);
if (HttpStatus.class.isAssignableFrom(bodyType.getType())) {
return (O) response.getStatus();
} else {
Optional body = response.getBody();
if (body.isEmpty() && response.getBody(Argument.of(byte[].class)).isPresent()) {
throw decorate(new HttpClientResponseException(
"Failed to decode the body for the given content type [%s]".formatted(response.getContentType().orElse(null)),
response
));
} else {
return body.orElseThrow(() -> decorate(new HttpClientResponseException(
"Empty body",
response
)));
}
}
}
@Override
public boolean isRunning() {
return DefaultHttpClient.this.isRunning();
}
@Override
public BlockingHttpClient start() {
return DefaultHttpClient.this.start().toBlocking();
}
@Override
public BlockingHttpClient stop() {
return DefaultHttpClient.this.stop().toBlocking();
}
};
}
@NonNull
private MutableHttpRequest> toMutableRequest(io.micronaut.http.HttpRequest request) {
return MutableHttpRequestWrapper.wrapIfNecessary(conversionService, request);
}
@SuppressWarnings("SubscriberImplementation")
@Override
public Publisher>> eventStream(@NonNull io.micronaut.http.HttpRequest request) {
setupConversionService(request);
return eventStreamOrError(request, null);
}
private Publisher>> eventStreamOrError(@NonNull io.micronaut.http.HttpRequest request, @NonNull Argument> errorType) {
if (request instanceof MutableHttpRequest> httpRequest) {
httpRequest.accept(MediaType.TEXT_EVENT_STREAM_TYPE);
}
return Flux.create(emitter ->
dataStream(request, errorType).subscribe(new Subscriber<>() {
private Subscription dataSubscription;
private CurrentEvent currentEvent;
@Override
public void onSubscribe(Subscription s) {
this.dataSubscription = s;
Disposable cancellable = () -> dataSubscription.cancel();
emitter.onCancel(cancellable);
if (!emitter.isCancelled() && emitter.requestedFromDownstream() > 0) {
// request the first chunk
dataSubscription.request(1);
}
}
@Override
public void onNext(ByteBuffer> buffer) {
try {
int len = buffer.readableBytes();
// a length of zero indicates the start of a new event
// emit the current event
if (len == 0) {
try {
Event event = Event.of(byteBufferFactory.wrap(currentEvent.data))
.name(currentEvent.name)
.retry(currentEvent.retry)
.id(currentEvent.id);
emitter.next(
event
);
} finally {
currentEvent = null;
}
} else {
if (currentEvent == null) {
currentEvent = new CurrentEvent();
}
int colonIndex = buffer.indexOf((byte) ':');
// SSE comments start with colon, so skip
if (colonIndex > 0) {
// obtain the type
String type = buffer.slice(0, colonIndex).toString(StandardCharsets.UTF_8).trim();
int fromIndex = colonIndex + 1;
// skip the white space before the actual data
if (buffer.getByte(fromIndex) == ((byte) ' ')) {
fromIndex++;
}
if (fromIndex < len) {
int toIndex = len - fromIndex;
switch (type) {
case "data" -> {
ByteBuffer> content = buffer.slice(fromIndex, toIndex);
byte[] d = currentEvent.data;
if (d == null) {
currentEvent.data = content.toByteArray();
} else {
currentEvent.data = ArrayUtils.concat(d, content.toByteArray());
}
}
case "id" -> {
ByteBuffer> id = buffer.slice(fromIndex, toIndex);
currentEvent.id = id.toString(StandardCharsets.UTF_8).trim();
}
case "event" -> {
ByteBuffer> event = buffer.slice(fromIndex, toIndex);
currentEvent.name = event.toString(StandardCharsets.UTF_8).trim();
}
case "retry" -> {
ByteBuffer> retry = buffer.slice(fromIndex, toIndex);
String text = retry.toString(StandardCharsets.UTF_8);
if (!StringUtils.isEmpty(text)) {
currentEvent.retry = Duration.ofMillis(Long.parseLong(text));
}
}
default -> {
// ignore message
}
}
}
}
}
if (emitter.requestedFromDownstream() > 0 && !emitter.isCancelled()) {
dataSubscription.request(1);
}
} catch (Throwable e) {
onError(e);
} finally {
if (buffer instanceof ReferenceCounted counted) {
counted.release();
}
}
}
@Override
public void onError(Throwable t) {
dataSubscription.cancel();
if (t instanceof HttpClientException) {
emitter.error(t);
} else {
emitter.error(decorate(new HttpClientException("Error consuming Server Sent Events: " + t.getMessage(), t)));
}
}
@Override
public void onComplete() {
emitter.complete();
}
}), FluxSink.OverflowStrategy.BUFFER);
}
private static Mono toMono(ExecutionFlow flow, PropagatedContext context) {
return Mono.from(ReactivePropagation.propagate(context, ReactiveExecutionFlow.toPublisher(flow)));
}
@Override
public Publisher> eventStream(@NonNull io.micronaut.http.HttpRequest request,
@NonNull Argument eventType) {
setupConversionService(request);
return eventStream(request, eventType, DEFAULT_ERROR_TYPE);
}
@Override
public Publisher> eventStream(@NonNull io.micronaut.http.HttpRequest request, @NonNull Argument eventType, @NonNull Argument> errorType) {
setupConversionService(request);
MessageBodyReader reader = handlerRegistry.getReader(eventType, List.of(MediaType.APPLICATION_JSON_TYPE));
return Flux.from(eventStreamOrError(request, errorType)).map(byteBufferEvent -> {
ByteBuffer> data = byteBufferEvent.getData();
B decoded = reader.read(eventType, MediaType.APPLICATION_JSON_TYPE, request.getHeaders(), data);
return Event.of(byteBufferEvent, decoded);
});
}
@Override
public Publisher> dataStream(@NonNull io.micronaut.http.HttpRequest request) {
setupConversionService(request);
return dataStream(request, DEFAULT_ERROR_TYPE);
}
@Override
public Publisher> dataStream(@NonNull io.micronaut.http.HttpRequest request, @NonNull Argument> errorType) {
setupConversionService(request);
PropagatedContext propagatedContext = PropagatedContext.getOrEmpty();
return new MicronautFlux<>(toMono(resolveRequestURI(request), propagatedContext)
.flatMapMany(requestURI -> dataStreamImpl(toMutableRequest(request), errorType, propagatedContext, requestURI))
.map(bb -> {
if (bb.asNativeBuffer() instanceof ByteBuf byteBuf && byteBuf.refCnt() > 1) {
// if we aren't the exclusive owner of this buffer, we need to detect whether
// the downstream consumer releases it or not. For that, we need our own
// refCnt. A composite buffer provides that.
CompositeByteBuf composite = byteBuf.alloc().compositeBuffer(1);
composite.addComponent(true, byteBuf);
return byteBufferFactory.wrap(composite);
} else {
return bb;
}
}))
.doAfterNext(buffer -> {
Object o = buffer.asNativeBuffer();
if (o instanceof ByteBuf byteBuf) {
if (byteBuf.refCnt() > 0) {
ReferenceCountUtil.safeRelease(byteBuf);
}
}
});
}
@Override
public Publisher>> exchangeStream(@NonNull io.micronaut.http.HttpRequest request) {
return exchangeStream(request, DEFAULT_ERROR_TYPE);
}
@Override
public Publisher>> exchangeStream(@NonNull io.micronaut.http.HttpRequest request, @NonNull Argument> errorType) {
setupConversionService(request);
PropagatedContext propagatedContext = PropagatedContext.getOrEmpty();
return new MicronautFlux<>(toMono(resolveRequestURI(request), propagatedContext)
.flatMapMany(uri -> exchangeStreamImpl(propagatedContext, toMutableRequest(request), errorType, uri)))
.doAfterNext(byteBufferHttpResponse -> {
ByteBuffer> buffer = byteBufferHttpResponse.body();
if (buffer instanceof ReferenceCounted counted) {
counted.release();
}
});
}
@Override
public Publisher jsonStream(@NonNull io.micronaut.http.HttpRequest request, @NonNull Argument type) {
return jsonStream(request, type, DEFAULT_ERROR_TYPE);
}
@Override
public Publisher jsonStream(@NonNull io.micronaut.http.HttpRequest request, @NonNull Argument type, @NonNull Argument> errorType) {
setupConversionService(request);
PropagatedContext propagatedContext = PropagatedContext.getOrEmpty();
return Flux.from(toMono(resolveRequestURI(request), propagatedContext)
.flatMapMany(requestURI -> jsonStreamImpl(propagatedContext, toMutableRequest(request), type, errorType, requestURI)));
}
@SuppressWarnings("unchecked")
@Override
public Publisher