11use crate :: config:: config:: Config ;
2- use crate :: models:: envelope:: envelope:: { RequestEnvelope , ResponseEnvelope } ;
2+ use crate :: models:: envelope:: envelope:: { RequestEnvelope , ResponseEnvelope , TargetDetails } ;
33use crate :: models:: middleware:: chain:: MiddlewareChain ;
44use crate :: models:: middleware:: middleware:: build_middleware_instances_for_pipeline;
55use crate :: models:: pipelines:: config:: Pipeline ;
@@ -51,11 +51,12 @@ impl PipelineExecutor {
5151 ///
5252 /// # Flow
5353 /// 1. Endpoint service preprocessing
54- /// 2. Incoming middleware chain (left)
55- /// 3. Backend invocation
56- /// 4. Outgoing middleware chain (right)
57- /// 5. Endpoint service post-processing (protocol-aware)
58- /// 6. Return ResponseEnvelope
54+ /// 2. Resolve target_details from backend config
55+ /// 3. Incoming middleware chain (left)
56+ /// 4. Backend invocation
57+ /// 5. Outgoing middleware chain (right)
58+ /// 6. Endpoint service post-processing (protocol-aware)
59+ /// 7. Return ResponseEnvelope
5960 ///
6061 /// # Arguments
6162 /// * `envelope` - The request envelope to process
@@ -80,22 +81,88 @@ impl PipelineExecutor {
8081 // 1. Endpoint service preprocessing
8182 let envelope = Self :: process_endpoint_incoming ( envelope, pipeline, config) . await ?;
8283
83- // 2. Incoming middleware chain (left)
84+ // 2. Resolve target_details from backend config (so middleware has access)
85+ let envelope = Self :: resolve_target_details ( envelope, pipeline, config) . await ?;
86+
87+ // 3. Incoming middleware chain (left)
8488 let envelope = Self :: process_incoming_middleware ( envelope, pipeline, config) . await ?;
8589
86- // 3 . Backend invocation
90+ // 4 . Backend invocation
8791 let response = Self :: process_backends ( envelope, pipeline, config) . await ?;
8892
89- // 4 . Outgoing middleware chain (right)
93+ // 5 . Outgoing middleware chain (right)
9094 let mut response = Self :: process_outgoing_middleware ( response, pipeline, config) . await ?;
9195
92- // 5 . Endpoint service post-processing (protocol-aware)
96+ // 6 . Endpoint service post-processing (protocol-aware)
9397 Self :: process_endpoint_outgoing ( & mut response, pipeline, config, ctx) . await ?;
9498
9599 tracing:: info!( "Pipeline execution completed successfully" ) ;
96100 Ok ( response)
97101 }
98102
103+ /// Resolve target_details from backend configuration
104+ ///
105+ /// This populates target_details early so middleware has access to target info.
106+ /// The backend will use these details if present, or create its own if not.
107+ async fn resolve_target_details (
108+ mut envelope : RequestEnvelope < Vec < u8 > > ,
109+ pipeline : & Pipeline ,
110+ config : & Config ,
111+ ) -> Result < RequestEnvelope < Vec < u8 > > , PipelineError > {
112+ // Skip if target_details already set (e.g., by endpoint)
113+ if envelope. target_details . is_some ( ) {
114+ return Ok ( envelope) ;
115+ }
116+
117+ // Get first backend from pipeline
118+ let backend_name = match pipeline. backends . first ( ) {
119+ Some ( name) => name,
120+ None => return Ok ( envelope) , // No backends, leave target_details as None
121+ } ;
122+
123+ let backend = match config. backends . get ( backend_name) {
124+ Some ( b) => b,
125+ None => return Ok ( envelope) , // Backend not found, leave as None
126+ } ;
127+
128+ // Build base_url from backend's resolved connection
129+ let base_url = backend
130+ . connection
131+ . as_ref ( )
132+ . map ( |c| c. to_base_url ( ) )
133+ . or_else ( || {
134+ backend
135+ . options
136+ . as_ref ( )
137+ . and_then ( |opts| opts. get ( "base_url" ) )
138+ . and_then ( |v| v. as_str ( ) )
139+ . map ( |s| s. to_string ( ) )
140+ } )
141+ . unwrap_or_default ( ) ;
142+
143+ if base_url. is_empty ( ) {
144+ tracing:: debug!( "No base_url available for target_details resolution" ) ;
145+ return Ok ( envelope) ;
146+ }
147+
148+ // Extract path without query string
149+ let path = crate :: models:: services:: path_utils:: extract_path ( & envelope) ;
150+
151+ // Create TargetDetails
152+ let mut target =
153+ TargetDetails :: from_request_details ( base_url, & envelope. request_details ) ;
154+ target. uri = path;
155+
156+ tracing:: debug!(
157+ "Resolved target_details: {} {}" ,
158+ target. method,
159+ target. full_url( ) . unwrap_or_else( |_| "<invalid>" . to_string( ) )
160+ ) ;
161+
162+ envelope. target_details = Some ( target) ;
163+ Ok ( envelope)
164+ }
165+
99166 /// Process endpoint incoming request
100167 async fn process_endpoint_incoming (
101168 envelope : RequestEnvelope < Vec < u8 > > ,
@@ -405,6 +472,10 @@ impl PipelineExecutor {
405472#[ cfg( test) ]
406473mod tests {
407474 use super :: * ;
475+ use crate :: models:: backends:: backends:: Backend ;
476+ use crate :: models:: connection:: ConnectionConfig ;
477+ use crate :: models:: envelope:: envelope:: RequestEnvelope ;
478+ use crate :: models:: pipelines:: config:: PipelineMiddleware ;
408479
409480 #[ test]
410481 fn test_pipeline_error_display ( ) {
@@ -420,4 +491,137 @@ mod tests {
420491 let err: PipelineError = "test" . into ( ) ;
421492 assert_eq ! ( err. to_string( ) , "Service error: test" ) ;
422493 }
494+
495+ #[ tokio:: test]
496+ async fn test_resolve_target_details_from_backend_connection ( ) {
497+ let mut config = Config :: default ( ) ;
498+
499+ // Add a backend with connection config
500+ config. backends . insert (
501+ "test_backend" . to_string ( ) ,
502+ Backend {
503+ service : "http" . to_string ( ) ,
504+ target_ref : None ,
505+ connection : Some ( ConnectionConfig {
506+ host : "api.example.com" . to_string ( ) ,
507+ port : Some ( 443 ) ,
508+ protocol : Some ( "https" . to_string ( ) ) ,
509+ base_path : Some ( "/v1" . to_string ( ) ) ,
510+ } ) ,
511+ authentication : None ,
512+ timeout_secs : None ,
513+ max_retries : None ,
514+ options : None ,
515+ } ,
516+ ) ;
517+
518+ let pipeline = Pipeline {
519+ description : "test pipeline" . to_string ( ) ,
520+ networks : vec ! [ ] ,
521+ endpoints : vec ! [ ] ,
522+ backends : vec ! [ "test_backend" . to_string( ) ] ,
523+ middleware : PipelineMiddleware :: default ( ) ,
524+ } ;
525+
526+ // Create envelope with request details
527+ let envelope = RequestEnvelope :: builder ( )
528+ . method ( "GET" )
529+ . uri ( "/users?id=123" )
530+ . query_params ( HashMap :: from ( [ (
531+ "id" . to_string ( ) ,
532+ vec ! [ "123" . to_string( ) ] ,
533+ ) ] ) )
534+ . original_data ( vec ! [ ] )
535+ . build ( )
536+ . unwrap ( ) ;
537+
538+ // Should have no target_details initially
539+ assert ! ( envelope. target_details. is_none( ) ) ;
540+
541+ // Resolve target details
542+ let result = PipelineExecutor :: resolve_target_details ( envelope, & pipeline, & config) . await ;
543+ assert ! ( result. is_ok( ) ) ;
544+
545+ let envelope = result. unwrap ( ) ;
546+ assert ! ( envelope. target_details. is_some( ) ) ;
547+
548+ let target = envelope. target_details . unwrap ( ) ;
549+ assert_eq ! ( target. base_url, "https://api.example.com:443/v1" ) ;
550+ assert_eq ! ( target. method, "GET" ) ;
551+ assert_eq ! ( target. uri, "/users" ) ; // Path without query string
552+ assert_eq ! (
553+ target. query_params. get( "id" ) ,
554+ Some ( & vec![ "123" . to_string( ) ] )
555+ ) ;
556+ }
557+
558+ #[ tokio:: test]
559+ async fn test_resolve_target_details_no_backend ( ) {
560+ let config = Config :: default ( ) ;
561+
562+ let pipeline = Pipeline {
563+ description : "test pipeline" . to_string ( ) ,
564+ networks : vec ! [ ] ,
565+ endpoints : vec ! [ ] ,
566+ backends : vec ! [ ] , // No backends
567+ middleware : PipelineMiddleware :: default ( ) ,
568+ } ;
569+
570+ let envelope = RequestEnvelope :: builder ( )
571+ . method ( "GET" )
572+ . uri ( "/test" )
573+ . original_data ( vec ! [ ] )
574+ . build ( )
575+ . unwrap ( ) ;
576+
577+ let result = PipelineExecutor :: resolve_target_details ( envelope, & pipeline, & config) . await ;
578+ assert ! ( result. is_ok( ) ) ;
579+
580+ // Should return envelope unchanged (no target_details)
581+ let envelope = result. unwrap ( ) ;
582+ assert ! ( envelope. target_details. is_none( ) ) ;
583+ }
584+
585+ #[ tokio:: test]
586+ async fn test_resolve_target_details_preserves_existing ( ) {
587+ let config = Config :: default ( ) ;
588+
589+ let pipeline = Pipeline {
590+ description : "test pipeline" . to_string ( ) ,
591+ networks : vec ! [ ] ,
592+ endpoints : vec ! [ ] ,
593+ backends : vec ! [ ] ,
594+ middleware : PipelineMiddleware :: default ( ) ,
595+ } ;
596+
597+ // Create envelope with existing target_details
598+ let existing_target = TargetDetails {
599+ base_url : "https://existing.com" . to_string ( ) ,
600+ method : "POST" . to_string ( ) ,
601+ uri : "/existing" . to_string ( ) ,
602+ headers : HashMap :: new ( ) ,
603+ cookies : HashMap :: new ( ) ,
604+ query_params : HashMap :: new ( ) ,
605+ metadata : HashMap :: new ( ) ,
606+ } ;
607+
608+ let envelope = RequestEnvelope :: builder ( )
609+ . method ( "GET" )
610+ . uri ( "/test" )
611+ . target_details ( Some ( existing_target. clone ( ) ) )
612+ . original_data ( vec ! [ ] )
613+ . build ( )
614+ . unwrap ( ) ;
615+
616+ let result = PipelineExecutor :: resolve_target_details ( envelope, & pipeline, & config) . await ;
617+ assert ! ( result. is_ok( ) ) ;
618+
619+ let envelope = result. unwrap ( ) ;
620+ let target = envelope. target_details . unwrap ( ) ;
621+
622+ // Should preserve existing target_details
623+ assert_eq ! ( target. base_url, "https://existing.com" ) ;
624+ assert_eq ! ( target. method, "POST" ) ;
625+ assert_eq ! ( target. uri, "/existing" ) ;
626+ }
423627}
0 commit comments