Skip to main content

tako_rs_core/router/
dispatch.rs

1//! Request dispatch: route matching, the middleware/timeout pipeline, and the
2//! TSR / 405 / 404 cold paths.
3
4use std::sync::Arc;
5use std::sync::atomic::Ordering;
6
7use http::Method;
8use http::StatusCode;
9use smallvec::SmallVec;
10
11use super::Router;
12use crate::body::TakoBody;
13use crate::extractors::params::PathParams;
14use crate::handler::BoxHandler;
15use crate::middleware::Next;
16use crate::route::Route;
17#[cfg(feature = "signals")]
18use crate::signals::Signal;
19#[cfg(feature = "signals")]
20use crate::signals::SignalArbiter;
21#[cfg(feature = "signals")]
22use crate::signals::ids;
23use crate::types::Request;
24use crate::types::Response;
25
26/// Builds an empty-body response with the given status code without going
27/// through `http::response::Builder`. The builder API returns `Result` to
28/// surface invalid header values, but for the router's hot-path 404 / 405 /
29/// 408 / 505 responses we have no headers to fail on, so the result is
30/// statically infallible. This helper avoids `.expect("valid …")` calls in
31/// the dispatch path.
32#[inline]
33pub(crate) fn empty_status_response(status: StatusCode) -> Response {
34  let mut resp = http::Response::new(TakoBody::empty());
35  *resp.status_mut() = status;
36  resp
37}
38
39impl Router {
40  /// Executes the given endpoint through the global middleware chain.
41  ///
42  /// This helper is used for cases like TSR redirects and default 404 responses,
43  /// ensuring that router-level middleware (e.g., CORS) always runs.
44  async fn run_with_global_middlewares_for_endpoint(
45    &self,
46    req: Request,
47    endpoint: BoxHandler,
48  ) -> Response {
49    if self.has_global_middleware.load(Ordering::Acquire) {
50      Next {
51        global_middlewares: self.middlewares.load_full(),
52        route_middlewares: Arc::default(),
53        index: 0,
54        endpoint,
55      }
56      .run(req)
57      .await
58    } else {
59      endpoint.call(req).await
60    }
61  }
62
63  /// Dispatches an incoming request to the appropriate route handler.
64  #[inline]
65  pub async fn dispatch(&self, mut req: Request) -> Response {
66    // Per-router state: only inject when at least one `with_state` was called.
67    // The atomic load is monomorphic and cheap; the Arc clone (atomic incref)
68    // only happens for routers that actually use instance-local state.
69    if self.has_router_state.load(Ordering::Acquire) {
70      req.extensions_mut().insert(Arc::clone(&self.router_state));
71    }
72
73    // App-level request signal — emitted here so every transport gets it for
74    // free without duplicating the boilerplate. The cost is a single string
75    // formatting pair per request and is gated to the `signals` feature.
76    #[cfg(feature = "signals")]
77    let (req_method_str, req_path_str) = (req.method().to_string(), req.uri().path().to_string());
78    #[cfg(feature = "signals")]
79    {
80      SignalArbiter::emit_app(
81        Signal::with_capacity(ids::REQUEST_STARTED, 2)
82          .meta("method", req_method_str.clone())
83          .meta("path", req_path_str.clone()),
84      )
85      .await;
86    }
87
88    // Phase 1: Route lookup using a borrowed path — no String allocation on the
89    // hot path. The block scope ensures all borrows on `req` are released before
90    // we need to mutate it.
91    let route_match = {
92      if let Some(method_router) = self.inner.get(req.method())
93        && let Ok(matched) = method_router.at(req.uri().path())
94      {
95        let route = Arc::clone(matched.value);
96        let mut it = matched.params.iter();
97        let first = it.next();
98        let params = first.map(|(fk, fv)| {
99          let mut p = SmallVec::<[(String, String); 4]>::new();
100          p.push((fk.to_string(), fv.to_string()));
101          for (k, v) in it {
102            p.push((k.to_string(), v.to_string()));
103          }
104          PathParams(p)
105        });
106        Some((route, params))
107      } else {
108        None
109      }
110    };
111
112    // Phase 2: Dispatch — `req` is no longer borrowed, safe to mutate.
113    let response = if let Some((route, params)) = route_match {
114      // Protocol guard: short-circuit dispatch *but fall through* to the shared
115      // completion tail (error-handler + REQUEST_COMPLETED signal). Returning
116      // here would leak the in-flight signal pair (REQUEST_STARTED already
117      // emitted above without a matching REQUEST_COMPLETED).
118      if let Some(res) = Self::enforce_protocol_guard(&route, &req) {
119        res
120      } else {
121        #[cfg(feature = "signals")]
122        let route_signals = route.signal_arbiter();
123
124        // Initialize route-level plugins on first request
125        #[cfg(feature = "plugins")]
126        route.setup_plugins_once();
127
128        // Inject route-level SIMD JSON config into request extensions
129        if let Some(mode) = route.get_simd_json_mode() {
130          req.extensions_mut().insert(mode);
131        }
132
133        if let Some(params) = params {
134          req.extensions_mut().insert(params);
135        }
136
137        // Inject the matched route template (e.g. `/users/{id}`) so handlers
138        // and middleware can label metrics/logs by the routing key, not the
139        // concrete URI.
140        req
141          .extensions_mut()
142          .insert(crate::router_state::MatchedPath(route.path.clone()));
143
144        // Determine effective timeout: route-level overrides router-level
145        let effective_timeout = route.get_timeout().or(self.timeout);
146
147        // Fast atomic check: skip ArcSwap loads entirely when no middleware is registered.
148        let needs_chain = self.has_global_middleware.load(Ordering::Acquire)
149          || route.has_middleware.load(Ordering::Acquire);
150
151        #[cfg(feature = "signals")]
152        {
153          // Reuse the strings already formatted for REQUEST_STARTED instead of
154          // re-allocating per request on the hot path. Cheap `String::clone` is
155          // a single Vec dup; route-level signals consume the clones for the
156          // STARTED emission and the final move into ROUTE_REQUEST_COMPLETED.
157          let method_str = req_method_str.clone();
158          let path_str = req_path_str.clone();
159          let route_template = route.path.clone();
160
161          route_signals
162            .emit(
163              Signal::with_capacity(ids::ROUTE_REQUEST_STARTED, 3)
164                .meta("method", method_str.clone())
165                .meta("path", path_str.clone())
166                .meta("route", route_template.clone()),
167            )
168            .await;
169
170          let response = if !needs_chain && effective_timeout.is_none() {
171            route.handler.call(req).await
172          } else {
173            let next = Next {
174              global_middlewares: self.middlewares.load_full(),
175              route_middlewares: route.middlewares.load_full(),
176              index: 0,
177              endpoint: route.handler.clone(),
178            };
179            self.run_with_timeout(req, next, effective_timeout).await
180          };
181
182          route_signals
183            .emit(
184              Signal::with_capacity(ids::ROUTE_REQUEST_COMPLETED, 4)
185                .meta("method", method_str)
186                .meta("path", path_str)
187                .meta("route", route_template)
188                .meta("status", response.status().as_u16().to_string()),
189            )
190            .await;
191
192          response
193        }
194
195        #[cfg(not(feature = "signals"))]
196        {
197          if !needs_chain && effective_timeout.is_none() {
198            route.handler.call(req).await
199          } else {
200            let next = Next {
201              global_middlewares: self.middlewares.load_full(),
202              route_middlewares: route.middlewares.load_full(),
203              index: 0,
204              endpoint: route.handler.clone(),
205            };
206            self.run_with_timeout(req, next, effective_timeout).await
207          }
208        }
209      }
210    } else {
211      // Cold path: no direct match — try TSR redirect / 405 / fallback.
212      // String allocation is acceptable here.
213      let tsr_path = {
214        let p = req.uri().path();
215        if p.ends_with('/') {
216          p.trim_end_matches('/').to_string()
217        } else {
218          format!("{p}/")
219        }
220      };
221
222      if let Some(method_router) = self.inner.get(req.method())
223        && let Ok(matched) = method_router.at(&tsr_path)
224        && matched.value.tsr
225      {
226        let handler = move |_req: Request| {
227          let tsr_path = tsr_path.clone();
228          async move {
229            // `tsr_path` is reconstructed from registered route segments and
230            // the incoming URI path. It can technically contain bytes that
231            // are invalid in an HTTP header value (CR/LF/NUL) if the request
232            // path is crafted maliciously — in that case fall back to a
233            // bare 308 without a `Location` header rather than panicking.
234            match http::HeaderValue::from_str(&tsr_path) {
235              Ok(loc) => {
236                let mut resp = empty_status_response(StatusCode::TEMPORARY_REDIRECT);
237                resp.headers_mut().insert(http::header::LOCATION, loc);
238                resp
239              }
240              Err(_) => empty_status_response(StatusCode::TEMPORARY_REDIRECT),
241            }
242          }
243        };
244
245        self
246          .run_with_global_middlewares_for_endpoint(req, BoxHandler::new::<_, (Request,)>(handler))
247          .await
248      } else {
249        // Method-mismatch detection: if the same path is registered for any
250        // *other* method, RFC 9110 mandates 405 with an `Allow` header rather
251        // than 404. This is the cold path; iterating the 9 standard methods
252        // is cheap.
253        let allowed = self.collect_allowed_methods(req.uri().path());
254        if !allowed.is_empty() {
255          let allow_value = join_methods(&allowed);
256          let handler = move |_req: Request| {
257            let allow_value = allow_value.clone();
258            async move {
259              // `allow_value` is built from `Method::as_str()` for the
260              // registered methods, so it only contains ASCII method tokens
261              // — `HeaderValue::from_str` is statically infallible. Use the
262              // fallible API and ignore the impossible error rather than
263              // panicking.
264              let mut resp = empty_status_response(StatusCode::METHOD_NOT_ALLOWED);
265              if let Ok(v) = http::HeaderValue::from_str(&allow_value) {
266                resp.headers_mut().insert(http::header::ALLOW, v);
267              }
268              resp
269            }
270          };
271          self
272            .run_with_global_middlewares_for_endpoint(
273              req,
274              BoxHandler::new::<_, (Request,)>(handler),
275            )
276            .await
277        } else if let Some(handler) = &self.fallback {
278          self
279            .run_with_global_middlewares_for_endpoint(req, handler.clone())
280            .await
281        } else {
282          let handler = |_req: Request| async { empty_status_response(StatusCode::NOT_FOUND) };
283
284          self
285            .run_with_global_middlewares_for_endpoint(
286              req,
287              BoxHandler::new::<_, (Request,)>(handler),
288            )
289            .await
290        }
291      }
292    };
293
294    let response = self.maybe_apply_error_handler(response);
295
296    #[cfg(feature = "signals")]
297    {
298      SignalArbiter::emit_app(
299        Signal::with_capacity(ids::REQUEST_COMPLETED, 3)
300          .meta("method", req_method_str)
301          .meta("path", req_path_str)
302          .meta("status", response.status().as_u16().to_string()),
303      )
304      .await;
305    }
306
307    response
308  }
309
310  /// Applies the appropriate error handler if one is set:
311  /// - 5xx → [`Router::error_handler`]
312  /// - 4xx → [`Router::client_error_handler`]
313  fn maybe_apply_error_handler(&self, response: Response) -> Response {
314    let status = response.status();
315    if status.is_server_error() {
316      if let Some(handler) = &self.error_handler {
317        return handler(response);
318      }
319    } else if status.is_client_error()
320      && let Some(handler) = &self.client_error_handler
321    {
322      return handler(response);
323    }
324    response
325  }
326
327  /// Returns every method that has a route matching the given path.
328  ///
329  /// Used by the 405 / `Allow` cold-path branch in [`Router::dispatch`]; not on
330  /// the fast path. Iterates all standard methods (O(9)) plus any custom ones.
331  fn collect_allowed_methods(&self, path: &str) -> SmallVec<[Method; 4]> {
332    let mut allowed = SmallVec::<[Method; 4]>::new();
333    for (method, m) in self.inner.iter() {
334      if m.at(path).is_ok() {
335        allowed.push(method);
336      }
337    }
338    allowed
339  }
340
341  /// Ensures the request HTTP version satisfies the route's configured protocol guard.
342  /// Returns `Some(Response)` with 505 HTTP Version Not Supported when the request
343  /// doesn't match the guard, otherwise returns `None` to continue dispatch.
344  fn enforce_protocol_guard(route: &Route, req: &Request) -> Option<Response> {
345    if let Some(guard) = route.protocol_guard()
346      && guard != req.version()
347    {
348      return Some(empty_status_response(
349        StatusCode::HTTP_VERSION_NOT_SUPPORTED,
350      ));
351    }
352    None
353  }
354}
355
356/// Joins a slice of HTTP methods into a comma-separated `Allow`-header value.
357fn join_methods(methods: &[Method]) -> String {
358  let mut out = String::with_capacity(methods.len() * 8);
359  for (i, m) in methods.iter().enumerate() {
360    if i > 0 {
361      out.push_str(", ");
362    }
363    out.push_str(m.as_str());
364  }
365  out
366}