diff --git a/backend/cmd/server/VERSION b/backend/cmd/server/VERSION index 79e0dd8a4..a2d633db7 100644 --- a/backend/cmd/server/VERSION +++ b/backend/cmd/server/VERSION @@ -1 +1 @@ -0.1.46 +0.1.61 diff --git a/backend/internal/pkg/response/response.go b/backend/internal/pkg/response/response.go index 43fe12d4e..c5b41d6e9 100644 --- a/backend/internal/pkg/response/response.go +++ b/backend/internal/pkg/response/response.go @@ -2,6 +2,7 @@ package response import ( + "log" "math" "net/http" @@ -74,6 +75,12 @@ func ErrorFrom(c *gin.Context, err error) bool { } statusCode, status := infraerrors.ToHTTP(err) + + // Log internal errors with full details for debugging + if statusCode >= 500 && c.Request != nil { + 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) return true } diff --git a/backend/internal/repository/openai_oauth_service.go b/backend/internal/repository/openai_oauth_service.go index b7f3606f4..394d3a1a5 100644 --- a/backend/internal/repository/openai_oauth_service.go +++ b/backend/internal/repository/openai_oauth_service.go @@ -2,11 +2,11 @@ package repository import ( "context" - "fmt" + "net/http" "net/url" - "strings" "time" + infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors" "github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/Wei-Shaw/sub2api/internal/service" "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) { - client := createOpenAIReqClient(s.tokenURL, proxyURL) + client := createOpenAIReqClient(proxyURL) if redirectURI == "" { redirectURI = openai.DefaultRedirectURI @@ -39,23 +39,24 @@ func (s *openaiOAuthService) ExchangeCode(ctx context.Context, code, codeVerifie resp, err := client.R(). SetContext(ctx). + SetHeader("User-Agent", "codex-cli/0.91.0"). SetFormDataFromValues(formData). SetSuccessResult(&tokenResp). Post(s.tokenURL) 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() { - 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 } 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.Set("grant_type", "refresh_token") @@ -67,29 +68,25 @@ func (s *openaiOAuthService) RefreshToken(ctx context.Context, refreshToken, pro resp, err := client.R(). SetContext(ctx). + SetHeader("User-Agent", "codex-cli/0.91.0"). SetFormDataFromValues(formData). SetSuccessResult(&tokenResp). Post(s.tokenURL) 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() { - 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 } -func createOpenAIReqClient(tokenURL, proxyURL string) *req.Client { - forceHTTP2 := false - if parsedURL, err := url.Parse(tokenURL); err == nil { - forceHTTP2 = strings.EqualFold(parsedURL.Scheme, "https") - } +func createOpenAIReqClient(proxyURL string) *req.Client { return getSharedReqClient(reqClientOptions{ - ProxyURL: proxyURL, - Timeout: 120 * time.Second, - ForceHTTP2: forceHTTP2, + ProxyURL: proxyURL, + Timeout: 120 * time.Second, }) } diff --git a/backend/internal/repository/req_client_pool_test.go b/backend/internal/repository/req_client_pool_test.go index cf7e8bd05..904ed4d6e 100644 --- a/backend/internal/repository/req_client_pool_test.go +++ b/backend/internal/repository/req_client_pool_test.go @@ -77,21 +77,9 @@ func TestGetSharedReqClient_ImpersonateAndProxy(t *testing.T) { require.Equal(t, "http://proxy.local:8080|4s|true|false", buildReqClientKey(opts)) } -func TestCreateOpenAIReqClient_ForceHTTP2Enabled(t *testing.T) { - sharedReqClients = sync.Map{} - client := createOpenAIReqClient("https://auth.openai.com/oauth/token", "http://proxy.local:8080") - require.Equal(t, "2", forceHTTPVersion(t, client)) -} - -func TestCreateOpenAIReqClient_ForceHTTP2DisabledForHTTP(t *testing.T) { - sharedReqClients = sync.Map{} - client := createOpenAIReqClient("http://localhost/oauth/token", "http://proxy.local:8080") - require.Equal(t, "", forceHTTPVersion(t, client)) -} - func TestCreateOpenAIReqClient_Timeout120Seconds(t *testing.T) { sharedReqClients = sync.Map{} - client := createOpenAIReqClient("https://auth.openai.com/oauth/token", "http://proxy.local:8080") + client := createOpenAIReqClient("http://proxy.local:8080") require.Equal(t, 120*time.Second, client.GetClient().Timeout) } diff --git a/backend/internal/service/openai_oauth_service.go b/backend/internal/service/openai_oauth_service.go index 182e08fe7..ca7470b99 100644 --- a/backend/internal/service/openai_oauth_service.go +++ b/backend/internal/service/openai_oauth_service.go @@ -2,9 +2,10 @@ package service import ( "context" - "fmt" + "net/http" "time" + infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors" "github.com/Wei-Shaw/sub2api/internal/pkg/openai" ) @@ -35,12 +36,12 @@ func (s *OpenAIOAuthService) GenerateAuthURL(ctx context.Context, proxyID *int64 // Generate PKCE values state, err := openai.GenerateState() 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() 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) @@ -48,14 +49,17 @@ func (s *OpenAIOAuthService) GenerateAuthURL(ctx context.Context, proxyID *int64 // Generate session ID sessionID, err := openai.GenerateSessionID() 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 var proxyURL string if proxyID != nil { 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() } } @@ -110,14 +114,17 @@ func (s *OpenAIOAuthService) ExchangeCode(ctx context.Context, input *OpenAIExch // Get session session, ok := s.sessionStore.Get(input.SessionID) 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 if input.ProxyID != nil { 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() } } @@ -131,7 +138,7 @@ func (s *OpenAIOAuthService) ExchangeCode(ctx context.Context, input *OpenAIExch // Exchange code for token tokenResp, err := s.oauthClient.ExchangeCode(ctx, input.Code, session.CodeVerifier, redirectURI, proxyURL) if err != nil { - return nil, fmt.Errorf("failed to exchange code: %w", err) + return nil, err } // 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 func (s *OpenAIOAuthService) RefreshAccountToken(ctx context.Context, account *Account) (*OpenAITokenInfo, error) { 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() 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