@@ -18,6 +18,7 @@ import (
1818 "github.com/stretchr/testify/require"
1919
2020 "github.com/envoyproxy/ai-gateway/internal/apischema/openai"
21+ "github.com/envoyproxy/ai-gateway/internal/extproc/backendauth"
2122 "github.com/envoyproxy/ai-gateway/internal/extproc/translator"
2223 "github.com/envoyproxy/ai-gateway/internal/filterapi"
2324 "github.com/envoyproxy/ai-gateway/internal/internalapi"
@@ -316,3 +317,145 @@ func Test_responsesProcessorUpstreamFilter_SetBackend(t *testing.T) {
316317 require .True (t , p2 .stream )
317318 require .NotNil (t , p2 .translator )
318319}
320+
321+ func Test_responsesProcessorUpstreamFilter_ProcessRequestHeaders (t * testing.T ) {
322+ t .Run ("ok sets header/body mutation and metrics" , func (t * testing.T ) {
323+ // prepare translator to expect raw body and return header/body mutation
324+ mt := & mockResponsesTranslator {t : t , expBody : []byte (`{"model":"m1"}` )}
325+ expHeadMut := & extprocv3.HeaderMutation {
326+ SetHeaders : []* corev3.HeaderValueOption {{Header : & corev3.HeaderValue {Key : "x-test" , Value : "v" }}},
327+ }
328+ expBodyMut := & extprocv3.BodyMutation {}
329+ mt .retHeaderMutation = expHeadMut
330+ mt .retBodyMutation = expBodyMut
331+
332+ mm := & mockResponsesMetrics {}
333+
334+ // build upstream filter with original request body present (as router would set)
335+ headers := map [string ]string {":path" : "/foo" }
336+ p := & responsesProcessorUpstreamFilter {translator : mt , metrics : mm , requestHeaders : headers , config : & processorConfig {}}
337+ p .originalRequestBodyRaw = []byte (`{"model":"m1"}` )
338+ p .originalRequestBody = & openai.ResponseRequest {Model : "m1" }
339+
340+ res , err := p .ProcessRequestHeaders (t .Context (), nil )
341+ require .NoError (t , err )
342+ require .NotNil (t , res )
343+ rh := res .Response .(* extprocv3.ProcessingResponse_RequestHeaders ).RequestHeaders .Response
344+ require .Equal (t , expHeadMut , rh .HeaderMutation )
345+ require .Equal (t , expBodyMut , rh .BodyMutation )
346+ // metrics should have been started and models set
347+ require .True (t , mm .started )
348+ require .Equal (t , internalapi .OriginalModel ("m1" ), mm .originalModel )
349+ require .Equal (t , internalapi .RequestModel ("m1" ), mm .requestModel )
350+ })
351+
352+ t .Run ("translator error records failure" , func (t * testing.T ) {
353+ mt := & mockResponsesTranslator {t : t }
354+ mt .retErr = errors .New ("translate fail" )
355+ mm := & mockResponsesMetrics {}
356+ // Ensure logger is non-nil so deferred error logging doesn't panic
357+ p := & responsesProcessorUpstreamFilter {translator : mt , metrics : mm , requestHeaders : map [string ]string {}, config : & processorConfig {}, logger : slog .Default ()}
358+ p .originalRequestBodyRaw = []byte (`{"model":"m1"}` )
359+ p .originalRequestBody = & openai.ResponseRequest {Model : "m1" }
360+
361+ _ , err := p .ProcessRequestHeaders (t .Context (), nil )
362+ require .Error (t , err )
363+ mm .RequireRequestFailure (t )
364+ })
365+ }
366+
367+ func Test_responsesProcessorUpstreamFilter_ProcessRequestBody_panics (t * testing.T ) {
368+ p := & responsesProcessorUpstreamFilter {}
369+ require .Panics (t , func () { p .ProcessRequestBody (t .Context (), & extprocv3.HttpBody {}) })
370+ }
371+
372+ // processorFunc is a lightweight test helper implementing Processor via function fields.
373+ type processorFunc struct {
374+ processRequestHeadersFunc func (context.Context , * corev3.HeaderMap ) (* extprocv3.ProcessingResponse , error )
375+ processRequestBodyFunc func (context.Context , * extprocv3.HttpBody ) (* extprocv3.ProcessingResponse , error )
376+ processResponseHeadersFunc func (context.Context , * corev3.HeaderMap ) (* extprocv3.ProcessingResponse , error )
377+ processResponseBodyFunc func (context.Context , * extprocv3.HttpBody ) (* extprocv3.ProcessingResponse , error )
378+ setBackendFunc func (context.Context , * filterapi.Backend , backendauth.Handler , Processor ) error
379+ }
380+
381+ func (p processorFunc ) ProcessRequestHeaders (ctx context.Context , h * corev3.HeaderMap ) (* extprocv3.ProcessingResponse , error ) {
382+ if p .processRequestHeadersFunc != nil {
383+ return p .processRequestHeadersFunc (ctx , h )
384+ }
385+ return & extprocv3.ProcessingResponse {Response : & extprocv3.ProcessingResponse_RequestHeaders {}}, nil
386+ }
387+ func (p processorFunc ) ProcessRequestBody (ctx context.Context , b * extprocv3.HttpBody ) (* extprocv3.ProcessingResponse , error ) {
388+ if p .processRequestBodyFunc != nil {
389+ return p .processRequestBodyFunc (ctx , b )
390+ }
391+ return & extprocv3.ProcessingResponse {Response : & extprocv3.ProcessingResponse_RequestBody {}}, nil
392+ }
393+ func (p processorFunc ) ProcessResponseHeaders (ctx context.Context , h * corev3.HeaderMap ) (* extprocv3.ProcessingResponse , error ) {
394+ if p .processResponseHeadersFunc != nil {
395+ return p .processResponseHeadersFunc (ctx , h )
396+ }
397+ return & extprocv3.ProcessingResponse {Response : & extprocv3.ProcessingResponse_ResponseHeaders {}}, nil
398+ }
399+ func (p processorFunc ) ProcessResponseBody (ctx context.Context , b * extprocv3.HttpBody ) (* extprocv3.ProcessingResponse , error ) {
400+ if p .processResponseBodyFunc != nil {
401+ return p .processResponseBodyFunc (ctx , b )
402+ }
403+ return & extprocv3.ProcessingResponse {Response : & extprocv3.ProcessingResponse_ResponseBody {}}, nil
404+ }
405+ func (p processorFunc ) SetBackend (ctx context.Context , be * filterapi.Backend , h backendauth.Handler , rp Processor ) error {
406+ if p .setBackendFunc != nil {
407+ return p .setBackendFunc (ctx , be , h , rp )
408+ }
409+ return nil
410+ }
411+
412+ func Test_responsesProcessorRouterFilter_ResponseDelegation (t * testing.T ) {
413+ t .Run ("passThrough when upstreamFilter nil" , func (t * testing.T ) {
414+ // router filter with nil upstreamFilter should delegate to passThroughProcessor
415+ r := & responsesProcessorRouterFilter {}
416+
417+ // ProcessResponseHeaders should return a ResponseHeaders-type ProcessingResponse
418+ hdrResp , err := r .ProcessResponseHeaders (t .Context (), nil )
419+ require .NoError (t , err )
420+ require .NotNil (t , hdrResp )
421+ _ , ok := hdrResp .Response .(* extprocv3.ProcessingResponse_ResponseHeaders )
422+ require .True (t , ok )
423+
424+ // ProcessResponseBody should return a ResponseBody-type ProcessingResponse
425+ bodyResp , err := r .ProcessResponseBody (t .Context (), & extprocv3.HttpBody {})
426+ require .NoError (t , err )
427+ require .NotNil (t , bodyResp )
428+ _ , ok = bodyResp .Response .(* extprocv3.ProcessingResponse_ResponseBody )
429+ require .True (t , ok )
430+ })
431+
432+ t .Run ("delegates to upstreamFilter when set" , func (t * testing.T ) {
433+ // Create a spy upstream filter that records calls and returns distinct responses
434+ called := struct { hdr , body bool }{}
435+ // Replace methods via function values by wrapping an implementation of Processor
436+ var upstream Processor = processorFunc {
437+ processResponseHeadersFunc : func (context.Context , * corev3.HeaderMap ) (* extprocv3.ProcessingResponse , error ) {
438+ called .hdr = true
439+ return & extprocv3.ProcessingResponse {Response : & extprocv3.ProcessingResponse_ResponseHeaders {}}, nil
440+ },
441+ processResponseBodyFunc : func (context.Context , * extprocv3.HttpBody ) (* extprocv3.ProcessingResponse , error ) {
442+ called .body = true
443+ return & extprocv3.ProcessingResponse {Response : & extprocv3.ProcessingResponse_ResponseBody {}}, nil
444+ },
445+ }
446+
447+ r := & responsesProcessorRouterFilter {upstreamFilter : upstream }
448+
449+ hdrResp , err := r .ProcessResponseHeaders (t .Context (), nil )
450+ require .NoError (t , err )
451+ require .True (t , called .hdr )
452+ _ , ok := hdrResp .Response .(* extprocv3.ProcessingResponse_ResponseHeaders )
453+ require .True (t , ok )
454+
455+ bodyResp , err := r .ProcessResponseBody (t .Context (), & extprocv3.HttpBody {})
456+ require .NoError (t , err )
457+ require .True (t , called .body )
458+ _ , ok = bodyResp .Response .(* extprocv3.ProcessingResponse_ResponseBody )
459+ require .True (t , ok )
460+ })
461+ }
0 commit comments