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

com.vmware.xenon.common.http.netty.NettyHttpServerResponseHandler Maven / Gradle / Ivy

There is a newer version: 1.6.18
Show newest version
/*
 * Copyright (c) 2014-2015 VMware, Inc. 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 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 com.vmware.xenon.common.http.netty;

import java.net.ProtocolException;
import java.util.EnumSet;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http2.HttpConversionUtil;

import com.vmware.xenon.common.Operation;
import com.vmware.xenon.common.ServerSentEvent;
import com.vmware.xenon.common.ServiceErrorResponse;
import com.vmware.xenon.common.ServiceErrorResponse.ErrorDetail;
import com.vmware.xenon.common.Utils;
import com.vmware.xenon.common.http.netty.NettyHttpEventStreamHandler.EventStreamHeadersMessage;
import com.vmware.xenon.common.http.netty.NettyHttpEventStreamHandler.EventStreamMessage;

/**
 * Processes responses from a remote HTTP server and completes the request associated with the
 * channel
 */
public class NettyHttpServerResponseHandler extends SimpleChannelInboundHandler {
    public static final Logger LOGGER = Logger.getLogger(NettyHttpServerResponseHandler.class
            .getName());

    private NettyChannelPool pool;
    private Logger logger = Logger.getLogger(getClass().getName());

    public NettyHttpServerResponseHandler(NettyChannelPool pool) {
        this.pool = pool;
    }

    @Override
    public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) {

        if (msg instanceof FullHttpResponse) {
            FullHttpResponse response = (FullHttpResponse) msg;
            Operation request = findOperation(ctx, response, true);
            if (request == null) {
                // This will happen when a client-side timeout occurs
                this.logger.warning("No request in channel " + ctx.channel().id().asLongText());
                return;
            }

            request.setStatusCode(response.status().code());
            parseResponseHeaders(request, response);
            completeRequest(ctx, request, response.content());
        } else if (msg instanceof EventStreamMessage) {
            EventStreamMessage sseMessage = (EventStreamMessage) msg;
            ServerSentEvent event = sseMessage.event;
            if (event != null && ServerSentEvent.EVENT_TYPE_ERROR.equals(event.event)) {
                Operation request = findOperation(ctx, msg, true);
                this.handleEventStreamError(request, event);
            } else {
                Operation request = findOperation(ctx, msg, false);
                request.sendServerSentEvent(event);
            }
        } else if (msg instanceof EventStreamHeadersMessage) {
            EventStreamHeadersMessage sseHeaders = (EventStreamHeadersMessage) msg;
            Operation request = findOperation(ctx, msg, false);
            request.setStatusCode(sseHeaders.originalResponse.status().code());
            parseResponseHeaders(request, sseHeaders.originalResponse);
            request.sendHeaders();
        }
    }

    /**
     * We find the operation differently for HTTP/1.1 and HTTP/2
     *
     * For HTTP/1.1, there is only one request per channel, and it's stored as an attribute
     * on the channel
     *
     * For HTTP/2, we have multiple requests and have to check a map in the associated
     * NettyChannelContext
     */
    private Operation findOperation(ChannelHandlerContext ctx, HttpObject msg, boolean remove) {
        Operation request;

        if (msg instanceof HttpResponse && ctx.channel().hasAttr(NettyChannelContext.HTTP2_KEY)) {
            Integer streamId = ((HttpResponse) msg).headers()
                    .getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text());
            if (streamId == null) {
                this.logger.warning("HTTP/2 message has no stream ID: ignoring.");
                return null;
            }
            NettyChannelContext channelContext = ctx.channel().attr(NettyChannelContext.CHANNEL_CONTEXT_KEY).get();
            if (channelContext == null) {
                this.logger.warning(
                        "HTTP/2 channel is missing associated channel context: ignoring response on stream "
                                + streamId);
                return null;
            }
            request = channelContext.getOperationForStream(streamId);
            if (request == null) {
                this.logger.warning("Can't find operation for stream " + streamId);
                return null;
            }
            // We only have one request/response per stream, so remove the association.
            channelContext.removeOperationForStream(streamId);
        } else {
            if (remove) {
                request = ctx.channel().attr(NettyChannelContext.OPERATION_KEY).getAndSet(null);
            } else {
                request = ctx.channel().attr(NettyChannelContext.OPERATION_KEY).get();
            }
            if (request == null) {
                this.logger.warning("Can't find operation for channel " + ctx.channel().id().asLongText());
                return null;
            }
        }
        return request;
    }

    private void parseResponseHeaders(Operation request, HttpResponse nettyResponse) {
        HttpHeaders headers = nettyResponse.headers();
        if (headers.isEmpty()) {
            return;
        }

        if (LOGGER.isLoggable(Level.FINEST)) {
            logResponseFraming(request, nettyResponse);
        }

        request.setKeepAlive(HttpUtil.isKeepAlive(nettyResponse));
        headers.remove(HttpHeaderNames.CONNECTION);

        if (HttpUtil.isContentLengthSet(nettyResponse)) {
            request.setContentLength(HttpUtil.getContentLength(nettyResponse));
            headers.remove(HttpHeaderNames.CONTENT_LENGTH);
        }

        String contentType = headers.get(HttpHeaderNames.CONTENT_TYPE);
        headers.remove(HttpHeaderNames.CONTENT_TYPE);
        if (contentType != null) {
            request.setContentType(contentType);
        }

        if (request.isConnectionSharing()) {
            headers.remove(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text());
            headers.remove(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text());
        }

        if (headers.isEmpty()) {
            return;
        }

        for (Entry h : headers) {
            String key = h.getKey();
            String value = h.getValue();
            if (Operation.STREAM_ID_HEADER.equals(key)) {
                // Prevent allocation of response headers in Operation and hide the stream ID
                // header, since it is manipulated by the HTTP layer, not services
                continue;
            }
            request.addResponseHeader(key, value);
        }
    }

    private void completeRequest(ChannelHandlerContext ctx, Operation request, ByteBuf content) {
        decodeResponseBody(request, content);
        this.pool.returnOrClose((NettyChannelContext) request.getSocketContext(),
                !request.isKeepAlive());
    }

    private void decodeResponseBody(Operation request, ByteBuf content) {
        if (!content.isReadable()) {
            if (checkResponseForError(request)) {
                return;
            }
            // skip body decode, request had no body
            request.setContentLength(0).setBodyNoCloning(null).complete();
            return;
        }

        try {
            Utils.decodeBody(request, content.nioBuffer(), false);
            if (checkResponseForError(request)) {
                return;
            }
            completeRequest(request);
        } catch (Exception e) {
            request.fail(e);
            return;
        }
    }

    private void completeRequest(Operation request) {
        if (checkResponseForError(request)) {
            return;
        }
        request.complete();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        Operation request = ctx.channel().attr(NettyChannelContext.OPERATION_KEY).getAndSet(null);

        if (request == null) {
            // This will happen when using HTTP/2 because we have multiple requests
            // associated with the channel. For now, we're just logging, but we could
            // find all the requests and fail all of them. That's slightly risky because
            // we don't understand why we failed, and we may get responses for them later.
            this.logger.info(
                    "Channel exception but no HTTP/1.1 request to fail:" + cause.getMessage());
            return;
        }

        // I/O exception this code recommends retry since it never made it to the remote end
        request.setStatusCode(Operation.STATUS_CODE_BAD_REQUEST);
        request.setBody(ServiceErrorResponse.create(cause, request.getStatusCode(),
                EnumSet.of(ErrorDetail.SHOULD_RETRY)));
        request.fail(cause);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        try {
            if (!ctx.channel().hasAttr(NettyChannelContext.HTTP2_KEY)) {
                Operation request = ctx.channel().attr(NettyChannelContext.OPERATION_KEY)
                        .getAndSet(null);
                if (request != null && Operation.MEDIA_TYPE_TEXT_EVENT_STREAM
                        .equals(request.getContentType())) {
                    // In case of event stream, complete the request -- the consumer is responsible to either
                    // retry the request or interpret this as the end of the stream.
                    request.complete();
                    this.pool.returnOrClose((NettyChannelContext) request.getSocketContext(),
                            !request.isKeepAlive());
                }
            }
        } finally {
            super.channelInactive(ctx);
        }
    }

    private boolean checkResponseForError(Operation op) {
        if (op.getStatusCode() < Operation.STATUS_CODE_FAILURE_THRESHOLD) {
            return false;
        }
        String errorMsg = String.format("Service %s returned error %d for %s. id %d",
                op.getUri(), op.getStatusCode(), op.getAction(), op.getId());

        if (!op.hasBody()) {
            ServiceErrorResponse rsp = ServiceErrorResponse.create(new ProtocolException(errorMsg),
                    op.getStatusCode());
            op.setBodyNoCloning(rsp);
        } else if (Operation.MEDIA_TYPE_APPLICATION_JSON.equals(op.getContentType())) {
            try {
                Object originalBody = op.getBodyRaw();
                ServiceErrorResponse rsp = op.getErrorResponseBody();
                if (rsp != null) {
                    errorMsg += " message " + rsp.message;
                }
                op.setBodyNoCloning(originalBody);
            } catch (Exception e) {
                this.logger.warning("Error response body not JSON: " + e.getMessage());
                ServiceErrorResponse rsp = ServiceErrorResponse.create(e,
                        op.getStatusCode());
                op.setBodyNoCloning(rsp);
            }
        }

        op.fail(new ProtocolException(errorMsg));
        return true;
    }

    private void handleEventStreamError(Operation op, ServerSentEvent event) {
        String errorMsg = String.format("Service %s returned error for %s. id %d",
                op.getUri(), op.getAction(), op.getId());
        ServiceErrorResponse rsp = Utils.fromJson(event.data, ServiceErrorResponse.class);
        errorMsg += " message " + rsp.message;
        op.setBodyNoCloning(rsp);
        op.fail(new ProtocolException(errorMsg));
    }

    public static void logResponseFraming(Operation op, HttpResponse response) {
        if (response.headers().isEmpty()) {
            return;
        }
        StringBuilder s = new StringBuilder();
        s.append(op.getAction().toString())
                .append(" ")
                .append(op.getUri().toString())
                .append("\n");
        response.headers().forEach((e) -> {
            s.append(e.toString()).append("\n");
        });
        LOGGER.info(s.toString());
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy