This page contains example interceptors that can be registered with Subscription.
The following interceptor can be used to add any authorization header, like Basic Auth or Bearer token, before calling a REST hook deliverer for each resource delivery message.
/*-
* #%L
* Smile CDR - CDR
* %%
* Copyright (C) 2016 - 2026 Smile CDR, Inc.
* %%
* All rights reserved.
* #L%
*/
package com.smilecdr.demo.subscription;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
import ca.uhn.fhir.jpa.subscription.model.ResourceDeliveryMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
/**
* This class is a Hook Interceptor that is invoked immediately before REST HOOK
* deliveries. It demonstrates a simple OAuth2 Client Credentials flow to dynamically
* retrieve a Bearer token and inject it as an Authorization header
*/
@Interceptor
public class SubscriptionRestHookDynamicHeaderInterceptor {
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionRestHookDynamicHeaderInterceptor.class);
// TODO: Replace with your OAuth2 token endpoint URL
private static final String TOKEN_ENDPOINT = "https://your-auth-server.example.com/oauth2/token";
// TODO: Replace with your OAuth2 client_id
private static final String CLIENT_ID = "your-client-id";
// TODO: Replace with your OAuth2 client_secret
private static final String CLIENT_SECRET = "your-client-secret";
private final RestTemplate myRestTemplate = new RestTemplate();
/**
* The {@link Pointcut#SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY} is invoked immediately before each
* REST HOOK subscription delivery.
*
* @param theDeliveryMessage This object contains details about the subscription invocation, including
* the contents of the resource that will be delivered.
* @param theSubscription This object contains details about the subscription, including the delivery endpoint,
* headers to add, etc. A new copy of this object is created for every individual delivery
* so it is fine to modify it, and this will not affect other deliveries or
* subscriptions.
* @return Return <code>true</code> if the subscription should proceed, or <code>false</code> if it
* should be aborted.
*/
@Hook(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY)
public boolean beforeRestHookDelivery(
ResourceDeliveryMessage theDeliveryMessage, CanonicalSubscription theSubscription) {
try {
final String accessToken = fetchAccessToken();
theSubscription.addHeader("Authorization: Bearer " + accessToken);
} catch (Exception theException) {
ourLog.error("Failed to fetch access token from {}; aborting subscription delivery", TOKEN_ENDPOINT, theException);
return false;
}
return true;
}
/**
* Performs an OAuth2 Client Credentials grant request
* and returns the raw access token string. This example is using
* client credentials secret flow but it can be updated to use
* client credentials jwt assertion flow
*/
@SuppressWarnings("unchecked")
protected String fetchAccessToken() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setBasicAuth(CLIENT_ID, CLIENT_SECRET);
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "client_credentials");
Map<String, Object> response = myRestTemplate.postForObject(
TOKEN_ENDPOINT, new HttpEntity<>(body, headers), Map.class);
if (response == null || !response.containsKey("access_token")) {
throw new RuntimeException("Token endpoint response did not contain an access_token field");
}
return (String) response.get("access_token");
}
}
An interceptor on the SUBSCRIPTION_BEFORE_MESSAGE_DELIVERY pointcut can add custom headers to the messages that a MESSAGE subscription delivers to its channel. They arrive as Kafka record headers, Pulsar message properties, or ActiveMQ JMS message properties. See Custom Header Delivery for an example interceptor and the headers it produces.
To group messages, set their message key with an interceptor: messages with the same key are processed in the order they were sent. The key can be any value your interceptor computes from the message, such as a patient identifier, an encounter, or a value from an extension. This works for every subscription channel type, including REST hook and MESSAGE subscriptions. How each message broker keeps messages with the same key in order is described in Message Ordering and Partition Keys.
A subscription message passes through two queues: the matching queue, where it is checked against the active subscriptions, and the delivery queue, where it waits to be delivered. The message key has to be set for each queue, so this example uses two interceptors: one for the matching queue and one for the delivery queue.
This example uses the MRN of each Patient resource as the key, so messages for Patient resources that share an MRN are kept in order. To group messages another way, replace getMessageKey in both interceptors. Both must return the same key for a message; otherwise messages can be processed out of order.
The following interceptor sets the key before the message is sent to the matching queue. Register it in the FHIR Storage module:
/**
* Sets the message key of each subscription message before it is sent to the matching queue, so that messages
* with the same key are matched in order. This example uses the patient's MRN as the key; replace
* {@link #getMessageKey(IBaseResource)} to group messages another way, and make the same change in
* {@link DeliveryQueueMessageKeyInterceptor}. Register it in the FHIR Storage module.
*/
@Interceptor
public class MatchingQueueMessageKeyInterceptor {
// TODO: Replace with the identifier system of your MRNs
private static final String MRN_SYSTEM = "http://example.org/mrn";
private FhirContext myFhirContext;
@Autowired
public void setFhirContext(FhirContext theFhirContext) {
myFhirContext = theFhirContext;
}
@Hook(Pointcut.SUBSCRIPTION_RESOURCE_MODIFIED)
public void resourceModified(ResourceModifiedMessage theMessage) {
String messageKey = getMessageKey(theMessage.getResource(myFhirContext));
if (messageKey != null) {
theMessage.setPayloadMessageKey(messageKey);
}
}
/**
* @return the patient's MRN, or <code>null</code> to keep the default message key, the resource ID
*/
private String getMessageKey(IBaseResource theResource) {
// Only Patient resources have an MRN; messages about other resources keep the default key
if (theResource instanceof Patient patient) {
return patient.getIdentifier().stream()
.filter(identifier -> MRN_SYSTEM.equals(identifier.getSystem()) && identifier.hasValue())
.map(Identifier::getValue)
.findFirst()
.orElse(null);
}
return null;
}
}
The following interceptor sets the key before the message is sent to the delivery queue. Register it in the Subscription module:
/**
* Sets the message key of each subscription message before it is sent to the delivery queue, so that messages
* with the same key are delivered in order. This example uses the patient's MRN as the key; replace
* {@link #getMessageKey(IBaseResource)} to group messages another way, and make the same change in
* {@link MatchingQueueMessageKeyInterceptor}. Register it in the Subscription module.
*/
@Interceptor
public class DeliveryQueueMessageKeyInterceptor {
// TODO: Replace with the identifier system of your MRNs
private static final String MRN_SYSTEM = "http://example.org/mrn";
private FhirContext myFhirContext;
@Autowired
public void setFhirContext(FhirContext theFhirContext) {
myFhirContext = theFhirContext;
}
@Hook(Pointcut.SUBSCRIPTION_RESOURCE_MATCHED)
public void resourceMatched(ResourceDeliveryMessage theMessage) {
String messageKey = getMessageKey(theMessage.getPayload(myFhirContext));
if (messageKey != null) {
theMessage.setPayloadMessageKey(messageKey);
}
}
/**
* @return the patient's MRN, or <code>null</code> to keep the default message key, the resource ID
*/
private String getMessageKey(IBaseResource theResource) {
// Only Patient resources have an MRN; messages about other resources keep the default key
if (theResource instanceof Patient patient) {
return patient.getIdentifier().stream()
.filter(identifier -> MRN_SYSTEM.equals(identifier.getSystem()) && identifier.hasValue())
.map(Identifier::getValue)
.findFirst()
.orElse(null);
}
return null;
}
}
Register the interceptors, using the IDs of your FHIR Storage and Subscription modules:
module.persistence.config.interceptor_bean_types=com.smilecdr.demo.subscription.MatchingQueueMessageKeyInterceptor
module.subscription.config.interceptor_bean_types=com.smilecdr.demo.subscription.DeliveryQueueMessageKeyInterceptor
If only one of them is registered, messages are kept in order on one queue only. Messages for which getMessageKey returns no key keep the default key, the resource ID.
The following example shows an interceptor that can be used as a starter Subscription interceptor, implementing a hook method for each available pointcut.
/*-
* #%L
* Smile CDR - CDR
* %%
* Copyright (C) 2016 - 2026 Smile CDR, Inc.
* %%
* All rights reserved.
* #L%
*/
package com.smilecdr.demo.subscription;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
import ca.uhn.fhir.jpa.subscription.model.ResourceDeliveryMessage;
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage;
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
import ca.uhn.fhir.util.StopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Sample Subscription Interceptor implementing all SUBSCRIPTION_XXX pointcuts.
* It is intended to be used in Subscription module 'Interceptor Bean Types'.
* Can be used as a starting point for your subscription interceptor.
*/
@SuppressWarnings({"unused", "EmptyTryBlock"})
@Interceptor
public class SubscriptionInterceptorTemplate {
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionInterceptorTemplate.class);
@Hook(Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED)
public void subscriptionAfterActiveSubscriptionRegistered(CanonicalSubscription theCanonicalSubscription) {
ourLog.info("Interceptor SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED - started");
StopWatch stopWatch = new StopWatch();
try {
// your implementation goes here
} finally {
ourLog.info(
"Interceptor SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED - ended, execution took {}",
stopWatch);
}
}
@Hook(Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_UNREGISTERED)
public void subscriptionAfterActiveSubscriptionUnregistered() {
ourLog.info("Interceptor SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_UNREGISTERED - started");
StopWatch stopWatch = new StopWatch();
try {
// your implementation goes here
} finally {
ourLog.info(
"Interceptor SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_UNREGISTERED - ended, execution took {}",
stopWatch);
}
}
@Hook(Pointcut.SUBSCRIPTION_RESOURCE_MODIFIED)
public boolean subscriptionResourceModified(ResourceModifiedMessage theResourceModifiedMessage) {
ourLog.info("Interceptor SUBSCRIPTION_RESOURCE_MODIFIED - started");
StopWatch stopWatch = new StopWatch();
try {
// your implementation goes here
} finally {
ourLog.info("Interceptor SUBSCRIPTION_RESOURCE_MODIFIED - ended, execution took {}", stopWatch);
}
return true;
}
@Hook(Pointcut.SUBSCRIPTION_RESOURCE_DID_NOT_MATCH_ANY_SUBSCRIPTIONS)
public void subscriptionResourceDidNotMatchAnySubscriptions(ResourceModifiedMessage theResourceModifiedMessage) {
ourLog.info("Interceptor SUBSCRIPTION_RESOURCE_DID_NOT_MATCH_ANY_SUBSCRIPTIONS - started");
StopWatch stopWatch = new StopWatch();
try {
// your implementation goes here
} finally {
ourLog.info(
"Interceptor SUBSCRIPTION_RESOURCE_DID_NOT_MATCH_ANY_SUBSCRIPTIONS - ended, execution took {}",
stopWatch);
}
}
@Hook(Pointcut.SUBSCRIPTION_RESOURCE_MATCHED)
public boolean subscriptionResourceMatched(
CanonicalSubscription theCanonicalSubscription,
ResourceDeliveryMessage theResourceDeliveryMessage,
InMemoryMatchResult theInMemoryMatchResult) {
ourLog.info("Interceptor SUBSCRIPTION_RESOURCE_MATCHED - started");
StopWatch stopWatch = new StopWatch();
try {
// your implementation goes here
} finally {
ourLog.info("Interceptor SUBSCRIPTION_RESOURCE_MATCHED - ended, execution took {}", stopWatch);
}
return true;
}
@Hook(Pointcut.SUBSCRIPTION_BEFORE_PERSISTED_RESOURCE_CHECKED)
public boolean subscriptionBeforePersistedResourceChecked(ResourceModifiedMessage theResourceModifiedMessage) {
ourLog.info("Interceptor SUBSCRIPTION_BEFORE_PERSISTED_RESOURCE_CHECKED - started");
StopWatch stopWatch = new StopWatch();
try {
// your implementation goes here
} finally {
ourLog.info(
"Interceptor SUBSCRIPTION_BEFORE_PERSISTED_RESOURCE_CHECKED - ended, execution took {}", stopWatch);
}
return true;
}
@Hook(Pointcut.SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED)
public void subscriptionAfterPersistedResourceChecked(ResourceModifiedMessage theResourceModifiedMessage) {
ourLog.info("Interceptor SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED - started");
StopWatch stopWatch = new StopWatch();
try {
// your implementation goes here
} finally {
ourLog.info(
"Interceptor SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED - ended, execution took {}", stopWatch);
}
}
@Hook(Pointcut.SUBSCRIPTION_BEFORE_DELIVERY)
public boolean subscriptionBeforeDelivery(
CanonicalSubscription theCanonicalSubscription, ResourceDeliveryMessage theResourceDeliveryMessage) {
ourLog.info("Interceptor SUBSCRIPTION_BEFORE_DELIVERY - started");
StopWatch stopWatch = new StopWatch();
try {
// your implementation goes here
} finally {
ourLog.info("Interceptor SUBSCRIPTION_BEFORE_DELIVERY - ended, execution took {}", stopWatch);
}
return true;
}
@Hook(Pointcut.SUBSCRIPTION_AFTER_DELIVERY)
public void subscriptionAfterDelivery(
CanonicalSubscription theCanonicalSubscription, ResourceDeliveryMessage theResourceDeliveryMessage) {
ourLog.info("Interceptor SUBSCRIPTION_AFTER_DELIVERY - started");
StopWatch stopWatch = new StopWatch();
try {
// your implementation goes here
} finally {
ourLog.info("Interceptor SUBSCRIPTION_AFTER_DELIVERY - ended, execution took {}", stopWatch);
}
}
@Hook(Pointcut.SUBSCRIPTION_AFTER_DELIVERY_FAILED)
public boolean subscriptionAfterDeliveryFailed(
ResourceDeliveryMessage theResourceDeliveryMessage, Exception theException) {
ourLog.info("Interceptor SUBSCRIPTION_AFTER_DELIVERY_FAILED - started");
StopWatch stopWatch = new StopWatch();
try {
// your implementation goes here
} finally {
ourLog.info("Interceptor SUBSCRIPTION_AFTER_DELIVERY_FAILED - ended, execution took {}", stopWatch);
}
return true;
}
@Hook(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY)
public boolean subscriptionBeforeRestHookDelivery(
CanonicalSubscription theCanonicalSubscription, ResourceDeliveryMessage theResourceDeliveryMessage) {
ourLog.info("Interceptor SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY - started");
StopWatch stopWatch = new StopWatch();
try {
// your implementation goes here
} finally {
ourLog.info("Interceptor SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY - ended, execution took {}", stopWatch);
}
return true;
}
@Hook(Pointcut.SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY)
public void subscriptionAfterRestHookDelivery(
CanonicalSubscription theCanonicalSubscription, ResourceDeliveryMessage theResourceDeliveryMessage) {
ourLog.info("Interceptor SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY - started");
StopWatch stopWatch = new StopWatch();
try {
// your implementation goes here
} finally {
ourLog.info("Interceptor SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY - ended, execution took {}", stopWatch);
}
}
@Hook(Pointcut.SUBSCRIPTION_BEFORE_MESSAGE_DELIVERY)
public boolean subscriptionBeforeMessageDelivery(
CanonicalSubscription theCanonicalSubscription,
ResourceDeliveryMessage theResourceDeliveryMessage,
ResourceModifiedJsonMessage theResourceModifiedJsonMessage) {
ourLog.info("Interceptor SUBSCRIPTION_BEFORE_MESSAGE_DELIVERY - started");
StopWatch stopWatch = new StopWatch();
try {
// your implementation goes here
} finally {
ourLog.info("Interceptor SUBSCRIPTION_BEFORE_MESSAGE_DELIVERY - ended, execution took {}", stopWatch);
}
return true;
}
@Hook(Pointcut.SUBSCRIPTION_AFTER_MESSAGE_DELIVERY)
public void subscriptionAfterMessageDelivery(
CanonicalSubscription theCanonicalSubscription, ResourceDeliveryMessage theResourceDeliveryMessage) {
ourLog.info("Interceptor SUBSCRIPTION_AFTER_MESSAGE_DELIVERY - started");
StopWatch stopWatch = new StopWatch();
try {
// your implementation goes here
} finally {
ourLog.info("Interceptor SUBSCRIPTION_AFTER_MESSAGE_DELIVERY - ended, execution took {}", stopWatch);
}
}
}