summaryrefslogtreecommitdiffstats
path: root/errors.go
diff options
context:
space:
mode:
Diffstat (limited to 'errors.go')
-rw-r--r--errors.go139
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
+}