connectrpc_axum/
error.rs

1use axum::{
2    Json,
3    body::Body,
4    http::{HeaderMap, HeaderName, HeaderValue, StatusCode, header},
5    response::{IntoResponse, Response},
6};
7use serde::{Serialize, Serializer};
8
9use crate::context::RequestProtocol;
10
11/// Connect RPC error codes, matching the codes defined in the Connect protocol.
12#[derive(Clone, Copy, Debug, Serialize)]
13#[serde(rename_all = "snake_case")]
14pub enum Code {
15    Ok = 0,
16    Canceled = 1,
17    Unknown = 2,
18    InvalidArgument = 3,
19    DeadlineExceeded = 4,
20    NotFound = 5,
21    AlreadyExists = 6,
22    PermissionDenied = 7,
23    ResourceExhausted = 8,
24    FailedPrecondition = 9,
25    Aborted = 10,
26    OutOfRange = 11,
27    Unimplemented = 12,
28    Internal = 13,
29    Unavailable = 14,
30    DataLoss = 15,
31    Unauthenticated = 16,
32}
33
34/// A self-describing error detail following the Connect protocol.
35///
36/// Error details are structured Protobuf messages attached to errors,
37/// allowing clients to receive strongly-typed error information.
38/// This maps to `google.protobuf.Any` on the wire.
39///
40/// # Wire Format
41///
42/// Details are serialized as JSON objects with `type` and `value` fields:
43/// ```json
44/// {"type": "google.rpc.RetryInfo", "value": "base64-encoded-protobuf"}
45/// ```
46///
47/// # Example
48///
49/// ```ignore
50/// use prost::Message;
51///
52/// // Encode a google.rpc.RetryInfo message
53/// let retry_delay = prost_types::Duration { seconds: 5, nanos: 0 };
54/// let mut bytes = Vec::new();
55/// // ... encode RetryInfo with retry_delay field
56///
57/// let detail = ErrorDetail::new("google.rpc.RetryInfo", bytes);
58/// ```
59#[derive(Clone, Debug)]
60pub struct ErrorDetail {
61    /// Fully-qualified type name (e.g., "google.rpc.RetryInfo").
62    type_url: String,
63    /// Protobuf-encoded message bytes.
64    value: Vec<u8>,
65}
66
67impl ErrorDetail {
68    /// Create a new error detail with a type URL and protobuf-encoded bytes.
69    pub fn new<S: Into<String>>(type_url: S, value: Vec<u8>) -> Self {
70        Self {
71            type_url: type_url.into(),
72            value,
73        }
74    }
75
76    /// Get the fully-qualified type name.
77    pub fn type_url(&self) -> &str {
78        &self.type_url
79    }
80
81    /// Get the protobuf-encoded value bytes.
82    pub fn value(&self) -> &[u8] {
83        &self.value
84    }
85}
86
87/// An error that captures the key pieces of information for Connect RPC:
88/// a code, an optional message, metadata (HTTP headers), and optional error details.
89#[derive(Clone, Debug)]
90pub struct ConnectError {
91    code: Code,
92    message: Option<String>,
93    details: Vec<ErrorDetail>,
94    meta: Option<HeaderMap>,
95}
96
97impl ConnectError {
98    /// Create a new error with a code and message.
99    pub fn new<S: Into<String>>(code: Code, message: S) -> Self {
100        Self {
101            code,
102            message: Some(message.into()),
103            details: vec![],
104            meta: None,
105        }
106    }
107
108    /// Create a new error with just a code.
109    pub fn from_code(code: Code) -> Self {
110        Self {
111            code,
112            message: None,
113            details: vec![],
114            meta: None,
115        }
116    }
117
118    /// Create an unimplemented error.
119    pub fn new_unimplemented() -> Self {
120        Self {
121            code: Code::Unimplemented,
122            message: Some("The requested service has not been implemented.".to_string()),
123            details: vec![],
124            meta: None,
125        }
126    }
127
128    /// Create an invalid argument error.
129    pub fn new_invalid_argument<S: Into<String>>(message: S) -> Self {
130        Self::new(Code::InvalidArgument, message)
131    }
132
133    /// Create a not found error.
134    pub fn new_not_found<S: Into<String>>(message: S) -> Self {
135        Self::new(Code::NotFound, message)
136    }
137
138    /// Create a permission denied error.
139    pub fn new_permission_denied<S: Into<String>>(message: S) -> Self {
140        Self::new(Code::PermissionDenied, message)
141    }
142
143    /// Create an unauthenticated error.
144    pub fn new_unauthenticated<S: Into<String>>(message: S) -> Self {
145        Self::new(Code::Unauthenticated, message)
146    }
147
148    /// Create an internal error.
149    pub fn new_internal<S: Into<String>>(message: S) -> Self {
150        Self::new(Code::Internal, message)
151    }
152
153    /// Create an unavailable error.
154    pub fn new_unavailable<S: Into<String>>(message: S) -> Self {
155        Self::new(Code::Unavailable, message)
156    }
157
158    /// Create an already exists error.
159    pub fn new_already_exists<S: Into<String>>(message: S) -> Self {
160        Self::new(Code::AlreadyExists, message)
161    }
162
163    /// Create a resource exhausted error.
164    pub fn new_resource_exhausted<S: Into<String>>(message: S) -> Self {
165        Self::new(Code::ResourceExhausted, message)
166    }
167
168    /// Create a failed precondition error.
169    pub fn new_failed_precondition<S: Into<String>>(message: S) -> Self {
170        Self::new(Code::FailedPrecondition, message)
171    }
172
173    /// Create an aborted error.
174    pub fn new_aborted<S: Into<String>>(message: S) -> Self {
175        Self::new(Code::Aborted, message)
176    }
177
178    /// Get the error code.
179    pub fn code(&self) -> Code {
180        self.code
181    }
182
183    /// Get the error message.
184    pub fn message(&self) -> Option<&str> {
185        self.message.as_deref()
186    }
187
188    /// Get the error details.
189    pub fn details(&self) -> &[ErrorDetail] {
190        &self.details
191    }
192
193    /// Add an error detail with type URL and protobuf-encoded bytes.
194    ///
195    /// # Example
196    ///
197    /// ```ignore
198    /// use prost::Message;
199    ///
200    /// let duration = prost_types::Duration { seconds: 5, nanos: 0 };
201    /// let mut bytes = Vec::new();
202    /// duration.encode(&mut bytes).unwrap();
203    ///
204    /// // Wrap in RetryInfo (field 1)
205    /// let mut retry_info_bytes = vec![0x0a, bytes.len() as u8];
206    /// retry_info_bytes.extend(bytes);
207    ///
208    /// let err = ConnectError::new(Code::ResourceExhausted, "rate limited")
209    ///     .add_detail("google.rpc.RetryInfo", retry_info_bytes);
210    /// ```
211    pub fn add_detail<S: Into<String>>(mut self, type_url: S, value: Vec<u8>) -> Self {
212        self.details.push(ErrorDetail::new(type_url, value));
213        self
214    }
215
216    /// Add a pre-constructed ErrorDetail.
217    pub fn add_error_detail(mut self, detail: ErrorDetail) -> Self {
218        self.details.push(detail);
219        self
220    }
221
222    /// Get the metadata headers, if any.
223    pub fn meta(&self) -> Option<&HeaderMap> {
224        self.meta.as_ref()
225    }
226
227    /// Get mutable access to metadata headers.
228    /// Lazily initializes the HeaderMap if not present.
229    pub fn meta_mut(&mut self) -> &mut HeaderMap {
230        self.meta.get_or_insert_with(HeaderMap::new)
231    }
232
233    /// Add a metadata header.
234    pub fn with_meta<K, V>(mut self, key: K, value: V) -> Self
235    where
236        K: AsRef<str>,
237        V: AsRef<str>,
238    {
239        let key_str = key.as_ref();
240        let val_str = value.as_ref();
241
242        match HeaderName::from_bytes(key_str.as_bytes()) {
243            Ok(name) => match HeaderValue::from_str(val_str) {
244                Ok(val) => {
245                    self.meta_mut().append(name, val);
246                }
247                Err(e) => {
248                    tracing::debug!(
249                        key = key_str,
250                        value = val_str,
251                        error = %e,
252                        "invalid header value, metadata dropped"
253                    );
254                }
255            },
256            Err(e) => {
257                tracing::debug!(
258                    key = key_str,
259                    error = %e,
260                    "invalid header name, metadata dropped"
261                );
262            }
263        }
264        self
265    }
266
267    /// Set metadata from HeaderMap.
268    pub fn set_meta_from_headers(mut self, headers: &HeaderMap) -> Self {
269        self.meta = Some(headers.clone());
270        self
271    }
272}
273
274impl ConnectError {
275    /// Convert this error into an HTTP response using the specified protocol.
276    ///
277    /// This is the primary method used by handler wrappers to convert errors
278    /// to responses with the correct encoding based on the request protocol.
279    pub(crate) fn into_response_with_protocol(self, protocol: RequestProtocol) -> Response {
280        // For streaming protocols, errors must be returned as EndStream frames
281        // with HTTP 200, not as HTTP error status codes
282        if protocol.is_streaming() {
283            return self.into_streaming_error_response(protocol);
284        }
285
286        // For unary protocols, use HTTP status codes
287        let status_code = self.http_status_code();
288
289        // Create the error response body
290        let error_body = ErrorResponseBody {
291            code: self.code,
292            message: self.message,
293            details: self.details,
294        };
295
296        // Start with the base response
297        let mut response = (status_code, Json(error_body)).into_response();
298
299        // Set the correct content-type for errors
300        response.headers_mut().insert(
301            header::CONTENT_TYPE,
302            HeaderValue::from_static(protocol.error_content_type()),
303        );
304
305        // Add metadata as headers
306        if let Some(meta) = &self.meta {
307            let headers = response.headers_mut();
308            headers.extend(meta.iter().map(|(k, v)| (k.clone(), v.clone())));
309        }
310
311        response
312    }
313}
314
315impl IntoResponse for ConnectError {
316    fn into_response(self) -> Response {
317        // Fallback to default protocol (ConnectUnaryJson)
318        // Handler wrappers should use into_response_with_protocol() instead
319        self.into_response_with_protocol(RequestProtocol::default())
320    }
321}
322
323impl ConnectError {
324    /// Convert error code to HTTP status code (for unary responses only)
325    fn http_status_code(&self) -> StatusCode {
326        match self.code {
327            Code::Ok => StatusCode::OK,
328            // 499 Client Closed Request (nginx extension) - client canceled the operation
329            Code::Canceled => StatusCode::from_u16(499).unwrap(),
330            Code::Unknown => StatusCode::INTERNAL_SERVER_ERROR,
331            Code::InvalidArgument => StatusCode::BAD_REQUEST,
332            // 504 Gateway Timeout - server-side deadline exceeded
333            Code::DeadlineExceeded => StatusCode::GATEWAY_TIMEOUT,
334            Code::NotFound => StatusCode::NOT_FOUND,
335            Code::AlreadyExists => StatusCode::CONFLICT,
336            Code::PermissionDenied => StatusCode::FORBIDDEN,
337            Code::ResourceExhausted => StatusCode::TOO_MANY_REQUESTS,
338            Code::FailedPrecondition => StatusCode::BAD_REQUEST,
339            Code::Aborted => StatusCode::CONFLICT,
340            Code::OutOfRange => StatusCode::BAD_REQUEST,
341            Code::Unimplemented => StatusCode::NOT_IMPLEMENTED,
342            Code::Internal => StatusCode::INTERNAL_SERVER_ERROR,
343            Code::Unavailable => StatusCode::SERVICE_UNAVAILABLE,
344            Code::DataLoss => StatusCode::INTERNAL_SERVER_ERROR,
345            Code::Unauthenticated => StatusCode::UNAUTHORIZED,
346        }
347    }
348
349    /// Create a streaming error response with proper EndStream framing.
350    ///
351    /// Per the Connect protocol, streaming responses must:
352    /// - Always return HTTP 200
353    /// - Use application/connect+json or application/connect+proto content-type
354    /// - Deliver errors in an EndStream frame (flags = 0x02)
355    ///
356    /// This method should be used by streaming handlers when returning errors
357    /// before the stream has started. The `use_proto` flag determines the
358    /// response encoding (protobuf vs JSON).
359    pub fn into_streaming_response(self, use_proto: bool) -> Response {
360        let content_type = if use_proto {
361            "application/connect+proto"
362        } else {
363            "application/connect+json"
364        };
365        self.into_streaming_error_response_with_content_type(content_type)
366    }
367
368    /// Internal helper for creating streaming error responses.
369    fn into_streaming_error_response(self, protocol: crate::context::RequestProtocol) -> Response {
370        self.into_streaming_error_response_with_content_type(protocol.error_content_type())
371    }
372
373    /// Create a streaming error response with the specified content-type.
374    fn into_streaming_error_response_with_content_type(
375        self,
376        content_type: &'static str,
377    ) -> Response {
378        // Use build_end_stream_frame which properly includes error metadata in the
379        // EndStream JSON payload's "metadata" field (per Connect protocol spec)
380        let frame = crate::pipeline::build_end_stream_frame(Some(&self), None);
381
382        // Build the response with HTTP 200 and streaming content-type
383        Response::builder()
384            .status(StatusCode::OK)
385            .header(header::CONTENT_TYPE, HeaderValue::from_static(content_type))
386            .body(Body::from(frame))
387            .unwrap_or_else(|_| internal_error_streaming_response(content_type))
388    }
389}
390
391// ---- Conversions ----
392
393impl From<std::convert::Infallible> for ConnectError {
394    fn from(infallible: std::convert::Infallible) -> Self {
395        match infallible {}
396    }
397}
398
399impl From<(StatusCode, String)> for ConnectError {
400    /// Convert an HTTP status code and message into a ConnectError.
401    ///
402    /// This provides a simple DX helper to lift common HTTP errors into
403    /// Connect's error space.
404    fn from(value: (StatusCode, String)) -> Self {
405        let (status, message) = value;
406        ConnectError::new(status.into(), message)
407    }
408}
409
410impl From<StatusCode> for Code {
411    fn from(status: StatusCode) -> Self {
412        match status {
413            StatusCode::OK => Code::Ok,
414            StatusCode::BAD_REQUEST => Code::InvalidArgument,
415            StatusCode::UNAUTHORIZED => Code::Unauthenticated,
416            StatusCode::FORBIDDEN => Code::PermissionDenied,
417            StatusCode::NOT_FOUND => Code::NotFound,
418            StatusCode::CONFLICT => Code::AlreadyExists,
419            StatusCode::REQUEST_TIMEOUT => Code::DeadlineExceeded,
420            StatusCode::TOO_MANY_REQUESTS => Code::ResourceExhausted,
421            StatusCode::NOT_IMPLEMENTED => Code::Unimplemented,
422            StatusCode::SERVICE_UNAVAILABLE => Code::Unavailable,
423            StatusCode::INTERNAL_SERVER_ERROR => Code::Internal,
424            _ => Code::Unknown,
425        }
426    }
427}
428
429/// The JSON body structure for error responses.
430#[derive(Serialize)]
431struct ErrorResponseBody {
432    code: Code,
433    #[serde(skip_serializing_if = "Option::is_none")]
434    message: Option<String>,
435    #[serde(skip_serializing_if = "Vec::is_empty")]
436    details: Vec<ErrorDetail>,
437}
438
439impl Serialize for ErrorDetail {
440    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
441    where
442        S: Serializer,
443    {
444        use base64::Engine;
445        use serde::ser::SerializeStruct;
446
447        let mut s = serializer.serialize_struct("ErrorDetail", 2)?;
448
449        // Strip "type.googleapis.com/" prefix if present (Connect uses short type names)
450        let type_name = self
451            .type_url
452            .strip_prefix("type.googleapis.com/")
453            .unwrap_or(&self.type_url);
454        s.serialize_field("type", type_name)?;
455
456        // Connect protocol uses raw base64 (no padding)
457        let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(&self.value);
458        s.serialize_field("value", &encoded)?;
459
460        s.end()
461    }
462}
463
464impl Serialize for ConnectError {
465    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
466    where
467        S: Serializer,
468    {
469        // Serialize only the parts that should go in the JSON body
470        ErrorResponseBody {
471            code: self.code,
472            message: self.message.clone(),
473            details: self.details.clone(),
474        }
475        .serialize(serializer)
476    }
477}
478
479// ---- Conversions from tonic types (feature-gated) ----
480#[cfg(feature = "tonic")]
481impl From<::tonic::Code> for Code {
482    fn from(code: ::tonic::Code) -> Self {
483        match code {
484            ::tonic::Code::Ok => Code::Ok,
485            ::tonic::Code::Cancelled => Code::Canceled,
486            ::tonic::Code::Unknown => Code::Unknown,
487            ::tonic::Code::InvalidArgument => Code::InvalidArgument,
488            ::tonic::Code::DeadlineExceeded => Code::DeadlineExceeded,
489            ::tonic::Code::NotFound => Code::NotFound,
490            ::tonic::Code::AlreadyExists => Code::AlreadyExists,
491            ::tonic::Code::PermissionDenied => Code::PermissionDenied,
492            ::tonic::Code::ResourceExhausted => Code::ResourceExhausted,
493            ::tonic::Code::FailedPrecondition => Code::FailedPrecondition,
494            ::tonic::Code::Aborted => Code::Aborted,
495            ::tonic::Code::OutOfRange => Code::OutOfRange,
496            ::tonic::Code::Unimplemented => Code::Unimplemented,
497            ::tonic::Code::Internal => Code::Internal,
498            ::tonic::Code::Unavailable => Code::Unavailable,
499            ::tonic::Code::DataLoss => Code::DataLoss,
500            ::tonic::Code::Unauthenticated => Code::Unauthenticated,
501        }
502    }
503}
504
505#[cfg(feature = "tonic")]
506impl From<::tonic::Status> for ConnectError {
507    fn from(status: ::tonic::Status) -> Self {
508        let code: Code = status.code().into();
509        let msg = status.message().to_string();
510
511        // Note: Tonic status can carry metadata, but Connect error metadata is HTTP headers.
512        // We currently carry just code + message to align with Connect JSON shape.
513        // Details are not directly accessible from `tonic::Status`.
514        if msg.is_empty() {
515            ConnectError::from_code(code)
516        } else {
517            ConnectError::new(code, msg)
518        }
519    }
520}
521
522#[cfg(feature = "tonic")]
523impl From<Code> for ::tonic::Code {
524    fn from(code: Code) -> Self {
525        match code {
526            Code::Ok => ::tonic::Code::Ok,
527            Code::Canceled => ::tonic::Code::Cancelled,
528            Code::Unknown => ::tonic::Code::Unknown,
529            Code::InvalidArgument => ::tonic::Code::InvalidArgument,
530            Code::DeadlineExceeded => ::tonic::Code::DeadlineExceeded,
531            Code::NotFound => ::tonic::Code::NotFound,
532            Code::AlreadyExists => ::tonic::Code::AlreadyExists,
533            Code::PermissionDenied => ::tonic::Code::PermissionDenied,
534            Code::ResourceExhausted => ::tonic::Code::ResourceExhausted,
535            Code::FailedPrecondition => ::tonic::Code::FailedPrecondition,
536            Code::Aborted => ::tonic::Code::Aborted,
537            Code::OutOfRange => ::tonic::Code::OutOfRange,
538            Code::Unimplemented => ::tonic::Code::Unimplemented,
539            Code::Internal => ::tonic::Code::Internal,
540            Code::Unavailable => ::tonic::Code::Unavailable,
541            Code::DataLoss => ::tonic::Code::DataLoss,
542            Code::Unauthenticated => ::tonic::Code::Unauthenticated,
543        }
544    }
545}
546
547#[cfg(feature = "tonic")]
548impl From<ConnectError> for ::tonic::Status {
549    fn from(err: ConnectError) -> Self {
550        let code: ::tonic::Code = err.code().into();
551        ::tonic::Status::new(code, err.message().unwrap_or("").to_string())
552    }
553}
554
555// ---- Safe fallback responses for serialization/encoding failures ----
556
557/// Create a safe 500 Internal Server Error response for unary requests.
558///
559/// This is used when serialization or encoding fails and we cannot produce
560/// a proper ConnectError response. The body is a hardcoded JSON string that
561/// cannot fail to serialize.
562pub(crate) fn internal_error_response(content_type: &'static str) -> Response {
563    // Hardcoded JSON that cannot fail - no dynamic content
564    const ERROR_BODY: &[u8] = br#"{"code":"internal","message":"Internal serialization error"}"#;
565
566    Response::builder()
567        .status(StatusCode::INTERNAL_SERVER_ERROR)
568        .header(header::CONTENT_TYPE, HeaderValue::from_static(content_type))
569        .body(Body::from(ERROR_BODY.to_vec()))
570        // This cannot fail: status is valid, header is valid static strings, body is valid bytes
571        .unwrap_or_else(|_| {
572            // Ultimate fallback - empty 500 response
573            let mut response = Response::new(Body::empty());
574            *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
575            response
576        })
577}
578
579/// Create a safe EndStream error frame for streaming responses.
580///
581/// This is used when encoding a message in a stream fails. Returns bytes
582/// for an EndStream frame (flags=0x02) containing an internal error.
583pub(crate) fn internal_error_end_stream_frame() -> Vec<u8> {
584    // Hardcoded EndStream JSON payload that cannot fail
585    const ERROR_PAYLOAD: &[u8] =
586        br#"{"error":{"code":"internal","message":"Internal serialization error"}}"#;
587
588    let mut frame = Vec::with_capacity(5 + ERROR_PAYLOAD.len());
589    frame.push(0b0000_0010); // EndStream flag
590    frame.extend_from_slice(&(ERROR_PAYLOAD.len() as u32).to_be_bytes());
591    frame.extend_from_slice(ERROR_PAYLOAD);
592    frame
593}
594
595/// Create a safe streaming response with an internal error EndStream frame.
596///
597/// This is used when we cannot build a proper streaming response and need
598/// to return a safe fallback.
599pub(crate) fn internal_error_streaming_response(content_type: &'static str) -> Response {
600    let frame = internal_error_end_stream_frame();
601
602    Response::builder()
603        .status(StatusCode::OK)
604        .header(header::CONTENT_TYPE, HeaderValue::from_static(content_type))
605        .body(Body::from(frame))
606        .unwrap_or_else(|_| {
607            // Ultimate fallback - empty 200 response
608            Response::new(Body::empty())
609        })
610}
611
612#[cfg(test)]
613mod tests {
614    use super::*;
615    use axum::http::HeaderMap;
616
617    // ---- Code and ConnectError basic tests ----
618
619    #[test]
620    fn test_connect_error_new() {
621        let err = ConnectError::new(Code::NotFound, "resource not found");
622        assert!(matches!(err.code(), Code::NotFound));
623        assert_eq!(err.message(), Some("resource not found"));
624        assert!(err.details().is_empty());
625        assert!(err.meta().is_none());
626    }
627
628    #[test]
629    fn test_connect_error_from_code() {
630        let err = ConnectError::from_code(Code::Internal);
631        assert!(matches!(err.code(), Code::Internal));
632        assert!(err.message().is_none());
633    }
634
635    #[test]
636    fn test_connect_error_convenience_constructors() {
637        let err = ConnectError::new_unimplemented();
638        assert!(matches!(err.code(), Code::Unimplemented));
639        assert!(err.message().is_some());
640
641        let err = ConnectError::new_invalid_argument("bad input");
642        assert!(matches!(err.code(), Code::InvalidArgument));
643        assert_eq!(err.message(), Some("bad input"));
644
645        let err = ConnectError::new_not_found("missing");
646        assert!(matches!(err.code(), Code::NotFound));
647
648        let err = ConnectError::new_permission_denied("forbidden");
649        assert!(matches!(err.code(), Code::PermissionDenied));
650
651        let err = ConnectError::new_unauthenticated("no auth");
652        assert!(matches!(err.code(), Code::Unauthenticated));
653
654        let err = ConnectError::new_internal("server error");
655        assert!(matches!(err.code(), Code::Internal));
656
657        let err = ConnectError::new_unavailable("try later");
658        assert!(matches!(err.code(), Code::Unavailable));
659
660        let err = ConnectError::new_already_exists("duplicate");
661        assert!(matches!(err.code(), Code::AlreadyExists));
662
663        let err = ConnectError::new_resource_exhausted("quota exceeded");
664        assert!(matches!(err.code(), Code::ResourceExhausted));
665
666        let err = ConnectError::new_failed_precondition("precondition failed");
667        assert!(matches!(err.code(), Code::FailedPrecondition));
668
669        let err = ConnectError::new_aborted("aborted");
670        assert!(matches!(err.code(), Code::Aborted));
671    }
672
673    // ---- Metadata tests ----
674
675    #[test]
676    fn test_with_meta_valid_headers() {
677        let err = ConnectError::new(Code::Internal, "error")
678            .with_meta("x-request-id", "req-123")
679            .with_meta("x-trace-id", "trace-456");
680
681        let meta = err.meta().expect("metadata should be present");
682        assert_eq!(meta.get("x-request-id").unwrap(), "req-123");
683        assert_eq!(meta.get("x-trace-id").unwrap(), "trace-456");
684    }
685
686    #[test]
687    fn test_with_meta_invalid_header_name_is_dropped() {
688        // Invalid header name (contains spaces) should be silently dropped
689        let err = ConnectError::new(Code::Internal, "error")
690            .with_meta("invalid header", "value")
691            .with_meta("x-valid", "kept");
692
693        let meta = err.meta().expect("metadata should be present");
694        assert!(meta.get("invalid header").is_none());
695        assert_eq!(meta.get("x-valid").unwrap(), "kept");
696    }
697
698    #[test]
699    fn test_with_meta_invalid_header_value_is_dropped() {
700        // Invalid header value (contains non-visible ASCII) should be silently dropped
701        let err = ConnectError::new(Code::Internal, "error")
702            .with_meta("x-bad-value", "value\x00with\x01null")
703            .with_meta("x-valid", "kept");
704
705        let meta = err.meta().expect("metadata should be present");
706        assert!(meta.get("x-bad-value").is_none());
707        assert_eq!(meta.get("x-valid").unwrap(), "kept");
708    }
709
710    #[test]
711    fn test_with_meta_multiple_values_same_key() {
712        let err = ConnectError::new(Code::Internal, "error")
713            .with_meta("x-multi", "value1")
714            .with_meta("x-multi", "value2");
715
716        let meta = err.meta().expect("metadata should be present");
717        let values: Vec<_> = meta.get_all("x-multi").iter().collect();
718        assert_eq!(values.len(), 2);
719        assert_eq!(values[0], "value1");
720        assert_eq!(values[1], "value2");
721    }
722
723    #[test]
724    fn test_meta_mut() {
725        let mut err = ConnectError::new(Code::Internal, "error");
726
727        // First call initializes the map
728        err.meta_mut().insert(
729            HeaderName::from_static("x-custom"),
730            HeaderValue::from_static("value"),
731        );
732
733        assert!(err.meta().is_some());
734        assert_eq!(err.meta().unwrap().get("x-custom").unwrap(), "value");
735
736        // Second call returns the same map
737        err.meta_mut().insert(
738            HeaderName::from_static("x-another"),
739            HeaderValue::from_static("value2"),
740        );
741
742        assert_eq!(err.meta().unwrap().len(), 2);
743    }
744
745    #[test]
746    fn test_set_meta_from_headers() {
747        let mut headers = HeaderMap::new();
748        headers.insert(
749            HeaderName::from_static("x-from-map"),
750            HeaderValue::from_static("map-value"),
751        );
752        headers.insert(
753            HeaderName::from_static("x-another"),
754            HeaderValue::from_static("another-value"),
755        );
756
757        let err = ConnectError::new(Code::Internal, "error").set_meta_from_headers(&headers);
758
759        let meta = err.meta().expect("metadata should be present");
760        assert_eq!(meta.get("x-from-map").unwrap(), "map-value");
761        assert_eq!(meta.get("x-another").unwrap(), "another-value");
762    }
763
764    #[test]
765    fn test_set_meta_from_headers_replaces_existing() {
766        let err = ConnectError::new(Code::Internal, "error").with_meta("x-old", "old-value");
767
768        let mut headers = HeaderMap::new();
769        headers.insert(
770            HeaderName::from_static("x-new"),
771            HeaderValue::from_static("new-value"),
772        );
773
774        let err = err.set_meta_from_headers(&headers);
775
776        let meta = err.meta().expect("metadata should be present");
777        // Old metadata is replaced
778        assert!(meta.get("x-old").is_none());
779        assert_eq!(meta.get("x-new").unwrap(), "new-value");
780    }
781
782    // ---- Details tests ----
783
784    #[test]
785    fn test_add_detail() {
786        let err = ConnectError::new(Code::Internal, "error")
787            .add_detail("test.Type1", vec![1, 2, 3])
788            .add_detail("test.Type2", vec![4, 5, 6]);
789
790        assert_eq!(err.details().len(), 2);
791        assert_eq!(err.details()[0].type_url(), "test.Type1");
792        assert_eq!(err.details()[0].value(), &[1, 2, 3]);
793        assert_eq!(err.details()[1].type_url(), "test.Type2");
794        assert_eq!(err.details()[1].value(), &[4, 5, 6]);
795    }
796
797    #[test]
798    fn test_add_error_detail() {
799        let detail = ErrorDetail::new("google.rpc.RetryInfo", vec![10, 2, 8, 5]);
800        let err =
801            ConnectError::new(Code::ResourceExhausted, "rate limited").add_error_detail(detail);
802
803        assert_eq!(err.details().len(), 1);
804        assert_eq!(err.details()[0].type_url(), "google.rpc.RetryInfo");
805    }
806
807    // ---- HTTP status code mapping tests ----
808
809    #[test]
810    fn test_http_status_code_mapping() {
811        let test_cases = [
812            (Code::Ok, StatusCode::OK),
813            (Code::Canceled, StatusCode::from_u16(499).unwrap()), // Client Closed Request
814            (Code::Unknown, StatusCode::INTERNAL_SERVER_ERROR),
815            (Code::InvalidArgument, StatusCode::BAD_REQUEST),
816            (Code::DeadlineExceeded, StatusCode::GATEWAY_TIMEOUT), // 504
817            (Code::NotFound, StatusCode::NOT_FOUND),
818            (Code::AlreadyExists, StatusCode::CONFLICT),
819            (Code::PermissionDenied, StatusCode::FORBIDDEN),
820            (Code::ResourceExhausted, StatusCode::TOO_MANY_REQUESTS),
821            (Code::FailedPrecondition, StatusCode::BAD_REQUEST),
822            (Code::Aborted, StatusCode::CONFLICT),
823            (Code::OutOfRange, StatusCode::BAD_REQUEST),
824            (Code::Unimplemented, StatusCode::NOT_IMPLEMENTED),
825            (Code::Internal, StatusCode::INTERNAL_SERVER_ERROR),
826            (Code::Unavailable, StatusCode::SERVICE_UNAVAILABLE),
827            (Code::DataLoss, StatusCode::INTERNAL_SERVER_ERROR),
828            (Code::Unauthenticated, StatusCode::UNAUTHORIZED),
829        ];
830
831        for (code, expected_status) in test_cases {
832            let err = ConnectError::from_code(code);
833            assert_eq!(
834                err.http_status_code(),
835                expected_status,
836                "Code::{:?} should map to {:?}",
837                code,
838                expected_status
839            );
840        }
841    }
842
843    // ---- StatusCode to Code conversion tests ----
844
845    #[test]
846    fn test_status_code_to_code_conversion() {
847        assert!(matches!(Code::from(StatusCode::OK), Code::Ok));
848        assert!(matches!(
849            Code::from(StatusCode::BAD_REQUEST),
850            Code::InvalidArgument
851        ));
852        assert!(matches!(
853            Code::from(StatusCode::UNAUTHORIZED),
854            Code::Unauthenticated
855        ));
856        assert!(matches!(
857            Code::from(StatusCode::FORBIDDEN),
858            Code::PermissionDenied
859        ));
860        assert!(matches!(Code::from(StatusCode::NOT_FOUND), Code::NotFound));
861        assert!(matches!(
862            Code::from(StatusCode::CONFLICT),
863            Code::AlreadyExists
864        ));
865        assert!(matches!(
866            Code::from(StatusCode::REQUEST_TIMEOUT),
867            Code::DeadlineExceeded
868        ));
869        assert!(matches!(
870            Code::from(StatusCode::TOO_MANY_REQUESTS),
871            Code::ResourceExhausted
872        ));
873        assert!(matches!(
874            Code::from(StatusCode::NOT_IMPLEMENTED),
875            Code::Unimplemented
876        ));
877        assert!(matches!(
878            Code::from(StatusCode::SERVICE_UNAVAILABLE),
879            Code::Unavailable
880        ));
881        assert!(matches!(
882            Code::from(StatusCode::INTERNAL_SERVER_ERROR),
883            Code::Internal
884        ));
885        // Unknown status codes map to Code::Unknown
886        assert!(matches!(Code::from(StatusCode::IM_A_TEAPOT), Code::Unknown));
887    }
888
889    #[test]
890    fn test_from_status_code_and_string() {
891        let err: ConnectError = (StatusCode::NOT_FOUND, "item not found".to_string()).into();
892        assert!(matches!(err.code(), Code::NotFound));
893        assert_eq!(err.message(), Some("item not found"));
894    }
895
896    // ---- Response generation tests ----
897
898    #[test]
899    fn test_into_response_unary_includes_metadata_as_headers() {
900        let err = ConnectError::new(Code::Internal, "error")
901            .with_meta("x-request-id", "req-123")
902            .with_meta("x-trace-id", "trace-456");
903
904        let response = err.into_response_with_protocol(RequestProtocol::ConnectUnaryJson);
905
906        // Check status code
907        assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
908
909        // Check metadata appears as headers
910        assert_eq!(response.headers().get("x-request-id").unwrap(), "req-123");
911        assert_eq!(response.headers().get("x-trace-id").unwrap(), "trace-456");
912
913        // Check content-type
914        assert_eq!(
915            response.headers().get(header::CONTENT_TYPE).unwrap(),
916            "application/json"
917        );
918    }
919
920    #[test]
921    fn test_into_response_unary_proto_content_type() {
922        let err = ConnectError::new(Code::NotFound, "not found");
923        let response = err.into_response_with_protocol(RequestProtocol::ConnectUnaryProto);
924
925        assert_eq!(response.status(), StatusCode::NOT_FOUND);
926        assert_eq!(
927            response.headers().get(header::CONTENT_TYPE).unwrap(),
928            "application/json" // Errors are always JSON, even for proto requests
929        );
930    }
931
932    #[test]
933    fn test_into_response_streaming_returns_http_200() {
934        let err = ConnectError::new(Code::Internal, "stream error");
935        let response = err.into_response_with_protocol(RequestProtocol::ConnectStreamJson);
936
937        // Streaming errors must return HTTP 200
938        assert_eq!(response.status(), StatusCode::OK);
939        assert_eq!(
940            response.headers().get(header::CONTENT_TYPE).unwrap(),
941            "application/connect+json"
942        );
943    }
944
945    #[test]
946    fn test_into_streaming_response_json() {
947        let err = ConnectError::new(Code::Internal, "error");
948        let response = err.into_streaming_response(false);
949
950        assert_eq!(response.status(), StatusCode::OK);
951        assert_eq!(
952            response.headers().get(header::CONTENT_TYPE).unwrap(),
953            "application/connect+json"
954        );
955    }
956
957    #[test]
958    fn test_into_streaming_response_proto() {
959        let err = ConnectError::new(Code::Internal, "error");
960        let response = err.into_streaming_response(true);
961
962        assert_eq!(response.status(), StatusCode::OK);
963        assert_eq!(
964            response.headers().get(header::CONTENT_TYPE).unwrap(),
965            "application/connect+proto"
966        );
967    }
968
969    // ---- Serialization tests ----
970
971    #[test]
972    fn test_serialize_error_json() {
973        let err = ConnectError::new(Code::InvalidArgument, "bad input");
974        let json = serde_json::to_string(&err).unwrap();
975
976        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
977        assert_eq!(parsed["code"], "invalid_argument");
978        assert_eq!(parsed["message"], "bad input");
979        // details should be absent when empty (skip_serializing_if)
980        assert!(parsed.get("details").is_none());
981    }
982
983    #[test]
984    fn test_serialize_error_with_details() {
985        let err = ConnectError::new(Code::Internal, "error")
986            .add_detail("google.rpc.RetryInfo", vec![1, 2, 3]);
987
988        let json = serde_json::to_string(&err).unwrap();
989        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
990
991        // Details should be objects with type and value fields
992        assert!(parsed.get("details").is_some());
993        let details = parsed["details"].as_array().unwrap();
994        assert_eq!(details.len(), 1);
995
996        // Each detail should be {type, value} object
997        let detail = &details[0];
998        assert_eq!(detail["type"], "google.rpc.RetryInfo");
999        assert_eq!(detail["value"], "AQID"); // base64 of [1, 2, 3] (no padding)
1000    }
1001
1002    #[test]
1003    fn test_serialize_error_detail_strips_type_prefix() {
1004        // Type URLs with "type.googleapis.com/" prefix should have it stripped
1005        let err = ConnectError::new(Code::Internal, "error")
1006            .add_detail("type.googleapis.com/google.rpc.ErrorInfo", vec![1, 2]);
1007
1008        let json = serde_json::to_string(&err).unwrap();
1009        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1010
1011        let details = parsed["details"].as_array().unwrap();
1012        assert_eq!(details[0]["type"], "google.rpc.ErrorInfo"); // prefix stripped
1013    }
1014
1015    #[test]
1016    fn test_serialize_error_without_message() {
1017        let err = ConnectError::from_code(Code::Unknown);
1018        let json = serde_json::to_string(&err).unwrap();
1019
1020        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1021        assert_eq!(parsed["code"], "unknown");
1022        // message should be absent when None (skip_serializing_if)
1023        assert!(parsed.get("message").is_none());
1024    }
1025
1026    // ---- Fallback response tests ----
1027
1028    #[test]
1029    fn test_internal_error_response() {
1030        let response = internal_error_response("application/json");
1031
1032        assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
1033        assert_eq!(
1034            response.headers().get(header::CONTENT_TYPE).unwrap(),
1035            "application/json"
1036        );
1037    }
1038
1039    #[test]
1040    fn test_internal_error_end_stream_frame() {
1041        let frame = internal_error_end_stream_frame();
1042
1043        // Check frame structure: flags (1 byte) + length (4 bytes) + payload
1044        assert!(frame.len() > 5);
1045        assert_eq!(frame[0], 0b0000_0010); // EndStream flag
1046
1047        let length = u32::from_be_bytes([frame[1], frame[2], frame[3], frame[4]]) as usize;
1048        assert_eq!(frame.len(), 5 + length);
1049
1050        // Parse payload
1051        let payload = &frame[5..];
1052        let parsed: serde_json::Value = serde_json::from_slice(payload).unwrap();
1053        assert_eq!(parsed["error"]["code"], "internal");
1054    }
1055
1056    #[test]
1057    fn test_internal_error_streaming_response() {
1058        let response = internal_error_streaming_response("application/connect+json");
1059
1060        assert_eq!(response.status(), StatusCode::OK);
1061        assert_eq!(
1062            response.headers().get(header::CONTENT_TYPE).unwrap(),
1063            "application/connect+json"
1064        );
1065    }
1066
1067    // ---- Tonic conversion tests (feature-gated) ----
1068
1069    #[cfg(feature = "tonic")]
1070    mod tonic_tests {
1071        use super::*;
1072
1073        #[test]
1074        fn test_tonic_code_to_connect_code() {
1075            assert!(matches!(Code::from(::tonic::Code::Ok), Code::Ok));
1076            assert!(matches!(
1077                Code::from(::tonic::Code::Cancelled),
1078                Code::Canceled
1079            ));
1080            assert!(matches!(Code::from(::tonic::Code::Unknown), Code::Unknown));
1081            assert!(matches!(
1082                Code::from(::tonic::Code::InvalidArgument),
1083                Code::InvalidArgument
1084            ));
1085            assert!(matches!(
1086                Code::from(::tonic::Code::DeadlineExceeded),
1087                Code::DeadlineExceeded
1088            ));
1089            assert!(matches!(
1090                Code::from(::tonic::Code::NotFound),
1091                Code::NotFound
1092            ));
1093            assert!(matches!(
1094                Code::from(::tonic::Code::AlreadyExists),
1095                Code::AlreadyExists
1096            ));
1097            assert!(matches!(
1098                Code::from(::tonic::Code::PermissionDenied),
1099                Code::PermissionDenied
1100            ));
1101            assert!(matches!(
1102                Code::from(::tonic::Code::ResourceExhausted),
1103                Code::ResourceExhausted
1104            ));
1105            assert!(matches!(
1106                Code::from(::tonic::Code::FailedPrecondition),
1107                Code::FailedPrecondition
1108            ));
1109            assert!(matches!(Code::from(::tonic::Code::Aborted), Code::Aborted));
1110            assert!(matches!(
1111                Code::from(::tonic::Code::OutOfRange),
1112                Code::OutOfRange
1113            ));
1114            assert!(matches!(
1115                Code::from(::tonic::Code::Unimplemented),
1116                Code::Unimplemented
1117            ));
1118            assert!(matches!(
1119                Code::from(::tonic::Code::Internal),
1120                Code::Internal
1121            ));
1122            assert!(matches!(
1123                Code::from(::tonic::Code::Unavailable),
1124                Code::Unavailable
1125            ));
1126            assert!(matches!(
1127                Code::from(::tonic::Code::DataLoss),
1128                Code::DataLoss
1129            ));
1130            assert!(matches!(
1131                Code::from(::tonic::Code::Unauthenticated),
1132                Code::Unauthenticated
1133            ));
1134        }
1135
1136        #[test]
1137        fn test_connect_code_to_tonic_code() {
1138            assert_eq!(::tonic::Code::from(Code::Ok), ::tonic::Code::Ok);
1139            assert_eq!(
1140                ::tonic::Code::from(Code::Canceled),
1141                ::tonic::Code::Cancelled
1142            );
1143            assert_eq!(::tonic::Code::from(Code::Unknown), ::tonic::Code::Unknown);
1144            assert_eq!(
1145                ::tonic::Code::from(Code::InvalidArgument),
1146                ::tonic::Code::InvalidArgument
1147            );
1148            assert_eq!(
1149                ::tonic::Code::from(Code::DeadlineExceeded),
1150                ::tonic::Code::DeadlineExceeded
1151            );
1152            assert_eq!(::tonic::Code::from(Code::NotFound), ::tonic::Code::NotFound);
1153            assert_eq!(
1154                ::tonic::Code::from(Code::AlreadyExists),
1155                ::tonic::Code::AlreadyExists
1156            );
1157            assert_eq!(
1158                ::tonic::Code::from(Code::PermissionDenied),
1159                ::tonic::Code::PermissionDenied
1160            );
1161            assert_eq!(
1162                ::tonic::Code::from(Code::ResourceExhausted),
1163                ::tonic::Code::ResourceExhausted
1164            );
1165            assert_eq!(
1166                ::tonic::Code::from(Code::FailedPrecondition),
1167                ::tonic::Code::FailedPrecondition
1168            );
1169            assert_eq!(::tonic::Code::from(Code::Aborted), ::tonic::Code::Aborted);
1170            assert_eq!(
1171                ::tonic::Code::from(Code::OutOfRange),
1172                ::tonic::Code::OutOfRange
1173            );
1174            assert_eq!(
1175                ::tonic::Code::from(Code::Unimplemented),
1176                ::tonic::Code::Unimplemented
1177            );
1178            assert_eq!(::tonic::Code::from(Code::Internal), ::tonic::Code::Internal);
1179            assert_eq!(
1180                ::tonic::Code::from(Code::Unavailable),
1181                ::tonic::Code::Unavailable
1182            );
1183            assert_eq!(::tonic::Code::from(Code::DataLoss), ::tonic::Code::DataLoss);
1184            assert_eq!(
1185                ::tonic::Code::from(Code::Unauthenticated),
1186                ::tonic::Code::Unauthenticated
1187            );
1188        }
1189
1190        #[test]
1191        fn test_tonic_status_to_connect_error() {
1192            let status = ::tonic::Status::not_found("item not found");
1193            let err: ConnectError = status.into();
1194
1195            assert!(matches!(err.code(), Code::NotFound));
1196            assert_eq!(err.message(), Some("item not found"));
1197        }
1198
1199        #[test]
1200        fn test_tonic_status_empty_message() {
1201            let status = ::tonic::Status::new(::tonic::Code::Internal, "");
1202            let err: ConnectError = status.into();
1203
1204            assert!(matches!(err.code(), Code::Internal));
1205            assert!(err.message().is_none());
1206        }
1207
1208        #[test]
1209        fn test_connect_error_to_tonic_status() {
1210            let err = ConnectError::new(Code::PermissionDenied, "access denied");
1211            let status: ::tonic::Status = err.into();
1212
1213            assert_eq!(status.code(), ::tonic::Code::PermissionDenied);
1214            assert_eq!(status.message(), "access denied");
1215        }
1216
1217        #[test]
1218        fn test_connect_error_to_tonic_status_no_message() {
1219            let err = ConnectError::from_code(Code::Internal);
1220            let status: ::tonic::Status = err.into();
1221
1222            assert_eq!(status.code(), ::tonic::Code::Internal);
1223            assert_eq!(status.message(), "");
1224        }
1225    }
1226}