Register the endpoints list without a trailing slash, and stop hiding its errors
All checks were successful
CI / naming (push) Successful in 3s
CI / frontend (push) Successful in 13s
CI / migrations-mysql (push) Successful in 40s
CI / backend (push) Successful in 4m7s

An endpoint reported telemetry correctly for hours while the Fleet page showed
"No endpoints found". The PC detail page displayed its panels the whole time, so
ingest was plainly working; only the list was empty.

The list route was the one route in 331 paths registered with a trailing slash:

    @endpoints_bp.route('/', methods=['GET'])      ->  /api/endpoints/

The browser client calls it without one, so Flask answered a 308 redirect to an
ABSOLUTE url. On dev that resolves and nobody notices. Behind the IIS subpath it
cannot: with no ProxyFix, Werkzeug builds that Location from the scheme and host
it believes it has, so it emitted http:// and dropped the /shopdb prefix. A
browser will not follow an https page to http, the request failed, and the
page's catch block rendered the empty state.

Every other route in the plugin worked throughout, because none of them carries
a trailing slash - the detail panels call /endpoints/asset/<id>/status and were
never redirected. Detail works, list empty is the signature of this bug.

Registered as '' now, so the rule is /api/endpoints. That is what
CONTRIBUTING.md's API section spells and what every other list route in the
product already did. The inventory and the generated spec are updated to match.

Also here:

  - tests/test_core/test_no_redirecting_routes.py fails the build if any /api
    rule is registered with a trailing slash and strict slashes. Verified by
    planting the violation: it named the offending route, then passed once
    reverted. It takes the shared app fixture rather than building its own
    application - doing that rebinds SQLAlchemy and leaves the NEXT module
    without tables, which showed up as three unrelated "no such table: settings"
    failures visible only in a full run.

  - The Fleet page now renders "Could not load endpoints (HTTP nnn). This is a
    failed request, not an empty fleet." A list that reports a 403 and an empty
    fleet identically is what made this take as long as it did.

  - TRUSTED_PROXY_COUNT (default 0) applies ProxyFix when set. Off by default on
    purpose: it makes X-Forwarded-* trusted, and a client can send those itself,
    while the login rate limit and account lockout are keyed on the client
    address. Set it to 1 on an instance behind IIS.

Full suite 2124 passed, vitest 260 passed, naming and docs gates green.
This commit is contained in:
cproudlock
2026-08-28 17:48:06 -04:00
parent 5da071fdda
commit af42ba3589
8 changed files with 131 additions and 5 deletions

View File

@@ -0,0 +1,44 @@
"""No API route may answer a redirect just because of a trailing slash.
A rule registered as '/api/thing/' does not merely accept that URL - Werkzeug
answers '/api/thing' with a 308 to the slashed form, and the Location it builds
is ABSOLUTE. That is fine on the dev server, where scheme and host are what the
app thinks they are. It is not fine behind the IIS subpath: with no ProxyFix the
app emits http:// and without the /shopdb prefix, the browser refuses to follow
an https page to http, and the call fails.
What made it expensive is where the failure surfaced. The Fleet page caught the
error, logged it to the console and rendered "No endpoints found", which reads
exactly like a fleet with nothing in it. The agent had been reporting correctly
for hours. Every other route in the plugin worked, because every other route is
registered without a trailing slash - '/api/endpoints/' was the only one in 331
paths, and it was the only thing broken.
So the rule is: register API routes WITHOUT a trailing slash, or pass
strict_slashes=False so both spellings match directly. Either way no client can
be handed a redirect it cannot follow.
"""
def test_no_api_route_redirects_on_a_trailing_slash(app):
# Takes the shared `app` fixture rather than calling create_app itself.
# Building a second application inside a test rebinds SQLAlchemy and leaves
# the NEXT test module without tables - it passes alone and fails only in a
# full run, as "no such table: settings" three modules later. This file
# caused exactly that once already.
offenders = sorted(
rule.rule for rule in app.url_map.iter_rules()
if rule.rule.startswith('/api')
and rule.rule.endswith('/')
and rule.rule != '/api/'
and rule.strict_slashes
)
assert not offenders, (
'%d API route(s) are registered with a trailing slash AND strict '
'slashes, so Flask answers the unslashed URL with a 308 to an absolute '
'URL. Behind a reverse proxy that redirect is unfollowable and the call '
'fails, usually looking like an empty list rather than an error.\n'
'Register the route without the trailing slash, or pass '
'strict_slashes=False:\n %s'
% (len(offenders), '\n '.join(offenders)))