libgo: update to Go 1.11
[official-gcc.git] / libgo / go / net / http / fs_test.go
blob1d6380ddfb68ea64b9fe762128928c144a561dc5
1 // Copyright 2010 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
5 package http_test
7 import (
8 "bufio"
9 "bytes"
10 "errors"
11 "fmt"
12 "io"
13 "io/ioutil"
14 "mime"
15 "mime/multipart"
16 "net"
17 . "net/http"
18 "net/http/httptest"
19 "net/url"
20 "os"
21 "os/exec"
22 "path"
23 "path/filepath"
24 "reflect"
25 "regexp"
26 "runtime"
27 "strings"
28 "testing"
29 "time"
32 const (
33 testFile = "testdata/file"
34 testFileLen = 11
37 type wantRange struct {
38 start, end int64 // range [start,end)
41 var ServeFileRangeTests = []struct {
42 r string
43 code int
44 ranges []wantRange
46 {r: "", code: StatusOK},
47 {r: "bytes=0-4", code: StatusPartialContent, ranges: []wantRange{{0, 5}}},
48 {r: "bytes=2-", code: StatusPartialContent, ranges: []wantRange{{2, testFileLen}}},
49 {r: "bytes=-5", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 5, testFileLen}}},
50 {r: "bytes=3-7", code: StatusPartialContent, ranges: []wantRange{{3, 8}}},
51 {r: "bytes=0-0,-2", code: StatusPartialContent, ranges: []wantRange{{0, 1}, {testFileLen - 2, testFileLen}}},
52 {r: "bytes=0-1,5-8", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, 9}}},
53 {r: "bytes=0-1,5-", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, testFileLen}}},
54 {r: "bytes=5-1000", code: StatusPartialContent, ranges: []wantRange{{5, testFileLen}}},
55 {r: "bytes=0-,1-,2-,3-,4-", code: StatusOK}, // ignore wasteful range request
56 {r: "bytes=0-9", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen - 1}}},
57 {r: "bytes=0-10", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
58 {r: "bytes=0-11", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
59 {r: "bytes=10-11", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}},
60 {r: "bytes=10-", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}},
61 {r: "bytes=11-", code: StatusRequestedRangeNotSatisfiable},
62 {r: "bytes=11-12", code: StatusRequestedRangeNotSatisfiable},
63 {r: "bytes=12-12", code: StatusRequestedRangeNotSatisfiable},
64 {r: "bytes=11-100", code: StatusRequestedRangeNotSatisfiable},
65 {r: "bytes=12-100", code: StatusRequestedRangeNotSatisfiable},
66 {r: "bytes=100-", code: StatusRequestedRangeNotSatisfiable},
67 {r: "bytes=100-1000", code: StatusRequestedRangeNotSatisfiable},
70 func TestServeFile(t *testing.T) {
71 setParallel(t)
72 defer afterTest(t)
73 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
74 ServeFile(w, r, "testdata/file")
75 }))
76 defer ts.Close()
77 c := ts.Client()
79 var err error
81 file, err := ioutil.ReadFile(testFile)
82 if err != nil {
83 t.Fatal("reading file:", err)
86 // set up the Request (re-used for all tests)
87 var req Request
88 req.Header = make(Header)
89 if req.URL, err = url.Parse(ts.URL); err != nil {
90 t.Fatal("ParseURL:", err)
92 req.Method = "GET"
94 // straight GET
95 _, body := getBody(t, "straight get", req, c)
96 if !bytes.Equal(body, file) {
97 t.Fatalf("body mismatch: got %q, want %q", body, file)
100 // Range tests
101 Cases:
102 for _, rt := range ServeFileRangeTests {
103 if rt.r != "" {
104 req.Header.Set("Range", rt.r)
106 resp, body := getBody(t, fmt.Sprintf("range test %q", rt.r), req, c)
107 if resp.StatusCode != rt.code {
108 t.Errorf("range=%q: StatusCode=%d, want %d", rt.r, resp.StatusCode, rt.code)
110 if rt.code == StatusRequestedRangeNotSatisfiable {
111 continue
113 wantContentRange := ""
114 if len(rt.ranges) == 1 {
115 rng := rt.ranges[0]
116 wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
118 cr := resp.Header.Get("Content-Range")
119 if cr != wantContentRange {
120 t.Errorf("range=%q: Content-Range = %q, want %q", rt.r, cr, wantContentRange)
122 ct := resp.Header.Get("Content-Type")
123 if len(rt.ranges) == 1 {
124 rng := rt.ranges[0]
125 wantBody := file[rng.start:rng.end]
126 if !bytes.Equal(body, wantBody) {
127 t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
129 if strings.HasPrefix(ct, "multipart/byteranges") {
130 t.Errorf("range=%q content-type = %q; unexpected multipart/byteranges", rt.r, ct)
133 if len(rt.ranges) > 1 {
134 typ, params, err := mime.ParseMediaType(ct)
135 if err != nil {
136 t.Errorf("range=%q content-type = %q; %v", rt.r, ct, err)
137 continue
139 if typ != "multipart/byteranges" {
140 t.Errorf("range=%q content-type = %q; want multipart/byteranges", rt.r, typ)
141 continue
143 if params["boundary"] == "" {
144 t.Errorf("range=%q content-type = %q; lacks boundary", rt.r, ct)
145 continue
147 if g, w := resp.ContentLength, int64(len(body)); g != w {
148 t.Errorf("range=%q Content-Length = %d; want %d", rt.r, g, w)
149 continue
151 mr := multipart.NewReader(bytes.NewReader(body), params["boundary"])
152 for ri, rng := range rt.ranges {
153 part, err := mr.NextPart()
154 if err != nil {
155 t.Errorf("range=%q, reading part index %d: %v", rt.r, ri, err)
156 continue Cases
158 wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
159 if g, w := part.Header.Get("Content-Range"), wantContentRange; g != w {
160 t.Errorf("range=%q: part Content-Range = %q; want %q", rt.r, g, w)
162 body, err := ioutil.ReadAll(part)
163 if err != nil {
164 t.Errorf("range=%q, reading part index %d body: %v", rt.r, ri, err)
165 continue Cases
167 wantBody := file[rng.start:rng.end]
168 if !bytes.Equal(body, wantBody) {
169 t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
172 _, err = mr.NextPart()
173 if err != io.EOF {
174 t.Errorf("range=%q; expected final error io.EOF; got %v", rt.r, err)
180 func TestServeFile_DotDot(t *testing.T) {
181 tests := []struct {
182 req string
183 wantStatus int
185 {"/testdata/file", 200},
186 {"/../file", 400},
187 {"/..", 400},
188 {"/../", 400},
189 {"/../foo", 400},
190 {"/..\\foo", 400},
191 {"/file/a", 200},
192 {"/file/a..", 200},
193 {"/file/a/..", 400},
194 {"/file/a\\..", 400},
196 for _, tt := range tests {
197 req, err := ReadRequest(bufio.NewReader(strings.NewReader("GET " + tt.req + " HTTP/1.1\r\nHost: foo\r\n\r\n")))
198 if err != nil {
199 t.Errorf("bad request %q: %v", tt.req, err)
200 continue
202 rec := httptest.NewRecorder()
203 ServeFile(rec, req, "testdata/file")
204 if rec.Code != tt.wantStatus {
205 t.Errorf("for request %q, status = %d; want %d", tt.req, rec.Code, tt.wantStatus)
210 var fsRedirectTestData = []struct {
211 original, redirect string
213 {"/test/index.html", "/test/"},
214 {"/test/testdata", "/test/testdata/"},
215 {"/test/testdata/file/", "/test/testdata/file"},
218 func TestFSRedirect(t *testing.T) {
219 defer afterTest(t)
220 ts := httptest.NewServer(StripPrefix("/test", FileServer(Dir("."))))
221 defer ts.Close()
223 for _, data := range fsRedirectTestData {
224 res, err := Get(ts.URL + data.original)
225 if err != nil {
226 t.Fatal(err)
228 res.Body.Close()
229 if g, e := res.Request.URL.Path, data.redirect; g != e {
230 t.Errorf("redirect from %s: got %s, want %s", data.original, g, e)
235 type testFileSystem struct {
236 open func(name string) (File, error)
239 func (fs *testFileSystem) Open(name string) (File, error) {
240 return fs.open(name)
243 func TestFileServerCleans(t *testing.T) {
244 defer afterTest(t)
245 ch := make(chan string, 1)
246 fs := FileServer(&testFileSystem{func(name string) (File, error) {
247 ch <- name
248 return nil, errors.New("file does not exist")
250 tests := []struct {
251 reqPath, openArg string
253 {"/foo.txt", "/foo.txt"},
254 {"//foo.txt", "/foo.txt"},
255 {"/../foo.txt", "/foo.txt"},
257 req, _ := NewRequest("GET", "http://example.com", nil)
258 for n, test := range tests {
259 rec := httptest.NewRecorder()
260 req.URL.Path = test.reqPath
261 fs.ServeHTTP(rec, req)
262 if got := <-ch; got != test.openArg {
263 t.Errorf("test %d: got %q, want %q", n, got, test.openArg)
268 func TestFileServerEscapesNames(t *testing.T) {
269 defer afterTest(t)
270 const dirListPrefix = "<pre>\n"
271 const dirListSuffix = "\n</pre>\n"
272 tests := []struct {
273 name, escaped string
275 {`simple_name`, `<a href="simple_name">simple_name</a>`},
276 {`"'<>&`, `<a href="%22%27%3C%3E&">&#34;&#39;&lt;&gt;&amp;</a>`},
277 {`?foo=bar#baz`, `<a href="%3Ffoo=bar%23baz">?foo=bar#baz</a>`},
278 {`<combo>?foo`, `<a href="%3Ccombo%3E%3Ffoo">&lt;combo&gt;?foo</a>`},
279 {`foo:bar`, `<a href="./foo:bar">foo:bar</a>`},
282 // We put each test file in its own directory in the fakeFS so we can look at it in isolation.
283 fs := make(fakeFS)
284 for i, test := range tests {
285 testFile := &fakeFileInfo{basename: test.name}
286 fs[fmt.Sprintf("/%d", i)] = &fakeFileInfo{
287 dir: true,
288 modtime: time.Unix(1000000000, 0).UTC(),
289 ents: []*fakeFileInfo{testFile},
291 fs[fmt.Sprintf("/%d/%s", i, test.name)] = testFile
294 ts := httptest.NewServer(FileServer(&fs))
295 defer ts.Close()
296 for i, test := range tests {
297 url := fmt.Sprintf("%s/%d", ts.URL, i)
298 res, err := Get(url)
299 if err != nil {
300 t.Fatalf("test %q: Get: %v", test.name, err)
302 b, err := ioutil.ReadAll(res.Body)
303 if err != nil {
304 t.Fatalf("test %q: read Body: %v", test.name, err)
306 s := string(b)
307 if !strings.HasPrefix(s, dirListPrefix) || !strings.HasSuffix(s, dirListSuffix) {
308 t.Errorf("test %q: listing dir, full output is %q, want prefix %q and suffix %q", test.name, s, dirListPrefix, dirListSuffix)
310 if trimmed := strings.TrimSuffix(strings.TrimPrefix(s, dirListPrefix), dirListSuffix); trimmed != test.escaped {
311 t.Errorf("test %q: listing dir, filename escaped to %q, want %q", test.name, trimmed, test.escaped)
313 res.Body.Close()
317 func TestFileServerSortsNames(t *testing.T) {
318 defer afterTest(t)
319 const contents = "I am a fake file"
320 dirMod := time.Unix(123, 0).UTC()
321 fileMod := time.Unix(1000000000, 0).UTC()
322 fs := fakeFS{
323 "/": &fakeFileInfo{
324 dir: true,
325 modtime: dirMod,
326 ents: []*fakeFileInfo{
328 basename: "b",
329 modtime: fileMod,
330 contents: contents,
333 basename: "a",
334 modtime: fileMod,
335 contents: contents,
341 ts := httptest.NewServer(FileServer(&fs))
342 defer ts.Close()
344 res, err := Get(ts.URL)
345 if err != nil {
346 t.Fatalf("Get: %v", err)
348 defer res.Body.Close()
350 b, err := ioutil.ReadAll(res.Body)
351 if err != nil {
352 t.Fatalf("read Body: %v", err)
354 s := string(b)
355 if !strings.Contains(s, "<a href=\"a\">a</a>\n<a href=\"b\">b</a>") {
356 t.Errorf("output appears to be unsorted:\n%s", s)
360 func mustRemoveAll(dir string) {
361 err := os.RemoveAll(dir)
362 if err != nil {
363 panic(err)
367 func TestFileServerImplicitLeadingSlash(t *testing.T) {
368 defer afterTest(t)
369 tempDir, err := ioutil.TempDir("", "")
370 if err != nil {
371 t.Fatalf("TempDir: %v", err)
373 defer mustRemoveAll(tempDir)
374 if err := ioutil.WriteFile(filepath.Join(tempDir, "foo.txt"), []byte("Hello world"), 0644); err != nil {
375 t.Fatalf("WriteFile: %v", err)
377 ts := httptest.NewServer(StripPrefix("/bar/", FileServer(Dir(tempDir))))
378 defer ts.Close()
379 get := func(suffix string) string {
380 res, err := Get(ts.URL + suffix)
381 if err != nil {
382 t.Fatalf("Get %s: %v", suffix, err)
384 b, err := ioutil.ReadAll(res.Body)
385 if err != nil {
386 t.Fatalf("ReadAll %s: %v", suffix, err)
388 res.Body.Close()
389 return string(b)
391 if s := get("/bar/"); !strings.Contains(s, ">foo.txt<") {
392 t.Logf("expected a directory listing with foo.txt, got %q", s)
394 if s := get("/bar/foo.txt"); s != "Hello world" {
395 t.Logf("expected %q, got %q", "Hello world", s)
399 func TestDirJoin(t *testing.T) {
400 if runtime.GOOS == "windows" {
401 t.Skip("skipping test on windows")
403 wfi, err := os.Stat("/etc/hosts")
404 if err != nil {
405 t.Skip("skipping test; no /etc/hosts file")
407 test := func(d Dir, name string) {
408 f, err := d.Open(name)
409 if err != nil {
410 t.Fatalf("open of %s: %v", name, err)
412 defer f.Close()
413 gfi, err := f.Stat()
414 if err != nil {
415 t.Fatalf("stat of %s: %v", name, err)
417 if !os.SameFile(gfi, wfi) {
418 t.Errorf("%s got different file", name)
421 test(Dir("/etc/"), "/hosts")
422 test(Dir("/etc/"), "hosts")
423 test(Dir("/etc/"), "../../../../hosts")
424 test(Dir("/etc"), "/hosts")
425 test(Dir("/etc"), "hosts")
426 test(Dir("/etc"), "../../../../hosts")
428 // Not really directories, but since we use this trick in
429 // ServeFile, test it:
430 test(Dir("/etc/hosts"), "")
431 test(Dir("/etc/hosts"), "/")
432 test(Dir("/etc/hosts"), "../")
435 func TestEmptyDirOpenCWD(t *testing.T) {
436 test := func(d Dir) {
437 name := "fs_test.go"
438 f, err := d.Open(name)
439 if err != nil {
440 t.Fatalf("open of %s: %v", name, err)
442 defer f.Close()
444 test(Dir(""))
445 test(Dir("."))
446 test(Dir("./"))
449 func TestServeFileContentType(t *testing.T) {
450 defer afterTest(t)
451 const ctype = "icecream/chocolate"
452 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
453 switch r.FormValue("override") {
454 case "1":
455 w.Header().Set("Content-Type", ctype)
456 case "2":
457 // Explicitly inhibit sniffing.
458 w.Header()["Content-Type"] = []string{}
460 ServeFile(w, r, "testdata/file")
462 defer ts.Close()
463 get := func(override string, want []string) {
464 resp, err := Get(ts.URL + "?override=" + override)
465 if err != nil {
466 t.Fatal(err)
468 if h := resp.Header["Content-Type"]; !reflect.DeepEqual(h, want) {
469 t.Errorf("Content-Type mismatch: got %v, want %v", h, want)
471 resp.Body.Close()
473 get("0", []string{"text/plain; charset=utf-8"})
474 get("1", []string{ctype})
475 get("2", nil)
478 func TestServeFileMimeType(t *testing.T) {
479 defer afterTest(t)
480 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
481 ServeFile(w, r, "testdata/style.css")
483 defer ts.Close()
484 resp, err := Get(ts.URL)
485 if err != nil {
486 t.Fatal(err)
488 resp.Body.Close()
489 want := "text/css; charset=utf-8"
490 if h := resp.Header.Get("Content-Type"); h != want {
491 t.Errorf("Content-Type mismatch: got %q, want %q", h, want)
495 func TestServeFileFromCWD(t *testing.T) {
496 defer afterTest(t)
497 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
498 ServeFile(w, r, "fs_test.go")
500 defer ts.Close()
501 r, err := Get(ts.URL)
502 if err != nil {
503 t.Fatal(err)
505 r.Body.Close()
506 if r.StatusCode != 200 {
507 t.Fatalf("expected 200 OK, got %s", r.Status)
511 // Issue 13996
512 func TestServeDirWithoutTrailingSlash(t *testing.T) {
513 e := "/testdata/"
514 defer afterTest(t)
515 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
516 ServeFile(w, r, ".")
518 defer ts.Close()
519 r, err := Get(ts.URL + "/testdata")
520 if err != nil {
521 t.Fatal(err)
523 r.Body.Close()
524 if g := r.Request.URL.Path; g != e {
525 t.Errorf("got %s, want %s", g, e)
529 // Tests that ServeFile doesn't add a Content-Length if a Content-Encoding is
530 // specified.
531 func TestServeFileWithContentEncoding_h1(t *testing.T) { testServeFileWithContentEncoding(t, h1Mode) }
532 func TestServeFileWithContentEncoding_h2(t *testing.T) { testServeFileWithContentEncoding(t, h2Mode) }
533 func testServeFileWithContentEncoding(t *testing.T, h2 bool) {
534 defer afterTest(t)
535 cst := newClientServerTest(t, h2, HandlerFunc(func(w ResponseWriter, r *Request) {
536 w.Header().Set("Content-Encoding", "foo")
537 ServeFile(w, r, "testdata/file")
539 // Because the testdata is so small, it would fit in
540 // both the h1 and h2 Server's write buffers. For h1,
541 // sendfile is used, though, forcing a header flush at
542 // the io.Copy. http2 doesn't do a header flush so
543 // buffers all 11 bytes and then adds its own
544 // Content-Length. To prevent the Server's
545 // Content-Length and test ServeFile only, flush here.
546 w.(Flusher).Flush()
548 defer cst.close()
549 resp, err := cst.c.Get(cst.ts.URL)
550 if err != nil {
551 t.Fatal(err)
553 resp.Body.Close()
554 if g, e := resp.ContentLength, int64(-1); g != e {
555 t.Errorf("Content-Length mismatch: got %d, want %d", g, e)
559 func TestServeIndexHtml(t *testing.T) {
560 defer afterTest(t)
561 const want = "index.html says hello\n"
562 ts := httptest.NewServer(FileServer(Dir(".")))
563 defer ts.Close()
565 for _, path := range []string{"/testdata/", "/testdata/index.html"} {
566 res, err := Get(ts.URL + path)
567 if err != nil {
568 t.Fatal(err)
570 b, err := ioutil.ReadAll(res.Body)
571 if err != nil {
572 t.Fatal("reading Body:", err)
574 if s := string(b); s != want {
575 t.Errorf("for path %q got %q, want %q", path, s, want)
577 res.Body.Close()
581 func TestFileServerZeroByte(t *testing.T) {
582 defer afterTest(t)
583 ts := httptest.NewServer(FileServer(Dir(".")))
584 defer ts.Close()
586 res, err := Get(ts.URL + "/..\x00")
587 if err != nil {
588 t.Fatal(err)
590 b, err := ioutil.ReadAll(res.Body)
591 if err != nil {
592 t.Fatal("reading Body:", err)
594 if res.StatusCode == 200 {
595 t.Errorf("got status 200; want an error. Body is:\n%s", string(b))
599 type fakeFileInfo struct {
600 dir bool
601 basename string
602 modtime time.Time
603 ents []*fakeFileInfo
604 contents string
605 err error
608 func (f *fakeFileInfo) Name() string { return f.basename }
609 func (f *fakeFileInfo) Sys() interface{} { return nil }
610 func (f *fakeFileInfo) ModTime() time.Time { return f.modtime }
611 func (f *fakeFileInfo) IsDir() bool { return f.dir }
612 func (f *fakeFileInfo) Size() int64 { return int64(len(f.contents)) }
613 func (f *fakeFileInfo) Mode() os.FileMode {
614 if f.dir {
615 return 0755 | os.ModeDir
617 return 0644
620 type fakeFile struct {
621 io.ReadSeeker
622 fi *fakeFileInfo
623 path string // as opened
624 entpos int
627 func (f *fakeFile) Close() error { return nil }
628 func (f *fakeFile) Stat() (os.FileInfo, error) { return f.fi, nil }
629 func (f *fakeFile) Readdir(count int) ([]os.FileInfo, error) {
630 if !f.fi.dir {
631 return nil, os.ErrInvalid
633 var fis []os.FileInfo
635 limit := f.entpos + count
636 if count <= 0 || limit > len(f.fi.ents) {
637 limit = len(f.fi.ents)
639 for ; f.entpos < limit; f.entpos++ {
640 fis = append(fis, f.fi.ents[f.entpos])
643 if len(fis) == 0 && count > 0 {
644 return fis, io.EOF
645 } else {
646 return fis, nil
650 type fakeFS map[string]*fakeFileInfo
652 func (fs fakeFS) Open(name string) (File, error) {
653 name = path.Clean(name)
654 f, ok := fs[name]
655 if !ok {
656 return nil, os.ErrNotExist
658 if f.err != nil {
659 return nil, f.err
661 return &fakeFile{ReadSeeker: strings.NewReader(f.contents), fi: f, path: name}, nil
664 func TestDirectoryIfNotModified(t *testing.T) {
665 defer afterTest(t)
666 const indexContents = "I am a fake index.html file"
667 fileMod := time.Unix(1000000000, 0).UTC()
668 fileModStr := fileMod.Format(TimeFormat)
669 dirMod := time.Unix(123, 0).UTC()
670 indexFile := &fakeFileInfo{
671 basename: "index.html",
672 modtime: fileMod,
673 contents: indexContents,
675 fs := fakeFS{
676 "/": &fakeFileInfo{
677 dir: true,
678 modtime: dirMod,
679 ents: []*fakeFileInfo{indexFile},
681 "/index.html": indexFile,
684 ts := httptest.NewServer(FileServer(fs))
685 defer ts.Close()
687 res, err := Get(ts.URL)
688 if err != nil {
689 t.Fatal(err)
691 b, err := ioutil.ReadAll(res.Body)
692 if err != nil {
693 t.Fatal(err)
695 if string(b) != indexContents {
696 t.Fatalf("Got body %q; want %q", b, indexContents)
698 res.Body.Close()
700 lastMod := res.Header.Get("Last-Modified")
701 if lastMod != fileModStr {
702 t.Fatalf("initial Last-Modified = %q; want %q", lastMod, fileModStr)
705 req, _ := NewRequest("GET", ts.URL, nil)
706 req.Header.Set("If-Modified-Since", lastMod)
708 c := ts.Client()
709 res, err = c.Do(req)
710 if err != nil {
711 t.Fatal(err)
713 if res.StatusCode != 304 {
714 t.Fatalf("Code after If-Modified-Since request = %v; want 304", res.StatusCode)
716 res.Body.Close()
718 // Advance the index.html file's modtime, but not the directory's.
719 indexFile.modtime = indexFile.modtime.Add(1 * time.Hour)
721 res, err = c.Do(req)
722 if err != nil {
723 t.Fatal(err)
725 if res.StatusCode != 200 {
726 t.Fatalf("Code after second If-Modified-Since request = %v; want 200; res is %#v", res.StatusCode, res)
728 res.Body.Close()
731 func mustStat(t *testing.T, fileName string) os.FileInfo {
732 fi, err := os.Stat(fileName)
733 if err != nil {
734 t.Fatal(err)
736 return fi
739 func TestServeContent(t *testing.T) {
740 defer afterTest(t)
741 type serveParam struct {
742 name string
743 modtime time.Time
744 content io.ReadSeeker
745 contentType string
746 etag string
748 servec := make(chan serveParam, 1)
749 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
750 p := <-servec
751 if p.etag != "" {
752 w.Header().Set("ETag", p.etag)
754 if p.contentType != "" {
755 w.Header().Set("Content-Type", p.contentType)
757 ServeContent(w, r, p.name, p.modtime, p.content)
759 defer ts.Close()
761 type testCase struct {
762 // One of file or content must be set:
763 file string
764 content io.ReadSeeker
766 modtime time.Time
767 serveETag string // optional
768 serveContentType string // optional
769 reqHeader map[string]string
770 wantLastMod string
771 wantContentType string
772 wantContentRange string
773 wantStatus int
775 htmlModTime := mustStat(t, "testdata/index.html").ModTime()
776 tests := map[string]testCase{
777 "no_last_modified": {
778 file: "testdata/style.css",
779 wantContentType: "text/css; charset=utf-8",
780 wantStatus: 200,
782 "with_last_modified": {
783 file: "testdata/index.html",
784 wantContentType: "text/html; charset=utf-8",
785 modtime: htmlModTime,
786 wantLastMod: htmlModTime.UTC().Format(TimeFormat),
787 wantStatus: 200,
789 "not_modified_modtime": {
790 file: "testdata/style.css",
791 serveETag: `"foo"`, // Last-Modified sent only when no ETag
792 modtime: htmlModTime,
793 reqHeader: map[string]string{
794 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
796 wantStatus: 304,
798 "not_modified_modtime_with_contenttype": {
799 file: "testdata/style.css",
800 serveContentType: "text/css", // explicit content type
801 serveETag: `"foo"`, // Last-Modified sent only when no ETag
802 modtime: htmlModTime,
803 reqHeader: map[string]string{
804 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
806 wantStatus: 304,
808 "not_modified_etag": {
809 file: "testdata/style.css",
810 serveETag: `"foo"`,
811 reqHeader: map[string]string{
812 "If-None-Match": `"foo"`,
814 wantStatus: 304,
816 "not_modified_etag_no_seek": {
817 content: panicOnSeek{nil}, // should never be called
818 serveETag: `W/"foo"`, // If-None-Match uses weak ETag comparison
819 reqHeader: map[string]string{
820 "If-None-Match": `"baz", W/"foo"`,
822 wantStatus: 304,
824 "if_none_match_mismatch": {
825 file: "testdata/style.css",
826 serveETag: `"foo"`,
827 reqHeader: map[string]string{
828 "If-None-Match": `"Foo"`,
830 wantStatus: 200,
831 wantContentType: "text/css; charset=utf-8",
833 "range_good": {
834 file: "testdata/style.css",
835 serveETag: `"A"`,
836 reqHeader: map[string]string{
837 "Range": "bytes=0-4",
839 wantStatus: StatusPartialContent,
840 wantContentType: "text/css; charset=utf-8",
841 wantContentRange: "bytes 0-4/8",
843 "range_match": {
844 file: "testdata/style.css",
845 serveETag: `"A"`,
846 reqHeader: map[string]string{
847 "Range": "bytes=0-4",
848 "If-Range": `"A"`,
850 wantStatus: StatusPartialContent,
851 wantContentType: "text/css; charset=utf-8",
852 wantContentRange: "bytes 0-4/8",
854 "range_match_weak_etag": {
855 file: "testdata/style.css",
856 serveETag: `W/"A"`,
857 reqHeader: map[string]string{
858 "Range": "bytes=0-4",
859 "If-Range": `W/"A"`,
861 wantStatus: 200,
862 wantContentType: "text/css; charset=utf-8",
864 "range_no_overlap": {
865 file: "testdata/style.css",
866 serveETag: `"A"`,
867 reqHeader: map[string]string{
868 "Range": "bytes=10-20",
870 wantStatus: StatusRequestedRangeNotSatisfiable,
871 wantContentType: "text/plain; charset=utf-8",
872 wantContentRange: "bytes */8",
874 // An If-Range resource for entity "A", but entity "B" is now current.
875 // The Range request should be ignored.
876 "range_no_match": {
877 file: "testdata/style.css",
878 serveETag: `"A"`,
879 reqHeader: map[string]string{
880 "Range": "bytes=0-4",
881 "If-Range": `"B"`,
883 wantStatus: 200,
884 wantContentType: "text/css; charset=utf-8",
886 "range_with_modtime": {
887 file: "testdata/style.css",
888 modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 /* nanos */, time.UTC),
889 reqHeader: map[string]string{
890 "Range": "bytes=0-4",
891 "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
893 wantStatus: StatusPartialContent,
894 wantContentType: "text/css; charset=utf-8",
895 wantContentRange: "bytes 0-4/8",
896 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT",
898 "range_with_modtime_mismatch": {
899 file: "testdata/style.css",
900 modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 /* nanos */, time.UTC),
901 reqHeader: map[string]string{
902 "Range": "bytes=0-4",
903 "If-Range": "Wed, 25 Jun 2014 17:12:19 GMT",
905 wantStatus: StatusOK,
906 wantContentType: "text/css; charset=utf-8",
907 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT",
909 "range_with_modtime_nanos": {
910 file: "testdata/style.css",
911 modtime: time.Date(2014, 6, 25, 17, 12, 18, 123 /* nanos */, time.UTC),
912 reqHeader: map[string]string{
913 "Range": "bytes=0-4",
914 "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
916 wantStatus: StatusPartialContent,
917 wantContentType: "text/css; charset=utf-8",
918 wantContentRange: "bytes 0-4/8",
919 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT",
921 "unix_zero_modtime": {
922 content: strings.NewReader("<html>foo"),
923 modtime: time.Unix(0, 0),
924 wantStatus: StatusOK,
925 wantContentType: "text/html; charset=utf-8",
927 "ifmatch_matches": {
928 file: "testdata/style.css",
929 serveETag: `"A"`,
930 reqHeader: map[string]string{
931 "If-Match": `"Z", "A"`,
933 wantStatus: 200,
934 wantContentType: "text/css; charset=utf-8",
936 "ifmatch_star": {
937 file: "testdata/style.css",
938 serveETag: `"A"`,
939 reqHeader: map[string]string{
940 "If-Match": `*`,
942 wantStatus: 200,
943 wantContentType: "text/css; charset=utf-8",
945 "ifmatch_failed": {
946 file: "testdata/style.css",
947 serveETag: `"A"`,
948 reqHeader: map[string]string{
949 "If-Match": `"B"`,
951 wantStatus: 412,
953 "ifmatch_fails_on_weak_etag": {
954 file: "testdata/style.css",
955 serveETag: `W/"A"`,
956 reqHeader: map[string]string{
957 "If-Match": `W/"A"`,
959 wantStatus: 412,
961 "if_unmodified_since_true": {
962 file: "testdata/style.css",
963 modtime: htmlModTime,
964 reqHeader: map[string]string{
965 "If-Unmodified-Since": htmlModTime.UTC().Format(TimeFormat),
967 wantStatus: 200,
968 wantContentType: "text/css; charset=utf-8",
969 wantLastMod: htmlModTime.UTC().Format(TimeFormat),
971 "if_unmodified_since_false": {
972 file: "testdata/style.css",
973 modtime: htmlModTime,
974 reqHeader: map[string]string{
975 "If-Unmodified-Since": htmlModTime.Add(-2 * time.Second).UTC().Format(TimeFormat),
977 wantStatus: 412,
978 wantLastMod: htmlModTime.UTC().Format(TimeFormat),
981 for testName, tt := range tests {
982 var content io.ReadSeeker
983 if tt.file != "" {
984 f, err := os.Open(tt.file)
985 if err != nil {
986 t.Fatalf("test %q: %v", testName, err)
988 defer f.Close()
989 content = f
990 } else {
991 content = tt.content
993 for _, method := range []string{"GET", "HEAD"} {
994 //restore content in case it is consumed by previous method
995 if content, ok := content.(*strings.Reader); ok {
996 content.Seek(0, io.SeekStart)
999 servec <- serveParam{
1000 name: filepath.Base(tt.file),
1001 content: content,
1002 modtime: tt.modtime,
1003 etag: tt.serveETag,
1004 contentType: tt.serveContentType,
1006 req, err := NewRequest(method, ts.URL, nil)
1007 if err != nil {
1008 t.Fatal(err)
1010 for k, v := range tt.reqHeader {
1011 req.Header.Set(k, v)
1014 c := ts.Client()
1015 res, err := c.Do(req)
1016 if err != nil {
1017 t.Fatal(err)
1019 io.Copy(ioutil.Discard, res.Body)
1020 res.Body.Close()
1021 if res.StatusCode != tt.wantStatus {
1022 t.Errorf("test %q using %q: got status = %d; want %d", testName, method, res.StatusCode, tt.wantStatus)
1024 if g, e := res.Header.Get("Content-Type"), tt.wantContentType; g != e {
1025 t.Errorf("test %q using %q: got content-type = %q, want %q", testName, method, g, e)
1027 if g, e := res.Header.Get("Content-Range"), tt.wantContentRange; g != e {
1028 t.Errorf("test %q using %q: got content-range = %q, want %q", testName, method, g, e)
1030 if g, e := res.Header.Get("Last-Modified"), tt.wantLastMod; g != e {
1031 t.Errorf("test %q using %q: got last-modified = %q, want %q", testName, method, g, e)
1037 // Issue 12991
1038 func TestServerFileStatError(t *testing.T) {
1039 rec := httptest.NewRecorder()
1040 r, _ := NewRequest("GET", "http://foo/", nil)
1041 redirect := false
1042 name := "file.txt"
1043 fs := issue12991FS{}
1044 ExportServeFile(rec, r, fs, name, redirect)
1045 if body := rec.Body.String(); !strings.Contains(body, "403") || !strings.Contains(body, "Forbidden") {
1046 t.Errorf("wanted 403 forbidden message; got: %s", body)
1050 type issue12991FS struct{}
1052 func (issue12991FS) Open(string) (File, error) { return issue12991File{}, nil }
1054 type issue12991File struct{ File }
1056 func (issue12991File) Stat() (os.FileInfo, error) { return nil, os.ErrPermission }
1057 func (issue12991File) Close() error { return nil }
1059 func TestServeContentErrorMessages(t *testing.T) {
1060 defer afterTest(t)
1061 fs := fakeFS{
1062 "/500": &fakeFileInfo{
1063 err: errors.New("random error"),
1065 "/403": &fakeFileInfo{
1066 err: &os.PathError{Err: os.ErrPermission},
1069 ts := httptest.NewServer(FileServer(fs))
1070 defer ts.Close()
1071 c := ts.Client()
1072 for _, code := range []int{403, 404, 500} {
1073 res, err := c.Get(fmt.Sprintf("%s/%d", ts.URL, code))
1074 if err != nil {
1075 t.Errorf("Error fetching /%d: %v", code, err)
1076 continue
1078 if res.StatusCode != code {
1079 t.Errorf("For /%d, status code = %d; want %d", code, res.StatusCode, code)
1081 res.Body.Close()
1085 // verifies that sendfile is being used on Linux
1086 func TestLinuxSendfile(t *testing.T) {
1087 setParallel(t)
1088 defer afterTest(t)
1089 if runtime.GOOS != "linux" {
1090 t.Skip("skipping; linux-only test")
1092 if _, err := exec.LookPath("strace"); err != nil {
1093 t.Skip("skipping; strace not found in path")
1096 ln, err := net.Listen("tcp", "127.0.0.1:0")
1097 if err != nil {
1098 t.Fatal(err)
1100 lnf, err := ln.(*net.TCPListener).File()
1101 if err != nil {
1102 t.Fatal(err)
1104 defer ln.Close()
1106 syscalls := "sendfile,sendfile64"
1107 switch runtime.GOARCH {
1108 case "mips64", "mips64le", "s390x", "alpha":
1109 // strace on the above platforms doesn't support sendfile64
1110 // and will error out if we specify that with `-e trace='.
1111 syscalls = "sendfile"
1114 // Attempt to run strace, and skip on failure - this test requires SYS_PTRACE.
1115 if err := exec.Command("strace", "-f", "-q", "-e", "trace="+syscalls, os.Args[0], "-test.run=^$").Run(); err != nil {
1116 t.Skipf("skipping; failed to run strace: %v", err)
1119 var buf bytes.Buffer
1120 child := exec.Command("strace", "-f", "-q", "-e", "trace="+syscalls, os.Args[0], "-test.run=TestLinuxSendfileChild")
1121 child.ExtraFiles = append(child.ExtraFiles, lnf)
1122 child.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...)
1123 child.Stdout = &buf
1124 child.Stderr = &buf
1125 if err := child.Start(); err != nil {
1126 t.Skipf("skipping; failed to start straced child: %v", err)
1129 res, err := Get(fmt.Sprintf("http://%s/", ln.Addr()))
1130 if err != nil {
1131 t.Fatalf("http client error: %v", err)
1133 _, err = io.Copy(ioutil.Discard, res.Body)
1134 if err != nil {
1135 t.Fatalf("client body read error: %v", err)
1137 res.Body.Close()
1139 // Force child to exit cleanly.
1140 Post(fmt.Sprintf("http://%s/quit", ln.Addr()), "", nil)
1141 child.Wait()
1143 rx := regexp.MustCompile(`sendfile(64)?\(`)
1144 out := buf.String()
1145 if !rx.MatchString(out) {
1146 t.Errorf("no sendfile system call found in:\n%s", out)
1150 func getBody(t *testing.T, testName string, req Request, client *Client) (*Response, []byte) {
1151 r, err := client.Do(&req)
1152 if err != nil {
1153 t.Fatalf("%s: for URL %q, send error: %v", testName, req.URL.String(), err)
1155 b, err := ioutil.ReadAll(r.Body)
1156 if err != nil {
1157 t.Fatalf("%s: for URL %q, reading body: %v", testName, req.URL.String(), err)
1159 return r, b
1162 // TestLinuxSendfileChild isn't a real test. It's used as a helper process
1163 // for TestLinuxSendfile.
1164 func TestLinuxSendfileChild(*testing.T) {
1165 if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
1166 return
1168 defer os.Exit(0)
1169 fd3 := os.NewFile(3, "ephemeral-port-listener")
1170 ln, err := net.FileListener(fd3)
1171 if err != nil {
1172 panic(err)
1174 mux := NewServeMux()
1175 mux.Handle("/", FileServer(Dir("testdata")))
1176 mux.HandleFunc("/quit", func(ResponseWriter, *Request) {
1177 os.Exit(0)
1179 s := &Server{Handler: mux}
1180 err = s.Serve(ln)
1181 if err != nil {
1182 panic(err)
1186 // Issue 18984: tests that requests for paths beyond files return not-found errors
1187 func TestFileServerNotDirError(t *testing.T) {
1188 defer afterTest(t)
1189 ts := httptest.NewServer(FileServer(Dir("testdata")))
1190 defer ts.Close()
1192 res, err := Get(ts.URL + "/index.html/not-a-file")
1193 if err != nil {
1194 t.Fatal(err)
1196 res.Body.Close()
1197 if res.StatusCode != 404 {
1198 t.Errorf("StatusCode = %v; want 404", res.StatusCode)
1201 test := func(name string, dir Dir) {
1202 t.Run(name, func(t *testing.T) {
1203 _, err = dir.Open("/index.html/not-a-file")
1204 if err == nil {
1205 t.Fatal("err == nil; want != nil")
1207 if !os.IsNotExist(err) {
1208 t.Errorf("err = %v; os.IsNotExist(err) = %v; want true", err, os.IsNotExist(err))
1211 _, err = dir.Open("/index.html/not-a-dir/not-a-file")
1212 if err == nil {
1213 t.Fatal("err == nil; want != nil")
1215 if !os.IsNotExist(err) {
1216 t.Errorf("err = %v; os.IsNotExist(err) = %v; want true", err, os.IsNotExist(err))
1221 absPath, err := filepath.Abs("testdata")
1222 if err != nil {
1223 t.Fatal("get abs path:", err)
1226 test("RelativePath", Dir("testdata"))
1227 test("AbsolutePath", Dir(absPath))
1230 func TestFileServerCleanPath(t *testing.T) {
1231 tests := []struct {
1232 path string
1233 wantCode int
1234 wantOpen []string
1236 {"/", 200, []string{"/", "/index.html"}},
1237 {"/dir", 301, []string{"/dir"}},
1238 {"/dir/", 200, []string{"/dir", "/dir/index.html"}},
1240 for _, tt := range tests {
1241 var log []string
1242 rr := httptest.NewRecorder()
1243 req, _ := NewRequest("GET", "http://foo.localhost"+tt.path, nil)
1244 FileServer(fileServerCleanPathDir{&log}).ServeHTTP(rr, req)
1245 if !reflect.DeepEqual(log, tt.wantOpen) {
1246 t.Logf("For %s: Opens = %q; want %q", tt.path, log, tt.wantOpen)
1248 if rr.Code != tt.wantCode {
1249 t.Logf("For %s: Response code = %d; want %d", tt.path, rr.Code, tt.wantCode)
1254 type fileServerCleanPathDir struct {
1255 log *[]string
1258 func (d fileServerCleanPathDir) Open(path string) (File, error) {
1259 *(d.log) = append(*(d.log), path)
1260 if path == "/" || path == "/dir" || path == "/dir/" {
1261 // Just return back something that's a directory.
1262 return Dir(".").Open(".")
1264 return nil, os.ErrNotExist
1267 type panicOnSeek struct{ io.ReadSeeker }
1269 func Test_scanETag(t *testing.T) {
1270 tests := []struct {
1271 in string
1272 wantETag string
1273 wantRemain string
1275 {`W/"etag-1"`, `W/"etag-1"`, ""},
1276 {`"etag-2"`, `"etag-2"`, ""},
1277 {`"etag-1", "etag-2"`, `"etag-1"`, `, "etag-2"`},
1278 {"", "", ""},
1279 {"W/", "", ""},
1280 {`W/"truc`, "", ""},
1281 {`w/"case-sensitive"`, "", ""},
1282 {`"spaced etag"`, "", ""},
1284 for _, test := range tests {
1285 etag, remain := ExportScanETag(test.in)
1286 if etag != test.wantETag || remain != test.wantRemain {
1287 t.Errorf("scanETag(%q)=%q %q, want %q %q", test.in, etag, remain, test.wantETag, test.wantRemain)