@@ -64,9 +64,10 @@ pub struct HttpConnectionScope {
6464 state : Option < Py < PyDict > > ,
6565}
6666
67- impl HttpConnectionScope {
68- /// Create a new HttpConnectionScope from an http::Request
69- pub fn from_request ( request : & Request ) -> Self {
67+ impl TryFrom < & Request > for HttpConnectionScope {
68+ type Error = PyErr ;
69+
70+ fn try_from ( request : & Request ) -> Result < Self , Self :: Error > {
7071 // Extract HTTP version
7172 let http_version = match request. version ( ) {
7273 Version :: HTTP_09 => HttpVersion :: V1_0 , // fallback for HTTP/0.9
@@ -78,7 +79,8 @@ impl HttpConnectionScope {
7879 } ;
7980
8081 // Extract method
81- let method = HttpMethod :: from ( request. method ( ) . as_str ( ) ) ;
82+ let method = request. method ( ) . try_into ( )
83+ . map_err ( PyValueError :: new_err) ?;
8284
8385 // Extract scheme from URI or default to http
8486 let scheme = request
@@ -100,8 +102,11 @@ impl HttpConnectionScope {
100102 . unwrap_or ( "" )
101103 . to_string ( ) ;
102104
103- // Extract root path (default to empty)
104- let root_path = String :: new ( ) ;
105+ // Extract root path from DocumentRoot extension
106+ let root_path = request
107+ . document_root ( )
108+ . map ( |doc_root| doc_root. path . to_string_lossy ( ) . to_string ( ) )
109+ . unwrap_or_default ( ) ;
105110
106111 // Convert headers
107112 let headers: Vec < ( String , String ) > = request
@@ -130,7 +135,7 @@ impl HttpConnectionScope {
130135 ( None , None )
131136 } ;
132137
133- HttpConnectionScope {
138+ Ok ( HttpConnectionScope {
134139 http_version,
135140 method,
136141 scheme,
@@ -142,7 +147,7 @@ impl HttpConnectionScope {
142147 client,
143148 server,
144149 state : None ,
145- }
150+ } )
146151 }
147152}
148153
@@ -322,7 +327,7 @@ impl<'py> FromPyObject<'py> for HttpSendMessage {
322327 . ok_or_else ( || {
323328 PyValueError :: new_err ( "Missing 'headers' key in HTTP response start message" )
324329 } ) ?;
325-
330+
326331 // Convert headers from list of lists to vec of tuples
327332 let mut headers: Vec < ( String , String ) > = Vec :: new ( ) ;
328333 if let Ok ( headers_list) = headers_py. downcast :: < pyo3:: types:: PyList > ( ) {
@@ -331,20 +336,20 @@ impl<'py> FromPyObject<'py> for HttpSendMessage {
331336 if header_pair. len ( ) == 2 {
332337 let name = header_pair. get_item ( 0 ) ?;
333338 let value = header_pair. get_item ( 1 ) ?;
334-
339+
335340 // Convert bytes to string
336341 let name_str = if let Ok ( bytes) = name. downcast :: < pyo3:: types:: PyBytes > ( ) {
337342 String :: from_utf8_lossy ( bytes. as_bytes ( ) ) . to_string ( )
338343 } else {
339344 name. extract :: < String > ( ) ?
340345 } ;
341-
346+
342347 let value_str = if let Ok ( bytes) = value. downcast :: < pyo3:: types:: PyBytes > ( ) {
343348 String :: from_utf8_lossy ( bytes. as_bytes ( ) ) . to_string ( )
344349 } else {
345350 value. extract :: < String > ( ) ?
346351 } ;
347-
352+
348353 headers. push ( ( name_str, value_str) ) ;
349354 }
350355 }
@@ -390,6 +395,9 @@ pub enum HttpSendException {
390395#[ cfg( test) ]
391396mod tests {
392397 use super :: * ;
398+ use http_handler:: { RequestExt , extensions:: DocumentRoot } ;
399+ use http_handler:: { Method , Version , request:: Builder } ;
400+ use std:: { net:: { IpAddr , Ipv4Addr , SocketAddr } , path:: PathBuf } ;
393401
394402 macro_rules! dict_get {
395403 ( $dict: expr, $key: expr) => {
@@ -408,6 +416,119 @@ mod tests {
408416 } ;
409417 }
410418
419+ #[ test]
420+ fn test_http_connection_scope_from_request ( ) {
421+ // Create a test request with various headers and extensions
422+ let mut request = Builder :: new ( )
423+ . method ( Method :: POST )
424+ . uri ( "https://example.com:8443/api/v1/users?sort=name&limit=10" )
425+ . header ( "content-type" , "application/json" )
426+ . header ( "authorization" , "Bearer token123" )
427+ . header ( "user-agent" , "test-client/1.0" )
428+ . header ( "x-custom-header" , "custom-value" )
429+ . body ( bytes:: BytesMut :: from ( "request body" ) )
430+ . unwrap ( ) ;
431+
432+ // Set socket info extension
433+ let local_addr = SocketAddr :: new ( IpAddr :: V4 ( Ipv4Addr :: new ( 127 , 0 , 0 , 1 ) ) , 8443 ) ;
434+ let remote_addr = SocketAddr :: new ( IpAddr :: V4 ( Ipv4Addr :: new ( 192 , 168 , 1 , 100 ) ) , 12345 ) ;
435+ request. set_socket_info ( http_handler:: extensions:: SocketInfo :: new (
436+ Some ( local_addr) ,
437+ Some ( remote_addr) ,
438+ ) ) ;
439+
440+ // Set document root extension
441+ let doc_root = PathBuf :: from ( "/var/www/html" ) ;
442+ request. set_document_root ( DocumentRoot { path : doc_root. clone ( ) } ) ;
443+
444+ // Convert to ASGI scope
445+ let scope: HttpConnectionScope = ( & request) . try_into ( )
446+ . expect ( "Failed to convert request to HttpConnectionScope" ) ;
447+
448+ // Verify HTTP version
449+ assert_eq ! ( scope. http_version, HttpVersion :: V1_1 ) ;
450+
451+ // Verify method
452+ assert_eq ! ( scope. method, HttpMethod :: Post ) ;
453+
454+ // Verify scheme
455+ assert_eq ! ( scope. scheme, "https" ) ;
456+
457+ // Verify path
458+ assert_eq ! ( scope. path, "/api/v1/users" ) ;
459+
460+ // Verify raw_path (should be same as path in this implementation)
461+ assert_eq ! ( scope. raw_path, "/api/v1/users" ) ;
462+
463+ // Verify query_string
464+ assert_eq ! ( scope. query_string, "sort=name&limit=10" ) ;
465+
466+ // Verify root_path from DocumentRoot extension
467+ assert_eq ! ( scope. root_path, doc_root. to_string_lossy( ) ) ;
468+
469+ // Verify headers (should be lowercased)
470+ let expected_headers = vec ! [
471+ ( "content-type" . to_string( ) , "application/json" . to_string( ) ) ,
472+ ( "authorization" . to_string( ) , "Bearer token123" . to_string( ) ) ,
473+ ( "user-agent" . to_string( ) , "test-client/1.0" . to_string( ) ) ,
474+ ( "x-custom-header" . to_string( ) , "custom-value" . to_string( ) ) ,
475+ ] ;
476+ assert_eq ! ( scope. headers, expected_headers) ;
477+
478+ // Verify client socket info
479+ assert_eq ! ( scope. client, Some ( ( "192.168.1.100" . to_string( ) , 12345 ) ) ) ;
480+
481+ // Verify server socket info
482+ assert_eq ! ( scope. server, Some ( ( "127.0.0.1" . to_string( ) , 8443 ) ) ) ;
483+
484+ // Verify state is None (not set)
485+ assert ! ( scope. state. is_none( ) ) ;
486+ }
487+
488+ #[ test]
489+ fn test_http_connection_scope_from_request_minimal ( ) {
490+ // Test with minimal request (no extensions, no headers)
491+ let request = Builder :: new ( )
492+ . method ( Method :: GET )
493+ . uri ( "/" )
494+ . body ( bytes:: BytesMut :: new ( ) )
495+ . unwrap ( ) ;
496+
497+ let scope: HttpConnectionScope = ( & request) . try_into ( )
498+ . expect ( "Failed to convert request to HttpConnectionScope" ) ;
499+
500+ assert_eq ! ( scope. http_version, HttpVersion :: V1_1 ) ;
501+ assert_eq ! ( scope. method, HttpMethod :: Get ) ;
502+ assert_eq ! ( scope. scheme, "http" ) ; // default scheme
503+ assert_eq ! ( scope. path, "/" ) ;
504+ assert_eq ! ( scope. raw_path, "/" ) ;
505+ assert_eq ! ( scope. query_string, "" ) ;
506+ assert_eq ! ( scope. root_path, "" ) ; // no DocumentRoot extension
507+ assert_eq ! ( scope. headers, vec![ ] ) ; // no headers
508+ assert_eq ! ( scope. client, None ) ; // no socket info
509+ assert_eq ! ( scope. server, None ) ; // no socket info
510+ assert ! ( scope. state. is_none( ) ) ;
511+ }
512+
513+ #[ test]
514+ fn test_http_connection_scope_from_request_http2 ( ) {
515+ // Test HTTP/2 version handling
516+ let request = Builder :: new ( )
517+ . method ( Method :: PUT )
518+ . uri ( "http://api.example.com/resource/123" )
519+ . version ( Version :: HTTP_2 )
520+ . body ( bytes:: BytesMut :: new ( ) )
521+ . unwrap ( ) ;
522+
523+ let scope: HttpConnectionScope = ( & request) . try_into ( )
524+ . expect ( "Failed to convert request to HttpConnectionScope" ) ;
525+
526+ assert_eq ! ( scope. http_version, HttpVersion :: V2_0 ) ;
527+ assert_eq ! ( scope. method, HttpMethod :: Put ) ;
528+ assert_eq ! ( scope. scheme, "http" ) ;
529+ assert_eq ! ( scope. path, "/resource/123" ) ;
530+ }
531+
411532 #[ test]
412533 fn test_http_connection_scope_into_pyobject ( ) {
413534 Python :: with_gil ( |py| {
@@ -478,12 +599,10 @@ mod tests {
478599 let dict = PyDict :: new ( py) ;
479600 dict. set_item ( "type" , "http.response.start" ) . unwrap ( ) ;
480601 dict. set_item ( "status" , 200 ) . unwrap ( ) ;
481- dict
482- . set_item (
483- "headers" ,
484- vec ! [ ( "content-type" . to_string( ) , "text/plain" . to_string( ) ) ] ,
485- )
486- . unwrap ( ) ;
602+
603+ // Headers should be a list of lists in ASGI format
604+ let headers = vec ! [ vec![ "content-type" , "text/plain" ] ] ;
605+ dict. set_item ( "headers" , headers) . unwrap ( ) ;
487606 dict. set_item ( "trailers" , false ) . unwrap ( ) ;
488607
489608 let message: HttpSendMessage = dict. extract ( ) . unwrap ( ) ;
0 commit comments