This is an automated email from the ASF dual-hosted git repository. Croway pushed a commit to branch worktree-fhir-auth-tx-fix in repository https://gitbox.apache.org/repos/asf/camel-spring-boot-examples.git
commit 081150e48753a96f8e053a617b1dc63bbfd37eb3 Author: croway <[email protected]> AuthorDate: Thu Sep 3 19:24:52 2026 +0200 Fix fhir-auth-tx: catch FHIR client connection failures, refresh docs The route only caught ProtocolException, but connection failures to the FHIR server actually surface as FhirClientConnectionException or RuntimeCamelException. Since those went uncaught, the file consumer never marked the input file as handled, causing it to be reprocessed on every poll forever. Broaden the onException clause to cover both, add a bounded redelivery policy with backoff, add a catch-all handler as a backstop, and set moveFailed on the file consumer as defense in depth. Also update the stale README, which still described the old hl7v2.patient/HL7v2 flow instead of the actual patients.csv + transaction Bundle flow, and pointed at a personal GitHub fork for the auth-enabled HAPI server. Replace it with a Docker Compose setup (HAPI FHIR server behind an nginx reverse proxy enforcing HTTP Basic auth) under fhir-auth-tx/containers, and point serverUrl at it. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_016WKaCqZQd9uUiQR6QMZJkJ --- fhir-auth-tx/containers/docker-compose.yml | 20 +++++++++ fhir-auth-tx/containers/nginx/htpasswd | 1 + fhir-auth-tx/containers/nginx/nginx.conf | 20 +++++++++ fhir-auth-tx/readme.adoc | 49 ++++++++++++++++------ .../src/main/java/sample/camel/MyCamelRouter.java | 26 ++++++++---- .../src/main/resources/application.properties | 2 +- 6 files changed, 98 insertions(+), 20 deletions(-) diff --git a/fhir-auth-tx/containers/docker-compose.yml b/fhir-auth-tx/containers/docker-compose.yml new file mode 100644 index 00000000..a444efbe --- /dev/null +++ b/fhir-auth-tx/containers/docker-compose.yml @@ -0,0 +1,20 @@ +services: + fhir: + image: hapiproject/hapi:v7.2.0 + container_name: fhir-auth-tx-server + environment: + - hapi.fhir.fhir_version=DSTU3 + - hapi.fhir.reuse_cached_search_results_millis=-1 + expose: + - "8080" + + fhir-auth-proxy: + image: nginx:1.27-alpine + container_name: fhir-auth-tx-proxy + depends_on: + - fhir + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/htpasswd:/etc/nginx/htpasswd:ro + ports: + - "8080:8080" diff --git a/fhir-auth-tx/containers/nginx/htpasswd b/fhir-auth-tx/containers/nginx/htpasswd new file mode 100644 index 00000000..200248e6 --- /dev/null +++ b/fhir-auth-tx/containers/nginx/htpasswd @@ -0,0 +1 @@ +admin:$apr1$fhirauth$ejUiLM/yipVqUKQ8hQ4i7/ diff --git a/fhir-auth-tx/containers/nginx/nginx.conf b/fhir-auth-tx/containers/nginx/nginx.conf new file mode 100644 index 00000000..c47c4e1a --- /dev/null +++ b/fhir-auth-tx/containers/nginx/nginx.conf @@ -0,0 +1,20 @@ +events { + worker_connections 1024; +} + +http { + server { + listen 8080; + + location / { + auth_basic "FHIR Server"; + auth_basic_user_file /etc/nginx/htpasswd; + + proxy_pass http://fhir:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } +} diff --git a/fhir-auth-tx/readme.adoc b/fhir-auth-tx/readme.adoc index 32abd064..33ac12f0 100644 --- a/fhir-auth-tx/readme.adoc +++ b/fhir-auth-tx/readme.adoc @@ -7,13 +7,21 @@ This is an example application of the `camel-fhir` component. We'll be using `ca The Camel route is located in the `MyCamelRouter` class. -This example will read patients stored in csv files from a directory and convert them to FHIR dtsu3 patients and upload them to a configured FHIR server. Each file is uploaded in a new transaction. +This example will read patients stored in csv files from a directory, convert each row to a FHIR dstu3 Patient and +upload all the patients found in one file as a single FHIR transaction Bundle to a configured FHIR server that +requires basic authentication. Each file is uploaded in a new transaction. + +If the FHIR server cannot be reached, or rejects the request, the route retries a few times with an increasing +delay before giving up and logging the error, so a single bad or unreachable server does not cause the same file +to be reprocessed forever. The example assumes you have a running FHIR server at your disposal, which is configured for basic authentication. -You may use https://github.com/rkorytkowski/hapi-fhir/tree/basic-auth/hapi-fhir-jpaserver-example[hapi-fhir-jpa-server-example]. You can start it up by running `mvn jetty:run`. +The `containers` folder in this example contains a `docker-compose.yml` that starts a plain +https://hub.docker.com/r/hapiproject/hapi[hapiproject/hapi] FHIR server behind an `nginx` reverse proxy configured +with HTTP Basic authentication, so you don't need a custom FHIR server build to try this example. -By default, the example uses `\http://localhost:8080/hapi-fhir-jpaserver-example/baseDstu3` as the FHIR server URL, DSTU3 as the FHIR version, BASIC authentication (`admin` as username and `Admin123` as password) and `target/work/fhir/input` -as the directory to look for csv patients. +By default, the example uses `\http://localhost:8080/fhir` as the FHIR server URL, DSTU3 as the FHIR version, BASIC authentication (`admin` as username and `Admin123` as password) and `target/work/fhir/input` +as the directory to look for csv patients. However, you can edit the `application.properties` file to change the defaults and provide your own configuration. @@ -29,36 +37,53 @@ $ mvn package === Run +Before running the application, start the auth-enabled FHIR server: + +```sh +$ docker compose -f containers/docker-compose.yml up -d +``` + +This starts two containers: the HAPI FHIR JPA server itself, and an `nginx` proxy in front of it that enforces +HTTP Basic authentication (`admin` / `Admin123`) and is published on `\http://localhost:8080`. The FHIR server +container itself is not published to the host, so every request has to go through the authenticated proxy. + You can run this example using: ```sh $ mvn spring-boot:run ``` -When the Camel application runs, you should see a folder created under `target/work/fhir/input`. Copy the file `hl7v2.patient` +When the Camel application runs, you should see a folder created under `target/work/fhir/input`. Copy the file `patients.csv` located in the `src/main/data` folder into it. You should see the following output: ``` -2018-07-24 11:52:51.615 INFO 30666 --- [work/fhir/input] fhir-example: Converting hl7v2.patient -2018-07-24 11:52:52.700 INFO 30666 --- [work/fhir/input] fhir-example: Inserting Patient: {"resourceType":"Patient","id":"100005056","name":[{"family":"Freeman","given":["Vincent"]}]} -2018-07-24 11:52:56.995 INFO 30666 --- [ #2 - CamelFhir] fhir-example: Patient created successfully: ca.uhn.fhir.rest.api.MethodOutcome@270f03f1 +2026-09-03 11:52:51.615 INFO 30666 --- [work/fhir/input] fhir-example: Converting patients.csv +2026-09-03 11:52:52.700 INFO 30666 --- [ #2 - CamelFhir] fhir-example: Patients created successfully: org.hl7.fhir.dstu3.model.Bundle@13651cde ``` The Camel application can be stopped pressing kbd:[Ctrl+c] in the shell. +Once you are done, stop the FHIR server containers: + +```sh +$ docker compose -f containers/docker-compose.yml down +``` + === To get health check -To show a summary of spring boot health check +The application binds to a random HTTP port (`server.port=0` in `application.properties`) so that it never +collides with the FHIR server on port 8080. Look for a line such as `Tomcat started on port(s): 54321 (http)` +in the console output to find the actual port, then show a summary of the spring boot health check with: ---- -curl -XGET -s http://localhost:8080/actuator/health +curl -XGET -s http://localhost:<port>/actuator/health ---- === Help and contributions -If you hit any problem using Camel or have some feedback, +If you hit any problem using Camel or have some feedback, then please https://camel.apache.org/support.html[let us know]. -We also love contributors, +We also love contributors, so https://camel.apache.org/contributing.html[get involved] :-) The Camel riders! diff --git a/fhir-auth-tx/src/main/java/sample/camel/MyCamelRouter.java b/fhir-auth-tx/src/main/java/sample/camel/MyCamelRouter.java index c3965bbe..e9bdfe0e 100644 --- a/fhir-auth-tx/src/main/java/sample/camel/MyCamelRouter.java +++ b/fhir-auth-tx/src/main/java/sample/camel/MyCamelRouter.java @@ -16,18 +16,17 @@ */ package sample.camel; -import java.util.ArrayList; import java.util.List; +import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException; import org.apache.camel.LoggingLevel; +import org.apache.camel.RuntimeCamelException; import org.apache.camel.builder.RouteBuilder; import org.apache.hc.core5.http.ProtocolException; import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.Identifier; import org.hl7.fhir.dstu3.model.Patient; -import org.hl7.fhir.dstu3.model.Resource; -import org.hl7.fhir.instance.model.api.IBaseResource; import org.springframework.stereotype.Component; /** @@ -40,11 +39,24 @@ public class MyCamelRouter extends RouteBuilder { @Override public void configure() throws Exception { - from("file:{{input}}").routeId("fhir-example") - .onException(ProtocolException.class) + // connection/protocol failures talking to the FHIR server: retry a few times with + // backoff, then give up on this file so it isn't reprocessed forever on every poll + onException(ProtocolException.class, FhirClientConnectionException.class, RuntimeCamelException.class) + .maximumRedeliveries(3) + .redeliveryDelay(2000) + .backOffMultiplier(2) + .useExponentialBackOff() .handled(true) - .log(LoggingLevel.ERROR, "Error connecting to FHIR server with URL:{{serverUrl}}, please check the application.properties file ${exception.message}") - .end() + .log(LoggingLevel.ERROR, "Error connecting to FHIR server with URL:{{serverUrl}}, please check the application.properties file and that the server is reachable ${exception.message}") + .end(); + + // anything else unexpected: log and move on instead of looping on the same file + onException(Exception.class) + .handled(true) + .log(LoggingLevel.ERROR, "Error processing ${file:name}: ${exception.message}") + .end(); + + from("file:{{input}}?moveFailed=.error").routeId("fhir-example") .log("Converting ${file:name}") .unmarshal().csv() .process(exchange -> { diff --git a/fhir-auth-tx/src/main/resources/application.properties b/fhir-auth-tx/src/main/resources/application.properties index dcb3aaf5..f34522b5 100644 --- a/fhir-auth-tx/src/main/resources/application.properties +++ b/fhir-auth-tx/src/main/resources/application.properties @@ -15,7 +15,7 @@ ## limitations under the License. ## --------------------------------------------------------------------------- -serverUrl=http://localhost:8080/hapi-fhir-jpaserver-example/baseDstu3 +serverUrl=http://localhost:8080/fhir serverUser=admin serverPassword=Admin123
