feat: automatically support HEAD method for all GET routes#14792
feat: automatically support HEAD method for all GET routes#14792jonathan-fulton wants to merge 1 commit intofastapi:masterfrom
Conversation
) Following HTTP semantics and Starlette's behavior, GET routes now automatically respond to HEAD requests. HEAD returns the same headers as GET but with an empty body. Changes: - Add HEAD to methods set when GET is present in APIRoute - Skip auto-added HEAD (when paired with GET) in OpenAPI schema generation - Update generate_unique_id to use deterministic method selection - Add comprehensive tests for HEAD method support This allows HEAD requests to work out of the box for cache validation and resource checks, without requiring developers to define explicit HEAD routes. Explicit HEAD routes still work when defined before GET routes. Fixes fastapi#1773
CodSpeed Performance ReportMerging this PR will not alter performanceComparing Summary
Footnotes |
YuriiMotov
left a comment
There was a problem hiding this comment.
@jonathan-fulton, thank you for your interest and efforts!
I think we can improve this with the following approach: after including all routers analyze all routes and add HEAD methods to paths where there is a GET method but there is no HEAD method.
What do you think?
This would solve a couple of problems:
- overriding the explicitly added
HEADmethod if it goes afterGET - overriding the explicitly added
HEADmethod if it added to the same route asGET(usingapp.add_route("/", handler, methods=["GET", "HEAD"]))
Also, I would suggest adding an option to disable adding the HEAD methods automatically (HEAD method is not mandatory according to spec, and it might be undesired if app doesn't follow the REST recommendations regarding the idempotency of GET)
| # Skip auto-added HEAD method in OpenAPI when it's paired with GET. | ||
| # HEAD is automatically supported for all GET endpoints per HTTP semantics. | ||
| # But explicit HEAD-only routes should still appear in the schema. | ||
| if method == "HEAD" and "GET" in route.methods: | ||
| continue |
There was a problem hiding this comment.
It will also hide the HEAD method if user adds GET and HEAD methods explicitly using app.add_route("/", handler, methods=["GET", "HEAD"])
| # Automatically add HEAD for GET routes, following HTTP semantics and Starlette behavior | ||
| if "GET" in self.methods: | ||
| self.methods.add("HEAD") |
There was a problem hiding this comment.
This will be a breaking change for use cases when developers explicitly declare HEAD method after GET method.
This is probably not a big problem, but we should mark this PR\Release as Breaking changes and probably provide the way to disable this feature. Also, this should be explained in docs.
Also, I think we should add a separate route instead of extending the methods of existing route
| # Use a deterministic method for the operation ID. | ||
| # Prefer non-HEAD methods since HEAD is often auto-added for GET routes. | ||
| # Sort to ensure consistent ordering across Python versions. | ||
| methods = sorted(route.methods) | ||
| method = next((m for m in methods if m != "HEAD"), methods[0]) | ||
| operation_id = f"{operation_id}_{method.lower()}" |
There was a problem hiding this comment.
This will not be needed if we add a separate route instead of extending the methods of existing route.
|
@YuriiMotov Thank you for the thoughtful feedback! Your suggestions would definitely make this more robust. Let me address them: 1. Analyze routes after including all routers Great idea! This would avoid the issues you mentioned:
I'll refactor to do a post-processing pass after all routes are registered. 2. Add option to disable auto-HEAD Agreed - some apps may not follow REST idempotency for GET requests, and having an escape hatch is important. I'll add something like: app = FastAPI(auto_head_methods=True) # default
# or
app = FastAPI(auto_head_methods=False) # opt-outI'll work on implementing these changes and update the PR. Thanks again for the guidance! |
|
Update: I attempted to implement the post-processing approach, but ran into timing issues. The Challenge:
Current Implementation Advantages:
Questions for Discussion:
Happy to iterate further based on your guidance! The current implementation passes all tests and handles the common cases well. |
It's true that Starlette has the same issue with "order matters".. I have an idea, not sure how possible it is.. Opened a discussion in Starlette repo: Kludex/starlette#3128 |
Summary
Fixes #1773
FastAPI was returning 405 Method Not Allowed for HEAD requests to GET endpoints, contrary to HTTP specifications and Starlette's behavior. HEAD requests should work automatically for all GET endpoints.
Changes
APIRoute.__init__generate_unique_idto use deterministic method selectionFeatures
Testing
Added comprehensive tests in
tests/test_head_method.py