1 // Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
2 // resty source code and usage is governed by a MIT style
3 // license that can be found in the LICENSE file.
4 5 package resty
6 7 import (
8 "bytes"
9 "context"
10 "encoding/json"
11 "encoding/xml"
12 "fmt"
13 "io"
14 "net"
15 "net/http"
16 "net/url"
17 "reflect"
18 "strings"
19 "time"
20 )
21 22 //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
23 // Request struct and methods
24 //_______________________________________________________________________
25 26 // Request struct is used to compose and fire individual requests from
27 // Resty client. The [Request] provides an option to override client-level
28 // settings and also an option for the request composition.
29 type Request struct {
30 URL string
31 Method string
32 Token string
33 AuthScheme string
34 QueryParam url.Values
35 FormData url.Values
36 PathParams map[string]string
37 RawPathParams map[string]string
38 Header http.Header
39 Time time.Time
40 Body interface{}
41 Result interface{}
42 resultCurlCmd *string
43 Error interface{}
44 RawRequest *http.Request
45 SRV *SRVRecord
46 UserInfo *User
47 Cookies []*http.Cookie
48 Debug bool
49 50 // Attempt is to represent the request attempt made during a Resty
51 // request execution flow, including retry count.
52 Attempt int
53 54 isMultiPart bool
55 isFormData bool
56 setContentLength bool
57 isSaveResponse bool
58 notParseResponse bool
59 jsonEscapeHTML bool
60 trace bool
61 outputFile string
62 fallbackContentType string
63 forceContentType string
64 ctx context.Context
65 values map[string]interface{}
66 client *Client
67 bodyBuf *bytes.Buffer
68 clientTrace *clientTrace
69 log Logger
70 multipartBoundary string
71 multipartFiles []*File
72 multipartFields []*MultipartField
73 retryConditions []RetryConditionFunc
74 responseBodyLimit int
75 generateCurlOnDebug bool
76 unescapeQueryParams bool
77 }
78 79 // GenerateCurlCommand method generates the CURL command for the request.
80 func (r *Request) GenerateCurlCommand() string {
81 if !(r.Debug && r.generateCurlOnDebug) {
82 return ""
83 }
84 85 if r.resultCurlCmd != nil {
86 return *r.resultCurlCmd
87 }
88 89 if r.RawRequest == nil {
90 r.client.executeBefore(r) // mock with r.Get("/")
91 }
92 if r.resultCurlCmd == nil {
93 r.resultCurlCmd = new(string)
94 }
95 *r.resultCurlCmd = buildCurlRequest(r.RawRequest, r.client.httpClient.Jar)
96 return *r.resultCurlCmd
97 }
98 99 // Context method returns the Context if it is already set in the [Request]
100 // otherwise, it creates a new one using [context.Background].
101 func (r *Request) Context() context.Context {
102 if r.ctx == nil {
103 return context.Background()
104 }
105 return r.ctx
106 }
107 108 // SetContext method sets the [context.Context] for current [Request]. It allows
109 // to interrupt the request execution if `ctx.Done()` channel is closed.
110 // See https://blog.golang.org/context article and the package [context]
111 // documentation.
112 func (r *Request) SetContext(ctx context.Context) *Request {
113 r.ctx = ctx
114 return r
115 }
116 117 // SetHeader method sets a single header field and its value in the current request.
118 //
119 // For Example: To set `Content-Type` and `Accept` as `application/json`.
120 //
121 // client.R().
122 // SetHeader("Content-Type", "application/json").
123 // SetHeader("Accept", "application/json")
124 //
125 // It overrides the header value set at the client instance level.
126 func (r *Request) SetHeader(header, value string) *Request {
127 r.Header.Set(header, value)
128 return r
129 }
130 131 // SetHeaders method sets multiple header fields and their values at one go in the current request.
132 //
133 // For Example: To set `Content-Type` and `Accept` as `application/json`
134 //
135 // client.R().
136 // SetHeaders(map[string]string{
137 // "Content-Type": "application/json",
138 // "Accept": "application/json",
139 // })
140 //
141 // It overrides the header value set at the client instance level.
142 func (r *Request) SetHeaders(headers map[string]string) *Request {
143 for h, v := range headers {
144 r.SetHeader(h, v)
145 }
146 return r
147 }
148 149 // SetHeaderMultiValues sets multiple header fields and their values as a list of strings in the current request.
150 //
151 // For Example: To set `Accept` as `text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8`
152 //
153 // client.R().
154 // SetHeaderMultiValues(map[string][]string{
155 // "Accept": []string{"text/html", "application/xhtml+xml", "application/xml;q=0.9", "image/webp", "*/*;q=0.8"},
156 // })
157 //
158 // It overrides the header value set at the client instance level.
159 func (r *Request) SetHeaderMultiValues(headers map[string][]string) *Request {
160 for key, values := range headers {
161 r.SetHeader(key, strings.Join(values, ", "))
162 }
163 return r
164 }
165 166 // SetHeaderVerbatim method sets a single header field and its value verbatim in the current request.
167 //
168 // For Example: To set `all_lowercase` and `UPPERCASE` as `available`.
169 //
170 // client.R().
171 // SetHeaderVerbatim("all_lowercase", "available").
172 // SetHeaderVerbatim("UPPERCASE", "available")
173 //
174 // It overrides the header value set at the client instance level.
175 func (r *Request) SetHeaderVerbatim(header, value string) *Request {
176 r.Header[header] = []string{value}
177 return r
178 }
179 180 // SetQueryParam method sets a single parameter and its value in the current request.
181 // It will be formed as a query string for the request.
182 //
183 // For Example: `search=kitchen%20papers&size=large` in the URL after the `?` mark.
184 //
185 // client.R().
186 // SetQueryParam("search", "kitchen papers").
187 // SetQueryParam("size", "large")
188 //
189 // It overrides the query parameter value set at the client instance level.
190 func (r *Request) SetQueryParam(param, value string) *Request {
191 r.QueryParam.Set(param, value)
192 return r
193 }
194 195 // SetQueryParams method sets multiple parameters and their values at one go in the current request.
196 // It will be formed as a query string for the request.
197 //
198 // For Example: `search=kitchen%20papers&size=large` in the URL after the `?` mark.
199 //
200 // client.R().
201 // SetQueryParams(map[string]string{
202 // "search": "kitchen papers",
203 // "size": "large",
204 // })
205 //
206 // It overrides the query parameter value set at the client instance level.
207 func (r *Request) SetQueryParams(params map[string]string) *Request {
208 for p, v := range params {
209 r.SetQueryParam(p, v)
210 }
211 return r
212 }
213 214 // SetUnescapeQueryParams method sets the unescape query parameters choice for request URL.
215 // To prevent broken URL, resty replaces space (" ") with "+" in the query parameters.
216 //
217 // This method overrides the value set by [Client.SetUnescapeQueryParams]
218 //
219 // NOTE: Request failure is possible due to non-standard usage of Unescaped Query Parameters.
220 func (r *Request) SetUnescapeQueryParams(unescape bool) *Request {
221 r.unescapeQueryParams = unescape
222 return r
223 }
224 225 // SetQueryParamsFromValues method appends multiple parameters with multi-value
226 // ([url.Values]) at one go in the current request. It will be formed as
227 // query string for the request.
228 //
229 // For Example: `status=pending&status=approved&status=open` in the URL after the `?` mark.
230 //
231 // client.R().
232 // SetQueryParamsFromValues(url.Values{
233 // "status": []string{"pending", "approved", "open"},
234 // })
235 //
236 // It overrides the query parameter value set at the client instance level.
237 func (r *Request) SetQueryParamsFromValues(params url.Values) *Request {
238 for p, v := range params {
239 for _, pv := range v {
240 r.QueryParam.Add(p, pv)
241 }
242 }
243 return r
244 }
245 246 // SetQueryString method provides the ability to use string as an input to set URL query string for the request.
247 //
248 // client.R().
249 // SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more")
250 //
251 // It overrides the query parameter value set at the client instance level.
252 func (r *Request) SetQueryString(query string) *Request {
253 params, err := url.ParseQuery(strings.TrimSpace(query))
254 if err == nil {
255 for p, v := range params {
256 for _, pv := range v {
257 r.QueryParam.Add(p, pv)
258 }
259 }
260 } else {
261 r.log.Errorf("%v", err)
262 }
263 return r
264 }
265 266 // SetFormData method sets Form parameters and their values for the current request.
267 // It applies only to HTTP methods `POST` and `PUT`, and by default requests
268 // content type would be set as `application/x-www-form-urlencoded`.
269 //
270 // client.R().
271 // SetFormData(map[string]string{
272 // "access_token": "BC594900-518B-4F7E-AC75-BD37F019E08F",
273 // "user_id": "3455454545",
274 // })
275 //
276 // It overrides the form data value set at the client instance level.
277 func (r *Request) SetFormData(data map[string]string) *Request {
278 for k, v := range data {
279 r.FormData.Set(k, v)
280 }
281 return r
282 }
283 284 // SetFormDataFromValues method appends multiple form parameters with multi-value
285 // ([url.Values]) at one go in the current request.
286 //
287 // client.R().
288 // SetFormDataFromValues(url.Values{
289 // "search_criteria": []string{"book", "glass", "pencil"},
290 // })
291 //
292 // It overrides the form data value set at the client instance level.
293 func (r *Request) SetFormDataFromValues(data url.Values) *Request {
294 for k, v := range data {
295 for _, kv := range v {
296 r.FormData.Add(k, kv)
297 }
298 }
299 return r
300 }
301 302 // SetBody method sets the request body for the request. It supports various practical needs as easy.
303 // It's quite handy and powerful. Supported request body data types are `string`,
304 // `[]byte`, `struct`, `map`, `slice` and [io.Reader].
305 //
306 // Body value can be pointer or non-pointer. Automatic marshalling for JSON and XML content type, if it is `struct`, `map`, or `slice`.
307 //
308 // NOTE: [io.Reader] is processed in bufferless mode while sending a request.
309 //
310 // For Example:
311 //
312 // `struct` gets marshaled based on the request header `Content-Type`.
313 //
314 // client.R().
315 // SetBody(User{
316 // Username: "jeeva@myjeeva.com",
317 // Password: "welcome2resty",
318 // })
319 //
320 // 'map` gets marshaled based on the request header `Content-Type`.
321 //
322 // client.R().
323 // SetBody(map[string]interface{}{
324 // "username": "jeeva@myjeeva.com",
325 // "password": "welcome2resty",
326 // "address": &Address{
327 // Address1: "1111 This is my street",
328 // Address2: "Apt 201",
329 // City: "My City",
330 // State: "My State",
331 // ZipCode: 00000,
332 // },
333 // })
334 //
335 // `string` as a body input. Suitable for any need as a string input.
336 //
337 // client.R().
338 // SetBody(`{
339 // "username": "jeeva@getrightcare.com",
340 // "password": "admin"
341 // }`)
342 //
343 // `[]byte` as a body input. Suitable for raw requests such as file upload, serialize & deserialize, etc.
344 //
345 // client.R().
346 // SetBody([]byte("This is my raw request, sent as-is"))
347 //
348 // and so on.
349 func (r *Request) SetBody(body interface{}) *Request {
350 r.Body = body
351 return r
352 }
353 354 // SetResult method is to register the response `Result` object for automatic
355 // unmarshalling of the HTTP response if the response status code is
356 // between 200 and 299, and the content type is JSON or XML.
357 //
358 // Note: [Request.SetResult] input can be a pointer or non-pointer.
359 //
360 // The pointer with handle
361 //
362 // authToken := &AuthToken{}
363 // client.R().SetResult(authToken)
364 //
365 // // Can be accessed via -
366 // fmt.Println(authToken) OR fmt.Println(response.Result().(*AuthToken))
367 //
368 // OR -
369 //
370 // The pointer without handle or non-pointer
371 //
372 // client.R().SetResult(&AuthToken{})
373 // // OR
374 // client.R().SetResult(AuthToken{})
375 //
376 // // Can be accessed via -
377 // fmt.Println(response.Result().(*AuthToken))
378 func (r *Request) SetResult(res interface{}) *Request {
379 if res != nil {
380 r.Result = getPointer(res)
381 }
382 return r
383 }
384 385 // SetError method is to register the request `Error` object for automatic unmarshalling for the request,
386 // if the response status code is greater than 399 and the content type is either JSON or XML.
387 //
388 // NOTE: [Request.SetError] input can be a pointer or non-pointer.
389 //
390 // client.R().SetError(&AuthError{})
391 // // OR
392 // client.R().SetError(AuthError{})
393 //
394 // Accessing an error value from response instance.
395 //
396 // response.Error().(*AuthError)
397 //
398 // If this request Error object is nil, Resty will use the client-level error object Type if it is set.
399 func (r *Request) SetError(err interface{}) *Request {
400 r.Error = getPointer(err)
401 return r
402 }
403 404 // SetFile method sets a single file field name and its path for multipart upload.
405 //
406 // client.R().
407 // SetFile("my_file", "/Users/jeeva/Gas Bill - Sep.pdf")
408 func (r *Request) SetFile(param, filePath string) *Request {
409 r.isMultiPart = true
410 r.FormData.Set("@"+param, filePath)
411 return r
412 }
413 414 // SetFiles method sets multiple file field names and their paths for multipart uploads.
415 //
416 // client.R().
417 // SetFiles(map[string]string{
418 // "my_file1": "/Users/jeeva/Gas Bill - Sep.pdf",
419 // "my_file2": "/Users/jeeva/Electricity Bill - Sep.pdf",
420 // "my_file3": "/Users/jeeva/Water Bill - Sep.pdf",
421 // })
422 func (r *Request) SetFiles(files map[string]string) *Request {
423 r.isMultiPart = true
424 for f, fp := range files {
425 r.FormData.Set("@"+f, fp)
426 }
427 return r
428 }
429 430 // SetFileReader method is to set a file using [io.Reader] for multipart upload.
431 //
432 // client.R().
433 // SetFileReader("profile_img", "my-profile-img.png", bytes.NewReader(profileImgBytes)).
434 // SetFileReader("notes", "user-notes.txt", bytes.NewReader(notesBytes))
435 func (r *Request) SetFileReader(param, fileName string, reader io.Reader) *Request {
436 r.isMultiPart = true
437 r.multipartFiles = append(r.multipartFiles, &File{
438 Name: fileName,
439 ParamName: param,
440 Reader: reader,
441 })
442 return r
443 }
444 445 // SetMultipartFormData method allows simple form data to be attached to the request
446 // as `multipart:form-data`
447 func (r *Request) SetMultipartFormData(data map[string]string) *Request {
448 for k, v := range data {
449 r = r.SetMultipartField(k, "", "", strings.NewReader(v))
450 }
451 452 return r
453 }
454 455 // SetMultipartField method sets custom data with Content-Type using [io.Reader] for multipart upload.
456 func (r *Request) SetMultipartField(param, fileName, contentType string, reader io.Reader) *Request {
457 r.isMultiPart = true
458 r.multipartFields = append(r.multipartFields, &MultipartField{
459 Param: param,
460 FileName: fileName,
461 ContentType: contentType,
462 Reader: reader,
463 })
464 return r
465 }
466 467 // SetMultipartFields method sets multiple data fields using [io.Reader] for multipart upload.
468 //
469 // For Example:
470 //
471 // client.R().SetMultipartFields(
472 // &resty.MultipartField{
473 // Param: "uploadManifest1",
474 // FileName: "upload-file-1.json",
475 // ContentType: "application/json",
476 // Reader: strings.NewReader(`{"input": {"name": "Uploaded document 1", "_filename" : ["file1.txt"]}}`),
477 // },
478 // &resty.MultipartField{
479 // Param: "uploadManifest2",
480 // FileName: "upload-file-2.json",
481 // ContentType: "application/json",
482 // Reader: strings.NewReader(`{"input": {"name": "Uploaded document 2", "_filename" : ["file2.txt"]}}`),
483 // })
484 //
485 // If you have a `slice` of fields already, then call-
486 //
487 // client.R().SetMultipartFields(fields...)
488 func (r *Request) SetMultipartFields(fields ...*MultipartField) *Request {
489 r.isMultiPart = true
490 r.multipartFields = append(r.multipartFields, fields...)
491 return r
492 }
493 494 // SetMultipartBoundary method sets the custom multipart boundary for the multipart request.
495 // Typically, the `mime/multipart` package generates a random multipart boundary if not provided.
496 func (r *Request) SetMultipartBoundary(boundary string) *Request {
497 r.multipartBoundary = boundary
498 return r
499 }
500 501 // SetContentLength method sets the current request's HTTP header `Content-Length` value.
502 // By default, Resty won't set `Content-Length`.
503 //
504 // See [Client.SetContentLength]
505 //
506 // client.R().SetContentLength(true)
507 //
508 // It overrides the value set at the client instance level.
509 func (r *Request) SetContentLength(l bool) *Request {
510 r.setContentLength = l
511 return r
512 }
513 514 // SetBasicAuth method sets the basic authentication header in the current HTTP request.
515 //
516 // For Example:
517 //
518 // Authorization: Basic <base64-encoded-value>
519 //
520 // To set the header for username "go-resty" and password "welcome"
521 //
522 // client.R().SetBasicAuth("go-resty", "welcome")
523 //
524 // It overrides the credentials set by method [Client.SetBasicAuth].
525 func (r *Request) SetBasicAuth(username, password string) *Request {
526 r.UserInfo = &User{Username: username, Password: password}
527 return r
528 }
529 530 // SetAuthToken method sets the auth token header(Default Scheme: Bearer) in the current HTTP request. Header example:
531 //
532 // Authorization: Bearer <auth-token-value-comes-here>
533 //
534 // For Example: To set auth token BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F
535 //
536 // client.R().SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F")
537 //
538 // It overrides the Auth token set by method [Client.SetAuthToken].
539 func (r *Request) SetAuthToken(token string) *Request {
540 r.Token = token
541 return r
542 }
543 544 // SetAuthScheme method sets the auth token scheme type in the HTTP request.
545 //
546 // Example Header value structure:
547 //
548 // Authorization: <auth-scheme-value-set-here> <auth-token-value>
549 //
550 // For Example: To set the scheme to use OAuth
551 //
552 // client.R().SetAuthScheme("OAuth")
553 //
554 // // The outcome will be -
555 // Authorization: OAuth <auth-token-value>
556 //
557 // Information about Auth schemes can be found in [RFC 7235], IANA [HTTP Auth schemes]
558 //
559 // It overrides the `Authorization` scheme set by method [Client.SetAuthScheme].
560 //
561 // [RFC 7235]: https://tools.ietf.org/html/rfc7235
562 // [HTTP Auth schemes]: https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml#authschemes
563 func (r *Request) SetAuthScheme(scheme string) *Request {
564 r.AuthScheme = scheme
565 return r
566 }
567 568 // SetDigestAuth method sets the Digest Access auth scheme for the HTTP request.
569 // If a server responds with 401 and sends a Digest challenge in the WWW-Authenticate Header,
570 // the request will be resent with the appropriate Authorization Header.
571 //
572 // For Example: To set the Digest scheme with username "Mufasa" and password "Circle Of Life"
573 //
574 // client.R().SetDigestAuth("Mufasa", "Circle Of Life")
575 //
576 // Information about Digest Access Authentication can be found in [RFC 7616]
577 //
578 // It overrides the digest username and password set by method [Client.SetDigestAuth].
579 //
580 // [RFC 7616]: https://datatracker.ietf.org/doc/html/rfc7616
581 func (r *Request) SetDigestAuth(username, password string) *Request {
582 oldTransport := r.client.httpClient.Transport
583 r.client.OnBeforeRequest(func(c *Client, _ *Request) error {
584 c.httpClient.Transport = &digestTransport{
585 digestCredentials: digestCredentials{username, password},
586 transport: oldTransport,
587 }
588 return nil
589 })
590 r.client.OnAfterResponse(func(c *Client, _ *Response) error {
591 c.httpClient.Transport = oldTransport
592 return nil
593 })
594 595 return r
596 }
597 598 // SetOutput method sets the output file for the current HTTP request. The current
599 // HTTP response will be saved in the given file. It is similar to the `curl -o` flag.
600 //
601 // Absolute path or relative path can be used.
602 //
603 // If it is a relative path, then the output file goes under the output directory, as mentioned
604 // in the [Client.SetOutputDirectory].
605 //
606 // client.R().
607 // SetOutput("/Users/jeeva/Downloads/ReplyWithHeader-v5.1-beta.zip").
608 // Get("http://bit.ly/1LouEKr")
609 //
610 // NOTE: In this scenario [Response.Body] might be nil.
611 func (r *Request) SetOutput(file string) *Request {
612 r.outputFile = file
613 r.isSaveResponse = true
614 return r
615 }
616 617 // SetSRV method sets the details to query the service SRV record and execute the
618 // request.
619 //
620 // client.R().
621 // SetSRV(SRVRecord{"web", "testservice.com"}).
622 // Get("/get")
623 func (r *Request) SetSRV(srv *SRVRecord) *Request {
624 r.SRV = srv
625 return r
626 }
627 628 // SetDoNotParseResponse method instructs Resty not to parse the response body automatically.
629 // Resty exposes the raw response body as [io.ReadCloser]. If you use it, do not
630 // forget to close the body, otherwise, you might get into connection leaks, and connection
631 // reuse may not happen.
632 //
633 // NOTE: [Response] middlewares are not executed using this option. You have
634 // taken over the control of response parsing from Resty.
635 func (r *Request) SetDoNotParseResponse(parse bool) *Request {
636 r.notParseResponse = parse
637 return r
638 }
639 640 // SetResponseBodyLimit method sets a maximum body size limit in bytes on response,
641 // avoid reading too much data to memory.
642 //
643 // Client will return [resty.ErrResponseBodyTooLarge] if the body size of the body
644 // in the uncompressed response is larger than the limit.
645 // Body size limit will not be enforced in the following cases:
646 // - ResponseBodyLimit <= 0, which is the default behavior.
647 // - [Request.SetOutput] is called to save response data to the file.
648 // - "DoNotParseResponse" is set for client or request.
649 //
650 // It overrides the value set at the client instance level. see [Client.SetResponseBodyLimit]
651 func (r *Request) SetResponseBodyLimit(v int) *Request {
652 r.responseBodyLimit = v
653 return r
654 }
655 656 // SetPathParam method sets a single URL path key-value pair in the
657 // Resty current request instance.
658 //
659 // client.R().SetPathParam("userId", "sample@sample.com")
660 //
661 // Result:
662 // URL - /v1/users/{userId}/details
663 // Composed URL - /v1/users/sample@sample.com/details
664 //
665 // client.R().SetPathParam("path", "groups/developers")
666 //
667 // Result:
668 // URL - /v1/users/{userId}/details
669 // Composed URL - /v1/users/groups%2Fdevelopers/details
670 //
671 // It replaces the value of the key while composing the request URL.
672 // The values will be escaped using function [url.PathEscape].
673 //
674 // It overrides the path parameter set at the client instance level.
675 func (r *Request) SetPathParam(param, value string) *Request {
676 r.PathParams[param] = value
677 return r
678 }
679 680 // SetPathParams method sets multiple URL path key-value pairs at one go in the
681 // Resty current request instance.
682 //
683 // client.R().SetPathParams(map[string]string{
684 // "userId": "sample@sample.com",
685 // "subAccountId": "100002",
686 // "path": "groups/developers",
687 // })
688 //
689 // Result:
690 // URL - /v1/users/{userId}/{subAccountId}/{path}/details
691 // Composed URL - /v1/users/sample@sample.com/100002/groups%2Fdevelopers/details
692 //
693 // It replaces the value of the key while composing the request URL.
694 // The values will be escaped using function [url.PathEscape].
695 //
696 // It overrides the path parameter set at the client instance level.
697 func (r *Request) SetPathParams(params map[string]string) *Request {
698 for p, v := range params {
699 r.SetPathParam(p, v)
700 }
701 return r
702 }
703 704 // SetRawPathParam method sets a single URL path key-value pair in the
705 // Resty current request instance.
706 //
707 // client.R().SetPathParam("userId", "sample@sample.com")
708 //
709 // Result:
710 // URL - /v1/users/{userId}/details
711 // Composed URL - /v1/users/sample@sample.com/details
712 //
713 // client.R().SetPathParam("path", "groups/developers")
714 //
715 // Result:
716 // URL - /v1/users/{userId}/details
717 // Composed URL - /v1/users/groups/developers/details
718 //
719 // It replaces the value of the key while composing the request URL.
720 // The value will be used as-is and has not been escaped.
721 //
722 // It overrides the raw path parameter set at the client instance level.
723 func (r *Request) SetRawPathParam(param, value string) *Request {
724 r.RawPathParams[param] = value
725 return r
726 }
727 728 // SetRawPathParams method sets multiple URL path key-value pairs at one go in the
729 // Resty current request instance.
730 //
731 // client.R().SetPathParams(map[string]string{
732 // "userId": "sample@sample.com",
733 // "subAccountId": "100002",
734 // "path": "groups/developers",
735 // })
736 //
737 // Result:
738 // URL - /v1/users/{userId}/{subAccountId}/{path}/details
739 // Composed URL - /v1/users/sample@sample.com/100002/groups/developers/details
740 //
741 // It replaces the value of the key while composing the request URL.
742 // The value will be used as-is and has not been escaped.
743 //
744 // It overrides the raw path parameter set at the client instance level.
745 func (r *Request) SetRawPathParams(params map[string]string) *Request {
746 for p, v := range params {
747 r.SetRawPathParam(p, v)
748 }
749 return r
750 }
751 752 // ExpectContentType method allows to provide fallback `Content-Type` for automatic unmarshalling
753 // when the `Content-Type` response header is unavailable.
754 func (r *Request) ExpectContentType(contentType string) *Request {
755 r.fallbackContentType = contentType
756 return r
757 }
758 759 // ForceContentType method provides a strong sense of response `Content-Type` for
760 // automatic unmarshalling. Resty gives this a higher priority than the `Content-Type`
761 // response header.
762 //
763 // This means that if both [Request.ForceContentType] is set and
764 // the response `Content-Type` is available, `ForceContentType` will win.
765 func (r *Request) ForceContentType(contentType string) *Request {
766 r.forceContentType = contentType
767 return r
768 }
769 770 // SetJSONEscapeHTML method enables or disables the HTML escape on JSON marshal.
771 // By default, escape HTML is false.
772 //
773 // NOTE: This option only applies to the standard JSON Marshaller used by Resty.
774 //
775 // It overrides the value set at the client instance level, see [Client.SetJSONEscapeHTML]
776 func (r *Request) SetJSONEscapeHTML(b bool) *Request {
777 r.jsonEscapeHTML = b
778 return r
779 }
780 781 // SetCookie method appends a single cookie in the current request instance.
782 //
783 // client.R().SetCookie(&http.Cookie{
784 // Name:"go-resty",
785 // Value:"This is cookie value",
786 // })
787 //
788 // NOTE: Method appends the Cookie value into existing Cookie even if its already existing.
789 func (r *Request) SetCookie(hc *http.Cookie) *Request {
790 r.Cookies = append(r.Cookies, hc)
791 return r
792 }
793 794 // SetCookies method sets an array of cookies in the current request instance.
795 //
796 // cookies := []*http.Cookie{
797 // &http.Cookie{
798 // Name:"go-resty-1",
799 // Value:"This is cookie 1 value",
800 // },
801 // &http.Cookie{
802 // Name:"go-resty-2",
803 // Value:"This is cookie 2 value",
804 // },
805 // }
806 //
807 // // Setting a cookies into resty's current request
808 // client.R().SetCookies(cookies)
809 //
810 // NOTE: Method appends the Cookie value into existing Cookie even if its already existing.
811 func (r *Request) SetCookies(rs []*http.Cookie) *Request {
812 r.Cookies = append(r.Cookies, rs...)
813 return r
814 }
815 816 // SetLogger method sets given writer for logging Resty request and response details.
817 // By default, requests and responses inherit their logger from the client.
818 //
819 // Compliant to interface [resty.Logger].
820 //
821 // It overrides the logger value set at the client instance level.
822 func (r *Request) SetLogger(l Logger) *Request {
823 r.log = l
824 return r
825 }
826 827 // SetDebug method enables the debug mode on the current request. It logs
828 // the details current request and response.
829 //
830 // client.SetDebug(true)
831 //
832 // Also, it can be enabled at the request level for a particular request; see [Request.SetDebug].
833 // - For [Request], it logs information such as HTTP verb, Relative URL path,
834 // Host, Headers, and Body if it has one.
835 // - For [Response], it logs information such as Status, Response Time, Headers,
836 // and Body if it has one.
837 func (r *Request) SetDebug(d bool) *Request {
838 r.Debug = d
839 return r
840 }
841 842 // AddRetryCondition method adds a retry condition function to the request's
843 // array of functions is checked to determine if the request can be retried.
844 // The request will retry if any functions return true and the error is nil.
845 //
846 // NOTE: The request level retry conditions are checked before all retry
847 // conditions from the client instance.
848 func (r *Request) AddRetryCondition(condition RetryConditionFunc) *Request {
849 r.retryConditions = append(r.retryConditions, condition)
850 return r
851 }
852 853 //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
854 // HTTP request tracing
855 //_______________________________________________________________________
856 857 // EnableTrace method enables trace for the current request
858 // using [httptrace.ClientTrace] and provides insights.
859 //
860 // client := resty.New()
861 //
862 // resp, err := client.R().EnableTrace().Get("https://httpbin.org/get")
863 // fmt.Println("Error:", err)
864 // fmt.Println("Trace Info:", resp.Request.TraceInfo())
865 //
866 // See [Client.EnableTrace] is also available to get trace info for all requests.
867 func (r *Request) EnableTrace() *Request {
868 r.trace = true
869 return r
870 }
871 872 // EnableGenerateCurlOnDebug method enables the generation of CURL commands in the debug log.
873 // It works in conjunction with debug mode. It overrides the options set by the [Client].
874 //
875 // NOTE: Use with care.
876 // - Potential to leak sensitive data from [Request] and [Response] in the debug log.
877 // - Beware of memory usage since the request body is reread.
878 func (r *Request) EnableGenerateCurlOnDebug() *Request {
879 r.generateCurlOnDebug = true
880 return r
881 }
882 883 // DisableGenerateCurlOnDebug method disables the option set by [Request.EnableGenerateCurlOnDebug].
884 // It overrides the options set by the [Client].
885 func (r *Request) DisableGenerateCurlOnDebug() *Request {
886 r.generateCurlOnDebug = false
887 return r
888 }
889 890 // TraceInfo method returns the trace info for the request.
891 // If either the [Client.EnableTrace] or [Request.EnableTrace] function has not been called
892 // before the request is made, an empty [resty.TraceInfo] object is returned.
893 func (r *Request) TraceInfo() TraceInfo {
894 ct := r.clientTrace
895 896 if ct == nil {
897 return TraceInfo{}
898 }
899 900 ct.lock.RLock()
901 defer ct.lock.RUnlock()
902 903 ti := TraceInfo{
904 DNSLookup: 0,
905 TCPConnTime: 0,
906 ServerTime: 0,
907 IsConnReused: ct.gotConnInfo.Reused,
908 IsConnWasIdle: ct.gotConnInfo.WasIdle,
909 ConnIdleTime: ct.gotConnInfo.IdleTime,
910 RequestAttempt: r.Attempt,
911 }
912 913 if !ct.dnsStart.IsZero() && !ct.dnsDone.IsZero() {
914 ti.DNSLookup = ct.dnsDone.Sub(ct.dnsStart)
915 }
916 917 if !ct.tlsHandshakeDone.IsZero() && !ct.tlsHandshakeStart.IsZero() {
918 ti.TLSHandshake = ct.tlsHandshakeDone.Sub(ct.tlsHandshakeStart)
919 }
920 921 if !ct.gotFirstResponseByte.IsZero() && !ct.gotConn.IsZero() {
922 ti.ServerTime = ct.gotFirstResponseByte.Sub(ct.gotConn)
923 }
924 925 // Calculate the total time accordingly when connection is reused,
926 // and DNS start and get conn time may be zero if the request is invalid.
927 // See issue #1016.
928 requestStartTime := r.Time
929 if ct.gotConnInfo.Reused && !ct.getConn.IsZero() {
930 requestStartTime = ct.getConn
931 } else if !ct.dnsStart.IsZero() {
932 requestStartTime = ct.dnsStart
933 }
934 ti.TotalTime = ct.endTime.Sub(requestStartTime)
935 936 // Only calculate on successful connections
937 if !ct.connectDone.IsZero() {
938 ti.TCPConnTime = ct.connectDone.Sub(ct.dnsDone)
939 }
940 941 // Only calculate on successful connections
942 if !ct.gotConn.IsZero() {
943 ti.ConnTime = ct.gotConn.Sub(ct.getConn)
944 }
945 946 // Only calculate on successful connections
947 if !ct.gotFirstResponseByte.IsZero() {
948 ti.ResponseTime = ct.endTime.Sub(ct.gotFirstResponseByte)
949 }
950 951 // Capture remote address info when connection is non-nil
952 if ct.gotConnInfo.Conn != nil {
953 ti.RemoteAddr = ct.gotConnInfo.Conn.RemoteAddr()
954 }
955 956 return ti
957 }
958 959 //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
960 // HTTP verb method starts here
961 //_______________________________________________________________________
962 963 // Get method does GET HTTP request. It's defined in section 4.3.1 of RFC7231.
964 func (r *Request) Get(url string) (*Response, error) {
965 return r.Execute(MethodGet, url)
966 }
967 968 // Head method does HEAD HTTP request. It's defined in section 4.3.2 of RFC7231.
969 func (r *Request) Head(url string) (*Response, error) {
970 return r.Execute(MethodHead, url)
971 }
972 973 // Post method does POST HTTP request. It's defined in section 4.3.3 of RFC7231.
974 func (r *Request) Post(url string) (*Response, error) {
975 return r.Execute(MethodPost, url)
976 }
977 978 // Put method does PUT HTTP request. It's defined in section 4.3.4 of RFC7231.
979 func (r *Request) Put(url string) (*Response, error) {
980 return r.Execute(MethodPut, url)
981 }
982 983 // Delete method does DELETE HTTP request. It's defined in section 4.3.5 of RFC7231.
984 func (r *Request) Delete(url string) (*Response, error) {
985 return r.Execute(MethodDelete, url)
986 }
987 988 // Options method does OPTIONS HTTP request. It's defined in section 4.3.7 of RFC7231.
989 func (r *Request) Options(url string) (*Response, error) {
990 return r.Execute(MethodOptions, url)
991 }
992 993 // Patch method does PATCH HTTP request. It's defined in section 2 of RFC5789.
994 func (r *Request) Patch(url string) (*Response, error) {
995 return r.Execute(MethodPatch, url)
996 }
997 998 // Send method performs the HTTP request using the method and URL already defined
999 // for current [Request].
1000 //
1001 // req := client.R()
1002 // req.Method = resty.MethodGet
1003 // req.URL = "http://httpbin.org/get"
1004 // resp, err := req.Send()
1005 func (r *Request) Send() (*Response, error) {
1006 return r.Execute(r.Method, r.URL)
1007 }
1008 1009 // Execute method performs the HTTP request with the given HTTP method and URL
1010 // for current [Request].
1011 //
1012 // resp, err := client.R().Execute(resty.MethodGet, "http://httpbin.org/get")
1013 func (r *Request) Execute(method, url string) (*Response, error) {
1014 var addrs []*net.SRV
1015 var resp *Response
1016 var err error
1017 1018 defer func() {
1019 if rec := recover(); rec != nil {
1020 if err, ok := rec.(error); ok {
1021 r.client.onPanicHooks(r, err)
1022 } else {
1023 r.client.onPanicHooks(r, fmt.Errorf("panic %v", rec))
1024 }
1025 panic(rec)
1026 }
1027 }()
1028 1029 if r.isMultiPart && !(method == MethodPost || method == MethodPut || method == MethodPatch) {
1030 // No OnError hook here since this is a request validation error
1031 err := fmt.Errorf("multipart content is not allowed in HTTP verb [%v]", method)
1032 r.client.onInvalidHooks(r, err)
1033 return nil, err
1034 }
1035 1036 if r.SRV != nil {
1037 _, addrs, err = net.LookupSRV(r.SRV.Service, "tcp", r.SRV.Domain)
1038 if err != nil {
1039 r.client.onErrorHooks(r, nil, err)
1040 return nil, err
1041 }
1042 }
1043 1044 r.Method = method
1045 r.URL = r.selectAddr(addrs, url, 0)
1046 1047 if r.client.RetryCount == 0 {
1048 r.Attempt = 1
1049 resp, err = r.client.execute(r)
1050 r.client.onErrorHooks(r, resp, unwrapNoRetryErr(err))
1051 backToBufPool(r.bodyBuf)
1052 return resp, unwrapNoRetryErr(err)
1053 }
1054 1055 err = Backoff(
1056 func() (*Response, error) {
1057 r.Attempt++
1058 1059 r.URL = r.selectAddr(addrs, url, r.Attempt)
1060 1061 resp, err = r.client.execute(r)
1062 if err != nil {
1063 r.log.Warnf("%v, Attempt %v", err, r.Attempt)
1064 }
1065 1066 return resp, err
1067 },
1068 Retries(r.client.RetryCount),
1069 WaitTime(r.client.RetryWaitTime),
1070 MaxWaitTime(r.client.RetryMaxWaitTime),
1071 RetryConditions(append(r.retryConditions, r.client.RetryConditions...)),
1072 RetryHooks(r.client.RetryHooks),
1073 ResetMultipartReaders(r.client.RetryResetReaders),
1074 )
1075 1076 if err != nil {
1077 r.log.Errorf("%v", err)
1078 }
1079 1080 r.client.onErrorHooks(r, resp, unwrapNoRetryErr(err))
1081 backToBufPool(r.bodyBuf)
1082 return resp, unwrapNoRetryErr(err)
1083 }
1084 1085 //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
1086 // SRVRecord struct
1087 //_______________________________________________________________________
1088 1089 // SRVRecord struct holds the data to query the SRV record for the
1090 // following service.
1091 type SRVRecord struct {
1092 Service string
1093 Domain string
1094 }
1095 1096 func (r *Request) fmtBodyString(sl int64) (body string) {
1097 body = "***** NO CONTENT *****"
1098 if !isPayloadSupported(r.Method, r.client.AllowGetMethodPayload) ||
1099 r.Body == http.NoBody {
1100 return
1101 }
1102 1103 if _, ok := r.Body.(io.Reader); ok {
1104 body = "***** BODY IS io.Reader *****"
1105 return
1106 }
1107 1108 // multipart or form-data
1109 if r.isMultiPart || r.isFormData {
1110 bodySize := int64(r.bodyBuf.Len())
1111 if bodySize > sl {
1112 body = fmt.Sprintf("***** REQUEST TOO LARGE (size - %d) *****", bodySize)
1113 return
1114 }
1115 body = r.bodyBuf.String()
1116 return
1117 }
1118 1119 // request body data
1120 if r.Body == nil {
1121 return
1122 }
1123 var prtBodyBytes []byte
1124 var err error
1125 1126 contentType := r.Header.Get(hdrContentTypeKey)
1127 kind := kindOf(r.Body)
1128 if canJSONMarshal(contentType, kind) {
1129 var bodyBuf *bytes.Buffer
1130 bodyBuf, err = noescapeJSONMarshalIndent(&r.Body)
1131 if err == nil {
1132 prtBodyBytes = bodyBuf.Bytes()
1133 defer releaseBuffer(bodyBuf)
1134 }
1135 } else if IsXMLType(contentType) && (kind == reflect.Struct) {
1136 prtBodyBytes, err = xml.MarshalIndent(&r.Body, "", " ")
1137 } else if b, ok := r.Body.(string); ok {
1138 if IsJSONType(contentType) {
1139 bodyBytes := []byte(b)
1140 out := acquireBuffer()
1141 defer releaseBuffer(out)
1142 if err = json.Indent(out, bodyBytes, "", " "); err == nil {
1143 prtBodyBytes = out.Bytes()
1144 }
1145 } else {
1146 body = b
1147 }
1148 } else if b, ok := r.Body.([]byte); ok {
1149 body = fmt.Sprintf("***** BODY IS byte(s) (size - %d) *****", len(b))
1150 return
1151 }
1152 1153 if prtBodyBytes != nil && err == nil {
1154 body = string(prtBodyBytes)
1155 }
1156 1157 if len(body) > 0 {
1158 bodySize := int64(len([]byte(body)))
1159 if bodySize > sl {
1160 body = fmt.Sprintf("***** REQUEST TOO LARGE (size - %d) *****", bodySize)
1161 }
1162 }
1163 1164 return
1165 }
1166 1167 func (r *Request) selectAddr(addrs []*net.SRV, path string, attempt int) string {
1168 if addrs == nil {
1169 return path
1170 }
1171 1172 idx := attempt % len(addrs)
1173 domain := strings.TrimRight(addrs[idx].Target, ".")
1174 path = strings.TrimLeft(path, "/")
1175 1176 return fmt.Sprintf("%s://%s:%d/%s", r.client.scheme, domain, addrs[idx].Port, path)
1177 }
1178 1179 func (r *Request) initValuesMap() {
1180 if r.values == nil {
1181 r.values = make(map[string]interface{})
1182 }
1183 }
1184 1185 var noescapeJSONMarshal = func(v interface{}) (*bytes.Buffer, error) {
1186 buf := acquireBuffer()
1187 encoder := json.NewEncoder(buf)
1188 encoder.SetEscapeHTML(false)
1189 if err := encoder.Encode(v); err != nil {
1190 releaseBuffer(buf)
1191 return nil, err
1192 }
1193 1194 return buf, nil
1195 }
1196 1197 var noescapeJSONMarshalIndent = func(v interface{}) (*bytes.Buffer, error) {
1198 buf := acquireBuffer()
1199 encoder := json.NewEncoder(buf)
1200 encoder.SetEscapeHTML(false)
1201 encoder.SetIndent("", " ")
1202 1203 if err := encoder.Encode(v); err != nil {
1204 releaseBuffer(buf)
1205 return nil, err
1206 }
1207 1208 return buf, nil
1209 }
1210