//// Shared modeled-error-code extraction. AWS error responses carry the //// modeled error *code* (the Smithy shape's local name) in one of three //// places, depending on protocol: the `X-Amzn-Errortype` header //// (awsJson*, restJson1), a JSON body `__type` / `code` field, or a //// restXml `` element. This module pulls that local shape name //// out — namespace prefix, URI suffix, and Smithy `[Charlie,foo]` //// disambiguation suffix all stripped. //// //// Both the retry middleware (`aws/retry`), which inspects the raw //// `Response` *inside* the retry loop before any typed parsing, and the //// runtime (`aws/internal/client/runtime`), which builds the typed //// `ServiceError` *after* the loop, need the same extraction — so it //// lives here once rather than being duplicated across the two layers. import aws/internal/text_scan import gleam/bit_array import gleam/dict.{type Dict} import gleam/result import gleam/string /// Pull the wire-error-type local name out of an error response's /// already-lowercased headers + body. Prefers the `x-amzn-errortype` /// header (restJson1, awsJson*), then falls back to the body's /// `__type` / `code` (JSON) or `` (restXml). Returns the *local* /// shape name with namespace / URI / disambiguation suffixes stripped, /// or `"Unknown"` when nothing matches. pub fn from_headers_and_body( headers: Dict(String, String), body: BitArray, ) -> String { from_header_value_and_body(dict.get(headers, "x-amzn-errortype"), body) } /// Like `from_headers_and_body` but taking the `x-amzn-errortype` /// header value as a pre-resolved `Result`. The retry layer holds a /// `Response` (not a header dict) and reads the header via /// `response.get_header`, so this entry point lets it reuse the exact /// same extraction without rebuilding a dict. pub fn from_header_value_and_body( error_type_header: Result(String, Nil), body: BitArray, ) -> String { case error_type_header { Ok(v) -> normalise(v) Error(_) -> case bit_array.to_string(body) { Error(_) -> "Unknown" Ok(text) -> from_body(text) } } } /// Strip an error-type wire value down to its local Smithy shape name: /// drop any `Type:uri` suffix, any `,` disambiguation list, and any /// `namespace#` prefix. Exposed so callers that already hold a raw /// header value (not a full response) can normalise it consistently. pub fn normalise(raw: String) -> String { let s = case string.split_once(raw, ":") { Ok(#(prefix, _)) -> prefix Error(_) -> raw } let s = case string.split_once(s, ",") { Ok(#(prefix, _)) -> prefix Error(_) -> s } case string.split_once(s, "#") { Ok(#(_, local)) -> local Error(_) -> s } } fn from_body(body: String) -> String { let found = text_scan.json_string_after_key(body, "__type") |> result.lazy_or(fn() { text_scan.json_string_after_key(body, "code") }) |> result.lazy_or(fn() { from_xml(body) }) case found { Ok(v) -> normalise(v) Error(_) -> "Unknown" } } /// Pull the error code out of a restXml error body. Two shapes appear /// in the wild — S3-style `NoSuchBucket...` /// and SQS/SNS-style `X...`. /// In both, the first `` element holds the error type, so a /// single text search covers both without dragging in the full XML /// decoder. The trim + empty-check rejects `` / ` ` /// so the fallback to "Unknown" still fires for malformed bodies. fn from_xml(body: String) -> Result(String, Nil) { use raw <- result.try(text_scan.xml_tag_text(body, "Code")) case string.trim(raw) { "" -> Error(Nil) non_empty -> Ok(non_empty) } }