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

reactor.netty.http.client.HttpClientOperations Maven / Gradle / Ivy

There is a newer version: 1.2.1
Show newest version
/*
 * Copyright (c) 2011-2024 VMware, Inc. or its affiliates, All Rights Reserved.
 *
 * 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 reactor.netty.http.client;

import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.channels.ClosedChannelException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Supplier;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufHolder;
import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.compression.ZlibCodecFactory;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpConstants;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMessage;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
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.LastHttpContent;
import io.netty.handler.codec.http.cookie.ClientCookieDecoder;
import io.netty.handler.codec.http.cookie.ClientCookieEncoder;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;
import io.netty.handler.codec.http.multipart.HttpDataFactory;
import io.netty.handler.codec.http.multipart.HttpPostRequestEncoder;
import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtensionHandler;
import io.netty.handler.codec.http.websocketx.extensions.compression.DeflateFrameClientExtensionHandshaker;
import io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateClientExtensionHandshaker;
import io.netty.handler.codec.http2.Http2StreamChannel;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.util.ReferenceCountUtil;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscription;
import reactor.core.CoreSubscriber;
import reactor.core.Disposable;
import reactor.core.Exceptions;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Operators;
import reactor.core.publisher.Sinks;
import reactor.netty.Connection;
import reactor.netty.ConnectionObserver;
import reactor.netty.FutureMono;
import reactor.netty.NettyInbound;
import reactor.netty.NettyOutbound;
import reactor.netty.NettyPipeline;
import reactor.netty.channel.AbortedException;
import reactor.netty.channel.ChannelOperations;
import reactor.netty.http.Cookies;
import reactor.netty.http.HttpOperations;
import reactor.netty.http.logging.HttpMessageArgProviderFactory;
import reactor.netty.http.logging.HttpMessageLogFactory;
import reactor.util.Logger;
import reactor.util.Loggers;
import reactor.util.annotation.Nullable;
import reactor.util.context.ContextView;

import static io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateServerExtensionHandshaker.MAX_WINDOW_SIZE;
import static reactor.netty.ReactorNetty.format;

/**
 * Conversion between Netty types and Reactor types ({@link HttpOperations}.
 *
 * @author Stephane Maldini
 * @author Simon Baslé
 */
class HttpClientOperations extends HttpOperations
		implements HttpClientResponse, HttpClientRequest {

	final boolean                isSecure;
	final HttpRequest            nettyRequest;
	final HttpHeaders            requestHeaders;
	final ClientCookieEncoder    cookieEncoder;
	final ClientCookieDecoder    cookieDecoder;
	final List           cookieList;
	final Sinks.One trailerHeaders;

	Supplier[]          redirectedFrom = EMPTY_REDIRECTIONS;
	String                      resourceUrl;
	String                      path;
	Duration                    responseTimeout;

	volatile ResponseState responseState;

	boolean started;
	boolean retrying;
	boolean is100Continue;
	RedirectClientException redirecting;

	BiPredicate followRedirectPredicate;
	Consumer redirectRequestConsumer;
	HttpHeaders previousRequestHeaders;
	BiConsumer redirectRequestBiConsumer;
	volatile Throwable unprocessedOutboundError;

	static final String INBOUND_CANCEL_LOG = "Http client inbound receiver cancelled, closing channel.";

	HttpClientOperations(HttpClientOperations replaced) {
		super(replaced);
		this.started = replaced.started;
		this.retrying = replaced.retrying;
		this.redirecting = replaced.redirecting;
		this.redirectedFrom = replaced.redirectedFrom;
		this.redirectRequestConsumer = replaced.redirectRequestConsumer;
		this.previousRequestHeaders = replaced.previousRequestHeaders;
		this.redirectRequestBiConsumer = replaced.redirectRequestBiConsumer;
		this.isSecure = replaced.isSecure;
		this.nettyRequest = replaced.nettyRequest;
		this.responseState = replaced.responseState;
		this.followRedirectPredicate = replaced.followRedirectPredicate;
		this.requestHeaders = replaced.requestHeaders;
		this.cookieEncoder = replaced.cookieEncoder;
		this.cookieDecoder = replaced.cookieDecoder;
		this.cookieList = replaced.cookieList;
		this.resourceUrl = replaced.resourceUrl;
		this.path = replaced.path;
		this.responseTimeout = replaced.responseTimeout;
		this.is100Continue = replaced.is100Continue;
		this.trailerHeaders = replaced.trailerHeaders;
		// No need to copy the unprocessedOutboundError field from the replaced instance. The reason for this is that the
		// "unprocessedOutboundError" field contains an error that occurs when the connection of the HttpClientOperations
		// is already closed. In essence, this error represents the final state for the HttpClientOperations, and there's
		// no need to carry it over because it's considered as a terminal/concluding state.
	}

	HttpClientOperations(Connection c, ConnectionObserver listener, ClientCookieEncoder encoder,
			ClientCookieDecoder decoder, HttpMessageLogFactory httpMessageLogFactory) {
		super(c, listener, httpMessageLogFactory);
		this.isSecure = c.channel()
		                 .pipeline()
		                 .get(NettyPipeline.SslHandler) != null;
		this.nettyRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/");
		this.requestHeaders = nettyRequest.headers();
		this.cookieDecoder = decoder;
		this.cookieEncoder = encoder;
		this.cookieList = new ArrayList<>();
		this.trailerHeaders = Sinks.unsafe().one();
	}

	@Override
	public HttpClientRequest addCookie(Cookie cookie) {
		if (!hasSentHeaders()) {
			this.cookieList.add(cookie);
		}
		else {
			throw new IllegalStateException("Status and headers already sent");
		}
		return this;
	}

	@Override
	public HttpClientOperations addHandlerLast(ChannelHandler handler) {
		super.addHandlerLast(handler);
		return this;
	}

	@Override
	public HttpClientOperations addHandlerLast(String name, ChannelHandler handler) {
		super.addHandlerLast(name, handler);
		return this;
	}

	@Override
	public HttpClientOperations addHandlerFirst(ChannelHandler handler) {
		super.addHandlerFirst(handler);
		return this;
	}

	@Override
	public HttpClientOperations addHandlerFirst(String name, ChannelHandler handler) {
		super.addHandlerFirst(name, handler);
		return this;
	}

	@Override
	@SuppressWarnings("deprecation")
	public HttpClientOperations addHandler(ChannelHandler handler) {
		super.addHandler(handler);
		return this;
	}

	@Override
	@SuppressWarnings("FutureReturnValueIgnored")
	public HttpClientOperations addHandler(String name, ChannelHandler handler) {
		// Returned value is deliberately ignored
		super.addHandler(name, handler);
		return this;
	}

	@Override
	public HttpClientOperations replaceHandler(String name, ChannelHandler handler) {
		super.replaceHandler(name, handler);
		return this;
	}

	@Override
	public HttpClientOperations removeHandler(String name) {
		super.removeHandler(name);
		return this;
	}

	@Override
	public HttpClientRequest addHeader(CharSequence name, CharSequence value) {
		if (!hasSentHeaders()) {
			this.requestHeaders.add(name, value);
		}
		else {
			throw new IllegalStateException("Status and headers already sent");
		}
		return this;
	}

	@Override
	public InetSocketAddress address() {
		return (InetSocketAddress) channel().remoteAddress();
	}

	public void chunkedTransfer(boolean chunked) {
		if (!hasSentHeaders() && HttpUtil.isTransferEncodingChunked(nettyRequest) != chunked) {
			requestHeaders.remove(HttpHeaderNames.TRANSFER_ENCODING);
			HttpUtil.setTransferEncodingChunked(nettyRequest, chunked);
		}
	}

	@Override
	public HttpClientOperations withConnection(Consumer withConnection) {
		Objects.requireNonNull(withConnection, "withConnection");
		withConnection.accept(this);
		return this;
	}

	@Override
	public Map> cookies() {
		ResponseState responseState = this.responseState;
		if (responseState != null && responseState.cookieHolder != null) {
			return responseState.cookieHolder.getCachedCookies();
		}
		return Collections.emptyMap();
	}

	void followRedirectPredicate(BiPredicate predicate) {
		this.followRedirectPredicate = predicate;
	}

	void redirectRequestConsumer(@Nullable Consumer redirectRequestConsumer) {
		this.redirectRequestConsumer = redirectRequestConsumer;
	}

	@Override
	@SuppressWarnings("FutureReturnValueIgnored")
	protected void onInboundCancel() {
		if (isInboundDisposed()) {
			return;
		}
		//"FutureReturnValueIgnored" this is deliberate
		if (log.isDebugEnabled()) {
			log.debug(format(channel(), INBOUND_CANCEL_LOG));
		}
		channel().close();
	}

	@Override
	protected final void onUnprocessedOutboundError(Throwable t) {
		this.unprocessedOutboundError = t;
	}

	@Override
	protected void onInboundClose() {
		if (isInboundCancelled() || isInboundDisposed()) {
			listener().onStateChange(this, ConnectionObserver.State.DISCONNECTING);
			return;
		}
		listener().onStateChange(this, HttpClientState.RESPONSE_INCOMPLETE);
		if (responseState == null) {
			Throwable exception;
			if (markSentHeaderAndBody()) {
				exception = AbortedException.beforeSend();
			}
			else if (markSentBody()) {
				exception = new PrematureCloseException("Connection has been closed BEFORE response, while sending request body");
			}
			else {
				exception = new PrematureCloseException("Connection prematurely closed BEFORE response");
			}
			listener().onUncaughtException(this, addOutboundErrorCause(exception, unprocessedOutboundError));
			return;
		}
		super.onInboundError(addOutboundErrorCause(new PrematureCloseException("Connection prematurely closed DURING response"),
				unprocessedOutboundError));
	}

	@Override
	protected void afterInboundComplete() {
		if (redirecting != null) {
			listener().onUncaughtException(this, redirecting);
		}
		else {
			listener().onStateChange(this, HttpClientState.RESPONSE_COMPLETED);
		}
	}

	@Override
	public HttpClientRequest header(CharSequence name, CharSequence value) {
		if (!hasSentHeaders()) {
			this.requestHeaders.set(name, value);
		}
		else {
			throw new IllegalStateException("Status and headers already sent");
		}
		return this;
	}

	@Override
	public HttpClientRequest headers(HttpHeaders headers) {
		if (!hasSentHeaders()) {
			String host = requestHeaders.get(HttpHeaderNames.HOST);
			this.requestHeaders.set(headers);
			this.requestHeaders.set(HttpHeaderNames.HOST, host);
		}
		else {
			throw new IllegalStateException("Status and headers already sent");
		}
		return this;
	}

	@Override
	public boolean isFollowRedirect() {
		return followRedirectPredicate != null && redirectedFrom.length <= MAX_REDIRECTS;
	}

	@Override
	public HttpClientRequest responseTimeout(Duration maxReadOperationInterval) {
		if (!hasSentHeaders()) {
			this.responseTimeout = maxReadOperationInterval;
		}
		else {
			throw new IllegalStateException("Status and headers already sent");
		}
		return this;
	}

	@Override
	public boolean isKeepAlive() {
		ResponseState rs = responseState;
		if (rs != null) {
			return HttpUtil.isKeepAlive(rs.response);
		}
		return HttpUtil.isKeepAlive(nettyRequest);
	}

	@Override
	public boolean isWebsocket() {
		ChannelOperations ops = get(channel());
		return ops != null && ops.getClass().equals(WebsocketClientOperations.class);
	}

	@Override
	public HttpMethod method() {
		return nettyRequest.method();
	}

	@Override
	public final HttpClientOperations onDispose(Disposable onDispose) {
		super.onDispose(onDispose);
		return this;
	}

	@Override
	public ContextView currentContextView() {
		return currentContext();
	}

	@Override
	public String[] redirectedFrom() {
		Supplier[] redirectedFrom = this.redirectedFrom;
		String[] dest = new String[redirectedFrom.length];
		for (int i = 0; i < redirectedFrom.length; i++) {
			dest[i] = redirectedFrom[i].get();
		}
		return dest;
	}

	@Override
	public HttpHeaders requestHeaders() {
		return nettyRequest.headers();
	}

	@Override
	public HttpHeaders responseHeaders() {
		ResponseState responseState = this.responseState;
		if (responseState != null) {
			return responseState.headers;
		}
		throw new IllegalStateException("Response headers cannot be accessed without " + "server response");
	}

	@Override
	public NettyOutbound send(Publisher source) {
		if (!channel().isActive()) {
			return then(Mono.error(AbortedException.beforeSend()));
		}
		if (source instanceof Mono) {
			return super.send(source);
		}
		if (Objects.equals(method(), HttpMethod.GET) || Objects.equals(method(), HttpMethod.HEAD)) {

			ByteBufAllocator alloc = channel().alloc();
			return new PostHeadersNettyOutbound(Flux.from(source)
			                .collectList()
			                .doOnDiscard(ByteBuf.class, ByteBuf::release)
			                .flatMap(list -> {
				                if (markSentHeaderAndBody(list.toArray())) {
					                if (list.isEmpty()) {
						                return FutureMono.from(channel().writeAndFlush(newFullBodyMessage(Unpooled.EMPTY_BUFFER)));
					                }

					                ByteBuf output;
					                int i = list.size();
					                if (i == 1) {
						                output = list.get(0);
					                }
					                else {
						                CompositeByteBuf agg = alloc.compositeBuffer(list.size());

						                for (ByteBuf component : list) {
							                agg.addComponent(true, component);
						                }

						                output = agg;
					                }

					                if (output.readableBytes() > 0) {
						                return FutureMono.from(channel().writeAndFlush(newFullBodyMessage(output)));
					                }
					                output.release();
					                return FutureMono.from(channel().writeAndFlush(newFullBodyMessage(Unpooled.EMPTY_BUFFER)));
				                }
				                for (ByteBuf bb : list) {
				                	if (log.isDebugEnabled()) {
						                log.debug(format(channel(), "Ignoring accumulated bytebuf on http GET {}"), bb);
					                }
				                	bb.release();
				                }
				                return Mono.empty();
			                }), this, null);
		}

		return super.send(source);
	}

	final URI websocketUri() {
		URI uri;
		try {
			String url = uri();
			if (url.startsWith(HttpClient.HTTP_SCHEME) || url.startsWith(HttpClient.WS_SCHEME)) {
				uri = new URI(url);
			}
			else {
				String host = requestHeaders().get(HttpHeaderNames.HOST);
				uri = new URI((isSecure ? HttpClient.WSS_SCHEME :
				                          HttpClient.WS_SCHEME) + "://" + host + (url.startsWith("/") ? url : "/" + url));
			}
		}
		catch (URISyntaxException e) {
			throw new IllegalArgumentException(e);
		}
		return uri;
	}

	@Override
	public HttpResponseStatus status() {
		ResponseState responseState = this.responseState;
		if (responseState != null) {
			return responseState.response.status();
		}
		throw new IllegalStateException("Trying to access status() while missing response");
	}

	@Override
	public Mono trailerHeaders() {
		return trailerHeaders.asMono();
	}

	@Override
	public final String uri() {
		return this.nettyRequest.uri();
	}

	@Override
	public final String fullPath() {
		return this.path;
	}

	@Override
	public String resourceUrl() {
		return resourceUrl;
	}

	@Override
	public final HttpVersion version() {
		HttpVersion version = this.nettyRequest.protocolVersion();
		if (version.equals(HttpVersion.HTTP_1_0)) {
			return HttpVersion.HTTP_1_0;
		}
		else if (version.equals(HttpVersion.HTTP_1_1)) {
			return HttpVersion.HTTP_1_1;
		}
		throw new IllegalStateException(version.protocolName() + " not supported");
	}

	/**
	 * React on channel unwritability event while the http client request is being written.
	 *
	 * 

When using plain HTTP/1.1 and {@code HttpClient.send(Mono)}, if the socket becomes unwritable while writing, * we need to request for reads. This is necessary to read any early server response, such as a 400 bad request followed * by a socket close, while the request is still being written. Else, a "premature close exception before response" may be reported * to the user, causing confusion about the server's early response. * *

There is no need to request for reading in other cases * (H2/H2C/H1S/WebSocket), because in these cases the read interest has already been requested, or auto-read is enabled * *

Important notes: *

* - If the connection is unwritable and {@code send(Flux)} has been used, then {@code hasSentBody()} will * always return false, because when {@code send(Flux)} is used, {@code hasSentBody()} can only return true * if the request is fully written (see {@link #onOutboundComplete()} method which invokes {@code markSentBody()} * and sets the state to BODY_SENT). * So if channel is unwritable and {@code hasSentBody()} returns true, it means that {@code send(Mono)} has * been used (see {@link HttpOperations#send(Publisher)} where {@code markSentHeaderAndBody(b)} is setting * the state to BODY_SENT when the Publisher is a Mono). * *

- When the channel is unwritable, a channel read() has already been requested or is in auto-read if: *

  • Secure mode is used (Netty SslHandler requests read() when flushing).
  • *
  • HTTP2 is used.
  • *
  • WebSocket is used.
  • *
* *

See GH-2825 for more info */ @Override protected void onWritabilityChanged() { if (!isSecure && !channel().isWritable() && !channel().config().isAutoRead() && hasSentBody() && !(channel() instanceof Http2StreamChannel) && !isWebsocket()) { channel().read(); } } @Override protected void afterMarkSentHeaders() { //Noop } @Override protected void beforeMarkSentHeaders() { if (redirectedFrom.length > 0) { if (redirectRequestConsumer != null) { redirectRequestConsumer.accept(this); } if (redirectRequestBiConsumer != null && previousRequestHeaders != null) { redirectRequestBiConsumer.accept(previousRequestHeaders, this); previousRequestHeaders = null; } } if (!cookieList.isEmpty()) { requestHeaders.add(HttpHeaderNames.COOKIE, cookieEncoder.encode(cookieList)); } } @Override protected boolean isContentAlwaysEmpty() { return false; } @Override protected void onHeadersSent() { channel().read(); if (channel().parent() != null) { channel().parent().read(); } } @Override @SuppressWarnings("FutureReturnValueIgnored") protected void onOutboundComplete() { if (isWebsocket() || isInboundCancelled()) { return; } if (markSentHeaderAndBody()) { if (log.isDebugEnabled()) { log.debug(format(channel(), "No sendHeaders() called before complete, sending " + "zero-length header")); } //"FutureReturnValueIgnored" this is deliberate channel().writeAndFlush(newFullBodyMessage(Unpooled.EMPTY_BUFFER)); } else if (markSentBody()) { //"FutureReturnValueIgnored" this is deliberate channel().writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); } listener().onStateChange(this, HttpClientState.REQUEST_SENT); if (responseTimeout != null) { if (channel().pipeline().get(NettyPipeline.HttpMetricsHandler) != null) { if (channel().pipeline().get(NettyPipeline.ResponseTimeoutHandler) == null) { channel().pipeline().addBefore(NettyPipeline.HttpMetricsHandler, NettyPipeline.ResponseTimeoutHandler, new ReadTimeoutHandler(responseTimeout.toMillis(), TimeUnit.MILLISECONDS)); if (isPersistent()) { onTerminate().subscribe(null, null, () -> removeHandler(NettyPipeline.ResponseTimeoutHandler)); } } } else { addHandlerFirst(NettyPipeline.ResponseTimeoutHandler, new ReadTimeoutHandler(responseTimeout.toMillis(), TimeUnit.MILLISECONDS)); } } channel().read(); if (channel().parent() != null) { channel().parent().read(); } } @Override protected void onOutboundError(Throwable err) { if (isPersistent() && responseState == null) { if (log.isDebugEnabled()) { log.debug(format(channel(), "Outbound error happened"), err); } listener().onUncaughtException(this, err); if (markSentBody()) { markPersistent(false); } terminate(); return; } super.onOutboundError(err); } @Override protected void onInboundNext(ChannelHandlerContext ctx, Object msg) { if (msg instanceof HttpResponse) { HttpResponse response = (HttpResponse) msg; if (response.decoderResult() .isFailure()) { onInboundError(response.decoderResult() .cause()); ReferenceCountUtil.release(msg); return; } if (HttpResponseStatus.CONTINUE.equals(response.status())) { is100Continue = true; ReferenceCountUtil.release(msg); return; } if (started) { if (log.isDebugEnabled()) { log.debug(format(channel(), "HttpClientOperations cannot proceed more than one response {}"), httpMessageLogFactory().debug(HttpMessageArgProviderFactory.create(response))); } ReferenceCountUtil.release(msg); return; } is100Continue = false; started = true; setNettyResponse(response); if (!isKeepAlive()) { markPersistent(false); } if (isInboundCancelled()) { ReferenceCountUtil.release(msg); return; } if (log.isDebugEnabled()) { log.debug(format(channel(), "Received response (auto-read:{}) : {}"), channel().config().isAutoRead(), httpMessageLogFactory().debug(HttpMessageArgProviderFactory.create(response))); } if (notRedirected(response)) { try { listener().onStateChange(this, HttpClientState.RESPONSE_RECEIVED); } catch (Exception e) { onInboundError(e); ReferenceCountUtil.release(msg); return; } } else { // when redirecting no need of manual reading channel().config().setAutoRead(true); } if (msg instanceof FullHttpResponse) { FullHttpResponse request = (FullHttpResponse) msg; if (request.content().readableBytes() > 0) { super.onInboundNext(ctx, msg); } else { request.release(); } terminate(); } return; } if (msg instanceof LastHttpContent) { if (is100Continue) { ReferenceCountUtil.release(msg); channel().read(); return; } if (!started) { if (log.isDebugEnabled()) { log.debug(format(channel(), "HttpClientOperations received an incorrect end " + "delimiter (previously used connection?)")); } ReferenceCountUtil.release(msg); return; } if (log.isDebugEnabled()) { log.debug(format(channel(), "Received last HTTP packet")); } if (msg != LastHttpContent.EMPTY_LAST_CONTENT) { // When there is HTTP/2 response with INBOUND HEADERS(endStream=false) followed by INBOUND DATA(endStream=true length=0), // Netty sends LastHttpContent with empty buffer instead of EMPTY_LAST_CONTENT if (redirecting != null || ((LastHttpContent) msg).content().readableBytes() == 0) { ReferenceCountUtil.release(msg); } else { super.onInboundNext(ctx, msg); } } if (redirecting == null) { // EmitResult is ignored as it is guaranteed that there will be only one emission of LastHttpContent // Whether there are subscribers or the subscriber cancels is not of interest // Evaluated EmitResult: FAIL_TERMINATED, FAIL_OVERFLOW, FAIL_CANCELLED, FAIL_NON_SERIALIZED // FAIL_ZERO_SUBSCRIBER trailerHeaders.tryEmitValue(((LastHttpContent) msg).trailingHeaders()); } //force auto read to enable more accurate close selection now inbound is done channel().config().setAutoRead(true); if (markSentBody()) { markPersistent(false); } terminate(); return; } if (!started) { if (log.isDebugEnabled()) { if (msg instanceof ByteBufHolder) { msg = ((ByteBufHolder) msg).content(); } log.debug(format(channel(), "HttpClientOperations received an incorrect chunk {} " + "(previously used connection?)"), msg); } ReferenceCountUtil.release(msg); return; } if (redirecting != null) { ReferenceCountUtil.release(msg); // when redirecting auto-read is set to true, no need of manual reading return; } super.onInboundNext(ctx, msg); } @Override protected HttpMessage outboundHttpMessage() { return nettyRequest; } final boolean notRedirected(HttpResponse response) { if (isFollowRedirect() && followRedirectPredicate.test(this, this)) { try { redirecting = new RedirectClientException(response.headers(), response.status()); } catch (RuntimeException e) { if (log.isDebugEnabled()) { log.debug(format(channel(), "The request cannot be redirected"), e); } return true; } if (log.isDebugEnabled()) { log.debug(format(channel(), "Received redirect location: {}"), httpMessageLogFactory().debug(HttpMessageArgProviderFactory.create(response))); } return false; } return true; } @Override protected HttpMessage newFullBodyMessage(ByteBuf body) { HttpRequest request = new DefaultFullHttpRequest(version(), method(), uri(), body); requestHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, body.readableBytes()); requestHeaders.remove(HttpHeaderNames.TRANSFER_ENCODING); request.headers() .set(requestHeaders); return request; } @Override protected Throwable wrapInboundError(Throwable err) { if (err instanceof ClosedChannelException) { return new PrematureCloseException(err); } return super.wrapInboundError(err); } final HttpRequest getNettyRequest() { return nettyRequest; } final Mono send() { if (!channel().isActive()) { return Mono.error(AbortedException.beforeSend()); } return FutureMono.deferFuture(() -> markSentHeaderAndBody() ? channel().writeAndFlush(newFullBodyMessage(Unpooled.EMPTY_BUFFER)) : channel().newSucceededFuture()); } final void setNettyResponse(HttpResponse nettyResponse) { ResponseState state = responseState; if (state == null) { this.responseState = new ResponseState(nettyResponse, nettyResponse.headers(), cookieDecoder); } } @SuppressWarnings("FutureReturnValueIgnored") final void withWebsocketSupport(WebsocketClientSpec websocketClientSpec, boolean compress) { URI url = websocketUri(); //prevent further header to be sent for handshaking if (markSentHeaders()) { // Returned value is deliberately ignored addHandlerFirst(NettyPipeline.HttpAggregator, new HttpObjectAggregator(8192)); removeHandler(NettyPipeline.HttpMetricsHandler); if (websocketClientSpec.compress()) { requestHeaders().remove(HttpHeaderNames.ACCEPT_ENCODING); // Returned value is deliberately ignored removeHandler(NettyPipeline.HttpDecompressor); // Returned value is deliberately ignored PerMessageDeflateClientExtensionHandshaker perMessageDeflateClientExtensionHandshaker = new PerMessageDeflateClientExtensionHandshaker(6, ZlibCodecFactory.isSupportingWindowSizeAndMemLevel(), MAX_WINDOW_SIZE, websocketClientSpec.compressionAllowClientNoContext(), websocketClientSpec.compressionRequestedServerNoContext()); addHandlerFirst(NettyPipeline.WsCompressionHandler, new WebSocketClientExtensionHandler( perMessageDeflateClientExtensionHandshaker, new DeflateFrameClientExtensionHandshaker(false), new DeflateFrameClientExtensionHandshaker(true))); } if (log.isDebugEnabled()) { log.debug(format(channel(), "Attempting to perform websocket handshake with {}"), url); } WebsocketClientOperations ops = new WebsocketClientOperations(url, websocketClientSpec, this); if (!rebind(ops)) { log.error(format(channel(), "Error while rebinding websocket in channel attribute: " + get(channel()) + " to " + ops)); } } } static Throwable addOutboundErrorCause(Throwable exception, @Nullable Throwable cause) { if (cause != null) { cause.setStackTrace(new StackTraceElement[0]); exception.initCause(cause); } return exception; } static final class ResponseState { final HttpResponse response; final HttpHeaders headers; final Cookies cookieHolder; ResponseState(HttpResponse response, HttpHeaders headers, ClientCookieDecoder decoder) { this.response = response; this.headers = headers; this.cookieHolder = Cookies.newClientResponseHolder(headers, decoder); } } static final class SendForm extends Mono { static final HttpDataFactory DEFAULT_FACTORY = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE); final HttpClientOperations parent; final BiConsumer formCallback; final Consumer> progressCallback; SendForm(HttpClientOperations parent, BiConsumer formCallback, @Nullable Consumer> progressCallback) { this.parent = parent; this.formCallback = formCallback; this.progressCallback = progressCallback; } @Override public void subscribe(CoreSubscriber s) { if (!parent.markSentHeaders()) { Operators.error(s, new IllegalStateException("headers have already been sent")); return; } Subscription subscription = Operators.emptySubscription(); s.onSubscribe(subscription); if (parent.channel() .eventLoop() .inEventLoop()) { _subscribe(s); } else { parent.channel() .eventLoop() .execute(() -> _subscribe(s)); } } @SuppressWarnings("FutureReturnValueIgnored") void _subscribe(CoreSubscriber s) { HttpDataFactory df = DEFAULT_FACTORY; try { HttpClientFormEncoder encoder = new HttpClientFormEncoder(df, parent.nettyRequest, false, HttpConstants.DEFAULT_CHARSET, HttpPostRequestEncoder.EncoderMode.RFC1738); formCallback.accept(parent, encoder); encoder = encoder.applyChanges(parent.nettyRequest); df = encoder.newFactory; if (!encoder.isMultipart()) { parent.requestHeaders.remove(HttpHeaderNames.TRANSFER_ENCODING); } // Returned value is deliberately ignored parent.addHandlerFirst(NettyPipeline.ChunkedWriter, new ChunkedWriteHandler()); boolean chunked = HttpUtil.isTransferEncodingChunked(parent.nettyRequest); HttpRequest r = encoder.finalizeRequest(); if (!chunked) { HttpUtil.setTransferEncodingChunked(r, false); HttpUtil.setContentLength(r, encoder.length()); } ChannelFuture f = parent.channel() .writeAndFlush(r); if (encoder.isChunked()) { Flux tail = encoder.progressSink.asFlux().onBackpressureLatest(); if (encoder.cleanOnTerminate) { tail = tail.doOnCancel(encoder) .doAfterTerminate(encoder); } if (progressCallback != null) { progressCallback.accept(tail); } else { tail.subscribe(); } //"FutureReturnValueIgnored" this is deliberate parent.channel() .writeAndFlush(encoder); } else { Mono mono = FutureMono.from(f); if (encoder.cleanOnTerminate) { mono = mono.doOnCancel(encoder) .doAfterTerminate(encoder); } if (progressCallback != null) { progressCallback.accept(mono.cast(Long.class) .switchIfEmpty(Mono.just(encoder.length())) .flux()); } else { mono.subscribe(); } } s.onComplete(); } catch (Throwable e) { Exceptions.throwIfJvmFatal(e); df.cleanRequestHttpData(parent.nettyRequest); s.onError(Exceptions.unwrap(e)); } } } static final int MAX_REDIRECTS = 50; @SuppressWarnings({"unchecked", "rawtypes"}) static final Supplier[] EMPTY_REDIRECTIONS = (Supplier[]) new Supplier[0]; static final Logger log = Loggers.getLogger(HttpClientOperations.class); }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy