diff --git a/client/base_client.go b/client/base_client.go index 974560dcd..494841776 100644 --- a/client/base_client.go +++ b/client/base_client.go @@ -1,6 +1,7 @@ package client import ( + "context" "net/http" "net/url" "time" @@ -12,3 +13,37 @@ type BaseClient interface { SendRequest(method string, rawURL string, data url.Values, headers map[string]interface{}) (*http.Response, error) } + +// BaseClientWithCtx is an extension of BaseClient with the ability to associate a contex with +// the request +type BaseClientWithCtx interface { + BaseClient + SendRequestWithCtx(ctx context.Context, method string, rawURL string, data url.Values, + headers map[string]interface{}) (*http.Response, error) +} + +// wrapperClient wraps the lower level BaseClient to fulfill the BaseClientWithCtx interface. This +// allows the SDK to utilize the BaseClientWithCtx method throughout the codebase. +// +// All *WithCtx methods of a wrapped client will not actually use their context.Context argument. +type wrapperClient struct { + // embed the BaseClient so the functions remain accessible + BaseClient +} + +// SendRequestWithCtx passes the request through to the underlying BaseClient. The context.Context +// argument is not utilized. +func (w wrapperClient) SendRequestWithCtx(ctx context.Context, method string, rawURL string, data url.Values, + headers map[string]interface{}) (*http.Response, error) { + return w.SendRequest(method, rawURL, data, headers) +} + +// wrapBaseClientWithNoopCtx "upgrades" a BaseClient to BaseClientWithCtx so that requests can be +// send with a request context. +func wrapBaseClientWithNoopCtx(c BaseClient) BaseClientWithCtx { + // the default library client has SendRequestWithCtx, use it if available. + if typedClient, ok := c.(BaseClientWithCtx); ok { + return typedClient + } + return wrapperClient{BaseClient: c} +} diff --git a/client/client.go b/client/client.go index b7b8f874a..eddfdd059 100644 --- a/client/client.go +++ b/client/client.go @@ -2,6 +2,7 @@ package client import ( + "context" "encoding/json" "fmt" "net/http" @@ -44,7 +45,7 @@ func defaultHTTPClient() *http.Client { } } -func (c *Client) basicAuth() (string, string) { +func (c *Client) basicAuth() (username, password string) { return c.Credentials.Username, c.Credentials.Password } @@ -89,6 +90,12 @@ func (c *Client) doWithErr(req *http.Request) (*http.Response, error) { // SendRequest verifies, constructs, and authorizes an HTTP request. func (c *Client) SendRequest(method string, rawURL string, data url.Values, + headers map[string]interface{}) (*http.Response, error) { + return c.SendRequestWithCtx(context.TODO(), method, rawURL, data, headers) +} + +// SendRequestWithCtx verifies, constructs, and authorizes an HTTP request. +func (c *Client) SendRequestWithCtx(ctx context.Context, method string, rawURL string, data url.Values, headers map[string]interface{}) (*http.Response, error) { u, err := url.Parse(rawURL) if err != nil { @@ -112,7 +119,7 @@ func (c *Client) SendRequest(method string, rawURL string, data url.Values, valueReader = strings.NewReader(data.Encode()) } - req, err := http.NewRequest(method, u.String(), valueReader) + req, err := http.NewRequestWithContext(ctx, method, u.String(), valueReader) if err != nil { return nil, err } diff --git a/client/client_test.go b/client/client_test.go index d1e7d88aa..4f011201c 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -1,6 +1,7 @@ package client_test import ( + "context" "encoding/json" "io" "net/http" @@ -210,6 +211,33 @@ func TestClient_SetTimeoutTimesOut(t *testing.T) { assert.Error(t, err) } +func TestClient_SetTimeoutTimesOutViaContext(t *testing.T) { + handlerDelay := 100 * time.Microsecond + clientTimeout := 10 * time.Microsecond + assert.True(t, clientTimeout < handlerDelay) + + timeoutServer := httptest.NewServer(http.HandlerFunc( + func(writer http.ResponseWriter, _ *http.Request) { + d := map[string]interface{}{ + "response": "ok", + } + time.Sleep(100 * time.Microsecond) + encoder := json.NewEncoder(writer) + err := encoder.Encode(&d) + if err != nil { + t.Error(err) + } + writer.WriteHeader(http.StatusOK) + })) + defer timeoutServer.Close() + + c := NewClient("user", "pass") + ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Microsecond) + defer cancel() + _, err := c.SendRequestWithCtx(ctx, "GET", timeoutServer.URL, nil, nil) //nolint:bodyclose + assert.Error(t, err) +} + func TestClient_SetTimeoutSucceeds(t *testing.T) { timeoutServer := httptest.NewServer(http.HandlerFunc( func(writer http.ResponseWriter, request *http.Request) { diff --git a/client/page_util.go b/client/page_util.go index 1b05e1c88..aeb0a7d1e 100644 --- a/client/page_util.go +++ b/client/page_util.go @@ -1,6 +1,7 @@ package client import ( + "context" "encoding/json" "fmt" "strings" @@ -34,6 +35,15 @@ func GetNext(baseUrl string, response interface{}, getNextPage func(nextPageUri return getNextPage(nextPageUrl) } +func GetNextWithCtx(ctx context.Context, baseUrl string, response interface{}, getNextPage func(ctx context.Context, nextPageUri string) (interface{}, error)) (interface{}, error) { + nextPageUrl, err := getNextPageUrl(baseUrl, response) + if err != nil { + return nil, err + } + + return getNextPage(ctx, nextPageUrl) +} + func toMap(s interface{}) (map[string]interface{}, error) { var payload map[string]interface{} data, err := json.Marshal(s) diff --git a/client/request_handler.go b/client/request_handler.go index 89fe7883f..c530b0bd5 100644 --- a/client/request_handler.go +++ b/client/request_handler.go @@ -2,38 +2,14 @@ package client import ( + "context" "net/http" "net/url" "os" "strings" ) -type RequestHandler struct { - Client BaseClient - Edge string - Region string -} - -func NewRequestHandler(client BaseClient) *RequestHandler { - return &RequestHandler{ - Client: client, - Edge: os.Getenv("TWILIO_EDGE"), - Region: os.Getenv("TWILIO_REGION"), - } -} - -func (c *RequestHandler) sendRequest(method string, rawURL string, data url.Values, - headers map[string]interface{}) (*http.Response, error) { - parsedURL, err := c.BuildUrl(rawURL) - if err != nil { - return nil, err - } - - return c.Client.SendRequest(method, parsedURL, data, headers) -} - -// BuildUrl builds the target host string taking into account region and edge configurations. -func (c *RequestHandler) BuildUrl(rawURL string) (string, error) { +func buildUrlInternal(overrideEdge, overrideRegion, rawURL string) (string, error) { u, err := url.Parse(rawURL) if err != nil { return "", err @@ -63,12 +39,12 @@ func (c *RequestHandler) BuildUrl(rawURL string) (string, error) { region = pieces[2] } - if c.Edge != "" { - edge = c.Edge + if overrideEdge != "" { + edge = overrideEdge } - if c.Region != "" { - region = c.Region + if overrideRegion != "" { + region = overrideRegion } else if region == "" && edge != "" { region = "us1" } @@ -83,14 +59,96 @@ func (c *RequestHandler) BuildUrl(rawURL string) (string, error) { return u.String(), nil } +type RequestHandler struct { + Client BaseClient + Edge string + Region string +} + +func NewRequestHandler(client BaseClient) *RequestHandler { + return &RequestHandler{ + Client: client, + Edge: os.Getenv("TWILIO_EDGE"), + Region: os.Getenv("TWILIO_REGION"), + } +} + +func (c *RequestHandler) sendRequest(method string, rawURL string, data url.Values, + headers map[string]interface{}) (*http.Response, error) { + parsedURL, err := c.BuildUrl(rawURL) + if err != nil { + return nil, err + } + + return c.Client.SendRequest(method, parsedURL, data, headers) +} + +// BuildUrl builds the target host string taking into account region and edge configurations. +func (c *RequestHandler) BuildUrl(rawURL string) (string, error) { + return buildUrlInternal(c.Edge, c.Region, rawURL) +} + +// deprecated func (c *RequestHandler) Post(path string, bodyData url.Values, headers map[string]interface{}) (*http.Response, error) { return c.sendRequest(http.MethodPost, path, bodyData, headers) } +// deprecated func (c *RequestHandler) Get(path string, queryData url.Values, headers map[string]interface{}) (*http.Response, error) { return c.sendRequest(http.MethodGet, path, queryData, headers) } +// deprecated func (c *RequestHandler) Delete(path string, nothing url.Values, headers map[string]interface{}) (*http.Response, error) { return c.sendRequest(http.MethodDelete, path, nil, headers) } + +func UpgradeRequestHandler(h *RequestHandler) *RequestHandlerWithCtx { + return &RequestHandlerWithCtx{ + // wrapped client will supply context.TODO() to all API calls + Client: wrapBaseClientWithNoopCtx(h.Client), + Edge: h.Edge, + Region: h.Region, + } +} + +type RequestHandlerWithCtx struct { + Client BaseClientWithCtx + Edge string + Region string +} + +func (c *RequestHandlerWithCtx) sendRequest(ctx context.Context, method string, rawURL string, data url.Values, + headers map[string]interface{}) (*http.Response, error) { + parsedURL, err := c.BuildUrl(rawURL) + if err != nil { + return nil, err + } + + return c.Client.SendRequestWithCtx(ctx, method, parsedURL, data, headers) +} + +func NewRequestHandlerWithCtx(client BaseClientWithCtx) *RequestHandlerWithCtx { + return &RequestHandlerWithCtx{ + Client: client, + Edge: os.Getenv("TWILIO_EDGE"), + Region: os.Getenv("TWILIO_REGION"), + } +} + +// BuildUrl builds the target host string taking into account region and edge configurations. +func (c *RequestHandlerWithCtx) BuildUrl(rawURL string) (string, error) { + return buildUrlInternal(c.Edge, c.Region, rawURL) +} + +func (c *RequestHandlerWithCtx) Post(ctx context.Context, path string, bodyData url.Values, headers map[string]interface{}) (*http.Response, error) { + return c.sendRequest(ctx, http.MethodPost, path, bodyData, headers) +} + +func (c *RequestHandlerWithCtx) Get(ctx context.Context, path string, queryData url.Values, headers map[string]interface{}) (*http.Response, error) { + return c.sendRequest(ctx, http.MethodGet, path, queryData, headers) +} + +func (c *RequestHandlerWithCtx) Delete(ctx context.Context, path string, nothing url.Values, headers map[string]interface{}) (*http.Response, error) { + return c.sendRequest(ctx, http.MethodDelete, path, nil, headers) +}