Skip to content

Replace BaseHTTPMiddleware with pure ASGI middlewares#7230

Merged
erosselli merged 18 commits intomainfrom
erosselli/asgi-middlewares
Jan 28, 2026
Merged

Replace BaseHTTPMiddleware with pure ASGI middlewares#7230
erosselli merged 18 commits intomainfrom
erosselli/asgi-middlewares

Conversation

@erosselli
Copy link
Copy Markdown
Contributor

@erosselli erosselli commented Jan 15, 2026

Ticket ENG-2409

Description Of Changes

While debugging performance issues for the consent v3 APIs, we also ran some benchmarks on a no-op /ping endpoint and found it couldn't handle a large number of RPS. Trying to find the cause, we found that the BaseHTTPMiddleware class (used by all our middlewares!) is extremely slow . Even adding a no-op BaseHTTPMiddleware that just calls the next on the stack majorly affects performance.

This PR replace our middleware implementation with a pure ASGI middleware that does not rely on BaseHTTPMiddleware class. This is meant to be just a refactor of the underlying implementation, but all middleware behavior should stay the same.

Steps to Confirm

  1. All tests should still pass, since middlewares should stay functionally the same

Pre-Merge Checklist

  • Issue requirements met
  • All CI pipelines succeeded
  • CHANGELOG.md updated
    • Add a db-migration This indicates that a change includes a database migration label to the entry if your change includes a DB migration
    • Add a high-risk This issue suggests changes that have a high-probability of breaking existing code label to the entry if your change includes a high-risk change (i.e. potential for performance impact or unexpected regression) that should be flagged
    • Updates unreleased work already in Changelog, no new entry necessary
  • UX feedback:
    • All UX related changes have been reviewed by a designer
    • No UX review needed
  • Followup issues:
    • Followup issues created
    • No followup issues
  • Database migrations:
    • Ensure that your downrev is up to date with the latest revision on main
    • Ensure that your downgrade() migration is correct and works
      • If a downgrade migration is not possible for this change, please call this out in the PR description!
    • No migrations
  • Documentation:
    • Documentation complete, PR opened in fidesdocs
    • Documentation issue created in fidesdocs
    • If there are any new client scopes created as part of the pull request, remember to update public-facing documentation that references our scope registry
    • No documentation updates required

@erosselli erosselli added the do not merge Please don't merge yet, bad things will happen if you do label Jan 15, 2026
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Jan 15, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Review Updated (UTC)
fides-plus-nightly Ignored Ignored Preview Jan 28, 2026 7:31pm
fides-privacy-center Ignored Ignored Jan 28, 2026 7:31pm

Request Review

@erosselli erosselli marked this pull request as ready for review January 26, 2026 19:39
@erosselli erosselli requested a review from a team as a code owner January 26, 2026 19:39
@erosselli erosselli requested review from vcruces and removed request for a team January 26, 2026 19:39
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Jan 26, 2026

Greptile Overview

Greptile Summary

This PR refactors all middleware from BaseHTTPMiddleware to pure ASGI implementations to address significant performance issues discovered during benchmarking. The BaseHTTPMiddleware class has documented performance problems that severely impact request throughput.

Key Changes:

  • Converted 5 middleware classes to pure ASGI: LogRequestMiddleware, AnalyticsLoggingMiddleware, AuditLogMiddleware, ProfileRequestMiddleware, SecurityHeadersMiddleware, and RateLimitIPValidationMiddleware
  • Created new src/fides/api/asgi_middleware.py with 4 middleware implementations
  • Removed ~150 lines of old middleware code from main.py
  • Added comprehensive test coverage (1,400+ new test lines) for all new middleware implementations
  • All middleware behavior remains functionally identical - this is purely a performance optimization

Previous Issues Addressed:
All issues from previous review threads have been resolved:

  • Request body buffering in AuditLogMiddleware now properly captures and replays body for downstream handlers
  • ProfileRequestMiddleware is properly registered in app_setup.py for dev mode
  • API_PREFIX constant is imported from endpoint_utils instead of hardcoded
  • asyncio.create_task references are stored in class-level set to prevent premature garbage collection

Confidence Score: 4/5

  • This PR is safe to merge with moderate risk due to the performance-critical refactoring nature
  • Score reflects the high-quality implementation with comprehensive test coverage (1,400+ new test lines) and successful resolution of all previously identified issues. The refactoring maintains functional equivalence while improving performance. However, a score of 4 rather than 5 is appropriate because: (1) this is a high-risk change affecting core request processing middleware, (2) the PR exceeds recommended size limits with 13 files changed and 2,410 insertions, and (3) ASGI middleware operates at a lower level than BaseHTTPMiddleware, making subtle bugs harder to detect in testing
  • Pay close attention to src/fides/api/asgi_middleware.py during deployment and monitor request handling behavior

Important Files Changed

Filename Overview
src/fides/api/asgi_middleware.py New pure ASGI middleware implementations replacing BaseHTTPMiddleware for better performance
src/fides/api/app_setup.py Updates middleware registration to use new ASGI middleware classes
src/fides/api/main.py Removes old BaseHTTPMiddleware-based middleware implementations (~150 lines deleted)
src/fides/api/util/rate_limit.py Converts RateLimitIPValidationMiddleware to pure ASGI middleware
src/fides/api/util/security_headers.py Converts SecurityHeadersMiddleware to pure ASGI middleware

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

@erosselli
Copy link
Copy Markdown
Contributor Author

@greptile please re-review

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

@erosselli
Copy link
Copy Markdown
Contributor Author

@greptile please re-review

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

No files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@erosselli erosselli requested a review from adamsachs January 27, 2026 16:14
@erosselli erosselli removed the do not merge Please don't merge yet, bad things will happen if you do label Jan 27, 2026
Copy link
Copy Markdown
Contributor

@adamsachs adamsachs left a comment

Choose a reason for hiding this comment

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

great job finding what seems to be a serious performance improvement. from what i can tell, your pure ASGI replacements achieve parity with the existing middleware functionality 👍

the major tradeoff here is readability/maintainability: the pure ASGI versions are more verbose and 'clunky', as expected, since we're deliberately not using the BaseHTTPMiddleware abstraction anymore. but could we regain some of the benefits of that abstraction by just making our own? that's my main feedback here - it seems like there is quite a bit of room to make our 'own' base http middleware abstraction that's lighter weight (and more performant) than starlette's, but at least gives us some of benefits of a base class: shared/consistent code, less boilerplate, etc. that's not necessarily a blocker, but i'd like to at least know we're planning a quick followup, or else we risk never doing that. unless there's a specific reason you've decided not to make that abstraction?

beyond that, just a couple of nits i spotted along the way.

generally, what do you think are the biggest risks? i know we have some pretty solid test coverage, but some middleware things (e.g. rate limiting) can be pretty hard to cover in an automated test suite, so i'd be curious to know if there's anything you're particularly worried about and whether we can get ahead of some 'manual' (or more integration) testing to de-risk that.

Comment on lines +50 to +63
if scope["type"] != "http":
await self.app(scope, receive, send)
return

start_time = perf_counter()
status_code = 500 # Default in case of exception

# Extract request info from scope
method = scope.get("method", "UNKNOWN")
path = scope.get("path", "/")

# Get Fides-Client header
headers = dict(scope.get("headers", []))
fides_client = headers.get(b"fides-client", b"unknown").decode("latin-1")
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.

i think there's quite a bit of room here for a useful abstraction/standardization, specifically around the basic parsing/validation of the scope object: e.g. filtering out non-HTTP requests, retrieving the headers dict, etc.

should we not create a base class here that these can all inherit from, so that we get some of that boiler plate 'for free' on each middleware class, and so that its standardized? or at least some helper methods to reduce code duplication? i know we don't want to reinvent the BaseHTTPMiddleware wholesale, but i think it'd be prudent to build in a few more abstractions here, unless there's a compelling reason not to...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done in 1f7fcd8

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.

is this used anymore?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

weird, it has a bunch of tests but does seem unused..

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.

well you removed a use of it, so that would sorta make sense :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed as part of 1f7fcd8

# Validate header before SlowAPI processes the request
fastapi_app.add_middleware(RateLimitIPValidationMiddleware)
# Required for default rate limiting to work
fastapi_app.add_middleware(SlowAPIMiddleware)
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.

i think this is also a basehttpmiddleware. looks like they have a pure ASGI version though, SlowAPIASGIMiddleware (see laurentS/slowapi#113 for some background, i haven't looked into whether there are any tradeoffs/reasons to not switch over...)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

oh interesting! I kind of skipped over the external packages ones. Since we don't enable them by default, I might look into changing it as a follow-up PR

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.

oh ok so now we use our self-rolled rate limiting middleware instead of this one, by default?

because we do have rate limiting by default, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

So we might have it on in fides cloud but it requires setting rate_limit_client_ip_header , which is empty by default

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have sanity checked: if configured, we use both our rate limit one (which I think just does IP validation? perhaps confusingly named? ) and the slow API one.

I'll look into replacing the SlowAPI one with the ASGI version in a follow up PR

@@ -112,6 +118,19 @@
GZipMiddleware, minimum_size=1000, compresslevel=5
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.

this one looks good, seems to already be pure ASGI 👍

Copy link
Copy Markdown
Contributor

@vcruces vcruces left a comment

Choose a reason for hiding this comment

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

LGTM, really nice work! I imagine this wasn’t easy. Thanks for adding so many tests, they give confidence for something that seems hard to test 😓

sent_messages.append(message)

# Call the middleware - it should catch the exception and send a 500 response
await middleware(scope, receive, send)
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.

I might be mistaken and this may just be similar logic, but is this logic being repeated in a few places? If so, would it be worth extracting it for reuse?

@@ -0,0 +1,115 @@
# pylint: disable=missing-docstring, redefined-outer-name
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.

Given that we already have tests/ops/api/v1/test_middleware.py, would it make sense to rename this one (or the other)? I find it a bit confusing to tell what each file contains

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

oh that's some git mishap , I meant to have moved test_middleware into the /middleware folder but I see that both files are kept , I'll fix. thanks for the callout!

Copy link
Copy Markdown
Contributor

@adamsachs adamsachs left a comment

Choose a reason for hiding this comment

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

looks good, thank you for taking the time to put that base class in place - i think it makes all this code a lot cleaner, and will continue to pay dividends as we add more middleware!

ASGIApp = Callable[[Scope, Receive, Send], Awaitable[None]]


class BaseASGIMiddleware:
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.

love it! even more extensive than i imagined, this makes the middleware implementations so much cleaner.

we should submit a PR to https://github.com/Kludex/starlette that adds this to the core library :)

return
await self.handle_http(scope, receive, send)

async def handle_http(self, scope: Scope, receive: Receive, send: Send) -> None:
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.

you prefer to leave this with a default impl rather than as an abstractmethod (and making this class an ABC)? not saying that's wrong, just wanted to check it's a deliberate (or at least defensible) choice :)

Copy link
Copy Markdown
Contributor Author

@erosselli erosselli Jan 28, 2026

Choose a reason for hiding this comment

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

hmm yeah abstract method might be better, I did debate about it since this seemed like a reasonable default, but also the original BaseHTTPMiddleware does raise NotImplemented for the dispatch method.

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.

sure that works, makes sense to model BaseHTTPMiddleware closely in that respect 👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 054b051

@erosselli erosselli added this pull request to the merge queue Jan 28, 2026
Merged via the queue into main with commit a0d2d04 Jan 28, 2026
54 of 55 checks passed
@erosselli erosselli deleted the erosselli/asgi-middlewares branch January 28, 2026 20:54
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.

3 participants