v2 / vlib / net / s3 / errors.v
206 lines · 195 sloc · 5.28 KB · 4142432483c4e8de44ab7b0d6ac944f3251e03c8
Raw
1// Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module s3
5
6// S3Error is the structured error returned for every signing or service
7// failure. `code` is stable (e.g. `NoSuchKey`, `MissingCredentials`),
8// `message` is human-readable, `status` is the HTTP status (0 for
9// client-side errors), and `path` is the offending object key when known.
10//
11// V's `IError` interface only requires `msg()` and `code()`, so this type can
12// be returned via `!T` and inspected with type-assertion / `as`.
13pub struct S3Error {
14pub:
15 code string
16 message string
17 status int
18 path string
19 resource string // S3's <Resource> field, when present
20 request_id string // S3 RequestId, useful for support tickets
21}
22
23// msg renders the error in a single line. Includes the S3 error code so users
24// can switch on it without parsing the prose. Path is appended when known.
25pub fn (e &S3Error) msg() string {
26 mut buf := '[${e.code}] ${e.message}'
27 if e.status != 0 {
28 buf += ' (HTTP ${e.status})'
29 }
30 if e.path != '' {
31 buf += ' — ${e.path}'
32 }
33 return buf
34}
35
36// code returns a stable numeric code so callers can use `if err.code() == ...`.
37// We map a few well-known S3 codes; everything else returns 0 so callers fall
38// back to string comparison on `e.code`.
39pub fn (e &S3Error) code() int {
40 return match e.code {
41 'NoSuchKey' {
42 404
43 }
44 'NoSuchBucket' {
45 404
46 }
47 'BucketAlreadyExists', 'BucketAlreadyOwnedByYou' {
48 409
49 }
50 'AccessDenied', 'SignatureDoesNotMatch' {
51 403
52 }
53 'MissingCredentials', 'InvalidEndpoint', 'InvalidPath', 'InvalidMethod',
54 'InvalidSessionToken' {
55 400
56 }
57 else {
58 e.status
59 }
60 }
61}
62
63// new_error builds an S3Error from a code + message. Use this for client-side
64// validation failures (missing creds, invalid path, etc.).
65pub fn new_error(code string, message string) IError {
66 return &S3Error{
67 code: code
68 message: message
69 }
70}
71
72// new_http_error wraps an HTTP-level failure. Body is the raw response body
73// — `parse_xml_error` is responsible for digging out the structured
74// `<Error>` envelope when the server returns one.
75pub fn new_http_error(status int, path string, body string) IError {
76 parsed := parse_xml_error(body)
77 if parsed.code != '' {
78 return &S3Error{
79 code: parsed.code
80 message: parsed.message
81 status: status
82 path: path
83 resource: parsed.resource
84 request_id: parsed.request_id
85 }
86 }
87 // Body was empty or unparseable — fall back to a generic code keyed by status.
88 return &S3Error{
89 code: fallback_code_for(status)
90 message: if body.len > 0 { body } else { 'HTTP ${status}' }
91 status: status
92 path: path
93 }
94}
95
96// XmlErrorFields is what we pull out of an `<Error>` XML envelope.
97struct XmlErrorFields {
98 code string
99 message string
100 resource string
101 request_id string
102}
103
104// parse_xml_error extracts the standard S3 error XML:
105//
106// <Error>
107// <Code>...</Code>
108// <Message>...</Message>
109// <Resource>...</Resource>
110// <RequestId>...</RequestId>
111// </Error>
112//
113// We use a tiny tag scanner (no DOM) — the format is rigid enough that this
114// stays correct, faster than spinning up the full XML parser, and there is no
115// attribute parsing to worry about.
116fn parse_xml_error(body string) XmlErrorFields {
117 return XmlErrorFields{
118 code: extract_xml_tag(body, 'Code')
119 message: extract_xml_tag(body, 'Message')
120 resource: extract_xml_tag(body, 'Resource')
121 request_id: extract_xml_tag(body, 'RequestId')
122 }
123}
124
125// extract_xml_tag returns the inner text of `<tag>...</tag>` (first match,
126// case-sensitive) with the five predefined XML entities decoded, or '' if
127// the tag is absent.
128pub fn extract_xml_tag(body string, tag string) string {
129 open := '<' + tag + '>'
130 close := '</' + tag + '>'
131 start := body.index(open) or { return '' }
132 rest_off := start + open.len
133 end := body.index_after(close, rest_off) or { return '' }
134 return decode_xml_entities(body[rest_off..end])
135}
136
137// decode_xml_entities decodes the five predefined XML entities. We don't try
138// to handle arbitrary `&#nn;` sequences because S3 only ever uses these five.
139pub fn decode_xml_entities(s string) string {
140 if !s.contains('&') {
141 return s
142 }
143 mut out := []u8{cap: s.len}
144 mut i := 0
145 for i < s.len {
146 if s[i] != `&` {
147 out << s[i]
148 i++
149 continue
150 }
151 matched := match true {
152 s.len - i >= 5 && s[i..i + 5] == '&' {
153 out << `&`
154 i += 5
155 true
156 }
157 s.len - i >= 4 && s[i..i + 4] == '<' {
158 out << `<`
159 i += 4
160 true
161 }
162 s.len - i >= 4 && s[i..i + 4] == '>' {
163 out << `>`
164 i += 4
165 true
166 }
167 s.len - i >= 6 && s[i..i + 6] == '"' {
168 out << `"`
169 i += 6
170 true
171 }
172 s.len - i >= 6 && s[i..i + 6] == ''' {
173 out << `'`
174 i += 6
175 true
176 }
177 else {
178 false
179 }
180 }
181
182 if !matched {
183 out << s[i]
184 i++
185 }
186 }
187 return out.bytestr()
188}
189
190fn fallback_code_for(status int) string {
191 return match status {
192 301, 307 { 'PermanentRedirect' }
193 400 { 'BadRequest' }
194 401, 403 { 'AccessDenied' }
195 404 { 'NotFound' }
196 405 { 'MethodNotAllowed' }
197 409 { 'Conflict' }
198 411 { 'LengthRequired' }
199 412 { 'PreconditionFailed' }
200 416 { 'InvalidRange' }
201 429 { 'TooManyRequests' }
202 500 { 'InternalError' }
203 503 { 'ServiceUnavailable' }
204 else { 'HTTPError' }
205 }
206}
207