Remove the Swagger try-it page

It shipped 1.77 MB of vendored assets to every site - swagger-ui-bundle.min.js
alone is larger than Redoc - in order to provide an Execute button that only the
development config ever enabled. Production config had it off, testing had it
off hard, and when off the routes were never bound at all. The staticdocs
directory drops from 2.9 MB to 1.1 MB, and there is one fewer vendored
dependency to track for licences and advisories.

What replaces it: an agent has the MCP server's shopdb_call, and a human has
Redoc for reading plus the 324 harvested examples in the spec for a curl that is
mostly copy-paste. The read-only page was always the one that mattered.

The tests this leaves behind are the interesting part. That file was named for
try-it but half of it proved things about the page that stays, above all that
nothing it loads comes from the internet, which is what a CDN reference costs on
an air-gapped box. Those are kept and the file is renamed for what it covers.

Pointing the air-gap test at redoc.standalone.js immediately failed: the bundle
still carried a trailing sourceMappingURL for a .map that was never vendored,
so every devtools open was a guaranteed 404 with no internet to fall back on.
That was visible in a browser console and nobody had chased it, because the test
had only ever been parametrised over the two Swagger files. The pragma is gone
and the assertion now checks the TRAILING pragma specifically: Redoc's own code
contains that string three more times, in a data: URI it assembles and in a
worker blob, and a blunt "not in body" assertion fails on correct code.

A first attempt to strip it with a line-anchored pattern removed 507 KB, because
the bundle is minified onto long lines. Hence the anchored, byte-exact removal
and the assertion that can tell a directive from source.
This commit is contained in:
cproudlock
2026-09-20 14:57:37 -04:00
parent a97346ed81
commit 581baafee0
9 changed files with 181 additions and 661 deletions

View File

@@ -0,0 +1,179 @@
"""The API documentation page at /api/docs.
What is being proved is the thing that would hurt on the production box:
nothing the page loads comes from the internet. Production is air-gapped, so a
CDN reference, a webfont, or a source map pointing at a public host is not a
slow page - it is a page that never renders and a request that hangs.
The Swagger "try it" page this file used to cover was removed: it shipped
1.77 MB of vendored assets to every site in order to provide an Execute button
that only development configs ever enabled, and an agent now has the MCP
server's shopdb_call while a human has the spec's own worked examples.
These build their own Flask app around the docs blueprint rather than using the
session app from conftest. The blueprint touches no database, and a second
create_app() would rebind the SQLAlchemy extension and break whatever module
runs next (see the note on the gated_app fixture in test_public_read_surface).
"""
import os
import re
import pytest
from flask import Flask
from shopdb.core.api.docs import docs_bp
_STATICDOCS = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
'shopdb', 'core', 'api', 'staticdocs')
# The asset a browser actually fetches from this directory. It carried no
# air-gap test at all while this file parametrised over the two Swagger files -
# and the one real leak ever found here was Redoc reaching for cdn.redoc.ly.
_VENDORED = ('redoc.standalone.js',)
def _docs_app():
"""A bare app carrying only the docs blueprint, mounted where the real app
mounts it (register_blueprints uses /api/<name>)."""
application = Flask(__name__)
application.register_blueprint(docs_bp, url_prefix='/api/docs')
return application
@pytest.fixture
def client():
return _docs_app().test_client()
@pytest.mark.parametrize('filename', _VENDORED)
def test_a_vendored_asset_fetches_nothing_from_the_internet(filename):
"""The assets themselves, not just the page.
Scoped to what a BROWSER would actually request. The Redoc bundle
carries around a hundred http(s) strings that are prose - RFC and
json-schema citations, licence URLs, the example.com host in the spec's own
documentation - and none of them is fetched, so a blanket "no http in this
file" assertion would be a lie that fails on the next upstream bump. What
would really produce a request is a CDN or webfont reference, a stylesheet
url() pointing off-box, or a source map: hence these.
"""
body = open(os.path.join(_STATICDOCS, filename),
encoding='utf-8', errors='replace').read()
# NOT a host allowlist. The first version of this named five CDNs, so an
# upstream bump to esm.sh or cdn.skypack.dev walked straight through it, and
# the one real leak in this directory was cdn.redoc.ly - which was on no
# list. Match the SHAPES a browser fetches instead, whatever the host:
# a stylesheet import, a css url(), a script/link/img source.
fetching = [
(r'@import\s+(?:url\()?[\'"]?(?:https?:)?//', 'an @import from another origin'),
(r'url\(\s*[\'"]?(?:https?:)?//', 'a css url() on another origin'),
(r'(?:src|href)\s*=\s*[\'"](?:https?:)?//', 'an element loaded from another origin'),
(r'@font-face', 'a webfont, which is a fetch unless it is a data: URI'),
]
for pattern, what in fetching:
for match in re.finditer(pattern, body):
window = body[max(0, match.start() - 60):match.start() + 120]
# data: URIs are inline, not fetched, and this CSS is full of them.
if 'data:' in window[:len(window) // 2 + 40]:
continue
raise AssertionError(
'%s carries %s: ...%s...' % (filename, what, window.strip()[:140]))
# A map reference is a request too. The upstream .map files are not
# vendored (they are tens of megabytes and only devtools reads them), so
# any surviving pointer is a guaranteed 404 on a box that cannot reach the
# internet to try elsewhere.
# The TRAILING pragma only. Redoc's own code contains the same string three
# times - a data: URI it assembles and a worker blob - and those are source,
# not a directive. A bare "not in body" assertion fails on correct code, and
# trying to strip every occurrence destroyed half the bundle: the file is
# minified onto long lines, so a line-anchored pattern eats the rest of it.
trailing = body.rstrip().rsplit('\n', 1)[-1]
assert not trailing.startswith('//# sourceMappingURL'), (
'%s ends with a source-map pragma, and the .map is not vendored: every '
'devtools open is then a 404 on a box with no internet' % filename)
# Stylesheet resources: a vendored stylesheet must inline its images, and
# anything else would be an off-box fetch on first paint. CSS only - in the
# minified bundle "url(" is a function call, not a resource.
if filename.endswith('.css'):
for match in re.findall(r'url\(\s*["\']?([^"\')]+)', body):
assert match.startswith('data:'), (
'%s loads %s rather than an inline data: URI'
% (filename, match))
# ---------------------------------------------------------------------------
# The spec has to say where this instance actually lives
# ---------------------------------------------------------------------------
def test_the_spec_declares_the_mount_it_is_served_from(client):
"""Production runs under /shopdb, and the generated file cannot know that.
gen_openapi.py writes servers: [{"url": "/"}] - correct for a root install
and wrong for every subpath one - and wsgi.py only moves MOUNT_PATH into
SCRIPT_NAME at runtime. Served as written, both renderers built
https://host/api/... for an app mounted at /shopdb: a URL an operator copied
out of the docs page and got a 404 from, and the address Swagger's try-it
fired at.
"""
import json as _json
at_root = _json.loads(
client.get('/api/docs/openapi.json').get_data(as_text=True))
assert at_root['servers'] == [{'url': '/', 'description': 'this instance'}]
mounted = _json.loads(client.get(
'/api/docs/openapi.json',
environ_overrides={'SCRIPT_NAME': '/shopdb'}).get_data(as_text=True))
assert mounted['servers'] == [
{'url': '/shopdb', 'description': 'this instance'}]
# The paths themselves stay mount-free: the server prefix is what varies,
# and duplicating it into every path would double it under a subpath.
assert '/api/apitokens' in mounted['paths']
assert '/shopdb/api/apitokens' not in mounted['paths']
# ---------------------------------------------------------------------------
# The read-only page follows the OS theme
# ---------------------------------------------------------------------------
def test_the_redoc_page_follows_the_system_theme(client):
"""The app has a dark theme; /api/docs was the one page that stayed white.
Redoc ignores prefers-color-scheme and the <redoc spec-url> element takes no
theme, so the page has to go through Redoc.init - which is also why this
test pins the mechanism and not just the CSS. A future edit that puts the
element back would lose dark mode without changing a single colour.
"""
page = client.get('/api/docs/').get_data(as_text=True)
assert 'Redoc.init' in page
assert '<redoc spec-url' not in page, 'the element takes no theme'
# The page now follows the app's own stored choice first and only falls
# back to the OS, so assert the whole chain rather than the media query
# alone: the storage key, the attribute both the CSS and the Redoc theme
# key off, and the fallback.
assert '"shopdb-theme"' in page
assert 'data-theme' in page
assert 'prefers-color-scheme: dark' in page
assert 'var DARK=' in page and 'var LIGHT=' in page
# Re-render on a live scheme change, with the fallback for the older
# MediaQueryList that a kiosk browser may still have.
assert 'addEventListener("change",draw)' in page
assert 'q.addListener(draw)' in page
def test_the_docs_page_still_loads_nothing_from_another_origin(client):
"""Theming is the kind of change that quietly introduces a font or a CDN."""
import re as _re
page = client.get('/api/docs/').get_data(as_text=True)
assert not _re.findall(r'(?:src|href)\s*=\s*[\'"](?:https?:)?//', page)

View File

@@ -1,289 +0,0 @@
"""The Swagger UI "try it" page at /api/docs/try.
Two things are being proved, and they are the two that would hurt on the
production box:
1. It is OFF unless a site turns it on, and off means the URL does not exist.
The page fires real requests at this server with the browser's own session,
so a disabled deployment must not serve it, advertise it, or confirm it is
a thing by answering anything other than 404.
2. Nothing it loads comes from the internet. Production is air-gapped: a CDN
reference, a webfont, or a source map pointing at a public host is not a
slow page, it is a page that never renders and a request that hangs.
These build their own Flask app around the docs blueprint rather than using the
session app from conftest. The blueprint touches no database, and a second
create_app() would rebind the SQLAlchemy extension and break whatever module
runs next (see the note on the gated_app fixture in test_public_read_surface).
It also lets one test see the flag on and the next see it off, which is the
whole behaviour under test.
"""
import os
import re
import pytest
from flask import Flask
from shopdb.core.api.docs import docs_bp
# Everything the try-it page is allowed to load, and the files behind them.
_CSS_PATH = '/api/docs/swagger-ui.css'
_JS_PATH = '/api/docs/swagger-ui-bundle.min.js'
_STATICDOCS = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
'shopdb', 'core', 'api', 'staticdocs')
_VENDORED = ('swagger-ui.css', 'swagger-ui-bundle.min.js')
def _docs_app(tryit_enabled):
"""A bare app carrying only the docs blueprint, mounted where the real app
mounts it (register_blueprints uses /api/<name>)."""
application = Flask(__name__)
application.config['API_DOCS_TRYIT_ENABLED'] = tryit_enabled
application.register_blueprint(docs_bp, url_prefix='/api/docs')
return application
@pytest.fixture
def enabled_client():
return _docs_app(True).test_client()
@pytest.fixture
def disabled_client():
return _docs_app(False).test_client()
def test_enabled_page_renders_swagger_over_the_published_spec(enabled_client):
response = enabled_client.get('/api/docs/try')
assert response.status_code == 200
page = response.get_data(as_text=True)
assert 'SwaggerUIBundle(' in page
assert 'id="swagger-ui"' in page
# One spec, two renderers: the try-it page must read the same document the
# Redoc page serves, not a second copy that could drift from it.
assert '/api/docs/openapi.json' in page
def test_the_page_references_no_other_origin_at_all(enabled_client):
"""Absolute AND protocol-relative.
The first version of this scanned for https?://, so
<script src="//cdn.jsdelivr.net/npm/x.js"> - the form a copy-paste from any
CDN's own docs produces - matched nothing and passed.
"""
page = enabled_client.get('/api/docs/try').get_data(as_text=True)
offenders = re.findall(r'(?:src|href)\s*=\s*[\'"]((?:https?:)?//[^\'"]+)', page)
assert not offenders, 'the try-it page loads from another origin: %s' % offenders
def test_the_redoc_page_survives_try_it_being_unmounted(app):
"""The banner linking to the try-it page is built with url_for, and url_for
raises BuildError when those routes are not registered - which is every
production box. An unconditional link therefore 500s /api/docs/ in exactly
the deployment nobody tests locally.
"""
# The testing config is the unmounted case, and it is the one available
# here: ProductionConfig refuses to build without a real SECRET_KEY, which
# is correct of it and makes it useless as a fixture.
page = app.test_client().get('/api/docs/')
assert page.status_code == 200, page.get_data(as_text=True)[:200]
body = page.get_data(as_text=True)
assert 'redoc' in body.lower()
assert '/api/docs/try' not in body
def test_enabled_page_loads_only_the_vendored_assets(enabled_client):
page = enabled_client.get('/api/docs/try').get_data(as_text=True)
assert 'href="%s"' % _CSS_PATH in page
assert 'src="%s"' % _JS_PATH in page
for path, expected_type in ((_CSS_PATH, 'text/css'),
(_JS_PATH, 'application/javascript')):
asset = enabled_client.get(path)
assert asset.status_code == 200, path
assert expected_type in asset.headers['Content-Type']
assert asset.headers.get('Content-Length', '0') != '0'
def test_the_page_names_no_external_host(enabled_client):
"""Every URL on the page has to be same-origin. On an air-gapped box an
absolute http(s) URL is a request that hangs until it times out."""
page = enabled_client.get('/api/docs/try').get_data(as_text=True)
external = re.findall(r'https?://[^\s"\'<>)]+', page)
assert not external, 'try-it page references external hosts: %s' % external
def test_the_spec_validator_is_turned_off(enabled_client):
"""Swagger UI's one outbound default: unset, it posts the spec to a public
validator to draw a badge. That is a hung request here, and it would be an
upload of the internal API surface anywhere it worked."""
page = enabled_client.get('/api/docs/try').get_data(as_text=True)
assert re.search(r'validatorUrl:\s*null', page), (
'validatorUrl is not disabled; Swagger UI will call the public '
'validator')
def test_disabled_deployment_has_no_such_route(disabled_client):
"""404, not a rendered page explaining that it is disabled. The route is
never bound, so the URL confirms nothing."""
for path in ('/api/docs/try', '/api/docs/try/', _CSS_PATH, _JS_PATH):
assert disabled_client.get(path).status_code == 404, path
def test_the_redoc_page_advertises_try_it_only_when_it_exists(
enabled_client, disabled_client):
enabled_page = enabled_client.get('/api/docs/').get_data(as_text=True)
assert '/api/docs/try' in enabled_page
disabled_page = disabled_client.get('/api/docs/').get_data(as_text=True)
assert '/api/docs/try' not in disabled_page
# The word too, not just the href: a note about a view that 404s is worse
# than no note.
assert 'try-it' not in disabled_page
def test_every_url_follows_the_mount_path(enabled_client):
"""Prod runs under /shopdb. url_for has to carry the prefix onto the spec
and both assets, or the page loads nothing and can validate nothing."""
page = enabled_client.get(
'/api/docs/try',
environ_overrides={'SCRIPT_NAME': '/shopdb'}).get_data(as_text=True)
assert '/shopdb/api/docs/openapi.json' in page
assert '/shopdb%s' % _CSS_PATH in page
assert '/shopdb%s' % _JS_PATH in page
# No unprefixed twin left behind by a hardcoded path.
assert 'href="%s"' % _CSS_PATH not in page
assert 'src="%s"' % _JS_PATH not in page
redoc_page = enabled_client.get(
'/api/docs/',
environ_overrides={'SCRIPT_NAME': '/shopdb'}).get_data(as_text=True)
assert '/shopdb/api/docs/try' in redoc_page
@pytest.mark.parametrize('filename', _VENDORED)
def test_a_vendored_asset_fetches_nothing_from_the_internet(filename):
"""The assets themselves, not just the page.
Scoped to what a BROWSER would actually request. The swagger-ui bundle
carries around a hundred http(s) strings that are prose - RFC and
json-schema citations, licence URLs, the example.com host in the spec's own
documentation - and none of them is fetched, so a blanket "no http in this
file" assertion would be a lie that fails on the next upstream bump. What
would really produce a request is a CDN or webfont reference, a stylesheet
url() pointing off-box, or a source map: hence these.
"""
body = open(os.path.join(_STATICDOCS, filename),
encoding='utf-8', errors='replace').read()
# NOT a host allowlist. The first version of this named five CDNs, so an
# upstream bump to esm.sh or cdn.skypack.dev walked straight through it, and
# the one real leak in this directory was cdn.redoc.ly - which was on no
# list. Match the SHAPES a browser fetches instead, whatever the host:
# a stylesheet import, a css url(), a script/link/img source.
fetching = [
(r'@import\s+(?:url\()?[\'"]?(?:https?:)?//', 'an @import from another origin'),
(r'url\(\s*[\'"]?(?:https?:)?//', 'a css url() on another origin'),
(r'(?:src|href)\s*=\s*[\'"](?:https?:)?//', 'an element loaded from another origin'),
(r'@font-face', 'a webfont, which is a fetch unless it is a data: URI'),
]
for pattern, what in fetching:
for match in re.finditer(pattern, body):
window = body[max(0, match.start() - 60):match.start() + 120]
# data: URIs are inline, not fetched, and this CSS is full of them.
if 'data:' in window[:len(window) // 2 + 40]:
continue
raise AssertionError(
'%s carries %s: ...%s...' % (filename, what, window.strip()[:140]))
# A map reference is a request too. The upstream .map files are not
# vendored (they are tens of megabytes and only devtools reads them), so
# any surviving pointer is a guaranteed 404 on a box that cannot reach the
# internet to try elsewhere.
assert 'sourceMappingURL' not in body, (
'%s still points at a source map that is not vendored' % filename)
# Stylesheet resources: swagger-ui inlines its images as data: URIs, and
# anything else would be an off-box fetch on first paint. CSS only - in the
# minified bundle "url(" is a function call, not a resource.
if filename.endswith('.css'):
for match in re.findall(r'url\(\s*["\']?([^"\')]+)', body):
assert match.startswith('data:'), (
'%s loads %s rather than an inline data: URI'
% (filename, match))
# ---------------------------------------------------------------------------
# The spec has to say where this instance actually lives
# ---------------------------------------------------------------------------
def test_the_spec_declares_the_mount_it_is_served_from(enabled_client):
"""Production runs under /shopdb, and the generated file cannot know that.
gen_openapi.py writes servers: [{"url": "/"}] - correct for a root install
and wrong for every subpath one - and wsgi.py only moves MOUNT_PATH into
SCRIPT_NAME at runtime. Served as written, both renderers built
https://host/api/... for an app mounted at /shopdb: a URL an operator copied
out of the docs page and got a 404 from, and the address Swagger's try-it
fired at.
"""
import json as _json
at_root = _json.loads(
enabled_client.get('/api/docs/openapi.json').get_data(as_text=True))
assert at_root['servers'] == [{'url': '/', 'description': 'this instance'}]
mounted = _json.loads(enabled_client.get(
'/api/docs/openapi.json',
environ_overrides={'SCRIPT_NAME': '/shopdb'}).get_data(as_text=True))
assert mounted['servers'] == [
{'url': '/shopdb', 'description': 'this instance'}]
# The paths themselves stay mount-free: the server prefix is what varies,
# and duplicating it into every path would double it under a subpath.
assert '/api/apitokens' in mounted['paths']
assert '/shopdb/api/apitokens' not in mounted['paths']
# ---------------------------------------------------------------------------
# The read-only page follows the OS theme
# ---------------------------------------------------------------------------
def test_the_redoc_page_follows_the_system_theme(client):
"""The app has a dark theme; /api/docs was the one page that stayed white.
Redoc ignores prefers-color-scheme and the <redoc spec-url> element takes no
theme, so the page has to go through Redoc.init - which is also why this
test pins the mechanism and not just the CSS. A future edit that puts the
element back would lose dark mode without changing a single colour.
"""
page = client.get('/api/docs/').get_data(as_text=True)
assert 'Redoc.init' in page
assert '<redoc spec-url' not in page, 'the element takes no theme'
# The page now follows the app's own stored choice first and only falls
# back to the OS, so assert the whole chain rather than the media query
# alone: the storage key, the attribute both the CSS and the Redoc theme
# key off, and the fallback.
assert '"shopdb-theme"' in page
assert 'data-theme' in page
assert 'prefers-color-scheme: dark' in page
assert 'var DARK=' in page and 'var LIGHT=' in page
# Re-render on a live scheme change, with the fallback for the older
# MediaQueryList that a kiosk browser may still have.
assert 'addEventListener("change",draw)' in page
assert 'q.addListener(draw)' in page
def test_the_docs_page_still_loads_nothing_from_another_origin(client):
"""Theming is the kind of change that quietly introduces a font or a CDN."""
import re as _re
page = client.get('/api/docs/').get_data(as_text=True)
assert not _re.findall(r'(?:src|href)\s*=\s*[\'"](?:https?:)?//', page)