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
108use crate::xml::xml_escape;
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn xml_error_has_correct_structure() {
116        let (status, content_type, body) = xml_error_response(
117            StatusCode::BAD_REQUEST,
118            "InvalidAction",
119            "not found",
120            "req-1",
121        );
122        assert_eq!(status, StatusCode::BAD_REQUEST);
123        assert_eq!(content_type, "text/xml");
124        let body_str = String::from_utf8(body.to_vec()).unwrap();
125        assert!(body_str.contains("<Code>InvalidAction</Code>"));
126        assert!(body_str.contains("<RequestId>req-1</RequestId>"));
127    }
128
129    #[test]
130    fn json_error_has_correct_structure() {
131        let (status, content_type, body) =
132            json_error_response(StatusCode::BAD_REQUEST, "ValidationException", "bad input");
133        assert_eq!(status, StatusCode::BAD_REQUEST);
134        assert!(content_type.contains("json"));
135        let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
136        assert_eq!(v["__type"], "ValidationException");
137        assert_eq!(v["message"], "bad input");
138    }
139
140    #[test]
141    fn s3_xml_error_basic() {
142        let (status, content_type, body) =
143            s3_xml_error_response(StatusCode::NOT_FOUND, "NoSuchKey", "not found", "req-x");
144        assert_eq!(status, StatusCode::NOT_FOUND);
145        assert_eq!(content_type, "application/xml");
146        let body_str = String::from_utf8(body.to_vec()).unwrap();
147        assert!(body_str.contains("<Code>NoSuchKey</Code>"));
148        assert!(body_str.contains("<Message>not found</Message>"));
149        assert!(body_str.contains("<RequestId>req-x</RequestId>"));
150    }
151
152    #[test]
153    fn s3_xml_error_with_fields_includes_extra() {
154        let extras = vec![("BucketName".to_string(), "my-bucket".to_string())];
155        let (_, _, body) = s3_xml_error_response_with_fields(
156            StatusCode::CONFLICT,
157            "BucketAlreadyOwnedByYou",
158            "already owns",
159            "req-1",
160            &extras,
161        );
162        let body_str = String::from_utf8(body.to_vec()).unwrap();
163        assert!(body_str.contains("<BucketName>my-bucket</BucketName>"));
164    }
165
166    #[test]
167    fn xml_error_escapes_special_chars() {
168        let (_, _, body) = xml_error_response(StatusCode::BAD_REQUEST, "E", "a<b>c", "req-1");
169        let body_str = String::from_utf8(body.to_vec()).unwrap();
170        assert!(body_str.contains("a&lt;b&gt;c"));
171    }
172}