Skip to content

Submit long queries as form POST; fix Dataset reading, writing and negotiation - #37

Merged
namedgraph merged 3 commits into
developfrom
fix/sparql-client-get-post
Sep 21, 2026
Merged

namedgraph merged 3 commits into
developfrom
fix/sparql-client-get-post

Conversation

@namedgraph

Copy link
Copy Markdown
Member

Three defects surfaced by a dataspace of ~15.6M quads, each with a regression test that fails without its fix. 93 tests green (was 66).

A query too long for a URL is submitted as a form POST

SPARQLClient has carried both branches since 2020, but chose between them on the wrong map: params holds only the caller's graph URIs (normally empty) while the query is in mergedParams, so the length compared was the endpoint URI's ~27 characters and the POST branch was unreachable at any size.

Every query went out as GET, which only shows once the URL-encoded query passes what the hop in front of the endpoint allows on a request line. Measured against Varnish (32 KB http_req_size by default):

VALUES encoded GET URL via the platform direct to Fuseki
300 26,452 B 200 200
400 35,027 B 500 200
735 ~64 KB 500 200

The failure surfaces as SocketException: Broken pipe and a 500 — nothing that names a size. After the fix all three are 200.

DatasetProvider reads and writes datasets, and keeps their named graphs

isReadable/isWriteable asked for Model in the provider parameterised on Dataset. Since JAX-RS selects by the entity's Java type, no Dataset could be read or written at allQuadStoreClient.get/add/replace and SPARQLClient.loadDataset had no message body worker. Both providers claimed Model, and only the sort by declared generic type kept ModelProvider in front; the other way round, writeTo would have been handed a ModelCom and thrown ClassCastException.

writeTo also asked isTriples() first. Jena registers JSON-LD as triples and quads, so a dataset served as JSON-LD silently lost every named graph — on a format chosen by negotiation, not by the caller. Asking isQuads() first gives the whole dataset to any language that can carry it.

A dataset is offered as triples too

The triples branch of writeTo has existed since efc8d3a but was unreachable: the writable Dataset list was filtered to isQuads, so QuadStoreImpl never offered a triples variant and answered 406. The triples languages are now appended in a second pass, so every quad format precedes them — these variants carry no q, so their order is what a client accepting anything gets, and that must not be the lossy one. A wildcard Accept still gets quads; only an explicit Accept: text/turtle gets the default graph.

Reading stays quad-only by negotiation, deliberately: the Accept header is ours to choose and should not invite a lossy response, while the provider still parses triples if a remote endpoint answers a CONSTRUCT with them anyway — which is not ours to choose.

Verification

  • Core: 93 tests, 0 failures. Each fix verified to fail without it (e.g. expected: <POST> but was: <GET>, named graph lost in application/ld+json).
  • End to end on a live LinkedDataHub stack via Web-Client: the 47 KB label lookup that returned 500 now returns 200, and pages that logged a console 500 now log none.

🤖 Generated with Claude Code

namedgraph and others added 3 commits September 18, 2026 11:00
…meant to be.

SPARQLClient has carried both branches since 2020 - GET with the query in
the URL, and a form POST once that URL would outgrow maxGetRequestSize -
but it measured the wrong map to choose between them. `params` holds the
caller's default-graph-uri/named-graph-uri and nothing else, normally
empty; the query is in `mergedParams`. So the length compared was the
endpoint URI's, ~27 characters, and the POST branch was unreachable for a
query of any size.

Every query therefore went out as GET. That is invisible until the
URL-encoded query passes whatever the hop in front of the endpoint allows
on a request line - 32 KB for Varnish's default http_req_size, which a
label lookup over a few hundred object URIs reaches - and the connection
is then dropped mid-write, surfacing as a Broken pipe and a 500 rather
than anything that names a size.

SPARQLClientTest covers the switch in both directions, and the request
never leaves the process: a ClientRequestFilter records the method and
aborts. Without the one-word fix, testLargeQueryIsSentAsPost fails with
expected: <POST> but was: <GET>.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two defects, both from reading ModelProvider too closely.

isReadable/isWriteable asked for Model - `type == Model.class` and
`Model.class.isAssignableFrom(type)` - in the provider parameterised on
Dataset. JAX-RS selects a provider by the entity's Java type, so nothing
could read or write a Dataset entity at all: QuadStoreClient's get/add/
replace and SPARQLClient.loadDataset had no message body worker and
failed with "MessageBodyWriter not found" or a null entity. The mirror
image is that BOTH providers claimed Model, and only the sort by declared
generic type kept ModelProvider in front - had it gone the other way,
writeTo would have been handed a ModelCom and thrown ClassCastException.

writeTo then asked isTriples() first and reduced the dataset to its
default graph. Jena registers JSON-LD as triples AND quads, so a dataset
served as JSON-LD silently lost every named graph, on a format chosen by
content negotiation rather than by the caller. Asking isQuads() first
gives the whole dataset to any language that can carry it, and leaves the
default-graph reduction to the languages that genuinely cannot.

DatasetProviderTest covers provider selection by type, both quad and
triples languages in each direction, and the named graphs surviving every
quad format. ModelProviderTest gains the symmetric assertion, so the
exclusivity that keeps the two providers from competing is pinned from
both sides rather than resting on selection order.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…y its graphs.

DatasetProvider has written a dataset as triples since efc8d3a, reducing
it to its default graph for a language that cannot express named ones.
Nothing could ask for that: the writable Dataset list was filtered to
isQuads, so the only consumer of it - QuadStoreImpl's response variants -
never offered a triples representation and answered 406. The branch has
been unreachable ever since MediaTypes moved onto Jena's lang registries.

The triples languages are appended in a second pass, which is the whole
point of doing it in two: these variants carry no q of their own, so
their order decides what a client accepting anything is served, and that
must not be the lossy one. A wildcard Accept still gets quads; only a
client that explicitly asks for Turtle gets the default graph.

Reading stays quad-only by negotiation, deliberately: the Accept header
is ours to choose and should never invite a lossy response, while
DatasetProvider still parses triples if a remote endpoint answers a
CONSTRUCT with them regardless - which is not ours to choose.

MediaTypesTest pins the per-class offerings and the ordering invariant;
QuadStoreClientTest covers the round-trips, the triples request body, the
triples response, and a wildcard Accept keeping the named graphs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@namedgraph
namedgraph merged commit ad0c695 into develop Sep 21, 2026
@namedgraph
namedgraph deleted the fix/sparql-client-get-post branch September 21, 2026 07:21
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.

1 participant