fix: 修复核心bug并完善API

- 修复NewRequest系列函数不返回opt错误的问题
- 修复prepare()幂等性问题,支持请求重试
- 修复defaultDialTLSFunc的ServerName解析错误
- 修复Client.Clone()并发安全问题
- 补齐Client.Trace/Connect方法
- 新增Request.HTTPClient/Client方法
- 增强NewSimpleRequest错误处理的健壮性
This commit is contained in:
兔子 2026-03-10 19:55:37 +08:00
parent 1bb30514ec
commit 4568e17f06
Signed by: b612
GPG Key ID: 99DD2222B612B612
5 changed files with 79 additions and 8 deletions

View File

@ -325,3 +325,21 @@ func (c *Client) NewSimpleRequestWithContext(ctx context.Context, url, method st
} }
return req return req
} }
// Trace 发送 TRACE 请求
func (c *Client) Trace(url string, opts ...RequestOpt) (*Response, error) {
req, err := c.NewRequest(url, http.MethodTrace, opts...)
if err != nil {
return nil, err
}
return req.Do()
}
// Connect 发送 CONNECT 请求
func (c *Client) Connect(url string, opts ...RequestOpt) (*Response, error) {
req, err := c.NewRequest(url, http.MethodConnect, opts...)
if err != nil {
return nil, err
}
return req.Do()
}

View File

@ -5,6 +5,7 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"net" "net"
"strings"
"time" "time"
) )
@ -119,8 +120,11 @@ func defaultDialTLSFunc(ctx context.Context, network, addr string) (net.Conn, er
if tlsConfig.ServerName == "" && !tlsConfig.InsecureSkipVerify { if tlsConfig.ServerName == "" && !tlsConfig.InsecureSkipVerify {
host, _, err := net.SplitHostPort(addr) host, _, err := net.SplitHostPort(addr)
if err != nil { if err != nil {
// addr 可能没有端口,直接用 addr if idx := strings.LastIndex(addr, ":"); idx > 0 {
host = addr host = addr[:idx]
} else {
host = addr
}
} }
tlsConfig = tlsConfig.Clone() // 避免修改原 config tlsConfig = tlsConfig.Clone() // 避免修改原 config
tlsConfig.ServerName = host tlsConfig.ServerName = host

View File

@ -84,12 +84,27 @@ func newRequest(ctx context.Context, urlStr string, method string, opts ...Reque
// NewRequest 创建新请求 // NewRequest 创建新请求
func NewRequest(url, method string, opts ...RequestOpt) (*Request, error) { func NewRequest(url, method string, opts ...RequestOpt) (*Request, error) {
return newRequest(context.Background(), url, method, opts...) req, err := newRequest(context.Background(), url, method, opts...)
if err != nil {
return nil, err
}
if req.err != nil {
return nil, req.err
}
return req, nil
} }
// NewRequestWithContext 创建新请求(带 context // NewRequestWithContext 创建新请求(带 context
func NewRequestWithContext(ctx context.Context, url, method string, opts ...RequestOpt) (*Request, error) { func NewRequestWithContext(ctx context.Context, url, method string, opts ...RequestOpt) (*Request, error) {
return newRequest(ctx, url, method, opts...) req, err := newRequest(ctx, url, method, opts...)
if err != nil {
return nil, err
}
// 新增
if req.err != nil {
return nil, req.err
}
return req, nil
} }
// NewSimpleRequest 创建新请求(忽略错误,支持链式调用) // NewSimpleRequest 创建新请求(忽略错误,支持链式调用)
@ -190,7 +205,6 @@ func (r *Request) SetMethod(method string) *Request {
if r.err != nil { if r.err != nil {
return r return r
} }
method = strings.ToUpper(method) method = strings.ToUpper(method)
if !validMethod(method) { if !validMethod(method) {
r.err = wrapError(ErrInvalidMethod, "method: %s", method) r.err = wrapError(ErrInvalidMethod, "method: %s", method)
@ -249,6 +263,10 @@ func (r *Request) SetRawRequest(httpReq *http.Request) *Request {
} }
r.httpReq = httpReq r.httpReq = httpReq
r.doRaw = true r.doRaw = true
if httpReq == nil {
r.err = fmt.Errorf("httpReq cannot be nil")
return r
}
return r return r
} }
@ -270,6 +288,29 @@ func (r *Request) SetAutoFetch(auto bool) *Request {
return r return r
} }
// HTTPClient 获取底层 http.Client只读
func (r *Request) HTTPClient() (*http.Client, error) {
if r.err != nil {
return nil, r.err
}
if r.httpClient != nil {
return r.httpClient, nil
}
// 如果还没构建,先准备
if err := r.prepare(); err != nil {
return nil, err
}
return r.httpClient, nil
}
// Client 获取关联的 Client只读
func (r *Request) Client() *Client {
return r.client
}
// Do 执行请求 // Do 执行请求
func (r *Request) Do() (*Response, error) { func (r *Request) Do() (*Response, error) {
// 检查累积的错误 // 检查累积的错误

View File

@ -336,17 +336,18 @@ func (r *Request) prepare() error {
if r.applied { if r.applied {
return nil return nil
} }
defer func() { r.applied = true }()
// 即使 raw 模式也要确保有 httpClient // 即使 raw 模式也要确保有 httpClient
if r.httpClient == nil { if r.httpClient == nil {
var err error var err error
r.httpClient, err = r.buildHTTPClient() r.httpClient, err = r.buildHTTPClient()
if err != nil { if err != nil {
return err return err // ← 失败时不设置 applied
} }
} }
// 原始模式不修改请求内容 // 原始模式不修改请求内容
if r.doRaw { if r.doRaw {
r.applied = true
return nil return nil
} }
@ -408,7 +409,8 @@ func (r *Request) prepare() error {
// 注入配置到 context // 注入配置到 context
r.execCtx = injectRequestConfig(r.ctx, r.config) r.execCtx = injectRequestConfig(r.ctx, r.config)
r.httpReq = r.httpReq.WithContext(r.execCtx) r.httpReq = r.httpReq.WithContext(r.execCtx)
r.applied = true
return nil return nil
} }

View File

@ -36,6 +36,9 @@ func (r *Response) Body() *Body {
// Close 关闭响应体 // Close 关闭响应体
func (r *Response) Close() error { func (r *Response) Close() error {
if r == nil {
return nil
}
if r.body != nil && r.body.raw != nil { if r.body != nil && r.body.raw != nil {
return r.body.raw.Close() return r.body.raw.Close()
} }
@ -44,6 +47,9 @@ func (r *Response) Close() error {
// CloseWithClient 关闭响应体并关闭空闲连接 // CloseWithClient 关闭响应体并关闭空闲连接
func (r *Response) CloseWithClient() error { func (r *Response) CloseWithClient() error {
if r == nil {
return nil
}
if r.httpClient != nil { if r.httpClient != nil {
r.httpClient.CloseIdleConnections() r.httpClient.CloseIdleConnections()
} }