Skip to main content

camel_api/
error.rs

1use std::sync::Arc;
2use thiserror::Error;
3
4/// Typed security-validation error for fail-closed config/startup checks (ADR-0033).
5///
6/// Each variant corresponds to a specific Batch 1+ security validation that refuses
7/// to start with a misconfigured or dangerous default. Operators can `match` on
8/// these variants for programmatic error handling.
9#[derive(Debug, Clone, PartialEq, Eq, Error)]
10#[non_exhaustive]
11pub enum ConfigValidationError {
12    #[error(
13        "aggregator config requires at least one completion bound (size, timeout, predicate, or interval)"
14    )]
15    AggregatorMissingCompletionBound,
16
17    /// Raised when an Aggregator has none of: max_buckets, a Timeout completion
18    /// condition, or a bucket_ttl. At least one memory-release bound is mandatory
19    /// (R3-M2) so a unique-correlation-key flood cannot grow the bucket map
20    /// without limit.
21    #[error("aggregator requires at least one of max_buckets, completionTimeout, or bucket_ttl")]
22    AggregatorMissingMemoryBound,
23
24    /// Raised when an Aggregator has a Timeout completion condition but no
25    /// `bucket_ttl`. The R3-M3 timeout-task cap may skip spawning a dedicated
26    /// timeout task under flood; without `bucket_ttl` there is no fallback
27    /// eviction path and the bucket leaks until shutdown. Requiring `bucket_ttl`
28    /// whenever Timeout is present makes the cap-skip degradation safe by
29    /// construction.
30    #[error(
31        "aggregator Timeout completion requires bucket_ttl (memory-release bound for the timeout-task cap fallback)"
32    )]
33    AggregatorTimeoutRequiresTtl,
34
35    #[error("throttler max_requests must be > 0")]
36    ThrottlerMaxRequestsZero,
37
38    #[error("loop step must specify either 'count' or 'while', not both")]
39    LoopConflictingCountAndWhile,
40
41    #[error("loop step must specify either 'count' or 'while'")]
42    LoopMissingCountOrWhile,
43
44    #[error("SQL use_message_body_for_sql requires allow_dynamic_query=true")]
45    SqlDynamicQueryWithoutAllowDynamic,
46}
47
48/// Typed error for constructing an [`EndpointUri`](crate::EndpointUri) from a base URI
49/// plus a `parameters:` map.
50///
51/// Every variant names the offending key or input in its `Display` text so failures
52/// are diagnosable without losing the context of what was rejected.
53#[derive(Debug, Clone, PartialEq, Eq, Error)]
54#[non_exhaustive]
55pub enum EndpointUriError {
56    /// A `parameters:` key collides with a key already present in the base URI query.
57    #[error(
58        "endpoint URI parameter `{key}` duplicates a key already present in the base URI query"
59    )]
60    DuplicateKey { key: String },
61
62    /// The base URI has no non-empty scheme (no `:` before the path).
63    #[error("endpoint URI is missing a scheme (expected `scheme:path`)")]
64    MissingScheme,
65
66    /// The base URI query contains a pair with an empty key (e.g. `?=value`).
67    #[error("endpoint URI query contains a pair with an empty key")]
68    EmptyQueryKey,
69
70    /// A `parameters:` key is empty or contains a reserved/unsafe character.
71    #[error("endpoint URI parameter key `{key}` is empty or contains a reserved character")]
72    InvalidParamKey { key: String },
73}
74
75/// Core error type for the Camel framework.
76#[derive(Debug, Clone, Error)]
77#[non_exhaustive]
78pub enum CamelError {
79    #[error("Component not found: {0}")]
80    ComponentNotFound(String),
81
82    #[error("Endpoint creation failed: {0}")]
83    EndpointCreationFailed(String),
84
85    #[error("Processor error: {0}")]
86    ProcessorError(String),
87
88    /// Like `ProcessorError` but preserves the source error chain
89    /// for downstream inspection (e.g. via `std::error::Error::source()`).
90    #[error("Processor error: {0}")]
91    ProcessorErrorWithSource(String, #[source] Arc<dyn std::error::Error + Send + Sync>),
92
93    #[error("Type conversion failed: {0}")]
94    TypeConversionFailed(String),
95
96    #[error("Invalid URI: {0}")]
97    InvalidUri(String),
98
99    #[error("Channel closed")]
100    ChannelClosed,
101
102    #[error("Route error: {0}")]
103    RouteError(String),
104
105    #[error("IO error: {0}")]
106    Io(String),
107
108    #[error("Dead letter channel failed: {0}")]
109    DeadLetterChannelFailed(String),
110
111    #[error("Circuit breaker open: {0}")]
112    CircuitOpen(String),
113
114    #[error("HTTP {method} {url} failed: {status_code} {status_text}")]
115    HttpOperationFailed {
116        method: String,
117        url: String,
118        status_code: u16,
119        status_text: String,
120        response_body: Option<String>,
121    },
122
123    /// Producer `call()` failed on a shutdown signal — the consumer/semaphore
124    /// is closing and the producer cannot acquire a permit. Distinct from Stop EIP
125    /// (which is successful control flow). Used by JMS/OpenSearch producers. See ADR-0024.
126    #[error("Consumer stopping: semaphore closed during call")]
127    ConsumerStopping,
128
129    #[error("Configuration error: {0}")]
130    Config(String),
131
132    /// Typed security-validation error (ADR-0033). Promotes Batch 1+ config/startup
133    /// failure modes from stringly-typed `Config(_)` to a matchable enum so
134    /// operators can discriminate programmatically.
135    #[error("Configuration validation error: {0}")]
136    ConfigValidation(ConfigValidationError),
137
138    #[error("Body stream has already been consumed")]
139    AlreadyConsumed,
140
141    #[error("Stream size exceeded limit: {0}")]
142    StreamLimitExceeded(usize),
143
144    #[error("Unauthenticated: {0}")]
145    Unauthenticated(String),
146
147    #[error("Unauthorized: {0}")]
148    Unauthorized(String),
149
150    /// Auth provider (JWKS/introspection/token endpoint) is unreachable or failing.
151    /// Promotes the auth-provider-down signal from a stringly-typed ProcessorError to
152    /// a matchable variant; WebSocket and gRPC transports map it to 503 / UNAVAILABLE.
153    #[error("Auth provider unavailable: {0}")]
154    AuthProviderUnavailable(String),
155
156    #[error("Validation failed: {0}")]
157    ValidationError(String),
158
159    #[error("Template reload failed: {0}")]
160    TemplateReload(String),
161
162    /// Typed endpoint-URI construction error (see [`EndpointUriError`]). Promotes the
163    /// fail-closed `EndpointUri` merge failures from stringly-typed errors to a matchable
164    /// variant so operators can discriminate programmatically.
165    #[error("Endpoint URI error: {0}")]
166    EndpointUri(EndpointUriError),
167
168    /// The request body media type does not match the declared/consumed type
169    /// (REST DSL default-strict content negotiation, HTTP 415).
170    #[error("Unsupported media type: consumed {consumed}, declared {declared}")]
171    UnsupportedMediaType { consumed: String, declared: String },
172
173    /// The response representation cannot satisfy the client's Accept header
174    /// (REST DSL default-strict content negotiation, HTTP 406).
175    #[error("Not acceptable: accept {accept}, produced {produced}")]
176    NotAcceptable { accept: String, produced: String },
177}
178
179/// Classification marker for `CamelError::CircuitOpen`.
180///
181/// Shared named constant so the pipeline tracer's circuit-open exclusion
182/// (skip `increment_errors` — the breaker already recorded the rejection)
183/// and `classify` itself cannot drift apart (dashboard-observability D2).
184pub const CIRCUIT_OPEN: &str = "circuit_open";
185
186impl CamelError {
187    pub fn classify(&self) -> &'static str {
188        #[allow(unreachable_patterns)]
189        match self {
190            Self::ComponentNotFound(_) => "component",
191            Self::EndpointCreationFailed(_) | Self::InvalidUri(_) | Self::EndpointUri(_) => {
192                "endpoint"
193            }
194            Self::ProcessorError(_)
195            | Self::ProcessorErrorWithSource(_, _)
196            | Self::AuthProviderUnavailable(_) => "processor",
197            Self::TypeConversionFailed(_) | Self::AlreadyConsumed => "type_conversion",
198            Self::Io(_) => "io",
199            Self::RouteError(_) => "route",
200            Self::CircuitOpen(_) => CIRCUIT_OPEN,
201            Self::HttpOperationFailed { .. } => "http",
202            Self::Config(_) | Self::ConfigValidation(_) => "config",
203            Self::DeadLetterChannelFailed(_) => "dead_letter",
204            Self::ConsumerStopping => "consumer_stop",
205            Self::StreamLimitExceeded(_) => "stream",
206            Self::ChannelClosed => "channel",
207            Self::Unauthenticated(_) => "unauthenticated",
208            Self::Unauthorized(_) => "unauthorized",
209            Self::ValidationError(_) => "validation",
210            Self::TemplateReload(_) => "template",
211            Self::UnsupportedMediaType { .. } => "unsupported_media_type",
212            Self::NotAcceptable { .. } => "not_acceptable",
213            _ => "unknown",
214        }
215    }
216
217    /// Stable variant name used by `doTry` catch-by-variant matchers.
218    ///
219    /// `ProcessorErrorWithSource` and `AuthProviderUnavailable` alias to
220    /// `"ProcessorError"` — the variants are not distinguishable by name in MVP (see spec §5.4),
221    /// so existing `doTry` catch handlers keep matching.
222    ///
223    /// The enum is `#[non_exhaustive]`; this match lives in the defining crate (camel-api),
224    /// so internal exhaustive matching is allowed. Adding a new variant without updating
225    /// this method will fail to compile, surfaced by `variant_name_tests`.
226    pub fn variant_name(&self) -> &'static str {
227        match self {
228            Self::ComponentNotFound(_) => "ComponentNotFound",
229            Self::EndpointCreationFailed(_) => "EndpointCreationFailed",
230            Self::ProcessorError(_) => "ProcessorError",
231            Self::ProcessorErrorWithSource(_, _) => "ProcessorError",
232            Self::AuthProviderUnavailable(_) => "ProcessorError",
233            Self::TypeConversionFailed(_) => "TypeConversionFailed",
234            Self::InvalidUri(_) => "InvalidUri",
235            Self::ChannelClosed => "ChannelClosed",
236            Self::RouteError(_) => "RouteError",
237            Self::Io(_) => "Io",
238            Self::DeadLetterChannelFailed(_) => "DeadLetterChannelFailed",
239            Self::CircuitOpen(_) => "CircuitOpen",
240            Self::HttpOperationFailed { .. } => "HttpOperationFailed",
241            Self::ConsumerStopping => "ConsumerStopping",
242            Self::Config(_) => "Config",
243            Self::ConfigValidation(_) => "ConfigValidation",
244            Self::AlreadyConsumed => "AlreadyConsumed",
245            Self::StreamLimitExceeded(_) => "StreamLimitExceeded",
246            Self::Unauthenticated(_) => "Unauthenticated",
247            Self::Unauthorized(_) => "Unauthorized",
248            Self::ValidationError(_) => "ValidationError",
249            Self::TemplateReload(_) => "TemplateReload",
250            Self::EndpointUri(_) => "EndpointUri",
251            Self::UnsupportedMediaType { .. } => "UnsupportedMediaType",
252            Self::NotAcceptable { .. } => "NotAcceptable",
253        }
254    }
255}
256
257impl From<std::io::Error> for CamelError {
258    fn from(err: std::io::Error) -> Self {
259        CamelError::Io(err.to_string())
260    }
261}
262
263impl From<crate::template::TemplateError> for CamelError {
264    fn from(err: crate::template::TemplateError) -> Self {
265        CamelError::Config(err.to_string())
266    }
267}
268
269impl From<ConfigValidationError> for CamelError {
270    fn from(e: ConfigValidationError) -> Self {
271        CamelError::ConfigValidation(e)
272    }
273}
274
275impl From<EndpointUriError> for CamelError {
276    fn from(e: EndpointUriError) -> Self {
277        CamelError::EndpointUri(e)
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    fn all_error_samples() -> Vec<CamelError> {
286        vec![
287            CamelError::ComponentNotFound("x".to_string()),
288            CamelError::EndpointCreationFailed("x".to_string()),
289            CamelError::ProcessorError("x".to_string()),
290            CamelError::ProcessorErrorWithSource(
291                "x".to_string(),
292                Arc::new(std::io::Error::other("inner")),
293            ),
294            CamelError::TypeConversionFailed("x".to_string()),
295            CamelError::InvalidUri("x".to_string()),
296            CamelError::ChannelClosed,
297            CamelError::RouteError("x".to_string()),
298            CamelError::Io("x".to_string()),
299            CamelError::DeadLetterChannelFailed("x".to_string()),
300            CamelError::CircuitOpen("x".to_string()),
301            CamelError::HttpOperationFailed {
302                method: "GET".to_string(),
303                url: "https://example.com".to_string(),
304                status_code: 500,
305                status_text: "Internal Server Error".to_string(),
306                response_body: Some("error".to_string()),
307            },
308            CamelError::ConsumerStopping,
309            CamelError::Config("x".to_string()),
310            CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero),
311            CamelError::AlreadyConsumed,
312            CamelError::StreamLimitExceeded(42),
313            CamelError::Unauthenticated("token expired".to_string()),
314            CamelError::Unauthorized("missing admin role".to_string()),
315            CamelError::AuthProviderUnavailable("jwks down".to_string()),
316            CamelError::ValidationError("body does not match schema".to_string()),
317            CamelError::TemplateReload("reload failed".to_string()),
318            CamelError::EndpointUri(EndpointUriError::MissingScheme),
319            CamelError::UnsupportedMediaType {
320                consumed: "text/plain".to_string(),
321                declared: "application/json".to_string(),
322            },
323            CamelError::NotAcceptable {
324                accept: "application/xml".to_string(),
325                produced: "application/json".to_string(),
326            },
327        ]
328    }
329
330    #[test]
331    fn test_http_operation_failed_display() {
332        let err = CamelError::HttpOperationFailed {
333            method: "GET".to_string(),
334            url: "https://example.com/test".to_string(),
335            status_code: 404,
336            status_text: "Not Found".to_string(),
337            response_body: Some("page not found".to_string()),
338        };
339        let msg = format!("{err}");
340        assert!(msg.contains("404"));
341        assert!(msg.contains("Not Found"));
342    }
343
344    #[test]
345    fn test_http_operation_failed_clone() {
346        let err = CamelError::HttpOperationFailed {
347            method: "POST".to_string(),
348            url: "https://api.example.com/users".to_string(),
349            status_code: 500,
350            status_text: "Internal Server Error".to_string(),
351            response_body: None,
352        };
353        let cloned = err.clone();
354        assert!(matches!(
355            cloned,
356            CamelError::HttpOperationFailed {
357                status_code: 500,
358                ..
359            }
360        ));
361    }
362
363    #[test]
364    fn test_classify_maps_all_variants() {
365        assert_eq!(
366            CamelError::ComponentNotFound("x".to_string()).classify(),
367            "component"
368        );
369        assert_eq!(
370            CamelError::EndpointCreationFailed("x".to_string()).classify(),
371            "endpoint"
372        );
373        assert_eq!(
374            CamelError::ProcessorError("x".to_string()).classify(),
375            "processor"
376        );
377        assert_eq!(
378            CamelError::TypeConversionFailed("x".to_string()).classify(),
379            "type_conversion"
380        );
381        assert_eq!(
382            CamelError::InvalidUri("x".to_string()).classify(),
383            "endpoint"
384        );
385        assert_eq!(CamelError::ChannelClosed.classify(), "channel");
386        assert_eq!(CamelError::RouteError("x".to_string()).classify(), "route");
387        assert_eq!(CamelError::Io("x".to_string()).classify(), "io");
388        assert_eq!(
389            CamelError::DeadLetterChannelFailed("x".to_string()).classify(),
390            "dead_letter"
391        );
392        assert_eq!(
393            CamelError::CircuitOpen("x".to_string()).classify(),
394            "circuit_open"
395        );
396        assert_eq!(
397            CamelError::HttpOperationFailed {
398                method: "GET".to_string(),
399                url: "https://example.com".to_string(),
400                status_code: 500,
401                status_text: "Internal Server Error".to_string(),
402                response_body: None,
403            }
404            .classify(),
405            "http"
406        );
407        assert_eq!(CamelError::Config("x".to_string()).classify(), "config");
408        assert_eq!(
409            CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero)
410                .classify(),
411            "config"
412        );
413        assert_eq!(CamelError::AlreadyConsumed.classify(), "type_conversion");
414        assert_eq!(CamelError::StreamLimitExceeded(42).classify(), "stream");
415        assert_eq!(
416            CamelError::ValidationError("bad".to_string()).classify(),
417            "validation"
418        );
419    }
420
421    #[test]
422    fn test_classify_output_is_ascii_and_short() {
423        for error in all_error_samples() {
424            let class = error.classify();
425            assert!(class.is_ascii());
426            // "unsupported_media_type" (REST negotiation, L2) sets the floor at 22
427            assert!(class.len() <= 22, "class too long: {class}");
428        }
429    }
430
431    #[test]
432    fn test_auth_variants_classify() {
433        assert_eq!(
434            CamelError::Unauthenticated("x".to_string()).classify(),
435            "unauthenticated"
436        );
437        assert_eq!(
438            CamelError::Unauthorized("x".to_string()).classify(),
439            "unauthorized"
440        );
441    }
442
443    #[test]
444    fn test_validation_error_classify() {
445        assert_eq!(
446            CamelError::ValidationError("bad".to_string()).classify(),
447            "validation"
448        );
449    }
450
451    #[test]
452    fn template_reload_classifies_as_template() {
453        let err = CamelError::TemplateReload("boom".into());
454        assert_eq!(err.classify(), "template");
455    }
456
457    #[test]
458    fn template_reload_variant_name() {
459        let err = CamelError::TemplateReload("boom".into());
460        assert_eq!(err.variant_name(), "TemplateReload");
461    }
462
463    #[test]
464    fn test_auth_variants_are_clone() {
465        let err = CamelError::Unauthenticated("test".to_string());
466        let cloned = err.clone();
467        assert!(matches!(cloned, CamelError::Unauthenticated(_)));
468
469        let err2 = CamelError::Unauthorized("test".to_string());
470        let cloned2 = err2.clone();
471        assert!(matches!(cloned2, CamelError::Unauthorized(_)));
472    }
473
474    #[test]
475    fn classification_unchanged_for_callers() {
476        // Pins the contract the pipeline-tracer circuit_open exclusion
477        // relies on (dashboard-observability D2): CircuitOpen must keep
478        // classifying as "circuit_open" — callers and the tracer skip
479        // branch match on exactly this literal.
480        assert_eq!(
481            CamelError::CircuitOpen("breaker open".into()).classify(),
482            "circuit_open"
483        );
484    }
485
486    #[test]
487    fn auth_provider_unavailable_display_carries_detail() {
488        let err = CamelError::AuthProviderUnavailable("conn refused".into());
489        let msg = err.to_string();
490        assert!(msg.contains("conn refused"));
491        assert!(
492            msg.starts_with("Auth provider unavailable"),
493            "display should start with 'Auth provider unavailable', got: {msg}"
494        );
495    }
496
497    #[test]
498    fn classify_negotiation_errors() {
499        let unsupported = CamelError::UnsupportedMediaType {
500            consumed: "text/plain".into(),
501            declared: "application/json".into(),
502        };
503        let not_acceptable = CamelError::NotAcceptable {
504            accept: "application/xml".into(),
505            produced: "application/json".into(),
506        };
507        assert_eq!(unsupported.classify(), "unsupported_media_type");
508        assert_eq!(not_acceptable.classify(), "not_acceptable");
509    }
510
511    #[test]
512    fn variant_names_negotiation_errors() {
513        let unsupported = CamelError::UnsupportedMediaType {
514            consumed: "text/plain".into(),
515            declared: "application/json".into(),
516        };
517        let not_acceptable = CamelError::NotAcceptable {
518            accept: "application/xml".into(),
519            produced: "application/json".into(),
520        };
521        assert_eq!(unsupported.variant_name(), "UnsupportedMediaType");
522        assert_eq!(not_acceptable.variant_name(), "NotAcceptable");
523    }
524
525    #[test]
526    fn display_negotiation_errors() {
527        let unsupported = CamelError::UnsupportedMediaType {
528            consumed: "text/plain".into(),
529            declared: "application/json".into(),
530        };
531        let not_acceptable = CamelError::NotAcceptable {
532            accept: "application/xml".into(),
533            produced: "application/json".into(),
534        };
535        let unsupported_msg = unsupported.to_string();
536        assert!(unsupported_msg.contains("text/plain"));
537        assert!(unsupported_msg.contains("application/json"));
538        let not_acceptable_msg = not_acceptable.to_string();
539        assert!(not_acceptable_msg.contains("application/xml"));
540        assert!(not_acceptable_msg.contains("application/json"));
541    }
542}
543
544#[cfg(test)]
545mod variant_name_tests {
546    use super::{CamelError, ConfigValidationError, EndpointUriError};
547    use std::sync::Arc;
548
549    /// Representative value for each enum variant. This test fails to compile
550    /// when a new variant is added to CamelError without updating variant_name().
551    /// The enum is `#[non_exhaustive]` but this match lives in the same crate, so internal
552    /// exhaustive matching is allowed.
553    #[test]
554    fn variant_name_covers_all_variants() {
555        let cases: Vec<(CamelError, &str)> = vec![
556            (
557                CamelError::ComponentNotFound("x".into()),
558                "ComponentNotFound",
559            ),
560            (
561                CamelError::EndpointCreationFailed("x".into()),
562                "EndpointCreationFailed",
563            ),
564            (CamelError::ProcessorError("x".into()), "ProcessorError"),
565            (
566                CamelError::ProcessorErrorWithSource(
567                    "x".into(),
568                    Arc::new(std::io::Error::other("y")),
569                ),
570                "ProcessorError", // aliased
571            ),
572            (
573                CamelError::TypeConversionFailed("x".into()),
574                "TypeConversionFailed",
575            ),
576            (CamelError::InvalidUri("x".into()), "InvalidUri"),
577            (CamelError::ChannelClosed, "ChannelClosed"),
578            (CamelError::RouteError("x".into()), "RouteError"),
579            (CamelError::Io("x".into()), "Io"),
580            (
581                CamelError::DeadLetterChannelFailed("x".into()),
582                "DeadLetterChannelFailed",
583            ),
584            (CamelError::CircuitOpen("x".into()), "CircuitOpen"),
585            (
586                CamelError::HttpOperationFailed {
587                    method: "GET".into(),
588                    url: "https://example.com".into(),
589                    status_code: 500,
590                    status_text: "Internal Server Error".into(),
591                    response_body: None,
592                },
593                "HttpOperationFailed",
594            ),
595            (CamelError::Config("x".into()), "Config"),
596            (
597                CamelError::ConfigValidation(ConfigValidationError::ThrottlerMaxRequestsZero),
598                "ConfigValidation",
599            ),
600            (CamelError::AlreadyConsumed, "AlreadyConsumed"),
601            (CamelError::StreamLimitExceeded(42), "StreamLimitExceeded"),
602            (CamelError::Unauthenticated("x".into()), "Unauthenticated"),
603            (CamelError::Unauthorized("x".into()), "Unauthorized"),
604            (CamelError::ValidationError("bad".into()), "ValidationError"),
605            (CamelError::TemplateReload("x".into()), "TemplateReload"),
606            (
607                CamelError::EndpointUri(EndpointUriError::MissingScheme),
608                "EndpointUri",
609            ),
610            (
611                CamelError::UnsupportedMediaType {
612                    consumed: "text/plain".into(),
613                    declared: "application/json".into(),
614                },
615                "UnsupportedMediaType",
616            ),
617            (
618                CamelError::NotAcceptable {
619                    accept: "application/xml".into(),
620                    produced: "application/json".into(),
621                },
622                "NotAcceptable",
623            ),
624            (
625                CamelError::AuthProviderUnavailable("x".into()),
626                "ProcessorError",
627            ),
628        ];
629
630        for (err, expected) in cases {
631            assert_eq!(
632                err.variant_name(),
633                expected,
634                "variant_name mismatch for {:?}",
635                err
636            );
637        }
638    }
639
640    #[test]
641    fn auth_provider_unavailable_classifies_as_processor() {
642        let err = CamelError::AuthProviderUnavailable("jwks down".into());
643        assert_eq!(err.classify(), "processor");
644    }
645
646    #[test]
647    fn auth_provider_unavailable_variant_name_aliases_processor_error() {
648        let err = CamelError::AuthProviderUnavailable("jwks down".into());
649        assert_eq!(err.variant_name(), "ProcessorError");
650    }
651}