Compare commits

...

11 Commits

Author SHA1 Message Date
cf2c07899c chore: add readme
All checks were successful
Test / test (push) Successful in 2m11s
2026-02-15 20:54:41 +01:00
5be98f04c1 wip
All checks were successful
Test / test (push) Successful in 2m9s
2026-02-15 20:48:09 +01:00
38c65df769 wip
Some checks failed
Test / test (push) Failing after 26s
2026-02-15 20:42:43 +01:00
e12b1ffe3e wip
Some checks failed
Test / test (push) Failing after 1s
2026-02-15 20:40:16 +01:00
93b1dfef79 wip
Some checks failed
Test / test (push) Failing after 8s
2026-02-15 20:38:14 +01:00
8c32b74838 wip
Some checks failed
Test / test (push) Has been cancelled
2026-02-15 20:37:57 +01:00
437a285cc8 wip
Some checks failed
Test / test (push) Failing after 15s
2026-02-15 20:19:38 +01:00
19fd94a675 wip
Some checks failed
Test / test (push) Has been cancelled
2026-02-15 20:18:40 +01:00
42a613279d chore: change runner
Some checks failed
Test / test (push) Failing after 27s
2026-02-15 20:16:56 +01:00
e1eade237c chore: add pipeline
Some checks failed
Test / test (push) Has been cancelled
2026-02-15 20:15:51 +01:00
66e2b0ff2c tests: add test for read headers 2026-02-15 20:00:36 +01:00
3 changed files with 170 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
name: Test
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: x86_64
container: golang:1.26
steps:
- name: Checkout
run: |
git clone --depth=1 --branch="${GITHUB_REF_NAME}" "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" .
- name: Run tests
run: go test -v -race -coverprofile=coverage.out ./...

47
README.md Normal file
View File

@@ -0,0 +1,47 @@
# go-http-server
A minimal TCP-based HTTP server in Go for learning and experimentation. It demonstrates manual request-line and header parsing and simple connection handling.
Requires
- Go 1.26 or newer.
Quick start
1. Build
- cd into the project root (the directory that contains `go-http-server`) and run:
```
cd go-http-server
go build -o go-http-server
```
2. Run
- Start the server:
```
./go-http-server
```
- Or during development:
```
go run .
```
Testing
- Run unit tests:
```
cd go-http-server
go test ./...
```
Whats in the repo
- `main.go` — listener and connection lifecycle.
- `readheaders.go` — `readHeaders` and `parseRequestLine` logic; returns a `Headers` struct.
- `response_helper` — helpers for writing responses.
- `config` — runtime configuration (listen address, profiling toggle).
Notes
- The parser expects a request-line with exactly three tokens: `METHOD URI PROTO`.
- Header lines without a colon are ignored; values are trimmed.
- The server supports simple keep-alive behavior when the `Connection: keep-alive` header is present.
Contributing
- Open issues or PRs. Add tests for parsing or new behavior and run `go test ./...` before submitting.
License
- MIT

104
readheaders_test.go Normal file
View File

@@ -0,0 +1,104 @@
package main
import (
"bufio"
"strings"
"testing"
)
func TestParseRequestLine(t *testing.T) {
tests := []struct {
name string
input string
wantMethod string
wantUri string
wantProto string
}{
{"basic GET", "GET / HTTP/1.1\n", "GET", "/", "HTTP/1.1"},
{"POST with CRLF", "POST /path/resource HTTP/2.0\r\n", "POST", "/path/resource", "HTTP/2.0"},
{"extra spaces", " GET /spaced HTTP/1.0 \n", "GET", "/spaced", "HTTP/1.0"},
{"invalid single token", "INVALIDLINE\n", "", "", ""},
{"too many parts", "TOO MANY PARTS A B C\n", "", "", ""},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
m, u, p := parseRequestLine(tc.input)
if m != tc.wantMethod || u != tc.wantUri || p != tc.wantProto {
t.Fatalf("parseRequestLine(%q) = %q,%q,%q; want %q,%q,%q", tc.input, m, u, p, tc.wantMethod, tc.wantUri, tc.wantProto)
}
})
}
}
func TestReadHeaders_Valid(t *testing.T) {
// mix of CRLF and values with extra spaces, plus an invalid header line (no ':') which should be ignored
req := "" +
"GET /test HTTP/1.1\r\n" +
"Host: example.com\r\n" +
"X-Empty: value with spaces \r\n" +
"InvalidHeaderLineWithoutColon\r\n" +
"Connection: keep-alive\r\n" +
"\r\n"
r := bufio.NewReader(strings.NewReader(req))
h := readHeaders(r)
if h == nil {
t.Fatal("expected headers, got nil")
}
if h.Method != "GET" || h.Uri != "/test" || h.Proto != "HTTP/1.1" {
t.Fatalf("unexpected request line: got Method=%q Uri=%q Proto=%q", h.Method, h.Uri, h.Proto)
}
if got := h.KV["Host"]; got != "example.com" {
t.Fatalf("Host header = %q; want %q", got, "example.com")
}
if got := h.KV["Connection"]; got != "keep-alive" {
t.Fatalf("Connection header = %q; want %q", got, "keep-alive")
}
// value should be trimmed
if got := h.KV["X-Empty"]; got != "value with spaces" {
t.Fatalf("X-Empty header = %q; want trimmed %q", got, "value with spaces")
}
// invalid header line without ':' should be ignored
if _, ok := h.KV["InvalidHeaderLineWithoutColon"]; ok {
t.Fatalf("headers with no ':' should be ignored")
}
}
func TestReadHeaders_EmptyReader_ReturnsNil(t *testing.T) {
r := bufio.NewReader(strings.NewReader(""))
h := readHeaders(r)
if h != nil {
t.Fatalf("expected nil for empty reader, got %v", h)
}
}
func TestReadHeaders_MalformedRequestLine_ReturnsNil(t *testing.T) {
r := bufio.NewReader(strings.NewReader("BADLINE\r\nHost: example.com\r\n\r\n"))
h := readHeaders(r)
if h != nil {
t.Fatalf("expected nil for malformed request line, got %v", h)
}
}
func TestReadHeaders_StopsAtFirstBlankLine(t *testing.T) {
req := "" +
"GET /abc HTTP/1.1\r\n" +
"Header1: one\r\n" +
"\r\n" +
"Header2: should-not-be-read\r\n" // this should not be read as part of headers
r := bufio.NewReader(strings.NewReader(req))
h := readHeaders(r)
if h == nil {
t.Fatal("expected headers, got nil")
}
if _, ok := h.KV["Header2"]; ok {
t.Fatalf("Header2 should not be present; headers reading should stop at blank line")
}
}