Skip to main content

alibi_core/
endpoint.rs

1//! Trusted endpoint calls keep logical input separate from an optional HTTP request.
2
3use crate::AuthContext;
4use crate::session::SessionRequest;
5use crate::types::RequestExtensions;
6use crate::utils::LockUnpoisoned;
7use crate::utils::json::JsValue;
8use crate::wire::{SessionView, UserView};
9use crate::{AuthError, AuthRequest, AuthResult, AuthSchema, Headers, HttpMethod};
10use serde::{Serialize, de::DeserializeOwned};
11use std::collections::HashMap;
12use std::marker::PhantomData;
13use std::sync::{Arc, Mutex};
14
15/// An operation registered by an installed plugin, independently of its HTTP routes.
16#[derive(Clone, Debug)]
17pub struct EndpointDefinition {
18    pub name: &'static str,
19    pub operation_id: &'static str,
20    pub path: Option<String>,
21    pub method: HttpMethod,
22}
23
24/// Typed operation input. Dispatch decodes the final input after before-hook patches.
25#[derive(Clone, Debug)]
26pub struct ServerEndpoint<T> {
27    plugin: &'static str,
28    name: &'static str,
29    body: Option<JsValue>,
30    query: Option<JsValue>,
31    output: PhantomData<fn() -> T>,
32}
33
34impl<T> ServerEndpoint<T> {
35    #[must_use]
36    pub const fn new(plugin: &'static str, name: &'static str) -> Self {
37        Self {
38            plugin,
39            name,
40            body: None,
41            query: None,
42            output: PhantomData,
43        }
44    }
45
46    /// # Errors
47    /// Returns an error if application input cannot be serialized.
48    pub fn with_body(mut self, body: &impl Serialize) -> AuthResult<Self> {
49        self.body = Some(crate::utils::json::parse_value(
50            &crate::utils::json::to_string(body)?,
51        )?);
52        Ok(self)
53    }
54
55    /// # Errors
56    /// Returns an error if application input cannot be serialized.
57    pub fn with_query(mut self, query: &impl Serialize) -> AuthResult<Self> {
58        self.query = Some(crate::utils::json::parse_value(
59            &crate::utils::json::to_string(query)?,
60        )?);
61        Ok(self)
62    }
63
64    #[must_use]
65    pub fn with_body_value(mut self, body: JsValue) -> Self {
66        self.body = Some(body);
67        self
68    }
69
70    #[must_use]
71    pub fn with_query_value(mut self, query: JsValue) -> Self {
72        self.query = Some(query);
73        self
74    }
75
76    #[must_use]
77    pub const fn plugin(&self) -> &'static str {
78        self.plugin
79    }
80
81    #[must_use]
82    pub const fn name(&self) -> &'static str {
83        self.name
84    }
85
86    #[must_use]
87    pub fn input(self) -> (Option<JsValue>, Option<JsValue>) {
88        (self.body, self.query)
89    }
90}
91
92/// Per-call options. Absent headers remain distinct from an explicitly empty collection.
93#[derive(Clone, Debug, Default)]
94pub struct EndpointOptions {
95    pub headers: Option<HashMap<String, String>>,
96    pub request: Option<AuthRequest>,
97    pub method: Option<HttpMethod>,
98}
99
100#[derive(Clone, Debug, Default)]
101struct EndpointState {
102    session: Option<SessionView>,
103    principal: Option<(UserView, SessionView)>,
104    response_headers: Headers,
105    session_observation: Option<(UserView, SessionView)>,
106}
107
108struct VerifiedEndpointUser<S: AuthSchema> {
109    user: S::User,
110    config: Arc<crate::AuthConfig>,
111    database: Arc<dyn crate::AuthStore<S>>,
112}
113
114/// Runtime normalization phase, retaining absence in original logical inputs.
115#[derive(Clone, Copy, Debug, PartialEq, Eq)]
116pub enum EndpointPhase {
117    Raw,
118    Middleware,
119    Handler,
120}
121
122/// Genuine logical endpoint data available to hooks and application callbacks.
123///
124/// Clones share trusted session state, observed snapshots and queued headers.
125/// Every public dispatch constructs fresh state; supplied HTTP metadata cannot
126/// carry a caller-populated virtual principal into this context.
127#[derive(Clone, Debug)]
128pub struct EndpointCall {
129    operation_id: String,
130    path: Option<String>,
131    method: Option<HttpMethod>,
132    body: Option<JsValue>,
133    query: Option<JsValue>,
134    headers: Option<HashMap<String, String>>,
135    request: Option<AuthRequest>,
136    default_method: HttpMethod,
137    phase: EndpointPhase,
138    extensions: RequestExtensions,
139    state: Arc<Mutex<EndpointState>>,
140}
141
142impl EndpointCall {
143    #[must_use]
144    pub fn new(
145        definition: &EndpointDefinition,
146        body: Option<JsValue>,
147        query: Option<JsValue>,
148        options: EndpointOptions,
149    ) -> Self {
150        Self {
151            operation_id: definition.operation_id.into(),
152            path: definition.path.clone(),
153            method: options.method,
154            body,
155            query,
156            headers: options.headers.map(|headers| {
157                headers
158                    .into_iter()
159                    .map(|(name, value)| (name.to_ascii_lowercase(), value))
160                    .collect()
161            }),
162            request: options.request.map(fresh_request),
163            default_method: definition.method.clone(),
164            phase: EndpointPhase::Raw,
165            extensions: RequestExtensions::default(),
166            state: Arc::new(Mutex::new(EndpointState::default())),
167        }
168    }
169
170    /// Logical HTTP endpoint input after physical routing and body parsing.
171    /// The original physical request remains separate from normalized hook input.
172    #[must_use]
173    pub fn from_http_request(request: &AuthRequest, route: &crate::AuthRoute) -> Self {
174        let body = request
175            .extensions()
176            .get::<crate::types::ParsedRequestBody>()
177            .and_then(|body| match body.as_ref() {
178                crate::types::ParsedRequestBody::Value(value) => Some(value.clone()),
179                crate::types::ParsedRequestBody::Opaque(_) => None,
180            });
181        let query = JsValue::Object(
182            request
183                .query
184                .keys()
185                .map(|name| {
186                    let values = request.query_values(name).unwrap_or_default();
187                    let value = if values.len() > 1 {
188                        JsValue::Array(values.iter().cloned().map(JsValue::String).collect())
189                    } else {
190                        JsValue::String(request.query.get(name).cloned().unwrap_or_default())
191                    };
192                    (name.clone(), value)
193                })
194                .collect(),
195        );
196        let definition = EndpointDefinition {
197            name: "http",
198            operation_id: "http",
199            path: Some(route.context_path.as_ref().unwrap_or(&route.path).clone()),
200            method: route.method.clone(),
201        };
202        let mut call = Self::new(
203            &definition,
204            body,
205            Some(query),
206            EndpointOptions {
207                headers: Some(request.headers.clone()),
208                request: Some(
209                    crate::hooks::current_request_hook_context()
210                        .map_or_else(|| request.clone(), |context| context.request),
211                ),
212                method: Some(request.method.clone()),
213            },
214        );
215        call.operation_id.clone_from(&route.operation_id);
216        call
217    }
218
219    #[must_use]
220    pub fn operation_id(&self) -> &str {
221        &self.operation_id
222    }
223
224    #[must_use]
225    pub fn path(&self) -> Option<&str> {
226        self.path.as_deref()
227    }
228
229    #[must_use]
230    pub const fn method(&self) -> Option<&HttpMethod> {
231        self.method.as_ref()
232    }
233
234    #[must_use]
235    pub const fn body(&self) -> Option<&JsValue> {
236        self.body.as_ref()
237    }
238
239    #[must_use]
240    pub const fn query(&self) -> Option<&JsValue> {
241        self.query.as_ref()
242    }
243
244    #[must_use]
245    pub const fn headers(&self) -> Option<&HashMap<String, String>> {
246        self.headers.as_ref()
247    }
248
249    /// Replace logical headers after a physical/plugin header transformation.
250    /// Session state and the distinct physical Request are retained.
251    pub fn replace_headers(&mut self, headers: HashMap<String, String>) {
252        self.headers = Some(headers);
253    }
254
255    #[must_use]
256    pub const fn request(&self) -> Option<&AuthRequest> {
257        self.request.as_ref()
258    }
259
260    #[must_use]
261    pub const fn phase(&self) -> EndpointPhase {
262        self.phase
263    }
264
265    #[must_use]
266    pub fn has_body(&self) -> bool {
267        self.phase != EndpointPhase::Raw || self.body.is_some()
268    }
269
270    #[must_use]
271    pub fn has_query(&self) -> bool {
272        self.phase != EndpointPhase::Raw || self.query.is_some()
273    }
274
275    #[must_use]
276    pub fn has_method(&self) -> bool {
277        self.phase != EndpointPhase::Raw || self.method.is_some()
278    }
279
280    /// Middleware callbacks have their own path/query defaults; matchers retain raw input.
281    #[must_use]
282    pub fn middleware_context(&self) -> Self {
283        let mut context = self.clone();
284        context.phase = EndpointPhase::Middleware;
285        if context.path.as_ref().is_none_or(String::is_empty) {
286            context.path = Some("/".into());
287        }
288        context
289    }
290
291    /// A handler receives validated data and its registered method/path defaults.
292    #[must_use]
293    pub fn handler_context(&self, body: Option<JsValue>, query: Option<JsValue>) -> Self {
294        let mut context = self.clone();
295        context.phase = EndpointPhase::Handler;
296        if context.path.as_ref().is_none_or(String::is_empty) {
297            context.path = Some("virtual:".into());
298        }
299        if context.method.is_none() {
300            context.method = Some(context.default_method.clone());
301        }
302        context.body = body;
303        context.query = query;
304        context
305    }
306
307    /// The nested session getter preserves caller input and validates its own query.
308    pub(crate) fn session_read_context(&self) -> Self {
309        let mut context = self.clone();
310        context.phase = EndpointPhase::Handler;
311        if context.path.as_ref().is_none_or(String::is_empty) {
312            context.path = Some("virtual:".into());
313        }
314        context.method = Some(HttpMethod::Get);
315        let mut query = indexmap::IndexMap::new();
316        for name in ["disableCookieCache", "disableRefresh"] {
317            if self
318                .query
319                .as_ref()
320                .and_then(|value| value.get(name))
321                .is_some()
322            {
323                _ = query.insert(name.into(), JsValue::Bool(self.session_query_truthy(name)));
324            }
325        }
326        context.query = Some(JsValue::Object(query));
327        context
328    }
329
330    #[must_use]
331    pub const fn extensions(&self) -> &RequestExtensions {
332        &self.extensions
333    }
334
335    #[must_use]
336    pub const fn default_method(&self) -> &HttpMethod {
337        &self.default_method
338    }
339
340    #[must_use]
341    pub fn virtual_session(&self) -> Option<SessionView> {
342        self.state.lock_unpoisoned().session.clone()
343    }
344
345    /// Retain the actual model and snapshot after a trusted hook has authenticated them.
346    /// This is an application/plugin authority boundary, never client input.
347    pub fn establish_session<S: AuthSchema>(
348        &self,
349        user: S::User,
350        user_view: UserView,
351        session: SessionView,
352        context: &AuthContext<S>,
353    ) {
354        self.extensions.insert(VerifiedEndpointUser::<S> {
355            user,
356            config: context.config.clone(),
357            database: context.database.clone(),
358        });
359        let mut state = self.state.lock_unpoisoned();
360        state.principal = Some((user_view, session.clone()));
361        state.session = Some(session);
362    }
363
364    /// Callback-visible authenticated data. This observation does not confer authority.
365    #[must_use]
366    pub fn session(&self) -> Option<(UserView, SessionView)> {
367        self.state.lock_unpoisoned().principal.clone()
368    }
369
370    pub fn record_authenticated_session(&self, user: UserView, session: SessionView) {
371        self.state.lock_unpoisoned().principal = Some((user, session));
372    }
373
374    #[must_use]
375    pub fn authenticated_user<S: AuthSchema>(&self, context: &AuthContext<S>) -> Option<S::User> {
376        self.extensions
377            .get::<VerifiedEndpointUser<S>>()
378            .filter(|user| {
379                Arc::ptr_eq(&user.config, &context.config)
380                    && Arc::ptr_eq(&user.database, &context.database)
381            })
382            .map(|user| user.user.clone())
383    }
384
385    pub fn set_response_header(&self, name: impl Into<String>, value: impl Into<String>) {
386        _ = self
387            .state
388            .lock_unpoisoned()
389            .response_headers
390            .insert(name, value);
391    }
392
393    pub fn queue_response_header(&self, name: impl Into<String>, value: impl Into<String>) {
394        self.state
395            .lock_unpoisoned()
396            .response_headers
397            .append(name, value);
398    }
399
400    pub fn take_response_headers(&self) -> Headers {
401        std::mem::take(&mut self.state.lock_unpoisoned().response_headers)
402    }
403
404    pub fn set_session_hook_snapshot(&self, user: UserView, session: SessionView) {
405        self.state.lock_unpoisoned().session_observation = Some((user, session));
406    }
407
408    #[must_use]
409    pub fn session_hook_snapshot(&self) -> Option<(UserView, SessionView)> {
410        self.state.lock_unpoisoned().session_observation.clone()
411    }
412
413    /// # Errors
414    /// Returns a typed decoding error for the current, post-hook body.
415    pub fn body_as<T: DeserializeOwned + 'static>(&self) -> AuthResult<T> {
416        Ok(crate::utils::json::from_value(
417            self.body.clone().unwrap_or(JsValue::Null),
418        )?)
419    }
420
421    /// # Errors
422    /// Returns a typed decoding error for the current, post-hook query.
423    pub fn query_as<T: DeserializeOwned + 'static>(&self) -> AuthResult<T> {
424        Ok(crate::utils::json::from_value(
425            self.query.clone().unwrap_or(JsValue::Null),
426        )?)
427    }
428}
429
430/// Context patches accumulate without replacing the original input seen by later before hooks.
431#[derive(Clone, Debug, Default)]
432pub struct EndpointContextPatch {
433    pub method: Option<HttpMethod>,
434    pub request: Option<AuthRequest>,
435    pub path: Option<String>,
436    pub body: Option<JsValue>,
437    pub query: Option<JsValue>,
438    pub headers: Option<HashMap<String, String>>,
439}
440
441impl EndpointContextPatch {
442    pub fn merge(&mut self, patch: Self) {
443        if let Some(method) = patch.method {
444            self.method = Some(method);
445        }
446        if let Some(request) = patch.request {
447            self.request = Some(request);
448        }
449        if let Some(path) = patch.path {
450            self.path = Some(path);
451        }
452        merge_optional(&mut self.body, patch.body);
453        merge_optional(&mut self.query, patch.query);
454        if let Some(headers) = patch.headers {
455            self.headers.get_or_insert_with(HashMap::new).extend(
456                headers
457                    .into_iter()
458                    .map(|(name, value)| (name.to_ascii_lowercase(), value)),
459            );
460        }
461    }
462
463    pub fn apply(self, call: &mut EndpointCall) {
464        if let Some(method) = self.method {
465            call.method = Some(method);
466        }
467        if let Some(request) = self.request {
468            call.request = Some(fresh_request(request));
469        }
470        if let Some(path) = self.path {
471            call.path = Some(path);
472        }
473        merge_optional(&mut call.body, self.body);
474        merge_optional(&mut call.query, self.query);
475        if let Some(headers) = self.headers {
476            call.headers
477                .get_or_insert_with(HashMap::new)
478                .extend(headers);
479        }
480    }
481}
482
483fn merge_optional(target: &mut Option<JsValue>, patch: Option<JsValue>) {
484    if let Some(patch) = patch.filter(|value| !value.is_null()) {
485        if let Some(target) = target {
486            merge_value(target, patch);
487        } else {
488            *target = Some(patch);
489        }
490    }
491}
492
493fn merge_value(target: &mut JsValue, patch: JsValue) {
494    if let (JsValue::Object(target), JsValue::Object(patch)) = (&mut *target, &patch) {
495        for (name, value) in patch {
496            if value.is_null() || matches!(name.as_str(), "__proto__" | "constructor") {
497                continue;
498            }
499            if let Some(target) = target.get_mut(name) {
500                merge_value(target, value.clone());
501            } else {
502                _ = target.insert(name.clone(), value.clone());
503            }
504        }
505    } else {
506        *target = patch;
507    }
508}
509
510/// A before hook can accumulate input changes or finish without reaching the handler/after hooks.
511#[derive(Debug)]
512pub enum BeforeEndpointAction {
513    Patch(Box<EndpointContextPatch>),
514    Respond(EndpointResponse),
515    /// Reject with an intentional API error and its complete public body.
516    Reject(EndpointResponse),
517}
518
519/// Validated handler input. The dispatcher retains raw patched data for after hooks.
520#[derive(Clone, Debug, Default)]
521pub struct EndpointInput {
522    pub body: Option<JsValue>,
523    pub query: Option<JsValue>,
524}
525
526/// A returned value or intentional API error, available to completed endpoint hooks.
527#[derive(Debug)]
528pub struct EndpointResponse {
529    result: Result<JsValue, AuthError>,
530    error_body: Option<JsValue>,
531    headers: Headers,
532    status: Option<u16>,
533}
534
535impl EndpointResponse {
536    #[must_use]
537    pub fn value(value: JsValue) -> Self {
538        Self {
539            result: Ok(value),
540            error_body: None,
541            headers: Headers::new(),
542            status: None,
543        }
544    }
545
546    /// # Errors
547    /// Returns an error if the endpoint value cannot be serialized.
548    pub fn json(value: &impl Serialize) -> AuthResult<Self> {
549        Ok(Self::value(crate::utils::json::parse_value(
550            &crate::utils::json::to_string(value)?,
551        )?))
552    }
553
554    #[must_use]
555    pub fn error(error: AuthError) -> Self {
556        let status = Some(error.status_code());
557        Self {
558            result: Err(error),
559            error_body: None,
560            headers: Headers::new(),
561            status,
562        }
563    }
564
565    #[must_use]
566    pub fn with_error_body(mut self, body: JsValue) -> Self {
567        self.error_body = Some(body);
568        self
569    }
570
571    #[must_use]
572    pub const fn error_body(&self) -> Option<&JsValue> {
573        self.error_body.as_ref()
574    }
575
576    pub const fn result(&self) -> &Result<JsValue, AuthError> {
577        &self.result
578    }
579
580    #[must_use]
581    pub const fn headers(&self) -> &Headers {
582        &self.headers
583    }
584
585    #[must_use]
586    pub const fn status(&self) -> Option<u16> {
587        self.status
588    }
589
590    pub fn set_status(&mut self, status: Option<u16>) {
591        self.status = status;
592    }
593
594    #[must_use]
595    pub fn with_status(mut self, status: u16) -> Self {
596        self.status = Some(status);
597        self
598    }
599
600    #[must_use]
601    pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
602        merge_header(&mut self.headers, name.into(), value.into());
603        self
604    }
605
606    pub fn merge_headers(&mut self, headers: Headers) {
607        for (name, value) in headers {
608            merge_header(&mut self.headers, name, value);
609        }
610    }
611
612    /// Replace a returned value while retaining the actual handler status and headers.
613    pub fn replace(&mut self, value: JsValue) {
614        self.result = Ok(value);
615        self.error_body = None;
616    }
617
618    pub fn replace_error(&mut self, error: AuthError) {
619        self.result = Err(error);
620        self.error_body = None;
621    }
622
623    pub fn into_output<T>(self) -> Result<EndpointOutput<T>, EndpointError> {
624        match self.result {
625            Ok(value) => Ok(EndpointOutput {
626                value,
627                headers: self.headers,
628                status: self.status,
629                output: PhantomData,
630            }),
631            Err(error) => Err(EndpointError {
632                error,
633                headers: Some(self.headers),
634                body: self.error_body.map(Box::new),
635            }),
636        }
637    }
638}
639
640fn merge_header(headers: &mut Headers, name: String, value: String) {
641    if name.eq_ignore_ascii_case("set-cookie") {
642        headers.append(name, value);
643    } else {
644        _ = headers.insert(name, value);
645    }
646}
647
648/// Installed application middleware for logical endpoint calls.
649#[async_trait::async_trait]
650pub trait EndpointHook<S: AuthSchema>: Send + Sync {
651    /// Matchers observe raw logical context, before middleware normalization.
652    /// # Errors
653    /// Returns an application matcher error, which dispatch logs and masks.
654    fn matches_before(&self, _call: &EndpointCall, _ctx: &AuthContext<S>) -> AuthResult<bool> {
655        Ok(true)
656    }
657
658    /// # Errors
659    /// Propagates actual callback failures without reaching later hooks or the handler.
660    async fn before(
661        &self,
662        _call: &EndpointCall,
663        _ctx: &AuthContext<S>,
664    ) -> AuthResult<Option<BeforeEndpointAction>> {
665        Ok(None)
666    }
667
668    /// # Errors
669    /// Propagates actual after-matcher failures.
670    fn matches_after(
671        &self,
672        _call: &EndpointCall,
673        _ctx: &AuthContext<S>,
674        _response: &EndpointResponse,
675    ) -> AuthResult<bool> {
676        Ok(true)
677    }
678
679    /// # Errors
680    /// API errors replace the result and continue; ordinary failures abort remaining hooks.
681    async fn after(
682        &self,
683        _call: &EndpointCall,
684        _ctx: &AuthContext<S>,
685        response: EndpointResponse,
686    ) -> AuthResult<EndpointResponse> {
687        Ok(response)
688    }
689}
690
691/// A successful call retains the actual value, since after hooks can replace its shape.
692#[derive(Debug)]
693pub struct EndpointOutput<T> {
694    value: JsValue,
695    headers: Headers,
696    status: Option<u16>,
697    output: PhantomData<fn() -> T>,
698}
699
700impl<T> EndpointOutput<T> {
701    #[must_use]
702    pub const fn value(&self) -> &JsValue {
703        &self.value
704    }
705
706    #[must_use]
707    pub const fn headers(&self) -> &Headers {
708        &self.headers
709    }
710
711    #[must_use]
712    pub const fn status(&self) -> Option<u16> {
713        self.status
714    }
715}
716
717impl<T: DeserializeOwned + 'static> EndpointOutput<T> {
718    /// # Errors
719    /// Returns an error if an application hook replaced the expected output type.
720    pub fn decode(&self) -> AuthResult<T> {
721        Ok(crate::utils::json::from_value(self.value.clone())?)
722    }
723}
724
725/// Actual application/API failure, with headers accumulated before an API error.
726#[derive(Debug)]
727pub struct EndpointError {
728    pub error: AuthError,
729    pub headers: Option<Headers>,
730    pub body: Option<Box<JsValue>>,
731}
732
733impl From<AuthError> for EndpointError {
734    fn from(error: AuthError) -> Self {
735        Self {
736            error,
737            headers: None,
738            body: None,
739        }
740    }
741}
742
743impl std::fmt::Display for EndpointError {
744    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
745        self.error.fmt(formatter)
746    }
747}
748
749impl std::error::Error for EndpointError {
750    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
751        Some(&self.error)
752    }
753}
754
755/// Intentional endpoint/API errors continue through after hooks; ordinary failures propagate.
756#[must_use]
757pub fn is_endpoint_api_error(error: &AuthError) -> bool {
758    matches!(error, AuthError::Api { .. } | AuthError::Upstream { .. }) || error.status_code() < 500
759}
760
761fn fresh_request(request: AuthRequest) -> AuthRequest {
762    let pairs = request
763        .query
764        .keys()
765        .flat_map(|name| {
766            request
767                .query_values(name)
768                .into_iter()
769                .flatten()
770                .map(|value| (name.clone(), value.clone()))
771        })
772        .collect::<Vec<_>>();
773    let url = request.url().cloned();
774    let mut fresh = AuthRequest::from_parts(
775        request.method,
776        request.path,
777        request.headers,
778        request.body,
779        request.query,
780    );
781    if let Some(url) = url {
782        fresh = fresh.with_url(url);
783    }
784    fresh.set_query_pairs(pairs);
785    fresh
786}
787
788tokio::task_local! { static ENDPOINT_CALL_CONTEXT: Option<EndpointCall>; }
789
790#[must_use]
791pub fn current_endpoint_call_context() -> Option<EndpointCall> {
792    ENDPOINT_CALL_CONTEXT.try_with(Clone::clone).ok().flatten()
793}
794
795pub async fn with_endpoint_call_context<T>(
796    call: EndpointCall,
797    future: impl Future<Output = T>,
798) -> T {
799    let request = call
800        .request()
801        .map(crate::hooks::RequestHookContext::from_request);
802    crate::hooks::with_optional_request_hook_context(
803        request,
804        ENDPOINT_CALL_CONTEXT.scope(Some(call), future),
805    )
806    .await
807}
808
809/// Isolate an independent HTTP dispatch from an enclosing logical endpoint call.
810pub async fn without_endpoint_call_context<T>(future: impl Future<Output = T>) -> T {
811    ENDPOINT_CALL_CONTEXT.scope(None, future).await
812}