User's Guide
- Namespace
- Http Handler
- Asynchronous HTTP programming
- Headers
- Routing
- REST description
- I/O model
- API Reference
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
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 continueAsync::Throw
will “rethrow” the exception up to an eventual promise call-chain. This has the same effect than thethrow
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.
Note that common headers 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>();
Now, the XProtocolVersion
can be retrieved and added like any other header in the Header::Collection
class.
MIME types
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.
To define a route, you first have to instantiate an object of type Http::Router
:
Http::Router router
Then, you can start adding routes:
Routes::Post(router, "/record/:name/:value?", Routes::bind(&StatsEndpoint::doRecordMetric, this));
Routes::Get(router, "/value/:name", Routes::bind(&StatsEndpoint::doGetMetric, this));
Routes::Get(router, "/ready", Routes::bind(&Generic::handleReady));