View Javadoc
1   /*
2    * Copyright 2020 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.http3;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.handler.codec.quic.QuicStreamChannel;
20  import io.netty.util.AsciiString;
21  import io.netty.util.collection.IntObjectHashMap;
22  import io.netty.util.internal.logging.InternalLogger;
23  import io.netty.util.internal.logging.InternalLoggerFactory;
24  
25  import java.util.ArrayList;
26  import java.util.List;
27  import java.util.function.BiConsumer;
28  
29  import static io.netty.handler.codec.http3.Http3CodecUtils.closeOnFailure;
30  import static io.netty.handler.codec.http3.QpackDecoderStateSyncStrategy.ackEachInsert;
31  import static io.netty.handler.codec.http3.QpackUtil.decodePrefixedIntegerAsInt;
32  import static io.netty.handler.codec.http3.QpackUtil.encodePrefixedInteger;
33  import static io.netty.handler.codec.http3.QpackUtil.firstByteEquals;
34  import static io.netty.handler.codec.http3.QpackUtil.toIntOrThrow;
35  import static java.lang.Math.floorDiv;
36  
37  final class QpackDecoder {
38      private static final InternalLogger logger = InternalLoggerFactory.getInstance(QpackDecoder.class);
39      private static final QpackException DYNAMIC_TABLE_CAPACITY_EXCEEDS_MAX =
40              QpackException.newStatic(QpackDecoder.class, "setDynamicTableCapacity(...)",
41                      "QPACK - decoder dynamic table capacity exceeds max capacity.");
42      private static final QpackException HEADER_ILLEGAL_INDEX_VALUE =
43              QpackException.newStatic(QpackDecoder.class, "decodeIndexed(...)", "QPACK - illegal index value");
44      private static final QpackException NAME_ILLEGAL_INDEX_VALUE =
45              QpackException.newStatic(QpackDecoder.class, "decodeLiteralWithNameRef(...)",
46                      "QPACK - illegal name index value");
47      private static final QpackException INVALID_REQUIRED_INSERT_COUNT =
48              QpackException.newStatic(QpackDecoder.class, "decodeRequiredInsertCount(...)",
49                      "QPACK - invalid required insert count");
50      private static final QpackException MAX_BLOCKED_STREAMS_EXCEEDED =
51              QpackException.newStatic(QpackDecoder.class, "shouldWaitForDynamicTableUpdates(...)",
52                      "QPACK - exceeded max blocked streams");
53      private static final QpackException BLOCKED_STREAM_RESUMPTION_FAILED =
54              QpackException.newStatic(QpackDecoder.class, "sendInsertCountIncrementIfRequired(...)",
55                      "QPACK - failed to resume a blocked stream");
56  
57      private static final QpackException UNKNOWN_TYPE =
58              QpackException.newStatic(QpackDecoder.class, "decode(...)", "QPACK - unknown type");
59  
60      private final QpackHuffmanDecoder huffmanDecoder;
61      private final QpackDecoderDynamicTable dynamicTable;
62      private final long maxTableCapacity;
63      private final int maxBlockedStreams;
64      private final QpackDecoderStateSyncStrategy stateSyncStrategy;
65      /**
66       * Hashmap with key as the required insert count to unblock the stream and the value a {@link List} of
67       * {@link Runnable} to invoke when the stream can be unblocked.
68       */
69      private final IntObjectHashMap<List<Runnable>> blockedStreams;
70  
71      private final long maxEntries;
72      private final long fullRange;
73      private int blockedStreamsCount;
74      private long lastAckInsertCount;
75  
76      QpackDecoder(long maxTableCapacity, int maxBlockedStreams) {
77          this(maxTableCapacity, maxBlockedStreams, new QpackDecoderDynamicTable(), ackEachInsert());
78      }
79  
80      QpackDecoder(long maxTableCapacity, int maxBlockedStreams,
81                   QpackDecoderDynamicTable dynamicTable, QpackDecoderStateSyncStrategy stateSyncStrategy) {
82          huffmanDecoder = new QpackHuffmanDecoder();
83          this.maxTableCapacity = maxTableCapacity;
84          this.maxBlockedStreams = maxBlockedStreams;
85          this.stateSyncStrategy = stateSyncStrategy;
86          blockedStreams = new IntObjectHashMap<>(Math.min(16, maxBlockedStreams));
87          this.dynamicTable = dynamicTable;
88          maxEntries = QpackUtil.maxEntries(maxTableCapacity);
89          try {
90              fullRange = toIntOrThrow(2 * maxEntries);
91          } catch (QpackException e) {
92              throw new IllegalArgumentException(e);
93          }
94      }
95  
96      /**
97       * Decode the header block and add these to the {@link BiConsumer}. This method assumes the entire header block is
98       * contained in {@code in}. However, this method may not be able to decode the header block if the QPACK dynamic
99       * table does not contain all entries required to decode the header block.
100      * See <a href="https://www.rfc-editor.org/rfc/rfc9204.html#name-blocked-streams">blocked streams</a>.
101      * In such a case, this method will return {@code false} and would invoke {@code whenDecoded} when the stream is
102      * unblocked and the header block is completely decoded.
103      *
104      * @param qpackAttributes {@link QpackAttributes} for the channel.
105      * @param streamId for the stream on which this header block was received.
106      * @param in {@link ByteBuf} containing the header block.
107      * @param length Number of bytes to be read from {@code in}
108      * @param sink {@link BiConsumer} to
109      * @param whenDecoded {@link Runnable} to invoke when a blocked decode finishes decoding.
110      * @return {@code true} if the headers were decoded.
111      */
112     public boolean decode(QpackAttributes qpackAttributes, long streamId, ByteBuf in,
113                           int length, BiConsumer<CharSequence, CharSequence> sink, Runnable whenDecoded)
114             throws QpackException {
115         final int initialReaderIdx = in.readerIndex();
116         final int requiredInsertCount = decodeRequiredInsertCount(qpackAttributes, in);
117         if (shouldWaitForDynamicTableUpdates(requiredInsertCount)) {
118             blockedStreamsCount++;
119             blockedStreams.computeIfAbsent(requiredInsertCount, __ -> new ArrayList<>(2)).add(whenDecoded);
120             in.readerIndex(initialReaderIdx);
121             return false;
122         }
123 
124         in = in.readSlice(length - (in.readerIndex() - initialReaderIdx));
125         final int base = decodeBase(in, requiredInsertCount);
126 
127         while (in.isReadable()) {
128             byte b = in.getByte(in.readerIndex());
129             if (isIndexed(b)) {
130                 decodeIndexed(in, sink, base);
131             } else if (isIndexedWithPostBase(b)) {
132                 decodeIndexedWithPostBase(in, sink, base);
133             } else if (isLiteralWithNameRef(b)) {
134                 decodeLiteralWithNameRef(in, sink, base);
135             } else if (isLiteralWithPostBaseNameRef(b)) {
136                 decodeLiteralWithPostBaseNameRef(in, sink, base);
137             } else if (isLiteral(b)) {
138                 decodeLiteral(in, sink);
139             } else {
140                 throw UNKNOWN_TYPE;
141             }
142         }
143         if (requiredInsertCount > 0) {
144             assert !qpackAttributes.dynamicTableDisabled();
145             assert qpackAttributes.decoderStreamAvailable();
146 
147             stateSyncStrategy.sectionAcknowledged(requiredInsertCount);
148             final ByteBuf sectionAck = qpackAttributes.decoderStream().alloc().buffer(8);
149             encodePrefixedInteger(sectionAck, (byte) 0b1000_0000, 7, streamId);
150             closeOnFailure(qpackAttributes.decoderStream().writeAndFlush(sectionAck));
151         }
152         return true;
153     }
154 
155     /**
156      * Updates dynamic table capacity corresponding to the
157      * <a href="https://www.rfc-editor.org/rfc/rfc9204.html#name-set-dynamic-table-capacity">
158      *     encoder instruction.</a>
159      *
160      * @param capacity New capacity.
161      * @throws  QpackException If the capacity update fails.
162      */
163     void setDynamicTableCapacity(long capacity) throws QpackException {
164         if (capacity > maxTableCapacity) {
165             throw DYNAMIC_TABLE_CAPACITY_EXCEEDS_MAX;
166         }
167         dynamicTable.setCapacity(capacity);
168     }
169 
170     /**
171      * Inserts a header field with a name reference corresponding to the
172      * <a href="https://www.rfc-editor.org/rfc/rfc9204.html#name-insert-with-name-reference">
173      *     encoder instruction.</a>
174      *
175      *  @param qpackDecoderStream {@link QuicStreamChannel} for the QPACK decoder stream.
176      *  @param staticTableRef {@code true} if the name reference is to the static table, {@code false} if the reference
177      * is to the dynamic table.
178      * @param nameIdx Index of the name in the table.
179      * @param value Literal value.
180      * @throws QpackException if the insertion fails.
181      */
182     void insertWithNameReference(QuicStreamChannel qpackDecoderStream, boolean staticTableRef, int nameIdx,
183                                  CharSequence value) throws QpackException {
184         final QpackHeaderField entryForName;
185         if (staticTableRef) {
186             entryForName = QpackStaticTable.getField(nameIdx);
187         } else {
188             entryForName = dynamicTable.getEntryRelativeEncoderInstructions(nameIdx);
189         }
190         dynamicTable.add(new QpackHeaderField(entryForName.name, value));
191         sendInsertCountIncrementIfRequired(qpackDecoderStream);
192     }
193 
194     /**
195      * Inserts a header field with a literal name corresponding to the
196      * <a href="https://www.rfc-editor.org/rfc/rfc9204.html#name-insert-with-literal-name">
197      *     encoder instruction.</a>
198      *
199      * @param qpackDecoderStream {@link QuicStreamChannel} for the QPACK decoder stream.
200      * @param name of the field.
201      * @param value of the field.
202      * @throws QpackException if the insertion fails.
203      */
204     void insertLiteral(QuicStreamChannel qpackDecoderStream, CharSequence name, CharSequence value)
205             throws QpackException {
206         dynamicTable.add(new QpackHeaderField(name, value));
207         sendInsertCountIncrementIfRequired(qpackDecoderStream);
208     }
209 
210     /**
211      * Duplicates a previous entry corresponding to the
212      * <a href="https://www.rfc-editor.org/rfc/rfc9204.html#name-insert-with-literal-name">
213      *     encoder instruction.</a>
214      *
215      * @param qpackDecoderStream {@link QuicStreamChannel} for the QPACK decoder stream.
216      * @param index which is duplicated.
217      * @throws QpackException if duplication fails.
218      */
219     void duplicate(QuicStreamChannel qpackDecoderStream, int index)
220             throws QpackException {
221         dynamicTable.add(dynamicTable.getEntryRelativeEncoderInstructions(index));
222         sendInsertCountIncrementIfRequired(qpackDecoderStream);
223     }
224 
225     /**
226      * Callback when a bi-directional stream is
227      * <a href="https://www.rfc-editor.org/rfc/rfc9204.html#name-abandonment-of-a-stream"> abandoned</a>
228      *
229      * @param qpackDecoderStream {@link QuicStreamChannel} for the QPACK decoder stream.
230      * @param streamId which is abandoned.
231      */
232     void streamAbandoned(QuicStreamChannel qpackDecoderStream, long streamId) {
233         if (maxTableCapacity == 0) {
234             return;
235         }
236         // https://www.rfc-editor.org/rfc/rfc9204.html#section-4.4.2
237         //   0   1   2   3   4   5   6   7
238         // +---+---+---+---+---+---+---+---+
239         // | 0 | 1 |     Stream ID (6+)    |
240         // +---+---+-----------------------+
241         final ByteBuf cancel = qpackDecoderStream.alloc().buffer(8);
242         encodePrefixedInteger(cancel, (byte) 0b0100_0000, 6, streamId);
243         closeOnFailure(qpackDecoderStream.writeAndFlush(cancel));
244     }
245 
246     private static boolean isIndexed(byte b) {
247         // https://www.rfc-editor.org/rfc/rfc9204.html#name-indexed-field-line
248         //   0   1   2   3   4   5   6   7
249         // +---+---+---+---+---+---+---+---+
250         // | 1 | T |      Index (6+)       |
251         // +---+---+-----------------------+
252         return (b & 0b1000_0000) == 0b1000_0000;
253     }
254 
255     private static boolean isLiteralWithNameRef(byte b) {
256         // https://www.rfc-editor.org/rfc/rfc9204.html#name-literal-field-line-with-nam
257         //  0   1   2   3   4   5   6   7
258         // +---+---+---+---+---+---+---+---+
259         // | 0 | 1 | N | T |Name Index (4+)|
260         // +---+---+---+---+---------------+
261         return (b & 0b1100_0000) == 0b0100_0000;
262     }
263 
264     private static boolean isLiteral(byte b) {
265         // https://www.rfc-editor.org/rfc/rfc9204.html#name-literal-field-line-with-lit
266         //  0   1   2   3   4   5   6   7
267         // +---+---+---+---+---+---+---+---+
268         // | 0 | 0 | 1 | N | H |NameLen(3+)|
269         // +---+---+---+---+---+-----------+
270         return (b & 0b1110_0000) == 0b0010_0000;
271     }
272 
273     private static boolean isIndexedWithPostBase(byte b) {
274         // https://www.rfc-editor.org/rfc/rfc9204.html#name-indexed-field-line-with-pos
275         //   0   1   2   3   4   5   6   7
276         // +---+---+---+---+---+---+---+---+
277         // | 0 | 0 | 0 | 1 |  Index (4+)   |
278         // +---+---+---+---+---------------+
279         return (b & 0b1111_0000) == 0b0001_0000;
280     }
281 
282     private static boolean isLiteralWithPostBaseNameRef(byte b) {
283         // https://www.rfc-editor.org/rfc/rfc9204.html#name-literal-field-line-with-pos
284         //  0   1   2   3   4   5   6   7
285         // +---+---+---+---+---+---+---+---+
286         // | 0 | 0 | 0 | 0 | N |NameIdx(3+)|
287         // +---+---+---+---+---+-----------+
288         return (b & 0b1111_0000) == 0b0000_0000;
289     }
290 
291     private void decodeIndexed(ByteBuf in, BiConsumer<CharSequence, CharSequence> sink, int base)
292             throws QpackException {
293         // https://www.rfc-editor.org/rfc/rfc9204.html#name-indexed-field-line
294         //   0   1   2   3   4   5   6   7
295         // +---+---+---+---+---+---+---+---+
296         // | 1 | T |      Index (6+)       |
297         // +---+---+-----------------------+
298         //
299         // T == 1 implies static table
300         final QpackHeaderField field;
301         if (firstByteEquals(in, (byte) 0b1100_0000)) {
302             final int idx = decodePrefixedIntegerAsInt(in, 6);
303             assert idx >= 0;
304             if (idx >= QpackStaticTable.length) {
305                 throw HEADER_ILLEGAL_INDEX_VALUE;
306             }
307             field = QpackStaticTable.getField(idx);
308         } else {
309             final int idx = decodePrefixedIntegerAsInt(in, 6);
310             assert idx >= 0;
311             field = dynamicTable.getEntryRelativeEncodedField(base - idx - 1);
312         }
313         sink.accept(field.name, field.value);
314     }
315 
316     private void decodeIndexedWithPostBase(ByteBuf in, BiConsumer<CharSequence, CharSequence> sink, int base)
317             throws QpackException {
318         // https://www.rfc-editor.org/rfc/rfc9204.html#name-indexed-field-line-with-pos
319         //   0   1   2   3   4   5   6   7
320         // +---+---+---+---+---+---+---+---+
321         // | 0 | 0 | 0 | 1 |  Index (4+)   |
322         // +---+---+---+---+---------------+
323         final int idx = decodePrefixedIntegerAsInt(in, 4);
324         assert idx >= 0;
325         QpackHeaderField field = dynamicTable.getEntryRelativeEncodedField(base + idx);
326         sink.accept(field.name, field.value);
327     }
328 
329     private void decodeLiteralWithNameRef(ByteBuf in, BiConsumer<CharSequence, CharSequence> sink, int base)
330             throws QpackException {
331         final CharSequence name;
332         // https://www.rfc-editor.org/rfc/rfc9204.html#name-literal-field-line-with-nam
333         //    0   1   2   3   4   5   6   7
334         // +---+---+---+---+---+---+---+---+
335         // | 0 | 1 | N | T |Name Index (4+)|
336         // +---+---+---+---+---------------+
337         // | H |     Value Length (7+)     |
338         // +---+---------------------------+
339         // |  Value String (Length bytes)  |
340         // +-------------------------------+
341         //
342         // T == 1 implies static table
343         if (firstByteEquals(in, (byte) 0b0001_0000)) {
344             final int idx = decodePrefixedIntegerAsInt(in, 4);
345             assert idx >= 0;
346             if (idx >= QpackStaticTable.length) {
347                 throw NAME_ILLEGAL_INDEX_VALUE;
348             }
349             name = QpackStaticTable.getField(idx).name;
350         } else {
351             final int idx = decodePrefixedIntegerAsInt(in, 4);
352             assert idx >= 0;
353             name = dynamicTable.getEntryRelativeEncodedField(base - idx - 1).name;
354         }
355         final CharSequence value = decodeHuffmanEncodedLiteral(in, 7);
356         sink.accept(name, value);
357     }
358 
359     private void decodeLiteralWithPostBaseNameRef(ByteBuf in, BiConsumer<CharSequence, CharSequence> sink, int base)
360             throws QpackException {
361         // https://www.rfc-editor.org/rfc/rfc9204.html#name-literal-field-line-with-nam
362         //   0   1   2   3   4   5   6   7
363         // +---+---+---+---+---+---+---+---+
364         // | 0 | 0 | 0 | 0 | N |NameIdx(3+)|
365         // +---+---+---+---+---+-----------+
366         // | H |     Value Length (7+)     |
367         // +---+---------------------------+
368         // |  Value String (Length bytes)  |
369         // +-------------------------------+
370         final int idx = decodePrefixedIntegerAsInt(in, 3);
371         assert idx >= 0;
372         CharSequence name = dynamicTable.getEntryRelativeEncodedField(base + idx).name;
373         final CharSequence value = decodeHuffmanEncodedLiteral(in, 7);
374         sink.accept(name, value);
375     }
376 
377     private void decodeLiteral(ByteBuf in, BiConsumer<CharSequence, CharSequence> sink) throws QpackException {
378         // https://www.rfc-editor.org/rfc/rfc9204.html#name-literal-field-line-with-lit
379         //   0   1   2   3   4   5   6   7
380         // +---+---+---+---+---+---+---+---+
381         // | 0 | 0 | 1 | N | H |NameLen(3+)|
382         // +---+---+---+---+---+-----------+
383         // |  Name String (Length bytes)   |
384         // +---+---------------------------+
385         // | H |     Value Length (7+)     |
386         // +---+---------------------------+
387         // |  Value String (Length bytes)  |
388         // +-------------------------------+
389         final CharSequence name = decodeHuffmanEncodedLiteral(in, 3);
390         final CharSequence value = decodeHuffmanEncodedLiteral(in, 7);
391         sink.accept(name, value);
392     }
393 
394     private CharSequence decodeHuffmanEncodedLiteral(ByteBuf in, int prefix) throws QpackException {
395         assert prefix < 8;
396         final boolean huffmanEncoded = firstByteEquals(in, (byte) (1 << prefix));
397         final int length = decodePrefixedIntegerAsInt(in, prefix);
398         assert length >= 0;
399         if (huffmanEncoded) {
400             return huffmanDecoder.decode(in, length);
401         }
402         byte[] buf = new byte[length];
403         in.readBytes(buf);
404         return new AsciiString(buf, false);
405     }
406 
407     // Visible for testing
408     int decodeRequiredInsertCount(QpackAttributes qpackAttributes, ByteBuf buf) throws QpackException {
409         final long encodedInsertCount = QpackUtil.decodePrefixedInteger(buf, 8);
410         assert encodedInsertCount >= 0;
411         // https://www.rfc-editor.org/rfc/rfc9204.html#name-required-insert-count
412         // FullRange = 2 * MaxEntries
413         //   if EncodedInsertCount == 0:
414         //      ReqInsertCount = 0
415         //   else:
416         //      if EncodedInsertCount > FullRange:
417         //         Error
418         //      MaxValue = TotalNumberOfInserts + MaxEntries
419         //
420         //      # MaxWrapped is the largest possible value of
421         //      # ReqInsertCount that is 0 mod 2 * MaxEntries
422         //      MaxWrapped = floor(MaxValue / FullRange) * FullRange
423         //      ReqInsertCount = MaxWrapped + EncodedInsertCount - 1
424         //
425         //      # If ReqInsertCount exceeds MaxValue, the Encoder's value
426         //      # must have wrapped one fewer time
427         //      if ReqInsertCount > MaxValue:
428         //         if ReqInsertCount <= FullRange:
429         //            Error
430         //         ReqInsertCount -= FullRange
431         //
432         //      # Value of 0 must be encoded as 0.
433         //      if ReqInsertCount == 0:
434         //         Error
435         if (encodedInsertCount == 0) {
436             return 0;
437         }
438         if (qpackAttributes.dynamicTableDisabled() || encodedInsertCount > fullRange) {
439             throw INVALID_REQUIRED_INSERT_COUNT;
440         }
441 
442         final long maxValue = dynamicTable.insertCount() + maxEntries;
443         final long maxWrapped = floorDiv(maxValue, fullRange) * fullRange;
444         long requiredInsertCount = maxWrapped + encodedInsertCount - 1;
445 
446         if (requiredInsertCount > maxValue) {
447             if (requiredInsertCount <= fullRange) {
448                 throw INVALID_REQUIRED_INSERT_COUNT;
449             }
450             requiredInsertCount -= fullRange;
451         }
452         // requiredInsertCount can not be negative as encodedInsertCount read from the buffer can not be negative.
453         if (requiredInsertCount == 0) {
454             throw INVALID_REQUIRED_INSERT_COUNT;
455         }
456         return toIntOrThrow(requiredInsertCount);
457     }
458 
459     // Visible for testing
460     int decodeBase(ByteBuf buf, int requiredInsertCount) throws QpackException {
461         // https://www.rfc-editor.org/rfc/rfc9204.html#name-encoded-field-section-prefi
462         //   0   1   2   3   4   5   6   7
463         // +---+---------------------------+
464         // | S |      Delta Base (7+)      |
465         // +---+---------------------------+
466         final boolean s = (buf.getByte(buf.readerIndex()) & 0b1000_0000) == 0b1000_0000;
467         final int deltaBase = decodePrefixedIntegerAsInt(buf, 7);
468         assert deltaBase >= 0;
469         // https://www.rfc-editor.org/rfc/rfc9204.html#name-base
470         //    if S == 0:
471         //      Base = ReqInsertCount + DeltaBase
472         //   else:
473         //      Base = ReqInsertCount - DeltaBase - 1
474         return s ? requiredInsertCount - deltaBase - 1 : requiredInsertCount + deltaBase;
475     }
476 
477     private boolean shouldWaitForDynamicTableUpdates(int requiredInsertCount) throws QpackException {
478         if (requiredInsertCount > dynamicTable.insertCount()) {
479             if (blockedStreamsCount == maxBlockedStreams - 1) {
480                 throw MAX_BLOCKED_STREAMS_EXCEEDED;
481             }
482             return true;
483         }
484         return false;
485     }
486 
487     private void sendInsertCountIncrementIfRequired(QuicStreamChannel qpackDecoderStream) throws QpackException {
488         final int insertCount = dynamicTable.insertCount();
489         final List<Runnable> runnables = this.blockedStreams.get(insertCount);
490         if (runnables != null) {
491             boolean failed = false;
492             for (Runnable runnable : runnables) {
493                 try {
494                     runnable.run();
495                 } catch (Exception e) {
496                     failed = true;
497                     logger.error("Failed to resume a blocked stream {}.", runnable, e);
498                 }
499             }
500             if (failed) {
501                 throw BLOCKED_STREAM_RESUMPTION_FAILED;
502             }
503         }
504         if (stateSyncStrategy.entryAdded(insertCount)) {
505             // https://www.rfc-editor.org/rfc/rfc9204.html#name-insert-count-increment
506             //   0   1   2   3   4   5   6   7
507             // +---+---+---+---+---+---+---+---+
508             // | 0 | 0 |     Increment (6+)    |
509             // +---+---+-----------------------+
510             final ByteBuf incr = qpackDecoderStream.alloc().buffer(8);
511             encodePrefixedInteger(incr, (byte) 0b0, 6, insertCount - lastAckInsertCount);
512             lastAckInsertCount = insertCount;
513             closeOnFailure(qpackDecoderStream.writeAndFlush(incr));
514         }
515     }
516 }