1+ use http_body_util:: BodyExt ;
12use spin_world:: {
23 async_trait,
34 v1:: {
@@ -7,6 +8,8 @@ use spin_world::{
78} ;
89use tracing:: { field:: Empty , instrument, Level , Span } ;
910
11+ use crate :: intercept:: InterceptOutcome ;
12+
1013#[ async_trait]
1114impl spin_http:: Host for crate :: InstanceState {
1215 #[ instrument( name = "spin_outbound_http.send_request" , skip_all, err( level = Level :: INFO ) ,
@@ -19,7 +22,11 @@ impl spin_http::Host for crate::InstanceState {
1922 let uri = req. uri ;
2023 tracing:: trace!( "Sending outbound HTTP to {uri:?}" ) ;
2124
22- let abs_url = if !uri. starts_with ( '/' ) {
25+ if !req. params . is_empty ( ) {
26+ tracing:: warn!( "HTTP params field is deprecated" ) ;
27+ }
28+
29+ let req_url = if !uri. starts_with ( '/' ) {
2330 // Absolute URI
2431 let is_allowed = self
2532 . allowed_hosts
@@ -29,7 +36,7 @@ impl spin_http::Host for crate::InstanceState {
2936 if !is_allowed {
3037 return Err ( HttpError :: DestinationNotAllowed ) ;
3138 }
32- uri
39+ uri. parse ( ) . map_err ( |_| HttpError :: InvalidUrl ) ?
3340 } else {
3441 // Relative URI ("self" request)
3542 let is_allowed = self
@@ -47,36 +54,51 @@ impl spin_http::Host for crate::InstanceState {
4754 ) ;
4855 return Err ( HttpError :: InvalidUrl ) ;
4956 } ;
50- format ! ( "{origin}{uri}" )
57+ let path_and_query = uri. parse ( ) . map_err ( |_| HttpError :: InvalidUrl ) ?;
58+ origin. clone ( ) . into_uri ( Some ( path_and_query) )
5159 } ;
52- let req_url = reqwest:: Url :: parse ( & abs_url) . map_err ( |_| HttpError :: InvalidUrl ) ?;
53-
54- if !req. params . is_empty ( ) {
55- tracing:: warn!( "HTTP params field is deprecated" ) ;
56- }
57-
58- // Allow reuse of Client's internal connection pool for multiple requests
59- // in a single component execution
60- let client = self . spin_http_client . get_or_insert_with ( Default :: default) ;
6160
61+ // Build an http::Request for OutboundHttpInterceptor
6262 let mut req = {
63- let mut builder = client. request ( reqwest_method ( req. method ) , req_url) ;
63+ let mut builder = http:: Request :: builder ( )
64+ . method ( hyper_method ( req. method ) )
65+ . uri ( & req_url) ;
6466 for ( key, val) in req. headers {
6567 builder = builder. header ( key, val) ;
6668 }
67- builder
68- . body ( req. body . unwrap_or_default ( ) )
69- . build ( )
70- . map_err ( |err| {
71- tracing:: error!( "Error building outbound request: {err}" ) ;
72- HttpError :: RuntimeError
73- } ) ?
74- } ;
69+ builder. body ( req. body . unwrap_or_default ( ) )
70+ }
71+ . map_err ( |err| {
72+ tracing:: error!( "Error building outbound request: {err}" ) ;
73+ HttpError :: RuntimeError
74+ } ) ?;
75+
7576 spin_telemetry:: inject_trace_context ( req. headers_mut ( ) ) ;
7677
78+ if let Some ( interceptor) = & self . request_interceptor {
79+ let intercepted_request = std:: mem:: take ( & mut req) . into ( ) ;
80+ match interceptor. intercept ( intercepted_request) . await {
81+ Ok ( InterceptOutcome :: Continue ( intercepted_request) ) => {
82+ req = intercepted_request. into_vec_request ( ) . unwrap ( ) ;
83+ }
84+ Ok ( InterceptOutcome :: Complete ( resp) ) => return response_from_hyper ( resp) . await ,
85+ Err ( err) => {
86+ tracing:: error!( "Error in outbound HTTP interceptor: {err}" ) ;
87+ return Err ( HttpError :: RuntimeError ) ;
88+ }
89+ }
90+ }
91+
92+ // Convert http::Request to reqwest::Request
93+ let req = reqwest:: Request :: try_from ( req) . map_err ( |_| HttpError :: InvalidUrl ) ?;
94+
95+ // Allow reuse of Client's internal connection pool for multiple requests
96+ // in a single component execution
97+ let client = self . spin_http_client . get_or_insert_with ( Default :: default) ;
98+
7799 let resp = client. execute ( req) . await . map_err ( log_reqwest_error) ?;
78100
79- tracing:: trace!( "Returning response from outbound request to {abs_url }" ) ;
101+ tracing:: trace!( "Returning response from outbound request to {req_url }" ) ;
80102 span. record ( "http.response.status_code" , resp. status ( ) . as_u16 ( ) ) ;
81103 response_from_reqwest ( resp) . await
82104 }
@@ -111,18 +133,52 @@ fn record_request_fields(span: &Span, req: &Request) {
111133 }
112134}
113135
114- fn reqwest_method ( m : Method ) -> reqwest :: Method {
136+ fn hyper_method ( m : Method ) -> http :: Method {
115137 match m {
116- Method :: Get => reqwest :: Method :: GET ,
117- Method :: Post => reqwest :: Method :: POST ,
118- Method :: Put => reqwest :: Method :: PUT ,
119- Method :: Delete => reqwest :: Method :: DELETE ,
120- Method :: Patch => reqwest :: Method :: PATCH ,
121- Method :: Head => reqwest :: Method :: HEAD ,
122- Method :: Options => reqwest :: Method :: OPTIONS ,
138+ Method :: Get => http :: Method :: GET ,
139+ Method :: Post => http :: Method :: POST ,
140+ Method :: Put => http :: Method :: PUT ,
141+ Method :: Delete => http :: Method :: DELETE ,
142+ Method :: Patch => http :: Method :: PATCH ,
143+ Method :: Head => http :: Method :: HEAD ,
144+ Method :: Options => http :: Method :: OPTIONS ,
123145 }
124146}
125147
148+ async fn response_from_hyper ( mut resp : crate :: Response ) -> Result < Response , HttpError > {
149+ let status = resp. status ( ) . as_u16 ( ) ;
150+
151+ let headers = resp
152+ . headers ( )
153+ . into_iter ( )
154+ . map ( |( key, val) | {
155+ Ok ( (
156+ key. to_string ( ) ,
157+ val. to_str ( )
158+ . map_err ( |_| {
159+ tracing:: error!( "Non-ascii response header {key} = {val:?}" ) ;
160+ HttpError :: RuntimeError
161+ } ) ?
162+ . to_string ( ) ,
163+ ) )
164+ } )
165+ . collect :: < Result < Vec < _ > , _ > > ( ) ?;
166+
167+ let body = resp
168+ . body_mut ( )
169+ . collect ( )
170+ . await
171+ . map_err ( |_| HttpError :: RuntimeError ) ?
172+ . to_bytes ( )
173+ . to_vec ( ) ;
174+
175+ Ok ( Response {
176+ status,
177+ headers : Some ( headers) ,
178+ body : Some ( body) ,
179+ } )
180+ }
181+
126182fn log_reqwest_error ( err : reqwest:: Error ) -> HttpError {
127183 let error_desc = if err. is_timeout ( ) {
128184 "timeout error"
0 commit comments