diff options
Diffstat (limited to 'errors.go')
| -rw-r--r-- | errors.go | 139 |
1 files changed, 139 insertions, 0 deletions
diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..98a2385 --- /dev/null +++ b/errors.go @@ -0,0 +1,139 @@ +package cepis + +import ( + "encoding/json" + "errors" + "fmt" + "time" +) + +// Err is the root of every error this package returns. Callers who only care +// that something went wrong can match one value: +// +// if errors.Is(err, cepis.Err) { ... } +// +// Below it, the split is by the decision the caller has to make, not by the +// HTTP status that caused it. [ErrInvalidCEP] and [ErrCEPNotFound] are +// problems with the request, and retrying them is pointless — the same CEP +// fails again. [ErrNetwork], [ErrServer] and [ErrRateLimited] are problems +// with transport, so retrying may help; the client already retried on its own +// before returning any of them. +var Err = errors.New("cepis") + +// The kinds of failure, each wrapping [Err]. Match them with errors.Is: +// +// if errors.Is(err, cepis.ErrCEPNotFound) { ... } +var ( + // ErrInvalidCEP means the CEP is not 8 digits. Usually returned without + // any request being made: the client validates the format before going + // to the network. It is also what a 400 from the API becomes — the + // format passed local validation, so the server knows something this + // client does not. + ErrInvalidCEP = fmt.Errorf("%w: invalid CEP", Err) + + // ErrCEPNotFound means the CEP is well-formed but absent from the DNE + // database. Looking a CEP up is conceptually a key lookup, and this is + // the missing key. + ErrCEPNotFound = fmt.Errorf("%w: CEP not found", Err) + + // ErrRateLimited means the API (or the edge in front of it) answered 429 + // on every attempt. Error.RetryAfter carries what the server asked for. + ErrRateLimited = fmt.Errorf("%w: rate limited", Err) + + // ErrServer means the API answered 5xx on every attempt. Error.Status + // holds the status seen on the last one. + ErrServer = fmt.Errorf("%w: server error", Err) + + // ErrNetwork means the request never completed: DNS, refused connection, + // timeout, TLS. Returned only after every configured attempt failed. + ErrNetwork = fmt.Errorf("%w: network error", Err) + + // ErrInvalidConfig means an [Option] was given a value the client cannot + // work with. Only [New] returns it. + ErrInvalidConfig = fmt.Errorf("%w: invalid configuration", Err) + + // ErrInvalidResponse means a success status came back with a body that + // cannot be parsed. In practice something between you and the API is + // answering instead of it — a captive Wi-Fi portal, a corporate proxy, + // an HTML error page. Useful as a diagnosis: the problem is not in your + // code. + ErrInvalidResponse = fmt.Errorf("%w: invalid response", Err) +) + +// Error is the concrete type behind every failure in this package. It carries +// the context a caller may need to act on — which CEP, which status, how long +// the server asked to be left alone — and it matches its kind through +// errors.Is: +// +// var cerr *cepis.Error +// if errors.As(err, &cerr) && errors.Is(cerr, cepis.ErrRateLimited) { +// reschedule(cerr.RetryAfter) +// } +type Error struct { + // Kind is one of the package sentinels, and what errors.Is matches. + Kind error + + // CEP is the normalised CEP the call was about, when there was one. + CEP string + + // Status is the last HTTP status seen, or 0 when no response arrived. + Status int + + // RetryAfter is what the server asked for through the Retry-After + // header, or 0 when it did not say. It is reported as received, before + // the client's own cap is applied to it. + RetryAfter time.Duration + + // Message describes the failure without the "cepis: " prefix. + Message string + + // Cause is the underlying error, when one exists — typically the + // net/http error behind [ErrNetwork]. + Cause error +} + +// Error implements the error interface. +func (e *Error) Error() string { + msg := e.Message + if msg == "" { + if e.Kind != nil { + msg = e.Kind.Error() + } else { + msg = Err.Error() + } + } else { + msg = "cepis: " + msg + } + if e.Cause != nil { + return msg + ": " + e.Cause.Error() + } + return msg +} + +// Unwrap exposes both the kind and the underlying cause, so errors.Is matches +// the package sentinels and errors.As reaches a net.Error underneath. +func (e *Error) Unwrap() []error { + switch { + case e.Kind != nil && e.Cause != nil: + return []error{e.Kind, e.Cause} + case e.Kind != nil: + return []error{e.Kind} + case e.Cause != nil: + return []error{e.Cause} + } + return nil +} + +func newError(kind error, format string, args ...any) *Error { + return &Error{Kind: kind, Message: fmt.Sprintf(format, args...)} +} + +func apiMessage(body []byte, fallback string) string { + var payload struct { + Mensagem string `json:"mensagem"` + } + if err := json.Unmarshal(body, &payload); err == nil && payload.Mensagem != "" { + return payload.Mensagem + } + return fallback +} |