Pistache

An elegant C++ REST framework.

User's Guide

Namespace

Most of the components provided by Pistache live in the Net namespace. It is thus recommended to directly import this namespace with a using-declaration:

using namespace Net;

Http Handler

Requests that are received by Pistache are handled by a special class called Http::Handler. This class declares a bunch of virtual methods that can be overriden to handle special events that occur on the socket and/or connection.

The onRequest() function must be overriden. This function is called whenever Pistache received data and correctly parsed it as an http request.

virtual void onRequest(const Http::Request& request, Http::ResponseWriter response);

The first argument is an object of type Http::Request representing the request itself. It contains a bunch of informations including:

  • The resource associated to the request
  • The query parameters
  • The headers
  • The body of the request

The Request object gives a read-only access to these informations. You can access them through a couple of getters but can not modify them. An http request is immutable.

Sending a response

ResponseWriter is an object from which the final http response is sent to the client. The onRequest() function does not return anything (void). Instead, the response is sent through the ResponseWriter class. This class provides a bunch of send() function overloads to send the response:

Async::Promise<ssize_t> send(Code code);

You can use this overload to send a response with an empty body and a given HTTP Code (e.g Http::Code::Ok)

Async::Promise<ssize_t> send(
            Code code,
            const std::string& body,
            const Mime::MediaType &mime = Mime::MediaType());

This overload can be used to send a response with static, fixed-size content (body). A MIME type can also be specified, which will be sent through the Content-Type header.

template<size_t N>
Async::Promise<ssize_t> send(
            Code code,
            const char (&arr)[N],
            const Mime::MediaType& mime = Mime::MediaType());

This version can also be used to send a fixed-size response with a body except that it does not need to construct a string (no memory is allocated). The size of the content is directly deduced by the compiler. This version only works with raw string literals.

These functions are asynchronous, meaning that they do not return a plain old ssize_t value indicating the number of bytes being sent, but instead a Promise that will be fulfilled later on. See the next section for more details on asynchronous programming with Pistache.

Response streaming

Sometimes, content that is to be sent back to the user can not be known in advance, thus the length can not be determined in advance. For that matter, the HTTP specification defines a special data-transfer mechanism called chunked encoding where data is sent in a series of chunks. This mechanism uses the Transfer-Encoding HTTP header in place of the Content-Length one.

To stream content, Pistache provides a special ResponseStream class. To get a ResponseStream from a ResponseWriter, call the stream() member function:

auto stream = response.stream(Http::Code::Ok);

To initate a stream, you have to pass the HTTP status code to the stream function (here Http::Code::Ok or HTTP 200). The ResponseStream class provides an iostream like interface that overloads the << operator.

stream << "PO"
stream << "NG"

The first line will write a chunk of size 2 with the content PO to the stream’s buffer. The second line will write a second chunk of size 2 with the content “NG”. To end the stream and flush the content, use the special ends marker:

stream << ends

The ends marker will write the last chunk of size 0 and send the final data over the network. To simply flush the stream’s buffer without ending the stream, you can use the flush marker:

stream << flush

Headers writing

After starting a stream, headers become immutable. They must be written to the response before creating a ResponseStream:

response.headers()
    .add<Header::Server>("lys")
    .add<Header::ContentType>(MIME(Text, Plain));

auto stream = response.stream();
stream << "PO" << "NG" << ends;

Static file serving

In addition to text content serving, Pistache provides a way to serve static files through the Http::serveFile function:

if (request.resource() == "/doc" && request.method == Http::Method::Get) {
    Http::serveFile(response, "README.md");
}

Return value

serveFile also returns a Promise representing the total number of bytes being sent to the wire

Controlling timeout

Sometimes, you might require to timeout after a certain amount of time. For example, if you are designing an HTTP API with soft real-time constraints, you will have a time constraint to send a response back to the client. That is why Pistache provides the ability to control the timeout on a per-request basis. To arm a timeout on a response, you can use the timeoufterAfter() member function directly on the ResponseWriter object:

response.timeoutAfter(std::chrono::milliseconds(500));

This will trigger a timeout if a response has not been sent within 500 milliseconds. timeoutAfter accepts any kind of duration.

When a timeout triggers, the onTimeout() function from your handler will be called. By default, this method does nothing. If you want to handle your timeout properly, you should then override this function inside your own handler:

void onTimeout(const Http::Request& request, Http::ResponseWriter writer) {
    request.send(Http::Code::No_Content);
}

The Request object that is passed to the onTimeout is the exact same request that triggered the timeout. The ResponseWriter is a complete new writer object.

ResponseWriter state

Since the ResponseWriter object is a complete new object, state is not preserved with the ResponseWriter from the onRequest() callback, which means that you will have to write the complete response again, including headers and cookies.

Asynchronous HTTP programming

Interfaces provided by Pistaches are asynchronous and non-blocking. Asynchronous programming allows for code to continue executing even if the result of a given call is not available yet. Calls that provide an asynchronous interface are referred to asynchronous calls.

An example of such a call is the send() function provided by the ResponseWriter interface. This function returns the number of bytes written to the socket file descriptor associated to the connection. However, instead of returning directly the value to the caller and thus blocking the caller, it wraps the value into a component called a Promise.

A Promise is the Pistache’s implementation of the Promises/A+ standard available in many Javascript implementations. Simply put, during an asynchronous call, a Promise separates the launch of an asynchronous operation from the retrieval of its result. While the asynchronous might still be running, a Promise<T> is directly returned to the caller to retrieve the final result when it becomes available. A so called continuation can be attach to a Promise to execute a callback when the result becomes available (when the Promise has been resolved or fulfilled).

auto res = response.send(Http::Code::Ok, "Hello World");
res.then(
    [](ssize_t bytes) { std::cout << bytes << " bytes have been sent" << std::endl; },
    Async::NoExcept
);

The then() member is used to attach a callback to the Promise. The first argument is a callable that will be called when the Promise has been succesfully resolved. If, for some reason, an error occurs during the asynchronous operation, a Promise can be rejected and will then fail. In this case, the second callable will be called. Async::NoExcept is a special callback that will call std::terminate() if the promise failed. This is the equivalent of the noexcept keyword.

Other generic callbacks can also be used in this case:

  • Async::IgnoreException will simply ignore the exception and let the program continue
  • Async::Throw will “rethrow” the exception up to an eventual promise call-chain. This has the same effect than the throw keyword, except that it is suitable for promises.

Exceptions in promises callbacks are propagated through an exception_ptr. Promises can also be chained together to create a whole asynchronous pipeline:

1 auto fetchOp = fetchDatabase();
2 fetchOp
3  .then(
4     [](const User& user) { return fetchUserInfo(user); },
5     Async::Throw)
6  .then(
7     [](const UserInfo& info) { std::cout << "User name = " << info.name << std::endl; },
8     [](exception_ptr ptr) { std::cout << "An exception occured during user retrieval" << std::endl;
9 });

Line 5 will propagate the exception if fetchDatabase() failed and rejected the promise.

Headers

Overview

Inspired by the Rust eco-system and Hyper, HTTP headers are represented as type-safe plain objects. Instead of representing headers as a pair of (key: string, value: value), the choice has been made to represent them as plain objects. This greatly reduces the risk of typo errors that can not catched by the compiler with plain old strings.

Instead, objects give the compiler the ability to catch errors directly at compile-time, as the user can not add or request a header through its name: it has to use the whole type. Types being enforced at compile-time, it helps reducing common typo errors.

With Pistache, each HTTP Header is a class that inherits from the Http::Header base class and use the NAME() macro to define the name of the header. List of all headers inside an HTTP request or response are stored inside an internal std::unordered_map, wrapped in an Header::Collection class. Invidual headers can be retrieved or added to this object through the whole type of the header:

auto headers = request.headers();
auto ct = headers.get<Http::Header::ContentType>();

get<H> will return a std::shared_ptr<H> where H: Header (H inherits from Header). If the header does not exist, get<H> will throw an exception. tryGet<H> provides a non-throwing alternative that, instead, returns a null pointer.

Built-in headers

Headers provided by Pistache live in the Http::Header namespace

Defining your own header

Common headers defined by the HTTP RFC (RFC2616) are already implemented and available. However, some APIs might define extra headers that do not exist in Pistache. To support your own header types, you can define and register your own HTTP Header by first declaring a class that inherits the Http::Header class:

class XProtocolVersion : public Http::Header {
};

Since every header has a name, the NAME() macro must be used to name the header properly:

class XProtocolVersion : public Http::Header {
    NAME("X-Protocol-Version")
};

The Http::Header base class provides two virtual methods that you must override in your own implementation:

 void parse(const std::string& data); 

This function is used to parse the header from the string representation. Alternatively, to avoid allocating memory for the string representation, a raw version can be used:

void parseRaw(const char* str, size_t len);

str will directly point to the header buffer from the raw http stream. The len parameter is the total length of the header’s value.

 void write(std::ostream& stream) const 

When writing the response back to the client, the write function is used to serialize the header into the network buffer.

Let’s combine these functions together to finalize the implementation of our previously declared header:

class XProtocolVersion : public Http::Header {
public:

    NAME("X-Protocol-Version")

    XProtocolVersion()
     : minor(-1)
     , major(-1)
    { }

    void parse(const std::string& data) {
        auto pos = data.find('.');
        if (pos != std::string::npos) {
            minor = std::stoi(data.substr(0, pos));
            major = std::stoi(data.substr(pos + 1));
        }
    }

    void write(std::ostream& os) const {
        os << minor << "." << major;
    }
private:
    int minor;
    int major;
};

And that’s it. Now all we have to do is registering the header to the registry system:

Header::Registry::registerHeader<XProtocolVersion>();

Header's instantation

You should always provide a default constructor for your header so that it can be instantiated by the registry system

Now, the XProtocolVersion can be retrieved and added like any other header in the Header::Collection class.

Unknown headers

Headers that are not known to the registry system are stored as a raw pair of strings in the Collection class. getRaw() can be used to retrieve a raw header:

auto myHeader = request.headers().getRaw("x-raw-header");
myHeader.name() // x-raw-header
myHeader.value() // returns the value of the header as a string

MIME types

MIME Types (or Media Type) are also fully typed. Such types are for example used in an HTTP request or response to describe the data contained in the body of the message (Content-Type header, …) and are composed of a type, subtype, and optional suffix and parameters.

MIME Types are represented by the Mime::MediaType class, implemented in the mime.h header. A MIME type can be directly constructed from a string:

auto mime = Http::Mime::MediaType::fromString("application/json");

However, to enforce type-safety, common types are all represented as enumerations:

Http::Mime::MediaType m1(Http::Mime::Type::Application, Http::Mime::Subtype::Json);

To avoid such a typing pain, a MIME macro is also provided:

auto m1 = MIME(Application, Json);

For suffix MIMEs, use the special MIME3 macro:

auto m1 = MIME3(Application, Json, Zip);

If you like typing, you can also use the long form:

Http::Mime::MediaType m1(Http::Mime::Type::Application, Http::Mime::Subtype::Json, Http::Mime::Suffix::Zip);

The toString() function can be used to get the string representation of a given MIME type:

auto m1 = MIME(Text, Html);
m1.toString(); // text/html

Routing

HTTP routing consists of binding an HTTP route to a C++ callback. A special component called an HTTP router will be in charge of dispatching HTTP requests to the right C++ callback. A route is composed of an HTTP verb associated to a resource:

GET /users/1

Here, GET is the verb and /users/1 is the associated resource.

HTTP methods

A bunch of HTTP methods (verbs) are supported by Pistache:

  • GET: The GET method is used by the client (e.g browser) to retrieve a ressource identified by an URI. For example, to retrieve an user identified by an id, a client will issue a GET to the /users/:id Request-URI.
  • POST: the POST method is used to post or send new information to a certain ressource. The server will then read and store the data associated to the request. POST is a common way of transmitting data from an HTML form. POST can also be used to create a new resource or update information of an existing resource. For example, to create a new user, a client will issue a POST to the /users path with the data of the user to create in its body.
  • PUT: PUT is very similar to POST except that PUT is idempotent, meaning that two requests to the same Request-URI with the same identical content should have the same effect and should produce the same result.
  • DELETE: the DELETE method is used to delete a resource associated to a given Request-URI. For example, to remove an user, a client might issue a DELETE call to the /users/:id Request-URI.

To sum up, POST and PUT are used to Create and/or Update, GET is used to Read and DELETE is used to Delete information.

Route patterns

Static routes

Static routes are the simplest ones as they do rely on dynamic parts of the Request-URI. For example /users/all is a static route that will exactly match the /users/all Request-URI.

Dynamic routes

However, it is often useful to define routes that have dynamic parts. For example, to retrieve a specific user by its id, the id is needed to query the storage. Dynamic routes thus have parameters that are then matched one by one by the HTTP router. In a dynamic route, parameters are identified by a column :

/users/:id

Here, :id is a dynamic parameter. When a request comes in, the router will try to match the :id parameter to the corresponding part of the request. For example, if the server receives a request to /users/13, the router will match the 13 value to the :id parameter.

Some parameters, like :id are named. However, Pistache also allows splat (wildcard) parameters, identified by a star *:

/link/*/to/*

Defining routes

To define your routes, you first have to instantiate an HTTP router:

Http::Router router

Then, use the Routes::<Method>() functions to add some routes:

Routes::Get(router, "/users/all", Routes::bind(&UsersApi::getAllUsers, this));
Routes::Post(router, "/users/:id", Routes::bind(&UsersApi::getUserId, this));
Routes::Get(router, "/link/*/to/*", Routes::bind(&UsersApi::linkUsers, this));

Routes::bind is a special function that will generate a corresponding C++ callback that will then be called by the router if a given route matches the Request-URI.

Callbacks

A C++ callback associated to a route must have the following signature:

void(const Rest::Request&, Http::ResponseWriter);

A callback can either be a non-static free or member function. For member functions, a pointer to the corresponding instance must be passed to the Routes::bind function so that the router knows on which instance to invoke the member function.

The first parameter of the callback is Rest::Request and not an Http::Request. A Rest::Request is an Http::Request will additional functions. Named and splat parameters are for example retrieved through this object:

void UsersApi::getUserId(const Rest::Request& request, Http::ResponseWriter response) {
    auto id = request.param(":id").as<int>();
    // ...
}

void UsersApi::linkUsers(const Rest::Request& request, Http::ResponseWriter response) {
    auto u1 = request.splatAt(0).as<std::string>();
    auto u2 = request.splatAt(1).as<std::string>();
    // ...
}

As you can see, parameters are also typed. To cast a parameter to the appropriate type, use the as<T> member template.

Cast safety

An exception will be thrown if the parameter can not be casted to the right type

Installing the handler

Once the routes have been defined, the final Http::Handler must be set to the HTTP Endpoint. To retrieve the handler, just call the handler() member function on the router object:

endpoint.setHandler(router.handler());

REST description

Documentation writing for this part is still in progress, please refer to the rest_description example

Swagger

API Reference