Skip to content

fix(query): don't swallow cursor:eval errors as HTTP 200 'Invalid context-item' (regression in 0.9.7)#46

Open
joewiz wants to merge 1 commit into
eXist-db:developfrom
joewiz:fix/query-error-passthrough
Open

fix(query): don't swallow cursor:eval errors as HTTP 200 'Invalid context-item' (regression in 0.9.7)#46
joewiz wants to merge 1 commit into
eXist-db:developfrom
joewiz:fix/query-error-passthrough

Conversation

@joewiz

@joewiz joewiz commented Jun 6, 2026

Copy link
Copy Markdown
Member

[This PR was co-authored with Claude Code. -Joe]

Regression fix for 0.9.7. Spotted while running eXide develop HEAD's cypress suite against eXist 7 beta3 + existdb-openapi 0.9.7: two specs (query_error_display_spec, query_error_structured_spec) failed because the existdb-openapi /api/query endpoint was swallowing every error from cursor:eval as HTTP 200 { error: "Invalid context-item: …" }.

Root cause

The try/catch I added in #41 (context-item support) was too broad:

try {
    ...cursor:eval($expression, $mlp, $context-item)...
} catch query:context-path-not-found {
    map { "error": $err:description }
} catch * {
    map { "error": "Invalid context-item: " || $err:description }    -- ← catches EVERYTHING
}

A user's //does-not-exist:foo (an XPST0081 namespace error from cursor:eval) came back as:

HTTP 200
{ "error": "Invalid context-item: ... No namespace defined for prefix does-not-exist:foo" }

Two contracts broken at once:

  1. Wrong label. The context item was fine; the user's XQuery referenced an undeclared prefix. Calling it a context-item failure misdirects the user.
  2. Wrong status. eXide's #run handler (src/eXide.js:937) detects query errors via if (!response.ok). HTTP 200 → it thinks the query succeeded, tries to read data.cursor (undefined), and the error pill / panel never lights up. That's the symptom the eXide cypress specs were catching.

Fix

  • The parse-xml call moves into its own try/catch inside a new private helper query:resolve-context-item. That's the only place the "Invalid context-item:" prefix is now applied.
  • Resolution returns a {status, error} map for the two failure modes — 404 for a missing context-path, 400 for malformed context-item XML — and the caller wraps it in roaster:response(status, ...) so the HTTP code matches.
  • cursor:eval errors are no longer caught. They propagate to Roaster, which renders the standard XPathException → JSON shape ({code, description, line, column, module, value}) with an HTTP 500. That's exactly what eXide's editor.evalError handler already expects.
  • modules/api.json updated to declare 400, 404, and 500 responses so Roaster doesn't fall back to XML serialization on them.

After the fix

//does-not-exist:foo  →  HTTP 500
{
  "code": "err:XPST0081",
  "description": "It is a static error if a QName ... No namespace defined for prefix does-not-exist:foo ...",
  "line": 1, "column": 3, "module": null, "value": null
}

1 + "a"               →  HTTP 500 with err:XPTY0004 + full structured fields

<not-well-formed>     →  HTTP 400 { "error": "Invalid context-item: ..." }   (correctly labelled this time)

/db/nonexistent.xml   →  HTTP 404 { "error": "context-path not found: ..." }

1 to 3                →  HTTP 200 { "cursor": "...", "items": 3, ... }   (unchanged)

Verified

  • 33 query.cy.js tests pass on existdb-openapi (3 new regression tests covering HTTP-status-on-error and the no-rewrapping behaviour).
  • eXide develop HEAD's full cypress suite: 260 passing, 0 failing, 2 intentional skips — up from 258/262 before the fix. The two failing specs (query_error_display_spec, query_error_structured_spec) both pass now.
  • PMD clean.

Apologies

This was a regression I shipped in 0.9.7. Cutting a 0.9.8 once this lands.

…text-item'

Regression introduced in eXist-db#41 (context-item support): the new try/catch
caught EVERY error from query:execute and rewrapped it as

    HTTP 200 { error: "Invalid context-item: <original cause>" }

That broke two downstream contracts at once:

- The original cause was mislabeled. A simple XPST0081 namespace error
  now read "Invalid context-item: ... No namespace defined for prefix
  does-not-exist:foo", which is wrong — the context item was fine; the
  user's XQuery referenced an undeclared prefix.
- The HTTP status was 200. eXide's #run handler relies on
  \`if (!response.ok)\` to detect query errors and surface the pill +
  panel; with 200, it thinks the query succeeded and tries to read
  data.cursor (undefined). Caught by running eXide's query_error_display
  / query_error_structured cypress specs against eXist 7 beta3.

Fix:

- Move the parse-xml() call into its own try/catch inside a new
  query:resolve-context-item helper, so its failure is the only thing
  that gets the "Invalid context-item:" prefix.
- Resolution returns an { status, error } map for the two error modes
  (404 for missing context-path, 400 for malformed context-item-xml);
  the caller wraps that in roaster:response(status, ...) so the HTTP
  code matches.
- cursor:eval errors are no longer caught at all — they propagate to
  Roaster, which renders the standard XPathException → JSON shape
  ({ code, description, line, column, module, value }) and a 500
  status. That's what eXide's evalError handler already expects (see
  src/eXide.js:937).

api.json updated to declare the 400, 404, and 500 responses so
Roaster doesn't fall back to XML serialization on them.

Cypress: existing context-item error tests updated to assert the new
HTTP status codes; one new regression test pins the cursor:eval-error
path to "HTTP 5xx + structured XPathException + no Invalid-context-item
prefix".

Verified against eXide develop HEAD: all 36 cypress specs pass
(260/262, with 2 intentional skips), up from 258/262 before the fix.
The two specs that were failing — query_error_display and
query_error_structured — both pass now.
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