Add more specific error handling and handle generic errors more effectively

The client library should type more generic errors from the server.
This commit is contained in:
Clayton Coleman
2015-03-23 22:56:22 -04:00
parent e5914915b8
commit 30358e8b04
11 changed files with 168 additions and 14 deletions

View File

@@ -22,6 +22,7 @@ import (
"fmt"
"io"
"io/ioutil"
"mime"
"net/http"
"net/url"
"path"
@@ -621,12 +622,15 @@ func (r *Request) transformResponse(body []byte, resp *http.Response, req *http.
// no-op, we've been upgraded
case resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusPartialContent:
if !isStatusResponse {
var err error
err = &UnexpectedStatusError{
var err error = &UnexpectedStatusError{
Request: req,
Response: resp,
Body: string(body),
}
message := "unknown"
if isTextResponse(resp) {
message = strings.TrimSpace(string(body))
}
// TODO: handle other error classes we know about
switch resp.StatusCode {
case http.StatusConflict:
@@ -638,7 +642,21 @@ func (r *Request) transformResponse(body []byte, resp *http.Response, req *http.
case http.StatusNotFound:
err = errors.NewNotFound(r.resource, r.resourceName)
case http.StatusBadRequest:
err = errors.NewBadRequest(err.Error())
err = errors.NewBadRequest(message)
case http.StatusUnauthorized:
err = errors.NewUnauthorized(message)
case http.StatusForbidden:
err = errors.NewForbidden(r.resource, r.resourceName, err)
case errors.StatusUnprocessableEntity:
err = errors.NewInvalid(r.resource, r.resourceName, nil)
case errors.StatusServerTimeout:
retryAfter, _ := retryAfter(resp)
err = errors.NewServerTimeout(r.resource, r.verb, retryAfter)
case errors.StatusTooManyRequests:
retryAfter, _ := retryAfter(resp)
err = errors.NewServerTimeout(r.resource, r.verb, retryAfter)
case http.StatusInternalServerError:
err = errors.NewInternalError(fmt.Errorf(message))
}
return nil, false, err
}
@@ -657,6 +675,30 @@ func (r *Request) transformResponse(body []byte, resp *http.Response, req *http.
return body, created, nil
}
// isTextResponse returns true if the response appears to be a textual media type.
func isTextResponse(resp *http.Response) bool {
contentType := resp.Header.Get("Content-Type")
if len(contentType) == 0 {
return true
}
media, _, err := mime.ParseMediaType(contentType)
if err != nil {
return false
}
return strings.HasPrefix(media, "text/")
}
// retryAfter returns the value of the Retry-After header and true, or 0 and false if
// the header was missing or not a valid number.
func retryAfter(resp *http.Response) (int, bool) {
if h := resp.Header.Get("Retry-After"); len(h) > 0 {
if i, err := strconv.Atoi(h); err == nil {
return i, true
}
}
return 0, false
}
// Result contains the result of calling Request.Do().
type Result struct {
body []byte

View File

@@ -222,6 +222,7 @@ func TestTransformResponse(t *testing.T) {
Data []byte
Created bool
Error bool
ErrFn func(err error) bool
}{
{Response: &http.Response{StatusCode: 200}, Data: []byte{}},
{Response: &http.Response{StatusCode: 201}, Data: []byte{}, Created: true},
@@ -230,6 +231,30 @@ func TestTransformResponse(t *testing.T) {
{Response: &http.Response{StatusCode: 422}, Error: true},
{Response: &http.Response{StatusCode: 409}, Error: true},
{Response: &http.Response{StatusCode: 404}, Error: true},
{Response: &http.Response{StatusCode: 401}, Error: true},
{
Response: &http.Response{
StatusCode: 401,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: ioutil.NopCloser(bytes.NewReader(invalid)),
},
Error: true,
ErrFn: func(err error) bool {
return err.Error() != "aaaaa" && apierrors.IsUnauthorized(err)
},
},
{
Response: &http.Response{
StatusCode: 401,
Header: http.Header{"Content-Type": []string{"text/any"}},
Body: ioutil.NopCloser(bytes.NewReader(invalid)),
},
Error: true,
ErrFn: func(err error) bool {
return err.Error() == "aaaaa" && apierrors.IsUnauthorized(err)
},
},
{Response: &http.Response{StatusCode: 403}, Error: true},
{Response: &http.Response{StatusCode: 200, Body: ioutil.NopCloser(bytes.NewReader(invalid))}, Data: invalid},
{Response: &http.Response{StatusCode: 200, Body: ioutil.NopCloser(bytes.NewReader(invalid))}, Data: invalid},
}
@@ -246,6 +271,18 @@ func TestTransformResponse(t *testing.T) {
hasErr := err != nil
if hasErr != test.Error {
t.Errorf("%d: unexpected error: %t %v", i, test.Error, err)
} else if hasErr && test.Response.StatusCode > 399 {
status, ok := err.(APIStatus)
if !ok {
t.Errorf("%d: response should have been transformable into APIStatus: %v", i, err)
continue
}
if status.Status().Code != test.Response.StatusCode {
t.Errorf("%d: status code did not match response: %#v", i, status.Status())
}
}
if test.ErrFn != nil && !test.ErrFn(err) {
t.Errorf("%d: error function did not match: %v", i, err)
}
if !(test.Data == nil && response == nil) && !api.Semantic.DeepDerivative(test.Data, response) {
t.Errorf("%d: unexpected response: %#v %#v", i, test.Data, response)

View File

@@ -225,7 +225,13 @@ func TestDoRequestSuccess(t *testing.T) {
}
func TestDoRequestFailed(t *testing.T) {
status := &api.Status{Status: api.StatusFailure, Reason: api.StatusReasonInvalid, Details: &api.StatusDetails{ID: "test", Kind: "test"}}
status := &api.Status{
Code: http.StatusNotFound,
Status: api.StatusFailure,
Reason: api.StatusReasonNotFound,
Message: " \"\" not found",
Details: &api.StatusDetails{},
}
expectedBody, _ := latest.Codec.Encode(status)
fakeHandler := util.FakeHandler{
StatusCode: 404,
@@ -253,7 +259,7 @@ func TestDoRequestFailed(t *testing.T) {
}
actual := ss.Status()
if !reflect.DeepEqual(status, &actual) {
t.Errorf("Unexpected mis-match. Expected %#v. Saw %#v", status, actual)
t.Errorf("Unexpected mis-match: %s", util.ObjectDiff(status, &actual))
}
}