Merge lp:~axwalk/gwacl/management-api-307-redirects into lp:gwacl

Proposed by Andrew Wilkins
Status: Merged
Approved by: Andrew Wilkins
Approved revision: 232
Merged at revision: 231
Proposed branch: lp:~axwalk/gwacl/management-api-307-redirects
Merge into: lp:gwacl
Diff against target: 185 lines (+104/-3)
3 files modified
x509dispatcher.go (+35/-1)
x509dispatcher_test.go (+56/-2)
x509session.go (+13/-0)
To merge this branch: bzr merge lp:~axwalk/gwacl/management-api-307-redirects
Reviewer Review Type Date Requested Status
Ian Booth Approve
Review via email: mp+204618@code.launchpad.net

Commit message

Add handling of temporary redirects in the management API.

Description of the change

Add handling of temporary redirects in the management API.

To post a comment you must log in.
Revision history for this message
Ian Booth (wallyworld) wrote :

Looks good. Did you test it live? I think that should be done before landing.

review: Approve
Revision history for this message
Andrew Wilkins (axwalk) wrote :

I tested bootstrap and destroy-env a bunch of times. I'll run some deployment tests before landing.

Revision history for this message
Andrew Wilkins (axwalk) wrote :

Seems there's still a problem.

232. By Andrew Wilkins

Also check redirect status code for POST/DELETE, etc.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'x509dispatcher.go'
2--- x509dispatcher.go 2013-08-09 13:59:43 +0000
3+++ x509dispatcher.go 2014-02-05 04:46:43 +0000
4@@ -5,9 +5,11 @@
5
6 import (
7 "bytes"
8+ "fmt"
9 "io/ioutil"
10 "launchpad.net/gwacl/fork/http"
11 . "launchpad.net/gwacl/logging"
12+ "net/url"
13 )
14
15 type X509Request struct {
16@@ -84,7 +86,7 @@
17 httpRequest.Header.Set("Content-Type", request.ContentType)
18 httpRequest.Header.Set("x-ms-version", request.APIVersion)
19 retrier := session.retryPolicy.getForkedHttpRetrier(session.client)
20- httpResponse, err := retrier.RetryRequest(httpRequest)
21+ httpResponse, err := handleRequestRedirects(retrier, httpRequest)
22 if err != nil {
23 return nil, err
24 }
25@@ -108,3 +110,35 @@
26
27 return response, nil
28 }
29+
30+func handleRequestRedirects(retrier *forkedHttpRetrier, request *http.Request) (*http.Response, error) {
31+ const maxRedirects = 10
32+ // Handle temporary redirects.
33+ redirects := -1
34+ for {
35+ redirects++
36+ if redirects >= maxRedirects {
37+ return nil, fmt.Errorf("stopped after %d redirects", redirects)
38+ }
39+ response, err := retrier.RetryRequest(request)
40+ // For GET and HEAD, we cause the request execution
41+ // to return httpRedirectErr if a temporary redirect
42+ // is returned, and then deal with it here.
43+ if err, ok := err.(*url.Error); ok && err.Err == httpRedirectErr {
44+ request.URL, err.Err = url.Parse(err.URL)
45+ if err.Err != nil {
46+ return nil, err
47+ }
48+ continue
49+ }
50+ // For other methods, we must check the response code.
51+ if err == nil && response.StatusCode == http.StatusTemporaryRedirect {
52+ request.URL, err = response.Location()
53+ if err != nil {
54+ return nil, err
55+ }
56+ continue
57+ }
58+ return response, err
59+ }
60+}
61
62=== modified file 'x509dispatcher_test.go'
63--- x509dispatcher_test.go 2013-08-09 14:59:50 +0000
64+++ x509dispatcher_test.go 2014-02-05 04:46:43 +0000
65@@ -22,6 +22,7 @@
66 // channel, and finally returns the given status code. If body is not nil, it
67 // will be returned as the request body.
68 func makeRecordingHTTPServer(requests chan *Request, status int, body []byte, headers http.Header) *httptest.Server {
69+ var server *httptest.Server
70 returnRequest := func(w http.ResponseWriter, r *http.Request) {
71 // Capture all the request body content for later inspection.
72 requestBody, err := ioutil.ReadAll(r.Body)
73@@ -29,7 +30,8 @@
74 panic(err)
75 }
76 requests <- &Request{r, requestBody}
77-
78+ // Set a default Location so we can test redirect loops easily.
79+ w.Header().Set("Location", server.URL+r.URL.Path)
80 for header, values := range headers {
81 for _, value := range values {
82 w.Header().Set(header, value)
83@@ -42,7 +44,8 @@
84 }
85 serveMux := http.NewServeMux()
86 serveMux.HandleFunc("/", returnRequest)
87- return httptest.NewServer(serveMux)
88+ server = httptest.NewServer(serveMux)
89+ return server
90 }
91
92 func (*x509DispatcherSuite) TestGetRequestDoesHTTPGET(c *C) {
93@@ -184,3 +187,54 @@
94
95 c.Check(response.Header[customHeader], DeepEquals, customValue)
96 }
97+
98+func (*x509DispatcherSuite) TestRequestsFollowRedirects(c *C) {
99+ httpRequests := make(chan *Request, 2)
100+ serverConflict := makeRecordingHTTPServer(httpRequests, http.StatusConflict, nil, nil)
101+ defer serverConflict.Close()
102+ redirPath := "/else/where"
103+ responseHeaders := make(http.Header)
104+ responseHeaders.Set("Location", serverConflict.URL+redirPath)
105+ serverRedir := makeRecordingHTTPServer(httpRequests, http.StatusTemporaryRedirect, nil, responseHeaders)
106+ defer serverRedir.Close()
107+ session, err := newX509Session("subscriptionid", "", "West US", NoRetryPolicy)
108+ c.Assert(err, IsNil)
109+ path := "/foo/bar"
110+ version := "test-version"
111+
112+ // Test both GET and DELETE: DELETE does not normally
113+ // automatically follow redirects, however Azure requires
114+ // us to.
115+ requests := []*X509Request{
116+ newX509RequestGET(serverRedir.URL+path, version),
117+ newX509RequestDELETE(serverRedir.URL+path, version),
118+ }
119+ for _, request := range requests {
120+ response, err := performX509Request(session, request)
121+ c.Assert(err, IsNil)
122+ c.Assert(response.StatusCode, Equals, http.StatusConflict)
123+ c.Assert(httpRequests, HasLen, 2)
124+ c.Assert((<-httpRequests).URL.String(), Equals, path)
125+ c.Assert((<-httpRequests).URL.String(), Equals, redirPath)
126+ }
127+}
128+
129+func (*x509DispatcherSuite) TestRequestsLimitRedirects(c *C) {
130+ httpRequests := make(chan *Request, 10)
131+ serverRedir := makeRecordingHTTPServer(httpRequests, http.StatusTemporaryRedirect, nil, nil)
132+ defer serverRedir.Close()
133+ session, err := newX509Session("subscriptionid", "", "West US", NoRetryPolicy)
134+ c.Assert(err, IsNil)
135+ path := "/foo/bar"
136+ version := "test-version"
137+ request := newX509RequestGET(serverRedir.URL+path, version)
138+
139+ response, err := performX509Request(session, request)
140+ c.Assert(err, ErrorMatches, "stopped after 10 redirects")
141+ c.Assert(response, IsNil)
142+ c.Assert(httpRequests, HasLen, 10)
143+ close(httpRequests)
144+ for req := range httpRequests {
145+ c.Assert(req.URL.String(), Equals, path)
146+ }
147+}
148
149=== modified file 'x509session.go'
150--- x509session.go 2013-10-30 22:57:57 +0000
151+++ x509session.go 2014-02-05 04:46:43 +0000
152@@ -4,6 +4,7 @@
153 package gwacl
154
155 import (
156+ "errors"
157 "fmt"
158 "launchpad.net/gwacl/fork/http"
159 "launchpad.net/gwacl/fork/tls"
160@@ -19,6 +20,11 @@
161 retryPolicy RetryPolicy
162 }
163
164+// httpRedirectErr is a unique error used to prevent
165+// net/http from automatically following redirects.
166+// See commentary on CheckRedirect in newX509Session.
167+var httpRedirectErr = errors.New("redirect")
168+
169 // newX509Session creates and returns a new x509Session based on credentials
170 // and X509 certificate files.
171 // For testing purposes, certFile can be passed as the empty string and it
172@@ -43,6 +49,13 @@
173 // hit the above Go bug.
174 DisableKeepAlives: true,
175 },
176+ // See https://code.google.com/p/go/issues/detail?id=4800
177+ // We need to follow temporary redirects (307s), while
178+ // retaining headers. We also need to follow redirects
179+ // for POST and DELETE automatically.
180+ CheckRedirect: func(req *http.Request, via []*http.Request) error {
181+ return httpRedirectErr
182+ },
183 }
184
185 endpointURL := GetEndpoint(location).ManagementAPI()

Subscribers

People subscribed via source and target branches

to all changes: