big-fat-dinosours-w-short-front-legs/
├── CMakeLists.txt (builds the project)
├── .gitignore (keeps remote repo clean)
├── cmake/ (generates coverage report)
├── docker/ (container setup— for base, production, coverage, + Cloud Build)
├── include/ (header files for source code implementations)
├── src/ (src code to run the server, parse config, handle requests, etc.)
└── tests/ (contains unit test files, test configs, and integration test script)
└── test_static/ (where temp files are made for static handler tests
The top‑level CMakeLists.txt drives all of our build setup. To do so, it:
- Defines the project
- Enforces out‑of‑source builds
- Sets sensible defaults
- Pulls in external dependencies
GoogleTestfor unit testing (via add_subdirectory)Boost(static libs: system, log, log_setup, regex)Python 3interpreter for integration tests- Adds
include/to the compiler’s header search path
- Declares our core libraries & executable
config_lib– handles parsing and storing configurationnetwork_lib– implements the HTTP server, handlers, MIME types, etc.webserver– main executable (src/_main.cc), linked against both libs plusBoostandpthread
- Configures testing (via
CTest) for unit tests and our integration test - Enables code coverage reports
- Provides a reset target
make reset(or cmake --build . --target reset) wipes out all generated files and cache so you can start fresh
- Create a /build directory in the root folder of the repo
- Ensure you are in the dev environment, then navigate to the /build directory and run:
cmake ..
- After this finished, run:
make
- If this completes without error, the build is complete
- If you want to re-build, without deleting the build directory, run the following in sequence:
make reset(removes old artifacts and runs cmake .. again)make
- Navigate to the /build directory with
cd build, ensure that the build is complete: - Run:
make testORctest
- Create a config that you want to be used for this server instance in the /tests directory (for example, see:
my_configortemp_config) - Configure the config with the server port and handler paths you desire for echo and static handling
- Create relevant files for the static handler at the indicated path
- Navigate to the /build directory, ensure that the build is complete
/bin/webserver ../tests/my_config- If you want to serve a static file request, run the following in another terminal:
curl -i http://localhost:8080/static/{$RELATIVE-PATH-TO-STATIC-FILE} - If you want to serve an echo request, run either of the following in another terminal:
curl -i http://localhost:${PORT}/echocurl -i \ -X POST \ -H "User-Agent: TestAgent/1.0" \ -H "X-Custom-Header: test-value" \ -d "hello_echo" \http://localhost:${PORT}/echo/my/path
In our code, a base IRequestHandler class creates the virtual functions CanHandle and HandleRequest. These are overridden by derived classes such as EchoHandler, StaticHandler, and NotFoundHandler.
- Create a new
.hand.ccfile for their handler, having it extend theIRequestHandler classand override the two virtual IRequestHandler functions,CanHandleandHandleRequestat minimum. - After, they should also add their handler to the mapping member
handlers_in server_runner.cc’s setup_server function so that the handler’s methods are callable. - Then, they should register the new .cc file in CMakeLists.txt’s
network_liblibrary by adding the .cc file to network_lib’sadd_library()call. - Additionally, one could create unit tests via a
<handler_name>_test.ccfile in thetests/directory, and register the test by adding it as an executable viaadd_executable(), linking it with the required libraries viatarget_link_libraries(), and making it discoverable by gtest viagtest_discover_tests().
struct IRequestHandler {
virtual ~IRequestHandler() = default;
// “Can I serve this request?”
virtual bool CanHandle(const HttpRequest &req) const = 0;
// Fill in resp (status‐line, headers, body)
virtual void HandleRequest(const HttpRequest &req, HttpResponse &resp) = 0;
};
using IRequestHandlerPtr = std::shared_ptr<IRequestHandler>;class EchoHandler : public IRequestHandler {
public:
// returns true if the request is an echo request
bool CanHandle(const HttpRequest &req) const override;
// fills in resp with the request details
// including the raw request string
void HandleRequest(const HttpRequest &req, HttpResponse &resp) override;
};EchoHandler overrides the virtual functions with the following implementation: echo_handler.cc (omitting logs)
// returns true if the request is an echo request
bool EchoHandler::CanHandle(const HttpRequest &req) const {
static constexpr char const* PREFIX = "/echo";
// exactly “/echo” or any path that starts with “/echo/”
return req.path == PREFIX
|| req.path.rfind(std::string(PREFIX) + "/", 0) == 0;
}
// fills in resp with the request details
// including the raw request string
void EchoHandler::HandleRequest(const HttpRequest &req, HttpResponse &resp) {
resp.version = "HTTP/1.1";
resp.status_code = 200;
resp.reason = "OK";
resp.headers["Content-Type"] = "text/plain";
resp.body = req.raw; // echo entire raw request
}Then, in server_runner.cc’s setup_server function, an instance of EchoHandler is added to the handler mappings:
{...}
// Build handler list from every top‐level 'handler { ... }' block
handlers_.clear();
auto handlers_conf = config.GetHandlerConfigs();
for (auto &hc : handlers_conf) {
if (hc.type == "echo")
handlers_.push_back(std::static_pointer_cast<IRequestHandler>
(std::make_shared<EchoHandler>()));
else if (hc.type == "static"){
std::string root = hc.args.count("root") ? hc.args["root"] : "";
handlers_.push_back(std::static_pointer_cast<IRequestHandler>(
std::make_shared<StaticHandler>(hc.path, hc.root)));
}
}
handlers_.push_back(std::static_pointer_cast<IRequestHandler>(
std::make_shared<NotFoundHandler>()));Then, when the session is established, upon receiving a request, EchoHandler’s CanHandle and HandleRequest functions are called if the request is an echo request in session’s handle_read function:
{...}
HttpResponse resp;
for (auto &h : handlers_) {
if (h->CanHandle(req)) {
h->HandleRequest(req, resp);
}
{...}
}Parses TCP data into an HttpRequest object, extracting method, path, headers, and body.
Parses the raw HTTP request into an HttpRequest Object
Stores the request into method, path, version, headers,
body, and raw request data
Builds HttpResponse objects, letting handlers set status codes, headers, and body content which then get flattened into bytes to send back over the network.
Returns the response as a string
Returns a 404 response
Contains a lookup table mapping file extensions (e.g. .html, .png) to their correct MIME strings—used for serving static files so clients know how to interpret the payload.
Builds a map of common MIME type extensions
Extracts an extension from the filename/path, and
determines the MIME type
A pure virtual abstract class that documents common functions implemented/overridden by all derived handlers (echo, static, 404)
Provides a simple HTTP handler that echoes the requests it gets.
Determines if the HTTP Request is an echo request based on the path
Builds response with the request details to echo it back
Includes the raw request in the response
Provides the StaticHandler, which maps URL paths to files on disk under your webroot: it checks for file existence, reads file contents into the response body, and uses the MIME lookup (mime_types.cc) to set the correct Content-Type header when serving assets.
StaticHandler(std::string uri_prefix, std::string doc_root): prefix_(std::move(uri_prefix)), root_(std::move(doc_root)) {}
Takes a prefix and root and saves them as member variables
Determines if the request is to a static file based on the path
Builds request by serving the file at the path, filling in the HTTP response information along with the body of the file information
Implements the fallback “404 Not Found” handler: when no other route matches, this returns a 404 status and a simple error page.
Always returns true to handle all requests as the fallback
Returns a standard 404 response
Orchestrates the main server loop—binding to your chosen port, accepting connections, handing off each parsed HttpRequest to the right handler, and sending back the resulting HttpResponse.
static std::vector<IRequestHandlerPtr> handlers_;
Sets up the server by parsing the config file, retrieving
& validating the port number, and builds the handler mapping,
with the 404 handler being the last one, as a fallback
Defines the Server class, which glues together configuration, handler registration, and the server runner: allowing ServerRunner to begin accepting connections.
Implements the Session abstraction for a single client connection—responsible for reading raw socket data, invoking the request parser, dispatching the parsed HttpRequest to the correct handler, and writing back the serialized HttpResponse, all while managing connection lifetimes (keep‑alive, timeouts).
Determines which handler in the mapping is able to serve
the incoming HTTP request and calls the corresponding handler
Our CRUDHandler listens under an /api prefix. You can perform standard CRUD (Create, Read, Update, Delete) operations on any “entity” (e.g. Shoes, Users, Products) by hitting these URLs:
We expose the following endpoints under /api/{entity}:
| Action | Method | Path | Description |
|---|---|---|---|
| Create | POST | /api/{entity} | Create a new resource |
| List | GET | /api/{entity} | Retrieve all resources |
| Read | GET | /api/{entity}/{id} | Retrieve one resource by ID |
| Update | PUT | /api/{entity}/{id} | Update an existing resource |
| Delete | DELETE | /api/{entity}/{id} | Delete an existing resource |
All requests and responses use application/json.
Request Headers
Content-Type: application/json
Accept: application/json
Request Body
{
// entity-specific fields
}Response Body
- Create/Read/Update: full JSON of the resource, including
id. - Delete: JSON message confirming deletion.
Request
curl -i -X POST http://localhost:8080/api/shoes \
-H "Content-Type: application/json" \
-d '{"name":"AirMax","size":9,"color":"red"}'
Response
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": 1,
"name": "AirMax",
"size": 9,
"color": "red"
}Request
curl -i http://localhost:8080/api/shoes
Response
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"id": 1,
"name": "AirMax",
"size": 9,
"color": "red"
}
]Request
curl -i http://localhost:8080/api/shoes/1
Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 1,
"name": "AirMax",
"size": 9,
"color": "red"
}Request
curl -i -X PUT http://localhost:8080/api/shoes/1 \
-H "Content-Type: application/json" \
-d '{"size":10}'
Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 1,
"name": "AirMax",
"size": 10,
"color": "red"
}Request
curl -i -X DELETE http://localhost:8080/api/shoes/1
Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"message": "Shoe with id 1 deleted successfully"
}