Return generic error messages to clients for server-side errors#530
Return generic error messages to clients for server-side errors#530ChristianPavilonis wants to merge 4 commits intomainfrom
Conversation
58c5906 to
820599c
Compare
prk-Jr
left a comment
There was a problem hiding this comment.
Summary
This hardens client-facing error bodies and the overall direction looks right. GitHub CI is passing, but I found a couple of follow-up improvements around classification drift and regression coverage.
Non-blocking
🤔 thinking
- InvalidUtf8 is currently surfaced as Invalid request data, but its only producer today is embedded config decoding in settings_data.rs, so it still reads like a client error for a server bootstrap failure.
♻️ refactor
- status_code() and user_message() now encode the exposure policy in separate matches. Centralizing that mapping behind a small helper or enum would make it harder for classification and response text to drift apart.
aram356
left a comment
There was a problem hiding this comment.
@ChristianPavilonis Please resolve conflicts
Replace user_message() passthrough of Display output with a match that returns generic messages for 5xx/502/503 errors while keeping brief descriptions for 4xx client errors. Full error details are already logged via log::error! and never lost. Closes #437
0e8dc86 to
b955ece
Compare
aram356
left a comment
There was a problem hiding this comment.
Summary
Well-scoped PR that prevents server-side error details from leaking to clients. The secure-by-default wildcard pattern (new variants automatically get the generic message) is the right approach for a security boundary.
Blocking
❓ question
InvalidUtf8classification: Only produced bysettings_data.rs:24(server bootstrap), yet classified as a 4xx client error returning "Invalid request data". Should this fall through to the generic message? (crates/trusted-server-core/src/error.rs:131)GdprConsentmessage suppression: The only 4xx error that fully hides its detail message. The rationale (consent strings may contain user data) is sound, but confirming intent and adding a doc comment on the variant would help future maintainers. (crates/trusted-server-core/src/error.rs:127)
Non-blocking
🤔 thinking
status_code()anduser_message()can drift apart: Both methods encode client-vs-server classification in separatematchblocks. If a new variant is classified as 4xx instatus_code()but hits the wildcard inuser_message(), it would return 400 with "An internal error occurred" — confusing but safe by default. Not a concern for this PR, but worth noting.
⛏ nitpick
BadRequestdoc comment precision: Says "message is returned to clients" but the message is formatted viaformat!("Bad request: {message}"), not returned verbatim. (crates/trusted-server-core/src/error.rs:20)
🏕 camp site
- PR body references old crate path
crates/common/src/error.rs— the crate was renamed totrusted-server-corein PR #517. - PR checklist says "Uses
tracingmacros" but per CLAUDE.md the project uses thelogcrate.
CI Status
- fmt: PASS
- clippy: PASS
- rust tests: PASS
- js tests: PASS
- format checks: PASS
aram356
left a comment
There was a problem hiding this comment.
Summary
Security-focused change that replaces the user_message() passthrough (which exposed internal error details like store names, config paths, proxy addresses) with categorized responses: client errors (4xx) get brief safe descriptions, server errors (5xx/502/503) get a generic message. Well-scoped single-file change with good test coverage.
Non-blocking
🤔 thinking
- Wildcard match may silently suppress future client errors: The
_ =>catch-all inuser_message()defaults new variants to the generic message, even if they're 4xx client errors. Consider matching exhaustively to force explicit decisions. (error.rs:134)
♻️ refactor
BadRequestformat string duplicated:user_message()and#[display]both hardcode"Bad request: {message}"— could diverge silently. (error.rs:128)
🌱 seedling
- Consider a compile-time guarantee that
status_code()anduser_message()stay in sync: Both methods independently classify variants as client vs. server errors. A future follow-up could extract the classification into a single method (e.g.,is_client_error()) or match exhaustively in both to prevent divergence.
📝 note
InvalidUtf8status code change is correct: The 400 → 500 change is appropriate — the only usage is for the embedded TOML settings file, which is a server-side resource.
CI Status
- fmt: PASS
- clippy: PASS
- rust tests: PASS
- js tests: PASS
- integration tests: PASS
- browser integration tests: PASS
- CodeQL: PASS
| Self::InvalidHeaderValue { .. } => "Invalid header value".to_string(), | ||
| // Server/integration errors (5xx/502/503) — generic message only. | ||
| // Full details are already logged via log::error! in to_error_response. | ||
| _ => "An internal error occurred".to_string(), |
There was a problem hiding this comment.
🤔 thinking — Wildcard match may silently suppress future client errors
The _ => catch-all means any new variant added to TrustedServerError will default to the generic "An internal error occurred" message — even if it's a 4xx client error. This is safe-by-default (no leaks), but it could silently give users unhelpful messages for legitimate client errors.
Consider matching exhaustively (no _) so the compiler forces an explicit decision when new variants are added. The status_code() method already matches exhaustively, so user_message() would stay consistent.
Alternatively, the existing test server_errors_return_generic_message partially mitigates this — but only if someone remembers to add new variants to the test.
| self.to_string() | ||
| match self { | ||
| // Client errors (4xx) — safe to surface a brief description | ||
| Self::BadRequest { message } => format!("Bad request: {message}"), |
There was a problem hiding this comment.
♻️ refactor — BadRequest format string duplicated between Display and user_message()
user_message() returns format!("Bad request: {message}") which duplicates the #[display("Bad request: {message}")] attribute. If one changes, the other could silently diverge.
Suggestion: Reuse Display for the BadRequest arm:
Self::BadRequest { .. } => self.to_string(),| Self::InsecureDefault { .. } => StatusCode::INTERNAL_SERVER_ERROR, | ||
| Self::InvalidHeaderValue { .. } => StatusCode::BAD_REQUEST, | ||
| Self::InvalidUtf8 { .. } => StatusCode::BAD_REQUEST, | ||
| Self::InvalidUtf8 { .. } => StatusCode::INTERNAL_SERVER_ERROR, |
There was a problem hiding this comment.
📝 note — InvalidUtf8 status code change from 400 → 500 is correct
The only usage is for the embedded trusted-server.toml file (settings_data.rs:24), which is a server-side resource. Invalid UTF-8 there is a server error, not a client error.
Summary
user_message()passthrough with categorized responsesChanges
crates/trusted-server-core/src/error.rsself.to_string()inuser_message()with amatchthat surfaces safe messages for client errors and a generic message for server errors. Add doc comments onBadRequest(user-visible message warning),GdprConsent(why detail is suppressed), anduser_messagetrait method (updated semantics). Add two unit tests covering all variants.Closes
Closes #437
Test plan
cargo test --workspacecargo clippy --all-targets --all-features -- -D warningscargo fmt --all -- --checkcd crates/js/lib && npx vitest runcd crates/js/lib && npm run formatcd docs && npm run formatcargo build --bin trusted-server-fastly --release --target wasm32-wasip1fastly compute serveChecklist
unwrap()in production code — useexpect("should ...")logmacros (notprintln!)