1
An HTTP/1.1 server, from scratch, in Go[github]
1
2
An HTTP/1.1 server built from scratch in Go directly on raw TCP,
3
without using net/http for the server itself — it carves the
4
request line, header block, and body out of the byte stream by
5
hand, validates field names against the RFC tchar set, and reads
6
bodies strictly against Content-Length.
7
8
Serializes responses by hand and supports chunked transfer
9
encoding with trailer fields (X-Content-SHA256, X-Content-Length)
10
computed once the body is fully sent, plus streamed file
11
responses, reverse proxying, goroutine-per-connection concurrency
12
and graceful shutdown. Built end-to-end as the Boot.dev 'Learn
13
HTTP Protocol' course, starting from raw UDP datagrams.
14
15
16
18
19
func ParseHeaders(lines []string) (Headers, error) {
20
res := make(map[string]string)
21
for _, line := range lines {
22
name, value, err := parseHeader(line)
23
if err != nil {
24
return res, fmt.Errorf("cannot parse header '%s': %w", line, err)
25
}
26
27
// 1. field name is case insensitive
28
// so we just make all lowercase for simplicity
29
lowerCasedName := strings.ToLower(name)
30
31
// 2. if there are repetitive field names - their values are joined with ' ,'
32
if _, hasValue := res[lowerCasedName]; hasValue {
33
res[lowerCasedName] = fmt.Sprintf("%s,%s", res[lowerCasedName], value)
34
} else {
35
res[lowerCasedName] = value
36
}
37
}
38
39
return res, nil
40
}
41
42
43
44
45
46
47
48
49
50
51
func ParseHeaders(lines []string) (Headers, error) {
res := make(map[string]string)
for _, line := range lines {
name, value, err := parseHeader(line)
if err != nil {
return res, fmt.Errorf("cannot parse header '%s': %w", line, err)
}
// 1. field name is case insensitive
// so we just make all lowercase for simplicity
lowerCasedName := strings.ToLower(name)
// 2. if there are repetitive field names - their values are joined with ' ,'
if _, hasValue := res[lowerCasedName]; hasValue {
res[lowerCasedName] = fmt.Sprintf("%s,%s", res[lowerCasedName], value)
} else {
res[lowerCasedName] = value
}
}
return res, nil
}
func parseRequestLine(requestLine string) (RequestLine, error) {
res := RequestLine{}
requestLineChunks := strings.Fields(requestLine)
if len(requestLineChunks) != 3 {
return res, fmt.Errorf(
"request line should consist of three parts separated by ' ', but was %s",
requestLine,
)
}
if !slices.Contains(allowedMethods, requestLineChunks[0]) {
return res, fmt.Errorf(
"method must be one of [%s], but was '%s'",
allowedMethods,
requestLineChunks[0],
)
}
res.Method = requestLineChunks[0]
res.RequestTarget = requestLineChunks[1]
r := regexp.MustCompile(`HTTP/\d\.\d`)
if !r.Match([]byte(requestLineChunks[2])) {
return res, fmt.Errorf(
"http protocol must be of format 'HTTP/digit.digit', but was: %s",
requestLineChunks[2],
)
}
res.HttpVersion = strings.Split(requestLineChunks[2], "/")[1]
return res, nil
}
func (lr linesReader) ReadLine() lineInfo {
CR := byte('\r') // 13
LF := byte('\n') // 10
line := []byte{}
for {
b := make([]byte, 1)
read, err := lr.r.Read(b)
if err != nil && !errors.Is(err, io.EOF) {
return lineInfo{string(line), err}
}
// if the end of the stream
if errors.Is(err, io.EOF) && read == 0 {
return lineInfo{string(line), err}
}
// if its LF and prev was CR
if b[0] == LF && len(line) > 0 && line[len(line)-1] == CR {
return lineInfo{string(line[0 : len(line)-1]), nil}
}
line = append(line, b[0])
}
}
func WriteBodySource(target io.Writer, source io.ReadCloser) error {
_, err := target.Write([]byte("\r\n"))
if err != nil {
return err
}
buf := make([]byte, 64)
for {
n, err := source.Read(buf)
if err != nil && err != io.EOF {
return err
}
if n == 0 && err == io.EOF {
return source.Close()
}
_, err = target.Write(buf[0:n])
if err != nil {
return err
}
}
}