8ab5e2a004
Before we handled containerd errors, using an invalid platform produced a 500 status:
```bash
curl -v \
-X POST \
--unix-socket /var/run/docker.sock \
"http://localhost:2375/v1.40/images/create?fromImage=hello-world&platform=foobar&tag=latest" \
-H "Content-Type: application/json"
```
```
* Connected to localhost (docker.sock) port 80 (#0)
> POST /v1.40/images/create?fromImage=hello-world&platform=foobar&tag=latest HTTP/1.1
> Host: localhost:2375
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Type: application/json
>
< HTTP/1.1 500 Internal Server Error
< Api-Version: 1.40
< Content-Length: 85
< Content-Type: application/json
< Date: Mon, 15 Jul 2019 15:25:44 GMT
< Docker-Experimental: true
< Ostype: linux
< Server: Docker/19.03.0-rc2 (linux)
<
{"message":"\"foobar\": unknown operating system or architecture: invalid argument"}
```
That problem is now fixed, and the API correctly returns a 4xx status:
```bash
curl -v \
-X POST \
--unix-socket /var/run/docker.sock \
"http://localhost:2375/v1.40/images/create?fromImage=hello-world&platform=foobar&tag=latest" \
-H "Content-Type: application/json"
```
```
* Connected to localhost (/var/run/docker.sock) port 80 (#0)
> POST /v1.40/images/create?fromImage=hello-world&platform=foobar&tag=latest HTTP/1.1
> Host: localhost:2375
> User-Agent: curl/7.52.1
> Accept: */*
> Content-Type: application/json
>
< HTTP/1.1 400 Bad Request
< Api-Version: 1.41
< Content-Type: application/json
< Docker-Experimental: true
< Ostype: linux
< Server: Docker/dev (linux)
< Date: Mon, 15 Jul 2019 15:13:42 GMT
< Content-Length: 85
<
{"message":"\"foobar\": unknown operating system or architecture: invalid argument"}
* Curl_http_done: called premature == 0
```
This patch adds tests to validate the behaviour
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 9d1b4f5fc3
)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
198 lines
5.9 KiB
Go
198 lines
5.9 KiB
Go
package errdefs // import "github.com/docker/docker/errdefs"
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
|
|
containerderrors "github.com/containerd/containerd/errdefs"
|
|
"github.com/docker/distribution/registry/api/errcode"
|
|
"github.com/sirupsen/logrus"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
// GetHTTPErrorStatusCode retrieves status code from error message.
|
|
func GetHTTPErrorStatusCode(err error) int {
|
|
if err == nil {
|
|
logrus.WithFields(logrus.Fields{"error": err}).Error("unexpected HTTP error handling")
|
|
return http.StatusInternalServerError
|
|
}
|
|
|
|
var statusCode int
|
|
|
|
// Stop right there
|
|
// Are you sure you should be adding a new error class here? Do one of the existing ones work?
|
|
|
|
// Note that the below functions are already checking the error causal chain for matches.
|
|
switch {
|
|
case IsNotFound(err):
|
|
statusCode = http.StatusNotFound
|
|
case IsInvalidParameter(err):
|
|
statusCode = http.StatusBadRequest
|
|
case IsConflict(err):
|
|
statusCode = http.StatusConflict
|
|
case IsUnauthorized(err):
|
|
statusCode = http.StatusUnauthorized
|
|
case IsUnavailable(err):
|
|
statusCode = http.StatusServiceUnavailable
|
|
case IsForbidden(err):
|
|
statusCode = http.StatusForbidden
|
|
case IsNotModified(err):
|
|
statusCode = http.StatusNotModified
|
|
case IsNotImplemented(err):
|
|
statusCode = http.StatusNotImplemented
|
|
case IsSystem(err) || IsUnknown(err) || IsDataLoss(err) || IsDeadline(err) || IsCancelled(err):
|
|
statusCode = http.StatusInternalServerError
|
|
default:
|
|
statusCode = statusCodeFromGRPCError(err)
|
|
if statusCode != http.StatusInternalServerError {
|
|
return statusCode
|
|
}
|
|
statusCode = statusCodeFromContainerdError(err)
|
|
if statusCode != http.StatusInternalServerError {
|
|
return statusCode
|
|
}
|
|
statusCode = statusCodeFromDistributionError(err)
|
|
if statusCode != http.StatusInternalServerError {
|
|
return statusCode
|
|
}
|
|
if e, ok := err.(causer); ok {
|
|
return GetHTTPErrorStatusCode(e.Cause())
|
|
}
|
|
|
|
logrus.WithFields(logrus.Fields{
|
|
"module": "api",
|
|
"error_type": fmt.Sprintf("%T", err),
|
|
}).Debugf("FIXME: Got an API for which error does not match any expected type!!!: %+v", err)
|
|
}
|
|
|
|
if statusCode == 0 {
|
|
statusCode = http.StatusInternalServerError
|
|
}
|
|
|
|
return statusCode
|
|
}
|
|
|
|
// FromStatusCode creates an errdef error, based on the provided HTTP status-code
|
|
func FromStatusCode(err error, statusCode int) error {
|
|
if err == nil {
|
|
return err
|
|
}
|
|
switch statusCode {
|
|
case http.StatusNotFound:
|
|
err = NotFound(err)
|
|
case http.StatusBadRequest:
|
|
err = InvalidParameter(err)
|
|
case http.StatusConflict:
|
|
err = Conflict(err)
|
|
case http.StatusUnauthorized:
|
|
err = Unauthorized(err)
|
|
case http.StatusServiceUnavailable:
|
|
err = Unavailable(err)
|
|
case http.StatusForbidden:
|
|
err = Forbidden(err)
|
|
case http.StatusNotModified:
|
|
err = NotModified(err)
|
|
case http.StatusNotImplemented:
|
|
err = NotImplemented(err)
|
|
case http.StatusInternalServerError:
|
|
if !IsSystem(err) && !IsUnknown(err) && !IsDataLoss(err) && !IsDeadline(err) && !IsCancelled(err) {
|
|
err = 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 = InvalidParameter(err)
|
|
case statusCode >= 500 && statusCode < 600:
|
|
err = System(err)
|
|
default:
|
|
err = Unknown(err)
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// statusCodeFromGRPCError returns status code according to gRPC error
|
|
func statusCodeFromGRPCError(err error) int {
|
|
switch status.Code(err) {
|
|
case codes.InvalidArgument: // code 3
|
|
return http.StatusBadRequest
|
|
case codes.NotFound: // code 5
|
|
return http.StatusNotFound
|
|
case codes.AlreadyExists: // code 6
|
|
return http.StatusConflict
|
|
case codes.PermissionDenied: // code 7
|
|
return http.StatusForbidden
|
|
case codes.FailedPrecondition: // code 9
|
|
return http.StatusBadRequest
|
|
case codes.Unauthenticated: // code 16
|
|
return http.StatusUnauthorized
|
|
case codes.OutOfRange: // code 11
|
|
return http.StatusBadRequest
|
|
case codes.Unimplemented: // code 12
|
|
return http.StatusNotImplemented
|
|
case codes.Unavailable: // code 14
|
|
return http.StatusServiceUnavailable
|
|
default:
|
|
if e, ok := err.(causer); ok {
|
|
return statusCodeFromGRPCError(e.Cause())
|
|
}
|
|
// codes.Canceled(1)
|
|
// codes.Unknown(2)
|
|
// codes.DeadlineExceeded(4)
|
|
// codes.ResourceExhausted(8)
|
|
// codes.Aborted(10)
|
|
// codes.Internal(13)
|
|
// codes.DataLoss(15)
|
|
return http.StatusInternalServerError
|
|
}
|
|
}
|
|
|
|
// statusCodeFromDistributionError returns status code according to registry errcode
|
|
// code is loosely based on errcode.ServeJSON() in docker/distribution
|
|
func statusCodeFromDistributionError(err error) int {
|
|
switch errs := err.(type) {
|
|
case errcode.Errors:
|
|
if len(errs) < 1 {
|
|
return http.StatusInternalServerError
|
|
}
|
|
if _, ok := errs[0].(errcode.ErrorCoder); ok {
|
|
return statusCodeFromDistributionError(errs[0])
|
|
}
|
|
case errcode.ErrorCoder:
|
|
return errs.ErrorCode().Descriptor().HTTPStatusCode
|
|
default:
|
|
if e, ok := err.(causer); ok {
|
|
return statusCodeFromDistributionError(e.Cause())
|
|
}
|
|
}
|
|
return http.StatusInternalServerError
|
|
}
|
|
|
|
// statusCodeFromContainerdError returns status code for containerd errors when
|
|
// consumed directly (not through gRPC)
|
|
func statusCodeFromContainerdError(err error) int {
|
|
switch {
|
|
case containerderrors.IsInvalidArgument(err):
|
|
return http.StatusBadRequest
|
|
case containerderrors.IsNotFound(err):
|
|
return http.StatusNotFound
|
|
case containerderrors.IsAlreadyExists(err):
|
|
return http.StatusConflict
|
|
case containerderrors.IsFailedPrecondition(err):
|
|
return http.StatusPreconditionFailed
|
|
case containerderrors.IsUnavailable(err):
|
|
return http.StatusServiceUnavailable
|
|
case containerderrors.IsNotImplemented(err):
|
|
return http.StatusNotImplemented
|
|
default:
|
|
return http.StatusInternalServerError
|
|
}
|
|
}
|