From 09205e8f6711a776499a14cf8adc6bd380db5d81 Mon Sep 17 00:00:00 2001 From: Joel Hendrix Date: Tue, 25 Jun 2019 09:40:17 -0700 Subject: [PATCH] v12.2.0 (#411) * Deserialize additionalInfo in ARM error * Allow a new authorizer to be created from a configuration file by specifying a resource instead of a base url. This enables resource like KeyVault and Container Registry to use an authorizer configured from a configuration file. * [WIP] Using the Context from the timeout if provided (#315) * Using the timeout from the context if available - Makes PollingDuration optional * Renaming the registration start time * Making PollingDuration not a pointer * fixing a broken reference * Add NewAuthorizerFromCli method which uses Azure CLI to obtain a token for the currently logged in user, for local development scenarios. (#316) * Adding User assigned identity support for the MSIConfig authorizor (#332) * Adding ByteSlicePtr (#399) * Adding a new `WithXML` method (#402) * Add HTTP status code response helpers (#403) Added IsHTTPStatus() and HasHTTPStatus() methods to autorest.Response * adding a new preparer for `MERGE` used in the Storage API's (#406) * New Preparer/Responder for `Unmarshalling Bytes` (#407) * New Preparer: WithBytes * New Responder: `ByUnmarshallingBytes` * Reusing the bytes, rather than copying them * Fixing the broken test / switching to read the bytes directly * Support HTTP-Date in Retry-After header (#410) RFC specifies Retry-After header can be integer value expressing seconds or an HTTP-Date indicating when to try again. Removed superfluous check for HTTP status code. * v12.2.0 --- CHANGELOG.md | 12 +++++++++ autorest/client.go | 16 ++++++++++++ autorest/client_test.go | 31 +++++++++++++++++++++++ autorest/mocks/helpers.go | 13 ++++++++++ autorest/mocks/mocks.go | 8 ++++++ autorest/preparer.go | 45 +++++++++++++++++++++++++++++++++ autorest/preparer_test.go | 51 ++++++++++++++++++++++++++++++++++++++ autorest/responder.go | 19 ++++++++++++++ autorest/responder_test.go | 19 ++++++++++++++ autorest/sender.go | 18 ++++++++++---- autorest/sender_test.go | 27 ++++++++++++++++++++ autorest/version.go | 2 +- 12 files changed, 255 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dc767c2a..1b753704c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # CHANGELOG +## v12.2.0 + +### New Features + +- Added `autorest.WithXML`, `autorest.AsMerge`, `autorest.WithBytes` preparer decorators. +- Added `autorest.ByUnmarshallingBytes` response decorator. +- Added `Response.IsHTTPStatus` and `Response.HasHTTPStatus` helper methods for inspecting HTTP status code in `autorest.Response` types. + +### Bug Fixes + +- `autorest.DelayWithRetryAfter` now supports HTTP-Dates in the `Retry-After` header and is not limited to just 429 status codes. + ## v12.1.0 ### New Features diff --git a/autorest/client.go b/autorest/client.go index cfc7ed757..92da6adb2 100644 --- a/autorest/client.go +++ b/autorest/client.go @@ -73,6 +73,22 @@ type Response struct { *http.Response `json:"-"` } +// IsHTTPStatus returns true if the returned HTTP status code matches the provided status code. +// If there was no response (i.e. the underlying http.Response is nil) the return value is false. +func (r Response) IsHTTPStatus(statusCode int) bool { + if r.Response == nil { + return false + } + return r.Response.StatusCode == statusCode +} + +// HasHTTPStatus returns true if the returned HTTP status code matches one of the provided status codes. +// If there was no response (i.e. the underlying http.Response is nil) or not status codes are provided +// the return value is false. +func (r Response) HasHTTPStatus(statusCodes ...int) bool { + return ResponseHasStatusCode(r.Response, statusCodes...) +} + // LoggingInspector implements request and response inspectors that log the full request and // response to a supplied log. type LoggingInspector struct { diff --git a/autorest/client_test.go b/autorest/client_test.go index 9ae7ed608..ca896714d 100644 --- a/autorest/client_test.go +++ b/autorest/client_test.go @@ -435,6 +435,37 @@ func TestCookies(t *testing.T) { } } +func TestResponseIsHTTPStatus(t *testing.T) { + r := Response{} + if r.IsHTTPStatus(http.StatusBadRequest) { + t.Fatal("autorest: expected false for nil response") + } + r.Response = &http.Response{StatusCode: http.StatusOK} + if r.IsHTTPStatus(http.StatusBadRequest) { + t.Fatal("autorest: expected false") + } + if !r.IsHTTPStatus(http.StatusOK) { + t.Fatal("autorest: expected true") + } +} + +func TestResponseHasHTTPStatus(t *testing.T) { + r := Response{} + if r.HasHTTPStatus(http.StatusBadRequest, http.StatusInternalServerError) { + t.Fatal("autorest: expected false for nil response") + } + r.Response = &http.Response{StatusCode: http.StatusAccepted} + if r.HasHTTPStatus(http.StatusBadRequest, http.StatusInternalServerError) { + t.Fatal("autorest: expected false") + } + if !r.HasHTTPStatus(http.StatusOK, http.StatusCreated, http.StatusAccepted) { + t.Fatal("autorest: expected true") + } + if r.HasHTTPStatus() { + t.Fatal("autorest: expected false for no status codes") + } +} + func randomString(n int) string { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" r := rand.New(rand.NewSource(time.Now().UTC().UnixNano())) diff --git a/autorest/mocks/helpers.go b/autorest/mocks/helpers.go index e2aab19c9..f8b2f8b1a 100644 --- a/autorest/mocks/helpers.go +++ b/autorest/mocks/helpers.go @@ -90,6 +90,19 @@ func NewResponse() *http.Response { return NewResponseWithContent("") } +// NewResponseWithBytes instantiates a new response with the passed bytes as the body content. +func NewResponseWithBytes(input []byte) *http.Response { + return &http.Response{ + Status: "200 OK", + StatusCode: 200, + Proto: "HTTP/1.0", + ProtoMajor: 1, + ProtoMinor: 0, + Body: NewBodyWithBytes(input), + Request: NewRequest(), + } +} + // NewResponseWithContent instantiates a new response with the passed string as the body content. func NewResponseWithContent(c string) *http.Response { return &http.Response{ diff --git a/autorest/mocks/mocks.go b/autorest/mocks/mocks.go index cc1579e0f..a00a27dd8 100644 --- a/autorest/mocks/mocks.go +++ b/autorest/mocks/mocks.go @@ -37,6 +37,14 @@ func NewBody(s string) *Body { return (&Body{s: s}).reset() } +// NewBodyWithBytes creates a new instance of Body. +func NewBodyWithBytes(b []byte) *Body { + return &Body{ + b: b, + isOpen: true, + } +} + // NewBodyClose creates a new instance of Body. func NewBodyClose(s string) *Body { return &Body{s: s} diff --git a/autorest/preparer.go b/autorest/preparer.go index 6d67bd733..6e955a4ba 100644 --- a/autorest/preparer.go +++ b/autorest/preparer.go @@ -17,6 +17,7 @@ package autorest import ( "bytes" "encoding/json" + "encoding/xml" "fmt" "io" "io/ioutil" @@ -190,6 +191,9 @@ func AsGet() PrepareDecorator { return WithMethod("GET") } // AsHead returns a PrepareDecorator that sets the HTTP method to HEAD. func AsHead() PrepareDecorator { return WithMethod("HEAD") } +// AsMerge returns a PrepareDecorator that sets the HTTP method to MERGE. +func AsMerge() PrepareDecorator { return WithMethod("MERGE") } + // AsOptions returns a PrepareDecorator that sets the HTTP method to OPTIONS. func AsOptions() PrepareDecorator { return WithMethod("OPTIONS") } @@ -225,6 +229,25 @@ func WithBaseURL(baseURL string) PrepareDecorator { } } +// WithBytes returns a PrepareDecorator that takes a list of bytes +// which passes the bytes directly to the body +func WithBytes(input *[]byte) PrepareDecorator { + return func(p Preparer) Preparer { + return PreparerFunc(func(r *http.Request) (*http.Request, error) { + r, err := p.Prepare(r) + if err == nil { + if input == nil { + return r, fmt.Errorf("Input Bytes was nil") + } + + r.ContentLength = int64(len(*input)) + r.Body = ioutil.NopCloser(bytes.NewReader(*input)) + } + return r, err + }) + } +} + // WithCustomBaseURL returns a PrepareDecorator that replaces brace-enclosed keys within the // request base URL (i.e., http.Request.URL) with the corresponding values from the passed map. func WithCustomBaseURL(baseURL string, urlParameters map[string]interface{}) PrepareDecorator { @@ -377,6 +400,28 @@ func WithJSON(v interface{}) PrepareDecorator { } } +// WithXML returns a PrepareDecorator that encodes the data passed as XML into the body of the +// request and sets the Content-Length header. +func WithXML(v interface{}) PrepareDecorator { + return func(p Preparer) Preparer { + return PreparerFunc(func(r *http.Request) (*http.Request, error) { + r, err := p.Prepare(r) + if err == nil { + b, err := xml.Marshal(v) + if err == nil { + // we have to tack on an XML header + withHeader := xml.Header + string(b) + bytesWithHeader := []byte(withHeader) + + r.ContentLength = int64(len(bytesWithHeader)) + r.Body = ioutil.NopCloser(bytes.NewReader(bytesWithHeader)) + } + } + return r, err + }) + } +} + // WithPath returns a PrepareDecorator that adds the supplied path to the request URL. If the path // is absolute (that is, it begins with a "/"), it replaces the existing path. func WithPath(path string) PrepareDecorator { diff --git a/autorest/preparer_test.go b/autorest/preparer_test.go index b4d19967b..3ace6554f 100644 --- a/autorest/preparer_test.go +++ b/autorest/preparer_test.go @@ -163,6 +163,30 @@ func ExampleWithBaseURL_second() { // Output: parse :: missing protocol scheme } +// Create a request whose Body is a byte array +func TestWithBytes(t *testing.T) { + input := []byte{41, 82, 109} + + r, err := Prepare(&http.Request{}, + WithBytes(&input)) + if err != nil { + t.Fatalf("ERROR: %v\n", err) + } + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatalf("ERROR: %v\n", err) + } + + if len(b) != len(input) { + t.Fatalf("Expected the Body to contain %d bytes but got %d", len(input), len(b)) + } + + if !reflect.DeepEqual(b, input) { + t.Fatalf("Body doesn't contain the same bytes: %s (Expected %s)", b, input) + } +} + func ExampleWithCustomBaseURL() { r, err := Prepare(&http.Request{}, WithCustomBaseURL("https://{account}.{service}.core.windows.net/", @@ -238,6 +262,26 @@ func ExampleWithJSON() { // Output: Request Body contains {"name":"Rob Pike","age":42} } +// Create a request whose Body is the XML encoding of a structure +func ExampleWithXML() { + t := mocks.T{Name: "Rob Pike", Age: 42} + + r, err := Prepare(&http.Request{}, + WithXML(&t)) + if err != nil { + fmt.Printf("ERROR: %v\n", err) + } + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + fmt.Printf("ERROR: %v\n", err) + } else { + fmt.Printf("Request Body contains %s\n", string(b)) + } + // Output: Request Body contains + // Rob Pike42 +} + // Create a request from a path with escaped parameters func ExampleWithEscapedPathParameters() { params := map[string]interface{}{ @@ -448,6 +492,13 @@ func TestAsHead(t *testing.T) { } } +func TestAsMerge(t *testing.T) { + r, _ := Prepare(mocks.NewRequest(), AsMerge()) + if r.Method != "MERGE" { + t.Fatal("autorest: AsMerge failed to set HTTP method header to MERGE") + } +} + func TestAsOptions(t *testing.T) { r, _ := Prepare(mocks.NewRequest(), AsOptions()) if r.Method != "OPTIONS" { diff --git a/autorest/responder.go b/autorest/responder.go index a908a0adb..349e1963a 100644 --- a/autorest/responder.go +++ b/autorest/responder.go @@ -153,6 +153,25 @@ func ByClosingIfError() RespondDecorator { } } +// ByUnmarshallingBytes returns a RespondDecorator that copies the Bytes returned in the +// response Body into the value pointed to by v. +func ByUnmarshallingBytes(v *[]byte) RespondDecorator { + return func(r Responder) Responder { + return ResponderFunc(func(resp *http.Response) error { + err := r.Respond(resp) + if err == nil { + bytes, errInner := ioutil.ReadAll(resp.Body) + if errInner != nil { + err = fmt.Errorf("Error occurred reading http.Response#Body - Error = '%v'", errInner) + } else { + *v = bytes + } + } + return err + }) + } +} + // ByUnmarshallingJSON returns a RespondDecorator that decodes a JSON document returned in the // response Body into the value pointed to by v. func ByUnmarshallingJSON(v interface{}) RespondDecorator { diff --git a/autorest/responder_test.go b/autorest/responder_test.go index 4a57b1e3b..1d823ebbc 100644 --- a/autorest/responder_test.go +++ b/autorest/responder_test.go @@ -48,6 +48,25 @@ func ExampleWithErrorUnlessOK() { // Output: GET of https://microsoft.com/a/b/c/ returned HTTP 200 } +func TestByUnmarshallingBytes(t *testing.T) { + expected := []byte("Lorem Ipsum Dolor") + + // we'll create a fixed-sized array here, since that's the expectation + bytes := make([]byte, len(expected)) + + Respond(mocks.NewResponseWithBytes(expected), + ByUnmarshallingBytes(&bytes), + ByClosing()) + + if len(bytes) != len(expected) { + t.Fatalf("Expected Response to be %d bytes but got %d bytes", len(expected), len(bytes)) + } + + if !reflect.DeepEqual(expected, bytes) { + t.Fatalf("Expected Response to be %s but got %s", expected, bytes) + } +} + func ExampleByUnmarshallingJSON() { c := ` { diff --git a/autorest/sender.go b/autorest/sender.go index 6665d7c00..2f4b0ddda 100644 --- a/autorest/sender.go +++ b/autorest/sender.go @@ -248,16 +248,24 @@ func DoRetryForStatusCodes(attempts int, backoff time.Duration, codes ...int) Se } } -// DelayWithRetryAfter invokes time.After for the duration specified in the "Retry-After" header in -// responses with status code 429 +// DelayWithRetryAfter invokes time.After for the duration specified in the "Retry-After" header. +// The value of Retry-After can be either the number of seconds or a date in RFC1123 format. +// The function returns true after successfully waiting for the specified duration. If there is +// no Retry-After header or the wait is cancelled the return value is false. func DelayWithRetryAfter(resp *http.Response, cancel <-chan struct{}) bool { if resp == nil { return false } - retryAfter, _ := strconv.Atoi(resp.Header.Get("Retry-After")) - if resp.StatusCode == http.StatusTooManyRequests && retryAfter > 0 { + var dur time.Duration + ra := resp.Header.Get("Retry-After") + if retryAfter, _ := strconv.Atoi(ra); retryAfter > 0 { + dur = time.Duration(retryAfter) * time.Second + } else if t, err := time.Parse(time.RFC1123, ra); err == nil { + dur = t.Sub(time.Now()) + } + if dur > 0 { select { - case <-time.After(time.Duration(retryAfter) * time.Second): + case <-time.After(dur): return true case <-cancel: return false diff --git a/autorest/sender_test.go b/autorest/sender_test.go index 218637394..29bc7e91a 100644 --- a/autorest/sender_test.go +++ b/autorest/sender_test.go @@ -818,6 +818,33 @@ func TestDelayWithRetryAfterWithSuccess(t *testing.T) { } } +func TestDelayWithRetryAfterWithSuccessDateTime(t *testing.T) { + resumeAt := time.Now().Add(2 * time.Second).Round(time.Second) + + client := mocks.NewSender() + resp := mocks.NewResponseWithStatus("503 Service temporarily unavailable", http.StatusServiceUnavailable) + mocks.SetResponseHeader(resp, "Retry-After", resumeAt.Format(time.RFC1123)) + client.AppendResponse(resp) + client.AppendResponse(mocks.NewResponseWithStatus("200 OK", http.StatusOK)) + + r, _ := SendWithSender(client, mocks.NewRequest(), + DoRetryForStatusCodes(1, time.Duration(time.Second), http.StatusServiceUnavailable), + ) + + if time.Now().Before(resumeAt) { + t.Fatal("autorest: DelayWithRetryAfter failed stopped too soon") + } + + Respond(r, + ByDiscardingBody(), + ByClosing()) + + if client.Attempts() != 2 { + t.Fatalf("autorest: Sender#DelayWithRetryAfter -- Got: StatusCode %v in %v attempts; Want: StatusCode 200 OK in 2 attempts -- ", + r.Status, client.Attempts()-1) + } +} + type temporaryError struct { message string } diff --git a/autorest/version.go b/autorest/version.go index d3e57b509..8ba0f591d 100644 --- a/autorest/version.go +++ b/autorest/version.go @@ -19,7 +19,7 @@ import ( "runtime" ) -const number = "v12.1.0" +const number = "v12.2.0" var ( userAgent = fmt.Sprintf("Go/%s (%s-%s) go-autorest/%s",