28
testFile = "testdata/file"
31
testFile = "testdata/file"
35
type wantRange struct {
36
start, end int64 // range [start,end)
32
39
var ServeFileRangeTests = []struct {
37
{0, testFileLength, "", StatusOK},
38
{0, 5, "0-4", StatusPartialContent},
39
{2, testFileLength, "2-", StatusPartialContent},
40
{testFileLength - 5, testFileLength, "-5", StatusPartialContent},
41
{3, 8, "3-7", StatusPartialContent},
42
{0, 0, "20-", StatusRequestedRangeNotSatisfiable},
44
{r: "", code: StatusOK},
45
{r: "bytes=0-4", code: StatusPartialContent, ranges: []wantRange{{0, 5}}},
46
{r: "bytes=2-", code: StatusPartialContent, ranges: []wantRange{{2, testFileLen}}},
47
{r: "bytes=-5", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 5, testFileLen}}},
48
{r: "bytes=3-7", code: StatusPartialContent, ranges: []wantRange{{3, 8}}},
49
{r: "bytes=20-", code: StatusRequestedRangeNotSatisfiable},
50
{r: "bytes=0-0,-2", code: StatusPartialContent, ranges: []wantRange{{0, 1}, {testFileLen - 2, testFileLen}}},
51
{r: "bytes=0-1,5-8", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, 9}}},
52
{r: "bytes=0-1,5-", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, testFileLen}}},
53
{r: "bytes=0-,1-,2-,3-,4-", code: StatusOK}, // ignore wasteful range request
45
56
func TestServeFile(t *testing.T) {
46
58
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
47
59
ServeFile(w, r, "testdata/file")
67
79
_, body := getBody(t, "straight get", req)
68
if !equal(body, file) {
80
if !bytes.Equal(body, file) {
69
81
t.Fatalf("body mismatch: got %q, want %q", body, file)
73
for i, rt := range ServeFileRangeTests {
74
req.Header.Set("Range", "bytes="+rt.r)
76
req.Header["Range"] = nil
86
for _, rt := range ServeFileRangeTests {
88
req.Header.Set("Range", rt.r)
78
r, body := getBody(t, fmt.Sprintf("test %d", i), req)
79
if r.StatusCode != rt.code {
80
t.Errorf("range=%q: StatusCode=%d, want %d", rt.r, r.StatusCode, rt.code)
90
resp, body := getBody(t, fmt.Sprintf("range test %q", rt.r), req)
91
if resp.StatusCode != rt.code {
92
t.Errorf("range=%q: StatusCode=%d, want %d", rt.r, resp.StatusCode, rt.code)
82
94
if rt.code == StatusRequestedRangeNotSatisfiable {
85
h := fmt.Sprintf("bytes %d-%d/%d", rt.start, rt.end-1, testFileLength)
89
cr := r.Header.Get("Content-Range")
91
t.Errorf("header mismatch: range=%q: got %q, want %q", rt.r, cr, h)
93
if !equal(body, file[rt.start:rt.end]) {
94
t.Errorf("body mismatch: range=%q: got %q, want %q", rt.r, body, file[rt.start:rt.end])
97
wantContentRange := ""
98
if len(rt.ranges) == 1 {
100
wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
102
cr := resp.Header.Get("Content-Range")
103
if cr != wantContentRange {
104
t.Errorf("range=%q: Content-Range = %q, want %q", rt.r, cr, wantContentRange)
106
ct := resp.Header.Get("Content-Type")
107
if len(rt.ranges) == 1 {
109
wantBody := file[rng.start:rng.end]
110
if !bytes.Equal(body, wantBody) {
111
t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
113
if strings.HasPrefix(ct, "multipart/byteranges") {
114
t.Errorf("range=%q content-type = %q; unexpected multipart/byteranges", rt.r, ct)
117
if len(rt.ranges) > 1 {
118
typ, params, err := mime.ParseMediaType(ct)
120
t.Errorf("range=%q content-type = %q; %v", rt.r, ct, err)
123
if typ != "multipart/byteranges" {
124
t.Errorf("range=%q content-type = %q; want multipart/byteranges", rt.r, typ)
127
if params["boundary"] == "" {
128
t.Errorf("range=%q content-type = %q; lacks boundary", rt.r, ct)
131
if g, w := resp.ContentLength, int64(len(body)); g != w {
132
t.Errorf("range=%q Content-Length = %d; want %d", rt.r, g, w)
135
mr := multipart.NewReader(bytes.NewReader(body), params["boundary"])
136
for ri, rng := range rt.ranges {
137
part, err := mr.NextPart()
139
t.Errorf("range=%q, reading part index %d: %v", rt.r, ri, err)
142
wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
143
if g, w := part.Header.Get("Content-Range"), wantContentRange; g != w {
144
t.Errorf("range=%q: part Content-Range = %q; want %q", rt.r, g, w)
146
body, err := ioutil.ReadAll(part)
148
t.Errorf("range=%q, reading part index %d body: %v", rt.r, ri, err)
151
wantBody := file[rng.start:rng.end]
152
if !bytes.Equal(body, wantBody) {
153
t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
156
_, err = mr.NextPart()
158
t.Errorf("range=%q; expected final error io.EOF; got %v", rt.r, err)
404
func TestFileServerZeroByte(t *testing.T) {
406
ts := httptest.NewServer(FileServer(Dir(".")))
409
res, err := Get(ts.URL + "/..\x00")
413
b, err := ioutil.ReadAll(res.Body)
415
t.Fatal("reading Body:", err)
417
if res.StatusCode == 200 {
418
t.Errorf("got status 200; want an error. Body is:\n%s", string(b))
422
type fakeFileInfo struct {
430
func (f *fakeFileInfo) Name() string { return f.basename }
431
func (f *fakeFileInfo) Sys() interface{} { return nil }
432
func (f *fakeFileInfo) ModTime() time.Time { return f.modtime }
433
func (f *fakeFileInfo) IsDir() bool { return f.dir }
434
func (f *fakeFileInfo) Size() int64 { return int64(len(f.contents)) }
435
func (f *fakeFileInfo) Mode() os.FileMode {
437
return 0755 | os.ModeDir
442
type fakeFile struct {
445
path string // as opened
448
func (f *fakeFile) Close() error { return nil }
449
func (f *fakeFile) Stat() (os.FileInfo, error) { return f.fi, nil }
450
func (f *fakeFile) Readdir(count int) ([]os.FileInfo, error) {
452
return nil, os.ErrInvalid
454
var fis []os.FileInfo
455
for _, fi := range f.fi.ents {
456
fis = append(fis, fi)
461
type fakeFS map[string]*fakeFileInfo
463
func (fs fakeFS) Open(name string) (File, error) {
464
name = path.Clean(name)
467
println("fake filesystem didn't find file", name)
468
return nil, os.ErrNotExist
470
return &fakeFile{ReadSeeker: strings.NewReader(f.contents), fi: f, path: name}, nil
473
func TestDirectoryIfNotModified(t *testing.T) {
475
const indexContents = "I am a fake index.html file"
476
fileMod := time.Unix(1000000000, 0).UTC()
477
fileModStr := fileMod.Format(TimeFormat)
478
dirMod := time.Unix(123, 0).UTC()
479
indexFile := &fakeFileInfo{
480
basename: "index.html",
482
contents: indexContents,
488
ents: []*fakeFileInfo{indexFile},
490
"/index.html": indexFile,
493
ts := httptest.NewServer(FileServer(fs))
496
res, err := Get(ts.URL)
500
b, err := ioutil.ReadAll(res.Body)
504
if string(b) != indexContents {
505
t.Fatalf("Got body %q; want %q", b, indexContents)
509
lastMod := res.Header.Get("Last-Modified")
510
if lastMod != fileModStr {
511
t.Fatalf("initial Last-Modified = %q; want %q", lastMod, fileModStr)
514
req, _ := NewRequest("GET", ts.URL, nil)
515
req.Header.Set("If-Modified-Since", lastMod)
517
res, err = DefaultClient.Do(req)
521
if res.StatusCode != 304 {
522
t.Fatalf("Code after If-Modified-Since request = %v; want 304", res.StatusCode)
526
// Advance the index.html file's modtime, but not the directory's.
527
indexFile.modtime = indexFile.modtime.Add(1 * time.Hour)
529
res, err = DefaultClient.Do(req)
533
if res.StatusCode != 200 {
534
t.Fatalf("Code after second If-Modified-Since request = %v; want 200; res is %#v", res.StatusCode, res)
539
func mustStat(t *testing.T, fileName string) os.FileInfo {
540
fi, err := os.Stat(fileName)
328
547
func TestServeContent(t *testing.T) {
332
content io.ReadSeeker
549
type serveParam struct {
552
content io.ReadSeeker
334
ch := make(chan req, 1)
556
servec := make(chan serveParam, 1)
335
557
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
560
w.Header().Set("ETag", p.etag)
562
if p.contentType != "" {
563
w.Header().Set("Content-Type", p.contentType)
337
565
ServeContent(w, r, p.name, p.modtime, p.content)
341
css, err := os.Open("testdata/style.css")
347
ch <- req{"style.css", time.Time{}, css}
348
res, err := Get(ts.URL)
352
if g, e := res.Header.Get("Content-Type"), "text/css; charset=utf-8"; g != e {
353
t.Errorf("style.css: content type = %q, want %q", g, e)
355
if g := res.Header.Get("Last-Modified"); g != "" {
356
t.Errorf("want empty Last-Modified; got %q", g)
359
fi, err := css.Stat()
363
ch <- req{"style.html", fi.ModTime(), css}
364
res, err = Get(ts.URL)
368
if g, e := res.Header.Get("Content-Type"), "text/html; charset=utf-8"; g != e {
369
t.Errorf("style.html: content type = %q, want %q", g, e)
371
if g := res.Header.Get("Last-Modified"); g == "" {
372
t.Errorf("want non-empty last-modified")
569
type testCase struct {
572
serveETag string // optional
573
serveContentType string // optional
574
reqHeader map[string]string
576
wantContentType string
579
htmlModTime := mustStat(t, "testdata/index.html").ModTime()
580
tests := map[string]testCase{
581
"no_last_modified": {
582
file: "testdata/style.css",
583
wantContentType: "text/css; charset=utf-8",
586
"with_last_modified": {
587
file: "testdata/index.html",
588
wantContentType: "text/html; charset=utf-8",
589
modtime: htmlModTime,
590
wantLastMod: htmlModTime.UTC().Format(TimeFormat),
593
"not_modified_modtime": {
594
file: "testdata/style.css",
595
modtime: htmlModTime,
596
reqHeader: map[string]string{
597
"If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
601
"not_modified_modtime_with_contenttype": {
602
file: "testdata/style.css",
603
serveContentType: "text/css", // explicit content type
604
modtime: htmlModTime,
605
reqHeader: map[string]string{
606
"If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
610
"not_modified_etag": {
611
file: "testdata/style.css",
613
reqHeader: map[string]string{
614
"If-None-Match": `"foo"`,
619
file: "testdata/style.css",
621
reqHeader: map[string]string{
622
"Range": "bytes=0-4",
624
wantStatus: StatusPartialContent,
625
wantContentType: "text/css; charset=utf-8",
627
// An If-Range resource for entity "A", but entity "B" is now current.
628
// The Range request should be ignored.
630
file: "testdata/style.css",
632
reqHeader: map[string]string{
633
"Range": "bytes=0-4",
637
wantContentType: "text/css; charset=utf-8",
640
for testName, tt := range tests {
641
f, err := os.Open(tt.file)
643
t.Fatalf("test %q: %v", testName, err)
647
servec <- serveParam{
648
name: filepath.Base(tt.file),
652
contentType: tt.serveContentType,
654
req, err := NewRequest("GET", ts.URL, nil)
658
for k, v := range tt.reqHeader {
661
res, err := DefaultClient.Do(req)
665
io.Copy(ioutil.Discard, res.Body)
667
if res.StatusCode != tt.wantStatus {
668
t.Errorf("test %q: status = %d; want %d", testName, res.StatusCode, tt.wantStatus)
670
if g, e := res.Header.Get("Content-Type"), tt.wantContentType; g != e {
671
t.Errorf("test %q: content-type = %q, want %q", testName, g, e)
673
if g, e := res.Header.Get("Last-Modified"), tt.wantLastMod; g != e {
674
t.Errorf("test %q: last-modified = %q, want %q", testName, g, e)
376
679
// verifies that sendfile is being used on Linux
377
680
func TestLinuxSendfile(t *testing.T) {
378
682
if runtime.GOOS != "linux" {
379
t.Logf("skipping; linux-only test")
683
t.Skip("skipping; linux-only test")
382
_, err := exec.LookPath("strace")
384
t.Logf("skipping; strace not found in path")
685
if _, err := exec.LookPath("strace"); err != nil {
686
t.Skip("skipping; strace not found in path")
388
689
ln, err := net.Listen("tcp", "127.0.0.1:0")