Serve a spec that knows where the instance is mounted
/api/docs on the production site handed out
https://<host>/api/apitokens for an app that lives at /shopdb. Copy that URL
and it 404s; Swagger's try-it fired at the same wrong address.
The mount is a DEPLOYMENT fact and the spec is a BUILD artifact.
gen_openapi.py writes servers: [{"url": "/"}] because at generation time nobody
knows where the app will be hung, and wsgi.py only moves MOUNT_PATH from
PATH_INFO into SCRIPT_NAME when the process starts. Served as written, the file
was right for a root install and wrong for every subpath one - which is how
production runs.
The endpoint now sets `servers` per request from request.script_root: empty at
root, /shopdb under the subpath method, and correct behind a proxy that sets the
prefix. The paths themselves stay mount-free, because the prefix belongs in the
server entry and duplicating it into each path would double it.
The file is about a megabyte, so it is parsed once and cached by mtime, and each
request gets a shallow copy with only `servers` replaced - two requests arriving
on different mounts cannot race on one dict.
A test pins both cases and the fact that paths stay unprefixed. Planting a wrong
server URL fails it.
This commit is contained in:
@@ -18,7 +18,7 @@ import os
|
||||
|
||||
from markupsafe import escape
|
||||
|
||||
from flask import Blueprint, Response, current_app, send_file, url_for
|
||||
from flask import Blueprint, Response, current_app, request, send_file, url_for
|
||||
|
||||
docs_bp = Blueprint('docs', __name__)
|
||||
|
||||
@@ -98,13 +98,50 @@ def docs_index():
|
||||
return Response(page, mimetype='text/html')
|
||||
|
||||
|
||||
_SPEC_CACHE = {}
|
||||
|
||||
|
||||
def _spec_with_server():
|
||||
"""The generated spec, with `servers` set to where this instance is mounted.
|
||||
|
||||
THE MOUNT IS A DEPLOYMENT FACT, NOT A BUILD FACT. gen_openapi.py writes
|
||||
servers: [{"url": "/"}] because it cannot know where the app will be hung,
|
||||
and wsgi.py moves MOUNT_PATH from PATH_INFO into SCRIPT_NAME at runtime. So
|
||||
an instance under /shopdb served a spec whose paths start at /api, and both
|
||||
renderers dutifully built https://host/api/apitokens - a URL that 404s.
|
||||
Anyone who copied it out of the docs page got a broken call, and Swagger's
|
||||
try-it fired at the wrong place.
|
||||
|
||||
request.script_root is the runtime answer: '' for a root install, '/shopdb'
|
||||
under the subpath method, and correct behind a proxy that sets the prefix.
|
||||
|
||||
The file is ~1 MB, so it is parsed once and cached by mtime; each request
|
||||
gets a shallow copy with only `servers` replaced, which keeps two requests
|
||||
on different mounts from racing on one dict.
|
||||
"""
|
||||
stamp = os.path.getmtime(_SPEC)
|
||||
if _SPEC_CACHE.get('stamp') != stamp:
|
||||
with open(_SPEC, 'r', encoding='utf-8') as handle:
|
||||
_SPEC_CACHE['spec'] = json.load(handle)
|
||||
_SPEC_CACHE['stamp'] = stamp
|
||||
|
||||
root = request.script_root or ''
|
||||
spec = dict(_SPEC_CACHE['spec'])
|
||||
spec['servers'] = [{
|
||||
'url': root or '/',
|
||||
'description': 'this instance',
|
||||
}]
|
||||
return spec
|
||||
|
||||
|
||||
@docs_bp.route('/openapi.json')
|
||||
def openapi_spec():
|
||||
"""The generated OpenAPI 3.1 spec (machine + LLM readable)."""
|
||||
if not os.path.isfile(_SPEC):
|
||||
return Response('{"error":"openapi.json not generated"}',
|
||||
status=404, mimetype='application/json')
|
||||
return send_file(_SPEC, mimetype='application/json')
|
||||
return Response(json.dumps(_spec_with_server()),
|
||||
mimetype='application/json')
|
||||
|
||||
|
||||
@docs_bp.route('/redoc.standalone.js')
|
||||
|
||||
Reference in New Issue
Block a user