Browse code

Set ping version even on error

In some cases a server may return an error on the ping response but
still provide version details. The client should use these values when
available.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>

Brian Goff authored on 2017/06/27 01:06:34
Showing 3 changed files
... ...
@@ -18,13 +18,15 @@ func (cli *Client) Ping(ctx context.Context) (types.Ping, error) {
18 18
 	}
19 19
 	defer ensureReaderClosed(serverResp)
20 20
 
21
-	ping.APIVersion = serverResp.header.Get("API-Version")
21
+	if serverResp.header != nil {
22
+		ping.APIVersion = serverResp.header.Get("API-Version")
22 23
 
23
-	if serverResp.header.Get("Docker-Experimental") == "true" {
24
-		ping.Experimental = true
24
+		if serverResp.header.Get("Docker-Experimental") == "true" {
25
+			ping.Experimental = true
26
+		}
27
+		ping.OSType = serverResp.header.Get("OSType")
25 28
 	}
26 29
 
27
-	ping.OSType = serverResp.header.Get("OSType")
28
-
29
-	return ping, nil
30
+	err = cli.checkResponseErr(serverResp)
31
+	return ping, err
30 32
 }
31 33
new file mode 100644
... ...
@@ -0,0 +1,82 @@
0
+package client
1
+
2
+import (
3
+	"errors"
4
+	"io/ioutil"
5
+	"net/http"
6
+	"strings"
7
+	"testing"
8
+
9
+	"github.com/stretchr/testify/assert"
10
+	"golang.org/x/net/context"
11
+)
12
+
13
+// TestPingFail tests that when a server sends a non-successful response that we
14
+// can still grab API details, when set.
15
+// Some of this is just excercising the code paths to make sure there are no
16
+// panics.
17
+func TestPingFail(t *testing.T) {
18
+	var withHeader bool
19
+	client := &Client{
20
+		client: newMockClient(func(req *http.Request) (*http.Response, error) {
21
+			resp := &http.Response{StatusCode: http.StatusInternalServerError}
22
+			if withHeader {
23
+				resp.Header = http.Header{}
24
+				resp.Header.Set("API-Version", "awesome")
25
+				resp.Header.Set("Docker-Experimental", "true")
26
+			}
27
+			resp.Body = ioutil.NopCloser(strings.NewReader("some error with the server"))
28
+			return resp, nil
29
+		}),
30
+	}
31
+
32
+	ping, err := client.Ping(context.Background())
33
+	assert.Error(t, err)
34
+	assert.Equal(t, false, ping.Experimental)
35
+	assert.Equal(t, "", ping.APIVersion)
36
+
37
+	withHeader = true
38
+	ping2, err := client.Ping(context.Background())
39
+	assert.Error(t, err)
40
+	assert.Equal(t, true, ping2.Experimental)
41
+	assert.Equal(t, "awesome", ping2.APIVersion)
42
+}
43
+
44
+// TestPingWithError tests the case where there is a protocol error in the ping.
45
+// This test is mostly just testing that there are no panics in this code path.
46
+func TestPingWithError(t *testing.T) {
47
+	client := &Client{
48
+		client: newMockClient(func(req *http.Request) (*http.Response, error) {
49
+			resp := &http.Response{StatusCode: http.StatusInternalServerError}
50
+			resp.Header = http.Header{}
51
+			resp.Header.Set("API-Version", "awesome")
52
+			resp.Header.Set("Docker-Experimental", "true")
53
+			resp.Body = ioutil.NopCloser(strings.NewReader("some error with the server"))
54
+			return resp, errors.New("some error")
55
+		}),
56
+	}
57
+
58
+	ping, err := client.Ping(context.Background())
59
+	assert.Error(t, err)
60
+	assert.Equal(t, false, ping.Experimental)
61
+	assert.Equal(t, "", ping.APIVersion)
62
+}
63
+
64
+// TestPingSuccess tests that we are able to get the expected API headers/ping
65
+// details on success.
66
+func TestPingSuccess(t *testing.T) {
67
+	client := &Client{
68
+		client: newMockClient(func(req *http.Request) (*http.Response, error) {
69
+			resp := &http.Response{StatusCode: http.StatusInternalServerError}
70
+			resp.Header = http.Header{}
71
+			resp.Header.Set("API-Version", "awesome")
72
+			resp.Header.Set("Docker-Experimental", "true")
73
+			resp.Body = ioutil.NopCloser(strings.NewReader("some error with the server"))
74
+			return resp, nil
75
+		}),
76
+	}
77
+	ping, err := client.Ping(context.Background())
78
+	assert.Error(t, err)
79
+	assert.Equal(t, true, ping.Experimental)
80
+	assert.Equal(t, "awesome", ping.APIVersion)
81
+}
... ...
@@ -24,6 +24,7 @@ type serverResponse struct {
24 24
 	body       io.ReadCloser
25 25
 	header     http.Header
26 26
 	statusCode int
27
+	reqURL     *url.URL
27 28
 }
28 29
 
29 30
 // head sends an http request to the docker API using the method HEAD.
... ...
@@ -118,11 +119,18 @@ func (cli *Client) sendRequest(ctx context.Context, method, path string, query u
118 118
 	if err != nil {
119 119
 		return serverResponse{}, err
120 120
 	}
121
-	return cli.doRequest(ctx, req)
121
+	resp, err := cli.doRequest(ctx, req)
122
+	if err != nil {
123
+		return resp, err
124
+	}
125
+	if err := cli.checkResponseErr(resp); err != nil {
126
+		return resp, err
127
+	}
128
+	return resp, nil
122 129
 }
123 130
 
124 131
 func (cli *Client) doRequest(ctx context.Context, req *http.Request) (serverResponse, error) {
125
-	serverResp := serverResponse{statusCode: -1}
132
+	serverResp := serverResponse{statusCode: -1, reqURL: req.URL}
126 133
 
127 134
 	resp, err := ctxhttp.Do(ctx, cli.client, req)
128 135
 	if err != nil {
... ...
@@ -179,35 +187,42 @@ func (cli *Client) doRequest(ctx context.Context, req *http.Request) (serverResp
179 179
 
180 180
 	if resp != nil {
181 181
 		serverResp.statusCode = resp.StatusCode
182
+		serverResp.body = resp.Body
183
+		serverResp.header = resp.Header
182 184
 	}
185
+	return serverResp, nil
186
+}
183 187
 
184
-	if serverResp.statusCode < 200 || serverResp.statusCode >= 400 {
185
-		body, err := ioutil.ReadAll(resp.Body)
186
-		if err != nil {
187
-			return serverResp, err
188
-		}
189
-		if len(body) == 0 {
190
-			return serverResp, fmt.Errorf("Error: request returned %s for API route and version %s, check if the server supports the requested API version", http.StatusText(serverResp.statusCode), req.URL)
191
-		}
188
+func (cli *Client) checkResponseErr(serverResp serverResponse) error {
189
+	if serverResp.statusCode >= 200 && serverResp.statusCode < 400 {
190
+		return nil
191
+	}
192 192
 
193
-		var errorMessage string
194
-		if (cli.version == "" || versions.GreaterThan(cli.version, "1.23")) &&
195
-			resp.Header.Get("Content-Type") == "application/json" {
196
-			var errorResponse types.ErrorResponse
197
-			if err := json.Unmarshal(body, &errorResponse); err != nil {
198
-				return serverResp, fmt.Errorf("Error reading JSON: %v", err)
199
-			}
200
-			errorMessage = errorResponse.Message
201
-		} else {
202
-			errorMessage = string(body)
203
-		}
193
+	body, err := ioutil.ReadAll(serverResp.body)
194
+	if err != nil {
195
+		return err
196
+	}
197
+	if len(body) == 0 {
198
+		return fmt.Errorf("Error: request returned %s for API route and version %s, check if the server supports the requested API version", http.StatusText(serverResp.statusCode), serverResp.reqURL)
199
+	}
204 200
 
205
-		return serverResp, fmt.Errorf("Error response from daemon: %s", strings.TrimSpace(errorMessage))
201
+	var ct string
202
+	if serverResp.header != nil {
203
+		ct = serverResp.header.Get("Content-Type")
206 204
 	}
207 205
 
208
-	serverResp.body = resp.Body
209
-	serverResp.header = resp.Header
210
-	return serverResp, nil
206
+	var errorMessage string
207
+	if (cli.version == "" || versions.GreaterThan(cli.version, "1.23")) && ct == "application/json" {
208
+		var errorResponse types.ErrorResponse
209
+		if err := json.Unmarshal(body, &errorResponse); err != nil {
210
+			return fmt.Errorf("Error reading JSON: %v", err)
211
+		}
212
+		errorMessage = errorResponse.Message
213
+	} else {
214
+		errorMessage = string(body)
215
+	}
216
+
217
+	return fmt.Errorf("Error response from daemon: %s", strings.TrimSpace(errorMessage))
211 218
 }
212 219
 
213 220
 func (cli *Client) addHeaders(req *http.Request, headers headers) *http.Request {
... ...
@@ -239,9 +254,9 @@ func encodeData(data interface{}) (*bytes.Buffer, error) {
239 239
 }
240 240
 
241 241
 func ensureReaderClosed(response serverResponse) {
242
-	if body := response.body; body != nil {
242
+	if response.body != nil {
243 243
 		// Drain up to 512 bytes and close the body to let the Transport reuse the connection
244
-		io.CopyN(ioutil.Discard, body, 512)
244
+		io.CopyN(ioutil.Discard, response.body, 512)
245 245
 		response.body.Close()
246 246
 	}
247 247
 }