Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* 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.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.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.order.Ordered;
import io.micronaut.core.propagation.PropagatedContext;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.ArgumentUtils;
import io.micronaut.core.util.ArrayUtils;
import io.micronaut.core.util.ObjectUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.HttpAttributes;
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.MutableHttpResponse;
import io.micronaut.http.bind.DefaultRequestBinderRegistry;
import io.micronaut.http.bind.RequestBinderRegistry;
import io.micronaut.http.body.ChunkedMessageBodyReader;
import io.micronaut.http.body.ContextlessMessageBodyHandlerRegistry;
import io.micronaut.http.body.MessageBodyHandlerRegistry;
import io.micronaut.http.body.MessageBodyReader;
import io.micronaut.http.client.BlockingHttpClient;
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.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.exceptions.ResponseClosedException;
import io.micronaut.http.client.filter.ClientFilterResolutionContext;
import io.micronaut.http.client.filter.DefaultHttpClientFilterResolver;
import io.micronaut.http.client.filters.ClientServerContextFilter;
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.CodecException;
import io.micronaut.http.codec.MediaTypeCodecRegistry;
import io.micronaut.http.context.ContextPathUtils;
import io.micronaut.http.context.ServerRequestContext;
import io.micronaut.http.filter.FilterOrder;
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.NettyByteBufMessageBodyHandler;
import io.micronaut.http.netty.body.NettyCharSequenceBodyWriter;
import io.micronaut.http.netty.body.NettyJsonHandler;
import io.micronaut.http.netty.body.NettyJsonStreamHandler;
import io.micronaut.http.netty.body.NettyWritableBodyWriter;
import io.micronaut.http.netty.channel.ChannelPipelineCustomizer;
import io.micronaut.http.netty.reactive.HotObservable;
import io.micronaut.http.netty.stream.DefaultStreamedHttpResponse;
import io.micronaut.http.netty.stream.JsonSubscriber;
import io.micronaut.http.netty.stream.StreamedHttpRequest;
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.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.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPipeline;
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.channel.socket.nio.NioDatagramChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.TooLongFrameException;
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.FullHttpMessage;
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.HttpObject;
import io.netty.handler.codec.http.HttpObjectAggregator;
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.LastHttpContent;
import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;
import io.netty.handler.codec.http.multipart.FileUpload;
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.handler.flow.FlowControlHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.resolver.AddressResolverGroup;
import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.concurrent.DefaultThreadFactory;
import io.netty.util.concurrent.FastThreadLocalThread;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.Promise;
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 java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
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.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
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,
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 java.net.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;
/**
* 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
*/
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(loadBalancer,
null,
configuration,
contextPath,
new DefaultHttpClientFilterResolver(null, annotationMetadataResolver, Arrays.asList(filters)),
null,
threadFactory,
nettyClientSslBuilder,
codecRegistry,
handlerRegistry,
WebSocketBeanRegistry.EMPTY,
new DefaultRequestBinderRegistry(conversionService),
null,
NioSocketChannel::new,
NioDatagramChannel::new,
CompositeNettyClientCustomizer.EMPTY,
null,
conversionService,
null);
}
/**
* 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
*/
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
) {
ArgumentUtils.requireNonNull("nettyClientSslBuilder", nettyClientSslBuilder);
ArgumentUtils.requireNonNull("codecRegistry", codecRegistry);
ArgumentUtils.requireNonNull("webSocketBeanRegistry", webSocketBeanRegistry);
ArgumentUtils.requireNonNull("requestBinderRegistry", requestBinderRegistry);
ArgumentUtils.requireNonNull("configuration", configuration);
ArgumentUtils.requireNonNull("filterResolver", filterResolver);
ArgumentUtils.requireNonNull("socketChannelFactory", socketChannelFactory);
this.loadBalancer = loadBalancer;
this.defaultCharset = configuration.getDefaultCharset();
if (StringUtils.isNotEmpty(contextPath)) {
if (contextPath.charAt(0) != '/') {
contextPath = '/' + contextPath;
}
this.contextPath = contextPath;
} else {
this.contextPath = null;
}
this.configuration = configuration;
this.mediaTypeCodecRegistry = codecRegistry;
this.handlerRegistry = handlerRegistry;
this.log = configuration.getLoggerName().map(LoggerFactory::getLogger).orElse(DEFAULT_LOG);
this.filterResolver = filterResolver;
if (clientFilterEntries != null) {
this.clientFilterEntries = clientFilterEntries;
} else {
this.clientFilterEntries = filterResolver.resolveFilterEntries(
new ClientFilterResolutionContext(null, AnnotationMetadata.EMPTY_METADATA)
);
}
this.webSocketRegistry = webSocketBeanRegistry;
this.requestBinderRegistry = requestBinderRegistry;
this.informationalServiceId = informationalServiceId;
this.conversionService = conversionService;
this.connectionManager = new ConnectionManager(
log,
eventLoopGroup,
threadFactory,
configuration,
explicitHttpVersion,
socketChannelFactory,
udpChannelFactory,
nettyClientSslBuilder,
clientCustomizer,
informationalServiceId,
resolverGroup);
}
/**
* @param uri The URL
*/
public DefaultHttpClient(@Nullable URI uri) {
this(uri, new DefaultHttpClientConfiguration());
}
/**
*
*/
public DefaultHttpClient() {
this((URI) null, new DefaultHttpClientConfiguration());
}
/**
* @param uri The URI
* @param configuration The {@link HttpClientConfiguration} object
*/
public DefaultHttpClient(@Nullable URI uri, @NonNull HttpClientConfiguration configuration) {
this(uri, configuration, new NettyClientSslBuilder(new ResourceResolver()));
}
/**
* Constructor used by micronaut-oracle-cloud.
*
* @param uri The URI
* @param configuration The {@link HttpClientConfiguration} object
* @param clientSslBuilder The SSL builder
*/
public DefaultHttpClient(@Nullable URI uri, @NonNull HttpClientConfiguration configuration, @NonNull ClientSslBuilder clientSslBuilder) {
this(
uri == null ? null : LoadBalancer.fixed(uri), configuration, null, new DefaultThreadFactory(MultithreadEventLoopGroup.class),
clientSslBuilder,
createDefaultMediaTypeRegistry(),
createDefaultMessageBodyHandlerRegistry(),
AnnotationMetadataResolver.DEFAULT,
ConversionService.SHARED);
}
/**
* @param loadBalancer The {@link LoadBalancer} to use for selecting servers
* @param configuration The {@link HttpClientConfiguration} object
*/
public DefaultHttpClient(@Nullable LoadBalancer loadBalancer, HttpClientConfiguration configuration) {
this(loadBalancer,
configuration, null, new DefaultThreadFactory(MultithreadEventLoopGroup.class),
new NettyClientSslBuilder(new ResourceResolver()),
createDefaultMediaTypeRegistry(),
createDefaultMessageBodyHandlerRegistry(),
AnnotationMetadataResolver.DEFAULT,
ConversionService.SHARED);
}
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
*/
public MediaTypeCodecRegistry getMediaTypeCodecRegistry() {
return mediaTypeCodecRegistry;
}
/**
* Sets the {@link MediaTypeCodecRegistry} used by this client.
*
* @param mediaTypeCodecRegistry The registry to use. Should not be null
*/
@Deprecated
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
*/
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 io.micronaut.http.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 Flux.from(DefaultHttpClient.this.exchange(request, bodyType, errorType, blockHint))
.blockFirst();
// 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
)));
}
}
}
};
}
@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);
}
@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.findReader(eventType, List.of(MediaType.APPLICATION_JSON_TYPE)).orElseThrow(() -> new CodecException("JSON codec not present"));
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);
final io.micronaut.http.HttpRequest