/* * Copyright 2014 The Netty Project * * The Netty Project licenses this file to you under the Apache License, version 2.0 (the * "License"); you may not use this file except in compliance with the License. You may obtain a * copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package io.netty.handler.codec.http2; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http2.Http2FrameReader.Configuration; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.UnstableApi; import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_MAX_FRAME_SIZE; import static io.netty.handler.codec.http2.Http2CodecUtil.FRAME_HEADER_LENGTH; import static io.netty.handler.codec.http2.Http2CodecUtil.INT_FIELD_LENGTH; import static io.netty.handler.codec.http2.Http2CodecUtil.PING_FRAME_PAYLOAD_LENGTH; import static io.netty.handler.codec.http2.Http2CodecUtil.PRIORITY_ENTRY_LENGTH; import static io.netty.handler.codec.http2.Http2CodecUtil.SETTINGS_INITIAL_WINDOW_SIZE; import static io.netty.handler.codec.http2.Http2CodecUtil.SETTINGS_MAX_FRAME_SIZE; import static io.netty.handler.codec.http2.Http2CodecUtil.SETTING_ENTRY_LENGTH; import static io.netty.handler.codec.http2.Http2CodecUtil.headerListSizeExceeded; import static io.netty.handler.codec.http2.Http2CodecUtil.isMaxFrameSizeValid; import static io.netty.handler.codec.http2.Http2CodecUtil.readUnsignedInt; import static io.netty.handler.codec.http2.Http2Error.FLOW_CONTROL_ERROR; import static io.netty.handler.codec.http2.Http2Error.FRAME_SIZE_ERROR; import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR; import static io.netty.handler.codec.http2.Http2Exception.connectionError; import static io.netty.handler.codec.http2.Http2Exception.streamError; import static io.netty.handler.codec.http2.Http2FrameTypes.CONTINUATION; import static io.netty.handler.codec.http2.Http2FrameTypes.DATA; import static io.netty.handler.codec.http2.Http2FrameTypes.GO_AWAY; import static io.netty.handler.codec.http2.Http2FrameTypes.HEADERS; import static io.netty.handler.codec.http2.Http2FrameTypes.PING; import static io.netty.handler.codec.http2.Http2FrameTypes.PRIORITY; import static io.netty.handler.codec.http2.Http2FrameTypes.PUSH_PROMISE; import static io.netty.handler.codec.http2.Http2FrameTypes.RST_STREAM; import static io.netty.handler.codec.http2.Http2FrameTypes.SETTINGS; import static io.netty.handler.codec.http2.Http2FrameTypes.WINDOW_UPDATE; /** * A {@link Http2FrameReader} that supports all frame types defined by the HTTP/2 specification. */ @UnstableApi public class DefaultHttp2FrameReader implements Http2FrameReader, Http2FrameSizePolicy, Configuration { private final Http2HeadersDecoder headersDecoder; /** * {@code true} = reading headers, {@code false} = reading payload. */ private boolean readingHeaders = true; /** * Once set to {@code true} the value will never change. This is set to {@code true} if an unrecoverable error which * renders the connection unusable. */ private boolean readError; private byte frameType; private int streamId; private Http2Flags flags; private int payloadLength; private HeadersContinuation headersContinuation; private int maxFrameSize; /** * Create a new instance. * <p> * Header names will be validated. */ public DefaultHttp2FrameReader() { this(true); } /** * Create a new instance. * @param validateHeaders {@code true} to validate headers. {@code false} to not validate headers. * @see DefaultHttp2HeadersDecoder(boolean) */ public DefaultHttp2FrameReader(boolean validateHeaders) { this(new DefaultHttp2HeadersDecoder(validateHeaders)); } public DefaultHttp2FrameReader(Http2HeadersDecoder headersDecoder) { this.headersDecoder = headersDecoder; maxFrameSize = DEFAULT_MAX_FRAME_SIZE; } @Override public Http2HeadersDecoder.Configuration headersConfiguration() { return headersDecoder.configuration(); } @Override public Configuration configuration() { return this; } @Override public Http2FrameSizePolicy frameSizePolicy() { return this; } @Override public void maxFrameSize(int max) throws Http2Exception { if (!isMaxFrameSizeValid(max)) { throw streamError(streamId, FRAME_SIZE_ERROR, "Invalid MAX_FRAME_SIZE specified in sent settings: %d", max); } maxFrameSize = max; } @Override public int maxFrameSize() { return maxFrameSize; } @Override public void close() { closeHeadersContinuation(); } private void closeHeadersContinuation() { if (headersContinuation != null) { headersContinuation.close(); headersContinuation = null; } } @Override public void readFrame(ChannelHandlerContext ctx, ByteBuf input, Http2FrameListener listener) throws Http2Exception { if (readError) { input.skipBytes(input.readableBytes()); return; } try { do { if (readingHeaders) { processHeaderState(input); if (readingHeaders) { // Wait until the entire header has arrived. return; } } // The header is complete, fall into the next case to process the payload. // This is to ensure the proper handling of zero-length payloads. In this // case, we don't want to loop around because there may be no more data // available, causing us to exit the loop. Instead, we just want to perform // the first pass at payload processing now. processPayloadState(ctx, input, listener); if (!readingHeaders) { // Wait until the entire payload has arrived. return; } } while (input.isReadable()); } catch (Http2Exception e) { readError = !Http2Exception.isStreamError(e); throw e; } catch (RuntimeException e) { readError = true; throw e; } catch (Throwable cause) { readError = true; PlatformDependent.throwException(cause); } } private void processHeaderState(ByteBuf in) throws Http2Exception { if (in.readableBytes() < FRAME_HEADER_LENGTH) { // Wait until the entire frame header has been read. return; } // Read the header and prepare the unmarshaller to read the frame. payloadLength = in.readUnsignedMedium(); if (payloadLength > maxFrameSize) { throw connectionError(FRAME_SIZE_ERROR, "Frame length: %d exceeds maximum: %d", payloadLength, maxFrameSize); } frameType = in.readByte(); flags = new Http2Flags(in.readUnsignedByte()); streamId = readUnsignedInt(in); // We have consumed the data, next time we read we will be expecting to read the frame payload. readingHeaders = false; switch (frameType) { case DATA: verifyDataFrame(); break; case HEADERS: verifyHeadersFrame(); break; case PRIORITY: verifyPriorityFrame(); break; case RST_STREAM: verifyRstStreamFrame(); break; case SETTINGS: verifySettingsFrame(); break; case PUSH_PROMISE: verifyPushPromiseFrame(); break; case PING: verifyPingFrame(); break; case GO_AWAY: verifyGoAwayFrame(); break; case WINDOW_UPDATE: verifyWindowUpdateFrame(); break; case CONTINUATION: verifyContinuationFrame(); break; default: // Unknown frame type, could be an extension. verifyUnknownFrame(); break; } } private void processPayloadState(ChannelHandlerContext ctx, ByteBuf in, Http2FrameListener listener) throws Http2Exception { if (in.readableBytes() < payloadLength) { // Wait until the entire payload has been read. return; } // Get a view of the buffer for the size of the payload. ByteBuf payload = in.readSlice(payloadLength); // We have consumed the data, next time we read we will be expecting to read a frame header. readingHeaders = true; // Read the payload and fire the frame event to the listener. switch (frameType) { case DATA: readDataFrame(ctx, payload, listener); break; case HEADERS: readHeadersFrame(ctx, payload, listener); break; case PRIORITY: readPriorityFrame(ctx, payload, listener); break; case RST_STREAM: readRstStreamFrame(ctx, payload, listener); break; case SETTINGS: readSettingsFrame(ctx, payload, listener); break; case PUSH_PROMISE: readPushPromiseFrame(ctx, payload, listener); break; case PING: readPingFrame(ctx, payload, listener); break; case GO_AWAY: readGoAwayFrame(ctx, payload, listener); break; case WINDOW_UPDATE: readWindowUpdateFrame(ctx, payload, listener); break; case CONTINUATION: readContinuationFrame(payload, listener); break; default: readUnknownFrame(ctx, payload, listener); break; } } private void verifyDataFrame() throws Http2Exception { verifyAssociatedWithAStream(); verifyNotProcessingHeaders(); verifyPayloadLength(payloadLength); if (payloadLength < flags.getPaddingPresenceFieldLength()) { throw streamError(streamId, FRAME_SIZE_ERROR, "Frame length %d too small.", payloadLength); } } private void verifyHeadersFrame() throws Http2Exception { verifyAssociatedWithAStream(); verifyNotProcessingHeaders(); verifyPayloadLength(payloadLength); int requiredLength = flags.getPaddingPresenceFieldLength() + flags.getNumPriorityBytes(); if (payloadLength < requiredLength) { throw streamError(streamId, FRAME_SIZE_ERROR, "Frame length too small." + payloadLength); } } private void verifyPriorityFrame() throws Http2Exception { verifyAssociatedWithAStream(); verifyNotProcessingHeaders(); if (payloadLength != PRIORITY_ENTRY_LENGTH) { throw streamError(streamId, FRAME_SIZE_ERROR, "Invalid frame length %d.", payloadLength); } } private void verifyRstStreamFrame() throws Http2Exception { verifyAssociatedWithAStream(); verifyNotProcessingHeaders(); if (payloadLength != INT_FIELD_LENGTH) { throw connectionError(FRAME_SIZE_ERROR, "Invalid frame length %d.", payloadLength); } } private void verifySettingsFrame() throws Http2Exception { verifyNotProcessingHeaders(); verifyPayloadLength(payloadLength); if (streamId != 0) { throw connectionError(PROTOCOL_ERROR, "A stream ID must be zero."); } if (flags.ack() && payloadLength > 0) { throw connectionError(FRAME_SIZE_ERROR, "Ack settings frame must have an empty payload."); } if (payloadLength % SETTING_ENTRY_LENGTH > 0) { throw connectionError(FRAME_SIZE_ERROR, "Frame length %d invalid.", payloadLength); } } private void verifyPushPromiseFrame() throws Http2Exception { verifyNotProcessingHeaders(); verifyPayloadLength(payloadLength); // Subtract the length of the promised stream ID field, to determine the length of the // rest of the payload (header block fragment + payload). int minLength = flags.getPaddingPresenceFieldLength() + INT_FIELD_LENGTH; if (payloadLength < minLength) { throw streamError(streamId, FRAME_SIZE_ERROR, "Frame length %d too small.", payloadLength); } } private void verifyPingFrame() throws Http2Exception { verifyNotProcessingHeaders(); if (streamId != 0) { throw connectionError(PROTOCOL_ERROR, "A stream ID must be zero."); } if (payloadLength != PING_FRAME_PAYLOAD_LENGTH) { throw connectionError(FRAME_SIZE_ERROR, "Frame length %d incorrect size for ping.", payloadLength); } } private void verifyGoAwayFrame() throws Http2Exception { verifyNotProcessingHeaders(); verifyPayloadLength(payloadLength); if (streamId != 0) { throw connectionError(PROTOCOL_ERROR, "A stream ID must be zero."); } if (payloadLength < 8) { throw connectionError(FRAME_SIZE_ERROR, "Frame length %d too small.", payloadLength); } } private void verifyWindowUpdateFrame() throws Http2Exception { verifyNotProcessingHeaders(); verifyStreamOrConnectionId(streamId, "Stream ID"); if (payloadLength != INT_FIELD_LENGTH) { throw connectionError(FRAME_SIZE_ERROR, "Invalid frame length %d.", payloadLength); } } private void verifyContinuationFrame() throws Http2Exception { verifyAssociatedWithAStream(); verifyPayloadLength(payloadLength); if (headersContinuation == null) { throw connectionError(PROTOCOL_ERROR, "Received %s frame but not currently processing headers.", frameType); } if (streamId != headersContinuation.getStreamId()) { throw connectionError(PROTOCOL_ERROR, "Continuation stream ID does not match pending headers. " + "Expected %d, but received %d.", headersContinuation.getStreamId(), streamId); } if (payloadLength < flags.getPaddingPresenceFieldLength()) { throw streamError(streamId, FRAME_SIZE_ERROR, "Frame length %d too small for padding.", payloadLength); } } private void verifyUnknownFrame() throws Http2Exception { verifyNotProcessingHeaders(); } private void readDataFrame(ChannelHandlerContext ctx, ByteBuf payload, Http2FrameListener listener) throws Http2Exception { int padding = readPadding(payload); verifyPadding(padding); // Determine how much data there is to read by removing the trailing // padding. int dataLength = lengthWithoutTrailingPadding(payload.readableBytes(), padding); ByteBuf data = payload.readSlice(dataLength); listener.onDataRead(ctx, streamId, data, padding, flags.endOfStream()); payload.skipBytes(payload.readableBytes()); } private void readHeadersFrame(final ChannelHandlerContext ctx, ByteBuf payload, Http2FrameListener listener) throws Http2Exception { final int headersStreamId = streamId; final Http2Flags headersFlags = flags; final int padding = readPadding(payload); verifyPadding(padding); // The callback that is invoked is different depending on whether priority information // is present in the headers frame. if (flags.priorityPresent()) { long word1 = payload.readUnsignedInt(); final boolean exclusive = (word1 & 0x80000000L) != 0; final int streamDependency = (int) (word1 & 0x7FFFFFFFL); if (streamDependency == streamId) { throw streamError(streamId, PROTOCOL_ERROR, "A stream cannot depend on itself."); } final short weight = (short) (payload.readUnsignedByte() + 1); final ByteBuf fragment = payload.readSlice(lengthWithoutTrailingPadding(payload.readableBytes(), padding)); // Create a handler that invokes the listener when the header block is complete. headersContinuation = new HeadersContinuation() { @Override public int getStreamId() { return headersStreamId; } @Override public void processFragment(boolean endOfHeaders, ByteBuf fragment, Http2FrameListener listener) throws Http2Exception { final HeadersBlockBuilder hdrBlockBuilder = headersBlockBuilder(); hdrBlockBuilder.addFragment(fragment, ctx.alloc(), endOfHeaders); if (endOfHeaders) { listener.onHeadersRead(ctx, headersStreamId, hdrBlockBuilder.headers(), streamDependency, weight, exclusive, padding, headersFlags.endOfStream()); } } }; // Process the initial fragment, invoking the listener's callback if end of headers. headersContinuation.processFragment(flags.endOfHeaders(), fragment, listener); resetHeadersContinuationIfEnd(flags.endOfHeaders()); return; } // The priority fields are not present in the frame. Prepare a continuation that invokes // the listener callback without priority information. headersContinuation = new HeadersContinuation() { @Override public int getStreamId() { return headersStreamId; } @Override public void processFragment(boolean endOfHeaders, ByteBuf fragment, Http2FrameListener listener) throws Http2Exception { final HeadersBlockBuilder hdrBlockBuilder = headersBlockBuilder(); hdrBlockBuilder.addFragment(fragment, ctx.alloc(), endOfHeaders); if (endOfHeaders) { listener.onHeadersRead(ctx, headersStreamId, hdrBlockBuilder.headers(), padding, headersFlags.endOfStream()); } } }; // Process the initial fragment, invoking the listener's callback if end of headers. final ByteBuf fragment = payload.readSlice(lengthWithoutTrailingPadding(payload.readableBytes(), padding)); headersContinuation.processFragment(flags.endOfHeaders(), fragment, listener); resetHeadersContinuationIfEnd(flags.endOfHeaders()); } private void resetHeadersContinuationIfEnd(boolean endOfHeaders) { if (endOfHeaders) { closeHeadersContinuation(); } } private void readPriorityFrame(ChannelHandlerContext ctx, ByteBuf payload, Http2FrameListener listener) throws Http2Exception { long word1 = payload.readUnsignedInt(); boolean exclusive = (word1 & 0x80000000L) != 0; int streamDependency = (int) (word1 & 0x7FFFFFFFL); if (streamDependency == streamId) { throw streamError(streamId, PROTOCOL_ERROR, "A stream cannot depend on itself."); } short weight = (short) (payload.readUnsignedByte() + 1); listener.onPriorityRead(ctx, streamId, streamDependency, weight, exclusive); } private void readRstStreamFrame(ChannelHandlerContext ctx, ByteBuf payload, Http2FrameListener listener) throws Http2Exception { long errorCode = payload.readUnsignedInt(); listener.onRstStreamRead(ctx, streamId, errorCode); } private void readSettingsFrame(ChannelHandlerContext ctx, ByteBuf payload, Http2FrameListener listener) throws Http2Exception { if (flags.ack()) { listener.onSettingsAckRead(ctx); } else { int numSettings = payloadLength / SETTING_ENTRY_LENGTH; Http2Settings settings = new Http2Settings(); for (int index = 0; index < numSettings; ++index) { char id = (char) payload.readUnsignedShort(); long value = payload.readUnsignedInt(); try { settings.put(id, Long.valueOf(value)); } catch (IllegalArgumentException e) { switch(id) { case SETTINGS_MAX_FRAME_SIZE: throw connectionError(PROTOCOL_ERROR, e, e.getMessage()); case SETTINGS_INITIAL_WINDOW_SIZE: throw connectionError(FLOW_CONTROL_ERROR, e, e.getMessage()); default: throw connectionError(PROTOCOL_ERROR, e, e.getMessage()); } } } listener.onSettingsRead(ctx, settings); } } private void readPushPromiseFrame(final ChannelHandlerContext ctx, ByteBuf payload, Http2FrameListener listener) throws Http2Exception { final int pushPromiseStreamId = streamId; final int padding = readPadding(payload); verifyPadding(padding); final int promisedStreamId = readUnsignedInt(payload); // Create a handler that invokes the listener when the header block is complete. headersContinuation = new HeadersContinuation() { @Override public int getStreamId() { return pushPromiseStreamId; } @Override public void processFragment(boolean endOfHeaders, ByteBuf fragment, Http2FrameListener listener) throws Http2Exception { headersBlockBuilder().addFragment(fragment, ctx.alloc(), endOfHeaders); if (endOfHeaders) { listener.onPushPromiseRead(ctx, pushPromiseStreamId, promisedStreamId, headersBlockBuilder().headers(), padding); } } }; // Process the initial fragment, invoking the listener's callback if end of headers. final ByteBuf fragment = payload.readSlice(lengthWithoutTrailingPadding(payload.readableBytes(), padding)); headersContinuation.processFragment(flags.endOfHeaders(), fragment, listener); resetHeadersContinuationIfEnd(flags.endOfHeaders()); } private void readPingFrame(ChannelHandlerContext ctx, ByteBuf payload, Http2FrameListener listener) throws Http2Exception { ByteBuf data = payload.readSlice(payload.readableBytes()); if (flags.ack()) { listener.onPingAckRead(ctx, data); } else { listener.onPingRead(ctx, data); } } private static void readGoAwayFrame(ChannelHandlerContext ctx, ByteBuf payload, Http2FrameListener listener) throws Http2Exception { int lastStreamId = readUnsignedInt(payload); long errorCode = payload.readUnsignedInt(); ByteBuf debugData = payload.readSlice(payload.readableBytes()); listener.onGoAwayRead(ctx, lastStreamId, errorCode, debugData); } private void readWindowUpdateFrame(ChannelHandlerContext ctx, ByteBuf payload, Http2FrameListener listener) throws Http2Exception { int windowSizeIncrement = readUnsignedInt(payload); if (windowSizeIncrement == 0) { throw streamError(streamId, PROTOCOL_ERROR, "Received WINDOW_UPDATE with delta 0 for stream: %d", streamId); } listener.onWindowUpdateRead(ctx, streamId, windowSizeIncrement); } private void readContinuationFrame(ByteBuf payload, Http2FrameListener listener) throws Http2Exception { // Process the initial fragment, invoking the listener's callback if end of headers. final ByteBuf continuationFragment = payload.readSlice(payload.readableBytes()); headersContinuation.processFragment(flags.endOfHeaders(), continuationFragment, listener); resetHeadersContinuationIfEnd(flags.endOfHeaders()); } private void readUnknownFrame(ChannelHandlerContext ctx, ByteBuf payload, Http2FrameListener listener) throws Http2Exception { payload = payload.readSlice(payload.readableBytes()); listener.onUnknownFrame(ctx, frameType, streamId, flags, payload); } /** * If padding is present in the payload, reads the next byte as padding. The padding also includes the one byte * width of the pad length field. Otherwise, returns zero. */ private int readPadding(ByteBuf payload) { if (!flags.paddingPresent()) { return 0; } return payload.readUnsignedByte() + 1; } private void verifyPadding(int padding) throws Http2Exception { int len = lengthWithoutTrailingPadding(payloadLength, padding); if (len < 0) { throw connectionError(PROTOCOL_ERROR, "Frame payload too small for padding."); } } /** * The padding parameter consists of the 1 byte pad length field and the trailing padding bytes. This method * returns the number of readable bytes without the trailing padding. */ private static int lengthWithoutTrailingPadding(int readableBytes, int padding) { return padding == 0 ? readableBytes : readableBytes - (padding - 1); } /** * Base class for processing of HEADERS and PUSH_PROMISE header blocks that potentially span * multiple frames. The implementation of this interface will perform the final callback to the * {@link Http2FrameListener} once the end of headers is reached. */ private abstract class HeadersContinuation { private final HeadersBlockBuilder builder = new HeadersBlockBuilder(); /** * Returns the stream for which headers are currently being processed. */ abstract int getStreamId(); /** * Processes the next fragment for the current header block. * * @param endOfHeaders whether the fragment is the last in the header block. * @param fragment the fragment of the header block to be added. * @param listener the listener to be notified if the header block is completed. */ abstract void processFragment(boolean endOfHeaders, ByteBuf fragment, Http2FrameListener listener) throws Http2Exception; final HeadersBlockBuilder headersBlockBuilder() { return builder; } /** * Free any allocated resources. */ final void close() { builder.close(); } } /** * Utility class to help with construction of the headers block that may potentially span * multiple frames. */ protected class HeadersBlockBuilder { private ByteBuf headerBlock; /** * The local header size maximum has been exceeded while accumulating bytes. * @throws Http2Exception A connection error indicating too much data has been received. */ private void headerSizeExceeded() throws Http2Exception { close(); headerListSizeExceeded(headersDecoder.configuration().maxHeaderListSizeGoAway()); } /** * Adds a fragment to the block. * * @param fragment the fragment of the headers block to be added. * @param alloc allocator for new blocks if needed. * @param endOfHeaders flag indicating whether the current frame is the end of the headers. * This is used for an optimization for when the first fragment is the full * block. In that case, the buffer is used directly without copying. */ final void addFragment(ByteBuf fragment, ByteBufAllocator alloc, boolean endOfHeaders) throws Http2Exception { if (headerBlock == null) { if (fragment.readableBytes() > headersDecoder.configuration().maxHeaderListSizeGoAway()) { headerSizeExceeded(); } if (endOfHeaders) { // Optimization - don't bother copying, just use the buffer as-is. Need // to retain since we release when the header block is built. headerBlock = fragment.retain(); } else { headerBlock = alloc.buffer(fragment.readableBytes()); headerBlock.writeBytes(fragment); } return; } if (headersDecoder.configuration().maxHeaderListSizeGoAway() - fragment.readableBytes() < headerBlock.readableBytes()) { headerSizeExceeded(); } if (headerBlock.isWritable(fragment.readableBytes())) { // The buffer can hold the requested bytes, just write it directly. headerBlock.writeBytes(fragment); } else { // Allocate a new buffer that is big enough to hold the entire header block so far. ByteBuf buf = alloc.buffer(headerBlock.readableBytes() + fragment.readableBytes()); buf.writeBytes(headerBlock); buf.writeBytes(fragment); headerBlock.release(); headerBlock = buf; } } /** * Builds the headers from the completed headers block. After this is called, this builder * should not be called again. */ Http2Headers headers() throws Http2Exception { try { return headersDecoder.decodeHeaders(streamId, headerBlock); } finally { close(); } } /** * Closes this builder and frees any resources. */ void close() { if (headerBlock != null) { headerBlock.release(); headerBlock = null; } // Clear the member variable pointing at this instance. headersContinuation = null; } } /** * Verify that current state is not processing on header block * @throws Http2Exception thrown if {@link #headersContinuation} is not null */ private void verifyNotProcessingHeaders() throws Http2Exception { if (headersContinuation != null) { throw connectionError(PROTOCOL_ERROR, "Received frame of type %s while processing headers on stream %d.", frameType, headersContinuation.getStreamId()); } } private void verifyPayloadLength(int payloadLength) throws Http2Exception { if (payloadLength > maxFrameSize) { throw connectionError(PROTOCOL_ERROR, "Total payload length %d exceeds max frame length.", payloadLength); } } private void verifyAssociatedWithAStream() throws Http2Exception { if (streamId == 0) { throw connectionError(PROTOCOL_ERROR, "Frame of type %s must be associated with a stream.", frameType); } } private static void verifyStreamOrConnectionId(int streamId, String argumentName) throws Http2Exception { if (streamId < 0) { throw connectionError(PROTOCOL_ERROR, "%s must be >= 0", argumentName); } } }