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.
34 testFile = "testdata/file"
38 type wantRange struct {
39 start, end int64 // range [start,end)
42 var ServeFileRangeTests = []struct {
47 {r: "", code: StatusOK},
48 {r: "bytes=0-4", code: StatusPartialContent, ranges: []wantRange{{0, 5}}},
49 {r: "bytes=2-", code: StatusPartialContent, ranges: []wantRange{{2, testFileLen}}},
50 {r: "bytes=-5", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 5, testFileLen}}},
51 {r: "bytes=3-7", code: StatusPartialContent, ranges: []wantRange{{3, 8}}},
52 {r: "bytes=0-0,-2", code: StatusPartialContent, ranges: []wantRange{{0, 1}, {testFileLen - 2, testFileLen}}},
53 {r: "bytes=0-1,5-8", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, 9}}},
54 {r: "bytes=0-1,5-", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, testFileLen}}},
55 {r: "bytes=5-1000", code: StatusPartialContent, ranges: []wantRange{{5, testFileLen}}},
56 {r: "bytes=0-,1-,2-,3-,4-", code: StatusOK}, // ignore wasteful range request
57 {r: "bytes=0-9", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen - 1}}},
58 {r: "bytes=0-10", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
59 {r: "bytes=0-11", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
60 {r: "bytes=10-11", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}},
61 {r: "bytes=10-", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}},
62 {r: "bytes=11-", code: StatusRequestedRangeNotSatisfiable},
63 {r: "bytes=11-12", code: StatusRequestedRangeNotSatisfiable},
64 {r: "bytes=12-12", code: StatusRequestedRangeNotSatisfiable},
65 {r: "bytes=11-100", code: StatusRequestedRangeNotSatisfiable},
66 {r: "bytes=12-100", code: StatusRequestedRangeNotSatisfiable},
67 {r: "bytes=100-", code: StatusRequestedRangeNotSatisfiable},
68 {r: "bytes=100-1000", code: StatusRequestedRangeNotSatisfiable},
71 func TestServeFile(t *testing.T) { run(t, testServeFile) }
72 func testServeFile(t *testing.T, mode testMode) {
73 ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
74 ServeFile(w, r, "testdata/file")
80 file, err := os.ReadFile(testFile)
82 t.Fatal("reading file:", err)
85 // set up the Request (re-used for all tests)
87 req.Header = make(Header)
88 if req.URL, err = url.Parse(ts.URL); err != nil {
89 t.Fatal("ParseURL:", err)
92 // Get contents via various methods.
94 // See https://go.dev/issue/59471 for a proposal to limit the set of methods handled.
95 // For now, test the historical behavior.
96 for _, method := range []string{
106 _, body := getBody(t, method, req, c)
107 if !bytes.Equal(body, file) {
108 t.Fatalf("body mismatch for %v request: got %q, want %q", method, body, file)
113 req.Method = MethodHead
114 resp, body := getBody(t, "HEAD", req, c)
116 t.Fatalf("body mismatch for HEAD request: got %q, want empty", body)
118 if got, want := resp.Header.Get("Content-Length"), fmt.Sprint(len(file)); got != want {
119 t.Fatalf("Content-Length mismatch for HEAD request: got %v, want %v", got, want)
123 req.Method = MethodGet
125 for _, rt := range ServeFileRangeTests {
127 req.Header.Set("Range", rt.r)
129 resp, body := getBody(t, fmt.Sprintf("range test %q", rt.r), req, c)
130 if resp.StatusCode != rt.code {
131 t.Errorf("range=%q: StatusCode=%d, want %d", rt.r, resp.StatusCode, rt.code)
133 if rt.code == StatusRequestedRangeNotSatisfiable {
136 wantContentRange := ""
137 if len(rt.ranges) == 1 {
139 wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
141 cr := resp.Header.Get("Content-Range")
142 if cr != wantContentRange {
143 t.Errorf("range=%q: Content-Range = %q, want %q", rt.r, cr, wantContentRange)
145 ct := resp.Header.Get("Content-Type")
146 if len(rt.ranges) == 1 {
148 wantBody := file[rng.start:rng.end]
149 if !bytes.Equal(body, wantBody) {
150 t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
152 if strings.HasPrefix(ct, "multipart/byteranges") {
153 t.Errorf("range=%q content-type = %q; unexpected multipart/byteranges", rt.r, ct)
156 if len(rt.ranges) > 1 {
157 typ, params, err := mime.ParseMediaType(ct)
159 t.Errorf("range=%q content-type = %q; %v", rt.r, ct, err)
162 if typ != "multipart/byteranges" {
163 t.Errorf("range=%q content-type = %q; want multipart/byteranges", rt.r, typ)
166 if params["boundary"] == "" {
167 t.Errorf("range=%q content-type = %q; lacks boundary", rt.r, ct)
170 if g, w := resp.ContentLength, int64(len(body)); g != w {
171 t.Errorf("range=%q Content-Length = %d; want %d", rt.r, g, w)
174 mr := multipart.NewReader(bytes.NewReader(body), params["boundary"])
175 for ri, rng := range rt.ranges {
176 part, err := mr.NextPart()
178 t.Errorf("range=%q, reading part index %d: %v", rt.r, ri, err)
181 wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
182 if g, w := part.Header.Get("Content-Range"), wantContentRange; g != w {
183 t.Errorf("range=%q: part Content-Range = %q; want %q", rt.r, g, w)
185 body, err := io.ReadAll(part)
187 t.Errorf("range=%q, reading part index %d body: %v", rt.r, ri, err)
190 wantBody := file[rng.start:rng.end]
191 if !bytes.Equal(body, wantBody) {
192 t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
195 _, err = mr.NextPart()
197 t.Errorf("range=%q; expected final error io.EOF; got %v", rt.r, err)
203 func TestServeFile_DotDot(t *testing.T) {
208 {"/testdata/file", 200},
217 {"/file/a\\..", 400},
219 for _, tt := range tests {
220 req, err := ReadRequest(bufio.NewReader(strings.NewReader("GET " + tt.req + " HTTP/1.1\r\nHost: foo\r\n\r\n")))
222 t.Errorf("bad request %q: %v", tt.req, err)
225 rec := httptest.NewRecorder()
226 ServeFile(rec, req, "testdata/file")
227 if rec.Code != tt.wantStatus {
228 t.Errorf("for request %q, status = %d; want %d", tt.req, rec.Code, tt.wantStatus)
233 // Tests that this doesn't panic. (Issue 30165)
234 func TestServeFileDirPanicEmptyPath(t *testing.T) {
235 rec := httptest.NewRecorder()
236 req := httptest.NewRequest("GET", "/", nil)
238 ServeFile(rec, req, "testdata")
240 if res.StatusCode != 301 {
241 t.Errorf("code = %v; want 301", res.Status)
245 // Tests that ranges are ignored with serving empty content. (Issue 54794)
246 func TestServeContentWithEmptyContentIgnoreRanges(t *testing.T) {
247 for _, r := range []string{
251 rec := httptest.NewRecorder()
252 req := httptest.NewRequest("GET", "/", nil)
253 req.Header.Set("Range", r)
254 ServeContent(rec, req, "nothing", time.Now(), bytes.NewReader(nil))
256 if res.StatusCode != 200 {
257 t.Errorf("code = %v; want 200", res.Status)
259 bodyLen := rec.Body.Len()
261 t.Errorf("body.Len() = %v; want 0", res.Status)
266 var fsRedirectTestData = []struct {
267 original, redirect string
269 {"/test/index.html", "/test/"},
270 {"/test/testdata", "/test/testdata/"},
271 {"/test/testdata/file/", "/test/testdata/file"},
274 func TestFSRedirect(t *testing.T) { run(t, testFSRedirect) }
275 func testFSRedirect(t *testing.T, mode testMode) {
276 ts := newClientServerTest(t, mode, StripPrefix("/test", FileServer(Dir(".")))).ts
278 for _, data := range fsRedirectTestData {
279 res, err := ts.Client().Get(ts.URL + data.original)
284 if g, e := res.Request.URL.Path, data.redirect; g != e {
285 t.Errorf("redirect from %s: got %s, want %s", data.original, g, e)
290 type testFileSystem struct {
291 open func(name string) (File, error)
294 func (fs *testFileSystem) Open(name string) (File, error) {
298 func TestFileServerCleans(t *testing.T) {
300 ch := make(chan string, 1)
301 fs := FileServer(&testFileSystem{func(name string) (File, error) {
303 return nil, errors.New("file does not exist")
306 reqPath, openArg string
308 {"/foo.txt", "/foo.txt"},
309 {"//foo.txt", "/foo.txt"},
310 {"/../foo.txt", "/foo.txt"},
312 req, _ := NewRequest("GET", "http://example.com", nil)
313 for n, test := range tests {
314 rec := httptest.NewRecorder()
315 req.URL.Path = test.reqPath
316 fs.ServeHTTP(rec, req)
317 if got := <-ch; got != test.openArg {
318 t.Errorf("test %d: got %q, want %q", n, got, test.openArg)
323 func TestFileServerEscapesNames(t *testing.T) { run(t, testFileServerEscapesNames) }
324 func testFileServerEscapesNames(t *testing.T, mode testMode) {
325 const dirListPrefix = "<pre>\n"
326 const dirListSuffix = "\n</pre>\n"
330 {`simple_name`, `<a href="simple_name">simple_name</a>`},
331 {`"'<>&`, `<a href="%22%27%3C%3E&">"'<>&</a>`},
332 {`?foo=bar#baz`, `<a href="%3Ffoo=bar%23baz">?foo=bar#baz</a>`},
333 {`<combo>?foo`, `<a href="%3Ccombo%3E%3Ffoo"><combo>?foo</a>`},
334 {`foo:bar`, `<a href="./foo:bar">foo:bar</a>`},
337 // We put each test file in its own directory in the fakeFS so we can look at it in isolation.
339 for i, test := range tests {
340 testFile := &fakeFileInfo{basename: test.name}
341 fs[fmt.Sprintf("/%d", i)] = &fakeFileInfo{
343 modtime: time.Unix(1000000000, 0).UTC(),
344 ents: []*fakeFileInfo{testFile},
346 fs[fmt.Sprintf("/%d/%s", i, test.name)] = testFile
349 ts := newClientServerTest(t, mode, FileServer(&fs)).ts
350 for i, test := range tests {
351 url := fmt.Sprintf("%s/%d", ts.URL, i)
352 res, err := ts.Client().Get(url)
354 t.Fatalf("test %q: Get: %v", test.name, err)
356 b, err := io.ReadAll(res.Body)
358 t.Fatalf("test %q: read Body: %v", test.name, err)
361 if !strings.HasPrefix(s, dirListPrefix) || !strings.HasSuffix(s, dirListSuffix) {
362 t.Errorf("test %q: listing dir, full output is %q, want prefix %q and suffix %q", test.name, s, dirListPrefix, dirListSuffix)
364 if trimmed := strings.TrimSuffix(strings.TrimPrefix(s, dirListPrefix), dirListSuffix); trimmed != test.escaped {
365 t.Errorf("test %q: listing dir, filename escaped to %q, want %q", test.name, trimmed, test.escaped)
371 func TestFileServerSortsNames(t *testing.T) { run(t, testFileServerSortsNames) }
372 func testFileServerSortsNames(t *testing.T, mode testMode) {
373 const contents = "I am a fake file"
374 dirMod := time.Unix(123, 0).UTC()
375 fileMod := time.Unix(1000000000, 0).UTC()
380 ents: []*fakeFileInfo{
395 ts := newClientServerTest(t, mode, FileServer(&fs)).ts
397 res, err := ts.Client().Get(ts.URL)
399 t.Fatalf("Get: %v", err)
401 defer res.Body.Close()
403 b, err := io.ReadAll(res.Body)
405 t.Fatalf("read Body: %v", err)
408 if !strings.Contains(s, "<a href=\"a\">a</a>\n<a href=\"b\">b</a>") {
409 t.Errorf("output appears to be unsorted:\n%s", s)
413 func mustRemoveAll(dir string) {
414 err := os.RemoveAll(dir)
420 func TestFileServerImplicitLeadingSlash(t *testing.T) { run(t, testFileServerImplicitLeadingSlash) }
421 func testFileServerImplicitLeadingSlash(t *testing.T, mode testMode) {
422 tempDir := t.TempDir()
423 if err := os.WriteFile(filepath.Join(tempDir, "foo.txt"), []byte("Hello world"), 0644); err != nil {
424 t.Fatalf("WriteFile: %v", err)
426 ts := newClientServerTest(t, mode, StripPrefix("/bar/", FileServer(Dir(tempDir)))).ts
427 get := func(suffix string) string {
428 res, err := ts.Client().Get(ts.URL + suffix)
430 t.Fatalf("Get %s: %v", suffix, err)
432 b, err := io.ReadAll(res.Body)
434 t.Fatalf("ReadAll %s: %v", suffix, err)
439 if s := get("/bar/"); !strings.Contains(s, ">foo.txt<") {
440 t.Logf("expected a directory listing with foo.txt, got %q", s)
442 if s := get("/bar/foo.txt"); s != "Hello world" {
443 t.Logf("expected %q, got %q", "Hello world", s)
447 func TestDirJoin(t *testing.T) {
448 if runtime.GOOS == "windows" {
449 t.Skip("skipping test on windows")
451 wfi, err := os.Stat("/etc/hosts")
453 t.Skip("skipping test; no /etc/hosts file")
455 test := func(d Dir, name string) {
456 f, err := d.Open(name)
458 t.Fatalf("open of %s: %v", name, err)
463 t.Fatalf("stat of %s: %v", name, err)
465 if !os.SameFile(gfi, wfi) {
466 t.Errorf("%s got different file", name)
469 test(Dir("/etc/"), "/hosts")
470 test(Dir("/etc/"), "hosts")
471 test(Dir("/etc/"), "../../../../hosts")
472 test(Dir("/etc"), "/hosts")
473 test(Dir("/etc"), "hosts")
474 test(Dir("/etc"), "../../../../hosts")
476 // Not really directories, but since we use this trick in
477 // ServeFile, test it:
478 test(Dir("/etc/hosts"), "")
479 test(Dir("/etc/hosts"), "/")
480 test(Dir("/etc/hosts"), "../")
483 func TestEmptyDirOpenCWD(t *testing.T) {
484 test := func(d Dir) {
486 f, err := d.Open(name)
488 t.Fatalf("open of %s: %v", name, err)
497 func TestServeFileContentType(t *testing.T) { run(t, testServeFileContentType) }
498 func testServeFileContentType(t *testing.T, mode testMode) {
499 const ctype = "icecream/chocolate"
500 ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
501 switch r.FormValue("override") {
503 w.Header().Set("Content-Type", ctype)
505 // Explicitly inhibit sniffing.
506 w.Header()["Content-Type"] = []string{}
508 ServeFile(w, r, "testdata/file")
510 get := func(override string, want []string) {
511 resp, err := ts.Client().Get(ts.URL + "?override=" + override)
515 if h := resp.Header["Content-Type"]; !reflect.DeepEqual(h, want) {
516 t.Errorf("Content-Type mismatch: got %v, want %v", h, want)
520 get("0", []string{"text/plain; charset=utf-8"})
521 get("1", []string{ctype})
525 func TestServeFileMimeType(t *testing.T) { run(t, testServeFileMimeType) }
526 func testServeFileMimeType(t *testing.T, mode testMode) {
527 ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
528 ServeFile(w, r, "testdata/style.css")
530 resp, err := ts.Client().Get(ts.URL)
535 want := "text/css; charset=utf-8"
536 if h := resp.Header.Get("Content-Type"); h != want {
537 t.Errorf("Content-Type mismatch: got %q, want %q", h, want)
541 func TestServeFileFromCWD(t *testing.T) { run(t, testServeFileFromCWD) }
542 func testServeFileFromCWD(t *testing.T, mode testMode) {
543 ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
544 ServeFile(w, r, "fs_test.go")
546 r, err := ts.Client().Get(ts.URL)
551 if r.StatusCode != 200 {
552 t.Fatalf("expected 200 OK, got %s", r.Status)
557 func TestServeDirWithoutTrailingSlash(t *testing.T) { run(t, testServeDirWithoutTrailingSlash) }
558 func testServeDirWithoutTrailingSlash(t *testing.T, mode testMode) {
560 ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
563 r, err := ts.Client().Get(ts.URL + "/testdata")
568 if g := r.Request.URL.Path; g != e {
569 t.Errorf("got %s, want %s", g, e)
573 // Tests that ServeFile doesn't add a Content-Length if a Content-Encoding is
575 func TestServeFileWithContentEncoding(t *testing.T) { run(t, testServeFileWithContentEncoding) }
576 func testServeFileWithContentEncoding(t *testing.T, mode testMode) {
577 cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
578 w.Header().Set("Content-Encoding", "foo")
579 ServeFile(w, r, "testdata/file")
581 // Because the testdata is so small, it would fit in
582 // both the h1 and h2 Server's write buffers. For h1,
583 // sendfile is used, though, forcing a header flush at
584 // the io.Copy. http2 doesn't do a header flush so
585 // buffers all 11 bytes and then adds its own
586 // Content-Length. To prevent the Server's
587 // Content-Length and test ServeFile only, flush here.
590 resp, err := cst.c.Get(cst.ts.URL)
595 if g, e := resp.ContentLength, int64(-1); g != e {
596 t.Errorf("Content-Length mismatch: got %d, want %d", g, e)
600 // Tests that ServeFile does not generate representation metadata when
601 // file has not been modified, as per RFC 7232 section 4.1.
602 func TestServeFileNotModified(t *testing.T) { run(t, testServeFileNotModified) }
603 func testServeFileNotModified(t *testing.T, mode testMode) {
604 cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
605 w.Header().Set("Content-Type", "application/json")
606 w.Header().Set("Content-Encoding", "foo")
607 w.Header().Set("Etag", `"123"`)
608 ServeFile(w, r, "testdata/file")
610 // Because the testdata is so small, it would fit in
611 // both the h1 and h2 Server's write buffers. For h1,
612 // sendfile is used, though, forcing a header flush at
613 // the io.Copy. http2 doesn't do a header flush so
614 // buffers all 11 bytes and then adds its own
615 // Content-Length. To prevent the Server's
616 // Content-Length and test ServeFile only, flush here.
619 req, err := NewRequest("GET", cst.ts.URL, nil)
623 req.Header.Set("If-None-Match", `"123"`)
624 resp, err := cst.c.Do(req)
628 b, err := io.ReadAll(resp.Body)
631 t.Fatal("reading Body:", err)
634 t.Errorf("non-empty body")
636 if g, e := resp.StatusCode, StatusNotModified; g != e {
637 t.Errorf("status mismatch: got %d, want %d", g, e)
639 // HTTP1 transport sets ContentLength to 0.
640 if g, e1, e2 := resp.ContentLength, int64(-1), int64(0); g != e1 && g != e2 {
641 t.Errorf("Content-Length mismatch: got %d, want %d or %d", g, e1, e2)
643 if resp.Header.Get("Content-Type") != "" {
644 t.Errorf("Content-Type present, but it should not be")
646 if resp.Header.Get("Content-Encoding") != "" {
647 t.Errorf("Content-Encoding present, but it should not be")
651 func TestServeIndexHtml(t *testing.T) { run(t, testServeIndexHtml) }
652 func testServeIndexHtml(t *testing.T, mode testMode) {
653 for i := 0; i < 2; i++ {
658 h = FileServer(Dir("."))
661 h = FileServer(FS(os.DirFS(".")))
664 t.Run(name, func(t *testing.T) {
665 const want = "index.html says hello\n"
666 ts := newClientServerTest(t, mode, h).ts
668 for _, path := range []string{"/testdata/", "/testdata/index.html"} {
669 res, err := ts.Client().Get(ts.URL + path)
673 b, err := io.ReadAll(res.Body)
675 t.Fatal("reading Body:", err)
677 if s := string(b); s != want {
678 t.Errorf("for path %q got %q, want %q", path, s, want)
686 func TestServeIndexHtmlFS(t *testing.T) { run(t, testServeIndexHtmlFS) }
687 func testServeIndexHtmlFS(t *testing.T, mode testMode) {
688 const want = "index.html says hello\n"
689 ts := newClientServerTest(t, mode, FileServer(Dir("."))).ts
692 for _, path := range []string{"/testdata/", "/testdata/index.html"} {
693 res, err := ts.Client().Get(ts.URL + path)
697 b, err := io.ReadAll(res.Body)
699 t.Fatal("reading Body:", err)
701 if s := string(b); s != want {
702 t.Errorf("for path %q got %q, want %q", path, s, want)
708 func TestFileServerZeroByte(t *testing.T) { run(t, testFileServerZeroByte) }
709 func testFileServerZeroByte(t *testing.T, mode testMode) {
710 ts := newClientServerTest(t, mode, FileServer(Dir("."))).ts
712 c, err := net.Dial("tcp", ts.Listener.Addr().String())
717 _, err = fmt.Fprintf(c, "GET /..\x00 HTTP/1.0\r\n\r\n")
722 bufr := bufio.NewReader(io.TeeReader(c, &got))
723 res, err := ReadResponse(bufr, nil)
725 t.Fatal("ReadResponse: ", err)
727 if res.StatusCode == 200 {
728 t.Errorf("got status 200; want an error. Body is:\n%s", got.Bytes())
732 func TestFileServerNamesEscape(t *testing.T) { run(t, testFileServerNamesEscape) }
733 func testFileServerNamesEscape(t *testing.T, mode testMode) {
734 ts := newClientServerTest(t, mode, FileServer(Dir("testdata"))).ts
735 for _, path := range []string{
737 "/NUL", // don't read from device files on Windows
739 res, err := ts.Client().Get(ts.URL + path)
744 if res.StatusCode < 400 || res.StatusCode > 599 {
745 t.Errorf("Get(%q): got status %v, want 4xx or 5xx", path, res.StatusCode)
751 type fakeFileInfo struct {
760 func (f *fakeFileInfo) Name() string { return f.basename }
761 func (f *fakeFileInfo) Sys() any { return nil }
762 func (f *fakeFileInfo) ModTime() time.Time { return f.modtime }
763 func (f *fakeFileInfo) IsDir() bool { return f.dir }
764 func (f *fakeFileInfo) Size() int64 { return int64(len(f.contents)) }
765 func (f *fakeFileInfo) Mode() fs.FileMode {
767 return 0755 | fs.ModeDir
772 func (f *fakeFileInfo) String() string {
773 return fs.FormatFileInfo(f)
776 type fakeFile struct {
779 path string // as opened
783 func (f *fakeFile) Close() error { return nil }
784 func (f *fakeFile) Stat() (fs.FileInfo, error) { return f.fi, nil }
785 func (f *fakeFile) Readdir(count int) ([]fs.FileInfo, error) {
787 return nil, fs.ErrInvalid
789 var fis []fs.FileInfo
791 limit := f.entpos + count
792 if count <= 0 || limit > len(f.fi.ents) {
793 limit = len(f.fi.ents)
795 for ; f.entpos < limit; f.entpos++ {
796 fis = append(fis, f.fi.ents[f.entpos])
799 if len(fis) == 0 && count > 0 {
806 type fakeFS map[string]*fakeFileInfo
808 func (fsys fakeFS) Open(name string) (File, error) {
809 name = path.Clean(name)
812 return nil, fs.ErrNotExist
817 return &fakeFile{ReadSeeker: strings.NewReader(f.contents), fi: f, path: name}, nil
820 func TestDirectoryIfNotModified(t *testing.T) { run(t, testDirectoryIfNotModified) }
821 func testDirectoryIfNotModified(t *testing.T, mode testMode) {
822 const indexContents = "I am a fake index.html file"
823 fileMod := time.Unix(1000000000, 0).UTC()
824 fileModStr := fileMod.Format(TimeFormat)
825 dirMod := time.Unix(123, 0).UTC()
826 indexFile := &fakeFileInfo{
827 basename: "index.html",
829 contents: indexContents,
835 ents: []*fakeFileInfo{indexFile},
837 "/index.html": indexFile,
840 ts := newClientServerTest(t, mode, FileServer(fs)).ts
842 res, err := ts.Client().Get(ts.URL)
846 b, err := io.ReadAll(res.Body)
850 if string(b) != indexContents {
851 t.Fatalf("Got body %q; want %q", b, indexContents)
855 lastMod := res.Header.Get("Last-Modified")
856 if lastMod != fileModStr {
857 t.Fatalf("initial Last-Modified = %q; want %q", lastMod, fileModStr)
860 req, _ := NewRequest("GET", ts.URL, nil)
861 req.Header.Set("If-Modified-Since", lastMod)
868 if res.StatusCode != 304 {
869 t.Fatalf("Code after If-Modified-Since request = %v; want 304", res.StatusCode)
873 // Advance the index.html file's modtime, but not the directory's.
874 indexFile.modtime = indexFile.modtime.Add(1 * time.Hour)
880 if res.StatusCode != 200 {
881 t.Fatalf("Code after second If-Modified-Since request = %v; want 200; res is %#v", res.StatusCode, res)
886 func mustStat(t *testing.T, fileName string) fs.FileInfo {
887 fi, err := os.Stat(fileName)
894 func TestServeContent(t *testing.T) { run(t, testServeContent) }
895 func testServeContent(t *testing.T, mode testMode) {
896 type serveParam struct {
899 content io.ReadSeeker
903 servec := make(chan serveParam, 1)
904 ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
907 w.Header().Set("ETag", p.etag)
909 if p.contentType != "" {
910 w.Header().Set("Content-Type", p.contentType)
912 ServeContent(w, r, p.name, p.modtime, p.content)
915 type testCase struct {
916 // One of file or content must be set:
918 content io.ReadSeeker
921 serveETag string // optional
922 serveContentType string // optional
923 reqHeader map[string]string
925 wantContentType string
926 wantContentRange string
929 htmlModTime := mustStat(t, "testdata/index.html").ModTime()
930 tests := map[string]testCase{
931 "no_last_modified": {
932 file: "testdata/style.css",
933 wantContentType: "text/css; charset=utf-8",
936 "with_last_modified": {
937 file: "testdata/index.html",
938 wantContentType: "text/html; charset=utf-8",
939 modtime: htmlModTime,
940 wantLastMod: htmlModTime.UTC().Format(TimeFormat),
943 "not_modified_modtime": {
944 file: "testdata/style.css",
945 serveETag: `"foo"`, // Last-Modified sent only when no ETag
946 modtime: htmlModTime,
947 reqHeader: map[string]string{
948 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
952 "not_modified_modtime_with_contenttype": {
953 file: "testdata/style.css",
954 serveContentType: "text/css", // explicit content type
955 serveETag: `"foo"`, // Last-Modified sent only when no ETag
956 modtime: htmlModTime,
957 reqHeader: map[string]string{
958 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
962 "not_modified_etag": {
963 file: "testdata/style.css",
965 reqHeader: map[string]string{
966 "If-None-Match": `"foo"`,
970 "not_modified_etag_no_seek": {
971 content: panicOnSeek{nil}, // should never be called
972 serveETag: `W/"foo"`, // If-None-Match uses weak ETag comparison
973 reqHeader: map[string]string{
974 "If-None-Match": `"baz", W/"foo"`,
978 "if_none_match_mismatch": {
979 file: "testdata/style.css",
981 reqHeader: map[string]string{
982 "If-None-Match": `"Foo"`,
985 wantContentType: "text/css; charset=utf-8",
987 "if_none_match_malformed": {
988 file: "testdata/style.css",
990 reqHeader: map[string]string{
991 "If-None-Match": `,`,
994 wantContentType: "text/css; charset=utf-8",
997 file: "testdata/style.css",
999 reqHeader: map[string]string{
1000 "Range": "bytes=0-4",
1002 wantStatus: StatusPartialContent,
1003 wantContentType: "text/css; charset=utf-8",
1004 wantContentRange: "bytes 0-4/8",
1007 file: "testdata/style.css",
1009 reqHeader: map[string]string{
1010 "Range": "bytes=0-4",
1013 wantStatus: StatusPartialContent,
1014 wantContentType: "text/css; charset=utf-8",
1015 wantContentRange: "bytes 0-4/8",
1017 "range_match_weak_etag": {
1018 file: "testdata/style.css",
1020 reqHeader: map[string]string{
1021 "Range": "bytes=0-4",
1022 "If-Range": `W/"A"`,
1025 wantContentType: "text/css; charset=utf-8",
1027 "range_no_overlap": {
1028 file: "testdata/style.css",
1030 reqHeader: map[string]string{
1031 "Range": "bytes=10-20",
1033 wantStatus: StatusRequestedRangeNotSatisfiable,
1034 wantContentType: "text/plain; charset=utf-8",
1035 wantContentRange: "bytes */8",
1037 // An If-Range resource for entity "A", but entity "B" is now current.
1038 // The Range request should be ignored.
1040 file: "testdata/style.css",
1042 reqHeader: map[string]string{
1043 "Range": "bytes=0-4",
1047 wantContentType: "text/css; charset=utf-8",
1049 "range_with_modtime": {
1050 file: "testdata/style.css",
1051 modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 /* nanos */, time.UTC),
1052 reqHeader: map[string]string{
1053 "Range": "bytes=0-4",
1054 "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
1056 wantStatus: StatusPartialContent,
1057 wantContentType: "text/css; charset=utf-8",
1058 wantContentRange: "bytes 0-4/8",
1059 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT",
1061 "range_with_modtime_mismatch": {
1062 file: "testdata/style.css",
1063 modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 /* nanos */, time.UTC),
1064 reqHeader: map[string]string{
1065 "Range": "bytes=0-4",
1066 "If-Range": "Wed, 25 Jun 2014 17:12:19 GMT",
1068 wantStatus: StatusOK,
1069 wantContentType: "text/css; charset=utf-8",
1070 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT",
1072 "range_with_modtime_nanos": {
1073 file: "testdata/style.css",
1074 modtime: time.Date(2014, 6, 25, 17, 12, 18, 123 /* nanos */, time.UTC),
1075 reqHeader: map[string]string{
1076 "Range": "bytes=0-4",
1077 "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
1079 wantStatus: StatusPartialContent,
1080 wantContentType: "text/css; charset=utf-8",
1081 wantContentRange: "bytes 0-4/8",
1082 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT",
1084 "unix_zero_modtime": {
1085 content: strings.NewReader("<html>foo"),
1086 modtime: time.Unix(0, 0),
1087 wantStatus: StatusOK,
1088 wantContentType: "text/html; charset=utf-8",
1090 "ifmatch_matches": {
1091 file: "testdata/style.css",
1093 reqHeader: map[string]string{
1094 "If-Match": `"Z", "A"`,
1097 wantContentType: "text/css; charset=utf-8",
1100 file: "testdata/style.css",
1102 reqHeader: map[string]string{
1106 wantContentType: "text/css; charset=utf-8",
1109 file: "testdata/style.css",
1111 reqHeader: map[string]string{
1116 "ifmatch_fails_on_weak_etag": {
1117 file: "testdata/style.css",
1119 reqHeader: map[string]string{
1120 "If-Match": `W/"A"`,
1124 "if_unmodified_since_true": {
1125 file: "testdata/style.css",
1126 modtime: htmlModTime,
1127 reqHeader: map[string]string{
1128 "If-Unmodified-Since": htmlModTime.UTC().Format(TimeFormat),
1131 wantContentType: "text/css; charset=utf-8",
1132 wantLastMod: htmlModTime.UTC().Format(TimeFormat),
1134 "if_unmodified_since_false": {
1135 file: "testdata/style.css",
1136 modtime: htmlModTime,
1137 reqHeader: map[string]string{
1138 "If-Unmodified-Since": htmlModTime.Add(-2 * time.Second).UTC().Format(TimeFormat),
1141 wantLastMod: htmlModTime.UTC().Format(TimeFormat),
1144 for testName, tt := range tests {
1145 var content io.ReadSeeker
1147 f, err := os.Open(tt.file)
1149 t.Fatalf("test %q: %v", testName, err)
1154 content = tt.content
1156 for _, method := range []string{"GET", "HEAD"} {
1157 //restore content in case it is consumed by previous method
1158 if content, ok := content.(*strings.Reader); ok {
1159 content.Seek(0, io.SeekStart)
1162 servec <- serveParam{
1163 name: filepath.Base(tt.file),
1165 modtime: tt.modtime,
1167 contentType: tt.serveContentType,
1169 req, err := NewRequest(method, ts.URL, nil)
1173 for k, v := range tt.reqHeader {
1174 req.Header.Set(k, v)
1178 res, err := c.Do(req)
1182 io.Copy(io.Discard, res.Body)
1184 if res.StatusCode != tt.wantStatus {
1185 t.Errorf("test %q using %q: got status = %d; want %d", testName, method, res.StatusCode, tt.wantStatus)
1187 if g, e := res.Header.Get("Content-Type"), tt.wantContentType; g != e {
1188 t.Errorf("test %q using %q: got content-type = %q, want %q", testName, method, g, e)
1190 if g, e := res.Header.Get("Content-Range"), tt.wantContentRange; g != e {
1191 t.Errorf("test %q using %q: got content-range = %q, want %q", testName, method, g, e)
1193 if g, e := res.Header.Get("Last-Modified"), tt.wantLastMod; g != e {
1194 t.Errorf("test %q using %q: got last-modified = %q, want %q", testName, method, g, e)
1201 func TestServerFileStatError(t *testing.T) {
1202 rec := httptest.NewRecorder()
1203 r, _ := NewRequest("GET", "http://foo/", nil)
1206 fs := issue12991FS{}
1207 ExportServeFile(rec, r, fs, name, redirect)
1208 if body := rec.Body.String(); !strings.Contains(body, "403") || !strings.Contains(body, "Forbidden") {
1209 t.Errorf("wanted 403 forbidden message; got: %s", body)
1213 type issue12991FS struct{}
1215 func (issue12991FS) Open(string) (File, error) { return issue12991File{}, nil }
1217 type issue12991File struct{ File }
1219 func (issue12991File) Stat() (fs.FileInfo, error) { return nil, fs.ErrPermission }
1220 func (issue12991File) Close() error { return nil }
1222 func TestServeContentErrorMessages(t *testing.T) { run(t, testServeContentErrorMessages) }
1223 func testServeContentErrorMessages(t *testing.T, mode testMode) {
1225 "/500": &fakeFileInfo{
1226 err: errors.New("random error"),
1228 "/403": &fakeFileInfo{
1229 err: &fs.PathError{Err: fs.ErrPermission},
1232 ts := newClientServerTest(t, mode, FileServer(fs)).ts
1234 for _, code := range []int{403, 404, 500} {
1235 res, err := c.Get(fmt.Sprintf("%s/%d", ts.URL, code))
1237 t.Errorf("Error fetching /%d: %v", code, err)
1240 if res.StatusCode != code {
1241 t.Errorf("For /%d, status code = %d; want %d", code, res.StatusCode, code)
1247 // verifies that sendfile is being used on Linux
1248 func TestLinuxSendfile(t *testing.T) {
1251 if runtime.GOOS != "linux" {
1252 t.Skip("skipping; linux-only test")
1254 if _, err := exec.LookPath("strace"); err != nil {
1255 t.Skip("skipping; strace not found in path")
1258 ln, err := net.Listen("tcp", "127.0.0.1:0")
1262 lnf, err := ln.(*net.TCPListener).File()
1268 // Attempt to run strace, and skip on failure - this test requires SYS_PTRACE.
1269 if err := exec.Command("strace", "-f", "-q", os.Args[0], "-test.run=^$").Run(); err != nil {
1270 t.Skipf("skipping; failed to run strace: %v", err)
1273 filename := fmt.Sprintf("1kb-%d", os.Getpid())
1274 filepath := path.Join(os.TempDir(), filename)
1276 if err := os.WriteFile(filepath, bytes.Repeat([]byte{'a'}, 1<<10), 0755); err != nil {
1279 defer os.Remove(filepath)
1281 var buf strings.Builder
1282 child := exec.Command("strace", "-f", "-q", os.Args[0], "-test.run=TestLinuxSendfileChild")
1283 child.ExtraFiles = append(child.ExtraFiles, lnf)
1284 child.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...)
1287 if err := child.Start(); err != nil {
1288 t.Skipf("skipping; failed to start straced child: %v", err)
1291 res, err := Get(fmt.Sprintf("http://%s/%s", ln.Addr(), filename))
1293 t.Fatalf("http client error: %v", err)
1295 _, err = io.Copy(io.Discard, res.Body)
1297 t.Fatalf("client body read error: %v", err)
1301 // Force child to exit cleanly.
1302 Post(fmt.Sprintf("http://%s/quit", ln.Addr()), "", nil)
1305 rx := regexp.MustCompile(`\b(n64:)?sendfile(64)?\(`)
1307 if !rx.MatchString(out) {
1308 t.Errorf("no sendfile system call found in:\n%s", out)
1312 func getBody(t *testing.T, testName string, req Request, client *Client) (*Response, []byte) {
1313 r, err := client.Do(&req)
1315 t.Fatalf("%s: for URL %q, send error: %v", testName, req.URL.String(), err)
1317 b, err := io.ReadAll(r.Body)
1319 t.Fatalf("%s: for URL %q, reading body: %v", testName, req.URL.String(), err)
1324 // TestLinuxSendfileChild isn't a real test. It's used as a helper process
1325 // for TestLinuxSendfile.
1326 func TestLinuxSendfileChild(*testing.T) {
1327 if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
1331 fd3 := os.NewFile(3, "ephemeral-port-listener")
1332 ln, err := net.FileListener(fd3)
1336 mux := NewServeMux()
1337 mux.Handle("/", FileServer(Dir(os.TempDir())))
1338 mux.HandleFunc("/quit", func(ResponseWriter, *Request) {
1341 s := &Server{Handler: mux}
1348 // Issues 18984, 49552: tests that requests for paths beyond files return not-found errors
1349 func TestFileServerNotDirError(t *testing.T) {
1350 run(t, func(t *testing.T, mode testMode) {
1351 t.Run("Dir", func(t *testing.T) {
1352 testFileServerNotDirError(t, mode, func(path string) FileSystem { return Dir(path) })
1354 t.Run("FS", func(t *testing.T) {
1355 testFileServerNotDirError(t, mode, func(path string) FileSystem { return FS(os.DirFS(path)) })
1360 func testFileServerNotDirError(t *testing.T, mode testMode, newfs func(string) FileSystem) {
1361 ts := newClientServerTest(t, mode, FileServer(newfs("testdata"))).ts
1363 res, err := ts.Client().Get(ts.URL + "/index.html/not-a-file")
1368 if res.StatusCode != 404 {
1369 t.Errorf("StatusCode = %v; want 404", res.StatusCode)
1372 test := func(name string, fsys FileSystem) {
1373 t.Run(name, func(t *testing.T) {
1374 _, err = fsys.Open("/index.html/not-a-file")
1376 t.Fatal("err == nil; want != nil")
1378 if !errors.Is(err, fs.ErrNotExist) {
1379 t.Errorf("err = %v; errors.Is(err, fs.ErrNotExist) = %v; want true", err,
1380 errors.Is(err, fs.ErrNotExist))
1383 _, err = fsys.Open("/index.html/not-a-dir/not-a-file")
1385 t.Fatal("err == nil; want != nil")
1387 if !errors.Is(err, fs.ErrNotExist) {
1388 t.Errorf("err = %v; errors.Is(err, fs.ErrNotExist) = %v; want true", err,
1389 errors.Is(err, fs.ErrNotExist))
1394 absPath, err := filepath.Abs("testdata")
1396 t.Fatal("get abs path:", err)
1399 test("RelativePath", newfs("testdata"))
1400 test("AbsolutePath", newfs(absPath))
1403 func TestFileServerCleanPath(t *testing.T) {
1409 {"/", 200, []string{"/", "/index.html"}},
1410 {"/dir", 301, []string{"/dir"}},
1411 {"/dir/", 200, []string{"/dir", "/dir/index.html"}},
1413 for _, tt := range tests {
1415 rr := httptest.NewRecorder()
1416 req, _ := NewRequest("GET", "http://foo.localhost"+tt.path, nil)
1417 FileServer(fileServerCleanPathDir{&log}).ServeHTTP(rr, req)
1418 if !reflect.DeepEqual(log, tt.wantOpen) {
1419 t.Logf("For %s: Opens = %q; want %q", tt.path, log, tt.wantOpen)
1421 if rr.Code != tt.wantCode {
1422 t.Logf("For %s: Response code = %d; want %d", tt.path, rr.Code, tt.wantCode)
1427 type fileServerCleanPathDir struct {
1431 func (d fileServerCleanPathDir) Open(path string) (File, error) {
1432 *(d.log) = append(*(d.log), path)
1433 if path == "/" || path == "/dir" || path == "/dir/" {
1434 // Just return back something that's a directory.
1435 return Dir(".").Open(".")
1437 return nil, fs.ErrNotExist
1440 type panicOnSeek struct{ io.ReadSeeker }
1442 func Test_scanETag(t *testing.T) {
1448 {`W/"etag-1"`, `W/"etag-1"`, ""},
1449 {`"etag-2"`, `"etag-2"`, ""},
1450 {`"etag-1", "etag-2"`, `"etag-1"`, `, "etag-2"`},
1453 {`W/"truc`, "", ""},
1454 {`w/"case-sensitive"`, "", ""},
1455 {`"spaced etag"`, "", ""},
1457 for _, test := range tests {
1458 etag, remain := ExportScanETag(test.in)
1459 if etag != test.wantETag || remain != test.wantRemain {
1460 t.Errorf("scanETag(%q)=%q %q, want %q %q", test.in, etag, remain, test.wantETag, test.wantRemain)
1465 // Issue 40940: Ensure that we only accept non-negative suffix-lengths
1466 // in "Range": "bytes=-N", and should reject "bytes=--2".
1467 func TestServeFileRejectsInvalidSuffixLengths(t *testing.T) {
1468 run(t, testServeFileRejectsInvalidSuffixLengths, []testMode{http1Mode, https1Mode, http2Mode})
1470 func testServeFileRejectsInvalidSuffixLengths(t *testing.T, mode testMode) {
1471 cst := newClientServerTest(t, mode, FileServer(Dir("testdata"))).ts
1478 {"bytes=--6", 416, "invalid range\n"},
1479 {"bytes=--0", 416, "invalid range\n"},
1480 {"bytes=---0", 416, "invalid range\n"},
1481 {"bytes=-6", 206, "hello\n"},
1482 {"bytes=6-", 206, "html says hello\n"},
1483 {"bytes=-6-", 416, "invalid range\n"},
1484 {"bytes=-0", 206, ""},
1485 {"bytes=", 200, "index.html says hello\n"},
1488 for _, tt := range tests {
1490 t.Run(tt.r, func(t *testing.T) {
1491 req, err := NewRequest("GET", cst.URL+"/index.html", nil)
1495 req.Header.Set("Range", tt.r)
1496 res, err := cst.Client().Do(req)
1500 if g, w := res.StatusCode, tt.wantCode; g != w {
1501 t.Errorf("StatusCode mismatch: got %d want %d", g, w)
1503 slurp, err := io.ReadAll(res.Body)
1508 if g, w := string(slurp), tt.wantBody; g != w {
1509 t.Fatalf("Content mismatch:\nGot: %q\nWant: %q", g, w)
1515 func TestFileServerMethods(t *testing.T) {
1516 run(t, testFileServerMethods)
1518 func testFileServerMethods(t *testing.T, mode testMode) {
1519 ts := newClientServerTest(t, mode, FileServer(Dir("testdata"))).ts
1521 file, err := os.ReadFile(testFile)
1523 t.Fatal("reading file:", err)
1526 // Get contents via various methods.
1528 // See https://go.dev/issue/59471 for a proposal to limit the set of methods handled.
1529 // For now, test the historical behavior.
1530 for _, method := range []string{
1540 req, _ := NewRequest(method, ts.URL+"/file", nil)
1542 res, err := ts.Client().Do(req)
1546 body, err := io.ReadAll(res.Body)
1552 if method == MethodHead {
1555 if !bytes.Equal(body, wantBody) {
1556 t.Fatalf("%v: got body %q, want %q", method, body, wantBody)
1558 if got, want := res.Header.Get("Content-Length"), fmt.Sprint(len(file)); got != want {
1559 t.Fatalf("%v: got Content-Length %q, want %q", method, got, want)
1564 func TestFileServerFS(t *testing.T) {
1565 filename := "index.html"
1566 contents := []byte("index.html says hello")
1567 fsys := fstest.MapFS{
1568 filename: {Data: contents},
1570 ts := newClientServerTest(t, http1Mode, FileServerFS(fsys)).ts
1573 res, err := ts.Client().Get(ts.URL + "/" + filename)
1577 b, err := io.ReadAll(res.Body)
1579 t.Fatal("reading Body:", err)
1581 if s := string(b); s != string(contents) {
1582 t.Errorf("for path %q got %q, want %q", filename, s, contents)
1587 func TestServeFileFS(t *testing.T) {
1588 filename := "index.html"
1589 contents := []byte("index.html says hello")
1590 fsys := fstest.MapFS{
1591 filename: {Data: contents},
1593 ts := newClientServerTest(t, http1Mode, HandlerFunc(func(w ResponseWriter, r *Request) {
1594 ServeFileFS(w, r, fsys, filename)
1598 res, err := ts.Client().Get(ts.URL + "/" + filename)
1602 b, err := io.ReadAll(res.Body)
1604 t.Fatal("reading Body:", err)
1606 if s := string(b); s != string(contents) {
1607 t.Errorf("for path %q got %q, want %q", filename, s, contents)