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