gateway_upstream_transport_error.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "net/http"
  6. "time"
  7. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  8. "github.com/gin-gonic/gin"
  9. "go.uber.org/zap"
  10. )
  11. // gatewayTransportErrorTempUnschedDuration is how long an account is temporarily
  12. // unscheduled after a durable transport failure (matches the OpenAI-side
  13. // openAITransportErrorTempUnschedDuration).
  14. const gatewayTransportErrorTempUnschedDuration = 10 * time.Minute
  15. // gatewayTransportFailoverBody is the Anthropic-format error body attached to
  16. // the failover error for a transport-level failure. Kept identical to the
  17. // legacy inline 502 body so the client-visible payload is unchanged if
  18. // failover is ultimately exhausted.
  19. var gatewayTransportFailoverBody = []byte(`{"type":"error","error":{"type":"upstream_error","message":"Upstream request failed"}}`)
  20. // handleUpstreamTransportError handles a transport-level upstream failure on
  21. // the Anthropic/Bedrock forward paths (Do/DoWithTLS returned a non-HTTP error:
  22. // proxy / DNS / TCP / TLS). It:
  23. // 1. records the failure in Ops error logs (status 0, kind=request_error) —
  24. // the caller passes path-specific fields (UpstreamURL, Passthrough) via
  25. // event; identity, proxy attribution and classification fields are
  26. // filled here from the same account snapshot that built the transport;
  27. // 2. for durable faults (expired/rejected proxy creds, dead proxy,
  28. // DNS/routing) temporarily unschedules the account and logs a stable warn
  29. // event that alert rules can key on;
  30. // 3. returns an error that is *UpstreamFailoverError (so the handler fails
  31. // over to a healthy account) for all non-canceled errors, or the original
  32. // error for context.Canceled (client gone — no failover, no eviction).
  33. //
  34. // It deliberately does NOT write to the response: the handler owns the
  35. // response (failover, or a protocol-correct error once failover is exhausted).
  36. func (s *GatewayService) handleUpstreamTransportError(ctx context.Context, c *gin.Context, account *Account, err error, event OpsUpstreamErrorEvent) error {
  37. if local := AccountTrafficFailover(err); local != nil {
  38. MarkOpsClientBusinessLimited(c, OpsClientBusinessLimitedReasonLocalPolicyDenied)
  39. return local
  40. }
  41. safeErr := sanitizeUpstreamErrorMessage(err.Error())
  42. setOpsUpstreamError(c, 0, safeErr, "")
  43. event.ProxyID, event.ProxyName = opsUpstreamProxyAttribution(account)
  44. event.Platform = account.Platform
  45. event.AccountID = account.ID
  46. event.AccountName = account.Name
  47. event.UpstreamStatusCode = 0
  48. event.Kind = "request_error"
  49. event.Message = safeErr
  50. appendOpsUpstreamError(c, event)
  51. // Client disconnected: do NOT fail over to another account and do NOT
  52. // evict this one — the upstream never had a chance to exhibit a fault.
  53. if errors.Is(err, context.Canceled) || (errors.Is(err, context.DeadlineExceeded) && errors.Is(ctx.Err(), context.DeadlineExceeded)) {
  54. return err
  55. }
  56. // Transport attempt left local validation; count Ollama Cloud activity.
  57. scheduleOllamaCloudUsageActivity(s.deferredService, account)
  58. if classifyUpstreamTransportError(err).Persistent {
  59. s.tempUnscheduleTransportError(ctx, account, safeErr)
  60. }
  61. return &UpstreamFailoverError{
  62. StatusCode: http.StatusBadGateway,
  63. ResponseBody: gatewayTransportFailoverBody,
  64. }
  65. }
  66. // tempUnscheduleTransportError marks an account temporarily unschedulable
  67. // after a durable transport failure. Unlike the OpenAI side there is no
  68. // in-memory scheduler block on this path: the Anthropic/Bedrock scheduler
  69. // reads the persisted temp-unschedulable state, so the DB write is the single
  70. // source of truth (same as tempUnscheduleGoogleConfigError /
  71. // tempUnscheduleEmptyResponse).
  72. //
  73. // Log semantics:
  74. // - "gateway.account_temp_unscheduled_transport" — DB write succeeded.
  75. // - "gateway.account_temp_unschedule_transport_failed" — DB write attempted
  76. // but returned an error (the account remains schedulable).
  77. func (s *GatewayService) tempUnscheduleTransportError(ctx context.Context, account *Account, safeErr string) {
  78. if s == nil || account == nil || s.accountRepo == nil {
  79. return
  80. }
  81. until := time.Now().Add(gatewayTransportErrorTempUnschedDuration)
  82. reason := "upstream transport error (proxy/network): " + safeErr
  83. bgCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), openAIAccountStateUpdateTimeout)
  84. defer cancel()
  85. if err := s.accountRepo.SetTempUnschedulable(bgCtx, account.ID, until, reason); err != nil {
  86. logger.L().With(zap.String("component", "service.gateway")).Warn(
  87. "gateway.account_temp_unschedule_transport_failed",
  88. zap.Int64("account_id", account.ID),
  89. zap.Error(err),
  90. )
  91. return
  92. }
  93. logger.L().With(zap.String("component", "service.gateway")).Warn(
  94. "gateway.account_temp_unscheduled_transport",
  95. zap.Int64("account_id", account.ID),
  96. zap.String("account_name", account.Name),
  97. zap.String("platform", account.Platform),
  98. zap.Time("until", until),
  99. zap.String("reason", reason),
  100. )
  101. }