Dynamically composes html markup generated by microservices.
Composer is an infrastructure middleware for micro frontends. Composer allows a web page to be composed out of ui fragments that are served from different micro services. Fragments consist of some html markup and optionally some css and/or javascript assets. Fragments are included into a template page at runtime.
Composer is a (partial) reimplementation of the system that we use at REWE digital for building mirco frontend based web applications.
Composer contains a basic routing engine leveraging the engine offered by the underlying Apollo Framework. It routes an incoming request to some internal endpoint to fetch the response for the requested route. A route type controls whether the response then should be fed into the composition engine (template route) or whether it just should be returned to the client unmodified (proxy route).
Routes can be configured via the configuration file using HCON - human optimised configuration notation.
Example:
{ path: "/", method: "GET", type: "PROXY", target: "https://www.rewe-digital.com/" }
Configures a proxy route for the path /
and method GET
that forwards all requests to the target https://rewe-digital.com
.
Path parameters can be captured using the syntax of the underlying routing engine (https://github.com/danielnorberg/rut), for example
{ path: "/<param>", method: "GET", type: "PROXY", target: "https://www.rewe-digital.com/some/{param}" }
Specify an include
The html body of the response from a template route is fed into the composition engine. The body is parsed looking for include tags that specify fragments to be included into the response delivered to the caller. The name of the include tag can be changed via configuration, the default is rewe-digital-include.
Example
<body>
<rewe-digital-include path="https://some.path/to/fragment"></rewe-digital-include>
</body>
Specifies that some content fragment should be included from the path https://some.path/to/fragment
.
Specify content to be included
Content fragment are parsed from the html body of the response of the call to the included path. Here, another custom html tag is used to mark that part of the body, that should be included. The name of this content tag can be changed via configuration, the default is rewe-digital-content.
Example
<body>
<rewe-digital-content>
<div>Content goes here</div>
</rewe-digital-content>
</body>
In this example, an include tag pointing to the above content would be replaced with the markup enclosed by the content tag.
Using content tags to mark the content to be included allows the response of some content service to contain a fully working html document (including for example a html head element), thus allowing a team to test their content page without having to actually include it into some template.
Specify assets required for included content
Content fragments usually need some assets like css and/or javascript to work. For this. content responses can mark asset links resp. script includes in their head section using a special data attribute. The name of this attribute is data-rd-options and can be changed via configuration. If an asset link/script in the html head of the content response contains the data-rd-options
attribute with value include
, this link/script is included in the head of the document returned to the caller.
Example
<link href="../static/footer/css/core.css" data-rd-options="include" rel="stylesheet" media="screen" />
For this to work, a couple of conventions must be followed. For example, css should be scoped to the content fragment it belongs to.
Recursive includes
Include tags are resolved in parallel. Included content can again specify includes using the include tag. When building the response, these include tags are resolved recursively. Asset links are written "in order", the order is determined by the order in which include tags occur.
The maximal depth for the recursion can be limited via the configuration property composer.html.max-recursion
. If the limit is exceeded, the fallback for the include is used.
Fallback content
If an include could not be resolved (for example, because the endpoint called for content returned an empty response), the markup enclosed by the include tag can specify a fallback. Here, the content tag can be used to mark the part that should be used as fallback content.
Example
<rewe-digital-include path="returns/empty/response">
<div>not part of fallback</div>
<rewe-digital-content>
<div>the fallback</div>
</rewe-digital-content>
</rewe-digital-include>
Read-Timeouts
The underlying OkHttp Client can be configured with global timeouts. The http.client.connectTimeout, readTimeout, writeTimeout
settings are not sufficient for scenarious where each routing target has different requirements regarding response time.
Hence, an optional ttl
parameter can be configured for routes. This config setting applies an individual read timeout in milliseconds (ms) to the route.
Example:
{ path: "/", method: "GET", type: "PROXY", ttl: 2000, target: "https://www.rewe-digital.com/" }
The same requirement is valid for included services, therefore the template service can add an optional ttl
attribute to the ìnclude
tag.
Example
<body>
<rewe-digital-include path="https://some.path/to/fragment" ttl="500"></rewe-digital-include>
</body>
Composer can cache responses from upstream services according to their cache-control headers. The cache is activated via the configuration property composer.http.cache.enabled
. Under the hood, Composer uses Caffeine as cache implementation. Each instance of Composer maintains it's own in-memory cache.
Right now, Composer only caches resources with max-age > 0
. Composer does not support conditional gets, thus, a resource with no-cache
is simply not cached.
Session data management
Composer contains an optional (toggled via configuration) session mechanism. A session is stored as encrypted cookie. Session values are forwarded to called services (template and content) as http headers using the prefix x-rd-
. So, a session value named foo
with value bar
would be forwarded as x-rd-foo: bar
.
Services can set session values by sending the appropriate response header. Thus, a service (template or content fragment) sending the response header x-rd-foo: bar
would set a new or overwrite an existing session attribute named foo
with the value bar
. To remove a session attribute, an empty value can be provided.
Changes to the session are collected during request processing and are then available for the next request. Thus, if a session contains foo: bar
, all services that are called during processing of an external request receive the request header x-rd-foo: bar
. If one of the services changes the value by sending an appropriate response header, this update happens after all services are called. The next external request then would contain the updated value.
Session interceptors
Composer allows preprocessing the session via an interceptor chain. Interceptors run after the session object is created from the incomming request. Interceptors can completely rewrite the session data by adding, updating or removing session attributes. They are chained in that the n+1th interceptor receives the output of the nth interceptor.
The interceptor chain is configured via the configuration file. Currently, two interceptors are supported:
LocalSessionIdInterceptor
: this interceptor adds a session-id attribute and manages a session time-to-live. If a session is timed-out, all session attributes are removed. Example:{ type: com.rewedigital.composer.session.LocalSessionIdInterceptor, args: { ttl: 3600, renew-after: 1800 }}
configures the interceptor to use a time-to-live of 3600 seconds, the session is renewed if the remaining time is smaller than 1800 seconds.RemoteHttpSessionInterceptor
: this interceptor posts a json representation of the session to some configured endpoint and updates the session with data from response.
Composer is currently under development and probably not production ready.
Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0.txt)