github.com/gogf/gf/v2@v2.7.4/net/gclient/gclient_request.go (about) 1 // Copyright GoFrame Author(https://goframe.org). All Rights Reserved. 2 // 3 // This Source Code Form is subject to the terms of the MIT License. 4 // If a copy of the MIT was not distributed with this file, 5 // You can obtain one at https://github.com/gogf/gf. 6 7 package gclient 8 9 import ( 10 "bytes" 11 "context" 12 "io" 13 "mime/multipart" 14 "net/http" 15 "os" 16 "strings" 17 "time" 18 19 "github.com/gogf/gf/v2/encoding/gjson" 20 "github.com/gogf/gf/v2/errors/gcode" 21 "github.com/gogf/gf/v2/errors/gerror" 22 "github.com/gogf/gf/v2/internal/httputil" 23 "github.com/gogf/gf/v2/internal/json" 24 "github.com/gogf/gf/v2/internal/utils" 25 "github.com/gogf/gf/v2/os/gfile" 26 "github.com/gogf/gf/v2/os/gtime" 27 "github.com/gogf/gf/v2/text/gregex" 28 "github.com/gogf/gf/v2/text/gstr" 29 "github.com/gogf/gf/v2/util/gconv" 30 ) 31 32 // Get send GET request and returns the response object. 33 // Note that the response object MUST be closed if it'll never be used. 34 func (c *Client) Get(ctx context.Context, url string, data ...interface{}) (*Response, error) { 35 return c.DoRequest(ctx, http.MethodGet, url, data...) 36 } 37 38 // Put send PUT request and returns the response object. 39 // Note that the response object MUST be closed if it'll never be used. 40 func (c *Client) Put(ctx context.Context, url string, data ...interface{}) (*Response, error) { 41 return c.DoRequest(ctx, http.MethodPut, url, data...) 42 } 43 44 // Post sends request using HTTP method POST and returns the response object. 45 // Note that the response object MUST be closed if it'll never be used. 46 func (c *Client) Post(ctx context.Context, url string, data ...interface{}) (*Response, error) { 47 return c.DoRequest(ctx, http.MethodPost, url, data...) 48 } 49 50 // Delete send DELETE request and returns the response object. 51 // Note that the response object MUST be closed if it'll never be used. 52 func (c *Client) Delete(ctx context.Context, url string, data ...interface{}) (*Response, error) { 53 return c.DoRequest(ctx, http.MethodDelete, url, data...) 54 } 55 56 // Head send HEAD request and returns the response object. 57 // Note that the response object MUST be closed if it'll never be used. 58 func (c *Client) Head(ctx context.Context, url string, data ...interface{}) (*Response, error) { 59 return c.DoRequest(ctx, http.MethodHead, url, data...) 60 } 61 62 // Patch send PATCH request and returns the response object. 63 // Note that the response object MUST be closed if it'll never be used. 64 func (c *Client) Patch(ctx context.Context, url string, data ...interface{}) (*Response, error) { 65 return c.DoRequest(ctx, http.MethodPatch, url, data...) 66 } 67 68 // Connect send CONNECT request and returns the response object. 69 // Note that the response object MUST be closed if it'll never be used. 70 func (c *Client) Connect(ctx context.Context, url string, data ...interface{}) (*Response, error) { 71 return c.DoRequest(ctx, http.MethodConnect, url, data...) 72 } 73 74 // Options send OPTIONS request and returns the response object. 75 // Note that the response object MUST be closed if it'll never be used. 76 func (c *Client) Options(ctx context.Context, url string, data ...interface{}) (*Response, error) { 77 return c.DoRequest(ctx, http.MethodOptions, url, data...) 78 } 79 80 // Trace send TRACE request and returns the response object. 81 // Note that the response object MUST be closed if it'll never be used. 82 func (c *Client) Trace(ctx context.Context, url string, data ...interface{}) (*Response, error) { 83 return c.DoRequest(ctx, http.MethodTrace, url, data...) 84 } 85 86 // PostForm is different from net/http.PostForm. 87 // It's a wrapper of Post method, which sets the Content-Type as "multipart/form-data;". 88 // and It will automatically set boundary characters for the request body and Content-Type. 89 // 90 // It's Seem like the following case: 91 // 92 // Content-Type: multipart/form-data; boundary=----Boundarye4Ghaog6giyQ9ncN 93 // 94 // And form data is like: 95 // ------Boundarye4Ghaog6giyQ9ncN 96 // Content-Disposition: form-data; name="checkType" 97 // 98 // none 99 // 100 // It's used for sending form data. 101 // Note that the response object MUST be closed if it'll never be used. 102 func (c *Client) PostForm(ctx context.Context, url string, data map[string]string) (resp *Response, err error) { 103 body := new(bytes.Buffer) 104 w := multipart.NewWriter(body) 105 for k, v := range data { 106 err := w.WriteField(k, v) 107 if err != nil { 108 return nil, err 109 } 110 } 111 err = w.Close() 112 if err != nil { 113 return nil, err 114 } 115 return c.ContentType(w.FormDataContentType()).Post(ctx, url, body) 116 } 117 118 // DoRequest sends request with given HTTP method and data and returns the response object. 119 // Note that the response object MUST be closed if it'll never be used. 120 // 121 // Note that it uses "multipart/form-data" as its Content-Type if it contains file uploading, 122 // else it uses "application/x-www-form-urlencoded". It also automatically detects the post 123 // content for JSON format, and for that it automatically sets the Content-Type as 124 // "application/json". 125 func (c *Client) DoRequest( 126 ctx context.Context, method, url string, data ...interface{}, 127 ) (resp *Response, err error) { 128 var requestStartTime = gtime.Now() 129 req, err := c.prepareRequest(ctx, method, url, data...) 130 if err != nil { 131 return nil, err 132 } 133 134 // Metrics. 135 c.handleMetricsBeforeRequest(req) 136 defer c.handleMetricsAfterRequestDone(req, requestStartTime) 137 138 // Client middleware. 139 if len(c.middlewareHandler) > 0 { 140 mdlHandlers := make([]HandlerFunc, 0, len(c.middlewareHandler)+1) 141 mdlHandlers = append(mdlHandlers, c.middlewareHandler...) 142 mdlHandlers = append(mdlHandlers, func(cli *Client, r *http.Request) (*Response, error) { 143 return cli.callRequest(r) 144 }) 145 ctx = context.WithValue(req.Context(), clientMiddlewareKey, &clientMiddleware{ 146 client: c, 147 handlers: mdlHandlers, 148 handlerIndex: -1, 149 }) 150 req = req.WithContext(ctx) 151 resp, err = c.Next(req) 152 } else { 153 resp, err = c.callRequest(req) 154 } 155 if resp != nil && resp.Response != nil { 156 req.Response = resp.Response 157 } 158 return resp, err 159 } 160 161 // prepareRequest verifies request parameters, builds and returns http request. 162 func (c *Client) prepareRequest(ctx context.Context, method, url string, data ...interface{}) (req *http.Request, err error) { 163 method = strings.ToUpper(method) 164 if len(c.prefix) > 0 { 165 url = c.prefix + gstr.Trim(url) 166 } 167 if !gstr.ContainsI(url, httpProtocolName) { 168 url = httpProtocolName + `://` + url 169 } 170 var ( 171 params string 172 allowFileUploading = true 173 ) 174 if len(data) > 0 { 175 switch c.header[httpHeaderContentType] { 176 case httpHeaderContentTypeJson: 177 switch data[0].(type) { 178 case string, []byte: 179 params = gconv.String(data[0]) 180 default: 181 if b, err := json.Marshal(data[0]); err != nil { 182 return nil, err 183 } else { 184 params = string(b) 185 } 186 } 187 allowFileUploading = false 188 189 case httpHeaderContentTypeXml: 190 switch data[0].(type) { 191 case string, []byte: 192 params = gconv.String(data[0]) 193 default: 194 if b, err := gjson.New(data[0]).ToXml(); err != nil { 195 return nil, err 196 } else { 197 params = string(b) 198 } 199 } 200 allowFileUploading = false 201 202 default: 203 params = httputil.BuildParams(data[0], c.noUrlEncode) 204 } 205 } 206 if method == http.MethodGet { 207 var bodyBuffer *bytes.Buffer 208 if params != "" { 209 switch c.header[httpHeaderContentType] { 210 case 211 httpHeaderContentTypeJson, 212 httpHeaderContentTypeXml: 213 bodyBuffer = bytes.NewBuffer([]byte(params)) 214 default: 215 // It appends the parameters to the url 216 // if http method is GET and Content-Type is not specified. 217 if gstr.Contains(url, "?") { 218 url = url + "&" + params 219 } else { 220 url = url + "?" + params 221 } 222 bodyBuffer = bytes.NewBuffer(nil) 223 } 224 } else { 225 bodyBuffer = bytes.NewBuffer(nil) 226 } 227 if req, err = http.NewRequest(method, url, bodyBuffer); err != nil { 228 err = gerror.Wrapf(err, `http.NewRequest failed with method "%s" and URL "%s"`, method, url) 229 return nil, err 230 } 231 } else { 232 if allowFileUploading && strings.Contains(params, httpParamFileHolder) { 233 // File uploading request. 234 var ( 235 buffer = bytes.NewBuffer(nil) 236 writer = multipart.NewWriter(buffer) 237 isFileUploading = false 238 ) 239 for _, item := range strings.Split(params, "&") { 240 array := strings.Split(item, "=") 241 if len(array) < 2 { 242 continue 243 } 244 if len(array[1]) > 6 && strings.Compare(array[1][0:6], httpParamFileHolder) == 0 { 245 path := array[1][6:] 246 if !gfile.Exists(path) { 247 return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `"%s" does not exist`, path) 248 } 249 var ( 250 file io.Writer 251 formFileName = gfile.Basename(path) 252 formFieldName = array[0] 253 ) 254 // it sets post content type as `application/octet-stream` 255 if file, err = writer.CreateFormFile(formFieldName, formFileName); err != nil { 256 return nil, gerror.Wrapf( 257 err, `CreateFormFile failed with "%s", "%s"`, formFieldName, formFileName, 258 ) 259 } 260 var f *os.File 261 if f, err = gfile.Open(path); err != nil { 262 return nil, err 263 } 264 if _, err = io.Copy(file, f); err != nil { 265 _ = f.Close() 266 return nil, gerror.Wrapf( 267 err, `io.Copy failed from "%s" to form "%s"`, path, formFieldName, 268 ) 269 } 270 if err = f.Close(); err != nil { 271 return nil, gerror.Wrapf(err, `close file descriptor failed for "%s"`, path) 272 } 273 isFileUploading = true 274 } else { 275 var ( 276 fieldName = array[0] 277 fieldValue = array[1] 278 ) 279 if err = writer.WriteField(fieldName, fieldValue); err != nil { 280 return nil, gerror.Wrapf( 281 err, `write form field failed with "%s", "%s"`, fieldName, fieldValue, 282 ) 283 } 284 } 285 } 286 // Close finishes the multipart message and writes the trailing 287 // boundary end line to the output. 288 if err = writer.Close(); err != nil { 289 return nil, gerror.Wrapf(err, `form writer close failed`) 290 } 291 292 if req, err = http.NewRequest(method, url, buffer); err != nil { 293 return nil, gerror.Wrapf( 294 err, `http.NewRequest failed for method "%s" and URL "%s"`, method, url, 295 ) 296 } 297 if isFileUploading { 298 req.Header.Set(httpHeaderContentType, writer.FormDataContentType()) 299 } 300 } else { 301 // Normal request. 302 paramBytes := []byte(params) 303 if req, err = http.NewRequest(method, url, bytes.NewReader(paramBytes)); err != nil { 304 err = gerror.Wrapf(err, `http.NewRequest failed for method "%s" and URL "%s"`, method, url) 305 return nil, err 306 } 307 if v, ok := c.header[httpHeaderContentType]; ok { 308 // Custom Content-Type. 309 req.Header.Set(httpHeaderContentType, v) 310 } else if len(paramBytes) > 0 { 311 if (paramBytes[0] == '[' || paramBytes[0] == '{') && json.Valid(paramBytes) { 312 // Auto-detecting and setting the post content format: JSON. 313 req.Header.Set(httpHeaderContentType, httpHeaderContentTypeJson) 314 } else if gregex.IsMatchString(httpRegexParamJson, params) { 315 // If the parameters passed like "name=value", it then uses form type. 316 req.Header.Set(httpHeaderContentType, httpHeaderContentTypeForm) 317 } 318 } 319 } 320 } 321 322 // Context. 323 if ctx != nil { 324 req = req.WithContext(ctx) 325 } 326 // Custom header. 327 if len(c.header) > 0 { 328 for k, v := range c.header { 329 req.Header.Set(k, v) 330 } 331 } 332 // It's necessary set the req.Host if you want to custom the host value of the request. 333 // It uses the "Host" value from header if it's not empty. 334 if reqHeaderHost := req.Header.Get(httpHeaderHost); reqHeaderHost != "" { 335 req.Host = reqHeaderHost 336 } 337 // Custom Cookie. 338 if len(c.cookies) > 0 { 339 headerCookie := "" 340 for k, v := range c.cookies { 341 if len(headerCookie) > 0 { 342 headerCookie += ";" 343 } 344 headerCookie += k + "=" + v 345 } 346 if len(headerCookie) > 0 { 347 req.Header.Set(httpHeaderCookie, headerCookie) 348 } 349 } 350 // HTTP basic authentication. 351 if len(c.authUser) > 0 { 352 req.SetBasicAuth(c.authUser, c.authPass) 353 } 354 return req, nil 355 } 356 357 // callRequest sends request with give http.Request, and returns the responses object. 358 // Note that the response object MUST be closed if it'll never be used. 359 func (c *Client) callRequest(req *http.Request) (resp *Response, err error) { 360 resp = &Response{ 361 request: req, 362 } 363 // Dump feature. 364 // The request body can be reused for dumping 365 // raw HTTP request-response procedure. 366 reqBodyContent, _ := io.ReadAll(req.Body) 367 resp.requestBody = reqBodyContent 368 for { 369 req.Body = utils.NewReadCloser(reqBodyContent, false) 370 if resp.Response, err = c.Do(req); err != nil { 371 err = gerror.Wrapf(err, `request failed`) 372 // The response might not be nil when err != nil. 373 if resp.Response != nil { 374 _ = resp.Response.Body.Close() 375 } 376 if c.retryCount > 0 { 377 c.retryCount-- 378 time.Sleep(c.retryInterval) 379 } else { 380 // return resp, err 381 break 382 } 383 } else { 384 break 385 } 386 } 387 return resp, err 388 }