Skip to content

fix: register CORSMiddleware outermost so OPTIONS preflight bypasses auth gate (#59052) - #59111

Closed
wesleysimplicio wants to merge 3 commits into
NousResearch:mainfrom
wesleysimplicio:simplicio/fix-59052-cors-middleware-order
Closed

fix: register CORSMiddleware outermost so OPTIONS preflight bypasses auth gate (#59052)#59111
wesleysimplicio wants to merge 3 commits into
NousResearch:mainfrom
wesleysimplicio:simplicio/fix-59052-cors-middleware-order

Conversation

@wesleysimplicio

Copy link
Copy Markdown
Contributor

Description

Fixes #59052.

OPTIONS preflight requests to /api/* return 401 because CORSMiddleware was registered first (via app.add_middleware at line 289), making it the innermost middleware in the Starlette stack. Auth middlewares registered later (auth_middleware, _dashboard_auth_gate, _token_auth_seam via @app.middleware) wrapped around it, running first on incoming requests. An OPTIONS preflight carries no session token header, so auth_middleware rejects it with 401 before CORSMiddleware ever sees it.

Root Cause

In Starlette 1.0.1, add_middleware inserts at index 0 of user_middleware, and build_middleware_stack iterates in reverse — so the last-registered middleware is outermost (runs first on incoming requests).

Current order (before fix):

  1. _token_auth_seam (outermost — registered last via @app.middleware)
  2. auth_middleware
  3. _dashboard_auth_gate
  4. _plugin_api_runtime_gate
  5. host_header_middleware
  6. CORSMiddleware (innermost — registered first via app.add_middleware)

An OPTIONS preflight to /api/some-endpoint hits auth_middleware at step 2, which calls _has_valid_session_token() — the preflight has no X-Hermes-Session-Token header → 401 before CORS ever runs.

Fix

Moved app.add_middleware(CORSMiddleware, ...) to after all @app.middleware decorators (after _token_auth_seam). CORSMiddleware is now registered last → outermost:

  1. CORSMiddleware (outermost — handles OPTIONS before auth runs)
  2. _token_auth_seam
  3. auth_middleware
  4. _dashboard_auth_gate
  5. _plugin_api_runtime_gate
  6. host_header_middleware

CORSMiddleware.preflight_response() returns a 200 with CORS headers for OPTIONS requests and does NOT call call_next — so the request never reaches any auth middleware.

Testing

  • Verified Starlette 1.0.1 middleware ordering behavior (add_middleware inserts at position 0; build_middleware_stack reverses)
  • Lint passes (ruff)
  • Manual test: curl -X OPTIONS -H 'Origin: http://localhost:5173' http://localhost:9119/api/sessions should return 200 with CORS headers, not 401

Remove 11 stale # noqa comments (E731 on fallback lambdas, F401 on
re-exported imports) and 5 stale # type: ignore[import-not-found]
comments (SUPERVISOR_REGISTRY imports). The SUPERVISOR_REGISTRY symbol
is defined in tools/browser_supervisor.py and listed in __all__; other
modules (browser_dialog_tool.py) already import it without suppression.

Stale suppressions detected via: ruff check --select RUF100 --fix
Issue NousResearch#59026 - the langfuse SDK can be removed from the active
environment, causing tracing to silently stop with no operator warning.

Changes:
- Add logger.warning() in _get_langfuse() when the SDK import fails
- Add a prominent warning section to README.md about the dependency
- Add requirements.txt for the langfuse>=2.36,<3.0 dependency
…auth gate (NousResearch#59052)

Bug: OPTIONS preflight requests to /api/* return 401 because
CORSMiddleware was registered first (line 289) via app.add_middleware,
making it the innermost middleware in the Starlette stack. Auth
middlewares registered later (auth_middleware, _dashboard_auth_gate,
_token_auth_seam via @app.middleware) wrapped around it, running
first on incoming requests. An OPTIONS preflight has no session token
header, so auth_middleware rejects it before CORSMiddleware ever sees it.

Fix: Move app.add_middleware(CORSMiddleware, ...) to after all
@app.middleware decorators (after token_auth_seam). In Starlette 1.0.1,
add_middleware inserts at index 0 of user_middleware, and
build_middleware_stack reverses the list — so the last-registered
middleware is outermost (runs first). CORS is now outermost after
ServerErrorMiddleware, handling OPTIONS preflight before auth runs.

Fixes NousResearch#59052
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/dashboard Web dashboard / control panel UI (dashboard/, landing) comp/cli CLI entry point, hermes_cli/, setup wizard area/auth Authentication, OAuth, credential pools duplicate This issue or pull request already exists labels Jul 5, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Duplicate of #59072 — same CORS-middleware-reordering fix in hermes_cli/web_server.py for the same issue #59052, but #59072 is earlier and cleaner (only web_server.py + a test). This PR additionally bundles unrelated stacked-branch files (plugins/observability/langfuse/*, tools/browser_tool.py).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/cli CLI entry point, hermes_cli/, setup wizard comp/dashboard Web dashboard / control panel UI (dashboard/, landing) duplicate This issue or pull request already exists P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OPTIONS preflight returns 401 on /api/* — dashboard auth middleware ordered before CORS

2 participants