A production-grade HTTP/1.1 server implementation built from the ground up using only TCP sockets and the HTTP specification (RFC 9110 & RFC 9112). This project demonstrates how HTTP works under the hood by implementing a streaming parser, request/response handling, and concurrent connection management.
.
├── cmd/
│ ├── httpServer/ # Main HTTP server application
│ │ └── main.go # Routes: /, /video, /httpbin/*
│ ├── tcpListener/ # Raw TCP listener (debugging)
│ │ └── main.go
│ └── udpSender/ # UDP client (experiments)
│ └── main.go
├── internal/
│ ├── headers/ # HTTP header parsing
│ │ ├── headers.go # Set, Get, Parse, Iterate
│ │ └── headers_test.go
│ ├── request/ # HTTP request parsing
│ │ ├── request.go # State machine parser
│ │ └── request_test.go # Chunked reader tests
│ ├── response/ # HTTP response generation
│ │ └── response.go # Writer, status codes, headers
│ └── server/ # HTTP server implementation
│ └── server.go # Accept, handle, respond
├── assets/
│ └── demo.mp4 # Test video file
├── Notes.md # Implementation notes (detailed)
├── README.md # This file
└── go.mod
1. Basic HTML Response:
curl http://localhost:42069/
# Returns: 200 OK with HTML page2. Error Handling:
curl http://localhost:42069/problem
# Returns: 400 Bad Request with error HTML
curl http://localhost:42069/woopsie-daisy
# Returns: 500 Internal Server Error3. Binary Data (Video):
curl http://localhost:42069/video --output test.mp4
# Downloads demo.mp4
# Or open in browser:
open http://localhost:42069/video4. Chunked Transfer Encoding (Streaming):
curl http://localhost:42069/httpbin/stream/10
# Streams 10 chunks from httpbin.org
# Uses Transfer-Encoding: chunked
# Includes trailers: X-Content-SHA256, X-Content-Length5. POST with Body:
curl -X POST http://localhost:42069/test \
-H "Content-Type: application/json" \
-d '{"hello":"world"}'
# Returns: 200 OK (parses body correctly)HTTP requests arrive over TCP in arbitrary chunks. The parser handles incomplete data:
// Data might arrive like this:
Chunk 1: "GET /path HT" // Incomplete request line
Chunk 2: "TP/1.1\r\nHost: loc" // Complete line + partial header
Chunk 3: "alhost\r\n\r\n" // Complete header + empty lineState transitions:
StateInit (parse request line)
↓
StateHeaders (parse headers until empty line)
↓
hasBody() check → Yes: StateBody | No: StateDone
↓
StateBody (accumulate bytes until Content-Length reached)
↓
StateDone (parsing complete)
parseRequestLine(data []byte) (*RequestLine, int, error)
- Looks for
\r\ndelimiter - Returns 0 bytes consumed if incomplete
- Parses:
GET /path HTTP/1.1
Headers.Parse(data []byte) (int, bool, error)
- Parses multiple headers line by line
- Detects empty line (
\r\n\r\n) signaling end - Combines duplicate headers with commas
Request.parse(data []byte) (int, error)
- Main state machine loop
- Calls appropriate parser based on current state
- Returns bytes consumed
buf := make([]byte, 1024)
bufLen := 0
for !request.done() {
n, _ := reader.Read(buf[bufLen:]) // Read into buffer
bufLen += n
readN, _ := request.parse(buf[:bufLen]) // Parse accumulated data
copy(buf, buf[readN:bufLen]) // Shift unparsed data
bufLen -= readN
}Why this works:
- Accumulates data across multiple reads
- Parses as much as possible each iteration
- Keeps unparsed data for next read
- Handles any chunk size (1 byte to 1MB)
Unlike request line and headers (delimiter-based), body uses byte counting:
length := getInt(headers, "content-length", 0) // Get expected size
remaining := min(length - len(body), len(chunk)) // How much to read
body = append(body, chunk[:remaining]...) // Accumulate
if len(body) == length {
// Body complete!
}For streaming responses where size is unknown:
Transfer-Encoding: chunked\r\n
\r\n
20\r\n ← Hex size (32 bytes)
{32 bytes of data}\r\n
1a\r\n ← Hex size (26 bytes)
{26 bytes of data}\r\n
0\r\n ← Zero-length chunk
X-Trailer: value\r\n ← Optional trailers
\r\n
- RFC 9110 - HTTP Semantics (applies to all HTTP versions)
- RFC 9112 - HTTP/1.1 Message Syntax and Routing
- RFC 7231 - HTTP/1.1 Semantics and Content (older)
- ❌ HTTP/2 and HTTP/3 not supported
- ❌ HTTPS/TLS not implemented
- ❌ Chunked request body parsing (only responses)
- ❌ Request body larger than buffer size
- ❌ Keep-alive connections (closes after each request)
- ❌ Compression (gzip, brotli)
- ❌ Multipart form data