gitee.com/larksuite/oapi-sdk-go/v3@v3.0.3/core/httptransport.go (about) 1 /* 2 * MIT License 3 * 4 * Copyright (c) 2022 Lark Technologies Pte. Ltd. 5 * 6 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 * 8 * The above copyright notice and this permission notice, shall be included in all copies or substantial portions of the Software. 9 * 10 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 */ 12 13 package larkcore 14 15 import ( 16 "context" 17 "encoding/json" 18 "errors" 19 "fmt" 20 "net" 21 "net/http" 22 "net/url" 23 "strings" 24 ) 25 26 var reqTranslator ReqTranslator 27 28 func NewHttpClient(config *Config) { 29 if config.HttpClient == nil { 30 if config.ReqTimeout == 0 { 31 config.HttpClient = http.DefaultClient 32 } else { 33 config.HttpClient = &http.Client{Timeout: config.ReqTimeout} 34 } 35 } 36 } 37 38 func validateTokenType(accessTokenTypes []AccessTokenType, option *RequestOption) error { 39 if option == nil || len(accessTokenTypes) == 0 || len(accessTokenTypes) > 1 { 40 return nil 41 } 42 43 accessTokenType := accessTokenTypes[0] 44 if accessTokenType == AccessTokenTypeTenant && option.UserAccessToken != "" { 45 return errors.New("tenant token type not match user access token") 46 } 47 if accessTokenType == AccessTokenTypeUser && option.TenantAccessToken != "" { 48 return errors.New("user token type not match tenant access token") 49 } 50 return nil 51 } 52 53 func determineTokenType(accessTokenTypes []AccessTokenType, option *RequestOption, enableTokenCache bool) AccessTokenType { 54 if !enableTokenCache { 55 if option.UserAccessToken != "" { 56 return AccessTokenTypeUser 57 } 58 if option.TenantAccessToken != "" { 59 return AccessTokenTypeTenant 60 } 61 if option.AppAccessToken != "" { 62 return AccessTokenTypeApp 63 } 64 65 return AccessTokenTypeNone 66 } 67 accessibleTokenTypeSet := make(map[AccessTokenType]struct{}) 68 accessTokenType := accessTokenTypes[0] 69 for _, t := range accessTokenTypes { 70 if t == AccessTokenTypeTenant { 71 accessTokenType = t // default 72 } 73 accessibleTokenTypeSet[t] = struct{}{} 74 } 75 if option.TenantKey != "" { 76 if _, ok := accessibleTokenTypeSet[AccessTokenTypeTenant]; ok { 77 accessTokenType = AccessTokenTypeTenant 78 } 79 } 80 if option.UserAccessToken != "" { 81 if _, ok := accessibleTokenTypeSet[AccessTokenTypeUser]; ok { 82 accessTokenType = AccessTokenTypeUser 83 } 84 } 85 86 return accessTokenType 87 } 88 89 func validate(config *Config, option *RequestOption, accessTokenType AccessTokenType) error { 90 if config.AppId == "" { 91 return &IllegalParamError{msg: "AppId is empty"} 92 } 93 94 if config.AppSecret == "" { 95 return &IllegalParamError{msg: "AppSecret is empty"} 96 } 97 98 if !config.EnableTokenCache { 99 if accessTokenType == AccessTokenTypeNone { 100 return nil 101 } 102 if option.UserAccessToken == "" && option.TenantAccessToken == "" && option.AppAccessToken == "" { 103 return &IllegalParamError{msg: "accessToken is empty"} 104 } 105 } 106 107 if config.AppType == AppTypeMarketplace && accessTokenType == AccessTokenTypeTenant && option.TenantKey == "" { 108 return &IllegalParamError{msg: "tenant key is empty"} 109 } 110 111 if accessTokenType == AccessTokenTypeUser && option.UserAccessToken == "" { 112 return &IllegalParamError{msg: "user access token is empty"} 113 } 114 115 if option.Header != nil { 116 if option.Header.Get(HttpHeaderKeyRequestId) != "" { 117 return &IllegalParamError{msg: fmt.Sprintf("use %s as header key is not allowed", HttpHeaderKeyRequestId)} 118 } 119 if option.Header.Get(httpHeaderRequestId) != "" { 120 return &IllegalParamError{msg: fmt.Sprintf("use %s as header key is not allowed", httpHeaderRequestId)} 121 } 122 if option.Header.Get(HttpHeaderKeyLogId) != "" { 123 return &IllegalParamError{msg: fmt.Sprintf("use %s as header key is not allowed", HttpHeaderKeyLogId)} 124 } 125 } 126 127 return nil 128 } 129 130 func doSend(ctx context.Context, rawRequest *http.Request, httpClient HttpClient, logger Logger) (*ApiResp, error) { 131 if httpClient == nil { 132 httpClient = http.DefaultClient 133 } 134 resp, err := httpClient.Do(rawRequest) 135 if err != nil { 136 if er, ok := err.(*url.Error); ok { 137 if er.Timeout() { 138 return nil, &ClientTimeoutError{msg: er.Error()} 139 } 140 141 if e, ok := er.Err.(*net.OpError); ok && e.Op == "dial" { 142 return nil, &DialFailedError{msg: er.Error()} 143 } 144 } 145 return nil, err 146 } 147 148 if resp.StatusCode == http.StatusGatewayTimeout { 149 logID := resp.Header.Get(HttpHeaderKeyLogId) 150 if logID == "" { 151 logID = resp.Header.Get(HttpHeaderKeyRequestId) 152 } 153 logger.Info(ctx, fmt.Sprintf("req path:%s, server time out,requestId:%s", 154 rawRequest.URL.RequestURI(), logID)) 155 return nil, &ServerTimeoutError{msg: "server time out error"} 156 } 157 body, err := readResponse(resp) 158 if err != nil { 159 return nil, err 160 } 161 162 return &ApiResp{ 163 StatusCode: resp.StatusCode, 164 Header: resp.Header, 165 RawBody: body, 166 }, nil 167 } 168 169 func Request(ctx context.Context, req *ApiReq, config *Config, options ...RequestOptionFunc) (*ApiResp, error) { 170 option := &RequestOption{} 171 for _, optionFunc := range options { 172 optionFunc(option) 173 } 174 175 err := validateTokenType(req.SupportedAccessTokenTypes, option) 176 if err != nil { 177 return nil, err 178 } 179 accessTokenType := determineTokenType(req.SupportedAccessTokenTypes, option, config.EnableTokenCache) 180 err = validate(config, option, accessTokenType) 181 if err != nil { 182 return nil, err 183 } 184 185 return doRequest(ctx, req, accessTokenType, config, option) 186 187 } 188 189 func doRequest(ctx context.Context, httpReq *ApiReq, accessTokenType AccessTokenType, config *Config, option *RequestOption) (*ApiResp, error) { 190 var rawResp *ApiResp 191 var errResult error 192 for i := 0; i < 2; i++ { 193 req, err := reqTranslator.translate(ctx, httpReq, accessTokenType, config, option) 194 if err != nil { 195 return nil, err 196 } 197 198 if config.LogReqAtDebug { 199 config.Logger.Debug(ctx, fmt.Sprintf("req:%v", req)) 200 } else { 201 config.Logger.Debug(ctx, fmt.Sprintf("req:%s,%s", httpReq.HttpMethod, httpReq.ApiPath)) 202 } 203 rawResp, err = doSend(ctx, req, config.HttpClient, config.Logger) 204 if config.LogReqAtDebug { 205 config.Logger.Debug(ctx, fmt.Sprintf("resp:%v", rawResp)) 206 } 207 _, isDialError := err.(*DialFailedError) 208 if err != nil && !isDialError { 209 return nil, err 210 } 211 errResult = err 212 if isDialError { 213 continue 214 } 215 216 fileDownloadSuccess := option.FileDownload && rawResp.StatusCode == http.StatusOK 217 if fileDownloadSuccess || !strings.Contains(rawResp.Header.Get(contentTypeHeader), contentTypeJson) { 218 break 219 } 220 221 codeError := &CodeError{} 222 err = json.Unmarshal(rawResp.RawBody, codeError) 223 if err != nil { 224 return nil, err 225 } 226 227 code := codeError.Code 228 if code == errCodeAppTicketInvalid { 229 applyAppTicket(ctx, config) 230 } 231 232 if accessTokenType == AccessTokenTypeNone { 233 break 234 } 235 236 if !config.EnableTokenCache { 237 break 238 } 239 240 if code != errCodeAccessTokenInvalid && code != errCodeAppAccessTokenInvalid && 241 code != errCodeTenantAccessTokenInvalid { 242 break 243 } 244 } 245 246 if errResult != nil { 247 return nil, errResult 248 } 249 return rawResp, nil 250 }