Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions indexer/pkg/solingest/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ func TestTemporalLoggerErrorLevel(t *testing.T) {
err: errors.New("Code: 62. DB::Exception: Syntax error"),
want: slog.LevelError,
},
{
name: "third-party http 503 demoted to warn",
msg: "Activity error.",
err: errors.New("validatorsapp refresh: failed to get validators: unexpected status code 503: <html><body><h1>503 Service Unavailable</h1></body></html>"),
want: slog.LevelWarn,
},
{
name: "context cancellation demoted to warn",
msg: "Activity error.",
Expand Down
41 changes: 26 additions & 15 deletions utils/pkg/dberror/dberror.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,21 @@ import (
// eofRe matches "eof" as a standalone word in a lowercased error message.
var eofRe = regexp.MustCompile(`\beof\b`)

// awsRespErrRe matches the AWS SDK v2 "https response error StatusCode: ..."
// shape (lowercased by Classify before matching) for a transient S3 status: a
// 200 (S3's documented "200 OK with an error mid-body" blip, which the SDK
// surfaces as a response error but does not retry internally — transient per
// S3's own guidance) or the SDK's DefaultRetryableHTTPStatusCodes set of
// {500, 502, 503, 504}. 501 NotImplemented and 505 are deliberately not
// matched: they are permanent endpoint/configuration failures, not blips. The
// "https response error statuscode:" prefix scopes the match to AWS SDK v2
// messages, so ClickHouse, Neo4j, and Influx errors never hit it. Actionable
// 4xx (403 AccessDenied, 404 NoSuchBucket/NoSuchKey) are also excluded so
// they keep paging. The trailing \b prevents matching a status inside a
// longer digit run (e.g. "statuscode: 2001").
var awsRespErrRe = regexp.MustCompile(`https response error statuscode: (200|500|502|503|504)\b`)
// awsRespErrRe matches the AWS SDK v2 "https response error StatusCode: 200"
// shape (lowercased by Classify): S3's documented "200 OK with an error
// mid-body" blip, which the SDK surfaces as a response error but does not
// retry internally — transient per S3's own guidance. The prefix keeps 200
// scoped to AWS SDK messages; a bare "status code 200" elsewhere stays
// non-transient. Retryable 5xx are handled shape-independently by
// httpStatusRe. The trailing \b prevents matching a longer digit run.
var awsRespErrRe = regexp.MustCompile(`https response error statuscode: 200\b`)

// httpStatusRe captures the first "status code NNN" mention in an error
// string — the failed request's own status, since wrapping prepends; a status
// quoted later (e.g. in a response body) must not classify. Classify treats
// the SDK-retryable set {500, 502, 503, 504} as transient; 501/505 are
// permanent endpoint failures and 4xx are actionable, so all keep paging.
var httpStatusRe = regexp.MustCompile(`status[ _]?code[:= ]?\s*(\d{3})\b`)

// ErrTransient is a sentinel that explicitly marks an error as transient for
// IsTransient, independent of its message. Wrap a return with it (e.g. via
Expand Down Expand Up @@ -111,12 +113,21 @@ func Classify(err error) ErrorType {
return ErrorTypeConnectivity
}

// AWS SDK v2 transient S3 responses (200-with-embedded-error, retryable
// 5xx server errors) — self-healing blips, not actionable.
// AWS SDK v2 transient 200-with-embedded-error responses — self-healing
// blips, not actionable.
if awsRespErrRe.MatchString(errStr) {
return ErrorTypeConnectivity
}

// Retryable 5xx from any HTTP upstream; a non-5xx first status falls
// through to the remaining patterns.
if m := httpStatusRe.FindStringSubmatch(errStr); m != nil {
switch m[1] {
case "500", "502", "503", "504":
return ErrorTypeConnectivity
}
}

// Connection/connectivity patterns
connectivityPatterns := []string{
"connectivityerror",
Expand Down
11 changes: 11 additions & 0 deletions utils/pkg/dberror/dberror_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ func TestClassifyAndIsTransient(t *testing.T) {
// A non-AWS message mentioning statuscode: 200 without the SDK prefix must not match.
{"non-aws statuscode 200", errors.New("handler returned statuscode: 200 but body was empty"), dberror.ErrorTypeUnknown, false},

// Retryable 5xx in the "status code NNN" shape any HTTP client emits (the
// validators.app shape that paged on 2026-08-30); 4xx and 501 stay actionable.
{"validatorsapp 503", errors.New(`validatorsapp refresh: failed to get validators: unexpected status code 503: <html><body><h1>503 Service Unavailable</h1></body></html>`), dberror.ErrorTypeConnectivity, true},
{"generic status code 500", errors.New("unexpected status code 500: internal server error"), dberror.ErrorTypeConnectivity, true},
{"generic status_code variant", errors.New("request failed: status_code=502"), dberror.ErrorTypeConnectivity, true},
{"generic status code 400 stays actionable", errors.New("unexpected status code 400: bad request"), dberror.ErrorTypeUnknown, false},
{"generic status code 501 stays actionable", errors.New("unexpected status code 501: not implemented"), dberror.ErrorTypeUnknown, false},
// The request's own status decides — wrapping prepends, so the first
// mention is it. A 4xx whose body quotes a 5xx stays actionable.
{"embedded 5xx inside a 4xx", errors.New(`unexpected status code 400: {"err":"upstream returned status code 503"}`), dberror.ErrorTypeUnknown, false},

// Non-transient: real, actionable failures should still escalate to ERROR.
{"syntax error", errors.New("Code: 62. DB::Exception: Syntax error"), dberror.ErrorTypeQuery, false},
{"access denied", errors.New("access denied for user"), dberror.ErrorTypeAuth, false},
Expand Down
Loading