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