Skip to content

fix(node): opaque GraphQL DB error messages (#250) - #255

Open
Ayush7614 wants to merge 3 commits into
Gitlawb:mainfrom
Ayush7614:fix/250-opaque-graphql-db-errors
Open

fix(node): opaque GraphQL DB error messages (#250)#255
Ayush7614 wants to merge 3 commits into
Gitlawb:mainfrom
Ayush7614:fix/250-opaque-graphql-db-errors

Conversation

@Ayush7614

@Ayush7614 Ayush7614 commented Jul 27, 2026

Copy link
Copy Markdown

Summary

Test plan

  • cargo test -p gitlawb-node graphql_db_err_is_opaque — helper rejects raw schema text
  • repos_query_db_error_message_is_opaque — anonymous { repos { ... } } after a schema break returns only the opaque message (CI / DATABASE_URL)

Fixes #250

Summary by CodeRabbit

  • Bug Fixes
    • Improved GraphQL error handling to hide database and schema details from users.
    • Preserved clear business-related error messages where appropriate.
    • Added safe handling for negative task-list limits, returning an empty result instead of triggering a database request.
    • Standardized error responses across task, repository, and update operations.

Route query and mutation DB failures through a shared helper that
logs the cause chain server-side and returns a generic client
message, so unauthenticated POST /graphql cannot leak sqlx text.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Ayush7614, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 46 minutes

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4bf85176-a7d0-4adf-9bc6-9df7c93c1dc7

📥 Commits

Reviewing files that changed from the base of the PR and between 9938e53 and 88a436a.

📒 Files selected for processing (2)
  • crates/gitlawb-node/src/graphql/mod.rs
  • crates/gitlawb-node/src/graphql/query.rs
📝 Walkthrough

Walkthrough

GraphQL error mapping is centralized to redact SQLx/database details while preserving business errors. Mutation and query resolvers adopt the helpers, and task queries clamp limits to 0..=200.

Changes

GraphQL error handling and query validation

Layer / File(s) Summary
Shared GraphQL error mapping
crates/gitlawb-node/src/graphql/mod.rs
Adds centralized SQLx detection, opaque database messages, application error mapping, logging, and unit tests.
Mutation error integration
crates/gitlawb-node/src/graphql/mutation.rs
Routes task mutation failures through shared mapping and verifies database redaction versus business-error preservation.
Query error integration and limit validation
crates/gitlawb-node/src/graphql/query.rs
Updates query error mapping, clamps task limits, and tests schema-error redaction and negative-limit behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant GraphQLResolver
  participant ErrorMapper
  participant Database
  Client->>GraphQLResolver: Execute mutation or query
  GraphQLResolver->>Database: Perform database operation
  Database-->>GraphQLResolver: Return success or error
  GraphQLResolver->>ErrorMapper: Convert failure
  ErrorMapper-->>Client: Opaque DB or preserved business error
Loading

Possibly related PRs

  • Gitlawb/node#87: Modifies the same GraphQL task mutation resolvers for signer/DID authorization.

Suggested reviewers: kevincodex1

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers summary and tests, but it misses most required template sections like motivation, kind of change, and reviewer verification. Add the missing template sections: motivation/context, kind of change, concrete changes, verification steps, and checklist items.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately describes the main change: making GraphQL DB errors opaque.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Ayush7614

Copy link
Copy Markdown
Author

@beardthelion — this PR addresses #250 that you filed. GraphQL never went through AppError, so the opaque helper is separate from #247 as you noted.

@beardthelion beardthelion added crate:node gitlawb-node — the serving node and REST API kind:bug Defect fix — wrong or unsafe behavior labels Jul 27, 2026

@beardthelion beardthelion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The approach is right and the suppression is genuinely complete inside the module. I checked the part that usually goes wrong: Error::new sets source: None, and both source fields carry #[serde(skip)], so nothing can reach the wire even by accident, and {e:#} does render the full anyhow chain into the log rather than trading a client leak for an operator blindspot. I also checked the sink a grep for the old map_err shape cannot see, since async-graphql auto-converts any Display into an error message on a bare ?. No resolver uses one today, so the sweep holds.

Two things block it, and one of them is not style.

Findings

  • [P1] Drop the redundant closures, CI cannot pass as written
    crates/gitlawb-node/src/graphql/query.rs:17
    All twelve sites are written .map_err(|e| crate::graphql::graphql_db_err(e)), which trips clippy::redundant_closure, and .github/workflows/pr-checks.yml:49 runs cargo clippy --workspace --all-targets -- -D warnings. I ran clippy on this head and got twelve of them. .map_err(crate::graphql::graphql_db_err) is enough: I applied it to the six in query.rs and the count went to six, with cargo check clean.

  • [P1] Only send real DB faults through the opaque helper
    crates/gitlawb-node/src/graphql/mutation.rs:79
    claim_task returns anyhow!("task not claimable: not found or already claimed") (db/mod.rs:2619) and finish_task returns anyhow!("task not found or not in claimed state") (db/mod.rs:2641). Neither ever contained sqlx text, so neither was leaking. With this change a signed agent that loses a claim race is told a database error occurred, which is wrong and gives it nothing to act on, and losing a claim race is the ordinary path in a task queue rather than an edge case. Downcasting to sqlx::Error at the call site, or returning a typed variant from the db layer, keeps those two messages while still closing the leak.

  • [P2] Cover the other eleven sites, mutations first
    crates/gitlawb-node/src/graphql/mutation.rs:57
    The #[sqlx::test] makes exactly one line load-bearing. list_all_repos_deduped selects d.is_public (db/mod.rs:1215), so dropping that column fails the first call and ? returns before query.rs:30 ever runs; I confirmed by reverting each line separately, and only query.rs:17 goes red. The unit test in mod.rs calls the helper directly, so it stays green even with every call site reverted. mutation.rs:175 already has a test module using test_support::test_state and Request::new(q).data(AuthenticatedDid(...)), so the mutation-side test has both a home and a pattern to copy.

  • [P2] Clamp the tasks limit before it reaches the query
    crates/gitlawb-node/src/graphql/query.rs:112
    limit: i64 goes unclamped into LIMIT $3 (db/mod.rs:2572), and Postgres rejects a negative limit outright (2201W: LIMIT must not be negative). So an anonymous { tasks(limit: -1) { id } } is a deterministic failure that now writes an error-level log line on every request, where before it only returned one. The REST feed already clamps at api/events.rs:129-134. Worth considering warn! rather than error! for client-caused failures generally.

  • [P3] Share one constant with #247 instead of adding a second
    crates/gitlawb-node/src/graphql/mod.rs:20
    #247 defines DB_ERROR_MESSAGE: &str = "a database error occurred" at error.rs:94, the same string, and this adds a second constant with its own test and a different log event name for the same concept. Whichever lands second should fold into the first. The #226 reference in the comment above also does not hold on this base: error.rs:150 still returns e.to_string() for AppError::Db, so a reader would conclude the leak class closes when this merges, and it does not.

Not asks, just so they are on the record. api/tasks.rs:138-143 still puts e.to_string() into the REST body on an anonymous route, which is the same class as #250 but untouched here, so it wants its own issue rather than scope creep into this one. And async-graphql has a custom-error-conversion feature that removes the blanket Display conversion, which would make a future bare ? in a resolver a compile error instead of a silent re-leak. That is the durable version of this fix if it appeals later, not something to do now.

One thing that is not yours: PR Checks has never run on this head, so the green you see is a triage check rather than the suite. I have approved the run, which means it will now go red on the clippy item above rather than staying silent about it.

Use function-pointer map_err to clear redundant_closure. Opaque only
when a sqlx::Error is in the anyhow chain so claim/finish business
messages survive. Clamp GraphQL tasks(limit); add mutation coverage.
@Ayush7614

Copy link
Copy Markdown
Author

@beardthelion — addressed your review (and the clippy red):

  1. [P1] redundant closures — all anyhow sites are now .map_err(crate::graphql::graphql_db_err); the AppError collector site uses .map_err(crate::graphql::graphql_app_err). clippy -D warnings clean locally.
  2. [P1] sqlx-only opaque — helper opaques only when sqlx::Error is in the anyhow chain; claim/finish business messages (task not claimable…, etc.) are kept and logged at warn!. Unit test + mutation assert cover this.
  3. [P2] more coveragecreate_task_db_error_message_is_opaque (mutation), claim business-message assert, tasks_negative_limit_clamped.
  4. [P2] clamp tasks limitlimit.clamp(0, 200) before SQL so tasks(limit: -1) cannot trip Postgres 2201W.
  5. [P3] constant / comment — noted that GRAPHQL_DB_ERROR_MESSAGE should fold into fix(node): opaque AppError::Internal and AppError::Db HTTP bodies (#226) #247’s DB_ERROR_MESSAGE when that lands; comment no longer claims GET /ipfs/{cid} leaks raw sqlx/DB error text in the 500 body to unauthenticated callers (sibling of #106) #226 already closed the class on this base.

Ready for another look.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
crates/gitlawb-node/src/graphql/mod.rs (1)

71-91: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Good coverage for graphql_db_err; consider adding tests for graphql_app_err too.

The two tests here pin down graphql_db_err's sqlx-vs-business split well, but graphql_app_err (Lines 47-58) has no direct unit test verifying its per-variant behavior — which would have caught the message-collapsing issue flagged above.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/gitlawb-node/src/graphql/mod.rs` around lines 71 - 91, The test module
covers graphql_db_err but lacks direct coverage for graphql_app_err. Add unit
tests targeting graphql_app_err that exercise each error variant and assert the
expected variant-specific messages, including preservation of the original
business message and the configured generic message where applicable.
crates/gitlawb-node/src/graphql/query.rs (1)

108-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the clamp in the GraphQL schema.

limit is silently clamped to 0..=200, but unlike ref_updates' limit param (Line 52-55), there's no #[graphql(desc = ...)] telling API consumers that requests above 200 are truncated without error. Worth adding for schema-introspection consistency.

🔧 Proposed fix
-        #[graphql(default = 50)] limit: i64,
+        #[graphql(default = 50, desc = "Max 200; larger values are clamped to 200.")] limit: i64,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/gitlawb-node/src/graphql/query.rs` around lines 108 - 125, The GraphQL
schema for the tasks query does not document the `limit` clamp. Update the
`limit` argument in `tasks` with a `#[graphql(desc = ...)]` description stating
that values are clamped to 0–200 and values above 200 are truncated without
error, matching the documentation style used by `ref_updates`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/gitlawb-node/src/graphql/mod.rs`:
- Around line 47-58: Update graphql_app_err to preserve and return the safe
messages for non-Db, non-Internal AppError variants, while keeping Db and
Internal responses opaque. Adjust logging so expected client/business errors use
warn-level logging rather than error-level logging, and retain error-level
logging for genuinely opaque failures. Use the existing AppError variants and
graphql_db_err behavior as the reference.

---

Nitpick comments:
In `@crates/gitlawb-node/src/graphql/mod.rs`:
- Around line 71-91: The test module covers graphql_db_err but lacks direct
coverage for graphql_app_err. Add unit tests targeting graphql_app_err that
exercise each error variant and assert the expected variant-specific messages,
including preservation of the original business message and the configured
generic message where applicable.

In `@crates/gitlawb-node/src/graphql/query.rs`:
- Around line 108-125: The GraphQL schema for the tasks query does not document
the `limit` clamp. Update the `limit` argument in `tasks` with a `#[graphql(desc
= ...)]` description stating that values are clamped to 0–200 and values above
200 are truncated without error, matching the documentation style used by
`ref_updates`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bad9390a-53e0-4639-a1ee-27ac3c675684

📥 Commits

Reviewing files that changed from the base of the PR and between 111cff7 and 9938e53.

📒 Files selected for processing (3)
  • crates/gitlawb-node/src/graphql/mod.rs
  • crates/gitlawb-node/src/graphql/mutation.rs
  • crates/gitlawb-node/src/graphql/query.rs

Comment thread crates/gitlawb-node/src/graphql/mod.rs
Keep Db/Internal opaque; pass through NotFound/BadRequest/etc. and document
the tasks limit clamp in the schema.
@Ayush7614

Copy link
Copy Markdown
Author

@coderabbitai thanks — addressed in 88a436a:

  1. graphql_app_err: only Db / Internal stay opaque. Other AppError variants (e.g. NotFound, BadRequest) now surface their safe to_string() messages and log at warn!.
  2. Unit tests cover opaque Db/Internal and pass-through for safe variants.
  3. Documented the tasks limit clamp in the GraphQL schema (desc).

@beardthelion FYI — this was the remaining CodeRabbit major on #255.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@Ayush7614 Thanks for addressing this. The updated graphql_app_err behavior, accompanying coverage, and schema documentation resolve the concern.

🐇 ✅


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

crate:node gitlawb-node — the serving node and REST API kind:bug Defect fix — wrong or unsafe behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unauthenticated POST /graphql leaks raw sqlx/Postgres error text in query resolver errors (sibling of #226/#106)

2 participants