1use std::{collections::HashMap, path::Path, sync::Arc, thread::JoinHandle};
2
3use armour_rpc::RpcError;
4use armour_rpc::protocol::{
5 OpCode, Request, RequestPayload, read_bound, read_bytes, read_u8, read_u32_be, read_u64_be,
6 read_upsert_key,
7};
8use compio::buf::{IoBuf, IoBufMut};
9use compio::io::framed::codec::{Decoder, Encoder};
10use compio::{
11 io::{
12 AsyncRead, AsyncWrite,
13 framed::{Framed, frame::LengthDelimited},
14 },
15 net::{TcpListener, ToSocketAddrsAsync, UnixListener},
16 runtime::RuntimeBuilder,
17};
18use futures_util::{
19 SinkExt, StreamExt,
20 future::{Either, select},
21 pin_mut,
22};
23
24use super::db::Db;
25use super::handler::RpcHandler;
26
27#[derive(Debug)]
30enum Response {
31 Ok(ResponsePayload),
32 Err { code: u16, message: String },
33}
34
35#[derive(Debug)]
36pub(crate) struct CollectionMeta {
37 pub name: String,
38 pub hashname: u64,
39 pub typ_hash: u64,
40 pub version: u16,
41 pub count: u64,
42}
43
44#[derive(Debug)]
45enum ResponsePayload {
46 Empty,
47 Bool(bool),
48 OptionalData(Option<Vec<u8>>),
49 OptionalLen(Option<u32>),
50 OptionalKV(Option<(Vec<u8>, Vec<u8>)>),
51 KeyValues(Vec<(Vec<u8>, Vec<u8>)>),
52 Keys(Vec<Vec<u8>>),
53 Key(Vec<u8>),
54 Count(u64),
55 Collections(Vec<CollectionMeta>),
56 SchemaBytes(Vec<u8>),
57}
58
59struct RpcCodec;
62
63impl<B: IoBuf> Decoder<Request, B> for RpcCodec {
64 type Error = RpcError;
65
66 fn decode(&mut self, buf: &compio::buf::Slice<B>) -> Result<Request, Self::Error> {
67 let bytes: &[u8] = buf;
68 let mut pos = 0;
69 let op_byte = read_u8(bytes, &mut pos)?;
70 let op = OpCode::from_repr(op_byte)
71 .ok_or_else(|| RpcError::Protocol("unknown opcode".to_string()))?;
72 let hashname = read_u64_be(bytes, &mut pos)?;
73
74 let payload = match op {
75 OpCode::Get | OpCode::Contains => {
76 let key = read_bytes(bytes, &mut pos)?;
77 RequestPayload::Key(key)
78 }
79 OpCode::First | OpCode::Last | OpCode::ListCollections | OpCode::GetSchema => {
80 RequestPayload::Empty
81 }
82 OpCode::Count => {
83 let exact = read_u8(bytes, &mut pos)? != 0;
84 RequestPayload::Count { exact }
85 }
86 OpCode::EntryLen => {
87 let key = read_bytes(bytes, &mut pos)?;
88 RequestPayload::EntryLen { key }
89 }
90 OpCode::Range | OpCode::RangeKeys => {
91 let start = read_bound(bytes, &mut pos)?;
92 let end = read_bound(bytes, &mut pos)?;
93 let limit = read_u32_be(bytes, &mut pos)?;
94 RequestPayload::Range { start, end, limit }
95 }
96 OpCode::Upsert => {
97 let key = read_upsert_key(bytes, &mut pos)?;
98 let flag_byte = read_u8(bytes, &mut pos)?;
99 let flag = match flag_byte {
100 0 => None,
101 1 => Some(true),
102 2 => Some(false),
103 _ => {
104 return Err(RpcError::Protocol("invalid upsert flag".to_string()));
105 }
106 };
107 let value = read_bytes(bytes, &mut pos)?;
108 RequestPayload::Upsert { key, flag, value }
109 }
110 OpCode::Remove => {
111 let key = read_bytes(bytes, &mut pos)?;
112 let soft = read_u8(bytes, &mut pos)? != 0;
113 RequestPayload::Remove { key, soft }
114 }
115 OpCode::Take => {
116 let key = read_bytes(bytes, &mut pos)?;
117 let soft = read_u8(bytes, &mut pos)? != 0;
118 RequestPayload::Take { key, soft }
119 }
120 OpCode::ApplyBatch => {
121 let count = read_u32_be(bytes, &mut pos)? as usize;
122 const MAX_PREALLOC: usize = 64 * 1024;
123 let mut items = Vec::with_capacity(count.min(MAX_PREALLOC));
124 for _ in 0..count {
125 let key = read_bytes(bytes, &mut pos)?;
126 let has_val = read_u8(bytes, &mut pos)?;
127 let val = match has_val {
128 0 => None,
129 1 => Some(read_bytes(bytes, &mut pos)?),
130 _ => {
131 return Err(RpcError::Protocol("invalid batch value tag".to_string()));
132 }
133 };
134 items.push((key, val));
135 }
136 RequestPayload::Batch(items)
137 }
138 };
139
140 Ok(Request {
141 op,
142 hashname,
143 payload,
144 })
145 }
146}
147
148impl<B: IoBufMut> Encoder<Response, B> for RpcCodec {
149 type Error = RpcError;
150
151 fn encode(&mut self, item: Response, buf: &mut B) -> Result<(), Self::Error> {
152 let mut tmp = Vec::new();
153 match item {
154 Response::Ok(payload) => {
155 tmp.push(0x00);
156 encode_ok_payload(&mut tmp, payload);
157 }
158 Response::Err { code, message } => encode_err_frame(&mut tmp, code, &message),
159 }
160 buf.extend_from_slice(&tmp)
161 .map_err(|e| RpcError::Other(e.to_string()))?;
162 Ok(())
163 }
164}
165
166fn encode_err_frame(buf: &mut Vec<u8>, code: u16, message: &str) {
173 buf.push(0x01);
174 buf.extend_from_slice(&code.to_be_bytes());
175 let max = message.len().min(u16::MAX as usize);
176 let end = message.floor_char_boundary(max);
177 let msg = &message.as_bytes()[..end];
178 buf.extend_from_slice(&(msg.len() as u16).to_be_bytes());
179 buf.extend_from_slice(msg);
180}
181
182fn err_response(code: u16, original: &str) -> Response {
203 if code == 501 {
204 tracing::warn!("RPC unsupported request ({code}): {original}");
205 Response::Err {
206 code,
207 message: original.into(),
208 }
209 } else if code >= 500 {
210 tracing::error!("RPC server error ({code}): {original}");
211 Response::Err {
212 code,
213 message: "internal error".into(),
214 }
215 } else {
216 Response::Err {
217 code,
218 message: original.into(),
219 }
220 }
221}
222
223fn encode_ok_payload(buf: &mut Vec<u8>, payload: ResponsePayload) {
224 match payload {
225 ResponsePayload::Empty => {}
226 ResponsePayload::Bool(b) => {
227 buf.push(if b { 1 } else { 0 });
228 }
229 ResponsePayload::OptionalData(opt) => match opt {
230 None => buf.push(0),
231 Some(data) => {
232 buf.push(1);
233 buf.extend_from_slice(&(data.len() as u32).to_be_bytes());
234 buf.extend_from_slice(&data);
235 }
236 },
237 ResponsePayload::OptionalLen(opt) => match opt {
238 None => buf.push(0),
239 Some(len) => {
240 buf.push(1);
241 buf.extend_from_slice(&len.to_be_bytes());
242 }
243 },
244 ResponsePayload::OptionalKV(opt) => match opt {
245 None => buf.push(0),
246 Some((key, val)) => {
247 buf.push(1);
248 buf.extend_from_slice(&(key.len() as u32).to_be_bytes());
249 buf.extend_from_slice(&key);
250 buf.extend_from_slice(&(val.len() as u32).to_be_bytes());
251 buf.extend_from_slice(&val);
252 }
253 },
254 ResponsePayload::KeyValues(pairs) => {
255 buf.extend_from_slice(&(pairs.len() as u32).to_be_bytes());
256 for (key, val) in pairs {
257 buf.extend_from_slice(&(key.len() as u32).to_be_bytes());
258 buf.extend_from_slice(&key);
259 buf.extend_from_slice(&(val.len() as u32).to_be_bytes());
260 buf.extend_from_slice(&val);
261 }
262 }
263 ResponsePayload::Keys(keys) => {
264 buf.extend_from_slice(&(keys.len() as u32).to_be_bytes());
265 for key in keys {
266 buf.extend_from_slice(&(key.len() as u32).to_be_bytes());
267 buf.extend_from_slice(&key);
268 }
269 }
270 ResponsePayload::Key(key) => {
271 buf.extend_from_slice(&(key.len() as u32).to_be_bytes());
272 buf.extend_from_slice(&key);
273 }
274 ResponsePayload::Count(n) => {
275 buf.extend_from_slice(&n.to_be_bytes());
276 }
277 ResponsePayload::SchemaBytes(bytes) => {
278 buf.extend_from_slice(&bytes);
279 }
280 ResponsePayload::Collections(collections) => {
281 buf.extend_from_slice(&(collections.len() as u32).to_be_bytes());
282 for c in collections {
283 let name = c.name.as_bytes();
284 buf.extend_from_slice(&(name.len() as u32).to_be_bytes());
285 buf.extend_from_slice(name);
286 buf.extend_from_slice(&(name.len() as u32).to_be_bytes());
288 buf.extend_from_slice(name);
289 buf.extend_from_slice(&c.hashname.to_be_bytes());
290 buf.extend_from_slice(&c.typ_hash.to_be_bytes());
291 buf.extend_from_slice(&c.version.to_be_bytes());
292 buf.extend_from_slice(&c.count.to_be_bytes());
293 }
294 }
295 }
296}
297
298pub type TreeMap = Arc<HashMap<u64, Arc<dyn RpcHandler>>>;
301
302const ACCEPT_BACKOFF_MIN: std::time::Duration = std::time::Duration::from_millis(5);
306const ACCEPT_BACKOFF_MAX: std::time::Duration = std::time::Duration::from_millis(500);
307
308async fn handle_connection<R, W>(
309 reader: R,
310 writer: W,
311 trees: TreeMap,
312 mut stop_rx: async_broadcast::Receiver<()>,
313) where
314 R: AsyncRead + Unpin + 'static,
315 W: AsyncWrite + Unpin + 'static,
316{
317 let mut framed = Framed::new::<Response, Request>(RpcCodec, LengthDelimited::new())
318 .with_reader(reader)
319 .with_writer(writer);
320
321 loop {
322 let next_request = framed.next();
323 let stop_signal = stop_rx.recv();
324 pin_mut!(next_request);
325 pin_mut!(stop_signal);
326
327 match select(next_request, stop_signal).await {
328 Either::Left((Some(result), _)) => {
329 let response = match result {
330 Ok(request) => dispatch(&trees, request),
331 Err(e) => err_response(400, &e.to_string()),
337 };
338 if framed.send(response).await.is_err() {
339 break;
340 }
341 }
342 Either::Left((None, _)) => break,
343 Either::Right(_) => break,
344 }
345 }
346}
347
348fn dispatch(trees: &TreeMap, request: Request) -> Response {
349 if request.op == OpCode::ListCollections {
350 return list_collections(trees);
351 }
352 if request.op == OpCode::GetSchema {
353 return get_schema(trees, request.hashname);
354 }
355
356 let handler = match trees.get(&request.hashname) {
357 Some(h) => h,
358 None => {
359 return Response::Err {
360 code: 404,
361 message: format!("tree not found: {:#X}", request.hashname),
362 };
363 }
364 };
365
366 let result = match (request.op, request.payload) {
367 (OpCode::Get, RequestPayload::Key(key)) => handler
368 .get(&key)
369 .map(|v| Response::Ok(ResponsePayload::OptionalData(v))),
370 (OpCode::Contains, RequestPayload::Key(key)) => handler
371 .contains(&key)
372 .map(|b| Response::Ok(ResponsePayload::Bool(b))),
373 (OpCode::First, RequestPayload::Empty) => handler
374 .first()
375 .map(|v| Response::Ok(ResponsePayload::OptionalKV(v))),
376 (OpCode::Last, RequestPayload::Empty) => handler
377 .last()
378 .map(|v| Response::Ok(ResponsePayload::OptionalKV(v))),
379 (OpCode::Range, RequestPayload::Range { start, end, limit }) => handler
380 .range(start, end, limit)
381 .map(|v| Response::Ok(ResponsePayload::KeyValues(v))),
382 (OpCode::RangeKeys, RequestPayload::Range { start, end, limit }) => handler
383 .range_keys(start, end, limit)
384 .map(|v| Response::Ok(ResponsePayload::Keys(v))),
385 (OpCode::Count, RequestPayload::Count { exact }) => handler
386 .count(exact)
387 .map(|n| Response::Ok(ResponsePayload::Count(n))),
388 (OpCode::Upsert, RequestPayload::Upsert { key, flag, value }) => handler
389 .upsert(key, flag, value)
390 .map(|k| Response::Ok(ResponsePayload::Key(k))),
391 (OpCode::Remove, RequestPayload::Remove { key, soft }) => handler
392 .remove(&key, soft)
393 .map(|_| Response::Ok(ResponsePayload::Empty)),
394 (OpCode::Take, RequestPayload::Take { key, soft }) => handler
395 .take(&key, soft)
396 .map(|v| Response::Ok(ResponsePayload::OptionalData(v))),
397 (OpCode::EntryLen, RequestPayload::EntryLen { key }) => handler
398 .entry_len(&key)
399 .map(|v| Response::Ok(ResponsePayload::OptionalLen(v))),
400 (OpCode::ApplyBatch, RequestPayload::Batch(items)) => handler
401 .apply_batch(items)
402 .map(|_| Response::Ok(ResponsePayload::Empty)),
403 _ => {
404 return Response::Err {
405 code: 400,
406 message: "invalid op/payload combination".into(),
407 };
408 }
409 };
410
411 match result {
412 Ok(resp) => resp,
413 Err(e) => err_response(e.status_code(), &e.to_string()),
414 }
415}
416
417fn get_schema(trees: &TreeMap, hashname: u64) -> Response {
418 let handler = match trees.get(&hashname) {
419 Some(h) => h,
420 None => {
421 return Response::Err {
422 code: 404,
423 message: format!("tree not found: {hashname:#X}"),
424 };
425 }
426 };
427 let schema = handler.schema();
428 match serde_json::to_vec(&schema) {
429 Ok(bytes) => Response::Ok(ResponsePayload::SchemaBytes(bytes)),
430 Err(e) => err_response(500, &e.to_string()),
432 }
433}
434
435fn list_collections(trees: &TreeMap) -> Response {
436 let collections = trees
437 .iter()
438 .map(|(&hashname, handler)| {
439 let (typ_hash, version) = handler.info();
440 let count = handler.count(false).unwrap_or_else(|e| {
441 tracing::warn!("count() failed for '{}': {e}", handler.name());
442 0
443 });
444 CollectionMeta {
445 name: handler.name().to_string(),
446 hashname,
447 typ_hash,
448 version,
449 count,
450 }
451 })
452 .collect();
453 Response::Ok(ResponsePayload::Collections(collections))
454}
455
456async fn accept_tcp(
457 listener: TcpListener,
458 trees: TreeMap,
459 mut shutdown_rx: async_broadcast::Receiver<()>,
460 mut local_rx: async_broadcast::Receiver<()>,
461) {
462 let mut backoff = ACCEPT_BACKOFF_MIN;
463 loop {
464 let accept_fut = listener.accept();
465 let global_stop = shutdown_rx.recv();
469 let local_stop = local_rx.recv();
470 pin_mut!(accept_fut);
471 pin_mut!(global_stop);
472 pin_mut!(local_stop);
473 let stop_fut = select(global_stop, local_stop);
474
475 match select(accept_fut, stop_fut).await {
476 Either::Left((Ok((stream, _addr)), _)) => {
477 backoff = ACCEPT_BACKOFF_MIN;
478 let trees = trees.clone();
479 let (reader, writer) = stream.into_split();
480 let rx = shutdown_rx.clone();
481 compio::runtime::spawn(handle_connection(reader, writer, trees, rx)).detach();
482 }
483 Either::Left((Err(e), _)) => {
484 tracing::error!("tcp accept error: {e}");
485 compio::time::sleep(backoff).await;
486 backoff = (backoff * 2).min(ACCEPT_BACKOFF_MAX);
487 }
488 Either::Right(_) => {
489 tracing::info!("shutting down TCP server");
490 break;
491 }
492 }
493 }
494}
495
496async fn accept_uds(
497 listener: UnixListener,
498 trees: TreeMap,
499 mut shutdown_rx: async_broadcast::Receiver<()>,
500 mut local_rx: async_broadcast::Receiver<()>,
501) {
502 let mut backoff = ACCEPT_BACKOFF_MIN;
503 loop {
504 let accept_fut = listener.accept();
505 let global_stop = shutdown_rx.recv();
506 let local_stop = local_rx.recv();
507 pin_mut!(accept_fut);
508 pin_mut!(global_stop);
509 pin_mut!(local_stop);
510 let stop_fut = select(global_stop, local_stop);
511
512 match select(accept_fut, stop_fut).await {
513 Either::Left((Ok((stream, _addr)), _)) => {
514 backoff = ACCEPT_BACKOFF_MIN;
515 let trees = trees.clone();
516 let (reader, writer) = stream.into_split();
517 let rx = shutdown_rx.clone();
518 compio::runtime::spawn(handle_connection(reader, writer, trees, rx)).detach();
519 }
520 Either::Left((Err(e), _)) => {
521 tracing::error!("uds accept error: {e}");
522 compio::time::sleep(backoff).await;
523 backoff = (backoff * 2).min(ACCEPT_BACKOFF_MAX);
524 }
525 Either::Right(_) => {
526 tracing::info!("shutting down UDS server");
527 break;
528 }
529 }
530 }
531}
532
533pub struct RpcHandle {
538 thread: Option<JoinHandle<()>>,
539 shutdown_tx: async_broadcast::Sender<()>,
540}
541
542impl RpcHandle {
543 pub fn stop(&mut self) {
544 let _ = self.shutdown_tx.try_broadcast(());
545 if let Some(h) = self.thread.take() {
546 let _ = h.join();
547 }
548 }
549}
550
551impl Drop for RpcHandle {
552 fn drop(&mut self) {
553 self.stop();
554 }
555}
556
557impl Db {
560 pub fn listen_tcp(&self, addr: impl ToSocketAddrsAsync + Send + 'static) {
575 let trees = self.build_tree_map();
576 let shutdown_rx = self.shutdown.subscribe_broadcast();
577 let (local_tx, local_rx) = async_broadcast::broadcast::<()>(1);
581 let shutdown_tx = local_tx.clone();
582
583 let thread = std::thread::spawn(move || {
584 let runtime = match RuntimeBuilder::new().build() {
588 Ok(rt) => rt,
589 Err(e) => {
590 tracing::error!("failed to build compio runtime for TCP RPC listener: {e}");
591 return;
592 }
593 };
594 runtime.block_on(async move {
595 let listener = match TcpListener::bind(addr).await {
596 Ok(l) => l,
597 Err(e) => {
598 tracing::error!("failed to bind TCP RPC listener: {e}");
599 return;
600 }
601 };
602 accept_tcp(listener, trees, shutdown_rx, local_rx).await;
603 });
604 });
605 crate::sync::lock(&self.rpc_handles).push(RpcHandle {
606 thread: Some(thread),
607 shutdown_tx,
608 });
609 }
610
611 pub fn listen_uds(&self, path: impl AsRef<Path> + Send + 'static) {
612 let trees = self.build_tree_map();
613 let shutdown_rx = self.shutdown.subscribe_broadcast();
614 let (local_tx, local_rx) = async_broadcast::broadcast::<()>(1);
615 let shutdown_tx = local_tx.clone();
616
617 let thread = std::thread::spawn(move || {
618 let runtime = match RuntimeBuilder::new().build() {
619 Ok(rt) => rt,
620 Err(e) => {
621 tracing::error!("failed to build compio runtime for UDS RPC listener: {e}");
622 return;
623 }
624 };
625 runtime.block_on(async move {
626 let _ = std::fs::remove_file(path.as_ref());
627 let listener = match UnixListener::bind(path).await {
628 Ok(l) => l,
629 Err(e) => {
630 tracing::error!("failed to bind UDS RPC listener: {e}");
631 return;
632 }
633 };
634 accept_uds(listener, trees, shutdown_rx, local_rx).await;
635 });
636 });
637 crate::sync::lock(&self.rpc_handles).push(RpcHandle {
638 thread: Some(thread),
639 shutdown_tx,
640 });
641 }
642}
643
644#[cfg(test)]
645mod err_frame_tests {
646 use super::encode_err_frame;
647
648 #[test]
652 fn err_frame_len_prefix_matches_written_bytes_when_over_u16() {
653 let big = "x".repeat(70_000);
654
655 let buggy_prefix = big.len() as u16; assert_ne!(
658 buggy_prefix as usize,
659 big.len(),
660 "sanity: pre-fix u16 prefix disagreed with the byte count"
661 );
662
663 let mut buf = Vec::new();
664 encode_err_frame(&mut buf, 500, &big);
665
666 let len = u16::from_be_bytes([buf[3], buf[4]]) as usize;
668 let msg_written = buf.len() - 5;
669 assert_eq!(
670 len, msg_written,
671 "len prefix must equal bytes actually written"
672 );
673 assert_eq!(len, u16::MAX as usize, "message capped at u16::MAX");
674 }
675
676 #[test]
677 fn err_frame_small_message_roundtrips() {
678 let mut buf = Vec::new();
679 encode_err_frame(&mut buf, 404, "not found");
680 assert_eq!(buf[0], 0x01);
681 assert_eq!(u16::from_be_bytes([buf[1], buf[2]]), 404);
682 let len = u16::from_be_bytes([buf[3], buf[4]]) as usize;
683 assert_eq!(len, "not found".len());
684 assert_eq!(&buf[5..], b"not found");
685 }
686
687 #[test]
688 fn err_frame_truncates_at_utf8_boundary() {
689 let message = format!("{}€", "a".repeat(u16::MAX as usize - 1));
690 let mut buf = Vec::new();
691 encode_err_frame(&mut buf, 500, &message);
692 let len = u16::from_be_bytes([buf[3], buf[4]]) as usize;
693 let encoded = &buf[5..5 + len];
694 assert_eq!(len, u16::MAX as usize - 1);
695 assert_eq!(std::str::from_utf8(encoded).unwrap(), "a".repeat(len));
696 assert_eq!(buf.len(), 5 + len);
697 }
698}
699
700#[cfg(test)]
701mod rpc_handle_tests {
702 use super::Db;
703 use std::time::Duration;
704
705 #[test]
709 fn rpc_handle_stop_terminates_listener_without_global_shutdown() {
710 let dir = tempfile::tempdir().unwrap();
711 let db = Db::open_test(dir.path()).unwrap();
712 let sock = dir.path().join("rpc.sock");
713 db.listen_uds(sock);
714
715 std::thread::sleep(Duration::from_millis(150));
717
718 let handle = crate::sync::lock(&db.rpc_handles)
720 .pop()
721 .expect("one rpc handle");
722
723 let (tx, rx) = std::sync::mpsc::channel();
726 std::thread::spawn(move || {
727 let mut handle = handle;
728 handle.stop();
729 let _ = tx.send(());
730 });
731
732 let stopped = rx.recv_timeout(Duration::from_secs(5)).is_ok();
733 assert!(
734 stopped,
735 "RpcHandle::stop did not terminate the listener (accept loop ignored the local signal)"
736 );
737 }
738}
739
740#[cfg(test)]
741mod dispatch_status_code_tests {
742 use crate::error::{DbError, SchemaMismatchKind};
743
744 #[test]
745 fn key_not_found_maps_to_404() {
746 assert_eq!(DbError::KeyNotFound.status_code(), 404);
747 }
748 #[test]
749 fn key_exists_maps_to_409() {
750 assert_eq!(DbError::KeyExists.status_code(), 409);
751 }
752 #[test]
753 fn client_error_maps_to_400() {
754 assert_eq!(DbError::Client("bad").status_code(), 400);
755 }
756 #[test]
757 fn not_implemented_maps_to_501() {
758 assert_eq!(DbError::NotImplemented.status_code(), 501);
759 }
760 #[test]
761 fn schema_mismatch_maps_to_422() {
762 let e = DbError::SchemaMismatch {
763 name: "x".into(),
764 kind: SchemaMismatchKind::MissingStep { from: 0 },
765 };
766 assert_eq!(e.status_code(), 422);
767 }
768 #[test]
769 fn tx_conflict_maps_to_409() {
770 assert_eq!(DbError::TxConflict.status_code(), 409);
772 }
773 #[test]
774 fn shard_mismatch_stays_500() {
775 assert_eq!(DbError::ShardMismatch.status_code(), 500);
777 }
778}
779
780#[cfg(test)]
781mod err_response_policy_tests {
782 use super::{Response, err_response};
783
784 fn unpack(r: Response) -> (u16, String) {
785 match r {
786 Response::Err { code, message } => (code, message),
787 Response::Ok(_) => panic!("expected Err, got Ok"),
788 }
789 }
790
791 #[test]
792 fn client_error_4xx_forwards_message_verbatim() {
793 let (code, msg) = unpack(err_response(404, "tree not found: 0xDEAD"));
795 assert_eq!(code, 404);
796 assert_eq!(msg, "tree not found: 0xDEAD");
797 }
798
799 #[test]
800 fn server_error_5xx_sends_opaque_message() {
801 let original = "corrupted entry at offset 12345 in /var/data/shard_007/000012.data";
804 let (code, msg) = unpack(err_response(500, original));
805 assert_eq!(code, 500);
806 assert_eq!(msg, "internal error");
807 assert!(
808 !msg.contains("12345") && !msg.contains("/var/data"),
809 "5xx response must not echo internal details"
810 );
811 }
812
813 #[test]
814 fn not_implemented_501_forwards_message() {
815 let (code, msg) = unpack(err_response(501, "not implemented"));
820 assert_eq!(code, 501);
821 assert_eq!(msg, "not implemented");
822 }
823}