Transforming a request object synchronously is trivial. By "synchronously" we mean in the same thread, in non-blocking fashion.
Just call request.newBuilder
to create a new HttpRequest.Builder
object.
It has already been initialised with the copy of the original request
. Modify
the builder as desired, construct a new version, and pass it to the chain.proceed()
,
as the example demonstrates:
import com.hotels.styx.api.LiveHttpRequest;
import com.hotels.styx.api.LiveHttpResponse;
import com.hotels.styx.api.Eventual;
import com.hotels.styx.api.plugins.spi.Plugin;
import static com.hotels.styx.api.HttpHeaderNames.USER_AGENT;
public class SyncRequestPlugin implements Plugin {
@Override
public Eventual<LiveHttpResponse> intercept(LiveHttpRequest request, Chain chain) {
return chain.proceed(
request.newBuilder()
.header(USER_AGENT, "Styx/1.0 just testing plugins")
.build()
);
}
}
This example demonstrates how to synchronously transform a HTTP response. We will
call Eventual.map
to add an "X-Foo" header to the response.
import com.hotels.styx.api.HttpInterceptor;
import com.hotels.styx.api.LiveHttpRequest;
import com.hotels.styx.api.LiveHttpResponse;
import com.hotels.styx.api.Eventual;
import com.hotels.styx.api.plugins.spi.Plugin;
public class SyncResponsePlugin implements Plugin {
@Override
public Eventual<LiveHttpResponse> intercept(LiveHttpRequest request, HttpInterceptor.Chain chain) {
return chain.proceed(request)
.map(response -> response.newBuilder()
.header("X-Foo", "bar")
.build()
);
}
}
Sometimes it is necessary to transform the request asynchronously. For example, you may need to look up external key value stores to parameterise the request transformation. The example below shows how to modify a request URL path based on an asynchronous lookup to an external database.
import com.hotels.styx.api.HttpInterceptor;
import com.hotels.styx.api.LiveHttpRequest;
import com.hotels.styx.api.LiveHttpResponse;
import com.hotels.styx.api.Eventual;
import com.hotels.styx.api.Url;
import com.hotels.styx.api.plugins.spi.Plugin;
import java.util.concurrent.CompletableFuture;
public class AsyncRequestInterceptor implements Plugin {
@Override
public Eventual<LiveHttpResponse> intercept(LiveHttpRequest request, HttpInterceptor.Chain chain) {
return asyncUrlReplacement(request.url()) // (1)
.map(newUrl -> request.newBuilder() // (4)
.url(newUrl)
.build())
.flatMap(chain::proceed); // (5)
}
private static Eventual<Url> asyncUrlReplacement(Url url) {
return Eventual.from(pathReplacementService(url.path())) // (3)
.map(newPath -> new Url.Builder(url)
.path(newPath)
.build());
}
private static CompletableFuture<String> pathReplacementService(String url) {
// Pretend to make a call here:
return CompletableFuture.completedFuture("/replacement/path"); // (2)
}
}
Step 1. We call the asyncUrlReplacement
, which returns an Eventual<Url>
.
The asyncUrlReplacement
wraps a call to the remote service and converts
the outcome into an Eventual
.
Step 2. A call to pathReplacementService
makes a non-blocking call to the remote key/value store.
Well, at least we pretend to call the key value store, but in this example we'll just return a
completed future of a constant value.
Step 3. CompletableFuture
is converted to an Eventual
, so that other asynchronous
operations like chain.proceed
can be bound to it later on.
Step 4. The eventual outcome from the asyncUrlReplacement
yields a new, modified URL instance.
We'll transform the request
by substituting the URL path with the new one.
This is a quick synchronous operation so we'll do it in a map
operator.
Step 5. Finally, we will bind the outcome from chain.proceed
into the response Eventual
.
Remember that chain.proceed
returns an Eventual<LiveHttpResponse>
. It is, therefore,
interface compatible and can be flatMap
'd to the response Eventual
. The resulting
response Eventual
chain is returned from the intercept
.
This example demonstrates asynchronous response processing. Here we pretend that callTo3rdParty
method
makes a non-blocking request to retrieve string that is inserted into a response header.
A callTo3rdParty
returns a CompletableFuture
which asynchronously produces a string value. We
will call this function when the HTTP response is received.
import com.hotels.styx.api.HttpInterceptor;
import com.hotels.styx.api.LiveHttpRequest;
import com.hotels.styx.api.LiveHttpResponse;
import com.hotels.styx.api.Eventual;
import com.hotels.styx.api.plugins.spi.Plugin;
import java.util.concurrent.CompletableFuture;
public class AsyncResponseInterceptor implements Plugin {
private static final String X_MY_HEADER = "X-My-Header";
@Override
public Eventual<LiveHttpResponse> intercept(LiveHttpRequest request, HttpInterceptor.Chain chain) {
return chain.proceed(request) // (1)
.flatMap(response -> // (3)
Eventual.from(thirdPartyHeaderService(response.header(X_MY_HEADER).orElse("default"))) // (2)
.map(value -> // (4)
response.newBuilder()
.header(X_MY_HEADER, value)
.build())
);
}
private static CompletableFuture<String> thirdPartyHeaderService(String myHeader) {
// Pretend to make a call here:
return CompletableFuture.completedFuture("value");
}
}
Step 1. We start by calling chain.proceed(request)
to obtain a response Eventual
.
Step 2. A callTo3rdParty
returns a CompletableFuture
. It is converted to an Eventual
with
Eventual.from
and bound to the response Eventual
.
Step 3. The flatMap
operator binds callToThirdParty
into the response Eventual
.
Step 4. We will transform the HTTP response by inserting an outcome of callToThirdParty
, or value
,
into the response headers.
Styx exposes HTTP messages to interceptors as streaming LiveHttpRequest
and LiveHttpResponse
messages.
In this form, the interceptors can process the content in a streaming fashion. That is, they
can look into, and modify the content as it streams through.
Alternatively, live messages can be aggregated to HttpRequest
or HttpResponse
messages. The full HTTP message body is then available for the interceptor to use. Note that content
aggregation is always an asynchronous operation. This is because the streaming HTTP message is
exposing the content, in byte buffers, as it arrives from the network, and Styx must wait until
all content has been received.
Note that once the content is aggregated by using the aggregate()
method, the LiveHttpRequest
/ LiveHttpResponse
object is not valid anymore and thus the HttpRequest
/HttpResponse
returned by aggregate()
should be used.
import com.hotels.styx.api.LiveHttpRequest;
import com.hotels.styx.api.LiveHttpResponse;
import com.hotels.styx.api.Eventual;
import com.hotels.styx.api.plugins.spi.Plugin;
public class RequestAggregationPlugin implements Plugin {
private static final int MAX_CONTENT_LENGTH = 100000;
@Override
public Eventual<LiveHttpResponse> intercept(LiveHttpRequest request, Chain chain) {
return request.aggregate(MAX_CONTENT_LENGTH)
.map(fullRequest -> {
String body = fullRequest.bodyAs(UTF_8);
return fullRequest.newBuilder()
.body(body + "EXTRA TEXT", UTF_8)
.build().stream();
}).flatMap(chain::proceed);
}
}
maxContentBytes
is a safety valve that prevents Styx from accumulating too much content
and running out of memory. Styx only accumulates up to maxContentBytes
of content.
aggregate()
fails when the content stream exceeds this amount, and Styx emits a
ContentOverflowException
,
Streaming HTTP body content can be transformed both synchronously and asynchronously. However there are some pitfalls you need to know:
-
Reference counting: Styx exposes the content stream as a
ByteStream
of reference countedBuffer
objects. -
Continuity (or discontinuity) of Styx content
ByteStream
: each content transformation withmap
orflatMap
is a composition of anotherByteStream
. So is each content transformation linked to some sourceByteStream
, an ultimate source being the Styx server core. It is the consumer's responsibility to ensure this link never gets broken. That is, you are not allowed to just replace the content stream with another one, unless it composes to the previous content source.
In this rather contrived example we will transform HTTP response content to upper case letters.
public class MyPlugin extends Plugin {
@Override
public Eventual<LiveHttpResponse> intercept(LiveHttpRequest request, Chain chain) {
return chain.proceed(
request.newBuilder()
.body(byteStream -> // 1
byteStream.map(buf -> { // 2
String upperCase = new String(buf.content(), UTF_8).toUpperCase(); // 3
return new Buffer(upperCase, UTF_8);
}))
.build()
);
}
}
A call to request.newBuilder
opens up a new request builder that allows the request
to be transformed.
The body transformation involves two lambda methods:
-
.body(Function<ByteStream, ByteStream>)
accepts a lambda that modifies the request byte stream. Here we provide a lambda that accepts aByteStream
and returns another by applying a synchronousmap
operator on the stream. -
The
ByteStream
is modified by applying amap
operator that synchronously modifies eachBuffer
that streams through. Themap
involves another lambda that accepts aBuffer
and returns a modified buffer. -
In this example, the content of the buffer is interpreted as
String
and converted to upper case.
This example demonstrates how HTTP response content can be transformed synchronously.
public class AsyncResponseContentStreamTransformation implements Plugin {
@Override
public Eventual<LiveHttpResponse> intercept(LiveHttpRequest request, Chain chain) {
return chain.proceed(request)
.map(response -> // 1
response.newBuilder()
.body(byteStream -> // 2
byteStream.map(buf -> { // 3
String upperCase = new String(buf.content(), UTF_8).toUpperCase(); // 4
return new Buffer(upperCase, UTF_8);
}))
.build());
}
}
This is quite similar to the request transformation, but it involves an additional lambda
expression to capture the HTTP response from its Eventual
envelope:
chain.proceed
returns anEventual
HTTP response. We apply anEventual.map
operator to tap into this response. Themap
operator accepts a lambda expression that handles the response when it is available.
After that, we will transform the response just like we did in the previous example for response content transformations.
Not supported at the moment. Please raise a feature request and the Styx team can implement this.
Not supported at the moment. Please raise a feature request and the Styx team can implement this.