Files
proxy_server/utils/httphelper/http_helper.go
2025-08-23 16:38:04 +08:00

291 lines
9.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package httphelper
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// HTTPClient 定义一个通用的 HTTP 客户端结构
type HTTPClient struct {
Client *http.Client // 底层的 http.Client 实例
BaseURL string // 基础 URL所有请求将基于此 URL
Headers map[string]string // 默认请求头
}
// NewHTTPClient 创建一个新的 HTTPClient 实例
// timeout: 请求超时时间,例如 10 * time.Second
// baseURL: 基础 URL例如 "https://api.example.com"
// defaultHeaders: 默认请求头,例如 {"Content-Type": "application/json"}
func NewHTTPClient(timeout time.Duration, baseURL string, defaultHeaders map[string]string) *HTTPClient {
return &HTTPClient{
Client: &http.Client{
Timeout: timeout,
},
BaseURL: baseURL,
Headers: defaultHeaders,
}
}
// applyHeaders 为请求应用默认和自定义请求头
func (c *HTTPClient) applyHeaders(req *http.Request, customHeaders map[string]string) {
// 应用默认请求头
for key, value := range c.Headers {
req.Header.Set(key, value)
}
// 应用自定义请求头(覆盖默认请求头)
for key, value := range customHeaders {
req.Header.Set(key, value)
}
}
// doRequest 执行实际的 HTTP 请求
// method: HTTP 方法 (GET, POST, PUT, DELETE等)
// path: 请求路径,将与 BaseURL 拼接
// requestBody: 请求体数据,如果为 GET/DELETE 请求则为 nil
// customHeaders: 自定义请求头,将覆盖默认请求头
// responseData: 用于存储响应数据的目标结构体(指针类型),如果为 nil 则表示不需要 JSON 解码
// rawResponse: 用于存储原始响应体字节切片(*[]byte如果为 nil 则表示不需要原始响应
func (c *HTTPClient) DoRequest(
method, path string,
requestBody interface{},
customHeaders map[string]string,
responseData interface{},
rawResponse *[]byte, // 新增参数:指向字节切片的指针,用于存储原始响应
) (int, error) {
// 拼接完整的 URL
url := c.BaseURL + path
var reqBodyReader io.Reader
if requestBody != nil {
// 将请求体编码为 JSON
jsonBody, err := json.Marshal(requestBody)
if err != nil {
return -1, fmt.Errorf("json marshal request body failed: %w", err)
}
reqBodyReader = bytes.NewBuffer(jsonBody)
}
// 创建新的 HTTP 请求
req, err := http.NewRequest(method, url, reqBodyReader)
if err != nil {
return -1, fmt.Errorf("create http request failed: %w", err)
}
// 应用请求头
c.applyHeaders(req, customHeaders)
// 默认添加 gzip 接收头
req.Header.Set("Accept-Encoding", "gzip")
// 发送请求
resp, err := c.Client.Do(req)
if err != nil {
return -1, fmt.Errorf("send http request failed: %w", err)
}
defer resp.Body.Close()
// 检查 HTTP 状态码
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest {
bodyBytes, _ := io.ReadAll(resp.Body) // 读取错误响应体
return resp.StatusCode, fmt.Errorf("http request failed with status: %d, body: %s", resp.StatusCode, string(bodyBytes))
}
// 解码响应(支持 gzip
var reader io.Reader = resp.Body
if resp.Header.Get("Content-Encoding") == "gzip" {
gzipReader, err := gzip.NewReader(resp.Body)
if err != nil {
return http.StatusOK, fmt.Errorf("create gzip reader failed: %w", err)
}
defer gzipReader.Close()
reader = gzipReader
}
// 首先读取整个响应体,然后决定如何处理
bodyBytes, err := io.ReadAll(reader)
if err != nil {
return http.StatusOK, fmt.Errorf("read response body failed: %w", err)
}
// 如果提供了原始响应目标,则填充它
if rawResponse != nil {
*rawResponse = bodyBytes
}
// 如果提供了 JSON 解码目标,则尝试解码
if responseData != nil {
err = json.Unmarshal(bodyBytes, responseData) // 直接对字节切片使用 Unmarshal
if err != nil {
return http.StatusOK, fmt.Errorf("json decode response body failed: %w", err)
}
}
return http.StatusOK, nil
}
// Get 发送 GET 请求
// path: 请求路径
// customHeaders: 自定义请求头
// responseData: 用于存储响应数据的目标结构体(指针类型)
func (c *HTTPClient) Get(path string, customHeaders map[string]string, responseData interface{}) (int, error) {
return c.DoRequest(http.MethodGet, path, nil, customHeaders, responseData, nil) // rawResponse 传递 nil
}
// Post 发送 POST 请求
// path: 请求路径
// requestBody: 请求体数据
// customHeaders: 自定义请求头
// responseData: 用于存储响应数据的目标结构体(指针类型)
func (c *HTTPClient) Post(path string, requestBody interface{}, customHeaders map[string]string, responseData interface{}) (int, error) {
return c.DoRequest(http.MethodPost, path, requestBody, customHeaders, responseData, nil) // rawResponse 传递 nil
}
// PostWithContentType 发送 POST 请求,支持自定义 Content-Type如 application/json 或 multipart/form-data
// contentType: 请求体类型(如 "application/json", "multipart/form-data"
// requestBody: 请求体可以是结构体、map、或 multipart/form 格式)
// 注意:如果是 multipart/form-data请确保 requestBody 是 io.Reader 和 contentType 是完整的包含 boundary 的值。
func (c *HTTPClient) PostWithContentType(path string, requestBody interface{}, contentType string, customHeaders map[string]string, responseData interface{}) error {
// 拼接 URL
url := c.BaseURL + path
var reqBodyReader io.Reader
switch body := requestBody.(type) {
case io.Reader:
reqBodyReader = body
default:
// 默认处理为 JSON
jsonBody, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("json marshal request body failed: %w", err)
}
reqBodyReader = bytes.NewBuffer(jsonBody)
if contentType == "" {
contentType = "application/json"
}
}
// 创建请求
req, err := http.NewRequest(http.MethodPost, url, reqBodyReader)
if err != nil {
return fmt.Errorf("create http request failed: %w", err)
}
// 设置 Content-Type
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
// 应用其他头部
c.applyHeaders(req, customHeaders)
// gzip 支持
req.Header.Set("Accept-Encoding", "gzip")
// 发出请求
resp, err := c.Client.Do(req)
if err != nil {
return fmt.Errorf("send http request failed: %w", err)
}
defer resp.Body.Close()
// 检查状态码
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest {
bodyBytes, _ := io.ReadAll(resp.Body)
return fmt.Errorf("http request failed with status: %d, body: %s", resp.StatusCode, string(bodyBytes))
}
// 读取响应
var reader io.Reader = resp.Body
if resp.Header.Get("Content-Encoding") == "gzip" {
gzipReader, err := gzip.NewReader(resp.Body)
if err != nil {
return fmt.Errorf("create gzip reader failed: %w", err)
}
defer gzipReader.Close()
reader = gzipReader
}
// 首先读取整个响应体,然后决定如何处理
bodyBytes, err := io.ReadAll(reader)
if err != nil {
return fmt.Errorf("read response body failed: %w", err)
}
// 如果提供了 JSON 解码目标,则尝试解码
if responseData != nil {
err = json.Unmarshal(bodyBytes, responseData)
if err != nil {
return fmt.Errorf("json decode response body failed: %w", err)
}
}
return nil
}
// Put 发送 PUT 请求
// path: 请求路径
// requestBody: 请求体数据
// customHeaders: 自定义请求头
// responseData: 用于存储响应数据的目标结构体(指针类型)
// returns 状态码 和 错误信息
func (c *HTTPClient) Put(path string, requestBody interface{}, customHeaders map[string]string, responseData interface{}) (int, error) {
return c.DoRequest(http.MethodPut, path, requestBody, customHeaders, responseData, nil) // rawResponse 传递 nil
}
// Delete 发送 DELETE 请求
// path: 请求路径
// customHeaders: 自定义请求头
// responseData: 用于存储响应数据的目标结构体(指针类型)
// returns 状态码 和 错误信息
func (c *HTTPClient) Delete(path string, customHeaders map[string]string, responseData interface{}) (int, error) {
// DELETE 请求通常没有请求体,但某些 RESTful API 可能支持
return c.DoRequest(http.MethodDelete, path, nil, customHeaders, responseData, nil) // rawResponse 传递 nil
}
// Patch 发送 PATCH 请求
// path: 请求路径
// requestBody: 请求体数据
// customHeaders: 自定义请求头
// responseData: 用于存储响应数据的目标结构体(指针类型)
// returns 状态码 和 错误信息
func (c *HTTPClient) Patch(path string, requestBody interface{}, customHeaders map[string]string, responseData interface{}) (int, error) {
return c.DoRequest(http.MethodPatch, path, requestBody, customHeaders, responseData, nil) // rawResponse 传递 nil
}
// GetRaw 发送 GET 请求并返回原始响应体
// path: 请求路径
// customHeaders: 自定义请求头
// 返回值: 原始响应体字节切片或错误
// 状态码
// 错误信息
func (c *HTTPClient) GetRaw(path string, customHeaders map[string]string) ([]byte, int, error) {
var raw []byte
// responseData 传递 nilrawResponse 传递 &raw
statusCode, err := c.DoRequest(http.MethodGet, path, nil, customHeaders, nil, &raw)
if err != nil {
return nil, statusCode, err
}
return raw, statusCode, nil
}
// PostRaw 发送 POST 请求并返回原始响应体
// path: 请求路径
// customHeaders: 自定义请求头
// 返回值: 原始响应体字节切片或错误
// 状态码
// 错误信息
func (c *HTTPClient) PostRaw(path string, customHeaders map[string]string) ([]byte, int, error) {
var raw []byte
// responseData 传递 nilrawResponse 传递 &raw
statusCode, err := c.DoRequest(http.MethodPost, path, nil, customHeaders, nil, &raw)
if err != nil {
return nil, statusCode, err
}
return raw, statusCode, nil
}