summaryrefslogtreecommitdiffstats
path: root/errors.go
blob: 98a2385fdfe2cba0216cd44dfa1240a0f8e73d39 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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
}