github.com/cloudwego/hertz@v0.9.3/pkg/protocol/client/client.go (about) 1 /* 2 * Copyright 2022 CloudWeGo Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 * The MIT License (MIT) 17 * 18 * Copyright (c) 2015-present Aliaksandr Valialkin, VertaMedia, Kirill Danshin, Erik Dubbelboer, FastHTTP Authors 19 * 20 * Permission is hereby granted, free of charge, to any person obtaining a copy 21 * of this software and associated documentation files (the "Software"), to deal 22 * in the Software without restriction, including without limitation the rights 23 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 24 * copies of the Software, and to permit persons to whom the Software is 25 * furnished to do so, subject to the following conditions: 26 * 27 * The above copyright notice and this permission notice shall be included in 28 * all copies or substantial portions of the Software. 29 * 30 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 31 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 32 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 33 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 34 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 35 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 36 * THE SOFTWARE. 37 * 38 * This file may have been modified by CloudWeGo authors. All CloudWeGo 39 * Modifications are Copyright 2022 CloudWeGo Authors. 40 */ 41 42 package client 43 44 import ( 45 "context" 46 "sync" 47 "time" 48 49 "github.com/cloudwego/hertz/internal/bytestr" 50 "github.com/cloudwego/hertz/pkg/common/config" 51 "github.com/cloudwego/hertz/pkg/common/errors" 52 "github.com/cloudwego/hertz/pkg/common/timer" 53 "github.com/cloudwego/hertz/pkg/protocol" 54 "github.com/cloudwego/hertz/pkg/protocol/consts" 55 ) 56 57 const defaultMaxRedirectsCount = 16 58 59 var ( 60 errTimeout = errors.New(errors.ErrTimeout, errors.ErrorTypePublic, "host client") 61 errMissingLocation = errors.NewPublic("missing Location header for http redirect") 62 errTooManyRedirects = errors.NewPublic("too many redirects detected when doing the request") 63 64 clientURLResponseChPool sync.Pool 65 ) 66 67 type HostClient interface { 68 Doer 69 SetDynamicConfig(dc *DynamicConfig) 70 CloseIdleConnections() 71 ShouldRemove() bool 72 ConnectionCount() int 73 } 74 75 type Doer interface { 76 Do(ctx context.Context, req *protocol.Request, resp *protocol.Response) error 77 } 78 79 // DefaultRetryIf Default retry condition, mainly used for idempotent requests. 80 // If this cannot be satisfied, you can implement your own retry condition. 81 func DefaultRetryIf(req *protocol.Request, resp *protocol.Response, err error) bool { 82 // cannot retry if the request body is not rewindable 83 if req.IsBodyStream() { 84 return false 85 } 86 87 if isIdempotent(req, resp, err) { 88 return true 89 } 90 91 return false 92 } 93 94 func isIdempotent(req *protocol.Request, resp *protocol.Response, err error) bool { 95 return req.Header.IsGet() || 96 req.Header.IsHead() || 97 req.Header.IsPut() || 98 req.Header.IsDelete() || 99 req.Header.IsOptions() || 100 req.Header.IsTrace() 101 } 102 103 // DynamicConfig is config set which will be confirmed when starts a request. 104 type DynamicConfig struct { 105 Addr string 106 ProxyURI *protocol.URI 107 IsTLS bool 108 } 109 110 // RetryIfFunc signature of retry if function 111 // Judge whether to retry by request,response or error , return true is retry 112 type RetryIfFunc func(req *protocol.Request, resp *protocol.Response, err error) bool 113 114 type clientURLResponse struct { 115 statusCode int 116 body []byte 117 err error 118 } 119 120 func GetURL(ctx context.Context, dst []byte, url string, c Doer, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error) { 121 req := protocol.AcquireRequest() 122 req.SetOptions(requestOptions...) 123 124 statusCode, body, err = doRequestFollowRedirectsBuffer(ctx, req, dst, url, c) 125 126 protocol.ReleaseRequest(req) 127 return statusCode, body, err 128 } 129 130 func GetURLTimeout(ctx context.Context, dst []byte, url string, timeout time.Duration, c Doer, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error) { 131 deadline := time.Now().Add(timeout) 132 return GetURLDeadline(ctx, dst, url, deadline, c, requestOptions...) 133 } 134 135 func GetURLDeadline(ctx context.Context, dst []byte, url string, deadline time.Time, c Doer, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error) { 136 timeout := -time.Since(deadline) 137 if timeout <= 0 { 138 return 0, dst, errTimeout 139 } 140 141 var ch chan clientURLResponse 142 chv := clientURLResponseChPool.Get() 143 if chv == nil { 144 chv = make(chan clientURLResponse, 1) 145 } 146 ch = chv.(chan clientURLResponse) 147 148 req := protocol.AcquireRequest() 149 req.SetOptions(requestOptions...) 150 151 // Note that the request continues execution on errTimeout until 152 // client-specific ReadTimeout exceeds. This helps to limit load 153 // on slow hosts by MaxConns* concurrent requests. 154 // 155 // Without this 'hack' the load on slow host could exceed MaxConns* 156 // concurrent requests, since timed out requests on client side 157 // usually continue execution on the host. 158 go func() { 159 statusCodeCopy, bodyCopy, errCopy := doRequestFollowRedirectsBuffer(ctx, req, dst, url, c) 160 ch <- clientURLResponse{ 161 statusCode: statusCodeCopy, 162 body: bodyCopy, 163 err: errCopy, 164 } 165 }() 166 167 tc := timer.AcquireTimer(timeout) 168 select { 169 case resp := <-ch: 170 protocol.ReleaseRequest(req) 171 clientURLResponseChPool.Put(chv) 172 statusCode = resp.statusCode 173 body = resp.body 174 err = resp.err 175 case <-tc.C: 176 body = dst 177 err = errTimeout 178 } 179 timer.ReleaseTimer(tc) 180 181 return statusCode, body, err 182 } 183 184 func PostURL(ctx context.Context, dst []byte, url string, postArgs *protocol.Args, c Doer, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error) { 185 req := protocol.AcquireRequest() 186 req.Header.SetMethodBytes(bytestr.StrPost) 187 req.Header.SetContentTypeBytes(bytestr.StrPostArgsContentType) 188 req.SetOptions(requestOptions...) 189 190 if postArgs != nil { 191 if _, err := postArgs.WriteTo(req.BodyWriter()); err != nil { 192 return 0, nil, err 193 } 194 } 195 196 statusCode, body, err = doRequestFollowRedirectsBuffer(ctx, req, dst, url, c) 197 198 protocol.ReleaseRequest(req) 199 return statusCode, body, err 200 } 201 202 func doRequestFollowRedirectsBuffer(ctx context.Context, req *protocol.Request, dst []byte, url string, c Doer) (statusCode int, body []byte, err error) { 203 resp := protocol.AcquireResponse() 204 bodyBuf := resp.BodyBuffer() 205 oldBody := bodyBuf.B 206 bodyBuf.B = dst 207 208 statusCode, _, err = DoRequestFollowRedirects(ctx, req, resp, url, defaultMaxRedirectsCount, c) 209 210 // In HTTP2 scenario, client use stream mode to create a request and its body is in body stream. 211 // In HTTP1, only client recv body exceed max body size and client is in stream mode can trig it. 212 body = resp.Body() 213 bodyBuf.B = oldBody 214 protocol.ReleaseResponse(resp) 215 216 return statusCode, body, err 217 } 218 219 func DoRequestFollowRedirects(ctx context.Context, req *protocol.Request, resp *protocol.Response, url string, maxRedirectsCount int, c Doer) (statusCode int, body []byte, err error) { 220 redirectsCount := 0 221 222 for { 223 req.SetRequestURI(url) 224 req.ParseURI() 225 226 if err = c.Do(ctx, req, resp); err != nil { 227 break 228 } 229 statusCode = resp.Header.StatusCode() 230 if !StatusCodeIsRedirect(statusCode) { 231 break 232 } 233 234 redirectsCount++ 235 if redirectsCount > maxRedirectsCount { 236 err = errTooManyRedirects 237 break 238 } 239 location := resp.Header.PeekLocation() 240 if len(location) == 0 { 241 err = errMissingLocation 242 break 243 } 244 url = getRedirectURL(url, location) 245 } 246 247 return statusCode, body, err 248 } 249 250 // StatusCodeIsRedirect returns true if the status code indicates a redirect. 251 func StatusCodeIsRedirect(statusCode int) bool { 252 return statusCode == consts.StatusMovedPermanently || 253 statusCode == consts.StatusFound || 254 statusCode == consts.StatusSeeOther || 255 statusCode == consts.StatusTemporaryRedirect || 256 statusCode == consts.StatusPermanentRedirect 257 } 258 259 func getRedirectURL(baseURL string, location []byte) string { 260 u := protocol.AcquireURI() 261 u.Update(baseURL) 262 u.UpdateBytes(location) 263 redirectURL := u.String() 264 protocol.ReleaseURI(u) 265 return redirectURL 266 } 267 268 func DoTimeout(ctx context.Context, req *protocol.Request, resp *protocol.Response, timeout time.Duration, c Doer) error { 269 if timeout <= 0 { 270 return errTimeout 271 } 272 // Note: it will overwrite the reqTimeout. 273 req.SetOptions(config.WithRequestTimeout(timeout)) 274 return c.Do(ctx, req, resp) 275 } 276 277 func DoDeadline(ctx context.Context, req *protocol.Request, resp *protocol.Response, deadline time.Time, c Doer) error { 278 timeout := time.Until(deadline) 279 if timeout <= 0 { 280 return errTimeout 281 } 282 // Note: it will overwrite the reqTimeout. 283 req.SetOptions(config.WithRequestTimeout(timeout)) 284 return c.Do(ctx, req, resp) 285 }