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.
35 testFile = "testdata/file"
39 type wantRange struct {
40 start, end int64 // range [start,end)
43 var ServeFileRangeTests = []struct {
48 {r: "", code: StatusOK},
49 {r: "bytes=0-4", code: StatusPartialContent, ranges: []wantRange{{0, 5}}},
50 {r: "bytes=2-", code: StatusPartialContent, ranges: []wantRange{{2, testFileLen}}},
51 {r: "bytes=-5", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 5, testFileLen}}},
52 {r: "bytes=3-7", code: StatusPartialContent, ranges: []wantRange{{3, 8}}},
53 {r: "bytes=0-0,-2", code: StatusPartialContent, ranges: []wantRange{{0, 1}, {testFileLen - 2, testFileLen}}},
54 {r: "bytes=0-1,5-8", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, 9}}},
55 {r: "bytes=0-1,5-", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, testFileLen}}},
56 {r: "bytes=5-1000", code: StatusPartialContent, ranges: []wantRange{{5, testFileLen}}},
57 {r: "bytes=0-,1-,2-,3-,4-", code: StatusOK}, // ignore wasteful range request
58 {r: "bytes=0-9", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen - 1}}},
59 {r: "bytes=0-10", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
60 {r: "bytes=0-11", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
61 {r: "bytes=10-11", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}},
62 {r: "bytes=10-", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}},
63 {r: "bytes=11-", code: StatusRequestedRangeNotSatisfiable},
64 {r: "bytes=11-12", code: StatusRequestedRangeNotSatisfiable},
65 {r: "bytes=12-12", code: StatusRequestedRangeNotSatisfiable},
66 {r: "bytes=11-100", code: StatusRequestedRangeNotSatisfiable},
67 {r: "bytes=12-100", code: StatusRequestedRangeNotSatisfiable},
68 {r: "bytes=100-", code: StatusRequestedRangeNotSatisfiable},
69 {r: "bytes=100-1000", code: StatusRequestedRangeNotSatisfiable},
72 func TestServeFile(t *testing.T) { run(t, testServeFile) }
73 func testServeFile(t *testing.T, mode testMode) {
74 ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
75 ServeFile(w, r, "testdata/file")
81 file, err := os.ReadFile(testFile)
83 t.Fatal("reading file:", err)
86 // set up the Request (re-used for all tests)
88 req.Header = make(Header)
89 if req.URL, err = url.Parse(ts.URL); err != nil {
90 t.Fatal("ParseURL:", err)
93 // Get contents via various methods.
95 // See https://go.dev/issue/59471 for a proposal to limit the set of methods handled.
96 // For now, test the historical behavior.
97 for _, method := range []string{
107 _, body := getBody(t, method, req, c)
108 if !bytes.Equal(body, file) {
109 t.Fatalf("body mismatch for %v request: got %q, want %q", method, body, file)
114 req.Method = MethodHead
115 resp, body := getBody(t, "HEAD", req, c)
117 t.Fatalf("body mismatch for HEAD request: got %q, want empty", body)
119 if got, want := resp.Header.Get("Content-Length"), fmt.Sprint(len(file)); got != want {
120 t.Fatalf("Content-Length mismatch for HEAD request: got %v, want %v", got, want)
124 req.Method = MethodGet
126 for _, rt := range ServeFileRangeTests {
128 req.Header.Set("Range", rt.r)
130 resp, body := getBody(t, fmt.Sprintf("range test %q", rt.r), req, c)
131 if resp.StatusCode != rt.code {
132 t.Errorf("range=%q: StatusCode=%d, want %d", rt.r, resp.StatusCode, rt.code)
134 if rt.code == StatusRequestedRangeNotSatisfiable {
137 wantContentRange := ""
138 if len(rt.ranges) == 1 {
140 wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
142 cr := resp.Header.Get("Content-Range")
143 if cr != wantContentRange {
144 t.Errorf("range=%q: Content-Range = %q, want %q", rt.r, cr, wantContentRange)
146 ct := resp.Header.Get("Content-Type")
147 if len(rt.ranges) == 1 {
149 wantBody := file[rng.start:rng.end]
150 if !bytes.Equal(body, wantBody) {
151 t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
153 if strings.HasPrefix(ct, "multipart/byteranges") {
154 t.Errorf("range=%q content-type = %q; unexpected multipart/byteranges", rt.r, ct)
157 if len(rt.ranges) > 1 {
158 typ, params, err := mime.ParseMediaType(ct)
160 t.Errorf("range=%q content-type = %q; %v", rt.r, ct, err)
163 if typ != "multipart/byteranges" {
164 t.Errorf("range=%q content-type = %q; want multipart/byteranges", rt.r, typ)
167 if params["boundary"] == "" {
168 t.Errorf("range=%q content-type = %q; lacks boundary", rt.r, ct)
171 if g, w := resp.ContentLength, int64(len(body)); g != w {
172 t.Errorf("range=%q Content-Length = %d; want %d", rt.r, g, w)
175 mr := multipart.NewReader(bytes.NewReader(body), params["boundary"])
176 for ri, rng := range rt.ranges {
177 part, err := mr.NextPart()
179 t.Errorf("range=%q, reading part index %d: %v", rt.r, ri, err)
182 wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
183 if g, w := part.Header.Get("Content-Range"), wantContentRange; g != w {
184 t.Errorf("range=%q: part Content-Range = %q; want %q", rt.r, g, w)
186 body, err := io.ReadAll(part)
188 t.Errorf("range=%q, reading part index %d body: %v", rt.r, ri, err)
191 wantBody := file[rng.start:rng.end]
192 if !bytes.Equal(body, wantBody) {
193 t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
196 _, err = mr.NextPart()
198 t.Errorf("range=%q; expected final error io.EOF; got %v", rt.r, err)
204 func TestServeFile_DotDot(t *testing.T) {
209 {"/testdata/file", 200},
218 {"/file/a\\..", 400},
220 for _, tt := range tests {
221 req, err := ReadRequest(bufio.NewReader(strings.NewReader("GET " + tt.req + " HTTP/1.1\r\nHost: foo\r\n\r\n")))
223 t.Errorf("bad request %q: %v", tt.req, err)
226 rec := httptest.NewRecorder()
227 ServeFile(rec, req, "testdata/file")
228 if rec.Code != tt.wantStatus {
229 t.Errorf("for request %q, status = %d; want %d", tt.req, rec.Code, tt.wantStatus)
234 // Tests that this doesn't panic. (Issue 30165)
235 func TestServeFileDirPanicEmptyPath(t *testing.T) {
236 rec := httptest.NewRecorder()
237 req := httptest.NewRequest("GET", "/", nil)
239 ServeFile(rec, req, "testdata")
241 if res.StatusCode != 301 {
242 t.Errorf("code = %v; want 301", res.Status)
246 // Tests that ranges are ignored with serving empty content. (Issue 54794)
247 func TestServeContentWithEmptyContentIgnoreRanges(t *testing.T) {
248 for _, r := range []string{
252 rec := httptest.NewRecorder()
253 req := httptest.NewRequest("GET", "/", nil)
254 req.Header.Set("Range", r)
255 ServeContent(rec, req, "nothing", time.Now(), bytes.NewReader(nil))
257 if res.StatusCode != 200 {
258 t.Errorf("code = %v; want 200", res.Status)
260 bodyLen := rec.Body.Len()
262 t.Errorf("body.Len() = %v; want 0", res.Status)
267 var fsRedirectTestData = []struct {
268 original, redirect string
270 {"/test/index.html", "/test/"},
271 {"/test/testdata", "/test/testdata/"},
272 {"/test/testdata/file/", "/test/testdata/file"},
275 func TestFSRedirect(t *testing.T) { run(t, testFSRedirect) }
276 func testFSRedirect(t *testing.T, mode testMode) {
277 ts := newClientServerTest(t, mode, StripPrefix("/test", FileServer(Dir(".")))).ts
279 for _, data := range fsRedirectTestData {
280 res, err := ts.Client().Get(ts.URL + data.original)
285 if g, e := res.Request.URL.Path, data.redirect; g != e {
286 t.Errorf("redirect from %s: got %s, want %s", data.original, g, e)
291 type testFileSystem struct {
292 open func(name string) (File, error)
295 func (fs *testFileSystem) Open(name string) (File, error) {
299 func TestFileServerCleans(t *testing.T) {
301 ch := make(chan string, 1)
302 fs := FileServer(&testFileSystem{func(name string) (File, error) {
304 return nil, errors.New("file does not exist")
307 reqPath, openArg string
309 {"/foo.txt", "/foo.txt"},
310 {"//foo.txt", "/foo.txt"},
311 {"/../foo.txt", "/foo.txt"},
313 req, _ := NewRequest("GET", "http://example.com", nil)
314 for n, test := range tests {
315 rec := httptest.NewRecorder()
316 req.URL.Path = test.reqPath
317 fs.ServeHTTP(rec, req)
318 if got := <-ch; got != test.openArg {
319 t.Errorf("test %d: got %q, want %q", n, got, test.openArg)
324 func TestFileServerEscapesNames(t *testing.T) { run(t, testFileServerEscapesNames) }
325 func testFileServerEscapesNames(t *testing.T, mode testMode) {
326 const dirListPrefix = "<pre>\n"
327 const dirListSuffix = "\n</pre>\n"
331 {`simple_name`, `<a href="simple_name">simple_name</a>`},
332 {`"'<>&`, `<a href="%22%27%3C%3E&">"'<>&</a>`},
333 {`?foo=bar#baz`, `<a href="%3Ffoo=bar%23baz">?foo=bar#baz</a>`},
334 {`<combo>?foo`, `<a href="%3Ccombo%3E%3Ffoo"><combo>?foo</a>`},
335 {`foo:bar`, `<a href="./foo:bar">foo:bar</a>`},
338 // We put each test file in its own directory in the fakeFS so we can look at it in isolation.
340 for i, test := range tests {
341 testFile := &fakeFileInfo{basename: test.name}
342 fs[fmt.Sprintf("/%d", i)] = &fakeFileInfo{
344 modtime: time.Unix(1000000000, 0).UTC(),
345 ents: []*fakeFileInfo{testFile},
347 fs[fmt.Sprintf("/%d/%s", i, test.name)] = testFile
350 ts := newClientServerTest(t, mode, FileServer(&fs)).ts
351 for i, test := range tests {
352 url := fmt.Sprintf("%s/%d", ts.URL, i)
353 res, err := ts.Client().Get(url)
355 t.Fatalf("test %q: Get: %v", test.name, err)
357 b, err := io.ReadAll(res.Body)
359 t.Fatalf("test %q: read Body: %v", test.name, err)
362 if !strings.HasPrefix(s, dirListPrefix) || !strings.HasSuffix(s, dirListSuffix) {
363 t.Errorf("test %q: listing dir, full output is %q, want prefix %q and suffix %q", test.name, s, dirListPrefix, dirListSuffix)
365 if trimmed := strings.TrimSuffix(strings.TrimPrefix(s, dirListPrefix), dirListSuffix); trimmed != test.escaped {
366 t.Errorf("test %q: listing dir, filename escaped to %q, want %q", test.name, trimmed, test.escaped)
372 func TestFileServerSortsNames(t *testing.T) { run(t, testFileServerSortsNames) }
373 func testFileServerSortsNames(t *testing.T, mode testMode) {
374 const contents = "I am a fake file"
375 dirMod := time.Unix(123, 0).UTC()
376 fileMod := time.Unix(1000000000, 0).UTC()
381 ents: []*fakeFileInfo{
396 ts := newClientServerTest(t, mode, FileServer(&fs)).ts
398 res, err := ts.Client().Get(ts.URL)
400 t.Fatalf("Get: %v", err)
402 defer res.Body.Close()
404 b, err := io.ReadAll(res.Body)
406 t.Fatalf("read Body: %v", err)
409 if !strings.Contains(s, "<a href=\"a\">a</a>\n<a href=\"b\">b</a>") {
410 t.Errorf("output appears to be unsorted:\n%s", s)
414 func mustRemoveAll(dir string) {
415 err := os.RemoveAll(dir)
421 func TestFileServerImplicitLeadingSlash(t *testing.T) { run(t, testFileServerImplicitLeadingSlash) }
422 func testFileServerImplicitLeadingSlash(t *testing.T, mode testMode) {
423 tempDir := t.TempDir()
424 if err := os.WriteFile(filepath.Join(tempDir, "foo.txt"), []byte("Hello world"), 0644); err != nil {
425 t.Fatalf("WriteFile: %v", err)
427 ts := newClientServerTest(t, mode, StripPrefix("/bar/", FileServer(Dir(tempDir)))).ts
428 get := func(suffix string) string {
429 res, err := ts.Client().Get(ts.URL + suffix)
431 t.Fatalf("Get %s: %v", suffix, err)
433 b, err := io.ReadAll(res.Body)
435 t.Fatalf("ReadAll %s: %v", suffix, err)
440 if s := get("/bar/"); !strings.Contains(s, ">foo.txt<") {
441 t.Logf("expected a directory listing with foo.txt, got %q", s)
443 if s := get("/bar/foo.txt"); s != "Hello world" {
444 t.Logf("expected %q, got %q", "Hello world", s)
448 func TestDirJoin(t *testing.T) {
449 if runtime.GOOS == "windows" {
450 t.Skip("skipping test on windows")
452 wfi, err := os.Stat("/etc/hosts")
454 t.Skip("skipping test; no /etc/hosts file")
456 test := func(d Dir, name string) {
457 f, err := d.Open(name)
459 t.Fatalf("open of %s: %v", name, err)
464 t.Fatalf("stat of %s: %v", name, err)
466 if !os.SameFile(gfi, wfi) {
467 t.Errorf("%s got different file", name)
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")
475 test(Dir("/etc"), "../../../../hosts")
477 // Not really directories, but since we use this trick in
478 // ServeFile, test it:
479 test(Dir("/etc/hosts"), "")
480 test(Dir("/etc/hosts"), "/")
481 test(Dir("/etc/hosts"), "../")
484 func TestEmptyDirOpenCWD(t *testing.T) {
485 test := func(d Dir) {
487 f, err := d.Open(name)
489 t.Fatalf("open of %s: %v", name, err)
498 func TestServeFileContentType(t *testing.T) { run(t, testServeFileContentType) }
499 func testServeFileContentType(t *testing.T, mode testMode) {
500 const ctype = "icecream/chocolate"
501 ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
502 switch r.FormValue("override") {
504 w.Header().Set("Content-Type", ctype)
506 // Explicitly inhibit sniffing.
507 w.Header()["Content-Type"] = []string{}
509 ServeFile(w, r, "testdata/file")
511 get := func(override string, want []string) {
512 resp, err := ts.Client().Get(ts.URL + "?override=" + override)
516 if h := resp.Header["Content-Type"]; !reflect.DeepEqual(h, want) {
517 t.Errorf("Content-Type mismatch: got %v, want %v", h, want)
521 get("0", []string{"text/plain; charset=utf-8"})
522 get("1", []string{ctype})
526 func TestServeFileMimeType(t *testing.T) { run(t, testServeFileMimeType) }
527 func testServeFileMimeType(t *testing.T, mode testMode) {
528 ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
529 ServeFile(w, r, "testdata/style.css")
531 resp, err := ts.Client().Get(ts.URL)
536 want := "text/css; charset=utf-8"
537 if h := resp.Header.Get("Content-Type"); h != want {
538 t.Errorf("Content-Type mismatch: got %q, want %q", h, want)
542 func TestServeFileFromCWD(t *testing.T) { run(t, testServeFileFromCWD) }
543 func testServeFileFromCWD(t *testing.T, mode testMode) {
544 ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
545 ServeFile(w, r, "fs_test.go")
547 r, err := ts.Client().Get(ts.URL)
552 if r.StatusCode != 200 {
553 t.Fatalf("expected 200 OK, got %s", r.Status)
558 func TestServeDirWithoutTrailingSlash(t *testing.T) { run(t, testServeDirWithoutTrailingSlash) }
559 func testServeDirWithoutTrailingSlash(t *testing.T, mode testMode) {
561 ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
564 r, err := ts.Client().Get(ts.URL + "/testdata")
569 if g := r.Request.URL.Path; g != e {
570 t.Errorf("got %s, want %s", g, e)
574 // Tests that ServeFile adds a Content-Length even if a Content-Encoding is
576 func TestServeFileWithContentEncoding(t *testing.T) { run(t, testServeFileWithContentEncoding) }
577 func testServeFileWithContentEncoding(t *testing.T, mode testMode) {
578 cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
579 w.Header().Set("Content-Encoding", "foo")
580 ServeFile(w, r, "testdata/file")
582 // Because the testdata is so small, it would fit in
583 // both the h1 and h2 Server's write buffers. For h1,
584 // sendfile is used, though, forcing a header flush at
585 // the io.Copy. http2 doesn't do a header flush so
586 // buffers all 11 bytes and then adds its own
587 // Content-Length. To prevent the Server's
588 // Content-Length and test ServeFile only, flush here.
591 resp, err := cst.c.Get(cst.ts.URL)
596 if g, e := resp.ContentLength, int64(11); g != e {
597 t.Errorf("Content-Length mismatch: got %d, want %d", g, e)
601 // Tests that ServeFile does not generate representation metadata when
602 // file has not been modified, as per RFC 7232 section 4.1.
603 func TestServeFileNotModified(t *testing.T) { run(t, testServeFileNotModified) }
604 func testServeFileNotModified(t *testing.T, mode testMode) {
605 cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
606 w.Header().Set("Content-Type", "application/json")
607 w.Header().Set("Content-Encoding", "foo")
608 w.Header().Set("Etag", `"123"`)
609 ServeFile(w, r, "testdata/file")
611 // Because the testdata is so small, it would fit in
612 // both the h1 and h2 Server's write buffers. For h1,
613 // sendfile is used, though, forcing a header flush at
614 // the io.Copy. http2 doesn't do a header flush so
615 // buffers all 11 bytes and then adds its own
616 // Content-Length. To prevent the Server's
617 // Content-Length and test ServeFile only, flush here.
620 req, err := NewRequest("GET", cst.ts.URL, nil)
624 req.Header.Set("If-None-Match", `"123"`)
625 resp, err := cst.c.Do(req)
629 b, err := io.ReadAll(resp.Body)
632 t.Fatal("reading Body:", err)
635 t.Errorf("non-empty body")
637 if g, e := resp.StatusCode, StatusNotModified; g != e {
638 t.Errorf("status mismatch: got %d, want %d", g, e)
640 // HTTP1 transport sets ContentLength to 0.
641 if g, e1, e2 := resp.ContentLength, int64(-1), int64(0); g != e1 && g != e2 {
642 t.Errorf("Content-Length mismatch: got %d, want %d or %d", g, e1, e2)
644 if resp.Header.Get("Content-Type") != "" {
645 t.Errorf("Content-Type present, but it should not be")
647 if resp.Header.Get("Content-Encoding") != "" {
648 t.Errorf("Content-Encoding present, but it should not be")
652 func TestServeIndexHtml(t *testing.T) { run(t, testServeIndexHtml) }
653 func testServeIndexHtml(t *testing.T, mode testMode) {
654 for i := 0; i < 2; i++ {
659 h = FileServer(Dir("."))
662 h = FileServer(FS(os.DirFS(".")))
665 t.Run(name, func(t *testing.T) {
666 const want = "index.html says hello\n"
667 ts := newClientServerTest(t, mode, h).ts
669 for _, path := range []string{"/testdata/", "/testdata/index.html"} {
670 res, err := ts.Client().Get(ts.URL + path)
674 b, err := io.ReadAll(res.Body)
676 t.Fatal("reading Body:", err)
678 if s := string(b); s != want {
679 t.Errorf("for path %q got %q, want %q", path, s, want)
687 func TestServeIndexHtmlFS(t *testing.T) { run(t, testServeIndexHtmlFS) }
688 func testServeIndexHtmlFS(t *testing.T, mode testMode) {
689 const want = "index.html says hello\n"
690 ts := newClientServerTest(t, mode, FileServer(Dir("."))).ts
693 for _, path := range []string{"/testdata/", "/testdata/index.html"} {
694 res, err := ts.Client().Get(ts.URL + path)
698 b, err := io.ReadAll(res.Body)
700 t.Fatal("reading Body:", err)
702 if s := string(b); s != want {
703 t.Errorf("for path %q got %q, want %q", path, s, want)
709 func TestFileServerZeroByte(t *testing.T) { run(t, testFileServerZeroByte) }
710 func testFileServerZeroByte(t *testing.T, mode testMode) {
711 ts := newClientServerTest(t, mode, FileServer(Dir("."))).ts
713 c, err := net.Dial("tcp", ts.Listener.Addr().String())
718 _, err = fmt.Fprintf(c, "GET /..\x00 HTTP/1.0\r\n\r\n")
723 bufr := bufio.NewReader(io.TeeReader(c, &got))
724 res, err := ReadResponse(bufr, nil)
726 t.Fatal("ReadResponse: ", err)
728 if res.StatusCode == 200 {
729 t.Errorf("got status 200; want an error. Body is:\n%s", got.Bytes())
733 func TestFileServerNamesEscape(t *testing.T) { run(t, testFileServerNamesEscape) }
734 func testFileServerNamesEscape(t *testing.T, mode testMode) {
735 ts := newClientServerTest(t, mode, FileServer(Dir("testdata"))).ts
736 for _, path := range []string{
738 "/NUL", // don't read from device files on Windows
740 res, err := ts.Client().Get(ts.URL + path)
745 if res.StatusCode < 400 || res.StatusCode > 599 {
746 t.Errorf("Get(%q): got status %v, want 4xx or 5xx", path, res.StatusCode)
752 type fakeFileInfo struct {
761 func (f *fakeFileInfo) Name() string { return f.basename }
762 func (f *fakeFileInfo) Sys() any { return nil }
763 func (f *fakeFileInfo) ModTime() time.Time { return f.modtime }
764 func (f *fakeFileInfo) IsDir() bool { return f.dir }
765 func (f *fakeFileInfo) Size() int64 { return int64(len(f.contents)) }
766 func (f *fakeFileInfo) Mode() fs.FileMode {
768 return 0755 | fs.ModeDir
773 func (f *fakeFileInfo) String() string {
774 return fs.FormatFileInfo(f)
777 type fakeFile struct {
780 path string // as opened
784 func (f *fakeFile) Close() error { return nil }
785 func (f *fakeFile) Stat() (fs.FileInfo, error) { return f.fi, nil }
786 func (f *fakeFile) Readdir(count int) ([]fs.FileInfo, error) {
788 return nil, fs.ErrInvalid
790 var fis []fs.FileInfo
792 limit := f.entpos + count
793 if count <= 0 || limit > len(f.fi.ents) {
794 limit = len(f.fi.ents)
796 for ; f.entpos < limit; f.entpos++ {
797 fis = append(fis, f.fi.ents[f.entpos])
800 if len(fis) == 0 && count > 0 {
807 type fakeFS map[string]*fakeFileInfo
809 func (fsys fakeFS) Open(name string) (File, error) {
810 name = path.Clean(name)
813 return nil, fs.ErrNotExist
818 return &fakeFile{ReadSeeker: strings.NewReader(f.contents), fi: f, path: name}, nil
821 func TestDirectoryIfNotModified(t *testing.T) { run(t, testDirectoryIfNotModified) }
822 func testDirectoryIfNotModified(t *testing.T, mode testMode) {
823 const indexContents = "I am a fake index.html file"
824 fileMod := time.Unix(1000000000, 0).UTC()
825 fileModStr := fileMod.Format(TimeFormat)
826 dirMod := time.Unix(123, 0).UTC()
827 indexFile := &fakeFileInfo{
828 basename: "index.html",
830 contents: indexContents,
836 ents: []*fakeFileInfo{indexFile},
838 "/index.html": indexFile,
841 ts := newClientServerTest(t, mode, FileServer(fs)).ts
843 res, err := ts.Client().Get(ts.URL)
847 b, err := io.ReadAll(res.Body)
851 if string(b) != indexContents {
852 t.Fatalf("Got body %q; want %q", b, indexContents)
856 lastMod := res.Header.Get("Last-Modified")
857 if lastMod != fileModStr {
858 t.Fatalf("initial Last-Modified = %q; want %q", lastMod, fileModStr)
861 req, _ := NewRequest("GET", ts.URL, nil)
862 req.Header.Set("If-Modified-Since", lastMod)
869 if res.StatusCode != 304 {
870 t.Fatalf("Code after If-Modified-Since request = %v; want 304", res.StatusCode)
874 // Advance the index.html file's modtime, but not the directory's.
875 indexFile.modtime = indexFile.modtime.Add(1 * time.Hour)
881 if res.StatusCode != 200 {
882 t.Fatalf("Code after second If-Modified-Since request = %v; want 200; res is %#v", res.StatusCode, res)
887 func mustStat(t *testing.T, fileName string) fs.FileInfo {
888 fi, err := os.Stat(fileName)
895 func TestServeContent(t *testing.T) { run(t, testServeContent) }
896 func testServeContent(t *testing.T, mode testMode) {
897 type serveParam struct {
900 content io.ReadSeeker
904 servec := make(chan serveParam, 1)
905 ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
908 w.Header().Set("ETag", p.etag)
910 if p.contentType != "" {
911 w.Header().Set("Content-Type", p.contentType)
913 ServeContent(w, r, p.name, p.modtime, p.content)
916 type testCase struct {
917 // One of file or content must be set:
919 content io.ReadSeeker
922 serveETag string // optional
923 serveContentType string // optional
924 reqHeader map[string]string
926 wantContentType string
927 wantContentRange string
930 htmlModTime := mustStat(t, "testdata/index.html").ModTime()
931 tests := map[string]testCase{
932 "no_last_modified": {
933 file: "testdata/style.css",
934 wantContentType: "text/css; charset=utf-8",
937 "with_last_modified": {
938 file: "testdata/index.html",
939 wantContentType: "text/html; charset=utf-8",
940 modtime: htmlModTime,
941 wantLastMod: htmlModTime.UTC().Format(TimeFormat),
944 "not_modified_modtime": {
945 file: "testdata/style.css",
946 serveETag: `"foo"`, // Last-Modified sent only when no ETag
947 modtime: htmlModTime,
948 reqHeader: map[string]string{
949 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
953 "not_modified_modtime_with_contenttype": {
954 file: "testdata/style.css",
955 serveContentType: "text/css", // explicit content type
956 serveETag: `"foo"`, // Last-Modified sent only when no ETag
957 modtime: htmlModTime,
958 reqHeader: map[string]string{
959 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
963 "not_modified_etag": {
964 file: "testdata/style.css",
966 reqHeader: map[string]string{
967 "If-None-Match": `"foo"`,
971 "not_modified_etag_no_seek": {
972 content: panicOnSeek{nil}, // should never be called
973 serveETag: `W/"foo"`, // If-None-Match uses weak ETag comparison
974 reqHeader: map[string]string{
975 "If-None-Match": `"baz", W/"foo"`,
979 "if_none_match_mismatch": {
980 file: "testdata/style.css",
982 reqHeader: map[string]string{
983 "If-None-Match": `"Foo"`,
986 wantContentType: "text/css; charset=utf-8",
988 "if_none_match_malformed": {
989 file: "testdata/style.css",
991 reqHeader: map[string]string{
992 "If-None-Match": `,`,
995 wantContentType: "text/css; charset=utf-8",
998 file: "testdata/style.css",
1000 reqHeader: map[string]string{
1001 "Range": "bytes=0-4",
1003 wantStatus: StatusPartialContent,
1004 wantContentType: "text/css; charset=utf-8",
1005 wantContentRange: "bytes 0-4/8",
1008 file: "testdata/style.css",
1010 reqHeader: map[string]string{
1011 "Range": "bytes=0-4",
1014 wantStatus: StatusPartialContent,
1015 wantContentType: "text/css; charset=utf-8",
1016 wantContentRange: "bytes 0-4/8",
1018 "range_match_weak_etag": {
1019 file: "testdata/style.css",
1021 reqHeader: map[string]string{
1022 "Range": "bytes=0-4",
1023 "If-Range": `W/"A"`,
1026 wantContentType: "text/css; charset=utf-8",
1028 "range_no_overlap": {
1029 file: "testdata/style.css",
1031 reqHeader: map[string]string{
1032 "Range": "bytes=10-20",
1034 wantStatus: StatusRequestedRangeNotSatisfiable,
1035 wantContentType: "text/plain; charset=utf-8",
1036 wantContentRange: "bytes */8",
1038 // An If-Range resource for entity "A", but entity "B" is now current.
1039 // The Range request should be ignored.
1041 file: "testdata/style.css",
1043 reqHeader: map[string]string{
1044 "Range": "bytes=0-4",
1048 wantContentType: "text/css; charset=utf-8",
1050 "range_with_modtime": {
1051 file: "testdata/style.css",
1052 modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 /* nanos */, time.UTC),
1053 reqHeader: map[string]string{
1054 "Range": "bytes=0-4",
1055 "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
1057 wantStatus: StatusPartialContent,
1058 wantContentType: "text/css; charset=utf-8",
1059 wantContentRange: "bytes 0-4/8",
1060 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT",
1062 "range_with_modtime_mismatch": {
1063 file: "testdata/style.css",
1064 modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 /* nanos */, time.UTC),
1065 reqHeader: map[string]string{
1066 "Range": "bytes=0-4",
1067 "If-Range": "Wed, 25 Jun 2014 17:12:19 GMT",
1069 wantStatus: StatusOK,
1070 wantContentType: "text/css; charset=utf-8",
1071 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT",
1073 "range_with_modtime_nanos": {
1074 file: "testdata/style.css",
1075 modtime: time.Date(2014, 6, 25, 17, 12, 18, 123 /* nanos */, time.UTC),
1076 reqHeader: map[string]string{
1077 "Range": "bytes=0-4",
1078 "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
1080 wantStatus: StatusPartialContent,
1081 wantContentType: "text/css; charset=utf-8",
1082 wantContentRange: "bytes 0-4/8",
1083 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT",
1085 "unix_zero_modtime": {
1086 content: strings.NewReader("<html>foo"),
1087 modtime: time.Unix(0, 0),
1088 wantStatus: StatusOK,
1089 wantContentType: "text/html; charset=utf-8",
1091 "ifmatch_matches": {
1092 file: "testdata/style.css",
1094 reqHeader: map[string]string{
1095 "If-Match": `"Z", "A"`,
1098 wantContentType: "text/css; charset=utf-8",
1101 file: "testdata/style.css",
1103 reqHeader: map[string]string{
1107 wantContentType: "text/css; charset=utf-8",
1110 file: "testdata/style.css",
1112 reqHeader: map[string]string{
1117 "ifmatch_fails_on_weak_etag": {
1118 file: "testdata/style.css",
1120 reqHeader: map[string]string{
1121 "If-Match": `W/"A"`,
1125 "if_unmodified_since_true": {
1126 file: "testdata/style.css",
1127 modtime: htmlModTime,
1128 reqHeader: map[string]string{
1129 "If-Unmodified-Since": htmlModTime.UTC().Format(TimeFormat),
1132 wantContentType: "text/css; charset=utf-8",
1133 wantLastMod: htmlModTime.UTC().Format(TimeFormat),
1135 "if_unmodified_since_false": {
1136 file: "testdata/style.css",
1137 modtime: htmlModTime,
1138 reqHeader: map[string]string{
1139 "If-Unmodified-Since": htmlModTime.Add(-2 * time.Second).UTC().Format(TimeFormat),
1142 wantLastMod: htmlModTime.UTC().Format(TimeFormat),
1145 for testName, tt := range tests {
1146 var content io.ReadSeeker
1148 f, err := os.Open(tt.file)
1150 t.Fatalf("test %q: %v", testName, err)
1155 content = tt.content
1157 for _, method := range []string{"GET", "HEAD"} {
1158 //restore content in case it is consumed by previous method
1159 if content, ok := content.(*strings.Reader); ok {
1160 content.Seek(0, io.SeekStart)
1163 servec <- serveParam{
1164 name: filepath.Base(tt.file),
1166 modtime: tt.modtime,
1168 contentType: tt.serveContentType,
1170 req, err := NewRequest(method, ts.URL, nil)
1174 for k, v := range tt.reqHeader {
1175 req.Header.Set(k, v)
1179 res, err := c.Do(req)
1183 io.Copy(io.Discard, res.Body)
1185 if res.StatusCode != tt.wantStatus {
1186 t.Errorf("test %q using %q: got status = %d; want %d", testName, method, res.StatusCode, tt.wantStatus)
1188 if g, e := res.Header.Get("Content-Type"), tt.wantContentType; g != e {
1189 t.Errorf("test %q using %q: got content-type = %q, want %q", testName, method, g, e)
1191 if g, e := res.Header.Get("Content-Range"), tt.wantContentRange; g != e {
1192 t.Errorf("test %q using %q: got content-range = %q, want %q", testName, method, g, e)
1194 if g, e := res.Header.Get("Last-Modified"), tt.wantLastMod; g != e {
1195 t.Errorf("test %q using %q: got last-modified = %q, want %q", testName, method, g, e)
1202 func TestServerFileStatError(t *testing.T) {
1203 rec := httptest.NewRecorder()
1204 r, _ := NewRequest("GET", "http://foo/", nil)
1207 fs := issue12991FS{}
1208 ExportServeFile(rec, r, fs, name, redirect)
1209 if body := rec.Body.String(); !strings.Contains(body, "403") || !strings.Contains(body, "Forbidden") {
1210 t.Errorf("wanted 403 forbidden message; got: %s", body)
1214 type issue12991FS struct{}
1216 func (issue12991FS) Open(string) (File, error) { return issue12991File{}, nil }
1218 type issue12991File struct{ File }
1220 func (issue12991File) Stat() (fs.FileInfo, error) { return nil, fs.ErrPermission }
1221 func (issue12991File) Close() error { return nil }
1223 func TestServeContentErrorMessages(t *testing.T) { run(t, testServeContentErrorMessages) }
1224 func testServeContentErrorMessages(t *testing.T, mode testMode) {
1226 "/500": &fakeFileInfo{
1227 err: errors.New("random error"),
1229 "/403": &fakeFileInfo{
1230 err: &fs.PathError{Err: fs.ErrPermission},
1233 ts := newClientServerTest(t, mode, FileServer(fs)).ts
1235 for _, code := range []int{403, 404, 500} {
1236 res, err := c.Get(fmt.Sprintf("%s/%d", ts.URL, code))
1238 t.Errorf("Error fetching /%d: %v", code, err)
1241 if res.StatusCode != code {
1242 t.Errorf("For /%d, status code = %d; want %d", code, res.StatusCode, code)
1248 // verifies that sendfile is being used on Linux
1249 func TestLinuxSendfile(t *testing.T) {
1252 if runtime.GOOS != "linux" {
1253 t.Skip("skipping; linux-only test")
1255 if _, err := exec.LookPath("strace"); err != nil {
1256 t.Skip("skipping; strace not found in path")
1259 ln, err := net.Listen("tcp", "127.0.0.1:0")
1263 lnf, err := ln.(*net.TCPListener).File()
1269 // Attempt to run strace, and skip on failure - this test requires SYS_PTRACE.
1270 if err := testenv.Command(t, "strace", "-f", "-q", os.Args[0], "-test.run=^$").Run(); err != nil {
1271 t.Skipf("skipping; failed to run strace: %v", err)
1274 filename := fmt.Sprintf("1kb-%d", os.Getpid())
1275 filepath := path.Join(os.TempDir(), filename)
1277 if err := os.WriteFile(filepath, bytes.Repeat([]byte{'a'}, 1<<10), 0755); err != nil {
1280 defer os.Remove(filepath)
1282 var buf strings.Builder
1283 child := testenv.Command(t, "strace", "-f", "-q", os.Args[0], "-test.run=^TestLinuxSendfileChild$")
1284 child.ExtraFiles = append(child.ExtraFiles, lnf)
1285 child.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...)
1288 if err := child.Start(); err != nil {
1289 t.Skipf("skipping; failed to start straced child: %v", err)
1292 res, err := Get(fmt.Sprintf("http://%s/%s", ln.Addr(), filename))
1294 t.Fatalf("http client error: %v", err)
1296 _, err = io.Copy(io.Discard, res.Body)
1298 t.Fatalf("client body read error: %v", err)
1302 // Force child to exit cleanly.
1303 Post(fmt.Sprintf("http://%s/quit", ln.Addr()), "", nil)
1306 rx := regexp.MustCompile(`\b(n64:)?sendfile(64)?\(`)
1308 if !rx.MatchString(out) {
1309 t.Errorf("no sendfile system call found in:\n%s", out)
1313 func getBody(t *testing.T, testName string, req Request, client *Client) (*Response, []byte) {
1314 r, err := client.Do(&req)
1316 t.Fatalf("%s: for URL %q, send error: %v", testName, req.URL.String(), err)
1318 b, err := io.ReadAll(r.Body)
1320 t.Fatalf("%s: for URL %q, reading body: %v", testName, req.URL.String(), err)
1325 // TestLinuxSendfileChild isn't a real test. It's used as a helper process
1326 // for TestLinuxSendfile.
1327 func TestLinuxSendfileChild(*testing.T) {
1328 if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
1332 fd3 := os.NewFile(3, "ephemeral-port-listener")
1333 ln, err := net.FileListener(fd3)
1337 mux := NewServeMux()
1338 mux.Handle("/", FileServer(Dir(os.TempDir())))
1339 mux.HandleFunc("/quit", func(ResponseWriter, *Request) {
1342 s := &Server{Handler: mux}
1349 // Issues 18984, 49552: tests that requests for paths beyond files return not-found errors
1350 func TestFileServerNotDirError(t *testing.T) {
1351 run(t, func(t *testing.T, mode testMode) {
1352 t.Run("Dir", func(t *testing.T) {
1353 testFileServerNotDirError(t, mode, func(path string) FileSystem { return Dir(path) })
1355 t.Run("FS", func(t *testing.T) {
1356 testFileServerNotDirError(t, mode, func(path string) FileSystem { return FS(os.DirFS(path)) })
1361 func testFileServerNotDirError(t *testing.T, mode testMode, newfs func(string) FileSystem) {
1362 ts := newClientServerTest(t, mode, FileServer(newfs("testdata"))).ts
1364 res, err := ts.Client().Get(ts.URL + "/index.html/not-a-file")
1369 if res.StatusCode != 404 {
1370 t.Errorf("StatusCode = %v; want 404", res.StatusCode)
1373 test := func(name string, fsys FileSystem) {
1374 t.Run(name, func(t *testing.T) {
1375 _, err = fsys.Open("/index.html/not-a-file")
1377 t.Fatal("err == nil; want != nil")
1379 if !errors.Is(err, fs.ErrNotExist) {
1380 t.Errorf("err = %v; errors.Is(err, fs.ErrNotExist) = %v; want true", err,
1381 errors.Is(err, fs.ErrNotExist))
1384 _, err = fsys.Open("/index.html/not-a-dir/not-a-file")
1386 t.Fatal("err == nil; want != nil")
1388 if !errors.Is(err, fs.ErrNotExist) {
1389 t.Errorf("err = %v; errors.Is(err, fs.ErrNotExist) = %v; want true", err,
1390 errors.Is(err, fs.ErrNotExist))
1395 absPath, err := filepath.Abs("testdata")
1397 t.Fatal("get abs path:", err)
1400 test("RelativePath", newfs("testdata"))
1401 test("AbsolutePath", newfs(absPath))
1404 func TestFileServerCleanPath(t *testing.T) {
1410 {"/", 200, []string{"/", "/index.html"}},
1411 {"/dir", 301, []string{"/dir"}},
1412 {"/dir/", 200, []string{"/dir", "/dir/index.html"}},
1414 for _, tt := range tests {
1416 rr := httptest.NewRecorder()
1417 req, _ := NewRequest("GET", "http://foo.localhost"+tt.path, nil)
1418 FileServer(fileServerCleanPathDir{&log}).ServeHTTP(rr, req)
1419 if !reflect.DeepEqual(log, tt.wantOpen) {
1420 t.Logf("For %s: Opens = %q; want %q", tt.path, log, tt.wantOpen)
1422 if rr.Code != tt.wantCode {
1423 t.Logf("For %s: Response code = %d; want %d", tt.path, rr.Code, tt.wantCode)
1428 type fileServerCleanPathDir struct {
1432 func (d fileServerCleanPathDir) Open(path string) (File, error) {
1433 *(d.log) = append(*(d.log), path)
1434 if path == "/" || path == "/dir" || path == "/dir/" {
1435 // Just return back something that's a directory.
1436 return Dir(".").Open(".")
1438 return nil, fs.ErrNotExist
1441 type panicOnSeek struct{ io.ReadSeeker }
1443 func Test_scanETag(t *testing.T) {
1449 {`W/"etag-1"`, `W/"etag-1"`, ""},
1450 {`"etag-2"`, `"etag-2"`, ""},
1451 {`"etag-1", "etag-2"`, `"etag-1"`, `, "etag-2"`},
1454 {`W/"truc`, "", ""},
1455 {`w/"case-sensitive"`, "", ""},
1456 {`"spaced etag"`, "", ""},
1458 for _, test := range tests {
1459 etag, remain := ExportScanETag(test.in)
1460 if etag != test.wantETag || remain != test.wantRemain {
1461 t.Errorf("scanETag(%q)=%q %q, want %q %q", test.in, etag, remain, test.wantETag, test.wantRemain)
1466 // Issue 40940: Ensure that we only accept non-negative suffix-lengths
1467 // in "Range": "bytes=-N", and should reject "bytes=--2".
1468 func TestServeFileRejectsInvalidSuffixLengths(t *testing.T) {
1469 run(t, testServeFileRejectsInvalidSuffixLengths, []testMode{http1Mode, https1Mode, http2Mode})
1471 func testServeFileRejectsInvalidSuffixLengths(t *testing.T, mode testMode) {
1472 cst := newClientServerTest(t, mode, FileServer(Dir("testdata"))).ts
1479 {"bytes=--6", 416, "invalid range\n"},
1480 {"bytes=--0", 416, "invalid range\n"},
1481 {"bytes=---0", 416, "invalid range\n"},
1482 {"bytes=-6", 206, "hello\n"},
1483 {"bytes=6-", 206, "html says hello\n"},
1484 {"bytes=-6-", 416, "invalid range\n"},
1485 {"bytes=-0", 206, ""},
1486 {"bytes=", 200, "index.html says hello\n"},
1489 for _, tt := range tests {
1491 t.Run(tt.r, func(t *testing.T) {
1492 req, err := NewRequest("GET", cst.URL+"/index.html", nil)
1496 req.Header.Set("Range", tt.r)
1497 res, err := cst.Client().Do(req)
1501 if g, w := res.StatusCode, tt.wantCode; g != w {
1502 t.Errorf("StatusCode mismatch: got %d want %d", g, w)
1504 slurp, err := io.ReadAll(res.Body)
1509 if g, w := string(slurp), tt.wantBody; g != w {
1510 t.Fatalf("Content mismatch:\nGot: %q\nWant: %q", g, w)
1516 func TestFileServerMethods(t *testing.T) {
1517 run(t, testFileServerMethods)
1519 func testFileServerMethods(t *testing.T, mode testMode) {
1520 ts := newClientServerTest(t, mode, FileServer(Dir("testdata"))).ts
1522 file, err := os.ReadFile(testFile)
1524 t.Fatal("reading file:", err)
1527 // Get contents via various methods.
1529 // See https://go.dev/issue/59471 for a proposal to limit the set of methods handled.
1530 // For now, test the historical behavior.
1531 for _, method := range []string{
1541 req, _ := NewRequest(method, ts.URL+"/file", nil)
1543 res, err := ts.Client().Do(req)
1547 body, err := io.ReadAll(res.Body)
1553 if method == MethodHead {
1556 if !bytes.Equal(body, wantBody) {
1557 t.Fatalf("%v: got body %q, want %q", method, body, wantBody)
1559 if got, want := res.Header.Get("Content-Length"), fmt.Sprint(len(file)); got != want {
1560 t.Fatalf("%v: got Content-Length %q, want %q", method, got, want)
1565 func TestFileServerFS(t *testing.T) {
1566 filename := "index.html"
1567 contents := []byte("index.html says hello")
1568 fsys := fstest.MapFS{
1569 filename: {Data: contents},
1571 ts := newClientServerTest(t, http1Mode, FileServerFS(fsys)).ts
1574 res, err := ts.Client().Get(ts.URL + "/" + filename)
1578 b, err := io.ReadAll(res.Body)
1580 t.Fatal("reading Body:", err)
1582 if s := string(b); s != string(contents) {
1583 t.Errorf("for path %q got %q, want %q", filename, s, contents)
1588 func TestServeFileFS(t *testing.T) {
1589 filename := "index.html"
1590 contents := []byte("index.html says hello")
1591 fsys := fstest.MapFS{
1592 filename: {Data: contents},
1594 ts := newClientServerTest(t, http1Mode, HandlerFunc(func(w ResponseWriter, r *Request) {
1595 ServeFileFS(w, r, fsys, filename)
1599 res, err := ts.Client().Get(ts.URL + "/" + filename)
1603 b, err := io.ReadAll(res.Body)
1605 t.Fatal("reading Body:", err)
1607 if s := string(b); s != string(contents) {
1608 t.Errorf("for path %q got %q, want %q", filename, s, contents)