1. Introduction
HTTP requests received by a Crails server are first directed through the request pipe. The pipe consists of request parsers and handlers. The request run through each of these handlers sequentially, until one of the parser or handler responds to or rejects the query.
You can configure which parsers and handlers are enabled in your server, and in which order they
should run, by editing the config/request_pipe.cpp
, which looks like this:
#include <crails/request_handlers/file.hpp>
#include <crails/request_handlers/action.hpp>
#include <crails/request_parsers/url_parser.hpp>
#include <crails/request_parsers/form_parser.hpp>
#include <crails/request_parsers/multipart_parser.hpp>
#include "server.hpp"
using namespace Crails;
void ApplicationServer::initialize_request_pipe()
{
add_request_parser(new RequestUrlParser);
add_request_parser(new RequestFormParser);
add_request_parser(new RequestMultipartParser);
add_request_handler(new ActionRequestHandler);
add_request_handler(new FileRequestHandler);
}
The request pipe first runs the parsers, then the handlers. In both case, they will run in the same order
as the calls for add_request_parser
and add_request_handler
.
1.1 Adding support for queries in json format
Crails comes with a request parser that can load the json body of a request into the query's params
object. To add this request parser, you would need to include its header file, and register it by calling
add_request_parser
from the initialize_request_pipe
method.
In the following example, we use the json request parser with only the action request handler, to configure our crails server as a lightweight web service.
#include <crails/request_parsers/json_parser.hpp>
#include <crails/request_handlers/action.hpp>
#include "server.hpp"
using namespace Crails;
void ApplicationServer::initialize_request_pipe()
{
add_request_parser(new RequestJsonParser);
add_request_handler(new ActionRequestHandler);
}
2. Writing your own request handler
There are cases in which you may want to intercept queries before they reach the application's router.
Or perhaps routing isn't a good strategy for you, and you want to replace ActionRequestHandler
with a solution which better fits your needs.
That's when writing a request handler becomes a viable option. Writing request handlers is pretty straightforward. Here's an example:
#pragma once
#include <crails/request_handler.hpp>
class MyRequestHandler : public Crails::RequestHandler
{
public:
MyRequestHandler() : Crails::RequestHandler("my request handler's name")
{
}
void operator()(Crails::Context& context, std::function<void(bool) callback) const override
{
if (context.params["uri"].as<string>() == "/my_request_handler")
{
context.response.set_response(
Crails::HttpStatus::ok,
"Hi ! This is your very own request handler speaking !"
);
callback(true);
}
else
callback(false);
}
};
Simple stuff, really: overload operator()
, receive a
Crails::Context
and a callback object.
The callback must be called at least once, and only once.
Calling the callback with the
value true
will cause the request pipe to stop running the requests through the
handlers, and send directly the response.
Calling the callback with the value false
will pass the request on to the next request handler,
or if there are no more handlers to run, will respond to the query with a 404 not found status.
The request pipe can work asynchronously. You may safely invoke the callback from any thread.
However, to make sure that the Crails::Context
object doesn't get deleted,
you must keep around a reference to its shared pointer:
auto context_ptr = context.shared_from_this();
some_asynchronous_function([context_ptr, callback]()
{
callback(true);
});
3. Writing your own parser
The process to writing a request parser is very similar to the request handler. Let's try a practical use case where we'll implement a partial YAML parser. First steps first, let's scaffold our request parser:
#pragma once
#include <crails/request_parser.hpp>
class MyRequestParser : public Crails::RequestParser
{
public:
void operator()(
Crails::Context& context,
std::function<void(Crails::RequestParser::Status)> callback) const
{
callback(Continue);
}
};
While this is similar to a request handler, you'll notice that the callback takes a different kind of parameter. This parameter is used by parsers to tell the request pipe whether the request is completely parsed, or if other parsers may take a look at it.
There are three values you may send to the callback:
-
Continue
is used when other parsers in the pipe should be allowed to take a look at the request. -
Stop
is used when the request has been completely parsed. The pipe skips all the other parsers and goes straight to the handlers. -
Abort
is used when the parser has already solved the request. It is typically used when the request is invalid. The following parsers and handlers won't run.
3.1 Accepting a request
We only want our parser to run when the client is sending us a YAML body. Other
requests must not be treated by our parser. To achieve that, we will check the
"Accept" header on the request by using content_type_matches
, a helper
method provided by Crails::RequestParser
:
#pragma once
#include <crails/request_parser.hpp>
class MyRequestParser : public Crails::RequestParser
{
public:
void operator()(
Crails::Context& context,
std::function<void(Crails::RequestParser::Status)> callback) const
{
static const std::regex is_yaml("text/yaml", std::regex_constants::optimize);
bool has_acceptable_type = content_type_matches(params, is_yaml);
bool has_relevant_method = context.params["method"].as<std::string>() != "GET";
if (has_acceptable_type && has_relevant_method)
callback(Stop);
else
callback(Continue);
}
};
By using callback(Stop)
when we recognize that a request should be handled
by our parser, we stop the parsing process and send the request directly to the request
handlers. But we haven't actually parsed the query. Now that we know how to intercept it,
let's see how to actually read its contents.
3.2 Reading the body of a request
Since we are going to parse YAML, we will need access to the query's body. However, the
request pipe doesn't wait for the request body to arrive before starting to run the parsers
and handlers. If your parser needs to wait for the request body to have been delivered,
you'll need to use Crails::BodyParser
and the wait_for_body
helper:
#pragma once
#include <crails/request_parser.hpp>
class MyRequestParser : public Crails::BodyParser
{
public:
void operator()(
Crails::Context& context,
std::function<void(Crails::RequestParser::Status)> callback) const
{
static const std::regex is_yaml("text/vnd.yaml", std::regex_constants::optimize);
bool has_acceptable_type = content_type_matches(params, is_yaml);
bool has_relevant_method = context.params["method"].as<std::string>() != "GET";
if (has_acceptable_type && has_relevant_method)
{
wait_for_body(context, [&context, callback]()
{
bool was_valid_yaml = context.response.get_status_code() != Crails::HttpStatus::bad_request);
callback(was_valid_yaml ? Stop : Abort);
});
}
else
callback(Continue);
}
void body_received(Crails::Context& context, const std::string& body) const override
{
bool success = parse_yaml(context, body);
if (!success)
context.response.set_status_code(Crails::HttpStatus::bad_request);
}
private:
bool parse_yaml(Crails::Context& context, const std::string& body) const
{
// This is where we'll implement our YAML parser
return false;
}
};
In this example, we've replace Crails::RequestParser
with Crails::BodyParser
,
a more specific type of parser designed for parsing request bodies, while the former type of parsers
are best used to handle the contents of the request headers.
We then use wait_for_body
so that our parser only runs if or when the body is available.
The parameters for wait_for_body
are the request's Context
and a callback
that will callback to the request pipeline with the parser's status code, after the request has been parsed.
Once the request body has been entirely received, it will be forwarded to the body_received
method: this is where we call our own custom method, which we called parse_yaml
, in which we
will later implement the actual parser.
After body_received
returns, the callback we passed as parameter to wait_for_body
will be called in turn, and the request pipeline will proceed to either the request handlers or end
the request, depending on whether we responded respectively with Stop
or Abort
.
Continue
status after parsing a request body,
however this may have some wicked repercussions. A request body can only be received a single time: if
two body parsers were to use wait_for_body
on the same request, the pipeline would hang
undefinitely. As such, it is best to make it so that body parsers are always the last parser to run in
a pipeline, and do not allow other parsers to run after them.
If the parsing fails, we immediately set a response status (400 bad request). This is later
used in the wait_for_body
callback to determine whether the parsing was successful or not.
3.3 Fill in the Params object
We've seen how to receive a request body, but we aren't yet doing anything with it. In the following chapter, we'll use the yaml-cpp library to parse our YAML body and store it without the context's params attribute.
The params
attribute inherits from the DataTree
class, which is used to store data in a structure independent from the query format. Here's how we would store a YAML::Node
into a DataTree
:
static void load_yaml_tree(YAML::Node node, DataTree& tree)
{
for (auto it = node.begin() ; it != node.end() ; ++it)
{
std::string key = it->first.as<string>();
tree[key] = it->second.as<string>();
}
}
This simple function maps a YAML map of strings into our DataTree
object. Let's try something a bit more complex, where
we can recursively load any structure of YAML:
static void load_yaml_map(YAML::Node node, Data branch);
static void load_yaml_array(YAML::Node node, Data branch);
static void load_yaml_node(YAML::Node node, Data branch);
// We'll first rewrite our function to use `load_yaml_node`, which
// we will design to be recursively callable on any branch of the tree:
static void load_yaml_tree(YAML::Node node, DataTree& tree)
{
load_yaml_node(node, tree.as_data());
}
// Now we implement `load_yam_node` to filter node by types:
// Sequences and Map will have their own functions, while
// other types will be assigned to the branch as character strings:
static void load_yaml_node(YAML::Node node, Data branch)
{
switch (node.Type())
{
case YAML::NodeType::Null:
break ;
case YAML::NodeType::Sequence:
load_yaml_array(node, branch);
break ;
case YAML::NodeType::Map:
load_yaml_map(node, branch);
break ;
default:
branch = node.as<string>();
break ;
}
}
// For maps, we will just register them as new branches
// of the tree:
static void load_yaml_map(YAML::Node node, Data branch)
{
for (auto it = node.begin() ; it != node.end() ; ++it)
{
std::string key = it->first.as<string>();
load_yaml_node(it->second, branch[key]);
}
}
// As for arrays/sequences, we store them using the
// `push_back` method of the Data object:
static void load_yaml_array(YAML::Node node, Data branch)
{
for (auto it = node.begin() ; it != node.end() ; ++it)
branch.push_back(it->second.as<string>());
}
We now have implemented all the functions needed to store a YAML node into a DataTree. Let's
now implement the body_received
callback of our MyRequestParser
class:
void MyRequestParser::body_received(Crails::Context& context, const std::string& body) const
{
YAML::Node node = YAML::Load(body);
load_yaml_tree(node, context.params);
}
Now there's still an issue remaining: if the parsing fail, YAML::Load
will throw an
exception, and our server will respond with an error 500. That would be inaccurate, as the error
lies in the query. Let's update our body_received
method to handle parsing errors:
void MyRequestParser::body_received(Crails::Context& context, const std::string& body) const
{
try
{
YAML::Node node = YAML::Load(body);
load_yaml_tree(node, context.params);
}
catch (const std::exception& e)
{
context.response.set_status_code(Crails::HttpStatus::bad_request);
}
}
3.4 Streaming a request body
In some very specific cases, you may receive a request body that's too big to reasonably handle in a single step. In which case, you'll want to receive the body asynchronously, chunk by chunk: this can be done by setting a callback on the context's connection object.
#pragma once
#include <crails/request_parser.hpp>
class MyRequestParser : public Crails::RequestParser
{
public:
void operator()(
Crails::Context& context,
std::function<void(Crails::RequestParser::Status)> callback
) const
{
auto& connection = *context.connection;
auto& request = connection.get_request();
if (*(request.content_length()) > 0)
{
connection.on_received_body_chunk([this, callback](std::string_view chunk)
{
// Insert your handler here
});
}
else
callback(Continue);
}
};
This is the basic gist of things. Note that we check for the body's content length first: if you expect for an empty body to arrive, your query will just hang undefinitely, resulting in a memory leak.
Now we can't completely handle the request without knowing when it ends. This requires us to record how many bytes we've read... but since request parsers are stateless, we'll have to create a different object to record our state:
#pragma once
#include <crails/request_parser.hpp>
class MyParserState
{
int bytes_remaining;
};
Then we'll create a smart pointer towards our state, and store it within our callback for
Connection::on_received_body_chunk
:
class MyRequestParser : public Crails::RequestParser
{
public:
void operator()(
Crails::Context& context,
std::function<void(Crails::RequestParser::Status)> callback
) const
{
auto& connection = *context.connection;
auto& request = connection.get_request();
if (*(request.content_length()) > 0)
{
auto state = std::make_shared<MyParserState>();
state->bytes_remaining = *request.content_length();
connection.on_received_body_chunk(
std::bind(
&MyRequestParser::read_body_chunk,
state,
std::placeholders::_1,
callback
)
);
}
else
callback(Continue);
}
...
};
on_received_body_chunk
callbacks gets removed by the server as soon as a requests completes
or gets interrupted, ensuring any object we store in these callbacks get destroyed whenever they're no longer
needed.
Now all that's left to do is implement the read_body_chunk
method, update our state and call the
pipeline's callback once the body has been fully received:
class MyRequestParser : public Crails::RequestParser
{
...
private:
void read_body_chunk(
std::shared_ptr<MyParserState> state,
std::string_view chunk,
std::function<void(Crails::RequestParser::Status)> callback
) const
{
state->bytes_remaining -= chunk.length();
if (state->bytes_remaining <= 0)
callback(Stop);
}
};
3.5 Set the size limit for request bodies
TODO