Skip to content

Commit 504c992

Browse files
committed
fix(sof): honor Accept header on export status polls (406 for fhir+xml)
Per SoF PR HL7/sql-on-fhir#365 (Common Operation Behavior — Asynchronous Delivery), each status poll's headers govern that poll's response, including the 200 OK completion manifest. The status handler now rejects Accept: application/fhir+xml (without fhir+json) with 406 Not Acceptable + OperationOutcome, matching the run operations' behavior, instead of silently returning JSON.
1 parent bce395a commit 504c992

2 files changed

Lines changed: 55 additions & 0 deletions

File tree

crates/rest/src/handlers/sof/export.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ use helios_persistence::tenant::TenantContext;
4747
use helios_persistence::types::{
4848
SearchParamType, SearchParameter, SearchPrefix, SearchQuery, SearchValue,
4949
};
50+
use helios_sof::fhir_format::accept_requires_unsupported_fhir_xml;
5051
use serde::Deserialize;
5152
use serde_json::{Value, json};
5253

@@ -1018,10 +1019,24 @@ pub async fn get_export_status_handler<S>(
10181019
State(state): State<AppState<S>>,
10191020
tenant: TenantExtractor,
10201021
Path(job_id): Path<String>,
1022+
headers: HeaderMap,
10211023
) -> Result<Response, RestError>
10221024
where
10231025
S: ResourceStorage + Send + Sync + 'static,
10241026
{
1027+
// Spec Common Operation Behavior — Asynchronous Delivery: the `Accept`
1028+
// header on each poll governs that poll's response, including the
1029+
// completion manifest. The FHIR XML representation is not supported →
1030+
// 406, same as the run operations.
1031+
let accept = headers.get(header::ACCEPT).and_then(|v| v.to_str().ok());
1032+
if accept_requires_unsupported_fhir_xml(accept) {
1033+
return Err(RestError::NotAcceptable {
1034+
message: "the application/fhir+xml representation is not supported; \
1035+
use application/fhir+json"
1036+
.to_string(),
1037+
});
1038+
}
1039+
10251040
let controller = match state.export_controller() {
10261041
Some(c) => c,
10271042
None => {

crates/rest/tests/sof_export.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,46 @@ mod sof_export_tests {
177177
);
178178
}
179179

180+
/// `Accept: application/fhir+xml` (without fhir+json) on a status poll is
181+
/// not supported → `406 Not Acceptable` + OperationOutcome, same as the
182+
/// run operations. The poll's `Accept` header governs the manifest
183+
/// representation per Common Operation Behavior — Asynchronous Delivery.
184+
#[tokio::test]
185+
async fn test_export_status_poll_accept_fhir_xml_returns_406() {
186+
let (server, backend) = create_test_server_with_export().await;
187+
seed_patients(&backend).await;
188+
189+
let submit_resp = server
190+
.post("/ViewDefinition/$viewdefinition-export")
191+
.add_header(PREFER, "respond-async")
192+
.add_header(X_TENANT_ID, "test-tenant")
193+
.json(&patient_view())
194+
.await;
195+
assert_eq!(submit_resp.status_code(), StatusCode::ACCEPTED);
196+
197+
let location = submit_resp
198+
.headers()
199+
.get("content-location")
200+
.unwrap()
201+
.to_str()
202+
.unwrap()
203+
.to_string();
204+
205+
let poll = server
206+
.get(&location)
207+
.add_header(X_TENANT_ID, "test-tenant")
208+
.add_header(HeaderName::from_static("accept"), "application/fhir+xml")
209+
.await;
210+
assert_eq!(
211+
poll.status_code(),
212+
StatusCode::NOT_ACCEPTABLE,
213+
"{}",
214+
poll.text()
215+
);
216+
let body: Value = poll.json();
217+
assert_eq!(body["resourceType"], json!("OperationOutcome"));
218+
}
219+
180220
// =========================================================================
181221
// 3. Cancel → 202 Accepted, then poll → 404
182222
// =========================================================================

0 commit comments

Comments
 (0)