Rules
Rules defines what should happen to particular requests and under which conditions. They assemble the authentication & authorization and error pipelines and bring previously configured mechanisms to life.
You can compare the relation between mechanisms and rules to the relation between a car catalog at a car dealer and an actual real car you receive. Mechanisms are what you can see and select from the catalog (though, in heimdall, you first define that catalog yourself). The rule is your finished car, built from real components with defined behavior.
In that sense, when you define a rule, you specify which mechanisms it should be built from, and optionally apply some "tuning" to better fit your needs.
Rule Types
Heimdall supports two types of rules:
The upstream specific rule, also called the regular rule. You can define as many regular rules as needed. These rules are dynamic by nature and can appear or disappear together with the upstream services that defines them.
The default rule, which, if defined, is used by the regular rules to inherit their behavior and is also executed if no other rule matches.
Memory Footprint
Although the first paragraphs compare mechanisms and rules to car parts and a finished car, in reality things are a bit more complex.
To minimize the memory footprint, heimdall instantiates all defined mechanisms at startup. And since all mechanisms are stateless, the following is happening at runtime, when the rule is loaded:
If a rule, respectively its pipeline just references a mechanism without providing additional configuration, the already instantiated mechanism is used.
Otherwise, if the pipeline overrides any parts of the default mechanism configuration, a shallow copy of the referenced mechanism instance (created at start up) is created, and only those parts, which differ are replaced with new objects, representing the pipeline specific configuration.
Execution of Rules
The diagram below sketches the logic executed by heimdall for each and every incoming request.
Any rule matching request? - This is the first step executed by heimdall in which it tries to find a matching rule. If there is no matching rule, heimdall either falls back to the default rule if available, or the request is denied. Otherwise, the rule specific authentication & authorization pipeline is executed.
Execute authentication & authorization pipeline - when a rule is matched, the mechanisms defined in its authentication & authorization pipeline are executed.
Forward request, respectively respond to the API gateway - when the above steps succeed, heimdall, depending on the operating mode, responds with, respectively forwards whatever was defined in the pipeline (usually this is a set of HTTP headers). Otherwise
Execute error pipeline is executed if any of the mechanisms, defined in the authentication & authorization pipeline fail. This again results in a response, this time however, based on the definition in the used error handler.
Matching of Rules
As written above, an upstream-specific rule is only executed if it matches an incoming request.
The actual matching is primarily based on the request’s host and path, which are evaluated against expressions defined in the loaded rules. This matching process is optimized for performance and guaranteed to run with O(log n) time complexity. Host expressions can use exact values, or wildcards. For paths, both static segments and wildcards are supported. Wildcards may span a single segment or the remainder of the path/host and, if used in path expressions, can also be named. The latter allows their captured values to be reused — for example, to apply additional pattern constraints or to reference them later in the rule’s pipeline. HTTP scheme and method can also be taken into account during matching.
The values used for matching — host, path, scheme, and method — are typically extracted directly from the incoming HTTP request. If configured to do so, heimdall can instead rely on values from standard X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-Uri, and X-Forwarded-Method headers.
Default Rule & Inheritance
The Rule Types section tells, that a default rule can be used as a base to inherit behavior for the regular rule.
In principle, there is a need to differentiate two things:
the defined rule, which is what you define
the effective rule, which is what is executed at runtime
This is comparable to the polymorphism concept in programming languages. So, how does it work?
Imagine the concept of a rule as a Java interface defining the following methods:
public interface Rule {
public void executeAuthenticationStage(req Request)
public void executeAuthorizationStage(req Request)
public void executeFinalizationStage(req Request)
public void handleError(req Request)
}And the logic described in Execution of Rules is implemented similar to what is shown in the snippet below
Rule rule = findMatchingRule(req)
if (rule == null) {
throw new NotFoundError()
}
try {
// execution of the authentication & authorization pipeline
rule.executeAuthenticationStage(req)
rule.executeAuthorizationStage(req)
rule.executeFinalizationStage(req)
// further logic related to response creation
} catch(Exception e) {
// execution of the error pipeline
rule.handleError(req, e)
// further logic related to response creation
}with findMatchingRule returning a specific instance of a class implementing our Rule interface matching the request.
Since default behavior is available for cases such as empty error pipelines or optional authentication & authorization pipeline stages, internally, there is some kind of base rule in place, all other rules inherit from. So something like shown in the snippet below
public abstract class BaseRule implements Rule {
public abstract void executeAuthenticationStage(req Request)
public void executeAuthorizationStage(req Request) {}
public void executeFinalizationStage(req Request) {}
public void handleError(req Request, e Exception) {
handlerErrorDefault(req, e)
}
}If there is no default rule configured, an upstream specific rule can then be considered as a class inheriting from that BaseRule and must implement at least the executeAuthenticationStage method, similar to what is shown below
public class MySpecificRule extends BaseRule {
public void executeAuthenticationStage(req Request) { ... }
}If however, there is a default rule configured, on one hand, it can be considered as yet another class deriving from our BaseClass. So, something like
public class DefaultRule extends BaseRule {
public void executeAuthenticationStage(req Request) { ... }
public void executeAuthorizationStage(req Request) { ... }
public void executeFinalizationStage(req Request) { ... }
public void handleError(req Request, e Exception) { ... }
}with at least the aforesaid executeAuthenticationStage method being implemented, as this is also required for the regular rule.
On the other hand, the definition of a regular, respectively upstream specific rule is then not a class deriving from the BaseRule, but from the DefaultRule. That way, upstream specific rules are only required, if the behavior of the default rule would not fit the given requirements of a particular service, respectively endpoint. So, if e.g. a rule requires only the authentication stage to be different from the default rule, you would only specify the required authentication mechanisms. That would result in something like shown in the snippet below.
public class SpecificRule extends DefaultRule {
public void executeAuthenticationStage(req Request) { ... }
}And if there is a need to have the authorization stage deviating from the default rule, you would only specify the required authorization and contextualization mechanisms, resulting in something like
public class SpecificRule extends DefaultRule {
public void executeAuthorizationStage(req Request) { ... }
}| You cannot override individual mechanisms within a stage. Once you define any mechanism for a given stage in a pipeline, the entire stage definition replaces the inherited one. |
Last updated on Oct 20, 2025