Merge pull request #365 from smashwilson/centralize-http

Centralize HTTP handling and allow custom http.Clients
diff --git a/openstack/blockstorage/v1/apiversions/requests.go b/openstack/blockstorage/v1/apiversions/requests.go
index 016bf37..f5a793c 100644
--- a/openstack/blockstorage/v1/apiversions/requests.go
+++ b/openstack/blockstorage/v1/apiversions/requests.go
@@ -3,8 +3,6 @@
 import (
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
-
-	"github.com/racker/perigee"
 )
 
 // List lists all the Cinder API versions available to end-users.
@@ -18,11 +16,9 @@
 // type from the result, call the Extract method on the GetResult.
 func Get(client *gophercloud.ServiceClient, v string) GetResult {
 	var res GetResult
-	_, err := perigee.Request("GET", getURL(client, v), perigee.Options{
-		MoreHeaders: client.AuthenticatedHeaders(),
-		OkCodes:     []int{200},
-		Results:     &res.Body,
+	_, res.Err = client.Request("GET", getURL(client, v), gophercloud.RequestOpts{
+		OkCodes:      []int{200},
+		JSONResponse: &res.Body,
 	})
-	res.Err = err
 	return res
 }
diff --git a/openstack/blockstorage/v1/snapshots/requests.go b/openstack/blockstorage/v1/snapshots/requests.go
index 443f696..1b313a6 100644
--- a/openstack/blockstorage/v1/snapshots/requests.go
+++ b/openstack/blockstorage/v1/snapshots/requests.go
@@ -5,8 +5,6 @@
 
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
-
-	"github.com/racker/perigee"
 )
 
 // CreateOptsBuilder allows extensions to add additional parameters to the
@@ -69,11 +67,10 @@
 		return res
 	}
 
-	_, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
-		MoreHeaders: client.AuthenticatedHeaders(),
-		OkCodes:     []int{200, 201},
-		ReqBody:     &reqBody,
-		Results:     &res.Body,
+	_, res.Err = client.Request("POST", createURL(client), gophercloud.RequestOpts{
+		OkCodes:      []int{200, 201},
+		JSONBody:     &reqBody,
+		JSONResponse: &res.Body,
 	})
 	return res
 }
@@ -81,9 +78,8 @@
 // Delete will delete the existing Snapshot with the provided ID.
 func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
 	var res DeleteResult
-	_, res.Err = perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
-		MoreHeaders: client.AuthenticatedHeaders(),
-		OkCodes:     []int{202, 204},
+	_, res.Err = client.Request("DELETE", deleteURL(client, id), gophercloud.RequestOpts{
+		OkCodes: []int{202, 204},
 	})
 	return res
 }
@@ -92,10 +88,9 @@
 // object from the response, call the Extract method on the GetResult.
 func Get(client *gophercloud.ServiceClient, id string) GetResult {
 	var res GetResult
-	_, res.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
-		Results:     &res.Body,
-		MoreHeaders: client.AuthenticatedHeaders(),
-		OkCodes:     []int{200},
+	_, res.Err = client.Request("GET", getURL(client, id), gophercloud.RequestOpts{
+		OkCodes:      []int{200},
+		JSONResponse: &res.Body,
 	})
 	return res
 }
@@ -178,11 +173,10 @@
 		return res
 	}
 
-	_, res.Err = perigee.Request("PUT", updateMetadataURL(client, id), perigee.Options{
-		MoreHeaders: client.AuthenticatedHeaders(),
-		OkCodes:     []int{200},
-		ReqBody:     &reqBody,
-		Results:     &res.Body,
+	_, res.Err = client.Request("PUT", updateMetadataURL(client, id), gophercloud.RequestOpts{
+		OkCodes:      []int{200},
+		JSONBody:     &reqBody,
+		JSONResponse: &res.Body,
 	})
 	return res
 }
diff --git a/openstack/blockstorage/v1/volumes/requests.go b/openstack/blockstorage/v1/volumes/requests.go
index f4332de..e620ca6 100644
--- a/openstack/blockstorage/v1/volumes/requests.go
+++ b/openstack/blockstorage/v1/volumes/requests.go
@@ -5,8 +5,6 @@
 
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
-
-	"github.com/racker/perigee"
 )
 
 // CreateOptsBuilder allows extensions to add additional parameters to the
@@ -85,11 +83,10 @@
 		return res
 	}
 
-	_, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
-		MoreHeaders: client.AuthenticatedHeaders(),
-		ReqBody:     &reqBody,
-		Results:     &res.Body,
-		OkCodes:     []int{200, 201},
+	_, res.Err = client.Request("POST", createURL(client), gophercloud.RequestOpts{
+		OkCodes:      []int{200, 201},
+		JSONBody:     &reqBody,
+		JSONResponse: &res.Body,
 	})
 	return res
 }
@@ -97,9 +94,8 @@
 // Delete will delete the existing Volume with the provided ID.
 func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
 	var res DeleteResult
-	_, res.Err = perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
-		MoreHeaders: client.AuthenticatedHeaders(),
-		OkCodes:     []int{202, 204},
+	_, res.Err = client.Request("DELETE", deleteURL(client, id), gophercloud.RequestOpts{
+		OkCodes: []int{202, 204},
 	})
 	return res
 }
@@ -108,10 +104,9 @@
 // from the response, call the Extract method on the GetResult.
 func Get(client *gophercloud.ServiceClient, id string) GetResult {
 	var res GetResult
-	_, res.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
-		Results:     &res.Body,
-		MoreHeaders: client.AuthenticatedHeaders(),
-		OkCodes:     []int{200},
+	_, res.Err = client.Request("GET", getURL(client, id), gophercloud.RequestOpts{
+		JSONResponse: &res.Body,
+		OkCodes:      []int{200},
 	})
 	return res
 }
@@ -207,11 +202,10 @@
 		return res
 	}
 
-	_, res.Err = perigee.Request("PUT", updateURL(client, id), perigee.Options{
-		MoreHeaders: client.AuthenticatedHeaders(),
-		OkCodes:     []int{200},
-		ReqBody:     &reqBody,
-		Results:     &res.Body,
+	_, res.Err = client.Request("PUT", updateURL(client, id), gophercloud.RequestOpts{
+		OkCodes:      []int{200},
+		JSONBody:     &reqBody,
+		JSONResponse: &res.Body,
 	})
 	return res
 }
diff --git a/openstack/blockstorage/v1/volumetypes/requests.go b/openstack/blockstorage/v1/volumetypes/requests.go
index 87e20f6..6fedaa6 100644
--- a/openstack/blockstorage/v1/volumetypes/requests.go
+++ b/openstack/blockstorage/v1/volumetypes/requests.go
@@ -1,7 +1,6 @@
 package volumetypes
 
 import (
-	"github.com/racker/perigee"
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
 )
@@ -45,11 +44,11 @@
 		return res
 	}
 
-	_, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
-		MoreHeaders: client.AuthenticatedHeaders(),
-		OkCodes:     []int{200, 201},
-		ReqBody:     &reqBody,
-		Results:     &res.Body,
+	_, res.Err = client.Request("POST", createURL(client), gophercloud.RequestOpts{
+		MoreHeaders:  client.AuthenticatedHeaders(),
+		OkCodes:      []int{200, 201},
+		JSONBody:     &reqBody,
+		JSONResponse: &res.Body,
 	})
 	return res
 }
@@ -57,7 +56,7 @@
 // Delete will delete the volume type with the provided ID.
 func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
 	var res DeleteResult
-	_, res.Err = perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
+	_, res.Err = client.Request("DELETE", deleteURL(client, id), gophercloud.RequestOpts{
 		MoreHeaders: client.AuthenticatedHeaders(),
 		OkCodes:     []int{202},
 	})
@@ -68,10 +67,10 @@
 // type from the result, call the Extract method on the GetResult.
 func Get(client *gophercloud.ServiceClient, id string) GetResult {
 	var res GetResult
-	_, err := perigee.Request("GET", getURL(client, id), perigee.Options{
-		MoreHeaders: client.AuthenticatedHeaders(),
-		OkCodes:     []int{200},
-		Results:     &res.Body,
+	_, err := client.Request("GET", getURL(client, id), gophercloud.RequestOpts{
+		MoreHeaders:  client.AuthenticatedHeaders(),
+		OkCodes:      []int{200},
+		JSONResponse: &res.Body,
 	})
 	res.Err = err
 	return res
diff --git a/pagination/http.go b/pagination/http.go
index 1e108c8..cabcccd 100644
--- a/pagination/http.go
+++ b/pagination/http.go
@@ -7,7 +7,6 @@
 	"net/url"
 	"strings"
 
-	"github.com/racker/perigee"
 	"github.com/rackspace/gophercloud"
 )
 
@@ -19,7 +18,7 @@
 
 // PageResultFrom parses an HTTP response as JSON and returns a PageResult containing the
 // results, interpreting it as JSON if the content type indicates.
-func PageResultFrom(resp http.Response) (PageResult, error) {
+func PageResultFrom(resp *http.Response) (PageResult, error) {
 	var parsedBody interface{}
 
 	defer resp.Body.Close()
@@ -46,19 +45,10 @@
 	}, err
 }
 
-// Request performs a Perigee request and extracts the http.Response from the result.
-func Request(client *gophercloud.ServiceClient, headers map[string]string, url string) (http.Response, error) {
-	h := client.AuthenticatedHeaders()
-	for key, value := range headers {
-		h[key] = value
-	}
-
-	resp, err := perigee.Request("GET", url, perigee.Options{
-		MoreHeaders: h,
+// Request performs an HTTP request and extracts the http.Response from the result.
+func Request(client *gophercloud.ServiceClient, headers map[string]string, url string) (*http.Response, error) {
+	return client.Request("GET", url, gophercloud.RequestOpts{
+		MoreHeaders: headers,
 		OkCodes:     []int{200, 204},
 	})
-	if err != nil {
-		return http.Response{}, err
-	}
-	return resp.HttpResponse, nil
 }
diff --git a/provider_client.go b/provider_client.go
index 7754c20..092a078 100644
--- a/provider_client.go
+++ b/provider_client.go
@@ -1,5 +1,14 @@
 package gophercloud
 
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+)
+
 // ProviderClient stores details that are required to interact with any
 // services within a specific provider's API.
 //
@@ -24,10 +33,155 @@
 	// EndpointLocator describes how this provider discovers the endpoints for
 	// its constituent services.
 	EndpointLocator EndpointLocator
+
+	// HTTPClient allows users to interject arbitrary http, https, or other transit behaviors.
+	HTTPClient http.Client
 }
 
 // AuthenticatedHeaders returns a map of HTTP headers that are common for all
 // authenticated service requests.
 func (client *ProviderClient) AuthenticatedHeaders() map[string]string {
+	if client.TokenID == "" {
+		return map[string]string{}
+	}
 	return map[string]string{"X-Auth-Token": client.TokenID}
 }
+
+// RequestOpts customizes the behavior of the provider.Request() method.
+type RequestOpts struct {
+	// JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The
+	// content type of the request will default to "application/json" unless overridden by MoreHeaders.
+	// It's an error to specify both a JSONBody and a RawBody.
+	JSONBody interface{}
+	// RawBody contains an io.Reader that will be consumed by the request directly. No content-type
+	// will be set unless one is provided explicitly by MoreHeaders.
+	RawBody io.Reader
+
+	// JSONResponse, if provided, will be populated with the contents of the response body parsed as
+	// JSON.
+	JSONResponse *interface{}
+	// OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If
+	// the response has a different code, an error will be returned.
+	OkCodes []int
+
+	// MoreHeaders specifies additional HTTP headers to be provide on the request. If a header is
+	// provided with a blank value (""), that header will be *omitted* instead: use this to suppress
+	// the default Accept header or an inferred Content-Type, for example.
+	MoreHeaders map[string]string
+}
+
+// UnexpectedResponseCodeError is returned by the Request method when a response code other than
+// those listed in OkCodes is encountered.
+type UnexpectedResponseCodeError struct {
+	URL      string
+	Method   string
+	Expected []int
+	Actual   int
+	Body     []byte
+}
+
+func (err *UnexpectedResponseCodeError) Error() string {
+	return fmt.Sprintf(
+		"Expected HTTP response code %v when accessing [%s %s], but got %d instead\n%s",
+		err.Expected, err.Method, err.URL, err.Actual, err.Body,
+	)
+}
+
+var applicationJSON = "application/json"
+
+// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
+// header will automatically be provided.
+func (client *ProviderClient) Request(method, url string, options RequestOpts) (*http.Response, error) {
+	var body io.Reader
+	var contentType *string
+
+	// Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided
+	// io.Reader as-is. Default the content-type to application/json.
+
+	if options.JSONBody != nil {
+		if options.RawBody != nil {
+			panic("Please provide only one of JSONBody or RawBody to gophercloud.Request().")
+		}
+
+		rendered, err := json.Marshal(options.JSONBody)
+		if err != nil {
+			return nil, err
+		}
+
+		body = bytes.NewReader(rendered)
+		contentType = &applicationJSON
+	}
+
+	if options.RawBody != nil {
+		body = options.RawBody
+	}
+
+	// Construct the http.Request.
+
+	req, err := http.NewRequest(method, url, body)
+	if err != nil {
+		return nil, err
+	}
+
+	// Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to
+	// modify or omit any header.
+
+	if contentType != nil {
+		req.Header.Set("Content-Type", *contentType)
+	}
+	req.Header.Set("Accept", applicationJSON)
+
+	for k, v := range client.AuthenticatedHeaders() {
+		req.Header.Add(k, v)
+	}
+
+	if options.MoreHeaders != nil {
+		for k, v := range options.MoreHeaders {
+			fmt.Printf("Applying header [%s: %v]\n", k, v)
+			if v != "" {
+				req.Header.Set(k, v)
+			} else {
+				req.Header.Del(k)
+			}
+		}
+	}
+
+	// Issue the request.
+
+	resp, err := client.HTTPClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+
+	// Validate the response code, if requested to do so.
+
+	if options.OkCodes != nil {
+		var ok bool
+		for _, code := range options.OkCodes {
+			if resp.StatusCode == code {
+				ok = true
+				break
+			}
+		}
+		if !ok {
+			body, _ := ioutil.ReadAll(resp.Body)
+			resp.Body.Close()
+			return resp, &UnexpectedResponseCodeError{
+				URL:      url,
+				Method:   method,
+				Expected: options.OkCodes,
+				Actual:   resp.StatusCode,
+				Body:     body,
+			}
+		}
+	}
+
+	// Parse the response body as JSON, if requested to do so.
+
+	if options.JSONResponse != nil {
+		defer resp.Body.Close()
+		json.NewDecoder(resp.Body).Decode(options.JSONResponse)
+	}
+
+	return resp, nil
+}
diff --git a/rackspace/lb/v1/nodes/requests.go b/rackspace/lb/v1/nodes/requests.go
index 77894aa..1ee00a4 100644
--- a/rackspace/lb/v1/nodes/requests.go
+++ b/rackspace/lb/v1/nodes/requests.go
@@ -124,7 +124,7 @@
 		return res
 	}
 
-	pr, err := pagination.PageResultFrom(resp.HttpResponse)
+	pr, err := pagination.PageResultFrom(&resp.HttpResponse)
 	if err != nil {
 		res.Err = err
 		return res