package service import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/pkg/logger" "github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/gin-gonic/gin" "github.com/tidwall/gjson" "go.uber.org/zap" ) func logOpenAIInstructionsRequiredDebug( ctx context.Context, c *gin.Context, account *Account, upstreamStatusCode int, upstreamMsg string, requestBody []byte, upstreamBody []byte, ) { msg := strings.TrimSpace(upstreamMsg) if !isOpenAIInstructionsRequiredError(upstreamStatusCode, msg, upstreamBody) { return } if ctx == nil { ctx = context.Background() } accountID := int64(0) accountName := "" if account != nil { accountID = account.ID accountName = strings.TrimSpace(account.Name) } userAgent := "" originator := "" if c != nil { userAgent = strings.TrimSpace(c.GetHeader("User-Agent")) originator = strings.TrimSpace(c.GetHeader("originator")) } fields := []zap.Field{ zap.String("component", "service.openai_gateway"), zap.Int64("account_id", accountID), zap.String("account_name", accountName), zap.Int("upstream_status_code", upstreamStatusCode), zap.String("upstream_error_message", msg), zap.String("request_user_agent", userAgent), zap.Bool("codex_official_client_match", openai.IsCodexOfficialClientByHeaders(userAgent, originator)), } fields = appendCodexCLIOnlyRejectedRequestFields(fields, c, requestBody) logger.FromContext(ctx).With(fields...).Warn("OpenAI 上游返回 Instructions are required,已记录请求详情用于排查") } func isOpenAIInstructionsRequiredError(upstreamStatusCode int, upstreamMsg string, upstreamBody []byte) bool { if upstreamStatusCode != http.StatusBadRequest { return false } hasInstructionRequired := func(text string) bool { lower := strings.ToLower(strings.TrimSpace(text)) if lower == "" { return false } if strings.Contains(lower, "instructions are required") { return true } if strings.Contains(lower, "required parameter: 'instructions'") { return true } if strings.Contains(lower, "required parameter: instructions") { return true } if strings.Contains(lower, "missing required parameter") && strings.Contains(lower, "instructions") { return true } return strings.Contains(lower, "instruction") && strings.Contains(lower, "required") } if hasInstructionRequired(upstreamMsg) { return true } if len(upstreamBody) == 0 { return false } errMsg := gjson.GetBytes(upstreamBody, "error.message").String() errMsgLower := strings.ToLower(strings.TrimSpace(errMsg)) errCode := strings.ToLower(strings.TrimSpace(gjson.GetBytes(upstreamBody, "error.code").String())) errParam := strings.ToLower(strings.TrimSpace(gjson.GetBytes(upstreamBody, "error.param").String())) errType := strings.ToLower(strings.TrimSpace(gjson.GetBytes(upstreamBody, "error.type").String())) if errParam == "instructions" { return true } if hasInstructionRequired(errMsg) { return true } if strings.Contains(errCode, "missing_required_parameter") && strings.Contains(errMsgLower, "instructions") { return true } if strings.Contains(errType, "invalid_request") && strings.Contains(errMsgLower, "instructions") && strings.Contains(errMsgLower, "required") { return true } return false } func isOpenAITransientProcessingError(upstreamStatusCode int, upstreamMsg string, upstreamBody []byte) bool { if upstreamStatusCode < http.StatusBadRequest { return false } hasOpenAIServerOverloadedCode := func(payload []byte) bool { code := strings.ToLower(strings.TrimSpace(gjson.GetBytes(payload, "error.code").String())) if code == "" { code = strings.ToLower(strings.TrimSpace(gjson.GetBytes(payload, "response.error.code").String())) } return code == "server_is_overloaded" || code == "slow_down" } if len(upstreamBody) > 0 && hasOpenAIServerOverloadedCode(upstreamBody) { return true } if isOpenAICapacityShedMessage(upstreamMsg) || isOpenAICapacityShedMessage(gjson.GetBytes(upstreamBody, "error.message").String()) || isOpenAICapacityShedMessage(gjson.GetBytes(upstreamBody, "response.error.message").String()) || isOpenAICapacityShedMessage(string(upstreamBody)) { return true } if upstreamStatusCode != http.StatusBadRequest && upstreamStatusCode != http.StatusServiceUnavailable { return false } if upstreamStatusCode != http.StatusBadRequest { return false } match := func(text string) bool { lower := strings.ToLower(strings.TrimSpace(text)) if lower == "" { return false } if strings.Contains(lower, "an error occurred while processing your request") { return true } if strings.Contains(lower, "selected model is at capacity") { return true } return strings.Contains(lower, "you can retry your request") && strings.Contains(lower, "help.openai.com") && strings.Contains(lower, "request id") } if match(upstreamMsg) { return true } if len(upstreamBody) == 0 { return false } if match(gjson.GetBytes(upstreamBody, "error.message").String()) { return true } return match(string(upstreamBody)) } func isOpenAICapacityShedMessage(text string) bool { lower := strings.ToLower(strings.TrimSpace(text)) return strings.Contains(lower, "server is overloaded") || strings.Contains(lower, "servers are overloaded") || strings.Contains(lower, "servers are currently overloaded") } func isOpenAIRequestScopedCapacityShed(upstreamMsg string, upstreamBody []byte) bool { return isOpenAIUpstreamCapacityShedEvent(upstreamBody) || isOpenAICapacityShedMessage(upstreamMsg) || isOpenAICapacityShedMessage(string(upstreamBody)) } func isOpenAIContextWindowError(upstreamMsg string, upstreamBody []byte) bool { match := func(text string) bool { lower := strings.ToLower(strings.TrimSpace(text)) if lower == "" { return false } if strings.Contains(lower, "context_too_large") || strings.Contains(lower, "context_length_exceeded") { return true } if strings.Contains(lower, "maximum context length") || strings.Contains(lower, "max context length") { return true } hasExceeded := strings.Contains(lower, "exceed") || strings.Contains(lower, "too large") || strings.Contains(lower, "too long") if strings.Contains(lower, "context window") && hasExceeded { return true } if strings.Contains(lower, "context length") && hasExceeded { return true } return strings.Contains(lower, "token limit") && strings.Contains(lower, "context") && hasExceeded } if match(upstreamMsg) { return true } if len(upstreamBody) == 0 { return false } for _, path := range []string{ "error.message", "response.error.message", "message", "error.code", "response.error.code", "code", } { if match(gjson.GetBytes(upstreamBody, path).String()) { return true } } return match(string(upstreamBody)) } func (s *OpenAIGatewayService) shouldFailoverUpstreamError(statusCode int) bool { switch statusCode { case 401, 402, 403, 405, 429, 529: return true default: return statusCode >= 500 } } func (s *OpenAIGatewayService) shouldFailoverOpenAIUpstreamResponse(statusCode int, upstreamMsg string, upstreamBody []byte) bool { if isOpenAIContextWindowError(upstreamMsg, upstreamBody) { return false } if isOpenAIRequestBodyTooLargeError(statusCode, upstreamMsg, upstreamBody) { return true } if s.shouldFailoverUpstreamError(statusCode) { return true } return isOpenAITransientProcessingError(statusCode, upstreamMsg, upstreamBody) } // OpenAIRequestBodyTooLargeClientMessage is the fixed downstream message used // after all account-specific request body limit failovers are exhausted. const OpenAIRequestBodyTooLargeClientMessage = "Request payload is too large" const openAIRequestBodyTooLargeReason = GatewayFailureReason("openai_request_body_too_large") func isOpenAIRequestBodyTooLargeError(statusCode int, upstreamMsg string, upstreamBody []byte) bool { return statusCode == http.StatusRequestEntityTooLarge && !isOpenAIContextWindowError(upstreamMsg, upstreamBody) } func newOpenAIUpstreamFailoverError( statusCode int, responseHeaders http.Header, responseBody []byte, upstreamMsg string, retryableOnSameAccount bool, ) *UpstreamFailoverError { requestScopedCapacity := isOpenAIRequestScopedCapacityShed(upstreamMsg, responseBody) failoverErr := &UpstreamFailoverError{ StatusCode: statusCode, ResponseBody: responseBody, ResponseHeaders: responseHeaders.Clone(), RetryableOnSameAccount: retryableOnSameAccount || requestScopedCapacity, RequestScopedTransient: requestScopedCapacity, } if isOpenAIRequestBodyTooLargeError(statusCode, upstreamMsg, responseBody) { failoverErr.RetryableOnSameAccount = false failoverErr.RequestScopedTransient = false failoverErr.Scope = GatewayFailureScopeAccount failoverErr.Reason = openAIRequestBodyTooLargeReason failoverErr.NextAccountAction = NextAccountRetry failoverErr.ClientStatusCode = http.StatusRequestEntityTooLarge failoverErr.ClientMessage = OpenAIRequestBodyTooLargeClientMessage } return failoverErr } // IsOpenAIRequestBodyTooLarge reports whether another account may accept the // same request even though the selected account rejected its serialized size. func (e *UpstreamFailoverError) IsOpenAIRequestBodyTooLarge() bool { return e != nil && e.Reason == openAIRequestBodyTooLargeReason } func marshalOpenAIUpstreamJSON(v any) ([]byte, error) { var buf bytes.Buffer enc := json.NewEncoder(&buf) enc.SetEscapeHTML(false) if err := enc.Encode(v); err != nil { return nil, err } out := buf.Bytes() if len(out) > 0 && out[len(out)-1] == '\n' { out = out[:len(out)-1] } return out, nil } func openAIUpstreamErrorBodyReadLimitForConfig(cfg *config.Config) int64 { limit := openAIUpstreamErrorBodyReadLimit if cfg != nil && cfg.Gateway.LogUpstreamErrorBody && cfg.Gateway.LogUpstreamErrorBodyMaxBytes > int(limit) { limit = int64(cfg.Gateway.LogUpstreamErrorBodyMaxBytes) } return limit } func (s *OpenAIGatewayService) readUpstreamErrorBody(resp *http.Response) []byte { if resp == nil || resp.Body == nil { return nil } cfg := (*config.Config)(nil) if s != nil { cfg = s.cfg } body, _ := io.ReadAll(io.LimitReader(resp.Body, openAIUpstreamErrorBodyReadLimitForConfig(cfg))) return body } func (s *OpenAIGatewayService) handleFailoverSideEffects(ctx context.Context, resp *http.Response, account *Account, responseBody []byte, canonicalModel ...string) bool { if len(canonicalModel) > 0 { return s.handleOpenAIAccountUpstreamError(ctx, account, resp.StatusCode, resp.Header, responseBody, canonicalModel[0]) } return s.handleOpenAIAccountUpstreamError(ctx, account, resp.StatusCode, resp.Header, responseBody) } func (s *OpenAIGatewayService) handleErrorResponse( ctx context.Context, resp *http.Response, c *gin.Context, account *Account, requestBody []byte, requestedModel ...string, ) (*OpenAIForwardResult, error) { body := s.readUpstreamErrorBody(resp) body = s.redactAgentIdentitySensitiveBody(ctx, account, body) // cyber_policy 硬阻断:透传上游原始错误体给客户端(不重包成通用 502),不冷却账号。 // 当前请求恒透传(需求1);标记供 handler 事后写风控/邮件。400 cyber 不可 failover // (shouldFailoverUpstreamError(400)=false),故走到此处即可安全早返回。 if hit, code, cyberMsg := detectOpenAICyberPolicy(body); hit { MarkOpsCyberPolicy(c, CyberPolicyMark{ Code: code, Message: cyberMsg, Body: truncateString(string(body), 4096), UpstreamStatus: resp.StatusCode, }) setOpsUpstreamError(c, resp.StatusCode, cyberMsg, truncateString(string(body), 2048)) writeOpenAIPassthroughResponseHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter) contentType := resp.Header.Get("Content-Type") if contentType == "" { contentType = "application/json" } c.Data(resp.StatusCode, contentType, body) if cyberMsg == "" { return nil, fmt.Errorf("openai cyber_policy: %d", resp.StatusCode) } return nil, fmt.Errorf("openai cyber_policy: %s", cyberMsg) } if account != nil && account.Platform == PlatformGrok && isGrokContentPolicyRejection(resp.StatusCode, body) { clientMsg := grokContentPolicyClientMessage(body) setOpsUpstreamError(c, resp.StatusCode, clientMsg, truncateString(string(body), 2048)) writeOpenAIPassthroughResponseHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter) MarkResponseCommitted(c) c.JSON(http.StatusForbidden, gin.H{ "error": gin.H{ "type": "invalid_request_error", "message": clientMsg, }, }) return nil, fmt.Errorf("grok content policy rejection: %s", clientMsg) } upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(body)) upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg) upstreamDetail := "" if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody { maxBytes := s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes if maxBytes <= 0 { maxBytes = 2048 } upstreamDetail = truncateString(string(body), maxBytes) } setOpsUpstreamError(c, resp.StatusCode, upstreamMsg, upstreamDetail) logOpenAIInstructionsRequiredDebug(ctx, c, account, resp.StatusCode, upstreamMsg, requestBody, body) if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody { logger.LegacyPrintf("service.openai_gateway", "OpenAI upstream error %d (account=%d platform=%s type=%s): %s", resp.StatusCode, account.ID, account.Platform, account.Type, truncateForLog(body, s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes), ) } if isOpenAIRequestBodyTooLargeError(resp.StatusCode, upstreamMsg, body) { appendOpsUpstreamError(c, OpsUpstreamErrorEvent{ Platform: account.Platform, AccountID: account.ID, AccountName: account.Name, UpstreamStatusCode: resp.StatusCode, UpstreamRequestID: resp.Header.Get("x-request-id"), Kind: "failover", Message: upstreamMsg, Detail: upstreamDetail, }) s.handleOpenAIAccountUpstreamError(ctx, account, resp.StatusCode, resp.Header, body, requestedModel...) return nil, newOpenAIUpstreamFailoverError( resp.StatusCode, resp.Header, body, upstreamMsg, false, ) } if status, errType, errMsg, matched := applyErrorPassthroughRule( c, PlatformOpenAI, resp.StatusCode, body, http.StatusBadGateway, "upstream_error", "Upstream request failed", ); matched { MarkResponseCommitted(c) c.JSON(status, gin.H{ "error": gin.H{ "type": errType, "message": errMsg, }, }) if upstreamMsg == "" { upstreamMsg = errMsg } if upstreamMsg == "" { return nil, fmt.Errorf("upstream error: %d (passthrough rule matched)", resp.StatusCode) } return nil, fmt.Errorf("upstream error: %d (passthrough rule matched) message=%s", resp.StatusCode, upstreamMsg) } // Check custom error codes if !account.ShouldHandleErrorCode(resp.StatusCode) { appendOpsUpstreamError(c, OpsUpstreamErrorEvent{ Platform: account.Platform, AccountID: account.ID, AccountName: account.Name, UpstreamStatusCode: resp.StatusCode, UpstreamRequestID: resp.Header.Get("x-request-id"), Kind: "http_error", Message: upstreamMsg, Detail: upstreamDetail, }) MarkResponseCommitted(c) c.JSON(http.StatusInternalServerError, gin.H{ "error": gin.H{ "type": "upstream_error", "message": "Upstream gateway error", }, }) if upstreamMsg == "" { return nil, fmt.Errorf("upstream error: %d (not in custom error codes)", resp.StatusCode) } return nil, fmt.Errorf("upstream error: %d (not in custom error codes) message=%s", resp.StatusCode, upstreamMsg) } // Handle upstream error (mark account status) var reqModel string if len(requestedModel) > 0 { reqModel = strings.TrimSpace(requestedModel[0]) } if reqModel == "" { reqModel, _, _ = extractOpenAIRequestMetaFromBody(requestBody) reqModel = canonicalOpenAIAccountSchedulingModel(account, reqModel) } shouldDisable := s.handleOpenAIAccountUpstreamError(ctx, account, resp.StatusCode, resp.Header, body, reqModel) kind := "http_error" if shouldDisable { kind = "failover" } appendOpsUpstreamError(c, OpsUpstreamErrorEvent{ Platform: account.Platform, AccountID: account.ID, AccountName: account.Name, UpstreamStatusCode: resp.StatusCode, UpstreamRequestID: resp.Header.Get("x-request-id"), Kind: kind, Message: upstreamMsg, Detail: upstreamDetail, }) if shouldDisable { return nil, &UpstreamFailoverError{ StatusCode: resp.StatusCode, ResponseBody: body, RetryableOnSameAccount: false, } } MarkResponseCommitted(c) // 上游 400 是确定性的请求错误:同一份请求体换账号、重试多少次都会失败。归一成 // 502 upstream_error 会让下游网关把它当成可重试的上游故障反复重放(#5479 实测 // 30 个失败请求被放大成 60 次上游调用),同时抹掉客户端定位问题所需的 code/param。 // // 走到这里说明 shouldFailoverOpenAIUpstreamResponse 已判定该 400 不可 failover, // 即 server_is_overloaded / at capacity 这类可重试的 400 不会到达此处。 // // 兄弟路径早已这么做:handleCompatErrorResponse(ChatCompletions / Anthropic) // 回真实状态码 + invalid_request_error + 真实 message;/v1/images 还额外透传 // code/param。原生 Responses 是唯一漏掉的一条。 if isOpenAIDeterministicClientError(resp.StatusCode) { writeOpenAIUpstreamClientError(c, resp.StatusCode, body, upstreamMsg) if upstreamMsg == "" { return nil, fmt.Errorf("upstream error: %d", resp.StatusCode) } return nil, fmt.Errorf("upstream error: %d message=%s", resp.StatusCode, upstreamMsg) } // Return appropriate error response var errType, errMsg string var statusCode int switch resp.StatusCode { case 401: statusCode = http.StatusBadGateway errType = "upstream_error" errMsg = "Upstream authentication failed, please contact administrator" case 402: statusCode = http.StatusBadGateway errType = "upstream_error" errMsg = "Upstream payment required: insufficient balance or billing issue" case 403: statusCode = http.StatusBadGateway errType = "upstream_error" errMsg = "Upstream access forbidden, please contact administrator" case 429: statusCode = http.StatusTooManyRequests errType = "rate_limit_error" errMsg = "Upstream rate limit exceeded, please retry later" default: statusCode = http.StatusBadGateway errType = "upstream_error" errMsg = "Upstream request failed" } if isOpenAIContextWindowError(upstreamMsg, body) && upstreamMsg != "" { errMsg = upstreamMsg } c.JSON(statusCode, gin.H{ "error": gin.H{ "type": errType, "message": errMsg, }, }) if upstreamMsg == "" { return nil, fmt.Errorf("upstream error: %d", resp.StatusCode) } return nil, fmt.Errorf("upstream error: %d message=%s", resp.StatusCode, upstreamMsg) } // compatErrorWriter is the signature for format-specific error writers used by // the compat paths (Chat Completions and Anthropic Messages). type compatErrorWriter func(c *gin.Context, statusCode int, errType, message string) // handleCompatErrorResponse is the shared non-failover error handler for the // Chat Completions and Anthropic Messages compat paths. It mirrors the logic of // handleErrorResponse (passthrough rules, ShouldHandleErrorCode, rate-limit // tracking, secondary failover) but delegates the final error write to the // format-specific writer function. func (s *OpenAIGatewayService) handleCompatErrorResponse( resp *http.Response, c *gin.Context, account *Account, writeError compatErrorWriter, requestedModel ...string, ) (*OpenAIForwardResult, error) { body := s.readUpstreamErrorBody(resp) body = s.redactAgentIdentitySensitiveBody(context.Background(), account, body) // cyber_policy:兼容路径(Chat Completions / Anthropic)以各自格式回写错误, // 不原样透传 responses 格式的 cyber body(否则对下游格式不合法)。cyber 是上游网络 // 安全策略拦截,不冷却账号,故标记后直接以兼容格式回写错误并返回,跳过下方 // handleOpenAIAccountUpstreamError(避免自定义 temp-unschedulable 规则误冷却)。 if hit, code, cyberMsg := detectOpenAICyberPolicy(body); hit { MarkOpsCyberPolicy(c, CyberPolicyMark{ Code: code, Message: cyberMsg, Body: truncateString(string(body), 4096), UpstreamStatus: resp.StatusCode, }) setOpsUpstreamError(c, resp.StatusCode, cyberMsg, truncateString(string(body), 2048)) clientMsg := cyberMsg if clientMsg == "" { clientMsg = "Request blocked by upstream cyber-security policy" } writeError(c, resp.StatusCode, "invalid_request_error", clientMsg) if cyberMsg == "" { return nil, fmt.Errorf("openai cyber_policy: %d", resp.StatusCode) } return nil, fmt.Errorf("openai cyber_policy: %s", cyberMsg) } if account != nil && account.Platform == PlatformGrok && isGrokContentPolicyRejection(resp.StatusCode, body) { clientMsg := grokContentPolicyClientMessage(body) setOpsUpstreamError(c, resp.StatusCode, clientMsg, truncateString(string(body), 2048)) MarkResponseCommitted(c) writeError(c, http.StatusForbidden, "invalid_request_error", clientMsg) return nil, fmt.Errorf("grok content policy rejection: %s", clientMsg) } upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(body)) if upstreamMsg == "" { upstreamMsg = fmt.Sprintf("Upstream error: %d", resp.StatusCode) } upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg) upstreamDetail := "" if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody { maxBytes := s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes if maxBytes <= 0 { maxBytes = 2048 } upstreamDetail = truncateString(string(body), maxBytes) } setOpsUpstreamError(c, resp.StatusCode, upstreamMsg, upstreamDetail) // Apply error passthrough rules if status, errType, errMsg, matched := applyErrorPassthroughRule( c, account.Platform, resp.StatusCode, body, http.StatusBadGateway, "api_error", "Upstream request failed", ); matched { MarkResponseCommitted(c) writeError(c, status, errType, errMsg) if upstreamMsg == "" { upstreamMsg = errMsg } if upstreamMsg == "" { return nil, fmt.Errorf("upstream error: %d (passthrough rule matched)", resp.StatusCode) } return nil, fmt.Errorf("upstream error: %d (passthrough rule matched) message=%s", resp.StatusCode, upstreamMsg) } // Check custom error codes — if the account does not handle this status, // return a generic error without exposing upstream details. if !account.ShouldHandleErrorCode(resp.StatusCode) { appendOpsUpstreamError(c, OpsUpstreamErrorEvent{ Platform: account.Platform, AccountID: account.ID, AccountName: account.Name, UpstreamStatusCode: resp.StatusCode, UpstreamRequestID: resp.Header.Get("x-request-id"), Kind: "http_error", Message: upstreamMsg, Detail: upstreamDetail, }) MarkResponseCommitted(c) writeError(c, http.StatusInternalServerError, "api_error", "Upstream gateway error") if upstreamMsg == "" { return nil, fmt.Errorf("upstream error: %d (not in custom error codes)", resp.StatusCode) } return nil, fmt.Errorf("upstream error: %d (not in custom error codes) message=%s", resp.StatusCode, upstreamMsg) } // Track rate limits and decide whether to trigger secondary failover. var modelForCooldown string if len(requestedModel) > 0 { modelForCooldown = requestedModel[0] } shouldDisable := s.handleOpenAIAccountUpstreamError( c.Request.Context(), account, resp.StatusCode, resp.Header, body, modelForCooldown, ) kind := "http_error" if shouldDisable { kind = "failover" } appendOpsUpstreamError(c, OpsUpstreamErrorEvent{ Platform: account.Platform, AccountID: account.ID, AccountName: account.Name, UpstreamStatusCode: resp.StatusCode, UpstreamRequestID: resp.Header.Get("x-request-id"), Kind: kind, Message: upstreamMsg, Detail: upstreamDetail, }) if shouldDisable { return nil, &UpstreamFailoverError{ StatusCode: resp.StatusCode, ResponseBody: body, RetryableOnSameAccount: false, } } MarkResponseCommitted(c) // Map status code to error type and write response errType := "api_error" switch { case resp.StatusCode == 400: errType = "invalid_request_error" case resp.StatusCode == 404: errType = "not_found_error" case resp.StatusCode == 429: errType = "rate_limit_error" case resp.StatusCode >= 500: errType = "api_error" } writeError(c, resp.StatusCode, errType, upstreamMsg) return nil, fmt.Errorf("upstream error: %d %s", resp.StatusCode, upstreamMsg) }