Skip to content

Styx 1.0 API

Mikko Karjalainen edited this page Oct 24, 2018 · 5 revisions

Background

The Styx API as in 0.7 release train was developed for a small internal audience. It was also an experimental "best guess" before any "real" plugins existed.

Since then the audience has expanded and many new plugins have been implemented by different development teams. In the Styx core team we have received feedback and questions. We have made observations. We have identified areas of improvements in the 3rd party API.

Based on these observations we are introducing a new API. It better decouples the API fromStyx implementation aspects. It makes the API safer and more convenient to use. It will be better for 3rd party consumption.

The new API:

  • The new API is no longer pinned to 3rd party libraries like Rx Java. We have removed any 3rd party definitions like rx.Observale from the public API. This is a significant improvement because we will be able to change the underlying implementation and libraries without invalidating backwards compatibility.

  • The new API is safer. This is because rx.Observable is very generic reactive stream abstraction, but Styx has a very specific use case of processing live network data streams. Many rx.Observable operations are not useful, and others are outright dangerous in this context. By replacing rx.Observable with classes more relevant to HTTP proxying, the new 1.0 API only allows safe and meaningful data processing operations.

  • Revamped HTTP message class hierarchy. In particular:

    • The old HttpRequest and HttpResponse classes have been renamed to LiveHttpRequest and LiveHttpResponse to emphasise that the content body streams through as a sequence of byte buffer events. Rationale: We have witnessed new plugin developers breaking their application by treating "live" HTTP streams as normal Java beans. Often this would lead to a broken content stream and errors further up the plugin pipeline. The Live mnemonic in the class name is supposed to attract plugin developer's attention hoping to reduce unintentional mistakes.

    • The new HttpRequest and HttpResponse objects are now immutable and "full" message objects containing both HTTP headers and body payload in a single object. They are designed to be far more convenient in other applications like unit tests, admin handlers and pretty much everywhere outside of proxy interceptors.

  • New ByteStream class represents the body content stream in LiveHttpRequest and LiveHttpResponse objects. It is no longer an Observable<ByteBuf>.

  • New Eventual class replaces Observable<HttpResponse> in the HttpInterceptor.intercept method.

  • Removed leaked abstractions from Netty, Guava, etc.

Plugin Migration

Replace Streaming Message Types

The old HttpRequest and HttpResponse classes have been renamed to LiveHttpRequest and LiveHttpResponse, respectively.

The LiveHttpRequest and LiveHttpResponse appear prominently in HttpInterceptor.intercept and Chain.proceed methods. Plugin code must be renamed accordingly.

Replace Observables with Eventual

The Interceptor.intercept and Chain.proceed methods now return an Eventual<LiveHttpResponse>.

Replace any usage of Observable with new Eventual type. It supports:

  • Creating new Eventual objects from value: of
  • Creating new Eventual objects from a Reactive Streams Publisher
  • Creating new Eventual objects from a Java 8 CompletionStage
  • Creating new Eventual that fails with a Throwable: error
  • Synchronously mapping values with map
  • Asynchronously mapping values with flatMap
  • Mapping errors with: onError

Replace Netty Definitions

Replace Netty HTTP response status codes with Styx equivalents:

  • from: import static io.netty.handler.codec.http.HttpResponseStatus
  • to: import static com.hotels.styx.api.messages.HttpResponseStatus

Replace Netty HTTP method names with Styx equivalents:

  • from: import io.netty.handler.codec.http.HttpMethod
  • to: import com.hotels.styx.api.messages.HttpMethod

and

  • from: import static io.netty.handler.codec.http.HttpMethod.*
  • to: import static com.hotels.styx.api.messages.HttpMethod.*

HTTP Message Builders

New Import Locations

Static methods for building HTTP messages used to be part of the Builder class. To reduce line noise we have moved the builders up to the message class itself like so:

  • from: import static com.hotels.styx.api.HttpResponse.Builder.response
  • to: import static com.hotels.styx.api.LiveHttpResponse.response

The relevant builders from the HttpRequest class have also been moved:

  • from:
import static com.hotels.styx.api.HttpRequest.Builder.head
import static com.hotels.styx.api.HttpRequest.Builder.post
import static com.hotels.styx.api.HttpRequest.Builder.delete
import static com.hotels.styx.api.HttpRequest.Builder.put
import static com.hotels.styx.api.HttpRequest.Builder.patch
  • to: import static com.hotels.styx.api.LiveHttpRequest.*

Http Message Constructors

private HttpRequest httpRequest = new HttpRequest.Builder(HttpMethod.GET).uri("www.google.com").build();

Now becomes:

private LiveHttpRequest httpRequest = new LiveHttpRequest.Builder(HttpMethod.GET, "www.google.com").build();

Streaming vs Full HTTP messages

The new API will represent HTTP messages in two different flavours:

  1. Streaming HTTP messages: LiveHttpRequest and LiveHttpResponse.
  2. Immutable HTTP messages: HttpRequest, HttpResponse.

The live and full variants are not interface compatible as per Liskov substitution principle and therefore they form two separate class hierarchies. However it is easy to convert between the two:

From immutable to live:

// In immutable HttpRequest:
LiveHttpRequest stream()
// In immutable HttpResponse:
LiveHttpResponse stream()

From live message to immutable:

// In LiveHttpRequest
Eventual<HttpRequest> aggregate(int maxContentBytes)
// In LiveHttpResponse
Eventual<HttpResponse> aggregate(int maxContentBytes)

Message Body Handling

In the spirit of streaming HTTP messages, the LiveHttpRequest and LiveHttpResponse content can only be set as ByteStream object. Therefore, the following builder methods have been removed:

  • body(HttpMessage body)
  • body(ByteBuf content)
  • body(String content)
  • body(byte[] content)
  • body(ByteBuf content)

The only way to set the content via the streaming message builders (LiveHttpRequest.Builder or LiveHttpResponse.Builder) is

  • body(ByteStream content)

This is to emphasise the streaming nature of LiveHttpRequest and LiveHttpResponse messages. Use the immutable HttpRequest and HttpResponse classes to construct a HTTP message with full content. They have the following builder methods for the message body:

body(String content, Charset charset) body(String content, Charset charset, boolean setContentLength) body(byte[] content, boolean setContentLength)

Setting Message Content

In 0.7 API you can:

 response(OK).body("Hello").build();

This is no longer possible. Now you must:

 response(OK).body(Eventual.of(new ByteStream(aPublisher))).build()

Where aPublisher is a Reactive Streams compatible Publisher.

That is, the content stream must be created as a ByteStream instance, which is constructed from some compatible publisher.

The preferred way, when the response body is of a fixed size and known in advance, is to create an immutable HTTP response and convert that to a live response:

  HttpResponse.response(OK)
      .body("Hello", UTF_8)
      .build()
      .stream();

Aggregating Message Content

Previously it was necessary to use response.decode method which returns a HttpResponse.DecodedResponse instance. Like so:

private ResponseWithBody doRequest(HttpRequest request) {
    return client.sendRequest(request)
            .flatMap(response -> response.decode(byteBuf -> byteBuf.toString(UTF_8), 0x100000))
            .map(ResponseWithBody::new)
            .toBlocking()
            .single();
}
  
protected static class ResponseWithBody {
    private final HttpResponse response;
    private final String body;
 
    private ResponseWithBody(HttpResponse.DecodedResponse<String> decodedResponse) {
        this.response = decodedResponse.responseBuilder().body(decodedResponse.body()).build();
        this.body = decodedResponse.body();
    }
 
    public HttpResponseStatus status() {
        return response.status();
    }
 
    public HttpResponse response() {
        return response;
    }
 
    public String body() {
        return body;
    }
}

With new immutable HTTP messages this is much easier:

private HttpResponse doRequest(LiveHttpRequest request) {
    return await(client.sendRequest(request)
            .flatMap(response -> response.aggregate(0x100000))
            .asCompletableFuture());
}

Notes:

  • The above example is from a unit test for a Styx plugin. Obviously you wouldn't block the thread in a Styx interceptor!
  • A call to asCompletableFuture converts StyxObservable<FullHttpResponse> to a Java 8 CompletableFuture<FullHttpResponse>.
  • A call to await blocks and waits for the result from CompletableFuture<FullHttpResponse>. It conveniently converts the checked exceptions from CompletableFuture.get() into RuntimeExceptions, so the doRequest method signature is not polluted.

Admin Interface Handlers

  1. The HttpHandler.handle now has a new argument HttpInterceptor.Context which needs to be added to each implementation as a second parameter. However, in most cases the parameter can be safely ignored.

  2. Admin interface handlers now return a Eventual<LiveHttpResponse> instead of rx.Observable.

The Original Request

The response.request() is removed. You need to capture the request object from the intercept closure. Also, we might provide the original request via HTTP context. But will do this only if there are some really compelling reasons to do so.