io.micronaut.http.client.netty.Http1ResponseHandler Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2017-2024 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.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.http.body.CloseableByteBody;
import io.micronaut.http.body.stream.BodySizeLimits;
import io.micronaut.http.body.stream.BufferConsumer;
import io.micronaut.http.client.exceptions.ResponseClosedException;
import io.micronaut.http.netty.body.AvailableNettyByteBody;
import io.micronaut.http.netty.body.StreamingNettyByteBody;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.CompositeByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.util.ReferenceCountUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* Inbound message handler for the HTTP/1.1 client. Also used for HTTP/2 and /3 through message
* translators.
*
* @author Jonas Konrad
* @since 4.7.0
*/
@Internal
final class Http1ResponseHandler extends SimpleChannelInboundHandlerInstrumented {
private static final Logger LOG = LoggerFactory.getLogger(Http1ResponseHandler.class);
private ReaderState> state;
public Http1ResponseHandler(ResponseListener listener) {
super(false);
state = new BeforeResponse(listener);
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
ctx.read();
}
@Override
protected void channelReadInstrumented(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
if (msg.decoderResult().isFailure()) {
ReferenceCountUtil.release(msg);
exceptionCaught(ctx, msg.decoderResult().cause());
return;
}
//noinspection unchecked,rawtypes
((ReaderState) state).read(ctx, msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
state.channelReadComplete(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
state.channelInactive(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
state.exceptionCaught(ctx, cause);
}
private void transitionToState(ChannelHandlerContext ctx, ReaderState> fromState, ReaderState> nextState) {
if (!ctx.executor().inEventLoop()) {
throw new IllegalStateException("Not on event loop");
}
if (state != fromState) {
throw new IllegalStateException("Wrong source state");
}
fromState.leave(ctx);
state = nextState;
}
private abstract static sealed class ReaderState {
abstract void read(ChannelHandlerContext ctx, M msg);
void channelReadComplete(ChannelHandlerContext ctx) {
ctx.read();
}
abstract void exceptionCaught(ChannelHandlerContext ctx, Throwable cause);
void channelInactive(ChannelHandlerContext ctx) {
exceptionCaught(ctx, new ResponseClosedException("Connection closed before response was received"));
}
void leave(ChannelHandlerContext ctx) {
}
}
/**
* Before any response data has been received.
*/
private final class BeforeResponse extends ReaderState {
private final ResponseListener listener;
BeforeResponse(ResponseListener listener) {
this.listener = listener;
}
@Override
void read(ChannelHandlerContext ctx, HttpResponse msg) {
ReaderState nextState;
if (msg.status().code() == HttpResponseStatus.CONTINUE.code()) {
listener.continueReceived(ctx);
nextState = new DiscardingContinueContent(this);
} else {
nextState = new BufferedContent(listener, msg);
}
transitionToState(ctx, this, nextState);
if (msg instanceof HttpContent c) {
nextState.read(ctx, c);
}
}
@Override
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
transitionToState(ctx, this, AfterContent.INSTANCE);
listener.fail(ctx, cause);
}
}
/**
* After the {@link HttpResponse}, but before the first {@link #channelReadComplete}. We
* optimistically buffer data until {@link #channelReadComplete} so that we may return it as a
* more efficient {@link AvailableNettyByteBody}. If there's too much data, fall back to
* streaming.
*/
private final class BufferedContent extends ReaderState {
private final ResponseListener listener;
private final HttpResponse response;
private List buffered;
BufferedContent(ResponseListener listener, HttpResponse response) {
this.listener = listener;
this.response = response;
}
@Override
void read(ChannelHandlerContext ctx, HttpContent msg) {
if (msg.content().isReadable()) {
if (buffered == null) {
buffered = new ArrayList<>();
}
buffered.add(msg.content());
} else {
msg.release();
}
if (msg instanceof LastHttpContent) {
List buffered = this.buffered;
this.buffered = null;
transitionToState(ctx, this, AfterContent.INSTANCE);
BodySizeLimits limits = listener.sizeLimits();
if (buffered == null) {
complete(AvailableNettyByteBody.empty());
} else if (buffered.size() == 1) {
complete(AvailableNettyByteBody.createChecked(ctx.channel().eventLoop(), limits, buffered.get(0)));
} else {
CompositeByteBuf composite = ctx.alloc().compositeBuffer();
composite.addComponents(true, buffered);
complete(AvailableNettyByteBody.createChecked(ctx.channel().eventLoop(), limits, composite));
}
listener.finish(ctx);
}
}
@Override
void channelReadComplete(ChannelHandlerContext ctx) {
devolveToStreaming(ctx);
state.channelReadComplete(ctx); // check if there's demand
}
@Override
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
devolveToStreaming(ctx);
state.exceptionCaught(ctx, cause);
}
private void devolveToStreaming(ChannelHandlerContext ctx) {
assert ctx.executor().inEventLoop();
UnbufferedContent unbufferedContent = new UnbufferedContent(listener, ctx, response);
if (buffered != null) {
for (ByteBuf buf : buffered) {
unbufferedContent.add(buf);
}
}
transitionToState(ctx, this, unbufferedContent);
complete(new StreamingNettyByteBody(unbufferedContent.streaming));
}
private void complete(CloseableByteBody body) {
assert state != this : "should have been replaced already";
listener.complete(response, body);
}
}
/**
* Normal content handler, streaming data into a {@link StreamingNettyByteBody}.
*/
private final class UnbufferedContent extends ReaderState implements BufferConsumer.Upstream {
private final ResponseListener listener;
private final ChannelHandlerContext streamingContext;
private final StreamingNettyByteBody.SharedBuffer streaming;
private final boolean wasAutoRead;
private long demand;
UnbufferedContent(ResponseListener listener, ChannelHandlerContext ctx, HttpResponse response) {
this.listener = listener;
streaming = new StreamingNettyByteBody.SharedBuffer(ctx.channel().eventLoop(), listener.sizeLimits(), this);
if (!listener.isHeadResponse()) {
streaming.setExpectedLengthFrom(response.headers());
}
streamingContext = ctx;
wasAutoRead = ctx.channel().config().isAutoRead();
ctx.channel().config().setAutoRead(false);
}
@Override
void leave(ChannelHandlerContext ctx) {
ctx.channel().config().setAutoRead(wasAutoRead);
}
void add(ByteBuf buf) {
if (buf.isReadable()) {
demand -= buf.readableBytes();
streaming.add(buf);
} else {
buf.release();
}
}
@Override
void read(ChannelHandlerContext ctx, HttpContent msg) {
add(msg.content());
if (msg instanceof LastHttpContent) {
transitionToState(ctx, this, AfterContent.INSTANCE);
streaming.complete();
listener.finish(ctx);
}
}
@Override
void channelReadComplete(ChannelHandlerContext ctx) {
if (demand > 0) {
ctx.read();
}
}
@Override
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
streaming.error(cause);
}
@Override
public void start() {
if (streamingContext.executor().inEventLoop()) {
start0();
} else {
streamingContext.executor().execute(this::start0);
}
}
private void start0() {
onBytesConsumed0(1);
}
@Override
public void onBytesConsumed(long bytesConsumed) {
if (streamingContext.executor().inEventLoop()) {
onBytesConsumed0(bytesConsumed);
} else {
streamingContext.executor().execute(() -> onBytesConsumed0(bytesConsumed));
}
}
private void onBytesConsumed0(long bytesConsumed) {
if (state != this) {
return;
}
long oldDemand = demand;
long newDemand = oldDemand + bytesConsumed;
if (newDemand < oldDemand) {
// overflow
newDemand = Long.MAX_VALUE;
}
this.demand = newDemand;
if (oldDemand <= 0 && newDemand > 0) {
streamingContext.read();
}
}
@Override
public void allowDiscard() {
if (streamingContext.executor().inEventLoop()) {
allowDiscard0();
} else {
streamingContext.executor().execute(this::allowDiscard0);
}
}
private void allowDiscard0() {
if (state == this) {
transitionToState(streamingContext, this, new DiscardingContent(listener, streaming));
disregardBackpressure();
}
listener.allowDiscard();
}
@Override
public void disregardBackpressure() {
if (streamingContext.executor().inEventLoop()) {
disregardBackpressure0();
} else {
streamingContext.executor().execute(this::disregardBackpressure0);
}
}
private void disregardBackpressure0() {
long oldDemand = demand;
demand = Long.MAX_VALUE;
if (oldDemand <= 0 && state == this) {
streamingContext.read();
}
}
}
/**
* Short-circuiting handler that discards incoming content.
*/
private final class DiscardingContent extends ReaderState {
private final ResponseListener listener;
private final StreamingNettyByteBody.SharedBuffer streaming;
DiscardingContent(ResponseListener listener, StreamingNettyByteBody.SharedBuffer streaming) {
this.listener = listener;
this.streaming = streaming;
}
@Override
void read(ChannelHandlerContext ctx, HttpContent msg) {
msg.release();
if (msg instanceof LastHttpContent) {
transitionToState(ctx, this, AfterContent.INSTANCE);
listener.finish(ctx);
}
}
@Override
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
streaming.error(cause);
}
}
/**
* Short-circuiting handler that discards incoming content of a CONTINUE response.
*/
private final class DiscardingContinueContent extends ReaderState {
private final BeforeResponse beforeResponse;
DiscardingContinueContent(BeforeResponse beforeResponse) {
this.beforeResponse = beforeResponse;
}
@Override
void read(ChannelHandlerContext ctx, HttpContent msg) {
msg.release();
if (msg instanceof LastHttpContent) {
transitionToState(ctx, this, this.beforeResponse);
}
}
@Override
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
this.beforeResponse.exceptionCaught(ctx, cause);
}
}
/**
* Special handler that is used after the {@link LastHttpContent}. There should be no more
* incoming messages at this point.
*/
private static final class AfterContent extends ReaderState {
static final AfterContent INSTANCE = new AfterContent();
@Override
void read(ChannelHandlerContext ctx, HttpContent msg) {
if (LOG.isWarnEnabled()) {
LOG.warn("Discarding unexpected message {}", msg);
}
ReferenceCountUtil.release(msg);
}
@Override
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ctx.fireExceptionCaught(cause);
}
@Override
void channelInactive(ChannelHandlerContext ctx) {
ctx.fireChannelInactive();
}
}
/**
* The response listener.
*/
interface ResponseListener {
/**
* Size limits for the request body.
*
* @return The size limits
*/
@NonNull
default BodySizeLimits sizeLimits() {
return BodySizeLimits.UNLIMITED;
}
/**
* {@code true} iff we expect a response to a HEAD request. This influences handling of
* {@code Content-Length}.
*
* @return {@code true} iff this is a HEAD response
*/
default boolean isHeadResponse() {
return false;
}
/**
* Called when the handler receives a {@code CONTINUE} response, so the listener should
* proceed with sending the request body.
*
* @param ctx The handler context
*/
default void continueReceived(@NonNull ChannelHandlerContext ctx) {
}
/**
* Called when there is a failure before
* {@link #complete(HttpResponse, CloseableByteBody)} is called, i.e. we didn't even
* receive (valid) headers.
*
* @param ctx The handler context
* @param t The failure
*/
void fail(@NonNull ChannelHandlerContext ctx, @NonNull Throwable t);
/**
* Called when the headers (and potentially some or all of the body) are fully received.
*
* @param response The response status, headers...
* @param body The response body, potentially streaming
*/
void complete(@NonNull HttpResponse response, @NonNull CloseableByteBody body);
/**
* Called when the last piece of the body is received. This handler can be removed and the
* connection can be returned to the connection pool.
*
* @param ctx The handler context
*/
void finish(@NonNull ChannelHandlerContext ctx);
/**
* Called when the body passed to {@link #complete(HttpResponse, CloseableByteBody)} has
* been discarded. We may want to close the connection in that case to avoid having to
* receive unnecessary data.
*/
default void allowDiscard() {
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy