package httpsrv import ( "bufio" "fmt" "io" mrand "math/rand" "net" "net/http" "strings" "testing" "time" ) func startTestServer(t *testing.T, handler func(*Request, *ResponseWriter)) (*Server, string) { t.Helper() srv := &Server{Handler: handler, MaxBodyBytes: 1 << 20} started := false for i := 0; i < 40; i++ { srv.Addr = fmt.Sprintf("127.0.0.1:%d", 30000+mrand.Intn(5000)) if err := srv.Start(); err == nil { started = true break } } if !started { t.Fatal("could not bind a test port") } go srv.Serve() // Start has already bound and listening(2) is done, so connections queue // in the kernel backlog even before the accept goroutine is scheduled. time.Sleep(2 * time.Millisecond) t.Cleanup(func() { srv.Close() }) return srv, srv.Addr } func TestGetRequestParsing(t *testing.T) { var gotMethod, gotPath, gotQuery, gotHeader, gotRemote string _, addr := startTestServer(t, func(req *Request, w *ResponseWriter) { gotMethod, gotPath, gotQuery = req.Method, req.Path, req.RawQuery gotHeader = req.Header["x-test"] gotRemote = req.RemoteAddr w.SetHeader("content-type", "text/plain") w.WriteHeader(200) w.WriteString("ok") }) req, _ := http.NewRequest("GET", "http://"+addr+"/some/path?a=b&c=d", nil) req.Header.Set("X-Test", "value") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } body, _ := io.ReadAll(resp.Body) resp.Body.Close() if resp.StatusCode != 200 || string(body) != "ok" { t.Fatalf("response: %d %q", resp.StatusCode, body) } if gotMethod != "GET" || gotPath != "/some/path" || gotQuery != "a=b&c=d" { t.Fatalf("request line: %q %q %q", gotMethod, gotPath, gotQuery) } if gotHeader != "value" { t.Fatalf("header: %q", gotHeader) } if gotRemote == "" { t.Fatal("missing remote address") } } func TestPostBodyAndDuplicateHeaders(t *testing.T) { var body, multi string _, addr := startTestServer(t, func(req *Request, w *ResponseWriter) { body = string(req.Body) multi = req.Header["x-multi"] w.WriteHeader(201) w.WriteString("created") }) req, _ := http.NewRequest("POST", "http://"+addr+"/upload", strings.NewReader("payload")) req.Header.Add("X-Multi", "one") req.Header.Add("X-Multi", "two") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() if resp.StatusCode != 201 { t.Fatalf("status %d", resp.StatusCode) } if body != "payload" { t.Fatalf("body %q", body) } if multi != "one, two" { t.Fatalf("combined headers %q", multi) } } func TestExpectContinue(t *testing.T) { var body string _, addr := startTestServer(t, func(req *Request, w *ResponseWriter) { body = string(req.Body) w.WriteHeader(200) w.WriteString("done") }) conn, err := net.Dial("tcp", addr) if err != nil { t.Fatal(err) } defer conn.Close() fmt.Fprintf(conn, "POST /x HTTP/1.1\r\nHost: %s\r\nContent-Length: 5\r\nExpect: 100-continue\r\n\r\n", addr) br := bufio.NewReader(conn) line, _ := br.ReadString('\n') if !strings.Contains(line, "100") { t.Fatalf("expected 100 Continue, got %q", line) } // Consume the rest of the interim response. for { h, _ := br.ReadString('\n') if h == "\r\n" || h == "" { break } } conn.Write([]byte("hello")) final, _ := br.ReadString('\n') if !strings.Contains(final, "200") { t.Fatalf("final status %q", final) } if body != "hello" { t.Fatalf("body %q", body) } } func TestMalformedRequest(t *testing.T) { _, addr := startTestServer(t, func(req *Request, w *ResponseWriter) { t.Error("handler must not run") }) conn, err := net.Dial("tcp", addr) if err != nil { t.Fatal(err) } defer conn.Close() fmt.Fprintf(conn, "BROKEN\r\n\r\n") br := bufio.NewReader(conn) line, _ := br.ReadString('\n') if !strings.Contains(line, "400") { t.Fatalf("expected 400, got %q", line) } } func TestOversizedBodyRejected(t *testing.T) { srv := &Server{MaxBodyBytes: 4} for i := 0; i < 40; i++ { srv.Addr = fmt.Sprintf("127.0.0.1:%d", 30000+mrand.Intn(5000)) if err := srv.Start(); err == nil { break } } if srv.ln == nil { t.Fatal("could not bind") } go srv.Serve() defer srv.Close() time.Sleep(50 * time.Millisecond) conn, err := net.Dial("tcp", srv.Addr) if err != nil { t.Fatal(err) } defer conn.Close() fmt.Fprintf(conn, "POST /x HTTP/1.1\r\nHost: %s\r\nContent-Length: 10\r\n\r\n0123456789", srv.Addr) br := bufio.NewReader(conn) line, _ := br.ReadString('\n') if !strings.Contains(line, "400") { t.Fatalf("expected 400, got %q", line) } } func TestHijack(t *testing.T) { _, addr := startTestServer(t, func(req *Request, w *ResponseWriter) { conn, _, err := req.Hijack() if err != nil { return } conn.Write([]byte("RAW")) conn.Close() }) conn, err := net.Dial("tcp", addr) if err != nil { t.Fatal(err) } defer conn.Close() fmt.Fprintf(conn, "GET /ws HTTP/1.1\r\nHost: %s\r\n\r\n", addr) conn.SetReadDeadline(time.Now().Add(3 * time.Second)) got, _ := io.ReadAll(conn) if string(got) != "RAW" { t.Fatalf("hijacked bytes: %q", got) } } func TestPathParamAndHelpers(t *testing.T) { req := &Request{Path: "/list/abcdef"} if req.PathParam() != "abcdef" { t.Fatalf("PathParam %q", req.PathParam()) } if StatusText(404) != "Not Found" || StatusText(299) != "Status" { t.Fatal("status text") } if canonicalHeader("content-length") != "Content-Length" { t.Fatalf("canonical header %q", canonicalHeader("content-length")) } }