Skip to content

Fix $validate fatal 5003 on Bundles with resource-less entries - #5791

Merged
Brendan Kowitz (brendankowitz) merged 5 commits into
mainfrom
brendankowitz-validate-bundle-regression-test
Sep 4, 2026
Merged

Fix $validate fatal 5003 on Bundles with resource-less entries#5791
Brendan Kowitz (brendankowitz) merged 5 commits into
mainfrom
brendankowitz-validate-bundle-regression-test

Conversation

@brendankowitz

@brendankowitz Brendan Kowitz (brendankowitz) commented Sep 3, 2026

Copy link
Copy Markdown
Member

Description

$validate silently fails for any Bundle containing an entry with no resource child — e.g. a transaction DELETE entry that carries only a request — when the Bundle also contains a Reference. It returns HTTP 200 with a fatal issue in the body, so the failure is invisible to status-code and exception telemetry. Observed live: service 5.0.22All OK; 5.0.53 → fatal 5003.

Root cause. Hl7.Fhir.Base changed ScopedNode.BundledResources() between 5.11.4 and 5.11.5:

// 5.11.4 (SAFE)
let resource = e.Children("resource").FirstOrDefault() as ScopedNode

// 5.11.5+ (BUGGY)
entry.Children("resource").First().ToScopedNode()
// ...plus a .Single() in the versioned-entry branch

The .First() is unguarded, so resolving a reference forces the bundle cache to build and throws InvalidOperationException: Sequence contains no elements. The legacy validator (Hl7.Fhir.Validation.Legacy) converts that into a fatal OperationOutcome issue with details code 5003, "Internal logic failure: Sequence contains no elements".

Why the pin didn't protect us. Directory.Packages.props already set Hl7FhirVersion=5.11.4, but that was only partially effective. Microsoft.Health.Fhir.Core and the shared Microsoft.Health.Fhir.Api carry a direct PackageReference to Hl7.Fhir.Base, so CPM pinned them. The version-specific R4/R4B/R5/Stu3.Api projects do not — they only ProjectReference Microsoft.Health.Fhir.Ignixa, which pulled Ignixa.Extensions.FirelySdk5Hl7.Fhir.Base >= 5.13.1. With no direct reference there is nothing for CPM to pin, so transitive highest-wins applied and a single Hl7.Fhir.Base.dll at 5.13.1 landed in the output, *.Web shipping artifacts included.

(A direct PackageReference pins per project, not transitively — adding it only to *.Api would still leave each *.Web leaf on 5.13.1. Verified empirically.)

The fix. Ignixa.Extensions.FirelySdk5 0.6.74 lowers its declared floor to 5.11.4, so the existing pin becomes effective again — no NU1605 suppression, no downgrade conflict. The 0.6.x line renames three serialization members, so IgnixaImportResourceParser is updated to match:

Meta.LastUpdated              ->  Meta.LastUpdatedOffset   (Meta auto-materializes, so
                                                            `??= new MetaJsonNode()` drops out)
ResourceJsonNode.MutableNode  ->  ToSourceNavigator().Meta<JsonNode>()
ReferenceJsonNode             ->  read "reference" off the raw JsonObject

5 files changed:

file change
Directory.Packages.props IgnixaPackageVersion 0.0.1630.6.74
IgnixaImportResourceParser.cs three API renames, no behavioural change
ProfileValidatorTests.cs unit-level regression test
Bundle-TransactionWithResourcelessEntry.json the breaking payload, as a reusable sample
BundleEdgeCaseTests.cs E2E regression test

Related issues

Addresses AB#206224.

Testing

Two regression tests, at both layers.

UnitProfileValidatorTests.GivenABundleWithAnEntryThatHasNoResource_WhenValidating_ThenNoInternalLogicFailureIsReported, driving the existing public seam ProfileValidator.TryValidate(ITypedElement, string).

E2EBundleEdgeCaseTests.GivenABundleWithAnEntryThatHasNoResource_WhenValidated_ThenNoInternalLogicFailureIsReturned, POSTing the Bundle to Bundle/$validate through the real HTTP pipeline. This layer matters specifically because the defect surfaces inside a 200 response body — an E2E assertion is what actually reflects what a caller observes. It lives with the other bundle-shape edge cases and carries the Bundle category trait.

The breaking payload is checked in as a reusable sample, TestFiles/Normative/Bundle-TransactionWithResourcelessEntry.json, following the existing bundle-sample convention rather than being inlined as an escaped string. Normative is the fallback folder for every FHIR version, so one file serves STU3, R4, R4B and R5.

Both tests use the same shape: a Patient carrying a managingOrganization reference (which forces the bundle cache to build), the referenced Organization, and an entry with only a request (DELETE) and no resource.

Both assert the absence of the crash, not the absence of all issues — ordinary validation warnings and errors remain allowed, so neither test becomes brittle. Both live in shared test projects and compile for STU3, R4, R4B and R5.

Proven to fail on the defect and pass on the fix:

  • Before the bump (Hl7.Fhir.Base/5.13.1): the unit test fails on all four FHIR versions — Assert.DoesNotContain() Failure: Filter matched in collection. The E2E sample file was verified by loading it through Samples.GetJson and running it against the real ProfileValidator pinned to 5.13.1, producing exactly:
    Loaded sample, 1321 chars
    [Fatal/Exception] details='Internal logic failure: Sequence contains no elements'
    
  • After the bump: Microsoft.Health.Fhir.R4.Web and the R4 test project both resolve Hl7.Fhir.Base/5.11.4, output dll 5.11.4+3605c91f, and:
suite result
new unit test — STU3 / R4 / R4B / R5 passed
E2E project build — STU3 / R4 / R4B / R5 0 errors
ImportResourceParserParityTests 35 passed, 0 failed
all Import + Ignixa tests (R4) 52 passed, 0 failed
Microsoft.Health.Fhir.R4.Core.UnitTests (full) 1748 total, 0 failed, 1 skipped
Microsoft.Health.Fhir.R4B.Api.UnitTests (full) 1441 passed, 0 failed

The E2E test itself requires a deployed server (or a data store for the in-proc fixture), so it runs in CI rather than locally — but its sample file and assertion were verified end-to-end against the validator in both the defective and fixed configurations, as shown above.

The parser migration is covered by the existing parity suite, which asserts equivalence with the Firely parser — including the missing-meta case (GivenMissingMetaAndInvalidVersion_WhenParsedOnIncrementalLoad_ThenBothProvidersResetVersion), which is what makes dropping the MetaJsonNode initialisation safe.

FHIR Team Checklist

  • Update the title of the PR to be succinct and less than 65 characters
  • Add a milestone to the PR for the sprint that it is merged (i.e. add S47)
  • Tag the PR with the type of update: Bug, Build, Dependencies, Enhancement, New-Feature or Documentation
  • Tag the PR with Open source, Azure API for FHIR (CosmosDB or common code) or Azure Healthcare APIs (SQL or common code) to specify where this change is intended to be released.
  • Tag the PR with Schema Version backward compatible or Schema Version backward incompatible or Schema Version unchanged if this adds or updates Sql script which is/is not backward compatible with the code.
  • When changing or adding behavior, if your code modifies the system design or changes design assumptions, please create and include an ADR.
  • CI is green before merge Build Status
  • Review squash-merge requirements

Semver Change (docs)

Patch

Notes

  • The unguarded .First()/.Single() is still present upstream in FirelyTeam/firely-net-sdk develop, so upgrading forward does not help — the test guards against drifting back onto an affected version.
  • Only Hl7.Fhir.Base ≤ 5.11.4 is safe. v5.11.5, v5.11.7, v5.12.0 and v5.13.x all contain the unguarded call.
  • This overlaps with the larger Ignixa 0.6.x adoption in Implement incremental Ignixa SDK adoption and FHIRPath seam #5776; this PR is the minimal subset needed to ship the fix to main now, and makes the same functional edits to IgnixaImportResourceParser as that branch.

@brendankowitz
Brendan Kowitz (brendankowitz) force-pushed the brendankowitz-validate-bundle-regression-test branch 2 times, most recently from 529a6d3 to 5768e2b Compare September 3, 2026 22:17
@brendankowitz
Brendan Kowitz (brendankowitz) changed the base branch from main to brendankowitz-implement-ignixa-fhirpath-seam September 3, 2026 22:17
@brendankowitz Brendan Kowitz (brendankowitz) changed the title Add regression test guarding $validate against Hl7.Fhir.Base bundle-entry crash Guard $validate against Hl7.Fhir.Base bundle-entry crash (test + Ignixa 0.6.74) Sep 3, 2026
Copilot AI added 2 commits September 3, 2026 15:40
Ignixa.Extensions.FirelySdk5 0.0.163 declared a floor of Hl7.Fhir.Base 5.13.1.
The version-specific *.Api projects have no direct PackageReference to
Hl7.Fhir.Base, so Central Package Management has nothing to pin there and
transitive highest-wins floated Base up to 5.13.1 for those projects and the
*.Web artifacts built from them - defeating the Hl7FhirVersion=5.11.4 pin in
Directory.Packages.props.

Base 5.11.5 and later contain an unguarded .First() in
ScopedNode.BundledResources() that throws on a Bundle entry with no 'resource'
child (e.g. a transaction DELETE entry). The legacy validator surfaces that as
a fatal internal OperationOutcome issue inside an HTTP 200 $validate response,
so it is invisible to status-code and exception telemetry.

Ignixa 0.6.74 lowers its declared floor to 5.11.4, so the existing pin becomes
effective again. The 0.6.x line renames a few serialization members, so
IgnixaImportResourceParser is updated to match:

  Meta.LastUpdated             -> Meta.LastUpdatedOffset (Meta auto-materializes)
  ResourceJsonNode.MutableNode -> ToSourceNavigator().Meta<JsonNode>()
  ReferenceJsonNode            -> read "reference" from the raw JsonObject

Verified that Microsoft.Health.Fhir.R4.Web and the R4 unit test project both
resolve Hl7.Fhir.Base 5.11.4, and that the import parser parity suite still
passes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bd62eba7-d2c6-46de-8a39-85defbbb4b5a
Guards against Hl7.Fhir.Base regressing to a version whose
ScopedNode.BundledResources() calls .First()/.Single() on an entry's 'resource'
child without guarding entries that have none, such as a transaction DELETE
entry that carries only a request.

Because $validate returns HTTP 200 with the fatal outcome in the response body,
this failure is invisible to status-code and exception telemetry, so a
regression test is the only practical guard.

The test asserts only the absence of that catastrophic issue; ordinary
validation warnings and errors remain allowed so the test does not become
brittle.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bd62eba7-d2c6-46de-8a39-85defbbb4b5a
@brendankowitz
Brendan Kowitz (brendankowitz) force-pushed the brendankowitz-validate-bundle-regression-test branch from 5768e2b to 21700bc Compare September 3, 2026 22:40
@brendankowitz
Brendan Kowitz (brendankowitz) changed the base branch from brendankowitz-implement-ignixa-fhirpath-seam to main September 3, 2026 22:40
@brendankowitz
Brendan Kowitz (brendankowitz) marked this pull request as ready for review September 3, 2026 22:41
// Asser
Assert.DoesNotContain("cid-0", internalValidator.Settings.ConstraintsToIgnore ?? []);
}

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.

Can we add a test that covers the issue the customer ran into? A bundle being validated with resource entries that contained references shouldn't report validation errors.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I believe it was this { "request": { "method": "DELETE", "url": "Observation/no-such-id" } } hitting the unguarded entry.Children("resource").First().ToScopedNode() on https://github.com/FirelyTeam/firely-net-sdk/pull/3099/changes#diff-872a636c2e24971aa1d68c2aac5ba903d2b34b3f82f738ce74e534bbd8a86ec5R238-R239

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'll add the breaking payload to the bundle e2e tests too

@brendankowitz Brendan Kowitz (brendankowitz) added the Bug Bug bug bug. label Sep 3, 2026
@brendankowitz Brendan Kowitz (brendankowitz) changed the title Guard $validate against Hl7.Fhir.Base bundle-entry crash (test + Ignixa 0.6.74) Fix $validate fatal 5003 on Bundles with resource-less entries Sep 3, 2026
@brendankowitz Brendan Kowitz (brendankowitz) added Dependencies Pull requests that update a dependency file Open source This change is only relevant to the OSS code or release. Azure Healthcare APIs Label denotes that the issue or PR is relevant to the FHIR service in the Azure Healthcare APIs Azure API for FHIR Label denotes that the issue or PR is relevant to the Azure API for FHIR Schema Version unchanged labels Sep 3, 2026
@codecov-commenter

Codecov Comments Bot (codecov-commenter) commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.92%. Comparing base (8a49535) to head (87b5388).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #5791      +/-   ##
==========================================
- Coverage   78.93%   78.92%   -0.02%     
==========================================
  Files        1016     1016              
  Lines       36940    36910      -30     
  Branches     5619     5616       -3     
==========================================
- Hits        29158    29130      -28     
+ Misses       6403     6398       -5     
- Partials     1379     1382       +3     

see 10 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.

Complements the ProfileValidator unit test by exercising the full HTTP path.
The defect surfaces as a fatal issue inside an HTTP 200 response body, so an
end-to-end assertion is what actually reflects what a caller observes.

Posts a transaction Bundle to Bundle/$validate containing a Patient with a
managingOrganization reference, the referenced Organization, and a DELETE
entry that carries only a request and no resource. Asserts that the returned
OperationOutcome contains no "Internal logic failure" issue and no fatal issue
with details code 5003; ordinary validation warnings and errors remain allowed.

Verified that this exact payload produces
"[Fatal/Exception] Internal logic failure: Sequence contains no elements"
when run against Hl7.Fhir.Base 5.13.1, and passes at 5.11.4.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bd62eba7-d2c6-46de-8a39-85defbbb4b5a
Copilot AI added 2 commits September 4, 2026 08:39
Extracts the breaking payload into a shared sample file,
TestFiles/Normative/Bundle-TransactionWithResourcelessEntry.json, so it is
reusable and follows the existing bundle-sample convention rather than being
inlined as an escaped string in a test method. The Normative folder is the
fallback for every FHIR version, so the sample serves STU3, R4, R4B and R5.

Relocates the test from ValidateTests to BundleEdgeCaseTests, which is where
bundle-shape edge cases live and which carries the Bundle category trait.

Verified by loading the sample through Samples.GetJson and running it against
the real ProfileValidator: it reproduces
"[Fatal/Exception] Internal logic failure: Sequence contains no elements"
on Hl7.Fhir.Base 5.13.1, and passes on 5.11.4.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bd62eba7-d2c6-46de-8a39-85defbbb4b5a
BundleEdgeCaseTests is declared with Format.All, so moving the test into that
class also generated an Xml variant. The test posts the sample as a raw JSON
string, so the Xml argument set sent an Xml content type and failed with
"System.FormatException : Invalid Xml encountered. Details: Data at the root
level is invalid. Line 1, position 1."

HttpIntegrationFixtureArgumentSetsAttribute supports a method-level override,
already used elsewhere (for example in BulkDeleteTests and BulkUpdateTests), so
this narrows only this test rather than the whole class.

Verified via test discovery that this test now yields only the (CosmosDb, Json)
and (SqlServer, Json) variants, while neighbouring tests in the class still
generate all four Json and Xml combinations.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bd62eba7-d2c6-46de-8a39-85defbbb4b5a
@brendankowitz
Brendan Kowitz (brendankowitz) merged commit 5228336 into main Sep 4, 2026
49 checks passed
@brendankowitz
Brendan Kowitz (brendankowitz) deleted the brendankowitz-validate-bundle-regression-test branch September 4, 2026 22:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Azure API for FHIR Label denotes that the issue or PR is relevant to the Azure API for FHIR Azure Healthcare APIs Label denotes that the issue or PR is relevant to the FHIR service in the Azure Healthcare APIs Bug Bug bug bug. Dependencies Pull requests that update a dependency file No-ADR ADR not needed No-PaaS-breaking-change Open source This change is only relevant to the OSS code or release. Schema Version unchanged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants