Introduce APIError type and make ErrorEvent.Err public

This makes it a little bit easier to act on API errors that happen while
streaming.
pull/177/head
Alexander Bakker 2023-03-11 12:10:46 +01:00
parent 9faaa4f0dc
commit 972ffb4771
6 changed files with 49 additions and 24 deletions

View File

@ -3,12 +3,26 @@ package mastodon
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
) )
type APIError struct {
prefix string
Message string
StatusCode int
}
func (e *APIError) Error() string {
errMsg := fmt.Sprintf("%s: %d %s", e.prefix, e.StatusCode, http.StatusText(e.StatusCode))
if e.Message == "" {
return errMsg
}
return fmt.Sprintf("%s: %s", errMsg, e.Message)
}
// Base64EncodeFileName returns the base64 data URI format string of the file with the file name. // Base64EncodeFileName returns the base64 data URI format string of the file with the file name.
func Base64EncodeFileName(filename string) (string, error) { func Base64EncodeFileName(filename string) (string, error) {
file, err := os.Open(filename) file, err := os.Open(filename)
@ -41,15 +55,18 @@ func Base64Encode(file *os.File) (string, error) {
func String(v string) *string { return &v } func String(v string) *string { return &v }
func parseAPIError(prefix string, resp *http.Response) error { func parseAPIError(prefix string, resp *http.Response) error {
errMsg := fmt.Sprintf("%s: %s", prefix, resp.Status) res := APIError{
prefix: prefix,
StatusCode: resp.StatusCode,
}
var e struct { var e struct {
Error string `json:"error"` Error string `json:"error"`
} }
json.NewDecoder(resp.Body).Decode(&e) json.NewDecoder(resp.Body).Decode(&e)
if e.Error != "" { if e.Error != "" {
errMsg = fmt.Sprintf("%s: %s", errMsg, e.Error) res.Message = e.Error
} }
return errors.New(errMsg) return &res
} }

View File

@ -73,7 +73,11 @@ func TestString(t *testing.T) {
func TestParseAPIError(t *testing.T) { func TestParseAPIError(t *testing.T) {
// No api error. // No api error.
r := ioutil.NopCloser(strings.NewReader(`<html><head><title>404</title></head></html>`)) r := ioutil.NopCloser(strings.NewReader(`<html><head><title>404</title></head></html>`))
err := parseAPIError("bad request", &http.Response{Status: "404 Not Found", Body: r}) err := parseAPIError("bad request", &http.Response{
Status: "404 Not Found",
StatusCode: http.StatusNotFound,
Body: r,
})
want := "bad request: 404 Not Found" want := "bad request: 404 Not Found"
if err.Error() != want { if err.Error() != want {
t.Fatalf("want %q but %q", want, err.Error()) t.Fatalf("want %q but %q", want, err.Error())
@ -81,7 +85,11 @@ func TestParseAPIError(t *testing.T) {
// With api error. // With api error.
r = ioutil.NopCloser(strings.NewReader(`{"error":"Record not found"}`)) r = ioutil.NopCloser(strings.NewReader(`{"error":"Record not found"}`))
err = parseAPIError("bad request", &http.Response{Status: "404 Not Found", Body: r}) err = parseAPIError("bad request", &http.Response{
Status: "404 Not Found",
StatusCode: http.StatusNotFound,
Body: r,
})
want = "bad request: 404 Not Found: Record not found" want = "bad request: 404 Not Found: Record not found"
if err.Error() != want { if err.Error() != want {
t.Fatalf("want %q but %q", want, err.Error()) t.Fatalf("want %q but %q", want, err.Error())

View File

@ -40,10 +40,10 @@ type DeleteEvent struct{ ID ID }
func (e *DeleteEvent) event() {} func (e *DeleteEvent) event() {}
// ErrorEvent is a struct for passing errors to app. // ErrorEvent is a struct for passing errors to app.
type ErrorEvent struct{ err error } type ErrorEvent struct{ Err error }
func (e *ErrorEvent) event() {} func (e *ErrorEvent) event() {}
func (e *ErrorEvent) Error() string { return e.err.Error() } func (e *ErrorEvent) Error() string { return e.Err.Error() }
// Event is an interface passing events to app. // Event is an interface passing events to app.
type Event interface { type Event interface {

View File

@ -73,8 +73,8 @@ data: {"content": "foo"}
} }
case *ErrorEvent: case *ErrorEvent:
passError = true passError = true
if event.err == nil { if event.Err == nil {
t.Fatalf("should be fail: %v", event.err) t.Fatalf("should be fail: %v", event.Err)
} }
} }
} }
@ -126,8 +126,8 @@ data: {"content": "foo"}
switch event := e.(type) { switch event := e.(type) {
case *ErrorEvent: case *ErrorEvent:
passError = true passError = true
if event.err == nil { if event.Err == nil {
t.Fatalf("should be fail: %v", event.err) t.Fatalf("should be fail: %v", event.Err)
} }
case *UpdateEvent: case *UpdateEvent:
cnt++ cnt++
@ -183,8 +183,8 @@ func TestDoStreaming(t *testing.T) {
for e := range q { for e := range q {
if event, ok := e.(*ErrorEvent); ok { if event, ok := e.(*ErrorEvent); ok {
passError = true passError = true
if event.err == nil { if event.Err == nil {
t.Fatalf("should be fail: %v", event.err) t.Fatalf("should be fail: %v", event.Err)
} }
} }
} }

View File

@ -88,7 +88,7 @@ func (c *WSClient) streamingWS(ctx context.Context, stream, tag string) (chan Ev
func (c *WSClient) handleWS(ctx context.Context, rawurl string, q chan Event) error { func (c *WSClient) handleWS(ctx context.Context, rawurl string, q chan Event) error {
conn, err := c.dialRedirect(rawurl) conn, err := c.dialRedirect(rawurl)
if err != nil { if err != nil {
q <- &ErrorEvent{err: err} q <- &ErrorEvent{Err: err}
// End. // End.
return err return err
@ -103,7 +103,7 @@ func (c *WSClient) handleWS(ctx context.Context, rawurl string, q chan Event) er
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
q <- &ErrorEvent{err: ctx.Err()} q <- &ErrorEvent{Err: ctx.Err()}
// End. // End.
return ctx.Err() return ctx.Err()
@ -113,7 +113,7 @@ func (c *WSClient) handleWS(ctx context.Context, rawurl string, q chan Event) er
var s Stream var s Stream
err := conn.ReadJSON(&s) err := conn.ReadJSON(&s)
if err != nil { if err != nil {
q <- &ErrorEvent{err: err} q <- &ErrorEvent{Err: err}
// Reconnect. // Reconnect.
break break

View File

@ -135,13 +135,13 @@ func wsTest(t *testing.T, q chan Event, cancel func()) {
t.Fatalf("want %q but %q", "1234567", events[3].(*DeleteEvent).ID) t.Fatalf("want %q but %q", "1234567", events[3].(*DeleteEvent).ID)
} }
if errorEvent, ok := events[4].(*ErrorEvent); !ok { if errorEvent, ok := events[4].(*ErrorEvent); !ok {
t.Fatalf("should be fail: %v", errorEvent.err) t.Fatalf("should be fail: %v", errorEvent.Err)
} }
if errorEvent, ok := events[5].(*ErrorEvent); !ok { if errorEvent, ok := events[5].(*ErrorEvent); !ok {
t.Fatalf("should be fail: %v", errorEvent.err) t.Fatalf("should be fail: %v", errorEvent.Err)
} }
if errorEvent, ok := events[6].(*ErrorEvent); !ok { if errorEvent, ok := events[6].(*ErrorEvent); !ok {
t.Fatalf("should be fail: %v", errorEvent.err) t.Fatalf("should be fail: %v", errorEvent.Err)
} }
} }
@ -168,7 +168,7 @@ func TestStreamingWS(t *testing.T) {
defer wg.Done() defer wg.Done()
e := <-q e := <-q
if errorEvent, ok := e.(*ErrorEvent); !ok { if errorEvent, ok := e.(*ErrorEvent); !ok {
t.Errorf("should be fail: %v", errorEvent.err) t.Errorf("should be fail: %v", errorEvent.Err)
} }
}() }()
wg.Wait() wg.Wait()
@ -204,7 +204,7 @@ func TestHandleWS(t *testing.T) {
defer wg.Done() defer wg.Done()
e := <-q e := <-q
if errorEvent, ok := e.(*ErrorEvent); !ok { if errorEvent, ok := e.(*ErrorEvent); !ok {
t.Errorf("should be fail: %v", errorEvent.err) t.Errorf("should be fail: %v", errorEvent.Err)
} }
}() }()
err := client.handleWS(context.Background(), ":", q) err := client.handleWS(context.Background(), ":", q)
@ -219,7 +219,7 @@ func TestHandleWS(t *testing.T) {
defer wg.Done() defer wg.Done()
e := <-q e := <-q
if errorEvent, ok := e.(*ErrorEvent); !ok { if errorEvent, ok := e.(*ErrorEvent); !ok {
t.Errorf("should be fail: %v", errorEvent.err) t.Errorf("should be fail: %v", errorEvent.Err)
} }
}() }()
err = client.handleWS(ctx, "ws://"+ts.Listener.Addr().String(), q) err = client.handleWS(ctx, "ws://"+ts.Listener.Addr().String(), q)
@ -232,7 +232,7 @@ func TestHandleWS(t *testing.T) {
defer wg.Done() defer wg.Done()
e := <-q e := <-q
if errorEvent, ok := e.(*ErrorEvent); !ok { if errorEvent, ok := e.(*ErrorEvent); !ok {
t.Errorf("should be fail: %v", errorEvent.err) t.Errorf("should be fail: %v", errorEvent.Err)
} }
}() }()
client.handleWS(context.Background(), "ws://"+ts.Listener.Addr().String(), q) client.handleWS(context.Background(), "ws://"+ts.Listener.Addr().String(), q)