Skip to content

fix(query): 4xx for unknown graphs and commit refs; urn:default names the default graph - #2005

Merged
bplatz merged 3 commits into
mainfrom
fix/protocol-caller-errors
Oct 3, 2026
Merged

bplatz merged 3 commits into
mainfrom
fix/protocol-caller-errors

Conversation

@bplatz

@bplatz bplatz commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

What this fixes

Mistakes a client makes when naming a dataset or a commit came back as 500 err:system/InternalError. They now come back as a 4xx: 400 err:db/InvalidQuery for a malformed reference, and a typed 404 for one that names something the ledger doesn't have.

Request Before After
default-graph-uri / named-graph-uri / FROM naming a graph the ledger doesn't hold, on /query/{ledger} or /query 500 "Unknown named graph" 404 err:db/GraphNotFound
@commit:sha256:abc (too short once sha256: is stripped) 500 400
@commit:<abbreviated CID> 500 400
@commit:<prefix> that matches several commits 500 400
@commit:<prefix> that matches no commit 500 404 err:db/CommitNotFound
Branch create at, commit show commit, or revert naming no commit 404 err:db/LedgerNotFound (revert: err:system/InternalError) 404 err:db/CommitNotFound
  • Where the commit cases apply: the ledger path, SPARQL FROM, and JSON-LD from / at. The too-short and abbreviated-CID cases also apply to branch create's at.
  • Typed 404s, not NotFound: ApiError::GraphNotFound and ApiError::CommitNotFound stay out of is_not_found(), which load_view_from_source reads as "this source isn't a ledger". GraphNotFound matches fix: typed dataset and graph references: one grammar, one resolver #2007's definition.
  • Revert runs through the committer, which flattened the error to a bare status. SubmissionError::CommitNotFound carries the type through; an exhaustive match on SubmissionError needs an arm for it.
  • Embedded API errors: the embedded within-ledger dataset errors (graph not in this ledger, history range, dataset clause on a streaming view) are InvalidQuery now.

urn:default

urn:default is the name /info lists the default graph under. The name is now one constant, fluree_db_core::DEFAULT_GRAPH_IRI, and ledger info uses it too. It names the default graph wherever a graph is read or a default graph is chosen:

  • a query's dataset: FROM / FROM NAMED, the protocol dataset parameters on both routes, ledger#urn:default, and the JSON-LD from / graph selectors
  • GRAPH <urn:default> in a query, as GRAPH <ledger-alias> does; GRAPH ?g never binds it
  • an update: USING, USING NAMED, WITH (which also writes the default graph), GRAPH in the WHERE, and the JSON-LD graph / from / fromNamed / ["graph", …] twins

The default graph is not a named graph, so a write that names a graph urn:default is a 400 (TransactError::DefaultGraphNameAsGraph): a GRAPH template or data quad, GRAPH ?g bound to it, a TriG block, JSON-LD @graph or ["graph", …] templates, CREATE/CLEAR/DROP/COPY/MOVE/ADD operands, a sync target, and bulk import. Before this, WITH <urn:default> INSERT wrote such a graph, which /info hid and nothing reading urn:default reached.

With #2004's union setting on, FROM <urn:default> reads the union, as FROM <ledger> and "from": "default" do; GRAPH <urn:default> still reads the default graph alone.

Bare commit id in a ledger path

A ledger address still requires its tag, since a bare 123456 is both a t and a commit prefix; at / --at keep accepting bare spellings as before. When a ledger address has no tag, the 400 now suggests the tagged form it most likely meant:

Invalid time travel format: 'bagaybq…'. Expected @t:, @time:, @recorded:, @commit:, or @snapshot: prefix; did you mean '@commit:bagaybq…'?

It gives the same kind of suggestion for a bare transaction number (@t:5) and a bare timestamp (@time:…).

Partially addresses #1908: this moves the dataset-addressing and commit-resolution callers of ApiError::query onto caller errors (invalid_query, GraphNotFound, CommitNotFound). The remaining callers still need the audit that issue describes.

Not changed

  • Under Raft, a revert naming no commit is still an untyped 404: the Raft committer builds its own errors.
  • The embedded view's "graph not in this ledger" error for a SPARQL FROM stays a 400. Telling an unknown graph from another ledger's address needs fix: typed dataset and graph references: one grammar, one resolver #2007's resolver.
  • A graph an earlier version registered under the name urn:default is no longer reachable: every read resolves the name to the default graph, and graph management refuses it.

Tests

  • fluree-db-server/tests/sparql_protocol_dataset_params.rs:
    • a_graph_the_ledger_does_not_hold_is_a_404, on both routes and both parameters, with the @type
    • urn_default_names_the_default_graph, covering the ledger route default and named, merged with a named graph, connection ledger#urn:default, and the JSON-LD from twin
  • fluree-db-server/tests/query_path_time_pin.rs:
    • a_malformed_commit_pin_is_a_400, covering the ledger path, FROM, JSON-LD from/at and branch create at
    • a_commit_reference_that_names_no_commit_is_a_404, covering the ledger path, FROM, JSON-LD from/at, branch create at, commit show and revert, with the @type
    • an_untagged_commit_id_in_the_path_names_its_tag
  • fluree-db-api/tests/it_default_graph_name.rs: urn:default in every update position, SPARQL and JSON-LD; each refused write form; bulk import; and GRAPH <urn:default> in a query with the union on and off.
  • fluree-db-api/tests/it_query_dataset.rs: sparql_within_ledger_from_urn_default_scopes_default_graph, plus a 400 assertion on the existing not-in-this-ledger test.
  • fluree-db-api/tests/it_named_graphs.rs: the unknown-named-graph test asserts GraphNotFound and 404.
  • fluree-db-api/tests/it_commit_identity.rs: a 400 assertion on the abbreviated-CID refusal.
  • fluree-db-core: untagged_spec_is_refused_with_its_tagged_spelling.
  • fluree-db-server: submission_error_variants_are_exhaustively_classified classifies CommitNotFound.

Each fix was reverted and its test watched fail. The new server tests failed against the unfixed code with the reported 500, the missing hint, or the old label. The urn:default pieces were reverted one at a time (update default-graph positions, the write refusal, query GRAPH, the update WHERE's named graphs, USING NAMED, the union alias, graph management), as were the GraphNotFound site and each CommitNotFound site (the shared resolver, the query pin, the committer); each turned its cases red.

…:default names the default graph

Mistakes a client makes naming a dataset or a commit reached it as a 500
InternalError. They are now InvalidQuery, a 400:

- a default-graph-uri, named-graph-uri or FROM graph the ledger does not
  hold ("Unknown named graph"), on the ledger and connection routes, and
  the embedded within-ledger dataset errors
- a commit pin that passes the address grammar but cannot be resolved: a
  prefix too short once sha256: is stripped, an abbreviated CID, a prefix
  too long, or one matching no commit or several. This covers the ledger
  path, SPARQL FROM, JSON-LD from/at and branch create's at.

The commit errors are not NotFound, because load_view_from_source takes a
NotFound from db_at to mean the source is not a ledger.

urn:default, the name ledger info lists the default graph under, now
names the default graph wherever a graph within a ledger is addressed:
FROM / FROM NAMED and the protocol dataset parameters on both routes,
ledger#urn:default, and JSON-LD graph selectors.

A ledger address still requires its time-spec tag, since a bare 123456
is both a t and a commit prefix. Refusing an untagged spec now names the
tagged spelling it most likely meant: @commit:<id>, @t:<N> or @time:<ts>.
@bplatz bplatz added bug Something isn't working as expected area:query Query execution, planning, fast paths, overlay, result formatting labels Sep 30, 2026
@bplatz
bplatz requested review from aaj3f and zonotope September 30, 2026 21:03

@aaj3f aaj3f 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.

This is really good cleanup, @bplatz -- FWIW, I think you and I were running into similar issues and investigating them from different symptoms. With that in mind, please do take a look at #2007 (especially if you introduce any new commits). I'll take the work to rebase (and semantically/logically adapt) to #2002, #2004, and this PR before merging #2007, but we should all just be clear-eyed about the alignment we're working towards across all of them. Full review below:


Caller mistakes in dataset and commit naming have been 500s for a long time, and retyping them at the source (select_graph, normalize_commit_ref, time_resolve, the within-ledger dataset errors) means SPARQL, JSON-LD and the embedded API all report the same 400 without any translation layer. The "did you mean @commit:…" hint is a lovely touch.

This PR got no CI (stacked base), so I ran clippy -D warnings and fmt --check on this head, and the tests on a trial merge with origin/main, which now carries db#1997. The merge is clean, all of #1997's new tests and all of this stack's pass on it, and so does the W3C testsuite-sparql. The two PRs don't conflict semantically either: an unknown graph in a query dataset is a 400 here, an unknown USING / WITH graph in an update reads empty there, both fail closed, and JSON-LD matches SPARQL on each surface.

The two that need to land first:

  1. fluree-db-core/src/graph_registry.rs:44 — urn:default names the default graph in dataset positions only. On a trial merge with main, USING <urn:default> reads empty and WITH <urn:default> INSERT writes a hidden named graph that FROM <urn:default> can't see. Fix path: make ir::names_ledger and the GRAPH alias check honor the name, or refuse it as a write target and narrow the comment.
  2. fluree-db-server/tests/sparql_protocol_dataset_params.rs:307 — agree with #2007 (mine, still a draft) on the unknown-graph status before this lands. This PR makes "a dataset clause names a graph the ledger doesn't have" 400 err:db/InvalidQuery; #2007 makes the same case 404 err:db/GraphNotFound. I merged your stack with #2007 on top locally, and a_graph_the_ledger_does_not_hold_is_a_400 is the one server test that fails (it gets the 404). There's no merge that keeps both, and whichever lands before #2000 is what 4.2.3 ships. Details and my lean inline.

The rest:

  • fluree-db-api/src/ledger_view.rs:524 — the NotFound behind branch create's mislabel is shared with commit show and revert; one variant fixes all three.

Two smaller overlaps with #2007, neither of which needs anything from you:

  • urn:default: #2007 replaces three of the four places this PR teaches it (dataset.rs:458, fluree_ext.rs:63, view/query.rs:1258) with one keyword table, GraphSel::keyword in fluree-db-core/src/dataset_ref.rs. I tried it as the single arm "default" | DEFAULT_GRAPH_IRI there, and with that one line both urn_default_names_the_default_graph and sparql_within_ledger_from_urn_default_scopes_default_graph pass on the merged tree, so I'll carry it when I rebase.
  • The 400 you added to sparql_within_ledger_from_alias_spelling_mismatch_is_rejected (it_query_dataset.rs:2544): #2007 retires that test, because it makes FROM <wl> on wl:main read the default graph. I'll take that on the rebase too.

The order I'm proposing is #2002 → #2004 → this → #2007 rebased on top, with the port on me. Happy to talk it through if you'd sequence it differently.

Adherence to repo commitments:

  • Patterns/abstractions: ✔ one constant for urn:default, errors typed where they arise; ⚠️ the name is now resolved in several query-side places (fluree_ext.rs, view/query.rs, dataset.rs, ledger_info.rs) and in none on the update side.
  • Performance (speed first, memory second): ✔ error typing and one string comparison in dataset resolution; no performance-degradation risk.
  • Deployment targets: ✔ API and core changes reach every host, and a 500 → 400 change is an improvement on each; nothing host-specific.
  • Testing: ✔ each new server test covers both routes and both protocol parameters plus the JSON-LD twin, and breaking the unknown-graph retype turns the 400 test red; ⚠️ no update-side urn:default case, and no CI on this head.
  • Conventions: ✔ thorough commit body; endpoint and dataset docs updated.

Verified locally: clippy -D warnings on fluree-db-core / -api / -server, fmt --check, and the core untagged_spec_is_refused_with_its_tagged_spelling at this head. On the trial merge: sparql_protocol_dataset_params (22), query_path_time_pin (15), sparql_dataset_semantics (18), it_query_dataset (75), it_commit_identity (2), it_named_graphs (51), it_trig_insert (17), it_sql_pushdown_lane (19), and testsuite-sparql (all 36 suites).

Approving now so you can merge without waiting on another pass from me — just be sure both are in before you do.

Comment thread fluree-db-core/src/graph_registry.rs Outdated
pub const FIRST_USER_GRAPH_ID: GraphId = 3;

/// The name ledger info gives the default graph, which has no IRI of its own.
/// Anywhere a graph within a ledger is addressed, it names the default graph.

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.

🔴 Must address before merge: "anywhere a graph within a ledger is addressed, it names the default graph" holds for dataset positions only, and on main a write can now put data under urn:default where nothing that reads urn:default as the default graph will see it.

This PR makes urn:default the default graph in FROM / FROM NAMED, the protocol parameters, ledger#urn:default and the JSON-LD selectors. Everywhere else it's still an ordinary IRI, and since db#1997 landed on main (USING / WITH of an unknown graph reads an empty graph), that splits one name four ways. On a trial merge of this branch with origin/main, with Alice in the default graph:

So someone who reads the new docs and writes WITH <urn:default> gets their data put somewhere hidden, silently. Your "Not changed" list already names the GRAPH <urn:default> and write cases; I'd fold them in here rather than leave them, since this constant's comment is the invariant they contradict.

Either direction is small: teach ir::names_ledger (fluree-db-transact/src/ir.rs:608, after a rebase onto main) and the GRAPH alias check (ExecutionContext::single_db_user_graph_id and the is_alias test in GraphOperator) the same name, so urn:default means the default graph everywhere; or refuse urn:default as a write target and narrow this comment to the positions that honor it. Commenting here because fluree-db-transact/src/ir.rs and fluree-db-query/src/graph.rs are not in this diff.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 65a0e48

Took both halves: urn:default reads as the default graph in every position that reads a graph or picks a default graph (USING, USING NAMED, WITH, GRAPH in a query or an update's WHERE, and the JSON-LD twins), and a write that names a graph by it is a 400 (GRAPH templates and quads, TriG, import, graph management, sync).

_ => {
let ids: Vec<_> = matches.iter().map(|h| &h[..7.min(h.len())]).collect();
Err(ApiError::query(format!(
Err(ApiError::invalid_query(format!(

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.

🟡 Optional: the unmatched-prefix NotFound that keeps branch create on err:db/LedgerNotFound is shared with commit show and revert.

fluree-db-api/src/ledger_view.rs:417 (resolve_commit_prefix, zero matches) is still ApiError::NotFound, and it's what branch create (ledger/loading.rs:451), commit show (graph_commit_builder.rs:257) and revert (revert.rs:372-384) all reach through resolve_commit. So the mislabel you note for branch create applies to all three. A commit-specific not-found (still a 404, with its own label) would fix all three at once. Minor and non-blocking — but if you agree it's right, I'd rather see it folded in now than lost in the backlog. Commenting here because ledger_view.rs:417 is not in this diff.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 65a0e48

ApiError::CommitNotFound (404, err:db/CommitNotFound). For consistency with GraphNotFound, a query's @commit: that matches nothing is now this 404 too; malformed, abbreviated or ambiguous refs stay 400. Revert had been surfacing err:system/InternalError (the committer flattened the error to a bare status), so SubmissionError::CommitNotFound carries the type through.

Comment thread fluree-db-api/src/time_resolve.rs Outdated
}

// Step 4: Return result based on match count
// A 400, not `NotFound`: `load_view_from_source` takes any `is_not_found()`

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.

👍 Nice: retyping at the source with the reason written down.

Putting the "a 400, not NotFound, because load_view_from_source reads is_not_found() as not-a-ledger" reasoning right on the line means the next person tempted to make this a 404 finds out why not before they break the from clause. And because the 400 comes from here and from select_graph, the embedded API, both HTTP routes and the JSON-LD selectors all agree without any server-side translation.

for key in ["default-graph-uri", "named-graph-uri"] {
let (status, json) =
get_query(&app, &path, NAMES, &format!("{key}={}", enc(&graph))).await;
assert_eq!(status, StatusCode::BAD_REQUEST, "{path} {key}: {json}");

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.

🔴 Must address before merge: this 400 contradicts #2007 (mine, draft), which answers the same case with 404 err:db/GraphNotFound — let's pick one before this lands.

Both PRs take "a dataset clause names a graph the ledger doesn't have" off the 500 and chose different answers: here it's 400 err:db/InvalidQuery (this assertion, and docs/api/endpoints.md:1225), and in #2007 it's 404 with @type err:db/GraphNotFound (sparql_dataset_semantics.rs:1029, it_query_dataset.rs:3802, docs/api/errors.md). I merged your stack with #2007 on top locally, and this test is the only one of 171 server grp_query tests that fails: it gets {"status":404,"@type":"err:db/GraphNotFound"}. The docs would contradict each other the same way, since endpoints.md merges cleanly.

My lean is 404 with the typed @type, mostly because it's what we already do for the other things a dataset member can name that don't exist (LedgerNotFound, and GraphSourceNotFound at fluree-db-server/src/error.rs:112-118). The swallowing problem the body calls out for commit refs (load_view_from_source treating any is_not_found() as "not a ledger") doesn't apply, because #2007's GraphNotFound is deliberately outside is_not_found(). I may be missing why #1908 wanted 400 for an unknown named graph specifically, though — if you'd rather keep 400, I'll change #2007 to match.

If we go with 404, the smallest version of this PR is probably to drop the unknown-graph row (this test, the Unknown named graph retype at view/fluree_ext.rs:108, the it_query_dataset.rs:2544 assertion and the endpoints.md sentence) and let #2007 own that case. The commit-ref 400s and the untagged-pin hint are independent of #2007; they merge cleanly with it and pass on the merged tree. Either way, it'd be good to settle before #2000, so 4.2.3 doesn't ship one status and the next release flip it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 65a0e48

Went with 404, but implemented here in #2007's exact shape (ApiError::GraphNotFound, err:db/GraphNotFound, outside is_not_found()) rather than dropping the case, so 4.2.3 doesn't ship the 500 if #2007 lands after it.

@aaj3f aaj3f mentioned this pull request Oct 2, 2026
Base automatically changed from feat/union-default-graph to main October 3, 2026 02:35
bplatz added 2 commits October 2, 2026 22:36
…mitNotFound

urn:default named the default graph only in dataset positions. Everywhere
else it was an ordinary IRI, so USING <urn:default> read an empty graph and
WITH <urn:default> INSERT wrote a named graph that no read of urn:default
reached. Now:

- Every position that reads a graph or chooses a default graph reads it as
  the default graph: USING, USING NAMED, WITH (read and write), GRAPH in an
  update's WHERE, the JSON-LD graph / from / fromNamed / ["graph", ...]
  twins, and GRAPH <urn:default> in a query, union default graph included.
  GRAPH ?g never binds it.
- A write that names a graph by it is refused with a 400
  (TransactError::DefaultGraphNameAsGraph): a GRAPH template or data quad,
  GRAPH ?g bound to it, a TriG block, JSON-LD @graph or ["graph", ...]
  templates, CREATE/CLEAR/COPY/MOVE/ADD operands, a sync target, and bulk
  import.

A dataset clause naming a graph the ledger does not hold is now a 404 with
@type err:db/GraphNotFound (ApiError::GraphNotFound), not a 400. Like the
other missing dataset members (LedgerNotFound, GraphSourceNotFound), and
kept out of is_not_found() so it never reads as a missing ledger.

A commit reference that names no commit is a 404 with @type
err:db/CommitNotFound (ApiError::CommitNotFound) on every surface: a query's
@commit: pin (was a 400), and branch create's at, commit show and revert,
which shared an unmatched-prefix NotFound labelled err:db/LedgerNotFound
(err:system/InternalError on revert, whose committer flattened the error to
a bare status). SubmissionError::CommitNotFound carries the type through the
committer. Malformed, abbreviated or ambiguous references stay 400.
@bplatz bplatz changed the title fix(query): 400s for unknown graphs and unresolvable commit refs; urn:default names the default graph fix(query): 4xx for unknown graphs and commit refs; urn:default names the default graph Oct 3, 2026
@bplatz
bplatz merged commit 35950e6 into main Oct 3, 2026
16 checks passed
@bplatz
bplatz deleted the fix/protocol-caller-errors branch October 3, 2026 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:query Query execution, planning, fast paths, overlay, result formatting bug Something isn't working as expected

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants