Browse code

Return remote API errors as JSON

Signed-off-by: Ben Firshman <ben@firshman.co.uk>

Ben Firshman authored on 2016/05/21 20:56:04
Showing 13 changed files
... ...
@@ -5,6 +5,9 @@ import (
5 5
 	"strings"
6 6
 
7 7
 	"github.com/Sirupsen/logrus"
8
+	"github.com/docker/engine-api/types"
9
+	"github.com/docker/engine-api/types/versions"
10
+	"github.com/gorilla/mux"
8 11
 )
9 12
 
10 13
 // httpStatusError is an interface
... ...
@@ -70,13 +73,19 @@ func GetHTTPErrorStatusCode(err error) int {
70 70
 	return statusCode
71 71
 }
72 72
 
73
-// WriteError decodes a specific docker error and sends it in the response.
74
-func WriteError(w http.ResponseWriter, err error) {
75
-	if err == nil || w == nil {
76
-		logrus.WithFields(logrus.Fields{"error": err, "writer": w}).Error("unexpected HTTP error handling")
77
-		return
73
+// MakeErrorHandler makes an HTTP handler that decodes a Docker error and
74
+// returns it in the response.
75
+func MakeErrorHandler(err error) http.HandlerFunc {
76
+	return func(w http.ResponseWriter, r *http.Request) {
77
+		statusCode := GetHTTPErrorStatusCode(err)
78
+		vars := mux.Vars(r)
79
+		if vars["version"] == "" || versions.GreaterThan(vars["version"], "1.23") {
80
+			response := &types.ErrorResponse{
81
+				Message: err.Error(),
82
+			}
83
+			WriteJSON(w, statusCode, response)
84
+		} else {
85
+			http.Error(w, err.Error(), statusCode)
86
+		}
78 87
 	}
79
-
80
-	statusCode := GetHTTPErrorStatusCode(err)
81
-	http.Error(w, err.Error(), statusCode)
82 88
 }
... ...
@@ -2,6 +2,7 @@ package server
2 2
 
3 3
 import (
4 4
 	"crypto/tls"
5
+	"fmt"
5 6
 	"net"
6 7
 	"net/http"
7 8
 	"strings"
... ...
@@ -10,6 +11,7 @@ import (
10 10
 	"github.com/docker/docker/api/server/httputils"
11 11
 	"github.com/docker/docker/api/server/middleware"
12 12
 	"github.com/docker/docker/api/server/router"
13
+	"github.com/docker/docker/errors"
13 14
 	"github.com/gorilla/mux"
14 15
 	"golang.org/x/net/context"
15 16
 )
... ...
@@ -136,7 +138,7 @@ func (s *Server) makeHTTPHandler(handler httputils.APIFunc) http.HandlerFunc {
136 136
 
137 137
 		if err := handlerFunc(ctx, w, r, vars); err != nil {
138 138
 			logrus.Errorf("Handler for %s %s returned error: %v", r.Method, r.URL.Path, err)
139
-			httputils.WriteError(w, err)
139
+			httputils.MakeErrorHandler(err)(w, r)
140 140
 		}
141 141
 	}
142 142
 }
... ...
@@ -172,6 +174,11 @@ func (s *Server) createMux() *mux.Router {
172 172
 		}
173 173
 	}
174 174
 
175
+	err := errors.NewRequestNotFoundError(fmt.Errorf("page not found"))
176
+	notFoundHandler := httputils.MakeErrorHandler(err)
177
+	m.HandleFunc(versionMatcher+"/{path:.*}", notFoundHandler)
178
+	m.NotFoundHandler = notFoundHandler
179
+
175 180
 	return m
176 181
 }
177 182
 
... ...
@@ -131,6 +131,7 @@ This section lists each version from latest to oldest.  Each listing includes a
131 131
 * `POST /images/(name)/tag` no longer has a `force` query parameter.
132 132
 * `GET /images/search` now supports maximum returned search results `limit`.
133 133
 * `POST /containers/{name:.*}/copy` is now removed and errors out starting from this API version.
134
+* API errors are now returned as JSON instead of plain text.
134 135
 
135 136
 ### v1.23 API changes
136 137
 
... ...
@@ -262,4 +263,3 @@ end point now returns the new boolean fields `CpuCfsPeriod`, `CpuCfsQuota`, and
262 262
 * `CgroupParent` can be passed in the host config to setup container cgroups under a specific cgroup.
263 263
 * `POST /build` closing the HTTP request cancels the build
264 264
 * `POST /containers/(id)/exec` includes `Warnings` field to response.
265
-
... ...
@@ -22,9 +22,19 @@ weight=-5
22 22
  - When the client API version is newer than the daemon's, these calls return an HTTP
23 23
    `400 Bad Request` error message.
24 24
 
25
-# 2. Endpoints
25
+# 2. Errors
26 26
 
27
-## 2.1 Containers
27
+The Remote API uses standard HTTP status codes to indicate the success or failure of the API call. The body of the response will be JSON in the following format:
28
+
29
+    {
30
+        "message": "page not found"
31
+    }
32
+
33
+The status codes that are returned for each endpoint are specified in the endpoint documentation below.
34
+
35
+# 3. Endpoints
36
+
37
+## 3.1 Containers
28 38
 
29 39
 ### List containers
30 40
 
... ...
@@ -1504,7 +1514,7 @@ Status Codes:
1504 1504
     - no such file or directory (**path** resource does not exist)
1505 1505
 - **500** – server error
1506 1506
 
1507
-## 2.2 Images
1507
+## 3.2 Images
1508 1508
 
1509 1509
 ### List Images
1510 1510
 
... ...
@@ -2112,7 +2122,7 @@ Status Codes:
2112 2112
 -   **200** – no error
2113 2113
 -   **500** – server error
2114 2114
 
2115
-## 2.3 Misc
2115
+## 3.3 Misc
2116 2116
 
2117 2117
 ### Check auth configuration
2118 2118
 
... ...
@@ -2834,7 +2844,7 @@ Status Codes:
2834 2834
 -   **404** – no such exec instance
2835 2835
 -   **500** - server error
2836 2836
 
2837
-## 2.4 Volumes
2837
+## 3.4 Volumes
2838 2838
 
2839 2839
 ### List volumes
2840 2840
 
... ...
@@ -2972,7 +2982,7 @@ Status Codes
2972 2972
 -   **409** - volume is in use and cannot be removed
2973 2973
 -   **500** - server error
2974 2974
 
2975
-## 2.5 Networks
2975
+## 3.5 Networks
2976 2976
 
2977 2977
 ### List networks
2978 2978
 
... ...
@@ -3296,9 +3306,9 @@ Status Codes
3296 3296
 -   **404** - no such network
3297 3297
 -   **500** - server error
3298 3298
 
3299
-# 3. Going further
3299
+# 4. Going further
3300 3300
 
3301
-## 3.1 Inside `docker run`
3301
+## 4.1 Inside `docker run`
3302 3302
 
3303 3303
 As an example, the `docker run` command line makes the following API calls:
3304 3304
 
... ...
@@ -3316,7 +3326,7 @@ As an example, the `docker run` command line makes the following API calls:
3316 3316
 
3317 3317
 - If in detached mode or only `stdin` is attached, display the container's id.
3318 3318
 
3319
-## 3.2 Hijacking
3319
+## 4.2 Hijacking
3320 3320
 
3321 3321
 In this version of the API, `/attach`, uses hijacking to transport `stdin`,
3322 3322
 `stdout`, and `stderr` on the same socket.
... ...
@@ -3331,7 +3341,7 @@ When Docker daemon detects the `Upgrade` header, it switches its status code
3331 3331
 from **200 OK** to **101 UPGRADED** and resends the same headers.
3332 3332
 
3333 3333
 
3334
-## 3.3 CORS Requests
3334
+## 4.3 CORS Requests
3335 3335
 
3336 3336
 To set cross origin requests to the remote api please give values to
3337 3337
 `--api-cors-header` when running Docker in daemon mode. Set * (asterisk) allows all,
... ...
@@ -85,8 +85,8 @@ func (s *DockerSuite) TestGetContainersWsAttachContainerNotFound(c *check.C) {
85 85
 	status, body, err := sockRequest("GET", "/containers/doesnotexist/attach/ws", nil)
86 86
 	c.Assert(status, checker.Equals, http.StatusNotFound)
87 87
 	c.Assert(err, checker.IsNil)
88
-	expected := "No such container: doesnotexist\n"
89
-	c.Assert(string(body), checker.Contains, expected)
88
+	expected := "No such container: doesnotexist"
89
+	c.Assert(getErrorMessage(c, body), checker.Contains, expected)
90 90
 }
91 91
 
92 92
 func (s *DockerSuite) TestPostContainersAttach(c *check.C) {
... ...
@@ -16,9 +16,10 @@ func (s *DockerSuite) TestAuthApi(c *check.C) {
16 16
 		Password: "no-password",
17 17
 	}
18 18
 
19
-	expected := "Get https://registry-1.docker.io/v2/: unauthorized: incorrect username or password\n"
19
+	expected := "Get https://registry-1.docker.io/v2/: unauthorized: incorrect username or password"
20 20
 	status, body, err := sockRequest("POST", "/auth", config)
21 21
 	c.Assert(err, check.IsNil)
22 22
 	c.Assert(status, check.Equals, http.StatusUnauthorized)
23
-	c.Assert(string(body), checker.Contains, expected, check.Commentf("Expected: %v, got: %v", expected, string(body)))
23
+	msg := getErrorMessage(c, body)
24
+	c.Assert(msg, checker.Contains, expected, check.Commentf("Expected: %v, got: %v", expected, msg))
24 25
 }
... ...
@@ -480,10 +480,10 @@ func (s *DockerSuite) TestContainerApiBadPort(c *check.C) {
480 480
 	jsonData := bytes.NewBuffer(nil)
481 481
 	json.NewEncoder(jsonData).Encode(config)
482 482
 
483
-	status, b, err := sockRequest("POST", "/containers/create", config)
483
+	status, body, err := sockRequest("POST", "/containers/create", config)
484 484
 	c.Assert(err, checker.IsNil)
485 485
 	c.Assert(status, checker.Equals, http.StatusInternalServerError)
486
-	c.Assert(strings.TrimSpace(string(b)), checker.Equals, `Invalid port specification: "aa80"`, check.Commentf("Incorrect error msg: %s", string(b)))
486
+	c.Assert(getErrorMessage(c, body), checker.Equals, `Invalid port specification: "aa80"`, check.Commentf("Incorrect error msg: %s", body))
487 487
 }
488 488
 
489 489
 func (s *DockerSuite) TestContainerApiCreate(c *check.C) {
... ...
@@ -509,12 +509,12 @@ func (s *DockerSuite) TestContainerApiCreate(c *check.C) {
509 509
 func (s *DockerSuite) TestContainerApiCreateEmptyConfig(c *check.C) {
510 510
 	config := map[string]interface{}{}
511 511
 
512
-	status, b, err := sockRequest("POST", "/containers/create", config)
512
+	status, body, err := sockRequest("POST", "/containers/create", config)
513 513
 	c.Assert(err, checker.IsNil)
514 514
 	c.Assert(status, checker.Equals, http.StatusInternalServerError)
515 515
 
516
-	expected := "Config cannot be empty in order to create a container\n"
517
-	c.Assert(string(b), checker.Equals, expected)
516
+	expected := "Config cannot be empty in order to create a container"
517
+	c.Assert(getErrorMessage(c, body), checker.Equals, expected)
518 518
 }
519 519
 
520 520
 func (s *DockerSuite) TestContainerApiCreateMultipleNetworksConfig(c *check.C) {
... ...
@@ -530,14 +530,15 @@ func (s *DockerSuite) TestContainerApiCreateMultipleNetworksConfig(c *check.C) {
530 530
 		},
531 531
 	}
532 532
 
533
-	status, b, err := sockRequest("POST", "/containers/create", config)
533
+	status, body, err := sockRequest("POST", "/containers/create", config)
534 534
 	c.Assert(err, checker.IsNil)
535 535
 	c.Assert(status, checker.Equals, http.StatusBadRequest)
536
+	msg := getErrorMessage(c, body)
536 537
 	// network name order in error message is not deterministic
537
-	c.Assert(string(b), checker.Contains, "Container cannot be connected to network endpoints")
538
-	c.Assert(string(b), checker.Contains, "net1")
539
-	c.Assert(string(b), checker.Contains, "net2")
540
-	c.Assert(string(b), checker.Contains, "net3")
538
+	c.Assert(msg, checker.Contains, "Container cannot be connected to network endpoints")
539
+	c.Assert(msg, checker.Contains, "net1")
540
+	c.Assert(msg, checker.Contains, "net2")
541
+	c.Assert(msg, checker.Contains, "net3")
541 542
 }
542 543
 
543 544
 func (s *DockerSuite) TestContainerApiCreateWithHostName(c *check.C) {
... ...
@@ -997,7 +998,7 @@ func (s *DockerSuite) TestContainerApiDeleteNotExist(c *check.C) {
997 997
 	status, body, err := sockRequest("DELETE", "/containers/doesnotexist", nil)
998 998
 	c.Assert(err, checker.IsNil)
999 999
 	c.Assert(status, checker.Equals, http.StatusNotFound)
1000
-	c.Assert(string(body), checker.Matches, "No such container: doesnotexist\n")
1000
+	c.Assert(getErrorMessage(c, body), checker.Matches, "No such container: doesnotexist")
1001 1001
 }
1002 1002
 
1003 1003
 func (s *DockerSuite) TestContainerApiDeleteForce(c *check.C) {
... ...
@@ -1247,8 +1248,8 @@ func (s *DockerSuite) TestPostContainersCreateWithWrongCpusetValues(c *check.C)
1247 1247
 	status, body, err := sockRequest("POST", "/containers/create?name="+name, c1)
1248 1248
 	c.Assert(err, checker.IsNil)
1249 1249
 	c.Assert(status, checker.Equals, http.StatusInternalServerError)
1250
-	expected := "Invalid value 1-42,, for cpuset cpus\n"
1251
-	c.Assert(string(body), checker.Equals, expected)
1250
+	expected := "Invalid value 1-42,, for cpuset cpus"
1251
+	c.Assert(getErrorMessage(c, body), checker.Equals, expected)
1252 1252
 
1253 1253
 	c2 := struct {
1254 1254
 		Image      string
... ...
@@ -1258,8 +1259,8 @@ func (s *DockerSuite) TestPostContainersCreateWithWrongCpusetValues(c *check.C)
1258 1258
 	status, body, err = sockRequest("POST", "/containers/create?name="+name, c2)
1259 1259
 	c.Assert(err, checker.IsNil)
1260 1260
 	c.Assert(status, checker.Equals, http.StatusInternalServerError)
1261
-	expected = "Invalid value 42-3,1-- for cpuset mems\n"
1262
-	c.Assert(string(body), checker.Equals, expected)
1261
+	expected = "Invalid value 42-3,1-- for cpuset mems"
1262
+	c.Assert(getErrorMessage(c, body), checker.Equals, expected)
1263 1263
 }
1264 1264
 
1265 1265
 func (s *DockerSuite) TestPostContainersCreateShmSizeNegative(c *check.C) {
... ...
@@ -1273,7 +1274,7 @@ func (s *DockerSuite) TestPostContainersCreateShmSizeNegative(c *check.C) {
1273 1273
 	status, body, err := sockRequest("POST", "/containers/create", config)
1274 1274
 	c.Assert(err, check.IsNil)
1275 1275
 	c.Assert(status, check.Equals, http.StatusInternalServerError)
1276
-	c.Assert(string(body), checker.Contains, "SHM size must be greater than 0")
1276
+	c.Assert(getErrorMessage(c, body), checker.Contains, "SHM size must be greater than 0")
1277 1277
 }
1278 1278
 
1279 1279
 func (s *DockerSuite) TestPostContainersCreateShmSizeHostConfigOmitted(c *check.C) {
... ...
@@ -1409,9 +1410,11 @@ func (s *DockerSuite) TestPostContainersCreateWithOomScoreAdjInvalidRange(c *che
1409 1409
 	status, b, err := sockRequest("POST", "/containers/create?name="+name, config)
1410 1410
 	c.Assert(err, check.IsNil)
1411 1411
 	c.Assert(status, check.Equals, http.StatusInternalServerError)
1412
+
1412 1413
 	expected := "Invalid value 1001, range for oom score adj is [-1000, 1000]"
1413
-	if !strings.Contains(string(b), expected) {
1414
-		c.Fatalf("Expected output to contain %q, got %q", expected, string(b))
1414
+	msg := getErrorMessage(c, b)
1415
+	if !strings.Contains(msg, expected) {
1416
+		c.Fatalf("Expected output to contain %q, got %q", expected, msg)
1415 1417
 	}
1416 1418
 
1417 1419
 	config = struct {
... ...
@@ -1423,8 +1426,9 @@ func (s *DockerSuite) TestPostContainersCreateWithOomScoreAdjInvalidRange(c *che
1423 1423
 	c.Assert(err, check.IsNil)
1424 1424
 	c.Assert(status, check.Equals, http.StatusInternalServerError)
1425 1425
 	expected = "Invalid value -1001, range for oom score adj is [-1000, 1000]"
1426
-	if !strings.Contains(string(b), expected) {
1427
-		c.Fatalf("Expected output to contain %q, got %q", expected, string(b))
1426
+	msg = getErrorMessage(c, b)
1427
+	if !strings.Contains(msg, expected) {
1428
+		c.Fatalf("Expected output to contain %q, got %q", expected, msg)
1428 1429
 	}
1429 1430
 }
1430 1431
 
... ...
@@ -2,7 +2,6 @@ package main
2 2
 
3 3
 import (
4 4
 	"net/http"
5
-	"strings"
6 5
 
7 6
 	"github.com/docker/docker/pkg/integration/checker"
8 7
 	"github.com/go-check/check"
... ...
@@ -15,31 +14,31 @@ func (s *DockerSuite) TestApiCreateWithNotExistImage(c *check.C) {
15 15
 		"Volumes": map[string]struct{}{"/tmp": {}},
16 16
 	}
17 17
 
18
-	status, resp, err := sockRequest("POST", "/containers/create?name="+name, config)
18
+	status, body, err := sockRequest("POST", "/containers/create?name="+name, config)
19 19
 	c.Assert(err, check.IsNil)
20 20
 	c.Assert(status, check.Equals, http.StatusNotFound)
21 21
 	expected := "No such image: test456:v1"
22
-	c.Assert(strings.TrimSpace(string(resp)), checker.Contains, expected)
22
+	c.Assert(getErrorMessage(c, body), checker.Contains, expected)
23 23
 
24 24
 	config2 := map[string]interface{}{
25 25
 		"Image":   "test456",
26 26
 		"Volumes": map[string]struct{}{"/tmp": {}},
27 27
 	}
28 28
 
29
-	status, resp, err = sockRequest("POST", "/containers/create?name="+name, config2)
29
+	status, body, err = sockRequest("POST", "/containers/create?name="+name, config2)
30 30
 	c.Assert(err, check.IsNil)
31 31
 	c.Assert(status, check.Equals, http.StatusNotFound)
32 32
 	expected = "No such image: test456:latest"
33
-	c.Assert(strings.TrimSpace(string(resp)), checker.Equals, expected)
33
+	c.Assert(getErrorMessage(c, body), checker.Equals, expected)
34 34
 
35 35
 	config3 := map[string]interface{}{
36 36
 		"Image": "sha256:0cb40641836c461bc97c793971d84d758371ed682042457523e4ae701efeaaaa",
37 37
 	}
38 38
 
39
-	status, resp, err = sockRequest("POST", "/containers/create?name="+name, config3)
39
+	status, body, err = sockRequest("POST", "/containers/create?name="+name, config3)
40 40
 	c.Assert(err, check.IsNil)
41 41
 	c.Assert(status, check.Equals, http.StatusNotFound)
42 42
 	expected = "No such image: sha256:0cb40641836c461bc97c793971d84d758371ed682042457523e4ae701efeaaaa"
43
-	c.Assert(strings.TrimSpace(string(resp)), checker.Equals, expected)
43
+	c.Assert(getErrorMessage(c, body), checker.Equals, expected)
44 44
 
45 45
 }
... ...
@@ -24,7 +24,7 @@ func (s *DockerSuite) TestExecApiCreateNoCmd(c *check.C) {
24 24
 	c.Assert(status, checker.Equals, http.StatusInternalServerError)
25 25
 
26 26
 	comment := check.Commentf("Expected message when creating exec command with no Cmd specified")
27
-	c.Assert(string(body), checker.Contains, "No exec command specified", comment)
27
+	c.Assert(getErrorMessage(c, body), checker.Contains, "No exec command specified", comment)
28 28
 }
29 29
 
30 30
 func (s *DockerSuite) TestExecApiCreateNoValidContentType(c *check.C) {
... ...
@@ -44,7 +44,7 @@ func (s *DockerSuite) TestExecApiCreateNoValidContentType(c *check.C) {
44 44
 	c.Assert(err, checker.IsNil)
45 45
 
46 46
 	comment := check.Commentf("Expected message when creating exec command with invalid Content-Type specified")
47
-	c.Assert(string(b), checker.Contains, "Content-Type specified", comment)
47
+	c.Assert(getErrorMessage(c, b), checker.Contains, "Content-Type specified", comment)
48 48
 }
49 49
 
50 50
 func (s *DockerSuite) TestExecApiCreateContainerPaused(c *check.C) {
... ...
@@ -59,7 +59,7 @@ func (s *DockerSuite) TestExecApiCreateContainerPaused(c *check.C) {
59 59
 	c.Assert(status, checker.Equals, http.StatusConflict)
60 60
 
61 61
 	comment := check.Commentf("Expected message when creating exec command with Container %s is paused", name)
62
-	c.Assert(string(body), checker.Contains, "Container "+name+" is paused, unpause the container before exec", comment)
62
+	c.Assert(getErrorMessage(c, body), checker.Contains, "Container "+name+" is paused, unpause the container before exec", comment)
63 63
 }
64 64
 
65 65
 func (s *DockerSuite) TestExecApiStart(c *check.C) {
... ...
@@ -60,9 +60,7 @@ func (s *DockerSuite) TestLogsApiNoStdoutNorStderr(c *check.C) {
60 60
 	c.Assert(err, checker.IsNil)
61 61
 
62 62
 	expected := "Bad parameters: you must choose at least one stream"
63
-	if !bytes.Contains(body, []byte(expected)) {
64
-		c.Fatalf("Expected %s, got %s", expected, string(body[:]))
65
-	}
63
+	c.Assert(getErrorMessage(c, body), checker.Contains, expected)
66 64
 }
67 65
 
68 66
 // Regression test for #12704
... ...
@@ -40,5 +40,5 @@ func (s *DockerSuite) TestResizeApiResponseWhenContainerNotStarted(c *check.C) {
40 40
 	c.Assert(status, check.Equals, http.StatusInternalServerError)
41 41
 	c.Assert(err, check.IsNil)
42 42
 
43
-	c.Assert(string(body), checker.Contains, "is not running", check.Commentf("resize should fail with message 'Container is not running'"))
43
+	c.Assert(getErrorMessage(c, body), checker.Contains, "is not running", check.Commentf("resize should fail with message 'Container is not running'"))
44 44
 }
... ...
@@ -60,7 +60,7 @@ func (s *DockerSuite) TestApiClientVersionNewerThanServer(c *check.C) {
60 60
 	c.Assert(err, checker.IsNil)
61 61
 	c.Assert(status, checker.Equals, http.StatusBadRequest)
62 62
 	expected := fmt.Sprintf("client is newer than server (client API version: %s, server API version: %s)", version, api.DefaultVersion)
63
-	c.Assert(strings.TrimSpace(string(body)), checker.Equals, expected)
63
+	c.Assert(getErrorMessage(c, body), checker.Equals, expected)
64 64
 }
65 65
 
66 66
 func (s *DockerSuite) TestApiClientVersionOldNotSupported(c *check.C) {
... ...
@@ -99,3 +99,44 @@ func (s *DockerSuite) TestApiDockerApiVersion(c *check.C) {
99 99
 		c.Fatalf("Out didn't have 'xxx' for the API version, had:\n%s", out)
100 100
 	}
101 101
 }
102
+
103
+func (s *DockerSuite) TestApiErrorJSON(c *check.C) {
104
+	httpResp, body, err := sockRequestRaw("POST", "/containers/create", strings.NewReader(`{}`), "application/json")
105
+	c.Assert(err, checker.IsNil)
106
+	c.Assert(httpResp.StatusCode, checker.Equals, http.StatusInternalServerError)
107
+	c.Assert(httpResp.Header.Get("Content-Type"), checker.Equals, "application/json")
108
+	b, err := readBody(body)
109
+	c.Assert(err, checker.IsNil)
110
+	c.Assert(getErrorMessage(c, b), checker.Equals, "Config cannot be empty in order to create a container")
111
+}
112
+
113
+func (s *DockerSuite) TestApiErrorPlainText(c *check.C) {
114
+	httpResp, body, err := sockRequestRaw("POST", "/v1.23/containers/create", strings.NewReader(`{}`), "application/json")
115
+	c.Assert(err, checker.IsNil)
116
+	c.Assert(httpResp.StatusCode, checker.Equals, http.StatusInternalServerError)
117
+	c.Assert(httpResp.Header.Get("Content-Type"), checker.Contains, "text/plain")
118
+	b, err := readBody(body)
119
+	c.Assert(err, checker.IsNil)
120
+	c.Assert(strings.TrimSpace(string(b)), checker.Equals, "Config cannot be empty in order to create a container")
121
+}
122
+
123
+func (s *DockerSuite) TestApiErrorNotFoundJSON(c *check.C) {
124
+	// 404 is a different code path to normal errors, so test separately
125
+	httpResp, body, err := sockRequestRaw("GET", "/notfound", nil, "application/json")
126
+	c.Assert(err, checker.IsNil)
127
+	c.Assert(httpResp.StatusCode, checker.Equals, http.StatusNotFound)
128
+	c.Assert(httpResp.Header.Get("Content-Type"), checker.Equals, "application/json")
129
+	b, err := readBody(body)
130
+	c.Assert(err, checker.IsNil)
131
+	c.Assert(getErrorMessage(c, b), checker.Equals, "page not found")
132
+}
133
+
134
+func (s *DockerSuite) TestApiErrorNotFoundPlainText(c *check.C) {
135
+	httpResp, body, err := sockRequestRaw("GET", "/v1.23/notfound", nil, "application/json")
136
+	c.Assert(err, checker.IsNil)
137
+	c.Assert(httpResp.StatusCode, checker.Equals, http.StatusNotFound)
138
+	c.Assert(httpResp.Header.Get("Content-Type"), checker.Contains, "text/plain")
139
+	b, err := readBody(body)
140
+	c.Assert(err, checker.IsNil)
141
+	c.Assert(strings.TrimSpace(string(b)), checker.Equals, "page not found")
142
+}
... ...
@@ -1507,3 +1507,10 @@ func waitForGoroutines(expected int) error {
1507 1507
 		}
1508 1508
 	}
1509 1509
 }
1510
+
1511
+// getErrorMessage returns the error message from an error API response
1512
+func getErrorMessage(c *check.C, body []byte) string {
1513
+	var resp types.ErrorResponse
1514
+	c.Assert(json.Unmarshal(body, &resp), check.IsNil)
1515
+	return strings.TrimSpace(resp.Message)
1516
+}