starnet/request.go
starainrt 732e81316c
fix(starnet): 重构请求执行链路并补齐代理/重试/trace边界
- 分离 Request 的配置态与执行态,修复二次 Do、raw 模式网络配置失效和 body 来源互斥问题
  - 新增 starnet trace 抽象,补齐 DNS/连接/TLS/重试事件,并优化动态 transport 缓存与代理解析路径
  - 收紧非法代理为 fail-fast,多目标目标回退仅限幂等请求,修复 Host/TLS/SNI 等语义边界
  - 补充防御性拷贝、专项回归测试、本地代理/TLS 用例与 README 行为说明
2026-04-19 15:39:51 +08:00

588 lines
12 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 starnet
import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
)
// Request HTTP 请求
type Request struct {
ctx context.Context
execCtx context.Context // 执行时的 context注入了配置
cancel context.CancelFunc
url string
method string
err error // 累积的错误
config *RequestConfig
client *Client
httpClient *http.Client
httpReq *http.Request
retry *retryPolicy
traceHooks *TraceHooks
traceState *traceState
applied bool // 是否已应用配置
doRaw bool // 是否使用原始请求(不修改)
autoFetch bool // 是否自动获取响应体
rawSourceExternal bool // 是否由 SetRawRequest/WithRawRequest 注入外部 raw request
rawTemplate *http.Request
}
func normalizeContext(ctx context.Context) context.Context {
if ctx != nil {
return ctx
}
return context.Background()
}
func cloneRawHTTPRequest(httpReq *http.Request, ctx context.Context) (*http.Request, error) {
if httpReq == nil {
return nil, fmt.Errorf("http request is nil")
}
cloned := httpReq.Clone(normalizeContext(ctx))
switch {
case httpReq.Body == nil || httpReq.Body == http.NoBody:
cloned.Body = httpReq.Body
case httpReq.GetBody != nil:
body, err := httpReq.GetBody()
if err != nil {
return cloned, wrapError(err, "clone raw request body")
}
cloned.Body = body
default:
return cloned, fmt.Errorf("cannot clone raw request with non-replayable body")
}
return cloned, nil
}
func (r *Request) rawBaseRequest() *http.Request {
if r == nil {
return nil
}
if r.rawTemplate != nil {
return r.rawTemplate
}
return r.httpReq
}
func (r *Request) invalidatePreparedState() {
if r == nil {
return
}
if r.cancel != nil {
r.cancel()
r.cancel = nil
}
r.execCtx = nil
r.traceState = nil
r.httpClient = nil
wasApplied := r.applied
r.applied = false
if !wasApplied || r.doRaw {
return
}
if err := r.rebuildPreparedRequestBase(); err != nil && r.err == nil {
r.err = err
}
}
func (r *Request) rebuildPreparedRequestBase() error {
if r == nil || r.doRaw {
return nil
}
ctx := r.ctx
if ctx == nil {
ctx = context.Background()
}
httpReq, err := http.NewRequestWithContext(ctx, r.method, r.url, nil)
if err != nil {
return wrapError(err, "rebuild http request")
}
r.httpReq = httpReq
r.syncRequestHost()
return nil
}
func (r *Request) rebuildRawRequestBase() error {
if r == nil || !r.doRaw {
return nil
}
baseReq := r.rawBaseRequest()
rawReq, err := cloneRawHTTPRequest(baseReq, normalizeContext(r.ctx))
if err != nil && baseReq != nil && baseReq == r.httpReq {
r.httpReq = baseReq.WithContext(normalizeContext(r.ctx))
return nil
}
if rawReq != nil {
r.httpReq = rawReq
}
return err
}
func (r *Request) rebuildExecutionRequestBase() error {
if r == nil {
return nil
}
if r.cancel != nil {
r.cancel()
r.cancel = nil
}
r.execCtx = nil
r.traceState = nil
r.applied = false
if r.doRaw {
return r.rebuildRawRequestBase()
}
return r.rebuildPreparedRequestBase()
}
// newRequest 创建新请求(内部使用)
func newRequest(ctx context.Context, urlStr string, method string, opts ...RequestOpt) (*Request, error) {
ctx = normalizeContext(ctx)
if method == "" {
method = http.MethodGet
}
method = strings.ToUpper(method)
// 创建 http.Request
httpReq, err := http.NewRequestWithContext(ctx, method, urlStr, nil)
if err != nil {
return nil, wrapError(err, "create http request")
}
// 初始化配置
config := &RequestConfig{
Network: NetworkConfig{
DialTimeout: DefaultDialTimeout,
Timeout: DefaultTimeout,
},
Headers: make(http.Header),
Queries: make(map[string][]string),
Body: BodyConfig{
FormData: make(map[string][]string),
},
}
// 设置默认 User-Agent
config.Headers.Set("User-Agent", DefaultUserAgent)
// POST 请求默认 Content-Type
if method == http.MethodPost {
config.Headers.Set("Content-Type", ContentTypeFormURLEncoded)
}
req := &Request{
ctx: ctx,
url: urlStr,
method: method,
config: config,
httpReq: httpReq,
autoFetch: DefaultFetchRespBody,
}
// 应用选项
for _, opt := range opts {
if opt != nil {
if err := opt(req); err != nil {
req.err = err
return req, nil // 不返回错误,累积到 req.err
}
}
}
return req, nil
}
// NewRequest 创建新请求
func NewRequest(url, method string, opts ...RequestOpt) (*Request, error) {
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
func NewRequestWithContext(ctx context.Context, url, method string, opts ...RequestOpt) (*Request, error) {
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 创建新请求(忽略错误,支持链式调用)
func NewSimpleRequest(url, method string, opts ...RequestOpt) *Request {
req, err := newRequest(context.Background(), url, method, opts...)
if err != nil {
// 返回一个带错误的请求
return &Request{
ctx: context.Background(),
url: url,
method: method,
err: err,
config: &RequestConfig{
Headers: make(http.Header),
Queries: make(map[string][]string),
Body: BodyConfig{
FormData: make(map[string][]string),
},
},
}
}
return req
}
// NewSimpleRequestWithContext 创建新请求(带 context忽略错误
func NewSimpleRequestWithContext(ctx context.Context, url, method string, opts ...RequestOpt) *Request {
ctx = normalizeContext(ctx)
req, err := newRequest(ctx, url, method, opts...)
if err != nil {
return &Request{
ctx: ctx,
url: url,
method: method,
err: err,
config: &RequestConfig{
Headers: make(http.Header),
Queries: make(map[string][]string),
Body: BodyConfig{
FormData: make(map[string][]string),
},
},
}
}
return req
}
// Clone 克隆请求
func (r *Request) Clone() *Request {
cloned := &Request{
ctx: r.ctx,
url: r.url,
method: r.method,
err: r.err,
config: r.config.Clone(),
client: r.client,
httpClient: r.httpClient,
retry: cloneRetryPolicy(r.retry),
traceHooks: r.traceHooks,
applied: false, // 重置应用状态
doRaw: r.doRaw,
autoFetch: r.autoFetch,
rawSourceExternal: r.rawSourceExternal,
}
// 重新创建 http.Request
if !r.doRaw {
cloned.httpReq, _ = http.NewRequestWithContext(cloned.ctx, cloned.method, cloned.url, nil)
} else {
rawTemplate, err := cloneRawHTTPRequest(r.rawBaseRequest(), cloned.ctx)
cloned.rawTemplate = rawTemplate
cloned.httpReq = rawTemplate
if err != nil && cloned.err == nil {
cloned.err = err
}
}
return cloned
}
// Err 获取累积的错误
func (r *Request) Err() error {
return r.err
}
// Context 获取 context
func (r *Request) Context() context.Context {
return r.ctx
}
// SetContext 设置 context
func (r *Request) SetContext(ctx context.Context) *Request {
return r.applyMutation(mutateContext(ctx))
}
// Method 获取 HTTP 方法
func (r *Request) Method() string {
return r.method
}
// SetMethod 设置 HTTP 方法
func (r *Request) SetMethod(method string) *Request {
if r.err != nil {
return r
}
method = strings.ToUpper(method)
if !validMethod(method) {
r.err = wrapError(ErrInvalidMethod, "method: %s", method)
return r
}
r.method = method
if r.httpReq != nil {
r.httpReq.Method = method
}
if r.doRaw && r.rawTemplate != nil {
r.rawTemplate.Method = method
}
r.invalidatePreparedState()
return r
}
// URL 获取 URL
func (r *Request) URL() string {
return r.url
}
// SetURL 设置 URL
func (r *Request) SetURL(urlStr string) *Request {
if r.err != nil {
return r
}
if r.doRaw {
r.err = fmt.Errorf("cannot set URL when using raw request")
return r
}
u, err := url.Parse(urlStr)
if err != nil {
r.err = wrapError(ErrInvalidURL, "url: %s", urlStr)
return r
}
r.url = urlStr
u.Host = removeEmptyPort(u.Host)
r.httpReq.URL = u
r.syncRequestHost()
r.invalidatePreparedState()
return r
}
func (r *Request) effectiveRequestHost() string {
if r == nil {
return ""
}
if r.config != nil && r.config.Host != "" {
return r.config.Host
}
if r.httpReq != nil && r.httpReq.URL != nil {
return removeEmptyPort(r.httpReq.URL.Host)
}
if r.url == "" {
return ""
}
u, err := url.Parse(r.url)
if err != nil {
return ""
}
return removeEmptyPort(u.Host)
}
func (r *Request) syncRequestHost() {
if r == nil || r.httpReq == nil {
return
}
r.httpReq.Host = r.effectiveRequestHost()
}
// RawRequest 获取底层 http.Request
func (r *Request) RawRequest() *http.Request {
if r != nil && r.doRaw && r.rawTemplate != nil && !r.applied {
return r.rawTemplate
}
return r.httpReq
}
// SetRawRequest 设置底层 http.Request启用原始模式
func (r *Request) SetRawRequest(httpReq *http.Request) *Request {
return r.applyMutation(mutateRawRequest(httpReq))
}
// EnableRawMode 启用原始模式(不修改请求)
func (r *Request) EnableRawMode() *Request {
if r.doRaw {
return r
}
r.doRaw = true
r.invalidatePreparedState()
return r
}
// DisableRawMode 禁用原始模式
func (r *Request) DisableRawMode() *Request {
if !r.doRaw {
return r
}
if r.rawSourceExternal {
r.err = fmt.Errorf("cannot disable raw mode after SetRawRequest")
return r
}
r.doRaw = false
r.invalidatePreparedState()
return r
}
// SetAutoFetch 设置是否自动获取响应体
func (r *Request) SetAutoFetch(auto bool) *Request {
r.autoFetch = auto
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 执行请求
func (r *Request) Do() (*Response, error) {
// 检查累积的错误
if r.err != nil {
return nil, r.err
}
if r.hasRetryPolicy() {
return r.doWithRetry()
}
return r.doOnce()
}
func (r *Request) doOnce() (*Response, error) {
if err := r.rebuildExecutionRequestBase(); err != nil {
return nil, wrapError(err, "rebuild execution request")
}
// 准备请求
if err := r.prepare(); err != nil {
return nil, wrapError(err, "prepare request")
}
// 执行请求
httpResp, err := r.httpClient.Do(r.httpReq)
if err != nil {
if r.cancel != nil {
r.cancel()
r.cancel = nil
}
return &Response{
Response: &http.Response{},
request: r,
httpClient: r.httpClient,
body: &Body{},
}, wrapError(err, "do request")
}
rawBody := httpResp.Body
if r.cancel != nil {
rawBody = &cancelReadCloser{
ReadCloser: httpResp.Body,
cancel: r.cancel,
}
}
// 创建响应
resp := &Response{
Response: httpResp,
request: r,
httpClient: r.httpClient,
cancel: r.cancel,
body: &Body{
raw: rawBody,
maxBytes: r.config.MaxRespBodyBytes,
},
}
r.cancel = nil
// 自动获取响应体
if r.autoFetch {
if err := resp.body.readAll(); err != nil {
_ = resp.Close()
return resp, err
}
}
return resp, nil
}
// Get 发送 GET 请求
func (r *Request) Get() (*Response, error) {
return r.SetMethod(http.MethodGet).Do()
}
// Post 发送 POST 请求
func (r *Request) Post() (*Response, error) {
return r.SetMethod(http.MethodPost).Do()
}
// Put 发送 PUT 请求
func (r *Request) Put() (*Response, error) {
return r.SetMethod(http.MethodPut).Do()
}
// Delete 发送 DELETE 请求
func (r *Request) Delete() (*Response, error) {
return r.SetMethod(http.MethodDelete).Do()
}
// Head 发送 HEAD 请求
func (r *Request) Head() (*Response, error) {
return r.SetMethod(http.MethodHead).Do()
}
// Patch 发送 PATCH 请求
func (r *Request) Patch() (*Response, error) {
return r.SetMethod(http.MethodPatch).Do()
}
// Options 发送 OPTIONS 请求
func (r *Request) Options() (*Response, error) {
return r.SetMethod(http.MethodOptions).Do()
}
// Trace 发送 TRACE 请求
func (r *Request) Trace() (*Response, error) {
return r.SetMethod(http.MethodTrace).Do()
}
// Connect 发送 CONNECT 请求
func (r *Request) Connect() (*Response, error) {
return r.SetMethod(http.MethodConnect).Do()
}