diff --git a/client/request_test.go b/client/request_test.go index 30de71382a..91d4b4b499 100644 --- a/client/request_test.go +++ b/client/request_test.go @@ -3,6 +3,7 @@ package client // import "github.com/docker/docker/client" import ( "bytes" "context" + "encoding/json" "fmt" "io" "math/rand" @@ -11,6 +12,7 @@ import ( "testing" "time" + "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/errdefs" "gotest.tools/v3/assert" @@ -92,6 +94,136 @@ func TestPlainTextError(t *testing.T) { assert.Check(t, is.ErrorType(err, errdefs.IsSystem)) } +// TestResponseErrors tests handling of error responses returned by the API. +// It includes test-cases for malformed and invalid error-responses, as well +// as plain text errors for backwards compatibility with API versions <1.24. +func TestResponseErrors(t *testing.T) { + errorResponse, err := json.Marshal(&types.ErrorResponse{ + Message: "Some error occurred", + }) + assert.NilError(t, err) + + tests := []struct { + doc string + apiVersion string + contentType string + response string + expected string + }{ + { + // Valid (types.ErrorResponse) error, but not using a fixture, to validate current implementation.. + doc: "JSON error (non-fixture)", + contentType: "application/json", + response: string(errorResponse), + expected: `Error response from daemon: Some error occurred`, + }, + { + // Valid (types.ErrorResponse) error. + doc: "JSON error", + contentType: "application/json", + response: `{"message":"Some error occurred"}`, + expected: `Error response from daemon: Some error occurred`, + }, + { + // Valid (types.ErrorResponse) error with additional fields. + doc: "JSON error with extra fields", + contentType: "application/json", + response: `{"message":"Some error occurred", "other_field": "some other field that's not part of types.ErrorResponse"}`, + expected: `Error response from daemon: Some error occurred`, + }, + { + // API versions before 1.24 did not support JSON errors, and return response as-is. + doc: "JSON error on old API", + apiVersion: "1.23", + contentType: "application/json", + response: `{"message":"Some error occurred"}`, + expected: `Error response from daemon: {"message":"Some error occurred"}`, + }, + { + doc: "plain-text error", + contentType: "text/plain", + response: `Some error occurred`, + expected: `Error response from daemon: Some error occurred`, + }, + { + // TODO(thaJeztah): consider returning (partial) raw response for these + doc: "malformed JSON", + contentType: "application/json", + response: `{"message":"Some error occurred`, + expected: `Error reading JSON: unexpected end of JSON input`, + }, + { + // Server response that's valid JSON, but not the expected (types.ErrorResponse) scheme + // TODO(thaJeztah): consider returning (partial) raw response for these + doc: "incorrect JSON scheme", + contentType: "application/json", + response: `{"error":"Some error occurred"}`, + expected: `Error response from daemon: `, + }, + { + // TODO(thaJeztah): improve handling of such errors; we can return the generic "502 Bad Gateway" instead + doc: "html error", + contentType: "text/html", + response: ` + +
+The server was unable to complete your request. Please try again later.
+If this problem persists, please contact support.
+ +`, + expected: `Error response from daemon: + + +The server was unable to complete your request. Please try again later.
+If this problem persists, please contact support.
+ +`, + }, + { + // TODO(thaJeztah): improve handling of these errors (JSON: invalid character '<' looking for beginning of value) + doc: "html error masquerading as JSON", + contentType: "application/json", + response: ` + + +The server was unable to complete your request. Please try again later.
+If this problem persists, please contact support.
+ +`, + expected: `Error reading JSON: invalid character '<' looking for beginning of value`, + }, + } + for _, tc := range tests { + t.Run(tc.doc, func(t *testing.T) { + client := &Client{ + version: tc.apiVersion, + client: newMockClient(func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusBadRequest, + Header: http.Header{"Content-Type": []string{tc.contentType}}, + Body: io.NopCloser(bytes.NewReader([]byte(tc.response))), + }, nil + }), + } + _, err := client.Ping(context.Background()) + assert.Check(t, is.Error(err, tc.expected)) + assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter)) + }) + } +} + func TestInfiniteError(t *testing.T) { infinitR := rand.New(rand.NewSource(42)) client := &Client{