Skip to content

Commit b61e334

Browse files
authored
feat: introduce base client that utilizes context (#196)
This PR introduces alternative base client interfaces, `BaseClientWithCtx` and `RequestHandlerWithCtx`, that allow `context.Context` objects to be provided to every API request. This will allow the Twilio SDK to support context cancellations, and for custom HTTP clients to access the context while executing the request.
1 parent 00173f6 commit b61e334

File tree

5 files changed

+170
-32
lines changed

5 files changed

+170
-32
lines changed

client/base_client.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package client
22

33
import (
4+
"context"
45
"net/http"
56
"net/url"
67
"time"
@@ -12,3 +13,37 @@ type BaseClient interface {
1213
SendRequest(method string, rawURL string, data url.Values,
1314
headers map[string]interface{}) (*http.Response, error)
1415
}
16+
17+
// BaseClientWithCtx is an extension of BaseClient with the ability to associate a contex with
18+
// the request
19+
type BaseClientWithCtx interface {
20+
BaseClient
21+
SendRequestWithCtx(ctx context.Context, method string, rawURL string, data url.Values,
22+
headers map[string]interface{}) (*http.Response, error)
23+
}
24+
25+
// wrapperClient wraps the lower level BaseClient to fulfill the BaseClientWithCtx interface. This
26+
// allows the SDK to utilize the BaseClientWithCtx method throughout the codebase.
27+
//
28+
// All *WithCtx methods of a wrapped client will not actually use their context.Context argument.
29+
type wrapperClient struct {
30+
// embed the BaseClient so the functions remain accessible
31+
BaseClient
32+
}
33+
34+
// SendRequestWithCtx passes the request through to the underlying BaseClient. The context.Context
35+
// argument is not utilized.
36+
func (w wrapperClient) SendRequestWithCtx(ctx context.Context, method string, rawURL string, data url.Values,
37+
headers map[string]interface{}) (*http.Response, error) {
38+
return w.SendRequest(method, rawURL, data, headers)
39+
}
40+
41+
// wrapBaseClientWithNoopCtx "upgrades" a BaseClient to BaseClientWithCtx so that requests can be
42+
// send with a request context.
43+
func wrapBaseClientWithNoopCtx(c BaseClient) BaseClientWithCtx {
44+
// the default library client has SendRequestWithCtx, use it if available.
45+
if typedClient, ok := c.(BaseClientWithCtx); ok {
46+
return typedClient
47+
}
48+
return wrapperClient{BaseClient: c}
49+
}

client/client.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
package client
33

44
import (
5+
"context"
56
"encoding/json"
67
"fmt"
78
"net/http"
@@ -44,7 +45,7 @@ func defaultHTTPClient() *http.Client {
4445
}
4546
}
4647

47-
func (c *Client) basicAuth() (string, string) {
48+
func (c *Client) basicAuth() (username, password string) {
4849
return c.Credentials.Username, c.Credentials.Password
4950
}
5051

@@ -89,6 +90,12 @@ func (c *Client) doWithErr(req *http.Request) (*http.Response, error) {
8990

9091
// SendRequest verifies, constructs, and authorizes an HTTP request.
9192
func (c *Client) SendRequest(method string, rawURL string, data url.Values,
93+
headers map[string]interface{}) (*http.Response, error) {
94+
return c.SendRequestWithCtx(context.TODO(), method, rawURL, data, headers)
95+
}
96+
97+
// SendRequestWithCtx verifies, constructs, and authorizes an HTTP request.
98+
func (c *Client) SendRequestWithCtx(ctx context.Context, method string, rawURL string, data url.Values,
9299
headers map[string]interface{}) (*http.Response, error) {
93100
u, err := url.Parse(rawURL)
94101
if err != nil {
@@ -112,7 +119,7 @@ func (c *Client) SendRequest(method string, rawURL string, data url.Values,
112119
valueReader = strings.NewReader(data.Encode())
113120
}
114121

115-
req, err := http.NewRequest(method, u.String(), valueReader)
122+
req, err := http.NewRequestWithContext(ctx, method, u.String(), valueReader)
116123
if err != nil {
117124
return nil, err
118125
}

client/client_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package client_test
22

33
import (
4+
"context"
45
"encoding/json"
56
"io"
67
"net/http"
@@ -210,6 +211,33 @@ func TestClient_SetTimeoutTimesOut(t *testing.T) {
210211
assert.Error(t, err)
211212
}
212213

214+
func TestClient_SetTimeoutTimesOutViaContext(t *testing.T) {
215+
handlerDelay := 100 * time.Microsecond
216+
clientTimeout := 10 * time.Microsecond
217+
assert.True(t, clientTimeout < handlerDelay)
218+
219+
timeoutServer := httptest.NewServer(http.HandlerFunc(
220+
func(writer http.ResponseWriter, _ *http.Request) {
221+
d := map[string]interface{}{
222+
"response": "ok",
223+
}
224+
time.Sleep(100 * time.Microsecond)
225+
encoder := json.NewEncoder(writer)
226+
err := encoder.Encode(&d)
227+
if err != nil {
228+
t.Error(err)
229+
}
230+
writer.WriteHeader(http.StatusOK)
231+
}))
232+
defer timeoutServer.Close()
233+
234+
c := NewClient("user", "pass")
235+
ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Microsecond)
236+
defer cancel()
237+
_, err := c.SendRequestWithCtx(ctx, "GET", timeoutServer.URL, nil, nil) //nolint:bodyclose
238+
assert.Error(t, err)
239+
}
240+
213241
func TestClient_SetTimeoutSucceeds(t *testing.T) {
214242
timeoutServer := httptest.NewServer(http.HandlerFunc(
215243
func(writer http.ResponseWriter, request *http.Request) {

client/page_util.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package client
22

33
import (
4+
"context"
45
"encoding/json"
56
"fmt"
67
"strings"
@@ -34,6 +35,15 @@ func GetNext(baseUrl string, response interface{}, getNextPage func(nextPageUri
3435
return getNextPage(nextPageUrl)
3536
}
3637

38+
func GetNextWithCtx(ctx context.Context, baseUrl string, response interface{}, getNextPage func(ctx context.Context, nextPageUri string) (interface{}, error)) (interface{}, error) {
39+
nextPageUrl, err := getNextPageUrl(baseUrl, response)
40+
if err != nil {
41+
return nil, err
42+
}
43+
44+
return getNextPage(ctx, nextPageUrl)
45+
}
46+
3747
func toMap(s interface{}) (map[string]interface{}, error) {
3848
var payload map[string]interface{}
3949
data, err := json.Marshal(s)

client/request_handler.go

Lines changed: 88 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,14 @@
22
package client
33

44
import (
5+
"context"
56
"net/http"
67
"net/url"
78
"os"
89
"strings"
910
)
1011

11-
type RequestHandler struct {
12-
Client BaseClient
13-
Edge string
14-
Region string
15-
}
16-
17-
func NewRequestHandler(client BaseClient) *RequestHandler {
18-
return &RequestHandler{
19-
Client: client,
20-
Edge: os.Getenv("TWILIO_EDGE"),
21-
Region: os.Getenv("TWILIO_REGION"),
22-
}
23-
}
24-
25-
func (c *RequestHandler) sendRequest(method string, rawURL string, data url.Values,
26-
headers map[string]interface{}) (*http.Response, error) {
27-
parsedURL, err := c.BuildUrl(rawURL)
28-
if err != nil {
29-
return nil, err
30-
}
31-
32-
return c.Client.SendRequest(method, parsedURL, data, headers)
33-
}
34-
35-
// BuildUrl builds the target host string taking into account region and edge configurations.
36-
func (c *RequestHandler) BuildUrl(rawURL string) (string, error) {
12+
func buildUrlInternal(overrideEdge, overrideRegion, rawURL string) (string, error) {
3713
u, err := url.Parse(rawURL)
3814
if err != nil {
3915
return "", err
@@ -63,12 +39,12 @@ func (c *RequestHandler) BuildUrl(rawURL string) (string, error) {
6339
region = pieces[2]
6440
}
6541

66-
if c.Edge != "" {
67-
edge = c.Edge
42+
if overrideEdge != "" {
43+
edge = overrideEdge
6844
}
6945

70-
if c.Region != "" {
71-
region = c.Region
46+
if overrideRegion != "" {
47+
region = overrideRegion
7248
} else if region == "" && edge != "" {
7349
region = "us1"
7450
}
@@ -83,14 +59,96 @@ func (c *RequestHandler) BuildUrl(rawURL string) (string, error) {
8359
return u.String(), nil
8460
}
8561

62+
type RequestHandler struct {
63+
Client BaseClient
64+
Edge string
65+
Region string
66+
}
67+
68+
func NewRequestHandler(client BaseClient) *RequestHandler {
69+
return &RequestHandler{
70+
Client: client,
71+
Edge: os.Getenv("TWILIO_EDGE"),
72+
Region: os.Getenv("TWILIO_REGION"),
73+
}
74+
}
75+
76+
func (c *RequestHandler) sendRequest(method string, rawURL string, data url.Values,
77+
headers map[string]interface{}) (*http.Response, error) {
78+
parsedURL, err := c.BuildUrl(rawURL)
79+
if err != nil {
80+
return nil, err
81+
}
82+
83+
return c.Client.SendRequest(method, parsedURL, data, headers)
84+
}
85+
86+
// BuildUrl builds the target host string taking into account region and edge configurations.
87+
func (c *RequestHandler) BuildUrl(rawURL string) (string, error) {
88+
return buildUrlInternal(c.Edge, c.Region, rawURL)
89+
}
90+
91+
// deprecated
8692
func (c *RequestHandler) Post(path string, bodyData url.Values, headers map[string]interface{}) (*http.Response, error) {
8793
return c.sendRequest(http.MethodPost, path, bodyData, headers)
8894
}
8995

96+
// deprecated
9097
func (c *RequestHandler) Get(path string, queryData url.Values, headers map[string]interface{}) (*http.Response, error) {
9198
return c.sendRequest(http.MethodGet, path, queryData, headers)
9299
}
93100

101+
// deprecated
94102
func (c *RequestHandler) Delete(path string, nothing url.Values, headers map[string]interface{}) (*http.Response, error) {
95103
return c.sendRequest(http.MethodDelete, path, nil, headers)
96104
}
105+
106+
func UpgradeRequestHandler(h *RequestHandler) *RequestHandlerWithCtx {
107+
return &RequestHandlerWithCtx{
108+
// wrapped client will supply context.TODO() to all API calls
109+
Client: wrapBaseClientWithNoopCtx(h.Client),
110+
Edge: h.Edge,
111+
Region: h.Region,
112+
}
113+
}
114+
115+
type RequestHandlerWithCtx struct {
116+
Client BaseClientWithCtx
117+
Edge string
118+
Region string
119+
}
120+
121+
func (c *RequestHandlerWithCtx) sendRequest(ctx context.Context, method string, rawURL string, data url.Values,
122+
headers map[string]interface{}) (*http.Response, error) {
123+
parsedURL, err := c.BuildUrl(rawURL)
124+
if err != nil {
125+
return nil, err
126+
}
127+
128+
return c.Client.SendRequestWithCtx(ctx, method, parsedURL, data, headers)
129+
}
130+
131+
func NewRequestHandlerWithCtx(client BaseClientWithCtx) *RequestHandlerWithCtx {
132+
return &RequestHandlerWithCtx{
133+
Client: client,
134+
Edge: os.Getenv("TWILIO_EDGE"),
135+
Region: os.Getenv("TWILIO_REGION"),
136+
}
137+
}
138+
139+
// BuildUrl builds the target host string taking into account region and edge configurations.
140+
func (c *RequestHandlerWithCtx) BuildUrl(rawURL string) (string, error) {
141+
return buildUrlInternal(c.Edge, c.Region, rawURL)
142+
}
143+
144+
func (c *RequestHandlerWithCtx) Post(ctx context.Context, path string, bodyData url.Values, headers map[string]interface{}) (*http.Response, error) {
145+
return c.sendRequest(ctx, http.MethodPost, path, bodyData, headers)
146+
}
147+
148+
func (c *RequestHandlerWithCtx) Get(ctx context.Context, path string, queryData url.Values, headers map[string]interface{}) (*http.Response, error) {
149+
return c.sendRequest(ctx, http.MethodGet, path, queryData, headers)
150+
}
151+
152+
func (c *RequestHandlerWithCtx) Delete(ctx context.Context, path string, nothing url.Values, headers map[string]interface{}) (*http.Response, error) {
153+
return c.sendRequest(ctx, http.MethodDelete, path, nil, headers)
154+
}

0 commit comments

Comments
 (0)