Overview
webserv is a toy HTTP/1.0 and HTTP/1.1 server for Linux, written in C++98 with
no external libraries. I built it to explore what sits underneath a web
framework: parsing a byte stream, managing connections, mapping requests to
files, and producing correct HTTP responses. It uses one non-blocking epoll
event loop and an nginx-inspired configuration file, and can host multiple
virtual servers, serve static sites, accept uploads, delete resources, redirect
requests, and execute CGI programs.
Visit the repository for source and setup instructions.
Collaborators
webserv was a collaborative project built with:
What I Built
My work focused on the request-handling path: turning bytes received from a client into a routed request and, ultimately, a complete HTTP response.
-
Incremental HTTP parsing. I implemented a stateful parser for request lines, headers, and bodies, so partial reads do not block the event loop. It validates request syntax, supported methods and protocol versions, required headers, duplicate/conflicting framing headers, and configurable size limits. Request bodies support both
Content-LengthandTransfer-Encoding: chunked. -
Routing and request policy. Requests are percent-decoded and normalized before the longest matching location is selected. The routing step chooses a virtual server from
Host, applies per-route method and body-size policies, builds a path beneath the configured root, and rejects traversal and symlink escapes. -
Handler pipeline. I split response generation into focused handlers for static files, directories and index files, autoindex pages, uploads,
DELETE, redirects, CGI, and errors. Each handler prepares status, headers, connection behaviour, and either an in-memory or streamed file body. -
Upload and error behaviour. Uploads are written incrementally to temporary storage before being placed in the configured upload directory; this keeps large bodies out of memory. Failures are translated into HTTP status codes with
Allowheaders where applicable, custom configured error pages when available, and a small fallback HTML page otherwise.
Details
Continuity despite unpredictable input
TCP is a byte stream: one recv() may contain half a request line, several
headers, or part of the next request. The parser therefore keeps its phase and
per-request progress between reads instead of assuming that a whole request
arrives at once. It advances only over bytes it has successfully understood,
reports back the number of bytes that were consumed so that the ring
buffer can get rid of the already consumed data in the connection buffer
then resumes at the same point when more data becomes available. This also
makes HTTP keep-alive possible: once a response is finished, the context is
reset for the next request without losing unread bytes already buffered from
the connection.
Non-blocking work as a state machine
The server does no waiting inside request handling. Socket I/O is performed
only after epoll signals that a descriptor is readable or writable, while
parsing, routing, and response generation move the connection through explicit
read, process, write, and close states. A handler that still needs a request
body yields control back to the event loop rather than blocking another
connection. Parser and CGI timeouts provide an escape hatch for clients or
programs that stop making progress.
Efficient, bounded buffering
Input is consumed from a reusable connection buffer, avoiding a new allocation for every network read. Small request bodies remain in memory, while larger bodies spill into a temporary file and are exposed to handlers through the same reader interface. Responses use the same idea: generated pages can be kept in memory, but static files are streamed. The result is predictable memory use even when serving files or accepting uploads that are much larger than the socket buffer.
Validate before touching the filesystem
Routing is more than matching a URI to a directory. The request target is percent-decoded and normalized before a route is selected; malformed escapes, attempts to walk above the root, and symlinks that resolve outside it are rejected. Method, body-framing, body-size, and upload permissions are checked before a handler accesses a resource. Keeping these decisions in one dispatch step makes the individual handlers small and gives errors a consistent HTTP response.
Responses are protocol messages, not just output
Each handler builds a complete HTTP response: a status code, headers, and a
body whose framing agrees with the request's HTTP version and connection
preferences. Static resources receive a MIME type, Content-Length, and
modification time; redirects carry a Location; and keep-alive is negotiated
through Connection rather than assumed. Internal failures are mapped to the
appropriate client-visible status—such as 404, 403, 405, 413, or
504—so a malformed request or unavailable resource produces a useful,
standards-shaped response instead of a dropped connection.
Notes
Build with make, then run ./webserv [configuration-file]. This is a
learning project, not a production server: it is intentionally limited to
C++98 and POSIX/Linux facilities—no Boost or other HTTP libraries—so parsing,
buffering, response framing, and file handling are all implemented in the
repository. The connection loop enforces request and body-progress timeouts and
caps the number of requests served on one connection, which keeps keep-alive
clients from holding resources forever.