1// Copyright (c) 2015-2019 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 5package resty 6 7import ( 8 "encoding/base64" 9 "encoding/json" 10 "encoding/xml" 11 "fmt" 12 "io" 13 "net" 14 "net/url" 15 "reflect" 16 "strings" 17) 18 19// SRVRecord holds the data to query the SRV record for the following service 20type SRVRecord struct { 21 Service string 22 Domain string 23} 24 25// SetHeader method is to set a single header field and its value in the current request. 26// Example: To set `Content-Type` and `Accept` as `application/json`. 27// resty.R(). 28// SetHeader("Content-Type", "application/json"). 29// SetHeader("Accept", "application/json") 30// 31// Also you can override header value, which was set at client instance level. 32// 33func (r *Request) SetHeader(header, value string) *Request { 34 r.Header.Set(header, value) 35 return r 36} 37 38// SetHeaders method sets multiple headers field and its values at one go in the current request. 39// Example: To set `Content-Type` and `Accept` as `application/json` 40// 41// resty.R(). 42// SetHeaders(map[string]string{ 43// "Content-Type": "application/json", 44// "Accept": "application/json", 45// }) 46// Also you can override header value, which was set at client instance level. 47// 48func (r *Request) SetHeaders(headers map[string]string) *Request { 49 for h, v := range headers { 50 r.SetHeader(h, v) 51 } 52 53 return r 54} 55 56// SetQueryParam method sets single parameter and its value in the current request. 57// It will be formed as query string for the request. 58// Example: `search=kitchen%20papers&size=large` in the URL after `?` mark. 59// resty.R(). 60// SetQueryParam("search", "kitchen papers"). 61// SetQueryParam("size", "large") 62// Also you can override query params value, which was set at client instance level 63// 64func (r *Request) SetQueryParam(param, value string) *Request { 65 r.QueryParam.Set(param, value) 66 return r 67} 68 69// SetQueryParams method sets multiple parameters and its values at one go in the current request. 70// It will be formed as query string for the request. 71// Example: `search=kitchen%20papers&size=large` in the URL after `?` mark. 72// resty.R(). 73// SetQueryParams(map[string]string{ 74// "search": "kitchen papers", 75// "size": "large", 76// }) 77// Also you can override query params value, which was set at client instance level 78// 79func (r *Request) SetQueryParams(params map[string]string) *Request { 80 for p, v := range params { 81 r.SetQueryParam(p, v) 82 } 83 84 return r 85} 86 87// SetMultiValueQueryParams method appends multiple parameters with multi-value 88// at one go in the current request. It will be formed as query string for the request. 89// Example: `status=pending&status=approved&status=open` in the URL after `?` mark. 90// resty.R(). 91// SetMultiValueQueryParams(url.Values{ 92// "status": []string{"pending", "approved", "open"}, 93// }) 94// Also you can override query params value, which was set at client instance level 95// 96func (r *Request) SetMultiValueQueryParams(params url.Values) *Request { 97 for p, v := range params { 98 for _, pv := range v { 99 r.QueryParam.Add(p, pv) 100 } 101 } 102 103 return r 104} 105 106// SetQueryString method provides ability to use string as an input to set URL query string for the request. 107// 108// Using String as an input 109// resty.R(). 110// SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more") 111// 112func (r *Request) SetQueryString(query string) *Request { 113 params, err := url.ParseQuery(strings.TrimSpace(query)) 114 if err == nil { 115 for p, v := range params { 116 for _, pv := range v { 117 r.QueryParam.Add(p, pv) 118 } 119 } 120 } else { 121 r.client.Log.Printf("ERROR %v", err) 122 } 123 return r 124} 125 126// SetFormData method sets Form parameters and their values in the current request. 127// It's applicable only HTTP method `POST` and `PUT` and requests content type would be set as 128// `application/x-www-form-urlencoded`. 129// resty.R(). 130// SetFormData(map[string]string{ 131// "access_token": "BC594900-518B-4F7E-AC75-BD37F019E08F", 132// "user_id": "3455454545", 133// }) 134// Also you can override form data value, which was set at client instance level 135// 136func (r *Request) SetFormData(data map[string]string) *Request { 137 for k, v := range data { 138 r.FormData.Set(k, v) 139 } 140 141 return r 142} 143 144// SetMultiValueFormData method appends multiple form parameters with multi-value 145// at one go in the current request. 146// resty.R(). 147// SetMultiValueFormData(url.Values{ 148// "search_criteria": []string{"book", "glass", "pencil"}, 149// }) 150// Also you can override form data value, which was set at client instance level 151// 152func (r *Request) SetMultiValueFormData(params url.Values) *Request { 153 for k, v := range params { 154 for _, kv := range v { 155 r.FormData.Add(k, kv) 156 } 157 } 158 159 return r 160} 161 162// SetBody method sets the request body for the request. It supports various realtime needs as easy. 163// We can say its quite handy or powerful. Supported request body data types is `string`, 164// `[]byte`, `struct`, `map`, `slice` and `io.Reader`. Body value can be pointer or non-pointer. 165// Automatic marshalling for JSON and XML content type, if it is `struct`, `map`, or `slice`. 166// 167// Note: `io.Reader` is processed as bufferless mode while sending request. 168// 169// Example: 170// 171// Struct as a body input, based on content type, it will be marshalled. 172// resty.R(). 173// SetBody(User{ 174// Username: "jeeva@myjeeva.com", 175// Password: "welcome2resty", 176// }) 177// 178// Map as a body input, based on content type, it will be marshalled. 179// resty.R(). 180// SetBody(map[string]interface{}{ 181// "username": "jeeva@myjeeva.com", 182// "password": "welcome2resty", 183// "address": &Address{ 184// Address1: "1111 This is my street", 185// Address2: "Apt 201", 186// City: "My City", 187// State: "My State", 188// ZipCode: 00000, 189// }, 190// }) 191// 192// String as a body input. Suitable for any need as a string input. 193// resty.R(). 194// SetBody(`{ 195// "username": "jeeva@getrightcare.com", 196// "password": "admin" 197// }`) 198// 199// []byte as a body input. Suitable for raw request such as file upload, serialize & deserialize, etc. 200// resty.R(). 201// SetBody([]byte("This is my raw request, sent as-is")) 202// 203func (r *Request) SetBody(body interface{}) *Request { 204 r.Body = body 205 return r 206} 207 208// SetResult method is to register the response `Result` object for automatic unmarshalling in the RESTful mode 209// if response status code is between 200 and 299 and content type either JSON or XML. 210// 211// Note: Result object can be pointer or non-pointer. 212// resty.R().SetResult(&AuthToken{}) 213// // OR 214// resty.R().SetResult(AuthToken{}) 215// 216// Accessing a result value 217// response.Result().(*AuthToken) 218// 219func (r *Request) SetResult(res interface{}) *Request { 220 r.Result = getPointer(res) 221 return r 222} 223 224// SetError method is to register the request `Error` object for automatic unmarshalling in the RESTful mode 225// if response status code is greater than 399 and content type either JSON or XML. 226// 227// Note: Error object can be pointer or non-pointer. 228// resty.R().SetError(&AuthError{}) 229// // OR 230// resty.R().SetError(AuthError{}) 231// 232// Accessing a error value 233// response.Error().(*AuthError) 234// 235func (r *Request) SetError(err interface{}) *Request { 236 r.Error = getPointer(err) 237 return r 238} 239 240// SetFile method is to set single file field name and its path for multipart upload. 241// resty.R(). 242// SetFile("my_file", "/Users/jeeva/Gas Bill - Sep.pdf") 243// 244func (r *Request) SetFile(param, filePath string) *Request { 245 r.isMultiPart = true 246 r.FormData.Set("@"+param, filePath) 247 return r 248} 249 250// SetFiles method is to set multiple file field name and its path for multipart upload. 251// resty.R(). 252// SetFiles(map[string]string{ 253// "my_file1": "/Users/jeeva/Gas Bill - Sep.pdf", 254// "my_file2": "/Users/jeeva/Electricity Bill - Sep.pdf", 255// "my_file3": "/Users/jeeva/Water Bill - Sep.pdf", 256// }) 257// 258func (r *Request) SetFiles(files map[string]string) *Request { 259 r.isMultiPart = true 260 261 for f, fp := range files { 262 r.FormData.Set("@"+f, fp) 263 } 264 265 return r 266} 267 268// SetFileReader method is to set single file using io.Reader for multipart upload. 269// resty.R(). 270// SetFileReader("profile_img", "my-profile-img.png", bytes.NewReader(profileImgBytes)). 271// SetFileReader("notes", "user-notes.txt", bytes.NewReader(notesBytes)) 272// 273func (r *Request) SetFileReader(param, fileName string, reader io.Reader) *Request { 274 r.isMultiPart = true 275 r.multipartFiles = append(r.multipartFiles, &File{ 276 Name: fileName, 277 ParamName: param, 278 Reader: reader, 279 }) 280 return r 281} 282 283// SetMultipartField method is to set custom data using io.Reader for multipart upload. 284func (r *Request) SetMultipartField(param, fileName, contentType string, reader io.Reader) *Request { 285 r.isMultiPart = true 286 r.multipartFields = append(r.multipartFields, &MultipartField{ 287 Param: param, 288 FileName: fileName, 289 ContentType: contentType, 290 Reader: reader, 291 }) 292 return r 293} 294 295// SetMultipartFields method is to set multiple data fields using io.Reader for multipart upload. 296// Example: 297// resty.R().SetMultipartFields( 298// &resty.MultipartField{ 299// Param: "uploadManifest1", 300// FileName: "upload-file-1.json", 301// ContentType: "application/json", 302// Reader: strings.NewReader(`{"input": {"name": "Uploaded document 1", "_filename" : ["file1.txt"]}}`), 303// }, 304// &resty.MultipartField{ 305// Param: "uploadManifest2", 306// FileName: "upload-file-2.json", 307// ContentType: "application/json", 308// Reader: strings.NewReader(`{"input": {"name": "Uploaded document 2", "_filename" : ["file2.txt"]}}`), 309// }) 310// 311// If you have slice already, then simply call- 312// resty.R().SetMultipartFields(fields...) 313func (r *Request) SetMultipartFields(fields ...*MultipartField) *Request { 314 r.isMultiPart = true 315 r.multipartFields = append(r.multipartFields, fields...) 316 return r 317} 318 319// SetContentLength method sets the HTTP header `Content-Length` value for current request. 320// By default go-resty won't set `Content-Length`. Also you have an option to enable for every 321// request. See `resty.SetContentLength` 322// resty.R().SetContentLength(true) 323// 324func (r *Request) SetContentLength(l bool) *Request { 325 r.setContentLength = true 326 return r 327} 328 329// SetBasicAuth method sets the basic authentication header in the current HTTP request. 330// For Header example: 331// Authorization: Basic <base64-encoded-value> 332// 333// To set the header for username "go-resty" and password "welcome" 334// resty.R().SetBasicAuth("go-resty", "welcome") 335// 336// This method overrides the credentials set by method `resty.SetBasicAuth`. 337// 338func (r *Request) SetBasicAuth(username, password string) *Request { 339 r.UserInfo = &User{Username: username, Password: password} 340 return r 341} 342 343// SetAuthToken method sets bearer auth token header in the current HTTP request. Header example: 344// Authorization: Bearer <auth-token-value-comes-here> 345// 346// Example: To set auth token BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F 347// 348// resty.R().SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F") 349// 350// This method overrides the Auth token set by method `resty.SetAuthToken`. 351// 352func (r *Request) SetAuthToken(token string) *Request { 353 r.Token = token 354 return r 355} 356 357// SetOutput method sets the output file for current HTTP request. Current HTTP response will be 358// saved into given file. It is similar to `curl -o` flag. Absolute path or relative path can be used. 359// If is it relative path then output file goes under the output directory, as mentioned 360// in the `Client.SetOutputDirectory`. 361// resty.R(). 362// SetOutput("/Users/jeeva/Downloads/ReplyWithHeader-v5.1-beta.zip"). 363// Get("http://bit.ly/1LouEKr") 364// 365// Note: In this scenario `Response.Body` might be nil. 366func (r *Request) SetOutput(file string) *Request { 367 r.outputFile = file 368 r.isSaveResponse = true 369 return r 370} 371 372// SetSRV method sets the details to query the service SRV record and execute the 373// request. 374// resty.R(). 375// SetSRV(SRVRecord{"web", "testservice.com"}). 376// Get("/get") 377func (r *Request) SetSRV(srv *SRVRecord) *Request { 378 r.SRV = srv 379 return r 380} 381 382// SetDoNotParseResponse method instructs `Resty` not to parse the response body automatically. 383// Resty exposes the raw response body as `io.ReadCloser`. Also do not forget to close the body, 384// otherwise you might get into connection leaks, no connection reuse. 385// 386// Please Note: Response middlewares are not applicable, if you use this option. Basically you have 387// taken over the control of response parsing from `Resty`. 388func (r *Request) SetDoNotParseResponse(parse bool) *Request { 389 r.notParseResponse = parse 390 return r 391} 392 393// SetPathParams method sets multiple URL path key-value pairs at one go in the 394// resty current request instance. 395// resty.R().SetPathParams(map[string]string{ 396// "userId": "sample@sample.com", 397// "subAccountId": "100002", 398// }) 399// 400// Result: 401// URL - /v1/users/{userId}/{subAccountId}/details 402// Composed URL - /v1/users/sample@sample.com/100002/details 403// It replace the value of the key while composing request URL. Also you can 404// override Path Params value, which was set at client instance level. 405func (r *Request) SetPathParams(params map[string]string) *Request { 406 for p, v := range params { 407 r.pathParams[p] = v 408 } 409 return r 410} 411 412// ExpectContentType method allows to provide fallback `Content-Type` for automatic unmarshalling 413// when `Content-Type` response header is unavailable. 414func (r *Request) ExpectContentType(contentType string) *Request { 415 r.fallbackContentType = contentType 416 return r 417} 418 419// SetJSONEscapeHTML method is to enable/disable the HTML escape on JSON marshal. 420// 421// NOTE: This option only applicable to standard JSON Marshaller. 422func (r *Request) SetJSONEscapeHTML(b bool) *Request { 423 r.jsonEscapeHTML = b 424 return r 425} 426 427//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ 428// HTTP verb method starts here 429//___________________________________ 430 431// Get method does GET HTTP request. It's defined in section 4.3.1 of RFC7231. 432func (r *Request) Get(url string) (*Response, error) { 433 return r.Execute(MethodGet, url) 434} 435 436// Head method does HEAD HTTP request. It's defined in section 4.3.2 of RFC7231. 437func (r *Request) Head(url string) (*Response, error) { 438 return r.Execute(MethodHead, url) 439} 440 441// Post method does POST HTTP request. It's defined in section 4.3.3 of RFC7231. 442func (r *Request) Post(url string) (*Response, error) { 443 return r.Execute(MethodPost, url) 444} 445 446// Put method does PUT HTTP request. It's defined in section 4.3.4 of RFC7231. 447func (r *Request) Put(url string) (*Response, error) { 448 return r.Execute(MethodPut, url) 449} 450 451// Delete method does DELETE HTTP request. It's defined in section 4.3.5 of RFC7231. 452func (r *Request) Delete(url string) (*Response, error) { 453 return r.Execute(MethodDelete, url) 454} 455 456// Options method does OPTIONS HTTP request. It's defined in section 4.3.7 of RFC7231. 457func (r *Request) Options(url string) (*Response, error) { 458 return r.Execute(MethodOptions, url) 459} 460 461// Patch method does PATCH HTTP request. It's defined in section 2 of RFC5789. 462func (r *Request) Patch(url string) (*Response, error) { 463 return r.Execute(MethodPatch, url) 464} 465 466// Execute method performs the HTTP request with given HTTP method and URL 467// for current `Request`. 468// resp, err := resty.R().Execute(resty.GET, "http://httpbin.org/get") 469// 470func (r *Request) Execute(method, url string) (*Response, error) { 471 var addrs []*net.SRV 472 var err error 473 474 if r.isMultiPart && !(method == MethodPost || method == MethodPut) { 475 return nil, fmt.Errorf("multipart content is not allowed in HTTP verb [%v]", method) 476 } 477 478 if r.SRV != nil { 479 _, addrs, err = net.LookupSRV(r.SRV.Service, "tcp", r.SRV.Domain) 480 if err != nil { 481 return nil, err 482 } 483 } 484 485 r.Method = method 486 r.URL = r.selectAddr(addrs, url, 0) 487 488 if r.client.RetryCount == 0 { 489 return r.client.execute(r) 490 } 491 492 var resp *Response 493 attempt := 0 494 _ = Backoff( 495 func() (*Response, error) { 496 attempt++ 497 498 r.URL = r.selectAddr(addrs, url, attempt) 499 500 resp, err = r.client.execute(r) 501 if err != nil { 502 r.client.Log.Printf("ERROR %v, Attempt %v", err, attempt) 503 if r.isContextCancelledIfAvailable() { 504 // stop Backoff from retrying request if request has been 505 // canceled by context 506 return resp, nil 507 } 508 } 509 510 return resp, err 511 }, 512 Retries(r.client.RetryCount), 513 WaitTime(r.client.RetryWaitTime), 514 MaxWaitTime(r.client.RetryMaxWaitTime), 515 RetryConditions(r.client.RetryConditions), 516 ) 517 518 return resp, err 519} 520 521//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ 522// Request Unexported methods 523//___________________________________ 524 525func (r *Request) fmtBodyString() (body string) { 526 body = "***** NO CONTENT *****" 527 if isPayloadSupported(r.Method, r.client.AllowGetMethodPayload) { 528 if _, ok := r.Body.(io.Reader); ok { 529 body = "***** BODY IS io.Reader *****" 530 return 531 } 532 533 // multipart or form-data 534 if r.isMultiPart || r.isFormData { 535 body = r.bodyBuf.String() 536 return 537 } 538 539 // request body data 540 if r.Body == nil { 541 return 542 } 543 var prtBodyBytes []byte 544 var err error 545 546 contentType := r.Header.Get(hdrContentTypeKey) 547 kind := kindOf(r.Body) 548 if canJSONMarshal(contentType, kind) { 549 prtBodyBytes, err = json.MarshalIndent(&r.Body, "", " ") 550 } else if IsXMLType(contentType) && (kind == reflect.Struct) { 551 prtBodyBytes, err = xml.MarshalIndent(&r.Body, "", " ") 552 } else if b, ok := r.Body.(string); ok { 553 if IsJSONType(contentType) { 554 bodyBytes := []byte(b) 555 out := acquireBuffer() 556 defer releaseBuffer(out) 557 if err = json.Indent(out, bodyBytes, "", " "); err == nil { 558 prtBodyBytes = out.Bytes() 559 } 560 } else { 561 body = b 562 return 563 } 564 } else if b, ok := r.Body.([]byte); ok { 565 body = base64.StdEncoding.EncodeToString(b) 566 } 567 568 if prtBodyBytes != nil && err == nil { 569 body = string(prtBodyBytes) 570 } 571 } 572 573 return 574} 575 576func (r *Request) selectAddr(addrs []*net.SRV, path string, attempt int) string { 577 if addrs == nil { 578 return path 579 } 580 581 idx := attempt % len(addrs) 582 domain := strings.TrimRight(addrs[idx].Target, ".") 583 path = strings.TrimLeft(path, "/") 584 585 return fmt.Sprintf("%s://%s:%d/%s", r.client.scheme, domain, addrs[idx].Port, path) 586} 587