void FilterManager::encodeHeaders(ActiveStreamEncoderFilter* filter, ResponseHeaderMap& headers,
                                  bool end_stream) {
  // See encodeHeaders() comments in include/envoy/http/filter.h for why the 1xx precondition holds.
  ASSERT(!CodeUtility::is1xx(Utility::getResponseStatus(headers)) ||
         Utility::getResponseStatus(headers) == enumToInt(Http::Code::SwitchingProtocols));
  filter_manager_callbacks_.resetIdleTimer();
  disarmRequestTimeout();

  // Headers filter iteration should always start with the next filter if available.
  std::list<ActiveStreamEncoderFilterPtr>::iterator entry =
      commonEncodePrefix(filter, end_stream, FilterIterationStartState::AlwaysStartFromNext);
  std::list<ActiveStreamEncoderFilterPtr>::iterator continue_data_entry = encoder_filters_.end();

  for (; entry != encoder_filters_.end(); entry++) {
    (*entry)->maybeEvaluateMatchTreeWithNewData(
        [&headers](auto& matching_data) { matching_data.onResponseHeaders(headers); });

    if ((*entry)->skipFilter()) {
      continue;
    }
    ASSERT(!(state_.filter_call_state_ & FilterCallState::EncodeHeaders));
    state_.filter_call_state_ |= FilterCallState::EncodeHeaders;
    (*entry)->end_stream_ = (end_stream && continue_data_entry == encoder_filters_.end());
    FilterHeadersStatus status = (*entry)->handle_->encodeHeaders(headers, (*entry)->end_stream_);
    if (state_.encoder_filter_chain_aborted_) {
      ENVOY_STREAM_LOG(trace,
                       "encodeHeaders filter iteration aborted due to local reply: filter={}",
                       *this, static_cast<const void*>((*entry).get()));
      status = FilterHeadersStatus::StopIteration;
    }

    ASSERT(!(status == FilterHeadersStatus::ContinueAndDontEndStream && !(*entry)->end_stream_),
           "Filters should not return FilterHeadersStatus::ContinueAndDontEndStream from "
           "encodeHeaders when end_stream is already false");

    state_.filter_call_state_ &= ~FilterCallState::EncodeHeaders;
    ENVOY_STREAM_LOG(trace, "encode headers called: filter={} status={}", *this,
                     static_cast<const void*>((*entry).get()), static_cast<uint64_t>(status));

    (*entry)->encode_headers_called_ = true;

    const auto continue_iteration = (*entry)->commonHandleAfterHeadersCallback(status, end_stream);

    // If this filter ended the stream, encodeComplete() should be called for it.
    if ((*entry)->end_stream_) {
      (*entry)->handle_->encodeComplete();
    }

    if (!continue_iteration) {
      if (!(*entry)->end_stream_) {
        maybeContinueEncoding(continue_data_entry);
      }
      return;
    }

    // Here we handle the case where we have a header only response, but a filter adds a body
    // to it. We need to not raise end_stream = true to further filters during inline iteration.
    if (end_stream && buffered_response_data_ && continue_data_entry == encoder_filters_.end()) {
      continue_data_entry = entry;
    }
  }

  // Check if the filter chain above did not remove critical headers or set malformed header values.
  // We could do this at the codec in order to prevent other places than the filter chain from
  // removing critical headers, but it will come with the implementation complexity.
  // See the previous attempt (#15658) for detail, and for now we choose to protect only against
  // filter chains.
  const auto status = HeaderUtility::checkRequiredResponseHeaders(headers);
  if (!status.ok()) {
    // If the check failed, then we reply with BadGateway, and stop the further processing.
    sendLocalReply(
        Http::Code::BadGateway, status.message(), nullptr, absl::nullopt,
        absl::StrCat(StreamInfo::ResponseCodeDetails::get().FilterRemovedRequiredResponseHeaders,
                     "{", StringUtil::replaceAllEmptySpace(status.message()), "}"));
    return;
  }

  const bool modified_end_stream = (end_stream && continue_data_entry == encoder_filters_.end());
  state_.non_100_response_headers_encoded_ = true;
  filter_manager_callbacks_.encodeHeaders(headers, modified_end_stream);
  if (state_.saw_downstream_reset_) {
    return;
  }
  maybeEndEncode(modified_end_stream);

  if (!modified_end_stream) {
    maybeContinueEncoding(continue_data_entry);
  }
}