diff --git a/.github/scripts/delete-history.sh b/.github/scripts/delete-history.sh index 736047ccc7..2db5b78d95 100755 --- a/.github/scripts/delete-history.sh +++ b/.github/scripts/delete-history.sh @@ -63,7 +63,7 @@ echo "▶️create patient" patient_id=$(create | jq -r '.id') # update the patient to create a second version -patient "$patient_id" "male" | update "$patient_id" +patient "$patient_id" "male" | update "$patient_id" > /dev/null # expect the history to contain two entries history="$(get_instance_history "$patient_id")" diff --git a/.github/scripts/evaluate-measure-subject-list-as-batch.sh b/.github/scripts/evaluate-measure-subject-list-as-batch.sh index c622c1467b..e753395fdc 100755 --- a/.github/scripts/evaluate-measure-subject-list-as-batch.sh +++ b/.github/scripts/evaluate-measure-subject-list-as-batch.sh @@ -118,7 +118,7 @@ base="http://localhost:8080/fhir" name=$1 expected_count=$2 -data=$(base64 < "modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/$name.cql" | tr -d '\n') +data=$(base64 -w 0 < "modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/$name.cql") library_uri=$(uuidgen | tr '[:upper:]' '[:lower:]') measure_uri=$(uuidgen | tr '[:upper:]' '[:lower:]') diff --git a/.github/scripts/evaluate-measure-util.sh b/.github/scripts/evaluate-measure-util.sh index 7fe5cb693c..60bd830d6e 100755 --- a/.github/scripts/evaluate-measure-util.sh +++ b/.github/scripts/evaluate-measure-util.sh @@ -97,7 +97,7 @@ create_measure() { create_bundle_library_measure() { local measure_uri="$1" local name="$2" - local data="$(base64 < "modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/$name.cql" | tr -d '\n')" + local data="$(base64 -w 0 < "modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/$name.cql")" local library_uri=$(uuidgen | tr '[:upper:]' '[:lower:]') local library="$(create_library "$library_uri" "$data")" local measure="$(create_measure "$measure_uri" "$library_uri")" diff --git a/.github/scripts/evaluate-patient-q1-measure.sh b/.github/scripts/evaluate-patient-q1-measure.sh index fd4b1c3b4e..e17deac14e 100755 --- a/.github/scripts/evaluate-patient-q1-measure.sh +++ b/.github/scripts/evaluate-patient-q1-measure.sh @@ -77,7 +77,7 @@ evaluate-measure() { base="http://localhost:8080/fhir" file="modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/q1.cql" -data=$(base64 "$file" | tr -d '\n') +data=$(base64 -w 0 < "$file") library_uri=$(uuidgen | tr '[:upper:]' '[:lower:]') measure_uri=$(uuidgen | tr '[:upper:]' '[:lower:]') diff --git a/.github/scripts/follow-single-patient-history-next-link-after-write.sh b/.github/scripts/follow-single-patient-history-next-link-after-write.sh index a4175dc4b8..591a678745 100755 --- a/.github/scripts/follow-single-patient-history-next-link-after-write.sh +++ b/.github/scripts/follow-single-patient-history-next-link-after-write.sh @@ -38,14 +38,14 @@ update() { patient_id=$(create | jq -r '.id') # update the patient to create a second version -patient "$patient_id" "male" | update "$patient_id" +patient "$patient_id" "male" | update "$patient_id" > /dev/null first_page="$(curl -sfH "Accept: application/fhir+json" "$base/Patient/$patient_id/_history?_count=1")" total="$(echo "$first_page" | jq -r .total)" next_link="$(echo "$first_page" | jq -r '.link[] | select(.relation == "next") | .url')" # update the patient to create a third version -patient "$patient_id" "female" | update "$patient_id" +patient "$patient_id" "female" | update "$patient_id" > /dev/null second_page="$(curl -sfH "Accept: application/fhir+json" "$next_link")" diff --git a/.github/scripts/read-binary-content-via-json-found.sh b/.github/scripts/read-binary-content-via-json-found.sh index 0c1a2e5ea9..8de6ddab2f 100755 --- a/.github/scripts/read-binary-content-via-json-found.sh +++ b/.github/scripts/read-binary-content-via-json-found.sh @@ -7,7 +7,7 @@ set -euo pipefail base="http://localhost:8080/fhir" # 10 KiB of random data, base64 encoded -data="$(openssl rand -base64 10240 | tr -d '\n')" +data="$(openssl rand 10240 | base64 -w 0)" binary() { cat < --json milestone`). If it's unclear whether a behaviour change is user-facing, ask rather than guess. ## Release Notes diff --git a/docs/api/interaction/read.md b/docs/api/interaction/read.md index d88dbedc48..9fd8b11848 100644 --- a/docs/api/interaction/read.md +++ b/docs/api/interaction/read.md @@ -16,6 +16,19 @@ ETag: W/"23" The version id in the `ETag` header is identical to `meta.versionId` of the returned resource and can be used in an `If-Match` header of a subsequent [update](update.md) to detect concurrent modifications. +## Binary Resources + +`Binary` resources can be read either as FHIR resource or in binary form. Blaze decides based on the `Accept` header of the request: + +* `application/fhir+json` and `application/fhir+xml` (as well as `*/*` or a missing `Accept` header) return the `Binary` resource itself, with its content Base64 encoded in `Binary.data`, +* every other media type returns the raw content of `Binary.data`. + +```sh +curl -H 'Accept: application/pdf' "http://localhost:8080/fhir/Binary/AT4S2E5FQTPTIQPP" +``` + +The `Content-Type` header of such a binary response is taken from `Binary.contentType`, defaulting to `application/octet-stream` if that property is missing. Blaze doesn't match it against the media types requested in the `Accept` header. + ## Handling Errors | Status Code | Description | diff --git a/docs/api/interaction/vread.md b/docs/api/interaction/vread.md index 0038109a54..d47627b197 100644 --- a/docs/api/interaction/vread.md +++ b/docs/api/interaction/vread.md @@ -14,6 +14,19 @@ Last-Modified: Tue, 24 Jun 2025 09:03:22 GMT ETag: W/"23" ``` +## Binary Resources + +As with the [read](read.md) interaction, every version of a `Binary` resource can be retrieved either as FHIR resource or in binary form. Blaze decides based on the `Accept` header of the request: + +* `application/fhir+json` and `application/fhir+xml` (as well as `*/*` or a missing `Accept` header) return the `Binary` resource itself, with its content Base64 encoded in `Binary.data`, +* every other media type returns the raw content of `Binary.data`. + +```sh +curl -H 'Accept: application/pdf' "http://localhost:8080/fhir/Binary/AT4S2E5FQTPTIQPP/_history/2" +``` + +The `Content-Type` header of such a binary response is taken from `Binary.contentType` of that version, defaulting to `application/octet-stream` if that property is missing. Blaze doesn't match it against the media types requested in the `Accept` header. + ## Handling Errors | Status Code | Description | diff --git a/evaluate-measure.sh b/evaluate-measure.sh index 2318aa9c7c..c015ec4a96 100755 --- a/evaluate-measure.sh +++ b/evaluate-measure.sh @@ -123,7 +123,7 @@ base=$1 [[ -z "$base" ]] && usage subject_type_lower=$(echo $subject_type | tr '[:upper:]' '[:lower:]') -data=$(base64 < "$file" | tr -d '\n') +data=$(base64 -w 0 < "$file") library_uri=$(uuidgen | tr '[:upper:]' '[:lower:]') measure_uri=$(uuidgen | tr '[:upper:]' '[:lower:]') diff --git a/modules/rest-api/src/blaze/rest_api/routes.clj b/modules/rest-api/src/blaze/rest_api/routes.clj index ce63f3d9d7..aa266e981f 100644 --- a/modules/rest-api/src/blaze/rest_api/routes.clj +++ b/modules/rest-api/src/blaze/rest_api/routes.clj @@ -214,6 +214,8 @@ :blaze.rest-api.interaction/handler)}))] ["/{vid}" (cond-> {:name (keyword name "versioned-instance")} + (= name "Binary") + (assoc :response-type :binary) (contains? interactions :vread) (assoc :get {:interaction "vread" :middleware [[wrap-db node db-sync-timeout]] diff --git a/modules/rest-api/test/blaze/rest_api_test.clj b/modules/rest-api/test/blaze/rest_api_test.clj index dfba3218d2..bce533a4f1 100644 --- a/modules/rest-api/test/blaze/rest_api_test.clj +++ b/modules/rest-api/test/blaze/rest_api_test.clj @@ -426,6 +426,52 @@ (given (call rest-api {:request-method :delete :uri "/Patient"}) :status := 200))) +(def ^:private binary-handler + (constantly + (ac/completed-future + (ring/response + {:fhir/type :fhir/Binary :id "0" + :contentType #fhir/code"application/zip" + :data #fhir/base64Binary"UEsDBAo="})))) + +(def ^:private binary-config + (assoc config + ::rest-api/resource-patterns + {:default + {:read #:blaze.rest-api.interaction{:handler binary-handler} + :vread #:blaze.rest-api.interaction{:handler binary-handler}}})) + +(deftest binary-test + (testing "read" + (with-system [{:blaze/keys [rest-api]} binary-config] + (testing "with binary accept header" + (given (call rest-api {:request-method :get :uri "/Binary/0" + :headers {"accept" "application/zip"}}) + :status := 200 + [:headers "Content-Type"] := "application/zip" + [:body vec] := [0x50 0x4B 0x03 0x04 0x0A])) + + (testing "with FHIR accept header" + (given (call rest-api {:request-method :get :uri "/Binary/0" + :headers {"accept" "application/fhir+json"}}) + :status := 200 + [:headers "Content-Type"] := "application/fhir+json;charset=utf-8")))) + + (testing "vread" + (with-system [{:blaze/keys [rest-api]} binary-config] + (testing "with binary accept header" + (given (call rest-api {:request-method :get :uri "/Binary/0/_history/1" + :headers {"accept" "application/zip"}}) + :status := 200 + [:headers "Content-Type"] := "application/zip" + [:body vec] := [0x50 0x4B 0x03 0x04 0x0A])) + + (testing "with FHIR accept header" + (given (call rest-api {:request-method :get :uri "/Binary/0/_history/1" + :headers {"accept" "application/fhir+json"}}) + :status := 200 + [:headers "Content-Type"] := "application/fhir+json;charset=utf-8"))))) + (def auth-config (-> (assoc-in config [:blaze/rest-api :auth-backends] [(ig/ref ::auth-backend)]) (assoc ::auth-backend {})))