Skip to content

feat: automatically support HEAD method for all GET routes#14792

Open
jonathan-fulton wants to merge 1 commit intofastapi:masterfrom
jonathan-fulton:feat/issue-1773-auto-head-method
Open

feat: automatically support HEAD method for all GET routes#14792
jonathan-fulton wants to merge 1 commit intofastapi:masterfrom
jonathan-fulton:feat/issue-1773-auto-head-method

Conversation

@jonathan-fulton
Copy link
Contributor

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

  1. Added HEAD to the methods set when GET is present in APIRoute.__init__
  2. Skip auto-added HEAD (when paired with GET) in OpenAPI schema generation
  3. Updated generate_unique_id to use deterministic method selection

Features

  • HEAD requests now work automatically for all GET endpoints
  • HEAD returns same headers as GET but with empty body
  • Explicit HEAD routes still work when defined before GET routes
  • HEAD is not shown in OpenAPI schema (it's implicit per HTTP semantics)

Testing

Added comprehensive tests in tests/test_head_method.py

)

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-hq
Copy link

codspeed-hq bot commented Feb 1, 2026

CodSpeed Performance Report

Merging this PR will not alter performance

Comparing jonathan-fulton:feat/issue-1773-auto-head-method (d201fdc) with master (c9629e0)1

Summary

✅ 20 untouched benchmarks

Footnotes

  1. No successful run was found on master (0892440) during the generation of this report, so c9629e0 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Copy link
Member

@YuriiMotov YuriiMotov left a comment

Choose a reason for hiding this comment

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

@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 HEAD method if it goes after GET
  • overriding the explicitly added HEAD method if it added to the same route as GET (using app.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)

Comment on lines +281 to +285
# 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
Copy link
Member

Choose a reason for hiding this comment

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

It will also hide the HEAD method if user adds GET and HEAD methods explicitly using app.add_route("/", handler, methods=["GET", "HEAD"])

Comment on lines +552 to +554
# Automatically add HEAD for GET routes, following HTTP semantics and Starlette behavior
if "GET" in self.methods:
self.methods.add("HEAD")
Copy link
Member

Choose a reason for hiding this comment

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

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

Comment on lines +127 to +132
# 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()}"
Copy link
Member

Choose a reason for hiding this comment

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

This will not be needed if we add a separate route instead of extending the methods of existing route.

@jonathan-fulton
Copy link
Contributor Author

@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:

  • Won't override explicitly added HEAD methods that come after GET
  • Won't override HEAD added via add_route(..., methods=["GET", "HEAD"])

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-out

I'll work on implementing these changes and update the PR. Thanks again for the guidance!

@github-actions github-actions bot removed the waiting label Feb 2, 2026
@jonathan-fulton
Copy link
Contributor Author

Update: I attempted to implement the post-processing approach, but ran into timing issues.

The Challenge:

  • setup() is called during __init__, before any routes are added via decorators (@app.get())
  • Moving HEAD addition to openapi() only affects the schema, not actual request routing
  • Adding a startup event would require ASGI lifecycle changes

Current Implementation Advantages:

  • Simple and predictable - HEAD is added at route creation time
  • Works correctly for the test cases
  • Matches how Starlette handles this internally

Questions for Discussion:

  1. Is there a specific scenario where the current implementation causes issues?
  2. Would the use case "explicit HEAD route after GET for same path" actually be common?
  3. Should we add an auto_head_methods=False parameter to APIRoute or FastAPI to allow opt-out?

Happy to iterate further based on your guidance! The current implementation passes all tests and handles the common cases well.

@YuriiMotov
Copy link
Member

YuriiMotov commented Feb 3, 2026

Current Implementation Advantages:
...

  • Matches how Starlette handles this internally

It's true that Starlette has the same issue with "order matters"..

I have an idea, not sure how possible it is..
What if Starlette stopped adding the HEAD methods automatically, and handled this during the routing. So that if there is no route with HEAD method, it would then use the route with GET method.
This way manually added HEAD methods would have precedence. And, this would automatically solve this issue for FastAPI as well.

Opened a discussion in Starlette repo: Kludex/starlette#3128

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.

Automatically support HEAD method for all GET routes, as Starlette does

2 participants