@Override
    public ChannelFuture writeData(ChannelHandlerContext ctx, int streamId, ByteBuf data,
            int padding, boolean endStream, ChannelPromise promise) {
        final SimpleChannelPromiseAggregator promiseAggregator =
                new SimpleChannelPromiseAggregator(promise, ctx.channel(), ctx.executor());
        ByteBuf frameHeader = null;
        try {
            verifyStreamId(streamId, STREAM_ID);
            verifyPadding(padding);

            int remainingData = data.readableBytes();
            Http2Flags flags = new Http2Flags();
            flags.endOfStream(false);
            flags.paddingPresent(false);
            // Fast path to write frames of payload size maxFrameSize first.
            if (remainingData > maxFrameSize) {
                frameHeader = ctx.alloc().buffer(FRAME_HEADER_LENGTH);
                writeFrameHeaderInternal(frameHeader, maxFrameSize, DATA, flags, streamId);
                do {
                    // Write the header.
                    ctx.write(frameHeader.retainedSlice(), promiseAggregator.newPromise());

                    // Write the payload.
                    ctx.write(data.readRetainedSlice(maxFrameSize), promiseAggregator.newPromise());

                    remainingData -= maxFrameSize;
                    // Stop iterating if remainingData == maxFrameSize so we can take care of reference counts below.
                } while (remainingData > maxFrameSize);
            }

            if (padding == 0) {
                // Write the header.
                if (frameHeader != null) {
                    frameHeader.release();
                    frameHeader = null;
                }
                ByteBuf frameHeader2 = ctx.alloc().buffer(FRAME_HEADER_LENGTH);
                flags.endOfStream(endStream);
                writeFrameHeaderInternal(frameHeader2, remainingData, DATA, flags, streamId);
                ctx.write(frameHeader2, promiseAggregator.newPromise());

                // Write the payload.
                ByteBuf lastFrame = data.readSlice(remainingData);
                data = null;
                ctx.write(lastFrame, promiseAggregator.newPromise());
            } else {
                if (remainingData != maxFrameSize) {
                    if (frameHeader != null) {
                        frameHeader.release();
                        frameHeader = null;
                    }
                } else {
                    remainingData -= maxFrameSize;
                    // Write the header.
                    ByteBuf lastFrame;
                    if (frameHeader == null) {
                        lastFrame = ctx.alloc().buffer(FRAME_HEADER_LENGTH);
                        writeFrameHeaderInternal(lastFrame, maxFrameSize, DATA, flags, streamId);
                    } else {
                        lastFrame = frameHeader.slice();
                        frameHeader = null;
                    }
                    ctx.write(lastFrame, promiseAggregator.newPromise());

                    // Write the payload.
                    lastFrame = data.readableBytes() != maxFrameSize ? data.readSlice(maxFrameSize) : data;
                    data = null;
                    ctx.write(lastFrame, promiseAggregator.newPromise());
                }

                do {
                    int frameDataBytes = min(remainingData, maxFrameSize);
                    int framePaddingBytes = min(padding, max(0, maxFrameSize - 1 - frameDataBytes));

                    // Decrement the remaining counters.
                    padding -= framePaddingBytes;
                    remainingData -= frameDataBytes;

                    // Write the header.
                    ByteBuf frameHeader2 = ctx.alloc().buffer(DATA_FRAME_HEADER_LENGTH);
                    flags.endOfStream(endStream && remainingData == 0 && padding == 0);
                    flags.paddingPresent(framePaddingBytes > 0);
                    writeFrameHeaderInternal(frameHeader2, framePaddingBytes + frameDataBytes, DATA, flags, streamId);
                    writePaddingLength(frameHeader2, framePaddingBytes);
                    ctx.write(frameHeader2, promiseAggregator.newPromise());

                    // Write the payload.
                    if (data != null) { // Make sure Data is not null
                        if (remainingData == 0) {
                            ByteBuf lastFrame = data.readSlice(frameDataBytes);
                            data = null;
                            ctx.write(lastFrame, promiseAggregator.newPromise());
                        } else {
                            ctx.write(data.readRetainedSlice(frameDataBytes), promiseAggregator.newPromise());
                        }
                    }
                    // Write the frame padding.
                    if (paddingBytes(framePaddingBytes) > 0) {
                        ctx.write(ZERO_BUFFER.slice(0, paddingBytes(framePaddingBytes)),
                                  promiseAggregator.newPromise());
                    }
                } while (remainingData != 0 || padding != 0);
            }
        } catch (Throwable cause) {
            if (frameHeader != null) {
                frameHeader.release();
            }
            // Use a try/finally here in case the data has been released before calling this method. This is not
            // necessary above because we internally allocate frameHeader.
            try {
                if (data != null) {
                    data.release();
                }
            } finally {
                promiseAggregator.setFailure(cause);
                promiseAggregator.doneAllocatingPromises();
            }
            return promiseAggregator;
        }
        return promiseAggregator.doneAllocatingPromises();
    }
@Test
    public void writeEmptyDataWithPadding() {
        int streamId = 1;

        ByteBuf payloadByteBuf = Unpooled.buffer();
        frameWriter.writeData(ctx, streamId, payloadByteBuf, 2, true, promise);

        assertEquals(0, payloadByteBuf.refCnt());

        byte[] expectedFrameBytes = {
            (byte) 0x00, (byte) 0x00, (byte) 0x02, // payload length
            (byte) 0x00, // payload type
            (byte) 0x09, // flags
            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, // stream id
            (byte) 0x01, (byte) 0x00, // padding
        };
        expectedOutbound = Unpooled.copiedBuffer(expectedFrameBytes);
        assertEquals(expectedOutbound, outbound);
    }