docs: public synchronized void processNextFrame(Frame frame, Constants.Direction direction) throws ProtocolException, StreamClosedException, FlowControlException {
  
          // Make it easy to follow frame processing in the trace by searching for "processNextFrame-" to see all frame processing
          boolean doDebugWhile = false;
          FlowControlException FCEToThrow = null;
          if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
              Tr.debug(tc, "processNextFrame-entry:  stream: " + myID + " frame type: " + frame.getFrameType().toString() + " direction: " + direction.toString()
                           + " H2InboundLink hc: " + muxLink.hashCode());
          }
          // if we've already sent a reset frame on this stream , process any window size changes then ignore the new frame
          if (rstStreamSent) {
              if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                  Tr.debug(tc, "{0} frame received on stream {1} after RST_STREAM sent", frame.getFrameType(), myID);
              }
              try {
                  updateStreamReadWindow();
              } catch (Http2Exception e) {
                  if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                      Tr.debug(tc, "hit an exception updating the read window on stream {0}", myID);
                  }
              }
              return;
          }
          if (isStreamClosed()) {
              // stream is already closed
              if (direction.equals(Constants.Direction.WRITING_OUT) && !muxLink.isWriteContinuationExpected()) {
                  if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                      Tr.debug(tc, "processNextFrame: stream is closed - cannot write out anything else on stream-id: " + myID);
                  }
                  if (frame.getFrameType().equals(FrameTypes.DATA)) {
                      // pass an exception to whatever servlet is writing
                      throw new StreamClosedException("stream was already closed!");
                  } else {
                      return;
                  }
              } else if (direction.equals(Constants.Direction.READ_IN) &&
                         !frame.getFrameType().equals(FrameTypes.PUSH_PROMISE)) {
                  // handle a frame recieved after stream closure
                  if (frame.getFrameType() == FrameTypes.PRIORITY || frame.getFrameType() == FrameTypes.RST_STREAM
                      || frame.getFrameType() == FrameTypes.WINDOW_UPDATE) {
                      // Ignore PRIORITY RST_STREAM and WINDOW_UPDATE in all closed situations
                      return;
                  }
                  if (muxLink.streamTable.containsKey(this.myID)) {
                      throw new StreamClosedException(frame.getFrameType() + " frame received on a closed stream");
                  } else {
                      throw new ProtocolException(frame.getFrameType() + " frame received on a closed stream");
  
                  }
  
              }
          }
  
          ADDITIONAL_FRAME addFrame = ADDITIONAL_FRAME.FIRST_TIME;
          Http2Exception addFrameException = null;
  
          while (addFrame != ADDITIONAL_FRAME.NO) {
  
              currentFrame = frame;
  
              // skip only first debug here, since it was done on entry
              if (doDebugWhile) {
                  if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                      Tr.debug(tc, "processNextFrame- in while: stream-id: " + myID + " frame type: " + frame.getFrameType().toString() + " direction: " + direction.toString());
                  }
              } else {
                  doDebugWhile = true;
              }
  
              // if looping to RESET, then load it up now
              if (addFrame == ADDITIONAL_FRAME.RESET) {
                  updateStreamState(StreamState.HALF_CLOSED_LOCAL);
                  currentFrame = new FrameRstStream(myID, addFrameException.getErrorCode(), false);
                  if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                      Tr.debug(tc, "processNextFrame: exception encountered.  Sending RST_STREAM on stream "
                                   + myID + " with the error code " + addFrameException.getErrorString());
                  }
                  direction = Constants.Direction.WRITING_OUT;
              }
  
              // if looping to GOAWAY, then load it up now
              if (addFrame == ADDITIONAL_FRAME.GOAWAY) {
                  updateStreamState(StreamState.HALF_CLOSED_LOCAL);
                  if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                      Tr.debug(tc, "processNextFrame: addFrame GOAWAY: HighestClientStreamId: " + muxLink.getHighestClientStreamId());
                  }
                  currentFrame = new FrameGoAway(0, addFrameException.getMessage().getBytes(), addFrameException.getErrorCode(), muxLink.getHighestClientStreamId(), false);
                  if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                      Tr.debug(tc, "processNextFrame: exception encountered.  Sending a GOAWAY frame with the error code " + addFrameException.getErrorString());
                  }
  
                  direction = Constants.Direction.WRITING_OUT;
              }
  
              frameType = currentFrame.getFrameType();
  
              // play with the flags so we don't try to reset or goaway twice
              if (frameType == FrameTypes.RST_STREAM && addFrame == ADDITIONAL_FRAME.FIRST_TIME) {
                  addFrame = ADDITIONAL_FRAME.RESET;
              } else if (frameType == FrameTypes.GOAWAY && addFrame == ADDITIONAL_FRAME.FIRST_TIME) {
                  addFrame = ADDITIONAL_FRAME.GOAWAY;
              }
  
              // validate the current frame.  If it's not valid, send a RESET if applicable otherwise close the connection
              try {
                  //If the frame is being read, validate against the server's connection settings
                  if (direction == Constants.Direction.READ_IN) {
                      currentFrame.validate(muxLink.getLocalConnectionSettings());
                  } else {
                      //If the frame is being written, validate against the client's connection settings
                      currentFrame.validate(muxLink.getRemoteConnectionSettings());
                  }
  
              } catch (Http2Exception e) {
                  if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                      Tr.debug(tc, "processNextFrame: " + currentFrame.getFrameType() + " received on stream " + this.myID +
                                   " is not a valid frame: " + e.getErrorString());
                  }
                  if (addFrame == ADDITIONAL_FRAME.FIRST_TIME || addFrame == ADDITIONAL_FRAME.RESET ||
                      addFrame == ADDITIONAL_FRAME.GOAWAY) {
                      if (e.isConnectionError()) {
                          addFrame = ADDITIONAL_FRAME.GOAWAY;
                      } else {
                          addFrame = ADDITIONAL_FRAME.RESET;
                      }
                      addFrameException = e;
                  } else {
                      // writing or reading of a Reset or Goaway has failed.
                      // if it is reading of Reset that failed, then send a Reset.
                      if (direction == Constants.Direction.READ_IN) {
                          addFrame = ADDITIONAL_FRAME.RESET;
                          addFrameException = e;
                      } else {
                          // TODO: otherwise just close the stream/connection at the TCP Channel layer and clean up resources
                      }
                      addFrame = ADDITIONAL_FRAME.NO;
                  }
                  continue;
              }
  
              if (direction == Constants.Direction.READ_IN) {
  
                  if (muxLink.checkIfGoAwaySendingOrClosing()) {
                      if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                          Tr.debug(tc, "processNextFrame: " + currentFrame.getFrameType() + " received on stream " + this.myID +
                                       " after a GOAWAY was sent or Closing invoked.  This frame will be ignored.");
                      }
                      return;
                  }
  
                  // This frame type is artificially generated, process it as a headers frame,
                  // as if it had come in off the wire
                  if (frameType == FrameTypes.PUSHPROMISEHEADERS) {
                      getHeadersFromFrame();
                      setHeadersComplete();
                      try {
                          processCompleteHeaders(true);
                          setReadyForRead();
                      } catch (Http2Exception he) {
                          if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                              Tr.debug(tc, "H2StreamProcessor.sendRequestToWc(): ProcessCompleteHeaders Exception: " + he);
                          }
  
                          //Send a reset so that the client knows the push_promise is dead
                          addFrame = ADDITIONAL_FRAME.RESET;
                          addFrameException = he;
                          continue;
  
                      }
  
                      return;
  
                  }
  
                  // Header frames must be received in a contiguous chunk; cannot interleave across streams
                  if (muxLink.isContinuationExpected() && (frameType != FrameTypes.CONTINUATION || !muxLink.isContinuationExpected())) {
                      addFrame = ADDITIONAL_FRAME.GOAWAY;
                      addFrameException = new ProtocolException("Did not receive the expected continuation frame");
                      continue;
                  }
  
                  if (frameType == FrameTypes.SETTINGS ||
                      frameType == FrameTypes.GOAWAY || frameType == FrameTypes.PING) {
  
                      switch (frameType) {
                          case SETTINGS:
                              try {
                                  processSETTINGSFrame();
                              } catch (Http2Exception e) {
                                  if (addFrame == ADDITIONAL_FRAME.FIRST_TIME) {
                                      addFrame = ADDITIONAL_FRAME.GOAWAY;
                                      addFrameException = e;
                                      addFrame = ADDITIONAL_FRAME.NO;
                                  }
                                  continue;
                              }
                              break;
  
                          case GOAWAY:
                              processGOAWAYFrame();
                              updateStreamState(StreamState.CLOSED);
                              break;
  
                          case PING:
                              processPINGFrame();
                              break;
  
                          default:
                              break;
                      }
                      return;
                  }
  
                  // Stream oriented frame to process
                  try {
                      verifyReadFrameSequence();
                  } catch (Http2Exception e) {
                      if (e.isConnectionError()) {
                          addFrame = ADDITIONAL_FRAME.GOAWAY;
                      } else if (addFrame == ADDITIONAL_FRAME.FIRST_TIME) {
                          addFrame = ADDITIONAL_FRAME.RESET;
                      } else {
                          addFrame = ADDITIONAL_FRAME.NO;
                      }
                      addFrameException = e;
                      continue;
                  }
  
                  // made it here, so frame seems to be legit given the frame state.
  
                  // since we have already verify priority frame or window update frame  is allowed,
                  // and since it will not change the state of the connection, process
                  // the priority and window_update frame outside of state specific processing
                  if (frameType == FrameTypes.PRIORITY) {
                      processPriorityFrame();
                      return;
                  }
                  if (frameType == FrameTypes.RST_STREAM) {
                      processRstFrame();
                      synchronized (this) {
                          this.notifyAll();
                      }
                      return;
                  }
  
                  try {
                      if (frameType == FrameTypes.WINDOW_UPDATE) {
                          processWindowUpdateFrame();
                          synchronized (this) {
                              this.notifyAll();
                          }
                      }
                  } catch (Http2Exception e) {
                      if (addFrame == ADDITIONAL_FRAME.FIRST_TIME) {
                          if (e.isConnectionError()) {
                              addFrame = ADDITIONAL_FRAME.GOAWAY;
                          } else {
                              addFrame = ADDITIONAL_FRAME.RESET;
                          }
                          addFrameException = e;
                      } else {
                          addFrame = ADDITIONAL_FRAME.NO;
                      }
                      continue;
                  }
  
                  // check to see if client is sending us data with a payload, so we can update the local frame control window
                  // after processing it
                  try {
                      updateStreamReadWindow();
                  } catch (Http2Exception e) {
                      if (addFrame == ADDITIONAL_FRAME.FIRST_TIME) {
                          if (e.isConnectionError()) {
                              addFrame = ADDITIONAL_FRAME.GOAWAY;
                          } else {
                              addFrame = ADDITIONAL_FRAME.RESET;
                          }
                          addFrameException = e;
                      } else {
                          addFrame = ADDITIONAL_FRAME.NO;
                      }
                      continue;
                  }
  
                  try {
                      readWriteTransitionState(direction);
                  } catch (CompressionException e) {
                      // if this is a compression exception, something has gone very wrong and the connection is hosed
                      if (addFrame == ADDITIONAL_FRAME.FIRST_TIME) {
                          addFrame = ADDITIONAL_FRAME.GOAWAY;
                          addFrameException = e;
                      } else {
                          addFrame = ADDITIONAL_FRAME.NO;
                      }
                      continue;
                  } catch (Http2Exception e) {
                      if (addFrame == ADDITIONAL_FRAME.FIRST_TIME) {
                          if (e.isConnectionError()) {
                              addFrame = ADDITIONAL_FRAME.GOAWAY;
                          } else {
                              addFrame = ADDITIONAL_FRAME.RESET;
                          }
                          addFrameException = e;
                      } else {
                          addFrame = ADDITIONAL_FRAME.NO;
                      }
                      continue;
                  }
              } else {
                  if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                      Tr.debug(tc, "processNextFrame Writing out data");
                  }
                  try {
                      verifyWriteFrameSequence();
                      readWriteTransitionState(direction);
                  } catch (CompressionException e) {
                      // if this is a compression exception, something has gone very wrong and the connection is hosed
                      if ((addFrame == ADDITIONAL_FRAME.FIRST_TIME) || (addFrame == ADDITIONAL_FRAME.RESET)) {
                          Tr.info(tc, "A Header compression error occurred!  This connection is no longer valid.");
                          addFrame = ADDITIONAL_FRAME.GOAWAY;
                          addFrameException = e;
                      } else {
                          addFrame = ADDITIONAL_FRAME.NO;
                      }
                      continue;
                  } catch (Http2Exception e) {
                      if (addFrame == ADDITIONAL_FRAME.FIRST_TIME) {
                          if ((frameType == FrameTypes.DATA) && (e instanceof FlowControlException)) {
                              FCEToThrow = (FlowControlException) e;
                          }
                          if (e.isConnectionError()) {
                              addFrame = ADDITIONAL_FRAME.GOAWAY;
                              addFrameException = e;
                          } else {
                              addFrame = ADDITIONAL_FRAME.RESET;
                          }
                          addFrameException = e;
                      } else {
                          addFrame = ADDITIONAL_FRAME.NO;
                      }
                      continue;
                  }
              }
              addFrame = ADDITIONAL_FRAME.NO;
          }
          // will only throw FCE if we were writing DATA frame and got an FCE
          if (FCEToThrow != null) {
              throw FCEToThrow;
          }
      }