1#![forbid(unsafe_code)]
2
3use blazingly_core::{
4 AppDefinition, BackgroundTask, BackgroundTaskError, BodyStreamError, HttpMethod, HttpUpgrade,
5 InputSource, OperationDescriptor, ResponseHeader, SecuritySchemeDescriptor, StreamingBody,
6};
7use blazingly_executor::{
8 DependencyError, ExecutableApp, ExecutionOutcome, FromInvocation,
9 HttpRequestParts as InvocationRequestParts, InputRejection, InvocationControl, InvocationInput,
10};
11use blazingly_json::{Value, json};
12use blazingly_openapi::{OpenApiAssetResponse, OpenApiConfig, OpenApiService};
13use serde::Serialize;
14use serde::de::DeserializeOwned;
15use std::any::{Any, TypeId};
16use std::borrow::Cow;
17use std::cell::{OnceCell, RefCell};
18use std::collections::{BTreeMap, HashMap};
19use std::fmt;
20use std::future::Future;
21use std::hash::{BuildHasherDefault, Hasher};
22use std::net::{IpAddr, SocketAddr};
23use std::rc::Rc;
24use std::str::Utf8Error;
25
26pub const DEFAULT_MAX_BODY_BYTES: usize = 1024 * 1024;
27
28pub const EMIT_VARIABLE: &str = "BLAZINGLY_EMIT";
31
32#[derive(Clone, Debug, Eq, PartialEq)]
34pub struct Request {
35 method: HttpMethod,
36 target: String,
37 headers: BTreeMap<String, String>,
38 body: Vec<u8>,
39 peer_addr: Option<SocketAddr>,
40 scheme: String,
41}
42
43pub trait HttpRequestView {
48 fn method(&self) -> HttpMethod;
49 fn target(&self) -> &str;
50 fn header_value(&self, name: &str, index: usize) -> Option<&str>;
51 fn body(&self) -> &[u8];
52
53 fn take_body_stream(&self) -> Option<StreamingBody> {
55 None
56 }
57
58 fn peer_addr(&self) -> Option<SocketAddr> {
60 None
61 }
62
63 #[allow(clippy::unnecessary_literal_bound)]
65 fn scheme(&self) -> &str {
66 "http"
67 }
68}
69
70pub struct HttpRequestContext<'request> {
76 request: &'request dyn HttpRequestView,
77 client_ip: Option<IpAddr>,
78 scheme: Cow<'request, str>,
79 host: Option<Cow<'request, str>>,
80 extensions: Vec<(TypeId, Box<dyn Any>)>,
81}
82
83impl<'request> HttpRequestContext<'request> {
84 fn new(request: &'request dyn HttpRequestView) -> Self {
85 Self {
86 request,
87 client_ip: request.peer_addr().map(|address| address.ip()),
88 scheme: Cow::Borrowed(request.scheme()),
89 host: None,
90 extensions: Vec::new(),
91 }
92 }
93
94 #[must_use]
95 pub fn request(&self) -> &dyn HttpRequestView {
96 self.request
97 }
98
99 #[must_use]
100 pub fn client_ip(&self) -> Option<IpAddr> {
101 self.client_ip
102 }
103
104 pub fn set_client_ip(&mut self, client_ip: IpAddr) {
105 self.client_ip = Some(client_ip);
106 }
107
108 #[must_use]
109 pub fn scheme(&self) -> &str {
110 &self.scheme
111 }
112
113 pub fn set_scheme(&mut self, scheme: impl Into<String>) {
114 self.scheme = Cow::Owned(scheme.into());
115 }
116
117 #[must_use]
118 pub fn host(&self) -> Option<&str> {
119 self.host
120 .as_deref()
121 .or_else(|| self.request.header_value("host", 0))
122 }
123
124 pub fn set_host(&mut self, host: impl Into<String>) {
125 self.host = Some(Cow::Owned(host.into()));
126 }
127
128 pub fn insert_extension<T: 'static>(&mut self, value: T) {
129 let type_id = TypeId::of::<T>();
130 if let Some((_, existing)) = self
131 .extensions
132 .iter_mut()
133 .find(|(existing, _)| *existing == type_id)
134 {
135 *existing = Box::new(value);
136 } else {
137 self.extensions.push((type_id, Box::new(value)));
138 }
139 }
140
141 #[must_use]
142 pub fn extension<T: 'static>(&self) -> Option<&T> {
143 self.extension_by_id(TypeId::of::<T>())?.downcast_ref()
144 }
145
146 #[must_use]
149 pub fn connection_info(&self) -> ConnectionInfo {
150 ConnectionInfo {
151 client_ip: self.client_ip,
152 scheme: self.scheme.as_ref().to_owned(),
153 host: self.host().map(str::to_owned),
154 }
155 }
156
157 fn extension_by_id(&self, type_id: TypeId) -> Option<&dyn Any> {
158 self.extensions
159 .iter()
160 .find(|(existing, _)| *existing == type_id)
161 .map(|(_, value)| value.as_ref())
162 }
163}
164
165#[derive(Clone, Debug, Eq, PartialEq)]
171pub struct ConnectionInfo {
172 client_ip: Option<IpAddr>,
173 scheme: String,
174 host: Option<String>,
175}
176
177impl ConnectionInfo {
178 #[must_use]
180 pub const fn client_ip(&self) -> Option<IpAddr> {
181 self.client_ip
182 }
183
184 #[must_use]
186 pub fn scheme(&self) -> &str {
187 &self.scheme
188 }
189
190 #[must_use]
192 pub fn host(&self) -> Option<&str> {
193 self.host.as_deref()
194 }
195
196 fn from_request(request: &(impl HttpRequestView + ?Sized)) -> Self {
197 Self {
198 client_ip: request.peer_addr().map(|address| address.ip()),
199 scheme: request.scheme().to_owned(),
200 host: request.header_value("host", 0).map(str::to_owned),
201 }
202 }
203}
204
205pub trait HttpMiddleware {
211 fn on_request(&self, _context: &mut HttpRequestContext<'_>) -> Option<Response> {
213 None
214 }
215
216 fn on_operation(
218 &self,
219 _context: &mut HttpRequestContext<'_>,
220 _operation: &OperationDescriptor,
221 _security_schemes: &[SecuritySchemeDescriptor],
222 ) -> Option<Response> {
223 None
224 }
225
226 fn on_response(
229 &self,
230 _context: &HttpRequestContext<'_>,
231 _operation: Option<&OperationDescriptor>,
232 _response: &mut Response,
233 ) {
234 }
235
236 fn verifies_security(&self) -> bool {
244 true
245 }
246}
247
248type OperationFilter = Rc<dyn Fn(&str) -> bool>;
249
250#[derive(Clone)]
251enum OperationPredicate {
252 Exact(Box<str>),
253 Prefix(Box<str>),
254 Filter(OperationFilter),
255}
256
257impl OperationPredicate {
258 fn matches(&self, operation_id: &str) -> bool {
259 match self {
260 Self::Exact(expected) => operation_id == expected.as_ref(),
261 Self::Prefix(prefix) => operation_id.starts_with(prefix.as_ref()),
262 Self::Filter(filter) => filter(operation_id),
263 }
264 }
265}
266
267#[derive(Clone, Default)]
281pub struct MiddlewareScope {
282 prefixes: Vec<Box<str>>,
283 operations: Vec<OperationPredicate>,
284}
285
286impl MiddlewareScope {
287 #[must_use]
289 pub const fn all() -> Self {
290 Self {
291 prefixes: Vec::new(),
292 operations: Vec::new(),
293 }
294 }
295
296 #[must_use]
298 pub fn prefix(prefix: &str) -> Self {
299 Self::all().with_prefix(prefix)
300 }
301
302 #[must_use]
304 pub fn operation(operation_id: &str) -> Self {
305 Self::all().with_operation(operation_id)
306 }
307
308 #[must_use]
312 pub fn with_prefix(mut self, prefix: &str) -> Self {
313 self.prefixes.push(normalize_prefix(prefix));
314 self
315 }
316
317 #[must_use]
319 pub fn with_operation(mut self, operation_id: &str) -> Self {
320 self.operations
321 .push(OperationPredicate::Exact(Box::from(operation_id)));
322 self
323 }
324
325 #[must_use]
328 pub fn with_operation_prefix(mut self, prefix: &str) -> Self {
329 self.operations
330 .push(OperationPredicate::Prefix(Box::from(prefix)));
331 self
332 }
333
334 #[must_use]
337 pub fn with_operation_filter<Filter>(mut self, filter: Filter) -> Self
338 where
339 Filter: Fn(&str) -> bool + 'static,
340 {
341 self.operations
342 .push(OperationPredicate::Filter(Rc::new(filter)));
343 self
344 }
345
346 #[must_use]
348 pub fn is_global(&self) -> bool {
349 self.prefixes.is_empty() && self.operations.is_empty()
350 }
351
352 #[must_use]
354 pub fn matches_request(&self, path: &str) -> bool {
355 self.operations.is_empty() && self.matches_path(path)
356 }
357
358 #[must_use]
360 pub fn matches_operation(&self, path: &str, operation_id: &str) -> bool {
361 self.matches_path(path)
362 && (self.operations.is_empty()
363 || self
364 .operations
365 .iter()
366 .any(|predicate| predicate.matches(operation_id)))
367 }
368
369 fn matches_path(&self, path: &str) -> bool {
370 self.prefixes.is_empty()
371 || self
372 .prefixes
373 .iter()
374 .any(|prefix| path_has_prefix(path, prefix))
375 }
376}
377
378impl fmt::Debug for MiddlewareScope {
379 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
380 formatter
381 .debug_struct("MiddlewareScope")
382 .field("prefixes", &self.prefixes)
383 .field("operations", &self.operations.len())
384 .finish()
385 }
386}
387
388fn normalize_prefix(prefix: &str) -> Box<str> {
389 let trimmed = prefix.trim_end_matches('/');
390 if trimmed.is_empty() {
391 Box::from("")
392 } else if trimmed.starts_with('/') {
393 Box::from(trimmed)
394 } else {
395 Box::from(format!("/{trimmed}"))
396 }
397}
398
399fn path_has_prefix(path: &str, prefix: &str) -> bool {
400 let Some(rest) = path.strip_prefix(prefix) else {
401 return false;
402 };
403 rest.is_empty() || rest.starts_with('/')
404}
405
406struct ScopedMiddleware {
407 scope: MiddlewareScope,
408 layer: Rc<dyn HttpMiddleware>,
409}
410
411impl ScopedMiddleware {
412 fn matches(&self, path: &str, operation: Option<&OperationDescriptor>) -> bool {
413 operation.map_or_else(
414 || self.scope.matches_request(path),
415 |operation| {
416 self.scope
417 .matches_operation(path, operation.contract.id.as_str())
418 },
419 )
420 }
421}
422
423#[derive(Clone, Copy, Debug, Eq, PartialEq)]
425pub enum HttpErrorSource {
426 Routing,
428 Request,
430 Rejection,
432 Domain,
434 Internal,
437}
438
439pub struct HttpError<'dispatch> {
441 source: HttpErrorSource,
442 status: u16,
443 code: &'dispatch str,
444 message: &'dispatch str,
445 method: HttpMethod,
446 path: &'dispatch str,
447 operation: Option<&'dispatch OperationDescriptor>,
448}
449
450impl HttpError<'_> {
451 #[must_use]
453 pub const fn source(&self) -> HttpErrorSource {
454 self.source
455 }
456
457 #[must_use]
459 pub const fn status(&self) -> u16 {
460 self.status
461 }
462
463 #[must_use]
465 pub const fn code(&self) -> &str {
466 self.code
467 }
468
469 #[must_use]
471 pub const fn message(&self) -> &str {
472 self.message
473 }
474
475 #[must_use]
477 pub const fn method(&self) -> HttpMethod {
478 self.method
479 }
480
481 #[must_use]
483 pub const fn path(&self) -> &str {
484 self.path
485 }
486
487 #[must_use]
489 pub const fn operation(&self) -> Option<&OperationDescriptor> {
490 self.operation
491 }
492}
493
494pub trait HttpErrorHandler {
510 fn on_error(&self, error: &HttpError<'_>, response: &mut Response);
512}
513
514#[derive(Clone, Debug, Default)]
526pub struct BackgroundTasks {
527 tasks: Rc<RefCell<Vec<BackgroundTask>>>,
528}
529
530impl BackgroundTasks {
531 #[must_use]
532 pub fn new() -> Self {
533 Self::default()
534 }
535
536 pub fn add_task(&self, task: BackgroundTask) {
538 self.tasks.borrow_mut().push(task);
539 }
540
541 pub fn add<Task, TaskFuture>(&self, task: Task)
543 where
544 Task: FnOnce() -> TaskFuture + 'static,
545 TaskFuture: Future<Output = Result<(), BackgroundTaskError>> + 'static,
546 {
547 self.add_task(BackgroundTask::new(task));
548 }
549
550 pub fn add_infallible<Task, TaskFuture>(&self, task: Task)
552 where
553 Task: FnOnce() -> TaskFuture + 'static,
554 TaskFuture: Future<Output = ()> + 'static,
555 {
556 self.add_task(BackgroundTask::infallible(task));
557 }
558
559 #[must_use]
561 pub fn len(&self) -> usize {
562 self.tasks.borrow().len()
563 }
564
565 #[must_use]
567 pub fn is_empty(&self) -> bool {
568 self.tasks.borrow().is_empty()
569 }
570
571 #[must_use]
573 pub fn take(&self) -> Vec<BackgroundTask> {
574 std::mem::take(&mut self.tasks.borrow_mut())
575 }
576}
577
578impl FromInvocation for BackgroundTasks {
579 fn from_invocation(
580 input: &InvocationInput<'_>,
581 name: &str,
582 _required: bool,
583 ) -> Result<Self, InputRejection> {
584 let InvocationInput::Http(request) = input else {
585 return Err(InputRejection::new(
586 500,
587 "background_tasks_transport_mismatch",
588 "after-response tasks are available only through HTTP",
589 ));
590 };
591 request
592 .extension(TypeId::of::<Self>())
593 .and_then(<dyn Any>::downcast_ref::<Self>)
594 .cloned()
595 .ok_or_else(|| {
596 InputRejection::new(
597 500,
598 "background_tasks_unavailable",
599 format!("this transport installed no after-response tasks for `{name}`"),
600 )
601 })
602 }
603}
604
605impl Request {
606 #[must_use]
607 pub fn new(method: HttpMethod, target: impl Into<String>) -> Self {
608 Self {
609 method,
610 target: target.into(),
611 headers: BTreeMap::new(),
612 body: Vec::new(),
613 peer_addr: None,
614 scheme: "http".to_owned(),
615 }
616 }
617
618 #[must_use]
619 pub fn get(target: impl Into<String>) -> Self {
620 Self::new(HttpMethod::Get, target)
621 }
622
623 #[must_use]
624 pub fn head(target: impl Into<String>) -> Self {
625 Self::new(HttpMethod::Head, target)
626 }
627
628 #[must_use]
629 pub fn post(target: impl Into<String>) -> Self {
630 Self::new(HttpMethod::Post, target)
631 }
632
633 #[must_use]
634 pub fn put(target: impl Into<String>) -> Self {
635 Self::new(HttpMethod::Put, target)
636 }
637
638 #[must_use]
639 pub fn patch(target: impl Into<String>) -> Self {
640 Self::new(HttpMethod::Patch, target)
641 }
642
643 #[must_use]
644 pub fn delete(target: impl Into<String>) -> Self {
645 Self::new(HttpMethod::Delete, target)
646 }
647
648 #[must_use]
649 pub fn options(target: impl Into<String>) -> Self {
650 Self::new(HttpMethod::Options, target)
651 }
652
653 #[must_use]
654 pub fn trace(target: impl Into<String>) -> Self {
655 Self::new(HttpMethod::Trace, target)
656 }
657
658 #[must_use]
659 pub fn connect(target: impl Into<String>) -> Self {
660 Self::new(HttpMethod::Connect, target)
661 }
662
663 #[must_use]
664 pub const fn method(&self) -> HttpMethod {
665 self.method
666 }
667
668 #[must_use]
669 pub fn target(&self) -> &str {
670 &self.target
671 }
672
673 #[must_use]
674 pub fn path(&self) -> &str {
675 self.target
676 .split_once('?')
677 .map_or(self.target.as_str(), |(path, _)| path)
678 }
679
680 #[must_use]
681 pub fn header(mut self, name: impl AsRef<str>, value: impl Into<String>) -> Self {
682 self.headers
683 .insert(normalize_header_name(name.as_ref()), value.into());
684 self
685 }
686
687 #[must_use]
688 pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
689 self.body = body.into();
690 self
691 }
692
693 #[must_use]
695 pub const fn peer_addr(mut self, peer_addr: SocketAddr) -> Self {
696 self.peer_addr = Some(peer_addr);
697 self
698 }
699
700 #[must_use]
702 pub fn scheme(mut self, scheme: impl Into<String>) -> Self {
703 self.scheme = scheme.into();
704 self
705 }
706
707 pub fn json(mut self, value: &impl Serialize) -> Result<Self, blazingly_json::Error> {
713 self.body = blazingly_json::to_vec(value)?;
714 self.headers
715 .insert("content-type".to_owned(), "application/json".to_owned());
716 Ok(self)
717 }
718
719 #[must_use]
720 pub fn get_header(&self, name: &str) -> Option<&str> {
721 self.headers
722 .get(name)
723 .or_else(|| {
724 self.headers
725 .iter()
726 .find(|(header, _)| header.eq_ignore_ascii_case(name))
727 .map(|(_, value)| value)
728 })
729 .map(String::as_str)
730 }
731
732 #[must_use]
733 pub fn headers(&self) -> &BTreeMap<String, String> {
734 &self.headers
735 }
736
737 #[must_use]
738 pub fn body_bytes(&self) -> &[u8] {
739 &self.body
740 }
741}
742
743impl HttpRequestView for Request {
744 fn method(&self) -> HttpMethod {
745 self.method()
746 }
747
748 fn target(&self) -> &str {
749 self.target()
750 }
751
752 fn header_value(&self, name: &str, index: usize) -> Option<&str> {
753 self.headers
754 .iter()
755 .filter(|(header, _)| header_name_matches(header, name))
756 .nth(index)
757 .map(|(_, value)| value.as_str())
758 }
759
760 fn body(&self) -> &[u8] {
761 self.body_bytes()
762 }
763
764 fn peer_addr(&self) -> Option<SocketAddr> {
765 self.peer_addr
766 }
767
768 fn scheme(&self) -> &str {
769 &self.scheme
770 }
771}
772
773#[derive(Debug)]
775pub struct Response {
776 status: u16,
777 headers: ResponseHeaders,
778 body: Vec<u8>,
779 stream: Option<StreamingBody>,
780 upgrade: Option<HttpUpgrade>,
781 background: Vec<BackgroundTask>,
782}
783
784impl Response {
785 #[must_use]
787 pub fn from_bytes(status: u16, body: impl Into<Vec<u8>>) -> Self {
788 Self {
789 status,
790 headers: ResponseHeaders::empty(),
791 body: body.into(),
792 stream: None,
793 upgrade: None,
794 background: Vec::new(),
795 }
796 }
797
798 #[must_use]
800 pub fn empty(status: u16) -> Self {
801 Self::from_bytes(status, Vec::new())
802 }
803
804 #[must_use]
807 pub fn payload_too_large(max_body_bytes: usize) -> Self {
808 BodyRejection::PayloadTooLarge { max_body_bytes }.into_response()
809 }
810
811 #[must_use]
812 pub const fn status(&self) -> u16 {
813 self.status
814 }
815
816 pub const fn set_status(&mut self, status: u16) {
818 self.status = status;
819 }
820
821 #[must_use]
822 pub fn get_header(&self, name: &str) -> Option<&str> {
823 self.headers.get(name)
824 }
825
826 pub fn headers(&self) -> impl Iterator<Item = (&str, &str)> {
827 self.headers.iter()
828 }
829
830 pub fn set_header(&mut self, name: impl AsRef<str>, value: impl Into<String>) {
833 self.headers.insert(
834 Cow::Owned(normalize_header_name(name.as_ref())),
835 Cow::Owned(value.into()),
836 );
837 }
838
839 pub fn remove_header(&mut self, name: &str) {
841 self.headers.remove(name);
842 }
843
844 #[must_use]
845 pub fn body(&self) -> &[u8] {
846 &self.body
847 }
848
849 pub fn replace_body(&mut self, body: impl Into<Vec<u8>>) -> bool {
852 if self.stream.is_some() {
853 return false;
854 }
855 self.body = body.into();
856 true
857 }
858
859 #[must_use]
861 pub const fn is_streaming(&self) -> bool {
862 self.stream.is_some()
863 }
864
865 #[must_use]
871 pub fn take_body_stream(&mut self) -> Option<StreamingBody> {
872 self.stream.take()
873 }
874
875 pub fn set_body_stream(&mut self, stream: StreamingBody) {
880 self.body.clear();
881 self.stream = Some(stream);
882 }
883
884 #[must_use]
885 pub const fn is_upgrade(&self) -> bool {
886 self.upgrade.is_some()
887 }
888
889 pub fn take_upgrade(&mut self) -> Option<HttpUpgrade> {
891 self.upgrade.take()
892 }
893
894 pub fn take_background_tasks(&mut self) -> Vec<BackgroundTask> {
897 std::mem::take(&mut self.background)
898 }
899
900 pub async fn run_background(&mut self) -> Result<(), BackgroundTaskError> {
907 let mut first_error = None;
908 for task in self.take_background_tasks() {
909 if let Err(error) = task.run().await
910 && first_error.is_none()
911 {
912 first_error = Some(error);
913 }
914 }
915 first_error.map_or(Ok(()), Err)
916 }
917
918 #[must_use]
920 pub fn exact_body_length(&self) -> Option<u64> {
921 self.stream.as_ref().map_or_else(
922 || u64::try_from(self.body.len()).ok(),
923 StreamingBody::exact_length,
924 )
925 }
926
927 pub async fn next_body_chunk(&mut self) -> Option<Result<Vec<u8>, BodyStreamError>> {
932 match self.stream.as_mut() {
933 Some(stream) => stream.next_chunk().await,
934 None => None,
935 }
936 }
937
938 pub async fn collect_body(mut self, limit: usize) -> Result<Vec<u8>, CollectBodyError> {
948 if self.stream.is_none() {
949 if self.body.len() > limit {
950 return Err(CollectBodyError::LimitExceeded { limit });
951 }
952 return Ok(self.body);
953 }
954
955 let initial_capacity = self
956 .exact_body_length()
957 .and_then(|length| usize::try_from(length).ok())
958 .unwrap_or(0)
959 .min(limit);
960 let mut body = Vec::with_capacity(initial_capacity);
961 while let Some(chunk) = self.next_body_chunk().await {
962 let chunk = chunk.map_err(CollectBodyError::Stream)?;
963 if body.len().saturating_add(chunk.len()) > limit {
964 return Err(CollectBodyError::LimitExceeded { limit });
965 }
966 body.extend_from_slice(&chunk);
967 }
968 Ok(body)
969 }
970
971 pub fn text(&self) -> Result<&str, Utf8Error> {
977 std::str::from_utf8(&self.body)
978 }
979
980 pub fn json<T: DeserializeOwned>(&self) -> Result<T, blazingly_json::Error> {
986 blazingly_json::from_slice(&self.body)
987 }
988}
989
990#[derive(Clone, Debug, Eq, PartialEq)]
992pub enum CollectBodyError {
993 Stream(BodyStreamError),
994 LimitExceeded { limit: usize },
995}
996
997impl fmt::Display for CollectBodyError {
998 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
999 match self {
1000 Self::Stream(error) => error.fmt(formatter),
1001 Self::LimitExceeded { limit } => {
1002 write!(
1003 formatter,
1004 "response body exceeds the {limit}-byte collection limit"
1005 )
1006 }
1007 }
1008 }
1009}
1010
1011impl std::error::Error for CollectBodyError {}
1012
1013pub struct HttpApp {
1018 app: ExecutableApp,
1019 router: Router,
1020 max_body_bytes: usize,
1021 openapi: Option<OpenApiService>,
1022 middleware: Vec<ScopedMiddleware>,
1023 error_handlers: Vec<Rc<dyn HttpErrorHandler>>,
1024 allow_unverified_security_schemes: bool,
1025}
1026
1027impl HttpApp {
1028 #[must_use]
1048 pub fn new(app: ExecutableApp) -> Self {
1049 emit_and_exit_if_requested(&app);
1050 let router = Router::new(&app);
1051 Self {
1052 app,
1053 router,
1054 max_body_bytes: DEFAULT_MAX_BODY_BYTES,
1055 openapi: None,
1056 middleware: Vec::new(),
1057 error_handlers: Vec::new(),
1058 allow_unverified_security_schemes: false,
1059 }
1060 }
1061
1062 #[must_use]
1063 pub const fn with_max_body_bytes(mut self, max_body_bytes: usize) -> Self {
1064 self.max_body_bytes = max_body_bytes;
1065 self
1066 }
1067
1068 #[must_use]
1076 pub const fn with_unverified_security_schemes(mut self, allow: bool) -> Self {
1077 self.allow_unverified_security_schemes = allow;
1078 self
1079 }
1080
1081 pub async fn startup(&self) -> Result<(), DependencyError> {
1087 self.app.startup().await
1088 }
1089
1090 pub async fn shutdown(&self) -> Result<(), DependencyError> {
1096 self.app.shutdown().await
1097 }
1098
1099 #[must_use]
1101 pub fn with_openapi(mut self, config: OpenApiConfig) -> Self {
1102 self.openapi = Some(OpenApiService::new(self.app.definition(), config));
1103 self
1104 }
1105
1106 #[must_use]
1108 pub fn with_middleware(mut self, middleware: impl HttpMiddleware + 'static) -> Self {
1109 self.middleware.push(ScopedMiddleware {
1110 scope: MiddlewareScope::all(),
1111 layer: Rc::new(middleware),
1112 });
1113 self
1114 }
1115
1116 #[must_use]
1118 pub fn with_shared_middleware(mut self, middleware: Rc<dyn HttpMiddleware>) -> Self {
1119 self.middleware.push(ScopedMiddleware {
1120 scope: MiddlewareScope::all(),
1121 layer: middleware,
1122 });
1123 self
1124 }
1125
1126 #[must_use]
1129 pub fn with_scoped_middleware(
1130 mut self,
1131 scope: MiddlewareScope,
1132 middleware: impl HttpMiddleware + 'static,
1133 ) -> Self {
1134 self.middleware.push(ScopedMiddleware {
1135 scope,
1136 layer: Rc::new(middleware),
1137 });
1138 self
1139 }
1140
1141 #[must_use]
1144 pub fn with_shared_scoped_middleware(
1145 mut self,
1146 scope: MiddlewareScope,
1147 middleware: Rc<dyn HttpMiddleware>,
1148 ) -> Self {
1149 self.middleware.push(ScopedMiddleware {
1150 scope,
1151 layer: middleware,
1152 });
1153 self
1154 }
1155
1156 #[must_use]
1158 pub fn with_error_handler(mut self, handler: impl HttpErrorHandler + 'static) -> Self {
1159 self.error_handlers.push(Rc::new(handler));
1160 self
1161 }
1162
1163 #[must_use]
1165 pub fn with_shared_error_handler(mut self, handler: Rc<dyn HttpErrorHandler>) -> Self {
1166 self.error_handlers.push(handler);
1167 self
1168 }
1169
1170 pub async fn call(&self, request: Request) -> Response {
1171 self.call_view(&request).await
1172 }
1173
1174 pub async fn call_view(&self, request: &impl HttpRequestView) -> Response {
1175 let dispatcher = self.dispatcher();
1176 dispatcher.dispatch(request, None).await
1177 }
1178
1179 pub async fn call_view_controlled(
1180 &self,
1181 request: &impl HttpRequestView,
1182 control: InvocationControl,
1183 ) -> Response {
1184 let dispatcher = self.dispatcher();
1185 dispatcher.dispatch(request, Some(control)).await
1186 }
1187
1188 fn dispatcher(&self) -> Dispatcher<'_> {
1189 Dispatcher {
1190 app: &self.app,
1191 router: &self.router,
1192 max_body_bytes: self.max_body_bytes,
1193 openapi: self.openapi.as_ref(),
1194 middleware: &self.middleware,
1195 error_handlers: &self.error_handlers,
1196 allow_unverified_security_schemes: self.allow_unverified_security_schemes,
1197 }
1198 }
1199
1200 #[must_use]
1205 pub fn request_body_source(&self, method: HttpMethod, target: &str) -> Option<InputSource> {
1206 let path = target.split_once('?').map_or(target, |(path, _)| path);
1207 self.router
1208 .recognize(method, path)
1209 .ok()
1210 .and_then(|route| route.body_source())
1211 }
1212}
1213
1214fn emit_and_exit_if_requested(app: &ExecutableApp) {
1222 static EMITTED: std::sync::Once = std::sync::Once::new();
1223 let Some(mode) = std::env::var_os(EMIT_VARIABLE) else {
1224 return;
1225 };
1226 if mode.is_empty() {
1227 return;
1228 }
1229 let mut code = 0;
1230 EMITTED.call_once(|| code = emit(app, &mode.to_string_lossy()));
1231 std::process::exit(code);
1232}
1233
1234fn emit(app: &ExecutableApp, mode: &str) -> i32 {
1238 use std::io::Write as _;
1239 let output = match mode {
1240 "openapi" => match openapi_document_text(app.definition()) {
1241 Ok(document) => document,
1242 Err(error) => {
1243 eprintln!("error: the OpenAPI document could not be serialized: {error}");
1244 return 1;
1245 }
1246 },
1247 "routes" => routes_table_text(app.definition()),
1248 unknown => {
1249 eprintln!("error: {EMIT_VARIABLE} must be `openapi` or `routes`, not `{unknown}`");
1250 return 2;
1251 }
1252 };
1253 let mut stdout = std::io::stdout().lock();
1254 if stdout.write_all(output.as_bytes()).is_err() || stdout.flush().is_err() {
1255 return 1;
1256 }
1257 0
1258}
1259
1260fn openapi_document_text(definition: &AppDefinition) -> Result<String, blazingly_json::Error> {
1262 let document = blazingly_openapi::to_value(definition);
1263 let mut text = blazingly_json::to_string_pretty(&document)?;
1264 text.push('\n');
1265 Ok(text)
1266}
1267
1268fn routes_table_text(definition: &AppDefinition) -> String {
1270 use std::fmt::Write as _;
1271 let mut table = String::from("METHOD\tPATH\tOPERATION\tSUMMARY\n");
1272 for operation in definition.operations() {
1273 let _ = writeln!(
1274 table,
1275 "{}\t{}\t{}\t{}",
1276 operation.http.method.as_str(),
1277 operation.http.path,
1278 operation.contract.id.as_str(),
1279 operation.contract.summary
1280 );
1281 }
1282 table
1283}
1284
1285pub struct TestApp<'app> {
1287 app: &'app ExecutableApp,
1288 router: Router,
1289 max_body_bytes: usize,
1290 openapi: Option<OpenApiService>,
1291 middleware: Vec<ScopedMiddleware>,
1292 error_handlers: Vec<Rc<dyn HttpErrorHandler>>,
1293 allow_unverified_security_schemes: bool,
1294}
1295
1296impl<'app> TestApp<'app> {
1297 #[must_use]
1298 pub fn new(app: &'app ExecutableApp) -> Self {
1299 Self {
1300 app,
1301 router: Router::new(app),
1302 max_body_bytes: DEFAULT_MAX_BODY_BYTES,
1303 openapi: None,
1304 middleware: Vec::new(),
1305 error_handlers: Vec::new(),
1306 allow_unverified_security_schemes: false,
1307 }
1308 }
1309
1310 #[must_use]
1311 pub const fn with_max_body_bytes(mut self, max_body_bytes: usize) -> Self {
1312 self.max_body_bytes = max_body_bytes;
1313 self
1314 }
1315
1316 #[must_use]
1322 pub const fn with_unverified_security_schemes(mut self, allow: bool) -> Self {
1323 self.allow_unverified_security_schemes = allow;
1324 self
1325 }
1326
1327 pub async fn startup(&self) -> Result<(), DependencyError> {
1333 self.app.startup().await
1334 }
1335
1336 pub async fn shutdown(&self) -> Result<(), DependencyError> {
1342 self.app.shutdown().await
1343 }
1344
1345 #[must_use]
1347 pub fn with_openapi(mut self, config: OpenApiConfig) -> Self {
1348 self.openapi = Some(OpenApiService::new(self.app.definition(), config));
1349 self
1350 }
1351
1352 #[must_use]
1354 pub fn with_middleware(mut self, middleware: impl HttpMiddleware + 'static) -> Self {
1355 self.middleware.push(ScopedMiddleware {
1356 scope: MiddlewareScope::all(),
1357 layer: Rc::new(middleware),
1358 });
1359 self
1360 }
1361
1362 #[must_use]
1364 pub fn with_shared_middleware(mut self, middleware: Rc<dyn HttpMiddleware>) -> Self {
1365 self.middleware.push(ScopedMiddleware {
1366 scope: MiddlewareScope::all(),
1367 layer: middleware,
1368 });
1369 self
1370 }
1371
1372 #[must_use]
1375 pub fn with_scoped_middleware(
1376 mut self,
1377 scope: MiddlewareScope,
1378 middleware: impl HttpMiddleware + 'static,
1379 ) -> Self {
1380 self.middleware.push(ScopedMiddleware {
1381 scope,
1382 layer: Rc::new(middleware),
1383 });
1384 self
1385 }
1386
1387 #[must_use]
1390 pub fn with_shared_scoped_middleware(
1391 mut self,
1392 scope: MiddlewareScope,
1393 middleware: Rc<dyn HttpMiddleware>,
1394 ) -> Self {
1395 self.middleware.push(ScopedMiddleware {
1396 scope,
1397 layer: middleware,
1398 });
1399 self
1400 }
1401
1402 #[must_use]
1404 pub fn with_error_handler(mut self, handler: impl HttpErrorHandler + 'static) -> Self {
1405 self.error_handlers.push(Rc::new(handler));
1406 self
1407 }
1408
1409 #[must_use]
1411 pub fn with_shared_error_handler(mut self, handler: Rc<dyn HttpErrorHandler>) -> Self {
1412 self.error_handlers.push(handler);
1413 self
1414 }
1415
1416 pub async fn call(&self, request: Request) -> Response {
1417 let dispatcher = self.dispatcher();
1418 dispatcher.dispatch(&request, None).await
1419 }
1420
1421 pub async fn call_controlled(&self, request: Request, control: InvocationControl) -> Response {
1422 let dispatcher = self.dispatcher();
1423 dispatcher.dispatch(&request, Some(control)).await
1424 }
1425
1426 fn dispatcher(&self) -> Dispatcher<'_> {
1427 Dispatcher {
1428 app: self.app,
1429 router: &self.router,
1430 max_body_bytes: self.max_body_bytes,
1431 openapi: self.openapi.as_ref(),
1432 middleware: &self.middleware,
1433 error_handlers: &self.error_handlers,
1434 allow_unverified_security_schemes: self.allow_unverified_security_schemes,
1435 }
1436 }
1437}
1438
1439struct Dispatcher<'app> {
1441 app: &'app ExecutableApp,
1442 router: &'app Router,
1443 max_body_bytes: usize,
1444 openapi: Option<&'app OpenApiService>,
1445 middleware: &'app [ScopedMiddleware],
1446 error_handlers: &'app [Rc<dyn HttpErrorHandler>],
1447 allow_unverified_security_schemes: bool,
1448}
1449
1450struct DispatchSite<'dispatch> {
1452 method: HttpMethod,
1453 path: &'dispatch str,
1454 operation: Option<&'dispatch OperationDescriptor>,
1455}
1456
1457impl Dispatcher<'_> {
1458 async fn dispatch<RequestView>(
1459 &self,
1460 request: &RequestView,
1461 control: Option<InvocationControl>,
1462 ) -> Response
1463 where
1464 RequestView: HttpRequestView,
1465 {
1466 if self.middleware.is_empty() {
1467 return self.dispatch_unlayered(request, control).await;
1468 }
1469 let middleware = self.middleware;
1470 let target = request.target();
1471 let mut site = DispatchSite {
1472 method: request.method(),
1473 path: target.split_once('?').map_or(target, |(path, _)| path),
1474 operation: None,
1475 };
1476 let mut context = HttpRequestContext::new(request);
1477 for layer in middleware {
1478 if layer.scope.matches_request(site.path)
1479 && let Some(response) = layer.layer.on_request(&mut context)
1480 {
1481 return complete_response(middleware, &context, &site, response);
1482 }
1483 }
1484
1485 if validate_url_encoding(target).is_err() {
1486 let response = self.fail(&site, invalid_url_encoding_failure());
1487 return complete_response(middleware, &context, &site, response);
1488 }
1489 if let Some(response) = self
1490 .openapi
1491 .and_then(|service| service.handle(site.method, site.path))
1492 {
1493 return complete_response(middleware, &context, &site, openapi_response(response));
1494 }
1495 let recognized = match self.router.recognize(site.method, site.path) {
1496 Ok(recognized) => recognized,
1497 Err(error) => {
1498 let response = self.fail(&site, route_miss_failure(&error));
1499 return complete_response(middleware, &context, &site, response);
1500 }
1501 };
1502 let Some(operation) = self.app.operation_at(recognized.operation_index()) else {
1503 let response = self.fail(&site, internal_failure());
1504 return complete_response(middleware, &context, &site, response);
1505 };
1506 let descriptor = operation.descriptor();
1507 site.operation = Some(descriptor);
1508 for layer in middleware {
1509 if layer.matches(site.path, site.operation)
1510 && let Some(response) = layer.layer.on_operation(
1511 &mut context,
1512 descriptor,
1513 self.app.definition().security_schemes(),
1514 )
1515 {
1516 return complete_response(middleware, &context, &site, response);
1517 }
1518 }
1519 if let Some(failure) = self.security_guard(site.path, descriptor) {
1520 let response = self.fail(&site, failure);
1521 return complete_response(middleware, &context, &site, response);
1522 }
1523 if let Some(body_source) = recognized.body_source() {
1524 match validate_body(request, self.max_body_bytes, body_source) {
1525 Ok(()) => {}
1526 Err(rejection) => {
1527 let response = self.fail(&site, rejection.into_failure());
1528 return complete_response(middleware, &context, &site, response);
1529 }
1530 }
1531 }
1532 let request_parts = RoutedRequestParts {
1533 request,
1534 route: &recognized,
1535 context: Some(&context),
1536 connection: OnceCell::new(),
1537 background: OnceCell::new(),
1538 };
1539 let outcome = if let Some(control) = control {
1540 operation
1541 .invoke_http_controlled(&request_parts, control)
1542 .await
1543 } else {
1544 operation.invoke_http(&request_parts).await
1545 };
1546 let mut response = match outcome_result(outcome) {
1547 Ok(response) => response,
1548 Err(failure) => self.fail(&site, failure),
1549 };
1550 response.background.extend(request_parts.scheduled_tasks());
1551 complete_response(middleware, &context, &site, response)
1552 }
1553
1554 async fn dispatch_unlayered<RequestView>(
1555 &self,
1556 request: &RequestView,
1557 control: Option<InvocationControl>,
1558 ) -> Response
1559 where
1560 RequestView: HttpRequestView + ?Sized,
1561 {
1562 let target = request.target();
1563 let mut site = DispatchSite {
1564 method: request.method(),
1565 path: target.split_once('?').map_or(target, |(path, _)| path),
1566 operation: None,
1567 };
1568 if validate_url_encoding(target).is_err() {
1569 return self.fail(&site, invalid_url_encoding_failure());
1570 }
1571 if let Some(response) = self
1572 .openapi
1573 .and_then(|service| service.handle(site.method, site.path))
1574 {
1575 return openapi_response(response);
1576 }
1577 let recognized = match self.router.recognize(site.method, site.path) {
1578 Ok(recognized) => recognized,
1579 Err(error) => return self.fail(&site, route_miss_failure(&error)),
1580 };
1581 let Some(operation) = self.app.operation_at(recognized.operation_index()) else {
1582 return self.fail(&site, internal_failure());
1583 };
1584 site.operation = Some(operation.descriptor());
1585 if let Some(failure) = self.security_guard(site.path, operation.descriptor()) {
1586 return self.fail(&site, failure);
1587 }
1588 if let Some(body_source) = recognized.body_source() {
1589 match validate_body(request, self.max_body_bytes, body_source) {
1590 Ok(()) => {}
1591 Err(rejection) => return self.fail(&site, rejection.into_failure()),
1592 }
1593 }
1594 let request_parts = RoutedRequestParts {
1595 request,
1596 route: &recognized,
1597 context: None,
1598 connection: OnceCell::new(),
1599 background: OnceCell::new(),
1600 };
1601 let outcome = if let Some(control) = control {
1602 operation
1603 .invoke_http_controlled(&request_parts, control)
1604 .await
1605 } else {
1606 operation.invoke_http(&request_parts).await
1607 };
1608 let mut response = match outcome_result(outcome) {
1609 Ok(response) => response,
1610 Err(failure) => self.fail(&site, failure),
1611 };
1612 response.background.extend(request_parts.scheduled_tasks());
1613 response
1614 }
1615
1616 fn fail(&self, site: &DispatchSite<'_>, mut failure: Failure) -> Response {
1619 let mut response = failure.response();
1620 if self.error_handlers.is_empty() {
1621 return response;
1622 }
1623 let error = HttpError {
1624 source: failure.source,
1625 status: failure.status,
1626 code: &failure.code,
1627 message: &failure.message,
1628 method: site.method,
1629 path: site.path,
1630 operation: site.operation,
1631 };
1632 for handler in self.error_handlers {
1633 handler.on_error(&error, &mut response);
1634 }
1635 if failure.source == HttpErrorSource::Domain {
1637 response.status = failure.status;
1638 }
1639 response
1640 }
1641
1642 fn security_guard(&self, path: &str, descriptor: &OperationDescriptor) -> Option<Failure> {
1649 if self.allow_unverified_security_schemes || descriptor.contract.security.is_empty() {
1650 return None;
1651 }
1652 if self
1653 .middleware
1654 .iter()
1655 .any(|layer| layer.layer.verifies_security() && layer.matches(path, Some(descriptor)))
1656 {
1657 return None;
1658 }
1659 Some(Failure::new(
1660 HttpErrorSource::Internal,
1661 500,
1662 "security_verifier_missing",
1663 "the operation declares a security scheme with no registered verifier",
1664 ))
1665 }
1666}
1667
1668struct Failure {
1670 source: HttpErrorSource,
1671 status: u16,
1672 code: Cow<'static, str>,
1673 message: Cow<'static, str>,
1674 details: Option<Value>,
1675 headers: Vec<ResponseHeader>,
1676}
1677
1678impl Failure {
1679 fn new(
1680 source: HttpErrorSource,
1681 status: u16,
1682 code: impl Into<Cow<'static, str>>,
1683 message: impl Into<Cow<'static, str>>,
1684 ) -> Self {
1685 Self {
1686 source,
1687 status,
1688 code: code.into(),
1689 message: message.into(),
1690 details: None,
1691 headers: Vec::new(),
1692 }
1693 }
1694
1695 fn with_details(mut self, details: Value) -> Self {
1696 self.details = Some(details);
1697 self
1698 }
1699
1700 fn with_headers(mut self, headers: Vec<ResponseHeader>) -> Self {
1701 self.headers = headers;
1702 self
1703 }
1704
1705 fn response(&mut self) -> Response {
1706 let response = error_response(self.status, &self.code, &self.message, self.details.take());
1707 with_outcome_headers(response, std::mem::take(&mut self.headers))
1708 }
1709
1710 fn into_response(mut self) -> Response {
1711 self.response()
1712 }
1713}
1714
1715fn invalid_url_encoding_failure() -> Failure {
1716 Failure::new(
1717 HttpErrorSource::Request,
1718 400,
1719 "invalid_url_encoding",
1720 "request target contains invalid percent encoding",
1721 )
1722}
1723
1724fn route_miss_failure(error: &RouteError) -> Failure {
1725 match error {
1726 RouteError::MethodNotAllowed { allowed } => {
1727 let allow = allowed
1728 .iter()
1729 .map(|method| method.as_str())
1730 .collect::<Vec<_>>()
1731 .join(", ");
1732 Failure::new(
1733 HttpErrorSource::Routing,
1734 405,
1735 "method_not_allowed",
1736 "HTTP method not allowed",
1737 )
1738 .with_headers(vec![ResponseHeader::new("allow", allow)])
1739 }
1740 RouteError::NotFound => Failure::new(
1741 HttpErrorSource::Routing,
1742 404,
1743 "not_found",
1744 "HTTP route not found",
1745 ),
1746 }
1747}
1748
1749fn complete_response(
1750 middleware: &[ScopedMiddleware],
1751 context: &HttpRequestContext<'_>,
1752 site: &DispatchSite<'_>,
1753 mut response: Response,
1754) -> Response {
1755 for layer in middleware.iter().rev() {
1756 if layer.matches(site.path, site.operation) {
1757 layer
1758 .layer
1759 .on_response(context, site.operation, &mut response);
1760 }
1761 }
1762 response
1763}
1764
1765fn openapi_response(asset: OpenApiAssetResponse) -> Response {
1766 let mut response = Response {
1767 status: asset.status,
1768 headers: ResponseHeaders::empty(),
1769 body: asset.body,
1770 stream: None,
1771 upgrade: None,
1772 background: Vec::new(),
1773 };
1774 for (name, value) in asset.headers {
1775 response = response.with_header(name, value);
1776 }
1777 response
1778}
1779
1780const METHODS: [HttpMethod; METHOD_COUNT] = [
1786 HttpMethod::Get,
1787 HttpMethod::Head,
1788 HttpMethod::Post,
1789 HttpMethod::Put,
1790 HttpMethod::Patch,
1791 HttpMethod::Delete,
1792 HttpMethod::Options,
1793 HttpMethod::Trace,
1794 HttpMethod::Connect,
1795];
1796
1797const METHOD_COUNT: usize = 9;
1798
1799const fn method_index(method: HttpMethod) -> usize {
1800 match method {
1801 HttpMethod::Get => 0,
1802 HttpMethod::Head => 1,
1803 HttpMethod::Post => 2,
1804 HttpMethod::Put => 3,
1805 HttpMethod::Patch => 4,
1806 HttpMethod::Delete => 5,
1807 HttpMethod::Options => 6,
1808 HttpMethod::Trace => 7,
1809 HttpMethod::Connect => 8,
1810 }
1811}
1812
1813const PATH_HASH_SEED: u64 = 0x51_7c_c1_b7_27_22_0a_95;
1814
1815#[derive(Default)]
1824struct PathHasher {
1825 hash: u64,
1826}
1827
1828impl PathHasher {
1829 fn mix(&mut self, word: u64) {
1830 self.hash = (self.hash.rotate_left(5) ^ word).wrapping_mul(PATH_HASH_SEED);
1831 }
1832}
1833
1834impl Hasher for PathHasher {
1835 fn write(&mut self, bytes: &[u8]) {
1836 let mut word = [0_u8; 8];
1837 let mut index = 0;
1838 while index + 8 <= bytes.len() {
1839 word.copy_from_slice(&bytes[index..index + 8]);
1840 self.mix(u64::from_le_bytes(word));
1841 index += 8;
1842 }
1843 let tail = &bytes[index..];
1844 if !tail.is_empty() {
1845 word = [0; 8];
1846 word[..tail.len()].copy_from_slice(tail);
1847 self.mix(u64::from_le_bytes(word));
1848 }
1849 self.mix(u64::try_from(bytes.len()).unwrap_or(u64::MAX));
1851 }
1852
1853 fn finish(&self) -> u64 {
1854 self.hash
1855 }
1856}
1857
1858type PathMap<Value> = HashMap<Box<str>, Value, BuildHasherDefault<PathHasher>>;
1859
1860struct StaticChild {
1862 head: u8,
1863 node: usize,
1864 segment: Box<str>,
1865}
1866
1867#[derive(Default)]
1868struct RouteNode {
1869 static_children: Vec<StaticChild>,
1872 parameter_child: Option<usize>,
1873 methods: u16,
1875 endpoints: [usize; METHOD_COUNT],
1876}
1877
1878impl RouteNode {
1879 fn static_child(&self, segment: &str) -> Option<usize> {
1880 let head = head_byte(segment);
1881 self.static_children
1882 .iter()
1883 .find(|child| child.head == head && &*child.segment == segment)
1884 .map(|child| child.node)
1885 }
1886}
1887
1888#[derive(Clone)]
1889struct CompiledEndpoint {
1890 operation_index: usize,
1891 parameter_names: Vec<String>,
1892 body_source: Option<InputSource>,
1893}
1894
1895#[derive(Default)]
1901struct StaticSlot {
1902 methods: u16,
1904 endpoints: [usize; METHOD_COUNT],
1905}
1906
1907pub struct Router {
1909 nodes: Vec<RouteNode>,
1910 endpoints: Vec<CompiledEndpoint>,
1912 static_routes: PathMap<StaticSlot>,
1913 static_filter: [u64; 2],
1917}
1918
1919impl Router {
1920 #[must_use]
1921 pub fn new(app: &ExecutableApp) -> Self {
1922 let mut router = Self {
1923 nodes: vec![RouteNode::default()],
1924 endpoints: Vec::new(),
1925 static_routes: PathMap::default(),
1926 static_filter: [0; 2],
1927 };
1928 for descriptor in app.definition().operations() {
1929 let Some(operation_index) = app.operation_index(&descriptor.contract.id) else {
1930 continue;
1931 };
1932 router.insert(descriptor, operation_index);
1933 }
1934 router
1935 }
1936
1937 fn insert(&mut self, descriptor: &OperationDescriptor, operation_index: usize) {
1938 let endpoint = CompiledEndpoint {
1939 operation_index,
1940 parameter_names: route_segments(&descriptor.http.path)
1941 .filter_map(path_parameter_name)
1942 .map(str::to_owned)
1943 .collect(),
1944 body_source: body_source(descriptor),
1945 };
1946 let method = method_index(descriptor.http.method);
1947 if endpoint.parameter_names.is_empty() {
1948 let path = descriptor.http.path.as_str();
1949 let (word, bit) = static_filter_slot(path);
1950 self.static_filter[word] |= bit;
1951 let slot = self.endpoints.len();
1952 self.endpoints.push(endpoint);
1953 let entry = self.static_routes.entry(path.into()).or_default();
1954 entry.endpoints[method] = slot;
1955 entry.methods |= 1_u16 << method;
1956 return;
1957 }
1958
1959 let mut node_index = 0;
1960 for segment in route_segments(&descriptor.http.path) {
1961 node_index = if path_parameter_name(segment).is_some() {
1962 if let Some(child) = self.nodes[node_index].parameter_child {
1963 child
1964 } else {
1965 let child = self.nodes.len();
1966 self.nodes.push(RouteNode::default());
1967 self.nodes[node_index].parameter_child = Some(child);
1968 child
1969 }
1970 } else if let Some(child) = self.nodes[node_index].static_child(segment) {
1971 child
1972 } else {
1973 let child = self.nodes.len();
1974 self.nodes.push(RouteNode::default());
1975 self.nodes[node_index].static_children.push(StaticChild {
1976 head: head_byte(segment),
1977 node: child,
1978 segment: segment.into(),
1979 });
1980 child
1981 };
1982 }
1983 let slot = self.endpoints.len();
1984 self.endpoints.push(endpoint);
1985 let node = &mut self.nodes[node_index];
1986 node.endpoints[method] = slot;
1987 node.methods |= 1_u16 << method;
1988 }
1989
1990 pub fn recognize<'router, 'path>(
1997 &'router self,
1998 method: HttpMethod,
1999 path: &'path str,
2000 ) -> Result<RouteMatch<'router, 'path>, RouteError> {
2001 let method = method_index(method);
2002 let (word, bit) = static_filter_slot(path);
2003 let slot = if self.static_filter[word] & bit == 0 {
2004 None
2005 } else {
2006 self.static_routes.get(path)
2007 };
2008 if let Some(slot) = slot
2009 && slot.methods & (1_u16 << method) != 0
2010 && let Some(endpoint) = self.endpoints.get(slot.endpoints[method])
2011 {
2012 return Ok(RouteMatch {
2013 endpoint,
2014 captures: CapturedSegments::new(),
2015 });
2016 }
2017
2018 let mut captures = CapturedSegments::new();
2019 let mut other_methods = false;
2020 if let Some(endpoint) = self.walk(
2021 0,
2022 Some(trim_leading_slash(path)),
2023 method,
2024 &mut captures,
2025 &mut other_methods,
2026 ) {
2027 return Ok(RouteMatch { endpoint, captures });
2028 }
2029
2030 let static_methods = slot.map_or(0, |slot| slot.methods);
2031 if static_methods == 0 && !other_methods {
2032 return Err(RouteError::NotFound);
2033 }
2034 Err(RouteError::MethodNotAllowed {
2035 allowed: self.allowed_methods(path, static_methods),
2036 })
2037 }
2038
2039 fn walk<'router, 'path>(
2046 &'router self,
2047 node_index: usize,
2048 rest: Option<&'path str>,
2049 method: usize,
2050 captures: &mut CapturedSegments<'path>,
2051 other_methods: &mut bool,
2052 ) -> Option<&'router CompiledEndpoint> {
2053 let node = &self.nodes[node_index];
2054 let Some(rest) = rest else {
2055 if node.methods & (1_u16 << method) == 0 {
2056 *other_methods |= node.methods != 0;
2057 return None;
2058 }
2059 return self.endpoints.get(node.endpoints[method]);
2060 };
2061 let (segment, tail) = split_segment(rest);
2062
2063 if let Some(child) = node.static_child(segment)
2064 && let Some(found) = self.walk(child, tail, method, captures, other_methods)
2065 {
2066 return Some(found);
2067 }
2068 let child = node.parameter_child?;
2069 captures.push(segment);
2070 if let Some(found) = self.walk(child, tail, method, captures, other_methods) {
2071 return Some(found);
2072 }
2073 captures.pop();
2074 None
2075 }
2076
2077 fn allowed_methods(&self, path: &str, static_methods: u16) -> Vec<HttpMethod> {
2080 let mut methods = static_methods;
2081 self.collect_methods(0, Some(trim_leading_slash(path)), &mut methods);
2082 METHODS
2083 .iter()
2084 .enumerate()
2085 .filter(|(index, _)| methods & (1_u16 << index) != 0)
2086 .map(|(_, method)| *method)
2087 .collect()
2088 }
2089
2090 fn collect_methods(&self, node_index: usize, rest: Option<&str>, methods: &mut u16) {
2091 let node = &self.nodes[node_index];
2092 let Some(rest) = rest else {
2093 *methods |= node.methods;
2094 return;
2095 };
2096 let (segment, tail) = split_segment(rest);
2097 if let Some(child) = node.static_child(segment) {
2098 self.collect_methods(child, tail, methods);
2099 }
2100 if let Some(child) = node.parameter_child {
2101 self.collect_methods(child, tail, methods);
2102 }
2103 }
2104}
2105
2106fn route_segments(path: &str) -> std::str::Split<'_, char> {
2107 trim_leading_slash(path).split('/')
2108}
2109
2110fn trim_leading_slash(path: &str) -> &str {
2111 path.strip_prefix('/').unwrap_or(path)
2112}
2113
2114fn split_segment(rest: &str) -> (&str, Option<&str>) {
2117 match rest.as_bytes().iter().position(|byte| *byte == b'/') {
2118 Some(index) => (&rest[..index], Some(&rest[index + 1..])),
2119 None => (rest, None),
2120 }
2121}
2122
2123const fn head_byte(segment: &str) -> u8 {
2126 match segment.as_bytes().first() {
2127 Some(byte) => *byte,
2128 None => 0,
2129 }
2130}
2131
2132fn static_filter_slot(path: &str) -> (usize, u64) {
2137 let byte = path.as_bytes().get(1).copied().unwrap_or(0);
2138 let slot = (usize::from(byte).wrapping_mul(3) ^ path.len()) & 127;
2139 (slot >> 6, 1_u64 << (slot & 63))
2140}
2141
2142#[derive(Clone, Debug, Eq, PartialEq)]
2144pub enum RouteError {
2145 MethodNotAllowed { allowed: Vec<HttpMethod> },
2146 NotFound,
2147}
2148
2149pub struct RouteMatch<'router, 'path> {
2151 endpoint: &'router CompiledEndpoint,
2152 captures: CapturedSegments<'path>,
2153}
2154
2155impl RouteMatch<'_, '_> {
2156 #[must_use]
2157 pub const fn operation_index(&self) -> usize {
2158 self.endpoint.operation_index
2159 }
2160
2161 #[must_use]
2162 pub const fn requires_json(&self) -> bool {
2163 matches!(self.endpoint.body_source, Some(InputSource::Json))
2164 }
2165
2166 #[must_use]
2167 pub const fn body_source(&self) -> Option<InputSource> {
2168 self.endpoint.body_source
2169 }
2170
2171 #[must_use]
2172 pub fn path_parameter(&self, name: &str) -> Option<Cow<'_, str>> {
2173 self.endpoint
2174 .parameter_names
2175 .iter()
2176 .position(|parameter| parameter == name)
2177 .and_then(|position| self.captures.get(position))
2178 .and_then(|value| decode_url_component(value, false).ok())
2179 }
2180}
2181
2182const INLINE_PATH_PARAMETERS: usize = 8;
2183
2184#[derive(Clone)]
2188enum CapturedSegments<'path> {
2189 Empty,
2190 Inline {
2191 values: [Option<&'path str>; INLINE_PATH_PARAMETERS],
2192 len: usize,
2193 },
2194 Heap(Vec<&'path str>),
2195}
2196
2197impl<'path> CapturedSegments<'path> {
2198 const fn new() -> Self {
2199 Self::Empty
2200 }
2201
2202 fn push(&mut self, value: &'path str) {
2203 match self {
2204 Self::Empty => {
2205 let mut values = [None; INLINE_PATH_PARAMETERS];
2206 values[0] = Some(value);
2207 *self = Self::Inline { values, len: 1 };
2208 }
2209 Self::Inline { values, len } if *len < INLINE_PATH_PARAMETERS => {
2210 values[*len] = Some(value);
2211 *len += 1;
2212 }
2213 Self::Inline { values, len } => {
2214 let mut heap = values[..*len]
2215 .iter()
2216 .filter_map(|value| *value)
2217 .collect::<Vec<_>>();
2218 heap.push(value);
2219 *self = Self::Heap(heap);
2220 }
2221 Self::Heap(values) => values.push(value),
2222 }
2223 }
2224
2225 fn pop(&mut self) {
2228 match self {
2229 Self::Empty => {}
2230 Self::Inline { len, .. } => *len = len.saturating_sub(1),
2231 Self::Heap(values) => {
2232 values.pop();
2233 }
2234 }
2235 }
2236
2237 fn get(&self, index: usize) -> Option<&'path str> {
2238 match self {
2239 Self::Inline { values, len } if index < *len => values[index],
2240 Self::Empty | Self::Inline { .. } => None,
2241 Self::Heap(values) => values.get(index).copied(),
2242 }
2243 }
2244}
2245
2246struct RoutedRequestParts<'request, 'router, 'path, 'context, RequestView: ?Sized> {
2247 request: &'request RequestView,
2248 route: &'request RouteMatch<'router, 'path>,
2249 context: Option<&'context HttpRequestContext<'request>>,
2250 connection: OnceCell<ConnectionInfo>,
2251 background: OnceCell<BackgroundTasks>,
2252}
2253
2254impl<RequestView> RoutedRequestParts<'_, '_, '_, '_, RequestView>
2255where
2256 RequestView: HttpRequestView + ?Sized,
2257{
2258 fn connection_info(&self) -> &ConnectionInfo {
2260 self.connection.get_or_init(|| {
2261 self.context.map_or_else(
2262 || ConnectionInfo::from_request(self.request),
2263 HttpRequestContext::connection_info,
2264 )
2265 })
2266 }
2267
2268 fn background_tasks(&self) -> &BackgroundTasks {
2271 self.background.get_or_init(BackgroundTasks::new)
2272 }
2273
2274 fn scheduled_tasks(&self) -> Vec<BackgroundTask> {
2276 self.background
2277 .get()
2278 .map_or_else(Vec::new, BackgroundTasks::take)
2279 }
2280}
2281
2282impl<RequestView> InvocationRequestParts for RoutedRequestParts<'_, '_, '_, '_, RequestView>
2283where
2284 RequestView: HttpRequestView + ?Sized,
2285{
2286 fn value(&self, source: InputSource, name: &str, index: usize) -> Option<Cow<'_, str>> {
2287 match source {
2288 InputSource::Path if index == 0 => self.route.path_parameter(name),
2289 InputSource::Query => query_value(self.request.target(), name, index),
2290 InputSource::Header => self.request.header_value(name, index).map(Cow::Borrowed),
2291 InputSource::Cookie => cookie_value(self.request, name, index),
2292 InputSource::Form => form_value(self.request.body(), name, index),
2293 InputSource::Path
2294 | InputSource::Json
2295 | InputSource::Multipart
2296 | InputSource::File
2297 | InputSource::Stream => None,
2298 }
2299 }
2300
2301 fn body(&self) -> &[u8] {
2302 self.request.body()
2303 }
2304
2305 fn take_body_stream(&self) -> Option<StreamingBody> {
2306 self.request.take_body_stream()
2307 }
2308
2309 fn extension(&self, type_id: TypeId) -> Option<&dyn Any> {
2310 if let Some(value) = self
2311 .context
2312 .and_then(|context| context.extension_by_id(type_id))
2313 {
2314 return Some(value);
2315 }
2316 if type_id == TypeId::of::<ConnectionInfo>() {
2317 return Some(self.connection_info());
2318 }
2319 if type_id == TypeId::of::<BackgroundTasks>() {
2320 return Some(self.background_tasks());
2321 }
2322 None
2323 }
2324
2325 fn method(&self) -> Option<HttpMethod> {
2326 Some(self.request.method())
2327 }
2328
2329 fn path(&self) -> Option<&str> {
2330 let target = self.request.target();
2331 Some(target.split('?').next().unwrap_or(target))
2332 }
2333
2334 fn peer_addr(&self) -> Option<SocketAddr> {
2335 self.request.peer_addr()
2336 }
2337
2338 fn scheme(&self) -> Option<&str> {
2339 Some(self.connection_info().scheme())
2340 }
2341
2342 fn host(&self) -> Option<&str> {
2343 self.connection_info().host()
2344 }
2345}
2346
2347fn body_source(descriptor: &OperationDescriptor) -> Option<InputSource> {
2348 descriptor
2349 .contract
2350 .inputs
2351 .iter()
2352 .map(|input| input.source)
2353 .find(|source| {
2354 matches!(
2355 source,
2356 InputSource::Json
2357 | InputSource::Form
2358 | InputSource::Multipart
2359 | InputSource::File
2360 | InputSource::Stream
2361 )
2362 })
2363}
2364
2365fn cookie_value<'request>(
2366 request: &'request (impl HttpRequestView + ?Sized),
2367 name: &str,
2368 index: usize,
2369) -> Option<Cow<'request, str>> {
2370 let mut header_index = 0;
2371 let mut found = 0;
2372 while let Some(header) = request.header_value("cookie", header_index) {
2373 for cookie in header.split(';') {
2374 let (cookie_name, value) = cookie.trim().split_once('=').unwrap_or((cookie.trim(), ""));
2375 if cookie_name == name {
2376 if found == index {
2377 return Some(Cow::Borrowed(value));
2378 }
2379 found += 1;
2380 }
2381 }
2382 header_index += 1;
2383 }
2384 None
2385}
2386
2387fn form_value<'body>(body: &'body [u8], name: &str, index: usize) -> Option<Cow<'body, str>> {
2388 let body = std::str::from_utf8(body).ok()?;
2389 let mut found = 0;
2390 for pair in body.split('&').filter(|pair| !pair.is_empty()) {
2391 let (raw_name, raw_value) = pair.split_once('=').unwrap_or((pair, ""));
2392 let decoded_name = decode_url_component(raw_name, true).ok()?;
2393 if decoded_name == name {
2394 if found == index {
2395 return decode_url_component(raw_value, true).ok();
2396 }
2397 found += 1;
2398 }
2399 }
2400 None
2401}
2402
2403fn path_parameter_name(segment: &str) -> Option<&str> {
2404 segment
2405 .strip_prefix('{')
2406 .and_then(|segment| segment.strip_suffix('}'))
2407 .filter(|name| !name.is_empty())
2408}
2409
2410fn query_value<'target>(
2411 target: &'target str,
2412 name: &str,
2413 index: usize,
2414) -> Option<Cow<'target, str>> {
2415 let (_, query) = target.split_once('?')?;
2416 let mut found = 0;
2417 for pair in query.split('&').filter(|pair| !pair.is_empty()) {
2418 let (raw_name, raw_value) = pair.split_once('=').unwrap_or((pair, ""));
2419 let decoded_name = decode_url_component(raw_name, true).ok()?;
2420 if decoded_name == name {
2421 if found == index {
2422 return decode_url_component(raw_value, true).ok();
2423 }
2424 found += 1;
2425 }
2426 }
2427 None
2428}
2429
2430fn header_name_matches(header: &str, argument: &str) -> bool {
2431 header.bytes().eq(argument.bytes().map(|byte| {
2432 if byte == b'_' {
2433 b'-'
2434 } else {
2435 byte.to_ascii_lowercase()
2436 }
2437 }))
2438}
2439
2440fn validate_url_encoding(target: &str) -> Result<(), ()> {
2441 let path = target.split_once('?').map_or(target, |(path, _)| path);
2442 decode_url_component(path, false)?;
2443 if let Some((_, query)) = target.split_once('?') {
2444 for pair in query.split('&') {
2445 let (name, value) = pair.split_once('=').unwrap_or((pair, ""));
2446 decode_url_component(name, true)?;
2447 decode_url_component(value, true)?;
2448 }
2449 }
2450 Ok(())
2451}
2452
2453fn decode_url_component(value: &str, plus_as_space: bool) -> Result<Cow<'_, str>, ()> {
2454 if !value.as_bytes().contains(&b'%') && (!plus_as_space || !value.as_bytes().contains(&b'+')) {
2455 return Ok(Cow::Borrowed(value));
2456 }
2457 let bytes = value.as_bytes();
2458 let mut decoded = Vec::with_capacity(bytes.len());
2459 let mut index = 0;
2460 while index < bytes.len() {
2461 match bytes[index] {
2462 b'%' => {
2463 let high = bytes
2464 .get(index + 1)
2465 .copied()
2466 .and_then(hex_value)
2467 .ok_or(())?;
2468 let low = bytes
2469 .get(index + 2)
2470 .copied()
2471 .and_then(hex_value)
2472 .ok_or(())?;
2473 decoded.push((high << 4) | low);
2474 index += 3;
2475 }
2476 b'+' if plus_as_space => {
2477 decoded.push(b' ');
2478 index += 1;
2479 }
2480 byte => {
2481 decoded.push(byte);
2482 index += 1;
2483 }
2484 }
2485 }
2486 String::from_utf8(decoded).map(Cow::Owned).map_err(|_| ())
2487}
2488
2489const fn hex_value(byte: u8) -> Option<u8> {
2490 match byte {
2491 b'0'..=b'9' => Some(byte - b'0'),
2492 b'a'..=b'f' => Some(byte - b'a' + 10),
2493 b'A'..=b'F' => Some(byte - b'A' + 10),
2494 _ => None,
2495 }
2496}
2497
2498enum BodyRejection {
2499 PayloadTooLarge { max_body_bytes: usize },
2500 UnsupportedMediaType,
2501}
2502
2503impl BodyRejection {
2504 fn into_failure(self) -> Failure {
2505 match self {
2506 Self::PayloadTooLarge { max_body_bytes } => Failure::new(
2507 HttpErrorSource::Request,
2508 413,
2509 "payload_too_large",
2510 "request body exceeds the configured limit",
2511 )
2512 .with_details(json!({ "maxBytes": max_body_bytes })),
2513 Self::UnsupportedMediaType => Failure::new(
2514 HttpErrorSource::Request,
2515 415,
2516 "unsupported_media_type",
2517 "request body media type does not match the operation input",
2518 ),
2519 }
2520 }
2521
2522 fn into_response(self) -> Response {
2523 self.into_failure().into_response()
2524 }
2525}
2526
2527fn validate_body(
2528 request: &(impl HttpRequestView + ?Sized),
2529 max_body_bytes: usize,
2530 source: InputSource,
2531) -> Result<(), BodyRejection> {
2532 if request.body().len() > max_body_bytes {
2533 return Err(BodyRejection::PayloadTooLarge { max_body_bytes });
2534 }
2535 if source == InputSource::Stream {
2536 return Ok(());
2537 }
2538
2539 let valid_media_type = request
2540 .header_value("content-type", 0)
2541 .is_some_and(|content_type| match source {
2542 InputSource::Json => is_json_media_type(content_type),
2543 InputSource::Form => media_type_is(content_type, "application/x-www-form-urlencoded"),
2544 InputSource::Multipart | InputSource::File => {
2545 media_type_is(content_type, "multipart/form-data")
2546 }
2547 InputSource::Stream => unreachable!("streaming bodies do not require a media type"),
2548 InputSource::Path | InputSource::Query | InputSource::Header | InputSource::Cookie => {
2549 true
2550 }
2551 });
2552 if !valid_media_type {
2553 return Err(BodyRejection::UnsupportedMediaType);
2554 }
2555
2556 Ok(())
2557}
2558
2559fn media_type_is(value: &str, expected: &str) -> bool {
2560 value
2561 .split(';')
2562 .next()
2563 .is_some_and(|media_type| media_type.trim().eq_ignore_ascii_case(expected))
2564}
2565
2566fn outcome_result(outcome: ExecutionOutcome) -> Result<Response, Failure> {
2567 match outcome {
2568 ExecutionOutcome::Success {
2569 status,
2570 headers,
2571 body,
2572 background,
2573 } => {
2574 let response = match body {
2575 Some(body) => Response {
2576 status,
2577 headers: json_headers(),
2578 body,
2579 stream: None,
2580 upgrade: None,
2581 background: Vec::new(),
2582 },
2583 None => Response {
2584 status,
2585 headers: ResponseHeaders::empty(),
2586 body: Vec::new(),
2587 stream: None,
2588 upgrade: None,
2589 background: Vec::new(),
2590 },
2591 };
2592 let mut response = with_outcome_headers(response, headers);
2593 response.background = background;
2594 Ok(response)
2595 }
2596 ExecutionOutcome::StreamingSuccess {
2597 status,
2598 headers,
2599 body,
2600 background,
2601 } => Ok(with_outcome_headers(
2602 Response {
2603 status,
2604 headers: ResponseHeaders::empty(),
2605 body: Vec::new(),
2606 stream: Some(body),
2607 upgrade: None,
2608 background,
2609 },
2610 headers,
2611 )),
2612 ExecutionOutcome::Upgrade {
2613 upgrade,
2614 background,
2615 } => {
2616 let headers = upgrade.headers().to_vec();
2617 Ok(with_outcome_headers(
2618 Response {
2619 status: 101,
2620 headers: ResponseHeaders::empty(),
2621 body: Vec::new(),
2622 stream: None,
2623 upgrade: Some(upgrade),
2624 background,
2625 },
2626 headers,
2627 ))
2628 }
2629 ExecutionOutcome::Rejected {
2630 status,
2631 code,
2632 message,
2633 details,
2634 } => Err(Failure {
2635 source: HttpErrorSource::Rejection,
2636 status,
2637 code: Cow::Owned(code),
2638 message: Cow::Owned(message),
2639 details,
2640 headers: Vec::new(),
2641 }),
2642 ExecutionOutcome::DomainError(error) => {
2643 let details = match error.details {
2644 Some(details) => match blazingly_json::from_slice(&details) {
2645 Ok(details) => Some(details),
2646 Err(_) => return Err(internal_failure()),
2647 },
2648 None => None,
2649 };
2650 Err(Failure {
2651 source: HttpErrorSource::Domain,
2652 status: error.status,
2653 code: Cow::Owned(error.code),
2654 message: Cow::Owned(error.message),
2655 details,
2656 headers: error.headers,
2657 })
2658 }
2659 ExecutionOutcome::InternalError { .. } => Err(internal_failure()),
2660 }
2661}
2662
2663fn with_outcome_headers(mut response: Response, headers: Vec<ResponseHeader>) -> Response {
2664 for header in headers {
2665 response = response.with_header(header.name, header.value);
2666 }
2667 response
2668}
2669
2670fn error_response(status: u16, code: &str, message: &str, details: Option<Value>) -> Response {
2671 let mut error = json!({
2672 "error": {
2673 "code": code,
2674 "message": message,
2675 }
2676 });
2677 if let Some(details) = details {
2678 error["error"]["details"] = details;
2679 }
2680 json_response(status, &error)
2681}
2682
2683fn internal_failure() -> Failure {
2684 Failure::new(
2685 HttpErrorSource::Internal,
2686 500,
2687 "internal_error",
2688 "the operation could not be completed",
2689 )
2690}
2691
2692fn json_response(status: u16, value: &Value) -> Response {
2693 let Ok(body) = blazingly_json::to_vec(value) else {
2694 return Response {
2695 status: 500,
2696 headers: json_headers(),
2697 body: br#"{"error":{"code":"internal_error","message":"the operation could not be completed"}}"#
2698 .to_vec(),
2699 stream: None,
2700 upgrade: None,
2701 background: Vec::new(),
2702 };
2703 };
2704
2705 Response {
2706 status,
2707 headers: json_headers(),
2708 body,
2709 stream: None,
2710 upgrade: None,
2711 background: Vec::new(),
2712 }
2713}
2714
2715fn json_headers() -> ResponseHeaders {
2716 ResponseHeaders::one("content-type", "application/json")
2717}
2718
2719fn is_json_media_type(value: &str) -> bool {
2720 value
2721 .split(';')
2722 .next()
2723 .is_some_and(|media_type| media_type.trim().eq_ignore_ascii_case("application/json"))
2724}
2725
2726fn normalize_header_name(name: &str) -> String {
2727 name.to_ascii_lowercase()
2728}
2729
2730impl Response {
2731 #[must_use]
2732 pub fn with_header(mut self, name: impl AsRef<str>, value: impl Into<String>) -> Self {
2733 self.set_header(name, value);
2734 self
2735 }
2736}
2737
2738const INLINE_RESPONSE_HEADERS: usize = 4;
2739type OwnedHeader = (Cow<'static, str>, Cow<'static, str>);
2740
2741#[derive(Clone, Debug, Eq, PartialEq)]
2742struct ResponseHeaders {
2743 inline: [Option<OwnedHeader>; INLINE_RESPONSE_HEADERS],
2744 overflow: Vec<OwnedHeader>,
2745}
2746
2747impl ResponseHeaders {
2748 fn empty() -> Self {
2749 Self {
2750 inline: std::array::from_fn(|_| None),
2751 overflow: Vec::new(),
2752 }
2753 }
2754
2755 fn one(name: &'static str, value: &'static str) -> Self {
2756 let mut headers = Self::empty();
2757 headers.inline[0] = Some((Cow::Borrowed(name), Cow::Borrowed(value)));
2758 headers
2759 }
2760
2761 fn insert(&mut self, name: Cow<'static, str>, value: Cow<'static, str>) {
2762 if !name.eq_ignore_ascii_case("set-cookie") {
2765 if let Some((_, existing)) = self
2766 .inline
2767 .iter_mut()
2768 .filter_map(Option::as_mut)
2769 .chain(self.overflow.iter_mut())
2770 .find(|(existing, _)| existing.eq_ignore_ascii_case(&name))
2771 {
2772 *existing = value;
2773 return;
2774 }
2775 }
2776 if let Some(slot) = self.inline.iter_mut().find(|slot| slot.is_none()) {
2777 *slot = Some((name, value));
2778 } else {
2779 self.overflow.push((name, value));
2780 }
2781 }
2782
2783 fn remove(&mut self, name: &str) {
2784 for slot in &mut self.inline {
2785 if slot
2786 .as_ref()
2787 .is_some_and(|(existing, _)| existing.eq_ignore_ascii_case(name))
2788 {
2789 *slot = None;
2790 }
2791 }
2792 self.overflow
2793 .retain(|(existing, _)| !existing.eq_ignore_ascii_case(name));
2794 }
2795
2796 fn get(&self, name: &str) -> Option<&str> {
2797 self.inline
2798 .iter()
2799 .filter_map(Option::as_ref)
2800 .chain(&self.overflow)
2801 .find(|(header, _)| header.eq_ignore_ascii_case(name))
2802 .map(|(_, value)| value.as_ref())
2803 }
2804
2805 fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
2806 self.inline
2807 .iter()
2808 .filter_map(Option::as_ref)
2809 .chain(&self.overflow)
2810 .map(|(name, value)| (name.as_ref(), value.as_ref()))
2811 }
2812}
2813
2814#[cfg(test)]
2815mod tests {
2816 use super::{
2817 BackgroundTasks, ConnectionInfo, HttpApp, HttpError, HttpErrorHandler, HttpErrorSource,
2818 HttpMiddleware, HttpRequestContext, MiddlewareScope, Request, Response, RouteError, Router,
2819 TestApp,
2820 };
2821 use blazingly_core::{
2822 HttpMethod, InputDescriptor, InputSource, OperationDescriptor, OperationFailure,
2823 PreparedJson, ResponseDescriptor, SecurityLocation, SecurityRequirement,
2824 SecuritySchemeDescriptor, SecuritySchemeKind, TypeDescriptor,
2825 };
2826 use blazingly_executor::{
2827 ExecutableApp, ExecutableOperation, ExecutionOutcome, Extension, FromInvocation,
2828 InputRejection, InvocationControl, InvocationInput, OperationFuture, OperationOutput,
2829 };
2830 use blazingly_json::{Value, json};
2831 use futures_lite::future;
2832 use std::cell::{Cell, RefCell};
2833 use std::future::Future;
2834 use std::net::{IpAddr, Ipv4Addr, SocketAddrV4};
2835 use std::pin::Pin;
2836 use std::rc::Rc;
2837 use std::task::{Context, Poll};
2838
2839 struct PassthroughLayer;
2840
2841 impl HttpMiddleware for PassthroughLayer {}
2842
2843 struct AuditLayer;
2844
2845 impl HttpMiddleware for AuditLayer {
2846 fn verifies_security(&self) -> bool {
2847 false
2848 }
2849 }
2850
2851 struct NormalizingProxy;
2852
2853 impl HttpMiddleware for NormalizingProxy {
2854 fn on_request(&self, context: &mut HttpRequestContext<'_>) -> Option<Response> {
2855 context.set_client_ip(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 7)));
2856 context.set_scheme("https");
2857 context.set_host("api.example");
2858 None
2859 }
2860 }
2861
2862 fn connection_operation(
2863 path: &str,
2864 id: &str,
2865 security: Vec<SecurityRequirement>,
2866 ) -> ExecutableOperation {
2867 let descriptor = OperationDescriptor::new(
2868 HttpMethod::Get,
2869 path,
2870 id,
2871 "Reports the normalized connection",
2872 None,
2873 vec![ResponseDescriptor::success(
2874 200,
2875 Some(TypeDescriptor::new("Connection")),
2876 )],
2877 )
2878 .expect("test operation id should be valid")
2879 .with_security(security);
2880 ExecutableOperation::typed(descriptor, |input| {
2881 let Extension(connection) =
2882 Extension::<ConnectionInfo>::from_invocation(&input, "connection", true)?;
2883 let body = json!({
2884 "scheme": connection.scheme(),
2885 "host": connection.host(),
2886 "clientIp": connection.client_ip().map(|address| address.to_string()),
2887 });
2888 Ok(Box::pin(async move {
2889 ExecutionOutcome::Success {
2890 status: 200,
2891 headers: Vec::new(),
2892 body: Some(blazingly_json::to_vec(&body).expect("connection body")),
2893 background: Vec::new(),
2894 }
2895 }) as OperationFuture)
2896 })
2897 }
2898
2899 fn executable() -> ExecutableApp {
2900 ExecutableApp::with_security_schemes(
2901 [
2902 connection_operation(
2903 "/secure",
2904 "http.secure",
2905 vec![SecurityRequirement::new("api_key")],
2906 ),
2907 connection_operation("/plain", "http.plain", Vec::new()),
2908 ],
2909 [SecuritySchemeDescriptor::new(
2910 "api_key",
2911 SecuritySchemeKind::ApiKey {
2912 location: SecurityLocation::Header,
2913 name: "x-api-key".to_owned(),
2914 },
2915 )],
2916 )
2917 .expect("secured operation graph should compile")
2918 }
2919
2920 fn error_code(response: &Response) -> String {
2921 let body: Value = response.json().expect("json error body");
2922 body["error"]["code"]
2923 .as_str()
2924 .expect("stable error code")
2925 .to_owned()
2926 }
2927
2928 fn prepared_operation() -> ExecutableOperation {
2932 let descriptor = OperationDescriptor::new(
2933 HttpMethod::Get,
2934 "/prepared",
2935 "http.prepared",
2936 "Encodes a borrowed view inside the operation",
2937 None,
2938 vec![ResponseDescriptor::success(
2939 200,
2940 Some(TypeDescriptor::new("Titles")),
2941 )],
2942 )
2943 .expect("test operation id should be valid");
2944 ExecutableOperation::typed(descriptor, |_| {
2945 Ok(Box::pin(async move {
2946 let owned = [String::from("first"), String::from("second")];
2947 let borrowed: Vec<&str> = owned.iter().map(String::as_str).collect();
2948 let body = PreparedJson::<TitlesSchema>::encode(&borrowed)
2949 .expect("the borrowed view encodes");
2950 OperationOutput::into_execution_outcome(body)
2951 }) as OperationFuture)
2952 })
2953 }
2954
2955 struct TitlesSchema;
2956
2957 impl blazingly_core::ApiSchema for TitlesSchema {
2958 fn type_descriptor() -> TypeDescriptor {
2959 TypeDescriptor::new("Titles")
2960 }
2961 }
2962
2963 #[test]
2964 fn a_prepared_body_reaches_the_wire_verbatim_as_json() {
2965 let executable = ExecutableApp::new([prepared_operation()])
2966 .expect("prepared operation graph should compile");
2967 let response = future::block_on(TestApp::new(&executable).call(Request::get("/prepared")));
2968
2969 assert_eq!(response.status(), 200);
2970 assert_eq!(
2971 response.get_header("content-type"),
2972 Some("application/json")
2973 );
2974 assert_eq!(response.body(), br#"["first","second"]"#);
2975 }
2976
2977 #[test]
2978 fn unlayered_dispatch_fails_closed_for_a_declared_scheme() {
2979 let executable = executable();
2980 let response = future::block_on(TestApp::new(&executable).call(Request::get("/secure")));
2981
2982 assert_eq!(response.status(), 500);
2983 assert_eq!(error_code(&response), "security_verifier_missing");
2984 }
2985
2986 #[test]
2987 fn owned_adapter_fails_closed_for_a_declared_scheme() {
2988 let app = HttpApp::new(executable());
2989 let response = future::block_on(app.call(Request::get("/secure")));
2990
2991 assert_eq!(response.status(), 500);
2992 assert_eq!(error_code(&response), "security_verifier_missing");
2993 }
2994
2995 #[test]
2996 fn unverified_scheme_opt_out_executes_the_operation() {
2997 let executable = executable();
2998 let response = future::block_on(
2999 TestApp::new(&executable)
3000 .with_unverified_security_schemes(true)
3001 .call(Request::get("/secure")),
3002 );
3003 assert_eq!(response.status(), 200);
3004
3005 let owned = HttpApp::new(executable).with_unverified_security_schemes(true);
3006 let response = future::block_on(owned.call(Request::get("/secure")));
3007 assert_eq!(response.status(), 200);
3008 }
3009
3010 #[test]
3011 fn routes_emission_lists_every_operation() {
3012 let executable = executable();
3013 let table = super::routes_table_text(executable.definition());
3014 assert!(table.starts_with("METHOD\tPATH\tOPERATION\tSUMMARY\n"));
3015 assert!(table.contains("GET\t/secure\thttp.secure\tReports the normalized connection\n"));
3016 assert!(table.contains("GET\t/plain\thttp.plain\tReports the normalized connection\n"));
3017 }
3018
3019 #[test]
3020 fn openapi_emission_serializes_the_default_document() {
3021 let executable = executable();
3022 let text = super::openapi_document_text(executable.definition())
3023 .expect("the OpenAPI document serializes");
3024 assert!(text.ends_with('\n'));
3025 let document: Value = blazingly_json::from_str(&text).expect("emitted JSON parses");
3026 assert_eq!(document["openapi"].as_str(), Some("3.1.0"));
3027 assert_eq!(
3028 document["paths"]["/secure"]["get"]["operationId"].as_str(),
3029 Some("http.secure")
3030 );
3031 assert_eq!(
3032 document["paths"]["/plain"]["get"]["operationId"].as_str(),
3033 Some("http.plain")
3034 );
3035 }
3036
3037 #[test]
3038 fn unsecured_operations_are_untouched_by_the_guard() {
3039 let executable = executable();
3040 let response = future::block_on(TestApp::new(&executable).call(Request::get("/plain")));
3041
3042 assert_eq!(response.status(), 200);
3043 }
3044
3045 #[test]
3046 fn layered_dispatch_runs_the_same_guard() {
3047 let executable = executable();
3048 let audited = future::block_on(
3049 TestApp::new(&executable)
3050 .with_middleware(AuditLayer)
3051 .call(Request::get("/secure")),
3052 );
3053 assert_eq!(audited.status(), 500);
3054 assert_eq!(error_code(&audited), "security_verifier_missing");
3055
3056 let verified = future::block_on(
3057 TestApp::new(&executable)
3058 .with_middleware(PassthroughLayer)
3059 .call(Request::get("/secure")),
3060 );
3061 assert_eq!(verified.status(), 200);
3062 }
3063
3064 type EventLog = Rc<RefCell<Vec<String>>>;
3065 type HandleExtractor = fn(&InvocationInput<'_>) -> Result<BackgroundTasks, InputRejection>;
3066
3067 struct RecordingLayer {
3068 name: &'static str,
3069 events: EventLog,
3070 }
3071
3072 impl HttpMiddleware for RecordingLayer {
3073 fn on_request(&self, _context: &mut HttpRequestContext<'_>) -> Option<Response> {
3074 self.events
3075 .borrow_mut()
3076 .push(format!("{}:request", self.name));
3077 None
3078 }
3079
3080 fn on_operation(
3081 &self,
3082 _context: &mut HttpRequestContext<'_>,
3083 operation: &OperationDescriptor,
3084 _security_schemes: &[SecuritySchemeDescriptor],
3085 ) -> Option<Response> {
3086 self.events.borrow_mut().push(format!(
3087 "{}:operation:{}",
3088 self.name,
3089 operation.contract.id.as_str()
3090 ));
3091 None
3092 }
3093
3094 fn on_response(
3095 &self,
3096 _context: &HttpRequestContext<'_>,
3097 _operation: Option<&OperationDescriptor>,
3098 _response: &mut Response,
3099 ) {
3100 self.events
3101 .borrow_mut()
3102 .push(format!("{}:response", self.name));
3103 }
3104 }
3105
3106 struct StatusStamp;
3107
3108 impl HttpMiddleware for StatusStamp {
3109 fn on_response(
3110 &self,
3111 _context: &HttpRequestContext<'_>,
3112 _operation: Option<&OperationDescriptor>,
3113 response: &mut Response,
3114 ) {
3115 let status = response.status().to_string();
3116 response.set_header("x-final-status", status);
3117 }
3118
3119 fn verifies_security(&self) -> bool {
3120 false
3121 }
3122 }
3123
3124 struct HouseStyle;
3125
3126 impl HttpErrorHandler for HouseStyle {
3127 fn on_error(&self, error: &HttpError<'_>, response: &mut Response) {
3128 response.set_header("x-error-source", format!("{:?}", error.source()));
3129 match error.source() {
3130 HttpErrorSource::Internal => {
3131 response.set_status(503);
3132 response.replace_body(br#"{"error":{"code":"unavailable"}}"#.to_vec());
3133 }
3134 HttpErrorSource::Domain => response.set_status(500),
3136 HttpErrorSource::Routing
3137 | HttpErrorSource::Request
3138 | HttpErrorSource::Rejection => {}
3139 }
3140 }
3141 }
3142
3143 fn scoped_executable() -> ExecutableApp {
3144 ExecutableApp::new([
3145 connection_operation("/ingest/events", "ingest.events", Vec::new()),
3146 connection_operation("/status", "status.read", Vec::new()),
3147 ])
3148 .expect("scoped operation graph should compile")
3149 }
3150
3151 fn ack_descriptor(path: &str, id: &str) -> OperationDescriptor {
3152 OperationDescriptor::new(
3153 HttpMethod::Get,
3154 path,
3155 id,
3156 "Reports an acknowledgement",
3157 None,
3158 vec![ResponseDescriptor::success(
3159 200,
3160 Some(TypeDescriptor::new("Ack")),
3161 )],
3162 )
3163 .expect("test operation id should be valid")
3164 }
3165
3166 fn outcome_operation(
3167 path: &str,
3168 id: &str,
3169 outcome: fn() -> ExecutionOutcome,
3170 ) -> ExecutableOperation {
3171 ExecutableOperation::typed(ack_descriptor(path, id), move |_| {
3172 Ok(Box::pin(async move { outcome() }) as OperationFuture)
3173 })
3174 }
3175
3176 fn extension_handle(input: &InvocationInput<'_>) -> Result<BackgroundTasks, InputRejection> {
3177 Extension::<BackgroundTasks>::from_invocation(input, "background", true)
3178 .map(|Extension(tasks)| tasks)
3179 }
3180
3181 fn bare_handle(input: &InvocationInput<'_>) -> Result<BackgroundTasks, InputRejection> {
3182 BackgroundTasks::from_invocation(input, "background", true)
3183 }
3184
3185 fn scheduling_operation(
3188 path: &str,
3189 id: &str,
3190 log: &EventLog,
3191 extract: HandleExtractor,
3192 outcome: fn() -> ExecutionOutcome,
3193 ) -> ExecutableOperation {
3194 let log = Rc::clone(log);
3195 ExecutableOperation::typed(ack_descriptor(path, id), move |input| {
3196 let tasks = extract(&input)?;
3197 let log = Rc::clone(&log);
3198 tasks.add_infallible(move || async move {
3199 log.borrow_mut().push("task".to_owned());
3200 });
3201 Ok(Box::pin(async move { outcome() }) as OperationFuture)
3202 })
3203 }
3204
3205 fn accepted() -> ExecutionOutcome {
3206 ExecutionOutcome::Success {
3207 status: 200,
3208 headers: Vec::new(),
3209 body: None,
3210 background: Vec::new(),
3211 }
3212 }
3213
3214 fn conflict() -> ExecutionOutcome {
3215 ExecutionOutcome::DomainError(OperationFailure::new(
3216 409,
3217 "conflict",
3218 "the event was already ingested",
3219 ))
3220 }
3221
3222 #[test]
3223 fn a_path_prefix_matches_only_on_segment_boundaries() {
3224 let scope = MiddlewareScope::prefix("/ingest");
3225 assert!(scope.matches_request("/ingest"));
3226 assert!(scope.matches_request("/ingest/events"));
3227 assert!(!scope.matches_request("/ingested"));
3228 assert!(!scope.matches_request("/"));
3229 assert!(MiddlewareScope::prefix("ingest/").matches_request("/ingest/events"));
3230 assert!(MiddlewareScope::all().is_global());
3231 assert!(!scope.is_global());
3232 }
3233
3234 #[test]
3235 fn operation_predicates_select_by_id_after_routing() {
3236 let scope = MiddlewareScope::all().with_operation_prefix("ingest.");
3237 assert!(scope.matches_operation("/anything", "ingest.events"));
3238 assert!(!scope.matches_operation("/anything", "status.read"));
3239 assert!(!scope.matches_request("/anything"));
3240
3241 let scope = MiddlewareScope::all()
3242 .with_operation_filter(|id| id.split('.').next_back() == Some("read"));
3243 assert!(scope.matches_operation("/anything", "status.read"));
3244 assert!(!scope.matches_operation("/anything", "status.write"));
3245
3246 let scope = MiddlewareScope::prefix("/ingest").with_operation("status.read");
3247 assert!(!scope.matches_operation("/status", "status.read"));
3248 }
3249
3250 #[test]
3251 fn a_prefix_scoped_layer_observes_only_its_subtree() {
3252 let executable = scoped_executable();
3253 let events: EventLog = Rc::new(RefCell::new(Vec::new()));
3254 let app = TestApp::new(&executable).with_scoped_middleware(
3255 MiddlewareScope::prefix("/ingest"),
3256 RecordingLayer {
3257 name: "ingest",
3258 events: Rc::clone(&events),
3259 },
3260 );
3261
3262 future::block_on(app.call(Request::get("/ingest/events")));
3263 let recorded = events.borrow().clone();
3264 assert_eq!(
3265 recorded,
3266 [
3267 "ingest:request",
3268 "ingest:operation:ingest.events",
3269 "ingest:response"
3270 ]
3271 );
3272
3273 events.borrow_mut().clear();
3274 future::block_on(app.call(Request::get("/status")));
3275 assert!(events.borrow().is_empty());
3276 }
3277
3278 #[test]
3279 fn an_operation_scoped_layer_starts_after_routing() {
3280 let executable = scoped_executable();
3281 let events: EventLog = Rc::new(RefCell::new(Vec::new()));
3282 let app = TestApp::new(&executable).with_scoped_middleware(
3283 MiddlewareScope::operation("ingest.events"),
3284 RecordingLayer {
3285 name: "op",
3286 events: Rc::clone(&events),
3287 },
3288 );
3289
3290 future::block_on(app.call(Request::get("/ingest/events")));
3291 let recorded = events.borrow().clone();
3292 assert_eq!(recorded, ["op:operation:ingest.events", "op:response"]);
3293
3294 events.borrow_mut().clear();
3295 future::block_on(app.call(Request::get("/status")));
3296 assert!(events.borrow().is_empty());
3297 }
3298
3299 #[test]
3300 fn a_scoped_verifier_does_not_cover_operations_outside_its_scope() {
3301 let executable = executable();
3302 let outside = future::block_on(
3303 TestApp::new(&executable)
3304 .with_scoped_middleware(MiddlewareScope::prefix("/other"), PassthroughLayer)
3305 .call(Request::get("/secure")),
3306 );
3307 assert_eq!(outside.status(), 500);
3308 assert_eq!(error_code(&outside), "security_verifier_missing");
3309
3310 let inside = future::block_on(
3311 TestApp::new(&executable)
3312 .with_scoped_middleware(MiddlewareScope::prefix("/secure"), PassthroughLayer)
3313 .call(Request::get("/secure")),
3314 );
3315 assert_eq!(inside.status(), 200);
3316 }
3317
3318 #[test]
3319 fn an_injected_handle_schedules_work_from_the_handler_body() {
3320 let log: EventLog = Rc::new(RefCell::new(Vec::new()));
3321 let executable = ExecutableApp::new([scheduling_operation(
3322 "/ingest",
3323 "tasks.ok",
3324 &log,
3325 extension_handle,
3326 accepted,
3327 )])
3328 .expect("scheduling operation graph should compile");
3329
3330 let mut response =
3331 future::block_on(TestApp::new(&executable).call(Request::get("/ingest")));
3332 assert_eq!(response.status(), 200);
3333 assert!(log.borrow().is_empty());
3334
3335 future::block_on(response.run_background()).expect("scheduled task");
3336 assert_eq!(log.borrow().clone(), ["task"]);
3337 }
3338
3339 #[test]
3340 fn scheduled_work_survives_a_failed_outcome() {
3341 let log: EventLog = Rc::new(RefCell::new(Vec::new()));
3342 let executable = ExecutableApp::new([scheduling_operation(
3343 "/ingest",
3344 "tasks.conflict",
3345 &log,
3346 bare_handle,
3347 conflict,
3348 )])
3349 .expect("scheduling operation graph should compile");
3350
3351 let mut response =
3352 future::block_on(TestApp::new(&executable).call(Request::get("/ingest")));
3353 assert_eq!(response.status(), 409);
3354 assert_eq!(error_code(&response), "conflict");
3355
3356 future::block_on(response.run_background()).expect("scheduled task");
3357 assert_eq!(log.borrow().clone(), ["task"]);
3358 }
3359
3360 struct ReadyWhenScheduled {
3363 scheduled: Rc<Cell<bool>>,
3364 }
3365
3366 impl Future for ReadyWhenScheduled {
3367 type Output = ();
3368
3369 fn poll(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll<()> {
3370 if self.scheduled.get() {
3371 Poll::Ready(())
3372 } else {
3373 Poll::Pending
3374 }
3375 }
3376 }
3377
3378 fn aborting_operation(log: &EventLog, scheduled: &Rc<Cell<bool>>) -> ExecutableOperation {
3379 let log = Rc::clone(log);
3380 let scheduled = Rc::clone(scheduled);
3381 ExecutableOperation::typed(ack_descriptor("/ingest", "tasks.aborted"), move |input| {
3382 let tasks = bare_handle(&input)?;
3383 let log = Rc::clone(&log);
3384 tasks.add_infallible(move || async move {
3385 log.borrow_mut().push("task".to_owned());
3386 });
3387 scheduled.set(true);
3388 Ok(Box::pin(async move { accepted() }) as OperationFuture)
3389 })
3390 }
3391
3392 #[test]
3393 fn scheduled_work_survives_an_aborted_invocation() {
3394 let log: EventLog = Rc::new(RefCell::new(Vec::new()));
3395 let scheduled = Rc::new(Cell::new(false));
3396 let executable = ExecutableApp::new([aborting_operation(&log, &scheduled)])
3397 .expect("scheduling operation graph should compile");
3398 let control = InvocationControl::new().with_timeout(ReadyWhenScheduled {
3399 scheduled: Rc::clone(&scheduled),
3400 });
3401
3402 let mut response = future::block_on(
3403 TestApp::new(&executable).call_controlled(Request::get("/ingest"), control),
3404 );
3405 assert_eq!(response.status(), 504);
3406
3407 future::block_on(response.run_background()).expect("scheduled task");
3408 assert_eq!(log.borrow().clone(), ["task"]);
3409 }
3410
3411 #[test]
3412 fn an_unused_handle_leaves_the_response_untouched() {
3413 let executable = scoped_executable();
3414 let mut response =
3415 future::block_on(TestApp::new(&executable).call(Request::get("/status")));
3416
3417 assert_eq!(response.status(), 200);
3418 assert!(response.take_background_tasks().is_empty());
3419 }
3420
3421 #[test]
3422 fn an_error_handler_restyles_every_dispatch_failure() {
3423 let executable = executable();
3424 let app = TestApp::new(&executable).with_error_handler(HouseStyle);
3425
3426 let missing = future::block_on(app.call(Request::get("/nope")));
3427 assert_eq!(missing.status(), 404);
3428 assert_eq!(missing.get_header("x-error-source"), Some("Routing"));
3429
3430 let unverified = future::block_on(app.call(Request::get("/secure")));
3431 assert_eq!(unverified.status(), 503);
3432 assert_eq!(unverified.get_header("x-error-source"), Some("Internal"));
3433 assert_eq!(error_code(&unverified), "unavailable");
3434
3435 let allowed = future::block_on(app.call(Request::post("/plain")));
3436 assert_eq!(allowed.status(), 405);
3437 assert_eq!(allowed.get_header("allow"), Some("GET"));
3438 assert_eq!(allowed.get_header("x-error-source"), Some("Routing"));
3439 }
3440
3441 #[test]
3442 fn a_typed_domain_status_survives_the_error_handler() {
3443 let executable = ExecutableApp::new([
3444 outcome_operation("/conflict", "errors.conflict", conflict),
3445 outcome_operation("/broken", "errors.broken", || {
3446 ExecutionOutcome::InternalError {
3447 code: "boom".to_owned(),
3448 message: "boom".to_owned(),
3449 }
3450 }),
3451 ])
3452 .expect("error operation graph should compile");
3453 let app = TestApp::new(&executable).with_error_handler(HouseStyle);
3454
3455 let domain = future::block_on(app.call(Request::get("/conflict")));
3456 assert_eq!(domain.status(), 409);
3457 assert_eq!(error_code(&domain), "conflict");
3458 assert_eq!(domain.get_header("x-error-source"), Some("Domain"));
3459
3460 let internal = future::block_on(app.call(Request::get("/broken")));
3461 assert_eq!(internal.status(), 503);
3462 assert_eq!(error_code(&internal), "unavailable");
3463 }
3464
3465 #[test]
3466 fn error_handlers_run_before_middleware_sees_the_response() {
3467 let executable = executable();
3468 let response = future::block_on(
3469 TestApp::new(&executable)
3470 .with_error_handler(HouseStyle)
3471 .with_middleware(StatusStamp)
3472 .call(Request::get("/secure")),
3473 );
3474
3475 assert_eq!(response.status(), 503);
3476 assert_eq!(response.get_header("x-final-status"), Some("503"));
3477 }
3478
3479 #[test]
3480 fn the_owned_adapter_registers_the_same_seams() {
3481 let app = HttpApp::new(executable())
3482 .with_scoped_middleware(MiddlewareScope::prefix("/other"), PassthroughLayer)
3483 .with_error_handler(HouseStyle);
3484 let response = future::block_on(app.call(Request::get("/secure")));
3485
3486 assert_eq!(response.status(), 503);
3487 assert_eq!(response.get_header("x-error-source"), Some("Internal"));
3488 }
3489
3490 #[test]
3491 fn normalized_connection_reaches_the_operation_context() {
3492 let executable = executable();
3493 let request = || {
3494 Request::get("/plain")
3495 .peer_addr(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 9000).into())
3496 .header("host", "internal:8080")
3497 };
3498
3499 let unlayered = future::block_on(TestApp::new(&executable).call(request()));
3500 let body: Value = unlayered.json().expect("connection body");
3501 assert_eq!(body["scheme"], "http");
3502 assert_eq!(body["host"], "internal:8080");
3503 assert_eq!(body["clientIp"], "127.0.0.1");
3504
3505 let layered = future::block_on(
3506 TestApp::new(&executable)
3507 .with_middleware(NormalizingProxy)
3508 .call(request()),
3509 );
3510 let body: Value = layered.json().expect("connection body");
3511 assert_eq!(body["scheme"], "https");
3512 assert_eq!(body["host"], "api.example");
3513 assert_eq!(body["clientIp"], "198.51.100.7");
3514 }
3515
3516 fn router_operation(method: HttpMethod, path: &str, id: &str) -> ExecutableOperation {
3517 let inputs = path
3518 .split('/')
3519 .filter_map(|segment| {
3520 segment
3521 .strip_prefix('{')
3522 .and_then(|segment| segment.strip_suffix('}'))
3523 })
3524 .map(|name| {
3525 InputDescriptor::new(name, InputSource::Path, true, TypeDescriptor::new("String"))
3526 })
3527 .collect::<Vec<_>>();
3528 let descriptor = OperationDescriptor::new(
3529 method,
3530 path,
3531 id,
3532 "Router fixture",
3533 None,
3534 vec![ResponseDescriptor::success(200, None)],
3535 )
3536 .expect("test operation id should be valid")
3537 .with_inputs(inputs);
3538 ExecutableOperation::typed(descriptor, |_| {
3539 Ok(Box::pin(async move {
3540 ExecutionOutcome::Success {
3541 status: 200,
3542 headers: Vec::new(),
3543 body: None,
3544 background: Vec::new(),
3545 }
3546 }) as OperationFuture)
3547 })
3548 }
3549
3550 fn router(routes: &[(HttpMethod, &str)]) -> Router {
3551 let operations = routes
3552 .iter()
3553 .enumerate()
3554 .map(|(index, (method, path))| {
3555 router_operation(*method, path, &format!("router.op{index}"))
3556 })
3557 .collect::<Vec<_>>();
3558 let app = ExecutableApp::new(operations).expect("router fixture compiles");
3559 Router::new(&app)
3560 }
3561
3562 fn allowed(router: &Router, method: HttpMethod, path: &str) -> Vec<HttpMethod> {
3563 match router.recognize(method, path) {
3564 Err(RouteError::MethodNotAllowed { allowed }) => allowed,
3565 other => panic!("{method:?} {path} should be a 405, got {:?}", other.is_ok()),
3566 }
3567 }
3568
3569 #[test]
3572 fn the_method_table_matches_the_http_method_order() {
3573 let mut sorted = super::METHODS;
3574 sorted.sort_unstable();
3575 assert_eq!(sorted, super::METHODS);
3576 for (index, method) in super::METHODS.into_iter().enumerate() {
3577 assert_eq!(super::method_index(method), index);
3578 }
3579 }
3580
3581 #[test]
3582 fn a_static_path_outranks_a_parameter_of_the_same_shape() {
3583 let router = router(&[
3584 (HttpMethod::Get, "/items"),
3585 (HttpMethod::Post, "/items"),
3586 (HttpMethod::Get, "/items/latest"),
3587 (HttpMethod::Put, "/items/latest"),
3588 (HttpMethod::Get, "/items/{id}"),
3589 (HttpMethod::Delete, "/items/{id}"),
3590 ]);
3591
3592 let statically = router
3593 .recognize(HttpMethod::Get, "/items/latest")
3594 .expect("static route");
3595 assert_eq!(statically.path_parameter("id"), None);
3596
3597 let dynamically = router
3600 .recognize(HttpMethod::Delete, "/items/latest")
3601 .expect("parameter route");
3602 assert_eq!(
3603 dynamically.path_parameter("id").as_deref(),
3604 Some("latest"),
3605 "the static probe must not consume the segment"
3606 );
3607 }
3608
3609 #[test]
3610 fn an_allow_list_unions_static_and_parameter_routes_in_enum_order() {
3611 let router = router(&[
3612 (HttpMethod::Get, "/items"),
3613 (HttpMethod::Post, "/items"),
3614 (HttpMethod::Get, "/items/latest"),
3615 (HttpMethod::Put, "/items/latest"),
3616 (HttpMethod::Get, "/items/{id}"),
3617 (HttpMethod::Delete, "/items/{id}"),
3618 ]);
3619
3620 assert_eq!(
3621 allowed(&router, HttpMethod::Patch, "/items"),
3622 vec![HttpMethod::Get, HttpMethod::Post]
3623 );
3624 assert_eq!(
3625 allowed(&router, HttpMethod::Post, "/items/latest"),
3626 vec![HttpMethod::Get, HttpMethod::Put, HttpMethod::Delete],
3627 "PUT precedes DELETE in the enum even though it follows alphabetically"
3628 );
3629 assert_eq!(
3630 allowed(&router, HttpMethod::Post, "/items/7"),
3631 vec![HttpMethod::Get, HttpMethod::Delete]
3632 );
3633 }
3634
3635 #[test]
3636 fn an_unknown_path_is_a_miss_rather_than_a_wrong_method() {
3637 let router = router(&[
3638 (HttpMethod::Get, "/items"),
3639 (HttpMethod::Get, "/items/{id}/tags/{tag}"),
3640 ]);
3641
3642 for path in ["/nope", "/items/7/tags", "/items/7/tags/x/y", "/", ""] {
3643 assert_eq!(
3644 router.recognize(HttpMethod::Get, path).err(),
3645 Some(RouteError::NotFound),
3646 "{path} should not exist"
3647 );
3648 }
3649 }
3650
3651 #[test]
3652 fn a_failed_static_descent_backtracks_into_the_parameter_child() {
3653 let router = router(&[
3654 (HttpMethod::Get, "/a/{p}/x"),
3655 (HttpMethod::Post, "/a/{p}/x"),
3656 (HttpMethod::Get, "/a/b/y"),
3657 ]);
3658
3659 let matched = router
3662 .recognize(HttpMethod::Get, "/a/b/x")
3663 .expect("parameter route after backtracking");
3664 assert_eq!(matched.path_parameter("p").as_deref(), Some("b"));
3665
3666 let statically = router
3667 .recognize(HttpMethod::Get, "/a/b/y")
3668 .expect("static branch");
3669 assert_eq!(statically.path_parameter("p"), None);
3670
3671 assert_eq!(
3672 router.recognize(HttpMethod::Get, "/a/c/y").err(),
3673 Some(RouteError::NotFound)
3674 );
3675 assert_eq!(
3676 allowed(&router, HttpMethod::Delete, "/a/b/x"),
3677 vec![HttpMethod::Get, HttpMethod::Post]
3678 );
3679 }
3680
3681 #[test]
3682 fn captures_survive_a_deep_backtrack() {
3683 let router = router(&[
3684 (HttpMethod::Get, "/{one}/{two}/{three}/leaf"),
3685 (HttpMethod::Get, "/a/b/c/other"),
3686 ]);
3687
3688 let matched = router
3689 .recognize(HttpMethod::Get, "/a/b/c/leaf")
3690 .expect("parameter route");
3691 assert_eq!(matched.path_parameter("one").as_deref(), Some("a"));
3692 assert_eq!(matched.path_parameter("two").as_deref(), Some("b"));
3693 assert_eq!(matched.path_parameter("three").as_deref(), Some("c"));
3694 assert_eq!(matched.path_parameter("four"), None);
3695 }
3696
3697 #[test]
3698 fn a_captured_segment_is_percent_decoded() {
3699 let router = router(&[(HttpMethod::Get, "/files/{name}")]);
3700
3701 let matched = router
3702 .recognize(HttpMethod::Get, "/files/a%20b%2Fc")
3703 .expect("parameter route");
3704 assert_eq!(matched.path_parameter("name").as_deref(), Some("a b/c"));
3705
3706 let plus = router
3707 .recognize(HttpMethod::Get, "/files/a+b")
3708 .expect("parameter route");
3709 assert_eq!(
3710 plus.path_parameter("name").as_deref(),
3711 Some("a+b"),
3712 "a path segment is not form encoded"
3713 );
3714
3715 let invalid = router
3716 .recognize(HttpMethod::Get, "/files/a%2")
3717 .expect("parameter route");
3718 assert_eq!(invalid.path_parameter("name"), None);
3719 }
3720
3721 #[test]
3722 fn empty_segments_are_matched_literally() {
3723 let router = router(&[(HttpMethod::Get, "/files/{name}/meta")]);
3724
3725 let matched = router
3726 .recognize(HttpMethod::Get, "/files//meta")
3727 .expect("empty segments are still segments");
3728 assert_eq!(matched.path_parameter("name").as_deref(), Some(""));
3729 }
3730
3731 #[test]
3732 fn every_method_reaches_its_own_static_slot() {
3733 let bindings = super::METHODS
3734 .into_iter()
3735 .map(|method| (method, "/one"))
3736 .collect::<Vec<_>>();
3737 let router = router(&bindings);
3738
3739 let mut seen = Vec::new();
3740 for method in super::METHODS {
3741 let matched = router.recognize(method, "/one").expect("static route");
3742 seen.push(matched.operation_index());
3743 }
3744 seen.sort_unstable();
3745 seen.dedup();
3746 assert_eq!(seen.len(), super::METHOD_COUNT, "slots must not alias");
3747 }
3748}