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    json_error_response_with_fields(status, code, message, &[])
63}
64
65/// Build an AWS JSON error response carrying additional top-level members (e.g.
66/// DynamoDB's `ConditionalCheckFailedException.Item`). Each field value is
67/// parsed as JSON when possible (so an object/array is embedded as-is) and
68/// otherwise emitted as a JSON string.
69pub fn json_error_response_with_fields(
70    status: StatusCode,
71    code: &str,
72    message: &str,
73    extra_fields: &[(String, String)],
74) -> (StatusCode, String, Bytes) {
75    let mut body = serde_json::Map::new();
76    body.insert("__type".to_string(), serde_json::json!(code));
77    body.insert("message".to_string(), serde_json::json!(message));
78    for (key, value) in extra_fields {
79        let parsed = serde_json::from_str(value)
80            .unwrap_or_else(|_| serde_json::Value::String(value.clone()));
81        body.insert(key.clone(), parsed);
82    }
83
84    (
85        status,
86        "application/x-amz-json-1.1".to_string(),
87        Bytes::from(serde_json::Value::Object(body).to_string()),
88    )
89}
90
91/// Build an S3-style XML error response.
92/// S3 uses `<Error>` (not `<ErrorResponse>`) with different field ordering.
93pub fn s3_xml_error_response(
94    status: StatusCode,
95    code: &str,
96    message: &str,
97    request_id: &str,
98) -> (StatusCode, String, Bytes) {
99    s3_xml_error_response_with_fields(status, code, message, request_id, &[])
100}
101
102/// Build an S3-style XML error response with additional fields (e.g., BucketName, Key).
103pub fn s3_xml_error_response_with_fields(
104    status: StatusCode,
105    code: &str,
106    message: &str,
107    request_id: &str,
108    extra_fields: &[(String, String)],
109) -> (StatusCode, String, Bytes) {
110    let mut buffer = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Error>\n");
111    buffer.push_str(&format!("  <Code>{}</Code>\n", xml_escape(code)));
112    buffer.push_str(&format!("  <Message>{}</Message>\n", xml_escape(message)));
113    for (key, value) in extra_fields {
114        buffer.push_str(&format!("  <{}>{}</{}>\n", key, xml_escape(value), key));
115    }
116    buffer.push_str(&format!(
117        "  <RequestId>{}</RequestId>\n",
118        xml_escape(request_id)
119    ));
120    buffer.push_str("</Error>");
121
122    (status, "application/xml".to_string(), Bytes::from(buffer))
123}
124
125use crate::xml::xml_escape;
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn xml_error_has_correct_structure() {
133        let (status, content_type, body) = xml_error_response(
134            StatusCode::BAD_REQUEST,
135            "InvalidAction",
136            "not found",
137            "req-1",
138        );
139        assert_eq!(status, StatusCode::BAD_REQUEST);
140        assert_eq!(content_type, "text/xml");
141        let body_str = String::from_utf8(body.to_vec()).unwrap();
142        assert!(body_str.contains("<Code>InvalidAction</Code>"));
143        assert!(body_str.contains("<RequestId>req-1</RequestId>"));
144    }
145
146    #[test]
147    fn json_error_has_correct_structure() {
148        let (status, content_type, body) =
149            json_error_response(StatusCode::BAD_REQUEST, "ValidationException", "bad input");
150        assert_eq!(status, StatusCode::BAD_REQUEST);
151        assert!(content_type.contains("json"));
152        let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
153        assert_eq!(v["__type"], "ValidationException");
154        assert_eq!(v["message"], "bad input");
155    }
156
157    #[test]
158    fn s3_xml_error_basic() {
159        let (status, content_type, body) =
160            s3_xml_error_response(StatusCode::NOT_FOUND, "NoSuchKey", "not found", "req-x");
161        assert_eq!(status, StatusCode::NOT_FOUND);
162        assert_eq!(content_type, "application/xml");
163        let body_str = String::from_utf8(body.to_vec()).unwrap();
164        assert!(body_str.contains("<Code>NoSuchKey</Code>"));
165        assert!(body_str.contains("<Message>not found</Message>"));
166        assert!(body_str.contains("<RequestId>req-x</RequestId>"));
167    }
168
169    #[test]
170    fn s3_xml_error_with_fields_includes_extra() {
171        let extras = vec![("BucketName".to_string(), "my-bucket".to_string())];
172        let (_, _, body) = s3_xml_error_response_with_fields(
173            StatusCode::CONFLICT,
174            "BucketAlreadyOwnedByYou",
175            "already owns",
176            "req-1",
177            &extras,
178        );
179        let body_str = String::from_utf8(body.to_vec()).unwrap();
180        assert!(body_str.contains("<BucketName>my-bucket</BucketName>"));
181    }
182
183    #[test]
184    fn xml_error_escapes_special_chars() {
185        let (_, _, body) = xml_error_response(StatusCode::BAD_REQUEST, "E", "a<b>c", "req-1");
186        let body_str = String::from_utf8(body.to_vec()).unwrap();
187        assert!(body_str.contains("a&lt;b&gt;c"));
188    }
189}