Skip to content

rest/persistence: conditional criteria travel as a string — repeated keys collapse, & mis-splits, five parsers diverge #861

Description

@aacruzgon

Summary

The HTTP conditional handlers hand ConditionalStorage a k=v&k=v string rebuilt from Query<HashMap<String, String>>. Repeated keys collapse to one (last wins), a decoded value containing & mis-splits in the backend, parameter order is HashMap iteration order, and five copies of the string parser have drifted. Discussion #28 designs every conditional operation around typed &[SearchParameter] criteria; the string API is where that contract was never implemented.

Evidence

  • crates/rest/src/handlers/update.rs:338 (conditional_update_handler), delete.rs:243, patch.rs:182 extract axum::extract::Query<std::collections::HashMap<String, String>>. The rejoin is byte-identical in all three (update.rs:366-370, delete.rs:256-260, patch.rs:189-193):
    let search_params: String = query.iter().map(|(k, v)| format!("{}={}", k, v)).collect::<Vec<_>>().join("&");
  • axum 0.8.4 Query::try_from_uri deserializes via serde_urlencoded into the HashMap; repeated keys are inserted in turn, so ?identifier=a&identifier=b becomes {"identifier": "b"} with no error. FHIR gives repeated parameters AND semantics.
  • None of the three handlers reads Uri/RawQuery, parse_query_pairs (crates/rest/src/extractors/query_pairs.rs:12, which preserves repeated keys), or SearchParams::from_map (extractors/search_params.rs:79, whose doc names conditional queries as its intended caller and has no such caller).
  • Backends split with split('&') then splitn(2, '=') and never percent-decode, so a decoded & in a value mis-splits while = survives. Re-encoding on the way out is therefore not a fix.
  • Five copies of the parser: backends/sqlite/storage.rs:2706, backends/postgres/storage.rs:2646, composite/storage.rs:196, backends/mongodb/storage.rs:367 (parse_simple_bundle_search_params), backends/mongodb/search_impl.rs:179. The mongodb/storage.rs:367 copy trims and drops pairs with an empty key or value, so Patient?identifier= is one criterion on SQLite/PostgreSQL and zero on MongoDB; zero criteria makes the in-transaction matcher return no matches and a duplicate is created. parse_url is likewise copied three times (sqlite:3439, postgres:3377, mongodb:3470).

The batch path (#860) works around this by decoding once with parse_query_pairs and re-joining, which keeps repeated keys but shares the & limit.

Design reference

#28 Transaction::create_if_none_exist / update_conditional / delete_conditional, the REST handler trait's update_conditional / delete_conditional, and TransactionEntryRequest.if_none_exist: Option<Vec<SearchParameter>> all take typed parameters.

Proposed fix

Change ConditionalStorage (and the in-transaction matchers) to take &[(String, String)] or &[SearchParameter]; have the HTTP handlers use parse_query_pairs; delete the five string parsers and converge parse_url into helios_persistence::core next to bundle_if_match_gate. Tests: repeated-key criteria on each backend; Patient?identifier= answering the same on all backends.

Found while validating #511 (see PR #860).

Activity

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions