From 1af30c50ca0ad81e2839ff4f2c5e70413f021d52 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 31 Dec 2018 13:59:59 +0100 Subject: [PATCH] Add httputils.FromStatusCode() This utility allows a client to convert an API response back to a typed error; allowing the client to perform different actions based on the type of error, without having to resort to string-matching the error. Signed-off-by: Sebastiaan van Stijn --- api/server/httputils/errors.go | 46 ++++++++++++++ api/server/httputils/errors_test.go | 93 +++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 api/server/httputils/errors_test.go diff --git a/api/server/httputils/errors.go b/api/server/httputils/errors.go index 2abdce2bcf..241cf097b3 100644 --- a/api/server/httputils/errors.go +++ b/api/server/httputils/errors.go @@ -76,6 +76,52 @@ func GetHTTPErrorStatusCode(err error) int { return statusCode } +// FromStatusCode creates an errdef error, based on the provided status-code +func FromStatusCode(err error, statusCode int) error { + if err == nil { + return err + } + switch statusCode { + case http.StatusNotFound: + err = errdefs.NotFound(err) + case http.StatusBadRequest: + err = errdefs.InvalidParameter(err) + case http.StatusConflict: + err = errdefs.Conflict(err) + case http.StatusUnauthorized: + err = errdefs.Unauthorized(err) + case http.StatusServiceUnavailable: + err = errdefs.Unavailable(err) + case http.StatusForbidden: + err = errdefs.Forbidden(err) + case http.StatusNotModified: + err = errdefs.NotModified(err) + case http.StatusNotImplemented: + err = errdefs.NotImplemented(err) + case http.StatusInternalServerError: + if !errdefs.IsSystem(err) && !errdefs.IsUnknown(err) && !errdefs.IsDataLoss(err) && !errdefs.IsDeadline(err) && !errdefs.IsCancelled(err) { + err = errdefs.System(err) + } + default: + logrus.WithFields(logrus.Fields{ + "module": "api", + "status_code": fmt.Sprintf("%d", statusCode), + }).Debugf("FIXME: Got an status-code for which error does not match any expected type!!!: %d", statusCode) + + switch { + case statusCode >= 200 && statusCode < 400: + // it's a client error + case statusCode >= 400 && statusCode < 500: + err = errdefs.InvalidParameter(err) + case statusCode >= 500 && statusCode < 600: + err = errdefs.System(err) + default: + err = errdefs.Unknown(err) + } + } + return err +} + func apiVersionSupportsJSONErrors(version string) bool { const firstAPIVersionWithJSONErrors = "1.23" return version == "" || versions.GreaterThan(version, firstAPIVersionWithJSONErrors) diff --git a/api/server/httputils/errors_test.go b/api/server/httputils/errors_test.go new file mode 100644 index 0000000000..f67a39213a --- /dev/null +++ b/api/server/httputils/errors_test.go @@ -0,0 +1,93 @@ +package httputils + +import ( + "fmt" + "net/http" + "testing" + + "github.com/docker/docker/errdefs" + "gotest.tools/assert" +) + +func TestFromStatusCode(t *testing.T) { + testErr := fmt.Errorf("some error occurred") + + testCases := []struct { + err error + status int + check func(error) bool + }{ + { + err: testErr, + status: http.StatusNotFound, + check: errdefs.IsNotFound, + }, + { + err: testErr, + status: http.StatusBadRequest, + check: errdefs.IsInvalidParameter, + }, + { + err: testErr, + status: http.StatusConflict, + check: errdefs.IsConflict, + }, + { + err: testErr, + status: http.StatusUnauthorized, + check: errdefs.IsUnauthorized, + }, + { + err: testErr, + status: http.StatusServiceUnavailable, + check: errdefs.IsUnavailable, + }, + { + err: testErr, + status: http.StatusForbidden, + check: errdefs.IsForbidden, + }, + { + err: testErr, + status: http.StatusNotModified, + check: errdefs.IsNotModified, + }, + { + err: testErr, + status: http.StatusNotImplemented, + check: errdefs.IsNotImplemented, + }, + { + err: testErr, + status: http.StatusInternalServerError, + check: errdefs.IsSystem, + }, + { + err: errdefs.Unknown(testErr), + status: http.StatusInternalServerError, + check: errdefs.IsUnknown, + }, + { + err: errdefs.DataLoss(testErr), + status: http.StatusInternalServerError, + check: errdefs.IsDataLoss, + }, + { + err: errdefs.Deadline(testErr), + status: http.StatusInternalServerError, + check: errdefs.IsDeadline, + }, + { + err: errdefs.Cancelled(testErr), + status: http.StatusInternalServerError, + check: errdefs.IsCancelled, + }, + } + + for _, tc := range testCases { + t.Run(http.StatusText(tc.status), func(t *testing.T) { + err := FromStatusCode(tc.err, tc.status) + assert.Check(t, tc.check(err), "unexpected error-type %T", err) + }) + } +}