elif_http/request/
pipeline.rs

1//! Request/Response pipeline that integrates routing and middleware
2//!
3//! This module provides the unified request processing pipeline that:
4//! - Integrates the new framework-native routing engine
5//! - Handles parameter injection from route matches
6//! - Executes middleware pipelines with route-specific configurations  
7//! - Provides clean error handling throughout the pipeline
8//! - Supports route-specific middleware execution
9
10use crate::middleware::v2::{MiddlewarePipelineV2, NextFuture};
11use crate::request::ElifRequest;
12use crate::response::{ElifResponse, ElifStatusCode};
13use crate::routing::{HttpMethod, RouteMatch, RouteMatcher};
14use std::collections::HashMap;
15use std::sync::Arc;
16use thiserror::Error;
17
18/// Errors that can occur during request pipeline processing
19#[derive(Error, Debug)]
20pub enum PipelineError {
21    #[error("Route not found: {method} {path}")]
22    RouteNotFound { method: HttpMethod, path: String },
23
24    #[error("Method not allowed: {method}")]
25    MethodNotAllowed { method: String },
26
27    #[error("Parameter error: {0}")]
28    Parameter(#[from] ParamError),
29
30    #[error("Middleware error: {0}")]
31    Middleware(String),
32
33    #[error("Handler error: {0}")]
34    Handler(String),
35
36    #[error("Internal pipeline error: {0}")]
37    Internal(String),
38}
39
40/// Errors that can occur during parameter extraction and parsing
41#[derive(Error, Debug)]
42pub enum ParamError {
43    #[error("Missing parameter: {0}")]
44    Missing(String),
45
46    #[error("Failed to parse parameter '{param}' with value '{value}': {error}")]
47    ParseError {
48        param: String,
49        value: String,
50        error: String,
51    },
52}
53
54/// Handler function type for request processing
55pub type HandlerFn = dyn Fn(ElifRequest) -> NextFuture<'static> + Send + Sync;
56
57/// Configuration for route-specific middleware groups
58#[derive(Debug, Clone)]
59pub struct MiddlewareGroup {
60    pub name: String,
61    pub pipeline: MiddlewarePipelineV2,
62}
63
64/// The main request processing pipeline
65pub struct RequestPipeline {
66    /// Route matcher for resolving incoming requests
67    matcher: Arc<RouteMatcher>,
68    /// Global middleware that runs for all requests
69    global_middleware: MiddlewarePipelineV2,
70    /// Named middleware groups for route-specific execution
71    middleware_groups: HashMap<String, MiddlewarePipelineV2>,
72    /// Route handlers mapped by route ID (wrapped in Arc for efficient sharing)
73    handlers: Arc<HashMap<String, Arc<HandlerFn>>>,
74}
75
76impl RequestPipeline {
77    /// Create a new request pipeline with a route matcher
78    pub fn new(matcher: RouteMatcher) -> Self {
79        Self {
80            matcher: Arc::new(matcher),
81            global_middleware: MiddlewarePipelineV2::new(),
82            middleware_groups: HashMap::new(),
83            handlers: Arc::new(HashMap::new()),
84        }
85    }
86
87    /// Add global middleware that runs for all requests
88    pub fn add_global_middleware<M>(mut self, middleware: M) -> Self
89    where
90        M: crate::middleware::v2::Middleware + 'static,
91    {
92        self.global_middleware = self.global_middleware.add(middleware);
93        self
94    }
95
96    /// Add a named middleware group for route-specific execution
97    pub fn add_middleware_group<S: Into<String>>(
98        mut self,
99        name: S,
100        pipeline: MiddlewarePipelineV2,
101    ) -> Self {
102        self.middleware_groups.insert(name.into(), pipeline);
103        self
104    }
105
106    /// Register a handler for a specific route ID
107    pub fn add_handler<S: Into<String>, F>(mut self, route_id: S, handler: F) -> Self
108    where
109        F: Fn(ElifRequest) -> NextFuture<'static> + Send + Sync + 'static,
110    {
111        // We need to get mutable access to the HashMap inside the Arc
112        // Since we're in a builder pattern, we can safely unwrap and modify
113        let handlers = Arc::get_mut(&mut self.handlers)
114            .expect("RequestPipeline handlers should be exclusively owned during building");
115        handlers.insert(route_id.into(), Arc::new(handler));
116        self
117    }
118
119    /// Process an incoming request through the complete pipeline
120    pub async fn process(&self, request: ElifRequest) -> ElifResponse {
121        match self.process_internal(request).await {
122            Ok(response) => response,
123            Err(error) => self.handle_pipeline_error(error),
124        }
125    }
126
127    /// Internal request processing with error handling
128    async fn process_internal(&self, request: ElifRequest) -> Result<ElifResponse, PipelineError> {
129        // 1. Route resolution
130        let route_match = self.resolve_route(&request)?;
131
132        // 2. Parameter injection
133        let request_with_params = self.inject_params(request, &route_match);
134
135        // 3. Build complete middleware pipeline for this route
136        let complete_pipeline = self.build_route_pipeline(&route_match)?;
137
138        // 4. Execute middleware + handler
139        // Use Arc::clone for cheap reference counting instead of cloning the entire HashMap
140        let route_id = route_match.route_id.clone();
141        let handlers = Arc::clone(&self.handlers);
142        let response = complete_pipeline.execute(request_with_params, move |req| {
143            let route_id = route_id.clone();
144            let handlers = Arc::clone(&handlers);
145            Box::pin(async move {
146                match handlers.get(&route_id) {
147                    Some(handler) => handler(req).await,
148                    None => {
149                        ElifResponse::internal_server_error()
150                            .with_json(&serde_json::json!({
151                                "error": {
152                                    "code": "handler_not_found",
153                                    "message": format!("No handler registered for route: {}", route_id)
154                                }
155                            }))
156                    }
157                }
158            })
159        }).await;
160
161        Ok(response)
162    }
163
164    /// Resolve incoming request to a matching route
165    fn resolve_route(&self, request: &ElifRequest) -> Result<RouteMatch, PipelineError> {
166        let http_method = match request.method.to_axum() {
167            &axum::http::Method::GET => HttpMethod::GET,
168            &axum::http::Method::POST => HttpMethod::POST,
169            &axum::http::Method::PUT => HttpMethod::PUT,
170            &axum::http::Method::DELETE => HttpMethod::DELETE,
171            &axum::http::Method::PATCH => HttpMethod::PATCH,
172            &axum::http::Method::HEAD => HttpMethod::HEAD,
173            &axum::http::Method::OPTIONS => HttpMethod::OPTIONS,
174            &axum::http::Method::TRACE => HttpMethod::TRACE,
175            unsupported => {
176                return Err(PipelineError::MethodNotAllowed {
177                    method: unsupported.to_string(),
178                });
179            }
180        };
181
182        self.matcher
183            .resolve(&http_method, request.path())
184            .ok_or_else(|| PipelineError::RouteNotFound {
185                method: http_method,
186                path: request.path().to_string(),
187            })
188    }
189
190    /// Inject route parameters into the request
191    fn inject_params(&self, mut request: ElifRequest, route_match: &RouteMatch) -> ElifRequest {
192        for (key, value) in &route_match.params {
193            request.add_path_param(key, value);
194        }
195        request
196    }
197
198    /// Build the complete middleware pipeline for a specific route
199    fn build_route_pipeline(
200        &self,
201        _route_match: &RouteMatch,
202    ) -> Result<MiddlewarePipelineV2, PipelineError> {
203        let pipeline = self.global_middleware.clone();
204
205        // Add route-specific middleware groups
206        // For now, we'll look for middleware group names in route metadata
207        // This can be extended to support route definition with middleware specifications
208
209        Ok(pipeline)
210    }
211
212    /// Handle pipeline errors by converting them to appropriate HTTP responses
213    fn handle_pipeline_error(&self, error: PipelineError) -> ElifResponse {
214        match error {
215            PipelineError::RouteNotFound { .. } => {
216                ElifResponse::not_found().with_json(&serde_json::json!({
217                    "error": {
218                        "code": "not_found",
219                        "message": "The requested resource was not found"
220                    }
221                }))
222            }
223            PipelineError::MethodNotAllowed { method } => ElifResponse::with_status(
224                ElifStatusCode::METHOD_NOT_ALLOWED,
225            )
226            .with_json(&serde_json::json!({
227                "error": {
228                    "code": "method_not_allowed",
229                    "message": format!("HTTP method '{}' is not supported", method),
230                    "hint": "Supported methods: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE"
231                }
232            })),
233            PipelineError::Parameter(param_error) => {
234                ElifResponse::bad_request().with_json(&serde_json::json!({
235                    "error": {
236                        "code": "parameter_error",
237                        "message": param_error.to_string()
238                    }
239                }))
240            }
241            PipelineError::Middleware(msg)
242            | PipelineError::Handler(msg)
243            | PipelineError::Internal(msg) => {
244                ElifResponse::internal_server_error().with_json(&serde_json::json!({
245                    "error": {
246                        "code": "internal_error",
247                        "message": msg
248                    }
249                }))
250            }
251        }
252    }
253
254    /// Get statistics about the pipeline
255    pub fn stats(&self) -> PipelineStats {
256        PipelineStats {
257            total_routes: self.matcher.all_routes().len(),
258            global_middleware_count: self.global_middleware.len(),
259            middleware_groups: self.middleware_groups.len(),
260            registered_handlers: self.handlers.len(),
261        }
262    }
263
264    /// Get the route matcher for introspection
265    pub fn matcher(&self) -> &RouteMatcher {
266        &self.matcher
267    }
268
269    /// Get global middleware pipeline for introspection
270    pub fn global_middleware(&self) -> &MiddlewarePipelineV2 {
271        &self.global_middleware
272    }
273
274    /// Get middleware groups for introspection
275    pub fn middleware_groups(&self) -> &HashMap<String, MiddlewarePipelineV2> {
276        &self.middleware_groups
277    }
278}
279
280impl std::fmt::Debug for RequestPipeline {
281    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282        f.debug_struct("RequestPipeline")
283            .field("matcher", &self.matcher)
284            .field("global_middleware", &self.global_middleware)
285            .field("middleware_groups", &self.middleware_groups)
286            .field("handlers", &self.handlers.len())
287            .finish()
288    }
289}
290
291/// Statistics about the request pipeline
292#[derive(Debug, Clone)]
293pub struct PipelineStats {
294    pub total_routes: usize,
295    pub global_middleware_count: usize,
296    pub middleware_groups: usize,
297    pub registered_handlers: usize,
298}
299
300/// Builder for creating request pipelines
301pub struct RequestPipelineBuilder {
302    matcher: Option<RouteMatcher>,
303    global_middleware: MiddlewarePipelineV2,
304    middleware_groups: HashMap<String, MiddlewarePipelineV2>,
305    handlers: HashMap<String, Arc<HandlerFn>>,
306}
307
308impl RequestPipelineBuilder {
309    /// Create a new pipeline builder
310    pub fn new() -> Self {
311        Self {
312            matcher: None,
313            global_middleware: MiddlewarePipelineV2::new(),
314            middleware_groups: HashMap::new(),
315            handlers: HashMap::new(),
316        }
317    }
318
319    /// Set the route matcher
320    pub fn matcher(mut self, matcher: RouteMatcher) -> Self {
321        self.matcher = Some(matcher);
322        self
323    }
324
325    /// Add global middleware
326    pub fn global_middleware<M>(mut self, middleware: M) -> Self
327    where
328        M: crate::middleware::v2::Middleware + 'static,
329    {
330        self.global_middleware = self.global_middleware.add(middleware);
331        self
332    }
333
334    /// Add a middleware group
335    pub fn middleware_group<S: Into<String>>(
336        mut self,
337        name: S,
338        pipeline: MiddlewarePipelineV2,
339    ) -> Self {
340        self.middleware_groups.insert(name.into(), pipeline);
341        self
342    }
343
344    /// Add a route handler
345    pub fn handler<S: Into<String>, F>(mut self, route_id: S, handler: F) -> Self
346    where
347        F: Fn(ElifRequest) -> NextFuture<'static> + Send + Sync + 'static,
348    {
349        self.handlers.insert(route_id.into(), Arc::new(handler));
350        self
351    }
352
353    /// Build the request pipeline
354    pub fn build(self) -> Result<RequestPipeline, PipelineError> {
355        let matcher = self
356            .matcher
357            .ok_or_else(|| PipelineError::Internal("Route matcher is required".to_string()))?;
358
359        Ok(RequestPipeline {
360            matcher: Arc::new(matcher),
361            global_middleware: self.global_middleware,
362            middleware_groups: self.middleware_groups,
363            handlers: Arc::new(self.handlers),
364        })
365    }
366}
367
368impl Default for RequestPipelineBuilder {
369    fn default() -> Self {
370        Self::new()
371    }
372}
373
374/// Helper functions for parameter extraction with better error handling
375pub mod parameter_extraction {
376    use super::{ElifRequest, ParamError};
377    use std::fmt::{Debug, Display};
378    use std::str::FromStr;
379
380    /// Extract and parse a path parameter with type conversion
381    pub fn extract_path_param<T>(request: &ElifRequest, name: &str) -> Result<T, ParamError>
382    where
383        T: FromStr,
384        T::Err: Debug + Display,
385    {
386        let param_value = request
387            .path_param(name)
388            .ok_or_else(|| ParamError::Missing(name.to_string()))?;
389
390        param_value.parse().map_err(|e| ParamError::ParseError {
391            param: name.to_string(),
392            value: param_value.clone(),
393            error: format!("{}", e),
394        })
395    }
396
397    /// Extract and parse a query parameter with type conversion
398    pub fn extract_query_param<T>(
399        request: &ElifRequest,
400        name: &str,
401    ) -> Result<Option<T>, ParamError>
402    where
403        T: FromStr,
404        T::Err: Debug + Display,
405    {
406        if let Some(param_value) = request.query_param(name) {
407            let parsed = param_value.parse().map_err(|e| ParamError::ParseError {
408                param: name.to_string(),
409                value: param_value.clone(),
410                error: format!("{}", e),
411            })?;
412            Ok(Some(parsed))
413        } else {
414            Ok(None)
415        }
416    }
417
418    /// Extract required query parameter with type conversion
419    pub fn extract_required_query_param<T>(
420        request: &ElifRequest,
421        name: &str,
422    ) -> Result<T, ParamError>
423    where
424        T: FromStr,
425        T::Err: Debug + Display,
426    {
427        extract_query_param(request, name)?.ok_or_else(|| ParamError::Missing(name.to_string()))
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434    use crate::middleware::v2::{LoggingMiddleware, MiddlewarePipelineV2};
435    use crate::response::{ElifResponse, ElifStatusCode};
436    use crate::routing::RouteMatcherBuilder;
437
438    #[tokio::test]
439    async fn test_basic_pipeline_processing() {
440        // Create a simple route matcher
441        let matcher = RouteMatcherBuilder::new()
442            .get("home".to_string(), "/".to_string())
443            .get("user_show".to_string(), "/users/{id}".to_string())
444            .build()
445            .unwrap();
446
447        // Create pipeline with a simple handler
448        let pipeline = RequestPipelineBuilder::new()
449            .matcher(matcher)
450            .handler("home", |_req| {
451                Box::pin(async move { ElifResponse::ok().with_text("Welcome home!") })
452            })
453            .handler("user_show", |req| {
454                Box::pin(async move {
455                    let user_id: u32 = match parameter_extraction::extract_path_param(&req, "id") {
456                        Ok(id) => id,
457                        Err(_) => return ElifResponse::bad_request().with_text("Invalid user ID"),
458                    };
459                    ElifResponse::ok().with_json(&serde_json::json!({
460                        "user_id": user_id,
461                        "message": "User details"
462                    }))
463                })
464            })
465            .build()
466            .unwrap();
467
468        // Test home route
469        let home_request = ElifRequest::new(
470            crate::request::ElifMethod::GET,
471            "/".parse().unwrap(),
472            crate::response::ElifHeaderMap::new(),
473        );
474
475        let home_response = pipeline.process(home_request).await;
476        assert_eq!(home_response.status_code(), ElifStatusCode::OK);
477
478        // Test user route with parameter
479        let user_request = ElifRequest::new(
480            crate::request::ElifMethod::GET,
481            "/users/123".parse().unwrap(),
482            crate::response::ElifHeaderMap::new(),
483        );
484
485        let user_response = pipeline.process(user_request).await;
486        assert_eq!(user_response.status_code(), ElifStatusCode::OK);
487    }
488
489    #[tokio::test]
490    async fn test_pipeline_with_middleware() {
491        let matcher = RouteMatcherBuilder::new()
492            .get("test".to_string(), "/test".to_string())
493            .build()
494            .unwrap();
495
496        let pipeline = RequestPipelineBuilder::new()
497            .matcher(matcher)
498            .global_middleware(LoggingMiddleware)
499            .handler("test", |_req| {
500                Box::pin(async move { ElifResponse::ok().with_text("Test response") })
501            })
502            .build()
503            .unwrap();
504
505        let request = ElifRequest::new(
506            crate::request::ElifMethod::GET,
507            "/test".parse().unwrap(),
508            crate::response::ElifHeaderMap::new(),
509        );
510
511        let response = pipeline.process(request).await;
512        assert_eq!(response.status_code(), ElifStatusCode::OK);
513    }
514
515    #[tokio::test]
516    async fn test_pipeline_route_not_found() {
517        let matcher = RouteMatcherBuilder::new()
518            .get("home".to_string(), "/".to_string())
519            .build()
520            .unwrap();
521
522        let pipeline = RequestPipelineBuilder::new()
523            .matcher(matcher)
524            .build()
525            .unwrap();
526
527        let request = ElifRequest::new(
528            crate::request::ElifMethod::GET,
529            "/nonexistent".parse().unwrap(),
530            crate::response::ElifHeaderMap::new(),
531        );
532
533        let response = pipeline.process(request).await;
534        assert_eq!(response.status_code(), ElifStatusCode::NOT_FOUND);
535    }
536
537    #[tokio::test]
538    async fn test_pipeline_handler_not_found() {
539        let matcher = RouteMatcherBuilder::new()
540            .get("test".to_string(), "/test".to_string())
541            .build()
542            .unwrap();
543
544        // Create pipeline without registering a handler
545        let pipeline = RequestPipelineBuilder::new()
546            .matcher(matcher)
547            .build()
548            .unwrap();
549
550        let request = ElifRequest::new(
551            crate::request::ElifMethod::GET,
552            "/test".parse().unwrap(),
553            crate::response::ElifHeaderMap::new(),
554        );
555
556        let response = pipeline.process(request).await;
557        assert_eq!(
558            response.status_code(),
559            ElifStatusCode::INTERNAL_SERVER_ERROR
560        );
561    }
562
563    #[tokio::test]
564    async fn test_parameter_extraction_helpers() {
565        let mut request = ElifRequest::new(
566            crate::request::ElifMethod::GET,
567            "/users/123?page=2&limit=10".parse().unwrap(),
568            crate::response::ElifHeaderMap::new(),
569        );
570
571        // Simulate parameter injection
572        request.add_path_param("id", "123");
573        request.add_query_param("page", "2");
574        request.add_query_param("limit", "10");
575
576        // Test path parameter extraction
577        let user_id: u32 = parameter_extraction::extract_path_param(&request, "id").unwrap();
578        assert_eq!(user_id, 123);
579
580        // Test query parameter extraction
581        let page: Option<u32> =
582            parameter_extraction::extract_query_param(&request, "page").unwrap();
583        assert_eq!(page, Some(2));
584
585        // Test required query parameter extraction
586        let limit: u32 =
587            parameter_extraction::extract_required_query_param(&request, "limit").unwrap();
588        assert_eq!(limit, 10);
589
590        // Test missing parameter
591        let result = parameter_extraction::extract_path_param::<u32>(&request, "nonexistent");
592        assert!(result.is_err());
593        assert!(matches!(result.unwrap_err(), ParamError::Missing(_)));
594
595        // Test invalid parameter parsing
596        request.add_path_param("invalid", "not_a_number");
597        let result = parameter_extraction::extract_path_param::<u32>(&request, "invalid");
598        assert!(result.is_err());
599        assert!(matches!(result.unwrap_err(), ParamError::ParseError { .. }));
600    }
601
602    #[tokio::test]
603    async fn test_pipeline_stats() {
604        let matcher = RouteMatcherBuilder::new()
605            .get("route1".to_string(), "/route1".to_string())
606            .get("route2".to_string(), "/route2".to_string())
607            .build()
608            .unwrap();
609
610        let middleware_group = MiddlewarePipelineV2::new().add(LoggingMiddleware);
611
612        let pipeline = RequestPipelineBuilder::new()
613            .matcher(matcher)
614            .global_middleware(LoggingMiddleware)
615            .middleware_group("auth", middleware_group)
616            .handler("route1", |_req| Box::pin(async move { ElifResponse::ok() }))
617            .handler("route2", |_req| Box::pin(async move { ElifResponse::ok() }))
618            .build()
619            .unwrap();
620
621        let stats = pipeline.stats();
622        assert_eq!(stats.total_routes, 2);
623        assert_eq!(stats.global_middleware_count, 1);
624        assert_eq!(stats.middleware_groups, 1);
625        assert_eq!(stats.registered_handlers, 2);
626    }
627
628    #[tokio::test]
629    async fn test_arc_optimization_efficiency() {
630        // This test demonstrates that Arc::clone is cheap even with many handlers
631        let matcher = RouteMatcherBuilder::new()
632            .get("test_route".to_string(), "/test".to_string())
633            .build()
634            .unwrap();
635
636        // Create pipeline with many handlers to test Arc efficiency
637        let mut builder = RequestPipelineBuilder::new().matcher(matcher);
638
639        // Add 100 handlers to test performance
640        for i in 0..100 {
641            builder = builder.handler(format!("handler_{}", i), |_req| {
642                Box::pin(async move { ElifResponse::ok() })
643            });
644        }
645
646        builder = builder.handler("test_route", |_req| {
647            Box::pin(async move { ElifResponse::ok().with_text("Test response") })
648        });
649
650        let pipeline = builder.build().unwrap();
651
652        // Process multiple requests to verify Arc::clone efficiency
653        for _ in 0..10 {
654            let request = ElifRequest::new(
655                crate::request::ElifMethod::GET,
656                "/test".parse().unwrap(),
657                crate::response::ElifHeaderMap::new(),
658            );
659
660            let response = pipeline.process(request).await;
661            assert_eq!(response.status_code(), ElifStatusCode::OK);
662        }
663
664        // Verify we have many handlers
665        let stats = pipeline.stats();
666        assert_eq!(stats.registered_handlers, 101);
667
668        // The key optimization: Arc::clone is O(1) regardless of HashMap size
669        // Previously this would have been O(n) for each request due to HashMap cloning
670    }
671
672    #[tokio::test]
673    async fn test_method_not_allowed_error() {
674        let matcher = RouteMatcherBuilder::new()
675            .get("test".to_string(), "/test".to_string())
676            .build()
677            .unwrap();
678
679        let pipeline = RequestPipelineBuilder::new()
680            .matcher(matcher)
681            .handler("test", |_req| {
682                Box::pin(async move { ElifResponse::ok().with_text("Test response") })
683            })
684            .build()
685            .unwrap();
686
687        // Create a request with CONNECT method (unsupported)
688        let connect_method = crate::request::ElifMethod::from_axum(axum::http::Method::CONNECT);
689        let request = ElifRequest::new(
690            connect_method,
691            "/test".parse().unwrap(),
692            crate::response::ElifHeaderMap::new(),
693        );
694
695        let response = pipeline.process(request).await;
696        assert_eq!(response.status_code(), ElifStatusCode::METHOD_NOT_ALLOWED);
697
698        // Verify error message structure
699        // Note: We can't easily test JSON content without body reading capabilities,
700        // but we can verify the status code which is the most important part
701    }
702
703    #[tokio::test]
704    async fn test_supported_methods_work() {
705        let matcher = RouteMatcherBuilder::new()
706            .get("get_test".to_string(), "/test".to_string())
707            .post("post_test".to_string(), "/test".to_string())
708            .put("put_test".to_string(), "/test".to_string())
709            .delete("delete_test".to_string(), "/test".to_string())
710            .build()
711            .unwrap();
712
713        let pipeline = RequestPipelineBuilder::new()
714            .matcher(matcher)
715            .handler("get_test", |_req| {
716                Box::pin(async move { ElifResponse::ok().with_text("GET") })
717            })
718            .handler("post_test", |_req| {
719                Box::pin(async move { ElifResponse::ok().with_text("POST") })
720            })
721            .handler("put_test", |_req| {
722                Box::pin(async move { ElifResponse::ok().with_text("PUT") })
723            })
724            .handler("delete_test", |_req| {
725                Box::pin(async move { ElifResponse::ok().with_text("DELETE") })
726            })
727            .build()
728            .unwrap();
729
730        // Test all supported methods work correctly
731        let methods = [
732            (crate::request::ElifMethod::GET, "GET"),
733            (crate::request::ElifMethod::POST, "POST"),
734            (crate::request::ElifMethod::PUT, "PUT"),
735            (crate::request::ElifMethod::DELETE, "DELETE"),
736        ];
737
738        for (method, _expected_response) in methods {
739            let request = ElifRequest::new(
740                method,
741                "/test".parse().unwrap(),
742                crate::response::ElifHeaderMap::new(),
743            );
744
745            let response = pipeline.process(request).await;
746            assert_eq!(response.status_code(), ElifStatusCode::OK);
747        }
748    }
749}