1use std::fmt;
2use std::path::PathBuf;
3use std::time::Duration;
4
5use http::StatusCode;
6
7use http::Method;
8use thiserror::Error;
9use url::Url;
10
11use crate::codec::OpaqueCarrierLocation;
12
13#[derive(Debug, Error)]
14#[non_exhaustive]
15pub enum ProxyError {
16 #[error("direct adapter allocation does not match the configured adapter")]
19 DirectAdapterMismatch,
20 #[error("upstream unreachable: {url}")]
21 UpstreamUnreachable {
22 url: Url,
23 #[source]
24 source: reqwest::Error,
25 },
26 #[error("adapter not found for {method} {path}")]
27 AdapterNotFound { path: String, method: Method },
28 #[error("body too large: limit {limit_bytes} bytes")]
29 BodyTooLarge { limit_bytes: u64 },
30 #[error("invalid json body")]
31 InvalidJson {
32 #[source]
33 source: serde_json::Error,
34 },
35 #[error("sse partial frame: {reason}")]
36 SsePartialFrame { reason: String },
37 #[error("unsurfaced pii at {field_path}")]
44 UnsurfacedPii { field_path: String },
45 #[error("adapter coverage contract is unproven on the legacy path")]
51 UnprovenCoverage,
52 #[error("request refused: {refusal}")]
56 Refused { refusal: gaze::BoundaryRefusal },
57 #[error("pipeline failed")]
58 Pipeline {
59 #[source]
60 source: gaze::Error,
61 },
62 #[error("http server failed")]
63 Server {
64 #[source]
65 source: std::io::Error,
66 },
67 #[error("daemon already running: pid {pid} ({pidfile})", pidfile = pidfile.display())]
68 DaemonAlreadyRunning { pid: u32, pidfile: PathBuf },
69 #[error("daemon stop timed out after {timeout:?}: pid {pid}")]
72 DaemonStopTimeout { pid: u32, timeout: Duration },
73 #[error("daemon is not running")]
74 DaemonNotRunning,
75 #[error("daemon pidfile stale: {pidfile}", pidfile = pidfile.display())]
76 DaemonPidfileStale { pidfile: PathBuf },
77 #[error("daemon io failed: {path}", path = path.display())]
78 DaemonIo {
79 path: PathBuf,
80 #[source]
81 source: std::io::Error,
82 },
83 #[error("daemon config failed: {detail}")]
84 DaemonConfig { detail: String },
85 #[error(
91 "daemon exited before serving ({code}); see {stderr_file}",
92 code = code.map_or_else(|| "terminated by signal".to_string(), |code| format!("exit code {code}")),
93 stderr_file = stderr_file.display()
94 )]
95 DaemonExitedEarly {
96 code: Option<i32>,
97 stderr_file: PathBuf,
98 },
99}
100
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
106pub enum ProxyErrorCode {
107 RouteRejected,
108 HeaderRejected,
109 InvalidUpstreamHeader,
110 SessionIdentityRequired,
111 UnexpectedSessionHeader,
112 SessionExpired,
113 SessionGenerationConflict,
114 SessionCommitFailure,
115 PrincipalRequired,
116 PrincipalRejected,
117 RegistryCapacity,
118 InvalidUpstreamUrl,
119 ProxyConfiguration,
120 UnsupportedContentEncoding,
121 InvalidFraming,
122 RequestBodyLimitExceeded,
123 ResponseBodyLimitExceeded,
124 HeaderLimitExceeded,
125 InvalidRequestFormat,
126 InvalidUpstreamResponseFormat,
127 DuplicateObjectKey,
128 InternalCoverageFailure,
129 ControlWouldMutate,
130 OpaqueMediaUninspected,
131 InvalidProvenance,
132 InvalidToken,
133 SignedMutationRequired,
134 SignedSurfaceMalformed,
135 InvalidSseLifecycle,
136 InvalidContentBlockIndex,
137 ConnectTimeout,
138 RequestTimeout,
139 TotalTimeout,
140 UpstreamRedirect,
141 UpstreamBadRequest,
142 UpstreamUnauthorized,
143 UpstreamForbidden,
144 UpstreamNotFound,
145 UpstreamConflict,
146 UpstreamPayloadTooLarge,
147 UpstreamRateLimited,
148 UpstreamClientFailure,
149 UpstreamUnavailable,
150 UpstreamServerFailure,
151 UpstreamUnreachable,
152 UpstreamProtocol,
153 InspectionInternal,
154 InvalidStateTransition,
155 ProtectionRefused,
157}
158
159impl ProxyErrorCode {
160 #[must_use]
162 pub const fn error(self, phase: ProxyErrorPhase) -> DirectProxyError {
163 DirectProxyError::new(self, phase)
164 }
165
166 #[must_use]
168 pub const fn http_status(self) -> StatusCode {
169 match self {
170 Self::RouteRejected => StatusCode::NOT_FOUND,
171 Self::HeaderRejected
172 | Self::SessionIdentityRequired
173 | Self::UnexpectedSessionHeader
174 | Self::InvalidRequestFormat
175 | Self::DuplicateObjectKey => StatusCode::BAD_REQUEST,
176 Self::PrincipalRequired | Self::UpstreamUnauthorized => StatusCode::UNAUTHORIZED,
177 Self::PrincipalRejected | Self::UpstreamForbidden => StatusCode::FORBIDDEN,
178 Self::SessionExpired => StatusCode::GONE,
179 Self::SessionGenerationConflict | Self::UpstreamConflict => StatusCode::CONFLICT,
180 Self::RegistryCapacity
181 | Self::InvalidUpstreamUrl
182 | Self::ProxyConfiguration
183 | Self::UpstreamUnavailable => StatusCode::SERVICE_UNAVAILABLE,
184 Self::RequestBodyLimitExceeded | Self::UpstreamPayloadTooLarge => {
185 StatusCode::PAYLOAD_TOO_LARGE
186 }
187 Self::ControlWouldMutate | Self::OpaqueMediaUninspected | Self::ProtectionRefused => {
188 StatusCode::UNPROCESSABLE_ENTITY
189 }
190 Self::ConnectTimeout | Self::RequestTimeout | Self::TotalTimeout => {
191 StatusCode::GATEWAY_TIMEOUT
192 }
193 Self::UpstreamBadRequest => StatusCode::BAD_REQUEST,
194 Self::UpstreamNotFound => StatusCode::NOT_FOUND,
195 Self::UpstreamRateLimited => StatusCode::TOO_MANY_REQUESTS,
196 Self::SessionCommitFailure
197 | Self::InspectionInternal
198 | Self::InvalidStateTransition => StatusCode::INTERNAL_SERVER_ERROR,
199 Self::InvalidUpstreamHeader
200 | Self::UnsupportedContentEncoding
201 | Self::InvalidFraming
202 | Self::ResponseBodyLimitExceeded
203 | Self::HeaderLimitExceeded
204 | Self::InvalidUpstreamResponseFormat
205 | Self::InternalCoverageFailure
206 | Self::InvalidProvenance
207 | Self::InvalidToken
208 | Self::SignedMutationRequired
209 | Self::SignedSurfaceMalformed
210 | Self::InvalidSseLifecycle
211 | Self::InvalidContentBlockIndex
212 | Self::UpstreamRedirect
213 | Self::UpstreamClientFailure
214 | Self::UpstreamServerFailure
215 | Self::UpstreamUnreachable
216 | Self::UpstreamProtocol => StatusCode::BAD_GATEWAY,
217 }
218 }
219}
220
221#[derive(Clone, Copy, Debug, Eq, PartialEq)]
223pub enum ProxyErrorPhase {
224 Ingress,
225 RequestValidation,
226 Session,
227 RequestTransform,
228 UpstreamConfiguration,
229 UpstreamConnect,
230 UpstreamHeaders,
231 UpstreamBody,
232 ResponseValidation,
233 ResponseTransform,
234 ResponseReplay,
235 Inspection,
236 Framework,
237}
238
239#[derive(Clone, Copy, Debug, Eq, PartialEq)]
241#[non_exhaustive]
242pub enum HeaderRejectionReason {
243 NameNotAllowlisted,
244 EmptyValue,
245 DuplicateSingleton,
246}
247
248impl HeaderRejectionReason {
249 #[must_use]
251 pub const fn as_str(self) -> &'static str {
252 match self {
253 Self::NameNotAllowlisted => "name_not_allowlisted",
254 Self::EmptyValue => "empty_value",
255 Self::DuplicateSingleton => "duplicate_singleton",
256 }
257 }
258}
259
260const MAX_STRUCTURAL_HEADER_NAME_BYTES: usize = 128;
261const PACKED_STRUCTURAL_HEADER_NAME_BYTES: usize = MAX_STRUCTURAL_HEADER_NAME_BYTES * 6 / 8;
262
263#[derive(Clone, Copy, Eq, PartialEq)]
266struct HeaderRejectionIdentifier {
267 packed_name: [u8; PACKED_STRUCTURAL_HEADER_NAME_BYTES],
268 name_length: u8,
269 reason: HeaderRejectionReason,
270}
271
272impl HeaderRejectionIdentifier {
273 fn new(name: &str, reason: HeaderRejectionReason) -> Option<Self> {
274 let name_length = u8::try_from(name.len()).ok()?;
275 if name.len() > MAX_STRUCTURAL_HEADER_NAME_BYTES {
276 return None;
277 }
278 let mut packed_name = [0; PACKED_STRUCTURAL_HEADER_NAME_BYTES];
279 for (index, byte) in name.bytes().enumerate() {
280 let code = encode_header_name_byte(byte)?;
281 let bit_offset = index * 6;
282 let byte_offset = bit_offset / 8;
283 let shift = bit_offset % 8;
284 packed_name[byte_offset] |= code << shift;
285 if shift > 2 {
286 packed_name[byte_offset + 1] |= code >> (8 - shift);
287 }
288 }
289 Some(Self {
290 packed_name,
291 name_length,
292 reason,
293 })
294 }
295
296 fn name(self) -> String {
297 let mut name = String::with_capacity(usize::from(self.name_length));
298 for index in 0..usize::from(self.name_length) {
299 let bit_offset = index * 6;
300 let byte_offset = bit_offset / 8;
301 let shift = bit_offset % 8;
302 let mut code = self.packed_name[byte_offset] >> shift;
303 if shift > 2 {
304 code |= self.packed_name[byte_offset + 1] << (8 - shift);
305 }
306 name.push(char::from(
307 decode_header_name_byte(code & 0x3f)
308 .expect("packed header names contain only validated token bytes"),
309 ));
310 }
311 name
312 }
313}
314
315fn encode_header_name_byte(byte: u8) -> Option<u8> {
316 match byte {
317 b'a'..=b'z' => Some(byte - b'a'),
318 b'0'..=b'9' => Some(26 + byte - b'0'),
319 b'!' => Some(36),
320 b'#' => Some(37),
321 b'$' => Some(38),
322 b'%' => Some(39),
323 b'&' => Some(40),
324 b'\'' => Some(41),
325 b'*' => Some(42),
326 b'+' => Some(43),
327 b'-' => Some(44),
328 b'.' => Some(45),
329 b'^' => Some(46),
330 b'_' => Some(47),
331 b'`' => Some(48),
332 b'|' => Some(49),
333 b'~' => Some(50),
334 _ => None,
335 }
336}
337
338fn decode_header_name_byte(code: u8) -> Option<u8> {
339 match code {
340 0..=25 => Some(b'a' + code),
341 26..=35 => Some(b'0' + code - 26),
342 36 => Some(b'!'),
343 37 => Some(b'#'),
344 38 => Some(b'$'),
345 39 => Some(b'%'),
346 40 => Some(b'&'),
347 41 => Some(b'\''),
348 42 => Some(b'*'),
349 43 => Some(b'+'),
350 44 => Some(b'-'),
351 45 => Some(b'.'),
352 46 => Some(b'^'),
353 47 => Some(b'_'),
354 48 => Some(b'`'),
355 49 => Some(b'|'),
356 50 => Some(b'~'),
357 _ => None,
358 }
359}
360
361impl fmt::Debug for HeaderRejectionIdentifier {
362 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
363 formatter
364 .debug_struct("Header")
365 .field("name", &self.name())
366 .field("reason", &self.reason)
367 .finish()
368 }
369}
370
371#[derive(Clone, Debug, Eq, PartialEq)]
372enum DirectProxyErrorIdentifier {
373 Header(HeaderRejectionIdentifier),
374 OpaqueCarrier(OpaqueCarrierLocation),
375 Refusal(gaze::BoundaryRefusal),
376}
377
378#[derive(Clone, Eq, PartialEq)]
386pub struct DirectProxyError {
387 code: ProxyErrorCode,
388 phase: ProxyErrorPhase,
389 retry_after_seconds: Option<u32>,
390 identifier: Option<DirectProxyErrorIdentifier>,
391}
392
393impl DirectProxyError {
394 #[must_use]
395 pub const fn new(code: ProxyErrorCode, phase: ProxyErrorPhase) -> Self {
396 Self {
397 code,
398 phase,
399 retry_after_seconds: None,
400 identifier: None,
401 }
402 }
403
404 #[must_use]
405 pub(crate) const fn with_retry_after_seconds(mut self, seconds: Option<u32>) -> Self {
406 self.retry_after_seconds = seconds;
407 self
408 }
409
410 pub(crate) fn with_header_rejection(
411 mut self,
412 name: &str,
413 reason: HeaderRejectionReason,
414 ) -> Self {
415 self.identifier =
416 HeaderRejectionIdentifier::new(name, reason).map(DirectProxyErrorIdentifier::Header);
417 self
418 }
419
420 pub(crate) fn with_opaque_carrier(mut self, opaque_carrier: OpaqueCarrierLocation) -> Self {
421 self.identifier = Some(DirectProxyErrorIdentifier::OpaqueCarrier(opaque_carrier));
422 self
423 }
424
425 pub(crate) fn with_refusal(mut self, refusal: gaze::BoundaryRefusal) -> Self {
426 self.identifier = Some(DirectProxyErrorIdentifier::Refusal(refusal));
427 self
428 }
429
430 #[must_use]
432 pub fn refusal(&self) -> Option<&gaze::BoundaryRefusal> {
433 match &self.identifier {
434 Some(DirectProxyErrorIdentifier::Refusal(refusal)) => Some(refusal),
435 _ => None,
436 }
437 }
438
439 #[must_use]
440 pub const fn code(&self) -> ProxyErrorCode {
441 self.code
442 }
443
444 #[must_use]
445 pub const fn phase(&self) -> ProxyErrorPhase {
446 self.phase
447 }
448
449 #[must_use]
450 pub const fn http_status(&self) -> StatusCode {
451 self.code.http_status()
452 }
453
454 #[must_use]
455 pub const fn retry_after_seconds(&self) -> Option<u32> {
456 self.retry_after_seconds
457 }
458
459 #[must_use]
461 pub fn header_name(&self) -> Option<String> {
462 match &self.identifier {
463 Some(DirectProxyErrorIdentifier::Header(identifier)) => Some(identifier.name()),
464 _ => None,
465 }
466 }
467
468 #[must_use]
470 pub const fn header_rejection_reason(&self) -> Option<HeaderRejectionReason> {
471 match &self.identifier {
472 Some(DirectProxyErrorIdentifier::Header(identifier)) => Some(identifier.reason),
473 _ => None,
474 }
475 }
476
477 #[must_use]
479 pub const fn opaque_carrier(&self) -> Option<OpaqueCarrierLocation> {
480 match &self.identifier {
481 Some(DirectProxyErrorIdentifier::OpaqueCarrier(location)) => Some(*location),
482 _ => None,
483 }
484 }
485
486 #[must_use]
487 pub const fn sse_error_frame(&self) -> &'static [u8] {
488 crate::server::DIRECT_PROXY_ERROR_FRAME
489 }
490}
491
492impl fmt::Debug for DirectProxyError {
493 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
494 let mut debug = formatter.debug_struct("DirectProxyError");
495 debug.field("code", &self.code).field("phase", &self.phase);
496 if let Some(identifier) = &self.identifier {
497 debug.field("identifier", identifier);
498 }
499 debug.finish()
500 }
501}
502
503impl fmt::Display for DirectProxyError {
504 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
505 write!(formatter, "proxy_{:?}_{:?}", self.phase, self.code)
506 }
507}
508
509impl std::error::Error for DirectProxyError {}