connectrpc_axum/context/
error.rs

1//! Response error type - bundles ConnectError with protocol for response formatting.
2
3use crate::context::RequestProtocol;
4use crate::context::protocol::SUPPORTED_CONTENT_TYPES;
5use crate::error::{Code, ConnectError};
6use axum::body::Body;
7use axum::http::StatusCode;
8use axum::response::Response;
9
10/// Error bundled with protocol for HTTP response formatting.
11///
12/// Used internally by the framework to carry protocol information
13/// alongside errors for proper JSON/Proto encoding in responses.
14#[derive(Debug)]
15pub struct ContextError(pub RequestProtocol, pub ConnectError);
16
17impl ContextError {
18    /// Create a new response error.
19    pub fn new(protocol: RequestProtocol, err: ConnectError) -> Self {
20        Self(protocol, err)
21    }
22
23    /// Create an internal error (hides details from client).
24    ///
25    /// The provided message is not exposed to clients for security.
26    /// Clients receive a generic "internal error" message.
27    pub fn internal(protocol: RequestProtocol, _msg: impl Into<String>) -> Self {
28        // Note: _msg could be logged here if needed
29        Self(
30            protocol,
31            ConnectError::new(Code::Internal, "internal error"),
32        )
33    }
34
35    /// Get the protocol.
36    pub fn protocol(&self) -> RequestProtocol {
37        self.0
38    }
39
40    /// Get the underlying ConnectError.
41    pub fn error(&self) -> &ConnectError {
42        &self.1
43    }
44
45    /// Convert to HTTP response with proper encoding.
46    pub fn into_response(self) -> Response {
47        self.1.into_response_with_protocol(self.0)
48    }
49
50    /// Extract the underlying ConnectError.
51    pub fn into_connect_error(self) -> ConnectError {
52        self.1
53    }
54}
55
56impl std::fmt::Display for ContextError {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        write!(f, "{}", self.1.message().unwrap_or("error"))
59    }
60}
61
62impl std::error::Error for ContextError {}
63
64// ============================================================================
65// Protocol Negotiation Error
66// ============================================================================
67
68/// Pre-protocol error for unsupported media types.
69///
70/// This error type produces raw HTTP 415 responses that bypass Connect error
71/// formatting. Per connect-go behavior, unsupported content-types and invalid
72/// GET encodings return HTTP 415 Unsupported Media Type with an `Accept-Post`
73/// header listing supported content types.
74///
75/// This is used before protocol detection completes, when the request cannot
76/// be handled by any supported protocol variant.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum ProtocolNegotiationError {
79    /// Content-Type or encoding is not supported.
80    UnsupportedMediaType,
81}
82
83impl ProtocolNegotiationError {
84    /// Convert to HTTP 415 response with Accept-Post header.
85    pub fn into_response(self) -> Response {
86        Response::builder()
87            .status(StatusCode::UNSUPPORTED_MEDIA_TYPE)
88            .header("Accept-Post", SUPPORTED_CONTENT_TYPES)
89            .body(Body::empty())
90            .unwrap()
91    }
92}
93
94impl std::fmt::Display for ProtocolNegotiationError {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        match self {
97            Self::UnsupportedMediaType => write!(f, "unsupported media type"),
98        }
99    }
100}
101
102impl std::error::Error for ProtocolNegotiationError {}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn test_response_error_new() {
110        let err = ContextError::new(
111            RequestProtocol::ConnectUnaryJson,
112            ConnectError::new(Code::InvalidArgument, "test error"),
113        );
114        assert!(matches!(err.error().code(), Code::InvalidArgument));
115        assert!(matches!(err.protocol(), RequestProtocol::ConnectUnaryJson));
116    }
117
118    #[test]
119    fn test_response_error_internal() {
120        let err = ContextError::internal(RequestProtocol::ConnectUnaryJson, "secret details");
121        // Internal errors hide the real message
122        assert!(matches!(err.error().code(), Code::Internal));
123        assert_eq!(err.error().message(), Some("internal error"));
124    }
125
126    #[test]
127    fn test_into_response() {
128        let err = ContextError::new(
129            RequestProtocol::ConnectUnaryJson,
130            ConnectError::new(Code::NotFound, "not found"),
131        );
132        let _response = err.into_response();
133    }
134
135    #[test]
136    fn test_display() {
137        let err = ContextError::new(
138            RequestProtocol::ConnectUnaryProto,
139            ConnectError::new(Code::NotFound, "not found"),
140        );
141        assert_eq!(format!("{err}"), "not found");
142    }
143
144    // --- ProtocolNegotiationError tests ---
145
146    #[test]
147    fn test_protocol_negotiation_error_into_response() {
148        use axum::http::StatusCode;
149
150        let err = ProtocolNegotiationError::UnsupportedMediaType;
151        let response = err.into_response();
152
153        // Should return HTTP 415
154        assert_eq!(response.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
155
156        // Should have Accept-Post header with supported content types
157        let accept_post = response.headers().get("Accept-Post");
158        assert!(accept_post.is_some());
159        let accept_post_value = accept_post.unwrap().to_str().unwrap();
160        assert!(accept_post_value.contains("application/json"));
161        assert!(accept_post_value.contains("application/proto"));
162        assert!(accept_post_value.contains("application/connect+json"));
163        assert!(accept_post_value.contains("application/connect+proto"));
164    }
165
166    #[test]
167    fn test_protocol_negotiation_error_display() {
168        let err = ProtocolNegotiationError::UnsupportedMediaType;
169        assert_eq!(format!("{err}"), "unsupported media type");
170    }
171
172    #[test]
173    fn test_protocol_negotiation_error_debug() {
174        let err = ProtocolNegotiationError::UnsupportedMediaType;
175        assert_eq!(format!("{err:?}"), "UnsupportedMediaType");
176    }
177}