Repository: netty/netty
Issue: Add padding length to HTTP/2 flow control

We're currently out-of-spec with HTTP/2, which requires that any padding be included in the flow-controlled bytes.


Issue comments:
@nmittler is this still something we need ?

@normanmaurer yeah we're still out of spec WRT padding.  It's low priority though and can wait until after we get draft 14 support.

Draft 14 is tracked in issue https://github.com/netty/netty/issues/2715.  

Fixed by #2805


Pull request review comments:
Isn't this still deferring all the padding until the last frame on the split?

I thought this was to address the TODO, but I see the PR description does not focus on addressing how to split padding.  If this does not address the TODO then leave it in there until we cover it.

Should we open another issue for this then or do you plan to address this before merge ?

@Scottmitch ultimately it is addressing the TODO ... I just made the decision to have all the padding at the end.  The main difference here is that the padding is now flow controlled as well.

@jpinner do you have any thoughts on how to split data frames that contain padding?  Are there any security concerns with keeping all of the padding at the end? Or is it preferred to split the padding as the frame is split?

I plan on addressing in our catch-all bug for TODOs (#2782)

@Scottmitch added the TODO back in.

Can you move the static imports back where they were before ?

@normanmaurer are you fine with keeping fully-qualified class names?

Also, is having static imports last a Netty convention?  If so, perhaps it can be incorporated into the build checks?

yeah this is fine...

done.


Patch that resolved the issue:
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2InboundFlowController.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2InboundFlowController.java
index 83bb670b11d..dd9c0427983 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2InboundFlowController.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2InboundFlowController.java
@@ -68,7 +68,7 @@ public int initialInboundWindowSize() {
     public void applyInboundFlowControl(int streamId, ByteBuf data, int padding,
             boolean endOfStream, FrameWriter frameWriter)
             throws Http2Exception {
-        int dataLength = data.readableBytes();
+        int dataLength = data.readableBytes() + padding;
         applyConnectionFlowControl(dataLength, frameWriter);
         applyStreamFlowControl(streamId, dataLength, endOfStream, frameWriter);
     }
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2OutboundFlowController.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2OutboundFlowController.java
index 815b7cafab4..63a41775948 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2OutboundFlowController.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2OutboundFlowController.java
@@ -28,10 +28,14 @@
 import java.util.List;
 import java.util.Queue;
 
-import static io.netty.handler.codec.http2.Http2CodecUtil.*;
-import static io.netty.handler.codec.http2.Http2Error.*;
-import static io.netty.handler.codec.http2.Http2Exception.*;
-import static java.lang.Math.*;
+import static io.netty.handler.codec.http2.Http2CodecUtil.CONNECTION_STREAM_ID;
+import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_WINDOW_SIZE;
+import static io.netty.handler.codec.http2.Http2Error.FLOW_CONTROL_ERROR;
+import static io.netty.handler.codec.http2.Http2Error.STREAM_CLOSED;
+import static io.netty.handler.codec.http2.Http2Exception.format;
+import static io.netty.handler.codec.http2.Http2Exception.protocolError;
+import static java.lang.Math.max;
+import static java.lang.Math.min;
 
 /**
  * Basic implementation of {@link Http2OutboundFlowController}.
@@ -178,7 +182,7 @@ public ChannelFuture writeData(ChannelHandlerContext ctx, int streamId, ByteBuf
             int window = state.writableWindow();
 
             OutboundFlowState.Frame frame = state.newFrame(ctx, promise, data, padding, endStream);
-            if (window >= data.readableBytes()) {
+            if (window >= frame.size()) {
                 // Window size is large enough to send entire data frame
                 frame.write();
                 ctx.flush();
@@ -561,8 +565,11 @@ private final class Frame {
                 promiseAggregator.add(promise);
             }
 
+            /**
+             * Gets the total size (in bytes) of this frame including the data and padding.
+             */
             int size() {
-                return data.readableBytes();
+                return data.readableBytes() + padding;
             }
 
             void enqueue() {
@@ -571,7 +578,7 @@ void enqueue() {
                     pendingWriteQueue.offer(this);
 
                     // Increment the number of pending bytes for this stream.
-                    incrementPendingBytes(data.readableBytes());
+                    incrementPendingBytes(size());
                 }
             }
 
@@ -599,13 +606,13 @@ void write() throws Http2Exception {
                 // Using a do/while loop because if the buffer is empty we still need to call
                 // the writer once to send the empty frame.
                 do {
-                    int bytesToWrite = data.readableBytes();
+                    int bytesToWrite = size();
                     int frameBytes = Math.min(bytesToWrite, frameWriter.maxFrameSize());
                     if (frameBytes == bytesToWrite) {
                         // All the bytes fit into a single HTTP/2 frame, just send it all.
                         connectionState().incrementStreamWindow(-bytesToWrite);
                         incrementStreamWindow(-bytesToWrite);
-                        ByteBuf slice = data.readSlice(bytesToWrite);
+                        ByteBuf slice = data.readSlice(data.readableBytes());
                         frameWriter.writeData(ctx, stream.id(), slice, padding, endStream, promise);
                         decrementPendingBytes(bytesToWrite);
                         return;
@@ -622,26 +629,34 @@ void write() throws Http2Exception {
              * removed from this branch of the priority tree.
              */
             void writeError(Http2Exception cause) {
-                decrementPendingBytes(data.readableBytes());
+                decrementPendingBytes(size());
                 data.release();
                 promise.setFailure(cause);
             }
 
             /**
-             * Creates a new frame that is a view of this frame's data buffer starting at the current read index with
-             * the given number of bytes. The reader index on the input frame is then advanced by the number of bytes.
-             * The returned frame will not have end-of-stream set and it will not be automatically placed in the pending
-             * queue.
+             * Creates a new frame that is a view of this frame's data. The {@code maxBytes} are
+             * first split from the data buffer. If not all the requested bytes are available, the
+             * remaining bytes are then split from the padding (if available).
              *
              * @param maxBytes
              *            the maximum number of bytes that is allowed in the created frame.
              * @return the partial frame.
              */
             Frame split(int maxBytes) {
-                // TODO: Should padding be included in the chunks or only the last frame?
-                maxBytes = min(maxBytes, data.readableBytes());
-                Frame frame = new Frame(ctx, promiseAggregator, data.readSlice(maxBytes).retain(), 0, false);
-                decrementPendingBytes(maxBytes);
+                // TODO: Should padding be spread across chunks or only at the end?
+
+                // Get the portion of the data buffer to be split. Limit to the readable bytes.
+                int dataSplit = min(maxBytes, data.readableBytes());
+
+                // Split any remaining bytes from the padding.
+                int paddingSplit = min(maxBytes - dataSplit, padding);
+
+                ByteBuf splitSlice = data.readSlice(dataSplit).retain();
+                Frame frame = new Frame(ctx, promiseAggregator, splitSlice, paddingSplit, false);
+
+                int totalBytesSplit = dataSplit + paddingSplit;
+                decrementPendingBytes(totalBytesSplit);
                 return frame;
             }
 
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2InboundFlowController.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2InboundFlowController.java
index 9deafb21157..7be4506cadf 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2InboundFlowController.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2InboundFlowController.java
@@ -24,6 +24,7 @@ public interface Http2InboundFlowController {
 
     /**
      * A writer of window update frames.
+     * TODO: Use Http2FrameWriter instead.
      */
     interface FrameWriter {
 


Test patch:
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2InboundFlowControllerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2InboundFlowControllerTest.java
index d2bffd06843..d3b5d326de9 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2InboundFlowControllerTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2InboundFlowControllerTest.java
@@ -59,13 +59,14 @@ public void setup() throws Http2Exception {
 
     @Test
     public void dataFrameShouldBeAccepted() throws Http2Exception {
-        applyFlowControl(10, false);
+        applyFlowControl(10, 0, false);
         verifyWindowUpdateNotSent();
     }
 
     @Test(expected = Http2Exception.class)
     public void connectionFlowControlExceededShouldThrow() throws Http2Exception {
-        applyFlowControl(DEFAULT_WINDOW_SIZE + 1, true);
+        // Window exceeded because of the padding.
+        applyFlowControl(DEFAULT_WINDOW_SIZE, 1, true);
     }
 
     @Test
@@ -75,7 +76,7 @@ public void halfWindowRemainingShouldUpdateConnectionWindow() throws Http2Except
         int windowDelta = DEFAULT_WINDOW_SIZE - newWindow;
 
         // Set end-of-stream on the frame, so no window update will be sent for the stream.
-        applyFlowControl(dataSize, true);
+        applyFlowControl(dataSize, 0, true);
         verify(frameWriter).writeFrame(eq(CONNECTION_STREAM_ID), eq(windowDelta));
     }
 
@@ -86,7 +87,7 @@ public void halfWindowRemainingShouldUpdateAllWindows() throws Http2Exception {
         int windowDelta = getWindowDelta(initialWindowSize, initialWindowSize, dataSize);
 
         // Don't set end-of-stream so we'll get a window update for the stream as well.
-        applyFlowControl(dataSize, false);
+        applyFlowControl(dataSize, 0, false);
         verify(frameWriter).writeFrame(eq(CONNECTION_STREAM_ID), eq(windowDelta));
         verify(frameWriter).writeFrame(eq(STREAM_ID), eq(windowDelta));
     }
@@ -95,7 +96,7 @@ public void halfWindowRemainingShouldUpdateAllWindows() throws Http2Exception {
     public void initialWindowUpdateShouldAllowMoreFrames() throws Http2Exception {
         // Send a frame that takes up the entire window.
         int initialWindowSize = DEFAULT_WINDOW_SIZE;
-        applyFlowControl(initialWindowSize, false);
+        applyFlowControl(initialWindowSize, 0, false);
 
         // Update the initial window size to allow another frame.
         int newInitialWindowSize = 2 * initialWindowSize;
@@ -105,7 +106,7 @@ public void initialWindowUpdateShouldAllowMoreFrames() throws Http2Exception {
         reset(frameWriter);
 
         // Send the next frame and verify that the expected window updates were sent.
-        applyFlowControl(initialWindowSize, false);
+        applyFlowControl(initialWindowSize, 0, false);
         int delta = newInitialWindowSize - initialWindowSize;
         verify(frameWriter).writeFrame(eq(CONNECTION_STREAM_ID), eq(delta));
         verify(frameWriter).writeFrame(eq(STREAM_ID), eq(delta));
@@ -116,9 +117,9 @@ private static int getWindowDelta(int initialSize, int windowSize, int dataSize)
         return initialSize - newWindowSize;
     }
 
-    private void applyFlowControl(int dataSize, boolean endOfStream) throws Http2Exception {
+    private void applyFlowControl(int dataSize, int padding, boolean endOfStream) throws Http2Exception {
         ByteBuf buf = dummyData(dataSize);
-        controller.applyInboundFlowControl(STREAM_ID, buf, 0, endOfStream, frameWriter);
+        controller.applyInboundFlowControl(STREAM_ID, buf, padding, endOfStream, frameWriter);
         buf.release();
     }
 
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2OutboundFlowControllerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2OutboundFlowControllerTest.java
index 30930507844..cf5147b5f32 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2OutboundFlowControllerTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2OutboundFlowControllerTest.java
@@ -91,91 +91,119 @@ public void setup() throws Http2Exception {
 
     @Test
     public void frameShouldBeSentImmediately() throws Http2Exception {
-        ByteBuf data = dummyData(10);
-        send(STREAM_A, data.slice());
-        verifyWrite(STREAM_A, data);
+        ByteBuf data = dummyData(5, 5);
+        send(STREAM_A, data.slice(0, 5), 5);
+        verifyWrite(STREAM_A, data.slice(0, 5), 5);
         assertEquals(1, data.refCnt());
-        data.release();
     }
 
     @Test
     public void frameShouldSplitForMaxFrameSize() throws Http2Exception {
         when(frameWriter.maxFrameSize()).thenReturn(5);
-        ByteBuf data = dummyData(10);
-        ByteBuf slice1 = data.slice(data.readerIndex(), 5);
+        ByteBuf data = dummyData(10, 0);
+        ByteBuf slice1 = data.slice(0, 5);
         ByteBuf slice2 = data.slice(5, 5);
-        send(STREAM_A, data.slice());
-        verifyWrite(STREAM_A, slice1);
-        verifyWrite(STREAM_A, slice2);
+        send(STREAM_A, data.slice(), 0);
+        verifyWrite(STREAM_A, slice1, 0);
+        verifyWrite(STREAM_A, slice2, 0);
         assertEquals(2, data.refCnt());
-        data.release(2);
     }
 
     @Test
     public void stalledStreamShouldQueueFrame() throws Http2Exception {
         controller.initialOutboundWindowSize(0);
 
-        ByteBuf data = dummyData(10);
-        send(STREAM_A, data);
+        ByteBuf data = dummyData(10, 5);
+        send(STREAM_A, data.slice(0, 10), 5);
         verifyNoWrite(STREAM_A);
         assertEquals(1, data.refCnt());
-        data.release();
     }
 
     @Test
-    public void nonZeroWindowShouldSendPartialFrame() throws Http2Exception {
+    public void frameShouldSplit() throws Http2Exception {
         controller.initialOutboundWindowSize(5);
 
-        ByteBuf data = dummyData(10);
-        send(STREAM_A, data);
+        ByteBuf data = dummyData(5, 5);
+        send(STREAM_A, data.slice(0, 5), 5);
 
         // Verify that a partial frame of 5 was sent.
         ArgumentCaptor<ByteBuf> argument = ArgumentCaptor.forClass(ByteBuf.class);
-        captureWrite(STREAM_A, argument, false);
+        // None of the padding should be sent in the frame.
+        captureWrite(STREAM_A, argument, 0, false);
         ByteBuf writtenBuf = argument.getValue();
         assertEquals(5, writtenBuf.readableBytes());
         assertEquals(data.slice(0, 5), writtenBuf);
         assertEquals(2, writtenBuf.refCnt());
         assertEquals(2, data.refCnt());
-        data.release(2);
+    }
+
+    @Test
+    public void frameShouldSplitPadding() throws Http2Exception {
+        controller.initialOutboundWindowSize(5);
+
+        ByteBuf data = dummyData(3, 7);
+        send(STREAM_A, data.slice(0, 3), 7);
+
+        // Verify that a partial frame of 5 was sent.
+        ArgumentCaptor<ByteBuf> argument = ArgumentCaptor.forClass(ByteBuf.class);
+        captureWrite(STREAM_A, argument, 2, false);
+        ByteBuf writtenBuf = argument.getValue();
+        assertEquals(3, writtenBuf.readableBytes());
+        assertEquals(data.slice(0, 3), writtenBuf);
+        assertEquals(2, writtenBuf.refCnt());
+        assertEquals(2, data.refCnt());
+    }
+
+    @Test
+    public void emptyFrameShouldSplitPadding() throws Http2Exception {
+        controller.initialOutboundWindowSize(5);
+
+        ByteBuf data = dummyData(0, 10);
+        send(STREAM_A, data.slice(0, 0), 10);
+
+        // Verify that a partial frame of 5 was sent.
+        ArgumentCaptor<ByteBuf> argument = ArgumentCaptor.forClass(ByteBuf.class);
+        captureWrite(STREAM_A, argument, 5, false);
+        ByteBuf writtenBuf = argument.getValue();
+        assertEquals(0, writtenBuf.readableBytes());
+        assertEquals(1, writtenBuf.refCnt());
+        assertEquals(1, data.refCnt());
     }
 
     @Test
     public void initialWindowUpdateShouldSendFrame() throws Http2Exception {
         controller.initialOutboundWindowSize(0);
 
-        ByteBuf data = dummyData(10);
-        send(STREAM_A, data.slice());
+        ByteBuf data = dummyData(10, 0);
+        send(STREAM_A, data.slice(), 0);
         verifyNoWrite(STREAM_A);
 
         // Verify that the entire frame was sent.
         controller.initialOutboundWindowSize(10);
         ArgumentCaptor<ByteBuf> argument = ArgumentCaptor.forClass(ByteBuf.class);
-        captureWrite(STREAM_A, argument, false);
+        captureWrite(STREAM_A, argument, 0, false);
         ByteBuf writtenBuf = argument.getValue();
         assertEquals(data, writtenBuf);
         assertEquals(1, data.refCnt());
-        data.release();
     }
 
     @Test
     public void initialWindowUpdateShouldSendPartialFrame() throws Http2Exception {
         controller.initialOutboundWindowSize(0);
 
-        ByteBuf data = dummyData(10);
-        send(STREAM_A, data);
+        ByteBuf data = dummyData(10, 0);
+        send(STREAM_A, data, 0);
         verifyNoWrite(STREAM_A);
 
         // Verify that a partial frame of 5 was sent.
         controller.initialOutboundWindowSize(5);
         ArgumentCaptor<ByteBuf> argument = ArgumentCaptor.forClass(ByteBuf.class);
-        captureWrite(STREAM_A, argument, false);
+        captureWrite(STREAM_A, argument, 0, false);
         ByteBuf writtenBuf = argument.getValue();
         assertEquals(5, writtenBuf.readableBytes());
         assertEquals(data.slice(0, 5), writtenBuf);
         assertEquals(2, writtenBuf.refCnt());
         assertEquals(2, data.refCnt());
-        data.release(2);
     }
 
     @Test
@@ -183,18 +211,17 @@ public void connectionWindowUpdateShouldSendFrame() throws Http2Exception {
         // Set the connection window size to zero.
         controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, -DEFAULT_WINDOW_SIZE);
 
-        ByteBuf data = dummyData(10);
-        send(STREAM_A, data.slice());
+        ByteBuf data = dummyData(10, 0);
+        send(STREAM_A, data.slice(), 0);
         verifyNoWrite(STREAM_A);
 
         // Verify that the entire frame was sent.
         controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, 10);
         ArgumentCaptor<ByteBuf> argument = ArgumentCaptor.forClass(ByteBuf.class);
-        captureWrite(STREAM_A, argument, false);
+        captureWrite(STREAM_A, argument, 0, false);
         ByteBuf writtenBuf = argument.getValue();
         assertEquals(data, writtenBuf);
         assertEquals(1, data.refCnt());
-        data.release();
     }
 
     @Test
@@ -202,20 +229,19 @@ public void connectionWindowUpdateShouldSendPartialFrame() throws Http2Exception
         // Set the connection window size to zero.
         controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, -DEFAULT_WINDOW_SIZE);
 
-        ByteBuf data = dummyData(10);
-        send(STREAM_A, data);
+        ByteBuf data = dummyData(10, 0);
+        send(STREAM_A, data, 0);
         verifyNoWrite(STREAM_A);
 
         // Verify that a partial frame of 5 was sent.
         controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, 5);
         ArgumentCaptor<ByteBuf> argument = ArgumentCaptor.forClass(ByteBuf.class);
-        captureWrite(STREAM_A, argument, false);
+        captureWrite(STREAM_A, argument, 0, false);
         ByteBuf writtenBuf = argument.getValue();
         assertEquals(5, writtenBuf.readableBytes());
         assertEquals(data.slice(0, 5), writtenBuf);
         assertEquals(2, writtenBuf.refCnt());
         assertEquals(2, data.refCnt());
-        data.release(2);
     }
 
     @Test
@@ -223,18 +249,17 @@ public void streamWindowUpdateShouldSendFrame() throws Http2Exception {
         // Set the stream window size to zero.
         controller.updateOutboundWindowSize(STREAM_A, -DEFAULT_WINDOW_SIZE);
 
-        ByteBuf data = dummyData(10);
-        send(STREAM_A, data.slice());
+        ByteBuf data = dummyData(10, 0);
+        send(STREAM_A, data.slice(), 0);
         verifyNoWrite(STREAM_A);
 
         // Verify that the entire frame was sent.
         controller.updateOutboundWindowSize(STREAM_A, 10);
         ArgumentCaptor<ByteBuf> argument = ArgumentCaptor.forClass(ByteBuf.class);
-        captureWrite(STREAM_A, argument, false);
+        captureWrite(STREAM_A, argument, 0, false);
         ByteBuf writtenBuf = argument.getValue();
         assertEquals(data, writtenBuf);
         assertEquals(1, data.refCnt());
-        data.release();
     }
 
     @Test
@@ -242,19 +267,51 @@ public void streamWindowUpdateShouldSendPartialFrame() throws Http2Exception {
         // Set the stream window size to zero.
         controller.updateOutboundWindowSize(STREAM_A, -DEFAULT_WINDOW_SIZE);
 
-        ByteBuf data = dummyData(10);
-        send(STREAM_A, data);
+        ByteBuf data = dummyData(10, 0);
+        send(STREAM_A, data, 0);
         verifyNoWrite(STREAM_A);
 
         // Verify that a partial frame of 5 was sent.
         controller.updateOutboundWindowSize(STREAM_A, 5);
         ArgumentCaptor<ByteBuf> argument = ArgumentCaptor.forClass(ByteBuf.class);
-        captureWrite(STREAM_A, argument, false);
+        captureWrite(STREAM_A, argument, 0, false);
         ByteBuf writtenBuf = argument.getValue();
         assertEquals(5, writtenBuf.readableBytes());
         assertEquals(2, writtenBuf.refCnt());
         assertEquals(2, data.refCnt());
-        data.release(2);
+    }
+
+    /**
+     * In this test, we give stream A padding and verify that it's padding is properly split.
+     *
+     * <pre>
+     *         0
+     *        / \
+     *       A   B
+     * </pre>
+     */
+    @Test
+    public void multipleStreamsShouldSplitPadding() throws Http2Exception {
+        // Block the connection
+        controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, -DEFAULT_WINDOW_SIZE);
+
+        // Try sending 10 bytes on each stream. They will be pending until we free up the
+        // connection.
+        send(STREAM_A, dummyData(3, 0), 7);
+        send(STREAM_B, dummyData(10, 0), 0);
+        verifyNoWrite(STREAM_A);
+        verifyNoWrite(STREAM_B);
+
+        // Open up the connection window.
+        controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, 10);
+        ArgumentCaptor<ByteBuf> captor = ArgumentCaptor.forClass(ByteBuf.class);
+
+        // Verify that 5 bytes from A were written: 3 from data and 2 from padding.
+        captureWrite(STREAM_A, captor, 2, false);
+        assertEquals(3, captor.getValue().readableBytes());
+
+        captureWrite(STREAM_B, captor, 0, false);
+        assertEquals(5, captor.getValue().readableBytes());
     }
 
     /**
@@ -279,10 +336,10 @@ public void blockedStreamShouldSpreadDataToChildren() throws Http2Exception {
 
         // Try sending 10 bytes on each stream. They will be pending until we free up the
         // connection.
-        send(STREAM_A, dummyData(10));
-        send(STREAM_B, dummyData(10));
-        send(STREAM_C, dummyData(10));
-        send(STREAM_D, dummyData(10));
+        send(STREAM_A, dummyData(10, 0), 0);
+        send(STREAM_B, dummyData(10, 0), 0);
+        send(STREAM_C, dummyData(10, 0), 0);
+        send(STREAM_D, dummyData(10, 0), 0);
         verifyNoWrite(STREAM_A);
         verifyNoWrite(STREAM_B);
         verifyNoWrite(STREAM_C);
@@ -295,14 +352,14 @@ public void blockedStreamShouldSpreadDataToChildren() throws Http2Exception {
         // Verify that no write was done for A, since it's blocked.
         verifyNoWrite(STREAM_A);
 
-        captureWrite(STREAM_B, captor, false);
+        captureWrite(STREAM_B, captor, 0, false);
         assertEquals(5, captor.getValue().readableBytes());
 
         // Verify that C and D each shared half of A's allowance. Since A's allowance (5) cannot
         // be split evenly, one will get 3 and one will get 2.
-        captureWrite(STREAM_C, captor, false);
+        captureWrite(STREAM_C, captor, 0, false);
         int c = captor.getValue().readableBytes();
-        captureWrite(STREAM_D, captor, false);
+        captureWrite(STREAM_D, captor, 0, false);
         int d = captor.getValue().readableBytes();
         assertEquals(5, c + d);
         assertEquals(1, Math.abs(c - d));
@@ -329,10 +386,10 @@ public void childrenShouldNotSendDataUntilParentBlocked() throws Http2Exception
         controller.updateOutboundWindowSize(STREAM_B, -DEFAULT_WINDOW_SIZE);
 
         // Send 10 bytes to each.
-        send(STREAM_A, dummyData(10));
-        send(STREAM_B, dummyData(10));
-        send(STREAM_C, dummyData(10));
-        send(STREAM_D, dummyData(10));
+        send(STREAM_A, dummyData(10, 0), 0);
+        send(STREAM_B, dummyData(10, 0), 0);
+        send(STREAM_C, dummyData(10, 0), 0);
+        send(STREAM_D, dummyData(10, 0), 0);
         verifyNoWrite(STREAM_A);
         verifyNoWrite(STREAM_B);
         verifyNoWrite(STREAM_C);
@@ -343,7 +400,7 @@ public void childrenShouldNotSendDataUntilParentBlocked() throws Http2Exception
         ArgumentCaptor<ByteBuf> captor = ArgumentCaptor.forClass(ByteBuf.class);
 
         // Verify that A received all the bytes.
-        captureWrite(STREAM_A, captor, false);
+        captureWrite(STREAM_A, captor, 0, false);
         assertEquals(10, captor.getValue().readableBytes());
         verifyNoWrite(STREAM_B);
         verifyNoWrite(STREAM_C);
@@ -371,10 +428,10 @@ public void parentShouldWaterFallDataToChildren() throws Http2Exception {
         controller.updateOutboundWindowSize(STREAM_B, -DEFAULT_WINDOW_SIZE);
 
         // Only send 5 to A so that it will allow data from its children.
-        send(STREAM_A, dummyData(5));
-        send(STREAM_B, dummyData(10));
-        send(STREAM_C, dummyData(10));
-        send(STREAM_D, dummyData(10));
+        send(STREAM_A, dummyData(5, 0), 0);
+        send(STREAM_B, dummyData(10, 0), 0);
+        send(STREAM_C, dummyData(10, 0), 0);
+        send(STREAM_D, dummyData(10, 0), 0);
         verifyNoWrite(STREAM_A);
         verifyNoWrite(STREAM_B);
         verifyNoWrite(STREAM_C);
@@ -387,14 +444,14 @@ public void parentShouldWaterFallDataToChildren() throws Http2Exception {
         // Verify that no write was done for B, since it's blocked.
         verifyNoWrite(STREAM_B);
 
-        captureWrite(STREAM_A, captor, false);
+        captureWrite(STREAM_A, captor, 0, false);
         assertEquals(5, captor.getValue().readableBytes());
 
         // Verify that C and D each shared half of A's allowance. Since A's allowance (5) cannot
         // be split evenly, one will get 3 and one will get 2.
-        captureWrite(STREAM_C, captor, false);
+        captureWrite(STREAM_C, captor, 0, false);
         int c = captor.getValue().readableBytes();
-        captureWrite(STREAM_D, captor, false);
+        captureWrite(STREAM_D, captor, 0, false);
         int d = captor.getValue().readableBytes();
         assertEquals(5, c + d);
         assertEquals(1, Math.abs(c - d));
@@ -432,10 +489,10 @@ public void reprioritizeShouldAdjustOutboundFlow() throws Http2Exception {
         controller.updateOutboundWindowSize(STREAM_B, -DEFAULT_WINDOW_SIZE);
 
         // Send 10 bytes to each.
-        send(STREAM_A, dummyData(10));
-        send(STREAM_B, dummyData(10));
-        send(STREAM_C, dummyData(10));
-        send(STREAM_D, dummyData(10));
+        send(STREAM_A, dummyData(10, 0), 0);
+        send(STREAM_B, dummyData(10, 0), 0);
+        send(STREAM_C, dummyData(10, 0), 0);
+        send(STREAM_D, dummyData(10, 0), 0);
         verifyNoWrite(STREAM_A);
         verifyNoWrite(STREAM_B);
         verifyNoWrite(STREAM_C);
@@ -449,9 +506,9 @@ public void reprioritizeShouldAdjustOutboundFlow() throws Http2Exception {
         ArgumentCaptor<ByteBuf> captor = ArgumentCaptor.forClass(ByteBuf.class);
 
         // Verify that A received all the bytes.
-        captureWrite(STREAM_A, captor, false);
+        captureWrite(STREAM_A, captor, 0, false);
         assertEquals(5, captor.getValue().readableBytes());
-        captureWrite(STREAM_D, captor, false);
+        captureWrite(STREAM_D, captor, 0, false);
         assertEquals(5, captor.getValue().readableBytes());
         verifyNoWrite(STREAM_B);
         verifyNoWrite(STREAM_C);
@@ -479,10 +536,10 @@ public void writeShouldPreferHighestWeight() throws Http2Exception {
         setPriority(STREAM_D, 0, (short) 100, false);
 
         // Send a bunch of data on each stream.
-        send(STREAM_A, dummyData(1000));
-        send(STREAM_B, dummyData(1000));
-        send(STREAM_C, dummyData(1000));
-        send(STREAM_D, dummyData(1000));
+        send(STREAM_A, dummyData(1000, 0), 0);
+        send(STREAM_B, dummyData(1000, 0), 0);
+        send(STREAM_C, dummyData(1000, 0), 0);
+        send(STREAM_D, dummyData(1000, 0), 0);
         verifyNoWrite(STREAM_A);
         verifyNoWrite(STREAM_B);
         verifyNoWrite(STREAM_C);
@@ -492,22 +549,22 @@ public void writeShouldPreferHighestWeight() throws Http2Exception {
         controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, 1000);
         ArgumentCaptor<ByteBuf> captor = ArgumentCaptor.forClass(ByteBuf.class);
 
-        captureWrite(STREAM_A, captor, false);
+        captureWrite(STREAM_A, captor, 0, false);
         int aWritten = captor.getValue().readableBytes();
         int min = aWritten;
         int max = aWritten;
 
-        captureWrite(STREAM_B, captor, false);
+        captureWrite(STREAM_B, captor, 0, false);
         int bWritten = captor.getValue().readableBytes();
         min = Math.min(min, bWritten);
         max = Math.max(max, bWritten);
 
-        captureWrite(STREAM_C, captor, false);
+        captureWrite(STREAM_C, captor, 0, false);
         int cWritten = captor.getValue().readableBytes();
         min = Math.min(min, cWritten);
         max = Math.max(max, cWritten);
 
-        captureWrite(STREAM_D, captor, false);
+        captureWrite(STREAM_D, captor, 0, false);
         int dWritten = captor.getValue().readableBytes();
         min = Math.min(min, dWritten);
         max = Math.max(max, dWritten);
@@ -542,29 +599,29 @@ public void samePriorityShouldWriteEqualData() throws Http2Exception {
         setPriority(STREAM_D, 0, DEFAULT_PRIORITY_WEIGHT, false);
 
         // Send a bunch of data on each stream.
-        send(STREAM_A, dummyData(400));
-        send(STREAM_B, dummyData(500));
-        send(STREAM_C, dummyData(0));
-        send(STREAM_D, dummyData(700));
+        send(STREAM_A, dummyData(400, 0), 0);
+        send(STREAM_B, dummyData(500, 0), 0);
+        send(STREAM_C, dummyData(0, 0), 0);
+        send(STREAM_D, dummyData(700, 0), 0);
         verifyNoWrite(STREAM_A);
         verifyNoWrite(STREAM_B);
         verifyNoWrite(STREAM_D);
 
         // The write will occur on C, because it's an empty frame.
         ArgumentCaptor<ByteBuf> captor = ArgumentCaptor.forClass(ByteBuf.class);
-        captureWrite(STREAM_C, captor, false);
+        captureWrite(STREAM_C, captor, 0, false);
         assertEquals(0, captor.getValue().readableBytes());
 
         // Allow 1000 bytes to be sent.
         controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, 999);
 
-        captureWrite(STREAM_A, captor, false);
+        captureWrite(STREAM_A, captor, 0, false);
         int aWritten = captor.getValue().readableBytes();
 
-        captureWrite(STREAM_B, captor, false);
+        captureWrite(STREAM_B, captor, 0, false);
         int bWritten = captor.getValue().readableBytes();
 
-        captureWrite(STREAM_D, captor, false);
+        captureWrite(STREAM_D, captor, 0, false);
         int dWritten = captor.getValue().readableBytes();
 
         assertEquals(999, aWritten + bWritten + dWritten);
@@ -601,10 +658,10 @@ public void subTreeBytesShouldBeCorrect() throws Http2Exception {
         streamSizes.put(STREAM_B, 500);
         streamSizes.put(STREAM_C, 600);
         streamSizes.put(STREAM_D, 700);
-        send(STREAM_A, dummyData(streamSizes.get(STREAM_A)));
-        send(STREAM_B, dummyData(streamSizes.get(STREAM_B)));
-        send(STREAM_C, dummyData(streamSizes.get(STREAM_C)));
-        send(STREAM_D, dummyData(streamSizes.get(STREAM_D)));
+        send(STREAM_A, dummyData(streamSizes.get(STREAM_A), 0), 0);
+        send(STREAM_B, dummyData(streamSizes.get(STREAM_B), 0), 0);
+        send(STREAM_C, dummyData(streamSizes.get(STREAM_C), 0), 0);
+        send(STREAM_D, dummyData(streamSizes.get(STREAM_D), 0), 0);
         verifyNoWrite(STREAM_A);
         verifyNoWrite(STREAM_B);
         verifyNoWrite(STREAM_C);
@@ -666,10 +723,10 @@ public void subTreeBytesShouldBeCorrectWithRestructure() throws Http2Exception {
         streamSizes.put(STREAM_B, 500);
         streamSizes.put(STREAM_C, 600);
         streamSizes.put(STREAM_D, 700);
-        send(STREAM_A, dummyData(streamSizes.get(STREAM_A)));
-        send(STREAM_B, dummyData(streamSizes.get(STREAM_B)));
-        send(STREAM_C, dummyData(streamSizes.get(STREAM_C)));
-        send(STREAM_D, dummyData(streamSizes.get(STREAM_D)));
+        send(STREAM_A, dummyData(streamSizes.get(STREAM_A), 0), 0);
+        send(STREAM_B, dummyData(streamSizes.get(STREAM_B), 0), 0);
+        send(STREAM_C, dummyData(streamSizes.get(STREAM_C), 0), 0);
+        send(STREAM_D, dummyData(streamSizes.get(STREAM_D), 0), 0);
         verifyNoWrite(STREAM_A);
         verifyNoWrite(STREAM_B);
         verifyNoWrite(STREAM_C);
@@ -736,11 +793,11 @@ public void subTreeBytesShouldBeCorrectWithAddition() throws Http2Exception {
         streamSizes.put(STREAM_C, 600);
         streamSizes.put(STREAM_D, 700);
         streamSizes.put(STREAM_E, 900);
-        send(STREAM_A, dummyData(streamSizes.get(STREAM_A)));
-        send(STREAM_B, dummyData(streamSizes.get(STREAM_B)));
-        send(STREAM_C, dummyData(streamSizes.get(STREAM_C)));
-        send(STREAM_D, dummyData(streamSizes.get(STREAM_D)));
-        send(STREAM_E, dummyData(streamSizes.get(STREAM_E)));
+        send(STREAM_A, dummyData(streamSizes.get(STREAM_A), 0), 0);
+        send(STREAM_B, dummyData(streamSizes.get(STREAM_B), 0), 0);
+        send(STREAM_C, dummyData(streamSizes.get(STREAM_C), 0), 0);
+        send(STREAM_D, dummyData(streamSizes.get(STREAM_D), 0), 0);
+        send(STREAM_E, dummyData(streamSizes.get(STREAM_E), 0), 0);
         verifyNoWrite(STREAM_A);
         verifyNoWrite(STREAM_B);
         verifyNoWrite(STREAM_C);
@@ -802,10 +859,10 @@ public void subTreeBytesShouldBeCorrectWithRemoval() throws Http2Exception {
         streamSizes.put(STREAM_B, 500);
         streamSizes.put(STREAM_C, 600);
         streamSizes.put(STREAM_D, 700);
-        send(STREAM_A, dummyData(streamSizes.get(STREAM_A)));
-        send(STREAM_B, dummyData(streamSizes.get(STREAM_B)));
-        send(STREAM_C, dummyData(streamSizes.get(STREAM_C)));
-        send(STREAM_D, dummyData(streamSizes.get(STREAM_D)));
+        send(STREAM_A, dummyData(streamSizes.get(STREAM_A), 0), 0);
+        send(STREAM_B, dummyData(streamSizes.get(STREAM_B), 0), 0);
+        send(STREAM_C, dummyData(streamSizes.get(STREAM_C), 0), 0);
+        send(STREAM_D, dummyData(streamSizes.get(STREAM_D), 0), 0);
         verifyNoWrite(STREAM_A);
         verifyNoWrite(STREAM_B);
         verifyNoWrite(STREAM_C);
@@ -844,12 +901,12 @@ private static int calculateStreamSizeSum(IntObjectMap<Integer> streamSizes, Lis
         return sum;
     }
 
-    private void send(int streamId, ByteBuf data) throws Http2Exception {
-        controller.writeData(ctx, streamId, data, 0, false, promise);
+    private void send(int streamId, ByteBuf data, int padding) throws Http2Exception {
+        controller.writeData(ctx, streamId, data, padding, false, promise);
     }
 
-    private void verifyWrite(int streamId, ByteBuf data) {
-        verify(frameWriter).writeData(eq(ctx), eq(streamId), eq(data), eq(0), eq(false), eq(promise));
+    private void verifyWrite(int streamId, ByteBuf data, int padding) {
+        verify(frameWriter).writeData(eq(ctx), eq(streamId), eq(data), eq(padding), eq(false), eq(promise));
     }
 
     private void verifyNoWrite(int streamId) {
@@ -857,20 +914,22 @@ private void verifyNoWrite(int streamId) {
                         eq(promise));
     }
 
-    private void captureWrite(int streamId, ArgumentCaptor<ByteBuf> captor, boolean endStream) {
-        verify(frameWriter).writeData(eq(ctx), eq(streamId), captor.capture(), eq(0), eq(endStream), eq(promise));
+    private void captureWrite(int streamId, ArgumentCaptor<ByteBuf> captor, int padding,
+            boolean endStream) {
+        verify(frameWriter).writeData(eq(ctx), eq(streamId), captor.capture(), eq(padding), eq(endStream), eq(promise));
     }
 
     private void setPriority(int stream, int parent, int weight, boolean exclusive) throws Http2Exception {
         connection.stream(stream).setPriority(parent, (short) weight, exclusive);
     }
 
-    private static ByteBuf dummyData(int size) {
+    private static ByteBuf dummyData(int size, int padding) {
         String repeatedData = "0123456789";
-        ByteBuf buffer = Unpooled.buffer(size);
+        ByteBuf buffer = Unpooled.buffer(size + padding);
         for (int index = 0; index < size; ++index) {
             buffer.writeByte(repeatedData.charAt(index % repeatedData.length()));
         }
+        buffer.writeZero(padding);
         return buffer;
     }
 }