Repository: envoyproxy/envoy
Pull request #38750: [ext_proc] Invoke response processors before continuing the filter chain.
Author: login: pradeepcrao
Created: 2025-03-14T16:04:05Z | Merged: 2025-03-19T17:59:19Z

Commit Message:
Additional Description:
Risk Level: Low
Testing: Added tests.
Docs Changes: N/A
Release Notes: Added release note.
Platform Specific Features: N/A
[Optional Runtime guard:] N/A (new extension)


Diff:
diff --git a/api/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto b/api/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto
index 7e4fbffb658bd..f7c1d8405e4d5 100644
--- a/api/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto
+++ b/api/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto
@@ -331,9 +331,14 @@ message ExternalProcessor {
   repeated ProcessingMode allowed_override_modes = 22;
 
   // Decorator to introduce custom logic that runs after a message received from
-  // the External Processor is processed.
+  // the External Processor is processed, but before continuing filter chain iteration.
+  //
+  // .. note::
+  //   Response processors are currently in alpha.
+  //
   // [#extension-category: envoy.http.ext_proc.response_processors]
-  config.core.v3.TypedExtensionConfig on_processing_response = 23;
+  config.core.v3.TypedExtensionConfig on_processing_response = 23
+      [(xds.annotations.v3.field_status).work_in_progress = true];
 }
 
 // ExtProcHttpService is used for HTTP communication between the filter and the external processing service.
diff --git a/api/envoy/extensions/http/ext_proc/response_processors/save_processing_response/v3/save_processing_response.proto b/api/envoy/extensions/http/ext_proc/response_processors/save_processing_response/v3/save_processing_response.proto
index bfea5fd1878e2..87f7b104d9e56 100644
--- a/api/envoy/extensions/http/ext_proc/response_processors/save_processing_response/v3/save_processing_response.proto
+++ b/api/envoy/extensions/http/ext_proc/response_processors/save_processing_response/v3/save_processing_response.proto
@@ -23,6 +23,10 @@ option (xds.annotations.v3.file_status).work_in_progress = true;
 // <envoy_v3_api_field_extensions.http.ext_proc.response_processors.save_processing_response.v3.SaveProcessingResponse.filter_state_name>`].
 // This extension supports saving of request and response headers and trailers,
 // and immediate response.
+//
+// .. note::
+//   Response processors are currently in alpha.
+//
 // [#next-free-field: 7]
 message SaveProcessingResponse {
   message SaveOptions {
diff --git a/changelogs/current.yaml b/changelogs/current.yaml
index 7958366859bf4..1ef1317bc1c7a 100644
--- a/changelogs/current.yaml
+++ b/changelogs/current.yaml
@@ -99,6 +99,12 @@ bug_fixes:
 - area: listener
   change: |
     Fixed a bug where the addresses cannot be updated partially even if the reuse port is enabled.
+- area: ext_proc
+  change: |
+    Fixed a bug for the :ref:`extension
+    <envoy_v3_api_msg_extensions.http.ext_proc.response_processors.save_processing_response.v3.SaveProcessingResponse>` where the
+    :ref:`response <envoy_v3_api_msg_service.ext_proc.v3.ProcessingResponse>` from the external processor was being saved to
+    filter state after iterating through the filter chain.
 
 removed_config_or_runtime:
 # *Normally occurs at the end of the* :ref:`deprecation period <deprecated>`
diff --git a/source/extensions/filters/http/ext_proc/ext_proc.cc b/source/extensions/filters/http/ext_proc/ext_proc.cc
index c315f3cd7650a..25ca7c5738b13 100644
--- a/source/extensions/filters/http/ext_proc/ext_proc.cc
+++ b/source/extensions/filters/http/ext_proc/ext_proc.cc
@@ -26,6 +26,7 @@ namespace ExternalProcessing {
 namespace {
 
 using envoy::config::common::mutation_rules::v3::HeaderMutationRules;
+using envoy::config::core::v3::TrafficDirection;
 using envoy::extensions::filters::http::ext_proc::v3::ExternalProcessor;
 using envoy::extensions::filters::http::ext_proc::v3::ExtProcPerRoute;
 using envoy::extensions::filters::http::ext_proc::v3::ProcessingMode;
@@ -1321,50 +1322,26 @@ void Filter::onReceiveMessage(std::unique_ptr<ProcessingResponse>&& r) {
   case ProcessingResponse::ResponseCase::kRequestHeaders:
     setDecoderDynamicMetadata(*response);
     processing_status = decoding_state_.handleHeadersResponse(response->request_headers());
-    if (on_processing_response_) {
-      on_processing_response_->afterProcessingRequestHeaders(*response, processing_status,
-                                                             decoder_callbacks_->streamInfo());
-    }
     break;
   case ProcessingResponse::ResponseCase::kResponseHeaders:
     setEncoderDynamicMetadata(*response);
     processing_status = encoding_state_.handleHeadersResponse(response->response_headers());
-    if (on_processing_response_) {
-      on_processing_response_->afterProcessingResponseHeaders(*response, processing_status,
-                                                              decoder_callbacks_->streamInfo());
-    }
     break;
   case ProcessingResponse::ResponseCase::kRequestBody:
     setDecoderDynamicMetadata(*response);
     processing_status = decoding_state_.handleBodyResponse(response->request_body());
-    if (on_processing_response_) {
-      on_processing_response_->afterProcessingRequestBody(*response, processing_status,
-                                                          decoder_callbacks_->streamInfo());
-    }
     break;
   case ProcessingResponse::ResponseCase::kResponseBody:
     setEncoderDynamicMetadata(*response);
     processing_status = encoding_state_.handleBodyResponse(response->response_body());
-    if (on_processing_response_) {
-      on_processing_response_->afterProcessingResponseBody(*response, processing_status,
-                                                           decoder_callbacks_->streamInfo());
-    }
     break;
   case ProcessingResponse::ResponseCase::kRequestTrailers:
     setDecoderDynamicMetadata(*response);
     processing_status = decoding_state_.handleTrailersResponse(response->request_trailers());
-    if (on_processing_response_) {
-      on_processing_response_->afterProcessingRequestTrailers(*response, processing_status,
-                                                              decoder_callbacks_->streamInfo());
-    }
     break;
   case ProcessingResponse::ResponseCase::kResponseTrailers:
     setEncoderDynamicMetadata(*response);
     processing_status = encoding_state_.handleTrailersResponse(response->response_trailers());
-    if (on_processing_response_) {
-      on_processing_response_->afterProcessingResponseTrailers(*response, processing_status,
-                                                               decoder_callbacks_->streamInfo());
-    }
     break;
   case ProcessingResponse::ResponseCase::kImmediateResponse:
     if (config_->disableImmediateResponse()) {
@@ -1373,11 +1350,6 @@ void Filter::onReceiveMessage(std::unique_ptr<ProcessingResponse>&& r) {
                        "Treat the immediate response message as spurious response.");
       processing_status =
           absl::FailedPreconditionError("unhandled immediate response due to config disabled it");
-
-      if (on_processing_response_) {
-        on_processing_response_->afterReceivingImmediateResponse(*response, processing_status,
-                                                                 decoder_callbacks_->streamInfo());
-      }
     } else {
       setDecoderDynamicMetadata(*response);
       // We won't be sending anything more to the stream after we
@@ -1387,8 +1359,8 @@ void Filter::onReceiveMessage(std::unique_ptr<ProcessingResponse>&& r) {
       onFinishProcessorCalls(Grpc::Status::Ok);
       closeStream();
       if (on_processing_response_) {
-        on_processing_response_->afterReceivingImmediateResponse(*response, processing_status,
-                                                                 decoder_callbacks_->streamInfo());
+        on_processing_response_->afterReceivingImmediateResponse(
+            response->immediate_response(), absl::OkStatus(), decoder_callbacks_->streamInfo());
       }
       sendImmediateResponse(response->immediate_response());
       processing_status = absl::OkStatus();
@@ -1710,6 +1682,49 @@ std::unique_ptr<OnProcessingResponse> FilterConfig::createOnProcessingResponse()
   return on_processing_response_factory_cb_();
 }
 
+void Filter::onProcessHeadersResponse(const envoy::service::ext_proc::v3::HeadersResponse& response,
+                                      absl::Status status, TrafficDirection traffic_direction) {
+  if (on_processing_response_) {
+    ASSERT(traffic_direction != TrafficDirection::UNSPECIFIED);
+    if (traffic_direction == TrafficDirection::INBOUND) {
+      on_processing_response_->afterProcessingRequestHeaders(response, status,
+                                                             decoder_callbacks_->streamInfo());
+    } else {
+      on_processing_response_->afterProcessingResponseHeaders(response, status,
+                                                              encoder_callbacks_->streamInfo());
+    }
+  }
+}
+
+void Filter::onProcessTrailersResponse(
+    const envoy::service::ext_proc::v3::TrailersResponse& response, absl::Status status,
+    TrafficDirection traffic_direction) {
+  if (on_processing_response_) {
+    ASSERT(traffic_direction != TrafficDirection::UNSPECIFIED);
+    if (traffic_direction == TrafficDirection::INBOUND) {
+      on_processing_response_->afterProcessingRequestTrailers(response, status,
+                                                              decoder_callbacks_->streamInfo());
+    } else {
+      on_processing_response_->afterProcessingResponseTrailers(response, status,
+                                                               encoder_callbacks_->streamInfo());
+    }
+  }
+}
+
+void Filter::onProcessBodyResponse(const envoy::service::ext_proc::v3::BodyResponse& response,
+                                   absl::Status status, TrafficDirection traffic_direction) {
+  if (on_processing_response_) {
+    ASSERT(traffic_direction != TrafficDirection::UNSPECIFIED);
+    if (traffic_direction == TrafficDirection::INBOUND) {
+      on_processing_response_->afterProcessingRequestBody(response, status,
+                                                          decoder_callbacks_->streamInfo());
+    } else {
+      on_processing_response_->afterProcessingResponseBody(response, status,
+                                                           encoder_callbacks_->streamInfo());
+    }
+  }
+}
+
 } // namespace ExternalProcessing
 } // namespace HttpFilters
 } // namespace Extensions
diff --git a/source/extensions/filters/http/ext_proc/ext_proc.h b/source/extensions/filters/http/ext_proc/ext_proc.h
index 1ce7d445ff8d9..c8b0555900521 100644
--- a/source/extensions/filters/http/ext_proc/ext_proc.h
+++ b/source/extensions/filters/http/ext_proc/ext_proc.h
@@ -466,6 +466,16 @@ class Filter : public Logger::Loggable<Logger::Id::ext_proc>,
   void onComplete(envoy::service::ext_proc::v3::ProcessingResponse& response) override;
   void onError() override;
 
+  void onProcessHeadersResponse(const envoy::service::ext_proc::v3::HeadersResponse& response,
+                                absl::Status status,
+                                envoy::config::core::v3::TrafficDirection traffic_direction);
+  void onProcessTrailersResponse(const envoy::service::ext_proc::v3::TrailersResponse& response,
+                                 absl::Status status,
+                                 envoy::config::core::v3::TrafficDirection traffic_direction);
+  void onProcessBodyResponse(const envoy::service::ext_proc::v3::BodyResponse& response,
+                             absl::Status status,
+                             envoy::config::core::v3::TrafficDirection traffic_direction);
+
 private:
   void mergePerRouteConfig();
   StreamOpenState openStream();
diff --git a/source/extensions/filters/http/ext_proc/on_processing_response.h b/source/extensions/filters/http/ext_proc/on_processing_response.h
index 7423ccce47673..8846fa0c94a22 100644
--- a/source/extensions/filters/http/ext_proc/on_processing_response.h
+++ b/source/extensions/filters/http/ext_proc/on_processing_response.h
@@ -24,47 +24,47 @@ class OnProcessingResponse {
   // Called after processing the response from the external processor with :ref:`request_headers
   // <envoy_v3_api_field_service.ext_proc.v3.ProcessingResponse.request_headers>` set.
   virtual void
-  afterProcessingRequestHeaders(const envoy::service::ext_proc::v3::ProcessingResponse& response,
+  afterProcessingRequestHeaders(const envoy::service::ext_proc::v3::HeadersResponse& response,
                                 absl::Status processing_status,
                                 Envoy::StreamInfo::StreamInfo&) PURE;
 
   // Called after processing the response from the external processor with :ref:`response_headers
   // <envoy_v3_api_field_service.ext_proc.v3.ProcessingResponse.response_headers>` set.
   virtual void
-  afterProcessingResponseHeaders(const envoy::service::ext_proc::v3::ProcessingResponse& response,
+  afterProcessingResponseHeaders(const envoy::service::ext_proc::v3::HeadersResponse& response,
                                  absl::Status processing_status,
                                  Envoy::StreamInfo::StreamInfo&) PURE;
 
   // Called after processing the response from the external processor with :ref:`request_body
   // <envoy_v3_api_field_service.ext_proc.v3.ProcessingResponse.request_body>` set.
   virtual void
-  afterProcessingRequestBody(const envoy::service::ext_proc::v3::ProcessingResponse& response,
+  afterProcessingRequestBody(const envoy::service::ext_proc::v3::BodyResponse& response,
                              absl::Status processing_status, Envoy::StreamInfo::StreamInfo&) PURE;
 
   // Called after processing the response from the external processor with :ref:`response_body
   // <envoy_v3_api_field_service.ext_proc.v3.ProcessingResponse.response_body>` set.
   virtual void
-  afterProcessingResponseBody(const envoy::service::ext_proc::v3::ProcessingResponse& response,
+  afterProcessingResponseBody(const envoy::service::ext_proc::v3::BodyResponse& response,
                               absl::Status processing_status, Envoy::StreamInfo::StreamInfo&) PURE;
 
   // Called after processing the response from the external processor with :ref:`request_trailers
   // <envoy_v3_api_field_service.ext_proc.v3.ProcessingResponse.request_trailers>` set.
   virtual void
-  afterProcessingRequestTrailers(const envoy::service::ext_proc::v3::ProcessingResponse& response,
+  afterProcessingRequestTrailers(const envoy::service::ext_proc::v3::TrailersResponse& response,
                                  absl::Status processing_status,
                                  Envoy::StreamInfo::StreamInfo&) PURE;
 
   // Called after processing the response from the external processor with :ref:`response_trailers
   // <envoy_v3_api_field_service.ext_proc.v3.ProcessingResponse.response_trailers>` set.
   virtual void
-  afterProcessingResponseTrailers(const envoy::service::ext_proc::v3::ProcessingResponse& response,
+  afterProcessingResponseTrailers(const envoy::service::ext_proc::v3::TrailersResponse& response,
                                   absl::Status processing_status,
                                   Envoy::StreamInfo::StreamInfo&) PURE;
 
   // Called after processing the response from the external processor with :ref:`immediate_response
   // <envoy_v3_api_field_service.ext_proc.v3.ProcessingResponse.immediate_response>` set.
   virtual void
-  afterReceivingImmediateResponse(const envoy::service::ext_proc::v3::ProcessingResponse& response,
+  afterReceivingImmediateResponse(const envoy::service::ext_proc::v3::ImmediateResponse& response,
                                   absl::Status processing_status,
                                   Envoy::StreamInfo::StreamInfo&) PURE;
 };
diff --git a/source/extensions/filters/http/ext_proc/processor_state.cc b/source/extensions/filters/http/ext_proc/processor_state.cc
index 19b4e6bc79de1..678f9c2ec0ad3 100644
--- a/source/extensions/filters/http/ext_proc/processor_state.cc
+++ b/source/extensions/filters/http/ext_proc/processor_state.cc
@@ -151,6 +151,7 @@ absl::Status ProcessorState::handleHeadersResponse(const HeadersResponse& respon
     if (common_response.has_header_mutation()) {
       const auto mut_status = processHeaderMutation(common_response);
       if (!mut_status.ok()) {
+        filter_.onProcessHeadersResponse(response, mut_status, trafficDirection());
         return mut_status;
       }
     }
@@ -202,6 +203,7 @@ absl::Status ProcessorState::handleHeadersResponse(const HeadersResponse& respon
             auto req = filter_.setupBodyChunk(*this, *bufferedData(), trailers_ == nullptr);
             filter_.sendBodyChunk(*this, ProcessorState::CallbackState::BufferedBodyCallback, req);
             clearWatermark();
+            filter_.onProcessHeadersResponse(response, absl::OkStatus(), trafficDirection());
             return absl::OkStatus();
           }
         } else {
@@ -210,6 +212,7 @@ absl::Status ProcessorState::handleHeadersResponse(const HeadersResponse& respon
           // Do not continue filter chain here so the pending body response have chance to be
           // served.
           sendBufferedDataInStreamedMode(trailers_ == nullptr);
+          filter_.onProcessHeadersResponse(response, absl::OkStatus(), trafficDirection());
           return absl::OkStatus();
         }
       } else if (body_mode_ == ProcessingMode::BUFFERED) {
@@ -217,13 +220,16 @@ absl::Status ProcessorState::handleHeadersResponse(const HeadersResponse& respon
         // we won't be able to modify the headers any more, so do nothing and
         // let the doData callback handle body chunks until the end is reached.
         clearWatermark();
+        filter_.onProcessHeadersResponse(response, absl::OkStatus(), trafficDirection());
         return absl::OkStatus();
       } else if (body_mode_ == ProcessingMode::STREAMED) {
         sendBufferedDataInStreamedMode(false);
+        filter_.onProcessHeadersResponse(response, absl::OkStatus(), trafficDirection());
         continueIfNecessary();
         return absl::OkStatus();
       } else if (body_mode_ == ProcessingMode::FULL_DUPLEX_STREAMED) {
         // There is no buffered data in this mode.
+        filter_.onProcessHeadersResponse(response, absl::OkStatus(), trafficDirection());
         continueIfNecessary();
         return absl::OkStatus();
       } else if (body_mode_ == ProcessingMode::BUFFERED_PARTIAL) {
@@ -253,6 +259,7 @@ absl::Status ProcessorState::handleHeadersResponse(const HeadersResponse& respon
           // the headers while we buffer the body up to the limit.
           clearWatermark();
         }
+        filter_.onProcessHeadersResponse(response, absl::OkStatus(), trafficDirection());
         return absl::OkStatus();
       }
       if (send_trailers_ && trailers_ != nullptr) {
@@ -260,6 +267,7 @@ absl::Status ProcessorState::handleHeadersResponse(const HeadersResponse& respon
         // is not interested in the body, so send them now.
         filter_.sendTrailers(*this, *trailers_);
         clearWatermark();
+        filter_.onProcessHeadersResponse(response, absl::OkStatus(), trafficDirection());
         return absl::OkStatus();
       }
     }
@@ -268,6 +276,7 @@ absl::Status ProcessorState::handleHeadersResponse(const HeadersResponse& respon
     // trailers, so we can just continue.
     ENVOY_STREAM_LOG(trace, "Clearing stored headers", *filter_callbacks_);
     headers_ = nullptr;
+    filter_.onProcessHeadersResponse(response, absl::OkStatus(), trafficDirection());
     continueIfNecessary();
     clearWatermark();
     return absl::OkStatus();
@@ -302,6 +311,7 @@ absl::Status ProcessorState::handleBodyResponse(const BodyResponse& response) {
   }
 
   if (!result.ok()) {
+    filter_.onProcessBodyResponse(response, result.status(), trafficDirection());
     return result.status();
   }
 
@@ -309,6 +319,7 @@ absl::Status ProcessorState::handleBodyResponse(const BodyResponse& response) {
   clearRouteCache(common_response);
   finalizeBodyResponse(*result);
 
+  filter_.onProcessBodyResponse(response, absl::OkStatus(), trafficDirection());
   return absl::OkStatus();
 }
 
@@ -466,11 +477,13 @@ absl::Status ProcessorState::handleTrailersResponse(const TrailersResponse& resp
           response.header_mutation(), *trailers_, false, filter_.config().mutationChecker(),
           filter_.stats().rejected_header_mutations_);
       if (!mut_status.ok()) {
+        filter_.onProcessTrailersResponse(response, mut_status, trafficDirection());
         return mut_status;
       }
     }
     trailers_ = nullptr;
     onFinishProcessorCall(Grpc::Status::Ok);
+    filter_.onProcessTrailersResponse(response, absl::OkStatus(), trafficDirection());
     continueIfNecessary();
     return absl::OkStatus();
   }
diff --git a/source/extensions/http/ext_proc/response_processors/save_processing_response/save_processing_response.cc b/source/extensions/http/ext_proc/response_processors/save_processing_response/save_processing_response.cc
index 17e4c38ff5bfa..2d6f2669b9299 100644
--- a/source/extensions/http/ext_proc/response_processors/save_processing_response/save_processing_response.cc
+++ b/source/extensions/http/ext_proc/response_processors/save_processing_response/save_processing_response.cc
@@ -19,56 +19,75 @@ SaveProcessingResponse::SaveProcessingResponse(const SaveProcessingResponseProto
       save_immediate_response_(config.save_immediate_response()) {}
 
 void SaveProcessingResponse::addToFilterState(
-    const SaveOptions& save_options,
-    const envoy::service::ext_proc::v3::ProcessingResponse& processing_response,
-    absl::Status status, Envoy::StreamInfo::StreamInfo& stream_info) {
-  if (!save_options.save_response) {
-    return;
+    envoy::service::ext_proc::v3::ProcessingResponse processing_response, absl::Status status,
+    Envoy::StreamInfo::StreamInfo& stream_info) {
+  SaveProcessingResponseFilterState* filter_state =
+      stream_info.filterState()->getDataMutable<SaveProcessingResponseFilterState>(
+          filter_state_name_);
+  if (filter_state == nullptr) {
+    auto shared_filter_state = std::make_shared<SaveProcessingResponseFilterState>();
+    filter_state = shared_filter_state.get();
+    stream_info.filterState()->setData(filter_state_name_, shared_filter_state,
+                                       Envoy::StreamInfo::FilterState::StateType::Mutable);
   }
-  if (status.ok() || save_options.save_on_error) {
-    SaveProcessingResponseFilterState* filter_state =
-        stream_info.filterState()->getDataMutable<SaveProcessingResponseFilterState>(
-            filter_state_name_);
-    if (filter_state == nullptr) {
-      auto shared_filter_state = std::make_shared<SaveProcessingResponseFilterState>();
-      filter_state = shared_filter_state.get();
-      stream_info.filterState()->setData(filter_state_name_, shared_filter_state,
-                                         Envoy::StreamInfo::FilterState::StateType::Mutable);
-    }
 
-    filter_state->response.emplace(SaveProcessingResponseFilterState::Response{
-        .processing_status = status, .processing_response = processing_response});
-  }
+  filter_state->response.emplace(SaveProcessingResponseFilterState::Response{
+      .processing_status = status, .processing_response = processing_response});
 }
 
 void SaveProcessingResponse::afterProcessingRequestHeaders(
-    const envoy::service::ext_proc::v3::ProcessingResponse& response, absl::Status status,
+    const envoy::service::ext_proc::v3::HeadersResponse& response, absl::Status status,
     Envoy::StreamInfo::StreamInfo& stream_info) {
-  addToFilterState(save_request_headers_, response, status, stream_info);
+  if (!shouldSaveResponse(save_request_headers_, status)) {
+    return;
+  }
+  envoy::service::ext_proc::v3::ProcessingResponse processing_response;
+  *processing_response.mutable_request_headers() = response;
+  addToFilterState(processing_response, status, stream_info);
 }
 
 void SaveProcessingResponse::afterProcessingResponseHeaders(
-    const envoy::service::ext_proc::v3::ProcessingResponse& response, absl::Status status,
+    const envoy::service::ext_proc::v3::HeadersResponse& response, absl::Status status,
     Envoy::StreamInfo::StreamInfo& stream_info) {
-  addToFilterState(save_response_headers_, response, status, stream_info);
+  if (!shouldSaveResponse(save_response_headers_, status)) {
+    return;
+  }
+  envoy::service::ext_proc::v3::ProcessingResponse processing_response;
+  *processing_response.mutable_response_headers() = response;
+  addToFilterState(processing_response, status, stream_info);
 }
 
 void SaveProcessingResponse::afterProcessingRequestTrailers(
-    const envoy::service::ext_proc::v3::ProcessingResponse& response, absl::Status status,
+    const envoy::service::ext_proc::v3::TrailersResponse& response, absl::Status status,
     Envoy::StreamInfo::StreamInfo& stream_info) {
-  addToFilterState(save_request_trailers_, response, status, stream_info);
+  if (!shouldSaveResponse(save_request_trailers_, status)) {
+    return;
+  }
+  envoy::service::ext_proc::v3::ProcessingResponse processing_response;
+  *processing_response.mutable_request_trailers() = response;
+  addToFilterState(processing_response, status, stream_info);
 }
 
 void SaveProcessingResponse::afterProcessingResponseTrailers(
-    const envoy::service::ext_proc::v3::ProcessingResponse& response, absl::Status status,
+    const envoy::service::ext_proc::v3::TrailersResponse& response, absl::Status status,
     Envoy::StreamInfo::StreamInfo& stream_info) {
-  addToFilterState(save_response_trailers_, response, status, stream_info);
+  if (!shouldSaveResponse(save_response_trailers_, status)) {
+    return;
+  }
+  envoy::service::ext_proc::v3::ProcessingResponse processing_response;
+  *processing_response.mutable_response_trailers() = response;
+  addToFilterState(processing_response, status, stream_info);
 }
 
 void SaveProcessingResponse::afterReceivingImmediateResponse(
-    const envoy::service::ext_proc::v3::ProcessingResponse& response, absl::Status status,
+    const envoy::service::ext_proc::v3::ImmediateResponse& response, absl::Status status,
     Envoy::StreamInfo::StreamInfo& stream_info) {
-  addToFilterState(save_immediate_response_, response, status, stream_info);
+  if (!shouldSaveResponse(save_immediate_response_, status)) {
+    return;
+  }
+  envoy::service::ext_proc::v3::ProcessingResponse processing_response;
+  *processing_response.mutable_immediate_response() = response;
+  addToFilterState(processing_response, status, stream_info);
 }
 
 } // namespace ExternalProcessing
diff --git a/source/extensions/http/ext_proc/response_processors/save_processing_response/save_processing_response.h b/source/extensions/http/ext_proc/response_processors/save_processing_response/save_processing_response.h
index 3e4799c4bf24c..7f70ed368038b 100644
--- a/source/extensions/http/ext_proc/response_processors/save_processing_response/save_processing_response.h
+++ b/source/extensions/http/ext_proc/response_processors/save_processing_response/save_processing_response.h
@@ -37,23 +37,22 @@ class SaveProcessingResponse
 
   SaveProcessingResponse(const SaveProcessingResponseProto&);
 
-  void
-  afterProcessingRequestHeaders(const envoy::service::ext_proc::v3::ProcessingResponse& response,
-                                absl::Status processing_status,
-                                Envoy::StreamInfo::StreamInfo&) override;
-  void afterProcessingResponseHeaders(const envoy::service::ext_proc::v3::ProcessingResponse&,
+  void afterProcessingRequestHeaders(const envoy::service::ext_proc::v3::HeadersResponse& response,
+                                     absl::Status processing_status,
+                                     Envoy::StreamInfo::StreamInfo&) override;
+  void afterProcessingResponseHeaders(const envoy::service::ext_proc::v3::HeadersResponse&,
                                       absl::Status, Envoy::StreamInfo::StreamInfo&) override;
   // Not implemented.
-  void afterProcessingRequestBody(const envoy::service::ext_proc::v3::ProcessingResponse&,
-                                  absl::Status, Envoy::StreamInfo::StreamInfo&) override{};
+  void afterProcessingRequestBody(const envoy::service::ext_proc::v3::BodyResponse&, absl::Status,
+                                  Envoy::StreamInfo::StreamInfo&) override{};
   // Not implemented.
-  void afterProcessingResponseBody(const envoy::service::ext_proc::v3::ProcessingResponse&,
-                                   absl::Status, Envoy::StreamInfo::StreamInfo&) override{};
-  void afterProcessingRequestTrailers(const envoy::service::ext_proc::v3::ProcessingResponse&,
+  void afterProcessingResponseBody(const envoy::service::ext_proc::v3::BodyResponse&, absl::Status,
+                                   Envoy::StreamInfo::StreamInfo&) override{};
+  void afterProcessingRequestTrailers(const envoy::service::ext_proc::v3::TrailersResponse&,
                                       absl::Status, Envoy::StreamInfo::StreamInfo&) override;
-  void afterProcessingResponseTrailers(const envoy::service::ext_proc::v3::ProcessingResponse&,
+  void afterProcessingResponseTrailers(const envoy::service::ext_proc::v3::TrailersResponse&,
                                        absl::Status, Envoy::StreamInfo::StreamInfo&) override;
-  void afterReceivingImmediateResponse(const envoy::service::ext_proc::v3::ProcessingResponse&,
+  void afterReceivingImmediateResponse(const envoy::service::ext_proc::v3::ImmediateResponse&,
                                        absl::Status, Envoy::StreamInfo::StreamInfo&) override;
 
 private:
@@ -65,10 +64,18 @@ class SaveProcessingResponse
     const bool save_on_error = false;
   };
 
-  void addToFilterState(const SaveOptions& save_options,
-                        const envoy::service::ext_proc::v3::ProcessingResponse& processing_response,
+  void addToFilterState(envoy::service::ext_proc::v3::ProcessingResponse processing_response,
                         absl::Status status, Envoy::StreamInfo::StreamInfo& stream_info);
 
+  // Answer whether the response from the external processor should be saved to
+  // filter state based on the configured save options for the response type and processing status.
+  bool shouldSaveResponse(SaveOptions save_options, absl::Status status) {
+    if (!save_options.save_response) {
+      return false;
+    }
+    return (status.ok() || save_options.save_on_error);
+  }
+
   const std::string filter_state_name_;
 
   SaveOptions save_request_headers_;
diff --git a/test/extensions/filters/http/ext_proc/BUILD b/test/extensions/filters/http/ext_proc/BUILD
index 6bdfce9301383..18a5a5517b1f6 100644
--- a/test/extensions/filters/http/ext_proc/BUILD
+++ b/test/extensions/filters/http/ext_proc/BUILD
@@ -188,12 +188,15 @@ envoy_extension_cc_test(
         ":tracer_test_filter_lib",
         ":utils_lib",
         "//source/extensions/filters/http/ext_proc:config",
+        "//source/extensions/filters/http/ext_proc:on_processing_response_interface",
         "//source/extensions/filters/http/set_metadata:config",
         "//source/extensions/retry/host/previous_hosts:config",
         "//test/common/http:common_lib",
         "//test/integration:http_integration_lib",
+        "//test/integration/filters:common_lib",
         "//test/integration/filters:stream_info_to_headers_filter_lib",
         "//test/proto:helloworld_proto_cc_proto",
+        "//test/test_common:registry_lib",
         "//test/test_common:test_runtime_lib",
         "//test/test_common:utility_lib",
         "@envoy_api//envoy/config/core/v3:pkg_cc_proto",
diff --git a/test/extensions/filters/http/ext_proc/ext_proc_integration_test.cc b/test/extensions/filters/http/ext_proc/ext_proc_integration_test.cc
index 7ef8ec2b1861f..072aab64bb87e 100644
--- a/test/extensions/filters/http/ext_proc/ext_proc_integration_test.cc
+++ b/test/extensions/filters/http/ext_proc/ext_proc_integration_test.cc
@@ -11,6 +11,7 @@
 
 #include "source/extensions/filters/http/ext_proc/config.h"
 #include "source/extensions/filters/http/ext_proc/ext_proc.h"
+#include "source/extensions/filters/http/ext_proc/on_processing_response.h"
 
 #include "test/common/http/common.h"
 #include "test/extensions/filters/http/ext_proc/logging_test_filter.pb.h"
@@ -18,7 +19,9 @@
 #include "test/extensions/filters/http/ext_proc/tracer_test_filter.pb.h"
 #include "test/extensions/filters/http/ext_proc/tracer_test_filter.pb.validate.h"
 #include "test/extensions/filters/http/ext_proc/utils.h"
+#include "test/integration/filters/common.h"
 #include "test/integration/http_integration.h"
+#include "test/test_common/registry.h"
 #include "test/test_common/test_runtime.h"
 #include "test/test_common/utility.h"
 
@@ -33,6 +36,7 @@ using envoy::config::route::v3::VirtualHost;
 using envoy::extensions::filters::http::ext_proc::v3::ExtProcPerRoute;
 using envoy::extensions::filters::http::ext_proc::v3::ProcessingMode;
 using envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager;
+using Envoy::Extensions::HttpFilters::ExternalProcessing::verifyMultipleHeaderValues;
 using Envoy::Protobuf::MapPair;
 using Envoy::ProtobufWkt::Any;
 using envoy::service::ext_proc::v3::BodyResponse;
@@ -50,7 +54,9 @@ using Extensions::HttpFilters::ExternalProcessing::DEFAULT_DEFERRED_CLOSE_TIMEOU
 using Extensions::HttpFilters::ExternalProcessing::HasNoHeader;
 using Extensions::HttpFilters::ExternalProcessing::HeaderProtosEqual;
 using Extensions::HttpFilters::ExternalProcessing::makeHeaderValue;
+using Extensions::HttpFilters::ExternalProcessing::OnProcessingResponseFactory;
 using Extensions::HttpFilters::ExternalProcessing::SingleHeaderValueIs;
+using Extensions::HttpFilters::ExternalProcessing::TestOnProcessingResponseFactory;
 
 using Http::LowerCaseString;
 
@@ -62,6 +68,30 @@ struct ConfigOptions {
   bool http1_codec = false;
   bool add_metadata = false;
   bool downstream_filter = true;
+  bool add_response_processor = false;
+};
+
+// A filter that sticks dynamic metadata info into headers for integration testing.
+class DynamicMetadataToHeadersFilter : public Http::PassThroughFilter {
+public:
+  constexpr static char name[] = "dynamic-metadata-to-headers-filter";
+
+  Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap&, bool) override {
+    return Http::FilterHeadersStatus::Continue;
+  }
+
+  Http::FilterHeadersStatus encodeHeaders(Http::ResponseHeaderMap& headers, bool) override {
+    if (decoder_callbacks_->streamInfo().dynamicMetadata().filter_metadata_size() > 0) {
+      const auto& md = decoder_callbacks_->streamInfo().dynamicMetadata().filter_metadata();
+      for (const auto& md_entry : md) {
+        std::string key_prefix = md_entry.first;
+        for (const auto& field : md_entry.second.fields()) {
+          headers.addCopy(Http::LowerCaseString(key_prefix), field.first);
+        }
+      }
+    }
+    return Http::FilterHeadersStatus::Continue;
+  }
 };
 
 // These tests exercise the ext_proc filter through Envoy's integration test
@@ -176,6 +206,20 @@ class ExtProcIntegrationTest : public HttpIntegrationTest,
         )EOF"));
       }
 
+      // Add dynamic_metadata_to_headers filter to inject dynamic metadata used for testing
+      if (config_option.add_response_processor) {
+        simple_filter_config_ =
+            std::make_unique<SimpleFilterConfig<DynamicMetadataToHeadersFilter>>();
+        registration_ = std::make_unique<
+            Envoy::Registry::InjectFactory<Server::Configuration::NamedHttpFilterConfigFactory>>(
+            *simple_filter_config_);
+        config_helper_.prependFilter(fmt::format(R"EOF(
+         name: dynamic-metadata-to-headers-filter
+         typed_config:
+           "@type": type.googleapis.com/google.protobuf.Struct
+        )EOF"));
+      }
+
       // Add logging test filter only in Envoy gRPC mode.
       // gRPC side stream logging is only supported in Envoy gRPC mode at the moment.
       if (clientType() == Grpc::ClientType::EnvoyGrpc && config_option.add_logging_filter &&
@@ -197,6 +241,16 @@ class ExtProcIntegrationTest : public HttpIntegrationTest,
       // assumptions of data flow, prior relying on eager processing.
     });
 
+    if (config_option.add_response_processor) {
+      processing_response_factory_ = std::make_unique<TestOnProcessingResponseFactory>();
+      processing_response_factory_registration_ =
+          std::make_unique<Envoy::Registry::InjectFactory<OnProcessingResponseFactory>>(
+              *processing_response_factory_);
+      ProtobufWkt::Struct config;
+      proto_config_.mutable_on_processing_response()->set_name("test-on-processing-response");
+      proto_config_.mutable_on_processing_response()->mutable_typed_config()->PackFrom(config);
+    }
+
     if (config_option.http1_codec) {
       setUpstreamProtocol(Http::CodecType::HTTP1);
       setDownstreamProtocol(Http::CodecType::HTTP1);
@@ -843,6 +897,13 @@ class ExtProcIntegrationTest : public HttpIntegrationTest,
     processor_stream_->sendGrpcMessage(response_trailer);
   }
 
+  std::unique_ptr<SimpleFilterConfig<DynamicMetadataToHeadersFilter>> simple_filter_config_;
+  std::unique_ptr<
+      Envoy::Registry::InjectFactory<Server::Configuration::NamedHttpFilterConfigFactory>>
+      registration_;
+  std::unique_ptr<TestOnProcessingResponseFactory> processing_response_factory_;
+  std::unique_ptr<Envoy::Registry::InjectFactory<OnProcessingResponseFactory>>
+      processing_response_factory_registration_;
   envoy::extensions::filters::http::ext_proc::v3::ExternalProcessor proto_config_{};
   bool protocol_config_encoded_ = false;
   ProtocolConfiguration protocol_config_{};
@@ -1748,7 +1809,9 @@ TEST_P(ExtProcIntegrationTest, MismatchedContentLengthAndBodyLength) {
 // an ext_proc server that responds to the response_headers message
 // by requesting to modify the response headers.
 TEST_P(ExtProcIntegrationTest, GetAndSetHeadersOnResponse) {
-  initializeConfig();
+  ConfigOptions config_options;
+  config_options.add_response_processor = true;
+  initializeConfig(config_options);
   HttpIntegrationTest::initialize();
   auto response = sendDownstreamRequest(absl::nullopt);
   processRequestHeadersMessage(*grpc_upstreams_[0], true, absl::nullopt);
@@ -1768,6 +1831,11 @@ TEST_P(ExtProcIntegrationTest, GetAndSetHeadersOnResponse) {
 
   verifyDownstreamResponse(*response, 201);
   EXPECT_THAT(response->headers(), SingleHeaderValueIs("x-response-processed", "1"));
+  // Verify that the response processor added headers to dynamic metadata
+  verifyMultipleHeaderValues(
+      response->headers(),
+      Envoy::Http::LowerCaseString("envoy-test-ext_proc-response_headers_response"), ":status",
+      "x-response-processed");
 }
 
 // Test the filter using the default configuration by connecting to
@@ -1836,7 +1904,9 @@ TEST_P(ExtProcIntegrationTest, GetAndSetHeadersOnResponseTwoStatuses) {
 // by checking the headers and modifying the trailers
 TEST_P(ExtProcIntegrationTest, GetAndSetHeadersAndTrailersOnResponse) {
   proto_config_.mutable_processing_mode()->set_response_trailer_mode(ProcessingMode::SEND);
-  initializeConfig();
+  ConfigOptions config_options;
+  config_options.add_response_processor = true;
+  initializeConfig(config_options);
   HttpIntegrationTest::initialize();
   auto response = sendDownstreamRequest(absl::nullopt);
   processRequestHeadersMessage(*grpc_upstreams_[0], true, absl::nullopt);
@@ -1858,6 +1928,9 @@ TEST_P(ExtProcIntegrationTest, GetAndSetHeadersAndTrailersOnResponse) {
   ASSERT_TRUE(response->trailers());
   EXPECT_THAT(*(response->trailers()), SingleHeaderValueIs("x-test-trailers", "Yes"));
   EXPECT_THAT(*(response->trailers()), SingleHeaderValueIs("x-modified-trailers", "xxx"));
+  EXPECT_THAT(
+      response->headers(),
+      SingleHeaderValueIs("envoy-test-ext_proc-response_trailers_response", "x-modified-trailers"));
 }
 
 // Test the filter using the default configuration by connecting to
@@ -1919,7 +1992,9 @@ TEST_P(ExtProcIntegrationTest, GetAndSetOnlyTrailersOnResponse) {
 // by requesting to modify the response body and headers.
 TEST_P(ExtProcIntegrationTest, GetAndSetBodyAndHeadersOnResponse) {
   proto_config_.mutable_processing_mode()->set_response_body_mode(ProcessingMode::BUFFERED);
-  initializeConfig();
+  ConfigOptions config_options;
+  config_options.add_response_processor = true;
+  initializeConfig(config_options);
   HttpIntegrationTest::initialize();
   auto response = sendDownstreamRequest(absl::nullopt);
   processRequestHeadersMessage(*grpc_upstreams_[0], true, absl::nullopt);
@@ -1952,6 +2027,9 @@ TEST_P(ExtProcIntegrationTest, GetAndSetBodyAndHeadersOnResponse) {
   // Verify that the content length header in the response is set by external processor,
   EXPECT_EQ(response->headers().getContentLengthValue(), "13");
   EXPECT_EQ("Hello, World!", response->body());
+  EXPECT_THAT(
+      response->headers(),
+      SingleHeaderValueIs("envoy-test-ext_proc-response_headers_response", "content-length"));
 }
 
 TEST_P(ExtProcIntegrationTest, GetAndSetBodyOnResponse) {
@@ -2275,7 +2353,9 @@ TEST_P(ExtProcIntegrationTest, GetAndRespondImmediatelyOnResponse) {
 // should still be seen by the downstream.
 TEST_P(ExtProcIntegrationTest, GetAndRespondImmediatelyOnStreamedRequestBody) {
   proto_config_.mutable_processing_mode()->set_request_body_mode(ProcessingMode::STREAMED);
-  initializeConfig();
+  ConfigOptions config_options;
+  config_options.add_response_processor = true;
+  initializeConfig(config_options);
   HttpIntegrationTest::initialize();
   auto response = sendDownstreamRequestWithBody("Evil content!", absl::nullopt);
   processRequestHeadersMessage(
@@ -2294,6 +2374,8 @@ TEST_P(ExtProcIntegrationTest, GetAndRespondImmediatelyOnStreamedRequestBody) {
   EXPECT_EQ("{\"reason\": \"Request too evil\"}", response->body());
   // The previously added request header is not sent to the client.
   EXPECT_THAT(response->headers(), HasNoHeader("foo"));
+  EXPECT_THAT(response->headers(),
+              SingleHeaderValueIs("envoy-test-ext_proc-request_headers_response", "foo"));
 }
 
 // Test immediate_response behavior with STREAMED response body.
diff --git a/test/extensions/filters/http/ext_proc/filter_test.cc b/test/extensions/filters/http/ext_proc/filter_test.cc
index 3e8e21e88a3b1..17b06dded283c 100644
--- a/test/extensions/filters/http/ext_proc/filter_test.cc
+++ b/test/extensions/filters/http/ext_proc/filter_test.cc
@@ -5367,9 +5367,9 @@ TEST_F(HttpFilterTest, OnProcessingResponseHeaders) {
   EXPECT_THAT(&request_headers_, HeaderMapEqualIgnoreOrder(&expected));
 
   ASSERT_TRUE(
-      dynamic_metadata_.filter_metadata().contains("envoy.test.ext_proc.request_headers_response"));
+      dynamic_metadata_.filter_metadata().contains("envoy-test-ext_proc-request_headers_response"));
   const auto& request_headers_struct_metadata =
-      dynamic_metadata_.filter_metadata().at("envoy.test.ext_proc.request_headers_response");
+      dynamic_metadata_.filter_metadata().at("envoy-test-ext_proc-request_headers_response");
   ProtobufWkt::Struct expected_request_headers;
   TestUtility::loadFromJson(R"EOF(
 {
@@ -5412,9 +5412,9 @@ TEST_F(HttpFilterTest, OnProcessingResponseHeaders) {
   EXPECT_THAT(&response_headers_, HeaderMapEqualIgnoreOrder(&final_expected_response));
 
   ASSERT_TRUE(dynamic_metadata_.filter_metadata().contains(
-      "envoy.test.ext_proc.response_headers_response"));
+      "envoy-test-ext_proc-response_headers_response"));
   const auto& response_headers_struct_metadata =
-      dynamic_metadata_.filter_metadata().at("envoy.test.ext_proc.response_headers_response");
+      dynamic_metadata_.filter_metadata().at("envoy-test-ext_proc-response_headers_response");
   ProtobufWkt::Struct expected_response_headers;
   TestUtility::loadFromJson(R"EOF(
 {
@@ -5632,9 +5632,9 @@ TEST_F(HttpFilterTest, OnProcessingResponseBodies) {
   EXPECT_EQ(FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_));
 
   ASSERT_TRUE(
-      dynamic_metadata_.filter_metadata().contains("envoy.test.ext_proc.request_body_response"));
+      dynamic_metadata_.filter_metadata().contains("envoy-test-ext_proc-request_body_response"));
   const auto& request_body_struct_metadata =
-      dynamic_metadata_.filter_metadata().at("envoy.test.ext_proc.request_body_response");
+      dynamic_metadata_.filter_metadata().at("envoy-test-ext_proc-request_body_response");
   ProtobufWkt::Struct expected_request_body;
   TestUtility::loadFromJson(R"EOF(
 {
@@ -5666,9 +5666,9 @@ TEST_F(HttpFilterTest, OnProcessingResponseBodies) {
   EXPECT_EQ("Hello, World!", buffered_response_data.toString());
 
   ASSERT_TRUE(
-      dynamic_metadata_.filter_metadata().contains("envoy.test.ext_proc.response_body_response"));
+      dynamic_metadata_.filter_metadata().contains("envoy-test-ext_proc-response_body_response"));
   const auto& response_body_struct_metadata =
-      dynamic_metadata_.filter_metadata().at("envoy.test.ext_proc.response_body_response");
+      dynamic_metadata_.filter_metadata().at("envoy-test-ext_proc-response_body_response");
   ProtobufWkt::Struct expected_response_body;
   TestUtility::loadFromJson(R"EOF(
 {
@@ -5797,108 +5797,6 @@ TEST_F(HttpFilterTest, SaveImmediateResponse) {
   expectFilterState(Envoy::ProtobufWkt::Struct());
 }
 
-TEST_F(HttpFilterTest, SaveImmediateResponseOnError) {
-  SaveProcessingResponseFactory factory;
-  Envoy::Registry::InjectFactory<OnProcessingResponseFactory> registration(factory);
-  initialize(R"EOF(
-  grpc_service:
-    envoy_grpc:
-      cluster_name: "ext_proc_server"
-  disable_immediate_response: true
-  on_processing_response:
-    name: "abc"
-    typed_config:
-      '@type': type.googleapis.com/envoy.extensions.http.ext_proc.response_processors.save_processing_response.v3.SaveProcessingResponse
-      save_immediate_response:
-        save_response: true
-        save_on_error: true
-  )EOF");
-
-  EXPECT_EQ(FilterHeadersStatus::StopIteration, filter_->decodeHeaders(request_headers_, false));
-  test_time_->advanceTimeWait(std::chrono::microseconds(10));
-  TestResponseHeaderMapImpl immediate_response_headers;
-  std::unique_ptr<ProcessingResponse> resp1 = std::make_unique<ProcessingResponse>();
-  auto* immediate_response = resp1->mutable_immediate_response();
-  immediate_response->mutable_status()->set_code(envoy::type::v3::StatusCode::BadRequest);
-  immediate_response->set_body("Bad request");
-  immediate_response->set_details("Got a bad request");
-  auto* immediate_headers = immediate_response->mutable_headers();
-  auto* hdr1 = immediate_headers->add_set_headers();
-  hdr1->mutable_header()->set_key("content-type");
-  hdr1->mutable_header()->set_raw_value("text/plain");
-  auto* hdr2 = immediate_headers->add_set_headers();
-  hdr2->mutable_append()->set_value(true);
-  hdr2->mutable_header()->set_key("x-another-thing");
-  hdr2->mutable_header()->set_raw_value("1");
-  auto* hdr3 = immediate_headers->add_set_headers();
-  hdr3->mutable_append()->set_value(true);
-  hdr3->mutable_header()->set_key("x-another-thing");
-  hdr3->mutable_header()->set_raw_value("2");
-  stream_callbacks_->onReceiveMessage(std::move(resp1));
-
-  Buffer::OwnedImpl req_data("foo");
-  EXPECT_EQ(FilterDataStatus::Continue, filter_->decodeData(req_data, true));
-  EXPECT_EQ(FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_));
-  EXPECT_EQ(FilterHeadersStatus::Continue, filter_->encodeHeaders(response_headers_, true));
-  Buffer::OwnedImpl resp_data("bar");
-  EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(resp_data, false));
-  Buffer::OwnedImpl empty_data;
-  EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(empty_data, true));
-  EXPECT_EQ(FilterTrailersStatus::Continue, filter_->encodeTrailers(response_trailers_));
-  auto filter_state = stream_info_.filterState()->getDataMutable<SaveProcessingResponseFilterState>(
-      SaveProcessingResponseFilterState::kFilterStateName);
-  ASSERT_TRUE(filter_state->response.has_value());
-  envoy::service::ext_proc::v3::ProcessingResponse expected_response;
-  TestUtility::loadFromJson(
-      R"EOF(
-{
-  "immediateResponse": {
-    "status": {
-      "code": "BadRequest"
-    },
-    "headers": {
-      "setHeaders": [{
-        "header": {
-          "key": "content-type",
-          "rawValue": "dGV4dC9wbGFpbg=="
-        }
-      }, {
-        "header": {
-          "key": "x-another-thing",
-          "rawValue": "MQ=="
-        },
-        "append": true
-      }, {
-        "header": {
-          "key": "x-another-thing",
-          "rawValue": "Mg=="
-        },
-        "append": true
-      }]
-    },
-    "body": "QmFkIHJlcXVlc3Q=",
-    "details": "Got a bad request"
-  }
-})EOF",
-      expected_response);
-
-  EXPECT_TRUE(TestUtility::protoEqual(filter_state->response.value().processing_response,
-                                      expected_response));
-
-  filter_state->response.reset();
-
-  filter_->onDestroy();
-
-  EXPECT_EQ(1, config_->stats().streams_started_.value());
-  EXPECT_EQ(1, config_->stats().stream_msgs_sent_.value());
-  EXPECT_EQ(0, config_->stats().stream_msgs_received_.value());
-  EXPECT_EQ(1, config_->stats().streams_closed_.value());
-
-  expectNoGrpcCall(envoy::config::core::v3::TrafficDirection::OUTBOUND);
-
-  expectFilterState(Envoy::ProtobufWkt::Struct());
-}
-
 TEST_F(HttpFilterTest, DontSaveImmediateResponse) {
   SaveProcessingResponseFactory factory;
   Envoy::Registry::InjectFactory<OnProcessingResponseFactory> registration(factory);
diff --git a/test/extensions/filters/http/ext_proc/utils.cc b/test/extensions/filters/http/ext_proc/utils.cc
index 3baaefeb516fb..b835c369e8343 100644
--- a/test/extensions/filters/http/ext_proc/utils.cc
+++ b/test/extensions/filters/http/ext_proc/utils.cc
@@ -47,75 +47,66 @@ envoy::config::core::v3::HeaderValue makeHeaderValue(const std::string& key,
 }
 
 void TestOnProcessingResponse::afterProcessingRequestHeaders(
-    const envoy::service::ext_proc::v3::ProcessingResponse& response, absl::Status,
+    const envoy::service::ext_proc::v3::HeadersResponse& response, absl::Status,
     Envoy::StreamInfo::StreamInfo& stream_info) {
-  ASSERT(response.has_request_headers());
-  ASSERT(response.request_headers().has_response() &&
-         response.request_headers().response().has_header_mutation());
-  stream_info.setDynamicMetadata(
-      "envoy.test.ext_proc.request_headers_response",
-      getHeaderMutations(response.request_headers().response().header_mutation()));
+  if (response.has_response() && response.response().has_header_mutation()) {
+    stream_info.setDynamicMetadata("envoy-test-ext_proc-request_headers_response",
+                                   getHeaderMutations(response.response().header_mutation()));
+  }
 }
 
 void TestOnProcessingResponse::afterProcessingResponseHeaders(
-    const envoy::service::ext_proc::v3::ProcessingResponse& response, absl::Status,
+    const envoy::service::ext_proc::v3::HeadersResponse& response, absl::Status,
     Envoy::StreamInfo::StreamInfo& stream_info) {
-  ASSERT(response.has_response_headers());
-  ASSERT(response.response_headers().has_response() &&
-         response.response_headers().response().has_header_mutation());
-  stream_info.setDynamicMetadata(
-      "envoy.test.ext_proc.response_headers_response",
-      getHeaderMutations(response.response_headers().response().header_mutation()));
+  if (response.has_response() && response.response().has_header_mutation()) {
+    stream_info.setDynamicMetadata("envoy-test-ext_proc-response_headers_response",
+                                   getHeaderMutations(response.response().header_mutation()));
+  }
 }
 
 void TestOnProcessingResponse::afterProcessingRequestBody(
-    const envoy::service::ext_proc::v3::ProcessingResponse& response, absl::Status,
+    const envoy::service::ext_proc::v3::BodyResponse& response, absl::Status,
     Envoy::StreamInfo::StreamInfo& stream_info) {
-  ASSERT(response.has_request_body());
-  ASSERT(response.request_body().has_response() &&
-         response.request_body().response().has_body_mutation());
-  stream_info.setDynamicMetadata(
-      "envoy.test.ext_proc.request_body_response",
-      getBodyMutation(response.request_body().response().body_mutation()));
+  if (response.has_response() && response.response().has_body_mutation()) {
+    stream_info.setDynamicMetadata("envoy-test-ext_proc-request_body_response",
+                                   getBodyMutation(response.response().body_mutation()));
+  }
 }
 
 void TestOnProcessingResponse::afterProcessingResponseBody(
-    const envoy::service::ext_proc::v3::ProcessingResponse& response, absl::Status,
+    const envoy::service::ext_proc::v3::BodyResponse& response, absl::Status,
     Envoy::StreamInfo::StreamInfo& stream_info) {
-  ASSERT(response.has_response_body());
-  ASSERT(response.response_body().has_response() &&
-         response.response_body().response().has_body_mutation());
-  stream_info.setDynamicMetadata(
-      "envoy.test.ext_proc.response_body_response",
-      getBodyMutation(response.response_body().response().body_mutation()));
+  if (response.has_response() && response.response().has_body_mutation()) {
+    stream_info.setDynamicMetadata("envoy-test-ext_proc-response_body_response",
+                                   getBodyMutation(response.response().body_mutation()));
+  }
 }
 
 void TestOnProcessingResponse::afterProcessingRequestTrailers(
-    const envoy::service::ext_proc::v3::ProcessingResponse& response, absl::Status,
+    const envoy::service::ext_proc::v3::TrailersResponse& response, absl::Status,
     Envoy::StreamInfo::StreamInfo& stream_info) {
-  ASSERT(response.has_request_trailers());
-  ASSERT(response.request_trailers().has_header_mutation());
-  stream_info.setDynamicMetadata("envoy.test.ext_proc.request_trailers_response",
-                                 getHeaderMutations(response.request_trailers().header_mutation()));
+  if (response.has_header_mutation()) {
+    stream_info.setDynamicMetadata("envoy-test-ext_proc-request_trailers_response",
+                                   getHeaderMutations(response.header_mutation()));
+  }
 }
 
 void TestOnProcessingResponse::afterProcessingResponseTrailers(
-    const envoy::service::ext_proc::v3::ProcessingResponse& response, absl::Status,
+    const envoy::service::ext_proc::v3::TrailersResponse& response, absl::Status,
     Envoy::StreamInfo::StreamInfo& stream_info) {
-  ASSERT(response.has_response_trailers());
-  ASSERT(response.response_trailers().has_header_mutation());
-  stream_info.setDynamicMetadata(
-      "envoy.test.ext_proc.response_trailers_response",
-      getHeaderMutations(response.response_trailers().header_mutation()));
+  if (response.has_header_mutation()) {
+    stream_info.setDynamicMetadata("envoy-test-ext_proc-response_trailers_response",
+                                   getHeaderMutations(response.header_mutation()));
+  }
 }
 
 void TestOnProcessingResponse::afterReceivingImmediateResponse(
-    const envoy::service::ext_proc::v3::ProcessingResponse& response, absl::Status,
+    const envoy::service::ext_proc::v3::ImmediateResponse& response, absl::Status,
     Envoy::StreamInfo::StreamInfo& stream_info) {
-  ASSERT(response.has_immediate_response());
-  ASSERT(response.immediate_response().has_headers());
-  stream_info.setDynamicMetadata("envoy.test.ext_proc.response_immediate_response",
-                                 getHeaderMutations(response.immediate_response().headers()));
+  if (response.has_headers()) {
+    stream_info.setDynamicMetadata("envoy-test-ext_proc-response_immediate_response",
+                                   getHeaderMutations(response.headers()));
+  }
 }
 } // namespace ExternalProcessing
 } // namespace HttpFilters
diff --git a/test/extensions/filters/http/ext_proc/utils.h b/test/extensions/filters/http/ext_proc/utils.h
index c3832f7348f01..a0bf78e78b6ed 100644
--- a/test/extensions/filters/http/ext_proc/utils.h
+++ b/test/extensions/filters/http/ext_proc/utils.h
@@ -50,6 +50,25 @@ MATCHER_P2(SingleHeaderValueIs, key, value,
   return hdr[0]->value() == value;
 }
 
+template <typename... Args>
+inline void verifyMultipleHeaderValues(const Envoy::Http::HeaderMap& headers,
+                                       Envoy::Http::LowerCaseString const& key, Args... values) {
+  static constexpr size_t num_values = sizeof...(Args);
+  static_assert(num_values > 0);
+  const auto hdr = headers.get(key);
+  EXPECT_EQ(hdr.size(), num_values);
+  if (hdr.size() != num_values) {
+    return;
+  }
+  std::vector<absl::string_view> header_values;
+  for (size_t idx = 0; idx < num_values; ++idx) {
+    header_values.push_back(hdr[idx]->value().getStringView());
+  }
+  EXPECT_THAT(header_values, testing::UnorderedElementsAreArray({
+                                 values...,
+                             }));
+}
+
 MATCHER_P2(SingleProtoHeaderValueIs, key, value,
            absl::StrFormat("Header \"%s\" equals \"%s\"", key, value)) {
   for (const auto& hdr : arg.headers()) {
@@ -65,23 +84,22 @@ envoy::config::core::v3::HeaderValue makeHeaderValue(const std::string& key,
 
 class TestOnProcessingResponse : public OnProcessingResponse {
 public:
-  void
-  afterProcessingRequestHeaders(const envoy::service::ext_proc::v3::ProcessingResponse& response,
-                                absl::Status processing_status,
-                                Envoy::StreamInfo::StreamInfo&) override;
+  void afterProcessingRequestHeaders(const envoy::service::ext_proc::v3::HeadersResponse& response,
+                                     absl::Status processing_status,
+                                     Envoy::StreamInfo::StreamInfo&) override;
 
-  void afterProcessingResponseHeaders(const envoy::service::ext_proc::v3::ProcessingResponse&,
+  void afterProcessingResponseHeaders(const envoy::service::ext_proc::v3::HeadersResponse&,
                                       absl::Status, Envoy::StreamInfo::StreamInfo&) override;
-  void afterProcessingRequestBody(const envoy::service::ext_proc::v3::ProcessingResponse&,
-                                  absl::Status, Envoy::StreamInfo::StreamInfo&) override;
-  void afterProcessingResponseBody(const envoy::service::ext_proc::v3::ProcessingResponse&,
-                                   absl::Status, Envoy::StreamInfo::StreamInfo&) override;
-  void afterProcessingRequestTrailers(const envoy::service::ext_proc::v3::ProcessingResponse&,
+  void afterProcessingRequestBody(const envoy::service::ext_proc::v3::BodyResponse&, absl::Status,
+                                  Envoy::StreamInfo::StreamInfo&) override;
+  void afterProcessingResponseBody(const envoy::service::ext_proc::v3::BodyResponse&, absl::Status,
+                                   Envoy::StreamInfo::StreamInfo&) override;
+  void afterProcessingRequestTrailers(const envoy::service::ext_proc::v3::TrailersResponse&,
                                       absl::Status, Envoy::StreamInfo::StreamInfo&) override;
-  void afterProcessingResponseTrailers(const envoy::service::ext_proc::v3::ProcessingResponse&,
+  void afterProcessingResponseTrailers(const envoy::service::ext_proc::v3::TrailersResponse&,
                                        absl::Status, Envoy::StreamInfo::StreamInfo&) override;
   void
-  afterReceivingImmediateResponse(const envoy::service::ext_proc::v3::ProcessingResponse& response,
+  afterReceivingImmediateResponse(const envoy::service::ext_proc::v3::ImmediateResponse& response,
                                   absl::Status processing_status,
                                   Envoy::StreamInfo::StreamInfo&) override;
 


Reviews:
user: login: yanjunxiang-google
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T15:46:26Z
user: login: pradeepcrao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T17:07:42Z
user: login: abeyad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T18:59:23Z
user: login: yanjunxiang-google
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T21:17:51Z
user: login: pradeepcrao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T23:23:27Z
user: login: pradeepcrao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T23:24:44Z
user: login: abeyad
body: /lgtm api
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T03:23:00Z
user: login: adisuissa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T13:43:43Z
user: login: pradeepcrao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T14:20:56Z
user: login: adisuissa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T14:31:05Z
user: login: yanjunxiang-google
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T15:33:32Z
user: login: stevenzzzz
body: Thanks for working on this! 

A high level comment on the timing of triggering the extension. 

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T17:46:25Z
user: login: pradeepcrao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T18:02:52Z
user: login: pradeepcrao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T18:03:02Z
user: login: pradeepcrao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T18:07:31Z
user: login: botengyao
body: Thank you, lgtm, and CI needs to be fixed.

/wait
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T19:40:13Z
user: login: pradeepcrao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T19:45:10Z
user: login: stevenzzzz
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T13:40:32Z
user: login: botengyao
body: lgtm, thanks! This also needs an api stamp.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-19T13:58:00Z
user: login: adisuissa
body: /lgtm api
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T15:57:35Z

Review comments:
diff_hunk: @@ -1357,8 +1334,8 @@ void Filter::onReceiveMessage(std::unique_ptr<ProcessingResponse>&& r) {
           absl::FailedPreconditionError("unhandled immediate response due to config disabled it");
 
       if (on_processing_response_) {
-        on_processing_response_->afterReceivingImmediateResponse(*response, processing_status,
-                                                                 decoder_callbacks_->streamInfo());
+        on_processing_response_->afterReceivingImmediateResponse(
path: source/extensions/filters/http/ext_proc/ext_proc.cc
user: login: yanjunxiang-google
body: In this case, the immediate response handling is disabled by filter configuration, i.e, the immediate response will be taken as server malfunction, and the response will be rejected.  Is there any reason to store such kind of immediate response?
created_at: 2025-03-17T15:46:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1357,8 +1334,8 @@ void Filter::onReceiveMessage(std::unique_ptr<ProcessingResponse>&& r) {
           absl::FailedPreconditionError("unhandled immediate response due to config disabled it");
 
       if (on_processing_response_) {
-        on_processing_response_->afterReceivingImmediateResponse(*response, processing_status,
-                                                                 decoder_callbacks_->streamInfo());
+        on_processing_response_->afterReceivingImmediateResponse(
path: source/extensions/filters/http/ext_proc/ext_proc.cc
user: login: pradeepcrao
body: The config allows for saving of the response even if the `processing_status` is an [error](https://github.com/envoyproxy/envoy/blob/main/api/envoy/extensions/http/ext_proc/response_processors/save_processing_response/v3/save_processing_response.proto#L34). However, in this case, it probably makes sense to not save the immediate response. Updated.
created_at: 2025-03-17T17:07:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -333,7 +333,8 @@ message ExternalProcessor {
   // Decorator to introduce custom logic that runs after a message received from
   // the External Processor is processed.
   // [#extension-category: envoy.http.ext_proc.response_processors]
-  config.core.v3.TypedExtensionConfig on_processing_response = 23;
+  config.core.v3.TypedExtensionConfig on_processing_response = 23
+      [(xds.annotations.v3.field_status).work_in_progress = true];
path: api/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto
user: login: abeyad
body: adding `work_in_progress` denotes that this field could be susceptible to breaking changes - is it okay to add this for use-cases that are already relying on on_processing_response?
created_at: 2025-03-17T18:59:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -333,7 +333,8 @@ message ExternalProcessor {
   // Decorator to introduce custom logic that runs after a message received from
   // the External Processor is processed.
   // [#extension-category: envoy.http.ext_proc.response_processors]
-  config.core.v3.TypedExtensionConfig on_processing_response = 23;
+  config.core.v3.TypedExtensionConfig on_processing_response = 23
+      [(xds.annotations.v3.field_status).work_in_progress = true];
path: api/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto
user: login: yanjunxiang-google
body: I think this feature is WIP, and does not work now.  It's better to mark it as WIP so folks won't accidentally try it.  We can remove it later on.
created_at: 2025-03-17T21:17:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -333,7 +333,8 @@ message ExternalProcessor {
   // Decorator to introduce custom logic that runs after a message received from
   // the External Processor is processed.
   // [#extension-category: envoy.http.ext_proc.response_processors]
-  config.core.v3.TypedExtensionConfig on_processing_response = 23;
+  config.core.v3.TypedExtensionConfig on_processing_response = 23
+      [(xds.annotations.v3.field_status).work_in_progress = true];
path: api/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto
user: login: pradeepcrao
body: This was added in https://github.com/envoyproxy/envoy/pull/37916 less than a month ago. In hindsight, the annotation should have been added in that PR. I don't think anyone else is using this extension point.
created_at: 2025-03-17T23:23:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -333,7 +333,8 @@ message ExternalProcessor {
   // Decorator to introduce custom logic that runs after a message received from
   // the External Processor is processed.
   // [#extension-category: envoy.http.ext_proc.response_processors]
-  config.core.v3.TypedExtensionConfig on_processing_response = 23;
+  config.core.v3.TypedExtensionConfig on_processing_response = 23
+      [(xds.annotations.v3.field_status).work_in_progress = true];
path: api/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto
user: login: pradeepcrao
body: @adisuissa WDYT? I added this now based on @yanjunxiang-google's comment.
created_at: 2025-03-17T23:24:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -333,7 +333,8 @@ message ExternalProcessor {
   // Decorator to introduce custom logic that runs after a message received from
   // the External Processor is processed.
   // [#extension-category: envoy.http.ext_proc.response_processors]
-  config.core.v3.TypedExtensionConfig on_processing_response = 23;
+  config.core.v3.TypedExtensionConfig on_processing_response = 23
+      [(xds.annotations.v3.field_status).work_in_progress = true];
path: api/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto
user: login: adisuissa
body: If the intention is to clarify that this is still under test, I suggest adding a note (see [here](https://github.com/envoyproxy/envoy/blob/53941d96922ae2bee829863929fc127921e167b3/api/envoy/extensions/upstreams/http/v3/http_protocol_options.proto#L155-L156)).

created_at: 2025-03-18T13:43:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -333,7 +333,8 @@ message ExternalProcessor {
   // Decorator to introduce custom logic that runs after a message received from
   // the External Processor is processed.
   // [#extension-category: envoy.http.ext_proc.response_processors]
-  config.core.v3.TypedExtensionConfig on_processing_response = 23;
+  config.core.v3.TypedExtensionConfig on_processing_response = 23
+      [(xds.annotations.v3.field_status).work_in_progress = true];
path: api/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto
user: login: pradeepcrao
body: Added a note, PTAL
created_at: 2025-03-18T14:20:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -333,7 +333,8 @@ message ExternalProcessor {
   // Decorator to introduce custom logic that runs after a message received from
   // the External Processor is processed.
   // [#extension-category: envoy.http.ext_proc.response_processors]
-  config.core.v3.TypedExtensionConfig on_processing_response = 23;
+  config.core.v3.TypedExtensionConfig on_processing_response = 23
+      [(xds.annotations.v3.field_status).work_in_progress = true];
path: api/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto
user: login: adisuissa
body: I guess the WIP is not needed, but I don't think it matters much.
/lgtm api
created_at: 2025-03-18T14:31:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -65,10 +64,16 @@ class SaveProcessingResponse
     const bool save_on_error = false;
   };
 
-  void addToFilterState(const SaveOptions& save_options,
-                        const envoy::service::ext_proc::v3::ProcessingResponse& processing_response,
+  void addToFilterState(envoy::service::ext_proc::v3::ProcessingResponse processing_response,
                         absl::Status status, Envoy::StreamInfo::StreamInfo& stream_info);
 
+  bool shouldSaveResponse(SaveOptions save_options, absl::Status status) {
path: source/extensions/http/ext_proc/response_processors/save_processing_response/save_processing_response.h
user: login: stevenzzzz
body: bikeshedding: can we document this behavior as a class comment? 
created_at: 2025-03-18T17:22:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1710,6 +1682,49 @@ std::unique_ptr<OnProcessingResponse> FilterConfig::createOnProcessingResponse()
   return on_processing_response_factory_cb_();
 }
 
+void Filter::onProcessHeadersResponse(const envoy::service::ext_proc::v3::HeadersResponse& response,
+                                      absl::Status status, TrafficDirection traffic_direction) {
+  if (on_processing_response_) {
+    ASSERT(traffic_direction != TrafficDirection::UNSPECIFIED);
+    if (traffic_direction == TrafficDirection::INBOUND) {
+      on_processing_response_->afterProcessingRequestHeaders(response, status,
+                                                             decoder_callbacks_->streamInfo());
+    } else {
+      on_processing_response_->afterProcessingResponseHeaders(response, status,
+                                                              decoder_callbacks_->streamInfo());
path: source/extensions/filters/http/ext_proc/ext_proc.cc
user: login: stevenzzzz
body: I undertand it's the same stream_callbacks_ underneath, but might worthy changing this to encode_callbacks_ so it aligns with the OUTBOUND direction. 
here and everywhere. 
created_at: 2025-03-18T17:39:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -332,8 +332,13 @@ message ExternalProcessor {
 
   // Decorator to introduce custom logic that runs after a message received from
path: api/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto
user: login: stevenzzzz
body: Let's be clear here about the stage.  
I think we are implementing it as "run after the response is received, consumed and right before continue the filter chain". 

OTOH, if we change the contract here to that we run the "extension" right after the response is received, but before filter consuming it, would that simplify the code change needed quite a bit? 

created_at: 2025-03-18T17:42:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1710,6 +1682,49 @@ std::unique_ptr<OnProcessingResponse> FilterConfig::createOnProcessingResponse()
   return on_processing_response_factory_cb_();
 }
 
+void Filter::onProcessHeadersResponse(const envoy::service::ext_proc::v3::HeadersResponse& response,
+                                      absl::Status status, TrafficDirection traffic_direction) {
+  if (on_processing_response_) {
+    ASSERT(traffic_direction != TrafficDirection::UNSPECIFIED);
+    if (traffic_direction == TrafficDirection::INBOUND) {
+      on_processing_response_->afterProcessingRequestHeaders(response, status,
+                                                             decoder_callbacks_->streamInfo());
+    } else {
+      on_processing_response_->afterProcessingResponseHeaders(response, status,
+                                                              decoder_callbacks_->streamInfo());
path: source/extensions/filters/http/ext_proc/ext_proc.cc
user: login: pradeepcrao
body: Done
created_at: 2025-03-18T18:02:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -65,10 +64,16 @@ class SaveProcessingResponse
     const bool save_on_error = false;
   };
 
-  void addToFilterState(const SaveOptions& save_options,
-                        const envoy::service::ext_proc::v3::ProcessingResponse& processing_response,
+  void addToFilterState(envoy::service::ext_proc::v3::ProcessingResponse processing_response,
                         absl::Status status, Envoy::StreamInfo::StreamInfo& stream_info);
 
+  bool shouldSaveResponse(SaveOptions save_options, absl::Status status) {
path: source/extensions/http/ext_proc/response_processors/save_processing_response/save_processing_response.h
user: login: pradeepcrao
body: Done.
created_at: 2025-03-18T18:03:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -332,8 +332,13 @@ message ExternalProcessor {
 
   // Decorator to introduce custom logic that runs after a message received from
path: api/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto
user: login: pradeepcrao
body: In the general case, it's better to run the decorator method after the processing is done, so we can check the processing status to avoid wasted work. It might not always be possible for the extension to deduce the processing status.
created_at: 2025-03-18T18:07:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -210,20 +212,24 @@ absl::Status ProcessorState::handleHeadersResponse(const HeadersResponse& respon
           // Do not continue filter chain here so the pending body response have chance to be
           // served.
           sendBufferedDataInStreamedMode(trailers_ == nullptr);
+          filter_.onProcessHeadersResponse(response, absl::OkStatus(), trafficDirection());
           return absl::OkStatus();
         }
       } else if (body_mode_ == ProcessingMode::BUFFERED) {
         // Here, we're not ready to continue processing because then
         // we won't be able to modify the headers any more, so do nothing and
         // let the doData callback handle body chunks until the end is reached.
         clearWatermark();
+        filter_.onProcessHeadersResponse(response, absl::OkStatus(), trafficDirection());
         return absl::OkStatus();
       } else if (body_mode_ == ProcessingMode::STREAMED) {
         sendBufferedDataInStreamedMode(false);
+        filter_.onProcessHeadersResponse(response, absl::OkStatus(), trafficDirection());
         continueIfNecessary();
         return absl::OkStatus();
       } else if (body_mode_ == ProcessingMode::FULL_DUPLEX_STREAMED) {
         // There is no buffered data in this mode.
+        filter_.onProcessHeadersResponse(response, absl::OkStatus(), trafficDirection());
path: source/extensions/filters/http/ext_proc/processor_state.cc
user: login: botengyao
body: This indeed needs a refactor to avoid multiple place calls, but will not block this PR.
created_at: 2025-03-18T19:29:17Z
author_association: MEMBER
diff_hunk: @@ -332,8 +332,13 @@ message ExternalProcessor {
 
   // Decorator to introduce custom logic that runs after a message received from
path: api/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto
user: login: botengyao
body: +1 to clearly document where is the response is passed to the extension, and after the processing makes sense to me.
created_at: 2025-03-18T19:37:04Z
author_association: MEMBER
diff_hunk: @@ -332,8 +332,13 @@ message ExternalProcessor {
 
   // Decorator to introduce custom logic that runs after a message received from
path: api/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto
user: login: pradeepcrao
body: Modified comment to state that the decorator is invoked before continuing filter chain iteration.
created_at: 2025-03-18T19:45:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -210,20 +212,24 @@ absl::Status ProcessorState::handleHeadersResponse(const HeadersResponse& respon
           // Do not continue filter chain here so the pending body response have chance to be
           // served.
           sendBufferedDataInStreamedMode(trailers_ == nullptr);
+          filter_.onProcessHeadersResponse(response, absl::OkStatus(), trafficDirection());
           return absl::OkStatus();
         }
       } else if (body_mode_ == ProcessingMode::BUFFERED) {
         // Here, we're not ready to continue processing because then
         // we won't be able to modify the headers any more, so do nothing and
         // let the doData callback handle body chunks until the end is reached.
         clearWatermark();
+        filter_.onProcessHeadersResponse(response, absl::OkStatus(), trafficDirection());
         return absl::OkStatus();
       } else if (body_mode_ == ProcessingMode::STREAMED) {
         sendBufferedDataInStreamedMode(false);
+        filter_.onProcessHeadersResponse(response, absl::OkStatus(), trafficDirection());
         continueIfNecessary();
         return absl::OkStatus();
       } else if (body_mode_ == ProcessingMode::FULL_DUPLEX_STREAMED) {
         // There is no buffered data in this mode.
+        filter_.onProcessHeadersResponse(response, absl::OkStatus(), trafficDirection());
path: source/extensions/filters/http/ext_proc/processor_state.cc
user: login: botengyao
body: A preexisting issue #37047 is here, would like see this is addressed at some point.
created_at: 2025-03-19T13:54:50Z
author_association: MEMBER

PR comments:
user: login: pradeepcrao
created_at: 2025-03-14T16:04:24Z
body: /assign @yanjunxiang-google 
author_association: CONTRIBUTOR
user: login: yanjunxiang-google
created_at: 2025-03-17T15:52:10Z
body: Recommend add (xds.annotations.v3.field_status).work_in_progress = true for on_processing_response configuration for now:

https://github.com/envoyproxy/envoy/blob/ecf8ac865a8bc079f8b050951b7e46500a787aa6/api/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto#L336
author_association: CONTRIBUTOR
user: login: yanjunxiang-google
created_at: 2025-03-17T15:52:32Z
body: /assign @stevenzzzz 
author_association: CONTRIBUTOR
user: login: pradeepcrao
created_at: 2025-03-17T17:12:24Z
body: > Recommend add (xds.annotations.v3.field_status).work_in_progress = true for on_processing_response configuration for now:
> 
> https://github.com/envoyproxy/envoy/blob/ecf8ac865a8bc079f8b050951b7e46500a787aa6/api/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto#L336

done.
author_association: CONTRIBUTOR
user: login: repokitteh-read-only[bot]
created_at: 2025-03-17T17:13:31Z
body: CC @envoyproxy/api-shepherds: Your approval is needed for changes made to `(api/envoy/|docs/root/api-docs/)`.
envoyproxy/api-shepherds assignee is @abeyad
CC @envoyproxy/api-watchers: FYI only for changes made to `(api/envoy/|docs/root/api-docs/)`.


<details>
	<summary>:cat:</summary>

Caused by: https://github.com/envoyproxy/envoy/pull/38750 was synchronize by pradeepcrao.

see: [more](https://github.com/envoyproxy/envoy/pull/38750), [trace](https://prod.repokitteh.app/traces/ui/envoyproxy/envoy/21ba1ee0-0353-11f0-8870-bf4f6993241d).
</details>
author_association: NONE
user: login: abeyad
created_at: 2025-03-17T19:00:11Z
body: Just FYI, there are also format issues causing CI to fail: https://github.com/envoyproxy/envoy/actions/runs/13905773038/job/38908423949
author_association: CONTRIBUTOR
user: login: pradeepcrao
created_at: 2025-03-17T23:19:13Z
body: > Just FYI, there are also format issues causing CI to fail: https://github.com/envoyproxy/envoy/actions/runs/13905773038/job/38908423949

Thanks, pushed a fix.
author_association: CONTRIBUTOR
user: login: pradeepcrao
created_at: 2025-03-17T23:24:10Z
body: /assign @adisuissa 
author_association: CONTRIBUTOR
user: login: yanjunxiang-google
created_at: 2025-03-18T15:33:05Z
body: LGTM
author_association: CONTRIBUTOR
user: login: stevenzzzz
created_at: 2025-03-19T13:36:34Z
body: LGTM. 

author_association: CONTRIBUTOR