| 1 | package suite |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "fmt" |
| 6 | ) |
| 7 | |
| 8 | // ErrNotAvailable is the whole vocabulary of an S3 refusal. |
| 9 | // |
| 10 | // Doctrine §4.2: a content fetch for anything outside the custody predicate |
| 11 | // answers 404, never 403, and the status is identical for "not team-visible" |
| 12 | // and "does not exist". Deleted, never team-visible, and departed from |
| 13 | // team-visibility are one answer here on purpose — a consumer that could tell |
| 14 | // them apart would learn that a private item exists. |
| 15 | var ErrNotAvailable = errors.New("suite: item not available") |
| 16 | |
| 17 | // ErrContractBreach marks a sibling answering in a way the contract forbids. |
| 18 | // It is separate from a refusal and from an outage because it is neither: it |
| 19 | // is a bug in the sibling that a consumer should surface rather than absorb. |
| 20 | var ErrContractBreach = errors.New("suite: sibling broke the contract") |
| 21 | |
| 22 | // DecodeS3 reads a content fetch. There is no asker anywhere in this call: |
| 23 | // S3 is custody-scoped and person-less, and memoria's draft 3 added a person |
| 24 | // to it, which both broke the ingest path — the warm-up, the rebuild and |
| 25 | // derived-artefact regeneration have no asker — and pulled individually |
| 26 | // granted rows into a store that may not hold them. |
| 27 | func DecodeS3(status int, body []byte) ([]byte, error) { |
| 28 | switch { |
| 29 | case status >= 200 && status <= 299: |
| 30 | return body, nil |
| 31 | |
| 32 | case status == 404: |
| 33 | return nil, ErrNotAvailable |
| 34 | |
| 35 | case status == 403: |
| 36 | // "404, never 403" is the rule, because 403 means "this exists and |
| 37 | // you may not have it" — the one fact custody hides. A sibling |
| 38 | // answering 403 is leaking, and folding it into ErrNotAvailable would |
| 39 | // keep that leak invisible to everyone downstream. |
| 40 | return nil, fmt.Errorf("%w: S3 answered 403 where the contract requires 404", ErrContractBreach) |
| 41 | |
| 42 | default: |
| 43 | return nil, fmt.Errorf("suite: S3 answered %d", status) |
| 44 | } |
| 45 | } |
| 46 | |