server_test.go raw
1 package httpsrv
2
3 import (
4 "bufio"
5 "fmt"
6 "io"
7 mrand "math/rand"
8 "net"
9 "net/http"
10 "strings"
11 "testing"
12 "time"
13 )
14
15 func startTestServer(t *testing.T, handler func(*Request, *ResponseWriter)) (*Server, string) {
16 t.Helper()
17 srv := &Server{Handler: handler, MaxBodyBytes: 1 << 20}
18 started := false
19 for i := 0; i < 40; i++ {
20 srv.Addr = fmt.Sprintf("127.0.0.1:%d", 30000+mrand.Intn(5000))
21 if err := srv.Start(); err == nil {
22 started = true
23 break
24 }
25 }
26 if !started {
27 t.Fatal("could not bind a test port")
28 }
29 go srv.Serve()
30 // Start has already bound and listening(2) is done, so connections queue
31 // in the kernel backlog even before the accept goroutine is scheduled.
32 time.Sleep(2 * time.Millisecond)
33 t.Cleanup(func() { srv.Close() })
34 return srv, srv.Addr
35 }
36
37 func TestGetRequestParsing(t *testing.T) {
38 var gotMethod, gotPath, gotQuery, gotHeader, gotRemote string
39 _, addr := startTestServer(t, func(req *Request, w *ResponseWriter) {
40 gotMethod, gotPath, gotQuery = req.Method, req.Path, req.RawQuery
41 gotHeader = req.Header["x-test"]
42 gotRemote = req.RemoteAddr
43 w.SetHeader("content-type", "text/plain")
44 w.WriteHeader(200)
45 w.WriteString("ok")
46 })
47
48 req, _ := http.NewRequest("GET", "http://"+addr+"/some/path?a=b&c=d", nil)
49 req.Header.Set("X-Test", "value")
50 resp, err := http.DefaultClient.Do(req)
51 if err != nil {
52 t.Fatal(err)
53 }
54 body, _ := io.ReadAll(resp.Body)
55 resp.Body.Close()
56 if resp.StatusCode != 200 || string(body) != "ok" {
57 t.Fatalf("response: %d %q", resp.StatusCode, body)
58 }
59 if gotMethod != "GET" || gotPath != "/some/path" || gotQuery != "a=b&c=d" {
60 t.Fatalf("request line: %q %q %q", gotMethod, gotPath, gotQuery)
61 }
62 if gotHeader != "value" {
63 t.Fatalf("header: %q", gotHeader)
64 }
65 if gotRemote == "" {
66 t.Fatal("missing remote address")
67 }
68 }
69
70 func TestPostBodyAndDuplicateHeaders(t *testing.T) {
71 var body, multi string
72 _, addr := startTestServer(t, func(req *Request, w *ResponseWriter) {
73 body = string(req.Body)
74 multi = req.Header["x-multi"]
75 w.WriteHeader(201)
76 w.WriteString("created")
77 })
78 req, _ := http.NewRequest("POST", "http://"+addr+"/upload", strings.NewReader("payload"))
79 req.Header.Add("X-Multi", "one")
80 req.Header.Add("X-Multi", "two")
81 resp, err := http.DefaultClient.Do(req)
82 if err != nil {
83 t.Fatal(err)
84 }
85 resp.Body.Close()
86 if resp.StatusCode != 201 {
87 t.Fatalf("status %d", resp.StatusCode)
88 }
89 if body != "payload" {
90 t.Fatalf("body %q", body)
91 }
92 if multi != "one, two" {
93 t.Fatalf("combined headers %q", multi)
94 }
95 }
96
97 func TestExpectContinue(t *testing.T) {
98 var body string
99 _, addr := startTestServer(t, func(req *Request, w *ResponseWriter) {
100 body = string(req.Body)
101 w.WriteHeader(200)
102 w.WriteString("done")
103 })
104 conn, err := net.Dial("tcp", addr)
105 if err != nil {
106 t.Fatal(err)
107 }
108 defer conn.Close()
109 fmt.Fprintf(conn, "POST /x HTTP/1.1\r\nHost: %s\r\nContent-Length: 5\r\nExpect: 100-continue\r\n\r\n", addr)
110 br := bufio.NewReader(conn)
111 line, _ := br.ReadString('\n')
112 if !strings.Contains(line, "100") {
113 t.Fatalf("expected 100 Continue, got %q", line)
114 }
115 // Consume the rest of the interim response.
116 for {
117 h, _ := br.ReadString('\n')
118 if h == "\r\n" || h == "" {
119 break
120 }
121 }
122 conn.Write([]byte("hello"))
123 final, _ := br.ReadString('\n')
124 if !strings.Contains(final, "200") {
125 t.Fatalf("final status %q", final)
126 }
127 if body != "hello" {
128 t.Fatalf("body %q", body)
129 }
130 }
131
132 func TestMalformedRequest(t *testing.T) {
133 _, addr := startTestServer(t, func(req *Request, w *ResponseWriter) {
134 t.Error("handler must not run")
135 })
136 conn, err := net.Dial("tcp", addr)
137 if err != nil {
138 t.Fatal(err)
139 }
140 defer conn.Close()
141 fmt.Fprintf(conn, "BROKEN\r\n\r\n")
142 br := bufio.NewReader(conn)
143 line, _ := br.ReadString('\n')
144 if !strings.Contains(line, "400") {
145 t.Fatalf("expected 400, got %q", line)
146 }
147 }
148
149 func TestOversizedBodyRejected(t *testing.T) {
150 srv := &Server{MaxBodyBytes: 4}
151 for i := 0; i < 40; i++ {
152 srv.Addr = fmt.Sprintf("127.0.0.1:%d", 30000+mrand.Intn(5000))
153 if err := srv.Start(); err == nil {
154 break
155 }
156 }
157 if srv.ln == nil {
158 t.Fatal("could not bind")
159 }
160 go srv.Serve()
161 defer srv.Close()
162 time.Sleep(50 * time.Millisecond)
163 conn, err := net.Dial("tcp", srv.Addr)
164 if err != nil {
165 t.Fatal(err)
166 }
167 defer conn.Close()
168 fmt.Fprintf(conn, "POST /x HTTP/1.1\r\nHost: %s\r\nContent-Length: 10\r\n\r\n0123456789", srv.Addr)
169 br := bufio.NewReader(conn)
170 line, _ := br.ReadString('\n')
171 if !strings.Contains(line, "400") {
172 t.Fatalf("expected 400, got %q", line)
173 }
174 }
175
176 func TestHijack(t *testing.T) {
177 _, addr := startTestServer(t, func(req *Request, w *ResponseWriter) {
178 conn, _, err := req.Hijack()
179 if err != nil {
180 return
181 }
182 conn.Write([]byte("RAW"))
183 conn.Close()
184 })
185 conn, err := net.Dial("tcp", addr)
186 if err != nil {
187 t.Fatal(err)
188 }
189 defer conn.Close()
190 fmt.Fprintf(conn, "GET /ws HTTP/1.1\r\nHost: %s\r\n\r\n", addr)
191 conn.SetReadDeadline(time.Now().Add(3 * time.Second))
192 got, _ := io.ReadAll(conn)
193 if string(got) != "RAW" {
194 t.Fatalf("hijacked bytes: %q", got)
195 }
196 }
197
198 func TestPathParamAndHelpers(t *testing.T) {
199 req := &Request{Path: "/list/abcdef"}
200 if req.PathParam() != "abcdef" {
201 t.Fatalf("PathParam %q", req.PathParam())
202 }
203 if StatusText(404) != "Not Found" || StatusText(299) != "Status" {
204 t.Fatal("status text")
205 }
206 if canonicalHeader("content-length") != "Content-Length" {
207 t.Fatalf("canonical header %q", canonicalHeader("content-length"))
208 }
209 }
210