Spaces:
Paused
Paused
| package httpclient | |
| import ( | |
| "fmt" | |
| "io" | |
| "net" | |
| "net/http" | |
| "net/url" | |
| "strings" | |
| "github.com/samber/lo" | |
| ) | |
| func ReadHTTPRequest(rawReq *http.Request) (*Request, error) { | |
| req := &Request{ | |
| Method: rawReq.Method, | |
| URL: rawReq.URL.String(), | |
| Path: rawReq.URL.Path, | |
| Query: rawReq.URL.Query(), | |
| Headers: rawReq.Header, | |
| Body: nil, | |
| Auth: &AuthConfig{}, | |
| RequestID: "", | |
| ClientIP: getClientIP(rawReq), | |
| RawRequest: rawReq, | |
| } | |
| body, err := io.ReadAll(rawReq.Body) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to read request body: %w", err) | |
| } | |
| req.Body = body | |
| return req, nil | |
| } | |
| func getClientIP(req *http.Request) string { | |
| if xff := req.Header.Get("X-Forwarded-For"); xff != "" { | |
| if before, _, ok := strings.Cut(xff, ","); ok { | |
| return strings.TrimSpace(before) | |
| } | |
| return xff | |
| } | |
| if xri := req.Header.Get("X-Real-IP"); xri != "" { | |
| return xri | |
| } | |
| if ip, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { | |
| return ip | |
| } | |
| return req.RemoteAddr | |
| } | |
| // IsHTTPStatusCodeRetryable checks if an HTTP status code is retryable. | |
| // 4xx status codes are generally not retryable except for 429 (Too Many Requests). | |
| // 5xx status codes are typically retryable. | |
| func IsHTTPStatusCodeRetryable(statusCode int) bool { | |
| if statusCode == http.StatusTooManyRequests { | |
| return true // 429 is retryable (rate limiting) | |
| } | |
| if statusCode >= 400 && statusCode < 500 { | |
| return false // Other 4xx errors are not retryable | |
| } | |
| if statusCode >= 500 { | |
| return true // 5xx errors are retryable | |
| } | |
| return false // Non-error status codes don't need retrying | |
| } | |
| // The golang std http client will handle the headers automatically. | |
| var libManagedHeaders = map[string]bool{ | |
| "Content-Length": true, | |
| "Transfer-Encoding": true, | |
| "Accept-Encoding": true, | |
| "Host": true, | |
| } | |
| var blockedHeaders = map[string]bool{ | |
| "Content-Type": true, | |
| "Connection": true, | |
| "X-Channel-Id": true, | |
| "X-Project-Id": true, | |
| "X-Real-IP": true, | |
| "X-Forwarded-For": true, | |
| "X-Forwarded-Proto": true, | |
| "X-Forwarded-Host": true, | |
| "X-Forwarded-Port": true, | |
| // Browser-only / hop-by-hop-ish headers that should not be forwarded to upstream. | |
| "Accept-Language": true, | |
| "Dnt": true, | |
| "Origin": true, | |
| "Referer": true, | |
| "Sec-Fetch-Dest": true, | |
| "Sec-Fetch-Mode": true, | |
| "Sec-Fetch-Site": true, | |
| "Sec-Fetch-User": true, | |
| "Sec-Ch-Ua": true, | |
| "Sec-Ch-Ua-Mobile": true, | |
| "Sec-Ch-Ua-Platform": true, | |
| } | |
| var sensitiveHeaders = map[string]bool{ | |
| "Authorization": true, | |
| "Api-Key": true, | |
| "X-Api-Key": true, | |
| "X-Api-Secret": true, | |
| "X-Api-Token": true, | |
| "X-Goog-Api-Key": true, | |
| "X-Google-Api-Key": true, | |
| "Cookie": true, | |
| "Set-Cookie": true, | |
| "Proxy-Authorization": true, | |
| "WWW-Authenticate": true, | |
| } | |
| var mergeWithAppendHeaders = map[string]bool{} | |
| // RegisterMergeWithAppendHeaders registers headers that should be appended instead of overwritten. | |
| // It is not goroutine-safe, should call when init. | |
| func RegisterMergeWithAppendHeaders(headers ...string) { | |
| for _, h := range headers { | |
| mergeWithAppendHeaders[http.CanonicalHeaderKey(h)] = true | |
| } | |
| } | |
| func MergeInboundRequest(dest, src *Request) *Request { | |
| if src == nil || len(src.Headers) == 0 && len(src.Query) == 0 { | |
| return dest | |
| } | |
| dest.Headers = MergeHTTPHeaders(dest.Headers, src.Headers) | |
| if !dest.SkipInboundQueryMerge { | |
| dest.Query = MergeHTTPQuery(dest.Query, src.Query) | |
| } | |
| return dest | |
| } | |
| // MergeHTTPQuery merges the source query parameters into the destination query parameters. | |
| // If a key already exists in the destination, it is not overwritten. | |
| func MergeHTTPQuery(dest, src url.Values) url.Values { | |
| if len(src) == 0 { | |
| return dest | |
| } | |
| if dest == nil { | |
| dest = make(url.Values) | |
| } | |
| for k, v := range src { | |
| if _, ok := dest[k]; !ok { | |
| dest[k] = v | |
| } | |
| } | |
| return dest | |
| } | |
| func MaskSensitiveHeaders(headers http.Header) http.Header { | |
| result := make(http.Header, len(headers)) | |
| for key, values := range headers { | |
| var newValues []string | |
| if _, ok := sensitiveHeaders[key]; !ok { | |
| newValues = values | |
| } else { | |
| newValues = append(newValues, "******") | |
| } | |
| result[key] = newValues | |
| } | |
| return result | |
| } | |
| // FinalizeAuthHeaders writes the auth config into headers and clears the in-memory auth field. | |
| func FinalizeAuthHeaders(req *Request) (*Request, error) { | |
| if req.Auth == nil { | |
| return req, nil | |
| } | |
| err := applyAuth(req.Headers, req.Auth) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to apply authentication: %w", err) | |
| } | |
| req.Auth = nil | |
| return req, nil | |
| } | |
| // MergeHTTPHeaders merges the source headers into the destination headers. | |
| // If a header is in the mergeWithAppendHeaders list, it adds non-duplicate values from the source. | |
| // Otherwise, it overwrites the destination header with the source values. | |
| // Blocked, sensitive, and library-managed headers are not merged. | |
| func MergeHTTPHeaders(dest, src http.Header) http.Header { | |
| for k, v := range src { | |
| if sensitiveHeaders[k] || libManagedHeaders[k] || blockedHeaders[k] { | |
| continue | |
| } | |
| if mergeWithAppendHeaders[k] { | |
| if existingValues, ok := dest[k]; ok { | |
| dest[k] = lo.Uniq(append(existingValues, v...)) | |
| } else { | |
| dest[k] = v | |
| } | |
| } else { | |
| dest[k] = v | |
| } | |
| } | |
| return dest | |
| } | |