This project was created as part of the 42 curriculum by lmoraes.
A non-blocking HTTP/1.1 server written from scratch in C++98. No frameworks, no libraries — just sockets, poll(), and the RFC.
The goal was to build an HTTP server that holds up under the same kind of constraints a production server has to deal with: many clients connected at the same time, no thread per request, configurable routes, and behavior consistent with the RFC under load.
The project forced me to think about networking, parsing, and resource management at a level most web work hides behind a framework.
- Listens on multiple
interface:portpairs defined in an NGINX-style config file - Handles many concurrent connections through a single
poll()loop (no threads, no fork-per-request, no read/write outside ofpoll) - Parses HTTP/1.1 requests by hand — request line, headers, body
- Serves
GET,POST, andDELETE - Serves static files, lists directories when
autoindexis on, applies index files and custom error pages - Supports HTTP redirects per route
- Supports file upload via
multipart/form-datawith a configurable upload directory - Runs CGI scripts based on file extension (executed via
execve, communicates over pipes) - Returns proper status codes and stays available under sustained load
- Enforces
client_max_body_sizeper server
┌──────────────┐
│ Config │ parses NGINX-style file once at boot
└──────┬───────┘
▼
┌──────────────┐
client ───────▶ │ poll() loop │ ──┐ one loop, all sockets non-blocking
└──────┬───────┘ │
▼ │
┌─────────────┐ │
│ Request │ ◀───┘ parses bytes as they arrive
│ parser │
└──────┬──────┘
▼
┌──────────────┐
│ Handler │ static / upload / CGI / redirect / error
└──────┬───────┘
▼
┌──────────────┐
│ Response │ writes back when socket is ready
└──────────────┘
Every socket — listening or connected — is registered with the same poll() call. When the kernel reports a socket as ready to read or write, the loop services it for one step and goes back to polling. No request blocks another, and no read or write happens without poll() clearing it first.
.
├── Makefile
├── main.cpp
├── includes/ # headers
├── src/ # server, parser, response, CGI, config
├── config/ # example config files
└── www/ # default document root for testing
Requires a C++98 compiler (g++ works) and make. Tested on Linux.
git clone https://github.com/lmoraesdev/webserv.git
cd webserv
make
./webserv config/default.confThen point a browser or curl at any of the host/port pairs declared in the config:
curl -i http://localhost:8080/
curl -i -X POST -F "file=@photo.png" http://localhost:8080/uploadThe server reads an NGINX-style config file. A minimal example:
server {
listen 8080;
root ./www;
index index.html;
error_page 404 /errors/404.html;
client_max_body_size 10M;
location / {
allow_methods GET POST DELETE;
autoindex on;
}
location /old {
return 301 /new;
}
location /upload {
allow_methods POST;
upload_store ./uploads;
}
location /cgi-bin/ {
cgi_extension .py;
cgi_path /usr/bin/python3;
}
}Multiple server blocks on different ports are supported.
- That
poll()looks simple in the man page and is brutal in practice — the bug isn't in poll, it's in how you treat partial reads, half-closed sockets, and the order of events you process - How TCP actually behaves: requests don't arrive in one piece, responses don't leave in one piece, and any code that assumes they do will eventually break
- Why writing an HTTP parser by hand makes you respect every framework that did it for you
- The cost of every
malloc, everyread, everyclosewhen there are no abstractions to hide them - That C++98 without smart pointers is a real exercise in RAII discipline
- RFC 7230 — HTTP/1.1 message syntax
- RFC 7231 — HTTP/1.1 semantics
- Beej's Guide to Network Programming
- NGINX directives
- CGI specification — RFC 3875
This project was delivered before AI assistants were allowed by the 42 curriculum. All design, code, and debugging were done manually using the RFCs, man pages, and peer discussions.