From 8e5548dd3b86c34b3b79385628c96a2a21fb4b6f Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Mon, 12 Oct 2026 01:15:15 -0400 Subject: [PATCH] Serve the OpenAPI document at /openapi.json /openapi.json redirected to /webapp/openapi.json. It now serves the document itself, which is where NodeNorm Redis (FastAPI's default) and most Translator services serve theirs, and the Swagger UI at / loads it from there. /webapp/openapi.json keeps working through the existing /webapp asset handler, because Babel Validation's targets.ini and the Translator diagram's nodenorm-es component are configured with that address. Removing it would break them and gain nothing; they can switch to the default once this is deployed. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++-- src/nodenorm/handlers/__init__.py | 6 ++++-- src/nodenorm/webapp/index.html | 2 +- tests/test_application.py | 2 +- tests/test_openapi.py | 16 +++++++++------- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 66a363d..cd1eb6f 100644 --- a/README.md +++ b/README.md @@ -193,8 +193,8 @@ API version 1.2.0 corrects conflation and semantic-type responses and rejects in flags. It retains the multi-Babel selectors, response headers and status health checks added in 1.1.0. The API is described by `src/nodenorm/webapp/openapi.json`, which is written by hand and served -verbatim at `/webapp/openapi.json` (with `/openapi.json` redirecting there); the Swagger UI at `/` -renders it. `/status` reports its `info.version` as the API version, so bump that when the API +verbatim at `/openapi.json`, as NodeNorm Redis and most Translator services serve theirs, and also +at the older `/webapp/openapi.json`; the Swagger UI at `/` renders it. `/status` reports its `info.version` as the API version, so bump that when the API changes. `tests/test_openapi.py` checks that the document validates and that its paths, methods, query parameters, POST body fields and the `SetIDResponse` schema match the handlers, so a handler change that isn't reflected in the JSON fails the test suite. The parameter and body checks find diff --git a/src/nodenorm/handlers/__init__.py b/src/nodenorm/handlers/__init__.py index 22a5849..18b04bf 100644 --- a/src/nodenorm/handlers/__init__.py +++ b/src/nodenorm/handlers/__init__.py @@ -47,8 +47,10 @@ def build_handlers() -> dict[str, tuple[str, Callable]]: favicon_handler = (r"/favicon\.ico", tornado.web.RedirectHandler, {"url": "/webapp/swaggerui/favicon-32x32.png"}) handler_collection.append(favicon_handler) - # NodeNorm Redis (FastAPI) serves its OpenAPI document at the root; keep that address working - openapi_handler = (r"/openapi\.json", tornado.web.RedirectHandler, {"url": "/webapp/openapi.json"}) + # The OpenAPI document is served at the root, where NodeNorm Redis (FastAPI) and most Translator + # services serve theirs. The asset handler above also serves it at /webapp/openapi.json, the + # address Babel Validation's targets and the Translator diagram are configured with. + openapi_handler = (r"/(openapi\.json)", tornado.web.StaticFileHandler, {"path": str(webapp_directory)}) handler_collection.append(openapi_handler) handlers = {handler[0]: handler for handler in handler_collection} diff --git a/src/nodenorm/webapp/index.html b/src/nodenorm/webapp/index.html index 080984a..ab7a197 100644 --- a/src/nodenorm/webapp/index.html +++ b/src/nodenorm/webapp/index.html @@ -22,7 +22,7 @@ // Initialize Swagger UI window.onload = function() { const ui = SwaggerUIBundle({ - url: "/webapp/openapi.json", + url: "/openapi.json", dom_id: '#swagger-ui', presets: [ SwaggerUIBundle.presets.apis, diff --git a/tests/test_application.py b/tests/test_application.py index 8e967c5..d62cff2 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -140,7 +140,7 @@ def test_root_serves_swagger_ui(self): assert response.code == 200 assert response.headers["Content-Type"].startswith("text/html") assert b"SwaggerUIBundle" in response.body - assert b"/webapp/openapi.json" in response.body + assert b'url: "/openapi.json"' in response.body def test_webapp_serves_openapi_spec(self): response = self.fetch("/webapp/openapi.json") diff --git a/tests/test_openapi.py b/tests/test_openapi.py index 131a2fa..566312a 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -171,17 +171,19 @@ def test_setid_response_schema_matches_the_dataclass(): assert documented == {field.name for field in dataclasses.fields(SetIDResponse)} -class TestOpenAPIRedirect(AsyncHTTPTestCase): - """/openapi.json is where NodeNorm Redis, SmartAPI and Babel Validation look for the document.""" +class TestOpenAPIAddresses(AsyncHTTPTestCase): + """/openapi.json is where NodeNorm Redis serves its document; /webapp/openapi.json is the older address.""" def get_app(self) -> tornado.web.Application: return tornado.web.Application(list(build_handlers().values())) - def test_openapi_json_redirects_to_the_document(self): - response = self.fetch("/openapi.json", follow_redirects=False) + def test_the_document_is_served_at_both_addresses(self): + for path in ("/openapi.json", "/webapp/openapi.json"): + response = self.fetch(path, follow_redirects=False) - assert response.code == 301 - assert response.headers["Location"] == "/webapp/openapi.json" + assert response.code == 200, path + assert response.headers["Content-Type"].startswith("application/json"), path + assert json.loads(response.body) == SPEC, path def test_served_document_is_valid_openapi(self): # test_document_is_valid_openapi() reads the file; this checks what the static route serves. @@ -191,7 +193,7 @@ def test_served_document_is_valid_openapi(self): assert response.headers["Content-Type"].startswith("application/json") validate(json.loads(response.body)) - def test_only_the_exact_path_redirects(self): + def test_only_the_exact_path_matches(self): assert self.fetch("/openapiXjson", follow_redirects=False).code == 404 def test_api_paths_are_matched_exactly(self):