Skip to main content

fakecloud_aws/
error.rs

1use bytes::Bytes;
2use http::StatusCode;
3use quick_xml::se::Serializer as XmlSerializer;
4use serde::Serialize;
5
6/// Build an AWS XML error response (used by Query protocol services: SQS, SNS, IAM, STS).
7pub fn xml_error_response(
8    status: StatusCode,
9    code: &str,
10    message: &str,
11    request_id: &str,
12) -> (StatusCode, String, Bytes) {
13    #[derive(Serialize)]
14    #[serde(rename = "ErrorResponse")]
15    struct ErrorResponse<'a> {
16        #[serde(rename = "Error")]
17        error: ErrorBody<'a>,
18        #[serde(rename = "RequestId")]
19        request_id: &'a str,
20    }
21
22    #[derive(Serialize)]
23    struct ErrorBody<'a> {
24        #[serde(rename = "Type")]
25        error_type: &'a str,
26        #[serde(rename = "Code")]
27        code: &'a str,
28        #[serde(rename = "Message")]
29        message: &'a str,
30    }
31
32    let error_type = if status.is_server_error() {
33        "Receiver"
34    } else {
35        "Sender"
36    };
37
38    let resp = ErrorResponse {
39        error: ErrorBody {
40            error_type,
41            code,
42            message,
43        },
44        request_id,
45    };
46
47    let mut buffer = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
48    let mut ser = XmlSerializer::new(&mut buffer);
49    ser.indent(' ', 2);
50    resp.serialize(ser)
51        .expect("XML serialization should not fail");
52
53    (status, "text/xml".to_string(), Bytes::from(buffer))
54}
55
56/// Build an AWS JSON error response (used by JSON protocol services: SSM, EventBridge, etc.).
57pub fn json_error_response(
58    status: StatusCode,
59    code: &str,
60    message: &str,
61) -> (StatusCode, String, Bytes) {
62    let body = serde_json::json!({
63        "__type": code,
64        "message": message,
65    });
66
67    (
68        status,
69        "application/x-amz-json-1.1".to_string(),
70        Bytes::from(body.to_string()),
71    )
72}
73
74/// Build an S3-style XML error response.
75/// S3 uses `<Error>` (not `<ErrorResponse>`) with different field ordering.
76pub fn s3_xml_error_response(
77    status: StatusCode,
78    code: &str,
79    message: &str,
80    request_id: &str,
81) -> (StatusCode, String, Bytes) {
82    s3_xml_error_response_with_fields(status, code, message, request_id, &[])
83}
84
85/// Build an S3-style XML error response with additional fields (e.g., BucketName, Key).
86pub fn s3_xml_error_response_with_fields(
87    status: StatusCode,
88    code: &str,
89    message: &str,
90    request_id: &str,
91    extra_fields: &[(String, String)],
92) -> (StatusCode, String, Bytes) {
93    let mut buffer = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Error>\n");
94    buffer.push_str(&format!("  <Code>{}</Code>\n", xml_escape(code)));
95    buffer.push_str(&format!("  <Message>{}</Message>\n", xml_escape(message)));
96    for (key, value) in extra_fields {
97        buffer.push_str(&format!("  <{}>{}</{}>\n", key, xml_escape(value), key));
98    }
99    buffer.push_str(&format!(
100        "  <RequestId>{}</RequestId>\n",
101        xml_escape(request_id)
102    ));
103    buffer.push_str("</Error>");
104
105    (status, "application/xml".to_string(), Bytes::from(buffer))
106}
107
108fn xml_escape(s: &str) -> String {
109    s.replace('&', "&amp;")
110        .replace('<', "&lt;")
111        .replace('>', "&gt;")
112        .replace('"', "&quot;")
113        .replace('\'', "&apos;")
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn xml_error_has_correct_structure() {
122        let (status, content_type, body) = xml_error_response(
123            StatusCode::BAD_REQUEST,
124            "InvalidAction",
125            "not found",
126            "req-1",
127        );
128        assert_eq!(status, StatusCode::BAD_REQUEST);
129        assert_eq!(content_type, "text/xml");
130        let body_str = String::from_utf8(body.to_vec()).unwrap();
131        assert!(body_str.contains("<Code>InvalidAction</Code>"));
132        assert!(body_str.contains("<RequestId>req-1</RequestId>"));
133    }
134
135    #[test]
136    fn json_error_has_correct_structure() {
137        let (status, content_type, body) =
138            json_error_response(StatusCode::BAD_REQUEST, "ValidationException", "bad input");
139        assert_eq!(status, StatusCode::BAD_REQUEST);
140        assert!(content_type.contains("json"));
141        let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
142        assert_eq!(v["__type"], "ValidationException");
143        assert_eq!(v["message"], "bad input");
144    }
145}