[feature] request module: Accept-header parsing and content negotiation#6477
Merged
duncdrum merged 1 commit intoJun 16, 2026
Merged
Conversation
0c43731 to
5a8052d
Compare
line-o
pushed a commit
that referenced
this pull request
Jun 15, 2026
Adds a "Choosing XQSuite vs Java tests" subsection to the testing conventions: default to XQSuite for XQuery-level behavior, and reach for Java only when XQSuite structurally can't express or exercise the behavior (pure-Java units, request/ response/session context that needs a live HTTP request, the HTTP/transport layer itself, or Java-level wiring such as broker pool / locking / transactions). Within Java, use the lightest vehicle that exercises the real behavior. Cites the request-module content-negotiation work (#6477) as precedent. AGENTS.md is the canonical, repo-rooted home for this guidance so it is visible to sessions regardless of which repo they are rooted in. Also removes the "Known Issues" section, whose three entries were all stale; two were never true: - groupby.collation "flaky" / ArrayIndexOutOfBoundsException: unsubstantiated. No issue, PR, commit, or CI evidence backs it; the test is deterministic and passes. Asserting an unbacked "known flake" risks agents dismissing real CI failures. - fn:filter / issue #3382: fixed. #3382 is closed and fn:filter now raises XPTY0004 when the predicate function does not return xs:boolean. - fn:doc() file:// restriction: unsubstantiated. DocUtils already routed file:/URL paths through SourceFactory when this entry was added (2026-03-15), so fn:doc could load file: documents all along; the later #6207 work only added security-gating, it did not lift a block that never existed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add two functions to the request module (http://exist-db.org/xquery/request) so apps no longer have to hand-roll HTTP content negotiation: request:negotiate-content-type($available as xs:string*) as xs:string? request:negotiate-content-type($available as xs:string*, $default as xs:string?) as xs:string? Selects the best media type the server can produce for the request's Accept header (RFC 7231 5.3.2: quality values + */* and type/* wildcards). An empty/absent Accept means no preference -> first offer. Returns the empty sequence (or $default) when nothing is acceptable, so the caller can respond 406 Not Acceptable. request:parse-accept-header() as map(*)* Parses the Accept header into a quality-ranked sequence of maps { "type", "quality", "parameters" }. The parsing and negotiation logic lives in a pure, request-independent helper (org.exist.http.AcceptHeader) so the REST server, Roaster, and other callers can reuse the same algorithm. No XQuery platform (RESTXQ, the request module, EXQuery) currently exposes a q-weighted negotiation primitive; the function shape follows the convergent practice in JAX-RS, Spring, Node's negotiator, Werkzeug, and Go's httputil. Tests: AcceptHeaderTest (15 unit cases over parse + negotiate) and two RESTServiceTest integration cases exercising the functions over HTTP. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5a8052d to
7db4b3c
Compare
duncdrum
approved these changes
Jun 16, 2026
duncdrum
added a commit
to duncdrum/exist
that referenced
this pull request
Jun 20, 2026
…negotiation [feature] request module: Accept-header parsing and content negotiation
duncdrum
added a commit
to duncdrum/exist
that referenced
this pull request
Jun 20, 2026
…negotiation [feature] request module: Accept-header parsing and content negotiation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
[This PR was co-authored with Claude Code. -Joe]
Summary
Adds reusable HTTP content negotiation to the
requestmodule (http://exist-db.org/xquery/request), so applications no longer have to hand-rollAccept-header parsing:Motivation
Content negotiation is currently left to each app developer. The
requestmodule exposes only the raw header (request:get-header("Accept")) — no quality-value parsing, no media-type matching — and the REST server does noAcceptnegotiation at all. This surfaced while reviewing the eXide login PR (eXist-db/eXide#801): the underlying gap is that there's no shared primitive, so every consumer either ignoresAcceptor re-derives a partial parser.Notably, no XQuery platform exposes a
q-weighted negotiation primitive — the RESTXQ spec stops at "absolute before wildcard" specificity, therequestmodule and EXQuery have nothing, and BaseX'sq-factor handling is an extension inside its own RESTXQ engine. The function shape here follows the convergent practice of mature HTTP frameworks: JAX-RSRequest.selectVariant, SpringMediaType, Node'snegotiator, Werkzeug'sMIMEAccept.best_match, and Go'shttputil.NegotiateContentType— all "give the server's offers, get the best match, or nothing → 406."What the functions do
request:negotiate-content-type($available[, $default])— selects the best media type the server can produce for the current request'sAcceptheader (RFC 7231 §5.3.2: quality values and*/*/type/*wildcards). An empty or absentAcceptmeans "no preference," so the first offer wins. Returns the empty sequence — or$default, in the 2-arg form — when nothing is acceptable, so the caller can respond406 Not Acceptable.request:parse-accept-header()— parses theAcceptheader into a quality-ranked sequence of maps, for callers that want to build their own logic:Implementation
The parsing and negotiation logic lives in a pure, request-independent helper (
org.exist.http.AcceptHeader), so the REST server, Roaster, and any other caller can reuse the exact same algorithm; the two XQuery functions are thin request-bound wrappers. A follow-up will have Roaster adopt this in place of its partial hand-rolled negotiation.Tests
AcceptHeaderTest— 15 unit cases overparse(ordering by quality then specificity,qdefaulting, parameters,q=0retention, malformed/empty input) andnegotiate(highest-quality pick, exact-over-wildcard,q=0rejection,type/*wildcards, no-Accept/*/*→ first offer, no-match → empty, tie-break by offer order).RESTServiceTest— 2 integration cases exercising both functions end-to-end over HTTP with a realAcceptheader.Related
Separate small PR fixes an eXist-core bug this work brushed against:
RESTServer.writeResultJSONnever setContent-Type(sooutput:media-typewas ignored for JSON REST results). That one stands alone.