KB-5A74

04 — API 500 Fix (matrix + pivot-query)

4 min read Revision 1
architecturerpapi-500matrixpivot-query2026-06-06

— 04 API 500 FIX — /api/registry/matrix and /api/registry/pivot-query

A. pivot-query 500 — jsonb filter Root cause (proven by the live 500 body): the handler builds the Directus filter group_spec _neq "{"groups":[]}" when type=cross. group_spec is a jsonb column (confirmed via information_schema). Directus 11.5 applying _neq with a JSON string literal against a jsonb field emits invalid SQL and returns 400 Bad Request; the handler catches and rethrows it as 500. Fix: remove the server-side jsonb _neq filter; fetch all is_active=true pivots, then keep the cross set in JS (group_spec.groups is a non-empty array). Proven live, non-disruptively, by running the fixed fetch logic via in-container node against Directus: http_status=200, active=37, cross_nonempty=12. Confirmed against Postgres jsonb: 37 active = 12 with non-empty groups + 25 empty. Patch: staged at patches/pivot-query.get.mjs (full drop-in replacement of the existing handler; node --check passes). It is an existing route, so it can be applied via the bind-mount express hotfix + restart.

B. matrix 500 — 897K-row Directus pull in a 512 MB container Root cause: the handler fetches /items/entity_labels?fields=entity_code,label_code&limit=-1 = 897,347 rows over Directus REST, plus /items/taxonomy, plus 152 per-collection limit=-1 fetches, plus universal_edges. Each $fetch is capped at 15s. Pulling 897K rows of JSON through Directus REST into a 512 MB Nitro process exceeds the timeout -> the awaited fetch throws -> unhandled -> Nitro 500 "Server Error" at ~28s wall time. entity_labels is the sole killer (edges=2,199; per-collection=152 managed/log collections of modest size). Fix substrate (created, birth-neutral): view v_rp_entity_label_facet_counts aggregates entity_labels to one row per entity_code with label_count and facet_count (facets via taxonomy.code -> facet_id). It collapses 897,347 raw rows to 226,052 aggregated count-rows and the full aggregate completes in well under a second. Sample read-back verified (e.g. DOT-086 label_count=9 facet_count=4). Fix patch: matrix.get.mjs replaces the entity_labels + taxonomy REST pull and the JS counting loop with a single rpQuery over v_rp_entity_label_facet_counts; facetsCovered now reads facet_count. Everything else (meta_catalog, per-collection entity fetch, edges, scoring) is unchanged. The import gains m as rpQuery (confirmed exported by this build's nitro.mjs). node --check passes. Staged at patches/matrix.get.mjs. Existing route -> bind-mount express hotfix eligible.

Both fixes follow the house rule: pass-through to PG/current contract, no Nuxt count math added; matrix now mirrors the pg-backed sibling endpoints instead of being a REST outlier.

Rollback: the DB substrate view drops cleanly via 99_rollback.sql. The handler swaps back from the operator backup taken by rp-apply-hotfix.sh (auto-rollback on smoke failure). The 28s slow route species-matrix (200 but ~18–22s) is the same entity-bloat pattern and is flagged in the smoke watch lane as the next candidate for the same pg-aggregate treatment.

Live deploy of these handler swaps requires a restart of incomex-nuxt (brief all-users SSR blip). That restart is staged in the operator packet, not executed here.

Back to Knowledge Hub knowledge/dev/reports/architecture/rp-production-api-operator-fix-ui-truth-smoke-2026-06-06/04-api-500-fix.md