1use std::{any::TypeId, collections::HashMap, fmt, sync::Arc};
2
3use crate::{
4 Dispatch, Dispatcher, Error, ErrorFormat, Handler, IntoResponse, Middleware, OpenApi,
5 OpenApiDocument, Request, Response, Routes, Scope,
6 error::JsonErrorFormat,
7 middleware::{MiddlewareEntry, MiddlewareTerminal, run as run_middleware},
8 router::{join_paths, validate_scope_prefix},
9};
10
11#[derive(Clone)]
12pub struct Config {
13 prefix: String,
14 error_format: Arc<dyn ErrorFormat>,
15}
16
17impl Default for Config {
18 fn default() -> Self {
19 Self {
20 prefix: String::new(),
21 error_format: Arc::new(JsonErrorFormat),
22 }
23 }
24}
25
26impl fmt::Debug for Config {
27 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28 formatter
29 .debug_struct("Config")
30 .field("prefix", &self.prefix)
31 .finish_non_exhaustive()
32 }
33}
34
35impl Config {
36 pub fn new() -> Self {
37 Self::default()
38 }
39
40 pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
41 self.prefix = prefix.into();
42 self
43 }
44
45 pub fn error_format(mut self, format: impl ErrorFormat) -> Self {
46 self.error_format = Arc::new(format);
47 self
48 }
49}
50
51pub struct Router {
52 dispatcher: Dispatcher,
53 scope: Scope,
54 openapi: Option<PublishedOpenApi>,
55 error_format: Arc<dyn ErrorFormat>,
56}
57
58struct PublishedOpenApi {
59 path: String,
60 configuration: OpenApi,
61 document: OpenApiDocument,
62 scalar_page: String,
63}
64
65impl Router {
66 pub fn new(config: Config, routes: impl Routes) -> Self {
67 let Config {
68 prefix,
69 error_format,
70 } = config;
71 let prefix = normalize_prefix(prefix);
72 validate_scope_prefix(&prefix)
73 .unwrap_or_else(|error| panic!("invalid Router prefix `{prefix}`: {error}"));
74 let mut router = Self {
75 dispatcher: Dispatcher::new(),
76 scope: Scope::new(prefix),
77 openapi: None,
78 error_format,
79 };
80
81 routes.apply(&mut router);
82
83 router
84 }
85
86 pub(crate) fn register<
87 Arguments: 'static,
88 Input: 'static,
89 H: Handler<Arguments, Input> + Send + Sync + 'static,
90 >(
91 &mut self,
92 method: crate::Method,
93 path: &'static str,
94 handler: H,
95 operation: crate::Operation,
96 middlewares: Vec<MiddlewareEntry>,
97 excluded_middlewares: Vec<TypeId>,
98 ) {
99 let path = join_paths(self.scope.prefix(), path);
100 self.dispatcher.register(
101 method,
102 path,
103 handler,
104 operation,
105 middlewares,
106 excluded_middlewares,
107 );
108 }
109
110 pub fn route(mut self, routes: impl Routes) -> Self {
111 routes.apply(&mut self);
112 self.refresh_openapi();
113 self
114 }
115
116 pub fn at(mut self, prefix: impl Into<String>) -> Self {
117 let prefix = normalize_prefix(prefix.into());
118 validate_scope_prefix(&prefix)
119 .unwrap_or_else(|error| panic!("invalid Router mount `{prefix}`: {error}"));
120
121 if prefix.is_empty() {
122 return self;
123 }
124
125 self.dispatcher.prepend(&prefix);
126 self.scope.prepend(&prefix);
127 if let Some(published) = &mut self.openapi {
128 published.path = join_paths(&prefix, &published.path);
129 }
130 self.refresh_openapi();
131 self
132 }
133
134 pub(crate) fn register_router(&mut self, mut router: Router) {
135 let parent_prefix = self.scope.prefix().to_owned();
136 if !parent_prefix.is_empty() {
137 router = router.at(parent_prefix);
138 }
139
140 self.dispatcher.add_scope(router.scope);
141 self.dispatcher.merge(router.dispatcher);
142 }
143
144 pub fn fallback<Arguments: 'static, Input: 'static>(
145 mut self,
146 handler: impl Handler<Arguments, Input> + Send + Sync + 'static,
147 ) -> Self {
148 self.dispatcher.set_fallback(self.scope.prefix(), handler);
149 self.refresh_openapi();
150 self
151 }
152
153 pub fn state<T: Send + Sync + 'static>(mut self, state: T) -> Self {
154 self.scope.state(state);
155 self
156 }
157
158 pub fn body_limit(mut self, limit: usize) -> Self {
159 self.scope.body_limit(limit);
160 self
161 }
162
163 pub fn middleware<M: Middleware>(mut self, middleware: M) -> Self {
164 self.scope.middleware(MiddlewareEntry::new(middleware));
165 self
166 }
167
168 pub fn openapi(mut self, path: impl Into<String>, configuration: OpenApi) -> Self {
169 let path = join_paths(self.scope.prefix(), &path.into());
170 self.publish_openapi(path, configuration);
171 self
172 }
173
174 pub fn openapi_document(&self) -> Option<&OpenApiDocument> {
175 self.openapi.as_ref().map(|published| &published.document)
176 }
177
178 pub async fn handle(&self, mut request: Request) -> Response {
179 let head = request.method == crate::Method::HEAD;
180 let terminal = match self.openapi.as_ref() {
181 Some(published) if request.path == published.path => RouterTerminal::OpenApi(published),
182 _ => RouterTerminal::Dispatch(self.dispatcher.resolve(&request)),
183 };
184 let exclusions = terminal.excluded_middlewares();
185 let mut states = HashMap::new();
186 let mut body_limit = None;
187 let mut middlewares = Vec::new();
188
189 if self.scope.matches(&request.path) {
190 apply_scope(
191 &self.scope,
192 exclusions,
193 &mut states,
194 &mut body_limit,
195 &mut middlewares,
196 );
197 }
198
199 for scope in self.dispatcher.matching_scopes(&request.path) {
200 apply_scope(
201 scope,
202 exclusions,
203 &mut states,
204 &mut body_limit,
205 &mut middlewares,
206 );
207 }
208
209 middlewares.extend(terminal.route_middlewares());
210 request.set_states(states);
211 request.set_body_limit(body_limit);
212
213 let response = run_middleware(&middlewares, &terminal, request).await;
214 let response = self.finalize_error(response);
215
216 if head {
217 response.without_body()
218 } else {
219 response
220 }
221 }
222
223 fn finalize_error(&self, mut response: Response) -> Response {
224 let (error, validation) = match response.take_error() {
225 Some(error) => error.into_parts(),
226 None if (400..=599).contains(&response.status()) => (
227 Error::new(
228 response.status(),
229 format!("http.{}", response.status()),
230 response_error_message(&response),
231 ),
232 None,
233 ),
234 None => return response,
235 };
236 let status = error.status();
237 let mut headers = response.take_headers();
238 remove_representation_headers(&mut headers);
239 let mut formatted = match validation.as_ref() {
240 Some(validation) => self.error_format.format_validation(&error, validation),
241 None => self.error_format.format(&error),
242 };
243
244 if let Some(format_error) = formatted.take_error() {
245 let (format_error, format_validation) = format_error.into_parts();
246 formatted = match format_validation.as_ref() {
247 Some(validation) => JsonErrorFormat.format_validation(&format_error, validation),
248 None => JsonErrorFormat.format(&format_error),
249 };
250 }
251
252 formatted.set_status(status);
253 formatted.merge_headers(headers);
254 formatted
255 }
256
257 fn publish_openapi(&mut self, path: String, configuration: OpenApi) {
258 self.dispatcher.validate_openapi_path(&path);
259 let document = self.build_openapi(&configuration);
260 let scalar_page = configuration.scalar_page(&document);
261 self.openapi = Some(PublishedOpenApi {
262 path,
263 configuration,
264 document,
265 scalar_page,
266 });
267 }
268
269 fn refresh_openapi(&mut self) {
270 let Some((path, configuration)) = self
271 .openapi
272 .as_ref()
273 .map(|published| (published.path.clone(), published.configuration.clone()))
274 else {
275 return;
276 };
277
278 self.publish_openapi(path, configuration);
279 }
280
281 fn build_openapi(&self, configuration: &OpenApi) -> OpenApiDocument {
282 configuration.build(self.dispatcher.openapi_routes())
283 }
284}
285
286fn response_error_message(response: &Response) -> String {
287 std::str::from_utf8(response.body())
288 .ok()
289 .filter(|message| !message.is_empty())
290 .map(str::to_owned)
291 .unwrap_or_else(|| status_message(response.status()).to_owned())
292}
293
294fn status_message(status: u16) -> &'static str {
295 match status {
296 400 => "Bad Request",
297 401 => "Unauthorized",
298 402 => "Payment Required",
299 403 => "Forbidden",
300 404 => "Not Found",
301 405 => "Method Not Allowed",
302 406 => "Not Acceptable",
303 408 => "Request Timeout",
304 409 => "Conflict",
305 410 => "Gone",
306 411 => "Length Required",
307 412 => "Precondition Failed",
308 413 => "Payload Too Large",
309 414 => "URI Too Long",
310 415 => "Unsupported Media Type",
311 416 => "Range Not Satisfiable",
312 417 => "Expectation Failed",
313 422 => "Unprocessable Content",
314 426 => "Upgrade Required",
315 429 => "Too Many Requests",
316 500 => "Internal Server Error",
317 501 => "Not Implemented",
318 502 => "Bad Gateway",
319 503 => "Service Unavailable",
320 504 => "Gateway Timeout",
321 505 => "HTTP Version Not Supported",
322 _ => "HTTP Error",
323 }
324}
325
326fn remove_representation_headers(headers: &mut crate::Headers) {
327 for name in [
328 "Content-Encoding",
329 "Content-Length",
330 "Content-Type",
331 "Transfer-Encoding",
332 ] {
333 headers.remove(name);
334 }
335}
336
337fn normalize_prefix(prefix: String) -> String {
338 if prefix == "/" { String::new() } else { prefix }
339}
340
341fn apply_scope<'scope>(
342 scope: &'scope Scope,
343 exclusions: &[TypeId],
344 states: &mut HashMap<TypeId, std::sync::Arc<dyn std::any::Any + Send + Sync>>,
345 body_limit: &mut Option<usize>,
346 middlewares: &mut Vec<&'scope MiddlewareEntry>,
347) {
348 states.extend(
349 scope
350 .states()
351 .iter()
352 .map(|(type_id, state)| (*type_id, state.clone())),
353 );
354 if let Some(limit) = scope.configured_body_limit() {
355 *body_limit = Some(limit);
356 }
357 middlewares.extend(
358 scope
359 .middlewares()
360 .iter()
361 .filter(|middleware| !exclusions.contains(&middleware.type_id())),
362 );
363}
364
365enum RouterTerminal<'router> {
366 OpenApi(&'router PublishedOpenApi),
367 Dispatch(Dispatch<'router>),
368}
369
370impl RouterTerminal<'_> {
371 fn excluded_middlewares(&self) -> &[TypeId] {
372 match self {
373 Self::OpenApi(_) => &[],
374 Self::Dispatch(dispatch) => dispatch.excluded_middlewares(),
375 }
376 }
377
378 fn route_middlewares(&self) -> &[MiddlewareEntry] {
379 match self {
380 Self::OpenApi(_) => &[],
381 Self::Dispatch(dispatch) => dispatch.route_middlewares(),
382 }
383 }
384}
385
386impl MiddlewareTerminal for RouterTerminal<'_> {
387 fn call(&self, request: Request) -> crate::middleware::MiddlewareFuture<'_> {
388 Box::pin(async move {
389 match self {
390 Self::OpenApi(published) => {
391 let mut response =
392 Response::bytes(200, published.scalar_page.as_bytes().to_vec());
393 response.set_header("Content-Type", "text/html; charset=utf-8");
394
395 let mut response = match request.method.as_str() {
396 "GET" => response,
397 "HEAD" => response,
398 "OPTIONS" => Response::empty(),
399 _ => Error::new(405, "route.method_not_allowed", "Method Not Allowed")
400 .into_response(),
401 };
402 response.set_header("Allow", "GET, HEAD, OPTIONS");
403 response
404 }
405 Self::Dispatch(dispatch) => dispatch.call(request).await,
406 }
407 })
408 }
409}
410
411#[cfg(test)]
412mod tests {
413 use std::{
414 error::Error as StdError,
415 fmt,
416 future::Future,
417 task::{Context, Poll, Waker},
418 };
419
420 use crate::{
421 Config, Error, Form, Headers, Method, Middleware, Next, OpenApi, Path, Query, Request,
422 RequestStream, Response, RouteMethods, Router, StreamError,
423 };
424
425 #[derive(crate::Schema)]
426 struct ItemPath {
427 id: u64,
428 }
429
430 #[derive(crate::Schema)]
431 #[allow(dead_code)]
432 struct SearchQuery {
433 query: String,
434 page: Option<u32>,
435 }
436
437 #[derive(crate::Schema)]
438 struct CreateItem {
439 name: String,
440 }
441
442 #[cfg(feature = "json")]
443 #[derive(crate::Schema, serde::Deserialize, serde::Serialize)]
444 struct JsonItem {
445 name: String,
446 }
447
448 struct EmptyStream;
449
450 impl RequestStream for EmptyStream {
451 fn poll_next(
452 &mut self,
453 _context: &mut Context<'_>,
454 ) -> Poll<Option<Result<(), StreamError>>> {
455 Poll::Ready(None)
456 }
457
458 fn chunk(&self) -> &[u8] {
459 &[]
460 }
461 }
462
463 async fn item(Path(path): Path<ItemPath>, Query(query): Query<SearchQuery>) -> String {
464 format!("{}:{}", path.id, query.query)
465 }
466
467 async fn create(Form(item): Form<CreateItem>) -> String {
468 item.name
469 }
470
471 async fn failure() -> Result<&'static str, Error> {
472 Err(Error::new(
473 409,
474 "sample.failed",
475 "The sample operation failed",
476 ))
477 }
478
479 #[derive(Debug)]
480 struct DatabaseError;
481
482 impl fmt::Display for DatabaseError {
483 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
484 formatter.write_str("database connection lost")
485 }
486 }
487
488 impl StdError for DatabaseError {}
489
490 async fn internal_failure() -> Result<&'static str, Error> {
491 Err(DatabaseError)?
492 }
493
494 async fn raw_error() -> Response {
495 Response::text(418, "custom raw error")
496 }
497
498 struct AddErrorHeader;
499
500 impl Middleware for AddErrorHeader {
501 async fn handle(&self, request: Request, next: Next<'_>) -> Response {
502 let mut response = next.run(request).await;
503 response
504 .headers()
505 .set("X-Error-Scope", "middleware")
506 .unwrap();
507 response
508 }
509 }
510
511 #[cfg(feature = "json")]
512 async fn create_json(crate::Json(item): crate::Json<JsonItem>) -> crate::Json<JsonItem> {
513 crate::Json(item)
514 }
515
516 fn request(method: &str, path: &str) -> Request {
517 Request::from_parts(
518 Method::try_from(method).unwrap(),
519 path,
520 None,
521 Headers::new(),
522 Box::new(EmptyStream),
523 )
524 }
525
526 fn block_on<F: Future>(future: F) -> F::Output {
527 let mut future = std::pin::pin!(future);
528 let waker = Waker::noop();
529 let mut context = Context::from_waker(waker);
530
531 loop {
532 match future.as_mut().poll(&mut context) {
533 Poll::Ready(output) => return output,
534 Poll::Pending => std::thread::yield_now(),
535 }
536 }
537 }
538
539 #[test]
540 fn publishes_route_and_schema_metadata() {
541 let application = Router::new(
542 Config::new(),
543 (
544 "/items/:id"
545 .GET(item)
546 .summary("Read an item")
547 .description("Reads one item by ID")
548 .tag("items")
549 .operation_id("readItem")
550 .openapi(|operation| {
551 operation.response_header(
552 200,
553 "X-Request-Id",
554 "Request identifier",
555 crate::SchemaMetadata::new(crate::SchemaKind::String).format("uuid"),
556 );
557 }),
558 "/items".POST(create),
559 ),
560 )
561 .openapi("/docs", OpenApi::new("Items", "1.0"));
562 let document = application.openapi_document().unwrap().as_str();
563
564 assert!(document.contains("\"/items/{id}\""));
565 assert!(document.contains("\"name\":\"id\",\"in\":\"path\""));
566 assert!(document.contains("\"name\":\"query\",\"in\":\"query\""));
567 assert!(document.contains("application/x-www-form-urlencoded"));
568 assert!(document.contains("\"413\""));
569 assert!(document.contains("\"summary\":\"Read an item\""));
570 assert!(document.contains("\"operationId\":\"readItem\""));
571 assert!(document.contains("\"X-Request-Id\""));
572
573 #[cfg(feature = "json")]
574 serde_json::from_str::<serde_json::Value>(document).unwrap();
575 }
576
577 #[test]
578 fn applies_one_custom_error_format_and_preserves_http_semantics() {
579 let application = Router::new(
580 Config::new().error_format(|error: &Error| {
581 Response::text(200, format!("{}:{}", error.code(), error.message()))
582 }),
583 "/failure".GET(failure),
584 )
585 .middleware(AddErrorHeader);
586 let mut response = block_on(application.handle(request("GET", "/failure")));
587
588 assert_eq!(response.status(), 409);
589 assert_eq!(response.content_type(), Some("text/plain; charset=utf-8"));
590 assert_eq!(
591 response.headers().get("x-error-scope"),
592 Some(b"middleware".as_slice()),
593 );
594 assert_eq!(
595 response.body(),
596 b"sample.failed:The sample operation failed",
597 );
598 }
599
600 #[test]
601 fn lets_a_custom_format_hide_internal_error_messages() {
602 let application = Router::new(
603 Config::new().error_format(|error: &Error| {
604 let message = if error.is_internal() {
605 "Internal Server Error"
606 } else {
607 error.message()
608 };
609
610 Response::text(error.status(), message)
611 }),
612 "/failure".GET(internal_failure),
613 );
614 let response = block_on(application.handle(request("GET", "/failure")));
615
616 assert_eq!(response.status(), 500);
617 assert_eq!(response.body(), b"Internal Server Error");
618 }
619
620 #[test]
621 fn normalizes_raw_error_responses_with_the_default_json_format() {
622 let application = Router::new(Config::new(), "/failure".GET(raw_error));
623 let response = block_on(application.handle(request("GET", "/failure")));
624
625 assert_eq!(response.status(), 418);
626 assert_eq!(response.content_type(), Some("application/json"));
627 assert_eq!(
628 response.body(),
629 br#"{"error":{"code":"http.418","message":"custom raw error","fields":[]}}"#,
630 );
631 }
632
633 #[test]
634 fn suppresses_a_formatted_error_body_for_head_requests() {
635 let application = Router::new(Config::new(), "/failure".GET(failure));
636 let get = block_on(application.handle(request("GET", "/failure")));
637 let expected_length = get.body().len().to_string();
638 let mut head = block_on(application.handle(request("HEAD", "/failure")));
639
640 assert_eq!(head.status(), 409);
641 assert_eq!(
642 head.headers().get("content-length"),
643 Some(expected_length.as_bytes()),
644 );
645 assert!(head.body().is_empty());
646 }
647
648 #[test]
649 fn suppresses_a_not_found_body_for_head_requests() {
650 let application = Router::new(Config::new(), "/ok".GET(|| async { "ok" }));
651 let get = block_on(application.handle(request("GET", "/missing")));
652 let expected_length = get.body().len().to_string();
653 let mut head = block_on(application.handle(request("HEAD", "/missing")));
654
655 assert_eq!(head.status(), 404);
656 assert_eq!(head.content_type(), Some("application/json"));
657 assert_eq!(
658 head.headers().get("content-length"),
659 Some(expected_length.as_bytes()),
660 );
661 assert!(head.body().is_empty());
662 }
663
664 #[test]
665 fn the_parent_router_controls_nested_error_formatting() {
666 let child = Router::new(
667 Config::new().error_format(|error: &Error| {
668 Response::text(200, format!("child:{}", error.code()))
669 }),
670 "/failure".GET(failure),
671 )
672 .at("/child");
673 let application = Router::new(
674 Config::new().error_format(|error: &Error| {
675 Response::text(200, format!("parent:{}", error.code()))
676 }),
677 child,
678 );
679 let response = block_on(application.handle(request("GET", "/child/failure")));
680
681 assert_eq!(response.status(), 409);
682 assert_eq!(response.body(), b"parent:sample.failed");
683 }
684
685 #[test]
686 fn omits_methods_without_openapi_operation_fields() {
687 let propfind = Method::from_bytes(b"PROPFIND").unwrap();
688 let application = Router::new(
689 Config::new(),
690 (
691 "/visible".GET(|| async { "visible" }),
692 "/tunnel".CONNECT(|| async { "tunnel" }),
693 "/properties".on(propfind, || async { "properties" }),
694 ),
695 )
696 .openapi("/docs", OpenApi::new("Methods", "1.0"));
697 let document = application.openapi_document().unwrap().as_str();
698
699 assert!(document.contains("\"/visible\""));
700 assert!(!document.contains("\"/tunnel\""));
701 assert!(!document.contains("\"/properties\""));
702 }
703
704 #[test]
705 fn serves_the_openapi_reference_with_head_and_method_handling() {
706 let application =
707 Router::new(Config::new(), ()).openapi("/docs", OpenApi::new("Empty", "1.0"));
708 let response = block_on(application.handle(request("GET", "/docs")));
709
710 assert_eq!(response.status(), 200);
711 assert_eq!(response.content_type(), Some("text/html; charset=utf-8"));
712 assert!(!response.body().is_empty());
713
714 let response = block_on(application.handle(request("HEAD", "/docs")));
715 assert_eq!(response.status(), 200);
716 assert!(response.body().is_empty());
717
718 let mut response = block_on(application.handle(request("POST", "/docs")));
719 assert_eq!(response.status(), 405);
720 assert_eq!(
721 response.headers().get("allow"),
722 Some(b"GET, HEAD, OPTIONS".as_slice()),
723 );
724 }
725
726 #[test]
727 fn serves_a_scalar_reference_for_the_openapi_document() {
728 let application = Router::new(Config::new(), "/items/:id".GET(item))
729 .openapi("/reference", OpenApi::new("Items & API", "1.0"));
730 let response = block_on(application.handle(request("GET", "/reference")));
731 let body = std::str::from_utf8(response.body()).unwrap();
732
733 assert_eq!(response.status(), 200);
734 assert_eq!(response.content_type(), Some("text/html; charset=utf-8"));
735 assert!(body.contains("<title>Items & API</title>"));
736 assert!(body.contains("Scalar.createApiReference('#app',{content:\"{"));
737 assert!(body.contains("\\\"openapi\\\":\\\"3.1.0\\\""));
738 assert!(body.contains("/items/{id}"));
739 assert!(!body.contains("Scalar.createApiReference('#app',{url:"));
740 assert!(body.contains("https://cdn.jsdelivr.net/npm/@scalar/api-reference"));
741 }
742
743 #[test]
744 fn scopes_openapi_routes_and_reference_with_config_and_mount_prefixes() {
745 let router = Router::new(Config::new().prefix("/v1"), "/items/:id".GET(item))
746 .openapi("/docs", OpenApi::new("Items", "1.0"))
747 .at("/service");
748 let document = router.openapi_document().unwrap().as_str();
749
750 assert!(document.contains("\"/service/v1/items/{id}\""));
751 assert_eq!(
752 block_on(router.handle(request("GET", "/service/v1/docs"))).status(),
753 200,
754 );
755 assert_eq!(
756 block_on(router.handle(request("GET", "/v1/service/docs"))).status(),
757 404,
758 );
759 }
760
761 #[cfg(feature = "json")]
762 #[test]
763 fn derives_json_request_and_response_components_from_schema() {
764 let application = Router::new(Config::new(), "/items".POST(create_json))
765 .openapi("/docs", OpenApi::new("Items", "1.0"));
766 let document = application.openapi_document().unwrap().as_str();
767
768 assert!(document.contains("\"application/json\":{\"schema\":{\"$ref\":"));
769 assert!(document.contains("\"components\":{\"schemas\":"));
770 serde_json::from_str::<serde_json::Value>(document).unwrap();
771 }
772}