Skip to content

Report a filter a code system can't do as a 4xx, not 500 - #383

Merged
grahamegrieve merged 4 commits into
HealthIntersections:mainfrom
jmandel:fix/unsupported-filter-4xx
Nov 14, 2026
Merged

grahamegrieve merged 4 commits into
HealthIntersections:mainfrom
jmandel:fix/unsupported-filter-4xx

Conversation

@jmandel

@jmandel jmandel commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A $expand whose compose uses a filter the code system doesn't support gets an HTTP 500 and a stack trace in the log, as if the server had failed. The request is what's wrong, and the tx-ecosystem IG says so: "A filter the server does not understand, or a filter with no value where one is required, SHALL produce an error - not an empty or a partial expansion" (requirements). The cause: filter() in several providers throws a plain Error, and the $expand handler maps anything that isn't an Issue to 500. ($validate-code checks doesFilter() first and already answers 400 FILTER_NOT_UNDERSTOOD. $expand calls filter() directly.)

Repro, on a server with LOINC, SNOMED CT and RxNorm loaded:

curl -s -o /dev/null -w '%{http_code}\n' "$B/ValueSet/\$expand" -H content-type:application/fhir+json --data-binary '{"resourceType":"Parameters","parameter":[{"name":"valueSet","resource":{"resourceType":"ValueSet","status":"active","compose":{"include":[{"system":"http://loinc.org","filter":[{"property":"LOINC_NUM","op":"regex","value":".*"}]}]}}},{"name":"count","valueInteger":10}]}'
filter before (main 482ea83) after
LOINC LOINC_NUM regex .* 500 422 not-supported, "The filter "LOINC_NUM regex .*" is not understood or supported"
SNOMED CT code regex 1234.* 500 ("Invalid concept ID: 1234.*") 422 not-supported, "The filter "concept regex 1234.*" is not understood or supported"
RxNorm code regex 1.* 500 422 not-supported, "The filter "code regex 1.*" is not understood or supported"
BCP-47 language regex en 500 (inferred: same plain Error; not measured) 422 not-supported, "The filter "language regex en" is not understood or supported"
SNOMED CT concept = 1234.*, inactive = maybe 500 500, unchanged

Change

Where filter() turns down a property or operator in LOINC, SNOMED CT, RxNorm, NDC, CPT, UCUM, OMOP, mime types, BCP-47, HGVS and the country, currency and area code providers, it now throws the issue cs-cs.js throws for a filter it doesn't understand, with the translated message:

throw new Issue('error', 'not-supported', null, 'FILTER_NOT_UNDERSTOOD',
  this.opContext.i18n.translate('FILTER_NOT_UNDERSTOOD', this.opContext.langs, [prop, op, value]), 'vs-invalid', 422);

cs-cs.js and cs-icf.js used issue code exception for this; they now use not-supported. ICD-11's existing refusal now uses the translated message instead of hard-coded English.

HGVS threw "Filters are not supported" from getPrepContext(), which $expand calls before filter(). It now inherits the base getPrepContext(), and filter() refuses the filter.

SNOMED CT's concept, inactive, moduleId and attribute filters checked the value before the operator, so concept regex 1234.* failed as "Invalid concept ID". They now check the operator first.

Bad filter values (e.g. an unknown or malformed SNOMED concept id) are left as they are: whether they are an error or dropped is being discussed (Filter on an invalid Concept, and #357). So SNOMED CT "Invalid concept ID" and "Invalid filter value", country "Invalid regex pattern", an invalid mime type base, an unknown BCP-47 subtag and BCP-47 exists with a value other than true/false still throw the plain Error they throw on main.

LOINC, RxNorm, NDC, CPT, OMOP, currency and area code each have one catch-all that turns down any filter they can't do, and that becomes the 422. The 422 is the status $expand already uses for this error (ICD-11, cs-cs.js, supplement filters); $validate-code still answers 400 from its doesFilter() check. Only the error paths in filter() change. ECL errors (constraint =) and the other plain Errors in these files aren't touched.

Tests

  • The existing "unsupported filter" tests for LOINC, NDC, CPT, OMOP, BCP-47, HGVS, country, currency and area code now check cause: 'not-supported', msgId: 'FILTER_NOT_UNDERSTOOD', statusCode: 422 and the translated message.
  • New:
    • a SNOMED CT test: unsupported filters (concept exists, concept regex 1234.*, inactive in / regex, moduleId regex, an attribute with is-a, an unknown property) give 422 not-supported
    • an RxNorm test (STY regex) in cs-rxnorm-filters
    • UCUM and mime types: an unsupported property and an unsupported operator give 422 not-supported with the translated message; BCP-47: variant = x
    • cs-cs: an unknown property gives 422 not-supported
    • HTTP tests in tests/tx/expand.test.js: a country display regex and a BCP-47 language regex filter give 422 not-supported with the translated message
  • The 16 touched suites: 924 tests pass (node v24.16.0).
  • All of tests/cs: 41 suites passed, 5 skipped (1,547 tests passed, 80 skipped).
  • All of tests/tx: 131 suites, 6,970 tests passed, 7 skipped.
  • tx-ecosystem test-cases.test.js: 5,251 passed.

🤖 Generated with Claude Code

LOINC, SNOMED CT, RxNorm, NDC, CPT and the country, currency and area
code providers threw a plain Error from filter() for a filter they
don't support or a filter value they can't use (e.g. LOINC_NUM regex,
SNOMED CT code regex). $expand reported that as a 500 with a stack
trace.

A property or operator a provider doesn't support now throws the Issue
the ICD-11 provider does (not-supported, FILTER_NOT_UNDERSTOOD, 422).
A supported filter whose value can't be used (SNOMED CT "Invalid
concept ID" / "Invalid filter value", country "Invalid regex pattern")
is 400 invalid / vs-invalid, as for an unknown SNOMED CT concept in a
filter. The messages are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Whether a filter value naming an unknown or malformed concept is an
error or is dropped is still open, so SNOMED CT "Invalid concept ID" /
"Invalid filter value" and country "Invalid regex pattern" go back to
the plain Error they are on main.

SNOMED CT checked the value before the operator, so an unsupported
operator with a value that isn't a concept id (concept regex 1234.*)
was still a 500. The concept, inactive, moduleId and attribute filters
now turn down an operator they don't support before looking at the
value: 422 not-supported, FILTER_NOT_UNDERSTOOD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jmandel

jmandel commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

I've narrowed this PR to the part nobody disputes: a filter property or operator a code system doesn't support now gets 422 not-supported FILTER_NOT_UNDERSTOOD, the same as ICD-11, where it used to be a 500. The tx-ecosystem IG requires an error for a filter the server doesn't understand.
I dropped the 400 for bad filter values (an unknown or malformed SNOMED CT concept id, a bad country regex), since whether those are an error or dropped is still open in Filter on an invalid Concept. They behave as they do on main. SNOMED CT now checks the operator before the value, so concept regex 1234.* gets the 422 instead of failing as a bad concept id.

@grahamegrieve

Copy link
Copy Markdown
Contributor

Thanks, the direction is right, and doing the SNOMED CT operator check before the value is a good catch. A few things before merging:

  1. Use the standard message. These now carry message id FILTER_NOT_UNDERSTOOD but hard-coded English text. Everywhere else that raises FILTER_NOT_UNDERSTOOD (expand.js, validate.js, cs-cs.js) uses the translated message from the messages file, so the client gets the expected text in its language. Please do the same through this.opContext.i18n.translate('FILTER_NOT_UNDERSTOOD', this.opContext.langs, [prop, op, value]), as cs-cs.js does.

  2. Cover the other providers too. These still throw a plain Error, so a 500, for a filter they can't do: UCUM (unsupported property and operator), OMOP ("not understood"), mime types (unsupported property and operator), BCP-47 (cs-lang.js) and HGVS ("Filters are not supported"). It's the same one-line change in each, and it makes the fix complete.

  3. Status code. $validate-code refuses an unsupported filter with 400 (validate.js), while $expand uses 422 (here, ICD-11, cs-cs.js and the supplement filter path). I'll settle on one for this error and add tx-ecosystem tests for an unsupported filter on $expand. Keep 422 for now, and I'll let you know if it changes. Also, while you're in cs-cs.js: it uses issue code exception for this. It should be not-supported, like the rest.

Leaving bad filter values (an unknown concept id) as they are is right, until the chat.fhir.org discussion settles it.

jmandel and others added 2 commits November 14, 2026 07:30
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Every provider that refuses a filter it can't do now reports
  FILTER_NOT_UNDERSTOOD with the message from the messages file
  (i18n.translate with prop, op, value), not hard-coded English
- UCUM, OMOP, mime types, BCP-47 and HGVS now refuse an unsupported
  property or operator as a 422 not-supported issue, not a plain Error
  (500). HGVS inherits the base getPrepContext so its filter() is reached
- cs-cs.js and cs-icf.js: issue code not-supported instead of exception
- ICD-11's existing refusal uses the translated message too

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jmandel

jmandel commented Nov 14, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. Done; upstream main merged in (merge commit), new commits on top.

  1. Every provider that raises FILTER_NOT_UNDERSTOOD now uses this.opContext.i18n.translate('FILTER_NOT_UNDERSTOOD', this.opContext.langs, [prop, op, value]), so the text is The filter "LOINC_NUM regex .*" is not understood or supported (or its translation). ICD-11's existing refusal had hard-coded English too, so I changed it the same way.

  2. UCUM (property, operator), OMOP, mime types (property, operator), BCP-47 (= on a component it can't compare, other operators, unknown property) and HGVS now throw the same 422 not-supported issue. HGVS also threw from getPrepContext(), which $expand calls before filter(), so it now inherits the base getPrepContext() and refuses in filter(). Bad values (malformed mime base, unknown BCP-47 subtag, exists maybe) are left as they are.

  3. Kept 422. cs-cs.js now uses not-supported instead of exception; cs-icf.js had the same exception code for this error, so I changed that too.

Tests: each newly covered provider has a test for an unsupported filter (422, not-supported, the translated text), plus cs-cs and an HTTP $expand test on BCP-47. All of tests/cs and tests/tx pass, and the tx-ecosystem tests (5,251).

@grahamegrieve grahamegrieve 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.

ok thanks

@grahamegrieve
grahamegrieve merged commit 06218ee into HealthIntersections:main Nov 14, 2026
4 of 5 checks passed
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.

2 participants