Skip to content

Restore the x-translator and termsOfService OpenAPI metadata, and serve the contact and license blocks - #300

Merged
gaurav merged 8 commits into
mainfrom
fix-issue-294
Sep 1, 2026
Merged

Restore the x-translator and termsOfService OpenAPI metadata, and serve the contact and license blocks#300
gaurav merged 8 commits into
mainfrom
fix-issue-294

Conversation

@gaurav

@gaurav gaurav commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

https://name-resolution-exp.apps.renci.org/openapi.json (v1.7.0) served an info block of only title, description and version. The x-translator block SmartAPI registration keys off, termsOfService, and the top-level tags and servers (including the x-maturity / x-location values ITRB sets) were all gone, with nothing changed in our source. This restores them, adds the contact and license blocks that openapi.yml has declared all along without ever serving, and puts the served document under test.

Fixes #294.

Why it broke

Since FastAPI 0.137.0, FastAPI.openapi() invalidates its own cache:

routes_version = self.router._get_routes_version()
if not self.openapi_schema or self._openapi_routes_version != routes_version:
    self.openapi_schema = get_openapi(...)   # overwrites ours

A schema assigned straight to app.openapi_schema — as api/server.py did — never carries a matching _openapi_routes_version stamp, so the first request to /openapi.json regenerates the default document and overwrites the custom one. requirements.txt left fastapi unpinned, so the image built for v1.7.0 picked this up silently. It fails open: the service starts, answers queries, and serves a valid-but-wrong spec. The attribute is no longer a supported extension point at any version; only the method is.

What's here

  • api/server.py overrides app.openapi with a custom_openapi() that caches into app.openapi_schema on first use.
  • api/apidocs.py drops if app.openapi_schema: return app.openapi_schema() — it called a dict, and the override would have made it reachable on the second request. Caching now lives in the wrapper, which keeps the builder pure and callable from tests.
  • fastapi is pinned to ~=0.141.1, so an unrelated rebuild cannot move the stack again.
  • openapi.yml carried loose email / name / x-id / x-role keys in info — a contact block that was never nested — so contact has never been served, not even by the working v1.5.2 deployment. Now nested, matching what NodeNorm serves.
  • construct_open_api_schema() copied every declared info key except license, so the MIT block never reached the spec either. Now copied.
  • openapi.yml is parsed once behind an lru_cache instead of once per caller — get_app_info() was re-reading the file on every /status request. Callers get a deepcopy, because construct_open_api_schema() rewrites the servers block in place from the environment and would otherwise edit a document already served to somebody.
  • CLAUDE.md records both traps: install the custom document by overriding the method, and remember that a key declared in openapi.yml is only served if the builder's allowlist copies it.

Testing

tests/test_openapi.py (new, no Solr required) goes through TestClient deliberately: asserting on construct_open_api_schema(app) directly passes throughout the bug, because the builder kept returning the right document while FastAPI served its own. The environment-override tests call the builder instead, since the served schema is cached after the first request and cannot observe an environment changed later.

  • 53/53 pass under both fastapi 0.141.1 and the older 0.115.2, so the fix does not depend on the pin landing.
  • Reverting only the server.py change makes test_openapi_json_carries_translator_metadata fail with KeyError: 'x-translator' — the test catches the regression that actually shipped.
  • Reverting only the deepcopy makes test_building_the_schema_twice_leaves_the_first_alone fail.
  • With OTEL_ENABLED=true, the FastAPI and httpx instrumentation still works under the starlette 1.6.0 that fastapi 0.141.1 pulls in — that was the one deployment risk in the pin.

tests/nameres/test_nameres_api.py::test_openapi_json in babel-validation should go green against exp once this deploys.

What it deliberately does not do

Before merging

Nothing blocking. The remaining loose ends are all filed:

NodeNormalization has the identical app.openapi_schema = ... line but pins fastapi~=0.108.0, so it is not affected yet; it will break the same way whenever that pin moves past 0.137.0. Filed separately there.

🤖 Generated with Claude Code

gaurav and others added 6 commits September 1, 2026 00:17
info carried loose email, name, x-id and x-role keys -- a contact block that
was never nested -- so construct_open_api_schema() had no 'contact' to copy and
the served spec has never had one. Nest them, matching the block NodeNorm
serves at https://nodenormalization-sri.renci.org/openapi.json.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Since FastAPI 0.137.0, openapi() rebuilds the schema whenever the app's
recorded routes version doesn't match the router's current one. A schema
assigned straight to app.openapi_schema never carries that stamp, so the first
request to /openapi.json overwrote it with FastAPI's default document: the
x-translator block SmartAPI registration keys off, termsOfService, tags and
servers all vanished from the v1.7.0 spec without anything failing.

Override the method instead, caching into app.openapi_schema on first use. That
also makes construct_open_api_schema()'s "if app.openapi_schema: return
app.openapi_schema()" reachable on the second request -- where it would call a
dict -- so drop it; the caching now lives in the wrapper, which leaves the
builder pure and callable from tests.

Fixes #294.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
openapi.yml has declared an MIT license block all along, but
construct_open_api_schema() copied everything except that, so the served spec
never mentioned the license -- the same omission as contact, found while
fixing #294.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was no test covering the served spec, which is why a whole-block
regression shipped in v1.7.0. These go through TestClient on purpose:
asserting on construct_open_api_schema() directly passes throughout the bug,
because the builder kept returning the right document while FastAPI served its
own instead.

The environment-override test calls the builder rather than the route, since
the served schema is cached after the first request and cannot see an
environment changed later.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fastapi was unpinned, so the image built for v1.7.0 picked up 0.137.0 and lost
the custom OpenAPI metadata silently. Pin it so an unrelated rebuild cannot
move the stack underneath us again.

Verified on Python 3.11 against the existing opentelemetry pins: the suite
passes, and with OTEL_ENABLED=true the FastAPI and httpx instrumentation still
works under the starlette 1.6.0 that 0.141.1 pulls in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The failure mode is silent -- the service starts, answers queries and serves a
valid-but-wrong spec -- so note both halves: install the custom document by
overriding the method, and test it through the route.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gaurav and others added 2 commits September 1, 2026 00:51
construct_open_api_schema() works off an allowlist, so contact and license were
declared in openapi.yml for years without ever reaching the served spec. The
silence is the trap worth writing down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
get_app_info() and construct_open_api_schema() each opened and parsed the file
separately, the former on every /status request. Read it behind an lru_cache
and hand out a deepcopy: construct_open_api_schema() rewrites the servers block
in place from the environment, so sharing the cached parse would let one build
reach back and edit a document already served to somebody. The new test fails
if the copy is dropped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gaurav gaurav changed the title Restore the x-translator, contact, license and termsOfService metadata in the served OpenAPI document Restore the x-translator and termsOfService OpenAPI metadata, and serve the contact and license blocks Sep 1, 2026
@gaurav
gaurav merged commit b7b0979 into main Sep 1, 2026
1 check passed
@gaurav
gaurav deleted the fix-issue-294 branch September 1, 2026 05:35
@github-project-automation github-project-automation Bot moved this from Backlog to Done in NameRes sprints Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Restore the x-translator, contact and termsOfService OpenAPI metadata dropped by FastAPI 0.137.0

1 participant