-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcodec.go
More file actions
159 lines (151 loc) · 4.53 KB
/
Copy pathcodec.go
File metadata and controls
159 lines (151 loc) · 4.53 KB
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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
package errx
import (
"encoding/json"
"errors"
"sync"
"time"
)
var (
migMu sync.RWMutex
codeMig = map[string]string{}
)
// RegisterCodeMigration maps a retired error code to its replacement.
// Decode rewrites old to new, so a newer service still recognises errors
// minted by an older one (and vice versa once both register the pair) —
// the cross-version-skew tolerance cockroachdb/errors gets from
// RegisterTypeMigration, here for the JSON wire form. Explicit, no init().
func RegisterCodeMigration(oldCode, newCode string) {
migMu.Lock()
codeMig[oldCode] = newCode
migMu.Unlock()
}
func migrateCode(code string) string {
if code == "" {
return code
}
migMu.RLock()
defer migMu.RUnlock()
// Follow a short chain (A->B->C) with a cycle guard.
for i := 0; i < 8; i++ {
n, ok := codeMig[code]
if !ok || n == code {
break
}
code = n
}
return code
}
// wire is the JSON projection used to ship an *Error across a service
// boundary and reconstruct a typed *Error on the far side. It is lossy by
// design: the cause and suppressed errors travel as messages (the remote
// concrete types are not importable here), but identity — domain, code,
// class, severity, retry metadata, fingerprint — survives, so errx.Code /
// HasCode / IsRetryable / ClassOf keep working after the hop. Only Safe
// fields cross; unsafe values are dropped, not redacted-in-place, so
// nothing sensitive ever leaves the process.
type wire struct {
Domain string `json:"domain,omitempty"`
Code string `json:"code,omitempty"`
Class Class `json:"class"`
Severity Severity `json:"severity"`
Msg string `json:"msg,omitempty"`
Public string `json:"public,omitempty"`
Hint string `json:"hint,omitempty"`
Owner string `json:"owner,omitempty"`
Retryable bool `json:"retryable,omitempty"`
RetryAfter string `json:"retryAfter,omitempty"`
TraceID string `json:"traceId,omitempty"`
SpanID string `json:"spanId,omitempty"`
Fields []Field `json:"fields,omitempty"` // safe fields only
Stack []Frame `json:"stack,omitempty"`
Suppressed []string `json:"suppressed,omitempty"`
Cause string `json:"cause,omitempty"`
Fp string `json:"fingerprint,omitempty"`
}
// MarshalJSON implements json.Marshaler so json.Marshal(err) yields the
// wire form. Unsafe fields are omitted entirely (never serialized).
func (e *Error) MarshalJSON() ([]byte, error) {
if e == nil {
return []byte("null"), nil
}
w := wire{
Domain: e.domain,
Code: e.code,
Class: e.class,
Severity: e.severity,
Msg: e.msg,
Public: e.publicMsg,
Hint: e.hint,
Owner: e.owner,
Retryable: e.retryable,
TraceID: e.traceID,
SpanID: e.spanID,
Stack: e.Frames(),
Fp: e.Fingerprint(),
}
if e.retryAfter > 0 {
w.RetryAfter = e.retryAfter.String()
}
for _, f := range e.fields {
if f.Safe {
w.Fields = append(w.Fields, f)
}
}
if e.cause != nil {
w.Cause = e.cause.Error()
}
for _, s := range e.suppressed {
w.Suppressed = append(w.Suppressed, s.Error())
}
return json.Marshal(w)
}
// Encode serializes err to the JSON wire form. err should contain an
// *Error; otherwise a minimal envelope with just the message is produced.
func Encode(err error) ([]byte, error) {
if e := Get(err); e != nil {
return json.Marshal(e)
}
if err == nil {
return []byte("null"), nil
}
return json.Marshal(wire{Msg: err.Error(), Class: ClassExpected, Severity: SevError})
}
// Decode reconstructs a typed *Error from the wire form. The cause and any
// suppressed errors come back as opaque errors.New values carrying their
// original messages (remote concrete types are not importable), but the
// reconstructed error keeps its identity so errx.Code / HasCode /
// IsRetryable / ClassOf behave the same across the boundary.
func Decode(data []byte) (*Error, error) {
var w wire
if err := json.Unmarshal(data, &w); err != nil {
return nil, err
}
e := &Error{
domain: w.Domain,
code: migrateCode(w.Code),
class: w.Class,
severity: w.Severity,
msg: w.Msg,
publicMsg: w.Public,
hint: w.Hint,
owner: w.Owner,
retryable: w.Retryable,
traceID: w.TraceID,
spanID: w.SpanID,
fields: w.Fields,
decoded: w.Stack,
fp: w.Fp,
}
if w.RetryAfter != "" {
if d, err := time.ParseDuration(w.RetryAfter); err == nil {
e.retryAfter = d
}
}
if w.Cause != "" {
e.cause = errors.New(w.Cause)
}
for _, s := range w.Suppressed {
e.suppressed = append(e.suppressed, errors.New(s))
}
return e, nil
}