Skip to content

Commit 3c1a8e3

Browse files
authored
RTDB error handling revamp (#345)
1 parent aa8a53f commit 3c1a8e3

File tree

6 files changed

+313
-105
lines changed

6 files changed

+313
-105
lines changed

db/db.go

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -68,17 +68,8 @@ func NewClient(ctx context.Context, c *internal.DatabaseConfig) (*Client, error)
6868
return nil, err
6969
}
7070

71-
ep := func(b []byte) string {
72-
var p struct {
73-
Error string `json:"error"`
74-
}
75-
if err := json.Unmarshal(b, &p); err != nil {
76-
return ""
77-
}
78-
return p.Error
79-
}
80-
hc.ErrParser = ep
81-
71+
hc.CreateErrFn = handleRTDBError
72+
hc.SuccessFn = internal.HasSuccessStatus
8273
return &Client{
8374
hc: hc,
8475
url: fmt.Sprintf("https://%s", p.Host),
@@ -102,24 +93,18 @@ func (c *Client) NewRef(path string) *Ref {
10293
}
10394
}
10495

105-
func (c *Client) send(
106-
ctx context.Context,
107-
method, path string,
108-
body internal.HTTPEntity,
109-
opts ...internal.HTTPOption) (*internal.Response, error) {
110-
111-
if strings.ContainsAny(path, invalidChars) {
112-
return nil, fmt.Errorf("invalid path with illegal characters: %q", path)
96+
func (c *Client) sendAndUnmarshal(
97+
ctx context.Context, req *internal.Request, v interface{}) (*internal.Response, error) {
98+
if strings.ContainsAny(req.URL, invalidChars) {
99+
return nil, fmt.Errorf("invalid path with illegal characters: %q", req.URL)
113100
}
101+
102+
req.URL = fmt.Sprintf("%s%s.json", c.url, req.URL)
114103
if c.authOverride != "" {
115-
opts = append(opts, internal.WithQueryParam(authVarOverride, c.authOverride))
104+
req.Opts = append(req.Opts, internal.WithQueryParam(authVarOverride, c.authOverride))
116105
}
117-
return c.hc.Do(ctx, &internal.Request{
118-
Method: method,
119-
URL: fmt.Sprintf("%s%s.json", c.url, path),
120-
Body: body,
121-
Opts: opts,
122-
})
106+
107+
return c.hc.DoAndUnmarshal(ctx, req, v)
123108
}
124109

125110
func parsePath(path string) []string {
@@ -131,3 +116,16 @@ func parsePath(path string) []string {
131116
}
132117
return segs
133118
}
119+
120+
func handleRTDBError(resp *internal.Response) error {
121+
err := internal.NewFirebaseError(resp)
122+
var p struct {
123+
Error string `json:"error"`
124+
}
125+
json.Unmarshal(resp.Body, &p)
126+
if p.Error != "" {
127+
err.String = fmt.Sprintf("http error status: %d; reason: %s", resp.Status, p.Error)
128+
}
129+
130+
return err
131+
}

db/query.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,14 @@ func (q *Query) Get(ctx context.Context, v interface{}) error {
109109
if err := initQueryParams(q, qp); err != nil {
110110
return err
111111
}
112-
resp, err := q.client.send(ctx, "GET", q.path, nil, internal.WithQueryParams(qp))
113-
if err != nil {
114-
return err
112+
113+
req := &internal.Request{
114+
Method: http.MethodGet,
115+
URL: q.path,
116+
Opts: []internal.HTTPOption{internal.WithQueryParams(qp)},
115117
}
116-
return resp.Unmarshal(http.StatusOK, v)
118+
_, err := q.client.sendAndUnmarshal(ctx, req, v)
119+
return err
117120
}
118121

119122
// GetOrdered executes the Query and returns the results as an ordered slice.

db/query_test.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ package db
1616
import (
1717
"context"
1818
"fmt"
19+
"net/http"
1920
"reflect"
2021
"testing"
22+
23+
"firebase.google.com/go/v4/errorutils"
2124
)
2225

2326
var sortableKeysResp = map[string]interface{}{
@@ -768,10 +771,15 @@ func TestQueryHttpError(t *testing.T) {
768771

769772
want := "http error status: 500; reason: test error"
770773
result, err := testref.OrderByChild("child").GetOrdered(context.Background())
771-
if err == nil || err.Error() != want {
772-
t.Errorf("GetOrdered() = %v; want = %v", err, want)
774+
if result != nil || err == nil || err.Error() != want {
775+
t.Fatalf("GetOrdered() = (%v, %v); want = (nil, %v)", result, err, want)
773776
}
774-
if result != nil {
775-
t.Errorf("GetOrdered() = %v; want = nil", result)
777+
if !errorutils.IsInternal(err) {
778+
t.Errorf("IsInternal(err) = false; want = true")
779+
}
780+
781+
resp := errorutils.HTTPResponse(err)
782+
if resp == nil || resp.StatusCode != http.StatusInternalServerError {
783+
t.Errorf("HTTPResponse(err) = %v; want = {StatusCode: %d}", resp, http.StatusInternalServerError)
776784
}
777785
}

db/ref.go

Lines changed: 98 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -75,33 +75,41 @@ func (r *Ref) Child(path string) *Ref {
7575
// therefore v has the same requirements as the json package. Specifically, it must be a pointer,
7676
// and must not be nil.
7777
func (r *Ref) Get(ctx context.Context, v interface{}) error {
78-
resp, err := r.send(ctx, "GET")
79-
if err != nil {
80-
return err
78+
req := &internal.Request{
79+
Method: http.MethodGet,
8180
}
82-
return resp.Unmarshal(http.StatusOK, v)
81+
_, err := r.sendAndUnmarshal(ctx, req, v)
82+
return err
8383
}
8484

8585
// GetWithETag retrieves the value at the current database location, along with its ETag.
8686
func (r *Ref) GetWithETag(ctx context.Context, v interface{}) (string, error) {
87-
resp, err := r.send(ctx, "GET", internal.WithHeader("X-Firebase-ETag", "true"))
87+
req := &internal.Request{
88+
Method: http.MethodGet,
89+
Opts: []internal.HTTPOption{
90+
internal.WithHeader("X-Firebase-ETag", "true"),
91+
},
92+
}
93+
resp, err := r.sendAndUnmarshal(ctx, req, v)
8894
if err != nil {
8995
return "", err
90-
} else if err := resp.Unmarshal(http.StatusOK, v); err != nil {
91-
return "", err
9296
}
97+
9398
return resp.Header.Get("Etag"), nil
9499
}
95100

96101
// GetShallow performs a shallow read on the current database location.
97102
//
98103
// Shallow reads do not retrieve the child nodes of the current reference.
99104
func (r *Ref) GetShallow(ctx context.Context, v interface{}) error {
100-
resp, err := r.send(ctx, "GET", internal.WithQueryParam("shallow", "true"))
101-
if err != nil {
102-
return err
105+
req := &internal.Request{
106+
Method: http.MethodGet,
107+
Opts: []internal.HTTPOption{
108+
internal.WithQueryParam("shallow", "true"),
109+
},
103110
}
104-
return resp.Unmarshal(http.StatusOK, v)
111+
_, err := r.sendAndUnmarshal(ctx, req, v)
112+
return err
105113
}
106114

107115
// GetIfChanged retrieves the value and ETag of the current database location only if the specified
@@ -112,16 +120,26 @@ func (r *Ref) GetShallow(ctx context.Context, v interface{}) error {
112120
// If the etag matches, returns false along with the same ETag passed into the function. No data
113121
// will be stored in v in this case.
114122
func (r *Ref) GetIfChanged(ctx context.Context, etag string, v interface{}) (bool, string, error) {
115-
resp, err := r.send(ctx, "GET", internal.WithHeader("If-None-Match", etag))
123+
req := &internal.Request{
124+
Method: http.MethodGet,
125+
Opts: []internal.HTTPOption{
126+
internal.WithHeader("If-None-Match", etag),
127+
},
128+
SuccessFn: successOrNotModified,
129+
}
130+
resp, err := r.sendAndUnmarshal(ctx, req, nil)
116131
if err != nil {
117132
return false, "", err
118133
}
134+
119135
if resp.Status == http.StatusNotModified {
120136
return false, etag, nil
121137
}
122-
if err := resp.Unmarshal(http.StatusOK, v); err != nil {
138+
139+
if err := json.Unmarshal(resp.Body, v); err != nil {
123140
return false, "", err
124141
}
142+
125143
return true, resp.Header.Get("ETag"), nil
126144
}
127145

@@ -131,28 +149,39 @@ func (r *Ref) GetIfChanged(ctx context.Context, etag string, v interface{}) (boo
131149
// v has the same requirements as the json package. Values like functions and channels cannot be
132150
// saved into Realtime Database.
133151
func (r *Ref) Set(ctx context.Context, v interface{}) error {
134-
resp, err := r.sendWithBody(ctx, "PUT", v, internal.WithQueryParam("print", "silent"))
135-
if err != nil {
136-
return err
152+
req := &internal.Request{
153+
Method: http.MethodPut,
154+
Body: internal.NewJSONEntity(v),
155+
Opts: []internal.HTTPOption{
156+
internal.WithQueryParam("print", "silent"),
157+
},
137158
}
138-
return resp.CheckStatus(http.StatusNoContent)
159+
_, err := r.sendAndUnmarshal(ctx, req, nil)
160+
return err
139161
}
140162

141163
// SetIfUnchanged conditionally sets the data at this location to the given value.
142164
//
143165
// Sets the data at this location to v only if the specified ETag matches. Returns true if the
144166
// value is written. Returns false if no changes are made to the database.
145167
func (r *Ref) SetIfUnchanged(ctx context.Context, etag string, v interface{}) (bool, error) {
146-
resp, err := r.sendWithBody(ctx, "PUT", v, internal.WithHeader("If-Match", etag))
168+
req := &internal.Request{
169+
Method: http.MethodPut,
170+
Body: internal.NewJSONEntity(v),
171+
Opts: []internal.HTTPOption{
172+
internal.WithHeader("If-Match", etag),
173+
},
174+
SuccessFn: successOrPreconditionFailed,
175+
}
176+
resp, err := r.sendAndUnmarshal(ctx, req, nil)
147177
if err != nil {
148178
return false, err
149179
}
180+
150181
if resp.Status == http.StatusPreconditionFailed {
151182
return false, nil
152183
}
153-
if err := resp.CheckStatus(http.StatusOK); err != nil {
154-
return false, err
155-
}
184+
156185
return true, nil
157186
}
158187

@@ -164,16 +193,18 @@ func (r *Ref) Push(ctx context.Context, v interface{}) (*Ref, error) {
164193
if v == nil {
165194
v = ""
166195
}
167-
resp, err := r.sendWithBody(ctx, "POST", v)
168-
if err != nil {
169-
return nil, err
196+
197+
req := &internal.Request{
198+
Method: http.MethodPost,
199+
Body: internal.NewJSONEntity(v),
170200
}
171201
var d struct {
172202
Name string `json:"name"`
173203
}
174-
if err := resp.Unmarshal(http.StatusOK, &d); err != nil {
204+
if _, err := r.sendAndUnmarshal(ctx, req, &d); err != nil {
175205
return nil, err
176206
}
207+
177208
return r.Child(d.Name), nil
178209
}
179210

@@ -182,11 +213,16 @@ func (r *Ref) Update(ctx context.Context, v map[string]interface{}) error {
182213
if len(v) == 0 {
183214
return fmt.Errorf("value argument must be a non-empty map")
184215
}
185-
resp, err := r.sendWithBody(ctx, "PATCH", v, internal.WithQueryParam("print", "silent"))
186-
if err != nil {
187-
return err
216+
217+
req := &internal.Request{
218+
Method: http.MethodPatch,
219+
Body: internal.NewJSONEntity(v),
220+
Opts: []internal.HTTPOption{
221+
internal.WithQueryParam("print", "silent"),
222+
},
188223
}
189-
return resp.CheckStatus(http.StatusNoContent)
224+
_, err := r.sendAndUnmarshal(ctx, req, nil)
225+
return err
190226
}
191227

192228
// UpdateFn represents a function type that can be passed into Transaction().
@@ -207,55 +243,65 @@ type UpdateFn func(TransactionNode) (interface{}, error)
207243
// The update function may also force an early abort by returning an error instead of returning a
208244
// value.
209245
func (r *Ref) Transaction(ctx context.Context, fn UpdateFn) error {
210-
resp, err := r.send(ctx, "GET", internal.WithHeader("X-Firebase-ETag", "true"))
246+
req := &internal.Request{
247+
Method: http.MethodGet,
248+
Opts: []internal.HTTPOption{
249+
internal.WithHeader("X-Firebase-ETag", "true"),
250+
},
251+
}
252+
resp, err := r.sendAndUnmarshal(ctx, req, nil)
211253
if err != nil {
212254
return err
213-
} else if err := resp.CheckStatus(http.StatusOK); err != nil {
214-
return err
215255
}
216-
etag := resp.Header.Get("Etag")
217256

257+
etag := resp.Header.Get("Etag")
218258
for i := 0; i < txnRetries; i++ {
219259
new, err := fn(&transactionNodeImpl{resp.Body})
220260
if err != nil {
221261
return err
222262
}
223-
resp, err = r.sendWithBody(ctx, "PUT", new, internal.WithHeader("If-Match", etag))
263+
264+
req := &internal.Request{
265+
Method: http.MethodPut,
266+
Body: internal.NewJSONEntity(new),
267+
Opts: []internal.HTTPOption{
268+
internal.WithHeader("If-Match", etag),
269+
},
270+
SuccessFn: successOrPreconditionFailed,
271+
}
272+
resp, err = r.sendAndUnmarshal(ctx, req, nil)
224273
if err != nil {
225274
return err
226275
}
276+
227277
if resp.Status == http.StatusOK {
228278
return nil
229-
} else if err := resp.CheckStatus(http.StatusPreconditionFailed); err != nil {
230-
return err
231279
}
280+
232281
etag = resp.Header.Get("ETag")
233282
}
234283
return fmt.Errorf("transaction aborted after failed retries")
235284
}
236285

237286
// Delete removes this node from the database.
238287
func (r *Ref) Delete(ctx context.Context) error {
239-
resp, err := r.send(ctx, "DELETE")
240-
if err != nil {
241-
return err
288+
req := &internal.Request{
289+
Method: http.MethodDelete,
242290
}
243-
return resp.CheckStatus(http.StatusOK)
291+
_, err := r.sendAndUnmarshal(ctx, req, nil)
292+
return err
244293
}
245294

246-
func (r *Ref) send(
247-
ctx context.Context,
248-
method string,
249-
opts ...internal.HTTPOption) (*internal.Response, error) {
250-
251-
return r.client.send(ctx, method, r.Path, nil, opts...)
295+
func (r *Ref) sendAndUnmarshal(
296+
ctx context.Context, req *internal.Request, v interface{}) (*internal.Response, error) {
297+
req.URL = r.Path
298+
return r.client.sendAndUnmarshal(ctx, req, v)
252299
}
253300

254-
func (r *Ref) sendWithBody(
255-
ctx context.Context,
256-
method string,
257-
body interface{},
258-
opts ...internal.HTTPOption) (*internal.Response, error) {
301+
func successOrNotModified(resp *internal.Response) bool {
302+
return internal.HasSuccessStatus(resp) || resp.Status == http.StatusNotModified
303+
}
259304

260-
return r.client.send(ctx, method, r.Path, internal.NewJSONEntity(body), opts...)
305+
func successOrPreconditionFailed(resp *internal.Response) bool {
306+
return internal.HasSuccessStatus(resp) || resp.Status == http.StatusPreconditionFailed
261307
}

0 commit comments

Comments
 (0)