diff --git a/lfsapi/errors.go b/lfsapi/errors.go new file mode 100644 index 00000000..58b13eeb --- /dev/null +++ b/lfsapi/errors.go @@ -0,0 +1,79 @@ +package lfsapi + +import ( + "fmt" + "net/http" + + "github.com/git-lfs/git-lfs/errors" +) + +type ClientError struct { + Message string `json:"message"` + DocumentationUrl string `json:"documentation_url,omitempty"` + RequestId string `json:"request_id,omitempty"` +} + +func (e *ClientError) Error() string { + msg := e.Message + if len(e.DocumentationUrl) > 0 { + msg += "\nDocs: " + e.DocumentationUrl + } + if len(e.RequestId) > 0 { + msg += "\nRequest ID: " + e.RequestId + } + return msg +} + +func (c *Client) handleResponse(res *http.Response) error { + if res.StatusCode < 400 { + return nil + } + + cliErr := &ClientError{} + err := decodeResponse(res, cliErr) + if err == nil { + if len(cliErr.Message) == 0 { + err = defaultError(res) + } else { + err = errors.Wrap(cliErr, "http") + } + } + + if res.StatusCode == 401 { + return errors.NewAuthError(err) + } + + if res.StatusCode > 499 && res.StatusCode != 501 && res.StatusCode != 507 && res.StatusCode != 509 { + return errors.NewFatalError(err) + } + + return err +} + +var ( + defaultErrors = map[int]string{ + 400: "Client error: %s", + 401: "Authorization error: %s\nCheck that you have proper access to the repository", + 403: "Authorization error: %s\nCheck that you have proper access to the repository", + 404: "Repository or object not found: %s\nCheck that it exists and that you have proper access to it", + 429: "Rate limit exceeded: %s", + 500: "Server error: %s", + 501: "Not Implemented: %s", + 507: "Insufficient server storage: %s", + 509: "Bandwidth limit exceeded: %s", + } +) + +func defaultError(res *http.Response) error { + var msgFmt string + + if f, ok := defaultErrors[res.StatusCode]; ok { + msgFmt = f + } else if res.StatusCode < 500 { + msgFmt = defaultErrors[400] + fmt.Sprintf(" from HTTP %d", res.StatusCode) + } else { + msgFmt = defaultErrors[500] + fmt.Sprintf(" from HTTP %d", res.StatusCode) + } + + return errors.Errorf(msgFmt, res.Request.URL) +} diff --git a/lfsapi/lfsapi.go b/lfsapi/lfsapi.go index ef334947..9feea05c 100644 --- a/lfsapi/lfsapi.go +++ b/lfsapi/lfsapi.go @@ -1,10 +1,42 @@ package lfsapi -import "net/http" +import ( + "encoding/json" + "net/http" + "regexp" + + "github.com/pkg/errors" +) + +var ( + lfsMediaTypeRE = regexp.MustCompile(`\Aapplication/vnd\.git\-lfs\+json(;|\z)`) + jsonMediaTypeRE = regexp.MustCompile(`\Aapplication/json(;|\z)`) +) type Client struct { } func (c *Client) Do(req *http.Request) (*http.Response, error) { - return http.DefaultClient.Do(req) + res, err := http.DefaultClient.Do(req) + if err != nil { + return res, err + } + + return res, c.handleResponse(res) +} + +func decodeResponse(res *http.Response, obj interface{}) error { + ctype := res.Header.Get("Content-Type") + if !(lfsMediaTypeRE.MatchString(ctype) || jsonMediaTypeRE.MatchString(ctype)) { + return nil + } + + err := json.NewDecoder(res.Body).Decode(obj) + res.Body.Close() + + if err != nil { + return errors.Wrapf(err, "Unable to parse HTTP response for %s %s", res.Request.Method, res.Request.URL) + } + + return nil } diff --git a/tq/verify_test.go b/tq/verify_test.go index 83a32447..205781e0 100644 --- a/tq/verify_test.go +++ b/tq/verify_test.go @@ -4,9 +4,11 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "strings" "sync/atomic" "testing" + "github.com/git-lfs/git-lfs/errors" "github.com/git-lfs/git-lfs/lfsapi" "github.com/stretchr/testify/assert" ) @@ -60,3 +62,213 @@ func TestVerifySuccess(t *testing.T) { assert.Nil(t, verifyUpload(c, tr)) assert.EqualValues(t, 1, called) } + +func TestVerifyAuthErrWithBody(t *testing.T) { + var called uint32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.String() != "/verify" { + w.WriteHeader(http.StatusNotFound) + return + } + + atomic.AddUint32(&called, 1) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + w.Write([]byte(`{"message":"custom auth error"}`)) + })) + defer srv.Close() + + c := &lfsapi.Client{} + tr := &Transfer{ + Oid: "abc", + Size: 123, + Actions: map[string]*Action{ + "verify": &Action{ + Href: srv.URL + "/verify", + }, + }, + } + + err := verifyUpload(c, tr) + assert.NotNil(t, err) + assert.True(t, errors.IsAuthError(err)) + assert.Equal(t, "Authentication required: http: custom auth error", err.Error()) + assert.EqualValues(t, 1, called) +} + +func TestVerifyFatalWithBody(t *testing.T) { + var called uint32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.String() != "/verify" { + w.WriteHeader(http.StatusNotFound) + return + } + + atomic.AddUint32(&called, 1) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + w.Write([]byte(`{"message":"custom fatal error"}`)) + })) + defer srv.Close() + + c := &lfsapi.Client{} + tr := &Transfer{ + Oid: "abc", + Size: 123, + Actions: map[string]*Action{ + "verify": &Action{ + Href: srv.URL + "/verify", + }, + }, + } + + err := verifyUpload(c, tr) + assert.NotNil(t, err) + assert.True(t, errors.IsFatalError(err)) + assert.Equal(t, "Fatal error: http: custom fatal error", err.Error()) + assert.EqualValues(t, 1, called) +} + +func TestVerifyWithNonFatal500WithBody(t *testing.T) { + c := &lfsapi.Client{} + tr := &Transfer{ + Oid: "abc", + Size: 123, + Actions: map[string]*Action{ + "verify": &Action{}, + }, + } + + var called uint32 + + nonFatalCodes := map[int]string{ + 501: "custom 501 error", + 507: "custom 507 error", + 509: "custom 509 error", + } + + for nonFatalCode, expectedErr := range nonFatalCodes { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.String() != "/verify" { + w.WriteHeader(http.StatusNotFound) + return + } + + atomic.AddUint32(&called, 1) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(nonFatalCode) + w.Write([]byte(`{"message":"` + expectedErr + `"}`)) + })) + + tr.Actions["verify"].Href = srv.URL + "/verify" + err := verifyUpload(c, tr) + t.Logf("non fatal code %d", nonFatalCode) + assert.NotNil(t, err) + assert.Equal(t, "http: "+expectedErr, err.Error()) + srv.Close() + } + + assert.EqualValues(t, 3, called) +} + +func TestVerifyAuthErrWithoutBody(t *testing.T) { + var called uint32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.String() != "/verify" { + w.WriteHeader(http.StatusNotFound) + return + } + + atomic.AddUint32(&called, 1) + w.WriteHeader(401) + })) + defer srv.Close() + + c := &lfsapi.Client{} + tr := &Transfer{ + Oid: "abc", + Size: 123, + Actions: map[string]*Action{ + "verify": &Action{ + Href: srv.URL + "/verify", + }, + }, + } + + err := verifyUpload(c, tr) + assert.NotNil(t, err) + assert.True(t, errors.IsAuthError(err)) + assert.True(t, strings.HasPrefix(err.Error(), "Authentication required: Authorization error:"), err.Error()) + assert.EqualValues(t, 1, called) +} + +func TestVerifyFatalWithoutBody(t *testing.T) { + var called uint32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.String() != "/verify" { + w.WriteHeader(http.StatusNotFound) + return + } + + atomic.AddUint32(&called, 1) + w.WriteHeader(500) + })) + defer srv.Close() + + c := &lfsapi.Client{} + tr := &Transfer{ + Oid: "abc", + Size: 123, + Actions: map[string]*Action{ + "verify": &Action{ + Href: srv.URL + "/verify", + }, + }, + } + + err := verifyUpload(c, tr) + assert.NotNil(t, err) + assert.True(t, errors.IsFatalError(err)) + assert.True(t, strings.HasPrefix(err.Error(), "Fatal error: Server error:"), err.Error()) + assert.EqualValues(t, 1, called) +} + +func TestVerifyWithNonFatal500WithoutBody(t *testing.T) { + c := &lfsapi.Client{} + tr := &Transfer{ + Oid: "abc", + Size: 123, + Actions: map[string]*Action{ + "verify": &Action{}, + }, + } + + var called uint32 + + nonFatalCodes := map[int]string{ + 501: "Not Implemented:", + 507: "Insufficient server storage:", + 509: "Bandwidth limit exceeded:", + } + + for nonFatalCode, errPrefix := range nonFatalCodes { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.String() != "/verify" { + w.WriteHeader(http.StatusNotFound) + return + } + + atomic.AddUint32(&called, 1) + w.WriteHeader(nonFatalCode) + })) + + tr.Actions["verify"].Href = srv.URL + "/verify" + err := verifyUpload(c, tr) + t.Logf("non fatal code %d", nonFatalCode) + assert.NotNil(t, err) + assert.True(t, strings.HasPrefix(err.Error(), errPrefix)) + srv.Close() + } + + assert.EqualValues(t, 3, called) +}