Skip to content

Fix TypeError: CORSMiddleware.__call__() missing 2 required positiona… - #523

Merged
taylorwilsdon merged 6 commits into
taylorwilsdon:mainfrom
jack-distl:claude/fix-cors-middleware-error-sPbo6
Mar 1, 2026
Merged

Fix TypeError: CORSMiddleware.__call__() missing 2 required positiona…#523
taylorwilsdon merged 6 commits into
taylorwilsdon:mainfrom
jack-distl:claude/fix-cors-middleware-error-sPbo6

Conversation

@jack-distl

@jack-distl jack-distl commented Mar 1, 2026

Copy link
Copy Markdown

…l arguments

The _wrap_well_known_endpoint function assumed all route endpoints are regular request handlers (async def handler(request) -> Response). However, the MCP SDK's cors_middleware wraps handlers with CORSMiddleware, which is an ASGI app expecting (scope, receive, send). When the wrapper called await endpoint(request) on a CORSMiddleware instance, it passed only 1 argument instead of the required 3 ASGI args.

The fix detects whether the endpoint is a regular handler function or an ASGI app (using the same inspect check as Starlette's Route constructor), and uses the appropriate calling convention:

  • Regular handlers: called as await endpoint(request) (existing behavior)
  • ASGI apps: invoked via the ASGI interface await endpoint(scope, receive, send) with response capture to apply cache-busting headers

https://claude.ai/code/session_011S5zFTWRfKBJBUEanrhvQg

Description

Brief description of the changes in this PR.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update

Testing

  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have tested this change manually

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • I have enabled "Allow edits from maintainers" for this pull request

Additional Notes

Add any other context about the pull request here.


⚠️ IMPORTANT: This repository requires that you enable "Allow edits from maintainers" when creating your pull request. This allows maintainers to make small fixes and improvements directly to your branch, speeding up the review process.

To enable this setting:

  1. When creating the PR, check the "Allow edits from maintainers" checkbox
  2. If you've already created the PR, you can enable this in the PR sidebar under "Allow edits from maintainers"

Summary by CodeRabbit

  • Bug Fixes
    • OAuth discovery endpoints now consistently return Cache-Control: no-store, must-revalidate and include a stable ETag; other routes remain unchanged.
  • Refactor
    • Centralized cache-control handling for well-known OAuth paths to ensure consistent response headers.
  • Tests
    • Added end-to-end tests validating header rewriting, ETag generation, cookie preservation, and routing behavior.

…l arguments

The _wrap_well_known_endpoint function assumed all route endpoints are regular
request handlers (async def handler(request) -> Response). However, the MCP
SDK's cors_middleware wraps handlers with CORSMiddleware, which is an ASGI app
expecting (scope, receive, send). When the wrapper called
`await endpoint(request)` on a CORSMiddleware instance, it passed only 1
argument instead of the required 3 ASGI args.

The fix detects whether the endpoint is a regular handler function or an ASGI
app (using the same inspect check as Starlette's Route constructor), and uses
the appropriate calling convention:
- Regular handlers: called as `await endpoint(request)` (existing behavior)
- ASGI apps: invoked via the ASGI interface `await endpoint(scope, receive, send)`
  with response capture to apply cache-busting headers

https://claude.ai/code/session_011S5zFTWRfKBJBUEanrhvQg
@coderabbitai

coderabbitai Bot commented Mar 1, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a new WellKnownCacheControlMiddleware in core/server.py that centrally applies Cache-Control: no-store, must-revalidate and an ETag (from _compute_scope_fingerprint) only for OAuth well-known endpoints. The middleware is inserted as the outermost HTTP middleware and replaces prior per-route wrapping.

Changes

Cohort / File(s) Summary
Well-known cache-control middleware
core/server.py
Adds WellKnownCacheControlMiddleware and well_known_cache_control_middleware instance; removes prior per-endpoint wrapping; uses MutableHeaders and _compute_scope_fingerprint() to set Cache-Control and ETag for OAuth well-known paths; reorders middleware stack and logging.
Middleware tests
tests/core/test_well_known_cache_control_middleware.py
Adds tests validating no-cache header rewrite and ETag generation on well-known OAuth discovery endpoints, and ensuring non-well-known endpoints keep original cache headers; includes environment/configuration scenario tests and cookie preservation checks.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ASGI_Server
  participant WellKnownMW as WellKnownCacheControlMiddleware
  participant SessionMW as SessionMiddleware
  participant App as Application

  Client->>ASGI_Server: HTTP request (path)
  ASGI_Server->>WellKnownMW: ASGI scope, receive, send
  WellKnownMW->>SessionMW: forward scope
  SessionMW->>App: forward scope
  App-->>SessionMW: response (headers, body)
  SessionMW-->>WellKnownMW: response (headers, body)
  WellKnownMW-->>WellKnownMW: if well-known path -> compute ETag, mutate headers (Cache-Control, ETag) via MutableHeaders
  WellKnownMW-->>ASGI_Server: send (possibly mutated) response
  ASGI_Server-->>Client: HTTP response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hop through scopes and headers bright,
I stamp ETags in the quiet night,
Well-known paths get a no-store song,
Other routes pass gently along,
A tidy rabbit tweak — swift and light.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately identifies the primary bug being fixed (TypeError in CORSMiddleware handling), though it is truncated and incomplete.
Description check ✅ Passed The PR description includes technical details about the bug and fix, but most checkbox items in the template remain unchecked, indicating incomplete testing verification and manual testing validation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/server.py`:
- Around line 105-115: The current code converts raw_headers into a dict
(headers) which collapses duplicate keys; instead, keep raw_headers and after
creating the Response object (Response(...)), iterate over raw_headers, decoding
keys/values (handle bytes) and call response.headers.append(decoded_key,
decoded_value) for each pair so duplicate headers like Set-Cookie are preserved
(refer to raw_headers, headers variable, Response, and
response.headers.append()).
- Line 103: Replace direct access to the private attribute by calling the public
receive property on the Request object: in the call to endpoint use
request.receive instead of request._receive so the invocation becomes await
endpoint(request.scope, request.receive, send); this avoids using the private
attribute and uses Starlette's documented Request.receive wrapper.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ca00c74 and cd326d0.

📒 Files selected for processing (1)
  • core/server.py

Comment thread core/server.py Outdated
elif message["type"] == "http.response.body":
body_parts.append(message.get("body", b""))

await endpoint(request.scope, request._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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Starlette Request.receive public API documentation

💡 Result:

Starlette’s official “Requests” documentation does not document a Request.receive method/property as part of its public API. Instead, it documents that Request wraps the ASGI scope and (optionally) the ASGI receive callable, and encourages using higher-level APIs like await request.body(), await request.json(), and request.stream() rather than working with the receive channel directly. [1]

If what you’re looking for is the raw ASGI receive callable, the closest “public” documentation you’ll find is in FastAPI’s autogenerated reference for Request (which is Starlette’s Request), where receive appears as a documented attribute/property name (alongside scope, headers, etc.), but it’s not described in detail there either. [2]

For background on how Starlette intends you to use receive (pass it into Request(...), then use the higher-level request interfaces), Encode’s ASGI/HTTP article explains this design at a conceptual level. [3]

Sources: Starlette Requests docs [1]; FastAPI Request reference (Starlette-based) [2]; Encode ASGI/HTTP article [3].


🏁 Script executed:

# Check the codebase for context about how endpoint is used
cd core && grep -r "endpoint" server.py | head -20

Repository: taylorwilsdon/google_workspace_mcp

Length of output: 1102


🏁 Script executed:

# Look for imports and see what Request object is being used
head -50 core/server.py

Repository: taylorwilsdon/google_workspace_mcp

Length of output: 1857


🏁 Script executed:

# Search for how Starlette Request handles receive
rg "class Request" -A 20 --type py | head -60

Repository: taylorwilsdon/google_workspace_mcp

Length of output: 60


🏁 Script executed:

# Also check if there's any Starlette version info in the codebase
rg "starlette" --type py | grep -i "version\|import" | head -20

Repository: taylorwilsdon/google_workspace_mcp

Length of output: 702


🌐 Web query:

Starlette Request object receive attribute availability source code

💡 Result:

In Starlette, the ASGI receive callable is stored on the Request instance as self._receive, and exposed via the receive property:

  • Request.__init__(scope, receive=empty_receive, send=empty_send) assigns self._receive = receive
  • @property def receive(self) -> Receive: return self._receive
  • If you instantiate Request(scope) without passing receive, the default empty_receive() raises RuntimeError("Receive channel has not been made available") if anything tries to read from it. [1]

This matches the docs: Request(scope, receive=None) and “If you don't need to access the request body you can instantiate a request without providing an argument to receive.” (But body/stream access needs a working receive callable.) [2]

FastAPI’s Request is Starlette’s Request and documents the same receive property and points to starlette/requests.py as the source. [3]

Sources:
[1] https://raw.githubusercontent.com/encode/starlette/master/starlette/requests.py
[2] https://www.starlette.io/requests/
[3] https://fastapi.tiangolo.com/reference/request/


Use the public Request.receive property instead of private _receive.

Line 103 directly accesses the private _receive attribute. Starlette's Request object provides a public receive property that wraps this attribute and is documented in the official API.

Proposed fix
-            await endpoint(request.scope, request._receive, send)
+            await endpoint(request.scope, request.receive, send)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await endpoint(request.scope, request._receive, send)
await endpoint(request.scope, request.receive, send)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/server.py` at line 103, Replace direct access to the private attribute
by calling the public receive property on the Request object: in the call to
endpoint use request.receive instead of request._receive so the invocation
becomes await endpoint(request.scope, request.receive, send); this avoids using
the private attribute and uses Starlette's documented Request.receive wrapper.

Comment thread core/server.py Outdated
Comment on lines +105 to +115
headers = {
(k.decode() if isinstance(k, bytes) else k): (
v.decode() if isinstance(v, bytes) else v
)
for k, v in raw_headers
}
response = Response(
content=b"".join(body_parts),
status_code=status_code,
headers=headers,
)

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's explore the repository structure and find the file
git ls-files | head -20

Repository: taylorwilsdon/google_workspace_mcp

Length of output: 530


🏁 Script executed:

# Check the specific file and its context
wc -l core/server.py

Repository: taylorwilsdon/google_workspace_mcp

Length of output: 96


🏁 Script executed:

# Read the relevant section around lines 105-115
sed -n '95,125p' core/server.py

Repository: taylorwilsdon/google_workspace_mcp

Length of output: 1260


🌐 Web query:

Starlette Response raw_headers MutableHeaders duplicate headers preservation

💡 Result:

In Starlette, duplicate header preservation depends on how you mutate headers:

  • Response(headers=...) cannot preserve duplicates, because it accepts a Mapping[str, str] and builds raw_headers from headers.items() (a mapping can only hold one value per key). (github.abhinayambati.codes)
  • response.headers[...] = ... (i.e., MutableHeaders.__setitem__) removes duplicates for that header name by design. (github.abhinayambati.codes)
  • To preserve duplicates, use either:
    • response.headers.append(key, value) — explicitly documented as “preserving any duplicate entries”. (github.abhinayambati.codes)
    • or mutate response.raw_headers directly (it’s the actual ASGI List[Tuple[bytes, bytes]] sent on the wire). Response.headers is just a MutableHeaders(raw=self.raw_headers) wrapper over that list. (github.abhinayambati.codes)

Practical patterns:

# Preserve duplicates (e.g., multiple Set-Cookie)
response.headers.append("set-cookie", cookie1)
response.headers.append("set-cookie", cookie2)
# Or do it at the raw ASGI level
response.raw_headers.append((b"set-cookie", cookie1.encode("latin-1")))
response.raw_headers.append((b"set-cookie", cookie2.encode("latin-1")))

(Starlette’s own Response.set_cookie() uses self.raw_headers.append((b"set-cookie", ...)), which is why multiple cookies work.) (github.abhinayambati.codes)

Citations:


Preserve raw headers; dict conversion collapses duplicate headers.

Lines 105–110 convert raw_headers to a dict, which loses all duplicate header keys (e.g., Set-Cookie, Vary). ASGI responses can include multiple headers with the same name, but converting to Mapping[str, str] retains only the last value per key. Instead, preserve the raw ASGI header list and use response.headers.append() to add new headers without collapsing duplicates:

Proposed fix
+from starlette.datastructures import MutableHeaders
...
-            headers = {
-                (k.decode() if isinstance(k, bytes) else k): (
-                    v.decode() if isinstance(v, bytes) else v
-                )
-                for k, v in raw_headers
-            }
-            response = Response(
-                content=b"".join(body_parts),
-                status_code=status_code,
-                headers=headers,
-            )
-            response.headers["Cache-Control"] = "no-store, must-revalidate"
-            response.headers["ETag"] = etag
+            response = Response(
+                content=b"".join(body_parts),
+                status_code=status_code,
+            )
+            response.raw_headers = list(raw_headers)
+            headers = MutableHeaders(raw=response.raw_headers)
+            headers.append("Cache-Control", "no-store, must-revalidate")
+            headers.append("ETag", etag)
             return response
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/server.py` around lines 105 - 115, The current code converts raw_headers
into a dict (headers) which collapses duplicate keys; instead, keep raw_headers
and after creating the Response object (Response(...)), iterate over
raw_headers, decoding keys/values (handle bytes) and call
response.headers.append(decoded_key, decoded_value) for each pair so duplicate
headers like Set-Cookie are preserved (refer to raw_headers, headers variable,
Response, and response.headers.append()).

@taylorwilsdon

Copy link
Copy Markdown
Owner

I'm not clear what we're trying to fix here. Do you have logs or a screenshot showing the issue you encountered? Thanks!

@taylorwilsdon taylorwilsdon added the question Further information is requested label Mar 1, 2026

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/server.py`:
- Around line 56-58: The current is_oauth_well_known check uses startswith and
thus matches unintended prefixes; change the logic in the is_oauth_well_known
assignment to only accept the exact well-known path or that path followed by a
slash (i.e., use path == "/.well-known/oauth-authorization-server" or
path.startswith("/.well-known/oauth-authorization-server/") and likewise for
"/.well-known/oauth-protected-resource") so you don't match things like
"/.well-known/oauth-authorization-server-extra".

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd326d0 and c9facbf.

📒 Files selected for processing (1)
  • core/server.py

Comment thread core/server.py Outdated

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
tests/core/test_well_known_cache_control_middleware.py (1)

54-88: Consider test isolation for module reload pattern.

The test reloads core.server module and modifies global configuration, which could affect subsequent tests in the same process. While this pattern is common for integration tests, consider adding cleanup in a fixture or documenting test ordering requirements.

Optional: Add cleanup fixture
import pytest

`@pytest.fixture`
def reset_server_module():
    """Reset server module state after test."""
    yield
    # Restore original module state
    import core.server as core_server
    importlib.reload(core_server)

Then use: def test_configured_server_...(monkeypatch, reset_server_module):

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/test_well_known_cache_control_middleware.py` around lines 54 - 88,
The test mutates and reloads the core.server module and global config (calls
reload_oauth_config, importlib.reload(core.server),
core.server.set_transport_mode, core.server.configure_server_for_http), which
can leak state to other tests; add a cleanup fixture (e.g., reset_server_module)
that yields and then importlib.reload(core.server) and any other affected
modules (and/or clear env changes applied by monkeypatch) after the test, and
include that fixture in the test signature (e.g., def
test_configured_server_applies_no_cache_to_served_oauth_discovery_routes(monkeypatch,
reset_server_module):) to ensure module state and environment are restored.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/core/test_well_known_cache_control_middleware.py`:
- Around line 54-88: The test mutates and reloads the core.server module and
global config (calls reload_oauth_config, importlib.reload(core.server),
core.server.set_transport_mode, core.server.configure_server_for_http), which
can leak state to other tests; add a cleanup fixture (e.g., reset_server_module)
that yields and then importlib.reload(core.server) and any other affected
modules (and/or clear env changes applied by monkeypatch) after the test, and
include that fixture in the test signature (e.g., def
test_configured_server_applies_no_cache_to_served_oauth_discovery_routes(monkeypatch,
reset_server_module):) to ensure module state and environment are restored.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c9facbf and e999982.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • core/server.py
  • tests/core/test_well_known_cache_control_middleware.py

@taylorwilsdon
taylorwilsdon merged commit d514c62 into taylorwilsdon:main Mar 1, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

question Further information is requested

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants