"io"
"io/ioutil"
"log"
- "math"
"net/http"
"net/url"
"os"
"strings"
"time"
- "git.curoverse.com/arvados.git/sdk/go/httpserver"
+ "git.arvados.org/arvados.git/sdk/go/httpserver"
)
// A Client is an HTTP client with an API endpoint and a set of
// arvadosclient.ArvadosClient.)
KeepServiceURIs []string `json:",omitempty"`
+ // HTTP headers to add/override in outgoing requests.
+ SendHeader http.Header
+
dd *DiscoveryDocument
ctx context.Context
// Do adds Authorization and X-Request-Id headers and then calls
// (*http.Client)Do().
func (c *Client) Do(req *http.Request) (*http.Response, error) {
- if auth, _ := req.Context().Value("Authorization").(string); auth != "" {
+ if auth, _ := req.Context().Value(contextKeyAuthorization{}).(string); auth != "" {
req.Header.Add("Authorization", auth)
} else if c.AuthToken != "" {
req.Header.Add("Authorization", "OAuth2 "+c.AuthToken)
}
if req.Header.Get("X-Request-Id") == "" {
- reqid, _ := req.Context().Value(contextKeyRequestID).(string)
+ reqid, _ := req.Context().Value(contextKeyRequestID{}).(string)
if reqid == "" {
- reqid, _ = c.context().Value(contextKeyRequestID).(string)
+ reqid, _ = c.context().Value(contextKeyRequestID{}).(string)
}
if reqid == "" {
reqid = reqIDGen.Next()
return c.httpClient().Do(req)
}
+func isRedirectStatus(code int) bool {
+ switch code {
+ case http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther, http.StatusTemporaryRedirect, http.StatusPermanentRedirect:
+ return true
+ default:
+ return false
+ }
+}
+
// DoAndDecode performs req and unmarshals the response (which must be
// JSON) into dst. Use this instead of RequestAndDecode if you need
// more control of the http.Request object.
+//
+// If the response status indicates an HTTP redirect, the Location
+// header value is unmarshalled to dst as a RedirectLocation
+// key/field.
func (c *Client) DoAndDecode(dst interface{}, req *http.Request) error {
resp, err := c.Do(req)
if err != nil {
if err != nil {
return err
}
- if resp.StatusCode != 200 {
- return newTransactionError(req, resp, buf)
- }
- if dst == nil {
+ switch {
+ case resp.StatusCode == http.StatusOK && dst == nil:
+ return nil
+ case resp.StatusCode == http.StatusOK:
+ return json.Unmarshal(buf, dst)
+
+ // If the caller uses a client with a custom CheckRedirect
+ // func, Do() might return the 3xx response instead of
+ // following it.
+ case isRedirectStatus(resp.StatusCode) && dst == nil:
return nil
+ case isRedirectStatus(resp.StatusCode):
+ // Copy the redirect target URL to dst.RedirectLocation.
+ buf, err := json.Marshal(map[string]string{"redirect_location": resp.Header.Get("Location")})
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(buf, dst)
+
+ default:
+ return newTransactionError(req, resp, buf)
}
- return json.Unmarshal(buf, dst)
}
// Convert an arbitrary struct to url.Values. For example,
return nil, err
}
var generic map[string]interface{}
- err = json.Unmarshal(j, &generic)
+ dec := json.NewDecoder(bytes.NewBuffer(j))
+ dec.UseNumber()
+ err = dec.Decode(&generic)
if err != nil {
return nil, err
}
urlValues.Set(k, v)
continue
}
- if v, ok := v.(float64); ok {
- // Unmarshal decodes all numbers as float64,
- // which can be written as 1.2345e4 in JSON,
- // but this form is not accepted for ints in
- // url params. If a number fits in an int64,
- // encode it as int64 rather than float64.
- if v, frac := math.Modf(v); frac == 0 && v <= math.MaxInt64 && v >= math.MinInt64 {
- urlValues.Set(k, fmt.Sprintf("%d", int64(v)))
- continue
+ if v, ok := v.(json.Number); ok {
+ urlValues.Set(k, v.String())
+ continue
+ }
+ if v, ok := v.(bool); ok {
+ if v {
+ urlValues.Set(k, "true")
+ } else {
+ // "foo=false", "foo=0", and "foo="
+ // are all taken as true strings, so
+ // don't send false values at all --
+ // rely on the default being false.
}
+ continue
}
j, err := json.Marshal(v)
if err != nil {
return nil, err
}
- if string(j) == "null" {
+ if bytes.Equal(j, []byte("null")) {
+ // don't add it to urlValues at all
continue
}
urlValues.Set(k, string(j))
}
if urlValues == nil {
// Nothing to send
- } else if method == "GET" || method == "HEAD" || body != nil {
- // Must send params in query part of URL (FIXME: what
- // if resulting URL is too long?)
+ } else if body != nil || ((method == "GET" || method == "HEAD") && len(urlValues.Encode()) < 1000) {
+ // Send params in query part of URL
u, err := url.Parse(urlString)
if err != nil {
return err
if err != nil {
return err
}
+ if (method == "GET" || method == "HEAD") && body != nil {
+ req.Header.Set("X-Http-Method-Override", method)
+ req.Method = "POST"
+ }
req = req.WithContext(ctx)
req.Header.Set("Content-type", "application/x-www-form-urlencoded")
+ for k, v := range c.SendHeader {
+ req.Header[k] = v
+ }
return c.DoAndDecode(dst, req)
}