From 73fbf8dfafe4a4c1454bcfb7343837efd9d2240a Mon Sep 17 00:00:00 2001 From: Russell Troxel Date: Tue, 28 Mar 2023 11:33:13 -0700 Subject: [PATCH] Add Panic Recovery & Logging to Client JSON Unmarshalling (#139) --- internal/client/client.go | 28 +++++++++++++++++++++++++++- internal/client/client_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/internal/client/client.go b/internal/client/client.go index b57f89a..09abe3b 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -4,8 +4,10 @@ import ( "crypto/tls" "encoding/json" "fmt" + "io" "net/http" "net/url" + "strings" "github.com/onedr0p/exportarr/internal/config" "go.uber.org/zap" @@ -62,6 +64,30 @@ func NewClient(config *config.Config) (*Client, error) { }, nil } +func (c *Client) unmarshalBody(b io.Reader, target interface{}) (err error) { + defer func() { + if r := recover(); r != nil { + // return recovered panic as error + err = fmt.Errorf("Recovered from panic: %s", r) + + log := zap.S() + if zap.S().Level() == zap.DebugLevel { + s := new(strings.Builder) + _, copyErr := io.Copy(s, b) + if copyErr != nil { + zap.S().Errorw("Failed to copy body to string in recover", + "error", err, "recover", r) + } + log = log.With("body", s.String()) + } + log.Errorw("Recovered while unmarshalling response", "error", r) + + } + }() + err = json.NewDecoder(b).Decode(target) + return +} + // DoRequest - Take a HTTP Request and return Unmarshaled data func (c *Client) DoRequest(endpoint string, target interface{}, queryParams ...map[string]string) error { values := c.URL.Query() @@ -84,5 +110,5 @@ func (c *Client) DoRequest(endpoint string, target interface{}, queryParams ...m return fmt.Errorf("Failed to execute HTTP Request(%s): %w", url, err) } defer resp.Body.Close() - return json.NewDecoder(resp.Body).Decode(target) + return c.unmarshalBody(resp.Body, target) } diff --git a/internal/client/client_test.go b/internal/client/client_test.go index df6bdbf..5c48724 100644 --- a/internal/client/client_test.go +++ b/internal/client/client_test.go @@ -1,6 +1,7 @@ package client import ( + "encoding/json" "fmt" "net/http" "net/http/httptest" @@ -79,3 +80,36 @@ func TestDoRequest(t *testing.T) { }) } } + +func TestDoRequest_PanicRecovery(t *testing.T) { + require := require.New(t) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ret := struct { + TestField string + TestField2 string + }{ + TestField: "asdf", + TestField2: "asdf2", + } + s, err := json.Marshal(ret) + require.NoError(err) + w.Write(s) + w.WriteHeader(http.StatusOK) + return + })) + defer ts.Close() + + c := &config.Config{ + URL: ts.URL, + ApiVersion: "v3", + } + + client, err := NewClient(c) + require.Nil(err, "NewClient should not return an error") + require.NotNil(client, "NewClient should return a client") + + err = client.DoRequest("test", nil) + require.NotPanics(func() { + require.Error(err, "DoRequest should return an error: %s", err) + }, "DoRequest should recover from a panic") +}