1use std::sync::Arc;
15
16use arrow_array::RecordBatch;
17use arrow_schema::{Schema, SchemaRef};
18use axum::{
19 body::Bytes,
20 extract::{Path, State},
21 http::{header, HeaderMap, HeaderValue, StatusCode},
22 response::{IntoResponse, Response},
23 routing::post,
24 Router,
25};
26use base64::Engine;
27use rand::RngCore;
28
29use crate::errors::{Result, RpcError};
30use std::collections::HashMap;
31
32use crate::metadata::{CALL_STATE_KEY, CANCEL_KEY, REQUEST_ID_KEY, STATE_KEY};
33use crate::server::{
34 build_error_metadata, build_log_metadata, cast_batch, validate_parameter_batch,
35 validate_protocol_version, CallContext, MethodType, Request, RpcServer,
36};
37use crate::stream::{empty_schema, Emitted, OutputCollector, StreamResult, StreamStateKind};
38use crate::unauthorized::{AuthReason, AUTH_PROXY_REQUIRED_HEADER, AUTH_REASON_HEADER};
39use crate::wire::{bytes_to_hex, empty_batch, md_get, Metadata, StreamReader, StreamWriter};
40use axum::http::HeaderName;
41
42pub const ARROW_CONTENT_TYPE: &str = "application/vnd.apache.arrow.stream";
43
44const LANDING_HTML: &str = include_str!("landing.html");
50
51const CLIENT_BUNDLE: &str = include_str!("vgi-client.js");
61
62#[derive(Clone, Debug, Default)]
70pub struct LandingInfo {
71 pub name: String,
73 pub doc: String,
75 pub version: String,
77}
78
79const SESSION_HEADER: &str = "vgi-session";
83const SESSION_ACCEPT_HEADER: &str = "vgi-session-accept";
84const SESSION_CLOSE_HEADER: &str = "vgi-session-close";
85const ECHO_HEADER_PREFIX: &str = "vgi-echo-";
86const STICKY_ENABLED_HEADER: &str = "vgi-sticky-enabled";
87const STICKY_DEFAULT_TTL_HEADER: &str = "vgi-sticky-default-ttl";
88const STICKY_ECHO_HEADERS_HEADER: &str = "vgi-sticky-echo-headers";
89const SESSION_ENDPOINT: &str = "__session__";
91
92const MIN_ZSTD_COMPRESS_BYTES: usize = 1024;
100
101pub const DEFAULT_RESPONSE_COMPRESSION_LEVEL: i32 = 1;
110
111const VGI_ACCEPT_ENCODING_HEADER: &str = "x-vgi-accept-encoding";
117const VGI_CONTENT_ENCODING_HEADER: &str = "x-vgi-content-encoding";
123const RPC_ERROR_HEADER: &str = "x-vgi-rpc-error";
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134enum ResponseEncoding {
135 Identity,
137 Zstd,
138 Gzip,
139}
140
141const RESPONSE_ENCODING_PREFERENCE: [ResponseEncoding; 2] =
146 [ResponseEncoding::Zstd, ResponseEncoding::Gzip];
147
148impl ResponseEncoding {
149 fn as_str(self) -> &'static str {
151 match self {
152 ResponseEncoding::Identity => "identity",
153 ResponseEncoding::Zstd => "zstd",
154 ResponseEncoding::Gzip => "gzip",
155 }
156 }
157
158 fn from_token(token: &str) -> Option<Self> {
160 match token {
161 "identity" => Some(ResponseEncoding::Identity),
162 "zstd" => Some(ResponseEncoding::Zstd),
163 "gzip" => Some(ResponseEncoding::Gzip),
164 _ => None,
165 }
166 }
167
168 fn producible(self, compression_level: Option<i32>) -> bool {
176 match self {
177 ResponseEncoding::Identity => true,
179 ResponseEncoding::Zstd | ResponseEncoding::Gzip => compression_level.is_some(),
184 }
185 }
186
187 fn decodable(self) -> bool {
190 match self {
191 ResponseEncoding::Identity => true,
192 ResponseEncoding::Zstd | ResponseEncoding::Gzip => true,
193 }
194 }
195}
196
197fn supported_encodings_header_value(compression_level: Option<i32>) -> String {
213 RESPONSE_ENCODING_PREFERENCE
214 .iter()
215 .filter(|e| {
216 **e != ResponseEncoding::Identity && e.producible(compression_level) && e.decodable()
217 })
218 .map(|e| e.as_str())
219 .collect::<Vec<_>>()
220 .join(", ")
221}
222
223fn parse_encoding_list(header_value: Option<&str>) -> Vec<ResponseEncoding> {
232 let mut out: Vec<ResponseEncoding> = Vec::new();
233 let Some(value) = header_value else {
234 return out;
235 };
236 for raw in value.split(',') {
237 let token = raw
238 .split(';')
239 .next()
240 .unwrap_or("")
241 .trim()
242 .to_ascii_lowercase();
243 if token.is_empty() {
244 continue;
245 }
246 if let Some(enc) = ResponseEncoding::from_token(&token) {
247 if !out.contains(&enc) {
248 out.push(enc);
249 }
250 }
251 }
252 out
253}
254
255fn pick_response_encoding(
278 headers: &HeaderMap,
279 compression_level: Option<i32>,
280) -> Option<(ResponseEncoding, bool)> {
281 let header_str = |name: &str| {
282 headers
283 .get(name)
284 .and_then(|v: &HeaderValue| v.to_str().ok())
285 };
286 let custom = parse_encoding_list(header_str(VGI_ACCEPT_ENCODING_HEADER));
287 let standard = parse_encoding_list(
288 headers
289 .get(header::ACCEPT_ENCODING)
290 .and_then(|v| v.to_str().ok()),
291 );
292 custom
293 .iter()
294 .copied()
295 .chain(standard.iter().copied().filter(|e| !custom.contains(e)))
296 .find(|enc| enc.producible(compression_level))
297 .map(|enc| {
298 let used_custom = custom.contains(&enc) && !standard.contains(&enc);
299 (enc, used_custom)
300 })
301}
302
303fn encode_response_body(encoding: ResponseEncoding, body: &[u8], level: i32) -> Option<Vec<u8>> {
308 match encoding {
309 ResponseEncoding::Identity => None,
310 ResponseEncoding::Zstd => zstd::encode_all(std::io::Cursor::new(body), level).ok(),
311 ResponseEncoding::Gzip => {
312 let level = u32::try_from(level.clamp(0, 9)).ok()?;
313 let mut encoder =
314 flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::new(level));
315 std::io::Write::write_all(&mut encoder, body).ok()?;
316 encoder.finish().ok()
317 }
318 }
319}
320
321pub struct HttpState {
331 server: Arc<RpcServer>,
332 token_key: [u8; 32],
333 extra_proxy_auth_headers: Vec<String>,
337 proxy_proof_required: bool,
340 call_state_cache_entries: usize,
342 call_states: std::sync::Mutex<HashMap<String, (u64, Arc<ResolvedCall>)>>,
347 token_ttl: std::time::Duration,
348 max_body_size: usize,
349 request_timeout: std::time::Duration,
353 authenticate: Option<crate::auth::Authenticate>,
354 #[allow(dead_code)]
355 oauth_metadata: Option<Arc<crate::auth::oauth::OAuthResourceMetadata>>,
356 oauth_metadata_json: Option<Vec<u8>>,
357 www_authenticate: Option<String>,
358 cors_max_age: u32,
359 prefix: String,
360 response_compression_level: Option<i32>,
361 landing_page_enabled: bool,
362 describe_page_enabled: bool,
363 health_enabled: bool,
364 max_request_bytes: Option<usize>,
365 max_response_bytes: Option<usize>,
370 max_externalized_response_bytes: Option<usize>,
382 upload_url_provider: Option<Arc<dyn crate::external::UploadUrlProvider>>,
383 sticky: Option<Arc<crate::sticky::StickyContext>>,
385 introspect: Option<Arc<crate::auth::introspect::TokenIntrospector>>,
389 landing_info: Option<LandingInfo>,
393 capability_headers: HeaderMap,
397 capability_has_any: bool,
400 cors_expose_headers: HeaderValue,
405 cors_allow_origin: Option<HeaderValue>,
408}
409
410#[derive(Default)]
412pub struct HttpStateBuilder {
413 server: Option<Arc<RpcServer>>,
414 token_key: Option<[u8; 32]>,
415 token_ttl: Option<std::time::Duration>,
416 max_body_size: Option<usize>,
417 request_timeout: Option<std::time::Duration>,
418 authenticate: Option<crate::auth::Authenticate>,
419 oauth_metadata: Option<Arc<crate::auth::oauth::OAuthResourceMetadata>>,
420 cors_origins: Option<String>,
421 cors_max_age: Option<u32>,
422 prefix: Option<String>,
423 response_compression_level: Option<Option<i32>>,
428 landing_page_enabled: Option<bool>,
429 describe_page_enabled: Option<bool>,
430 health_enabled: Option<bool>,
431 max_request_bytes: Option<usize>,
432 max_upload_bytes: Option<usize>,
433 max_response_bytes: Option<usize>,
434 max_externalized_response_bytes: Option<usize>,
435 upload_url_provider: Option<Arc<dyn crate::external::UploadUrlProvider>>,
436 enable_sticky: Option<bool>,
437 sticky_default_ttl: Option<std::time::Duration>,
438 sticky_echo_headers: Vec<(String, String)>,
439 landing_info: Option<LandingInfo>,
440 proxy_proof_required: Option<bool>,
441 extra_proxy_auth_headers: Vec<String>,
442 call_state_cache_entries: Option<usize>,
443 introspect_resolver: Option<crate::auth::introspect::TokenResolver>,
444 introspect_principals: Vec<String>,
445 introspect_default_ttl_seconds: Option<u64>,
446 introspect_rate_limit: Option<u32>,
447}
448
449impl HttpStateBuilder {
450 pub fn server(mut self, server: Arc<RpcServer>) -> Self {
451 self.server = Some(server);
452 self
453 }
454
455 pub fn token_key(mut self, key: &[u8]) -> Self {
461 assert!(
462 key.len() >= 32,
463 "token_key: signing key must be at least 32 bytes (got {}). \
464 Generate one with `openssl rand -hex 32`.",
465 key.len()
466 );
467 let mut k = [0u8; 32];
468 k.copy_from_slice(&key[..32]);
469 self.token_key = Some(k);
470 self
471 }
472
473 pub fn token_key_hex(self, hex: &str) -> Self {
477 let bytes = decode_hex_key(hex).expect("token_key_hex: invalid hex or wrong length");
478 self.token_key(&bytes)
479 }
480
481 pub fn token_key_base64(self, b64: &str) -> Self {
484 let bytes =
485 decode_base64_key(b64).expect("token_key_base64: invalid base64 or wrong length");
486 self.token_key(&bytes)
487 }
488
489 pub fn token_key_from_env(self, var: &str) -> Self {
494 let raw = std::env::var(var)
495 .unwrap_or_else(|_| panic!("token_key_from_env: env var {var} is unset or not UTF-8"));
496 let trimmed = raw.trim();
497 let bytes = decode_base64_key(trimmed)
498 .or_else(|_| decode_hex_key(trimmed))
499 .unwrap_or_else(|e| {
500 panic!("token_key_from_env: {var} is not valid base64 or hex ({e})")
501 });
502 self.token_key(&bytes)
503 }
504
505 pub fn producer_batch_limit(self, n: usize) -> Self {
510 assert_eq!(
511 n, 1,
512 "producer_batch_limit is fixed at 1 by the lock-step protocol"
513 );
514 self
515 }
516
517 pub fn call_state_cache_entries(mut self, n: usize) -> Self {
528 self.call_state_cache_entries = Some(n);
529 self
530 }
531
532 pub fn token_ttl(mut self, ttl: std::time::Duration) -> Self {
533 self.token_ttl = Some(ttl);
534 self
535 }
536
537 pub fn max_body_size(mut self, n: usize) -> Self {
542 self.max_body_size = Some(n);
543 self
544 }
545
546 pub fn request_timeout(mut self, d: std::time::Duration) -> Self {
548 self.request_timeout = Some(d);
549 self
550 }
551
552 pub fn authenticate(mut self, cb: crate::auth::Authenticate) -> Self {
555 self.authenticate = Some(cb);
556 self
557 }
558
559 pub fn proxy_auth_headers<I, S>(mut self, headers: I) -> Self
571 where
572 I: IntoIterator<Item = S>,
573 S: Into<String>,
574 {
575 self.extra_proxy_auth_headers = headers.into_iter().map(Into::into).collect();
576 self
577 }
578
579 pub fn proxy_proof_required(mut self, required: bool) -> Self {
580 self.proxy_proof_required = Some(required);
581 self
582 }
583
584 pub fn oauth_resource_metadata(
588 mut self,
589 metadata: crate::auth::oauth::OAuthResourceMetadata,
590 ) -> Self {
591 self.oauth_metadata = Some(Arc::new(metadata));
592 self
593 }
594
595 pub fn cors_origins(mut self, origins: impl Into<String>) -> Self {
598 self.cors_origins = Some(origins.into());
599 self
600 }
601
602 pub fn cors_max_age(mut self, seconds: u32) -> Self {
604 self.cors_max_age = Some(seconds);
605 self
606 }
607
608 pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
610 self.prefix = Some(prefix.into());
611 self
612 }
613
614 pub fn response_compression_level(mut self, level: i32) -> Self {
623 self.response_compression_level = Some(Some(level));
624 self
625 }
626
627 pub fn disable_response_compression(mut self) -> Self {
635 self.response_compression_level = Some(None);
636 self
637 }
638
639 pub fn enable_landing_page(mut self, enabled: bool) -> Self {
641 self.landing_page_enabled = Some(enabled);
642 self
643 }
644
645 pub fn enable_describe_page(mut self, enabled: bool) -> Self {
647 self.describe_page_enabled = Some(enabled);
648 self
649 }
650
651 pub fn enable_health(mut self, enabled: bool) -> Self {
653 self.health_enabled = Some(enabled);
654 self
655 }
656
657 pub fn max_request_bytes(mut self, n: usize) -> Self {
663 self.max_request_bytes = Some(n);
664 self
665 }
666
667 pub fn max_upload_bytes(mut self, n: usize) -> Self {
671 self.max_upload_bytes = Some(n);
672 self
673 }
674
675 pub fn max_response_bytes(mut self, n: usize) -> Self {
681 self.max_response_bytes = Some(n);
682 self
683 }
684
685 pub fn max_externalized_response_bytes(mut self, n: usize) -> Self {
689 self.max_externalized_response_bytes = Some(n);
690 self
691 }
692
693 pub fn upload_url_provider(
697 mut self,
698 provider: Arc<dyn crate::external::UploadUrlProvider>,
699 ) -> Self {
700 self.upload_url_provider = Some(provider);
701 self
702 }
703
704 pub fn enable_sticky(mut self, enabled: bool) -> Self {
709 self.enable_sticky = Some(enabled);
710 self
711 }
712
713 pub fn sticky_default_ttl(mut self, ttl: std::time::Duration) -> Self {
716 self.sticky_default_ttl = Some(ttl);
717 self
718 }
719
720 pub fn sticky_echo_headers(
726 mut self,
727 headers: impl IntoIterator<Item = (String, String)>,
728 ) -> Self {
729 self.sticky_echo_headers = headers.into_iter().collect();
730 self
731 }
732
733 pub fn landing_info(mut self, info: LandingInfo) -> Self {
738 self.landing_info = Some(info);
739 self
740 }
741
742 pub fn introspect_resolver(mut self, resolver: crate::auth::introspect::TokenResolver) -> Self {
756 self.introspect_resolver = Some(resolver);
757 self
758 }
759
760 pub fn introspect_principals<I, S>(mut self, principals: I) -> Self
768 where
769 I: IntoIterator<Item = S>,
770 S: Into<String>,
771 {
772 self.introspect_principals = principals.into_iter().map(Into::into).collect();
773 self
774 }
775
776 pub fn introspect_default_ttl(mut self, ttl: std::time::Duration) -> Self {
781 self.introspect_default_ttl_seconds = Some(ttl.as_secs());
782 self
783 }
784
785 pub fn introspect_rate_limit(mut self, per_second: u32) -> Self {
789 self.introspect_rate_limit = Some(per_second);
790 self
791 }
792
793 pub fn build(self) -> Arc<HttpState> {
794 let server = self.server.expect("HttpStateBuilder::server is required");
795 assert!(
802 !(self.cors_origins.as_deref() == Some("*") && self.authenticate.is_some()),
803 "HttpStateBuilder: cors_origins(\"*\") cannot be combined with an \
804 authenticate callback — browsers reject credentialed requests \
805 against a wildcard origin. Configure a specific origin."
806 );
807 let token_key = self.token_key.unwrap_or_else(|| {
808 tracing::warn!(
809 target: "vgi_rpc.http",
810 "no token_key configured; using ephemeral per-process AEAD key — \
811 state tokens will not survive restart or load-balance across workers"
812 );
813 let mut k = [0u8; 32];
814 rand::thread_rng().fill_bytes(&mut k);
815 k
816 });
817 let oauth_metadata_json = self
818 .oauth_metadata
819 .as_ref()
820 .map(|m| m.to_json().into_bytes());
821 let www_authenticate = self.oauth_metadata.as_ref().map(|m| m.www_authenticate());
822 let sticky = if self.enable_sticky.unwrap_or(false) {
823 let ttl = self
824 .sticky_default_ttl
825 .unwrap_or_else(|| std::time::Duration::from_secs(300));
826 Some(crate::sticky::StickyContext::new(
827 token_key,
828 ttl,
829 self.sticky_echo_headers,
830 server.server_id.clone(),
831 ))
832 } else {
833 None
834 };
835 let introspect = match self.introspect_resolver {
839 Some(resolver) => Some(Arc::new(crate::auth::introspect::TokenIntrospector::new(
840 resolver,
841 self.introspect_principals,
842 self.introspect_default_ttl_seconds
843 .unwrap_or(crate::auth::introspect::DEFAULT_INTROSPECT_TTL_SECONDS),
844 self.introspect_rate_limit
845 .unwrap_or(crate::auth::introspect::DEFAULT_INTROSPECT_RATE_LIMIT),
846 ))),
847 None => {
848 assert!(
849 self.introspect_principals.is_empty(),
850 "introspect_principals was given without introspect_resolver; the \
851 route stays disabled, so the allowlist would have no effect. Pass \
852 both or neither."
853 );
854 None
855 }
856 };
857 let cors_allow_origin = self
858 .cors_origins
859 .as_deref()
860 .and_then(|o| HeaderValue::from_str(o).ok());
861 let response_compression_level = self
864 .response_compression_level
865 .unwrap_or(Some(DEFAULT_RESPONSE_COMPRESSION_LEVEL));
866 let (capability_headers, capability_has_any, cors_expose_headers) =
867 build_capability_headers(CapabilityInputs {
868 max_request_bytes: self.max_request_bytes,
869 max_response_bytes: self.max_response_bytes,
870 max_externalized_response_bytes: self.max_externalized_response_bytes,
871 externalization_enabled: server.external_config().is_some(),
872 upload_url_support: self.upload_url_provider.is_some(),
873 max_upload_bytes: self.max_upload_bytes,
874 sticky: sticky.as_deref(),
875 response_compression_level,
876 proxy_proof_required: self.proxy_proof_required.unwrap_or(false),
877 introspect_enabled: introspect.is_some(),
878 });
879 Arc::new(HttpState {
880 extra_proxy_auth_headers: self.extra_proxy_auth_headers.clone(),
881 proxy_proof_required: self.proxy_proof_required.unwrap_or(false),
882 call_state_cache_entries: self
883 .call_state_cache_entries
884 .unwrap_or(CALL_STATE_CACHE_ENTRIES),
885 call_states: std::sync::Mutex::new(HashMap::new()),
886 server,
887 token_key,
888 token_ttl: self
889 .token_ttl
890 .unwrap_or_else(|| std::time::Duration::from_secs(300)),
891 max_body_size: self.max_body_size.unwrap_or(64 * 1024 * 1024),
892 request_timeout: self
893 .request_timeout
894 .unwrap_or_else(|| std::time::Duration::from_secs(30)),
895 authenticate: self.authenticate,
896 oauth_metadata: self.oauth_metadata,
897 oauth_metadata_json,
898 www_authenticate,
899 cors_max_age: self.cors_max_age.unwrap_or(7200),
900 prefix: self.prefix.unwrap_or_default(),
901 response_compression_level,
902 landing_page_enabled: self.landing_page_enabled.unwrap_or(true),
903 describe_page_enabled: self.describe_page_enabled.unwrap_or(true),
904 health_enabled: self.health_enabled.unwrap_or(true),
905 max_request_bytes: self.max_request_bytes,
906 max_response_bytes: self.max_response_bytes,
907 max_externalized_response_bytes: self.max_externalized_response_bytes,
908 upload_url_provider: self.upload_url_provider,
909 sticky,
910 introspect,
911 landing_info: self.landing_info,
912 capability_headers,
913 capability_has_any,
914 cors_expose_headers,
915 cors_allow_origin,
916 })
917 }
918}
919
920struct CapabilityInputs<'a> {
923 max_request_bytes: Option<usize>,
924 max_response_bytes: Option<usize>,
925 max_externalized_response_bytes: Option<usize>,
926 externalization_enabled: bool,
927 upload_url_support: bool,
928 max_upload_bytes: Option<usize>,
929 sticky: Option<&'a crate::sticky::StickyContext>,
930 response_compression_level: Option<i32>,
931 proxy_proof_required: bool,
932 introspect_enabled: bool,
933}
934
935const CORS_EXPOSE_FIXED: [&str; 7] = [
947 "Content-Encoding",
948 VGI_CONTENT_ENCODING_HEADER,
949 RPC_ERROR_HEADER,
950 "WWW-Authenticate",
951 AUTH_REASON_HEADER,
952 AUTH_PROXY_REQUIRED_HEADER,
953 REQUEST_ID_RESPONSE_HEADER,
958];
959
960pub const REQUEST_ID_RESPONSE_HEADER: &str = "x-request-id";
963
964fn build_capability_headers(inputs: CapabilityInputs<'_>) -> (HeaderMap, bool, HeaderValue) {
970 let CapabilityInputs {
971 max_request_bytes,
972 max_response_bytes,
973 max_externalized_response_bytes,
974 externalization_enabled,
975 upload_url_support,
976 max_upload_bytes,
977 sticky,
978 response_compression_level,
979 proxy_proof_required,
980 introspect_enabled,
981 } = inputs;
982 let mut out = HeaderMap::new();
983 let mut any = false;
984 if let Ok(v) = HeaderValue::from_str(&supported_encodings_header_value(
990 response_compression_level,
991 )) {
992 out.insert("vgi-supported-encodings", v);
993 }
994 if let Some(n) = max_request_bytes {
995 if let Ok(v) = HeaderValue::from_str(&n.to_string()) {
996 out.insert("vgi-max-request-bytes", v);
997 any = true;
998 }
999 }
1000 if let Some(n) = max_response_bytes {
1001 if let Ok(v) = HeaderValue::from_str(&n.to_string()) {
1002 out.insert("vgi-max-response-bytes", v);
1003 any = true;
1004 }
1005 }
1006 if let Some(n) = max_externalized_response_bytes {
1007 if let Ok(v) = HeaderValue::from_str(&n.to_string()) {
1008 out.insert("vgi-max-externalized-response-bytes", v);
1009 any = true;
1010 }
1011 }
1012 out.insert(
1015 "vgi-externalization-enabled",
1016 HeaderValue::from_static(if externalization_enabled {
1017 "true"
1018 } else {
1019 "false"
1020 }),
1021 );
1022 if upload_url_support {
1023 out.insert("vgi-upload-url-support", HeaderValue::from_static("true"));
1024 any = true;
1025 if let Some(n) = max_upload_bytes {
1026 if let Ok(v) = HeaderValue::from_str(&n.to_string()) {
1027 out.insert("vgi-max-upload-bytes", v);
1028 }
1029 }
1030 }
1031 if proxy_proof_required {
1036 out.insert(
1037 crate::auth::proof::PROOF_REQUIRED_HEADER,
1038 HeaderValue::from_static("true"),
1039 );
1040 any = true;
1041 }
1042 if introspect_enabled {
1048 out.insert(
1049 crate::auth::introspect::INTROSPECT_ENABLED_HEADER,
1050 HeaderValue::from_static("true"),
1051 );
1052 any = true;
1053 }
1054 if let Some(sticky) = sticky {
1057 out.insert(STICKY_ENABLED_HEADER, HeaderValue::from_static("true"));
1058 any = true;
1059 if let Ok(v) = HeaderValue::from_str(&sticky.default_ttl.as_secs().to_string()) {
1060 out.insert(STICKY_DEFAULT_TTL_HEADER, v);
1061 }
1062 if !sticky.echo_headers.is_empty() {
1063 let names = sticky
1064 .echo_headers
1065 .iter()
1066 .map(|(n, _)| n.as_str())
1067 .collect::<Vec<_>>()
1068 .join(",");
1069 if let Ok(v) = HeaderValue::from_str(&names) {
1070 out.insert(STICKY_ECHO_HEADERS_HEADER, v);
1071 }
1072 }
1073 } else {
1074 out.insert(STICKY_ENABLED_HEADER, HeaderValue::from_static("false"));
1075 }
1076
1077 let mut expose: Vec<String> = CORS_EXPOSE_FIXED.iter().map(|s| s.to_string()).collect();
1085 let mut cap_names: Vec<String> = out.keys().map(|k| k.as_str().to_string()).collect();
1086 if let Some(sticky) = sticky {
1091 cap_names.push(SESSION_HEADER.to_string());
1092 cap_names.push(SESSION_CLOSE_HEADER.to_string());
1093 cap_names.extend(
1094 sticky
1095 .echo_headers
1096 .iter()
1097 .map(|(n, _)| format!("{ECHO_HEADER_PREFIX}{n}")),
1098 );
1099 }
1100 cap_names.sort();
1101 expose.extend(cap_names);
1102 let expose = HeaderValue::from_str(&expose.join(", "))
1103 .unwrap_or_else(|_| HeaderValue::from_static("Content-Encoding, WWW-Authenticate"));
1104 (out, any, expose)
1105}
1106
1107impl HttpState {
1108 pub fn new(server: Arc<RpcServer>) -> Arc<Self> {
1111 Self::builder().server(server).build()
1112 }
1113
1114 pub fn builder() -> HttpStateBuilder {
1115 HttpStateBuilder::default()
1116 }
1117
1118 pub fn sticky_drain_handle(&self) -> Option<crate::sticky::DrainHandle> {
1121 self.sticky.as_ref().map(|c| c.drain_handle())
1122 }
1123
1124 pub fn token_ttl(&self) -> std::time::Duration {
1125 self.token_ttl
1126 }
1127
1128 pub fn max_body_size(&self) -> usize {
1129 self.max_body_size
1130 }
1131
1132 pub(crate) fn pack_cursor_token(
1139 &self,
1140 auth: &crate::auth::AuthContext,
1141 state_bytes: &[u8],
1142 call_id: &[u8; CALL_ID_LEN],
1143 ) -> String {
1144 let aad = compute_aad(auth);
1145 pack_cursor_token(
1146 &self.token_key,
1147 &aad,
1148 state_bytes,
1149 call_id,
1150 current_unix_secs(),
1151 )
1152 }
1153
1154 pub(crate) fn pack_call_token(
1158 &self,
1159 auth: &crate::auth::AuthContext,
1160 call_id: &[u8; CALL_ID_LEN],
1161 output_schema_bytes: &[u8],
1162 input_schema_bytes: &[u8],
1163 stream_id: &str,
1164 ) -> String {
1165 let aad = compute_call_aad(auth);
1166 let token = pack_call_token(
1167 &self.token_key,
1168 &aad,
1169 call_id,
1170 output_schema_bytes,
1171 input_schema_bytes,
1172 stream_id,
1173 current_unix_secs(),
1174 );
1175 self.cache_call(
1176 auth,
1177 call_id,
1178 Arc::new(ResolvedCall {
1179 output_schema_bytes: output_schema_bytes.to_vec(),
1180 input_schema_bytes: input_schema_bytes.to_vec(),
1181 stream_id: stream_id.to_string(),
1182 }),
1183 );
1184 token
1185 }
1186
1187 fn token_ttl_opt(&self) -> Option<std::time::Duration> {
1188 if self.token_ttl.is_zero() {
1189 None
1190 } else {
1191 Some(self.token_ttl)
1192 }
1193 }
1194
1195 pub(crate) fn unpack_cursor_token(
1198 &self,
1199 auth: &crate::auth::AuthContext,
1200 token: &str,
1201 ) -> Result<UnpackedCursor> {
1202 let aad = compute_aad(auth);
1203 unpack_cursor_token(&self.token_key, &aad, token, self.token_ttl_opt())
1204 }
1205
1206 fn proxy_auth_headers(&self) -> Vec<String> {
1215 let mut out = Vec::new();
1216 if self.proxy_proof_required {
1217 out.push(crate::auth::proof::PROOF_HEADER.to_string());
1218 }
1219 out.extend(self.extra_proxy_auth_headers.iter().cloned());
1220 out
1221 }
1222
1223 pub(crate) fn unauthorized_response(
1231 &self,
1232 mut headers: HeaderMap,
1233 reason: AuthReason,
1234 detail: &str,
1235 ) -> Response {
1236 let proxy_headers = self.proxy_auth_headers();
1237 let hint = if proxy_headers.is_empty() {
1238 None
1239 } else {
1240 Some(crate::unauthorized::proxy_hint(&proxy_headers))
1241 };
1242 if hint.is_some() {
1243 headers.insert(
1244 HeaderName::from_static(AUTH_PROXY_REQUIRED_HEADER),
1245 HeaderValue::from_static("true"),
1246 );
1247 }
1248 if let Ok(hv) = HeaderValue::from_str(reason.as_str()) {
1249 headers.insert(HeaderName::from_static(AUTH_REASON_HEADER), hv);
1250 }
1251 headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
1254 headers.insert(
1255 header::CONTENT_TYPE,
1256 HeaderValue::from_static("application/json"),
1257 );
1258 let body = crate::unauthorized::envelope(reason, detail, hint.as_deref());
1259 (StatusCode::UNAUTHORIZED, headers, body).into_response()
1260 }
1261
1262 fn cache_identity(auth: &crate::auth::AuthContext) -> String {
1264 if !auth.authenticated {
1265 "\u{0}anonymous".to_string()
1266 } else {
1267 format!("{}\u{0}{}", auth.domain, auth.principal)
1268 }
1269 }
1270
1271 fn cache_key(auth: &crate::auth::AuthContext, call_id: &[u8; CALL_ID_LEN]) -> String {
1272 format!("{:02x?}\u{0}{}", call_id, Self::cache_identity(auth))
1273 }
1274
1275 fn cache_call(
1276 &self,
1277 auth: &crate::auth::AuthContext,
1278 call_id: &[u8; CALL_ID_LEN],
1279 call: Arc<ResolvedCall>,
1280 ) {
1281 if self.call_state_cache_entries == 0 {
1282 return;
1283 }
1284 let mut cache = self.call_states.lock().unwrap();
1285 let key = Self::cache_key(auth, call_id);
1286 if cache.len() >= self.call_state_cache_entries && !cache.contains_key(&key) {
1287 cache.clear();
1291 }
1292 cache.insert(key, (current_unix_secs(), call));
1293 }
1294
1295 pub(crate) fn resolve_call(
1311 &self,
1312 auth: &crate::auth::AuthContext,
1313 cursor: &UnpackedCursor,
1314 call_token: Option<&str>,
1315 ) -> Result<Arc<ResolvedCall>> {
1316 let key = Self::cache_key(auth, &cursor.call_id);
1317 let ttl = self.token_ttl_opt();
1318 {
1319 let mut cache = self.call_states.lock().unwrap();
1320 if let Some((stored_at, call)) = cache.get(&key).cloned() {
1321 let fresh = match ttl {
1322 Some(t) => current_unix_secs().saturating_sub(stored_at) <= t.as_secs(),
1323 None => true,
1324 };
1325 if fresh {
1326 return Ok(call);
1327 }
1328 cache.remove(&key);
1329 }
1330 }
1331
1332 let Some(call_token) = call_token else {
1333 return Err(RpcError::runtime_error(
1334 "Missing call token in exchange request",
1335 ));
1336 };
1337 let aad = compute_call_aad(auth);
1338 let (call_id, resolved) = unpack_call_token(&self.token_key, &aad, call_token, ttl)?;
1339 if call_id != cursor.call_id {
1340 return Err(RpcError::runtime_error("Malformed state token"));
1344 }
1345 let resolved = Arc::new(resolved);
1346 self.cache_call(auth, &cursor.call_id, resolved.clone());
1347 Ok(resolved)
1348 }
1349}
1350
1351fn compute_aad(auth: &crate::auth::AuthContext) -> Vec<u8> {
1356 compute_aad_with(b"vgi_rpc.state.v4\x00", auth)
1357}
1358
1359fn compute_call_aad(auth: &crate::auth::AuthContext) -> Vec<u8> {
1365 compute_aad_with(b"vgi_rpc.call.v1\x00", auth)
1366}
1367
1368fn compute_aad_with(prefix: &[u8], auth: &crate::auth::AuthContext) -> Vec<u8> {
1369 if !auth.authenticated {
1370 let mut out = Vec::with_capacity(prefix.len() + b"\x00anonymous".len());
1371 out.extend_from_slice(prefix);
1372 out.extend_from_slice(b"\x00anonymous");
1373 return out;
1374 }
1375 let mut out =
1376 Vec::with_capacity(prefix.len() + 1 + auth.domain.len() + 1 + auth.principal.len());
1377 out.extend_from_slice(prefix);
1378 out.push(0x01);
1379 out.extend_from_slice(auth.domain.as_bytes());
1380 out.push(0);
1381 out.extend_from_slice(auth.principal.as_bytes());
1382 out
1383}
1384
1385pub(crate) const CURSOR_TOKEN_VERSION: u8 = 0x06;
1397pub(crate) const CALL_TOKEN_VERSION: u8 = 0x01;
1398
1399pub(crate) const CALL_ID_LEN: usize = 16;
1402
1403const CALL_STATE_CACHE_ENTRIES: usize = 4096;
1405
1406const TOKEN_CODEC_RAW: u8 = 0x00;
1409const TOKEN_CODEC_ZSTD: u8 = 0x01;
1410
1411const TOKEN_ZSTD_LEVEL: i32 = 3;
1416
1417const MAX_TOKEN_PLAINTEXT_BYTES: usize = 64 << 20;
1422
1423fn pack_token_payload(plaintext: &[u8]) -> Vec<u8> {
1434 if let Ok(packed) = zstd::bulk::compress(plaintext, TOKEN_ZSTD_LEVEL) {
1435 if packed.len() < plaintext.len() {
1436 let mut out = Vec::with_capacity(1 + packed.len());
1437 out.push(TOKEN_CODEC_ZSTD);
1438 out.extend_from_slice(&packed);
1439 return out;
1440 }
1441 }
1442 let mut out = Vec::with_capacity(1 + plaintext.len());
1443 out.push(TOKEN_CODEC_RAW);
1444 out.extend_from_slice(plaintext);
1445 out
1446}
1447
1448fn unpack_token_payload(data: &[u8]) -> Result<Vec<u8>> {
1454 let (tag, body) = data
1455 .split_first()
1456 .ok_or_else(|| RpcError::runtime_error("Malformed state token"))?;
1457 match *tag {
1458 TOKEN_CODEC_RAW => Ok(body.to_vec()),
1459 TOKEN_CODEC_ZSTD => zstd::bulk::decompress(body, MAX_TOKEN_PLAINTEXT_BYTES)
1460 .map_err(|_| RpcError::runtime_error("Malformed state token")),
1461 _ => Err(RpcError::runtime_error("Malformed state token")),
1462 }
1463}
1464
1465#[derive(Debug, Clone)]
1467pub(crate) struct UnpackedCursor {
1468 pub state_bytes: Vec<u8>,
1469 pub call_id: [u8; CALL_ID_LEN],
1472 #[allow(dead_code)]
1473 pub created_at: u64,
1474}
1475
1476#[derive(Debug, Clone)]
1480pub(crate) struct ResolvedCall {
1481 pub output_schema_bytes: Vec<u8>,
1482 pub input_schema_bytes: Vec<u8>,
1483 #[allow(dead_code)]
1488 pub stream_id: String,
1489}
1490
1491fn current_unix_secs() -> u64 {
1493 std::time::SystemTime::now()
1494 .duration_since(std::time::UNIX_EPOCH)
1495 .map(|d| d.as_secs())
1496 .unwrap_or(0)
1497}
1498
1499pub(crate) fn pack_cursor_token(
1526 token_key: &[u8; 32],
1527 aad: &[u8],
1528 state_bytes: &[u8],
1529 call_id: &[u8; CALL_ID_LEN],
1530 created_at: u64,
1531) -> String {
1532 let mut plaintext = Vec::with_capacity(8 + CALL_ID_LEN + 4 + state_bytes.len());
1533 plaintext.extend_from_slice(&created_at.to_le_bytes());
1534 plaintext.extend_from_slice(call_id);
1535 plaintext.extend_from_slice(&(state_bytes.len() as u32).to_le_bytes());
1536 plaintext.extend_from_slice(state_bytes);
1537
1538 crate::crypto::seal_base64(
1539 &pack_token_payload(&plaintext),
1540 token_key,
1541 aad,
1542 CURSOR_TOKEN_VERSION,
1543 )
1544}
1545
1546pub(crate) fn pack_call_token(
1549 token_key: &[u8; 32],
1550 aad: &[u8],
1551 call_id: &[u8; CALL_ID_LEN],
1552 output_schema_bytes: &[u8],
1553 input_schema_bytes: &[u8],
1554 stream_id: &str,
1555 created_at: u64,
1556) -> String {
1557 let mut plaintext = Vec::with_capacity(
1558 8 + CALL_ID_LEN
1559 + 4
1560 + output_schema_bytes.len()
1561 + 4
1562 + input_schema_bytes.len()
1563 + 4
1564 + stream_id.len(),
1565 );
1566 plaintext.extend_from_slice(&created_at.to_le_bytes());
1567 plaintext.extend_from_slice(call_id);
1568 plaintext.extend_from_slice(&(output_schema_bytes.len() as u32).to_le_bytes());
1569 plaintext.extend_from_slice(output_schema_bytes);
1570 plaintext.extend_from_slice(&(input_schema_bytes.len() as u32).to_le_bytes());
1571 plaintext.extend_from_slice(input_schema_bytes);
1572 plaintext.extend_from_slice(&(stream_id.len() as u32).to_le_bytes());
1573 plaintext.extend_from_slice(stream_id.as_bytes());
1574
1575 crate::crypto::seal_base64(
1576 &pack_token_payload(&plaintext),
1577 token_key,
1578 aad,
1579 CALL_TOKEN_VERSION,
1580 )
1581}
1582
1583fn open_token_plaintext(
1586 token_key: &[u8; 32],
1587 aad: &[u8],
1588 token: &str,
1589 version: u8,
1590 token_ttl: Option<std::time::Duration>,
1591) -> Result<(Vec<u8>, u64)> {
1592 let raw = base64::engine::general_purpose::STANDARD
1593 .decode(token.as_bytes())
1594 .map_err(|_| RpcError::runtime_error("Malformed state token"))?;
1595
1596 let sealed = crate::crypto::open_bytes(&raw, token_key, aad, version)
1597 .map_err(|_| RpcError::runtime_error("State token signature verification failed"))?;
1598 let plaintext = unpack_token_payload(&sealed)?;
1601
1602 if plaintext.len() < 8 + CALL_ID_LEN {
1603 return Err(RpcError::runtime_error("Malformed state token"));
1604 }
1605 let created_at = u64::from_le_bytes(plaintext[0..8].try_into().unwrap());
1606
1607 if let Some(ttl) = token_ttl {
1608 let now = current_unix_secs();
1609 if now > created_at && now - created_at > ttl.as_secs() {
1610 return Err(RpcError::runtime_error("State token expired"));
1611 }
1612 const MAX_CLOCK_SKEW_SECS: u64 = 60;
1617 if created_at > now && created_at - now > MAX_CLOCK_SKEW_SECS {
1618 return Err(RpcError::runtime_error(
1619 "State token timestamp is implausibly in the future",
1620 ));
1621 }
1622 }
1623
1624 Ok((plaintext, created_at))
1625}
1626
1627pub(crate) fn unpack_cursor_token(
1635 token_key: &[u8; 32],
1636 aad: &[u8],
1637 token: &str,
1638 token_ttl: Option<std::time::Duration>,
1639) -> Result<UnpackedCursor> {
1640 let (plaintext, created_at) =
1641 open_token_plaintext(token_key, aad, token, CURSOR_TOKEN_VERSION, token_ttl)?;
1642
1643 let mut call_id = [0u8; CALL_ID_LEN];
1644 call_id.copy_from_slice(&plaintext[8..8 + CALL_ID_LEN]);
1645
1646 let mut pos = 8 + CALL_ID_LEN;
1647 let state_bytes = read_segment(&plaintext, &mut pos)?;
1648 if pos != plaintext.len() {
1649 return Err(RpcError::runtime_error("Malformed state token"));
1650 }
1651
1652 Ok(UnpackedCursor {
1653 state_bytes,
1654 call_id,
1655 created_at,
1656 })
1657}
1658
1659pub(crate) fn unpack_call_token(
1662 token_key: &[u8; 32],
1663 aad: &[u8],
1664 token: &str,
1665 token_ttl: Option<std::time::Duration>,
1666) -> Result<([u8; CALL_ID_LEN], ResolvedCall)> {
1667 let (plaintext, _created_at) =
1668 open_token_plaintext(token_key, aad, token, CALL_TOKEN_VERSION, token_ttl)?;
1669
1670 let mut call_id = [0u8; CALL_ID_LEN];
1671 call_id.copy_from_slice(&plaintext[8..8 + CALL_ID_LEN]);
1672
1673 let mut pos = 8 + CALL_ID_LEN;
1674 let output_schema_bytes = read_segment(&plaintext, &mut pos)?;
1675 let input_schema_bytes = read_segment(&plaintext, &mut pos)?;
1676 let stream_id_bytes = read_segment(&plaintext, &mut pos)?;
1677 if pos != plaintext.len() {
1678 return Err(RpcError::runtime_error("Malformed state token"));
1679 }
1680 let stream_id = String::from_utf8(stream_id_bytes)
1681 .map_err(|_| RpcError::runtime_error("Malformed state token"))?;
1682
1683 Ok((
1684 call_id,
1685 ResolvedCall {
1686 output_schema_bytes,
1687 input_schema_bytes,
1688 stream_id,
1689 },
1690 ))
1691}
1692
1693fn read_segment(buf: &[u8], pos: &mut usize) -> Result<Vec<u8>> {
1694 if *pos + 4 > buf.len() {
1695 return Err(RpcError::runtime_error("Malformed state token"));
1696 }
1697 let len = u32::from_le_bytes(buf[*pos..*pos + 4].try_into().unwrap()) as usize;
1698 *pos += 4;
1699 if *pos + len > buf.len() {
1700 return Err(RpcError::runtime_error("Malformed state token"));
1701 }
1702 let out = buf[*pos..*pos + len].to_vec();
1703 *pos += len;
1704 Ok(out)
1705}
1706
1707fn write_schema_bytes(schema: &Schema) -> Result<Vec<u8>> {
1711 let empty = empty_batch(schema)?;
1712 crate::wire::write_one_batch(&empty, None)
1713}
1714
1715fn read_schema_bytes(bytes: &[u8]) -> Result<SchemaRef> {
1717 let r = StreamReader::new(bytes)?;
1718 Ok(r.schema())
1719}
1720
1721pub async fn shutdown_signal() {
1737 #[cfg(unix)]
1738 {
1739 use tokio::signal::unix::{signal, SignalKind};
1740 let mut term = match signal(SignalKind::terminate()) {
1741 Ok(s) => s,
1742 Err(_) => {
1743 let _ = tokio::signal::ctrl_c().await;
1744 return;
1745 }
1746 };
1747 let mut intr = match signal(SignalKind::interrupt()) {
1748 Ok(s) => s,
1749 Err(_) => {
1750 let _ = tokio::signal::ctrl_c().await;
1751 return;
1752 }
1753 };
1754 tokio::select! {
1755 _ = term.recv() => {},
1756 _ = intr.recv() => {},
1757 }
1758 }
1759 #[cfg(not(unix))]
1760 {
1761 let _ = tokio::signal::ctrl_c().await;
1762 }
1763}
1764
1765pub async fn serve_with_shutdown(
1769 state: Arc<HttpState>,
1770 listener: tokio::net::TcpListener,
1771) -> std::io::Result<()> {
1772 let app = build_router(state);
1773 axum::serve(listener, app)
1774 .with_graceful_shutdown(shutdown_signal())
1775 .await
1776}
1777
1778const MAX_RESPONSE_BYTES_HARD_CAP: usize = 256 * 1024 * 1024;
1792
1793fn response_buffer_ceiling(state: &HttpState) -> usize {
1799 match state.max_response_bytes {
1800 Some(soft) => MAX_RESPONSE_BYTES_HARD_CAP.max(soft.saturating_mul(2)),
1801 None => MAX_RESPONSE_BYTES_HARD_CAP,
1802 }
1803}
1804
1805pub fn build_router(state: Arc<HttpState>) -> Router {
1806 let body_limit = state.max_body_size;
1807 let request_timeout = state.request_timeout;
1808 build_router_inner(state.clone())
1809 .layer(axum::middleware::from_fn_with_state(
1810 state,
1811 postprocess_middleware,
1812 ))
1813 .layer(axum::extract::DefaultBodyLimit::disable())
1824 .layer(tower_http::limit::RequestBodyLimitLayer::new(body_limit))
1825 .layer(tower_http::timeout::TimeoutLayer::with_status_code(
1828 StatusCode::REQUEST_TIMEOUT,
1829 request_timeout,
1830 ))
1831 .layer(tower_http::catch_panic::CatchPanicLayer::new())
1836}
1837
1838async fn postprocess_middleware(
1839 axum::extract::State(state): axum::extract::State<Arc<HttpState>>,
1840 mut req: axum::http::Request<axum::body::Body>,
1841 next: axum::middleware::Next,
1842) -> Response {
1843 use axum::body::to_bytes;
1844 state.server.notify_transport(
1848 crate::transport::TransportKind::Http,
1849 crate::transport::TransportCapabilities::none(),
1850 );
1851 let req_acrh = req
1857 .headers()
1858 .get(header::ACCESS_CONTROL_REQUEST_HEADERS)
1859 .cloned();
1860 let request_id = req
1869 .headers()
1870 .get(REQUEST_ID_RESPONSE_HEADER)
1871 .and_then(|v| v.to_str().ok())
1872 .filter(|v| !v.is_empty() && v.len() <= 128)
1873 .map(str::to_owned)
1874 .unwrap_or_else(new_session_id);
1875 if let Ok(v) = HeaderValue::from_str(&request_id) {
1876 req.headers_mut()
1877 .insert(HeaderName::from_static(REQUEST_ID_RESPONSE_HEADER), v);
1878 }
1879 let req_encoding = pick_response_encoding(req.headers(), state.response_compression_level);
1880 let req_method = req.method().clone();
1881 let req_path = req.uri().path().to_owned();
1882
1883 if let Some(limit) = state.max_request_bytes {
1888 let exempt = req_path == "/health" || req_path.ends_with("/health");
1889 if !exempt {
1890 if let Some(cl) = req
1891 .headers()
1892 .get(header::CONTENT_LENGTH)
1893 .and_then(|v| v.to_str().ok())
1894 .and_then(|s| s.parse::<usize>().ok())
1895 {
1896 if cl > limit {
1897 let mut h = HeaderMap::new();
1898 attach_capability_headers(&state, &mut h, &req_method);
1899 attach_cors_headers(&state, &mut h, req_acrh.as_ref(), false);
1900 return (
1901 StatusCode::PAYLOAD_TOO_LARGE,
1902 h,
1903 format!(
1904 "Request body of {cl} bytes exceeds advertised \
1905 max_request_bytes={limit}. Use the upload-URL \
1906 flow (__upload_url__/init) to externalize."
1907 ),
1908 )
1909 .into_response();
1910 }
1911 }
1912
1913 let (parts, body) = req.into_parts();
1920 let bounded = to_bytes(body, limit.saturating_add(1)).await;
1921 let body = match bounded {
1922 Ok(body) if body.len() <= limit => body,
1923 Ok(body) => {
1924 let mut h = HeaderMap::new();
1925 attach_capability_headers(&state, &mut h, &req_method);
1926 attach_cors_headers(&state, &mut h, req_acrh.as_ref(), false);
1927 return (
1928 StatusCode::PAYLOAD_TOO_LARGE,
1929 h,
1930 format!(
1931 "Request body of {} bytes exceeds advertised max_request_bytes={limit}",
1932 body.len()
1933 ),
1934 )
1935 .into_response();
1936 }
1937 Err(_) => {
1938 let mut h = HeaderMap::new();
1939 attach_capability_headers(&state, &mut h, &req_method);
1940 attach_cors_headers(&state, &mut h, req_acrh.as_ref(), false);
1941 return (
1942 StatusCode::PAYLOAD_TOO_LARGE,
1943 h,
1944 format!("Request body exceeds advertised max_request_bytes={limit}"),
1945 )
1946 .into_response();
1947 }
1948 };
1949 req = axum::http::Request::from_parts(parts, axum::body::Body::from(body));
1950 }
1951 }
1952
1953 let resp = next.run(req).await;
1954 let (mut parts, body) = resp.into_parts();
1955 let response_limit = response_buffer_ceiling(&state);
1963 let bytes = match to_bytes(body, response_limit).await {
1964 Ok(b) => b,
1965 Err(_) => {
1966 let mut h = HeaderMap::new();
1967 attach_cors_headers(&state, &mut h, req_acrh.as_ref(), false);
1968 return (
1969 StatusCode::INTERNAL_SERVER_ERROR,
1970 h,
1971 "response body exceeded the configured size limit",
1972 )
1973 .into_response();
1974 }
1975 };
1976 let is_arrow = parts
1977 .headers
1978 .get(header::CONTENT_TYPE)
1979 .and_then(|v| v.to_str().ok())
1980 == Some(ARROW_CONTENT_TYPE);
1981
1982 attach_cors_headers(&state, &mut parts.headers, req_acrh.as_ref(), false);
1988 attach_capability_headers(&state, &mut parts.headers, &req_method);
1989 if let Ok(v) = HeaderValue::from_str(&request_id) {
1990 parts
1991 .headers
1992 .insert(HeaderName::from_static(REQUEST_ID_RESPONSE_HEADER), v);
1993 }
1994
1995 if is_arrow {
1996 if let (Some(level), Some((encoding, used_custom))) =
1997 (state.response_compression_level, req_encoding)
1998 {
1999 if bytes.len() >= MIN_ZSTD_COMPRESS_BYTES {
2007 if let Some(compressed) = encode_response_body(encoding, &bytes, level) {
2008 if compressed.len() < bytes.len() {
2011 let name = if used_custom {
2016 axum::http::HeaderName::from_static(VGI_CONTENT_ENCODING_HEADER)
2017 } else {
2018 header::CONTENT_ENCODING
2019 };
2020 parts
2021 .headers
2022 .insert(name, HeaderValue::from_static(encoding.as_str()));
2023 let compressed_len = compressed.len() as u64;
2024 let body_new = axum::body::Body::from(compressed);
2025 emit_deferred_access_records(&mut parts, compressed_len);
2026 return Response::from_parts(parts, body_new);
2027 }
2028 }
2029 }
2030 }
2031 }
2032 let body_len = bytes.len() as u64;
2033 emit_deferred_access_records(&mut parts, body_len);
2034 Response::from_parts(parts, axum::body::Body::from(bytes))
2035}
2036
2037fn emit_deferred_access_records(parts: &mut axum::http::response::Parts, response_bytes: u64) {
2045 if let Some(sink) = parts.extensions.remove::<crate::hooks::AccessSink>() {
2046 sink.emit(Some(response_bytes));
2047 }
2048}
2049
2050fn attach_capability_headers(
2056 state: &Arc<HttpState>,
2057 out: &mut HeaderMap,
2058 method: &axum::http::Method,
2059) {
2060 for (k, v) in state.capability_headers.iter() {
2062 out.insert(k.clone(), v.clone());
2063 }
2064 if state.capability_has_any && method == axum::http::Method::OPTIONS {
2065 out.insert(
2066 header::CACHE_CONTROL,
2067 HeaderValue::from_static("public, max-age=300"),
2068 );
2069 }
2070}
2071
2072fn build_router_inner(state: Arc<HttpState>) -> Router {
2073 let prefix = state.prefix.clone();
2074 let api = Router::new()
2075 .route("/:method", post(handle_unary).options(handle_preflight))
2076 .route(
2077 "/:method/init",
2078 post(handle_stream_init).options(handle_preflight),
2079 )
2080 .route(
2081 "/:method/exchange",
2082 post(handle_stream_exchange).options(handle_preflight),
2083 );
2084
2085 let api = if state.upload_url_provider.is_some() {
2086 api.route(
2087 "/__upload_url__/init",
2088 post(handle_upload_url).options(handle_preflight),
2089 )
2090 } else {
2091 api
2092 };
2093
2094 let api = if state.sticky.is_some() {
2095 api.route(
2096 "/__session__",
2097 axum::routing::delete(handle_delete_session).options(handle_preflight),
2098 )
2099 } else {
2100 api
2101 };
2102
2103 let api = api.route(
2109 crate::auth::introspect::INTROSPECT_ENDPOINT,
2110 post(handle_introspect_token).options(handle_preflight),
2111 );
2112
2113 let mut app = if prefix.is_empty() {
2114 api
2115 } else {
2116 Router::new().nest(&prefix, api)
2117 };
2118
2119 app = app.route(
2120 &format!(
2121 "{}{}",
2122 prefix,
2123 crate::auth::oauth::OAuthResourceMetadata::well_known_path()
2124 ),
2125 axum::routing::get(handle_oauth_metadata),
2126 );
2127
2128 if state.health_enabled {
2129 app = app.route(
2135 "/health",
2136 axum::routing::get(handle_health).options(handle_preflight),
2137 );
2138 }
2139 if state.landing_page_enabled {
2140 let landing_path = if prefix.is_empty() {
2141 "/".to_string()
2142 } else {
2143 prefix.clone()
2144 };
2145 app = app.route(&landing_path, axum::routing::get(handle_landing));
2146 }
2147 if state.describe_page_enabled {
2148 app = app.route(
2149 &format!("{prefix}/describe"),
2150 axum::routing::get(handle_describe_page),
2151 );
2152 }
2153 if state.landing_info.is_some() {
2156 app = app.route(
2157 &format!("{prefix}/vgi-client.js"),
2158 axum::routing::get(handle_client_bundle),
2159 );
2160 }
2161
2162 app.with_state(state)
2163}
2164
2165async fn handle_delete_session(
2169 State(state): State<Arc<HttpState>>,
2170 headers: HeaderMap,
2171) -> Response {
2172 let auth = match authenticate_request(&state, SESSION_ENDPOINT, &headers) {
2173 Ok(a) => a,
2174 Err(resp) => return resp,
2175 };
2176 let Some(ctx) = state.sticky.as_ref() else {
2177 return StatusCode::OK.into_response();
2178 };
2179 let session_header = headers.get(SESSION_HEADER).and_then(|v| v.to_str().ok());
2180 match crate::sticky::handle_delete(ctx, &auth, session_header) {
2181 crate::sticky::DeleteOutcome::Idempotent => StatusCode::OK.into_response(),
2182 crate::sticky::DeleteOutcome::Closed => {
2183 let mut h = HeaderMap::new();
2184 h.insert(SESSION_CLOSE_HEADER, HeaderValue::from_static("true"));
2185 (StatusCode::NO_CONTENT, h).into_response()
2186 }
2187 }
2188}
2189
2190async fn handle_introspect_token(
2198 State(state): State<Arc<HttpState>>,
2199 headers: HeaderMap,
2200 body: axum::body::Body,
2201) -> Response {
2202 use crate::auth::introspect::{IntrospectOutcome, MAX_INTROSPECT_BODY_BYTES};
2203
2204 let Some(introspector) = state.introspect.as_ref() else {
2205 return introspect_refusal(StatusCode::NOT_FOUND, "not_enabled", None);
2209 };
2210 let auth = match authenticate_request(
2211 &state,
2212 crate::auth::introspect::INTROSPECT_ENDPOINT,
2213 &headers,
2214 ) {
2215 Ok(a) => a,
2216 Err(resp) => return resp,
2217 };
2218 let bytes = axum::body::to_bytes(body, MAX_INTROSPECT_BODY_BYTES)
2222 .await
2223 .unwrap_or_default();
2224
2225 match introspector.introspect(&auth, &bytes) {
2226 IntrospectOutcome::Resolved {
2227 principal,
2228 token_name,
2229 ttl_seconds,
2230 } => {
2231 let body = serde_json::json!({
2235 "principal": principal,
2236 "token_name": token_name,
2237 "ttl_seconds": ttl_seconds,
2238 })
2239 .to_string();
2240 introspect_json(StatusCode::OK, body, None)
2241 }
2242 IntrospectOutcome::NotAnIntrospector => {
2243 introspect_refusal(StatusCode::FORBIDDEN, "not_an_introspector", None)
2244 }
2245 IntrospectOutcome::Unresolved => {
2246 introspect_refusal(StatusCode::NOT_FOUND, "unresolved", None)
2247 }
2248 IntrospectOutcome::RateLimited => {
2249 introspect_refusal(StatusCode::TOO_MANY_REQUESTS, "rate_limited", Some(1))
2250 }
2251 IntrospectOutcome::Unavailable {
2252 retry_after_seconds,
2253 } => introspect_refusal(
2254 StatusCode::SERVICE_UNAVAILABLE,
2255 "unavailable",
2256 Some(retry_after_seconds),
2257 ),
2258 }
2259}
2260
2261fn introspect_refusal(status: StatusCode, error: &str, retry_after: Option<u32>) -> Response {
2263 introspect_json(
2264 status,
2265 serde_json::json!({ "error": error }).to_string(),
2266 retry_after,
2267 )
2268}
2269
2270fn introspect_json(status: StatusCode, body: String, retry_after: Option<u32>) -> Response {
2271 let mut h = HeaderMap::new();
2272 h.insert(
2273 header::CONTENT_TYPE,
2274 HeaderValue::from_static("application/json"),
2275 );
2276 h.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
2279 if let Some(secs) = retry_after {
2280 if let Ok(v) = HeaderValue::from_str(&secs.to_string()) {
2281 h.insert(header::RETRY_AFTER, v);
2282 }
2283 }
2284 (status, h, body).into_response()
2285}
2286
2287async fn handle_preflight(State(state): State<Arc<HttpState>>, headers: HeaderMap) -> Response {
2288 let mut h = HeaderMap::new();
2289 attach_cors_headers(
2290 &state,
2291 &mut h,
2292 headers.get(header::ACCESS_CONTROL_REQUEST_HEADERS),
2293 true,
2294 );
2295 (StatusCode::NO_CONTENT, h).into_response()
2296}
2297
2298async fn handle_health(State(state): State<Arc<HttpState>>) -> Response {
2299 let body = serde_json::json!({
2300 "status": "ok",
2301 "server_id": state.server.server_id,
2302 "protocol": state.server.protocol_name(),
2303 })
2304 .to_string();
2305 let mut h = HeaderMap::new();
2306 h.insert(
2307 header::CONTENT_TYPE,
2308 HeaderValue::from_static("application/json"),
2309 );
2310 (StatusCode::OK, h, body).into_response()
2311}
2312
2313async fn handle_landing(
2317 State(state): State<Arc<HttpState>>,
2318 headers: HeaderMap,
2319 axum::extract::RawQuery(query): axum::extract::RawQuery,
2320) -> Response {
2321 let accept = headers
2322 .get(header::ACCEPT)
2323 .and_then(|v| v.to_str().ok())
2324 .unwrap_or("");
2325 let format_json = query.as_deref().map(query_has_format_json).unwrap_or(false);
2326 let want_json =
2327 format_json || (accept.contains("application/json") && !accept.contains("text/html"));
2328
2329 let mut h = HeaderMap::new();
2330 if want_json {
2331 h.insert(
2332 header::CONTENT_TYPE,
2333 HeaderValue::from_static("application/json"),
2334 );
2335 let info = state.landing_info.clone().unwrap_or_default();
2338 let body = serde_json::json!({
2339 "status": "ok",
2340 "server_id": state.server.server_id,
2341 "protocol": state.server.protocol_name(),
2342 "worker": info.name,
2343 "doc": info.doc,
2344 "version": info.version,
2345 "lang": "rust",
2346 "oauth": state.oauth_metadata.is_some(),
2347 "cupola_base": "https://cupola.query-farm.services",
2348 })
2349 .to_string();
2350 return (StatusCode::OK, h, body).into_response();
2351 }
2352 h.insert(
2353 header::CONTENT_TYPE,
2354 HeaderValue::from_static("text/html; charset=utf-8"),
2355 );
2356 (StatusCode::OK, h, LANDING_HTML).into_response()
2357}
2358
2359fn query_has_format_json(raw: &str) -> bool {
2361 raw.split('&')
2362 .any(|pair| pair == "format=json" || pair.strip_prefix("format=") == Some("json"))
2363}
2364
2365async fn handle_client_bundle() -> Response {
2367 let mut h = HeaderMap::new();
2368 h.insert(
2369 header::CONTENT_TYPE,
2370 HeaderValue::from_static("text/javascript; charset=utf-8"),
2371 );
2372 h.insert(
2375 header::CACHE_CONTROL,
2376 HeaderValue::from_static("public, max-age=3600"),
2377 );
2378 (StatusCode::OK, h, CLIENT_BUNDLE).into_response()
2379}
2380
2381async fn handle_describe_page(State(state): State<Arc<HttpState>>) -> Response {
2382 let body = render_describe_page(&state);
2383 let mut h = HeaderMap::new();
2384 h.insert(
2385 header::CONTENT_TYPE,
2386 HeaderValue::from_static("text/html; charset=utf-8"),
2387 );
2388 (StatusCode::OK, h, body).into_response()
2389}
2390
2391fn render_describe_page(state: &Arc<HttpState>) -> String {
2392 let mut body = String::from(
2393 "<!doctype html><html><head><meta charset=\"utf-8\"><title>API reference</title></head><body>",
2394 );
2395 body.push_str(&format!(
2396 "<h1>{}</h1><table><tr><th>method</th><th>type</th><th>doc</th></tr>",
2397 state.server.protocol_name()
2398 ));
2399 for name in state.server.sorted_method_names() {
2400 let m = &state.server.methods()[name];
2401 let kind = match m.method_type {
2402 crate::server::MethodType::Unary => "unary",
2403 _ => "stream",
2404 };
2405 let doc = m.doc.as_deref().unwrap_or("");
2406 body.push_str(&format!(
2407 "<tr><td><code>{name}</code></td><td>{kind}</td><td>{}</td></tr>",
2408 html_escape(doc)
2409 ));
2410 }
2411 body.push_str("</table></body></html>");
2412 body
2413}
2414
2415fn html_escape(s: &str) -> String {
2416 s.replace('&', "&")
2417 .replace('<', "<")
2418 .replace('>', ">")
2419}
2420
2421fn attach_cors_headers(
2422 state: &Arc<HttpState>,
2423 out: &mut HeaderMap,
2424 requested_headers: Option<&HeaderValue>,
2425 is_preflight: bool,
2426) {
2427 let Some(origin) = state.cors_allow_origin.as_ref() else {
2428 return;
2429 };
2430 out.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.clone());
2431 if state.authenticate.is_some() {
2438 out.insert(
2439 header::ACCESS_CONTROL_ALLOW_CREDENTIALS,
2440 HeaderValue::from_static("true"),
2441 );
2442 }
2443 out.insert(
2444 header::ACCESS_CONTROL_ALLOW_METHODS,
2445 HeaderValue::from_static("POST, GET, OPTIONS"),
2446 );
2447 match requested_headers {
2451 Some(v) => {
2452 out.insert(header::ACCESS_CONTROL_ALLOW_HEADERS, v.clone());
2453 }
2454 None => {
2455 out.insert(
2456 header::ACCESS_CONTROL_ALLOW_HEADERS,
2457 HeaderValue::from_static(
2465 "Content-Type, Authorization, Cookie, Accept-Encoding, \
2466 X-VGI-Accept-Encoding, VGI-Session, VGI-Session-Accept, \
2467 VGI-Proxy-Proof",
2468 ),
2469 );
2470 }
2471 }
2472 out.insert(
2477 header::ACCESS_CONTROL_EXPOSE_HEADERS,
2478 state.cors_expose_headers.clone(),
2479 );
2480 out.insert(
2484 HeaderName::from_static("cross-origin-resource-policy"),
2485 HeaderValue::from_static("cross-origin"),
2486 );
2487 if is_preflight {
2488 if let Ok(v) = HeaderValue::from_str(&state.cors_max_age.to_string()) {
2489 out.insert(header::ACCESS_CONTROL_MAX_AGE, v);
2490 }
2491 }
2492}
2493
2494async fn handle_oauth_metadata(State(state): State<Arc<HttpState>>) -> Response {
2495 match state.oauth_metadata_json.as_ref() {
2496 Some(body) => {
2497 let mut h = HeaderMap::new();
2498 h.insert(
2499 header::CONTENT_TYPE,
2500 HeaderValue::from_static("application/json"),
2501 );
2502 h.insert(
2503 header::CACHE_CONTROL,
2504 HeaderValue::from_static("public, max-age=60"),
2505 );
2506 (StatusCode::OK, h, body.clone()).into_response()
2507 }
2508 None => (StatusCode::NOT_FOUND, "").into_response(),
2509 }
2510}
2511
2512fn parse_cookies(raw: Option<&str>) -> std::collections::BTreeMap<String, String> {
2515 let mut out = std::collections::BTreeMap::new();
2516 let Some(raw) = raw else { return out };
2517 for part in raw.split(';') {
2518 let part = part.trim();
2519 if let Some((k, v)) = part.split_once('=') {
2520 let v = v.trim();
2521 let v = v
2522 .strip_prefix('"')
2523 .and_then(|s| s.strip_suffix('"'))
2524 .unwrap_or(v);
2525 out.insert(k.trim().to_string(), v.to_string());
2526 }
2527 }
2528 out
2529}
2530
2531fn headers_to_pairs(headers: &HeaderMap) -> Vec<(String, String)> {
2533 headers
2534 .iter()
2535 .filter_map(|(k, v)| {
2536 v.to_str()
2537 .ok()
2538 .map(|s| (k.as_str().to_string(), s.to_string()))
2539 })
2540 .collect()
2541}
2542
2543#[allow(clippy::result_large_err)]
2548fn authenticate_request(
2549 state: &Arc<HttpState>,
2550 method: &str,
2551 headers: &HeaderMap,
2552) -> std::result::Result<crate::auth::AuthContext, Response> {
2553 let Some(cb) = state.authenticate.as_ref() else {
2554 return Ok(crate::auth::AuthContext::anonymous());
2555 };
2556 let pairs = headers_to_pairs(headers);
2557 let req = crate::auth::AuthRequest {
2558 method,
2559 headers: &pairs,
2560 peer_addr: None,
2561 };
2562 match (cb)(&req) {
2563 Ok(ctx) => Ok(ctx),
2564 Err(err) if err.is_auth_unavailable() => {
2565 tracing::warn!(
2569 target: "vgi_rpc.http",
2570 error = %err.message,
2571 "authentication unavailable"
2572 );
2573 let mut h = HeaderMap::new();
2574 let retry_after = err
2575 .retry_after_seconds
2576 .unwrap_or(crate::errors::DEFAULT_AUTH_RETRY_AFTER_SECONDS);
2577 if let Ok(v) = HeaderValue::from_str(&retry_after.to_string()) {
2578 h.insert(header::RETRY_AFTER, v);
2579 }
2580 Err((
2581 StatusCode::SERVICE_UNAVAILABLE,
2582 h,
2583 "authentication service unavailable",
2584 )
2585 .into_response())
2586 }
2587 Err(err) => {
2588 let status = match err.error_type.as_str() {
2589 "PermissionError" | "ValueError" => StatusCode::UNAUTHORIZED,
2590 _ => StatusCode::INTERNAL_SERVER_ERROR,
2591 };
2592 let mut h = HeaderMap::new();
2593 if status == StatusCode::UNAUTHORIZED {
2600 if let Some(wa) = state.www_authenticate.as_deref() {
2601 if let Ok(hv) = HeaderValue::from_str(wa) {
2602 h.insert(header::WWW_AUTHENTICATE, hv);
2603 }
2604 }
2605 tracing::info!(
2606 target: "vgi_rpc.http",
2607 error = %err.message,
2608 "request authentication rejected"
2609 );
2610 let reason = err
2616 .auth_reason
2617 .unwrap_or(if err.error_type == "PermissionError" {
2618 AuthReason::InsufficientScope
2619 } else {
2620 AuthReason::Unauthorized
2621 });
2622 let detail = if err.auth_reason.is_some() {
2628 err.message.as_str()
2629 } else {
2630 ""
2631 };
2632 return Err(state.unauthorized_response(h, reason, detail));
2633 }
2634 let body = {
2635 tracing::error!(
2636 target: "vgi_rpc.http",
2637 error = %err.message,
2638 "authentication callback errored"
2639 );
2640 "internal error during authentication"
2641 };
2642 Err((status, h, body).into_response())
2643 }
2644 }
2645}
2646
2647fn arrow_response(status: StatusCode, body: Vec<u8>) -> Response {
2652 let mut headers = HeaderMap::new();
2653 headers.insert(
2654 header::CONTENT_TYPE,
2655 HeaderValue::from_static(ARROW_CONTENT_TYPE),
2656 );
2657 (status, headers, body).into_response()
2658}
2659
2660fn enforce_response_body_cap(
2665 state: &Arc<HttpState>,
2666 schema: &arrow_schema::Schema,
2667 body: Vec<u8>,
2668 method: &str,
2669 server_id: &str,
2670 request_id: &str,
2671) -> Response {
2672 if let Some(limit) = state.max_response_bytes {
2673 if body.len() > limit {
2674 let err = RpcError::runtime_error(format!(
2675 "HTTP body exceeds max_response_bytes ({} > {}) for method {:?}",
2676 body.len(),
2677 limit,
2678 method
2679 ));
2680 return cap_error_response(schema, &err, server_id, request_id);
2681 }
2682 }
2683 arrow_response(StatusCode::OK, body)
2684}
2685
2686fn external_cap_error(bytes: usize, limit: usize, method: &str) -> RpcError {
2695 RpcError::runtime_error(format!(
2696 "Externalised payload exceeds max_externalized_response_bytes ({bytes} > {limit}) for method {method:?}"
2697 ))
2698}
2699
2700fn cap_error_response(
2705 schema: &arrow_schema::Schema,
2706 err: &RpcError,
2707 server_id: &str,
2708 request_id: &str,
2709) -> Response {
2710 let mut buf = Vec::new();
2711 {
2712 let mut sw = StreamWriter::new(&mut buf, schema).unwrap();
2713 let md = build_error_metadata(err, server_id, request_id);
2714 let _ = sw.write(&empty_batch(schema).unwrap(), Some(&md));
2715 let _ = sw.finish();
2716 }
2717 let mut headers = HeaderMap::new();
2718 headers.insert(
2719 header::CONTENT_TYPE,
2720 HeaderValue::from_static(ARROW_CONTENT_TYPE),
2721 );
2722 headers.insert(RPC_ERROR_HEADER, HeaderValue::from_static("true"));
2723 (StatusCode::OK, headers, buf).into_response()
2724}
2725
2726fn stamp_rpc_error(resp: &mut Response) {
2739 resp.headers_mut()
2740 .insert(RPC_ERROR_HEADER, HeaderValue::from_static("true"));
2741}
2742
2743fn plain_error(status: StatusCode, msg: String) -> Response {
2744 (status, msg).into_response()
2745}
2746
2747fn has_arrow_ct(headers: &HeaderMap) -> bool {
2748 headers
2749 .get(header::CONTENT_TYPE)
2750 .and_then(|v| v.to_str().ok())
2751 .map(|s| s == ARROW_CONTENT_TYPE)
2752 .unwrap_or(false)
2753}
2754
2755fn maybe_decompress(headers: &HeaderMap, body: &Bytes, max_size: usize) -> Result<Vec<u8>> {
2756 let enc = headers
2757 .get(header::CONTENT_ENCODING)
2758 .and_then(|v| v.to_str().ok());
2759 if body.len() > max_size {
2760 return Err(RpcError::runtime_error(format!(
2761 "Request body exceeds max size ({} bytes > {})",
2762 body.len(),
2763 max_size
2764 )));
2765 }
2766 decode_content_encoding(body.as_ref(), enc, Some(max_size))
2767}
2768
2769fn zstd_window_log_for_limit(max_size: usize) -> u32 {
2770 const INTEROPERABLE_WINDOW_LOG_FLOOR: u32 = 19;
2775 let bounded = max_size.max(1 << INTEROPERABLE_WINDOW_LOG_FLOOR);
2776 let ceil_log = usize::BITS - bounded.saturating_sub(1).leading_zeros();
2777 ceil_log.clamp(INTEROPERABLE_WINDOW_LOG_FLOOR, 31)
2778}
2779
2780fn request_decode_limit(state: &HttpState) -> usize {
2781 state
2782 .max_request_bytes
2783 .unwrap_or(state.max_body_size)
2784 .min(state.max_body_size)
2785}
2786
2787fn request_decode_error(state: &Arc<HttpState>, error: &RpcError) -> Response {
2788 let status = if error.message.contains("exceeds max size") {
2789 StatusCode::PAYLOAD_TOO_LARGE
2790 } else {
2791 StatusCode::BAD_REQUEST
2792 };
2793 arrow_error(state, status, error, "")
2794}
2795
2796pub const MAX_CONTENT_CODINGS: usize = 4;
2800
2801pub fn decode_content_encoding(
2820 data: &[u8],
2821 content_encoding: Option<&str>,
2822 max_output_size: Option<usize>,
2823) -> Result<Vec<u8>> {
2824 let Some(header) = content_encoding.filter(|h| !h.trim().is_empty()) else {
2825 return Ok(data.to_vec());
2826 };
2827 let max_size = max_output_size.unwrap_or(usize::MAX);
2828 let codings = header
2829 .split(',')
2830 .map(|c| c.trim().to_ascii_lowercase())
2831 .filter(|c| !c.is_empty());
2832 if codings.clone().count() > MAX_CONTENT_CODINGS {
2836 return Err(RpcError::runtime_error(format!(
2837 "Content-Encoding lists more than {MAX_CONTENT_CODINGS} codings"
2838 )));
2839 }
2840 let mut result = data.to_vec();
2841 for name in codings.rev() {
2842 result = match name.as_str() {
2843 "zstd" => {
2844 let mut decoder = zstd::Decoder::new(result.as_slice())
2845 .map_err(|e| RpcError::runtime_error(format!("zstd decode: {e}")))?;
2846 decoder
2847 .window_log_max(zstd_window_log_for_limit(max_size))
2848 .map_err(|e| RpcError::runtime_error(format!("zstd window limit: {e}")))?;
2849 decode_bounded(decoder, result.len(), max_size, "zstd")?
2850 }
2851 "gzip" => decode_bounded(
2852 flate2::read::GzDecoder::new(result.as_slice()),
2853 result.len(),
2854 max_size,
2855 "gzip",
2856 )?,
2857 _ => result, };
2859 }
2860 Ok(result)
2861}
2862
2863fn decode_bounded(
2866 mut decoder: impl std::io::Read,
2867 input_len: usize,
2868 max_size: usize,
2869 coding: &str,
2870) -> Result<Vec<u8>> {
2871 let mut out = Vec::with_capacity(input_len.min(max_size).min(64 * 1024));
2872 let mut buf = [0u8; 16 * 1024];
2873 loop {
2874 let n = decoder
2875 .read(&mut buf)
2876 .map_err(|e| RpcError::runtime_error(format!("{coding} decode: {e}")))?;
2877 if n == 0 {
2878 break;
2879 }
2880 if out.len() + n > max_size {
2881 return Err(RpcError::runtime_error(format!(
2882 "Decompressed body exceeds max size ({}+ bytes > {})",
2883 out.len() + n,
2884 max_size
2885 )));
2886 }
2887 out.extend_from_slice(&buf[..n]);
2888 }
2889 Ok(out)
2890}
2891
2892fn parse_request_from_body(body: &[u8]) -> Result<Request> {
2893 let mut r = StreamReader::new(body)?;
2894 let (batch, metadata) = r
2895 .read_next()?
2896 .ok_or_else(|| RpcError::protocol_error("empty IPC stream"))?;
2897 r.drain()?;
2898 Request::from_read_batch(batch, metadata, true)
2899}
2900
2901fn error_stream_bytes(
2902 schema: &Schema,
2903 err: &RpcError,
2904 server_id: &str,
2905 request_id: &str,
2906) -> Vec<u8> {
2907 let mut buf = Vec::new();
2908 let mut w = StreamWriter::new(&mut buf, schema).unwrap();
2909 let md = build_error_metadata(err, server_id, request_id);
2910 let _ = w.write(&empty_batch(schema).unwrap(), Some(&md));
2911 let _ = w.finish();
2912 drop(w);
2913 buf
2914}
2915
2916fn arrow_error(
2920 state: &Arc<HttpState>,
2921 status: StatusCode,
2922 err: &RpcError,
2923 request_id: &str,
2924) -> Response {
2925 arrow_response(
2926 status,
2927 error_stream_bytes(&Schema::empty(), err, &state.server.server_id, request_id),
2928 )
2929}
2930
2931#[allow(clippy::result_large_err)]
2936fn sticky_for_request(
2937 state: &Arc<HttpState>,
2938 auth: &crate::auth::AuthContext,
2939 headers: &HeaderMap,
2940) -> std::result::Result<Option<Arc<crate::sticky::StickySinkImpl>>, Response> {
2941 let Some(ctx) = state.sticky.as_ref() else {
2942 return Ok(None);
2943 };
2944 let accept = headers
2945 .get(SESSION_ACCEPT_HEADER)
2946 .and_then(|v| v.to_str().ok())
2947 .map(|s| s.trim().eq_ignore_ascii_case("true"))
2948 .unwrap_or(false);
2949 let session_header = headers.get(SESSION_HEADER).and_then(|v| v.to_str().ok());
2950 match crate::sticky::resolve(ctx, auth, accept, session_header) {
2951 crate::sticky::StickyResolution::Sink(s) => Ok(Some(s)),
2952 crate::sticky::StickyResolution::Lost(err) => Err(cap_error_response(
2953 &Schema::empty(),
2954 &err,
2955 &state.server.server_id,
2956 "",
2957 )),
2958 }
2959}
2960
2961fn stamp_session_headers(
2964 resp: &mut Response,
2965 state: &Arc<HttpState>,
2966 sink: &Arc<crate::sticky::StickySinkImpl>,
2967) {
2968 let headers = resp.headers_mut();
2969 if let Some(token) = sink.mint_token() {
2970 if let Ok(v) = HeaderValue::from_str(&token) {
2971 headers.insert(SESSION_HEADER, v);
2972 }
2973 if let Some(ctx) = state.sticky.as_ref() {
2974 for (name, value) in &ctx.echo_headers {
2975 let full = format!("{ECHO_HEADER_PREFIX}{name}");
2976 if let (Ok(n), Ok(v)) = (
2977 axum::http::HeaderName::from_bytes(full.as_bytes()),
2978 HeaderValue::from_str(value),
2979 ) {
2980 headers.insert(n, v);
2981 }
2982 }
2983 }
2984 }
2985 if sink.was_closed() {
2986 headers.insert(SESSION_CLOSE_HEADER, HeaderValue::from_static("true"));
2987 }
2988}
2989
2990fn decode_hex_key(s: &str) -> std::result::Result<Vec<u8>, String> {
2991 let s = s.trim();
2992 if !s.len().is_multiple_of(2) {
2993 return Err("hex length must be even".into());
2994 }
2995 let mut out = Vec::with_capacity(s.len() / 2);
2996 let bytes = s.as_bytes();
2997 for pair in bytes.chunks_exact(2) {
2998 let hi = hex_nibble(pair[0])?;
2999 let lo = hex_nibble(pair[1])?;
3000 out.push((hi << 4) | lo);
3001 }
3002 if out.len() < 32 {
3003 return Err(format!(
3004 "signing key must be ≥ 32 bytes (got {} bytes)",
3005 out.len()
3006 ));
3007 }
3008 Ok(out)
3009}
3010
3011fn hex_nibble(c: u8) -> std::result::Result<u8, String> {
3012 match c {
3013 b'0'..=b'9' => Ok(c - b'0'),
3014 b'a'..=b'f' => Ok(c - b'a' + 10),
3015 b'A'..=b'F' => Ok(c - b'A' + 10),
3016 _ => Err(format!("invalid hex character: {:?}", c as char)),
3017 }
3018}
3019
3020fn decode_base64_key(s: &str) -> std::result::Result<Vec<u8>, String> {
3021 let s = s.trim().trim_end_matches('=');
3023 let mut padded = s.to_string();
3024 while !padded.len().is_multiple_of(4) {
3025 padded.push('=');
3026 }
3027 let bytes = base64::engine::general_purpose::STANDARD
3028 .decode(padded.as_bytes())
3029 .map_err(|e| format!("base64 decode: {e}"))?;
3030 if bytes.len() < 32 {
3031 return Err(format!(
3032 "signing key must be ≥ 32 bytes (got {} bytes)",
3033 bytes.len()
3034 ));
3035 }
3036 Ok(bytes)
3037}
3038
3039fn new_session_id() -> String {
3040 let mut b = [0u8; 16];
3041 rand::thread_rng().fill_bytes(&mut b);
3042 bytes_to_hex(&b)
3043}
3044
3045use crate::external::{
3052 upload_url_params_schema, upload_url_response_schema, MAX_UPLOAD_URL_COUNT, UPLOAD_URL_METHOD,
3053};
3054
3055async fn handle_upload_url(
3056 State(state): State<Arc<HttpState>>,
3057 headers: HeaderMap,
3058 body: Bytes,
3059) -> Response {
3060 let auth = match authenticate_request(&state, UPLOAD_URL_METHOD, &headers) {
3061 Ok(a) => a,
3062 Err(resp) => return resp,
3063 };
3064 let _ = auth;
3065 if !has_arrow_ct(&headers) {
3066 return plain_error(
3067 StatusCode::UNSUPPORTED_MEDIA_TYPE,
3068 "need arrow content type".into(),
3069 );
3070 }
3071 let provider = match state.upload_url_provider.as_ref() {
3072 Some(p) => p.clone(),
3073 None => return plain_error(StatusCode::NOT_FOUND, "upload-url not enabled".into()),
3074 };
3075
3076 let body = match maybe_decompress(&headers, &body, request_decode_limit(&state)) {
3077 Ok(b) => b,
3078 Err(e) => return request_decode_error(&state, &e),
3079 };
3080 let req = match parse_request_from_body(&body) {
3081 Ok(r) => r,
3082 Err(e) => return arrow_error(&state, StatusCode::BAD_REQUEST, &e, ""),
3083 };
3084 if req.method != UPLOAD_URL_METHOD {
3085 let err = RpcError::protocol_error(format!(
3086 "Method mismatch: expected '{UPLOAD_URL_METHOD}', got '{}'",
3087 req.method
3088 ));
3089 return arrow_error(&state, StatusCode::BAD_REQUEST, &err, &req.request_id);
3090 }
3091 if let Err(err) = validate_protocol_version(state.server.protocol_version(), &req.metadata) {
3092 return arrow_error(&state, StatusCode::BAD_REQUEST, &err, &req.request_id);
3093 }
3094 if let Err(err) = validate_parameter_batch(&req.batch, &upload_url_params_schema()) {
3095 return arrow_error(&state, StatusCode::BAD_REQUEST, &err, &req.request_id);
3096 }
3097 let mut count: i64 = 1;
3099 if let Some(arr) = req.column("count") {
3100 use arrow_array::Array;
3101 if let Some(c) = arr.as_any().downcast_ref::<arrow_array::Int64Array>() {
3102 if !c.is_empty() && !Array::is_null(c, 0) {
3103 count = c.value(0);
3104 }
3105 }
3106 }
3107 count = count.clamp(1, MAX_UPLOAD_URL_COUNT);
3108
3109 let urls_res = tokio::task::block_in_place(|| {
3112 let mut out = Vec::with_capacity(count as usize);
3113 for _ in 0..count {
3114 out.push(provider.generate_upload_url()?);
3115 }
3116 Ok::<_, RpcError>(out)
3117 });
3118
3119 let schema = upload_url_response_schema();
3120 let schema_ref = schema.as_ref();
3121 let mut body_buf = Vec::new();
3122 {
3123 let mut sw = match StreamWriter::new(&mut body_buf, schema_ref) {
3124 Ok(w) => w,
3125 Err(e) => {
3126 return arrow_error(
3127 &state,
3128 StatusCode::INTERNAL_SERVER_ERROR,
3129 &e,
3130 &req.request_id,
3131 )
3132 }
3133 };
3134 match urls_res {
3135 Ok(urls) => {
3136 use arrow_array::{StringArray, TimestampMicrosecondArray};
3137 let upload_arr = StringArray::from(
3138 urls.iter()
3139 .map(|u| u.upload_url.clone())
3140 .collect::<Vec<_>>(),
3141 );
3142 let download_arr = StringArray::from(
3143 urls.iter()
3144 .map(|u| u.download_url.clone())
3145 .collect::<Vec<_>>(),
3146 );
3147 let expires_arr = TimestampMicrosecondArray::from(
3148 urls.iter().map(|u| u.expires_at_micros).collect::<Vec<_>>(),
3149 )
3150 .with_timezone("UTC");
3151 let batch = match RecordBatch::try_new(
3152 schema.clone(),
3153 vec![
3154 Arc::new(upload_arr),
3155 Arc::new(download_arr),
3156 Arc::new(expires_arr),
3157 ],
3158 ) {
3159 Ok(b) => b,
3160 Err(e) => {
3161 let err = RpcError::runtime_error(format!("upload-url batch: {e}"));
3162 let md =
3163 build_error_metadata(&err, &state.server.server_id, &req.request_id);
3164 let _ = sw.write(&empty_batch(schema_ref).unwrap(), Some(&md));
3165 let _ = sw.finish();
3166 drop(sw);
3167 return arrow_response(StatusCode::OK, body_buf);
3168 }
3169 };
3170 let _ = sw.write(&batch, None);
3171 }
3172 Err(err) => {
3173 let md = build_error_metadata(&err, &state.server.server_id, &req.request_id);
3174 let _ = sw.write(&empty_batch(schema_ref).unwrap(), Some(&md));
3175 }
3176 }
3177 let _ = sw.finish();
3178 }
3179 arrow_response(StatusCode::OK, body_buf)
3180}
3181
3182async fn handle_unary(
3187 State(state): State<Arc<HttpState>>,
3188 Path(method): Path<String>,
3189 headers: HeaderMap,
3190 body: Bytes,
3191) -> Response {
3192 let auth = match authenticate_request(&state, &method, &headers) {
3196 Ok(a) => a,
3197 Err(resp) => return resp,
3198 };
3199 if !has_arrow_ct(&headers) {
3200 return plain_error(
3201 StatusCode::UNSUPPORTED_MEDIA_TYPE,
3202 "need arrow content type".into(),
3203 );
3204 }
3205 let sticky_sink = match sticky_for_request(&state, &auth, &headers) {
3208 Ok(s) => s,
3209 Err(resp) => return resp,
3210 };
3211 let cookies = parse_cookies(headers.get(header::COOKIE).and_then(|v| v.to_str().ok()));
3212 let server = state.server.clone();
3213
3214 let request_wire_bytes = body.len() as u64;
3218 let body = match maybe_decompress(&headers, &body, request_decode_limit(&state)) {
3219 Ok(b) => b,
3220 Err(e) => return request_decode_error(&state, &e),
3221 };
3222 let mut req = match parse_request_from_body(&body) {
3223 Ok(r) => r,
3224 Err(e) => return arrow_error(&state, StatusCode::BAD_REQUEST, &e, ""),
3225 };
3226 if req.method != method {
3227 let err = RpcError::protocol_error(format!(
3228 "Method mismatch: route names '{method}', request metadata names '{}'",
3229 req.method
3230 ));
3231 return arrow_error(&state, StatusCode::BAD_REQUEST, &err, &req.request_id);
3232 }
3233 if let Err(err) = validate_protocol_version(server.protocol_version(), &req.metadata) {
3234 return arrow_error(&state, StatusCode::BAD_REQUEST, &err, &req.request_id);
3235 }
3236
3237 if md_get(&req.metadata, crate::metadata::LOCATION_KEY).is_some() {
3242 if let Some(cfg) = server.external_config().as_ref() {
3243 let outer_md = req.metadata.clone();
3244 let outer_batch = req.batch.clone();
3245 let resolved = tokio::task::block_in_place(|| {
3246 crate::external::resolve_external_location(&outer_batch, &outer_md, cfg)
3247 });
3248 match resolved {
3249 Ok((inner_batch, _user_md)) => {
3250 req.batch = inner_batch;
3251 }
3252 Err(err) => {
3253 return cap_error_response(
3254 &Schema::empty(),
3255 &err,
3256 &server.server_id,
3257 &req.request_id,
3258 );
3259 }
3260 }
3261 }
3262 }
3263
3264 if server.describe_enabled() && method == crate::introspect::DESCRIBE_METHOD_NAME {
3266 let (batch, md) = match crate::introspect::build_describe(
3267 server.protocol_name(),
3268 server.methods(),
3269 &server.server_id,
3270 server.protocol_version(),
3271 ) {
3272 Ok(x) => x,
3273 Err(err) => {
3274 return arrow_error(
3275 &state,
3276 StatusCode::INTERNAL_SERVER_ERROR,
3277 &err,
3278 &req.request_id,
3279 );
3280 }
3281 };
3282 let mut buf = Vec::new();
3283 let _ = crate::introspect::write_describe_response(&mut buf, &batch, &md);
3284 return arrow_response(StatusCode::OK, buf);
3285 }
3286
3287 let Some(info) = server
3288 .method(&method)
3289 .filter(|m| m.method_type == MethodType::Unary)
3290 else {
3291 let err = RpcError::attribute_error(format!("Unknown method: '{}'", method));
3292 return arrow_error(&state, StatusCode::NOT_FOUND, &err, &req.request_id);
3293 };
3294 if let Err(err) = validate_parameter_batch(&req.batch, &info.params_schema) {
3295 return arrow_error(&state, StatusCode::BAD_REQUEST, &err, &req.request_id);
3296 }
3297
3298 let mut ctx = CallContext::with_auth_cookies(&server, &req, auth.clone(), cookies);
3299 if let Some(s) = sticky_sink.clone() {
3300 ctx.set_sticky(s);
3301 }
3302 let mut dispatch_info = crate::hooks::DispatchInfo::from_request(&server, &req, "unary", &auth);
3303 if let Some(id) = headers
3310 .get(REQUEST_ID_RESPONSE_HEADER)
3311 .and_then(|v| v.to_str().ok())
3312 .filter(|v| !v.is_empty())
3313 {
3314 dispatch_info.request_id = id.to_string();
3315 }
3316 let hook = server.dispatch_hook.clone();
3317 let access_sink = hook.as_ref().map(|_| crate::hooks::AccessSink::new());
3321 if hook.is_some() {
3322 dispatch_info.request_bytes = Some(request_wire_bytes);
3323 dispatch_info.access_sink = access_sink.clone();
3324 if let Ok(bytes) = crate::server::serialize_request_batch(&req.batch) {
3327 dispatch_info.request_data = bytes;
3328 }
3329 }
3330 let hook_token = hook.as_ref().map(|h| h.on_dispatch_start(&dispatch_info));
3331 let externalized = crate::external::ExternalizedScope::new();
3335
3336 let mut stats = crate::hooks::CallStatistics {
3337 input_batches: 1,
3338 input_rows: req.batch.num_rows() as u64,
3339 ..Default::default()
3340 };
3341
3342 let result =
3347 crate::server::call_guard(|| (info.unary.as_ref().unwrap())(&req, &ctx)).and_then(|r| r);
3348 let logs = ctx.drain_logs();
3349 let mut app_err: Option<RpcError> = None;
3350 let mut external_bytes_written = 0usize;
3355
3356 let mut buf = Vec::new();
3357 {
3358 let mut sw = StreamWriter::new(&mut buf, &info.result_schema).unwrap();
3359 for log in &logs {
3360 let md = build_log_metadata(log, &server.server_id, &req.request_id);
3361 let _ = sw.write(&empty_batch(&info.result_schema).unwrap(), Some(&md));
3362 }
3363 match result {
3364 Ok(batch_opt) => {
3365 let out_batch =
3366 batch_opt.unwrap_or_else(|| empty_batch(&info.result_schema).unwrap());
3367 stats.output_batches = 1;
3368 stats.output_rows = out_batch.num_rows() as u64;
3369 if let Some(cfg) = server.external_config().as_ref() {
3370 let externalized = tokio::task::block_in_place(|| {
3378 let prepared = crate::external::prepare_externalize_batch(
3383 &out_batch,
3384 &info.result_schema,
3385 None,
3386 cfg,
3387 )?;
3388 match prepared {
3389 None => Ok(None),
3390 Some(p) => {
3391 if let Some(limit) = state.max_externalized_response_bytes {
3392 if p.cap_bytes() > limit {
3393 return Err(external_cap_error(
3394 p.cap_bytes(),
3395 limit,
3396 &method,
3397 ));
3398 }
3399 }
3400 external_bytes_written += p.cap_bytes();
3401 crate::external::upload_prepared(p, cfg).map(Some)
3402 }
3403 }
3404 });
3405 match externalized {
3406 Ok(Some((ptr, md))) => {
3407 let _ = sw.write(&ptr, Some(&md));
3408 }
3409 Ok(None) => {
3410 let _ = sw.write(&out_batch, None);
3411 }
3412 Err(err) => {
3413 let md = build_error_metadata(&err, &server.server_id, &req.request_id);
3414 let _ = sw.write(&empty_batch(&info.result_schema).unwrap(), Some(&md));
3415 app_err = Some(err);
3416 }
3417 }
3418 } else {
3419 let _ = sw.write(&out_batch, None);
3420 }
3421 }
3422 Err(err) => {
3423 let md = build_error_metadata(&err, &server.server_id, &req.request_id);
3424 let _ = sw.write(&empty_batch(&info.result_schema).unwrap(), Some(&md));
3425 app_err = Some(err);
3426 }
3427 }
3428 let _ = sw.finish();
3429 }
3430
3431 dispatch_info.externalized_bytes = externalized.finish();
3432 if let Some(hook) = hook {
3433 hook.on_dispatch_end(
3434 hook_token.unwrap_or(0),
3435 &dispatch_info,
3436 app_err.as_ref(),
3437 &stats,
3438 );
3439 }
3440 let body_cap_error = state
3451 .max_response_bytes
3452 .filter(|limit| buf.len() > *limit)
3453 .map(|limit| {
3454 RpcError::runtime_error(format!(
3455 "HTTP body exceeds max_response_bytes ({} > {}) for method {:?}",
3456 buf.len(),
3457 limit,
3458 method
3459 ))
3460 })
3461 .or_else(|| {
3462 state
3463 .max_externalized_response_bytes
3464 .filter(|limit| external_bytes_written > *limit)
3465 .map(|limit| external_cap_error(external_bytes_written, limit, &method))
3466 });
3467 if let Some(err) = body_cap_error {
3468 let mut resp = cap_error_response(
3469 &info.result_schema,
3470 &err,
3471 &server.server_id,
3472 &req.request_id,
3473 );
3474 if let Some(s) = sticky_sink.as_ref() {
3475 stamp_session_headers(&mut resp, &state, s);
3476 }
3477 attach_access_sink(&mut resp, access_sink);
3478 return resp;
3479 }
3480 let mut resp = arrow_response(StatusCode::OK, buf);
3481 if app_err.is_some() {
3482 stamp_rpc_error(&mut resp);
3485 }
3486 if let Some(s) = sticky_sink.as_ref() {
3487 stamp_session_headers(&mut resp, &state, s);
3488 }
3489 attach_access_sink(&mut resp, access_sink);
3490 resp
3491}
3492
3493fn attach_access_sink(resp: &mut Response, sink: Option<crate::hooks::AccessSink>) {
3500 if let Some(sink) = sink {
3501 if !sink.is_empty() {
3502 resp.extensions_mut().insert(sink);
3503 }
3504 }
3505}
3506
3507async fn handle_stream_init(
3512 State(state): State<Arc<HttpState>>,
3513 Path(method): Path<String>,
3514 headers: HeaderMap,
3515 body: Bytes,
3516) -> Response {
3517 let auth = match authenticate_request(&state, &method, &headers) {
3518 Ok(a) => a,
3519 Err(resp) => return resp,
3520 };
3521 if !has_arrow_ct(&headers) {
3522 return plain_error(
3523 StatusCode::UNSUPPORTED_MEDIA_TYPE,
3524 "need arrow content type".into(),
3525 );
3526 }
3527 let sticky_sink = match sticky_for_request(&state, &auth, &headers) {
3528 Ok(s) => s,
3529 Err(resp) => return resp,
3530 };
3531 let auth_for_token = auth.clone();
3532 let cookies = parse_cookies(headers.get(header::COOKIE).and_then(|v| v.to_str().ok()));
3533 let server = state.server.clone();
3534 let body = match maybe_decompress(&headers, &body, request_decode_limit(&state)) {
3535 Ok(b) => b,
3536 Err(e) => return request_decode_error(&state, &e),
3537 };
3538 let mut req = match parse_request_from_body(&body) {
3539 Ok(r) => r,
3540 Err(e) => return arrow_error(&state, StatusCode::BAD_REQUEST, &e, ""),
3541 };
3542 if req.method != method {
3543 let err = RpcError::protocol_error(format!(
3544 "Method mismatch: route names '{method}', request metadata names '{}'",
3545 req.method
3546 ));
3547 return arrow_error(&state, StatusCode::BAD_REQUEST, &err, &req.request_id);
3548 }
3549 if let Err(err) = validate_protocol_version(server.protocol_version(), &req.metadata) {
3550 return arrow_error(&state, StatusCode::BAD_REQUEST, &err, &req.request_id);
3551 }
3552
3553 if md_get(&req.metadata, crate::metadata::LOCATION_KEY).is_some() {
3557 if let Some(cfg) = server.external_config().as_ref() {
3558 let resolved = tokio::task::block_in_place(|| {
3559 crate::external::resolve_external_location(&req.batch, &req.metadata, cfg)
3560 });
3561 match resolved {
3562 Ok((inner_batch, _user_md)) => req.batch = inner_batch,
3563 Err(err) => {
3564 return cap_error_response(
3565 &Schema::empty(),
3566 &err,
3567 &server.server_id,
3568 &req.request_id,
3569 );
3570 }
3571 }
3572 }
3573 }
3574
3575 let Some(info) = server
3576 .method(&method)
3577 .filter(|m| m.method_type != MethodType::Unary)
3578 else {
3579 let err = RpcError::attribute_error(format!("Unknown stream method: '{}'", method));
3580 return arrow_error(&state, StatusCode::NOT_FOUND, &err, &req.request_id);
3581 };
3582 if let Err(err) = validate_parameter_batch(&req.batch, &info.params_schema) {
3583 return arrow_error(&state, StatusCode::BAD_REQUEST, &err, &req.request_id);
3584 }
3585
3586 let mut ctx = CallContext::with_auth_cookies(&server, &req, auth, cookies);
3587 if let Some(s) = sticky_sink.clone() {
3588 ctx.set_sticky(s);
3589 }
3590 let init_result =
3593 crate::server::call_guard(|| (info.stream.as_ref().unwrap())(&req, &ctx)).and_then(|r| r);
3594 let init_logs = ctx.drain_logs();
3595
3596 let sr = match init_result {
3597 Ok(s) => s,
3598 Err(err) => {
3599 let mut resp = arrow_response(
3600 StatusCode::OK,
3601 error_stream_bytes(&empty_schema(), &err, &server.server_id, &req.request_id),
3602 );
3603 stamp_rpc_error(&mut resp);
3604 return resp;
3605 }
3606 };
3607
3608 let StreamResult {
3609 output_schema,
3610 input_schema,
3611 state: mut ss,
3612 header,
3613 header_metadata,
3614 } = sr;
3615
3616 let mut body_buf = Vec::new();
3617
3618 if let Some(header_batch) = header.as_ref() {
3620 let hdr_schema = header_batch.schema();
3621 let mut hw = StreamWriter::new(&mut body_buf, hdr_schema.as_ref()).unwrap();
3622 for log in &init_logs {
3623 let md = build_log_metadata(log, &server.server_id, &req.request_id);
3624 let _ = hw.write(&empty_batch(hdr_schema.as_ref()).unwrap(), Some(&md));
3625 }
3626 let _ = hw.write(header_batch, header_metadata.as_ref());
3627 let _ = hw.finish();
3628 }
3629
3630 let is_producer = matches!(ss, StreamStateKind::Producer(_));
3631 let stream_id = new_session_id();
3632
3633 let mut finished = false;
3634 let mut wrote_error = false;
3637 {
3638 let mut sw = StreamWriter::new(&mut body_buf, output_schema.as_ref()).unwrap();
3639 if header.is_none() {
3640 for log in &init_logs {
3641 let md = build_log_metadata(log, &server.server_id, &req.request_id);
3642 let _ = sw.write(&empty_batch(output_schema.as_ref()).unwrap(), Some(&md));
3643 }
3644 }
3645 let _ = header_metadata;
3646 if is_producer {
3647 let turn = run_producer(
3651 &mut sw,
3652 &mut ss,
3653 &output_schema,
3654 &state,
3655 &method,
3656 &req,
3657 sticky_sink.as_ref(),
3658 Some(req.metadata.as_ref()),
3659 );
3660 finished = turn.finished;
3661 wrote_error |= turn.errored;
3662 }
3663 if !finished {
3664 match build_init_tokens(
3665 &state,
3666 &auth_for_token,
3667 &ss,
3668 &output_schema,
3669 input_schema.as_ref(),
3670 &stream_id,
3671 ) {
3672 Ok((token, call_token)) => {
3673 let md = Metadata::from([
3676 (STATE_KEY.to_string(), token),
3677 (CALL_STATE_KEY.to_string(), call_token),
3678 ]);
3679 let _ = sw.write(&empty_batch(output_schema.as_ref()).unwrap(), Some(&md));
3680 }
3681 Err(err) => {
3682 let md = build_error_metadata(&err, &server.server_id, &req.request_id);
3686 let _ = sw.write(&empty_batch(output_schema.as_ref()).unwrap(), Some(&md));
3687 wrote_error = true;
3688 }
3689 }
3690 }
3691 let _ = sw.finish();
3692 }
3693
3694 let mut resp = arrow_response(StatusCode::OK, body_buf);
3695 if wrote_error {
3696 stamp_rpc_error(&mut resp);
3699 }
3700 if let Some(s) = sticky_sink.as_ref() {
3701 stamp_session_headers(&mut resp, &state, s);
3702 }
3703 resp
3704}
3705
3706fn build_init_tokens(
3712 state: &Arc<HttpState>,
3713 auth: &crate::auth::AuthContext,
3714 ss: &StreamStateKind,
3715 output_schema: &SchemaRef,
3716 input_schema: Option<&SchemaRef>,
3717 stream_id: &str,
3718) -> Result<(String, String)> {
3719 let out_schema_bytes = write_schema_bytes(output_schema.as_ref())?;
3720 let in_schema_bytes = match input_schema {
3721 Some(s) => write_schema_bytes(s.as_ref())?,
3722 None => Vec::new(),
3723 };
3724 let mut call_id = [0u8; CALL_ID_LEN];
3725 rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut call_id);
3726
3727 let call_token = state.pack_call_token(
3728 auth,
3729 &call_id,
3730 &out_schema_bytes,
3731 &in_schema_bytes,
3732 stream_id,
3733 );
3734 let cursor = build_cursor_token(state, auth, ss, &call_id)?;
3735 Ok((cursor, call_token))
3736}
3737
3738fn build_cursor_token(
3744 state: &Arc<HttpState>,
3745 auth: &crate::auth::AuthContext,
3746 ss: &StreamStateKind,
3747 call_id: &[u8; CALL_ID_LEN],
3748) -> Result<String> {
3749 let state_bytes = crate::server::call_guard(|| match ss {
3750 StreamStateKind::Producer(p) => p.encode_state(),
3751 StreamStateKind::Exchange(e) => e.encode_state(),
3752 })
3753 .and_then(|result| result)?;
3754 Ok(state.pack_cursor_token(auth, &state_bytes, call_id))
3755}
3756
3757enum StreamEmit {
3760 Pointer(RecordBatch, Metadata),
3762 Inline,
3765 Failed(RpcError),
3767}
3768
3769fn externalize_stream_batch(
3791 state: &Arc<HttpState>,
3792 batch: &RecordBatch,
3793 output_schema: &SchemaRef,
3794 metadata: Option<&Metadata>,
3795 method: &str,
3796 cumulative_external: &mut usize,
3797) -> StreamEmit {
3798 let Some(cfg) = state.server.external_config() else {
3799 return StreamEmit::Inline;
3800 };
3801 tokio::task::block_in_place(|| {
3805 let prepared =
3810 match crate::external::prepare_externalize_batch(batch, output_schema, metadata, cfg) {
3811 Ok(Some(p)) => p,
3812 Ok(None) => return StreamEmit::Inline,
3813 Err(err) => return StreamEmit::Failed(err),
3814 };
3815 let cap_bytes = prepared.cap_bytes();
3816 if let Some(limit) = state.max_externalized_response_bytes {
3817 let projected = *cumulative_external + cap_bytes;
3818 if projected > limit {
3819 return StreamEmit::Failed(external_cap_error(projected, limit, method));
3820 }
3821 }
3822 match crate::external::upload_prepared(prepared, cfg) {
3823 Ok((ptr, md)) => {
3824 *cumulative_external += cap_bytes;
3828 StreamEmit::Pointer(ptr, md)
3829 }
3830 Err(err) => StreamEmit::Failed(err),
3831 }
3832 })
3833}
3834
3835struct ProducerTurn {
3842 finished: bool,
3844 errored: bool,
3846}
3847
3848const FRAMEWORK_TICK_METADATA_KEYS: [&str; 3] = [STATE_KEY, CALL_STATE_KEY, CANCEL_KEY];
3854
3855fn strip_framework_tick_metadata(md: &Metadata) -> Metadata {
3859 md.iter()
3860 .filter(|(k, _)| !FRAMEWORK_TICK_METADATA_KEYS.contains(&k.as_str()))
3861 .map(|(k, v)| (k.clone(), v.clone()))
3862 .collect()
3863}
3864
3865#[allow(clippy::too_many_arguments)]
3875fn run_producer<W: std::io::Write>(
3876 sw: &mut StreamWriter<W>,
3877 ss: &mut StreamStateKind,
3878 output_schema: &SchemaRef,
3879 state: &Arc<HttpState>,
3880 method: &str,
3881 req: &Request,
3882 sticky: Option<&Arc<crate::sticky::StickySinkImpl>>,
3883 tick_md: Option<&Metadata>,
3884) -> ProducerTurn {
3885 let server = &state.server;
3886 let mut ctx = CallContext::for_request(server, req);
3888 if let Some(s) = sticky {
3889 ctx.set_sticky(s.clone());
3890 }
3891 if let Some(md) = tick_md {
3892 ctx.set_tick_metadata(md.clone());
3893 }
3894 let producer = match ss {
3895 StreamStateKind::Producer(p) => p,
3896 StreamStateKind::Exchange(_) => unreachable!(),
3897 };
3898 let mut cumulative_external = 0usize;
3902 let mut out = OutputCollector::new(output_schema.clone(), true);
3903 let result =
3904 crate::server::call_guard(|| producer.produce(&mut out, &ctx)).and_then(|result| result);
3905 for log in ctx.drain_logs() {
3906 let md = build_log_metadata(&log, &server.server_id, &req.request_id);
3907 let _ = sw.write(&empty_batch(output_schema.as_ref()).unwrap(), Some(&md));
3908 }
3909 if let Err(err) = result {
3910 let md = build_error_metadata(&err, &server.server_id, &req.request_id);
3911 let _ = sw.write(&empty_batch(output_schema.as_ref()).unwrap(), Some(&md));
3912 return ProducerTurn {
3913 finished: true,
3914 errored: true,
3915 };
3916 }
3917 let finished = out.finished();
3918 let mut emitted_data = false;
3919 for item in out.items.drain(..) {
3920 match item {
3921 Emitted::Log(log) => {
3922 let md = build_log_metadata(&log, &server.server_id, &req.request_id);
3923 let _ = sw.write(&empty_batch(output_schema.as_ref()).unwrap(), Some(&md));
3924 }
3925 Emitted::Batch { batch, metadata } => {
3926 match externalize_stream_batch(
3927 state,
3928 &batch,
3929 output_schema,
3930 metadata.as_ref(),
3931 method,
3932 &mut cumulative_external,
3933 ) {
3934 StreamEmit::Pointer(ptr, md) => {
3935 let _ = sw.write(&ptr, Some(&md));
3936 }
3937 StreamEmit::Inline => {
3938 let _ = sw.write(&batch, metadata.as_ref());
3939 }
3940 StreamEmit::Failed(err) => {
3941 let md = build_error_metadata(&err, &server.server_id, &req.request_id);
3946 let _ = sw.write(&empty_batch(output_schema.as_ref()).unwrap(), Some(&md));
3947 return ProducerTurn {
3948 finished: true,
3949 errored: true,
3950 };
3951 }
3952 }
3953 emitted_data = true;
3954 }
3955 }
3956 }
3957 ProducerTurn {
3958 finished: finished || !emitted_data,
3960 errored: false,
3961 }
3962}
3963
3964async fn handle_stream_exchange(
3969 State(state): State<Arc<HttpState>>,
3970 Path(method): Path<String>,
3971 headers: HeaderMap,
3972 body: Bytes,
3973) -> Response {
3974 let auth = match authenticate_request(&state, &method, &headers) {
3975 Ok(a) => a,
3976 Err(resp) => return resp,
3977 };
3978 if !has_arrow_ct(&headers) {
3979 return plain_error(
3980 StatusCode::UNSUPPORTED_MEDIA_TYPE,
3981 "need arrow content type".into(),
3982 );
3983 }
3984 let sticky_sink = match sticky_for_request(&state, &auth, &headers) {
3985 Ok(s) => s,
3986 Err(resp) => return resp,
3987 };
3988
3989 let server = state.server.clone();
3990 let body = match maybe_decompress(&headers, &body, request_decode_limit(&state)) {
3991 Ok(b) => b,
3992 Err(e) => return request_decode_error(&state, &e),
3993 };
3994 let (mut batch, mut metadata) = match read_input_batch(&body) {
3996 Ok(x) => x,
3997 Err(e) => return arrow_error(&state, StatusCode::BAD_REQUEST, &e, ""),
3998 };
3999
4000 if md_get(&metadata, crate::metadata::LOCATION_KEY).is_some() {
4004 if let Some(cfg) = server.external_config().as_ref() {
4005 let resolved = tokio::task::block_in_place(|| {
4006 crate::external::resolve_external_location(&batch, &metadata, cfg)
4007 });
4008 match resolved {
4009 Ok((inner_batch, inner_metadata)) => {
4010 batch = inner_batch;
4011 metadata = inner_metadata;
4012 }
4013 Err(err) => {
4014 return cap_error_response(&Schema::empty(), &err, &server.server_id, "");
4015 }
4016 }
4017 }
4018 }
4019
4020 let Some(token) = md_get(&metadata, STATE_KEY).map(str::to_owned) else {
4021 let err = RpcError::runtime_error("Missing state token in exchange request");
4022 return arrow_error(&state, StatusCode::BAD_REQUEST, &err, "");
4023 };
4024 let cancelled = md_get(&metadata, CANCEL_KEY).is_some();
4025 let call_token = md_get(&metadata, CALL_STATE_KEY).map(str::to_owned);
4026
4027 let unpacked = match state.unpack_cursor_token(&auth, &token) {
4032 Ok(u) => u,
4033 Err(err) => return arrow_error(&state, StatusCode::BAD_REQUEST, &err, ""),
4034 };
4035 let call = match state.resolve_call(&auth, &unpacked, call_token.as_deref()) {
4036 Ok(c) => c,
4037 Err(err) => return arrow_error(&state, StatusCode::BAD_REQUEST, &err, ""),
4038 };
4039
4040 let output_schema = match read_schema_bytes(&call.output_schema_bytes) {
4042 Ok(s) => s,
4043 Err(err) => return arrow_error(&state, StatusCode::BAD_REQUEST, &err, ""),
4044 };
4045 let input_schema: Option<SchemaRef> = if call.input_schema_bytes.is_empty() {
4046 None
4047 } else {
4048 match read_schema_bytes(&call.input_schema_bytes) {
4049 Ok(s) => Some(s),
4050 Err(err) => return arrow_error(&state, StatusCode::BAD_REQUEST, &err, ""),
4051 }
4052 };
4053
4054 let Some(info) = server
4056 .method(&method)
4057 .filter(|m| m.method_type != MethodType::Unary)
4058 else {
4059 let err = RpcError::attribute_error(format!("Unknown stream method: '{}'", method));
4060 return arrow_error(&state, StatusCode::NOT_FOUND, &err, "");
4061 };
4062 let Some(decoder) = info.state_decoder.as_ref() else {
4063 let err = RpcError::runtime_error(format!(
4064 "Stream method '{method}' is registered without a state decoder; \
4065 it cannot serve HTTP continuation requests"
4066 ));
4067 return arrow_error(&state, StatusCode::INTERNAL_SERVER_ERROR, &err, "");
4068 };
4069 let mut ss = match crate::server::call_guard(|| decoder(&unpacked.state_bytes))
4070 .and_then(|result| result)
4071 {
4072 Ok(s) => s,
4073 Err(err) => return arrow_error(&state, StatusCode::BAD_REQUEST, &err, ""),
4074 };
4075
4076 let req = Request {
4077 method: method.clone(),
4078 request_id: md_get(&metadata, REQUEST_ID_KEY).unwrap_or("").to_string(),
4079 batch: empty_batch(&Schema::empty()).unwrap(),
4080 metadata: Arc::new(metadata.clone()),
4081 };
4082 let mut ctx = CallContext::for_request(&server, &req);
4083 if let Some(s) = sticky_sink.clone() {
4084 ctx.set_sticky(s);
4085 }
4086
4087 let mut body_buf = Vec::new();
4088
4089 if cancelled {
4090 let cancel_result = crate::server::call_guard(|| match &mut ss {
4091 StreamStateKind::Producer(p) => p.on_cancel(&ctx),
4092 StreamStateKind::Exchange(e) => e.on_cancel(&ctx),
4093 });
4094 {
4095 let mut sw = StreamWriter::new(&mut body_buf, output_schema.as_ref()).unwrap();
4096 for log in ctx.drain_logs() {
4097 let md = build_log_metadata(&log, &server.server_id, &req.request_id);
4098 let _ = sw.write(&empty_batch(output_schema.as_ref()).unwrap(), Some(&md));
4099 }
4100 if let Err(err) = &cancel_result {
4101 let md = build_error_metadata(err, &server.server_id, &req.request_id);
4102 let _ = sw.write(&empty_batch(output_schema.as_ref()).unwrap(), Some(&md));
4103 }
4104 let _ = sw.finish();
4105 }
4106 let mut resp = arrow_response(StatusCode::OK, body_buf);
4107 if cancel_result.is_err() {
4108 stamp_rpc_error(&mut resp);
4109 }
4110 if let Some(s) = sticky_sink.as_ref() {
4111 stamp_session_headers(&mut resp, &state, s);
4112 }
4113 return resp;
4114 }
4115
4116 if matches!(ss, StreamStateKind::Producer(_)) {
4117 let mut wrote_error = false;
4119 {
4120 let mut sw = StreamWriter::new(&mut body_buf, output_schema.as_ref()).unwrap();
4121 let continuation_md = strip_framework_tick_metadata(&metadata);
4132 let turn = run_producer(
4133 &mut sw,
4134 &mut ss,
4135 &output_schema,
4136 &state,
4137 &method,
4138 &req,
4139 sticky_sink.as_ref(),
4140 Some(&continuation_md),
4141 );
4142 wrote_error |= turn.errored;
4143 if !turn.finished {
4144 match build_cursor_token(&state, &auth, &ss, &unpacked.call_id) {
4145 Ok(new_token) => {
4146 let md = Metadata::from([(STATE_KEY.to_string(), new_token)]);
4147 let _ = sw.write(&empty_batch(output_schema.as_ref()).unwrap(), Some(&md));
4148 }
4149 Err(err) => {
4150 let md = build_error_metadata(&err, &server.server_id, &req.request_id);
4151 let _ = sw.write(&empty_batch(output_schema.as_ref()).unwrap(), Some(&md));
4152 wrote_error = true;
4153 }
4154 }
4155 }
4156 let _ = sw.finish();
4157 }
4158 let mut resp = arrow_response(StatusCode::OK, body_buf);
4159 if wrote_error {
4160 stamp_rpc_error(&mut resp);
4161 }
4162 if let Some(s) = sticky_sink.as_ref() {
4163 stamp_session_headers(&mut resp, &state, s);
4164 }
4165 return resp;
4166 }
4167
4168 let casted = match &input_schema {
4170 Some(exp) if batch.schema() != *exp => match cast_batch(&batch, exp) {
4171 Ok(b) => b,
4172 Err(e) => {
4173 let mut sw = StreamWriter::new(&mut body_buf, output_schema.as_ref()).unwrap();
4174 let md = build_error_metadata(&e, &server.server_id, &req.request_id);
4175 let _ = sw.write(&empty_batch(output_schema.as_ref()).unwrap(), Some(&md));
4176 let _ = sw.finish();
4177 drop(sw);
4178 let mut resp = arrow_response(StatusCode::OK, body_buf);
4179 stamp_rpc_error(&mut resp);
4180 return resp;
4181 }
4182 },
4183 _ => batch,
4184 };
4185
4186 ctx.set_tick_metadata(strip_framework_tick_metadata(&metadata));
4199
4200 let mut out = OutputCollector::new(output_schema.clone(), false);
4201 let res = crate::server::call_guard(|| match &mut ss {
4202 StreamStateKind::Exchange(e) => e.exchange(&casted, &mut out, &ctx),
4203 _ => unreachable!(),
4204 })
4205 .and_then(|result| result);
4206
4207 let mut wrote_error = false;
4208 {
4209 let mut sw = StreamWriter::new(&mut body_buf, output_schema.as_ref()).unwrap();
4210 for log in ctx.drain_logs() {
4211 let md = build_log_metadata(&log, &server.server_id, &req.request_id);
4212 let _ = sw.write(&empty_batch(output_schema.as_ref()).unwrap(), Some(&md));
4213 }
4214 if let Err(err) = res {
4215 let md = build_error_metadata(&err, &server.server_id, &req.request_id);
4216 let _ = sw.write(&empty_batch(output_schema.as_ref()).unwrap(), Some(&md));
4217 wrote_error = true;
4218 } else {
4219 let new_token = match build_cursor_token(&state, &auth, &ss, &unpacked.call_id) {
4220 Ok(t) => t,
4221 Err(err) => {
4222 let md = build_error_metadata(&err, &server.server_id, &req.request_id);
4223 let _ = sw.write(&empty_batch(output_schema.as_ref()).unwrap(), Some(&md));
4224 let _ = sw.finish();
4225 drop(sw);
4226 let mut resp = arrow_response(StatusCode::OK, body_buf);
4227 stamp_rpc_error(&mut resp);
4228 return resp;
4229 }
4230 };
4231 let mut wrote_data = false;
4232 let mut cumulative_external = 0usize;
4235 for item in out.items.drain(..) {
4236 match item {
4237 Emitted::Log(log) => {
4238 let md = build_log_metadata(&log, &server.server_id, &req.request_id);
4239 let _ = sw.write(&empty_batch(output_schema.as_ref()).unwrap(), Some(&md));
4240 }
4241 Emitted::Batch { batch, metadata } => {
4242 let mut md = metadata.unwrap_or_default();
4247 md.insert(STATE_KEY.to_string(), new_token.clone());
4248 match externalize_stream_batch(
4249 &state,
4250 &batch,
4251 &output_schema,
4252 Some(&md),
4253 &method,
4254 &mut cumulative_external,
4255 ) {
4256 StreamEmit::Pointer(ptr, ptr_md) => {
4257 let _ = sw.write(&ptr, Some(&ptr_md));
4258 }
4259 StreamEmit::Inline => {
4260 let _ = sw.write(&batch, Some(&md));
4261 }
4262 StreamEmit::Failed(err) => {
4263 let emd =
4264 build_error_metadata(&err, &server.server_id, &req.request_id);
4265 let _ = sw.write(
4266 &empty_batch(output_schema.as_ref()).unwrap(),
4267 Some(&emd),
4268 );
4269 wrote_error = true;
4270 break;
4271 }
4272 }
4273 wrote_data = true;
4274 }
4275 }
4276 }
4277 if !wrote_data && !wrote_error {
4281 let md = Metadata::from([(STATE_KEY.to_string(), new_token)]);
4282 let _ = sw.write(&empty_batch(output_schema.as_ref()).unwrap(), Some(&md));
4283 }
4284 }
4285 let _ = sw.finish();
4286 }
4287
4288 let mut resp = enforce_response_body_cap(
4289 &state,
4290 output_schema.as_ref(),
4291 body_buf,
4292 &method,
4293 &server.server_id,
4294 "",
4295 );
4296 if wrote_error {
4297 stamp_rpc_error(&mut resp);
4300 }
4301 if let Some(s) = sticky_sink.as_ref() {
4302 stamp_session_headers(&mut resp, &state, s);
4303 }
4304 resp
4305}
4306
4307fn read_input_batch(body: &[u8]) -> Result<(RecordBatch, Metadata)> {
4308 let mut r = StreamReader::new(body)?;
4309 let (batch, metadata) = r
4310 .read_next()?
4311 .ok_or_else(|| RpcError::runtime_error("no batch in exchange request"))?;
4312 r.drain()?;
4313 Ok((batch, metadata))
4314}
4315
4316#[cfg(test)]
4317mod tests {
4318 use super::*;
4319 use std::time::Duration;
4320
4321 fn state_with_key() -> Arc<HttpState> {
4322 use crate::server::RpcServer;
4323 let server = Arc::new(RpcServer::builder().server_id("test").build());
4324 HttpState::builder()
4325 .server(server)
4326 .token_key(&[7u8; 32])
4327 .token_ttl(Duration::from_millis(50))
4328 .max_body_size(1024)
4329 .build()
4330 }
4331
4332 const TEST_CALL_ID: [u8; CALL_ID_LEN] = [9u8; CALL_ID_LEN];
4333
4334 fn sample_schema_bytes() -> Vec<u8> {
4335 use arrow_schema::{DataType, Field, Schema};
4336 write_schema_bytes(&Schema::new(vec![Field::new("x", DataType::Int64, false)])).unwrap()
4337 }
4338
4339 #[test]
4351 fn token_payload_compresses_when_redundant() {
4352 let plaintext = b"vgi-rpc-state-".repeat(1000);
4353 let packed = pack_token_payload(&plaintext);
4354 assert_eq!(packed[0], TOKEN_CODEC_ZSTD, "expected the zstd codec tag");
4355 assert!(
4356 packed.len() < plaintext.len() / 4,
4357 "expected real compression on a redundant payload, got {} from {}",
4358 packed.len(),
4359 plaintext.len()
4360 );
4361 }
4362
4363 #[test]
4364 fn token_payload_stays_raw_when_incompressible() {
4365 let plaintext: Vec<u8> = (0u8..=255).collect();
4369 let packed = pack_token_payload(&plaintext);
4370 assert_eq!(packed[0], TOKEN_CODEC_RAW, "expected the raw codec tag");
4371 assert_eq!(
4372 packed.len(),
4373 plaintext.len() + 1,
4374 "raw payload must not grow beyond the tag byte"
4375 );
4376 }
4377
4378 #[test]
4379 fn token_payload_round_trips_under_either_codec() {
4380 let incompressible: Vec<u8> = (0u8..=255).collect();
4381 let cases: Vec<Vec<u8>> = vec![
4382 Vec::new(),
4383 b"x".to_vec(),
4384 incompressible,
4385 b"vgi-rpc-state-".repeat(500),
4386 ];
4387 for plaintext in cases {
4388 let packed = pack_token_payload(&plaintext);
4389 let got = unpack_token_payload(&packed).expect("round trip");
4390 assert_eq!(got, plaintext, "round trip changed the payload");
4391 }
4392 }
4393
4394 #[test]
4395 fn token_payload_rejects_malformed_input() {
4396 assert!(unpack_token_payload(&[]).is_err(), "empty payload");
4400 assert!(
4401 unpack_token_payload(&[0x7f, b'p', b'a', b'y']).is_err(),
4402 "unknown codec tag"
4403 );
4404 let mut corrupt = vec![TOKEN_CODEC_ZSTD];
4405 corrupt.extend_from_slice(b"not-a-zstd-frame");
4406 assert!(unpack_token_payload(&corrupt).is_err(), "corrupt zstd body");
4407 }
4408
4409 #[tokio::test]
4410 async fn sealed_token_shrinks_with_a_compressible_state() {
4411 let s = state_with_key();
4416 let auth = crate::auth::AuthContext::anonymous();
4417 let state_bytes = b"vgi-rpc-call-state-".repeat(400);
4418 let token = s.pack_cursor_token(&auth, &state_bytes, &TEST_CALL_ID);
4419 assert!(
4420 token.len() < state_bytes.len() / 4,
4421 "sealed token ({}B) should be far smaller than its state ({}B)",
4422 token.len(),
4423 state_bytes.len()
4424 );
4425
4426 let unpacked = s.unpack_cursor_token(&auth, &token).unwrap();
4427 assert_eq!(unpacked.state_bytes, state_bytes, "state survived the seal");
4428 }
4429
4430 #[tokio::test]
4431 async fn pack_unpack_roundtrip() {
4432 let s = state_with_key();
4433 let auth = crate::auth::AuthContext::anonymous();
4434 let state_bytes = b"state-payload";
4435 let out_sch = sample_schema_bytes();
4436 let in_sch = sample_schema_bytes();
4437 let token = s.pack_cursor_token(&auth, state_bytes, &TEST_CALL_ID);
4438 let unpacked = s.unpack_cursor_token(&auth, &token).unwrap();
4439 assert_eq!(unpacked.state_bytes, state_bytes);
4440 assert_eq!(unpacked.call_id, TEST_CALL_ID);
4441
4442 let call_token = s.pack_call_token(&auth, &TEST_CALL_ID, &out_sch, &in_sch, "sid-123");
4444 let call = s.resolve_call(&auth, &unpacked, Some(&call_token)).unwrap();
4445 assert_eq!(call.output_schema_bytes, out_sch);
4446 assert_eq!(call.input_schema_bytes, in_sch);
4447 assert_eq!(call.stream_id, "sid-123");
4448 }
4449
4450 #[tokio::test]
4451 async fn unpack_rejects_tampered_ciphertext() {
4452 let s = state_with_key();
4453 let auth = crate::auth::AuthContext::anonymous();
4454 let token = s.pack_cursor_token(&auth, b"s", &TEST_CALL_ID);
4455 let mut bytes = base64::engine::general_purpose::STANDARD
4456 .decode(token.as_bytes())
4457 .unwrap();
4458 let cipher_idx = 1 + 24;
4461 bytes[cipher_idx] ^= 0x01;
4462 let tampered = base64::engine::general_purpose::STANDARD.encode(bytes);
4463 assert!(s.unpack_cursor_token(&auth, &tampered).is_err());
4464 }
4465
4466 #[tokio::test]
4467 async fn unpack_rejects_tampered_nonce() {
4468 let s = state_with_key();
4469 let auth = crate::auth::AuthContext::anonymous();
4470 let token = s.pack_cursor_token(&auth, b"s", &TEST_CALL_ID);
4471 let mut bytes = base64::engine::general_purpose::STANDARD
4472 .decode(token.as_bytes())
4473 .unwrap();
4474 bytes[1] ^= 0x01;
4476 let tampered = base64::engine::general_purpose::STANDARD.encode(bytes);
4477 assert!(s.unpack_cursor_token(&auth, &tampered).is_err());
4478 }
4479
4480 #[tokio::test]
4481 async fn unpack_rejects_unknown_version() {
4482 let s = state_with_key();
4483 let auth = crate::auth::AuthContext::anonymous();
4484 let token = s.pack_cursor_token(&auth, b"s", &TEST_CALL_ID);
4485 let mut bytes = base64::engine::general_purpose::STANDARD
4486 .decode(token.as_bytes())
4487 .unwrap();
4488 bytes[0] = 0x99;
4489 let tampered = base64::engine::general_purpose::STANDARD.encode(bytes);
4490 let err = s.unpack_cursor_token(&auth, &tampered).unwrap_err();
4491 assert!(err.message.contains("signature verification failed"));
4494 }
4495
4496 #[tokio::test]
4497 async fn unpack_rejects_malformed_base64() {
4498 let s = state_with_key();
4499 let auth = crate::auth::AuthContext::anonymous();
4500 let err = s.unpack_cursor_token(&auth, "not!base64!").unwrap_err();
4501 assert!(err.message.contains("Malformed"));
4502 }
4503
4504 #[tokio::test]
4505 async fn unpack_rejects_different_key() {
4506 use crate::server::RpcServer;
4507 let server = Arc::new(RpcServer::builder().server_id("t").build());
4508 let a = HttpState::builder()
4509 .server(server.clone())
4510 .token_key(&[1u8; 32])
4511 .build();
4512 let b = HttpState::builder()
4513 .server(server)
4514 .token_key(&[2u8; 32])
4515 .build();
4516 let auth = crate::auth::AuthContext::anonymous();
4517 let tok = a.pack_cursor_token(&auth, b"s", &TEST_CALL_ID);
4518 assert!(b.unpack_cursor_token(&auth, &tok).is_err());
4519 }
4520
4521 #[tokio::test]
4522 async fn unpack_rejects_expired_token() {
4523 let s = state_with_key(); let auth = crate::auth::AuthContext::anonymous();
4525 let aad = compute_aad(&auth);
4528 let stale = pack_cursor_token(&[7u8; 32], &aad, b"s", &TEST_CALL_ID, 0);
4529 let err = s.unpack_cursor_token(&auth, &stale).unwrap_err();
4530 assert!(err.message.contains("expired"), "got: {}", err.message);
4531 }
4532
4533 #[tokio::test]
4534 async fn unpack_rejects_different_principal() {
4535 let s = state_with_key();
4536 let alice = crate::auth::AuthContext::for_principal("bearer", "alice");
4537 let bob = crate::auth::AuthContext::for_principal("bearer", "bob");
4538 let tok = s.pack_cursor_token(&alice, b"s", &TEST_CALL_ID);
4539 assert!(s.unpack_cursor_token(&alice, &tok).is_ok());
4540 assert!(s.unpack_cursor_token(&bob, &tok).is_err());
4541 let anon = crate::auth::AuthContext::anonymous();
4542 assert!(s.unpack_cursor_token(&anon, &tok).is_err());
4543 }
4544
4545 #[tokio::test]
4546 async fn unpack_rejects_authenticated_replay_of_anonymous_token() {
4547 let s = state_with_key();
4548 let anon = crate::auth::AuthContext::anonymous();
4549 let alice = crate::auth::AuthContext::for_principal("bearer", "alice");
4550 let tok = s.pack_cursor_token(&anon, b"s", &TEST_CALL_ID);
4551 assert!(s.unpack_cursor_token(&alice, &tok).is_err());
4552 }
4553
4554 #[tokio::test]
4555 async fn unpack_rejects_cross_domain_replay() {
4556 let s = state_with_key();
4557 let bearer_alice = crate::auth::AuthContext::for_principal("bearer", "alice");
4558 let mtls_alice = crate::auth::AuthContext::for_principal("mtls", "alice");
4559 let tok = s.pack_cursor_token(&bearer_alice, b"s", &TEST_CALL_ID);
4560 assert!(s.unpack_cursor_token(&mtls_alice, &tok).is_err());
4561 }
4562
4563 #[tokio::test]
4564 async fn decompress_rejects_oversize() {
4565 let hdr = HeaderMap::new();
4566 let body = Bytes::from(vec![0u8; 1025]);
4567 let err = super::maybe_decompress(&hdr, &body, 1024).unwrap_err();
4568 assert!(err.message.contains("exceeds max size"));
4569 }
4570
4571 #[test]
4572 fn zstd_bounded_rejects_zip_bomb_without_full_alloc() {
4573 let huge = vec![0u8; 8 * 1024 * 1024];
4577 let compressed = zstd::encode_all(huge.as_slice(), 1).unwrap();
4578 assert!(compressed.len() < 100_000, "compressed should be tiny");
4579 let err =
4580 super::decode_content_encoding(&compressed, Some("zstd"), Some(64 * 1024)).unwrap_err();
4581 assert!(
4582 err.message.contains("exceeds max size"),
4583 "expected oversize error, got: {}",
4584 err.message
4585 );
4586 }
4587
4588 #[test]
4589 fn zstd_bounded_passes_small_payload() {
4590 let small = b"hello-world".repeat(10);
4591 let compressed = zstd::encode_all(small.as_slice(), 1).unwrap();
4592 let out = super::decode_content_encoding(&compressed, Some("zstd"), Some(1024)).unwrap();
4593 assert_eq!(out, small);
4594 }
4595
4596 #[test]
4597 fn zstd_bounded_rejects_large_window_for_small_output() {
4598 use std::io::Write;
4599
4600 let mut encoder = zstd::stream::Encoder::new(Vec::new(), 1).unwrap();
4601 encoder.window_log(20).unwrap();
4602 encoder.include_contentsize(false).unwrap();
4603 encoder.write_all(b"tiny").unwrap();
4604 let frame = encoder.finish().unwrap();
4605
4606 let err = super::decode_content_encoding(&frame, Some("zstd"), Some(1024))
4607 .expect_err("a 1 MiB history window must exceed the 512 KiB decoder floor");
4608 assert!(
4609 err.message.contains("window") || err.message.contains("memory"),
4610 "expected window-limit error, got: {}",
4611 err.message
4612 );
4613 }
4614
4615 fn gzip(data: &[u8]) -> Vec<u8> {
4616 use std::io::Write;
4617 let mut enc = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
4618 enc.write_all(data).unwrap();
4619 enc.finish().unwrap()
4620 }
4621
4622 #[test]
4623 fn decode_content_encoding_handles_gzip() {
4624 let payload = b"hello-gzip".repeat(10);
4625 let out = super::decode_content_encoding(&gzip(&payload), Some("gzip"), None).unwrap();
4626 assert_eq!(out, payload);
4627 }
4628
4629 #[test]
4630 fn decode_content_encoding_bounds_gzip_zip_bombs() {
4631 let huge = vec![0u8; 8 * 1024 * 1024];
4632 let compressed = gzip(&huge);
4633 assert!(compressed.len() < 100_000, "compressed should be tiny");
4634 let err =
4635 super::decode_content_encoding(&compressed, Some("gzip"), Some(64 * 1024)).unwrap_err();
4636 assert!(err.message.contains("exceeds max size"));
4637 }
4638
4639 #[test]
4640 fn decode_content_encoding_applies_a_coding_list_in_reverse() {
4641 let payload = b"layered".repeat(20);
4643 let body = zstd::encode_all(gzip(&payload).as_slice(), 1).unwrap();
4644 let out = super::decode_content_encoding(&body, Some("gzip, zstd"), None).unwrap();
4645 assert_eq!(out, payload);
4646 }
4647
4648 #[test]
4649 fn decode_content_encoding_refuses_an_absurd_coding_chain() {
4650 let chain = std::iter::repeat_n("zstd", super::MAX_CONTENT_CODINGS + 1)
4651 .collect::<Vec<_>>()
4652 .join(", ");
4653 let err = super::decode_content_encoding(b"x", Some(&chain), Some(1024)).unwrap_err();
4654 assert!(err.message.contains("more than"), "got: {}", err.message);
4655 }
4656
4657 #[test]
4658 fn decode_content_encoding_allows_a_chain_up_to_the_cap() {
4659 let payload = b"bounded".repeat(8);
4661 let body = zstd::encode_all(gzip(&gzip(&payload)).as_slice(), 1).unwrap();
4662 let out = super::decode_content_encoding(&body, Some("gzip, gzip, zstd"), Some(64 * 1024))
4663 .unwrap();
4664 assert_eq!(out, payload);
4665 }
4666
4667 #[test]
4668 fn decode_content_encoding_passes_through_identity_and_unknown() {
4669 let raw = b"plain bytes";
4670 for header in [None, Some(""), Some("identity"), Some("br")] {
4671 let out = super::decode_content_encoding(raw, header, None).unwrap();
4672 assert_eq!(out, raw, "header {header:?} should pass through");
4673 }
4674 }
4675
4676 fn headers(pairs: &[(&str, &str)]) -> HeaderMap {
4684 let mut h = HeaderMap::new();
4685 for (k, v) in pairs {
4686 h.insert(
4687 axum::http::HeaderName::from_bytes(k.as_bytes()).unwrap(),
4688 HeaderValue::from_str(v).unwrap(),
4689 );
4690 }
4691 h
4692 }
4693
4694 #[test]
4695 fn parse_encoding_list_is_ordered_deduped_and_ignores_q_values() {
4696 use super::ResponseEncoding::*;
4697 assert_eq!(super::parse_encoding_list(None), vec![]);
4698 assert_eq!(super::parse_encoding_list(Some("")), vec![]);
4699 assert_eq!(super::parse_encoding_list(Some(" , ,")), vec![]);
4700 assert_eq!(
4702 super::parse_encoding_list(Some("deflate, gzip, br, zstd")),
4703 vec![Gzip, Zstd]
4704 );
4705 assert_eq!(
4707 super::parse_encoding_list(Some("zstd;q=0.5, gzip")),
4708 vec![Zstd, Gzip]
4709 );
4710 assert_eq!(
4712 super::parse_encoding_list(Some(" ZSTD , gzip ,zstd")),
4713 vec![Zstd, Gzip]
4714 );
4715 assert_eq!(
4717 super::parse_encoding_list(Some("identity, zstd")),
4718 vec![Identity, Zstd]
4719 );
4720 assert_eq!(
4723 super::parse_encoding_list(Some("identity;q=0, zstd")),
4724 vec![Identity, Zstd]
4725 );
4726 }
4727
4728 #[test]
4729 fn custom_header_alone_negotiates_and_stamps_the_custom_response_header() {
4730 let h = headers(&[("x-vgi-accept-encoding", "zstd, gzip")]);
4734 let (enc, used_custom) = super::pick_response_encoding(&h, Some(3)).unwrap();
4735 assert_eq!(enc, super::ResponseEncoding::Zstd);
4736 assert!(
4737 used_custom,
4738 "zstd came only from the custom header, so the custom response \
4739 header must carry it"
4740 );
4741 }
4742
4743 #[test]
4744 fn custom_header_wins_over_a_gzip_first_standard_header() {
4745 let h = headers(&[
4750 ("accept-encoding", "deflate, gzip, br, zstd"),
4751 ("x-vgi-accept-encoding", "zstd, gzip"),
4752 ]);
4753 let (enc, used_custom) = super::pick_response_encoding(&h, Some(3)).unwrap();
4754 assert_eq!(enc, super::ResponseEncoding::Zstd);
4755 assert!(
4756 !used_custom,
4757 "zstd is in the standard header too, so stamp Content-Encoding"
4758 );
4759 }
4760
4761 #[test]
4762 fn no_encoding_headers_means_uncompressed() {
4763 assert!(super::pick_response_encoding(&HeaderMap::new(), Some(3)).is_none());
4764 }
4765
4766 #[test]
4767 fn an_unknown_codec_means_uncompressed() {
4768 for pairs in [
4769 &[("accept-encoding", "br, deflate")][..],
4770 &[("x-vgi-accept-encoding", "br")][..],
4771 ] {
4772 let h = headers(pairs);
4773 assert!(
4774 super::pick_response_encoding(&h, Some(3)).is_none(),
4775 "unexpected codec chosen for {pairs:?}"
4776 );
4777 }
4778 }
4779
4780 #[test]
4781 fn compression_disabled_means_uncompressed_whatever_the_client_offers() {
4782 let h = headers(&[
4785 ("accept-encoding", "zstd"),
4786 ("x-vgi-accept-encoding", "zstd"),
4787 ]);
4788 assert!(!matches!(
4789 super::pick_response_encoding(&h, None),
4790 Some((super::ResponseEncoding::Zstd, _))
4791 ));
4792 }
4793
4794 #[test]
4795 fn standard_header_alone_still_negotiates_on_the_standard_response_header() {
4796 let h = headers(&[("accept-encoding", "zstd, identity")]);
4797 let (enc, used_custom) = super::pick_response_encoding(&h, Some(3)).unwrap();
4798 assert_eq!(enc, super::ResponseEncoding::Zstd);
4799 assert!(!used_custom);
4800 }
4801
4802 #[test]
4803 fn gzip_only_offer_negotiates_and_round_trips() {
4804 use std::io::Read;
4805
4806 let h = headers(&[("accept-encoding", "gzip")]);
4807 let (enc, used_custom) = super::pick_response_encoding(&h, Some(3)).unwrap();
4808 assert_eq!(enc, super::ResponseEncoding::Gzip);
4809 assert!(!used_custom);
4810
4811 let body = b"gzip-response".repeat(1024);
4812 let encoded = super::encode_response_body(enc, &body, 3).unwrap();
4813 assert!(encoded.len() < body.len());
4814 let mut decoded = Vec::new();
4815 flate2::read::GzDecoder::new(encoded.as_slice())
4816 .read_to_end(&mut decoded)
4817 .unwrap();
4818 assert_eq!(decoded, body);
4819 }
4820
4821 #[test]
4822 fn q_values_do_not_reorder_the_walk() {
4823 let h = headers(&[("x-vgi-accept-encoding", "zstd;q=0.5, gzip")]);
4826 let (enc, _) = super::pick_response_encoding(&h, Some(3)).unwrap();
4827 assert_eq!(enc, super::ResponseEncoding::Zstd);
4828 }
4829
4830 #[test]
4831 fn identity_first_wins_and_suppresses_compression() {
4832 for pairs in [
4837 &[("x-vgi-accept-encoding", "identity")][..],
4838 &[
4839 ("x-vgi-accept-encoding", "identity"),
4840 ("accept-encoding", "gzip, zstd"),
4841 ][..],
4842 &[("x-vgi-accept-encoding", "identity, zstd")][..],
4843 &[("accept-encoding", "identity, gzip")][..],
4844 ] {
4845 let h = headers(pairs);
4846 let (enc, _) = super::pick_response_encoding(&h, Some(3)).unwrap();
4847 assert_eq!(
4848 enc,
4849 super::ResponseEncoding::Identity,
4850 "identity should win for {pairs:?}"
4851 );
4852 assert!(
4853 super::encode_response_body(enc, &vec![0u8; 4096], 3).is_none(),
4854 "identity must never produce a compressed body"
4855 );
4856 }
4857 }
4858
4859 #[test]
4860 fn identity_after_a_producible_codec_does_not_win() {
4861 let h = headers(&[("x-vgi-accept-encoding", "zstd, identity")]);
4862 let (enc, used_custom) = super::pick_response_encoding(&h, Some(3)).unwrap();
4863 assert_eq!(enc, super::ResponseEncoding::Zstd);
4864 assert!(used_custom);
4865 let h = headers(&[("x-vgi-accept-encoding", "gzip, identity, zstd")]);
4867 let (enc, _) = super::pick_response_encoding(&h, Some(3)).unwrap();
4868 assert_eq!(enc, super::ResponseEncoding::Gzip);
4869 }
4870
4871 #[test]
4872 fn supported_encodings_is_the_both_directions_intersection() {
4873 assert_eq!(
4874 super::supported_encodings_header_value(Some(3)),
4875 "zstd, gzip"
4876 );
4877 assert_eq!(super::supported_encodings_header_value(None), "");
4880 assert!(!super::supported_encodings_header_value(Some(3)).contains("identity"));
4882 }
4883
4884 #[test]
4885 fn decode_hex_key_roundtrip() {
4886 let key =
4887 decode_hex_key("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
4888 .unwrap();
4889 assert_eq!(key.len(), 32);
4890 assert_eq!(key[0], 0x00);
4891 assert_eq!(key[31], 0x1f);
4892 }
4893
4894 #[test]
4895 fn decode_hex_key_rejects_short() {
4896 assert!(decode_hex_key("deadbeef").is_err());
4897 }
4898
4899 #[test]
4900 fn decode_hex_key_rejects_bad_char() {
4901 assert!(decode_hex_key(&"zz".repeat(32)).is_err());
4902 }
4903
4904 #[test]
4905 fn decode_base64_key_accepts_padded() {
4906 let s = base64::engine::general_purpose::STANDARD.encode([7u8; 32]);
4907 let out = decode_base64_key(&s).unwrap();
4908 assert_eq!(out, vec![7u8; 32]);
4909 }
4910
4911 #[test]
4912 fn decode_base64_key_accepts_unpadded() {
4913 let s = base64::engine::general_purpose::STANDARD
4914 .encode([7u8; 32])
4915 .trim_end_matches('=')
4916 .to_string();
4917 let out = decode_base64_key(&s).unwrap();
4918 assert_eq!(out, vec![7u8; 32]);
4919 }
4920
4921 #[test]
4922 fn decode_base64_key_rejects_short() {
4923 let s = base64::engine::general_purpose::STANDARD.encode(b"short");
4924 assert!(decode_base64_key(&s).is_err());
4925 }
4926
4927 #[tokio::test]
4928 async fn token_key_hex_round_trips_through_token() {
4929 use crate::server::RpcServer;
4930 let server = Arc::new(RpcServer::builder().server_id("t").build());
4931 let a = HttpState::builder()
4932 .server(server.clone())
4933 .token_key_hex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
4934 .build();
4935 let b = HttpState::builder()
4936 .server(server)
4937 .token_key_hex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
4938 .build();
4939 let auth = crate::auth::AuthContext::anonymous();
4940 let tok = a.pack_cursor_token(&auth, b"s", &TEST_CALL_ID);
4941 assert_eq!(
4942 b.unpack_cursor_token(&auth, &tok).unwrap().call_id,
4943 TEST_CALL_ID
4944 );
4945 }
4946
4947 #[test]
4948 fn response_buffer_ceiling_never_below_hard_cap() {
4949 use crate::server::RpcServer;
4950 let mk = |soft: Option<usize>| {
4951 let server = Arc::new(RpcServer::builder().server_id("t").build());
4952 let mut b = HttpState::builder().server(server).token_key(&[9u8; 32]);
4953 if let Some(n) = soft {
4954 b = b.max_response_bytes(n);
4955 }
4956 b.build()
4957 };
4958 assert_eq!(
4960 response_buffer_ceiling(&mk(None)),
4961 MAX_RESPONSE_BYTES_HARD_CAP
4962 );
4963 assert_eq!(
4967 response_buffer_ceiling(&mk(Some(8))),
4968 MAX_RESPONSE_BYTES_HARD_CAP
4969 );
4970 let big = MAX_RESPONSE_BYTES_HARD_CAP;
4972 assert_eq!(response_buffer_ceiling(&mk(Some(big))), big * 2);
4973 }
4974}