fix(openai-oauth): 改进错误处理和代理支持

- 使用 ApplicationError 返回详细错误信息到前端
- 添加 User-Agent: codex-cli/0.91.0
- 移除 ForceHTTP2 以兼容 HTTP 代理
- 修复代理获取失败时静默忽略的问题
- 500 错误时记录完整错误日志

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Gemini Wen
2026-01-27 19:13:01 +08:00
parent 56a1e29cdd
commit ab092e88a8
3 changed files with 38 additions and 27 deletions

View File

@@ -2,6 +2,7 @@
package response package response
import ( import (
"log"
"math" "math"
"net/http" "net/http"
@@ -74,6 +75,12 @@ func ErrorFrom(c *gin.Context, err error) bool {
} }
statusCode, status := infraerrors.ToHTTP(err) statusCode, status := infraerrors.ToHTTP(err)
// Log internal errors with full details for debugging
if statusCode >= 500 {
log.Printf("[ERROR] %s %s\n Error: %s", c.Request.Method, c.Request.URL.Path, err.Error())
}
ErrorWithDetails(c, statusCode, status.Message, status.Reason, status.Metadata) ErrorWithDetails(c, statusCode, status.Message, status.Reason, status.Metadata)
return true return true
} }

View File

@@ -2,11 +2,11 @@ package repository
import ( import (
"context" "context"
"fmt" "net/http"
"net/url" "net/url"
"strings"
"time" "time"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/Wei-Shaw/sub2api/internal/pkg/openai"
"github.com/Wei-Shaw/sub2api/internal/service" "github.com/Wei-Shaw/sub2api/internal/service"
"github.com/imroc/req/v3" "github.com/imroc/req/v3"
@@ -22,7 +22,7 @@ type openaiOAuthService struct {
} }
func (s *openaiOAuthService) ExchangeCode(ctx context.Context, code, codeVerifier, redirectURI, proxyURL string) (*openai.TokenResponse, error) { func (s *openaiOAuthService) ExchangeCode(ctx context.Context, code, codeVerifier, redirectURI, proxyURL string) (*openai.TokenResponse, error) {
client := createOpenAIReqClient(s.tokenURL, proxyURL) client := createOpenAIReqClient(proxyURL)
if redirectURI == "" { if redirectURI == "" {
redirectURI = openai.DefaultRedirectURI redirectURI = openai.DefaultRedirectURI
@@ -39,23 +39,24 @@ func (s *openaiOAuthService) ExchangeCode(ctx context.Context, code, codeVerifie
resp, err := client.R(). resp, err := client.R().
SetContext(ctx). SetContext(ctx).
SetHeader("User-Agent", "codex-cli/0.91.0").
SetFormDataFromValues(formData). SetFormDataFromValues(formData).
SetSuccessResult(&tokenResp). SetSuccessResult(&tokenResp).
Post(s.tokenURL) Post(s.tokenURL)
if err != nil { if err != nil {
return nil, fmt.Errorf("request failed: %w", err) return nil, infraerrors.Newf(http.StatusBadGateway, "OPENAI_OAUTH_REQUEST_FAILED", "request failed: %v", err)
} }
if !resp.IsSuccessState() { if !resp.IsSuccessState() {
return nil, fmt.Errorf("token exchange failed: status %d, body: %s", resp.StatusCode, resp.String()) return nil, infraerrors.Newf(http.StatusBadGateway, "OPENAI_OAUTH_TOKEN_EXCHANGE_FAILED", "token exchange failed: status %d, body: %s", resp.StatusCode, resp.String())
} }
return &tokenResp, nil return &tokenResp, nil
} }
func (s *openaiOAuthService) RefreshToken(ctx context.Context, refreshToken, proxyURL string) (*openai.TokenResponse, error) { func (s *openaiOAuthService) RefreshToken(ctx context.Context, refreshToken, proxyURL string) (*openai.TokenResponse, error) {
client := createOpenAIReqClient(s.tokenURL, proxyURL) client := createOpenAIReqClient(proxyURL)
formData := url.Values{} formData := url.Values{}
formData.Set("grant_type", "refresh_token") formData.Set("grant_type", "refresh_token")
@@ -67,29 +68,25 @@ func (s *openaiOAuthService) RefreshToken(ctx context.Context, refreshToken, pro
resp, err := client.R(). resp, err := client.R().
SetContext(ctx). SetContext(ctx).
SetHeader("User-Agent", "codex-cli/0.91.0").
SetFormDataFromValues(formData). SetFormDataFromValues(formData).
SetSuccessResult(&tokenResp). SetSuccessResult(&tokenResp).
Post(s.tokenURL) Post(s.tokenURL)
if err != nil { if err != nil {
return nil, fmt.Errorf("request failed: %w", err) return nil, infraerrors.Newf(http.StatusBadGateway, "OPENAI_OAUTH_REQUEST_FAILED", "request failed: %v", err)
} }
if !resp.IsSuccessState() { if !resp.IsSuccessState() {
return nil, fmt.Errorf("token refresh failed: status %d, body: %s", resp.StatusCode, resp.String()) return nil, infraerrors.Newf(http.StatusBadGateway, "OPENAI_OAUTH_TOKEN_REFRESH_FAILED", "token refresh failed: status %d, body: %s", resp.StatusCode, resp.String())
} }
return &tokenResp, nil return &tokenResp, nil
} }
func createOpenAIReqClient(tokenURL, proxyURL string) *req.Client { func createOpenAIReqClient(proxyURL string) *req.Client {
forceHTTP2 := false
if parsedURL, err := url.Parse(tokenURL); err == nil {
forceHTTP2 = strings.EqualFold(parsedURL.Scheme, "https")
}
return getSharedReqClient(reqClientOptions{ return getSharedReqClient(reqClientOptions{
ProxyURL: proxyURL, ProxyURL: proxyURL,
Timeout: 120 * time.Second, Timeout: 120 * time.Second,
ForceHTTP2: forceHTTP2,
}) })
} }

View File

@@ -2,9 +2,10 @@ package service
import ( import (
"context" "context"
"fmt" "net/http"
"time" "time"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/Wei-Shaw/sub2api/internal/pkg/openai"
) )
@@ -35,12 +36,12 @@ func (s *OpenAIOAuthService) GenerateAuthURL(ctx context.Context, proxyID *int64
// Generate PKCE values // Generate PKCE values
state, err := openai.GenerateState() state, err := openai.GenerateState()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to generate state: %w", err) return nil, infraerrors.Newf(http.StatusInternalServerError, "OPENAI_OAUTH_STATE_FAILED", "failed to generate state: %v", err)
} }
codeVerifier, err := openai.GenerateCodeVerifier() codeVerifier, err := openai.GenerateCodeVerifier()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to generate code verifier: %w", err) return nil, infraerrors.Newf(http.StatusInternalServerError, "OPENAI_OAUTH_VERIFIER_FAILED", "failed to generate code verifier: %v", err)
} }
codeChallenge := openai.GenerateCodeChallenge(codeVerifier) codeChallenge := openai.GenerateCodeChallenge(codeVerifier)
@@ -48,14 +49,17 @@ func (s *OpenAIOAuthService) GenerateAuthURL(ctx context.Context, proxyID *int64
// Generate session ID // Generate session ID
sessionID, err := openai.GenerateSessionID() sessionID, err := openai.GenerateSessionID()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to generate session ID: %w", err) return nil, infraerrors.Newf(http.StatusInternalServerError, "OPENAI_OAUTH_SESSION_FAILED", "failed to generate session ID: %v", err)
} }
// Get proxy URL if specified // Get proxy URL if specified
var proxyURL string var proxyURL string
if proxyID != nil { if proxyID != nil {
proxy, err := s.proxyRepo.GetByID(ctx, *proxyID) proxy, err := s.proxyRepo.GetByID(ctx, *proxyID)
if err == nil && proxy != nil { if err != nil {
return nil, infraerrors.Newf(http.StatusBadRequest, "OPENAI_OAUTH_PROXY_NOT_FOUND", "proxy not found: %v", err)
}
if proxy != nil {
proxyURL = proxy.URL() proxyURL = proxy.URL()
} }
} }
@@ -110,14 +114,17 @@ func (s *OpenAIOAuthService) ExchangeCode(ctx context.Context, input *OpenAIExch
// Get session // Get session
session, ok := s.sessionStore.Get(input.SessionID) session, ok := s.sessionStore.Get(input.SessionID)
if !ok { if !ok {
return nil, fmt.Errorf("session not found or expired") return nil, infraerrors.New(http.StatusBadRequest, "OPENAI_OAUTH_SESSION_NOT_FOUND", "session not found or expired")
} }
// Get proxy URL // Get proxy URL: prefer input.ProxyID, fallback to session.ProxyURL
proxyURL := session.ProxyURL proxyURL := session.ProxyURL
if input.ProxyID != nil { if input.ProxyID != nil {
proxy, err := s.proxyRepo.GetByID(ctx, *input.ProxyID) proxy, err := s.proxyRepo.GetByID(ctx, *input.ProxyID)
if err == nil && proxy != nil { if err != nil {
return nil, infraerrors.Newf(http.StatusBadRequest, "OPENAI_OAUTH_PROXY_NOT_FOUND", "proxy not found: %v", err)
}
if proxy != nil {
proxyURL = proxy.URL() proxyURL = proxy.URL()
} }
} }
@@ -131,7 +138,7 @@ func (s *OpenAIOAuthService) ExchangeCode(ctx context.Context, input *OpenAIExch
// Exchange code for token // Exchange code for token
tokenResp, err := s.oauthClient.ExchangeCode(ctx, input.Code, session.CodeVerifier, redirectURI, proxyURL) tokenResp, err := s.oauthClient.ExchangeCode(ctx, input.Code, session.CodeVerifier, redirectURI, proxyURL)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to exchange code: %w", err) return nil, err
} }
// Parse ID token to get user info // Parse ID token to get user info
@@ -201,12 +208,12 @@ func (s *OpenAIOAuthService) RefreshToken(ctx context.Context, refreshToken stri
// RefreshAccountToken refreshes token for an OpenAI account // RefreshAccountToken refreshes token for an OpenAI account
func (s *OpenAIOAuthService) RefreshAccountToken(ctx context.Context, account *Account) (*OpenAITokenInfo, error) { func (s *OpenAIOAuthService) RefreshAccountToken(ctx context.Context, account *Account) (*OpenAITokenInfo, error) {
if !account.IsOpenAI() { if !account.IsOpenAI() {
return nil, fmt.Errorf("account is not an OpenAI account") return nil, infraerrors.New(http.StatusBadRequest, "OPENAI_OAUTH_INVALID_ACCOUNT", "account is not an OpenAI account")
} }
refreshToken := account.GetOpenAIRefreshToken() refreshToken := account.GetOpenAIRefreshToken()
if refreshToken == "" { if refreshToken == "" {
return nil, fmt.Errorf("no refresh token available") return nil, infraerrors.New(http.StatusBadRequest, "OPENAI_OAUTH_NO_REFRESH_TOKEN", "no refresh token available")
} }
var proxyURL string var proxyURL string