io.reactivex.netty.protocol.http.client.ClientRequestResponseConverter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of rx-netty Show documentation
Show all versions of rx-netty Show documentation
rx-netty developed by Netflix
/*
* Copyright 2014 Netflix, Inc.
*
* 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
*
* http://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.reactivex.netty.protocol.http.client;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufHolder;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.http.DefaultLastHttpContent;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpContent;
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.HttpResponse;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.util.AttributeKey;
import io.netty.util.ReferenceCountUtil;
import io.reactivex.netty.channel.AbstractConnectionEvent;
import io.reactivex.netty.channel.NewRxConnectionEvent;
import io.reactivex.netty.channel.ObservableConnection;
import io.reactivex.netty.client.ClientMetricsEvent;
import io.reactivex.netty.client.ConnectionReuseEvent;
import io.reactivex.netty.client.PooledConnectionReleasedEvent;
import io.reactivex.netty.metrics.Clock;
import io.reactivex.netty.metrics.MetricEventsSubject;
import io.reactivex.netty.protocol.http.UnicastContentSubject;
import io.reactivex.netty.util.MultipleFutureListener;
import rx.Observable;
import rx.Observer;
import rx.Subscriber;
import rx.functions.Action0;
import java.io.IOException;
/**
* A channel handler for {@link HttpClient} to convert netty's http request/response objects to {@link HttpClient}'s
* request/response objects. It handles the following message types:
*
* Reading Objects
*
- {@link HttpResponse}: Converts it to {@link HttpClientResponse}
- {@link HttpContent}: Converts it to the content of the previously generated
{@link HttpClientResponse}
- {@link FullHttpResponse}: Converts it to a {@link HttpClientResponse} with pre-populated content observable.
- Any other object: Assumes that it is a transformed HTTP content & pass it through to the content observable.
*
* Writing Objects
*
- {@link HttpClientRequest}: Converts it to a {@link HttpRequest}
- {@link ByteBuf} to an {@link HttpContent}
- Pass through any other message type.
*
* @author Nitesh Kant
*/
public class ClientRequestResponseConverter extends ChannelDuplexHandler {
/**
* This attribute stores the value of any dynamic idle timeout value sent via an HTTP keep alive header.
* This follows the proposal specified here: http://tools.ietf.org/id/draft-thomson-hybi-http-timeout-01.html
* The attribute can be extracted from an HTTP response header using the helper method
* {@link HttpClientResponse#getKeepAliveTimeoutSeconds()}
*/
public static final AttributeKey KEEP_ALIVE_TIMEOUT_MILLIS_ATTR = AttributeKey.valueOf("rxnetty_http_conn_keep_alive_timeout_millis");
public static final AttributeKey DISCARD_CONNECTION = AttributeKey.valueOf("rxnetty_http_discard_connection");
private final MetricEventsSubject> eventsSubject;
private ResponseState responseState; /*State associated with this handler. Since a handler instance is ALWAYS invoked by the same thread, this need not be thread-safe*/
public static final IOException CONN_CLOSE_BEFORE_RESPONSE = new IOException("Connection closed by peer before sending a response.");
public ClientRequestResponseConverter(MetricEventsSubject> eventsSubject) {
this.eventsSubject = eventsSubject;
responseState = new ResponseState();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Class> recievedMsgClass = msg.getClass();
/**
* Issue: https://github.com/Netflix/RxNetty/issues/129
* The state changes in a different method userEventTriggered() when the connection is reused. If the
* connection reuse event is generated as part of execution of this method (for the specific issue, as part of
* super.channelRead(ctx, rxResponse); below) it will so happen that we invoke onComplete (below code when the
* first response completes) on the new subject as opposed to the old response subject.
*/
final ResponseState stateToUse = responseState;
if (HttpResponse.class.isAssignableFrom(recievedMsgClass)) {
stateToUse.responseReceiveStartTimeMillis = Clock.newStartTimeMillis();
eventsSubject.onEvent(HttpClientMetricsEvent.RESPONSE_HEADER_RECEIVED);
@SuppressWarnings({"rawtypes", "unchecked"})
HttpResponse response = (HttpResponse) msg;
@SuppressWarnings({"rawtypes", "unchecked"})
HttpClientResponse rxResponse = new HttpClientResponse(response, stateToUse.contentSubject);
Long keepAliveTimeoutSeconds = rxResponse.getKeepAliveTimeoutSeconds();
if (null != keepAliveTimeoutSeconds) {
ctx.channel().attr(KEEP_ALIVE_TIMEOUT_MILLIS_ATTR).set(keepAliveTimeoutSeconds * 1000);
}
if (!rxResponse.getHeaders().isKeepAlive()) {
ctx.channel().attr(DISCARD_CONNECTION).set(true);
}
super.channelRead(ctx, rxResponse); // For FullHttpResponse, this assumes that after this call returns,
// someone has subscribed to the content observable, if not the content will be lost.
}
if (HttpContent.class.isAssignableFrom(recievedMsgClass)) {// This will be executed if the incoming message is a FullHttpResponse or only HttpContent.
eventsSubject.onEvent(HttpClientMetricsEvent.RESPONSE_CONTENT_RECEIVED);
ByteBuf content = ((ByteBufHolder) msg).content();
if (LastHttpContent.class.isAssignableFrom(recievedMsgClass)) {
stateToUse.responseReceiveComplete();
if (content.isReadable()) {
invokeContentOnNext(content, stateToUse);
}
stateToUse.sendOnComplete();
} else {
invokeContentOnNext(content, stateToUse);
}
} else if(!HttpResponse.class.isAssignableFrom(recievedMsgClass)){
invokeContentOnNext(msg, stateToUse);
}
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
Class> recievedMsgClass = msg.getClass();
final ResponseState stateToUse = responseState;
if (HttpClientRequest.class.isAssignableFrom(recievedMsgClass)) {
HttpClientRequest> rxRequest = (HttpClientRequest>) msg;
MultipleFutureListener allWritesListener = new MultipleFutureListener(promise);
Observable> contentSource = null;
switch (rxRequest.getContentSourceType()) {
case Raw:
if (!rxRequest.getHeaders().isContentLengthSet()) {
rxRequest.getHeaders().add(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED);
}
contentSource = rxRequest.getRawContentSource();
break;
case Typed:
if (!rxRequest.getHeaders().isContentLengthSet()) {
rxRequest.getHeaders().add(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED);
}
contentSource = rxRequest.getContentSource();
break;
case Absent:
if (!rxRequest.getHeaders().isContentLengthSet() && rxRequest.getMethod() != HttpMethod.GET) {
rxRequest.getHeaders().set(HttpHeaders.Names.CONTENT_LENGTH, 0);
}
break;
}
writeHttpHeaders(ctx, rxRequest, allWritesListener); // In all cases, write headers first.
if (null != contentSource) { // If content present then write Last Content after all content is written.
if (!rxRequest.getHeaders().isContentLengthSet()) {
rxRequest.getHeaders().add(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED);
}
writeContent(ctx, allWritesListener, contentSource, promise, rxRequest, stateToUse);
} else { // If no content then write Last Content immediately.
// In order for netty's codec to understand that HTTP request writing is over, we always have to write the
// LastHttpContent irrespective of whether it is chunked or not.
writeLastHttpContent(ctx, allWritesListener, rxRequest, stateToUse);
}
} else {
ctx.write(msg, promise); // pass through, since we do not understand this message.
}
}
protected void writeLastHttpContent(ChannelHandlerContext ctx, MultipleFutureListener allWritesListener,
HttpClientRequest> rxRequest,
final ResponseState responseState) {
writeAContentChunk(ctx, allWritesListener, new DefaultLastHttpContent())
.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
responseState.nowWaitingForResponse();
}
});
rxRequest.onWriteComplete();
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof ConnectionReuseEvent) {
responseState = new ResponseState(); // Reset the state on reuse.
responseState.setConnection((AbstractConnectionEvent>) evt);
} else if (evt instanceof NewRxConnectionEvent) {
responseState.setConnection((AbstractConnectionEvent>) evt);
} else if (evt instanceof PooledConnectionReleasedEvent) {
responseState.onConnectionClose();
}
super.userEventTriggered(ctx, evt);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
responseState.onConnectionClose();
super.channelInactive(ctx);
}
@SuppressWarnings("unchecked")
private static void invokeContentOnNext(Object nextObject, ResponseState stateToUse) {
try {
stateToUse.contentSubject.onNext(nextObject);
} catch (ClassCastException e) {
stateToUse.contentSubject.onError(e);
} finally {
ReferenceCountUtil.release(nextObject);
}
}
private void writeHttpHeaders(ChannelHandlerContext ctx, HttpClientRequest> rxRequest,
MultipleFutureListener allWritesListener) {
final long startTimeMillis = Clock.newStartTimeMillis();
eventsSubject.onEvent(HttpClientMetricsEvent.REQUEST_HEADERS_WRITE_START);
ChannelFuture writeFuture = ctx.write(rxRequest.getNettyRequest());
addWriteCompleteEvents(writeFuture, startTimeMillis, HttpClientMetricsEvent.REQUEST_HEADERS_WRITE_SUCCESS,
HttpClientMetricsEvent.REQUEST_HEADERS_WRITE_FAILED);
allWritesListener.listen(writeFuture);
}
private void writeContent(final ChannelHandlerContext ctx, final MultipleFutureListener allWritesListener,
final Observable> contentSource, final ChannelPromise promise,
final HttpClientRequest> rxRequest, final ResponseState responseState) {
contentSource.subscribe(new Subscriber
© 2015 - 2025 Weber Informatics LLC | Privacy Policy