com.twitter.http2.HttpConnectionHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.twitter.http2.HttpConnectionHandler.java

Source

/*
 * Copyright 2015 Twitter, 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 com.twitter.http2;

import java.io.IOException;
import java.net.SocketAddress;
import java.nio.channels.ClosedChannelException;
import java.util.List;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandler;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.util.internal.EmptyArrays;

import static com.twitter.http2.HttpCodecUtil.HTTP_CONNECTION_STREAM_ID;
import static com.twitter.http2.HttpCodecUtil.isServerId;

/**
 * Manages streams within an HTTP/2 connection.
 */
public class HttpConnectionHandler extends ByteToMessageDecoder
        implements HttpFrameDecoderDelegate, ChannelOutboundHandler {

    private static final HttpProtocolException PROTOCOL_EXCEPTION = new HttpProtocolException();

    static {
        PROTOCOL_EXCEPTION.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE);
    }

    private static final HttpSettingsFrame SETTINGS_ACK_FRAME = new DefaultHttpSettingsFrame().setAck(true);

    private static final int DEFAULT_HEADER_TABLE_SIZE = 4096;

    private static final int DEFAULT_WINDOW_SIZE = 65535;
    private int initialSendWindowSize = DEFAULT_WINDOW_SIZE;
    private int initialReceiveWindowSize = DEFAULT_WINDOW_SIZE;
    private volatile int initialConnectionReceiveWindowSize = DEFAULT_WINDOW_SIZE;

    private final HttpConnection httpConnection = new HttpConnection(initialSendWindowSize,
            initialReceiveWindowSize);
    private int lastStreamId;

    private static final int DEFAULT_MAX_CONCURRENT_STREAMS = Integer.MAX_VALUE;
    private int remoteConcurrentStreams = DEFAULT_MAX_CONCURRENT_STREAMS;
    private int localConcurrentStreams = DEFAULT_MAX_CONCURRENT_STREAMS;

    private boolean sentGoAwayFrame;
    private boolean receivedGoAwayFrame;

    private final ChannelFutureListener connectionErrorListener = new ConnectionErrorFutureListener();
    private ChannelFutureListener closingChannelFutureListener;

    private final boolean server;
    private final boolean handleStreamWindowUpdates;

    private final HttpFrameDecoder httpFrameDecoder;
    private final HttpFrameEncoder httpFrameEncoder;
    private final HttpHeaderBlockDecoder httpHeaderBlockDecoder;
    private final HttpHeaderBlockEncoder httpHeaderBlockEncoder;

    private HttpHeaderBlockFrame httpHeaderBlockFrame;
    private HttpSettingsFrame httpSettingsFrame;

    private boolean needSettingsAck;

    private boolean changeDecoderHeaderTableSize;
    private int headerTableSize;

    private boolean changeEncoderHeaderTableSize;
    private int lastHeaderTableSize = Integer.MAX_VALUE;
    private int minHeaderTableSize = Integer.MAX_VALUE;
    private boolean pushEnabled = true;

    private ChannelHandlerContext context;

    /**
     * Creates a new connection handler.
     *
     * @param server {@code true} if and only if this connection handler should
     *               handle the server endpoint of the connection.
     *               {@code false} if and only if this connection handler should
     *               handle the client endpoint of the connection.
     */
    public HttpConnectionHandler(boolean server) {
        this(server, true);
    }

    /**
     * Creates a new connection handler with the specified options.
     */
    public HttpConnectionHandler(boolean server, boolean handleStreamWindowUpdates) {
        this(server, handleStreamWindowUpdates, 8192, 16384);
    }

    /**
     * Creates a new connection handler with the specified options.
     */
    public HttpConnectionHandler(boolean server, int maxChunkSize, int maxHeaderSize) {
        this(server, true, maxChunkSize, maxHeaderSize);
    }

    /**
     * Creates a new connection handler with the specified options.
     */
    public HttpConnectionHandler(boolean server, boolean handleStreamWindowUpdates, int maxChunkSize,
            int maxHeaderSize) {
        this.server = server;
        this.handleStreamWindowUpdates = handleStreamWindowUpdates;
        httpFrameDecoder = new HttpFrameDecoder(server, this, maxChunkSize);
        httpFrameEncoder = new HttpFrameEncoder();
        httpHeaderBlockDecoder = new HttpHeaderBlockDecoder(maxHeaderSize, DEFAULT_HEADER_TABLE_SIZE);
        httpHeaderBlockEncoder = new HttpHeaderBlockEncoder(DEFAULT_HEADER_TABLE_SIZE);
    }

    public void setConnectionReceiveWindowSize(int connectionReceiveWindowSize) {
        if (connectionReceiveWindowSize < 0) {
            throw new IllegalArgumentException("connectionReceiveWindowSize");
        }
        // This will not send a window update frame immediately.
        // If this value increases the allowed receive window size,
        // a WINDOW_UPDATE frame will be sent when only half of the
        // session window size remains during data frame processing.
        // If this value decreases the allowed receive window size,
        // the window will be reduced as data frames are processed.
        initialConnectionReceiveWindowSize = connectionReceiveWindowSize;
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        super.handlerAdded(ctx);
        context = ctx;
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        httpFrameDecoder.decode(in);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void readDataFramePadding(int streamId, boolean endStream, int padding) {
        int deltaWindowSize = -1 * padding;
        int newConnectionWindowSize = httpConnection.updateReceiveWindowSize(HTTP_CONNECTION_STREAM_ID,
                deltaWindowSize);

        // Check if connection window size is reduced beyond allowable lower bound
        if (newConnectionWindowSize < 0) {
            issueConnectionError(HttpErrorCode.PROTOCOL_ERROR);
            return;
        }

        // Send a WINDOW_UPDATE frame if less than half the connection window size remains
        if (newConnectionWindowSize <= initialConnectionReceiveWindowSize / 2) {
            int windowSizeIncrement = initialConnectionReceiveWindowSize - newConnectionWindowSize;
            httpConnection.updateReceiveWindowSize(HTTP_CONNECTION_STREAM_ID, windowSizeIncrement);
            ByteBuf frame = httpFrameEncoder.encodeWindowUpdateFrame(HTTP_CONNECTION_STREAM_ID,
                    windowSizeIncrement);
            context.writeAndFlush(frame);
        }

        // Check if we received a DATA frame for a stream which is half-closed (remote) or closed
        if (httpConnection.isRemoteSideClosed(streamId)) {
            if (streamId <= lastStreamId) {
                issueStreamError(streamId, HttpErrorCode.STREAM_CLOSED);
            } else if (!sentGoAwayFrame) {
                issueStreamError(streamId, HttpErrorCode.PROTOCOL_ERROR);
            }
            return;
        }

        // Update receive window size
        int newWindowSize = httpConnection.updateReceiveWindowSize(streamId, deltaWindowSize);

        // Window size can become negative if we sent a SETTINGS frame that reduces the
        // size of the transfer window after the peer has written data frames.
        // The value is bounded by the length that SETTINGS frame decrease the window.
        // This difference is stored for the connection when writing the SETTINGS frame
        // and is cleared once we send a WINDOW_UPDATE frame.
        if (newWindowSize < httpConnection.getReceiveWindowSizeLowerBound(streamId)) {
            issueStreamError(streamId, HttpErrorCode.FLOW_CONTROL_ERROR);
            return;
        }

        // Send a WINDOW_UPDATE frame if less than half the stream window size remains
        // Recipient should not send a WINDOW_UPDATE frame as it consumes the last data frame.
        if (handleStreamWindowUpdates && newWindowSize <= initialReceiveWindowSize / 2 && !endStream) {
            int windowSizeIncrement = initialReceiveWindowSize - newWindowSize;
            httpConnection.updateReceiveWindowSize(streamId, windowSizeIncrement);
            ByteBuf frame = httpFrameEncoder.encodeWindowUpdateFrame(streamId, windowSizeIncrement);
            context.writeAndFlush(frame);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void readDataFrame(int streamId, boolean endStream, boolean endSegment, ByteBuf data) {
        // HTTP/2 DATA frame processing requirements:
        //
        // If an endpoint receives a data frame for a Stream-ID which is not open
        // and the endpoint has not sent a GOAWAY frame, it must issue a stream error
        // with the error code INVALID_STREAM for the Stream-ID.
        //
        // If an endpoint receives multiple data frames for invalid Stream-IDs,
        // it may close the connection.
        //
        // If an endpoint refuses a stream it must ignore any data frames for that stream.
        //
        // If an endpoint receives a data frame after the stream is half-closed (remote)
        // or closed, it must respond with a stream error of type STREAM_CLOSED.

        int deltaWindowSize = -1 * data.readableBytes();
        int newConnectionWindowSize = httpConnection.updateReceiveWindowSize(HTTP_CONNECTION_STREAM_ID,
                deltaWindowSize);

        // Check if connection window size is reduced beyond allowable lower bound
        if (newConnectionWindowSize < 0) {
            issueConnectionError(HttpErrorCode.PROTOCOL_ERROR);
            return;
        }

        // Send a WINDOW_UPDATE frame if less than half the connection window size remains
        if (newConnectionWindowSize <= initialConnectionReceiveWindowSize / 2) {
            int windowSizeIncrement = initialConnectionReceiveWindowSize - newConnectionWindowSize;
            httpConnection.updateReceiveWindowSize(HTTP_CONNECTION_STREAM_ID, windowSizeIncrement);
            ByteBuf frame = httpFrameEncoder.encodeWindowUpdateFrame(HTTP_CONNECTION_STREAM_ID,
                    windowSizeIncrement);
            context.writeAndFlush(frame);
        }

        // Check if we received a DATA frame for a stream which is half-closed (remote) or closed
        if (httpConnection.isRemoteSideClosed(streamId)) {
            if (streamId <= lastStreamId) {
                issueStreamError(streamId, HttpErrorCode.STREAM_CLOSED);
            } else if (!sentGoAwayFrame) {
                issueStreamError(streamId, HttpErrorCode.PROTOCOL_ERROR);
            }
            return;
        }

        // Update receive window size
        int newWindowSize = httpConnection.updateReceiveWindowSize(streamId, deltaWindowSize);

        // Window size can become negative if we sent a SETTINGS frame that reduces the
        // size of the transfer window after the peer has written data frames.
        // The value is bounded by the length that SETTINGS frame decrease the window.
        // This difference is stored for the connection when writing the SETTINGS frame
        // and is cleared once we send a WINDOW_UPDATE frame.
        if (newWindowSize < httpConnection.getReceiveWindowSizeLowerBound(streamId)) {
            issueStreamError(streamId, HttpErrorCode.FLOW_CONTROL_ERROR);
            return;
        }

        // Window size became negative due to sender writing frame before receiving SETTINGS
        // Send data frames upstream in initialReceiveWindowSize chunks
        if (newWindowSize < 0) {
            while (data.readableBytes() > initialReceiveWindowSize) {
                ByteBuf partialData = data.readBytes(initialReceiveWindowSize);
                HttpDataFrame partialDataFrame = new DefaultHttpDataFrame(streamId, partialData);
                context.fireChannelRead(partialDataFrame);
            }
        }

        // Send a WINDOW_UPDATE frame if less than half the stream window size remains
        // Recipient should not send a WINDOW_UPDATE frame as it consumes the last data frame.
        if (handleStreamWindowUpdates && newWindowSize <= initialReceiveWindowSize / 2 && !endStream) {
            int windowSizeIncrement = initialReceiveWindowSize - newWindowSize;
            httpConnection.updateReceiveWindowSize(streamId, windowSizeIncrement);
            ByteBuf frame = httpFrameEncoder.encodeWindowUpdateFrame(streamId, windowSizeIncrement);
            context.writeAndFlush(frame);
        }

        // Close the remote side of the stream if this is the last frame
        if (endStream) {
            halfCloseStream(streamId, true, context.channel().newSucceededFuture());
        }

        HttpDataFrame httpDataFrame = new DefaultHttpDataFrame(streamId, data);
        httpDataFrame.setLast(endStream);
        context.fireChannelRead(httpDataFrame);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void readHeadersFrame(int streamId, boolean endStream, boolean endSegment, boolean exclusive,
            int dependency, int weight) {
        // HTTP/2 HEADERS frame processing requirements:
        //
        // If an endpoint receives a HEADERS frame with a Stream-ID that is less than
        // any previously received HEADERS, it must issue a connection error of type
        // PROTOCOL_ERROR.
        //
        // If an endpoint receives multiple SYN_STREAM frames with the same active
        // Stream-ID, it must issue a stream error with the status code PROTOCOL_ERROR.
        //
        // The recipient can reject a stream by sending a stream error with the
        // status code REFUSED_STREAM.

        if (isRemoteInitiatedId(streamId)) {
            if (streamId <= lastStreamId) {
                // Check if we received a HEADERS frame for a stream which is half-closed (remote) or closed
                if (httpConnection.isRemoteSideClosed(streamId)) {
                    issueStreamError(streamId, HttpErrorCode.STREAM_CLOSED);
                    return;
                }
            } else {
                // Try to accept the stream
                if (!acceptStream(streamId, exclusive, dependency, weight)) {
                    issueStreamError(streamId, HttpErrorCode.REFUSED_STREAM);
                    return;
                }
            }
        } else {
            // Check if we received a HEADERS frame for a stream which is half-closed (remote) or closed
            if (httpConnection.isRemoteSideClosed(streamId)) {
                issueStreamError(streamId, HttpErrorCode.STREAM_CLOSED);
                return;
            }
        }

        // Close the remote side of the stream if this is the last frame
        if (endStream) {
            halfCloseStream(streamId, true, context.channel().newSucceededFuture());
        }

        HttpHeadersFrame httpHeadersFrame = new DefaultHttpHeadersFrame(streamId);
        httpHeadersFrame.setLast(endStream);
        httpHeadersFrame.setExclusive(exclusive);
        httpHeadersFrame.setDependency(dependency);
        httpHeadersFrame.setWeight(weight);
        httpHeaderBlockFrame = httpHeadersFrame;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void readPriorityFrame(int streamId, boolean exclusive, int dependency, int weight) {
        if (streamId == dependency) {
            issueStreamError(streamId, HttpErrorCode.PROTOCOL_ERROR);
        } else {
            setPriority(streamId, exclusive, dependency, weight);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void readRstStreamFrame(int streamId, int errorCode) {
        // If a RST_STREAM frame identifying an idle stream is received,
        // the recipient MUST treat this as a connection error of type
        // PROTOCOL_ERROR.
        removeStream(streamId, context.channel().newSucceededFuture());
        HttpRstStreamFrame httpRstStreamFrame = new DefaultHttpRstStreamFrame(streamId, errorCode);
        context.fireChannelRead(httpRstStreamFrame);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void readSettingsFrame(boolean ack) {
        needSettingsAck = !ack;
        httpSettingsFrame = new DefaultHttpSettingsFrame();
        httpSettingsFrame.setAck(ack);
        if (ack && changeDecoderHeaderTableSize) {
            httpHeaderBlockDecoder.setMaxHeaderTableSize(headerTableSize);
            changeDecoderHeaderTableSize = false;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void readSetting(int id, int value) {
        httpSettingsFrame.setValue(id, value);
        switch (id) {
        case HttpSettingsFrame.SETTINGS_HEADER_TABLE_SIZE:
            // Ignore 'negative' values -- they are too large for java
            if (value >= 0) {
                changeEncoderHeaderTableSize = true;
                lastHeaderTableSize = value;
                if (lastHeaderTableSize < minHeaderTableSize) {
                    minHeaderTableSize = lastHeaderTableSize;
                }
            }
            break;
        case HttpSettingsFrame.SETTINGS_ENABLE_PUSH:
            if (value == 0) {
                pushEnabled = false;
            } else if (value == 1) {
                pushEnabled = true;
            } else {
                issueConnectionError(HttpErrorCode.PROTOCOL_ERROR);
            }
            break;
        case HttpSettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS:
            if (value >= 0) {
                remoteConcurrentStreams = value;
            }
            break;
        case HttpSettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE:
            if (value >= 0) {
                updateInitialSendWindowSize(value);
            } else {
                issueConnectionError(HttpErrorCode.FLOW_CONTROL_ERROR);
            }
            break;
        case HttpSettingsFrame.SETTINGS_MAX_FRAME_SIZE:
            if (value != HttpCodecUtil.HTTP_MAX_LENGTH) {
                issueConnectionError(HttpErrorCode.PROTOCOL_ERROR);
            }
            break;
        default:
            // Ignore Unknown Settings
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void readSettingsEnd() {
        if (changeEncoderHeaderTableSize) {
            synchronized (httpHeaderBlockEncoder) {
                httpHeaderBlockEncoder.setDecoderMaxHeaderTableSize(minHeaderTableSize);
                httpHeaderBlockEncoder.setDecoderMaxHeaderTableSize(lastHeaderTableSize);

                // Writes of settings ack must occur in order
                ByteBuf frame = httpFrameEncoder.encodeSettingsFrame(SETTINGS_ACK_FRAME);
                context.writeAndFlush(frame);
            }
            changeEncoderHeaderTableSize = false;
            lastHeaderTableSize = Integer.MAX_VALUE;
            minHeaderTableSize = Integer.MAX_VALUE;
        } else if (needSettingsAck) {
            ByteBuf frame = httpFrameEncoder.encodeSettingsFrame(SETTINGS_ACK_FRAME);
            context.writeAndFlush(frame);
        }
        Object frame = httpSettingsFrame;
        httpSettingsFrame = null;
        context.fireChannelRead(frame);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void readPushPromiseFrame(int streamId, int promisedStreamId) {
        // TODO(jpinner) handle push promise frames
        // Any we receive must be associated with a "peer-initiated" stream.
        // Since we don't have a way currently to initiate streams, any
        // frame that we receive must be treated as a protocol error.
        issueConnectionError(HttpErrorCode.PROTOCOL_ERROR);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void readPingFrame(long data, boolean ack) {
        // HTTP/2 PING frame processing requirements:
        //
        // Receivers of a PING frame should send an identical frame to the sender
        // as soon as possible.
        //
        // Receivers of a PING frame must ignore frames that it did not initiate
        HttpPingFrame httpPingFrame = new DefaultHttpPingFrame(data);
        httpPingFrame.setPong(true);
        if (ack) {
            context.fireChannelRead(httpPingFrame);
        } else {
            ByteBuf frame = httpFrameEncoder.encodePingFrame(data, false);
            context.writeAndFlush(frame);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void readGoAwayFrame(int lastStreamId, int errorCode) {
        receivedGoAwayFrame = true;
        HttpGoAwayFrame httpGoAwayFrame = new DefaultHttpGoAwayFrame(lastStreamId, errorCode);
        context.fireChannelRead(httpGoAwayFrame);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void readWindowUpdateFrame(int streamId, int windowSizeIncrement) {
        // HTTP/2 WINDOW_UPDATE frame processing requirements:
        //
        // Receivers of a WINDOW_UPDATE that cause the window size to exceed 2^31
        // must send a RST_STREAM with the status code FLOW_CONTROL_ERROR.
        //
        // Sender should ignore all WINDOW_UPDATE frames associated with a stream
        // after sending the last frame for the stream.

        // Ignore frames for half-closed streams
        if (streamId != HTTP_CONNECTION_STREAM_ID && httpConnection.isLocalSideClosed(streamId)) {
            return;
        }

        // Check for numerical overflow
        if (httpConnection.getSendWindowSize(streamId) > Integer.MAX_VALUE - windowSizeIncrement) {
            if (streamId == HTTP_CONNECTION_STREAM_ID) {
                issueConnectionError(HttpErrorCode.PROTOCOL_ERROR);
            } else {
                issueStreamError(streamId, HttpErrorCode.FLOW_CONTROL_ERROR);
            }
            return;
        }

        updateSendWindowSize(context, streamId, windowSizeIncrement);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void readHeaderBlock(ByteBuf headerBlockFragment) {
        try {
            httpHeaderBlockDecoder.decode(headerBlockFragment, httpHeaderBlockFrame);
        } catch (IOException e) {
            httpHeaderBlockFrame = null;
            issueConnectionError(HttpErrorCode.PROTOCOL_ERROR);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void readHeaderBlockEnd() {
        httpHeaderBlockDecoder.endHeaderBlock(httpHeaderBlockFrame);

        if (httpHeaderBlockFrame == null) {
            return;
        }

        // Check if we received a valid Header Block
        if (httpHeaderBlockFrame.isInvalid()) {
            issueStreamError(httpHeaderBlockFrame.getStreamId(), HttpErrorCode.PROTOCOL_ERROR);
            return;
        }

        Object frame = httpHeaderBlockFrame;
        httpHeaderBlockFrame = null;
        context.fireChannelRead(frame);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void readFrameError(String message) {
        issueConnectionError(HttpErrorCode.PROTOCOL_ERROR);
    }

    @Override
    public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise)
            throws Exception {
        ctx.bind(localAddress, promise);
    }

    @Override
    public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress,
            ChannelPromise promise) throws Exception {
        ctx.connect(remoteAddress, localAddress, promise);
    }

    @Override
    public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
        // HTTP/2 connection requirements:
        //
        // When either endpoint closes the transport-level connection,
        // it must first send a GOAWAY frame.
        //
        // Avoid NotYetConnectedException
        if (!ctx.channel().isActive()) {
            ctx.disconnect(promise);
        } else {
            sendGoAwayFrame(ctx, promise);
        }
    }

    @Override
    public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
        // HTTP/2 connection requirements:
        //
        // When either endpoint closes the transport-level connection,
        // it must first send a GOAWAY frame.
        //
        // Avoid NotYetConnectedException
        if (!ctx.channel().isActive()) {
            ctx.close(promise);
        } else {
            sendGoAwayFrame(ctx, promise);
        }
    }

    @Override
    public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
        ctx.deregister(promise);
    }

    @Override
    public void read(ChannelHandlerContext ctx) throws Exception {
        ctx.read();
    }

    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        if (msg instanceof HttpDataFrame) {

            HttpDataFrame httpDataFrame = (HttpDataFrame) msg;
            int streamId = httpDataFrame.getStreamId();

            // Frames must not be sent on half-closed streams
            if (httpConnection.isLocalSideClosed(streamId)) {
                promise.setFailure(PROTOCOL_EXCEPTION);
                return;
            }

            // HTTP/2 DATA frame flow control processing requirements:
            //
            // Sender must not send a data frame with data length greater
            // than the transfer window size.
            //
            // After sending each data frame, the sender decrements its
            // transfer window size by the amount of data transmitted.
            //
            // When the window size becomes less than or equal to 0, the
            // sender must pause transmitting data frames.

            int dataLength = httpDataFrame.content().readableBytes();
            int sendWindowSize = httpConnection.getSendWindowSize(streamId);
            int connectionSendWindowSize = httpConnection.getSendWindowSize(HTTP_CONNECTION_STREAM_ID);
            sendWindowSize = Math.min(sendWindowSize, connectionSendWindowSize);

            if (sendWindowSize <= 0) {
                // Stream is stalled -- enqueue Data frame and return
                httpConnection.putPendingWrite(streamId, new HttpConnection.PendingWrite(httpDataFrame, promise));
                return;
            } else if (sendWindowSize < dataLength) {
                // Stream is not stalled but we cannot send the entire frame
                httpConnection.updateSendWindowSize(streamId, -1 * sendWindowSize);
                httpConnection.updateSendWindowSize(HTTP_CONNECTION_STREAM_ID, -1 * sendWindowSize);

                // Create a partial data frame whose length is the current window size
                ByteBuf data = httpDataFrame.content().readSlice(sendWindowSize);
                ByteBuf partialDataFrame = httpFrameEncoder.encodeDataFrame(streamId, false, data);

                // Enqueue the remaining data (will be the first frame queued)
                httpConnection.putPendingWrite(streamId, new HttpConnection.PendingWrite(httpDataFrame, promise));

                ChannelPromise writeFuture = ctx.channel().newPromise();

                // The transfer window size is pre-decremented when sending a data frame downstream.
                // Close the connection on write failures that leaves the transfer window in a corrupt state.
                writeFuture.addListener(connectionErrorListener);

                ctx.write(partialDataFrame, writeFuture);
                return;
            } else {
                // Window size is large enough to send entire data frame
                httpConnection.updateSendWindowSize(streamId, -1 * dataLength);
                httpConnection.updateSendWindowSize(HTTP_CONNECTION_STREAM_ID, -1 * dataLength);

                // The transfer window size is pre-decremented when sending a data frame downstream.
                // Close the connection on write failures that leaves the transfer window in a corrupt state.
                promise.addListener(connectionErrorListener);
            }

            // Close the local side of the stream if this is the last frame
            if (httpDataFrame.isLast()) {
                halfCloseStream(streamId, false, promise);
            }

            ByteBuf frame = httpFrameEncoder.encodeDataFrame(streamId, httpDataFrame.isLast(),
                    httpDataFrame.content());
            ctx.write(frame, promise);

        } else if (msg instanceof HttpHeadersFrame) {

            HttpHeadersFrame httpHeadersFrame = (HttpHeadersFrame) msg;
            int streamId = httpHeadersFrame.getStreamId();

            if (streamId <= lastStreamId) {
                // Frames must not be sent on half-closed (local) or closed streams
                if (httpConnection.isLocalSideClosed(streamId)) {
                    promise.setFailure(PROTOCOL_EXCEPTION);
                    return;
                }
            } else {
                if (isRemoteInitiatedId(streamId)) {
                    promise.setFailure(PROTOCOL_EXCEPTION);
                    return;
                }
                // Try to accept the stream
                boolean exclusive = httpHeadersFrame.isExclusive();
                int dependency = httpHeadersFrame.getDependency();
                int weight = httpHeadersFrame.getWeight();
                if (!acceptStream(streamId, exclusive, dependency, weight)) {
                    promise.setFailure(PROTOCOL_EXCEPTION);
                    return;
                }
            }

            // Close the local side of the stream if this is the last frame
            if (httpHeadersFrame.isLast()) {
                halfCloseStream(streamId, false, promise);
            }

            synchronized (httpHeaderBlockEncoder) {
                ByteBuf frame = httpFrameEncoder.encodeHeadersFrame(httpHeadersFrame.getStreamId(),
                        httpHeadersFrame.isLast(), httpHeadersFrame.isExclusive(), httpHeadersFrame.getDependency(),
                        httpHeadersFrame.getWeight(), httpHeaderBlockEncoder.encode(ctx, httpHeadersFrame));
                // Writes of compressed data must occur in order
                ctx.write(frame, promise);
            }

        } else if (msg instanceof HttpPriorityFrame) {

            HttpPriorityFrame httpPriorityFrame = (HttpPriorityFrame) msg;
            int streamId = httpPriorityFrame.getStreamId();
            boolean exclusive = httpPriorityFrame.isExclusive();
            int dependency = httpPriorityFrame.getDependency();
            int weight = httpPriorityFrame.getWeight();
            setPriority(streamId, exclusive, dependency, weight);
            ByteBuf frame = httpFrameEncoder.encodePriorityFrame(streamId, exclusive, dependency, weight);
            ctx.write(frame, promise);

        } else if (msg instanceof HttpRstStreamFrame) {

            HttpRstStreamFrame httpRstStreamFrame = (HttpRstStreamFrame) msg;
            removeStream(httpRstStreamFrame.getStreamId(), promise);
            ByteBuf frame = httpFrameEncoder.encodeRstStreamFrame(httpRstStreamFrame.getStreamId(),
                    httpRstStreamFrame.getErrorCode().getCode());
            ctx.write(frame, promise);

        } else if (msg instanceof HttpSettingsFrame) {

            // TODO(jpinner) currently cannot have more than one settings frame outstanding at a time

            HttpSettingsFrame httpSettingsFrame = (HttpSettingsFrame) msg;
            if (httpSettingsFrame.isAck()) {
                // Cannot send an acknowledgement frame
                promise.setFailure(PROTOCOL_EXCEPTION);
                return;
            }

            int newHeaderTableSize = httpSettingsFrame.getValue(HttpSettingsFrame.SETTINGS_HEADER_TABLE_SIZE);
            if (newHeaderTableSize >= 0) {
                headerTableSize = newHeaderTableSize;
                changeDecoderHeaderTableSize = true;
            }

            int newConcurrentStreams = httpSettingsFrame
                    .getValue(HttpSettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS);
            if (newConcurrentStreams >= 0) {
                localConcurrentStreams = newConcurrentStreams;
            }

            int newInitialWindowSize = httpSettingsFrame.getValue(HttpSettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE);
            if (newInitialWindowSize >= 0) {
                updateInitialReceiveWindowSize(newInitialWindowSize);
            }

            ByteBuf frame = httpFrameEncoder.encodeSettingsFrame(httpSettingsFrame);
            ctx.write(frame, promise);

        } else if (msg instanceof HttpPushPromiseFrame) {

            if (!pushEnabled) {
                promise.setFailure(PROTOCOL_EXCEPTION);
                return;
            }

            // TODO(jpinner) handle push promise frames
            promise.setFailure(PROTOCOL_EXCEPTION);

            //      synchronized (httpHeaderBlockEncoder) {
            //        HttpPushPromiseFrame httpPushPromiseFrame = (HttpPushPromiseFrame) msg;
            //        ChannelBuffer frame = httpFrameEncoder.encodePushPromiseFrame(
            //            httpPushPromiseFrame.getStreamId(),
            //            httpPushPromiseFrame.getPromisedStreamId(),
            //            httpHeaderBlockEncoder.encode(ctx, httpPushPromiseFrame)
            //        );
            //        // Writes of compressed data must occur in order
            //        Channels.write(ctx, e.getFuture(), frame, e.getRemoteAddress());
            //      }

        } else if (msg instanceof HttpPingFrame) {

            HttpPingFrame httpPingFrame = (HttpPingFrame) msg;
            if (httpPingFrame.isPong()) {
                // Cannot send a PONG frame
                promise.setFailure(PROTOCOL_EXCEPTION);
            } else {
                ByteBuf frame = httpFrameEncoder.encodePingFrame(httpPingFrame.getData(), false);
                ctx.write(frame, promise);
            }

        } else if (msg instanceof HttpGoAwayFrame) {

            // Why is this being sent? Intercept it and fail the write.
            // Should have sent a CLOSE ChannelStateEvent
            promise.setFailure(PROTOCOL_EXCEPTION);

        } else if (msg instanceof HttpWindowUpdateFrame) {

            HttpWindowUpdateFrame httpWindowUpdateFrame = (HttpWindowUpdateFrame) msg;
            int streamId = httpWindowUpdateFrame.getStreamId();

            if (handleStreamWindowUpdates || streamId == HTTP_CONNECTION_STREAM_ID) {
                // Why is this being sent? Intercept it and fail the write.
                promise.setFailure(PROTOCOL_EXCEPTION);
            } else {
                int windowSizeIncrement = httpWindowUpdateFrame.getWindowSizeIncrement();
                httpConnection.updateReceiveWindowSize(streamId, windowSizeIncrement);
                ByteBuf frame = httpFrameEncoder.encodeWindowUpdateFrame(streamId, windowSizeIncrement);
                ctx.write(frame, promise);
            }

        } else {

            ctx.write(msg, promise);
        }
    }

    @Override
    public void flush(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    // HTTP/2 Connection Error Handling:
    //
    // When a connection error occurs, the endpoint encountering the error must first
    // send a GOAWAY frame with the stream identifier of the most recently received stream
    // from the remote endpoint, and the error code for why the connection is terminating.
    //
    // After sending the GOAWAY frame, the endpoint must close the TCP connection.
    private void issueConnectionError(HttpErrorCode status) {
        ChannelFuture future = sendGoAwayFrame(status);
        future.addListener(ChannelFutureListener.CLOSE);
    }

    // Http/2 Stream Error Handling:
    //
    // Upon a stream error, the endpoint must send a RST_STREAM frame which contains
    // the Stream-ID for the stream where the error occurred and the error status which
    // caused the error.
    //
    // After sending the RST_STREAM, the stream is closed to the sending endpoint.
    //
    // Note: this is only called by the worker thread
    private void issueStreamError(int streamId, HttpErrorCode errorCode) {

        boolean fireMessageReceived = !httpConnection.isRemoteSideClosed(streamId);
        ChannelPromise promise = context.channel().newPromise();
        removeStream(streamId, promise);

        ByteBuf frame = httpFrameEncoder.encodeRstStreamFrame(streamId, errorCode.getCode());
        context.writeAndFlush(frame, promise);
        if (fireMessageReceived) {
            HttpRstStreamFrame httpRstStreamFrame = new DefaultHttpRstStreamFrame(streamId, errorCode);
            context.fireChannelRead(httpRstStreamFrame);
        }
    }

    // Helper functions

    private boolean isRemoteInitiatedId(int id) {
        boolean serverId = isServerId(id);
        return server && !serverId || !server && serverId;
    }

    // need to synchronize to prevent new streams from being created while updating active streams
    private synchronized void updateInitialSendWindowSize(int newInitialWindowSize) {
        int deltaWindowSize = newInitialWindowSize - initialSendWindowSize;
        initialSendWindowSize = newInitialWindowSize;
        httpConnection.updateAllSendWindowSizes(deltaWindowSize);
    }

    // need to synchronize to prevent new streams from being created while updating active streams
    private synchronized void updateInitialReceiveWindowSize(int newInitialWindowSize) {
        int deltaWindowSize = newInitialWindowSize - initialReceiveWindowSize;
        initialReceiveWindowSize = newInitialWindowSize;
        httpConnection.updateAllReceiveWindowSizes(deltaWindowSize);
    }

    // need to synchronize accesses to sentGoAwayFrame, lastStreamId, and initial window sizes
    private synchronized boolean acceptStream(int streamId, boolean exclusive, int dependency, int weight) {
        // Cannot initiate any new streams after receiving or sending GOAWAY
        if (receivedGoAwayFrame || sentGoAwayFrame) {
            return false;
        }

        boolean remote = isRemoteInitiatedId(streamId);
        int maxConcurrentStreams = remote ? localConcurrentStreams : remoteConcurrentStreams;
        if (httpConnection.numActiveStreams(remote) >= maxConcurrentStreams) {
            return false;
        }
        httpConnection.acceptStream(streamId, false, false, initialSendWindowSize, initialReceiveWindowSize,
                remote);
        if (remote) {
            lastStreamId = streamId;
        }
        setPriority(streamId, exclusive, dependency, weight);
        return true;
    }

    private synchronized boolean setPriority(int streamId, boolean exclusive, int dependency, int weight) {
        return httpConnection.setPriority(streamId, exclusive, dependency, weight);
    }

    private void halfCloseStream(int streamId, boolean remote, ChannelFuture future) {
        if (remote) {
            httpConnection.closeRemoteSide(streamId, isRemoteInitiatedId(streamId));
        } else {
            httpConnection.closeLocalSide(streamId, isRemoteInitiatedId(streamId));
        }
        if (closingChannelFutureListener != null && httpConnection.noActiveStreams()) {
            future.addListener(closingChannelFutureListener);
        }
    }

    private void removeStream(int streamId, ChannelFuture future) {
        httpConnection.removeStream(streamId, isRemoteInitiatedId(streamId));
        if (closingChannelFutureListener != null && httpConnection.noActiveStreams()) {
            future.addListener(closingChannelFutureListener);
        }
    }

    private void updateSendWindowSize(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement) {
        httpConnection.updateSendWindowSize(streamId, windowSizeIncrement);

        while (true) {
            // Check if we have unblocked a stalled stream
            HttpConnection.PendingWrite e = httpConnection.getPendingWrite(streamId);
            if (e == null) {
                break;
            }

            HttpDataFrame httpDataFrame = e.httpDataFrame;
            int dataFrameSize = httpDataFrame.content().readableBytes();
            int writeStreamId = httpDataFrame.getStreamId();
            int sendWindowSize = httpConnection.getSendWindowSize(writeStreamId);
            int connectionSendWindowSize = httpConnection.getSendWindowSize(HTTP_CONNECTION_STREAM_ID);
            sendWindowSize = Math.min(sendWindowSize, connectionSendWindowSize);

            if (sendWindowSize <= 0) {
                return;
            } else if (sendWindowSize < dataFrameSize) {
                // We can send a partial frame
                httpConnection.updateSendWindowSize(writeStreamId, -1 * sendWindowSize);
                httpConnection.updateSendWindowSize(HTTP_CONNECTION_STREAM_ID, -1 * sendWindowSize);

                // Create a partial data frame whose length is the current window size
                ByteBuf data = httpDataFrame.content().readSlice(sendWindowSize);
                ByteBuf partialDataFrame = httpFrameEncoder.encodeDataFrame(writeStreamId, false, data);

                ChannelPromise writeFuture = ctx.channel().newPromise();

                // The transfer window size is pre-decremented when sending a data frame downstream.
                // Close the connection on write failures that leaves the transfer window in a corrupt state.
                writeFuture.addListener(connectionErrorListener);

                ctx.writeAndFlush(partialDataFrame, writeFuture);
            } else {
                // Window size is large enough to send entire data frame
                httpConnection.removePendingWrite(writeStreamId);
                httpConnection.updateSendWindowSize(writeStreamId, -1 * dataFrameSize);
                httpConnection.updateSendWindowSize(HTTP_CONNECTION_STREAM_ID, -1 * dataFrameSize);

                // The transfer window size is pre-decremented when sending a data frame downstream.
                // Close the connection on write failures that leaves the transfer window in a corrupt state.
                e.promise.addListener(connectionErrorListener);

                // Close the local side of the stream if this is the last frame
                if (httpDataFrame.isLast()) {
                    halfCloseStream(writeStreamId, false, e.promise);
                }

                ByteBuf frame = httpFrameEncoder.encodeDataFrame(writeStreamId, httpDataFrame.isLast(),
                        httpDataFrame.content());

                ctx.writeAndFlush(frame, e.promise);
            }
        }
    }

    private void sendGoAwayFrame(ChannelHandlerContext ctx, ChannelPromise promise) {
        ChannelFuture future = sendGoAwayFrame(HttpErrorCode.NO_ERROR);
        if (httpConnection.noActiveStreams()) {
            future.addListener(new ClosingChannelFutureListener(ctx, promise));
        } else {
            closingChannelFutureListener = new ClosingChannelFutureListener(ctx, promise);
        }
    }

    private synchronized ChannelFuture sendGoAwayFrame(HttpErrorCode httpErrorCode) {
        if (!sentGoAwayFrame) {
            sentGoAwayFrame = true;
            ChannelPromise promise = context.channel().newPromise();
            ByteBuf frame = httpFrameEncoder.encodeGoAwayFrame(lastStreamId, httpErrorCode.getCode());
            context.writeAndFlush(frame, promise);
            return promise;
        }
        return context.channel().newSucceededFuture();
    }

    private final class ConnectionErrorFutureListener implements ChannelFutureListener {
        @Override
        public void operationComplete(ChannelFuture future) throws Exception {
            if (!future.isSuccess()) {
                issueConnectionError(HttpErrorCode.INTERNAL_ERROR);
            }
        }
    }

    private static final class ClosingChannelFutureListener implements ChannelFutureListener {
        private final ChannelHandlerContext ctx;
        private final ChannelPromise promise;

        ClosingChannelFutureListener(ChannelHandlerContext ctx, ChannelPromise promise) {
            this.ctx = ctx;
            this.promise = promise;
        }

        public void operationComplete(ChannelFuture sentGoAwayFuture) throws Exception {
            if (!(sentGoAwayFuture.cause() instanceof ClosedChannelException)) {
                ctx.close(promise);
            } else {
                promise.setSuccess();
            }
        }
    }
}