-
Notifications
You must be signed in to change notification settings - Fork 6
/
http.go
347 lines (296 loc) · 9.29 KB
/
http.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
package restli
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/PapaCharlie/go-restli/restlicodec"
)
const (
ProtocolVersion = "2.0.0"
IDHeader = "X-RestLi-Id"
MethodHeader = "X-RestLi-Method"
ProtocolVersionHeader = "X-RestLi-Protocol-Version"
ErrorResponseHeader = "X-RestLi-Error-Response"
MethodOverrideHeader = "X-HTTP-Method-Override"
ContentTypeHeader = "Content-Type"
MultipartMixedContentType = "multipart/mixed"
MultipartBoundary = "boundary"
ApplicationJsonContentType = "application/json"
FormUrlEncodedContentType = "application/x-www-form-urlencoded"
)
type Method int
// Disabled until https://github.com/golang/go/issues/45218 is resolved: go:generate stringer -type=Method -trimprefix Method_
const (
Method_Unknown = Method(iota)
Method_get
Method_create
Method_delete
Method_update
Method_partial_update
Method_batch_get
Method_batch_create
Method_batch_delete
Method_batch_update
Method_batch_partial_update
Method_get_all
Method_action
Method_finder
)
var MethodNameMapping = func() map[string]Method {
mapping := make(map[string]Method)
for m := Method_get; m <= Method_finder; m++ {
mapping[m.String()] = m
}
return mapping
}()
type Client struct {
*http.Client
HostnameResolver HostnameResolver
// Whether missing fields in a restli response should cause a MissingRequiredFields error to be returned. Note that
// even if the error is returned, the response will still be fully deserialized.
StrictResponseDeserialization bool
// When greater than 0, this enables request tunnelling. When a request's query is longer than this value, the
// request will instead be sent via POST, with the query encoded as a form query and the MethodOverrideHeader set to
// the original HTTP method.
QueryTunnellingThreshold int
}
func (c *Client) formatQueryUrl(rp ResourcePath, query QueryParamsEncoder) (*url.URL, error) {
path, err := rp.ResourcePath()
if err != nil {
return nil, err
}
if query != nil {
var params string
params, err = query.EncodeQueryParams()
if err != nil {
return nil, err
}
path += "?" + params
}
u, err := url.Parse(path)
if err != nil {
return nil, err
}
root := rp.RootResource()
hostUrl, err := c.HostnameResolver.ResolveHostnameAndContextForQuery(root, u)
if err != nil {
return nil, err
}
resolvedPath := "/" + strings.TrimSuffix(strings.TrimPrefix(hostUrl.EscapedPath(), "/"), "/")
if resolvedPath == "/" {
return hostUrl.ResolveReference(u), nil
}
if idx := strings.Index(resolvedPath, "/"+root); idx >= 0 &&
(len(resolvedPath) == idx+len(root)+1 || resolvedPath[idx+len(root)+1] == '/') {
resolvedPath = resolvedPath[:idx]
}
return hostUrl.Parse(resolvedPath + u.RequestURI())
}
type contextKey int
const (
extraRequestHeadersKey contextKey = iota
responseHeadersCaptorKey
methodCtxKey
resourcePathSegmentsCtxKey
entitySegmentsCtxKey
finderNameCtxKey
actionNameCtxKey
)
// ExtraRequestHeaders returns a context.Context to be passed into any generated client methods. Upon request creation,
// the given function will be executed, and the headers will be added to the request before being sent. Note that these
// headers will not override any existing headers such as Content-Type. Only new headers will be added to the request.
func ExtraRequestHeaders(ctx context.Context, f func() (http.Header, error)) context.Context {
return context.WithValue(ctx, extraRequestHeadersKey, f)
}
// AddResponseHeadersCaptor returns a new context and a http.Header. If the returned context is passed into any Client
// request, the returned http.Header will be populated with all the headers returned by the server.
func AddResponseHeadersCaptor(ctx context.Context) (context.Context, http.Header) {
headers := http.Header{}
ctx = context.WithValue(ctx, responseHeadersCaptorKey, headers)
return ctx, headers
}
func newRequest(
c *Client,
ctx context.Context,
rp ResourcePath,
query QueryParamsEncoder,
httpMethod string,
method Method,
contents restlicodec.Marshaler,
excludedFields restlicodec.PathSpec,
) (req *http.Request, err error) {
u, err := c.formatQueryUrl(rp, query)
if err != nil {
return nil, err
}
var body []byte
if contents != nil {
writer := restlicodec.NewCompactJsonWriterWithExcludedFields(excludedFields)
err = contents.MarshalRestLi(writer)
if err != nil {
return nil, err
}
body = []byte(writer.Finalize())
}
headers := http.Header{}
if body != nil {
headers.Set(ContentTypeHeader, ApplicationJsonContentType)
}
if c.QueryTunnellingThreshold > 0 && len(u.RawQuery) > c.QueryTunnellingThreshold {
var tunnelHeaders http.Header
body, tunnelHeaders = EncodeTunnelledQuery(httpMethod, u.RawQuery, body)
for k := range tunnelHeaders {
headers.Set(k, tunnelHeaders.Get(k))
}
httpMethod = http.MethodPost
u.RawQuery = ""
}
req, err = http.NewRequestWithContext(ctx, httpMethod, u.String(), bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set(ProtocolVersionHeader, ProtocolVersion)
req.Header.Set(MethodHeader, method.String())
req.Header.Set("Accept", ApplicationJsonContentType)
for k, v := range headers {
req.Header[k] = v
}
if extraHeaders, ok := req.Context().Value(extraRequestHeadersKey).(func() (http.Header, error)); ok {
extras, err := extraHeaders()
if err != nil {
return nil, err
}
for k, v := range extras {
if _, ok = req.Header[k]; !ok {
req.Header[k] = v
}
}
}
return req, nil
}
// NewGetRequest creates a GET http.Request and sets the expected rest.li headers
func NewGetRequest(
c *Client,
ctx context.Context,
rp ResourcePath,
query QueryParamsEncoder,
method Method,
) (*http.Request, error) {
return newRequest(c, ctx, rp, query, http.MethodGet, method, nil, nil)
}
// NewDeleteRequest creates a DELETE http.Request and sets the expected rest.li headers
func NewDeleteRequest(
c *Client,
ctx context.Context,
rp ResourcePath,
query QueryParamsEncoder,
method Method,
) (*http.Request, error) {
return newRequest(c, ctx, rp, query, http.MethodDelete, method, nil, nil)
}
func NewCreateRequest(
c *Client,
ctx context.Context,
rp ResourcePath,
query QueryParamsEncoder,
method Method,
create restlicodec.Marshaler,
readOnlyFields restlicodec.PathSpec,
) (*http.Request, error) {
return NewJsonRequest(c, ctx, rp, query, http.MethodPost, method, create, readOnlyFields)
}
// NewJsonRequest creates an http.Request with the given HTTP method and rest.li method, and populates the body of the
// request with the given restlicodec.Marshaler contents (see RawJsonRequest)
func NewJsonRequest(
c *Client,
ctx context.Context,
rp ResourcePath,
query QueryParamsEncoder,
httpMethod string,
restLiMethod Method,
contents restlicodec.Marshaler,
excludedFields restlicodec.PathSpec,
) (*http.Request, error) {
if contents == nil {
return nil, fmt.Errorf("go-restli: Must provide non-nil contents")
}
return newRequest(c, ctx, rp, query, httpMethod, restLiMethod, contents, excludedFields)
}
// Do is a very thin shim between the standard http.Client.Do. All it does it parse the response into a Error if
// the RestLi error header is set. A non-nil Response with a non-nil error will only occur if http.Client.Do returns
// such values (see the corresponding documentation). Otherwise, the response will only be non-nil if the error is nil.
// All (and only) network-related errors will be of type *url.Error. Other types of errors such as parse errors will use
// different error types.
func (c *Client) Do(req *http.Request) (*http.Response, error) {
res, err := c.Client.Do(req)
if err != nil {
return res, err
}
err = IsErrorResponse(res)
if err != nil {
return nil, err
}
return res, nil
}
// DoAndUnmarshal calls Do and attempts to unmarshal the response into the given value. The response body will always be
// read to EOF and closed, to ensure the connection can be reused.
func DoAndUnmarshal[V any](
c *Client,
req *http.Request,
unmarshaler restlicodec.GenericUnmarshaler[V],
) (v V, res *http.Response, err error) {
data, res, err := c.do(req)
if err != nil {
return v, res, err
}
r, err := restlicodec.NewJsonReader(data)
if err != nil {
return v, res, err
}
v, err = unmarshaler(r)
if _, mfe := err.(*restlicodec.MissingRequiredFieldsError); mfe && !c.StrictResponseDeserialization {
err = nil
}
return v, res, err
}
// DoAndIgnore calls Do and drops the response's body. The response body will always be read to EOF and closed, to
// ensure the connection can be reused.
func DoAndIgnore(c *Client, req *http.Request) (*http.Response, error) {
_, res, err := c.do(req)
return res, err
}
func (c *Client) do(req *http.Request) ([]byte, *http.Response, error) {
res, err := c.Do(req)
if err != nil {
return nil, res, err
}
if v := res.Header.Get(ProtocolVersionHeader); v != ProtocolVersion {
return nil, nil, &UnsupportedRestLiProtocolVersion{v}
}
data, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, nil, &url.Error{
Op: "ReadResponse",
URL: req.URL.String(),
Err: err,
}
}
err = res.Body.Close()
if err != nil {
return nil, nil, &url.Error{
Op: "CloseResponse",
URL: req.URL.String(),
Err: err,
}
}
if resHeaders, ok := req.Context().Value(responseHeadersCaptorKey).(http.Header); ok {
for k, v := range res.Header {
resHeaders[k] = v
}
}
return data, res, nil
}