1#![cfg_attr(not(feature = "std"), no_std)]
37
38#[cfg(feature = "alloc")]
39extern crate alloc;
40
41#[cfg(feature = "alloc")]
42pub mod accept_rule;
43pub mod arch;
44#[cfg(feature = "alloc")]
45pub mod arena;
46pub mod kv_decision;
47pub mod ring;
48#[cfg(feature = "alloc")]
49pub mod serving_state;
50#[cfg(feature = "alloc")]
53pub mod batch;
54#[cfg(feature = "std")]
55pub mod buffer;
56pub mod datagram_batch;
57pub mod factory;
58#[cfg(target_has_atomic = "64")]
62pub mod histogram;
63#[cfg(feature = "io-async")]
64pub mod io;
65pub mod markers;
66#[cfg(feature = "park")]
67pub mod park;
68pub mod per_core;
69#[cfg(feature = "std")]
72pub mod signal;
73pub mod time;
77#[cfg(feature = "live")]
79pub mod live;
80
81pub mod sized {
85 include!(concat!(env!("OUT_DIR"), "/proxima_core_sized.rs"));
86}
87
88#[cfg(feature = "registry")]
89pub use factory::FactoryRegistry;
90pub use factory::Named;
91#[cfg(feature = "config")]
92pub use factory::{Composition, Factory, FactorySpec};
93#[cfg(feature = "alloc")]
94pub use serving_state::{ServingFsmError, ServingState};
95
96#[cfg(feature = "alloc")]
97use alloc::boxed::Box;
98#[cfg(feature = "alloc")]
99use alloc::string::String;
100
101use core::time::Duration;
102
103use thiserror::Error;
104
105#[cfg(feature = "alloc")]
109pub type ErrorSource = Box<dyn core::error::Error + Send + Sync + 'static>;
110
111#[derive(Debug, Error)]
117#[cfg(feature = "alloc")]
118pub enum DecodeError {
119 #[error("missing header terminator")]
120 MissingHeaderTerminator,
121 #[error("empty status line")]
122 EmptyStatusLine,
123 #[error("malformed status line: {raw}")]
124 MalformedStatusLine { raw: String },
125 #[error("chunked: size CRLF missing")]
126 ChunkedSizeMissing,
127 #[error("chunked: payload short")]
128 ChunkedPayloadShort,
129 #[error("not utf-8 ({context}): {source}")]
130 NotUtf8 {
131 context: &'static str,
132 source: ErrorSource,
133 },
134 #[error("parse {context}: {source}")]
135 Parse {
136 context: &'static str,
137 source: ErrorSource,
138 },
139 #[error("missing required field: {field}")]
140 MissingField { field: &'static str },
141 #[error("decode ({context}): {source}")]
142 Other {
143 context: &'static str,
144 source: ErrorSource,
145 },
146}
147
148#[derive(Debug, Error)]
150#[cfg(feature = "alloc")]
151pub enum EncodeError {
152 #[error("json encode: {source}")]
153 Json { source: ErrorSource },
154 #[error("format convert ({context}): {source}")]
155 FormatConvert {
156 context: &'static str,
157 source: ErrorSource,
158 },
159 #[error("render ({context}): {source}")]
160 Render {
161 context: &'static str,
162 source: ErrorSource,
163 },
164 #[error("base64: {source}")]
165 Base64 { source: ErrorSource },
166 #[error("requires base64 string input")]
167 RequiresBase64Input,
168 #[error("encode ({context}): {source}")]
169 Other {
170 context: &'static str,
171 source: ErrorSource,
172 },
173}
174
175#[derive(Debug, Error)]
177#[cfg(feature = "alloc")]
178pub enum UpstreamError {
179 #[error("io ({operation}): {source}")]
180 Io {
181 operation: &'static str,
182 source: ErrorSource,
183 },
184 #[error("upstream returned status {status}")]
185 BadStatus { status: u16 },
186 #[error("upstream cancelled")]
187 Cancelled,
188 #[error("protocol ({context}): {source}")]
189 Protocol {
190 context: &'static str,
191 source: ErrorSource,
192 },
193 #[error("connect ({context}): {source}")]
194 Connect {
195 context: &'static str,
196 source: ErrorSource,
197 },
198 #[error("upstream ({context}): {source}")]
199 Other {
200 context: &'static str,
201 source: ErrorSource,
202 },
203}
204
205#[derive(Debug, Error)]
207#[cfg(feature = "alloc")]
208pub enum ConfigError {
209 #[error("{component}: requires field '{field}'")]
210 RequiredField {
211 component: &'static str,
212 field: &'static str,
213 },
214 #[error("{component}: requires an inner pipe")]
215 MissingInnerPipe { component: &'static str },
216 #[error("no upstreams configured")]
217 NoUpstreams,
218 #[error("parse ({context}): {source}")]
219 Parse {
220 context: &'static str,
221 source: ErrorSource,
222 },
223 #[error("spawn failed ({context}): {source}")]
224 SpawnFailed {
225 context: &'static str,
226 source: ErrorSource,
227 },
228 #[error("config ({context}): {source}")]
229 Other {
230 context: &'static str,
231 source: ErrorSource,
232 },
233}
234
235#[derive(Debug, Error)]
237#[cfg(feature = "alloc")]
238pub enum BodyError {
239 #[error("body cancelled")]
240 Cancelled,
241 #[error("worker dropped sender ({context})")]
242 WorkerDropped { context: &'static str },
243 #[error("worker panicked ({context})")]
244 WorkerPanic { context: &'static str },
245 #[error("serialize ({context}): {source}")]
246 Serialize {
247 context: &'static str,
248 source: ErrorSource,
249 },
250 #[error("body ({context}): {source}")]
251 Other {
252 context: &'static str,
253 source: ErrorSource,
254 },
255}
256
257#[derive(Debug, Error)]
259#[cfg(feature = "alloc")]
260pub enum NotFoundError {
261 #[error("no pipe registered as '{name}'")]
262 Pipe { name: String },
263 #[error("{kind} '{name}' not found")]
264 Resource { kind: &'static str, name: String },
265}
266
267#[derive(Debug, Error)]
269#[cfg(feature = "alloc")]
270pub enum RegistryError {
271 #[error("no {kind} named '{name}'")]
272 NotRegistered { kind: &'static str, name: String },
273 #[error("{kind} '{name}' already registered")]
274 AlreadyRegistered { kind: &'static str, name: String },
275}
276
277#[derive(Debug, Error)]
279#[cfg(feature = "alloc")]
280pub enum RecordError {
281 #[error("recording io ({operation}): {source}")]
282 Io {
283 operation: &'static str,
284 source: ErrorSource,
285 },
286 #[error("recording codec ({operation}): {source}")]
287 Codec {
288 operation: &'static str,
289 source: ErrorSource,
290 },
291 #[error("recording frame exceeds u32::MAX bytes")]
292 FrameTooLarge,
293 #[error("recording version mismatch: expected {expected}, got {got}")]
294 VersionMismatch { expected: u8, got: u8 },
295 #[error("recording buffer full at capacity {capacity}")]
296 BufferFull { capacity: usize },
297}
298
299#[derive(Debug, Error)]
304pub enum ProximaError {
305 #[error("upstream error: {0}")]
308 #[cfg(feature = "alloc")]
309 Upstream(String),
310
311 #[error("timeout after {0:?}")]
312 Timeout(Duration),
313
314 #[error("rate limited")]
315 RateLimited,
316
317 #[error("no upstream had data")]
318 NoData,
319
320 #[error("config: {0}")]
321 #[cfg(feature = "alloc")]
322 Config(String),
323
324 #[error("io: {0}")]
325 #[cfg(feature = "std")]
326 Io(#[from] std::io::Error),
327
328 #[error("decode: {0}")]
329 #[cfg(feature = "alloc")]
330 Decode(String),
331
332 #[error("encode: {0}")]
333 #[cfg(feature = "alloc")]
334 Encode(String),
335
336 #[error("body stream: {0}")]
337 #[cfg(feature = "alloc")]
338 Body(String),
339
340 #[error("not found: {0}")]
341 #[cfg(feature = "alloc")]
342 NotFound(String),
343
344 #[error("forbidden: {0}")]
348 #[cfg(feature = "alloc")]
349 Forbidden(String),
350
351 #[error("registry: {0}")]
352 #[cfg(feature = "alloc")]
353 Registry(String),
354
355 #[error("retries exhausted after {attempts} attempts: {last}")]
356 #[cfg(feature = "alloc")]
357 RetriesExhausted { attempts: usize, last: String },
358
359 #[error("recording: {0}")]
360 #[cfg(feature = "alloc")]
361 Record(String),
362
363 #[error("replay miss: {fingerprint}")]
364 #[cfg(feature = "alloc")]
365 ReplayMiss { fingerprint: String },
366
367 #[error("decode: {0}")]
372 #[cfg(feature = "alloc")]
373 DecodeKind(#[from] DecodeError),
374
375 #[error("encode: {0}")]
376 #[cfg(feature = "alloc")]
377 EncodeKind(#[from] EncodeError),
378
379 #[error("upstream: {0}")]
380 #[cfg(feature = "alloc")]
381 UpstreamKind(#[from] UpstreamError),
382
383 #[error("config: {0}")]
384 #[cfg(feature = "alloc")]
385 ConfigKind(#[from] ConfigError),
386
387 #[error("body: {0}")]
388 #[cfg(feature = "alloc")]
389 BodyKind(#[from] BodyError),
390
391 #[error("not found: {0}")]
392 #[cfg(feature = "alloc")]
393 NotFoundKind(#[from] NotFoundError),
394
395 #[error("registry: {0}")]
396 #[cfg(feature = "alloc")]
397 RegistryKind(#[from] RegistryError),
398
399 #[error("recording: {0}")]
400 #[cfg(feature = "alloc")]
401 RecordKind(#[from] RecordError),
402}
403
404impl ProximaError {
405 #[must_use]
409 pub fn is_retryable(&self) -> bool {
410 match self {
411 Self::Timeout(_) => true,
412 Self::RateLimited => false,
413 #[cfg(feature = "alloc")]
414 Self::Upstream(_) | Self::Body(_) => true,
415 #[cfg(feature = "std")]
416 Self::Io(_) => true,
417 #[cfg(feature = "alloc")]
418 Self::UpstreamKind(kind) => !matches!(kind, UpstreamError::BadStatus { .. }),
419 #[cfg(feature = "alloc")]
420 Self::BodyKind(kind) => matches!(
421 kind,
422 BodyError::WorkerDropped { .. } | BodyError::WorkerPanic { .. }
423 ),
424 _ => false,
425 }
426 }
427}
428
429pub type ProximaResult<T> = Result<T, ProximaError>;
430
431#[cfg(test)]
432#[cfg(feature = "alloc")]
433#[allow(clippy::unwrap_used, clippy::expect_used)]
434mod tests {
435 use super::*;
436 use alloc::format;
437
438 #[test]
439 fn timeout_is_retryable() {
440 assert!(ProximaError::Timeout(Duration::from_secs(1)).is_retryable());
441 }
442
443 #[test]
444 fn rate_limited_is_not_retryable() {
445 assert!(!ProximaError::RateLimited.is_retryable());
446 }
447
448 #[test]
449 fn legacy_upstream_is_retryable() {
450 assert!(ProximaError::Upstream("transient".into()).is_retryable());
451 }
452
453 #[test]
454 fn typed_upstream_io_is_retryable_but_bad_status_is_not() {
455 let io = UpstreamError::Io {
456 operation: "read",
457 source: Box::new(LightweightTestError("transient")),
458 };
459 assert!(ProximaError::from(io).is_retryable());
460
461 let bad = UpstreamError::BadStatus { status: 500 };
462 assert!(!ProximaError::from(bad).is_retryable());
463 }
464
465 #[test]
466 fn decode_kind_carries_typed_payload() {
467 let err = DecodeError::MissingField {
468 field: "pipeline_id",
469 };
470 let display = format!("{}", err);
471 assert!(display.contains("pipeline_id"));
472 }
473
474 #[test]
475 fn from_decode_error_to_proxima_error_via_from_impl() {
476 let err: ProximaError = DecodeError::EmptyStatusLine.into();
477 assert!(matches!(
478 err,
479 ProximaError::DecodeKind(DecodeError::EmptyStatusLine)
480 ));
481 }
482
483 #[test]
484 fn config_required_field_renders_component_and_field() {
485 let err = ConfigError::RequiredField {
486 component: "auth",
487 field: "allow",
488 };
489 let display = format!("{}", err);
490 assert!(display.contains("auth"));
491 assert!(display.contains("allow"));
492 }
493
494 #[test]
495 fn registry_not_registered_uses_kind_and_name() {
496 let err = RegistryError::NotRegistered {
497 kind: "codec factory",
498 name: "json".into(),
499 };
500 let display = format!("{}", err);
501 assert!(display.contains("codec factory"));
502 assert!(display.contains("json"));
503 }
504
505 #[test]
506 fn record_version_mismatch_carries_numbers() {
507 let err = RecordError::VersionMismatch {
508 expected: 3,
509 got: 2,
510 };
511 let display = format!("{}", err);
512 assert!(display.contains('3'));
513 assert!(display.contains('2'));
514 }
515
516 #[derive(Debug)]
517 struct LightweightTestError(&'static str);
518 impl core::fmt::Display for LightweightTestError {
519 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
520 f.write_str(self.0)
521 }
522 }
523 impl core::error::Error for LightweightTestError {}
524}