Extending WebSub Topic Matching¶
When a subscriber sends a request to a WebSub API, the Universal Gateway decides whether the topic the subscriber asked for matches one of the topics defined on the API. Out of the box, this decision is based on two rules:
- An exact string match against the topics defined on the API.
- A URI-template match (so dynamic patterns such as
orders/{id}/updatesare supported).
If neither rule accepts the subscriber's topic, the request is rejected as invalid.
These rules cover most use cases, but sometimes the topic scheme used by an external system cannot be expressed as an exact string or a URI template. Common examples include topics with versioning suffixes (orders.v2), hierarchical wildcards not supported by URI templates, or topics that need to be looked up against an external registry. For these cases, WSO2 API Manager exposes an extension point that lets you plug in your own topic-matching logic for WebSub APIs.
Note
This feature requires the following minimum update levels for WSO2 API Manager 4.6.0:
- All-in-One (
wso2am): 4.6.0.32 - Universal Gateway (
wso2am-universal-gw): 4.6.0.32
Run the WSO2 Update Tool to get the latest updates.
How it works¶
Before the built-in matcher runs, the Gateway invokes your custom matcher and passes it two pieces of information:
- The topic that the subscriber requested, taken from the
hub.topicvalue in the request. - The list of topics defined on the API.
Your matcher returns either the topic it considers a match (in which case the Gateway accepts the request) or nothing (in which case the Gateway falls back to the built-in rules). Because the built-in rules still run as a fallback, your matcher only needs to handle the extra cases you want to support — you do not need to reimplement exact and URI-template matching.
Implementing a custom matcher¶
Step 1 - Add the dependency¶
Create a Java project and add the following Maven dependency. Set ${carbon.apimgt.version} to the version that ships with your WSO2 API Manager release (you can find it in <API-M_HOME>/repository/components/plugins/ by looking at the version suffix of org.wso2.carbon.apimgt.common.gateway_<version>.jar).
<dependency>
<groupId>org.wso2.carbon.apimgt</groupId>
<artifactId>org.wso2.carbon.apimgt.common.gateway</artifactId>
<version>${carbon.apimgt.version}</version>
<scope>provided</scope>
</dependency>
Step 2 - Implement the ExtensionListener interface¶
Implement org.wso2.carbon.apimgt.common.gateway.extensionlistener.ExtensionListener and put your topic-matching logic in preProcessRequest. The other methods can return a "continue" response unchanged.
package com.example.apim.websub;
import org.wso2.carbon.apimgt.common.gateway.dto.ExtensionResponseDTO;
import org.wso2.carbon.apimgt.common.gateway.dto.ExtensionResponseStatus;
import org.wso2.carbon.apimgt.common.gateway.dto.ExtensionType;
import org.wso2.carbon.apimgt.common.gateway.dto.RequestContextDTO;
import org.wso2.carbon.apimgt.common.gateway.dto.ResponseContextDTO;
import org.wso2.carbon.apimgt.common.gateway.extensionlistener.ExtensionListener;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CustomWebSubTopicMatcher implements ExtensionListener {
private static final String WEBSUB_REQUESTED_TOPIC = "WEBSUB_REQUESTED_TOPIC";
private static final String WEBSUB_URL_PATTERNS = "WEBSUB_URL_PATTERNS";
private static final String WEBSUB_MATCHED_TOPIC = "WEBSUB_MATCHED_TOPIC";
@Override
public ExtensionResponseDTO preProcessRequest(RequestContextDTO requestContextDTO) {
Map<String, Object> customProps = requestContextDTO.getCustomProperty();
if (customProps == null) {
customProps = new HashMap<>();
}
String requestedTopic = (String) customProps.get(WEBSUB_REQUESTED_TOPIC);
@SuppressWarnings("unchecked")
List<String> apiTopics = (List<String>) customProps.get(WEBSUB_URL_PATTERNS);
String matchedTopic = match(requestedTopic, apiTopics);
if (matchedTopic != null) {
customProps.put(WEBSUB_MATCHED_TOPIC, matchedTopic);
}
ExtensionResponseDTO response = new ExtensionResponseDTO();
response.setCustomProperty(customProps);
response.setResponseStatus(ExtensionResponseStatus.CONTINUE.toString());
return response;
}
private String match(String requestedTopic, List<String> apiTopics) {
if (requestedTopic == null || apiTopics == null) {
return null;
}
// Example: match case-insensitively, ignoring any '.v<n>' version suffix.
// Replace this block with your own matching logic.
String normalized = requestedTopic.replaceAll("\\.v\\d+$", "").toLowerCase();
for (String topic : apiTopics) {
if (topic.equalsIgnoreCase(normalized)) {
return topic;
}
}
return null;
}
@Override
public ExtensionResponseDTO postProcessRequest(RequestContextDTO requestContextDTO) {
return continueResponse();
}
@Override
public ExtensionResponseDTO preProcessResponse(ResponseContextDTO responseContextDTO) {
return continueResponse();
}
@Override
public ExtensionResponseDTO postProcessResponse(ResponseContextDTO responseContextDTO) {
return continueResponse();
}
@Override
public String getType() {
return ExtensionType.WEBSUB_TOPIC_RESOLVER.toString();
}
private static ExtensionResponseDTO continueResponse() {
ExtensionResponseDTO response = new ExtensionResponseDTO();
response.setResponseStatus(ExtensionResponseStatus.CONTINUE.toString());
return response;
}
}
The values your matcher exchanges with the Gateway are passed through the request's custom-property map.
| Key | Type | Description |
|---|---|---|
|
|
The topic value the subscriber requested. Provided by the Gateway. |
|
|
The list of topics defined on the API. Provided by the Gateway. |
|
|
Set this to the topic you consider a match. Leave it unset (or |
Note
Your implementation must expose a public no-argument constructor. The class is instantiated by API Manager at startup.
Step 3 - Deploy the extension¶
- Build the project as a plain JAR (not an OSGi bundle):
mvn clean package - Copy the JAR into
<API-M_HOME>/repository/components/lib/on every Gateway node. - Register the class in
<API-M_HOME>/repository/conf/deployment.tomlon every Gateway node:[[apim.extension.listener]] type = "WEBSUB_TOPIC_RESOLVER" class = "com.example.apim.websub.CustomWebSubTopicMatcher" enable_extension_fault_sequence_mediation = false - Restart the Gateway nodes.
Distributed deployment¶
Note
In a fully distributed setup, add the JAR and the [[apim.extension.listener]] configuration to every Gateway node. Custom topic matching runs at request-processing time on the Gateway; the Control Plane, Traffic Manager, and Key Manager nodes are not involved.
Warning
Only one custom matcher can be registered at a time. If more than one [[apim.extension.listener]] block uses type = "WEBSUB_TOPIC_RESOLVER", the last one wins.
Verifying the extension¶
- Deploy a WebSub API that has at least one topic defined (see Create a WebSub/WebHook API).
- Enable
DEBUGlogging for your extension package by adding the following to<API-M_HOME>/repository/conf/log4j2.propertieson the Gateway:Appendlogger.custom-websub.name = com.example.apim.websub logger.custom-websub.level = DEBUGcustom-websubto theloggerslist. - Send a subscription request whose
hub.topicvalue only your custom matcher (not the built-in matcher) would accept, and confirm that the Gateway accepts it. - Send a subscription request whose
hub.topicvalue neither matcher would accept, and confirm that the Gateway rejects it.