Skip to content

Commit 493bd18

Browse files
authored
Merge pull request Wei-Shaw#680 from alfadb/fix/ops-normalize-nil-error-type
fix(ops): validate error_type against known whitelist before classification
2 parents 9fd95df + 093d7ba commit 493bd18

2 files changed

Lines changed: 89 additions & 6 deletions

File tree

backend/internal/handler/ops_error_logger.go

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -662,8 +662,10 @@ func OpsErrorLoggerMiddleware(ops *service.OpsService) gin.HandlerFunc {
662662
requestID = c.Writer.Header().Get("x-request-id")
663663
}
664664

665-
phase := classifyOpsPhase(parsed.ErrorType, parsed.Message, parsed.Code)
666-
isBusinessLimited := classifyOpsIsBusinessLimited(parsed.ErrorType, phase, parsed.Code, status, parsed.Message)
665+
normalizedType := normalizeOpsErrorType(parsed.ErrorType, parsed.Code)
666+
667+
phase := classifyOpsPhase(normalizedType, parsed.Message, parsed.Code)
668+
isBusinessLimited := classifyOpsIsBusinessLimited(normalizedType, phase, parsed.Code, status, parsed.Message)
667669

668670
errorOwner := classifyOpsErrorOwner(phase, parsed.Message)
669671
errorSource := classifyOpsErrorSource(phase, parsed.Message)
@@ -685,8 +687,8 @@ func OpsErrorLoggerMiddleware(ops *service.OpsService) gin.HandlerFunc {
685687
UserAgent: c.GetHeader("User-Agent"),
686688

687689
ErrorPhase: phase,
688-
ErrorType: normalizeOpsErrorType(parsed.ErrorType, parsed.Code),
689-
Severity: classifyOpsSeverity(parsed.ErrorType, status),
690+
ErrorType: normalizedType,
691+
Severity: classifyOpsSeverity(normalizedType, status),
690692
StatusCode: status,
691693
IsBusinessLimited: isBusinessLimited,
692694
IsCountTokens: isCountTokensRequest(c),
@@ -698,7 +700,7 @@ func OpsErrorLoggerMiddleware(ops *service.OpsService) gin.HandlerFunc {
698700
ErrorSource: errorSource,
699701
ErrorOwner: errorOwner,
700702

701-
IsRetryable: classifyOpsIsRetryable(parsed.ErrorType, status),
703+
IsRetryable: classifyOpsIsRetryable(normalizedType, status),
702704
RetryCount: 0,
703705
CreatedAt: time.Now(),
704706
}
@@ -939,8 +941,29 @@ func guessPlatformFromPath(path string) string {
939941
}
940942
}
941943

944+
// isKnownOpsErrorType returns true if t is a recognized error type used by the
945+
// ops classification pipeline. Upstream proxies sometimes return garbage values
946+
// (e.g. the Go-serialized literal "<nil>") which would pollute phase/severity
947+
// classification if accepted blindly.
948+
func isKnownOpsErrorType(t string) bool {
949+
switch t {
950+
case "invalid_request_error",
951+
"authentication_error",
952+
"rate_limit_error",
953+
"billing_error",
954+
"subscription_error",
955+
"upstream_error",
956+
"overloaded_error",
957+
"api_error",
958+
"not_found_error",
959+
"forbidden_error":
960+
return true
961+
}
962+
return false
963+
}
964+
942965
func normalizeOpsErrorType(errType string, code string) string {
943-
if errType != "" {
966+
if errType != "" && isKnownOpsErrorType(errType) {
944967
return errType
945968
}
946969
switch strings.TrimSpace(code) {

backend/internal/handler/ops_error_logger_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,63 @@ func TestOpsErrorLoggerMiddleware_DoesNotBreakOuterMiddlewares(t *testing.T) {
214214
})
215215
require.Equal(t, http.StatusNoContent, rec.Code)
216216
}
217+
218+
func TestIsKnownOpsErrorType(t *testing.T) {
219+
known := []string{
220+
"invalid_request_error",
221+
"authentication_error",
222+
"rate_limit_error",
223+
"billing_error",
224+
"subscription_error",
225+
"upstream_error",
226+
"overloaded_error",
227+
"api_error",
228+
"not_found_error",
229+
"forbidden_error",
230+
}
231+
for _, k := range known {
232+
require.True(t, isKnownOpsErrorType(k), "expected known: %s", k)
233+
}
234+
235+
unknown := []string{"<nil>", "null", "", "random_error", "some_new_type", "<nil>\u003e"}
236+
for _, u := range unknown {
237+
require.False(t, isKnownOpsErrorType(u), "expected unknown: %q", u)
238+
}
239+
}
240+
241+
func TestNormalizeOpsErrorType(t *testing.T) {
242+
tests := []struct {
243+
name string
244+
errType string
245+
code string
246+
want string
247+
}{
248+
// Known types pass through.
249+
{"known invalid_request_error", "invalid_request_error", "", "invalid_request_error"},
250+
{"known rate_limit_error", "rate_limit_error", "", "rate_limit_error"},
251+
{"known upstream_error", "upstream_error", "", "upstream_error"},
252+
253+
// Unknown/garbage types are rejected and fall through to code-based or default.
254+
{"nil literal from upstream", "<nil>", "", "api_error"},
255+
{"null string", "null", "", "api_error"},
256+
{"random string", "something_weird", "", "api_error"},
257+
258+
// Unknown type but known code still maps correctly.
259+
{"nil with INSUFFICIENT_BALANCE code", "<nil>", "INSUFFICIENT_BALANCE", "billing_error"},
260+
{"nil with USAGE_LIMIT_EXCEEDED code", "<nil>", "USAGE_LIMIT_EXCEEDED", "subscription_error"},
261+
262+
// Empty type falls through to code-based mapping.
263+
{"empty type with balance code", "", "INSUFFICIENT_BALANCE", "billing_error"},
264+
{"empty type with subscription code", "", "SUBSCRIPTION_NOT_FOUND", "subscription_error"},
265+
{"empty type no code", "", "", "api_error"},
266+
267+
// Known type overrides conflicting code-based mapping.
268+
{"known type overrides conflicting code", "rate_limit_error", "INSUFFICIENT_BALANCE", "rate_limit_error"},
269+
}
270+
for _, tt := range tests {
271+
t.Run(tt.name, func(t *testing.T) {
272+
got := normalizeOpsErrorType(tt.errType, tt.code)
273+
require.Equal(t, tt.want, got)
274+
})
275+
}
276+
}

0 commit comments

Comments
 (0)