Conversation
The PUT /api/db/resource handler was defaulting the mime-type to
"application/xml" when a client omitted it. That meant every upload
without an explicit Content-Type was routed into eXist's XML parser
regardless of the path's extension:
- .xq / .xqm content → XPST0003 ("Content is not allowed in prolog")
because XQuery source isn't well-formed XML.
- .svg → stored as application/xml (the parser happens to accept it,
but the stored mime is wrong; clients fetching back see XML rather
than SVG and lose any download/render-by-mime affordance).
- .json → also stored as application/xml or rejected, depending on
well-formedness.
Drop the default and let the 3-arg form of xmldb:store consult eXist's
MimeTable (see XMLDBStore.java lines 152-154: when the function is
called without an explicit mime-type it falls back to
MimeTable.getInstance().getContentTypeFor(docName), which honors the
server's mime-types.xml — `.xq` → application/xquery, `.svg` →
image/svg+xml, `.json` → application/json, etc.).
The OpenAPI spec for this endpoint already promised "MIME type is
auto-detected from the file extension if not specified"; this commit
makes the implementation match the documented contract. The spec
wording is also tightened to name mime-types.xml as the lookup source.
Concrete downstream impact:
- existdb-oxygen-plugin can drop its hardcoded MIME_BY_EXTENSION
table (currently in ExistURLConnection.java:51-70, ~20 lines)
and just omit mime-type on uploads — the server will pick the
right one from the path.
- Any future client that uploads .xq / .xqm / images / JSON
without sending a Content-Type gets the right behavior
out of the box rather than a 500.
Additional cleanup: the catch arm's text/html → store-as-binary
fallback is removed. It never actually worked — xmldb:store-as-binary
runs string content through eXist's XML parser anyway, and
xmldb:set-mime-type refuses to relabel a binary resource with an
XML-class mime (`text/html` is XML-class per mime-types.xml). The
fallback has been broken since written. Verified that no in-tree
consumer relies on it: eXide stores HTML as application/xhtml+xml
by default (only template-created HTML uses text/html), and the
oxygen plugin always supplies an explicit mime. Clients that need to
preserve raw bytes for unparseable content can now send
mime-type=application/octet-stream explicitly (covered by a new
test).
Cypress: 27 cases on db.cy.js, 7 new — auto-detect for .xq / .xqm /
.svg / .json, well-formed .html → text/html, unparseable HTML → 400
with parse-error message, and octet-stream preserves raw bytes. All
27 pass. Full cypress suite: 4 pre-existing failures on develop
(packages.cy.js × 1, query_scope.cy.js × 2, query_pool_reuse.cy.js
× 2) remain unchanged — verified by running them against develop
without this fix.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes eXist-db#35. Adds a path-in-URL sibling to /api/db/resource that streams via eXist's REST servlet. Uses controller.xq's <forward url="/rest{path}" absolute="yes"> to internally dispatch the request to /exist/rest, where the body streams directly into broker.storeDocument(InputSource) without materializing in JVM memory. The `absolute="yes"` attribute is what escapes the controller context — without it, /rest/* resolves relative to /db/apps/existdb-openapi/ rather than the servlet root. | URL shape | Use for | Behavior | |---|---|---| | `PUT /api/db/resource` | Text + metadata in single roundtrip | JSON envelope, path in body, Roaster handler — buffered (tens-of-MB ceiling) | | `PUT /api/db/resource/{path}` | Binary or any large upload | Raw bytes, Content-Type from header — streams via REST | The path-in-URL endpoint supports PUT, GET, and DELETE — forwarded through to /exist/rest, which returns its native responses (HTTP status + raw bytes on GET). The JSON-envelope endpoint at /api/db/resource is unchanged. Surveyed against eXist's WebDAV (Jackrabbit) and XML-RPC, eXide, xst, atom-editor-support, and fusion-studio-api: five of six peers use raw bytes over HTTP for binary transport (XML-RPC excepted, where base64 in the XML envelope is a wire-format artifact). None use base64-in-JSON. The path-in-URL forward matches the eXide / fusion-studio-api / WebDAV idiom (path-in-URL, raw bytes, mime from Content-Type) while keeping the JSON envelope available at the bare URL for text-with-metadata. Verified end-to-end via curl: 68-byte transparent PNG PUT → /exist/rest GET confirms storage at correct path with correct mime → /api/db/properties sees the resource → forwarded GET retrieves byte-identical bytes (PNG signature 89 50 4e 47 0d 0a 1a 0a survives). DELETE round-trips too. Cypress: 5 new tests in db.cy.js for the path-in-URL endpoint. They exercise PUT/GET/DELETE routing with text content (cy.request can't cleanly send raw bytes — Cypress's Buffer↔JSON serialization mangles them — so the binary round-trip is verified via curl in the PR description; the controller.xq forward logic is the same regardless of body type). Full cypress suite: 110/110. Memory ceiling: roaster's request-body parser is bypassed by this forward, so this endpoint is streaming-capable up to whatever Jetty/eXist accept (tens of GB with config tuning). The JSON-envelope endpoint at /api/db/resource still has Roaster's per-request buffering ceiling (~tens of MB for the default Jetty config). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
line-o
left a comment
There was a problem hiding this comment.
I would rather like to see us come up with a way to serve binaries from roaster efficiently.
If this is done within the controller I would want to avoid to have it within the api path.
|
Agreed that roaster would be the best option. An analysis of options is underway. |
|
[This response was co-authored with Claude Code. -Joe] Agreed on both points — the Roaster-native path is the right north star, and parking a The two ends are blocked by different thingsUpload side, Download side, What I think Roaster would needThree layers, roughly:
(1) gives the API surface, (2) gives it teeth, (3) is the "do what On
|
|
[This response was co-authored with Claude Code. -Joe] Surveyed the five neighboring systems for prior art on this — the convergence is striking and points at why a Roaster-side fix alone won't fully close the gap. Two patterns exist, nothing in betweenPattern A — Java-servlet-direct, fully streaming, never crosses XQuery:
Pattern B — XQuery-value binding, materialized to xs:base64Binary or equivalent:
Nothing in any of these surveyed systems offers a spec-driven binding (annotation-based or OpenAPI-based) that also exposes a raw InputStream to the handler. The choice everywhere has been: servlet-direct = streaming + no spec, or spec-driven = value materialization. The two haven't been bridged yet, in any of them. BaseX Request module specificallyNo streaming hook there either — full list of body-adjacent functions is What that means for the Roaster workA genuine free improvement Roaster can grab today: swap The bigger win — handler sees a real InputStream and can pipe directly to If both of those land, the |
|
[This response was co-authored with Claude Code. -Joe] Closing as superseded by #59, which consolidates all resource-content transport onto a single binary-safe |
…th, set-mime Adds the audit §2 items 2–6 to db-core (the single implementation), exposed through the thin roaster wrapper and api.json, with Cypress coverage: - item 2 — GET /api/db/resource?meta=full flattens the resource's metadata (owner, group, mode, acl, size, created, last-modified) alongside the content, sparing a second /properties round trip. Flat (not nested) to match both eXide's load response and our own /properties shape. Default (no meta) is unchanged. - item 3 — every list item carries a `writable` boolean (sm:has-access "w"), a file-browser affordance evaluated authoritatively server-side rather than left for the client to derive from mode bits. - item 5 — a flat (non-recursive) listing carries start/count pagination and a `total`; child collections then resources, sliced by start (1-based) and count (default: all). Tree listings are unaffected. - item 6 — get-resource always returns runPath. - item 4 — set MIME via POST /api/db/permissions (xmldb:set-mime-type). A mime incompatible with the resource's storage class (eXist only allows an XML-class mime on an XML resource, a binary-class mime on a binary one) now surfaces as a clean 400 instead of an undeclared-500 → SENR0001. writable + pagination are always-on (per design decision): default output is a superset of the prior shape, never a truncation. Serialization (PR eXist-db#48) and binary streaming (eXist-db#38/eXist-db#35) remain out of scope. Verified on a live eXist: 54 db Cypress tests pass (46 prior + 8 new), query.cy.js stays green (routing intact). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…th, set-mime Adds the audit §2 items 2–6 to db-core (the single implementation), exposed through the thin roaster wrapper and api.json, with Cypress coverage: - item 2 — GET /api/db/resource?meta=full flattens the resource's metadata (owner, group, mode, acl, size, created, last-modified) alongside the content, sparing a second /properties round trip. Flat (not nested) to match both eXide's load response and our own /properties shape. Default (no meta) is unchanged. - item 3 — every list item carries a `writable` boolean (sm:has-access "w"), a file-browser affordance evaluated authoritatively server-side rather than left for the client to derive from mode bits. - item 5 — a flat (non-recursive) listing carries start/count pagination and a `total`; child collections then resources, sliced by start (1-based) and count (default: all). Tree listings are unaffected. - item 6 — get-resource always returns runPath. - item 4 — set MIME via POST /api/db/permissions (xmldb:set-mime-type). A mime incompatible with the resource's storage class (eXist only allows an XML-class mime on an XML resource, a binary-class mime on a binary one) now surfaces as a clean 400 instead of an undeclared-500 → SENR0001. writable + pagination are always-on (per design decision): default output is a superset of the prior shape, never a truncation. Serialization (PR eXist-db#48) and binary streaming (eXist-db#38/eXist-db#35) remain out of scope. Verified on a live eXist: 54 db Cypress tests pass (46 prior + 8 new), query.cy.js stays green (routing intact). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t-db#38) Adds GET/PUT /api/db/resource/{path} — a binary-safe, roaster-native resource transport, the clean alternative to the JSON-envelope /api/db/resource (which is text/base64-only). Closes the binary side of eXist-db#35/eXist-db#38 without a controller workaround (Juri's concern about an /api/* side door). - GET streams a binary resource's raw bytes with response:stream-binary (NOT via the serializer, which would emit base64 text and corrupt it — the established roaster pattern, the same path eXist's REST server uses); an XML/text resource is returned as a node so roaster serializes it once with its stored mime (returning a pre-serialized string would be XML-escaped a second time). - PUT stores the raw request body via db-core (binary bodies arrive intact; mime inferred from the name), returning { stored, runPath } (201/200). No exist-core or roaster change needed — binary transport works on stock eXist via the existing response:stream-binary. (Zero-copy streaming of very large stored binaries is a separate exist-core optimization, tracked with exist-strategy.) Self-contained Cypress coverage (raw round-trip not base64-mangled, 201/200 + stored/runPath, XML serialized with mime, 404); full db suite stays green (59 + 4). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ource
Retire the JSON envelope and the path-in-URL endpoints; all resource content now
flows through the query-param /api/db/resource, identical in shape to its db
siblings.
- GET /api/db/resource?path= → raw content: binary streamed as-is; XML/text
serialized from the node tree (eXist has no raw byte form for XML). Content-Type
is the stored mime. download=true → Content-Disposition: attachment.
- PUT /api/db/resource?path= → raw request body (binary-safe); mime from
Content-Type or inferred from the name; returns { path } (201/200) so the caller
can reconcile name normalization.
- Serialization params: full W3C vocabulary + eXist extensions via the output:
namespace (eXist-db/exist#6447 — expand-xincludes, highlight-matches,
add-exist-id, process-xsl-pi, jsonp, insert-final-newline). Unsupported params
on a pre-#6447 eXist return a clean 400.
- Removed: JSON envelope, meta=full + X-Resource-* headers, runPath (derivable
client-side), the /api/resource/{path} and /api/db/resource/{path} endpoints,
and dbc:get-run-path.
- Metadata stays at GET /api/db/properties.
Supersedes eXist-db#38, eXist-db#56; folds + extends eXist-db#48. Depends on eXist-db#54, eXist-db#55.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…th, set-mime Adds the audit §2 items 2–6 to db-core (the single implementation), exposed through the thin roaster wrapper and api.json, with Cypress coverage: - item 2 — GET /api/db/resource?meta=full flattens the resource's metadata (owner, group, mode, acl, size, created, last-modified) alongside the content, sparing a second /properties round trip. Flat (not nested) to match both eXide's load response and our own /properties shape. Default (no meta) is unchanged. - item 3 — every list item carries a `writable` boolean (sm:has-access "w"), a file-browser affordance evaluated authoritatively server-side rather than left for the client to derive from mode bits. - item 5 — a flat (non-recursive) listing carries start/count pagination and a `total`; child collections then resources, sliced by start (1-based) and count (default: all). Tree listings are unaffected. - item 6 — get-resource always returns runPath. - item 4 — set MIME via POST /api/db/permissions (xmldb:set-mime-type). A mime incompatible with the resource's storage class (eXist only allows an XML-class mime on an XML resource, a binary-class mime on a binary one) now surfaces as a clean 400 instead of an undeclared-500 → SENR0001. writable + pagination are always-on (per design decision): default output is a superset of the prior shape, never a truncation. Serialization (PR #48) and binary streaming (#38/#35) remain out of scope. Verified on a live eXist: 54 db Cypress tests pass (46 prior + 8 new), query.cy.js stays green (routing intact). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t-db#38) Adds GET/PUT /api/db/resource/{path} — a binary-safe, roaster-native resource transport, the clean alternative to the JSON-envelope /api/db/resource (which is text/base64-only). Closes the binary side of eXist-db#35/eXist-db#38 without a controller workaround (Juri's concern about an /api/* side door). - GET streams a binary resource's raw bytes with response:stream-binary (NOT via the serializer, which would emit base64 text and corrupt it — the established roaster pattern, the same path eXist's REST server uses); an XML/text resource is returned as a node so roaster serializes it once with its stored mime (returning a pre-serialized string would be XML-escaped a second time). - PUT stores the raw request body via db-core (binary bodies arrive intact; mime inferred from the name), returning { stored, runPath } (201/200). No exist-core or roaster change needed — binary transport works on stock eXist via the existing response:stream-binary. (Zero-copy streaming of very large stored binaries is a separate exist-core optimization, tracked with exist-strategy.) Self-contained Cypress coverage (raw round-trip not base64-mangled, 201/200 + stored/runPath, XML serialized with mime, 404); full db suite stays green (59 + 4). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ource
Retire the JSON envelope and the path-in-URL endpoints; all resource content now
flows through the query-param /api/db/resource, identical in shape to its db
siblings.
- GET /api/db/resource?path= → raw content: binary streamed as-is; XML/text
serialized from the node tree (eXist has no raw byte form for XML). Content-Type
is the stored mime. download=true → Content-Disposition: attachment.
- PUT /api/db/resource?path= → raw request body (binary-safe); mime from
Content-Type or inferred from the name; returns { path } (201/200) so the caller
can reconcile name normalization.
- Serialization params: full W3C vocabulary + eXist extensions via the output:
namespace (eXist-db/exist#6447 — expand-xincludes, highlight-matches,
add-exist-id, process-xsl-pi, jsonp, insert-final-newline). Unsupported params
on a pre-#6447 eXist return a clean 400.
- Removed: JSON envelope, meta=full + X-Resource-* headers, runPath (derivable
client-side), the /api/resource/{path} and /api/db/resource/{path} endpoints,
and dbc:get-run-path.
- Metadata stays at GET /api/db/properties.
Supersedes eXist-db#38, eXist-db#56; folds + extends eXist-db#48. Depends on eXist-db#54, eXist-db#55.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t-db#38) Adds GET/PUT /api/db/resource/{path} — a binary-safe, roaster-native resource transport, the clean alternative to the JSON-envelope /api/db/resource (which is text/base64-only). Closes the binary side of eXist-db#35/eXist-db#38 without a controller workaround (Juri's concern about an /api/* side door). - GET streams a binary resource's raw bytes with response:stream-binary (NOT via the serializer, which would emit base64 text and corrupt it — the established roaster pattern, the same path eXist's REST server uses); an XML/text resource is returned as a node so roaster serializes it once with its stored mime (returning a pre-serialized string would be XML-escaped a second time). - PUT stores the raw request body via db-core (binary bodies arrive intact; mime inferred from the name), returning { stored, runPath } (201/200). No exist-core or roaster change needed — binary transport works on stock eXist via the existing response:stream-binary. (Zero-copy streaming of very large stored binaries is a separate exist-core optimization, tracked with exist-strategy.) Self-contained Cypress coverage (raw round-trip not base64-mangled, 201/200 + stored/runPath, XML serialized with mime, 404); full db suite stays green (59 + 4). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ource
Retire the JSON envelope and the path-in-URL endpoints; all resource content now
flows through the query-param /api/db/resource, identical in shape to its db
siblings.
- GET /api/db/resource?path= → raw content: binary streamed as-is; XML/text
serialized from the node tree (eXist has no raw byte form for XML). Content-Type
is the stored mime. download=true → Content-Disposition: attachment.
- PUT /api/db/resource?path= → raw request body (binary-safe); mime from
Content-Type or inferred from the name; returns { path } (201/200) so the caller
can reconcile name normalization.
- Serialization params: full W3C vocabulary + eXist extensions via the output:
namespace (eXist-db/exist#6447 — expand-xincludes, highlight-matches,
add-exist-id, process-xsl-pi, jsonp, insert-final-newline). Unsupported params
on a pre-#6447 eXist return a clean 400.
- Removed: JSON envelope, meta=full + X-Resource-* headers, runPath (derivable
client-side), the /api/resource/{path} and /api/db/resource/{path} endpoints,
and dbc:get-run-path.
- Metadata stays at GET /api/db/properties.
Supersedes eXist-db#38, eXist-db#56; folds + extends eXist-db#48. Depends on eXist-db#54, eXist-db#55.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t-db#38) Adds GET/PUT /api/db/resource/{path} — a binary-safe, roaster-native resource transport, the clean alternative to the JSON-envelope /api/db/resource (which is text/base64-only). Closes the binary side of eXist-db#35/eXist-db#38 without a controller workaround (Juri's concern about an /api/* side door). - GET streams a binary resource's raw bytes with response:stream-binary (NOT via the serializer, which would emit base64 text and corrupt it — the established roaster pattern, the same path eXist's REST server uses); an XML/text resource is returned as a node so roaster serializes it once with its stored mime (returning a pre-serialized string would be XML-escaped a second time). - PUT stores the raw request body via db-core (binary bodies arrive intact; mime inferred from the name), returning { stored, runPath } (201/200). No exist-core or roaster change needed — binary transport works on stock eXist via the existing response:stream-binary. (Zero-copy streaming of very large stored binaries is a separate exist-core optimization, tracked with exist-strategy.) Self-contained Cypress coverage (raw round-trip not base64-mangled, 201/200 + stored/runPath, XML serialized with mime, 404); full db suite stays green (59 + 4). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ource
Retire the JSON envelope and the path-in-URL endpoints; all resource content now
flows through the query-param /api/db/resource, identical in shape to its db
siblings.
- GET /api/db/resource?path= → raw content: binary streamed as-is; XML/text
serialized from the node tree (eXist has no raw byte form for XML). Content-Type
is the stored mime. download=true → Content-Disposition: attachment.
- PUT /api/db/resource?path= → raw request body (binary-safe); mime from
Content-Type or inferred from the name; returns { path } (201/200) so the caller
can reconcile name normalization.
- Serialization params: full W3C vocabulary + eXist extensions via the output:
namespace (eXist-db/exist#6447 — expand-xincludes, highlight-matches,
add-exist-id, process-xsl-pi, jsonp, insert-final-newline). Unsupported params
on a pre-#6447 eXist return a clean 400.
- Removed: JSON envelope, meta=full + X-Resource-* headers, runPath (derivable
client-side), the /api/resource/{path} and /api/db/resource/{path} endpoints,
and dbc:get-run-path.
- Metadata stays at GET /api/db/properties.
Supersedes eXist-db#38, eXist-db#56; folds + extends eXist-db#48. Depends on eXist-db#54, eXist-db#55.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t-db#38) Adds GET/PUT /api/db/resource/{path} — a binary-safe, roaster-native resource transport, the clean alternative to the JSON-envelope /api/db/resource (which is text/base64-only). Closes the binary side of eXist-db#35/eXist-db#38 without a controller workaround (Juri's concern about an /api/* side door). - GET streams a binary resource's raw bytes with response:stream-binary (NOT via the serializer, which would emit base64 text and corrupt it — the established roaster pattern, the same path eXist's REST server uses); an XML/text resource is returned as a node so roaster serializes it once with its stored mime (returning a pre-serialized string would be XML-escaped a second time). - PUT stores the raw request body via db-core (binary bodies arrive intact; mime inferred from the name), returning { stored, runPath } (201/200). No exist-core or roaster change needed — binary transport works on stock eXist via the existing response:stream-binary. (Zero-copy streaming of very large stored binaries is a separate exist-core optimization, tracked with exist-strategy.) Self-contained Cypress coverage (raw round-trip not base64-mangled, 201/200 + stored/runPath, XML serialized with mime, 404); full db suite stays green (59 + 4). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ource
Retire the JSON envelope and the path-in-URL endpoints; all resource content now
flows through the query-param /api/db/resource, identical in shape to its db
siblings.
- GET /api/db/resource?path= → raw content: binary streamed as-is; XML/text
serialized from the node tree (eXist has no raw byte form for XML). Content-Type
is the stored mime. download=true → Content-Disposition: attachment.
- PUT /api/db/resource?path= → raw request body (binary-safe); mime from
Content-Type or inferred from the name; returns { path } (201/200) so the caller
can reconcile name normalization.
- Serialization params: full W3C vocabulary + eXist extensions via the output:
namespace (eXist-db/exist#6447 — expand-xincludes, highlight-matches,
add-exist-id, process-xsl-pi, jsonp, insert-final-newline). Unsupported params
on a pre-#6447 eXist return a clean 400.
- Removed: JSON envelope, meta=full + X-Resource-* headers, runPath (derivable
client-side), the /api/resource/{path} and /api/db/resource/{path} endpoints,
and dbc:get-run-path.
- Metadata stays at GET /api/db/properties.
Supersedes eXist-db#38, eXist-db#56; folds + extends eXist-db#48. Depends on eXist-db#54, eXist-db#55.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t-db#38) Adds GET/PUT /api/db/resource/{path} — a binary-safe, roaster-native resource transport, the clean alternative to the JSON-envelope /api/db/resource (which is text/base64-only). Closes the binary side of eXist-db#35/eXist-db#38 without a controller workaround (Juri's concern about an /api/* side door). - GET streams a binary resource's raw bytes with response:stream-binary (NOT via the serializer, which would emit base64 text and corrupt it — the established roaster pattern, the same path eXist's REST server uses); an XML/text resource is returned as a node so roaster serializes it once with its stored mime (returning a pre-serialized string would be XML-escaped a second time). - PUT stores the raw request body via db-core (binary bodies arrive intact; mime inferred from the name), returning { stored, runPath } (201/200). No exist-core or roaster change needed — binary transport works on stock eXist via the existing response:stream-binary. (Zero-copy streaming of very large stored binaries is a separate exist-core optimization, tracked with exist-strategy.) Self-contained Cypress coverage (raw round-trip not base64-mangled, 201/200 + stored/runPath, XML serialized with mime, 404); full db suite stays green (59 + 4). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ource
Retire the JSON envelope and the path-in-URL endpoints; all resource content now
flows through the query-param /api/db/resource, identical in shape to its db
siblings.
- GET /api/db/resource?path= → raw content: binary streamed as-is; XML/text
serialized from the node tree (eXist has no raw byte form for XML). Content-Type
is the stored mime. download=true → Content-Disposition: attachment.
- PUT /api/db/resource?path= → raw request body (binary-safe); mime from
Content-Type or inferred from the name; returns { path } (201/200) so the caller
can reconcile name normalization.
- Serialization params: full W3C vocabulary + eXist extensions via the output:
namespace (eXist-db/exist#6447 — expand-xincludes, highlight-matches,
add-exist-id, process-xsl-pi, jsonp, insert-final-newline). Unsupported params
on a pre-#6447 eXist return a clean 400.
- Removed: JSON envelope, meta=full + X-Resource-* headers, runPath (derivable
client-side), the /api/resource/{path} and /api/db/resource/{path} endpoints,
and dbc:get-run-path.
- Metadata stays at GET /api/db/properties.
Supersedes eXist-db#38, eXist-db#56; folds + extends eXist-db#48. Depends on eXist-db#54, eXist-db#55.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t-db#38) Adds GET/PUT /api/db/resource/{path} — a binary-safe, roaster-native resource transport, the clean alternative to the JSON-envelope /api/db/resource (which is text/base64-only). Closes the binary side of eXist-db#35/eXist-db#38 without a controller workaround (Juri's concern about an /api/* side door). - GET streams a binary resource's raw bytes with response:stream-binary (NOT via the serializer, which would emit base64 text and corrupt it — the established roaster pattern, the same path eXist's REST server uses); an XML/text resource is returned as a node so roaster serializes it once with its stored mime (returning a pre-serialized string would be XML-escaped a second time). - PUT stores the raw request body via db-core (binary bodies arrive intact; mime inferred from the name), returning { stored, runPath } (201/200). No exist-core or roaster change needed — binary transport works on stock eXist via the existing response:stream-binary. (Zero-copy streaming of very large stored binaries is a separate exist-core optimization, tracked with exist-strategy.) Self-contained Cypress coverage (raw round-trip not base64-mangled, 201/200 + stored/runPath, XML serialized with mime, 404); full db suite stays green (59 + 4). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ource
Retire the JSON envelope and the path-in-URL endpoints; all resource content now
flows through the query-param /api/db/resource, identical in shape to its db
siblings.
- GET /api/db/resource?path= → raw content: binary streamed as-is; XML/text
serialized from the node tree (eXist has no raw byte form for XML). Content-Type
is the stored mime. download=true → Content-Disposition: attachment.
- PUT /api/db/resource?path= → raw request body (binary-safe); mime from
Content-Type or inferred from the name; returns { path } (201/200) so the caller
can reconcile name normalization.
- Serialization params: full W3C vocabulary + eXist extensions via the output:
namespace (eXist-db/exist#6447 — expand-xincludes, highlight-matches,
add-exist-id, process-xsl-pi, jsonp, insert-final-newline). Unsupported params
on a pre-#6447 eXist return a clean 400.
- Removed: JSON envelope, meta=full + X-Resource-* headers, runPath (derivable
client-side), the /api/resource/{path} and /api/db/resource/{path} endpoints,
and dbc:get-run-path.
- Metadata stays at GET /api/db/properties.
Supersedes eXist-db#38, eXist-db#56; folds + extends eXist-db#48. Depends on eXist-db#54, eXist-db#55.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
[This PR was co-authored with Claude Code. -Joe]
Closes #35.
Summary
Adds a path-in-URL sibling endpoint at
/api/db/resource/{path}that streams via eXist's REST servlet, complementing the existing JSON-envelope endpoint:PUT /api/db/resource(path in JSON body)PUT /api/db/resource/{path}(path in URL)Content-Typeheader — streams via /exist/restSame URL prefix (
/exist/apps/existdb-openapi/api/db/resource…), noweb.xmlchanges, no second URL space. controller.xq routes the path-in-URL variant to/exist/rest/{path}via<forward url="/rest{$path}" absolute="yes"/>. Theabsolute="yes"flag escapes the controller context so the URL resolves to the servlet root rather than/db/apps/existdb-openapi/rest/….Why this shape
The original PR design (base64 inside the JSON envelope) was rethought after surveying peer systems:
/api/storage)fetch(url, {body: <File>})request:get-data()%rest:PUT("{$body}")Five of six peers use raw bytes over HTTP for binary; nobody uses base64-in-JSON. The path-in-URL forward matches the eXide / fusion-studio-api / WebDAV idiom (path-in-URL, raw bytes, mime from
Content-Type) while keeping the JSON envelope available at the bare URL for text-with-metadata.A second concern that tipped this design: Roaster's request-body pipeline always materializes the body in JVM memory (see
roaster:content/body.xqm'sbody:parse— every Content-Type branch ends up reading the full body into XQuery values before the handler runs), so the JSON-envelope endpoint inherits a memory ceiling that grows with file size. A path-in-URL forward to/exist/restsidesteps Roaster entirely and streams through eXist'sRESTServer.java, where the body becomes an InputStream thatbroker.storeDocument(InputSource)consumes directly — no JVM buffer.controller.xq dispatch
The discovery that made this work: eXist URL-rewriting docs explicitly note that
absolute="yes"resolves theurlagainst the servlet context root rather than the controller context. Without it,/rest/...resolves to/db/apps/existdb-openapi/rest/...and the forward silently misfires (returns 201 but the request never reaches EXistServlet — an interesting URLRewrite framework quirk I hit while developing this).End-to-end verification
Manual curl with a 68-byte transparent PNG:
DELETE round-trips too:
Cypress
5 new tests in
db.cy.jsfor the path-in-URL endpoint covering PUT / GET / DELETE routing with text content. The controller.xq forward logic is body-type-agnostic; text exercises the same routing path as binary. (cy.request can't cleanly send raw binary bytes — Cypress'sBuffer↔ JSON serialization mangles them — so the binary round-trip is verified via the curl recipe above. Considered usingcy.taskto invoke node-side HTTP for the binary case but the marginal CI value didn't justify the plugin overhead.) Full cypress suite: 110/110 passing.OpenAPI spec
/api/db/resource/{path}documented as a sibling endpoint with PUT/GET/DELETE. The request/response bodies are described asapplication/octet-stream(type: string, format: binary) per OpenAPI conventions. The bare/api/db/resourceendpoint's description gained a note pointing at the path-in-URL variant for binary uploads.Out of scope / known limits
Content-Type: text/plainor appropriate).util:binary-to-stringinterprets bytes as UTF-8 and mangles anything non-UTF-8. Recommendation in the OpenAPI description: use the path-in-URL variant for binary GET.Dependency on #34
This PR is branched off #34 (
fix/store-resource-mime-auto-detect). The two PRs sharemodules/db.xqmbut the path-in-URL endpoint is implemented entirely incontroller.xq(no db.xqm changes specific to this PR beyond the comment update). Merge #34 first → this rebases cleanly.Considered and rejected: wrapping REST's response in a JSON envelope via
<view>A natural follow-up question: could a
<forward>+<view>pipeline normalize REST's minimal response (empty body, just an HTTP status) into the same{stored, runPath}/{removed}/{error}JSON envelope the bare/api/db/resourcehandler returns? The eXist URL-rewriting framework supports exactly this kind of two-stage chain (the documentation app and eXide both use it for HTML templating).Tried it on a side branch. The pattern composes cleanly when the first forward targets a DB-backed XQuery — the framework already knows how to dispatch through to a view handler in that case. But our first forward targets a servlet (EXistServlet for streaming) which lives outside the URL-rewriting tree, and the view's subsequent
<forward url="modules/wrap-rest-response.xq"/>fails to resolve cleanly:{$exist:controller}/modules/wrap-rest-response.xq) → 500 with NPE inXQueryServlet.processline 358 (Paths.get(path)with nullpath— neitherxquery.sourcenorxquery.urlattribute is set, so XQueryServlet falls through to the filesystem-path branch)modules/wrap-rest-response.xq) → 400 with empty body; the framework resolves relative paths against the current request path, which after the rest forward is/exist/rest/db/..., so the lookup never finds the XQueryThere's likely a way to make it work (manually setting
xquery.sourceorxquery.urlattributes on the view forward, or routing through a different servlet mapping) but it would be reverse-engineering framework internals to glue together what isn't designed to chain. The cost outweighs the benefit, given that REST's HTTP status codes already convey what clients need to know and the peer survey shows no precedent for envelope-wrapping binary responses (none of WebDAV, XML-RPC, eXide, xst, atom-editor-support, or fusion-studio-api do this).If we ever decide the response envelope matters enough to justify the work, the cleaner path would be a thin Java handler that takes raw bytes via servlet InputStream and emits JSON directly — but that requires a
web.xmlchange, which would prevent existdb-openapi from being a self-contained XAR install (the same reason we rejected the "bypass roaster with a Java servlet" option earlier in the binary-transport discussion).