Skip to content

Commit 7e80afc

Browse files
author
Chris Stockton
committed
feat: add pkg hookserrors for consistent error handling
Package hookserrors holds the Error type and some functions to Check responses for errors.
1 parent 062da5d commit 7e80afc

File tree

2 files changed

+361
-0
lines changed

2 files changed

+361
-0
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Package hookserrors holds the Error type and some functions to Check
2+
// responses for errors.
3+
package hookserrors
4+
5+
import (
6+
"encoding/json"
7+
"net/http"
8+
9+
"github.com/supabase/auth/internal/api/apierrors"
10+
)
11+
12+
// Error is the type propagated by hook endpoints to communicate failure.
13+
type Error struct {
14+
HTTPCode int `json:"http_code,omitempty"`
15+
Message string `json:"message,omitempty"`
16+
}
17+
18+
// Error implements the error interface by returning e.Message.
19+
func (e *Error) Error() string { return e.Message }
20+
21+
// As implements the errors.As interface to allow unwrapping as either an
22+
// Error or apierrors.HTTPError, depending on the needs of the caller.
23+
func (e *Error) As(target any) bool {
24+
switch T := target.(type) {
25+
case **Error:
26+
v := (*T)
27+
if v == nil {
28+
return false
29+
}
30+
v.HTTPCode = e.HTTPCode
31+
v.Message = e.Message
32+
return true
33+
case *Error:
34+
T.HTTPCode = e.HTTPCode
35+
T.Message = e.Message
36+
return true
37+
case **apierrors.HTTPError:
38+
v := (*T)
39+
if v == nil {
40+
return false
41+
}
42+
v.HTTPStatus = e.HTTPCode
43+
v.Message = e.Message
44+
return true
45+
case *apierrors.HTTPError:
46+
T.HTTPStatus = e.HTTPCode
47+
T.Message = e.Message
48+
return true
49+
default:
50+
return false
51+
}
52+
}
53+
54+
// Check will attempt to extract a hook Error from a byte slice and return a
55+
// non-nil error, otherwise Check returns nil if no error was found.
56+
func Check(b []byte) error {
57+
e, ok := fromBytes(b)
58+
if !ok {
59+
return nil
60+
}
61+
return check(e)
62+
}
63+
64+
func check(e *Error) error {
65+
if e == nil {
66+
return nil
67+
}
68+
69+
// TODO(cstockton): Changing this would be a BC break, but it also
70+
// doesn't seem to be the best API. For example returning an error object
71+
// with an http_code field set to 500 would not count as an error.
72+
if e.Message == "" {
73+
return nil
74+
}
75+
76+
httpCode := e.HTTPCode
77+
if httpCode == 0 {
78+
httpCode = http.StatusInternalServerError
79+
}
80+
81+
httpError := &apierrors.HTTPError{
82+
HTTPStatus: httpCode,
83+
Message: e.Message,
84+
}
85+
return httpError.WithInternalError(e)
86+
}
87+
88+
func fromBytes(b []byte) (*Error, bool) {
89+
var dst struct {
90+
Error *struct {
91+
HTTPCode int `json:"http_code,omitempty"`
92+
Message string `json:"message,omitempty"`
93+
} `json:"error,omitempty"`
94+
}
95+
if err := json.Unmarshal(b, &dst); err != nil {
96+
return nil, false
97+
}
98+
if dst.Error == nil {
99+
return nil, false
100+
}
101+
e := &Error{
102+
HTTPCode: dst.Error.HTTPCode,
103+
Message: dst.Error.Message,
104+
}
105+
return e, true
106+
}
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
package hookserrors
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/supabase/auth/internal/api/apierrors"
8+
)
9+
10+
func TestFromBytes(t *testing.T) {
11+
cases := []struct {
12+
from string
13+
ok bool
14+
exp *Error
15+
}{
16+
{from: `<b>text</b>`},
17+
{from: `null`},
18+
{from: `{}`},
19+
{from: `{"key": "val"}`},
20+
{from: `{"error": null}`},
21+
22+
{
23+
from: `{"error": {"message": "failed"}}`,
24+
ok: true, exp: &Error{HTTPCode: 0, Message: "failed"},
25+
},
26+
{
27+
from: `{"error": {"http_code": 400}}`,
28+
ok: true, exp: &Error{HTTPCode: 400},
29+
},
30+
{
31+
from: `{"error": {"message": "failed", "http_code": 400}}`,
32+
ok: true, exp: &Error{HTTPCode: 400, Message: "failed"},
33+
},
34+
{
35+
from: `{"error": {"message": "failed", "http_code": 403}}`,
36+
ok: true, exp: &Error{HTTPCode: 403, Message: "failed"},
37+
},
38+
}
39+
for idx, tc := range cases {
40+
t.Logf("test #%v - exp Check(%v) = (%#v, %v)", idx, tc.from, tc.exp, tc.ok)
41+
42+
e, ok := fromBytes([]byte(tc.from))
43+
if exp, got := tc.ok, ok; exp != got {
44+
t.Fatalf("exp %v; got %v", exp, got)
45+
}
46+
if !tc.ok {
47+
if e != nil {
48+
t.Fatalf("exp nil; got %v", e)
49+
}
50+
continue
51+
}
52+
if exp, got := tc.exp.HTTPCode, e.HTTPCode; exp != got {
53+
t.Fatalf("exp HTTPCode %v; got %v", exp, got)
54+
}
55+
if exp, got := tc.exp.Message, e.Message; exp != got {
56+
t.Fatalf("exp Message %q; got %q", exp, got)
57+
}
58+
59+
err := (error)(e)
60+
if exp, got := tc.exp.Message, err.Error(); exp != got {
61+
t.Fatalf("exp Error() %q; got %q", exp, got)
62+
}
63+
}
64+
}
65+
66+
func TestCheck(t *testing.T) {
67+
{
68+
if err := Check([]byte(`invalidjson`)); err != nil {
69+
t.Fatalf("exp nil err; got %v", err)
70+
}
71+
if err := Check([]byte(`{"error": nil}`)); err != nil {
72+
t.Fatalf("exp nil err; got %v", err)
73+
}
74+
if err := Check([]byte(`{"error": {"message": "failed"}}`)); err == nil {
75+
t.Fatal("exp non-nil err")
76+
}
77+
78+
{
79+
data := `{"error": {"message": "failed", "http_code": 403}}`
80+
err := Check([]byte(data))
81+
if err == nil {
82+
t.Fatal("exp non-nil err")
83+
}
84+
85+
e, ok := err.(*apierrors.HTTPError)
86+
if !ok {
87+
t.Fatal("exp error to be http.Error")
88+
}
89+
if exp, got := e.HTTPStatus, 403; exp != got {
90+
t.Fatalf("exp HTTPCode %v; got %v", exp, got)
91+
}
92+
if exp, got := e.Message, "failed"; exp != got {
93+
t.Fatalf("exp Message %q; got %q", exp, got)
94+
}
95+
}
96+
}
97+
98+
{
99+
if err := check(nil); err != nil {
100+
t.Fatalf("exp nil err; got %v", err)
101+
}
102+
if err := check(&Error{Message: ""}); err != nil {
103+
t.Fatalf("exp nil err; got %v", err)
104+
}
105+
if err := check(&Error{Message: "failed"}); err == nil {
106+
t.Fatal("exp non-nil err")
107+
}
108+
}
109+
}
110+
111+
func TestAs(t *testing.T) {
112+
113+
{
114+
err := &Error{
115+
Message: "failed",
116+
HTTPCode: 403,
117+
}
118+
e := new(apierrors.HTTPError)
119+
if !errors.As(err, &e) {
120+
t.Fatal("exp errors.As to return true")
121+
}
122+
if exp, got := e.HTTPStatus, 403; exp != got {
123+
t.Fatalf("exp HTTPCode %v; got %v", exp, got)
124+
}
125+
if exp, got := e.Message, "failed"; exp != got {
126+
t.Fatalf("exp Message %q; got %q", exp, got)
127+
}
128+
}
129+
130+
{
131+
err := &Error{
132+
Message: "failed",
133+
HTTPCode: 403,
134+
}
135+
e := new(Error)
136+
if !errors.As(err, &e) {
137+
t.Fatal("exp errors.As to return true")
138+
}
139+
if exp, got := e.HTTPCode, 403; exp != got {
140+
t.Fatalf("exp HTTPCode %v; got %v", exp, got)
141+
}
142+
if exp, got := e.Message, "failed"; exp != got {
143+
t.Fatalf("exp Message %q; got %q", exp, got)
144+
}
145+
}
146+
147+
{
148+
err := &Error{
149+
Message: "failed",
150+
HTTPCode: 403,
151+
}
152+
e := new(apierrors.HTTPError)
153+
if !err.As(&e) {
154+
t.Fatal("exp errors.As to return true")
155+
}
156+
if exp, got := e.HTTPStatus, 403; exp != got {
157+
t.Fatalf("exp HTTPCode %v; got %v", exp, got)
158+
}
159+
if exp, got := e.Message, "failed"; exp != got {
160+
t.Fatalf("exp Message %q; got %q", exp, got)
161+
}
162+
}
163+
164+
{
165+
err := &Error{
166+
Message: "failed",
167+
HTTPCode: 403,
168+
}
169+
e := new(Error)
170+
if !err.As(&e) {
171+
t.Fatal("exp errors.As to return true")
172+
}
173+
if exp, got := e.HTTPCode, 403; exp != got {
174+
t.Fatalf("exp HTTPCode %v; got %v", exp, got)
175+
}
176+
if exp, got := e.Message, "failed"; exp != got {
177+
t.Fatalf("exp Message %q; got %q", exp, got)
178+
}
179+
}
180+
181+
{
182+
err := &Error{
183+
Message: "failed",
184+
HTTPCode: 403,
185+
}
186+
e := new(apierrors.HTTPError)
187+
if !err.As(e) {
188+
t.Fatal("exp errors.As to return true")
189+
}
190+
if exp, got := e.HTTPStatus, 403; exp != got {
191+
t.Fatalf("exp HTTPCode %v; got %v", exp, got)
192+
}
193+
if exp, got := e.Message, "failed"; exp != got {
194+
t.Fatalf("exp Message %q; got %q", exp, got)
195+
}
196+
}
197+
198+
{
199+
err := &Error{
200+
Message: "failed",
201+
HTTPCode: 403,
202+
}
203+
e := new(Error)
204+
if !err.As(e) {
205+
t.Fatal("exp errors.As to return true")
206+
}
207+
if exp, got := e.HTTPCode, 403; exp != got {
208+
t.Fatalf("exp HTTPCode %v; got %v", exp, got)
209+
}
210+
if exp, got := e.Message, "failed"; exp != got {
211+
t.Fatalf("exp Message %q; got %q", exp, got)
212+
}
213+
}
214+
215+
{
216+
err := errors.New("sentinel")
217+
e := new(Error)
218+
if errors.As(err, &e) {
219+
t.Fatal("exp errors.As to return false")
220+
}
221+
}
222+
223+
{
224+
err := &Error{
225+
Message: "failed",
226+
HTTPCode: 403,
227+
}
228+
e := (*Error)(nil)
229+
if err.As(&e) {
230+
t.Fatal("exp errors.As to return false")
231+
}
232+
}
233+
234+
{
235+
err := &Error{
236+
Message: "failed",
237+
HTTPCode: 403,
238+
}
239+
e := (*apierrors.HTTPError)(nil)
240+
if err.As(&e) {
241+
t.Fatal("exp errors.As to return false")
242+
}
243+
}
244+
245+
{
246+
err := &Error{
247+
Message: "failed",
248+
HTTPCode: 403,
249+
}
250+
e := (*error)(nil)
251+
if err.As(&e) {
252+
t.Fatal("exp errors.As to return false")
253+
}
254+
}
255+
}

0 commit comments

Comments
 (0)