Summary
The Import page polls the recipient's GET /bulk-submit-status/{id} with the static HFS_OUTBOUND_BEARER_TOKEN. When that token expires mid-import (Keycloak's shipped realm gives it 300 s), the next poll answers 401 and the page marks the submission failed and stops polling for good, while the recipient — the same HFS process — keeps ingesting to completion. The card then reads Result · Processing finished at <time of the 401> · Output files 0 · Error files 0 · Failed, which is false on every count: the ingest is neither finished nor failed, and its files are still being written.
Environment
main 0eb99c238, HFS_STORAGE_BACKEND=sqlite-es, release build, Keycloak 26.1.5 with the repo realm.json (accessTokenLifespan 300), HFS_AUTH_ENABLED=true, HFS_OUTBOUND_BEARER_TOKEN=$(./docker/keycloak/get-token.sh) minted at 21:33:52Z, no interactive login. Submission created from the Import page (auth=none) at 21:33:56Z for the full 18.96 M Synthea corpus (#1171 T3).
What happens
Submission log on /ui/bulk-import/3ad87fb2-e899-422a-9574-41fc2063488a, newest first:
2026-09-28T21:41:07.810Z Status poll answered 401; polling stopped and the submission is marked failed.
2026-09-28T21:39:07.547Z Status: Processing 4% - 702,191 Resources written
2026-09-28T21:37:07.514Z Status: Processing 2% - 510,391 Resources written
2026-09-28T21:34:22.483Z Status: Processing 0% - 170,891 Resources written
2026-09-28T21:33:56.368Z Status: Queued - starting shortly
2026-09-28T21:33:56.350Z Bulk status kick-off request
2026-09-28T21:33:56.349Z Manifest accepted by the recipient (200).
Status card afterwards: Result — Processing finished at 2026-09-28T21:41:07.810Z, Output files 0, Error files 0, summary Status: Failed.
Server log, the poll that killed it (token exp 21:38:52Z; the 21:39:07 poll still passed inside the clock-skew leeway):
DEBUG request{method=GET uri=/bulk-submit-status/7fb981dd-ecb7-4b66-873d-82af27ab7e10}: tower_http::trace::on_response: finished processing request latency=0 ms status=401
Meanwhile the recipient is fine — GET /bulk-submit-status/7fb981dd-… with a freshly minted bearer a minute later:
HTTP/1.1 202 Accepted
x-progress: Processing 5% - 926,391 Resources written
retry-after: 120
(202 = still in progress, per the Bulk Data async pattern)
and the ingest goes on (the sample at 21:41 already had 925 MB in hfs.db and 8.3 M Elasticsearch documents; it is still running as this is filed).
Expected
A 401 on the status poll is a credential problem of the page, not a result of the submission. Either the poll re-mints / refreshes its bearer (the Import page already knows how to obtain one for auth=backend-services; for auth=none with HFS_OUTBOUND_BEARER_TOKEN a static token cannot be refreshed, so at least keep polling with backoff and say status unavailable: outbound token rejected (401) instead of a terminal Failed), or the card must not claim Processing finished at / Failed for a submission whose recipient never reported either. The same applies to a token that is revoked or rotated.
Related
Summary
The Import page polls the recipient's
GET /bulk-submit-status/{id}with the staticHFS_OUTBOUND_BEARER_TOKEN. When that token expires mid-import (Keycloak's shipped realm gives it 300 s), the next poll answers 401 and the page marks the submission failed and stops polling for good, while the recipient — the same HFS process — keeps ingesting to completion. The card then reads Result · Processing finished at <time of the 401> · Output files 0 · Error files 0 · Failed, which is false on every count: the ingest is neither finished nor failed, and its files are still being written.Environment
main0eb99c238,HFS_STORAGE_BACKEND=sqlite-es, release build, Keycloak 26.1.5 with the reporealm.json(accessTokenLifespan300),HFS_AUTH_ENABLED=true,HFS_OUTBOUND_BEARER_TOKEN=$(./docker/keycloak/get-token.sh)minted at 21:33:52Z, no interactive login. Submission created from the Import page (auth=none) at 21:33:56Z for the full 18.96 M Synthea corpus (#1171 T3).What happens
Submission log on
/ui/bulk-import/3ad87fb2-e899-422a-9574-41fc2063488a, newest first:Status card afterwards: Result — Processing finished at 2026-09-28T21:41:07.810Z, Output files 0, Error files 0, summary Status: Failed.
Server log, the poll that killed it (token
exp21:38:52Z; the 21:39:07 poll still passed inside the clock-skew leeway):Meanwhile the recipient is fine —
GET /bulk-submit-status/7fb981dd-…with a freshly minted bearer a minute later:(202 = still in progress, per the Bulk Data async pattern)
and the ingest goes on (the sample at 21:41 already had 925 MB in
hfs.dband 8.3 M Elasticsearch documents; it is still running as this is filed).Expected
A 401 on the status poll is a credential problem of the page, not a result of the submission. Either the poll re-mints / refreshes its bearer (the Import page already knows how to obtain one for
auth=backend-services; forauth=nonewithHFS_OUTBOUND_BEARER_TOKENa static token cannot be refreshed, so at least keep polling with backoff and say status unavailable: outbound token rejected (401) instead of a terminal Failed), or the card must not claim Processing finished at / Failed for a submission whose recipient never reported either. The same applies to a token that is revoked or rotated.Related
auth=nonesubmits with no bearer, and the status poll is unauthenticated even in backend-services mode #1436 (the poll carried no bearer at all — closed by fix(metadata): advertise SMART-on-FHIR in rest.security when auth is enabled #1442; this is the follow-up where it carries an expired one).HFS_OUTBOUND_BEARER_TOKENcannot outlive the IdP's access token lifespan; feat(ui): run page-initiated exports and SQL exports as the signed-in user #1488 discusses the realm side.