View Javadoc
1   /*
2    * Copyright 2014 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License, version 2.0 (the
5    * "License"); you may not use this file except in compliance with the License. You may obtain a
6    * 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 distributed under the License
11   * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12   * or implied. See the License for the specific language governing permissions and limitations under
13   * the License.
14   */
15  package io.netty.handler.codec.http2;
16  
17  import io.netty.buffer.ByteBuf;
18  import io.netty.channel.ChannelFuture;
19  import io.netty.channel.ChannelFutureListener;
20  import io.netty.channel.ChannelHandlerContext;
21  import io.netty.channel.ChannelPromise;
22  import io.netty.channel.CoalescingBufferQueue;
23  import io.netty.handler.codec.http.HttpStatusClass;
24  import io.netty.handler.codec.http2.Http2CodecUtil.SimpleChannelPromiseAggregator;
25  import io.netty.util.ReferenceCountUtil;
26  
27  import java.util.ArrayDeque;
28  import java.util.Queue;
29  
30  import static io.netty.handler.codec.http.HttpStatusClass.INFORMATIONAL;
31  import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
32  import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
33  import static io.netty.handler.codec.http2.Http2Exception.connectionError;
34  import static io.netty.handler.codec.http2.Http2Exception.streamError;
35  import static io.netty.util.internal.ObjectUtil.checkNotNull;
36  import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
37  import static java.lang.Integer.MAX_VALUE;
38  import static java.lang.Math.min;
39  
40  /**
41   * Default implementation of {@link Http2ConnectionEncoder}.
42   */
43  public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder, Http2SettingsReceivedConsumer {
44      private final Http2FrameWriter frameWriter;
45      private final Http2Connection connection;
46      private Http2LifecycleManager lifecycleManager;
47      // We prefer ArrayDeque to LinkedList because later will produce more GC.
48      // This initial capacity is plenty for SETTINGS traffic.
49      private final Queue<Http2Settings> outstandingLocalSettingsQueue = new ArrayDeque<Http2Settings>(4);
50      private Queue<Http2Settings> outstandingRemoteSettingsQueue;
51  
52      public DefaultHttp2ConnectionEncoder(Http2Connection connection, Http2FrameWriter frameWriter) {
53          this.connection = checkNotNull(connection, "connection");
54          this.frameWriter = checkNotNull(frameWriter, "frameWriter");
55          if (connection.remote().flowController() == null) {
56              connection.remote().flowController(new DefaultHttp2RemoteFlowController(connection));
57          }
58      }
59  
60      @Override
61      public void lifecycleManager(Http2LifecycleManager lifecycleManager) {
62          this.lifecycleManager = checkNotNull(lifecycleManager, "lifecycleManager");
63      }
64  
65      @Override
66      public Http2FrameWriter frameWriter() {
67          return frameWriter;
68      }
69  
70      @Override
71      public Http2Connection connection() {
72          return connection;
73      }
74  
75      @Override
76      public final Http2RemoteFlowController flowController() {
77          return connection().remote().flowController();
78      }
79  
80      @Override
81      public void remoteSettings(Http2Settings settings) throws Http2Exception {
82          Boolean pushEnabled = settings.pushEnabled();
83          Http2FrameWriter.Configuration config = configuration();
84          Http2HeadersEncoder.Configuration outboundHeaderConfig = config.headersConfiguration();
85          Http2FrameSizePolicy outboundFrameSizePolicy = config.frameSizePolicy();
86          if (pushEnabled != null) {
87              if (!connection.isServer() && pushEnabled) {
88                  throw connectionError(PROTOCOL_ERROR,
89                      "Client received a value of ENABLE_PUSH specified to other than 0");
90              }
91              connection.remote().allowPushTo(pushEnabled);
92          }
93  
94          Long maxConcurrentStreams = settings.maxConcurrentStreams();
95          if (maxConcurrentStreams != null) {
96              connection.local().maxActiveStreams((int) min(maxConcurrentStreams, MAX_VALUE));
97          }
98  
99          Long headerTableSize = settings.headerTableSize();
100         if (headerTableSize != null) {
101             outboundHeaderConfig.maxHeaderTableSize(headerTableSize);
102         }
103 
104         Long maxHeaderListSize = settings.maxHeaderListSize();
105         if (maxHeaderListSize != null && !connection.isServer()) {
106             // Servers ignore the MAX_HEADER_LIST_SIZE setting from clients.
107             // It's advisory in spec (RFC 9113 §6.5.2) and best praxis is to ignore it.
108             outboundHeaderConfig.maxHeaderListSize(maxHeaderListSize);
109         }
110 
111         Integer maxFrameSize = settings.maxFrameSize();
112         if (maxFrameSize != null) {
113             outboundFrameSizePolicy.maxFrameSize(maxFrameSize);
114         }
115 
116         Integer initialWindowSize = settings.initialWindowSize();
117         if (initialWindowSize != null) {
118             flowController().initialWindowSize(initialWindowSize);
119         }
120     }
121 
122     @Override
123     public ChannelFuture writeData(final ChannelHandlerContext ctx, final int streamId, ByteBuf data, int padding,
124             final boolean endOfStream, ChannelPromise promise) {
125         promise = promise.unvoid();
126         final Http2Stream stream;
127         try {
128             stream = requireStream(streamId);
129 
130             // Verify that the stream is in the appropriate state for sending DATA frames.
131             switch (stream.state()) {
132                 case OPEN:
133                 case HALF_CLOSED_REMOTE:
134                     // Allowed sending DATA frames in these states.
135                     break;
136                 default:
137                     throw new IllegalStateException("Stream " + stream.id() + " in unexpected state " + stream.state());
138             }
139         } catch (Throwable e) {
140             data.release();
141             return promise.setFailure(e);
142         }
143 
144         // Hand control of the frame to the flow controller.
145         flowController().addFlowControlled(stream,
146                 new FlowControlledData(stream, data, padding, endOfStream, promise));
147         return promise;
148     }
149 
150     @Override
151     public ChannelFuture writeHeaders(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding,
152             boolean endStream, ChannelPromise promise) {
153         return writeHeaders0(ctx, streamId, headers, false, 0, (short) 0, false, padding, endStream, promise);
154     }
155 
156     private static boolean validateHeadersSentState(Http2Stream stream, Http2Headers headers, boolean isServer,
157                                                     boolean endOfStream) {
158         boolean isInformational = isServer && HttpStatusClass.valueOf(headers.status()) == INFORMATIONAL;
159         if ((isInformational || !endOfStream) && stream.isHeadersSent() || stream.isTrailersSent()) {
160             throw new IllegalStateException("Stream " + stream.id() + " sent too many headers EOS: " + endOfStream);
161         }
162         return isInformational;
163     }
164 
165     @Override
166     public ChannelFuture writeHeaders(final ChannelHandlerContext ctx, final int streamId,
167             final Http2Headers headers, final int streamDependency, final short weight,
168             final boolean exclusive, final int padding, final boolean endOfStream, ChannelPromise promise) {
169         return writeHeaders0(ctx, streamId, headers, true, streamDependency,
170                 weight, exclusive, padding, endOfStream, promise);
171     }
172 
173     /**
174      * Write headers via {@link Http2FrameWriter}. If {@code hasPriority} is {@code false} it will ignore the
175      * {@code streamDependency}, {@code weight} and {@code exclusive} parameters.
176      */
177     private static ChannelFuture sendHeaders(Http2FrameWriter frameWriter, ChannelHandlerContext ctx, int streamId,
178                                        Http2Headers headers, final boolean hasPriority,
179                                        int streamDependency, final short weight,
180                                        boolean exclusive, final int padding,
181                                        boolean endOfStream, ChannelPromise promise) {
182         if (hasPriority) {
183             return frameWriter.writeHeaders(ctx, streamId, headers, streamDependency,
184                     weight, exclusive, padding, endOfStream, promise);
185         }
186         return frameWriter.writeHeaders(ctx, streamId, headers, padding, endOfStream, promise);
187     }
188 
189     private ChannelFuture writeHeaders0(final ChannelHandlerContext ctx, final int streamId,
190                                         final Http2Headers headers, final boolean hasPriority,
191                                         final int streamDependency, final short weight,
192                                         final boolean exclusive, final int padding,
193                                         final boolean endOfStream, ChannelPromise promise) {
194         try {
195             Http2Stream stream = connection.stream(streamId);
196             if (stream == null) {
197                 try {
198                     // We don't create the stream in a `halfClosed` state because if this is an initial
199                     // HEADERS frame we don't want the connection state to signify that the HEADERS have
200                     // been sent until after they have been encoded and placed in the outbound buffer.
201                     // Therefore, we let the `LifeCycleManager` will take care of transitioning the state
202                     // as appropriate.
203                     stream = connection.local().createStream(streamId, /*endOfStream*/ false);
204                 } catch (Http2Exception cause) {
205                     if (connection.remote().mayHaveCreatedStream(streamId)) {
206                         promise.tryFailure(new IllegalStateException("Stream no longer exists: " + streamId, cause));
207                         return promise;
208                     }
209                     throw cause;
210                 }
211             } else {
212                 switch (stream.state()) {
213                     case RESERVED_LOCAL:
214                         stream.open(endOfStream);
215                         break;
216                     case OPEN:
217                     case HALF_CLOSED_REMOTE:
218                         // Allowed sending headers in these states.
219                         break;
220                     default:
221                         throw new IllegalStateException("Stream " + stream.id() + " in unexpected state " +
222                                                         stream.state());
223                 }
224             }
225 
226             // Trailing headers must go through flow control if there are other frames queued in flow control
227             // for this stream.
228             Http2RemoteFlowController flowController = flowController();
229             if (!endOfStream || !flowController.hasFlowControlled(stream)) {
230                 // The behavior here should mirror that in FlowControlledHeaders
231 
232                 promise = promise.unvoid();
233                 boolean isInformational = validateHeadersSentState(stream, headers, connection.isServer(), endOfStream);
234 
235                 ChannelFuture future = sendHeaders(frameWriter, ctx, streamId, headers, hasPriority, streamDependency,
236                         weight, exclusive, padding, endOfStream, promise);
237 
238                 // Writing headers may fail during the encode state if they violate HPACK limits.
239                 Throwable failureCause = future.cause();
240                 if (failureCause == null) {
241                     // Synchronously set the headersSent flag to ensure that we do not subsequently write
242                     // other headers containing pseudo-header fields.
243                     //
244                     // This just sets internal stream state which is used elsewhere in the codec and doesn't
245                     // necessarily mean the write will complete successfully.
246                     stream.headersSent(isInformational);
247 
248                     if (!future.isSuccess()) {
249                         // Either the future is not done or failed in the meantime.
250                         notifyLifecycleManagerOnError(future, ctx);
251                     }
252                 } else {
253                     lifecycleManager.onError(ctx, true, failureCause);
254                 }
255 
256                 if (endOfStream) {
257                     // Must handle calling onError before calling closeStreamLocal, otherwise the error handler will
258                     // incorrectly think the stream no longer exists and so may not send RST_STREAM or perform similar
259                     // appropriate action.
260                     lifecycleManager.closeStreamLocal(stream, future);
261                 }
262 
263                 return future;
264             } else {
265                 // Pass headers to the flow-controller so it can maintain their sequence relative to DATA frames.
266                 flowController.addFlowControlled(stream,
267                         new FlowControlledHeaders(stream, headers, hasPriority, streamDependency,
268                                 weight, exclusive, padding, true, promise));
269                 return promise;
270             }
271         } catch (Throwable t) {
272             lifecycleManager.onError(ctx, true, t);
273             promise.tryFailure(t);
274             return promise;
275         }
276     }
277 
278     @Override
279     public ChannelFuture writePriority(ChannelHandlerContext ctx, int streamId, int streamDependency, short weight,
280             boolean exclusive, ChannelPromise promise) {
281         return frameWriter.writePriority(ctx, streamId, streamDependency, weight, exclusive, promise);
282     }
283 
284     @Override
285     public ChannelFuture writeRstStream(ChannelHandlerContext ctx, int streamId, long errorCode,
286             ChannelPromise promise) {
287         // Delegate to the lifecycle manager for proper updating of connection state.
288         return lifecycleManager.resetStream(ctx, streamId, errorCode, promise);
289     }
290 
291     @Override
292     public ChannelFuture writeSettings(ChannelHandlerContext ctx, Http2Settings settings,
293             ChannelPromise promise) {
294         outstandingLocalSettingsQueue.add(settings);
295         try {
296             Boolean pushEnabled = settings.pushEnabled();
297             if (pushEnabled != null && connection.isServer()) {
298                 throw connectionError(PROTOCOL_ERROR, "Server sending SETTINGS frame with ENABLE_PUSH specified");
299             }
300         } catch (Throwable e) {
301             return promise.setFailure(e);
302         }
303 
304         return frameWriter.writeSettings(ctx, settings, promise);
305     }
306 
307     @Override
308     public ChannelFuture writeSettingsAck(ChannelHandlerContext ctx, ChannelPromise promise) {
309         if (outstandingRemoteSettingsQueue == null) {
310             return frameWriter.writeSettingsAck(ctx, promise);
311         }
312         Http2Settings settings = outstandingRemoteSettingsQueue.poll();
313         if (settings == null) {
314             return promise.setFailure(new Http2Exception(INTERNAL_ERROR, "attempted to write a SETTINGS ACK with no " +
315                     " pending SETTINGS"));
316         }
317         SimpleChannelPromiseAggregator aggregator = new SimpleChannelPromiseAggregator(promise, ctx.channel(),
318                 ctx.executor());
319         // Acknowledge receipt of the settings. We should do this before we process the settings to ensure our
320         // remote peer applies these settings before any subsequent frames that we may send which depend upon
321         // these new settings. See https://github.com/netty/netty/issues/6520.
322         frameWriter.writeSettingsAck(ctx, aggregator.newPromise());
323 
324         // We create a "new promise" to make sure that status from both the write and the application are taken into
325         // account independently.
326         ChannelPromise applySettingsPromise = aggregator.newPromise();
327         try {
328             remoteSettings(settings);
329             applySettingsPromise.setSuccess();
330         } catch (Throwable e) {
331             applySettingsPromise.setFailure(e);
332             lifecycleManager.onError(ctx, true, e);
333         }
334         return aggregator.doneAllocatingPromises();
335     }
336 
337     @Override
338     public ChannelFuture writePing(ChannelHandlerContext ctx, boolean ack, long data, ChannelPromise promise) {
339         return frameWriter.writePing(ctx, ack, data, promise);
340     }
341 
342     @Override
343     public ChannelFuture writePushPromise(ChannelHandlerContext ctx, int streamId, int promisedStreamId,
344             Http2Headers headers, int padding, ChannelPromise promise) {
345         try {
346             if (connection.goAwayReceived()) {
347                 throw connectionError(PROTOCOL_ERROR, "Sending PUSH_PROMISE after GO_AWAY received.");
348             }
349 
350             Http2Stream stream = requireStream(streamId);
351             // Reserve the promised stream.
352             connection.local().reservePushStream(promisedStreamId, stream);
353 
354             promise = promise.unvoid();
355             ChannelFuture future = frameWriter.writePushPromise(ctx, streamId, promisedStreamId, headers, padding,
356                                                                 promise);
357             // Writing headers may fail during the encode state if they violate HPACK limits.
358             Throwable failureCause = future.cause();
359             if (failureCause == null) {
360                 // This just sets internal stream state which is used elsewhere in the codec and doesn't
361                 // necessarily mean the write will complete successfully.
362                 stream.pushPromiseSent();
363 
364                 if (!future.isSuccess()) {
365                     // Either the future is not done or failed in the meantime.
366                     notifyLifecycleManagerOnError(future, ctx);
367                 }
368             } else {
369                 lifecycleManager.onError(ctx, true, failureCause);
370             }
371             return future;
372         } catch (Throwable t) {
373             lifecycleManager.onError(ctx, true, t);
374             promise.tryFailure(t);
375             return promise;
376         }
377     }
378 
379     @Override
380     public ChannelFuture writeGoAway(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData,
381             ChannelPromise promise) {
382         return lifecycleManager.goAway(ctx, lastStreamId, errorCode, debugData, promise);
383     }
384 
385     @Override
386     public ChannelFuture writeWindowUpdate(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement,
387             ChannelPromise promise) {
388         return promise.setFailure(new UnsupportedOperationException("Use the Http2[Inbound|Outbound]FlowController" +
389                 " objects to control window sizes"));
390     }
391 
392     @Override
393     public ChannelFuture writeFrame(ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags,
394             ByteBuf payload, ChannelPromise promise) {
395         return frameWriter.writeFrame(ctx, frameType, streamId, flags, payload, promise);
396     }
397 
398     @Override
399     public void close() {
400         frameWriter.close();
401     }
402 
403     @Override
404     public Http2Settings pollSentSettings() {
405         return outstandingLocalSettingsQueue.poll();
406     }
407 
408     @Override
409     public Configuration configuration() {
410         return frameWriter.configuration();
411     }
412 
413     private Http2Stream requireStream(int streamId) {
414         Http2Stream stream = connection.stream(streamId);
415         if (stream == null) {
416             final String message;
417             if (connection.streamMayHaveExisted(streamId)) {
418                 message = "Stream no longer exists: " + streamId;
419             } else {
420                 message = "Stream does not exist: " + streamId;
421             }
422             throw new IllegalArgumentException(message);
423         }
424         return stream;
425     }
426 
427     @Override
428     public void consumeReceivedSettings(Http2Settings settings) {
429         if (outstandingRemoteSettingsQueue == null) {
430             outstandingRemoteSettingsQueue = new ArrayDeque<Http2Settings>(2);
431         }
432         outstandingRemoteSettingsQueue.add(settings);
433     }
434 
435     /**
436      * Wrap a DATA frame so it can be written subject to flow-control. Note that this implementation assumes it
437      * only writes padding once for the entire payload as opposed to writing it once per-frame. This makes the
438      * {@link #size} calculation deterministic thereby greatly simplifying the implementation.
439      * <p>
440      * If frame-splitting is required to fit within max-frame-size and flow-control constraints we ensure that
441      * the passed promise is not completed until last frame write.
442      * </p>
443      */
444     private final class FlowControlledData extends FlowControlledBase {
445         private final CoalescingBufferQueue queue;
446         private int dataSize;
447 
448         FlowControlledData(Http2Stream stream, ByteBuf buf, int padding, boolean endOfStream,
449                                    ChannelPromise promise) {
450             super(stream, padding, endOfStream, promise);
451             queue = new CoalescingBufferQueue(promise.channel());
452             queue.add(buf, promise);
453             dataSize = queue.readableBytes();
454         }
455 
456         @Override
457         public int size() {
458             return dataSize + padding;
459         }
460 
461         @Override
462         public void error(ChannelHandlerContext ctx, Throwable cause) {
463             queue.releaseAndFailAll(cause);
464             // Don't update dataSize because we need to ensure the size() method returns a consistent size even after
465             // error so we don't invalidate flow control when returning bytes to flow control.
466             //
467             // That said we will set dataSize and padding to 0 in the write(...) method if we cleared the queue
468             // because of an error.
469             lifecycleManager.onError(ctx, true, cause);
470         }
471 
472         @Override
473         public void write(ChannelHandlerContext ctx, int allowedBytes) {
474             int queuedData = queue.readableBytes();
475             if (!endOfStream) {
476                 if (queuedData == 0) {
477                     if (queue.isEmpty()) {
478                         // When the queue is empty it means we did clear it because of an error(...) call
479                         // (as otherwise we will have at least 1 entry in there), which will happen either when called
480                         // explicit or when the write itself fails. In this case just set dataSize and padding to 0
481                         // which will signal back that the whole frame was consumed.
482                         //
483                         // See https://github.com/netty/netty/issues/8707.
484                         padding = dataSize = 0;
485                     } else {
486                         // There's no need to write any data frames because there are only empty data frames in the
487                         // queue and it is not end of stream yet. Just complete their promises by getting the buffer
488                         // corresponding to 0 bytes and writing it to the channel (to preserve notification order).
489                         ChannelPromise writePromise = ctx.newPromise().addListener(this);
490                         ctx.write(queue.remove(0, writePromise), writePromise);
491                     }
492                     return;
493                 }
494 
495                 if (allowedBytes == 0) {
496                     return;
497                 }
498             }
499 
500             // Determine how much data to write.
501             int writableData = min(queuedData, allowedBytes);
502             ChannelPromise writePromise = ctx.newPromise().addListener(this);
503             ByteBuf toWrite = queue.remove(writableData, writePromise);
504             dataSize = queue.readableBytes();
505 
506             // The queue reported writableData readable bytes but produced fewer: a queued buffer was released or
507             // consumed while still referenced by the queue, so the stream's data is corrupted. Fail the stream.
508             int producedBytes = toWrite.readableBytes();
509             if (producedBytes < writableData) {
510                 ReferenceCountUtil.safeRelease(toWrite);
511                 // Set dataSize and padding to 0 to signal that the whole frame was consumed, so it is removed and
512                 // its bytes are returned to flow control (matching the error path above).
513                 padding = dataSize = 0;
514                 writePromise.tryFailure(streamError(stream.id(), INTERNAL_ERROR,
515                         "Stream %d flow-controlled queue produced %d bytes but reported %d",
516                         stream.id(), producedBytes, writableData));
517                 return;
518             }
519 
520             // Determine how much padding to write.
521             int writablePadding = min(allowedBytes - writableData, padding);
522             padding -= writablePadding;
523 
524             // Write the frame(s).
525             frameWriter().writeData(ctx, stream.id(), toWrite, writablePadding,
526                     endOfStream && size() == 0, writePromise);
527         }
528 
529         @Override
530         public boolean merge(ChannelHandlerContext ctx, Http2RemoteFlowController.FlowControlled next) {
531             FlowControlledData nextData;
532             if (FlowControlledData.class != next.getClass() ||
533                 MAX_VALUE - (nextData = (FlowControlledData) next).size() < size()) {
534                 return false;
535             }
536             nextData.queue.copyTo(queue);
537             dataSize = queue.readableBytes();
538             // Given that we're merging data into a frame it doesn't really make sense to accumulate padding.
539             padding = Math.max(padding, nextData.padding);
540             endOfStream = nextData.endOfStream;
541             return true;
542         }
543     }
544 
545     private void notifyLifecycleManagerOnError(ChannelFuture future, final ChannelHandlerContext ctx) {
546         future.addListener(future1 -> {
547             Throwable cause = future1.cause();
548             if (cause != null) {
549                 lifecycleManager.onError(ctx, true, cause);
550             }
551         });
552     }
553 
554     /**
555      * Wrap headers so they can be written subject to flow-control. While headers do not have cost against the
556      * flow-control window their order with respect to other frames must be maintained, hence if a DATA frame is
557      * blocked on flow-control a HEADER frame must wait until this frame has been written.
558      */
559     private final class FlowControlledHeaders extends FlowControlledBase {
560         private final Http2Headers headers;
561         private final boolean hasPriority;
562         private final int streamDependency;
563         private final short weight;
564         private final boolean exclusive;
565 
566         FlowControlledHeaders(Http2Stream stream, Http2Headers headers, boolean hasPriority,
567                               int streamDependency, short weight, boolean exclusive,
568                               int padding, boolean endOfStream, ChannelPromise promise) {
569             super(stream, padding, endOfStream, promise.unvoid());
570             this.headers = headers;
571             this.hasPriority = hasPriority;
572             this.streamDependency = streamDependency;
573             this.weight = weight;
574             this.exclusive = exclusive;
575         }
576 
577         @Override
578         public int size() {
579             return 0;
580         }
581 
582         @Override
583         public void error(ChannelHandlerContext ctx, Throwable cause) {
584             if (ctx != null) {
585                 lifecycleManager.onError(ctx, true, cause);
586             }
587             promise.tryFailure(cause);
588         }
589 
590         @Override
591         public void write(ChannelHandlerContext ctx, int allowedBytes) {
592             boolean isInformational = validateHeadersSentState(stream, headers, connection.isServer(), endOfStream);
593             // The code is currently requiring adding this listener before writing, in order to call onError() before
594             // closeStreamLocal().
595             promise.addListener(this);
596 
597             ChannelFuture f = sendHeaders(frameWriter, ctx, stream.id(), headers, hasPriority, streamDependency,
598                     weight, exclusive, padding, endOfStream, promise);
599             // Writing headers may fail during the encode state if they violate HPACK limits.
600             Throwable failureCause = f.cause();
601             if (failureCause == null) {
602                 // This just sets internal stream state which is used elsewhere in the codec and doesn't
603                 // necessarily mean the write will complete successfully.
604                 stream.headersSent(isInformational);
605             }
606         }
607 
608         @Override
609         public boolean merge(ChannelHandlerContext ctx, Http2RemoteFlowController.FlowControlled next) {
610             return false;
611         }
612     }
613 
614     /**
615      * Common base type for payloads to deliver via flow-control.
616      */
617     public abstract class FlowControlledBase implements Http2RemoteFlowController.FlowControlled,
618             ChannelFutureListener {
619         protected final Http2Stream stream;
620         protected ChannelPromise promise;
621         protected boolean endOfStream;
622         protected int padding;
623 
624         FlowControlledBase(final Http2Stream stream, int padding, boolean endOfStream,
625                 final ChannelPromise promise) {
626             checkPositiveOrZero(padding, "padding");
627             this.padding = padding;
628             this.endOfStream = endOfStream;
629             this.stream = stream;
630             this.promise = promise;
631         }
632 
633         @Override
634         public void writeComplete() {
635             if (endOfStream) {
636                 lifecycleManager.closeStreamLocal(stream, promise);
637             }
638         }
639 
640         @Override
641         public void operationComplete(ChannelFuture future) throws Exception {
642             if (!future.isSuccess()) {
643                 error(flowController().channelHandlerContext(), future.cause());
644             }
645         }
646     }
647 }