Skip to main content

rustack_logs_http/
response.rs

1//! CloudWatch Logs response serialization and error formatting.
2
3use rustack_logs_model::error::LogsError;
4
5use crate::body::LogsResponseBody;
6
7/// Content type for CloudWatch Logs JSON responses (awsJson1_1).
8pub const CONTENT_TYPE: &str = "application/x-amz-json-1.1";
9
10/// Serialize a CloudWatch Logs error into a JSON response body.
11///
12/// The error format follows the AWS CloudWatch Logs JSON protocol:
13///
14/// ```json
15/// {
16///   "__type": "ResourceNotFoundException",
17///   "message": "..."
18/// }
19/// ```
20///
21/// Note: CloudWatch Logs uses short error type names and lowercase `"message"` field.
22#[must_use]
23pub fn error_to_json(error: &LogsError) -> Vec<u8> {
24    let obj = serde_json::json!({
25        "__type": error.error_type(),
26        "message": error.message,
27    });
28    serde_json::to_vec(&obj).expect("JSON serialization of error cannot fail")
29}
30
31/// Convert a `LogsError` into a complete HTTP error response.
32#[must_use]
33pub fn error_to_response(error: &LogsError, request_id: &str) -> http::Response<LogsResponseBody> {
34    let json = error_to_json(error);
35    let crc = crc32fast::hash(&json);
36    let body = LogsResponseBody::from_json(json);
37
38    let mut response = http::Response::builder()
39        .status(error.status_code)
40        .header("content-type", CONTENT_TYPE)
41        .header("x-amzn-requestid", request_id)
42        .body(body)
43        .expect("valid error response");
44
45    if let Ok(hv) = http::HeaderValue::from_str(&crc.to_string()) {
46        response.headers_mut().insert("x-amz-crc32", hv);
47    }
48
49    response
50}
51
52/// Build a success response from JSON bytes.
53#[must_use]
54pub fn json_response(json: Vec<u8>, request_id: &str) -> http::Response<LogsResponseBody> {
55    let crc = crc32fast::hash(&json);
56    let body = LogsResponseBody::from_json(json);
57
58    let mut response = http::Response::builder()
59        .status(http::StatusCode::OK)
60        .header("content-type", CONTENT_TYPE)
61        .header("x-amzn-requestid", request_id)
62        .body(body)
63        .expect("valid JSON response");
64
65    if let Ok(hv) = http::HeaderValue::from_str(&crc.to_string()) {
66        response.headers_mut().insert("x-amz-crc32", hv);
67    }
68
69    response
70}
71
72#[cfg(test)]
73mod tests {
74    use rustack_logs_model::error::LogsErrorCode;
75
76    use super::*;
77
78    #[test]
79    fn test_should_format_error_json_with_short_type() {
80        let err = LogsError::with_message(
81            LogsErrorCode::ResourceNotFoundException,
82            "Log group my-group not found",
83        );
84        let json = error_to_json(&err);
85        let parsed: serde_json::Value = serde_json::from_slice(&json).expect("valid JSON");
86        assert_eq!(parsed["__type"], "ResourceNotFoundException");
87        assert_eq!(parsed["message"], "Log group my-group not found");
88    }
89
90    #[test]
91    fn test_should_build_error_response_with_correct_status() {
92        let err = LogsError::with_message(LogsErrorCode::ResourceNotFoundException, "not found");
93        let resp = error_to_response(&err, "test-req-123");
94        assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST);
95        assert_eq!(
96            resp.headers()
97                .get("content-type")
98                .expect("has content-type"),
99            CONTENT_TYPE,
100        );
101        assert_eq!(
102            resp.headers()
103                .get("x-amzn-requestid")
104                .expect("has request id"),
105            "test-req-123",
106        );
107        assert!(resp.headers().get("x-amz-crc32").is_some());
108    }
109
110    #[test]
111    fn test_should_build_json_success_response() {
112        let json = serde_json::to_vec(&serde_json::json!({"logGroupName": "/aws/test"}))
113            .expect("valid JSON");
114        let resp = json_response(json, "req-456");
115        assert_eq!(resp.status(), http::StatusCode::OK);
116        assert_eq!(
117            resp.headers()
118                .get("content-type")
119                .expect("has content-type"),
120            CONTENT_TYPE,
121        );
122        assert!(resp.headers().get("x-amz-crc32").is_some());
123    }
124
125    #[test]
126    fn test_should_use_1_1_content_type() {
127        assert_eq!(CONTENT_TYPE, "application/x-amz-json-1.1");
128    }
129}