Skip to main content

scm_sdk/
engine.rs

1use crate::{
2    Endpoint, Error, ErrorKind, Limits, OriginPolicy, RemoteOutcome, RequestOptions, Result,
3    Session,
4    auth::{AuthContext, Authentication, OAuthProvider},
5    config::{DestinationPolicy, DestinationPurpose},
6    transport::{Transport, TransportRequest, read_limited},
7};
8use bytes::Bytes;
9use http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode};
10use serde::{Serialize, de::DeserializeOwned};
11use std::{
12    fmt,
13    io::Write,
14    sync::{
15        Arc,
16        atomic::{AtomicBool, Ordering},
17    },
18    time::{Duration, SystemTime},
19};
20use tokio::sync::Semaphore;
21
22pub(crate) const SDK_USER_AGENT: &str =
23    concat!("PAN-AIRS/", env!("CARGO_PKG_VERSION"), "-rust-sdk");
24fn default_headers() -> HeaderMap {
25    let mut headers = HeaderMap::new();
26    headers.insert(
27        http::header::USER_AGENT,
28        HeaderValue::from_static(SDK_USER_AGENT),
29    );
30    headers.insert("service-name", HeaderValue::from_static("api"));
31    headers
32}
33
34/// An explicitly raw request for advanced integrations. Product clients validate their
35/// typed contracts before reaching this seam. Raw callers own service-specific validation.
36pub struct RawRequest {
37    name: &'static str,
38    method: Method,
39    path: String,
40    segments: Vec<String>,
41    query: Vec<(String, String)>,
42    headers: HeaderMap,
43    body: Option<Bytes>,
44    replay_safe: bool,
45    runtime_retries: Option<u32>,
46}
47impl RawRequest {
48    /// No replay is assumed for an arbitrary operation, including an arbitrary GET.
49    pub fn new(method: Method, path: impl Into<String>) -> Self {
50        Self {
51            name: "custom.request",
52            method,
53            path: path.into(),
54            segments: Vec::new(),
55            query: Vec::new(),
56            headers: default_headers(),
57            body: None,
58            replay_safe: false,
59            runtime_retries: None,
60        }
61    }
62    /// Declare a GET with read semantics, allowing bounded replay-safe retries.
63    pub fn read(path: impl Into<String>) -> Self {
64        Self::new(Method::GET, path)
65            .named("custom.read")
66            .safe_read()
67    }
68    pub fn query(mut self, values: impl IntoIterator<Item = (String, String)>) -> Self {
69        self.query.extend(values);
70        self
71    }
72    /// Append identifier data encoded once as a path segment. Literal slashes and
73    /// percent signs are data; recursively encoded traversal is rejected locally.
74    pub fn segment(mut self, value: impl AsRef<str>) -> Result<Self> {
75        let value = value.as_ref();
76        crate::config::encode_segment(value)?;
77        self.segments.push(value.to_owned());
78        Ok(self)
79    }
80    /// Authentication, routing authority and framing headers belong to the SDK.
81    pub fn header(mut self, name: &str, value: &str) -> Result<Self> {
82        let name = HeaderName::from_bytes(name.as_bytes())
83            .map_err(|_| Error::new(ErrorKind::Validation))?;
84        if forbidden_header(name.as_str()) {
85            return Err(Error::new(ErrorKind::Validation));
86        }
87        let mut value =
88            HeaderValue::from_str(value).map_err(|_| Error::new(ErrorKind::Validation))?;
89        value.set_sensitive(true);
90        self.headers.append(name, value);
91        Ok(self)
92    }
93    /// Send bytes with an explicit content type. Bytes remain exact across signing/retries.
94    pub fn body(mut self, bytes: Bytes, content_type: &str) -> Result<Self> {
95        content_type
96            .parse::<mime::Mime>()
97            .map_err(|_| Error::new(ErrorKind::Validation))?;
98        let value =
99            HeaderValue::from_str(content_type).map_err(|_| Error::new(ErrorKind::Validation))?;
100        self.headers.insert(http::header::CONTENT_TYPE, value);
101        self.body = Some(bytes);
102        Ok(self)
103    }
104    /// Encode an already constructed finite JSON value within an explicit byte bound.
105    pub fn json_value(self, value: &serde_json::Value, limit: usize) -> Result<Self> {
106        self.json(value, limit)
107    }
108    pub(crate) fn named(mut self, name: &'static str) -> Self {
109        self.name = name;
110        self
111    }
112    pub(crate) fn safe_read(mut self) -> Self {
113        self.replay_safe = true;
114        self
115    }
116    #[cfg(feature = "gateway")]
117    pub(crate) fn runtime_replay(&mut self, retries: Option<usize>) {
118        self.replay_safe = false;
119        self.runtime_retries = retries.map(|value| value as u32);
120    }
121    #[cfg(feature = "gateway")]
122    pub(crate) fn routing_headers(&mut self, headers: &HeaderMap) -> Result<()> {
123        if headers.keys().any(|name| self.headers.contains_key(name)) {
124            return Err(Error::new(ErrorKind::Validation));
125        }
126        self.headers.extend(headers.clone());
127        if headers_exceed(&self.headers) {
128            return Err(Error::new(ErrorKind::Limit));
129        }
130        Ok(())
131    }
132    #[cfg(feature = "dlp")]
133    pub(crate) fn content_type(mut self, value: &str) -> Result<Self> {
134        value
135            .parse::<mime::Mime>()
136            .map_err(|_| Error::new(ErrorKind::Validation))?;
137        self.headers.insert(
138            http::header::CONTENT_TYPE,
139            HeaderValue::from_str(value).map_err(|_| Error::new(ErrorKind::Validation))?,
140        );
141        Ok(self)
142    }
143    pub(crate) fn json<T: Serialize>(self, value: &T, limit: usize) -> Result<Self> {
144        let content_type = self
145            .headers
146            .get(http::header::CONTENT_TYPE)
147            .and_then(|value| value.to_str().ok())
148            .unwrap_or("application/json")
149            .to_owned();
150        self.body(serialize_bounded(value, limit)?, &content_type)
151    }
152}
153fn runtime_retryable(status: StatusCode) -> bool {
154    matches!(status.as_u16(), 500 | 502 | 503 | 504)
155}
156fn forbidden_header(name: &str) -> bool {
157    matches!(
158        name,
159        "authorization"
160            | "proxy-authorization"
161            | "cookie"
162            | "host"
163            | "content-length"
164            | "content-type"
165            | "transfer-encoding"
166            | "connection"
167            | "upgrade"
168            | "x-pan-token"
169            | "x-payload-hash"
170            | "x-portkey-api-key"
171            | "x-tsg-id"
172            | "service-name"
173            | "user-agent"
174    )
175}
176fn headers_exceed(headers: &HeaderMap) -> bool {
177    headers.len() > 128
178        || headers
179            .iter()
180            .try_fold(0usize, |n, (k, v)| {
181                n.checked_add(k.as_str().len())?
182                    .checked_add(v.as_bytes().len())
183            })
184            .is_none_or(|n| n > 16 * 1024)
185}
186impl fmt::Debug for RawRequest {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        f.debug_struct("RawRequest")
189            .field("operation", &self.name)
190            .field("method", &self.method)
191            .field("has_body", &self.body.is_some())
192            .finish_non_exhaustive()
193    }
194}
195
196/// Shared configuration for product clients. Clones share the pool, session and limiter.
197#[derive(Clone)]
198pub struct Client {
199    inner: Arc<Inner>,
200}
201struct BufferedResponse {
202    body: Bytes,
203    status: u16,
204    request_id: Option<String>,
205}
206impl BufferedResponse {
207    fn annotate(&self, error: Error) -> Error {
208        let error = error
209            .with_status(self.status)
210            .with_metadata(self.request_id.clone(), None, None)
211            .with_outcome(RemoteOutcome::Unknown);
212        crate::observability::failure(crate::observability::Operation::Decode, &error);
213        error
214    }
215}
216#[cfg(any(
217    feature = "scan",
218    feature = "management",
219    feature = "gateway",
220    feature = "dlp",
221    feature = "iam",
222    feature = "model-security",
223    feature = "red-team",
224    feature = "agentguard"
225))]
226pub(crate) enum AuthKind {
227    #[cfg(feature = "scan")]
228    Scan,
229    #[cfg(any(
230        feature = "management",
231        feature = "gateway",
232        feature = "dlp",
233        feature = "iam",
234        feature = "model-security",
235        feature = "red-team",
236        feature = "agentguard"
237    ))]
238    OAuth,
239    #[cfg(feature = "gateway")]
240    Runtime,
241    #[cfg(feature = "gateway")]
242    Public,
243}
244struct Inner {
245    endpoint: Endpoint,
246    auth: Authentication,
247    transport: Arc<dyn Transport>,
248    policy: Arc<dyn DestinationPolicy>,
249    limits: Limits,
250    session: Session,
251    permits: Arc<Semaphore>,
252    validation: Option<crate::validation::ValidationLimits>,
253}
254pub struct ClientBuilder {
255    endpoint: Endpoint,
256    auth: Authentication,
257    transport: Option<Arc<dyn Transport>>,
258    policy: Option<Arc<dyn DestinationPolicy>>,
259    limits: Limits,
260    session: Session,
261    oauth: Option<(AuthContext, crate::Secret, bool)>,
262    oauth_options: crate::auth::OAuthProviderOptions,
263    validation: Option<crate::validation::ValidationLimits>,
264}
265impl ClientBuilder {
266    pub fn new(endpoint: Endpoint) -> Self {
267        Self {
268            endpoint,
269            auth: Authentication::None,
270            transport: None,
271            policy: None,
272            limits: Limits::default(),
273            session: Session::new(),
274            oauth: None,
275            oauth_options: crate::auth::OAuthProviderOptions::default(),
276            validation: None,
277        }
278    }
279    /// A caller-supplied provider owns its token-exchange transport and policy.
280    pub fn authentication(mut self, value: Authentication) -> Self {
281        self.auth = value;
282        self.oauth = None;
283        self
284    }
285    /// Manage service credentials using this client's transport, destination policy,
286    /// and session for both token exchange and API requests. The default policy admits
287    /// only the API and issuer origins; a supplied policy must admit both explicitly.
288    pub fn oauth_credentials(
289        mut self,
290        context: AuthContext,
291        secret: crate::Secret,
292        tsg_header: bool,
293    ) -> Self {
294        self.auth = Authentication::None;
295        self.oauth = Some((context, secret, tsg_header));
296        self
297    }
298    /// Configure this client's immutable managed OAuth refresh policy and events.
299    pub fn oauth_provider_options(mut self, options: crate::auth::OAuthProviderOptions) -> Self {
300        self.oauth_options = options;
301        self
302    }
303    pub fn transport(mut self, value: Arc<dyn Transport>) -> Self {
304        self.transport = Some(value);
305        self
306    }
307    pub fn destination_policy(mut self, value: Arc<dyn DestinationPolicy>) -> Self {
308        self.policy = Some(value);
309        self
310    }
311    pub fn limits(mut self, value: Limits) -> Self {
312        self.limits = value;
313        self
314    }
315    /// Set native JSON resource bounds. Request/response byte limits still apply.
316    pub fn validation_limits(mut self, value: crate::validation::ValidationLimits) -> Self {
317        self.validation = Some(value);
318        self
319    }
320    pub fn session(mut self, value: Session) -> Self {
321        self.session = value;
322        self
323    }
324    pub fn build(self) -> Result<Client> {
325        self.limits.validate()?;
326        if let Some(value) = &self.validation {
327            value.validate()?;
328        }
329        let policy = self.policy.unwrap_or_else(|| {
330            let mut endpoints = vec![self.endpoint.clone()];
331            if let Some((context, _, _)) = &self.oauth {
332                endpoints.push(context.issuer().clone());
333            }
334            Arc::new(OriginPolicy::new(endpoints))
335        });
336        let transport = match self.transport {
337            Some(value) => value,
338            None => {
339                #[cfg(feature = "reqwest-transport")]
340                {
341                    Arc::new(crate::transport::ReqwestTransport::new()?) as Arc<dyn Transport>
342                }
343                #[cfg(not(feature = "reqwest-transport"))]
344                {
345                    return Err(Error::new(ErrorKind::Configuration));
346                }
347            }
348        };
349        let auth = match self.oauth {
350            Some((context, secret, tsg_header)) => {
351                let provider = OAuthProvider::new_with_options(
352                    context,
353                    secret,
354                    Arc::clone(&transport),
355                    Arc::clone(&policy),
356                    self.session.clone(),
357                    self.oauth_options,
358                )?;
359                Authentication::oauth(Arc::new(provider), tsg_header)
360            }
361            None => self.auth,
362        };
363        let permits = Arc::new(Semaphore::new(self.limits.max_concurrency));
364        Ok(Client {
365            inner: Arc::new(Inner {
366                endpoint: self.endpoint,
367                auth,
368                transport,
369                policy,
370                limits: self.limits,
371                session: self.session,
372                permits,
373                validation: self.validation,
374            }),
375        })
376    }
377}
378impl fmt::Debug for ClientBuilder {
379    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380        f.write_str("ClientBuilder(..)")
381    }
382}
383impl fmt::Debug for Client {
384    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385        f.debug_struct("Client")
386            .field("endpoint", &self.inner.endpoint)
387            .field("limits", &self.inner.limits)
388            .finish_non_exhaustive()
389    }
390}
391impl Client {
392    pub fn builder(endpoint: Endpoint) -> ClientBuilder {
393        ClientBuilder::new(endpoint)
394    }
395    pub fn limits(&self) -> &Limits {
396        &self.inner.limits
397    }
398    pub fn validation_limits(&self) -> Option<&crate::validation::ValidationLimits> {
399        self.inner.validation.as_ref()
400    }
401    #[cfg(any(
402        feature = "scan",
403        feature = "management",
404        feature = "gateway",
405        feature = "dlp",
406        feature = "iam",
407        feature = "model-security",
408        feature = "red-team",
409        feature = "agentguard"
410    ))]
411    pub(crate) fn contract_limits(
412        &self,
413        max_bytes: usize,
414    ) -> Result<crate::validation::ValidationLimits> {
415        let mut limits = self.inner.validation.clone().map_or_else(
416            || crate::validation::ValidationLimits::for_bytes(max_bytes),
417            Ok,
418        )?;
419        limits.max_input_bytes = limits.max_input_bytes.min(max_bytes);
420        limits.validate()?;
421        Ok(limits)
422    }
423    pub fn session(&self) -> Session {
424        self.inner.session.clone()
425    }
426    pub fn endpoint(&self) -> &Endpoint {
427        &self.inner.endpoint
428    }
429    /// Immutable OAuth identity, when this client uses a service-token provider.
430    pub fn oauth_context(&self) -> Option<&AuthContext> {
431        match &self.inner.auth {
432            Authentication::OAuth { provider, .. } => Some(provider.context()),
433            _ => None,
434        }
435    }
436    /// Select an additional explicit endpoint without broadening destination admission.
437    /// The new client shares authentication, transport, limits, cancellation, and permits.
438    pub fn with_endpoint(&self, endpoint: Endpoint) -> Result<Self> {
439        self.inner
440            .policy
441            .check(endpoint.url(), DestinationPurpose::Api)
442            .map_err(|_| Error::new(ErrorKind::Destination))?;
443        Ok(self.scoped(endpoint, self.inner.auth.clone()))
444    }
445    /// Bind or omit the tenant routing header using the provider's immutable identity.
446    /// Product clients that require tenant routing enable it in their constructor.
447    pub fn with_tsg_header(&self, enabled: bool) -> Result<Self> {
448        let Authentication::OAuth { provider, .. } = &self.inner.auth else {
449            return Err(Error::new(ErrorKind::Configuration));
450        };
451        Ok(self.scoped(
452            self.inner.endpoint.clone(),
453            Authentication::oauth(Arc::clone(provider), enabled),
454        ))
455    }
456    fn scoped(&self, endpoint: Endpoint, auth: Authentication) -> Self {
457        Self {
458            inner: Arc::new(Inner {
459                endpoint,
460                auth,
461                transport: Arc::clone(&self.inner.transport),
462                policy: Arc::clone(&self.inner.policy),
463                limits: self.inner.limits.clone(),
464                session: self.inner.session.clone(),
465                permits: Arc::clone(&self.inner.permits),
466                validation: self.inner.validation.clone(),
467            }),
468        }
469    }
470    pub(crate) fn operation_options(
471        &self,
472        options: Option<&RequestOptions>,
473    ) -> Result<RequestOptions> {
474        options.cloned().map_or_else(
475            || RequestOptions::with_timeout(self.inner.limits.operation_timeout),
476            Ok,
477        )
478    }
479    #[cfg(any(
480        feature = "scan",
481        feature = "management",
482        feature = "gateway",
483        feature = "dlp",
484        feature = "iam",
485        feature = "model-security",
486        feature = "red-team",
487        feature = "agentguard"
488    ))]
489    pub(crate) fn require_auth(&self, kind: AuthKind) -> Result<()> {
490        let valid = match kind {
491            #[cfg(feature = "scan")]
492            AuthKind::Scan => matches!(&self.inner.auth, Authentication::Scan { .. }),
493            #[cfg(any(
494                feature = "management",
495                feature = "gateway",
496                feature = "dlp",
497                feature = "iam",
498                feature = "model-security",
499                feature = "red-team",
500                feature = "agentguard"
501            ))]
502            AuthKind::OAuth => matches!(&self.inner.auth, Authentication::OAuth { .. }),
503            #[cfg(feature = "gateway")]
504            AuthKind::Runtime => matches!(&self.inner.auth, Authentication::RuntimeKey { .. }),
505            #[cfg(feature = "gateway")]
506            AuthKind::Public => matches!(&self.inner.auth, Authentication::None),
507        };
508        if valid {
509            Ok(())
510        } else {
511            Err(Error::new(ErrorKind::Configuration))
512        }
513    }
514    #[cfg(any(
515        feature = "scan",
516        feature = "management",
517        feature = "gateway",
518        feature = "dlp",
519        feature = "iam",
520        feature = "model-security",
521        feature = "red-team",
522        feature = "agentguard"
523    ))]
524    pub(crate) async fn validated_json<T: Serialize>(
525        &self,
526        body: &T,
527        schema: &str,
528        options: &RequestOptions,
529    ) -> Result<Bytes> {
530        options
531            .run(&self.inner.session, async {
532                let bytes = serialize_bounded(body, self.inner.limits.max_request_bytes)?;
533                let value = crate::json_decode::from_slice(&bytes)
534                    .map_err(|_| Error::new(ErrorKind::Validation))?;
535                let value = normalize_contract(
536                    schema,
537                    value,
538                    crate::validation::Mode::Request,
539                    &self.contract_limits(self.inner.limits.max_request_bytes)?,
540                )?;
541                serialize_bounded(&value, self.inner.limits.max_request_bytes)
542            })
543            .await
544    }
545    #[cfg(any(
546        feature = "scan",
547        feature = "management",
548        feature = "gateway",
549        feature = "dlp",
550        feature = "iam",
551        feature = "model-security",
552        feature = "red-team"
553    ))]
554    async fn prepare_model<T: Serialize>(
555        &self,
556        request: RawRequest,
557        body: &T,
558        schema: &str,
559        options: &RequestOptions,
560    ) -> Result<RawRequest> {
561        let name = request.name;
562        let bytes = self
563            .validated_json(body, schema, options)
564            .await
565            .map_err(|e| e.with_operation(name))?;
566        let content_type = request
567            .headers
568            .get(http::header::CONTENT_TYPE)
569            .and_then(|v| v.to_str().ok())
570            .unwrap_or("application/json")
571            .to_owned();
572        request.body(bytes, &content_type)
573    }
574    #[cfg(any(
575        feature = "scan",
576        feature = "management",
577        feature = "gateway",
578        feature = "dlp",
579        feature = "iam",
580        feature = "model-security",
581        feature = "red-team"
582    ))]
583    pub(crate) async fn send_model<T: Serialize, R: DeserializeOwned>(
584        &self,
585        request: RawRequest,
586        body: &T,
587        request_schema: &str,
588        response_schema: &str,
589        options: Option<&RequestOptions>,
590    ) -> Result<R> {
591        let options = self.operation_options(options)?;
592        let request = self
593            .prepare_model(request, body, request_schema, &options)
594            .await?;
595        self.send_contract(request, response_schema, Some(&options))
596            .await
597    }
598    #[cfg(any(
599        feature = "scan",
600        feature = "management",
601        feature = "gateway",
602        feature = "dlp",
603        feature = "iam",
604        feature = "model-security",
605        feature = "red-team",
606        feature = "agentguard"
607    ))]
608    pub(crate) async fn send_contract<R: DeserializeOwned>(
609        &self,
610        request: RawRequest,
611        schema: &str,
612        options: Option<&RequestOptions>,
613    ) -> Result<R> {
614        let name = request.name;
615        let options = self.operation_options(options)?;
616        let response = self.send_buffered(request, Some(&options)).await?;
617        self.decode_contract(&response, schema, &options)
618            .await
619            .map_err(|e| response.annotate(e).with_operation(name))
620    }
621    #[cfg(feature = "gateway")]
622    pub(crate) async fn send_contract_one_of<R: DeserializeOwned>(
623        &self,
624        request: RawRequest,
625        schemas: &[&str],
626        options: Option<&RequestOptions>,
627    ) -> Result<R> {
628        let name = request.name;
629        let options = self.operation_options(options)?;
630        let response = self.send_buffered(request, Some(&options)).await?;
631        options
632            .run(&self.inner.session, async {
633                let body = if response.body.is_empty() {
634                    b"{}".as_slice()
635                } else {
636                    &response.body
637                };
638                let value = crate::json_decode::from_slice(body)
639                    .map_err(|_| Error::new(ErrorKind::Decode))?;
640                let value = crate::validation::parse_one_of_with_limits(
641                    schemas,
642                    value,
643                    options.response_mode(),
644                    &self.contract_limits(self.inner.limits.max_response_bytes)?,
645                )
646                .map_err(|error| {
647                    Error::new(if error.kind() == ErrorKind::Limit {
648                        ErrorKind::Limit
649                    } else {
650                        ErrorKind::Decode
651                    })
652                })?;
653                crate::json_decode::from_value(value).map_err(|_| Error::new(ErrorKind::Decode))
654            })
655            .await
656            .map_err(|error| response.annotate(error).with_operation(name))
657    }
658    #[cfg(feature = "red-team")]
659    pub(crate) async fn send_model_optional<T: Serialize, R: DeserializeOwned>(
660        &self,
661        request: RawRequest,
662        body: &T,
663        request_schema: &str,
664        response_schema: &str,
665        options: Option<&RequestOptions>,
666    ) -> Result<Option<R>> {
667        let options = self.operation_options(options)?;
668        let request = self
669            .prepare_model(request, body, request_schema, &options)
670            .await?;
671        self.send_optional_contract(request, response_schema, Some(&options))
672            .await
673    }
674    #[cfg(any(feature = "dlp", feature = "red-team"))]
675    pub(crate) async fn send_optional_contract<R: DeserializeOwned>(
676        &self,
677        request: RawRequest,
678        schema: &str,
679        options: Option<&RequestOptions>,
680    ) -> Result<Option<R>> {
681        let name = request.name;
682        let options = self.operation_options(options)?;
683        let response = self.send_buffered(request, Some(&options)).await?;
684        if response.body.is_empty() {
685            return Ok(None);
686        }
687        self.decode_contract(&response, schema, &options)
688            .await
689            .map(Some)
690            .map_err(|e| response.annotate(e).with_operation(name))
691    }
692    #[cfg(any(
693        feature = "scan",
694        feature = "management",
695        feature = "gateway",
696        feature = "dlp",
697        feature = "iam",
698        feature = "model-security",
699        feature = "red-team",
700        feature = "agentguard"
701    ))]
702    async fn decode_contract<R: DeserializeOwned>(
703        &self,
704        response: &BufferedResponse,
705        schema: &str,
706        options: &RequestOptions,
707    ) -> Result<R> {
708        options
709            .run(&self.inner.session, async {
710                // The frozen request engine hydrates empty JSON successes as {}.
711                let body = if response.body.is_empty() {
712                    b"{}".as_slice()
713                } else {
714                    response.body.as_ref()
715                };
716                let value = crate::json_decode::from_slice(body)
717                    .map_err(|_| Error::new(ErrorKind::Decode))?;
718                let value = normalize_contract(
719                    schema,
720                    value,
721                    options.response_mode(),
722                    &self.contract_limits(self.inner.limits.max_response_bytes)?,
723                )
724                .map_err(|e| {
725                    Error::new(if e.kind() == ErrorKind::Limit {
726                        ErrorKind::Limit
727                    } else {
728                        ErrorKind::Decode
729                    })
730                })?;
731                crate::json_decode::from_value(value).map_err(|_| Error::new(ErrorKind::Decode))
732            })
733            .await
734    }
735    #[cfg(feature = "gateway")]
736    pub(crate) async fn send_contract_stream<R: DeserializeOwned>(
737        &self,
738        request: RawRequest,
739        schema: &'static str,
740        terminal: crate::streaming::TerminalPolicy,
741        options: Option<&RequestOptions>,
742    ) -> Result<crate::streaming::ContractStream<R>> {
743        let options = self.operation_options(options)?;
744        let name = request.name;
745        let response = self.send_stream(request, Some(&options)).await?;
746        let status = response.status().as_u16();
747        let request_id = response.request_id().map(str::to_owned);
748        let stream = crate::streaming::SseStream::new(
749            response,
750            self.inner.limits.max_event_bytes,
751            terminal,
752        )?;
753        let contract = crate::streaming::EventContract {
754            schema,
755            operation: name,
756            status,
757            request_id,
758            limits: self.contract_limits(self.inner.limits.max_event_bytes)?,
759        };
760        Ok(crate::streaming::ContractStream::new(
761            stream,
762            contract,
763            options,
764            self.inner.session.clone(),
765        ))
766    }
767    #[cfg(feature = "realtime")]
768    pub(crate) async fn open_realtime(
769        &self,
770        request: RawRequest,
771        settings: &crate::realtime::GatewayRealtimeOptions,
772        options: &RequestOptions,
773    ) -> Result<crate::realtime::GatewayRealtimeConnection> {
774        let lifecycle = crate::observability::Lifecycle::start(
775            crate::observability::Operation::RealtimeUpgrade,
776        );
777        let result = self
778            .open_realtime_observed(request, settings, options, &lifecycle)
779            .await;
780        lifecycle.finish(&result);
781        result
782    }
783    #[cfg(feature = "realtime")]
784    async fn open_realtime_observed(
785        &self,
786        mut request: RawRequest,
787        settings: &crate::realtime::GatewayRealtimeOptions,
788        options: &RequestOptions,
789        lifecycle: &crate::observability::Lifecycle,
790    ) -> Result<crate::realtime::GatewayRealtimeConnection> {
791        use crate::realtime::{GatewayRealtimeConnection, OPERATION, WebSocketRequest};
792        self.require_auth(AuthKind::Runtime)?;
793        settings.validate()?;
794        let mut settings = settings.clone();
795        settings.max_event_bytes = settings
796            .max_event_bytes
797            .min(self.inner.limits.max_event_bytes)
798            .min(settings.max_buffered_bytes);
799        settings.idle_timeout = settings
800            .idle_timeout
801            .min(self.inner.limits.stream_idle_timeout);
802        let validation = self.contract_limits(settings.max_event_bytes)?;
803        // The frozen WebSocket path bypasses common HTTP API headers.
804        request.headers.remove(http::header::USER_AGENT);
805        request.headers.remove("service-name");
806        let (mut url, body) = self.preflight(&request)?;
807        if request.method != Method::GET || !body.is_empty() {
808            return Err(Error::new(ErrorKind::Validation));
809        }
810        let scheme = match url.scheme() {
811            "https" => "wss",
812            "http" => "ws",
813            _ => return Err(Error::new(ErrorKind::Destination)),
814        };
815        url.set_scheme(scheme)
816            .map_err(|_| Error::new(ErrorKind::Destination))?;
817        self.inner
818            .policy
819            .check(&url, DestinationPurpose::Realtime)
820            .map_err(|_| Error::new(ErrorKind::Destination))?;
821        let sent = AtomicBool::new(false);
822        let deadline = options.deadline().min(
823            tokio::time::Instant::now()
824                .checked_add(settings.handshake_timeout)
825                .ok_or_else(|| Error::new(ErrorKind::Configuration))?,
826        );
827        let result = options
828            .run(&self.inner.session, async {
829                tokio::time::timeout_at(deadline, async {
830                    let permit = Arc::clone(&self.inner.permits)
831                        .acquire_owned()
832                        .await
833                        .map_err(|_| Error::new(ErrorKind::Cancelled))?;
834                    let mut headers = request.headers;
835                    self.inner
836                        .auth
837                        .prepare(&mut headers, &body, false, options)
838                        .await?;
839                    if headers_exceed(&headers) {
840                        return Err(Error::new(ErrorKind::Limit));
841                    }
842                    for value in headers.values_mut() {
843                        value.set_sensitive(true);
844                    }
845                    options.run(&self.inner.session, async { Ok(()) }).await?;
846                    lifecycle.dispatched();
847                    sent.store(true, Ordering::Relaxed);
848                    let upgrade = settings
849                        .connector
850                        .connect(WebSocketRequest {
851                            url,
852                            headers,
853                            handshake_timeout: deadline
854                                .saturating_duration_since(tokio::time::Instant::now()),
855                            max_event_bytes: settings.max_event_bytes,
856                        })
857                        .await?;
858                    lifecycle.response(
859                        upgrade.status.as_u16(),
860                        error_metadata(&upgrade.headers, None).0.is_some(),
861                    );
862                    if upgrade.status != StatusCode::SWITCHING_PROTOCOLS {
863                        return Err(Error::new(if upgrade.status.is_redirection() {
864                            ErrorKind::Destination
865                        } else {
866                            ErrorKind::Http
867                        })
868                        .with_status(upgrade.status.as_u16())
869                        .with_metadata(
870                            error_metadata(&upgrade.headers, None).0,
871                            None,
872                            retry_after(&upgrade.headers),
873                        )
874                        .with_failure_kind(crate::FailureKind::Http)
875                        .with_outcome(
876                            if upgrade.status.is_client_error() || upgrade.status.is_redirection() {
877                                RemoteOutcome::Rejected
878                            } else {
879                                RemoteOutcome::Unknown
880                            },
881                        ));
882                    }
883                    Ok(GatewayRealtimeConnection::new(
884                        upgrade,
885                        permit,
886                        options.clone(),
887                        self.inner.session.clone(),
888                        settings,
889                        validation,
890                    ))
891                })
892                .await
893                .unwrap_or_else(|_| Err(Error::new(ErrorKind::Deadline)))
894            })
895            .await;
896        result.map_err(|error| {
897            let outcome = if sent.load(Ordering::Relaxed)
898                && error.remote_outcome() == RemoteOutcome::NotSent
899            {
900                RemoteOutcome::Unknown
901            } else {
902                error.remote_outcome()
903            };
904            error.with_operation(OPERATION).with_outcome(outcome)
905        })
906    }
907
908    fn preflight(&self, request: &RawRequest) -> Result<(url::Url, Bytes)> {
909        let url = self
910            .inner
911            .endpoint
912            .resolve(&request.path, &request.segments, &request.query)?;
913        self.inner.endpoint.admit(&url)?;
914        self.inner
915            .policy
916            .check(&url, DestinationPurpose::Api)
917            .map_err(|_| Error::new(ErrorKind::Destination))?;
918        if request.runtime_retries.is_some()
919            && !matches!(&self.inner.auth, Authentication::RuntimeKey { .. })
920        {
921            return Err(Error::new(ErrorKind::Configuration));
922        }
923        let body = request.body.clone().unwrap_or_default();
924        if body.len() > self.inner.limits.max_request_bytes || headers_exceed(&request.headers) {
925            return Err(Error::new(ErrorKind::Limit).with_operation(request.name));
926        }
927        if request
928            .headers
929            .get(http::header::CONTENT_TYPE)
930            .and_then(|h| h.to_str().ok())
931            .and_then(|v| v.parse::<mime::Mime>().ok())
932            .is_some_and(|v| {
933                v.type_() == mime::APPLICATION
934                    && (v.subtype() == mime::JSON || v.suffix() == Some(mime::JSON))
935            })
936            && request.body.is_some()
937            && crate::json_decode::from_slice(&body).is_err()
938        {
939            return Err(Error::new(ErrorKind::Validation).with_operation(request.name));
940        }
941        Ok((url, body))
942    }
943    /// Start a caller-driven response body. Handshake includes queueing and OAuth;
944    /// idle/session limits apply while reading. Only explicit runtime replay may
945    /// retry a failed handshake; successful headers permanently end retry eligibility.
946    pub async fn send_stream(
947        &self,
948        request: RawRequest,
949        options: Option<&RequestOptions>,
950    ) -> Result<crate::streaming::ResponseStream> {
951        let lifecycle =
952            crate::observability::Lifecycle::start(crate::observability::Operation::HttpStream);
953        let result = self
954            .send_stream_observed(request, options, &lifecycle)
955            .await;
956        lifecycle.finish(&result);
957        result
958    }
959    async fn send_stream_observed(
960        &self,
961        request: RawRequest,
962        options: Option<&RequestOptions>,
963        lifecycle: &crate::observability::Lifecycle,
964    ) -> Result<crate::streaming::ResponseStream> {
965        let (url, body) = self.preflight(&request)?;
966        let explicit_deadline = options.is_some();
967        let options = match options {
968            Some(v) => v.clone(),
969            None => RequestOptions::with_timeout(self.inner.limits.stream_handshake_timeout)?,
970        };
971        let name = request.name;
972        let sent = AtomicBool::new(false);
973        let received = std::sync::Mutex::new(None::<(u16, Option<String>)>);
974        options
975            .run(&self.inner.session, async {
976                let permit = Arc::clone(&self.inner.permits)
977                    .acquire_owned()
978                    .await
979                    .map_err(|_| Error::new(ErrorKind::Cancelled))?;
980                let max_retries = request.runtime_retries.unwrap_or(0);
981                let mut retries = 0;
982                loop {
983                    if let Ok(mut slot) = received.lock() {
984                        *slot = None;
985                    }
986                    let mut headers = request.headers.clone();
987                    let generation = self
988                        .inner
989                        .auth
990                        .prepare(&mut headers, &body, request.body.is_some(), &options)
991                        .await?;
992                    if headers_exceed(&headers) {
993                        return Err(Error::new(ErrorKind::Limit));
994                    }
995                    options.run(&self.inner.session, async { Ok(()) }).await?;
996                    lifecycle.dispatched();
997                    sent.store(true, Ordering::Relaxed);
998                    let mut response = match self
999                        .inner
1000                        .transport
1001                        .send(TransportRequest::new(
1002                            request.method.clone(),
1003                            url.clone(),
1004                            headers,
1005                            body.clone(),
1006                            name,
1007                        ))
1008                        .await
1009                    {
1010                        Ok(response) => response,
1011                        Err(error) => {
1012                            if error.kind() == ErrorKind::Transport && retries < max_retries {
1013                                let delay = self.backoff(retries);
1014                                retries += 1;
1015                                tokio::time::sleep(delay).await;
1016                                continue;
1017                            }
1018                            return Err(error.with_failure_kind(crate::FailureKind::Network));
1019                        }
1020                    };
1021                    let status = response.status;
1022                    let request_id = error_metadata(&response.headers, None).0;
1023                    lifecycle.response(status.as_u16(), request_id.is_some());
1024                    if let Ok(mut slot) = received.lock() {
1025                        *slot = Some((status.as_u16(), request_id));
1026                    }
1027                    if status.is_redirection() {
1028                        return Err(Error::new(ErrorKind::Destination)
1029                            .with_status(status.as_u16())
1030                            .with_outcome(RemoteOutcome::Rejected));
1031                    }
1032                    if !status.is_success() {
1033                        let bytes =
1034                            read_limited(&mut response.body, self.inner.limits.max_error_bytes)
1035                                .await
1036                                .map_err(body_failure)?;
1037                        if auth_rejection(status, &response.headers)
1038                            && let Some(generation) = generation
1039                        {
1040                            self.inner.auth.on_unauthorized(generation);
1041                        }
1042                        let delay = retry_after(&response.headers);
1043                        if runtime_retryable(status) && retries < max_retries {
1044                            let wait = delay.unwrap_or_else(|| self.backoff(retries));
1045                            if wait <= self.inner.limits.max_retry_delay {
1046                                retries += 1;
1047                                tokio::time::sleep(wait).await;
1048                                continue;
1049                            }
1050                        }
1051                        let (id, code) = error_metadata(&response.headers, Some(&bytes));
1052                        return Err(Error::new(ErrorKind::Http)
1053                            .with_status(status.as_u16())
1054                            .with_metadata(id, code, delay)
1055                            .with_http_body(&bytes)
1056                            .with_outcome(if status.is_client_error() {
1057                                RemoteOutcome::Rejected
1058                            } else {
1059                                RemoteOutcome::Unknown
1060                            }));
1061                    }
1062                    // Returning successful headers ends the retry scope permanently.
1063                    return Ok(crate::streaming::ResponseStream::new(
1064                        response,
1065                        permit,
1066                        self.inner.session.clone(),
1067                        &options,
1068                        explicit_deadline,
1069                        &self.inner.limits,
1070                        name,
1071                    ));
1072                }
1073            })
1074            .await
1075            .map_err(|error| {
1076                let outcome = if sent.load(Ordering::Relaxed)
1077                    && error.remote_outcome() == RemoteOutcome::NotSent
1078                {
1079                    RemoteOutcome::Unknown
1080                } else {
1081                    error.remote_outcome()
1082                };
1083                let error = if let Some((status, request_id)) =
1084                    received.lock().ok().and_then(|s| s.clone())
1085                {
1086                    error.with_response_metadata(status, request_id)
1087                } else {
1088                    error
1089                };
1090                error.with_operation(name).with_outcome(outcome)
1091            })
1092    }
1093
1094    pub async fn send_bytes(
1095        &self,
1096        request: RawRequest,
1097        options: Option<&RequestOptions>,
1098    ) -> Result<Bytes> {
1099        self.send_buffered(request, options)
1100            .await
1101            .map(|response| response.body)
1102    }
1103    async fn send_buffered(
1104        &self,
1105        request: RawRequest,
1106        options: Option<&RequestOptions>,
1107    ) -> Result<BufferedResponse> {
1108        let lifecycle =
1109            crate::observability::Lifecycle::start(crate::observability::Operation::Http);
1110        let result = self
1111            .send_buffered_observed(request, options, &lifecycle)
1112            .await;
1113        lifecycle.finish(&result);
1114        result
1115    }
1116    async fn send_buffered_observed(
1117        &self,
1118        request: RawRequest,
1119        options: Option<&RequestOptions>,
1120        lifecycle: &crate::observability::Lifecycle,
1121    ) -> Result<BufferedResponse> {
1122        let default_options;
1123        let options = match options {
1124            Some(v) => v,
1125            None => {
1126                default_options =
1127                    RequestOptions::with_timeout(self.inner.limits.operation_timeout)?;
1128                &default_options
1129            }
1130        };
1131        let name = request.name;
1132        let (url, body) = self.preflight(&request)?;
1133        let sent = AtomicBool::new(false);
1134        let received = std::sync::Mutex::new(None::<(u16, Option<String>)>);
1135        let max_retries = request.runtime_retries.unwrap_or_else(|| {
1136            options
1137                .retry_limit()
1138                .unwrap_or(self.inner.limits.max_retries)
1139        });
1140        let task = async {
1141            let _permit = self
1142                .inner
1143                .permits
1144                .acquire()
1145                .await
1146                .map_err(|_| Error::new(ErrorKind::Cancelled))?;
1147            let mut retries = 0;
1148            let mut refreshed = false;
1149            loop {
1150                if let Ok(mut slot) = received.lock() {
1151                    *slot = None;
1152                }
1153                let mut headers = request.headers.clone();
1154                let generation = self
1155                    .inner
1156                    .auth
1157                    .prepare(&mut headers, &body, request.body.is_some(), options)
1158                    .await?;
1159                if headers_exceed(&headers) {
1160                    return Err(Error::new(ErrorKind::Limit));
1161                }
1162                options.run(&self.inner.session, async { Ok(()) }).await?;
1163                lifecycle.dispatched();
1164                sent.store(true, Ordering::Relaxed);
1165                let prepared = TransportRequest::new(
1166                    request.method.clone(),
1167                    url.clone(),
1168                    headers,
1169                    body.clone(),
1170                    name,
1171                );
1172                let mut response = match self.inner.transport.send(prepared).await {
1173                    Ok(value) => value,
1174                    Err(error) => {
1175                        if error.kind() == ErrorKind::Transport
1176                            && (request.replay_safe || request.runtime_retries.is_some())
1177                            && retries < max_retries
1178                        {
1179                            let delay = self.backoff(retries);
1180                            retries += 1;
1181                            tokio::time::sleep(delay).await;
1182                            continue;
1183                        }
1184                        return Err(error.with_failure_kind(crate::FailureKind::Network));
1185                    }
1186                };
1187                let status = response.status;
1188                let request_id = error_metadata(&response.headers, None).0;
1189                lifecycle.response(status.as_u16(), request_id.is_some());
1190                if let Ok(mut slot) = received.lock() {
1191                    *slot = Some((status.as_u16(), request_id.clone()));
1192                }
1193                if status.is_redirection() {
1194                    return Err(Error::new(ErrorKind::Destination)
1195                        .with_status(status.as_u16())
1196                        .with_outcome(RemoteOutcome::Rejected));
1197                }
1198                let delay = retry_after(&response.headers);
1199                if status.is_success() {
1200                    let body =
1201                        read_limited(&mut response.body, self.inner.limits.max_response_bytes)
1202                            .await
1203                            .map_err(body_failure)?;
1204                    return Ok(BufferedResponse {
1205                        body,
1206                        status: status.as_u16(),
1207                        request_id,
1208                    });
1209                }
1210                // Read only a bounded amount; never retain or format arbitrary upstream bodies.
1211                let error_body =
1212                    read_limited(&mut response.body, self.inner.limits.max_error_bytes)
1213                        .await
1214                        .map_err(|e| body_failure(e).with_status(status.as_u16()))?;
1215                let (request_id, code) = error_metadata(&response.headers, Some(&error_body));
1216                if auth_rejection(status, &response.headers)
1217                    && let Some(generation) = generation
1218                {
1219                    let invalidated = self.inner.auth.on_unauthorized(generation);
1220                    if request.replay_safe && !refreshed && invalidated && retries < max_retries {
1221                        refreshed = true;
1222                        retries += 1;
1223                        continue;
1224                    }
1225                }
1226                if (request.replay_safe && retryable(status)
1227                    || request.runtime_retries.is_some() && runtime_retryable(status))
1228                    && retries < max_retries
1229                {
1230                    let wait = delay.unwrap_or_else(|| self.backoff(retries));
1231                    if wait <= self.inner.limits.max_retry_delay {
1232                        retries += 1;
1233                        tokio::time::sleep(wait).await;
1234                        continue;
1235                    }
1236                }
1237                let outcome = if status.is_client_error() {
1238                    RemoteOutcome::Rejected
1239                } else {
1240                    RemoteOutcome::Unknown
1241                };
1242                return Err(Error::new(ErrorKind::Http)
1243                    .with_status(status.as_u16())
1244                    .with_metadata(request_id, code, delay)
1245                    .with_http_body(&error_body)
1246                    .with_outcome(outcome));
1247            }
1248        };
1249        options.run(&self.inner.session, task).await.map_err(|e| {
1250            let outcome =
1251                if sent.load(Ordering::Relaxed) && e.remote_outcome() == RemoteOutcome::NotSent {
1252                    RemoteOutcome::Unknown
1253                } else {
1254                    e.remote_outcome()
1255                };
1256            let e = if let Some((status, request_id)) =
1257                received.lock().ok().and_then(|slot| slot.clone())
1258            {
1259                e.with_response_metadata(status, request_id)
1260            } else {
1261                e
1262            };
1263            e.with_operation(name).with_outcome(outcome)
1264        })
1265    }
1266
1267    /// Read a bounded text response using UTF-8 replacement decoding, as Fetch does.
1268    /// A leading UTF-8 BOM is omitted; content-type does not select another encoding.
1269    pub async fn send_text(
1270        &self,
1271        request: RawRequest,
1272        options: Option<&RequestOptions>,
1273    ) -> Result<String> {
1274        let name = request.name;
1275        let options = self.operation_options(options)?;
1276        let response = self.send_buffered(request, Some(&options)).await?;
1277        options
1278            .run(&self.inner.session, async {
1279                let body = response
1280                    .body
1281                    .strip_prefix(&[0xef, 0xbb, 0xbf])
1282                    .unwrap_or(&response.body);
1283                Ok(String::from_utf8_lossy(body).into_owned())
1284            })
1285            .await
1286            .map_err(|e| response.annotate(e).with_operation(name))
1287    }
1288    pub async fn send_json<R: DeserializeOwned>(
1289        &self,
1290        request: RawRequest,
1291        options: Option<&RequestOptions>,
1292    ) -> Result<R> {
1293        let name = request.name;
1294        let options = self.operation_options(options)?;
1295        let response = self.send_buffered(request, Some(&options)).await?;
1296        options
1297            .run(&self.inner.session, async {
1298                crate::json_decode::from_slice(if response.body.is_empty() {
1299                    b"{}"
1300                } else {
1301                    &response.body
1302                })
1303                .and_then(crate::json_decode::from_value)
1304                .map_err(|_| Error::new(ErrorKind::Decode))
1305            })
1306            .await
1307            .map_err(|e| response.annotate(e).with_operation(name))
1308    }
1309    pub async fn send_optional_json<R: DeserializeOwned>(
1310        &self,
1311        request: RawRequest,
1312        options: Option<&RequestOptions>,
1313    ) -> Result<Option<R>> {
1314        let name = request.name;
1315        let options = self.operation_options(options)?;
1316        let response = self.send_buffered(request, Some(&options)).await?;
1317        if response.body.is_empty() {
1318            return Ok(None);
1319        }
1320        options
1321            .run(&self.inner.session, async {
1322                crate::json_decode::from_slice(&response.body)
1323                    .and_then(crate::json_decode::from_value)
1324                    .map(Some)
1325                    .map_err(|_| Error::new(ErrorKind::Decode))
1326            })
1327            .await
1328            .map_err(|e| response.annotate(e).with_operation(name))
1329    }
1330    pub async fn send_empty(
1331        &self,
1332        request: RawRequest,
1333        options: Option<&RequestOptions>,
1334    ) -> Result<()> {
1335        self.send_bytes(request, options).await.map(|_| ())
1336    }
1337    fn backoff(&self, retry: u32) -> Duration {
1338        self.inner
1339            .limits
1340            .retry_base_delay
1341            .saturating_mul(1u32 << retry)
1342            .min(self.inner.limits.max_retry_delay)
1343    }
1344}
1345fn auth_rejection(status: StatusCode, headers: &HeaderMap) -> bool {
1346    status == StatusCode::UNAUTHORIZED
1347        || (status == StatusCode::FORBIDDEN
1348            && headers.get("x-opa-decision").is_none_or(|v| v != "false"))
1349}
1350#[cfg(any(
1351    feature = "scan",
1352    feature = "management",
1353    feature = "gateway",
1354    feature = "dlp",
1355    feature = "iam",
1356    feature = "model-security",
1357    feature = "red-team",
1358    feature = "agentguard"
1359))]
1360fn normalize_contract(
1361    schema: &str,
1362    value: serde_json::Value,
1363    mode: crate::validation::Mode,
1364    limits: &crate::validation::ValidationLimits,
1365) -> Result<serde_json::Value> {
1366    if let Some(element) = schema.strip_suffix("[]") {
1367        crate::validation::parse_array_with_limits(element, value, mode, limits)
1368    } else {
1369        crate::validation::parse_with_limits(schema, value, mode, limits)
1370    }
1371}
1372fn retryable(status: StatusCode) -> bool {
1373    matches!(status.as_u16(), 429 | 502 | 503 | 504)
1374}
1375fn retry_after(headers: &HeaderMap) -> Option<Duration> {
1376    let value = headers
1377        .get(http::header::RETRY_AFTER)?
1378        .to_str()
1379        .ok()?
1380        .trim();
1381    if !value.is_empty()
1382        && value.bytes().all(|byte| byte.is_ascii_digit())
1383        && let Ok(seconds) = value.parse::<u64>()
1384    {
1385        return Some(Duration::from_secs(seconds));
1386    }
1387    Some(
1388        httpdate::parse_http_date(value)
1389            .ok()?
1390            .duration_since(SystemTime::now())
1391            .unwrap_or(Duration::ZERO),
1392    )
1393}
1394pub(crate) fn body_failure(error: Error) -> Error {
1395    let kind = if matches!(error.kind(), ErrorKind::Transport | ErrorKind::Io) {
1396        crate::FailureKind::Network
1397    } else {
1398        crate::FailureKind::Http
1399    };
1400    error.with_failure_kind(kind)
1401}
1402pub(crate) fn error_metadata(
1403    headers: &HeaderMap,
1404    body: Option<&[u8]>,
1405) -> (Option<String>, Option<String>) {
1406    let request_id = ["x-request-id", "x-correlation-id", "request-id"]
1407        .iter()
1408        .find_map(|k| {
1409            headers
1410                .get(*k)
1411                .and_then(|v| v.to_str().ok())
1412                .filter(|v| valid_metadata(v))
1413                .map(str::to_owned)
1414        });
1415    let code = body
1416        .and_then(|v| crate::json_decode::from_slice(v).ok())
1417        .and_then(|v| {
1418            v.get("code")
1419                .or_else(|| v.get("error").and_then(|v| v.get("code")))
1420                .or_else(|| v.get("data").and_then(|v| v.get("errorCode")))
1421                .or_else(|| v.get("errorCode"))
1422                .and_then(|v| v.as_str())
1423                .filter(|v| valid_metadata(v))
1424                .map(str::to_owned)
1425        });
1426    (request_id, code)
1427}
1428fn valid_metadata(value: &str) -> bool {
1429    !value.is_empty()
1430        && value.len() <= 128
1431        && value
1432            .bytes()
1433            .all(|v| v.is_ascii_alphanumeric() || matches!(v, b'-' | b'_' | b'.' | b':'))
1434}
1435
1436pub(crate) fn serialize_bounded<T: Serialize>(value: &T, limit: usize) -> Result<Bytes> {
1437    // Stop recursive model/extension serialization before the native validator
1438    // receives a JSON value. The hard ceiling matches the validator's maximum.
1439    struct DepthFormatter<'a> {
1440        depth: usize,
1441        exceeded: &'a std::cell::Cell<bool>,
1442    }
1443    impl serde_json::ser::Formatter for DepthFormatter<'_> {
1444        fn begin_array<W: Write + ?Sized>(&mut self, writer: &mut W) -> std::io::Result<()> {
1445            self.depth += 1;
1446            if self.depth > 128 {
1447                self.exceeded.set(true);
1448                return Err(std::io::Error::other("JSON depth limit"));
1449            }
1450            writer.write_all(b"[")
1451        }
1452        fn end_array<W: Write + ?Sized>(&mut self, writer: &mut W) -> std::io::Result<()> {
1453            self.depth -= 1;
1454            writer.write_all(b"]")
1455        }
1456        fn begin_object<W: Write + ?Sized>(&mut self, writer: &mut W) -> std::io::Result<()> {
1457            self.depth += 1;
1458            if self.depth > 128 {
1459                self.exceeded.set(true);
1460                return Err(std::io::Error::other("JSON depth limit"));
1461            }
1462            writer.write_all(b"{")
1463        }
1464        fn end_object<W: Write + ?Sized>(&mut self, writer: &mut W) -> std::io::Result<()> {
1465            self.depth -= 1;
1466            writer.write_all(b"}")
1467        }
1468    }
1469    struct Writer {
1470        value: Vec<u8>,
1471        limit: usize,
1472        exceeded: bool,
1473    }
1474    impl Write for Writer {
1475        fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
1476            if self
1477                .value
1478                .len()
1479                .checked_add(bytes.len())
1480                .is_none_or(|n| n > self.limit)
1481            {
1482                self.exceeded = true;
1483                return Err(std::io::Error::other("request byte limit"));
1484            }
1485            self.value.extend_from_slice(bytes);
1486            Ok(bytes.len())
1487        }
1488        fn flush(&mut self) -> std::io::Result<()> {
1489            Ok(())
1490        }
1491    }
1492    let mut writer = Writer {
1493        value: Vec::new(),
1494        limit,
1495        exceeded: false,
1496    };
1497    let depth_exceeded = std::cell::Cell::new(false);
1498    let failed = {
1499        let mut serializer = serde_json::Serializer::with_formatter(
1500            &mut writer,
1501            DepthFormatter {
1502                depth: 0,
1503                exceeded: &depth_exceeded,
1504            },
1505        );
1506        value.serialize(&mut serializer).is_err()
1507    };
1508    if failed {
1509        return Err(Error::new(if writer.exceeded || depth_exceeded.get() {
1510            ErrorKind::Limit
1511        } else {
1512            ErrorKind::Validation
1513        }));
1514    }
1515    Ok(Bytes::from(writer.value))
1516}
1517
1518#[cfg(test)]
1519mod tests;