Skip to content

Fix 400 and 500s on POST /connections/defaults in multi-team mode - #74615

Merged
vincbeck merged 1 commit into
apache:mainfrom
Pushkal-Gupta:fix-multi-team-bodyless-request-body
Oct 13, 2026
Merged

vincbeck merged 1 commit into
apache:mainfrom
Pushkal-Gupta:fix-multi-team-bodyless-request-body

Conversation

@Pushkal-Gupta

Copy link
Copy Markdown
Contributor

With core.multi_team turned on, POST /connections/defaults always fails with a 400 - even for a perfectly valid call. Three kinds of malformed body also come back as a 500 instead of a 400. Both have the same cause.

What's going on

In multi-team mode, authorization needs to know which team a request is about, so _collect_teams_to_check reads team_name out of the request body on POST and PUT. It only catches JSONDecodeError around that read.

That looked safe, and #66504 said why when it added the guard: every POST/PUT route in core_api declares a Pydantic body model, so FastAPI parses the body and returns a 422 long before the auth dependency ever runs.

One route doesn't declare a body model, though:

def create_default_connections(
    session: SessionDep,
):

FastAPI only parses a body when the route asks for one. So for /connections/defaults, nothing has touched the body by the time the dependency runs — that read is the first and only parse. The route has been around since #45363 (January 2025), sixteen months before the guard landed.

Two things follow, both only when multi-team is on.

The endpoint stops working. It takes no body by design, but the guard insists on a parseable one, so a correct call gets 400 Request body is not valid JSON instead of 204. Switch multi-team off and the same call returns 204, which is why this slipped by.

Three malformed bodies become 500s. json.loads doesn't only raise JSONDecodeError - it also raises UnicodeDecodeError, a plain ValueError when a number exceeds int_max_str_digits, and RecursionError on deeply nested input. None of those were caught.

POST /connections/defaults Before After
no body (a valid call) 400 204
invalid UTF-8 bytes 500 400
number over int_max_str_digits 500 400
deeply nested JSON 500 400
malformed JSON 400 400

How much is affected

Only this one route. Every other POST/PUT route behind requires_access_pool / requires_access_connection / requires_access_variable declares a body model, so FastAPI gets there first and answers 422 or 400 before the dependency runs. I fired all five payloads at both kinds of route to check.

The fix

Two small changes: skip the read when the body is empty, since a request with no body isn't naming a team anyway, and catch any parse failure rather than just JSONDecodeError.

Neither idea is new here. requires_access_backfill, a few hundred lines up in the same file, already does exactly this and even names the three extra exception types in its comment. parse_login_body in the simple auth manager does the same. _collect_teams_to_check just never caught up.

Fail-closed behaviour is unchanged. A non-empty body that can't be read is still rejected with a 400 before any authorization decision. An empty body reaches the authorization check with team_name=None - the same thing a GET presents - and routes that genuinely need a body still get FastAPI's 422, exactly as before. Nothing becomes authorized that wasn't already.

Worth saying plainly: both broken cases denied the request, so there was no authorization bypass here. It's a wrong status code and a leaked traceback, not a vulnerability, so it's raised as an ordinary bug.

Tests

Three new ones:

  • the three exception types × POST/PUT, checking we return 400 and never reach the authorization callback
  • an empty body, checking we don't try to parse it and that authorization runs with team_name=None
  • a route-level test that POST /connections/defaults returns 204 in multi-team mode - the actual reported symptom

All eight cases fail without the change to security.py; I checked by stashing just that file and re-running. The fourteen existing _collect_teams_to_check tests mocked request.json but not request.body, so they now mock both and
still assert what they always did.

mypy and ruff are clean, and the full airflow-core/tests/unit/api_fastapi/ suite passes at 4395.

No newsfragment — this is a bug fix, not a breaking or user-facing change.

One thing for a committer to weigh in on: the alternative would be to stop routing POST /connections/defaults through team scoping altogether, since creating default connections has no team dimension. I fixed the helper instead so it holds up for any future bodyless route, but I'm happy to go the other way if you'd prefer.

This affects v3-2-test too, where #66504 was backported as #67182.


Was generative AI tooling used to co-author this PR?
  • Yes — Claude Code (Opus 5)

@fat-catTW fat-catTW left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR
LGTM!

@vincbeck
vincbeck merged commit e87c35e into apache:main Oct 13, 2026
80 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:API Airflow's REST/HTTP API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants