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