Skip to content

ref(service): Classify service errors by kind#493

Draft
lcian wants to merge 23 commits into
mainfrom
lcian/feat/proper-errors-v3
Draft

ref(service): Classify service errors by kind#493
lcian wants to merge 23 commits into
mainfrom
lcian/feat/proper-errors-v3

Conversation

@lcian

@lcian lcian commented Jun 8, 2026

Copy link
Copy Markdown
Member

This is a different take on giving us more control over HTTP status codes when erroring out from the service layer that doesn't make callers more verbose, but actually less so in some cases.

  1. ServiceError variants gain a kind that's used to determine log level and HTTP status code when converting to ApiErrorResponse. The kind is implemented via https://crates.io/crates/derive-error-kind which lets us do it concisely via a error_kind attribute on each variant. This is a very simple proc macro that we could also vendor in.

  2. Existing constructors for specific Error variants remain, and map to ErrorKind::Internal.

  3. For errors that could map to different kinds depending on the context, we also introduce additional intermediate wrapper types (needed to support different kinds) and constructors on ServiceError.
    Namely: reqwest_transparent (alternative to the existing reqwest helper), serde_client (alternative to the existing serde helper), and the metadata/metadata_client pair.

  4. As a special case, we implement From<std::io::Error> for Error so that callers can keep using ? to propagate io errors. That's because so far we always want io::Error to map to ErrorKind::internal, so there's no reason to make callers more verbose in this case.
    This has the added benefit that the impl can encapsulate the unpack_client_error logic, to avoid repeating it in every caller.

  5. Also renames ClientError to ClientStreamError as that's a less confusing name.

Refs FS-358

@codecov

This comment has been minimized.

@lcian lcian changed the title ref(service): Classify service errors by kind ref(service): Refactor Error Jun 8, 2026
@lcian lcian changed the title ref(service): Refactor Error feat(service): Refactor Error Jun 8, 2026
@lcian lcian force-pushed the lcian/feat/proper-errors-v3 branch from 9ca4473 to 2953792 Compare June 8, 2026 11:56
@lcian lcian changed the title feat(service): Refactor Error ref(service): Classify service errors by kind Jun 8, 2026
Add service-level error kinds so API handlers can map failures without matching every backend-specific variant. Keep client metadata errors distinct from internal metadata failures and classify transient backend failures as retryable/service-unavailable paths.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
@lcian lcian force-pushed the lcian/feat/proper-errors-v3 branch from 2953792 to 445c724 Compare June 8, 2026 12:13
lcian and others added 9 commits June 8, 2026 14:30
Remove private wrapper constructor helpers that only duplicated the public Error helpers. Keep classification policy in the public constructors so call sites have fewer names to reason about.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Remove the service Error conversion from objectstore metadata errors. Map metadata parsing and serialization failures at each backend call site so the context and classification stay visible.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Update stream documentation links after the client stream error rename. This keeps rustdoc clean under CI's denied warnings.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Move reqwest retryability out of the shared error wrapper and back next to the GCS retry loop. Let ReqwestError handle only construction and service-level classification.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
@linear-code

linear-code Bot commented Jun 8, 2026

Copy link
Copy Markdown

FS-358

let metadata_headers = metadata.to_headers("").map_err(ServiceError::from)?;
let metadata_headers = metadata
.to_headers("")
.map_err(|cause| ServiceError::metadata("failed to serialize object metadata", cause))?;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improvement: here we have a valid Metadata, it's therefore our fault if we fail to serialize it, and now we can correctly map this to an internal error as opposed to always blaming the client.

@lcian lcian marked this pull request as ready for review June 8, 2026 14:23

@jan-auer jan-auer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the direction this is taking. Especially, it makes a lot of sense to introduce one flat kind that the server can match on to determine the status code.

The kinds are a bit broad and seem directly tied to the status codes that the server returns. Let's still keep more fine grained kinds (e.g. AtCapacity) and let the server choose how to map which of those, and which to group together. This decouples writing logic from the HTTP semantics a bit better.

#489 seems to take this one step further and replaces the typed error enum with a boxed error. This is ultimately what we should get to, although we could do that in multiple steps. What #493 (this PR) does better is that there are more convenient conversions that avoid .map_err(...) in all places.

One more thing to look into, possibly in a follow up, is an extension trait that we can call directly on Result. With that, it would get highly ergonomic to wrap an error and supply either the kind or kind + context.

@lcian lcian marked this pull request as draft July 7, 2026 09:00
StatusCode::INTERNAL_SERVER_ERROR
| StatusCode::BAD_GATEWAY
| StatusCode::SERVICE_UNAVAILABLE => ErrorKind::BackendUnavailable,
status if status.is_client_error() => ErrorKind::InvalidInput,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Backend authentication or authorization errors (401/403) are incorrectly classified as client InvalidInput, resulting in a misleading 400 BAD_REQUEST response to the client.
Severity: HIGH

Suggested Fix

Modify the kind_for_status function to handle specific backend 4xx status codes, such as 401 UNAUTHORIZED and 403 FORBIDDEN, separately. Instead of mapping all client errors to ErrorKind::InvalidInput, these specific codes should be mapped to a more appropriate server-side error kind, like ErrorKind::Internal, to avoid misrepresenting server-side issues as client errors.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: objectstore-service/src/error.rs#L374

Potential issue: The `kind_for_status` function incorrectly maps all 4xx HTTP status
codes from backend services to `ErrorKind::InvalidInput`. This causes server-side
issues, such as a `401 UNAUTHORIZED` from a storage backend due to misconfigured
credentials, to be re-mapped and returned to the end client as a `400 BAD_REQUEST`. This
misclassification masks the true source of the error, misleading API consumers into
believing their request was malformed when the problem is actually a server-side
configuration or authentication failure.

Also affects:

  • objectstore-server/src/endpoints/common.rs:92~114

Did we get this right? 👍 / 👎 to inform future reviews.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 79ab712. Configure here.

| StatusCode::SERVICE_UNAVAILABLE => ErrorKind::BackendUnavailable,
status if status.is_client_error() => ErrorKind::InvalidInput,
_ => ErrorKind::Internal,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backend forbidden classified client input

Medium Severity

kind_for_status labels every storage-backend HTTP 4xx (except a few status-specific cases) as ErrorKind::InvalidInput, and the API layer maps that kind to HTTP 400. Backend responses such as 403 Forbidden or 401 Unauthorized reflect service-to-backend auth or configuration problems, not malformed API client input, so callers get a misleading Bad Request instead of an server-side failure status.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 79ab712. Configure here.

# Conflicts:
#	objectstore-service/src/backend/s3_compatible.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants