From ee4ab5bdb7fc8aef8f8b8397934f6b3c15c86b47 Mon Sep 17 00:00:00 2001 From: Roger Peppe Date: Wed, 3 Apr 2024 09:37:12 +0100 Subject: [PATCH] ociregistry: implement MarshalError and use it in ociserver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This provides a standard way for code to convert from a regular Go error to the form appropriate for OCI error response bodies. It seems to make sense to put this inside the top level ociregistry package because the JSON-oriented `WireError` types are already implemented there, and the logic for stripping error code prefixes is directly related to the logic in the same package that adds them. Also in passing fix an unintended use of the deprecated `io/ioutil` package to use `io` instead. Also add a test for error stuttering to `ociclient` and fix the code to avoid stuttering the HTTP status code as exposed by that test. Fixes #31. Signed-off-by: Roger Peppe Change-Id: I22408dd3320b73bc52f37181987f79003dbaa115 Reviewed-on: https://review.gerrithub.io/c/cue-labs/oci/+/1191146 TryBot-Result: CUE porcuepine Reviewed-by: Daniel Martí --- ociregistry/error.go | 154 ++++++++++++++++++++----- ociregistry/error_test.go | 94 +++++++++++++++ ociregistry/ociclient/error.go | 5 +- ociregistry/ociclient/error_test.go | 29 +++++ ociregistry/ociserver/error.go | 63 +--------- ociregistry/ociserver/error_test.go | 8 +- ociregistry/ociserver/registry_test.go | 36 +++--- 7 files changed, 278 insertions(+), 111 deletions(-) create mode 100644 ociregistry/error_test.go diff --git a/ociregistry/error.go b/ociregistry/error.go index 6dc5b0a..f41c90d 100644 --- a/ociregistry/error.go +++ b/ociregistry/error.go @@ -15,15 +15,33 @@ package ociregistry import ( - "bytes" "encoding/json" "errors" + "fmt" "net/http" "strconv" "strings" "unicode" ) +var errorStatuses = map[string]int{ + ErrBlobUnknown.Code(): http.StatusNotFound, + ErrBlobUploadInvalid.Code(): http.StatusRequestedRangeNotSatisfiable, + ErrBlobUploadUnknown.Code(): http.StatusNotFound, + ErrDigestInvalid.Code(): http.StatusBadRequest, + ErrManifestBlobUnknown.Code(): http.StatusNotFound, + ErrManifestInvalid.Code(): http.StatusBadRequest, + ErrManifestUnknown.Code(): http.StatusNotFound, + ErrNameInvalid.Code(): http.StatusBadRequest, + ErrNameUnknown.Code(): http.StatusNotFound, + ErrSizeInvalid.Code(): http.StatusBadRequest, + ErrUnauthorized.Code(): http.StatusUnauthorized, + ErrDenied.Code(): http.StatusForbidden, + ErrUnsupported.Code(): http.StatusBadRequest, + ErrTooManyRequests.Code(): http.StatusTooManyRequests, + ErrRangeInvalid.Code(): http.StatusRequestedRangeNotSatisfiable, +} + // WireErrors is the JSON format used for error responses in // the OCI HTTP API. It should always contain at least one // error. @@ -70,26 +88,18 @@ func (e *WireError) Is(err error) bool { // Error implements the [error] interface. func (e *WireError) Error() string { - var buf strings.Builder - for _, r := range e.Code_ { - if r == '_' { - buf.WriteByte(' ') - } else { - buf.WriteRune(unicode.ToLower(r)) - } - } - if buf.Len() == 0 { - buf.WriteString("(no code)") - } + buf := make([]byte, 0, 128) + buf = appendErrorCodePrefix(buf, e.Code_) + if e.Message != "" { - buf.WriteString(": ") - buf.WriteString(e.Message) + buf = append(buf, ": "...) + buf = append(buf, e.Message...) } - if len(e.Detail_) != 0 && !bytes.Equal(e.Detail_, []byte("null")) { - buf.WriteString("; detail: ") - buf.Write(e.Detail_) - } - return buf.String() + // TODO: it would be nice to have some way to surface the detail + // in a message, but it's awkward to do so here because we don't + // really want the detail to be duplicated in the "message" + // and "detail" fields. + return string(buf) } // Code implements [Error.Code]. @@ -198,16 +208,14 @@ func (e *httpError) Is(err error) bool { // Error implements [error.Error]. func (e *httpError) Error() string { - var buf strings.Builder - buf.WriteString(strconv.Itoa(e.statusCode)) - buf.WriteString(" ") - buf.WriteString(http.StatusText(e.statusCode)) + buf := make([]byte, 0, 128) + buf = appendHTTPStatusPrefix(buf, e.statusCode) if e.underlying != nil { - buf.WriteString(": ") - buf.WriteString(e.underlying.Error()) + buf = append(buf, ": "...) + buf = append(buf, e.underlying.Error()...) } // TODO if underlying is nil, include some portion of e.body in the message? - return buf.String() + return string(buf) } // StatusCode implements [HTTPError.StatusCode]. @@ -225,6 +233,79 @@ func (e *httpError) ResponseBody() []byte { return e.body } +// MarshalError marshals the given error as JSON according +// to the OCI distribution specification. It also returns +// the associated HTTP status code, or [http.StatusInternalServerError] +// if no specific code can be found. +// +// If err is or wraps [Error], that code will be used for the "code" +// field in the marshaled error. +// +// If err wraps [HTTPError] and no HTTP status code is known +// for the error code, [HTTPError.StatusCode] will be used. +func MarshalError(err error) (errorBody []byte, httpStatus int) { + var e WireError + // TODO perhaps we should iterate through all the + // errors instead of just choosing one. + // See https://github.com/golang/go/issues/66455 + var ociErr Error + if errors.As(err, &ociErr) { + e.Code_ = ociErr.Code() + e.Detail_ = ociErr.Detail() + } + if e.Code_ == "" { + // This is contrary to spec, but it's what the Docker registry + // does, so it can't be too bad. + e.Code_ = "UNKNOWN" + } + // Use the HTTP status code from the error only when there isn't + // one implied from the error code. This means that the HTTP status + // is always consistent with the error code, but still allows a registry + // to choose custom HTTP status codes for other codes. + httpStatus = http.StatusInternalServerError + if status, ok := errorStatuses[e.Code_]; ok { + httpStatus = status + } else { + var httpErr HTTPError + if errors.As(err, &httpErr) { + httpStatus = httpErr.StatusCode() + } + } + // Prevent the message from containing a redundant + // error code prefix by stripping it before sending over the + // wire. This won't always work, but is enough to prevent + // adjacent stuttering of code prefixes when a client + // creates a WireError from an error response. + e.Message = trimErrorCodePrefix(err, httpStatus, e.Code_) + data, err := json.Marshal(WireErrors{ + Errors: []WireError{e}, + }) + if err != nil { + panic(fmt.Errorf("cannot marshal error: %v", err)) + } + return data, httpStatus +} + +// trimErrorCodePrefix returns err's string +// with any prefix codes added by [HTTPError] +// or [WireError] removed. +func trimErrorCodePrefix(err error, httpStatus int, errorCode string) string { + msg := err.Error() + buf := make([]byte, 0, 128) + if httpStatus != 0 { + buf = appendHTTPStatusPrefix(buf, httpStatus) + buf = append(buf, ": "...) + msg = strings.TrimPrefix(msg, string(buf)) + } + if errorCode != "" { + buf = buf[:0] + buf = appendErrorCodePrefix(buf, errorCode) + buf = append(buf, ": "...) + msg = strings.TrimPrefix(msg, string(buf)) + } + return msg +} + // The following values represent the known error codes. var ( ErrBlobUnknown = NewError("blob unknown to registry", "BLOB_UNKNOWN", nil) @@ -252,6 +333,27 @@ var ( ErrRangeInvalid = NewError("invalid content range", "RANGE_INVALID", nil) ) +func appendHTTPStatusPrefix(buf []byte, statusCode int) []byte { + buf = strconv.AppendInt(buf, int64(statusCode), 10) + buf = append(buf, ' ') + buf = append(buf, http.StatusText(statusCode)...) + return buf +} + +func appendErrorCodePrefix(buf []byte, code string) []byte { + if code == "" { + return append(buf, "(no code)"...) + } + for _, r := range code { + if r == '_' { + buf = append(buf, ' ') + } else { + buf = append(buf, string(unicode.ToLower(r))...) + } + } + return buf +} + func ref[T any](x T) *T { return &x } diff --git a/ociregistry/error_test.go b/ociregistry/error_test.go new file mode 100644 index 0000000..3c4ba17 --- /dev/null +++ b/ociregistry/error_test.go @@ -0,0 +1,94 @@ +package ociregistry + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "testing" + + "github.com/go-quicktest/qt" +) + +var errorTests = []struct { + testName string + err error + wantMsg string + wantMarshalData rawJSONMessage + wantMarshalHTTPStatus int +}{{ + testName: "RegularGoError", + err: fmt.Errorf("unknown error"), + wantMsg: "unknown error", + wantMarshalData: `{"errors":[{"code":"UNKNOWN","message":"unknown error"}]}`, + wantMarshalHTTPStatus: http.StatusInternalServerError, +}, { + testName: "RegistryError", + err: ErrBlobUnknown, + wantMsg: "blob unknown: blob unknown to registry", + wantMarshalData: `{"errors":[{"code":"BLOB_UNKNOWN","message":"blob unknown to registry"}]}`, + wantMarshalHTTPStatus: http.StatusNotFound, +}, { + testName: "WrappedRegistryErrorWithContextAtStart", + err: fmt.Errorf("some context: %w", ErrBlobUnknown), + wantMsg: "some context: blob unknown: blob unknown to registry", + wantMarshalData: `{"errors":[{"code":"BLOB_UNKNOWN","message":"some context: blob unknown: blob unknown to registry"}]}`, + wantMarshalHTTPStatus: http.StatusNotFound, +}, { + testName: "WrappedRegistryErrorWithContextAtEnd", + err: fmt.Errorf("%w: some context", ErrBlobUnknown), + wantMsg: "blob unknown: blob unknown to registry: some context", + wantMarshalData: `{"errors":[{"code":"BLOB_UNKNOWN","message":"blob unknown to registry: some context"}]}`, + wantMarshalHTTPStatus: http.StatusNotFound, +}, { + testName: "HTTPStatusIgnoredWithKnownCode", + err: NewHTTPError(fmt.Errorf("%w: some context", ErrBlobUnknown), http.StatusUnauthorized, nil, nil), + wantMsg: "401 Unauthorized: blob unknown: blob unknown to registry: some context", + // Note: the "401 Unauthorized" remains intact because it's not redundant with respect + // to the 404 HTTP response code. + wantMarshalData: `{"errors":[{"code":"BLOB_UNKNOWN","message":"401 Unauthorized: blob unknown: blob unknown to registry: some context"}]}`, + wantMarshalHTTPStatus: http.StatusNotFound, +}, { + testName: "HTTPStatusUsedWithUnknownCode", + err: NewHTTPError(NewError("a message with a code", "SOME_CODE", nil), http.StatusUnauthorized, nil, nil), + wantMsg: "401 Unauthorized: some code: a message with a code", + wantMarshalData: `{"errors":[{"code":"SOME_CODE","message":"a message with a code"}]}`, + wantMarshalHTTPStatus: http.StatusUnauthorized, +}, { + testName: "ErrorWithDetail", + err: NewError("a message with some detail", "SOME_CODE", json.RawMessage(`{"foo": true}`)), + wantMsg: `some code: a message with some detail`, + wantMarshalData: `{"errors":[{"code":"SOME_CODE","message":"a message with some detail","detail":{"foo":true}}]}`, + wantMarshalHTTPStatus: http.StatusInternalServerError, +}} + +func TestError(t *testing.T) { + for _, test := range errorTests { + t.Run(test.testName, func(t *testing.T) { + qt.Check(t, qt.ErrorMatches(test.err, test.wantMsg)) + data, httpStatus := MarshalError(test.err) + qt.Check(t, qt.Equals(httpStatus, test.wantMarshalHTTPStatus)) + qt.Check(t, qt.JSONEquals(data, test.wantMarshalData), qt.Commentf("marshal data: %s", data)) + + // Check that the marshaled error unmarshals into WireError OK and + // that the code matches appropriately. + var errs *WireErrors + err := json.Unmarshal(data, &errs) + qt.Assert(t, qt.IsNil(err)) + if ociErr := Error(nil); errors.As(test.err, &ociErr) { + qt.Assert(t, qt.IsTrue(errors.Is(errs, NewError("something", ociErr.Code(), nil)))) + } + }) + } +} + +type rawJSONMessage string + +func (m rawJSONMessage) MarshalJSON() ([]byte, error) { + return []byte(m), nil +} + +func (m *rawJSONMessage) UnmarshalJSON(data []byte) error { + *m = rawJSONMessage(data) + return nil +} diff --git a/ociregistry/ociclient/error.go b/ociregistry/ociclient/error.go index 21a2e9e..21f8901 100644 --- a/ociregistry/ociclient/error.go +++ b/ociregistry/ociclient/error.go @@ -55,6 +55,9 @@ func makeError1(resp *http.Response, bodyData []byte) error { // When we've made a HEAD request, we can't see any of // the actual error, so we'll have to make up something // from the HTTP status. + // TODO would we do better if we interpreted the HTTP status + // relative to the actual method that was called in order + // to come up with a more plausible error? var err error switch resp.StatusCode { case http.StatusNotFound: @@ -71,7 +74,7 @@ func makeError1(resp *http.Response, bodyData []byte) error { // Our caller will turn this into a non-nil error. return nil } - return fmt.Errorf("error response: %v: %w", resp.Status, err) + return err } if ctype := resp.Header.Get("Content-Type"); !isJSONMediaType(ctype) { return fmt.Errorf("non-JSON error response %q; body %q", ctype, bodyData) diff --git a/ociregistry/ociclient/error_test.go b/ociregistry/ociclient/error_test.go index 6454e8c..9726b5a 100644 --- a/ociregistry/ociclient/error_test.go +++ b/ociregistry/ociclient/error_test.go @@ -10,10 +10,39 @@ import ( "testing" "cuelabs.dev/go/oci/ociregistry" + "cuelabs.dev/go/oci/ociregistry/ociserver" "github.com/go-quicktest/qt" "github.com/opencontainers/go-digest" ) +func TestErrorStuttering(t *testing.T) { + // This checks that the stuttering observed in issue #31 + // isn't an issue when ociserver wraps ociclient. + srv := httptest.NewServer(ociserver.New(&ociregistry.Funcs{ + NewError: func(ctx context.Context, methodName, repo string) error { + return ociregistry.ErrManifestUnknown + }, + }, nil)) + defer srv.Close() + + srvURL, _ := url.Parse(srv.URL) + r, err := New(srvURL.Host, &Options{ + Insecure: true, + }) + qt.Assert(t, qt.IsNil(err)) + _, err = r.GetTag(context.Background(), "foo", "sometag") + qt.Check(t, qt.ErrorIs(err, ociregistry.ErrManifestUnknown)) + qt.Check(t, qt.ErrorMatches(err, `404 Not Found: manifest unknown: manifest unknown to registry`)) + + // ResolveTag uses HEAD rather than GET, so here we're testing + // the path where a response with no body gets turned back into + // something vaguely resembling the original error, which is why + // the code and message have changed. + _, err = r.ResolveTag(context.Background(), "foo", "sometag") + qt.Check(t, qt.ErrorIs(err, ociregistry.ErrNameUnknown)) + qt.Check(t, qt.ErrorMatches(err, `404 Not Found: name unknown: repository name not known to registry`)) +} + func TestNonJSONErrorResponse(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusTeapot) diff --git a/ociregistry/ociserver/error.go b/ociregistry/ociserver/error.go index 2fcb0e6..126562b 100644 --- a/ociregistry/ociserver/error.go +++ b/ociregistry/ociserver/error.go @@ -15,8 +15,6 @@ package ociserver import ( - "encoding/json" - "errors" "fmt" "net/http" @@ -24,71 +22,12 @@ import ( ) func writeError(resp http.ResponseWriter, err error) { - e := ociregistry.WireError{ - Message: err.Error(), - } - // TODO perhaps we should iterate through all the - // errors instead of just choosing one. - // See https://github.com/golang/go/issues/66455 - var ociErr ociregistry.Error - if errors.As(err, &ociErr) { - e.Code_ = ociErr.Code() - if detail := ociErr.Detail(); detail != nil { - data, err := json.Marshal(detail) - if err != nil { - panic(fmt.Errorf("cannot marshal error detail: %v", err)) - } - e.Detail_ = json.RawMessage(data) - } - } else { - // This is contrary to spec, but it's what the Docker registry - // does, so it can't be too bad. - e.Code_ = "UNKNOWN" - } - - // Use the HTTP status code from the error only when there isn't - // one implied from the error code. This means that the HTTP status - // is always consistent with the error code, but still allows a registry - // to return custom HTTP status codes for other codes. - httpStatus := http.StatusInternalServerError - if status, ok := errorStatuses[e.Code_]; ok { - httpStatus = status - } else { - var httpErr ociregistry.HTTPError - if errors.As(err, &httpErr) { - httpStatus = httpErr.StatusCode() - } - } + data, httpStatus := ociregistry.MarshalError(err) resp.Header().Set("Content-Type", "application/json") resp.WriteHeader(httpStatus) - - data, err := json.Marshal(ociregistry.WireErrors{ - Errors: []ociregistry.WireError{e}, - }) - if err != nil { - // TODO log - } resp.Write(data) } -var errorStatuses = map[string]int{ - ociregistry.ErrBlobUnknown.Code(): http.StatusNotFound, - ociregistry.ErrBlobUploadInvalid.Code(): http.StatusRequestedRangeNotSatisfiable, - ociregistry.ErrBlobUploadUnknown.Code(): http.StatusNotFound, - ociregistry.ErrDigestInvalid.Code(): http.StatusBadRequest, - ociregistry.ErrManifestBlobUnknown.Code(): http.StatusNotFound, - ociregistry.ErrManifestInvalid.Code(): http.StatusBadRequest, - ociregistry.ErrManifestUnknown.Code(): http.StatusNotFound, - ociregistry.ErrNameInvalid.Code(): http.StatusBadRequest, - ociregistry.ErrNameUnknown.Code(): http.StatusNotFound, - ociregistry.ErrSizeInvalid.Code(): http.StatusBadRequest, - ociregistry.ErrUnauthorized.Code(): http.StatusUnauthorized, - ociregistry.ErrDenied.Code(): http.StatusForbidden, - ociregistry.ErrUnsupported.Code(): http.StatusBadRequest, - ociregistry.ErrTooManyRequests.Code(): http.StatusTooManyRequests, - ociregistry.ErrRangeInvalid.Code(): http.StatusRequestedRangeNotSatisfiable, -} - func withHTTPCode(statusCode int, err error) error { return ociregistry.NewHTTPError(err, statusCode, nil, nil) } diff --git a/ociregistry/ociserver/error_test.go b/ociregistry/ociserver/error_test.go index c99b64f..8a26b68 100644 --- a/ociregistry/ociserver/error_test.go +++ b/ociregistry/ociserver/error_test.go @@ -16,7 +16,7 @@ package ociserver import ( "context" - "io/ioutil" + "io" "net/http" "net/http/httptest" "testing" @@ -40,7 +40,7 @@ func TestHTTPStatusOverriddenByErrorCode(t *testing.T) { resp, err := http.Get(s.URL + "/v2/foo/manifests/sometag") qt.Assert(t, qt.IsNil(err)) defer resp.Body.Close() - body, _ := ioutil.ReadAll(resp.Body) + body, _ := io.ReadAll(resp.Body) qt.Assert(t, qt.Equals(resp.StatusCode, http.StatusNotFound)) qt.Assert(t, qt.JSONEquals(body, &ociregistry.WireErrors{ Errors: []ociregistry.WireError{{ @@ -64,12 +64,12 @@ func TestHTTPStatusUsedForUnknownErrorCode(t *testing.T) { resp, err := http.Get(s.URL + "/v2/foo/manifests/sometag") qt.Assert(t, qt.IsNil(err)) defer resp.Body.Close() - body, _ := ioutil.ReadAll(resp.Body) + body, _ := io.ReadAll(resp.Body) qt.Assert(t, qt.Equals(resp.StatusCode, http.StatusTeapot)) qt.Assert(t, qt.JSONEquals(body, &ociregistry.WireErrors{ Errors: []ociregistry.WireError{{ Code_: "SOMECODE", - Message: "418 I'm a teapot: somecode: foo", + Message: "foo", }}, })) } diff --git a/ociregistry/ociserver/registry_test.go b/ociregistry/ociserver/registry_test.go index 6b5ff7c..5f219a7 100644 --- a/ociregistry/ociserver/registry_test.go +++ b/ociregistry/ociserver/registry_test.go @@ -88,14 +88,14 @@ func TestCalls(t *testing.T) { URL: "/v2/bad", WantCode: http.StatusNotFound, WantHeader: map[string]string{"Docker-Distribution-API-Version": "registry/2.0"}, - WantBody: `{"errors":[{"code":"UNKNOWN","message":"404 Not Found: page not found"}]}`, + WantBody: `{"errors":[{"code":"UNKNOWN","message":"page not found"}]}`, }, { Description: "GET_non_existent_blob", Method: "GET", URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", WantCode: http.StatusNotFound, - WantBody: `{"errors":[{"code":"NAME_UNKNOWN","message":"name unknown: repository name not known to registry"}]}`, + WantBody: `{"errors":[{"code":"NAME_UNKNOWN","message":"repository name not known to registry"}]}`, }, { Description: "HEAD_non_existent_blob", @@ -108,7 +108,7 @@ func TestCalls(t *testing.T) { Method: "GET", URL: "/v2/foo/blobs/sha256:asd", WantCode: http.StatusBadRequest, - WantBody: `{"errors":[{"code":"UNKNOWN","message":"400 Bad Request: badly formed digest"}]}`, + WantBody: `{"errors":[{"code":"UNKNOWN","message":"badly formed digest"}]}`, }, { Description: "HEAD_bad_digest", @@ -121,7 +121,7 @@ func TestCalls(t *testing.T) { Method: "FOO", URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", WantCode: http.StatusMethodNotAllowed, - WantBody: `{"errors":[{"code":"UNKNOWN","message":"405 Method Not Allowed: method not allowed"}]}`, + WantBody: `{"errors":[{"code":"UNKNOWN","message":"method not allowed"}]}`, }, { Description: "GET_containerless_blob", @@ -214,7 +214,7 @@ func TestCalls(t *testing.T) { Method: "GET", URL: "/v2/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", WantCode: http.StatusNotFound, - WantBody: `{"errors":[{"code":"UNKNOWN","message":"404 Not Found: page not found"}]}`, + WantBody: `{"errors":[{"code":"UNKNOWN","message":"page not found"}]}`, }, { Description: "uploadurl", @@ -235,7 +235,7 @@ func TestCalls(t *testing.T) { Method: "PUT", URL: "/v2/foo/blobs/uploads/MQ", WantCode: http.StatusBadRequest, - WantBody: `{"errors":[{"code":"UNKNOWN","message":"400 Bad Request: badly formed digest"}]}`, + WantBody: `{"errors":[{"code":"UNKNOWN","message":"badly formed digest"}]}`, }, { Description: "monolithic_upload_good_digest", @@ -251,7 +251,7 @@ func TestCalls(t *testing.T) { URL: "/v2/foo/blobs/uploads?digest=sha256:fake", Body: "foo", WantCode: http.StatusBadRequest, - WantBody: `{"errors":[{"code":"UNKNOWN","message":"400 Bad Request: badly formed digest"}]}`, + WantBody: `{"errors":[{"code":"UNKNOWN","message":"badly formed digest"}]}`, }, { Description: "upload_good_digest", @@ -267,7 +267,7 @@ func TestCalls(t *testing.T) { URL: "/v2/foo/blobs/uploads/MQ?digest=sha256:baddigest", WantCode: http.StatusBadRequest, Body: "foo", - WantBody: `{"errors":[{"code":"UNKNOWN","message":"400 Bad Request: badly formed digest"}]}`, + WantBody: `{"errors":[{"code":"UNKNOWN","message":"badly formed digest"}]}`, }, { Description: "stream_upload", @@ -305,7 +305,7 @@ func TestCalls(t *testing.T) { Method: "GET", URL: "/v2/foo/manifests/latest", WantCode: http.StatusNotFound, - WantBody: `{"errors":[{"code":"NAME_UNKNOWN","message":"name unknown: repository name not known to registry"}]}`, + WantBody: `{"errors":[{"code":"NAME_UNKNOWN","message":"repository name not known to registry"}]}`, }, { Description: "head_missing_manifest", @@ -319,7 +319,7 @@ func TestCalls(t *testing.T) { Method: "GET", URL: "/v2/foo/manifests/bar", WantCode: http.StatusNotFound, - WantBody: `{"errors":[{"code":"MANIFEST_UNKNOWN","message":"manifest unknown: manifest unknown to registry"}]}`, + WantBody: `{"errors":[{"code":"MANIFEST_UNKNOWN","message":"manifest unknown to registry"}]}`, }, { Description: "head_missing_manifest_good_container", @@ -396,7 +396,7 @@ func TestCalls(t *testing.T) { Method: "BAR", URL: "/v2/foo/manifests/latest", WantCode: http.StatusMethodNotAllowed, - WantBody: `{"errors":[{"code":"UNKNOWN","message":"405 Method Not Allowed: method not allowed"}]}`, + WantBody: `{"errors":[{"code":"UNKNOWN","message":"method not allowed"}]}`, }, { Description: "Chunk_upload_start", @@ -418,7 +418,7 @@ func TestCalls(t *testing.T) { // TODO the original had 405 response here. Which is correct? WantCode: http.StatusBadRequest, Body: "foo", - WantBody: `{"errors":[{"code":"UNSUPPORTED","message":"unsupported: we don't understand your Content-Range"}]}`, + WantBody: `{"errors":[{"code":"UNSUPPORTED","message":"we don't understand your Content-Range"}]}`, }, { Description: "Chunk_upload_overlaps_previous_data", @@ -448,7 +448,7 @@ func TestCalls(t *testing.T) { Method: "DELETE", URL: "/v2/test/honk/manifests/latest", WantCode: http.StatusNotFound, - WantBody: `{"errors":[{"code":"NAME_UNKNOWN","message":"name unknown: repository name not known to registry"}]}`, + WantBody: `{"errors":[{"code":"NAME_UNKNOWN","message":"repository name not known to registry"}]}`, }, { Description: "DELETE_Unknown_manifest", @@ -456,7 +456,7 @@ func TestCalls(t *testing.T) { Method: "DELETE", URL: "/v2/honk/manifests/tag-honk", WantCode: http.StatusNotFound, - WantBody: `{"errors":[{"code":"MANIFEST_UNKNOWN","message":"manifest unknown: manifest unknown to registry: tag does not exist"}]}`, + WantBody: `{"errors":[{"code":"MANIFEST_UNKNOWN","message":"manifest unknown to registry: tag does not exist"}]}`, }, { Description: "DELETE_existing_manifest", @@ -501,7 +501,7 @@ func TestCalls(t *testing.T) { Method: "GET", URL: "/v2/foo/tags/list?n=1000", WantCode: http.StatusNotFound, - WantBody: `{"errors":[{"code":"NAME_UNKNOWN","message":"name unknown: repository name not known to registry"}]}`, + WantBody: `{"errors":[{"code":"NAME_UNKNOWN","message":"repository name not known to registry"}]}`, }, { Description: "list_repos", @@ -548,14 +548,14 @@ func TestCalls(t *testing.T) { Method: "GET", URL: "/v2/does-not-exist/referrers/" + digestOf("foo"), WantCode: http.StatusNotFound, - WantBody: `{"errors":[{"code":"NAME_UNKNOWN","message":"name unknown: repository name not known to registry"}]}`, + WantBody: `{"errors":[{"code":"NAME_UNKNOWN","message":"repository name not known to registry"}]}`, }, { Description: "fetch_references,_bad_target_(tag_vs._digest)", Method: "GET", URL: "/v2/foo/referrers/latest", WantCode: http.StatusBadRequest, - WantBody: `{"errors":[{"code":"UNKNOWN","message":"400 Bad Request: badly formed digest"}]}`, + WantBody: `{"errors":[{"code":"UNKNOWN","message":"badly formed digest"}]}`, }, { skip: true, @@ -659,7 +659,7 @@ func TestCalls(t *testing.T) { if string(body) != tc.WantBody { t.Logf("\n WantBody: `%s`,", body) - t.Errorf("Incorrect response body, got %q, want %q", body, tc.WantBody) + t.Errorf("Incorrect response body.\ngot:\n\t%q\n\twant:\n\t%q", body, tc.WantBody) } } t.Run(tc.Description, testf)