Restore the x-translator and termsOfService OpenAPI metadata, and serve the contact and license blocks - #300
Merged
Merged
Conversation
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
https://name-resolution-exp.apps.renci.org/openapi.json(v1.7.0) served aninfoblock of onlytitle,descriptionandversion. Thex-translatorblock SmartAPI registration keys off,termsOfService, and the top-leveltagsandservers(including thex-maturity/x-locationvalues ITRB sets) were all gone, with nothing changed in our source. This restores them, adds thecontactandlicenseblocks thatopenapi.ymlhas 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:A schema assigned straight to
app.openapi_schema— asapi/server.pydid — never carries a matching_openapi_routes_versionstamp, so the first request to/openapi.jsonregenerates the default document and overwrites the custom one.requirements.txtleftfastapiunpinned, 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.pyoverridesapp.openapiwith acustom_openapi()that caches intoapp.openapi_schemaon first use.api/apidocs.pydropsif app.openapi_schema: return app.openapi_schema()— it called adict, 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.fastapiis pinned to~=0.141.1, so an unrelated rebuild cannot move the stack again.openapi.ymlcarried looseemail/name/x-id/x-rolekeys ininfo— acontactblock that was never nested — socontacthas never been served, not even by the working v1.5.2 deployment. Now nested, matching what NodeNorm serves.construct_open_api_schema()copied every declaredinfokey exceptlicense, so the MIT block never reached the spec either. Now copied.openapi.ymlis parsed once behind anlru_cacheinstead of once per caller —get_app_info()was re-reading the file on every/statusrequest. Callers get adeepcopy, becauseconstruct_open_api_schema()rewrites theserversblock in place from the environment and would otherwise edit a document already served to somebody.CLAUDE.mdrecords both traps: install the custom document by overriding the method, and remember that a key declared inopenapi.ymlis only served if the builder's allowlist copies it.Testing
tests/test_openapi.py(new, no Solr required) goes throughTestClientdeliberately: asserting onconstruct_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.server.pychange makestest_openapi_json_carries_translator_metadatafail withKeyError: 'x-translator'— the test catches the regression that actually shipped.deepcopymakestest_building_the_schema_twice_leaves_the_first_alonefail.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_jsonin babel-validation should go green againstexponce this deploys.What it deliberately does not do
fastapi~=0.141.1resolves cleanly on Python 3.11 alongside the existing opentelemetry pins, so this is independent of Incremented Python versions to 3.12.4 #252 and can deploy without it.requirements.txtis still unpinned and can shift the same way; that needs a decision on approach rather than a quick edit, so it is Pin the rest of requirements.txt so a rebuild can't shift the stack silently #303.x-id/x-role. They are not OpenAPI 3.0.2 Contact fields, but NodeNorm serves them the same way, so this matches the existing convention rather than guessing at a better one — Confirm x-id and x-role belong inside info.contact against the SmartAPI validator #301 tracks confirming it against the SmartAPI validator.Before merging
Nothing blocking. The remaining loose ends are all filed:
x-idandx-rolebelong insideinfo.contactagainst the SmartAPI validator.httpxinTestClient, which every test uses.requirements.txt.NodeNormalization has the identical
app.openapi_schema = ...line but pinsfastapi~=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