* fix(relay): set Request.GetBody so the HTTP/2 transport can transparently retry after an upstream stream reset
The outbound request body is a type-erased io.Reader over BodyStorage, so
net/http cannot derive Request.GetBody (it only does so for *bytes.Reader,
*bytes.Buffer and *strings.Reader). With GetBody nil, the HTTP/2 transport
cannot transparently retry a request once the body has been written and the
upstream resets the stream with a retryable error (REFUSED_STREAM, or a
connection-level GOAWAY); the relay request then fails with:
http2: Transport: cannot retry err [...] after Request.Body was written;
define Request.GetBody to avoid this error
This affects every relay path that goes through DoApiRequest (chat, claude,
gemini, responses, embedding, image, rerank).
BodyStorage (memory and disk) already implements io.Seeker, so replay support
only needed wiring:
- NewOutboundJSONBody additionally returns a getBody that rewinds the storage
and hands out a fresh non-closing reader. The transport only calls GetBody
after the previous attempt's body has been abandoned, so the rewind cannot
race an in-flight read.
- RelayInfo carries it in the new UpstreamRequestGetBody field, set alongside
UpstreamRequestBodySize by the handlers that build storage-backed bodies.
- applyUpstreamGetBody (symmetric with applyUpstreamContentLength) wires it
into DoApiRequest/DoFormRequest/DoTaskApiRequest, only when req.GetBody is
still nil.
Also remove the hand-rolled GetBody override in DoTaskApiRequest: it returned
the same already-consumed reader, so any transport-level replay would have
silently sent an empty body, and it clobbered the correct snapshot-based
GetBody that net/http derives from the *bytes.Reader bodies the task adaptors
pass in. For non-replayable bodies GetBody now stays nil, so a retry fails
loudly instead of corrupting the request.
Covered by unit tests plus an end-to-end raw-frame HTTP/2 test that resets
the first stream with REFUSED_STREAM after the body is written and asserts
the transport transparently retries with the complete body.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(relay): hand out independent readers from GetBody (address review)
Per the http.Request.GetBody contract ("returns a new copy of Body"),
each call must yield a reader with its own cursor. The previous
implementation rewound and reused the shared BodyStorage, so two
consecutive GetBody readers would interfere with each other, and a
replay could disturb the primary body's offset under extreme transport
timing (e.g. attempt N's body write not yet fully abandoned when the
transport builds attempt N+1).
Instead of snapshotting the payload (an extra copy), add
BodyStorage.NewReader, which returns an independent zero-copy reader:
- memory mode: a fresh bytes.Reader over the same immutable backing
array;
- disk mode: a separate file descriptor over the cache file, so the
transport closing a replayed body only closes that descriptor.
NewOutboundJSONBody's getBody now simply hands out storage.NewReader,
and once the handler releases the storage, GetBody fails with
ErrStorageClosed instead of replaying stale data.
Tests: interleaved reads across two replay readers and the primary
body each observe exactly their own byte stream, for both the memory
and the disk-backed storage; the existing GetBody and HTTP/2 retry
suites still pass (h2 e2e tests flake-free with -count=20).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(relay): bind replayable metadata on pass-through requests
* fix(relay): reset upstream body metadata between channels
* test(relay): cover replay across retries and channel attempts
* fix(relay): stop following upstream redirects
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
611 lines
19 KiB
Go
611 lines
19 KiB
Go
package channel
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
common2 "github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/logger"
|
|
"github.com/QuantumNous/new-api/relay/common"
|
|
"github.com/QuantumNous/new-api/relay/constant"
|
|
"github.com/QuantumNous/new-api/relay/helper"
|
|
"github.com/QuantumNous/new-api/relaykit/types"
|
|
"github.com/QuantumNous/new-api/service"
|
|
"github.com/QuantumNous/new-api/setting/operation_setting"
|
|
|
|
"github.com/bytedance/gopkg/util/gopool"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
// applyUpstreamContentLength populates req.ContentLength when the upstream
|
|
// body is wrapped in a BodyStorage (see relay/common/outbound_body.go).
|
|
//
|
|
// net/http.NewRequest only auto-detects ContentLength for *bytes.Reader,
|
|
// *bytes.Buffer and *strings.Reader. When the body is a type-erased io.Reader
|
|
// (which is the case for ReaderOnly(BodyStorage)), the Content-Length header
|
|
// would otherwise be omitted, forcing chunked transfer encoding and breaking
|
|
// some upstreams that require an explicit Content-Length.
|
|
func applyUpstreamContentLength(req *http.Request, info *common.RelayInfo) {
|
|
if info == nil {
|
|
return
|
|
}
|
|
if info.UpstreamRequestBodySize > 0 && req.ContentLength <= 0 {
|
|
req.ContentLength = info.UpstreamRequestBodySize
|
|
}
|
|
}
|
|
|
|
// applyUpstreamGetBody populates req.GetBody when the upstream body is wrapped
|
|
// in a BodyStorage (see relay/common/outbound_body.go).
|
|
//
|
|
// net/http.NewRequest only auto-populates GetBody for *bytes.Reader,
|
|
// *bytes.Buffer and *strings.Reader. When the body is a type-erased io.Reader
|
|
// (which is the case for ReaderOnly(BodyStorage)), GetBody would otherwise stay
|
|
// nil, and the HTTP/2 transport cannot transparently retry the request once the
|
|
// upstream resets the stream after the body was already written; the request
|
|
// then fails with "http2: Transport: cannot retry err ... after Request.Body
|
|
// was written; define Request.GetBody to avoid this error".
|
|
func applyUpstreamGetBody(req *http.Request, info *common.RelayInfo) {
|
|
if info == nil || info.UpstreamRequestGetBody == nil {
|
|
return
|
|
}
|
|
if req.GetBody == nil {
|
|
req.GetBody = info.UpstreamRequestGetBody
|
|
}
|
|
}
|
|
|
|
// ApplyUpstreamBodyMetadata restores metadata that net/http cannot infer when
|
|
// a BodyStorage is exposed through a type-erased reader. Provider adaptors
|
|
// that construct requests directly should call this before sending them.
|
|
func ApplyUpstreamBodyMetadata(req *http.Request, info *common.RelayInfo) {
|
|
applyUpstreamContentLength(req, info)
|
|
applyUpstreamGetBody(req, info)
|
|
}
|
|
|
|
func SetupApiRequestHeader(info *common.RelayInfo, c *gin.Context, req *http.Header) {
|
|
if info.RelayMode == constant.RelayModeAudioTranscription || info.RelayMode == constant.RelayModeAudioTranslation {
|
|
// multipart/form-data
|
|
} else if info.RelayMode == constant.RelayModeRealtime {
|
|
// websocket
|
|
} else {
|
|
req.Set("Content-Type", c.Request.Header.Get("Content-Type"))
|
|
req.Set("Accept", c.Request.Header.Get("Accept"))
|
|
if info.IsStream && c.Request.Header.Get("Accept") == "" {
|
|
req.Set("Accept", "text/event-stream")
|
|
}
|
|
}
|
|
}
|
|
|
|
const clientHeaderPlaceholderPrefix = "{client_header:"
|
|
|
|
const (
|
|
headerPassthroughAllKey = "*"
|
|
headerPassthroughRegexPrefix = "re:"
|
|
headerPassthroughRegexPrefixV2 = "regex:"
|
|
)
|
|
|
|
var passthroughSkipHeaderNamesLower = map[string]struct{}{
|
|
// RFC 7230 hop-by-hop headers.
|
|
"connection": {},
|
|
"keep-alive": {},
|
|
"proxy-authenticate": {},
|
|
"proxy-authorization": {},
|
|
"te": {},
|
|
"trailer": {},
|
|
"transfer-encoding": {},
|
|
"upgrade": {},
|
|
|
|
"cookie": {},
|
|
|
|
// Additional headers that should not be forwarded by name-matching passthrough rules.
|
|
"host": {},
|
|
"content-length": {},
|
|
"accept-encoding": {},
|
|
|
|
// Do not passthrough credentials by wildcard/regex.
|
|
"authorization": {},
|
|
"x-api-key": {},
|
|
"x-goog-api-key": {},
|
|
|
|
// WebSocket handshake headers are generated by the client/dialer.
|
|
"sec-websocket-key": {},
|
|
"sec-websocket-version": {},
|
|
"sec-websocket-extensions": {},
|
|
}
|
|
|
|
var headerPassthroughRegexCache sync.Map // map[string]*regexp.Regexp
|
|
|
|
func getHeaderPassthroughRegex(pattern string) (*regexp.Regexp, error) {
|
|
pattern = strings.TrimSpace(pattern)
|
|
if pattern == "" {
|
|
return nil, errors.New("empty regex pattern")
|
|
}
|
|
if v, ok := headerPassthroughRegexCache.Load(pattern); ok {
|
|
if re, ok := v.(*regexp.Regexp); ok {
|
|
return re, nil
|
|
}
|
|
headerPassthroughRegexCache.Delete(pattern)
|
|
}
|
|
compiled, err := regexp.Compile(pattern)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
actual, _ := headerPassthroughRegexCache.LoadOrStore(pattern, compiled)
|
|
if re, ok := actual.(*regexp.Regexp); ok {
|
|
return re, nil
|
|
}
|
|
return compiled, nil
|
|
}
|
|
|
|
func IsHeaderPassthroughRuleKey(key string) bool {
|
|
return isHeaderPassthroughRuleKey(key)
|
|
}
|
|
func isHeaderPassthroughRuleKey(key string) bool {
|
|
key = strings.TrimSpace(key)
|
|
if key == "" {
|
|
return false
|
|
}
|
|
if key == headerPassthroughAllKey {
|
|
return true
|
|
}
|
|
lower := strings.ToLower(key)
|
|
return strings.HasPrefix(lower, headerPassthroughRegexPrefix) || strings.HasPrefix(lower, headerPassthroughRegexPrefixV2)
|
|
}
|
|
|
|
func shouldSkipPassthroughHeader(name string) bool {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return true
|
|
}
|
|
lower := strings.ToLower(name)
|
|
if _, ok := passthroughSkipHeaderNamesLower[lower]; ok {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func applyHeaderOverridePlaceholders(template string, c *gin.Context, apiKey string) (string, bool, error) {
|
|
trimmed := strings.TrimSpace(template)
|
|
if strings.HasPrefix(trimmed, clientHeaderPlaceholderPrefix) {
|
|
afterPrefix := trimmed[len(clientHeaderPlaceholderPrefix):]
|
|
end := strings.Index(afterPrefix, "}")
|
|
if end < 0 || end != len(afterPrefix)-1 {
|
|
return "", false, fmt.Errorf("client_header placeholder must be the full value: %q", template)
|
|
}
|
|
|
|
name := strings.TrimSpace(afterPrefix[:end])
|
|
if name == "" {
|
|
return "", false, fmt.Errorf("client_header placeholder name is empty: %q", template)
|
|
}
|
|
if c == nil || c.Request == nil {
|
|
return "", false, fmt.Errorf("missing request context for client_header placeholder")
|
|
}
|
|
clientHeaderValue := c.Request.Header.Get(name)
|
|
if strings.TrimSpace(clientHeaderValue) == "" {
|
|
return "", false, nil
|
|
}
|
|
// Do not interpolate {api_key} inside client-supplied content.
|
|
return clientHeaderValue, true, nil
|
|
}
|
|
|
|
if strings.Contains(template, "{api_key}") {
|
|
template = strings.ReplaceAll(template, "{api_key}", apiKey)
|
|
}
|
|
if strings.TrimSpace(template) == "" {
|
|
return "", false, nil
|
|
}
|
|
return template, true, nil
|
|
}
|
|
|
|
// processHeaderOverride applies channel header overrides, with placeholder substitution.
|
|
// Supported placeholders:
|
|
// - {api_key}: resolved to the channel API key
|
|
// - {client_header:<name>}: resolved to the incoming request header value
|
|
//
|
|
// Header passthrough rules (keys only; values are ignored):
|
|
// - "*": passthrough all incoming headers by name (excluding unsafe headers)
|
|
// - "re:<regex>" / "regex:<regex>": passthrough headers whose names match the regex (Go regexp)
|
|
//
|
|
// Passthrough rules are applied first, then normal overrides are applied, so explicit overrides win.
|
|
func processHeaderOverride(info *common.RelayInfo, c *gin.Context) (map[string]string, error) {
|
|
headerOverride := make(map[string]string)
|
|
if info == nil {
|
|
return headerOverride, nil
|
|
}
|
|
|
|
headerOverrideSource := common.GetEffectiveHeaderOverride(info)
|
|
|
|
passAll := false
|
|
var passthroughRegex []*regexp.Regexp
|
|
if !info.IsChannelTest {
|
|
for k := range headerOverrideSource {
|
|
key := strings.TrimSpace(strings.ToLower(k))
|
|
if key == "" {
|
|
continue
|
|
}
|
|
if key == headerPassthroughAllKey {
|
|
passAll = true
|
|
continue
|
|
}
|
|
|
|
var pattern string
|
|
switch {
|
|
case strings.HasPrefix(key, headerPassthroughRegexPrefix):
|
|
pattern = strings.TrimSpace(key[len(headerPassthroughRegexPrefix):])
|
|
case strings.HasPrefix(key, headerPassthroughRegexPrefixV2):
|
|
pattern = strings.TrimSpace(key[len(headerPassthroughRegexPrefixV2):])
|
|
default:
|
|
continue
|
|
}
|
|
|
|
if pattern == "" {
|
|
return nil, types.NewError(fmt.Errorf("header passthrough regex pattern is empty: %q", k), types.ErrorCodeChannelHeaderOverrideInvalid)
|
|
}
|
|
compiled, err := getHeaderPassthroughRegex(pattern)
|
|
if err != nil {
|
|
return nil, types.NewError(err, types.ErrorCodeChannelHeaderOverrideInvalid)
|
|
}
|
|
passthroughRegex = append(passthroughRegex, compiled)
|
|
}
|
|
}
|
|
|
|
if passAll || len(passthroughRegex) > 0 {
|
|
if c == nil || c.Request == nil {
|
|
return nil, types.NewError(fmt.Errorf("missing request context for header passthrough"), types.ErrorCodeChannelHeaderOverrideInvalid)
|
|
}
|
|
for name := range c.Request.Header {
|
|
if shouldSkipPassthroughHeader(name) {
|
|
continue
|
|
}
|
|
if !passAll {
|
|
matched := false
|
|
for _, re := range passthroughRegex {
|
|
if re.MatchString(name) {
|
|
matched = true
|
|
break
|
|
}
|
|
}
|
|
if !matched {
|
|
continue
|
|
}
|
|
}
|
|
value := strings.TrimSpace(c.Request.Header.Get(name))
|
|
if value == "" {
|
|
continue
|
|
}
|
|
headerOverride[strings.ToLower(strings.TrimSpace(name))] = value
|
|
}
|
|
}
|
|
|
|
for k, v := range headerOverrideSource {
|
|
if isHeaderPassthroughRuleKey(k) {
|
|
continue
|
|
}
|
|
key := strings.TrimSpace(strings.ToLower(k))
|
|
if key == "" {
|
|
continue
|
|
}
|
|
|
|
str, ok := v.(string)
|
|
if !ok {
|
|
return nil, types.NewError(nil, types.ErrorCodeChannelHeaderOverrideInvalid)
|
|
}
|
|
if info.IsChannelTest && strings.HasPrefix(strings.TrimSpace(str), clientHeaderPlaceholderPrefix) {
|
|
continue
|
|
}
|
|
|
|
value, include, err := applyHeaderOverridePlaceholders(str, c, info.ApiKey)
|
|
if err != nil {
|
|
return nil, types.NewError(err, types.ErrorCodeChannelHeaderOverrideInvalid)
|
|
}
|
|
if !include {
|
|
continue
|
|
}
|
|
|
|
headerOverride[key] = value
|
|
}
|
|
return headerOverride, nil
|
|
}
|
|
|
|
func ResolveHeaderOverride(info *common.RelayInfo, c *gin.Context) (map[string]string, error) {
|
|
return processHeaderOverride(info, c)
|
|
}
|
|
|
|
func applyHeaderOverrideToRequest(req *http.Request, headerOverride map[string]string) {
|
|
if req == nil {
|
|
return
|
|
}
|
|
for key, value := range headerOverride {
|
|
req.Header.Set(key, value)
|
|
// set Host in req
|
|
if strings.EqualFold(key, "Host") {
|
|
req.Host = value
|
|
}
|
|
}
|
|
}
|
|
|
|
func DoApiRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody io.Reader) (*http.Response, error) {
|
|
fullRequestURL, err := a.GetRequestURL(info)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get request url failed: %w", err)
|
|
}
|
|
logger.LogDebug(c, "fullRequestURL: %s", common.SanitizeURLForLog(fullRequestURL))
|
|
req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("new request failed: %w", err)
|
|
}
|
|
ApplyUpstreamBodyMetadata(req, info)
|
|
headers := req.Header
|
|
err = a.SetupRequestHeader(c, &headers, info)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("setup request header failed: %w", err)
|
|
}
|
|
// 在 SetupRequestHeader 之后应用 Header Override,确保用户设置优先级最高
|
|
// 这样可以覆盖默认的 Authorization header 设置
|
|
headerOverride, err := processHeaderOverride(info, c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
applyHeaderOverrideToRequest(req, headerOverride)
|
|
resp, err := doRequest(c, req, info)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("do request failed: %w", err)
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func DoFormRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody io.Reader) (*http.Response, error) {
|
|
fullRequestURL, err := a.GetRequestURL(info)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get request url failed: %w", err)
|
|
}
|
|
logger.LogDebug(c, "fullRequestURL: %s", common.SanitizeURLForLog(fullRequestURL))
|
|
req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("new request failed: %w", err)
|
|
}
|
|
ApplyUpstreamBodyMetadata(req, info)
|
|
// set form data
|
|
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
|
|
headers := req.Header
|
|
err = a.SetupRequestHeader(c, &headers, info)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("setup request header failed: %w", err)
|
|
}
|
|
// 在 SetupRequestHeader 之后应用 Header Override,确保用户设置优先级最高
|
|
// 这样可以覆盖默认的 Authorization header 设置
|
|
headerOverride, err := processHeaderOverride(info, c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
applyHeaderOverrideToRequest(req, headerOverride)
|
|
resp, err := doRequest(c, req, info)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("do request failed: %w", err)
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func DoWssRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody io.Reader) (*websocket.Conn, error) {
|
|
fullRequestURL, err := a.GetRequestURL(info)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get request url failed: %w", err)
|
|
}
|
|
targetHeader := http.Header{}
|
|
err = a.SetupRequestHeader(c, &targetHeader, info)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("setup request header failed: %w", err)
|
|
}
|
|
// 在 SetupRequestHeader 之后应用 Header Override,确保用户设置优先级最高
|
|
// 这样可以覆盖默认的 Authorization header 设置
|
|
headerOverride, err := processHeaderOverride(info, c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for key, value := range headerOverride {
|
|
targetHeader.Set(key, value)
|
|
}
|
|
targetHeader.Set("Content-Type", c.Request.Header.Get("Content-Type"))
|
|
targetConn, _, err := websocket.DefaultDialer.Dial(fullRequestURL, targetHeader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("dial failed to %s: %w", common.SanitizeURLForLog(fullRequestURL), err)
|
|
}
|
|
// send request body
|
|
//all, err := io.ReadAll(requestBody)
|
|
//err = service.WssString(c, targetConn, string(all))
|
|
return targetConn, nil
|
|
}
|
|
|
|
func startPingKeepAlive(c *gin.Context, pingInterval time.Duration) (context.CancelFunc, <-chan struct{}) {
|
|
pingerCtx, stopPinger := context.WithCancel(context.Background())
|
|
done := make(chan struct{})
|
|
|
|
gopool.Go(func() {
|
|
defer close(done)
|
|
defer func() {
|
|
// 增加panic恢复处理
|
|
if r := recover(); r != nil {
|
|
logger.LogDebug(c, "SSE ping goroutine panic recovered: %v", r)
|
|
}
|
|
logger.LogDebug(c, "SSE ping goroutine stopped")
|
|
}()
|
|
|
|
if pingInterval <= 0 {
|
|
pingInterval = helper.DefaultPingInterval
|
|
}
|
|
|
|
ticker := time.NewTicker(pingInterval)
|
|
// 确保在任何情况下都清理ticker
|
|
defer func() {
|
|
ticker.Stop()
|
|
logger.LogDebug(c, "SSE ping ticker stopped")
|
|
}()
|
|
|
|
var pingMutex sync.Mutex
|
|
logger.LogDebug(c, "SSE ping goroutine started")
|
|
|
|
// 增加超时控制,防止goroutine长时间运行
|
|
maxPingDuration := 120 * time.Minute // 最大ping持续时间
|
|
pingTimeout := time.NewTimer(maxPingDuration)
|
|
defer pingTimeout.Stop()
|
|
|
|
for {
|
|
select {
|
|
// 发送 ping 数据
|
|
case <-ticker.C:
|
|
if err := sendPingData(c, &pingMutex); err != nil {
|
|
logger.LogDebug(c, "SSE ping error, stopping goroutine: %s", err.Error())
|
|
return
|
|
}
|
|
// 收到退出信号
|
|
case <-pingerCtx.Done():
|
|
return
|
|
// request 结束
|
|
case <-c.Request.Context().Done():
|
|
return
|
|
// 超时保护,防止goroutine无限运行
|
|
case <-pingTimeout.C:
|
|
logger.LogDebug(c, "SSE ping goroutine timeout, stopping")
|
|
return
|
|
}
|
|
}
|
|
})
|
|
|
|
return stopPinger, done
|
|
}
|
|
|
|
func sendPingData(c *gin.Context, mutex *sync.Mutex) error {
|
|
mutex.Lock()
|
|
defer mutex.Unlock()
|
|
|
|
// Bound the write so a slow client cannot block this goroutine forever;
|
|
// doRequest's defer waits for the pinger to exit before returning.
|
|
helper.ExtendWriteDeadline(c)
|
|
err := helper.PingData(c)
|
|
if err != nil {
|
|
logger.LogError(c, "SSE ping error: "+err.Error())
|
|
return err
|
|
}
|
|
|
|
logger.LogDebug(c, "SSE ping data sent")
|
|
return nil
|
|
}
|
|
|
|
func DoRequest(c *gin.Context, req *http.Request, info *common.RelayInfo) (*http.Response, error) {
|
|
return doRequest(c, req, info)
|
|
}
|
|
|
|
// keepUpstreamRedirectResponse stops net/http from following redirects while
|
|
// returning the upstream 3xx response to the relay without an extra error.
|
|
func keepUpstreamRedirectResponse(_ *http.Request, _ []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
}
|
|
|
|
func doRequest(c *gin.Context, req *http.Request, info *common.RelayInfo) (*http.Response, error) {
|
|
client, err := service.GetHttpClientWithProxySettings(info.ChannelSetting.Proxy, info.ChannelSetting)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("new proxy http client failed: %w", err)
|
|
}
|
|
// Clients are cached and shared across channels, so override redirect
|
|
// behavior on a shallow copy instead of mutating the cached client. This
|
|
// still reuses its transport and connection pools, including HTTP/2's
|
|
// transparent stream retries.
|
|
relayClient := *client
|
|
relayClient.CheckRedirect = keepUpstreamRedirectResponse
|
|
if common2.DebugEnabled && req != nil && req.URL != nil {
|
|
policy := service.NormalizeHTTPTransportPolicy(info.ChannelSetting)
|
|
logger.LogDebug(c, fmt.Sprintf(
|
|
"http transport select: host=%s protocol=%s shards=%d policy=%s",
|
|
req.URL.Host,
|
|
policy.Protocol,
|
|
policy.Shards,
|
|
policy.String(),
|
|
))
|
|
}
|
|
|
|
var stopPinger context.CancelFunc
|
|
var pingerDone <-chan struct{}
|
|
if info.IsStream {
|
|
helper.SetEventStreamHeaders(c)
|
|
// 处理流式请求的 ping 保活
|
|
generalSettings := operation_setting.GetGeneralSetting()
|
|
if generalSettings.PingIntervalEnabled && !info.DisablePing {
|
|
pingInterval := time.Duration(generalSettings.PingIntervalSeconds) * time.Second
|
|
stopPinger, pingerDone = startPingKeepAlive(c, pingInterval)
|
|
// 使用defer确保在任何情况下都能停止ping goroutine
|
|
defer func() {
|
|
if stopPinger != nil {
|
|
stopPinger()
|
|
<-pingerDone
|
|
logger.LogDebug(c, "SSE ping goroutine stopped by defer")
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
resp, err := relayClient.Do(req)
|
|
if err != nil {
|
|
logger.LogError(c, "do request failed: "+err.Error())
|
|
return nil, types.NewError(err, types.ErrorCodeDoRequestFailed, types.ErrOptionWithHideErrMsg("upstream error: do request failed"))
|
|
}
|
|
if resp == nil {
|
|
return nil, errors.New("resp is nil")
|
|
}
|
|
if common2.DebugEnabled {
|
|
policy := service.NormalizeHTTPTransportPolicy(info.ChannelSetting)
|
|
logger.LogDebug(c, fmt.Sprintf(
|
|
"http transport negotiated: host=%s protocol=%s shards=%d policy=%s negotiated=%s",
|
|
req.URL.Host,
|
|
policy.Protocol,
|
|
policy.Shards,
|
|
policy.String(),
|
|
resp.Proto,
|
|
))
|
|
}
|
|
|
|
if upID := resp.Header.Get(common2.RequestIdKey); upID != "" {
|
|
c.Set(common2.UpstreamRequestIdKey, upID)
|
|
}
|
|
|
|
_ = req.Body.Close()
|
|
_ = c.Request.Body.Close()
|
|
return resp, nil
|
|
}
|
|
|
|
func DoTaskApiRequest(a TaskAdaptor, c *gin.Context, info *common.RelayInfo, requestBody io.Reader) (*http.Response, error) {
|
|
fullRequestURL, err := a.BuildRequestURL(info)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("new request failed: %w", err)
|
|
}
|
|
ApplyUpstreamBodyMetadata(req, info)
|
|
// Do NOT wrap requestBody in a GetBody closure here: returning the same
|
|
// (already consumed) reader would make any transport-level retry silently
|
|
// replay an empty body. http.NewRequest already derives a correct,
|
|
// snapshot-based GetBody for *bytes.Reader/Buffer/strings.Reader bodies
|
|
// (which most task adaptors pass in); for type-erased readers,
|
|
// ApplyUpstreamBodyMetadata wires a replayable body when one is available.
|
|
// Otherwise GetBody stays nil so the transport fails the retry instead of
|
|
// sending a corrupted request.
|
|
|
|
err = a.BuildRequestHeader(c, req, info)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("setup request header failed: %w", err)
|
|
}
|
|
resp, err := doRequest(c, req, info)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("do request failed: %w", err)
|
|
}
|
|
return resp, nil
|
|
}
|