Skip to content

Return HTTP 400 (not 500) for mixed SMART v1/v2 scopes#5650

Closed
mikaelweave wants to merge 2 commits into
mainfrom
mikaelweave-smart-mixed-scopes-bad-request
Closed

Return HTTP 400 (not 500) for mixed SMART v1/v2 scopes#5650
mikaelweave wants to merge 2 commits into
mainfrom
mikaelweave-smart-mixed-scopes-bad-request

Conversation

@mikaelweave

Copy link
Copy Markdown
Contributor

Problem (ICM)

A request whose token mixes SMART v1 scopes (e.g. patient/Observation.read) and SMART v2 granular scopes (e.g. patient/Observation.rs) returned HTTP 500 instead of 400.

Root cause

SmartClinicalScopesMiddleware detected the mixed-scope case and threw BadHttpRequestException. However, the middleware runs in the ASP.NET pipeline before MVC. The FhirException → 400 mapping (OperationOutcomeExceptionFilterAttribute) is an MVC action filter, so it never runs for exceptions thrown from middleware. The exception was instead caught by UseExceptionHandler("/CustomError") / BaseExceptionMiddleware, both of which default to 500.

The originally-proposed fix (swapping BadHttpRequestExceptionBadRequestException) does not fix this — any exception type thrown from the middleware still surfaces as a 500.

Fix

Write a 400 Bad Request OperationOutcome response directly from the middleware and short-circuit the pipeline (return without calling _next), mirroring the existing pattern in ThrottlingMiddleware. The body is a FHIR OperationOutcome with severity: error, code: invalid, and the existing MixedSMARTV1AndV2ScopesAreNotAllowed message.

Tests

  • Unit (SmartClinicalScopesMiddlewareTests): updated the mixed-scope theory to assert the middleware now returns 400 with an OperationOutcome body ("code":"invalid" + the message), instead of throwing.
  • E2E (SmartClinicalScopesValidationTests): acquires a real token for SmartUserClient with a mixed scope (patient/Patient.read patient/Patient.rs) from the dev token endpoint, issues a FHIR request, and asserts 400 BadRequest with an OperationOutcome. Skips when security is not enabled on the test server.

Verification

  • Microsoft.Health.Fhir.Api builds clean (net8.0 + net9.0).
  • SMART middleware unit tests pass (R4, net8.0 + net9.0).
  • E2E project compiles (both DLLs emitted; the only build error in my local env is the pinned-SDK post-build check, unrelated to this change).

Follow-up (out of ICM scope)

Two sibling BadHttpRequestException throws in the same middleware for the fhirUser claim cases have the identical latent 500 behavior. Left unchanged to keep this PR focused on the ICM; worth a follow-up.

Mixed SMART v1 and v2 clinical scopes on a request returned HTTP 500 instead of 400. SmartClinicalScopesMiddleware threw BadHttpRequestException, but because the middleware runs before MVC, the FhirException->400 mapping (OperationOutcomeExceptionFilterAttribute) never runs and the exception surfaced as a 500.

Fix: write a 400 OperationOutcome response directly from the middleware and short-circuit the pipeline, mirroring ThrottlingMiddleware. Adds unit and E2E tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@mikaelweave mikaelweave requested a review from a team as a code owner June 30, 2026 18:09
@codecov-commenter

codecov-commenter commented Jun 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.62%. Comparing base (55c3896) to head (bbb2ec0).
⚠️ Report is 11 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #5650      +/-   ##
==========================================
+ Coverage   77.51%   77.62%   +0.11%     
==========================================
  Files        1004     1004              
  Lines       36772    36873     +101     
  Branches     5563     5587      +24     
==========================================
+ Hits        28503    28624     +121     
+ Misses       6918     6893      -25     
- Partials     1351     1356       +5     

see 11 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

… 400

Replace the hand-written JSON response in SmartClinicalScopesMiddleware with the
canonical FHIR error flow: build an OperationOutcome and execute an
OperationOutcomeResult through the MVC result pipeline (the same approach
BaseExceptionMiddleware uses), so the 400 response goes through the FHIR output
formatters and content negotiation.

Add a protected internal virtual ExecuteResultAsync seam and update the unit
test to verify the produced OperationOutcomeResult, mirroring
BaseExceptionMiddlewareTests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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