Skip to content

Commit 6ce524c

Browse files
authored
transport/http: NewExplicitClient (#971)
* transport/http: NewExplicitClient * transport/http: improve NewClient/NewExplicitClient relationship
1 parent ad60314 commit 6ce524c

3 files changed

Lines changed: 84 additions & 36 deletions

File tree

transport/http/client.go

Lines changed: 46 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ type HTTPClient interface {
2121
// Client wraps a URL and provides a method that implements endpoint.Endpoint.
2222
type Client struct {
2323
client HTTPClient
24-
method string
25-
tgt *url.URL
26-
enc EncodeRequestFunc
24+
req CreateRequestFunc
2725
dec DecodeResponseFunc
2826
before []RequestFunc
2927
after []ClientResponseFunc
@@ -32,22 +30,18 @@ type Client struct {
3230
}
3331

3432
// NewClient constructs a usable Client for a single remote method.
35-
func NewClient(
36-
method string,
37-
tgt *url.URL,
38-
enc EncodeRequestFunc,
39-
dec DecodeResponseFunc,
40-
options ...ClientOption,
41-
) *Client {
33+
func NewClient(method string, tgt *url.URL, enc EncodeRequestFunc, dec DecodeResponseFunc, options ...ClientOption) *Client {
34+
return NewExplicitClient(makeCreateRequestFunc(method, tgt, enc), dec, options...)
35+
}
36+
37+
// NewExplicitClient is like NewClient but uses a CreateRequestFunc instead of a
38+
// method, target URL, and EncodeRequestFunc, which allows for more control over
39+
// the outgoing HTTP request.
40+
func NewExplicitClient(req CreateRequestFunc, dec DecodeResponseFunc, options ...ClientOption) *Client {
4241
c := &Client{
43-
client: http.DefaultClient,
44-
method: method,
45-
tgt: tgt,
46-
enc: enc,
47-
dec: dec,
48-
before: []RequestFunc{},
49-
after: []ClientResponseFunc{},
50-
bufferedStream: false,
42+
client: http.DefaultClient,
43+
req: req,
44+
dec: dec,
5145
}
5246
for _, option := range options {
5347
option(c)
@@ -64,33 +58,35 @@ func SetClient(client HTTPClient) ClientOption {
6458
return func(c *Client) { c.client = client }
6559
}
6660

67-
// ClientBefore sets the RequestFuncs that are applied to the outgoing HTTP
61+
// ClientBefore adds one or more RequestFuncs to be applied to the outgoing HTTP
6862
// request before it's invoked.
6963
func ClientBefore(before ...RequestFunc) ClientOption {
7064
return func(c *Client) { c.before = append(c.before, before...) }
7165
}
7266

73-
// ClientAfter sets the ClientResponseFuncs applied to the incoming HTTP
74-
// request prior to it being decoded. This is useful for obtaining anything off
75-
// of the response and adding onto the context prior to decoding.
67+
// ClientAfter adds one or more ClientResponseFuncs, which are applied to the
68+
// incoming HTTP response prior to it being decoded. This is useful for
69+
// obtaining anything off of the response and adding it into the context prior
70+
// to decoding.
7671
func ClientAfter(after ...ClientResponseFunc) ClientOption {
7772
return func(c *Client) { c.after = append(c.after, after...) }
7873
}
7974

80-
// ClientFinalizer is executed at the end of every HTTP request.
81-
// By default, no finalizer is registered.
75+
// ClientFinalizer adds one or more ClientFinalizerFuncs to be executed at the
76+
// end of every HTTP request. Finalizers are executed in the order in which they
77+
// were added. By default, no finalizer is registered.
8278
func ClientFinalizer(f ...ClientFinalizerFunc) ClientOption {
8379
return func(s *Client) { s.finalizer = append(s.finalizer, f...) }
8480
}
8581

86-
// BufferedStream sets whether the Response.Body is left open, allowing it
82+
// BufferedStream sets whether the HTTP response body is left open, allowing it
8783
// to be read from later. Useful for transporting a file as a buffered stream.
88-
// That body has to be Closed to propery end the request.
84+
// That body has to be drained and closed to properly end the request.
8985
func BufferedStream(buffered bool) ClientOption {
9086
return func(c *Client) { c.bufferedStream = buffered }
9187
}
9288

93-
// Endpoint returns a usable endpoint that invokes the remote endpoint.
89+
// Endpoint returns a usable Go kit endpoint that calls the remote HTTP endpoint.
9490
func (c Client) Endpoint() endpoint.Endpoint {
9591
return func(ctx context.Context, request interface{}) (interface{}, error) {
9692
ctx, cancel := context.WithCancel(ctx)
@@ -111,30 +107,25 @@ func (c Client) Endpoint() endpoint.Endpoint {
111107
}()
112108
}
113109

114-
req, err := http.NewRequest(c.method, c.tgt.String(), nil)
110+
req, err := c.req(ctx, request)
115111
if err != nil {
116112
cancel()
117113
return nil, err
118114
}
119115

120-
if err = c.enc(ctx, req, request); err != nil {
121-
cancel()
122-
return nil, err
123-
}
124-
125116
for _, f := range c.before {
126117
ctx = f(ctx, req)
127118
}
128119

129120
resp, err = c.client.Do(req.WithContext(ctx))
130-
131121
if err != nil {
132122
cancel()
133123
return nil, err
134124
}
135125

136-
// If we expect a buffered stream, we don't cancel the context when the endpoint returns.
137-
// Instead, we should call the cancel func when closing the response body.
126+
// If the caller asked for a buffered stream, we don't cancel the
127+
// context when the endpoint returns. Instead, we should call the
128+
// cancel func when closing the response body.
138129
if c.bufferedStream {
139130
resp.Body = bodyWithCancel{ReadCloser: resp.Body, cancel: cancel}
140131
} else {
@@ -207,3 +198,22 @@ func EncodeXMLRequest(c context.Context, r *http.Request, request interface{}) e
207198
r.Body = ioutil.NopCloser(&b)
208199
return xml.NewEncoder(&b).Encode(request)
209200
}
201+
202+
//
203+
//
204+
//
205+
206+
func makeCreateRequestFunc(method string, target *url.URL, enc EncodeRequestFunc) CreateRequestFunc {
207+
return func(ctx context.Context, request interface{}) (*http.Request, error) {
208+
req, err := http.NewRequest(method, target.String(), nil)
209+
if err != nil {
210+
return nil, err
211+
}
212+
213+
if err = enc(ctx, req, request); err != nil {
214+
return nil, err
215+
}
216+
217+
return req, nil
218+
}
219+
}

transport/http/client_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package http_test
33
import (
44
"bytes"
55
"context"
6+
"fmt"
67
"io"
78
"io/ioutil"
89
"net/http"
910
"net/http/httptest"
1011
"net/url"
12+
"strings"
1113
"testing"
1214
"time"
1315

@@ -297,6 +299,36 @@ func TestSetClient(t *testing.T) {
297299
}
298300
}
299301

302+
func TestNewExplicitClient(t *testing.T) {
303+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
304+
fmt.Fprintf(w, "%d", r.ContentLength)
305+
}))
306+
defer srv.Close()
307+
308+
req := func(ctx context.Context, request interface{}) (*http.Request, error) {
309+
req, _ := http.NewRequest("POST", srv.URL, strings.NewReader(request.(string)))
310+
return req, nil
311+
}
312+
313+
dec := func(_ context.Context, resp *http.Response) (response interface{}, err error) {
314+
buf, err := ioutil.ReadAll(resp.Body)
315+
resp.Body.Close()
316+
return string(buf), err
317+
}
318+
319+
client := httptransport.NewExplicitClient(req, dec)
320+
321+
request := "hello world"
322+
response, err := client.Endpoint()(context.Background(), request)
323+
if err != nil {
324+
t.Fatal(err)
325+
}
326+
327+
if want, have := "11", response.(string); want != have {
328+
t.Fatalf("want %q, have %q", want, have)
329+
}
330+
}
331+
300332
func mustParse(s string) *url.URL {
301333
u, err := url.Parse(s)
302334
if err != nil {

transport/http/encode_decode.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ type DecodeRequestFunc func(context.Context, *http.Request) (request interface{}
1717
// encodes the object directly to the request body.
1818
type EncodeRequestFunc func(context.Context, *http.Request, interface{}) error
1919

20+
// CreateRequestFunc creates an outgoing HTTP request based on the passed
21+
// request object. It's designed to be used in HTTP clients, for client-side
22+
// endpoints. It's a more powerful version of EncodeRequestFunc, and can be used
23+
// if more fine-grained control of the HTTP request is required.
24+
type CreateRequestFunc func(context.Context, interface{}) (*http.Request, error)
25+
2026
// EncodeResponseFunc encodes the passed response object to the HTTP response
2127
// writer. It's designed to be used in HTTP servers, for server-side
2228
// endpoints. One straightforward EncodeResponseFunc could be something that

0 commit comments

Comments
 (0)