Add LOOP_VINFO_MAX_VECT_FACTOR
[official-gcc.git] / libgo / go / net / http / fs_test.go
blobbba5682115695ab57e01810b604e9d49aa240bdf
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()
78 var err error
80 file, err := ioutil.ReadFile(testFile)
81 if err != nil {
82 t.Fatal("reading file:", err)
85 // set up the Request (re-used for all tests)
86 var req Request
87 req.Header = make(Header)
88 if req.URL, err = url.Parse(ts.URL); err != nil {
89 t.Fatal("ParseURL:", err)
91 req.Method = "GET"
93 // straight GET
94 _, body := getBody(t, "straight get", req)
95 if !bytes.Equal(body, file) {
96 t.Fatalf("body mismatch: got %q, want %q", body, file)
99 // Range tests
100 Cases:
101 for _, rt := range ServeFileRangeTests {
102 if rt.r != "" {
103 req.Header.Set("Range", rt.r)
105 resp, body := getBody(t, fmt.Sprintf("range test %q", rt.r), req)
106 if resp.StatusCode != rt.code {
107 t.Errorf("range=%q: StatusCode=%d, want %d", rt.r, resp.StatusCode, rt.code)
109 if rt.code == StatusRequestedRangeNotSatisfiable {
110 continue
112 wantContentRange := ""
113 if len(rt.ranges) == 1 {
114 rng := rt.ranges[0]
115 wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
117 cr := resp.Header.Get("Content-Range")
118 if cr != wantContentRange {
119 t.Errorf("range=%q: Content-Range = %q, want %q", rt.r, cr, wantContentRange)
121 ct := resp.Header.Get("Content-Type")
122 if len(rt.ranges) == 1 {
123 rng := rt.ranges[0]
124 wantBody := file[rng.start:rng.end]
125 if !bytes.Equal(body, wantBody) {
126 t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
128 if strings.HasPrefix(ct, "multipart/byteranges") {
129 t.Errorf("range=%q content-type = %q; unexpected multipart/byteranges", rt.r, ct)
132 if len(rt.ranges) > 1 {
133 typ, params, err := mime.ParseMediaType(ct)
134 if err != nil {
135 t.Errorf("range=%q content-type = %q; %v", rt.r, ct, err)
136 continue
138 if typ != "multipart/byteranges" {
139 t.Errorf("range=%q content-type = %q; want multipart/byteranges", rt.r, typ)
140 continue
142 if params["boundary"] == "" {
143 t.Errorf("range=%q content-type = %q; lacks boundary", rt.r, ct)
144 continue
146 if g, w := resp.ContentLength, int64(len(body)); g != w {
147 t.Errorf("range=%q Content-Length = %d; want %d", rt.r, g, w)
148 continue
150 mr := multipart.NewReader(bytes.NewReader(body), params["boundary"])
151 for ri, rng := range rt.ranges {
152 part, err := mr.NextPart()
153 if err != nil {
154 t.Errorf("range=%q, reading part index %d: %v", rt.r, ri, err)
155 continue Cases
157 wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
158 if g, w := part.Header.Get("Content-Range"), wantContentRange; g != w {
159 t.Errorf("range=%q: part Content-Range = %q; want %q", rt.r, g, w)
161 body, err := ioutil.ReadAll(part)
162 if err != nil {
163 t.Errorf("range=%q, reading part index %d body: %v", rt.r, ri, err)
164 continue Cases
166 wantBody := file[rng.start:rng.end]
167 if !bytes.Equal(body, wantBody) {
168 t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
171 _, err = mr.NextPart()
172 if err != io.EOF {
173 t.Errorf("range=%q; expected final error io.EOF; got %v", rt.r, err)
179 func TestServeFile_DotDot(t *testing.T) {
180 tests := []struct {
181 req string
182 wantStatus int
184 {"/testdata/file", 200},
185 {"/../file", 400},
186 {"/..", 400},
187 {"/../", 400},
188 {"/../foo", 400},
189 {"/..\\foo", 400},
190 {"/file/a", 200},
191 {"/file/a..", 200},
192 {"/file/a/..", 400},
193 {"/file/a\\..", 400},
195 for _, tt := range tests {
196 req, err := ReadRequest(bufio.NewReader(strings.NewReader("GET " + tt.req + " HTTP/1.1\r\nHost: foo\r\n\r\n")))
197 if err != nil {
198 t.Errorf("bad request %q: %v", tt.req, err)
199 continue
201 rec := httptest.NewRecorder()
202 ServeFile(rec, req, "testdata/file")
203 if rec.Code != tt.wantStatus {
204 t.Errorf("for request %q, status = %d; want %d", tt.req, rec.Code, tt.wantStatus)
209 var fsRedirectTestData = []struct {
210 original, redirect string
212 {"/test/index.html", "/test/"},
213 {"/test/testdata", "/test/testdata/"},
214 {"/test/testdata/file/", "/test/testdata/file"},
217 func TestFSRedirect(t *testing.T) {
218 defer afterTest(t)
219 ts := httptest.NewServer(StripPrefix("/test", FileServer(Dir("."))))
220 defer ts.Close()
222 for _, data := range fsRedirectTestData {
223 res, err := Get(ts.URL + data.original)
224 if err != nil {
225 t.Fatal(err)
227 res.Body.Close()
228 if g, e := res.Request.URL.Path, data.redirect; g != e {
229 t.Errorf("redirect from %s: got %s, want %s", data.original, g, e)
234 type testFileSystem struct {
235 open func(name string) (File, error)
238 func (fs *testFileSystem) Open(name string) (File, error) {
239 return fs.open(name)
242 func TestFileServerCleans(t *testing.T) {
243 defer afterTest(t)
244 ch := make(chan string, 1)
245 fs := FileServer(&testFileSystem{func(name string) (File, error) {
246 ch <- name
247 return nil, errors.New("file does not exist")
249 tests := []struct {
250 reqPath, openArg string
252 {"/foo.txt", "/foo.txt"},
253 {"//foo.txt", "/foo.txt"},
254 {"/../foo.txt", "/foo.txt"},
256 req, _ := NewRequest("GET", "http://example.com", nil)
257 for n, test := range tests {
258 rec := httptest.NewRecorder()
259 req.URL.Path = test.reqPath
260 fs.ServeHTTP(rec, req)
261 if got := <-ch; got != test.openArg {
262 t.Errorf("test %d: got %q, want %q", n, got, test.openArg)
267 func TestFileServerEscapesNames(t *testing.T) {
268 defer afterTest(t)
269 const dirListPrefix = "<pre>\n"
270 const dirListSuffix = "\n</pre>\n"
271 tests := []struct {
272 name, escaped string
274 {`simple_name`, `<a href="simple_name">simple_name</a>`},
275 {`"'<>&`, `<a href="%22%27%3C%3E&">&#34;&#39;&lt;&gt;&amp;</a>`},
276 {`?foo=bar#baz`, `<a href="%3Ffoo=bar%23baz">?foo=bar#baz</a>`},
277 {`<combo>?foo`, `<a href="%3Ccombo%3E%3Ffoo">&lt;combo&gt;?foo</a>`},
278 {`foo:bar`, `<a href="./foo:bar">foo:bar</a>`},
281 // We put each test file in its own directory in the fakeFS so we can look at it in isolation.
282 fs := make(fakeFS)
283 for i, test := range tests {
284 testFile := &fakeFileInfo{basename: test.name}
285 fs[fmt.Sprintf("/%d", i)] = &fakeFileInfo{
286 dir: true,
287 modtime: time.Unix(1000000000, 0).UTC(),
288 ents: []*fakeFileInfo{testFile},
290 fs[fmt.Sprintf("/%d/%s", i, test.name)] = testFile
293 ts := httptest.NewServer(FileServer(&fs))
294 defer ts.Close()
295 for i, test := range tests {
296 url := fmt.Sprintf("%s/%d", ts.URL, i)
297 res, err := Get(url)
298 if err != nil {
299 t.Fatalf("test %q: Get: %v", test.name, err)
301 b, err := ioutil.ReadAll(res.Body)
302 if err != nil {
303 t.Fatalf("test %q: read Body: %v", test.name, err)
305 s := string(b)
306 if !strings.HasPrefix(s, dirListPrefix) || !strings.HasSuffix(s, dirListSuffix) {
307 t.Errorf("test %q: listing dir, full output is %q, want prefix %q and suffix %q", test.name, s, dirListPrefix, dirListSuffix)
309 if trimmed := strings.TrimSuffix(strings.TrimPrefix(s, dirListPrefix), dirListSuffix); trimmed != test.escaped {
310 t.Errorf("test %q: listing dir, filename escaped to %q, want %q", test.name, trimmed, test.escaped)
312 res.Body.Close()
316 func TestFileServerSortsNames(t *testing.T) {
317 defer afterTest(t)
318 const contents = "I am a fake file"
319 dirMod := time.Unix(123, 0).UTC()
320 fileMod := time.Unix(1000000000, 0).UTC()
321 fs := fakeFS{
322 "/": &fakeFileInfo{
323 dir: true,
324 modtime: dirMod,
325 ents: []*fakeFileInfo{
327 basename: "b",
328 modtime: fileMod,
329 contents: contents,
332 basename: "a",
333 modtime: fileMod,
334 contents: contents,
340 ts := httptest.NewServer(FileServer(&fs))
341 defer ts.Close()
343 res, err := Get(ts.URL)
344 if err != nil {
345 t.Fatalf("Get: %v", err)
347 defer res.Body.Close()
349 b, err := ioutil.ReadAll(res.Body)
350 if err != nil {
351 t.Fatalf("read Body: %v", err)
353 s := string(b)
354 if !strings.Contains(s, "<a href=\"a\">a</a>\n<a href=\"b\">b</a>") {
355 t.Errorf("output appears to be unsorted:\n%s", s)
359 func mustRemoveAll(dir string) {
360 err := os.RemoveAll(dir)
361 if err != nil {
362 panic(err)
366 func TestFileServerImplicitLeadingSlash(t *testing.T) {
367 defer afterTest(t)
368 tempDir, err := ioutil.TempDir("", "")
369 if err != nil {
370 t.Fatalf("TempDir: %v", err)
372 defer mustRemoveAll(tempDir)
373 if err := ioutil.WriteFile(filepath.Join(tempDir, "foo.txt"), []byte("Hello world"), 0644); err != nil {
374 t.Fatalf("WriteFile: %v", err)
376 ts := httptest.NewServer(StripPrefix("/bar/", FileServer(Dir(tempDir))))
377 defer ts.Close()
378 get := func(suffix string) string {
379 res, err := Get(ts.URL + suffix)
380 if err != nil {
381 t.Fatalf("Get %s: %v", suffix, err)
383 b, err := ioutil.ReadAll(res.Body)
384 if err != nil {
385 t.Fatalf("ReadAll %s: %v", suffix, err)
387 res.Body.Close()
388 return string(b)
390 if s := get("/bar/"); !strings.Contains(s, ">foo.txt<") {
391 t.Logf("expected a directory listing with foo.txt, got %q", s)
393 if s := get("/bar/foo.txt"); s != "Hello world" {
394 t.Logf("expected %q, got %q", "Hello world", s)
398 func TestDirJoin(t *testing.T) {
399 if runtime.GOOS == "windows" {
400 t.Skip("skipping test on windows")
402 wfi, err := os.Stat("/etc/hosts")
403 if err != nil {
404 t.Skip("skipping test; no /etc/hosts file")
406 test := func(d Dir, name string) {
407 f, err := d.Open(name)
408 if err != nil {
409 t.Fatalf("open of %s: %v", name, err)
411 defer f.Close()
412 gfi, err := f.Stat()
413 if err != nil {
414 t.Fatalf("stat of %s: %v", name, err)
416 if !os.SameFile(gfi, wfi) {
417 t.Errorf("%s got different file", name)
420 test(Dir("/etc/"), "/hosts")
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")
427 // Not really directories, but since we use this trick in
428 // ServeFile, test it:
429 test(Dir("/etc/hosts"), "")
430 test(Dir("/etc/hosts"), "/")
431 test(Dir("/etc/hosts"), "../")
434 func TestEmptyDirOpenCWD(t *testing.T) {
435 test := func(d Dir) {
436 name := "fs_test.go"
437 f, err := d.Open(name)
438 if err != nil {
439 t.Fatalf("open of %s: %v", name, err)
441 defer f.Close()
443 test(Dir(""))
444 test(Dir("."))
445 test(Dir("./"))
448 func TestServeFileContentType(t *testing.T) {
449 defer afterTest(t)
450 const ctype = "icecream/chocolate"
451 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
452 switch r.FormValue("override") {
453 case "1":
454 w.Header().Set("Content-Type", ctype)
455 case "2":
456 // Explicitly inhibit sniffing.
457 w.Header()["Content-Type"] = []string{}
459 ServeFile(w, r, "testdata/file")
461 defer ts.Close()
462 get := func(override string, want []string) {
463 resp, err := Get(ts.URL + "?override=" + override)
464 if err != nil {
465 t.Fatal(err)
467 if h := resp.Header["Content-Type"]; !reflect.DeepEqual(h, want) {
468 t.Errorf("Content-Type mismatch: got %v, want %v", h, want)
470 resp.Body.Close()
472 get("0", []string{"text/plain; charset=utf-8"})
473 get("1", []string{ctype})
474 get("2", nil)
477 func TestServeFileMimeType(t *testing.T) {
478 defer afterTest(t)
479 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
480 ServeFile(w, r, "testdata/style.css")
482 defer ts.Close()
483 resp, err := Get(ts.URL)
484 if err != nil {
485 t.Fatal(err)
487 resp.Body.Close()
488 want := "text/css; charset=utf-8"
489 if h := resp.Header.Get("Content-Type"); h != want {
490 t.Errorf("Content-Type mismatch: got %q, want %q", h, want)
494 func TestServeFileFromCWD(t *testing.T) {
495 defer afterTest(t)
496 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
497 ServeFile(w, r, "fs_test.go")
499 defer ts.Close()
500 r, err := Get(ts.URL)
501 if err != nil {
502 t.Fatal(err)
504 r.Body.Close()
505 if r.StatusCode != 200 {
506 t.Fatalf("expected 200 OK, got %s", r.Status)
510 // Issue 13996
511 func TestServeDirWithoutTrailingSlash(t *testing.T) {
512 e := "/testdata/"
513 defer afterTest(t)
514 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
515 ServeFile(w, r, ".")
517 defer ts.Close()
518 r, err := Get(ts.URL + "/testdata")
519 if err != nil {
520 t.Fatal(err)
522 r.Body.Close()
523 if g := r.Request.URL.Path; g != e {
524 t.Errorf("got %s, want %s", g, e)
528 // Tests that ServeFile doesn't add a Content-Length if a Content-Encoding is
529 // specified.
530 func TestServeFileWithContentEncoding_h1(t *testing.T) { testServeFileWithContentEncoding(t, h1Mode) }
531 func TestServeFileWithContentEncoding_h2(t *testing.T) { testServeFileWithContentEncoding(t, h2Mode) }
532 func testServeFileWithContentEncoding(t *testing.T, h2 bool) {
533 defer afterTest(t)
534 cst := newClientServerTest(t, h2, HandlerFunc(func(w ResponseWriter, r *Request) {
535 w.Header().Set("Content-Encoding", "foo")
536 ServeFile(w, r, "testdata/file")
538 // Because the testdata is so small, it would fit in
539 // both the h1 and h2 Server's write buffers. For h1,
540 // sendfile is used, though, forcing a header flush at
541 // the io.Copy. http2 doesn't do a header flush so
542 // buffers all 11 bytes and then adds its own
543 // Content-Length. To prevent the Server's
544 // Content-Length and test ServeFile only, flush here.
545 w.(Flusher).Flush()
547 defer cst.close()
548 resp, err := cst.c.Get(cst.ts.URL)
549 if err != nil {
550 t.Fatal(err)
552 resp.Body.Close()
553 if g, e := resp.ContentLength, int64(-1); g != e {
554 t.Errorf("Content-Length mismatch: got %d, want %d", g, e)
558 func TestServeIndexHtml(t *testing.T) {
559 defer afterTest(t)
560 const want = "index.html says hello\n"
561 ts := httptest.NewServer(FileServer(Dir(".")))
562 defer ts.Close()
564 for _, path := range []string{"/testdata/", "/testdata/index.html"} {
565 res, err := Get(ts.URL + path)
566 if err != nil {
567 t.Fatal(err)
569 b, err := ioutil.ReadAll(res.Body)
570 if err != nil {
571 t.Fatal("reading Body:", err)
573 if s := string(b); s != want {
574 t.Errorf("for path %q got %q, want %q", path, s, want)
576 res.Body.Close()
580 func TestFileServerZeroByte(t *testing.T) {
581 defer afterTest(t)
582 ts := httptest.NewServer(FileServer(Dir(".")))
583 defer ts.Close()
585 res, err := Get(ts.URL + "/..\x00")
586 if err != nil {
587 t.Fatal(err)
589 b, err := ioutil.ReadAll(res.Body)
590 if err != nil {
591 t.Fatal("reading Body:", err)
593 if res.StatusCode == 200 {
594 t.Errorf("got status 200; want an error. Body is:\n%s", string(b))
598 type fakeFileInfo struct {
599 dir bool
600 basename string
601 modtime time.Time
602 ents []*fakeFileInfo
603 contents string
604 err error
607 func (f *fakeFileInfo) Name() string { return f.basename }
608 func (f *fakeFileInfo) Sys() interface{} { return nil }
609 func (f *fakeFileInfo) ModTime() time.Time { return f.modtime }
610 func (f *fakeFileInfo) IsDir() bool { return f.dir }
611 func (f *fakeFileInfo) Size() int64 { return int64(len(f.contents)) }
612 func (f *fakeFileInfo) Mode() os.FileMode {
613 if f.dir {
614 return 0755 | os.ModeDir
616 return 0644
619 type fakeFile struct {
620 io.ReadSeeker
621 fi *fakeFileInfo
622 path string // as opened
623 entpos int
626 func (f *fakeFile) Close() error { return nil }
627 func (f *fakeFile) Stat() (os.FileInfo, error) { return f.fi, nil }
628 func (f *fakeFile) Readdir(count int) ([]os.FileInfo, error) {
629 if !f.fi.dir {
630 return nil, os.ErrInvalid
632 var fis []os.FileInfo
634 limit := f.entpos + count
635 if count <= 0 || limit > len(f.fi.ents) {
636 limit = len(f.fi.ents)
638 for ; f.entpos < limit; f.entpos++ {
639 fis = append(fis, f.fi.ents[f.entpos])
642 if len(fis) == 0 && count > 0 {
643 return fis, io.EOF
644 } else {
645 return fis, nil
649 type fakeFS map[string]*fakeFileInfo
651 func (fs fakeFS) Open(name string) (File, error) {
652 name = path.Clean(name)
653 f, ok := fs[name]
654 if !ok {
655 return nil, os.ErrNotExist
657 if f.err != nil {
658 return nil, f.err
660 return &fakeFile{ReadSeeker: strings.NewReader(f.contents), fi: f, path: name}, nil
663 func TestDirectoryIfNotModified(t *testing.T) {
664 defer afterTest(t)
665 const indexContents = "I am a fake index.html file"
666 fileMod := time.Unix(1000000000, 0).UTC()
667 fileModStr := fileMod.Format(TimeFormat)
668 dirMod := time.Unix(123, 0).UTC()
669 indexFile := &fakeFileInfo{
670 basename: "index.html",
671 modtime: fileMod,
672 contents: indexContents,
674 fs := fakeFS{
675 "/": &fakeFileInfo{
676 dir: true,
677 modtime: dirMod,
678 ents: []*fakeFileInfo{indexFile},
680 "/index.html": indexFile,
683 ts := httptest.NewServer(FileServer(fs))
684 defer ts.Close()
686 res, err := Get(ts.URL)
687 if err != nil {
688 t.Fatal(err)
690 b, err := ioutil.ReadAll(res.Body)
691 if err != nil {
692 t.Fatal(err)
694 if string(b) != indexContents {
695 t.Fatalf("Got body %q; want %q", b, indexContents)
697 res.Body.Close()
699 lastMod := res.Header.Get("Last-Modified")
700 if lastMod != fileModStr {
701 t.Fatalf("initial Last-Modified = %q; want %q", lastMod, fileModStr)
704 req, _ := NewRequest("GET", ts.URL, nil)
705 req.Header.Set("If-Modified-Since", lastMod)
707 res, err = DefaultClient.Do(req)
708 if err != nil {
709 t.Fatal(err)
711 if res.StatusCode != 304 {
712 t.Fatalf("Code after If-Modified-Since request = %v; want 304", res.StatusCode)
714 res.Body.Close()
716 // Advance the index.html file's modtime, but not the directory's.
717 indexFile.modtime = indexFile.modtime.Add(1 * time.Hour)
719 res, err = DefaultClient.Do(req)
720 if err != nil {
721 t.Fatal(err)
723 if res.StatusCode != 200 {
724 t.Fatalf("Code after second If-Modified-Since request = %v; want 200; res is %#v", res.StatusCode, res)
726 res.Body.Close()
729 func mustStat(t *testing.T, fileName string) os.FileInfo {
730 fi, err := os.Stat(fileName)
731 if err != nil {
732 t.Fatal(err)
734 return fi
737 func TestServeContent(t *testing.T) {
738 defer afterTest(t)
739 type serveParam struct {
740 name string
741 modtime time.Time
742 content io.ReadSeeker
743 contentType string
744 etag string
746 servec := make(chan serveParam, 1)
747 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
748 p := <-servec
749 if p.etag != "" {
750 w.Header().Set("ETag", p.etag)
752 if p.contentType != "" {
753 w.Header().Set("Content-Type", p.contentType)
755 ServeContent(w, r, p.name, p.modtime, p.content)
757 defer ts.Close()
759 type testCase struct {
760 // One of file or content must be set:
761 file string
762 content io.ReadSeeker
764 modtime time.Time
765 serveETag string // optional
766 serveContentType string // optional
767 reqHeader map[string]string
768 wantLastMod string
769 wantContentType string
770 wantContentRange string
771 wantStatus int
773 htmlModTime := mustStat(t, "testdata/index.html").ModTime()
774 tests := map[string]testCase{
775 "no_last_modified": {
776 file: "testdata/style.css",
777 wantContentType: "text/css; charset=utf-8",
778 wantStatus: 200,
780 "with_last_modified": {
781 file: "testdata/index.html",
782 wantContentType: "text/html; charset=utf-8",
783 modtime: htmlModTime,
784 wantLastMod: htmlModTime.UTC().Format(TimeFormat),
785 wantStatus: 200,
787 "not_modified_modtime": {
788 file: "testdata/style.css",
789 serveETag: `"foo"`, // Last-Modified sent only when no ETag
790 modtime: htmlModTime,
791 reqHeader: map[string]string{
792 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
794 wantStatus: 304,
796 "not_modified_modtime_with_contenttype": {
797 file: "testdata/style.css",
798 serveContentType: "text/css", // explicit content type
799 serveETag: `"foo"`, // Last-Modified sent only when no ETag
800 modtime: htmlModTime,
801 reqHeader: map[string]string{
802 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
804 wantStatus: 304,
806 "not_modified_etag": {
807 file: "testdata/style.css",
808 serveETag: `"foo"`,
809 reqHeader: map[string]string{
810 "If-None-Match": `"foo"`,
812 wantStatus: 304,
814 "not_modified_etag_no_seek": {
815 content: panicOnSeek{nil}, // should never be called
816 serveETag: `W/"foo"`, // If-None-Match uses weak ETag comparison
817 reqHeader: map[string]string{
818 "If-None-Match": `"baz", W/"foo"`,
820 wantStatus: 304,
822 "if_none_match_mismatch": {
823 file: "testdata/style.css",
824 serveETag: `"foo"`,
825 reqHeader: map[string]string{
826 "If-None-Match": `"Foo"`,
828 wantStatus: 200,
829 wantContentType: "text/css; charset=utf-8",
831 "range_good": {
832 file: "testdata/style.css",
833 serveETag: `"A"`,
834 reqHeader: map[string]string{
835 "Range": "bytes=0-4",
837 wantStatus: StatusPartialContent,
838 wantContentType: "text/css; charset=utf-8",
839 wantContentRange: "bytes 0-4/8",
841 "range_match": {
842 file: "testdata/style.css",
843 serveETag: `"A"`,
844 reqHeader: map[string]string{
845 "Range": "bytes=0-4",
846 "If-Range": `"A"`,
848 wantStatus: StatusPartialContent,
849 wantContentType: "text/css; charset=utf-8",
850 wantContentRange: "bytes 0-4/8",
852 "range_match_weak_etag": {
853 file: "testdata/style.css",
854 serveETag: `W/"A"`,
855 reqHeader: map[string]string{
856 "Range": "bytes=0-4",
857 "If-Range": `W/"A"`,
859 wantStatus: 200,
860 wantContentType: "text/css; charset=utf-8",
862 "range_no_overlap": {
863 file: "testdata/style.css",
864 serveETag: `"A"`,
865 reqHeader: map[string]string{
866 "Range": "bytes=10-20",
868 wantStatus: StatusRequestedRangeNotSatisfiable,
869 wantContentType: "text/plain; charset=utf-8",
870 wantContentRange: "bytes */8",
872 // An If-Range resource for entity "A", but entity "B" is now current.
873 // The Range request should be ignored.
874 "range_no_match": {
875 file: "testdata/style.css",
876 serveETag: `"A"`,
877 reqHeader: map[string]string{
878 "Range": "bytes=0-4",
879 "If-Range": `"B"`,
881 wantStatus: 200,
882 wantContentType: "text/css; charset=utf-8",
884 "range_with_modtime": {
885 file: "testdata/style.css",
886 modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 /* nanos */, time.UTC),
887 reqHeader: map[string]string{
888 "Range": "bytes=0-4",
889 "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
891 wantStatus: StatusPartialContent,
892 wantContentType: "text/css; charset=utf-8",
893 wantContentRange: "bytes 0-4/8",
894 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT",
896 "range_with_modtime_nanos": {
897 file: "testdata/style.css",
898 modtime: time.Date(2014, 6, 25, 17, 12, 18, 123 /* nanos */, time.UTC),
899 reqHeader: map[string]string{
900 "Range": "bytes=0-4",
901 "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
903 wantStatus: StatusPartialContent,
904 wantContentType: "text/css; charset=utf-8",
905 wantContentRange: "bytes 0-4/8",
906 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT",
908 "unix_zero_modtime": {
909 content: strings.NewReader("<html>foo"),
910 modtime: time.Unix(0, 0),
911 wantStatus: StatusOK,
912 wantContentType: "text/html; charset=utf-8",
914 "ifmatch_matches": {
915 file: "testdata/style.css",
916 serveETag: `"A"`,
917 reqHeader: map[string]string{
918 "If-Match": `"Z", "A"`,
920 wantStatus: 200,
921 wantContentType: "text/css; charset=utf-8",
923 "ifmatch_star": {
924 file: "testdata/style.css",
925 serveETag: `"A"`,
926 reqHeader: map[string]string{
927 "If-Match": `*`,
929 wantStatus: 200,
930 wantContentType: "text/css; charset=utf-8",
932 "ifmatch_failed": {
933 file: "testdata/style.css",
934 serveETag: `"A"`,
935 reqHeader: map[string]string{
936 "If-Match": `"B"`,
938 wantStatus: 412,
939 wantContentType: "text/plain; charset=utf-8",
941 "ifmatch_fails_on_weak_etag": {
942 file: "testdata/style.css",
943 serveETag: `W/"A"`,
944 reqHeader: map[string]string{
945 "If-Match": `W/"A"`,
947 wantStatus: 412,
948 wantContentType: "text/plain; charset=utf-8",
950 "if_unmodified_since_true": {
951 file: "testdata/style.css",
952 modtime: htmlModTime,
953 reqHeader: map[string]string{
954 "If-Unmodified-Since": htmlModTime.UTC().Format(TimeFormat),
956 wantStatus: 200,
957 wantContentType: "text/css; charset=utf-8",
958 wantLastMod: htmlModTime.UTC().Format(TimeFormat),
960 "if_unmodified_since_false": {
961 file: "testdata/style.css",
962 modtime: htmlModTime,
963 reqHeader: map[string]string{
964 "If-Unmodified-Since": htmlModTime.Add(-2 * time.Second).UTC().Format(TimeFormat),
966 wantStatus: 412,
967 wantContentType: "text/plain; charset=utf-8",
968 wantLastMod: htmlModTime.UTC().Format(TimeFormat),
971 for testName, tt := range tests {
972 var content io.ReadSeeker
973 if tt.file != "" {
974 f, err := os.Open(tt.file)
975 if err != nil {
976 t.Fatalf("test %q: %v", testName, err)
978 defer f.Close()
979 content = f
980 } else {
981 content = tt.content
984 servec <- serveParam{
985 name: filepath.Base(tt.file),
986 content: content,
987 modtime: tt.modtime,
988 etag: tt.serveETag,
989 contentType: tt.serveContentType,
991 req, err := NewRequest("GET", ts.URL, nil)
992 if err != nil {
993 t.Fatal(err)
995 for k, v := range tt.reqHeader {
996 req.Header.Set(k, v)
998 res, err := DefaultClient.Do(req)
999 if err != nil {
1000 t.Fatal(err)
1002 io.Copy(ioutil.Discard, res.Body)
1003 res.Body.Close()
1004 if res.StatusCode != tt.wantStatus {
1005 t.Errorf("test %q: status = %d; want %d", testName, res.StatusCode, tt.wantStatus)
1007 if g, e := res.Header.Get("Content-Type"), tt.wantContentType; g != e {
1008 t.Errorf("test %q: content-type = %q, want %q", testName, g, e)
1010 if g, e := res.Header.Get("Content-Range"), tt.wantContentRange; g != e {
1011 t.Errorf("test %q: content-range = %q, want %q", testName, g, e)
1013 if g, e := res.Header.Get("Last-Modified"), tt.wantLastMod; g != e {
1014 t.Errorf("test %q: last-modified = %q, want %q", testName, g, e)
1019 // Issue 12991
1020 func TestServerFileStatError(t *testing.T) {
1021 rec := httptest.NewRecorder()
1022 r, _ := NewRequest("GET", "http://foo/", nil)
1023 redirect := false
1024 name := "file.txt"
1025 fs := issue12991FS{}
1026 ExportServeFile(rec, r, fs, name, redirect)
1027 if body := rec.Body.String(); !strings.Contains(body, "403") || !strings.Contains(body, "Forbidden") {
1028 t.Errorf("wanted 403 forbidden message; got: %s", body)
1032 type issue12991FS struct{}
1034 func (issue12991FS) Open(string) (File, error) { return issue12991File{}, nil }
1036 type issue12991File struct{ File }
1038 func (issue12991File) Stat() (os.FileInfo, error) { return nil, os.ErrPermission }
1039 func (issue12991File) Close() error { return nil }
1041 func TestServeContentErrorMessages(t *testing.T) {
1042 defer afterTest(t)
1043 fs := fakeFS{
1044 "/500": &fakeFileInfo{
1045 err: errors.New("random error"),
1047 "/403": &fakeFileInfo{
1048 err: &os.PathError{Err: os.ErrPermission},
1051 ts := httptest.NewServer(FileServer(fs))
1052 defer ts.Close()
1053 for _, code := range []int{403, 404, 500} {
1054 res, err := DefaultClient.Get(fmt.Sprintf("%s/%d", ts.URL, code))
1055 if err != nil {
1056 t.Errorf("Error fetching /%d: %v", code, err)
1057 continue
1059 if res.StatusCode != code {
1060 t.Errorf("For /%d, status code = %d; want %d", code, res.StatusCode, code)
1062 res.Body.Close()
1066 // verifies that sendfile is being used on Linux
1067 func TestLinuxSendfile(t *testing.T) {
1068 setParallel(t)
1069 defer afterTest(t)
1070 if runtime.GOOS != "linux" {
1071 t.Skip("skipping; linux-only test")
1073 if _, err := exec.LookPath("strace"); err != nil {
1074 t.Skip("skipping; strace not found in path")
1077 ln, err := net.Listen("tcp", "127.0.0.1:0")
1078 if err != nil {
1079 t.Fatal(err)
1081 lnf, err := ln.(*net.TCPListener).File()
1082 if err != nil {
1083 t.Fatal(err)
1085 defer ln.Close()
1087 syscalls := "sendfile,sendfile64"
1088 switch runtime.GOARCH {
1089 case "mips64", "mips64le", "s390x", "alpha":
1090 // strace on the above platforms doesn't support sendfile64
1091 // and will error out if we specify that with `-e trace='.
1092 syscalls = "sendfile"
1093 case "mips64":
1094 t.Skip("TODO: update this test to be robust against various versions of strace on mips64. See golang.org/issue/33430")
1097 var buf bytes.Buffer
1098 child := exec.Command("strace", "-f", "-q", "-e", "trace="+syscalls, os.Args[0], "-test.run=TestLinuxSendfileChild")
1099 child.ExtraFiles = append(child.ExtraFiles, lnf)
1100 child.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...)
1101 child.Stdout = &buf
1102 child.Stderr = &buf
1103 if err := child.Start(); err != nil {
1104 t.Skipf("skipping; failed to start straced child: %v", err)
1107 res, err := Get(fmt.Sprintf("http://%s/", ln.Addr()))
1108 if err != nil {
1109 t.Fatalf("http client error: %v", err)
1111 _, err = io.Copy(ioutil.Discard, res.Body)
1112 if err != nil {
1113 t.Fatalf("client body read error: %v", err)
1115 res.Body.Close()
1117 // Force child to exit cleanly.
1118 Post(fmt.Sprintf("http://%s/quit", ln.Addr()), "", nil)
1119 child.Wait()
1121 rx := regexp.MustCompile(`sendfile(64)?\(\d+,\s*\d+,\s*NULL,\s*\d+`)
1122 out := buf.String()
1123 if !rx.MatchString(out) {
1124 t.Errorf("no sendfile system call found in:\n%s", out)
1128 func getBody(t *testing.T, testName string, req Request) (*Response, []byte) {
1129 r, err := DefaultClient.Do(&req)
1130 if err != nil {
1131 t.Fatalf("%s: for URL %q, send error: %v", testName, req.URL.String(), err)
1133 b, err := ioutil.ReadAll(r.Body)
1134 if err != nil {
1135 t.Fatalf("%s: for URL %q, reading body: %v", testName, req.URL.String(), err)
1137 return r, b
1140 // TestLinuxSendfileChild isn't a real test. It's used as a helper process
1141 // for TestLinuxSendfile.
1142 func TestLinuxSendfileChild(*testing.T) {
1143 if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
1144 return
1146 defer os.Exit(0)
1147 fd3 := os.NewFile(3, "ephemeral-port-listener")
1148 ln, err := net.FileListener(fd3)
1149 if err != nil {
1150 panic(err)
1152 mux := NewServeMux()
1153 mux.Handle("/", FileServer(Dir("testdata")))
1154 mux.HandleFunc("/quit", func(ResponseWriter, *Request) {
1155 os.Exit(0)
1157 s := &Server{Handler: mux}
1158 err = s.Serve(ln)
1159 if err != nil {
1160 panic(err)
1164 func TestFileServerCleanPath(t *testing.T) {
1165 tests := []struct {
1166 path string
1167 wantCode int
1168 wantOpen []string
1170 {"/", 200, []string{"/", "/index.html"}},
1171 {"/dir", 301, []string{"/dir"}},
1172 {"/dir/", 200, []string{"/dir", "/dir/index.html"}},
1174 for _, tt := range tests {
1175 var log []string
1176 rr := httptest.NewRecorder()
1177 req, _ := NewRequest("GET", "http://foo.localhost"+tt.path, nil)
1178 FileServer(fileServerCleanPathDir{&log}).ServeHTTP(rr, req)
1179 if !reflect.DeepEqual(log, tt.wantOpen) {
1180 t.Logf("For %s: Opens = %q; want %q", tt.path, log, tt.wantOpen)
1182 if rr.Code != tt.wantCode {
1183 t.Logf("For %s: Response code = %d; want %d", tt.path, rr.Code, tt.wantCode)
1188 type fileServerCleanPathDir struct {
1189 log *[]string
1192 func (d fileServerCleanPathDir) Open(path string) (File, error) {
1193 *(d.log) = append(*(d.log), path)
1194 if path == "/" || path == "/dir" || path == "/dir/" {
1195 // Just return back something that's a directory.
1196 return Dir(".").Open(".")
1198 return nil, os.ErrNotExist
1201 type panicOnSeek struct{ io.ReadSeeker }
1203 func Test_scanETag(t *testing.T) {
1204 tests := []struct {
1205 in string
1206 wantETag string
1207 wantRemain string
1209 {`W/"etag-1"`, `W/"etag-1"`, ""},
1210 {`"etag-2"`, `"etag-2"`, ""},
1211 {`"etag-1", "etag-2"`, `"etag-1"`, `, "etag-2"`},
1212 {"", "", ""},
1213 {"", "", ""},
1214 {"W/", "", ""},
1215 {`W/"truc`, "", ""},
1216 {`w/"case-sensitive"`, "", ""},
1218 for _, test := range tests {
1219 etag, remain := ExportScanETag(test.in)
1220 if etag != test.wantETag || remain != test.wantRemain {
1221 t.Errorf("scanETag(%q)=%q %q, want %q %q", test.in, etag, remain, test.wantETag, test.wantRemain)