Skip to content

Ribbon endpoint 500s for goslim_agr — KeyError on missing annotation_class (MF root GO:0003674 relabelled in GOlr); affects v0.3.x and v0.4.0 #165

Description

@kltm

Summary

GET /api/ontology/ribbon/ returns 500 for subset=goslim_agr (the
default Alliance / MOD gene-ribbon use). The failure is deterministic — it does
not depend on the subject — so the ribbon widget is effectively down for every
caller.

This is triggered by a GOlr ontology-data change, not by an API version. It
reproduces on both the current v0.3.x production build and v0.4.0,
which share the identical GOlr-based subset code — so rolling the API back does
not mitigate it. (The separate v0.4.0 GO-CAM index-file 500s were a
different fault, already fixed by #158/#161 — please don't conflate the two.)

Index-backed endpoints (/api/models/go, /api/gp/{id}/models, slimmer,
ontology/term, subgraph, bioentity) are all healthy; this is isolated to
the ribbon / subset path.

Live reproduction (for others to test)

1. Public API endpoint — currently 500ing on production:

https://api.geneontology.org/api/ontology/ribbon/?subset=goslim_agr&subject=MGI:98214&exclude_IBA=false&exclude_PB=false&cross_aspect=false
curl -sS -o /dev/null -w "HTTP %{http_code}\n" \
  "https://api.geneontology.org/api/ontology/ribbon/?subset=goslim_agr&subject=MGI:98214&exclude_IBA=false&exclude_PB=false&cross_aspect=false"
# -> HTTP 500   {"message":"An unexpected error occurred. Please try again later."}

2. Durable root-cause check at the GOlr layer (version-independent — this is
the underlying data condition, and will keep reproducing even if the API is
redeployed/rolled back):

curl -sG 'https://golr.geneontology.org/solr/select' \
  --data-urlencode 'q=annotation_class_label:"molecular_function"' \
  --data-urlencode 'fq=document_category:"ontology_class"' \
  --data-urlencode 'rows=0' --data-urlencode 'wt=json'
# -> "numFound":0   (biological_process and cellular_component each return 1)

Root cause

Subset resolution is served by GOlr in
app/utils/ontology_utils.py::get_ontology_subsets_by_id (present since the
Blazegraph-removal work; in v0.3.9, v0.3.11, and v0.4.0 alike). It:

  1. Queries GOlr for the slim terms and groups them by the source field, keying
    each category by the aspect string — molecular_function,
    biological_process, cellular_component (ontology_utils.py:102).
  2. Runs a second GOlr query and back-fills each category's root
    annotation_class only when an ontology doc has
    annotation_class_label == <the source string> (ontology_utils.py:111-118).

That self-join now breaks for molecular_function, because the MF root term
GO:0003674 has been relabelled in the current GOlr index:

# GOlr ontology_class doc for GO:0003674
annotation_class:        GO:0003674
annotation_class_label:  "gene product or complex activity"   # <- no longer "molecular_function"
source:                  molecular_function

# numFound for annotation_class_label:<aspect> (document_category:ontology_class)
molecular_function  -> 0      <-- no match, annotation_class never set
biological_process  -> 1      (GO:0008150)
cellular_component  -> 1      (GO:0005575)

So tr["molecular_function"] is produced without an annotation_class key,
then consumed unguarded:

  • ontology_utils.py:132 — in the goslim_agr reorder: if category["annotation_class"] == cat:KeyError
  • app/routers/ribbon.py:117category["id"] = category["annotation_class"] (same bare index on the non-reordered path; line 114 in v0.3.9)

→ unhandled KeyError → 500. Same data-drift class as #159 (GOlr
annotation_class / annotation_class_label drift).

Suggested fix

The aspect→root mapping is known unambiguously and shouldn't depend on a label
round-trip:

  • Resolve each category's root annotation_class from the aspect / GO ID
    rather than by matching annotation_class_label. ribbon.py already defines
    aspect_map = {"P": "GO:0008150", "F": "GO:0003674", "C": "GO:0005575"}; the
    same source→GO-ID mapping can populate annotation_class directly in
    get_ontology_subsets_by_id.
  • Defensively guard the unguarded reads (ontology_utils.py:132,
    ribbon.py:114-118) — .get(...) / skip incomplete categories — so a single
    ontology label change can never 500 the entire widget again.
  • Add a ribbon regression test covering subset=goslim_agr asserting all three
    aspects resolve.

Separately: if the GO:0003674 relabel to "gene product or complex activity"
in the GOlr index was unintended, fixing the ontology data is the other lever
— but the code should be robust to it regardless.

Related


— Filed by Claude Code agent on behalf of @kltm.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions