Browse code

client: improve test-coverage for error-responses

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Sebastiaan van Stijn authored on 2025/01/29 08:15:00
Showing 1 changed files
... ...
@@ -3,6 +3,7 @@ package client // import "github.com/docker/docker/client"
3 3
 import (
4 4
 	"bytes"
5 5
 	"context"
6
+	"encoding/json"
6 7
 	"fmt"
7 8
 	"io"
8 9
 	"math/rand"
... ...
@@ -11,6 +12,7 @@ import (
11 11
 	"testing"
12 12
 	"time"
13 13
 
14
+	"github.com/docker/docker/api/types"
14 15
 	"github.com/docker/docker/api/types/container"
15 16
 	"github.com/docker/docker/errdefs"
16 17
 	"gotest.tools/v3/assert"
... ...
@@ -92,6 +94,136 @@ func TestPlainTextError(t *testing.T) {
92 92
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
93 93
 }
94 94
 
95
+// TestResponseErrors tests handling of error responses returned by the API.
96
+// It includes test-cases for malformed and invalid error-responses, as well
97
+// as plain text errors for backwards compatibility with API versions <1.24.
98
+func TestResponseErrors(t *testing.T) {
99
+	errorResponse, err := json.Marshal(&types.ErrorResponse{
100
+		Message: "Some error occurred",
101
+	})
102
+	assert.NilError(t, err)
103
+
104
+	tests := []struct {
105
+		doc         string
106
+		apiVersion  string
107
+		contentType string
108
+		response    string
109
+		expected    string
110
+	}{
111
+		{
112
+			// Valid (types.ErrorResponse) error, but not using a fixture, to validate current implementation..
113
+			doc:         "JSON error (non-fixture)",
114
+			contentType: "application/json",
115
+			response:    string(errorResponse),
116
+			expected:    `Error response from daemon: Some error occurred`,
117
+		},
118
+		{
119
+			// Valid (types.ErrorResponse) error.
120
+			doc:         "JSON error",
121
+			contentType: "application/json",
122
+			response:    `{"message":"Some error occurred"}`,
123
+			expected:    `Error response from daemon: Some error occurred`,
124
+		},
125
+		{
126
+			// Valid (types.ErrorResponse) error with additional fields.
127
+			doc:         "JSON error with extra fields",
128
+			contentType: "application/json",
129
+			response:    `{"message":"Some error occurred", "other_field": "some other field that's not part of types.ErrorResponse"}`,
130
+			expected:    `Error response from daemon: Some error occurred`,
131
+		},
132
+		{
133
+			// API versions before 1.24 did not support JSON errors, and return response as-is.
134
+			doc:         "JSON error on old API",
135
+			apiVersion:  "1.23",
136
+			contentType: "application/json",
137
+			response:    `{"message":"Some error occurred"}`,
138
+			expected:    `Error response from daemon: {"message":"Some error occurred"}`,
139
+		},
140
+		{
141
+			doc:         "plain-text error",
142
+			contentType: "text/plain",
143
+			response:    `Some error occurred`,
144
+			expected:    `Error response from daemon: Some error occurred`,
145
+		},
146
+		{
147
+			// TODO(thaJeztah): consider returning (partial) raw response for these
148
+			doc:         "malformed JSON",
149
+			contentType: "application/json",
150
+			response:    `{"message":"Some error occurred`,
151
+			expected:    `Error reading JSON: unexpected end of JSON input`,
152
+		},
153
+		{
154
+			// Server response that's valid JSON, but not the expected (types.ErrorResponse) scheme
155
+			// TODO(thaJeztah): consider returning (partial) raw response for these
156
+			doc:         "incorrect JSON scheme",
157
+			contentType: "application/json",
158
+			response:    `{"error":"Some error occurred"}`,
159
+			expected:    `Error response from daemon: `,
160
+		},
161
+		{
162
+			// TODO(thaJeztah): improve handling of such errors; we can return the generic "502 Bad Gateway" instead
163
+			doc:         "html error",
164
+			contentType: "text/html",
165
+			response: `<!doctype html>
166
+<html lang="en">
167
+<head>
168
+  <title>502 Bad Gateway</title>
169
+</head>
170
+<body>
171
+  <h1>Bad Gateway</h1>
172
+  <p>The server was unable to complete your request. Please try again later.</p>
173
+  <p>If this problem persists, please <a href="https://example.com/support">contact support</a>.</p>
174
+</body>
175
+</html>`,
176
+			expected: `Error response from daemon: <!doctype html>
177
+<html lang="en">
178
+<head>
179
+  <title>502 Bad Gateway</title>
180
+</head>
181
+<body>
182
+  <h1>Bad Gateway</h1>
183
+  <p>The server was unable to complete your request. Please try again later.</p>
184
+  <p>If this problem persists, please <a href="https://example.com/support">contact support</a>.</p>
185
+</body>
186
+</html>`,
187
+		},
188
+		{
189
+			// TODO(thaJeztah): improve handling of these errors (JSON: invalid character '<' looking for beginning of value)
190
+			doc:         "html error masquerading as JSON",
191
+			contentType: "application/json",
192
+			response: `<!doctype html>
193
+<html lang="en">
194
+<head>
195
+  <title>502 Bad Gateway</title>
196
+</head>
197
+<body>
198
+  <h1>Bad Gateway</h1>
199
+  <p>The server was unable to complete your request. Please try again later.</p>
200
+  <p>If this problem persists, please <a href="https://example.com/support">contact support</a>.</p>
201
+</body>
202
+</html>`,
203
+			expected: `Error reading JSON: invalid character '<' looking for beginning of value`,
204
+		},
205
+	}
206
+	for _, tc := range tests {
207
+		t.Run(tc.doc, func(t *testing.T) {
208
+			client := &Client{
209
+				version: tc.apiVersion,
210
+				client: newMockClient(func(req *http.Request) (*http.Response, error) {
211
+					return &http.Response{
212
+						StatusCode: http.StatusBadRequest,
213
+						Header:     http.Header{"Content-Type": []string{tc.contentType}},
214
+						Body:       io.NopCloser(bytes.NewReader([]byte(tc.response))),
215
+					}, nil
216
+				}),
217
+			}
218
+			_, err := client.Ping(context.Background())
219
+			assert.Check(t, is.Error(err, tc.expected))
220
+			assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
221
+		})
222
+	}
223
+}
224
+
95 225
 func TestInfiniteError(t *testing.T) {
96 226
 	infinitR := rand.New(rand.NewSource(42))
97 227
 	client := &Client{