Merge pull request #54125 from thaJeztah/fix_httpstatus

httpstatus: handle missing errdefs HTTP status codes
This commit is contained in:
Sebastiaan van Stijn authored and GitHub committed 2026-11-04 22:54:49 +01:00
commit 54d812f66e
2 files changed
+183 -5

No files matched your search

+10 -5
View File
@@ -24,6 +24,8 @@ func FromError(err error) int {
// Note that the below functions are already checking the error causal chain for matches.
// Only check errors from the errdefs package, no new error type checking may be added
//
// TODO(thaJeztah): rewrite using github.com/containerd/errdefs/pkg/errhttp.ToHTTP after providing a utility to detect if a match was found.
switch {
case cerrdefs.IsNotFound(rerr):
return http.StatusNotFound
@@ -33,6 +35,10 @@ func FromError(err error) int {
return http.StatusConflict
case cerrdefs.IsUnauthorized(rerr):
return http.StatusUnauthorized
case cerrdefs.IsFailedPrecondition(rerr):
return http.StatusPreconditionFailed
case cerrdefs.IsResourceExhausted(rerr):
return http.StatusTooManyRequests
case cerrdefs.IsUnavailable(rerr):
return http.StatusServiceUnavailable
case cerrdefs.IsPermissionDenied(rerr):
@@ -111,14 +117,13 @@ func statusCodeFromGRPCError(err error) int {
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 {
if len(errs) > 0 {
return statusCodeFromDistributionError(errs[0])
}
return http.StatusInternalServerError
case errcode.ErrorCoder:
return errs.ErrorCode().Descriptor().HTTPStatusCode
default:
return http.StatusInternalServerError
}
return http.StatusInternalServerError
}
+173
View File
@@ -0,0 +1,173 @@
package httpstatus_test
import (
"context"
"errors"
"fmt"
"net/http"
"testing"
cerrdefs "github.com/containerd/errdefs"
"github.com/docker/distribution/registry/api/errcode"
"github.com/moby/moby/v2/daemon/server/httpstatus"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestFromError(t *testing.T) {
tests := []struct {
name string
err error
want int
}{
{
name: "nil",
want: http.StatusInternalServerError,
},
{
name: "not found",
err: cerrdefs.ErrNotFound,
want: http.StatusNotFound,
},
{
name: "invalid argument",
err: cerrdefs.ErrInvalidArgument,
want: http.StatusBadRequest,
},
{
name: "conflict",
err: cerrdefs.ErrConflict,
want: http.StatusConflict,
},
{
name: "unauthorized",
err: cerrdefs.ErrUnauthenticated,
want: http.StatusUnauthorized,
},
{
name: "unavailable",
err: cerrdefs.ErrUnavailable,
want: http.StatusServiceUnavailable,
},
{
name: "permission denied",
err: cerrdefs.ErrPermissionDenied,
want: http.StatusForbidden,
},
{
name: "not modified",
err: cerrdefs.ErrNotModified,
want: http.StatusNotModified,
},
{
name: "not implemented",
err: cerrdefs.ErrNotImplemented,
want: http.StatusNotImplemented,
},
{
name: "failed precondition",
err: cerrdefs.ErrFailedPrecondition,
want: http.StatusPreconditionFailed,
},
{
name: "resource exhausted",
err: cerrdefs.ErrResourceExhausted,
want: http.StatusTooManyRequests,
},
{
name: "internal",
err: cerrdefs.ErrInternal,
want: http.StatusInternalServerError,
},
{
name: "data loss",
err: cerrdefs.ErrDataLoss,
want: http.StatusInternalServerError,
},
{
name: "unknown",
err: cerrdefs.ErrUnknown,
want: http.StatusInternalServerError,
},
{
name: "unclassified",
err: errors.New("unclassified"),
want: http.StatusInternalServerError,
},
{
name: "canceled",
err: context.Canceled,
want: http.StatusInternalServerError,
},
{
name: "deadline exceeded",
err: context.DeadlineExceeded,
want: http.StatusInternalServerError,
},
{
name: "wrapped",
err: fmt.Errorf("wrapped: %w", cerrdefs.ErrNotFound),
want: http.StatusNotFound,
},
{
name: "outermost classification",
err: fmt.Errorf("wrapped: %w: %w", cerrdefs.ErrInvalidArgument, fmt.Errorf("wrapped: %w", cerrdefs.ErrNotFound)),
want: http.StatusBadRequest,
},
{
name: "wrapped unclassified error",
err: fmt.Errorf("wrapped: %w", errors.New("unclassified")),
want: http.StatusInternalServerError,
},
{
name: "joined errors",
err: errors.Join(errors.New("unclassified"), cerrdefs.ErrNotFound),
want: http.StatusNotFound,
},
{
name: "joined unclassified errors",
err: errors.Join(errors.New("first"), errors.New("second")),
want: http.StatusInternalServerError,
},
// gRPC errors
{
name: "gRPC not found",
err: status.Error(codes.NotFound, "not found"),
want: http.StatusNotFound,
},
{
name: "gRPC failed precondition",
err: status.Error(codes.FailedPrecondition, "failed"),
want: http.StatusBadRequest,
},
// distribution errors
{
name: "distribution unauthorized",
err: errcode.ErrorCodeUnauthorized,
want: http.StatusUnauthorized,
},
{
name: "distribution errors",
err: errcode.Errors{errcode.ErrorCodeDenied},
want: http.StatusForbidden,
},
{
name: "empty distribution errors",
err: errcode.Errors{},
want: http.StatusInternalServerError,
},
{
name: "wrapped distribution error",
err: fmt.Errorf("wrapped: %w", errcode.ErrorCodeDenied),
want: http.StatusForbidden,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert.Check(t, is.Equal(tc.want, httpstatus.FromError(tc.err)))
})
}
}