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:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user