Skip to content

Make the Flask integration points work on Flask 2.1+ / 3.x - #50

Open
BinaryFiddler wants to merge 2 commits into
mainfrom
chenyu/flask3-compat
Open

BinaryFiddler wants to merge 2 commits into
mainfrom
chenyu/flask3-compat

Conversation

@BinaryFiddler

@BinaryFiddler BinaryFiddler commented Aug 21, 2026 •

Copy link
Copy Markdown

Three spots in osprey_worker assume Flask 1.x and break on a modern Flask. Discord's smite services consume osprey-worker and are moving to Flask 3.1 (companion PR: discord/discord#311325), which surfaced all three.

None of these change behavior on the current Flask 1.x pin — they are version-compatibility fixes.

1. flask.signals.signals_available — removed in Flask 3.0

TraceMiddleware.__init__ read it to feature-detect blinker. Flask 3.0 made blinker a hard dependency and deleted the attribute, so any app calling ddtrace_utils.init_app() dies at startup:

File ".../ddtrace_utils/instrumentation/flask/middleware.py", line 70, in __init__
    if use_signals and not signals.signals_available:
AttributeError: module 'flask.signals' has no attribute 'signals_available'

self.use_signals is already assigned earlier in the constructor, so the feature-detect (and its now-unreachable _blinker_not_installed_msg) simply goes away. Verified: hasattr is True on 1.1.4, False on 3.1.3.

2. flask.templating._render was reordered in Flask 2.2

_patch_render monkeypatched it with the 1.x signature, so the patched function received its arguments transposed:

Flask inspect.signature(flask.templating._render)
1.1.4 (template, context, app)
3.1.3 (app, template, context)

Re-checked end-to-end after the fix: render_template_string('hi {{ n }}', n=42) returns 'hi 42' and the trace span still fires, on both versions.

3. Duplicate blueprint registration — ValueError on Flask 2.1+

ui_api's _register_with_prefix registered the same blueprint object twice (bare + under /api) under one name:

ValueError: The name 'abilities' is already registered for this blueprint. Use 'name=' to provide a unique name.

Giving the /api copy its own name= satisfies Flask, but renames the endpoint for every /api route (alerts.create_alert → alerts-api.create_alert), silently breaking anything keyed on request.endpoint — audit/exemption lookups and ddtrace span resource names among them. Under Flask 1.x both paths shared one endpoint, so consumers never had to care which prefix a request arrived on. (In the consuming smite service this would have leaked PII into an audit log; found by Bugbot there.)

So: register the blueprint once, then mirror its rules under /api reusing the same endpoint and view function. add_url_rule permits a repeated endpoint when the view function is identical.

Diffed across both Flask versions using the consuming service's real 18 blueprints / 125 routes:

Flask 1.1.4 + old name= approach shipped (alias)
(rule, methods) pairs served 253 253 253 — 0 lost, 0 gained
url_for() endpoints resolvable 128 128 128 — 0 missing, 0 changed
paths whose request.endpoint changed — 125 of 252 0 of 252

Verification

  • Boots the real consuming Flask app (smite_ui_api) on Flask 3.1.3 / Werkzeug 3.1.8, which previously failed at Long-running UDF support: per-stream window for async worker/coordinator #1.
  • 253 rules registered; every bare rule has an /api twin and vice versa.
  • Live WSGI requests: /_health → 200, /watchers/meta → 200; every probed route identical bare vs /api.
  • ruff check + ruff format --check clean.

🤖 Generated with Claude Code

Three spots assume Flask 1.x and break on a modern Flask. Discord's smite
services consume osprey-worker and are moving to Flask 3.1, which surfaced all
three; none of these changes alter behavior on the current Flask 1.x pin.

- ddtrace middleware read `flask.signals.signals_available`, removed in Flask
  3.0 when blinker became a hard dependency. On Flask 3 this is an
  AttributeError in `TraceMiddleware.__init__`, so any app calling
  `ddtrace_utils.init_app()` fails at startup. `self.use_signals` is already
  assigned earlier in the constructor, so the feature-detect just goes away.

- `_patch_render` monkeypatched `flask.templating._render` with the 1.x
  signature `(template, context, app)`. Flask 2.2 reordered it to
  `(app, template, context)`, so the patched function received its arguments
  transposed and template rendering broke.

- `ui_api`'s `_register_with_prefix` registered the same blueprint object twice
  (bare and under /api) under one name. Flask 2.1+ raises
  `ValueError: The name '<x>' is already registered for this blueprint`. The
  bare registration stays unnamed so its endpoint names -- what `url_for()`
  resolves against -- are unchanged; only the /api copy takes a suffixed name.
  Both URL paths keep routing exactly as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous approach gave the /api registration its own `name=` to satisfy
Flask 2.1's uniqueness check. That routes correctly, but it renames the
endpoint for every /api route (`alerts.create_alert` ->
`alerts-api.create_alert`), silently breaking anything keyed on
`request.endpoint` -- audit/exemption lookups and ddtrace span resource names
among them. Under Flask 1.x both paths shared one endpoint name, so consumers
never had to care which prefix a request arrived on.

Register the blueprint once, then mirror its rules under /api reusing the same
endpoint and view function. `add_url_rule` allows a repeated endpoint when the
view function is identical, so the served URL set, `url_for()` output and
`request.endpoint` are all byte-identical to the Flask 1.x behavior.

Measured against Flask 1.1.4 with the consuming service's real blueprints:
0 routes lost/gained, 0 url_for changes, and 0 of 252 paths change
`request.endpoint` (the named-registration approach changed 125).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant