Use response content-type on restclient errors
Also allow a new AcceptContentTypes field to allow the client to ask for a fallback serialization when getting responses from the server. This allows a new client to ask for protobuf and JSON, falling back to JSON when necessary. The changes to request.go allow error responses from non-JSON servers to be properly decoded.
This commit is contained in:
		@@ -131,6 +131,9 @@ type TLSClientConfig struct {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ContentConfig struct {
 | 
					type ContentConfig struct {
 | 
				
			||||||
 | 
						// AcceptContentTypes specifies the types the client will accept and is optional.
 | 
				
			||||||
 | 
						// If not set, ContentType will be used to define the Accept header
 | 
				
			||||||
 | 
						AcceptContentTypes string
 | 
				
			||||||
	// ContentType specifies the wire format used to communicate with the server.
 | 
						// ContentType specifies the wire format used to communicate with the server.
 | 
				
			||||||
	// This value will be set as the Accept header on requests made to the server, and
 | 
						// This value will be set as the Accept header on requests made to the server, and
 | 
				
			||||||
	// as the default content type on any object sent to the server. If not set,
 | 
						// as the default content type on any object sent to the server. If not set,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,6 +18,7 @@ package restclient
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
						"bytes"
 | 
				
			||||||
 | 
						"encoding/hex"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"io/ioutil"
 | 
						"io/ioutil"
 | 
				
			||||||
@@ -143,7 +144,10 @@ func NewRequest(client HTTPClient, verb string, baseURL *url.URL, versionedAPIPa
 | 
				
			|||||||
		backoffMgr:  backoff,
 | 
							backoffMgr:  backoff,
 | 
				
			||||||
		throttle:    throttle,
 | 
							throttle:    throttle,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if len(content.ContentType) > 0 {
 | 
						switch {
 | 
				
			||||||
 | 
						case len(content.AcceptContentTypes) > 0:
 | 
				
			||||||
 | 
							r.SetHeader("Accept", content.AcceptContentTypes)
 | 
				
			||||||
 | 
						case len(content.ContentType) > 0:
 | 
				
			||||||
		r.SetHeader("Accept", content.ContentType+", */*")
 | 
							r.SetHeader("Accept", content.ContentType+", */*")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return r
 | 
						return r
 | 
				
			||||||
@@ -888,16 +892,49 @@ func (r *Request) transformResponse(resp *http.Response, req *http.Request) Resu
 | 
				
			|||||||
			body = data
 | 
								body = data
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	glog.V(8).Infof("Response Body: %#v", string(body))
 | 
					
 | 
				
			||||||
 | 
						if glog.V(8) {
 | 
				
			||||||
 | 
							switch {
 | 
				
			||||||
 | 
							case bytes.IndexFunc(body, func(r rune) bool { return r < 0x0a }) != -1:
 | 
				
			||||||
 | 
								glog.Infof("Response Body:\n%s", hex.Dump(body))
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								glog.Infof("Response Body: %s", string(body))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// verify the content type is accurate
 | 
				
			||||||
 | 
						contentType := resp.Header.Get("Content-Type")
 | 
				
			||||||
 | 
						decoder := r.serializers.Decoder
 | 
				
			||||||
 | 
						if len(contentType) > 0 && (decoder == nil || (len(r.content.ContentType) > 0 && contentType != r.content.ContentType)) {
 | 
				
			||||||
 | 
							mediaType, params, err := mime.ParseMediaType(contentType)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return Result{err: errors.NewInternalError(err)}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							decoder, err = r.serializers.RenegotiatedDecoder(mediaType, params)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								// if we fail to negotiate a decoder, treat this as an unstructured error
 | 
				
			||||||
 | 
								switch {
 | 
				
			||||||
 | 
								case resp.StatusCode == http.StatusSwitchingProtocols:
 | 
				
			||||||
 | 
									// no-op, we've been upgraded
 | 
				
			||||||
 | 
								case resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusPartialContent:
 | 
				
			||||||
 | 
									return Result{err: r.transformUnstructuredResponseError(resp, req, body)}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return Result{
 | 
				
			||||||
 | 
									body:        body,
 | 
				
			||||||
 | 
									contentType: contentType,
 | 
				
			||||||
 | 
									statusCode:  resp.StatusCode,
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Did the server give us a status response?
 | 
						// Did the server give us a status response?
 | 
				
			||||||
	isStatusResponse := false
 | 
						isStatusResponse := false
 | 
				
			||||||
 | 
						status := &unversioned.Status{}
 | 
				
			||||||
	// Because release-1.1 server returns Status with empty APIVersion at paths
 | 
						// Because release-1.1 server returns Status with empty APIVersion at paths
 | 
				
			||||||
	// to the Extensions resources, we need to use DecodeInto here to provide
 | 
						// to the Extensions resources, we need to use DecodeInto here to provide
 | 
				
			||||||
	// default groupVersion, otherwise a status response won't be correctly
 | 
						// default groupVersion, otherwise a status response won't be correctly
 | 
				
			||||||
	// decoded.
 | 
						// decoded.
 | 
				
			||||||
	status := &unversioned.Status{}
 | 
						err := runtime.DecodeInto(decoder, body, status)
 | 
				
			||||||
	err := runtime.DecodeInto(r.serializers.Decoder, body, status)
 | 
					 | 
				
			||||||
	if err == nil && len(status.Status) > 0 {
 | 
						if err == nil && len(status.Status) > 0 {
 | 
				
			||||||
		isStatusResponse = true
 | 
							isStatusResponse = true
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -919,25 +956,6 @@ func (r *Request) transformResponse(resp *http.Response, req *http.Request) Resu
 | 
				
			|||||||
		return Result{err: errors.FromObject(status)}
 | 
							return Result{err: errors.FromObject(status)}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	contentType := resp.Header.Get("Content-Type")
 | 
					 | 
				
			||||||
	var decoder runtime.Decoder
 | 
					 | 
				
			||||||
	if contentType == r.content.ContentType {
 | 
					 | 
				
			||||||
		decoder = r.serializers.Decoder
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		mediaType, params, err := mime.ParseMediaType(contentType)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return Result{err: errors.NewInternalError(err)}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		decoder, err = r.serializers.RenegotiatedDecoder(mediaType, params)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return Result{
 | 
					 | 
				
			||||||
				body:        body,
 | 
					 | 
				
			||||||
				contentType: contentType,
 | 
					 | 
				
			||||||
				statusCode:  resp.StatusCode,
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return Result{
 | 
						return Result{
 | 
				
			||||||
		body:        body,
 | 
							body:        body,
 | 
				
			||||||
		contentType: contentType,
 | 
							contentType: contentType,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,6 +19,7 @@ package restclient
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
						"bytes"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"io/ioutil"
 | 
						"io/ioutil"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
@@ -283,6 +284,9 @@ func defaultSerializers() Serializers {
 | 
				
			|||||||
		Decoder:             testapi.Default.Codec(),
 | 
							Decoder:             testapi.Default.Codec(),
 | 
				
			||||||
		StreamingSerializer: testapi.Default.Codec(),
 | 
							StreamingSerializer: testapi.Default.Codec(),
 | 
				
			||||||
		Framer:              runtime.DefaultFramer,
 | 
							Framer:              runtime.DefaultFramer,
 | 
				
			||||||
 | 
							RenegotiatedDecoder: func(contentType string, params map[string]string) (runtime.Decoder, error) {
 | 
				
			||||||
 | 
								return testapi.Default.Codec(), nil
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -413,6 +417,161 @@ func TestTransformResponse(t *testing.T) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type renegotiator struct {
 | 
				
			||||||
 | 
						called      bool
 | 
				
			||||||
 | 
						contentType string
 | 
				
			||||||
 | 
						params      map[string]string
 | 
				
			||||||
 | 
						decoder     runtime.Decoder
 | 
				
			||||||
 | 
						err         error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (r *renegotiator) invoke(contentType string, params map[string]string) (runtime.Decoder, error) {
 | 
				
			||||||
 | 
						r.called = true
 | 
				
			||||||
 | 
						r.contentType = contentType
 | 
				
			||||||
 | 
						r.params = params
 | 
				
			||||||
 | 
						return r.decoder, r.err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestTransformResponseNegotiate(t *testing.T) {
 | 
				
			||||||
 | 
						invalid := []byte("aaaaa")
 | 
				
			||||||
 | 
						uri, _ := url.Parse("http://localhost")
 | 
				
			||||||
 | 
						testCases := []struct {
 | 
				
			||||||
 | 
							Response *http.Response
 | 
				
			||||||
 | 
							Data     []byte
 | 
				
			||||||
 | 
							Created  bool
 | 
				
			||||||
 | 
							Error    bool
 | 
				
			||||||
 | 
							ErrFn    func(err error) bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							ContentType       string
 | 
				
			||||||
 | 
							Called            bool
 | 
				
			||||||
 | 
							ExpectContentType string
 | 
				
			||||||
 | 
							Decoder           runtime.Decoder
 | 
				
			||||||
 | 
							NegotiateErr      error
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								ContentType: "application/json",
 | 
				
			||||||
 | 
								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)
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								ContentType: "application/json",
 | 
				
			||||||
 | 
								Response: &http.Response{
 | 
				
			||||||
 | 
									StatusCode: 401,
 | 
				
			||||||
 | 
									Header:     http.Header{"Content-Type": []string{"application/protobuf"}},
 | 
				
			||||||
 | 
									Body:       ioutil.NopCloser(bytes.NewReader(invalid)),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								Decoder: testapi.Default.Codec(),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								Called:            true,
 | 
				
			||||||
 | 
								ExpectContentType: "application/protobuf",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								Error: true,
 | 
				
			||||||
 | 
								ErrFn: func(err error) bool {
 | 
				
			||||||
 | 
									return err.Error() != "aaaaa" && apierrors.IsUnauthorized(err)
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								ContentType: "application/json",
 | 
				
			||||||
 | 
								Response: &http.Response{
 | 
				
			||||||
 | 
									StatusCode: 500,
 | 
				
			||||||
 | 
									Header:     http.Header{"Content-Type": []string{"application/,others"}},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								Decoder: testapi.Default.Codec(),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								Error: true,
 | 
				
			||||||
 | 
								ErrFn: func(err error) bool {
 | 
				
			||||||
 | 
									return err.Error() == "Internal error occurred: mime: expected token after slash" && err.(apierrors.APIStatus).Status().Code == 500
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								// no negotiation when no content type specified
 | 
				
			||||||
 | 
								Response: &http.Response{
 | 
				
			||||||
 | 
									StatusCode: 200,
 | 
				
			||||||
 | 
									Header:     http.Header{"Content-Type": []string{"text/any"}},
 | 
				
			||||||
 | 
									Body:       ioutil.NopCloser(bytes.NewReader(invalid)),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								Decoder: testapi.Default.Codec(),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								// no negotiation when no response content type specified
 | 
				
			||||||
 | 
								ContentType: "text/any",
 | 
				
			||||||
 | 
								Response: &http.Response{
 | 
				
			||||||
 | 
									StatusCode: 200,
 | 
				
			||||||
 | 
									Body:       ioutil.NopCloser(bytes.NewReader(invalid)),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								Decoder: testapi.Default.Codec(),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								// unrecognized content type is not handled
 | 
				
			||||||
 | 
								ContentType: "application/json",
 | 
				
			||||||
 | 
								Response: &http.Response{
 | 
				
			||||||
 | 
									StatusCode: 404,
 | 
				
			||||||
 | 
									Header:     http.Header{"Content-Type": []string{"application/unrecognized"}},
 | 
				
			||||||
 | 
									Body:       ioutil.NopCloser(bytes.NewReader(invalid)),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								Decoder: testapi.Default.Codec(),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								NegotiateErr:      fmt.Errorf("aaaa"),
 | 
				
			||||||
 | 
								Called:            true,
 | 
				
			||||||
 | 
								ExpectContentType: "application/unrecognized",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								Error: true,
 | 
				
			||||||
 | 
								ErrFn: func(err error) bool {
 | 
				
			||||||
 | 
									return err.Error() != "aaaaa" && apierrors.IsNotFound(err)
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for i, test := range testCases {
 | 
				
			||||||
 | 
							serializers := defaultSerializers()
 | 
				
			||||||
 | 
							negotiator := &renegotiator{
 | 
				
			||||||
 | 
								decoder: test.Decoder,
 | 
				
			||||||
 | 
								err:     test.NegotiateErr,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							serializers.RenegotiatedDecoder = negotiator.invoke
 | 
				
			||||||
 | 
							contentConfig := defaultContentConfig()
 | 
				
			||||||
 | 
							contentConfig.ContentType = test.ContentType
 | 
				
			||||||
 | 
							r := NewRequest(nil, "", uri, "", contentConfig, serializers, nil, nil)
 | 
				
			||||||
 | 
							if test.Response.Body == nil {
 | 
				
			||||||
 | 
								test.Response.Body = ioutil.NopCloser(bytes.NewReader([]byte{}))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							result := r.transformResponse(test.Response, &http.Request{})
 | 
				
			||||||
 | 
							_, err := result.body, result.err
 | 
				
			||||||
 | 
							hasErr := err != nil
 | 
				
			||||||
 | 
							if hasErr != test.Error {
 | 
				
			||||||
 | 
								t.Errorf("%d: unexpected error: %t %v", i, test.Error, err)
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							} else if hasErr && test.Response.StatusCode > 399 {
 | 
				
			||||||
 | 
								status, ok := err.(apierrors.APIStatus)
 | 
				
			||||||
 | 
								if !ok {
 | 
				
			||||||
 | 
									t.Errorf("%d: response should have been transformable into APIStatus: %v", i, err)
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if int(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 negotiator.called != test.Called {
 | 
				
			||||||
 | 
								t.Errorf("%d: negotiator called %t != %t", i, negotiator.called, test.Called)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !test.Called {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if negotiator.contentType != test.ExpectContentType {
 | 
				
			||||||
 | 
								t.Errorf("%d: unexpected content type: %s", i, negotiator.contentType)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestTransformUnstructuredError(t *testing.T) {
 | 
					func TestTransformUnstructuredError(t *testing.T) {
 | 
				
			||||||
	testCases := []struct {
 | 
						testCases := []struct {
 | 
				
			||||||
		Req *http.Request
 | 
							Req *http.Request
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -58,6 +58,7 @@ func NewClient(conf *restclient.Config) (*Client, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// TODO: it's questionable that this should be using anything other than unstructured schema and JSON
 | 
						// TODO: it's questionable that this should be using anything other than unstructured schema and JSON
 | 
				
			||||||
	conf.ContentType = runtime.ContentTypeJSON
 | 
						conf.ContentType = runtime.ContentTypeJSON
 | 
				
			||||||
 | 
						conf.AcceptContentTypes = runtime.ContentTypeJSON
 | 
				
			||||||
	streamingInfo, _ := api.Codecs.StreamingSerializerForMediaType("application/json;stream=watch", nil)
 | 
						streamingInfo, _ := api.Codecs.StreamingSerializerForMediaType("application/json;stream=watch", nil)
 | 
				
			||||||
	conf.NegotiatedSerializer = serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: codec}, streamingInfo)
 | 
						conf.NegotiatedSerializer = serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: codec}, streamingInfo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user