Source file src/net/http/fs_test.go

     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.
     4  
     5  package http_test
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"compress/gzip"
    11  	"errors"
    12  	"fmt"
    13  	"internal/testenv"
    14  	"io"
    15  	"io/fs"
    16  	"mime"
    17  	"mime/multipart"
    18  	"net"
    19  	"net/http"
    20  	. "net/http"
    21  	"net/http/httptest"
    22  	"net/url"
    23  	"os"
    24  	"os/exec"
    25  	"path"
    26  	"path/filepath"
    27  	"reflect"
    28  	"regexp"
    29  	"runtime"
    30  	"strconv"
    31  	"strings"
    32  	"testing"
    33  	"testing/fstest"
    34  	"time"
    35  )
    36  
    37  const (
    38  	testFile    = "testdata/file"
    39  	testFileLen = 11
    40  )
    41  
    42  type wantRange struct {
    43  	start, end int64 // range [start,end)
    44  }
    45  
    46  var ServeFileRangeTests = []struct {
    47  	r      string
    48  	code   int
    49  	ranges []wantRange
    50  }{
    51  	{r: "", code: StatusOK},
    52  	{r: "bytes=0-4", code: StatusPartialContent, ranges: []wantRange{{0, 5}}},
    53  	{r: "bytes=2-", code: StatusPartialContent, ranges: []wantRange{{2, testFileLen}}},
    54  	{r: "bytes=-5", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 5, testFileLen}}},
    55  	{r: "bytes=3-7", code: StatusPartialContent, ranges: []wantRange{{3, 8}}},
    56  	{r: "bytes=0-0,-2", code: StatusPartialContent, ranges: []wantRange{{0, 1}, {testFileLen - 2, testFileLen}}},
    57  	{r: "bytes=0-1,5-8", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, 9}}},
    58  	{r: "bytes=0-1,5-", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, testFileLen}}},
    59  	{r: "bytes=5-1000", code: StatusPartialContent, ranges: []wantRange{{5, testFileLen}}},
    60  	{r: "bytes=0-,1-,2-,3-,4-", code: StatusOK}, // ignore wasteful range request
    61  	{r: "bytes=0-9", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen - 1}}},
    62  	{r: "bytes=0-10", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
    63  	{r: "bytes=0-11", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
    64  	{r: "bytes=10-11", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}},
    65  	{r: "bytes=10-", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}},
    66  	{r: "bytes=11-", code: StatusRequestedRangeNotSatisfiable},
    67  	{r: "bytes=11-12", code: StatusRequestedRangeNotSatisfiable},
    68  	{r: "bytes=12-12", code: StatusRequestedRangeNotSatisfiable},
    69  	{r: "bytes=11-100", code: StatusRequestedRangeNotSatisfiable},
    70  	{r: "bytes=12-100", code: StatusRequestedRangeNotSatisfiable},
    71  	{r: "bytes=100-", code: StatusRequestedRangeNotSatisfiable},
    72  	{r: "bytes=100-1000", code: StatusRequestedRangeNotSatisfiable},
    73  }
    74  
    75  func TestServeFile(t *testing.T) { run(t, testServeFile) }
    76  func testServeFile(t *testing.T, mode testMode) {
    77  	ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
    78  		ServeFile(w, r, "testdata/file")
    79  	})).ts
    80  	c := ts.Client()
    81  
    82  	var err error
    83  
    84  	file, err := os.ReadFile(testFile)
    85  	if err != nil {
    86  		t.Fatal("reading file:", err)
    87  	}
    88  
    89  	// set up the Request (re-used for all tests)
    90  	var req Request
    91  	req.Header = make(Header)
    92  	if req.URL, err = url.Parse(ts.URL); err != nil {
    93  		t.Fatal("ParseURL:", err)
    94  	}
    95  
    96  	// Get contents via various methods.
    97  	//
    98  	// See https://go.dev/issue/59471 for a proposal to limit the set of methods handled.
    99  	// For now, test the historical behavior.
   100  	for _, method := range []string{
   101  		MethodGet,
   102  		MethodPost,
   103  		MethodPut,
   104  		MethodPatch,
   105  		MethodDelete,
   106  		MethodOptions,
   107  		MethodTrace,
   108  	} {
   109  		req.Method = method
   110  		_, body := getBody(t, method, req, c)
   111  		if !bytes.Equal(body, file) {
   112  			t.Fatalf("body mismatch for %v request: got %q, want %q", method, body, file)
   113  		}
   114  	}
   115  
   116  	// HEAD request.
   117  	req.Method = MethodHead
   118  	resp, body := getBody(t, "HEAD", req, c)
   119  	if len(body) != 0 {
   120  		t.Fatalf("body mismatch for HEAD request: got %q, want empty", body)
   121  	}
   122  	if got, want := resp.Header.Get("Content-Length"), fmt.Sprint(len(file)); got != want {
   123  		t.Fatalf("Content-Length mismatch for HEAD request: got %v, want %v", got, want)
   124  	}
   125  
   126  	// Range tests
   127  	req.Method = MethodGet
   128  Cases:
   129  	for _, rt := range ServeFileRangeTests {
   130  		if rt.r != "" {
   131  			req.Header.Set("Range", rt.r)
   132  		}
   133  		resp, body := getBody(t, fmt.Sprintf("range test %q", rt.r), req, c)
   134  		if resp.StatusCode != rt.code {
   135  			t.Errorf("range=%q: StatusCode=%d, want %d", rt.r, resp.StatusCode, rt.code)
   136  		}
   137  		if rt.code == StatusRequestedRangeNotSatisfiable {
   138  			continue
   139  		}
   140  		wantContentRange := ""
   141  		if len(rt.ranges) == 1 {
   142  			rng := rt.ranges[0]
   143  			wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
   144  		}
   145  		cr := resp.Header.Get("Content-Range")
   146  		if cr != wantContentRange {
   147  			t.Errorf("range=%q: Content-Range = %q, want %q", rt.r, cr, wantContentRange)
   148  		}
   149  		ct := resp.Header.Get("Content-Type")
   150  		if len(rt.ranges) == 1 {
   151  			rng := rt.ranges[0]
   152  			wantBody := file[rng.start:rng.end]
   153  			if !bytes.Equal(body, wantBody) {
   154  				t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
   155  			}
   156  			if strings.HasPrefix(ct, "multipart/byteranges") {
   157  				t.Errorf("range=%q content-type = %q; unexpected multipart/byteranges", rt.r, ct)
   158  			}
   159  		}
   160  		if len(rt.ranges) > 1 {
   161  			typ, params, err := mime.ParseMediaType(ct)
   162  			if err != nil {
   163  				t.Errorf("range=%q content-type = %q; %v", rt.r, ct, err)
   164  				continue
   165  			}
   166  			if typ != "multipart/byteranges" {
   167  				t.Errorf("range=%q content-type = %q; want multipart/byteranges", rt.r, typ)
   168  				continue
   169  			}
   170  			if params["boundary"] == "" {
   171  				t.Errorf("range=%q content-type = %q; lacks boundary", rt.r, ct)
   172  				continue
   173  			}
   174  			if g, w := resp.ContentLength, int64(len(body)); g != w {
   175  				t.Errorf("range=%q Content-Length = %d; want %d", rt.r, g, w)
   176  				continue
   177  			}
   178  			mr := multipart.NewReader(bytes.NewReader(body), params["boundary"])
   179  			for ri, rng := range rt.ranges {
   180  				part, err := mr.NextPart()
   181  				if err != nil {
   182  					t.Errorf("range=%q, reading part index %d: %v", rt.r, ri, err)
   183  					continue Cases
   184  				}
   185  				wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
   186  				if g, w := part.Header.Get("Content-Range"), wantContentRange; g != w {
   187  					t.Errorf("range=%q: part Content-Range = %q; want %q", rt.r, g, w)
   188  				}
   189  				body, err := io.ReadAll(part)
   190  				if err != nil {
   191  					t.Errorf("range=%q, reading part index %d body: %v", rt.r, ri, err)
   192  					continue Cases
   193  				}
   194  				wantBody := file[rng.start:rng.end]
   195  				if !bytes.Equal(body, wantBody) {
   196  					t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
   197  				}
   198  			}
   199  			_, err = mr.NextPart()
   200  			if err != io.EOF {
   201  				t.Errorf("range=%q; expected final error io.EOF; got %v", rt.r, err)
   202  			}
   203  		}
   204  	}
   205  }
   206  
   207  func TestServeFile_DotDot(t *testing.T) {
   208  	tests := []struct {
   209  		req        string
   210  		wantStatus int
   211  	}{
   212  		{"/testdata/file", 200},
   213  		{"/../file", 400},
   214  		{"/..", 400},
   215  		{"/../", 400},
   216  		{"/../foo", 400},
   217  		{"/..\\foo", 400},
   218  		{"/file/a", 200},
   219  		{"/file/a..", 200},
   220  		{"/file/a/..", 400},
   221  		{"/file/a\\..", 400},
   222  	}
   223  	for _, tt := range tests {
   224  		req, err := ReadRequest(bufio.NewReader(strings.NewReader("GET " + tt.req + " HTTP/1.1\r\nHost: foo\r\n\r\n")))
   225  		if err != nil {
   226  			t.Errorf("bad request %q: %v", tt.req, err)
   227  			continue
   228  		}
   229  		rec := httptest.NewRecorder()
   230  		ServeFile(rec, req, "testdata/file")
   231  		if rec.Code != tt.wantStatus {
   232  			t.Errorf("for request %q, status = %d; want %d", tt.req, rec.Code, tt.wantStatus)
   233  		}
   234  	}
   235  }
   236  
   237  // Tests that this doesn't panic. (Issue 30165)
   238  func TestServeFileDirPanicEmptyPath(t *testing.T) {
   239  	rec := httptest.NewRecorder()
   240  	req := httptest.NewRequest("GET", "/", nil)
   241  	req.URL.Path = ""
   242  	ServeFile(rec, req, "testdata")
   243  	res := rec.Result()
   244  	if res.StatusCode != 301 {
   245  		t.Errorf("code = %v; want 301", res.Status)
   246  	}
   247  }
   248  
   249  // Tests that ranges are ignored with serving empty content. (Issue 54794)
   250  func TestServeContentWithEmptyContentIgnoreRanges(t *testing.T) {
   251  	for _, r := range []string{
   252  		"bytes=0-128",
   253  		"bytes=1-",
   254  	} {
   255  		rec := httptest.NewRecorder()
   256  		req := httptest.NewRequest("GET", "/", nil)
   257  		req.Header.Set("Range", r)
   258  		ServeContent(rec, req, "nothing", time.Now(), bytes.NewReader(nil))
   259  		res := rec.Result()
   260  		if res.StatusCode != 200 {
   261  			t.Errorf("code = %v; want 200", res.Status)
   262  		}
   263  		bodyLen := rec.Body.Len()
   264  		if bodyLen != 0 {
   265  			t.Errorf("body.Len() = %v; want 0", res.Status)
   266  		}
   267  	}
   268  }
   269  
   270  var fsRedirectTestData = []struct {
   271  	original, redirect string
   272  }{
   273  	{"/test/index.html", "/test/"},
   274  	{"/test/testdata", "/test/testdata/"},
   275  	{"/test/testdata/file/", "/test/testdata/file"},
   276  }
   277  
   278  func TestFSRedirect(t *testing.T) { run(t, testFSRedirect) }
   279  func testFSRedirect(t *testing.T, mode testMode) {
   280  	ts := newClientServerTest(t, mode, StripPrefix("/test", FileServer(Dir(".")))).ts
   281  
   282  	for _, data := range fsRedirectTestData {
   283  		res, err := ts.Client().Get(ts.URL + data.original)
   284  		if err != nil {
   285  			t.Fatal(err)
   286  		}
   287  		res.Body.Close()
   288  		if g, e := res.Request.URL.Path, data.redirect; g != e {
   289  			t.Errorf("redirect from %s: got %s, want %s", data.original, g, e)
   290  		}
   291  	}
   292  }
   293  
   294  type testFileSystem struct {
   295  	open func(name string) (File, error)
   296  }
   297  
   298  func (fs *testFileSystem) Open(name string) (File, error) {
   299  	return fs.open(name)
   300  }
   301  
   302  func TestFileServerCleans(t *testing.T) {
   303  	defer afterTest(t)
   304  	ch := make(chan string, 1)
   305  	fs := FileServer(&testFileSystem{func(name string) (File, error) {
   306  		ch <- name
   307  		return nil, errors.New("file does not exist")
   308  	}})
   309  	tests := []struct {
   310  		reqPath, openArg string
   311  	}{
   312  		{"/foo.txt", "/foo.txt"},
   313  		{"//foo.txt", "/foo.txt"},
   314  		{"/../foo.txt", "/foo.txt"},
   315  	}
   316  	req, _ := NewRequest("GET", "http://example.com", nil)
   317  	for n, test := range tests {
   318  		rec := httptest.NewRecorder()
   319  		req.URL.Path = test.reqPath
   320  		fs.ServeHTTP(rec, req)
   321  		if got := <-ch; got != test.openArg {
   322  			t.Errorf("test %d: got %q, want %q", n, got, test.openArg)
   323  		}
   324  	}
   325  }
   326  
   327  func TestFileServerEscapesNames(t *testing.T) { run(t, testFileServerEscapesNames) }
   328  func testFileServerEscapesNames(t *testing.T, mode testMode) {
   329  	const dirListPrefix = "<!doctype html>\n<meta name=\"viewport\" content=\"width=device-width\">\n<pre>\n"
   330  	const dirListSuffix = "\n</pre>\n"
   331  	tests := []struct {
   332  		name, escaped string
   333  	}{
   334  		{`simple_name`, `<a href="simple_name">simple_name</a>`},
   335  		{`"'<>&`, `<a href="%22%27%3C%3E&">&#34;&#39;&lt;&gt;&amp;</a>`},
   336  		{`?foo=bar#baz`, `<a href="%3Ffoo=bar%23baz">?foo=bar#baz</a>`},
   337  		{`<combo>?foo`, `<a href="%3Ccombo%3E%3Ffoo">&lt;combo&gt;?foo</a>`},
   338  		{`foo:bar`, `<a href="./foo:bar">foo:bar</a>`},
   339  	}
   340  
   341  	// We put each test file in its own directory in the fakeFS so we can look at it in isolation.
   342  	fs := make(fakeFS)
   343  	for i, test := range tests {
   344  		testFile := &fakeFileInfo{basename: test.name}
   345  		fs[fmt.Sprintf("/%d", i)] = &fakeFileInfo{
   346  			dir:     true,
   347  			modtime: time.Unix(1000000000, 0).UTC(),
   348  			ents:    []*fakeFileInfo{testFile},
   349  		}
   350  		fs[fmt.Sprintf("/%d/%s", i, test.name)] = testFile
   351  	}
   352  
   353  	ts := newClientServerTest(t, mode, FileServer(&fs)).ts
   354  	for i, test := range tests {
   355  		url := fmt.Sprintf("%s/%d", ts.URL, i)
   356  		res, err := ts.Client().Get(url)
   357  		if err != nil {
   358  			t.Fatalf("test %q: Get: %v", test.name, err)
   359  		}
   360  		b, err := io.ReadAll(res.Body)
   361  		if err != nil {
   362  			t.Fatalf("test %q: read Body: %v", test.name, err)
   363  		}
   364  		s := string(b)
   365  		if !strings.HasPrefix(s, dirListPrefix) || !strings.HasSuffix(s, dirListSuffix) {
   366  			t.Errorf("test %q: listing dir, full output is %q, want prefix %q and suffix %q", test.name, s, dirListPrefix, dirListSuffix)
   367  		}
   368  		if trimmed := strings.TrimSuffix(strings.TrimPrefix(s, dirListPrefix), dirListSuffix); trimmed != test.escaped {
   369  			t.Errorf("test %q: listing dir, filename escaped to %q, want %q", test.name, trimmed, test.escaped)
   370  		}
   371  		res.Body.Close()
   372  	}
   373  }
   374  
   375  func TestFileServerSortsNames(t *testing.T) { run(t, testFileServerSortsNames) }
   376  func testFileServerSortsNames(t *testing.T, mode testMode) {
   377  	const contents = "I am a fake file"
   378  	dirMod := time.Unix(123, 0).UTC()
   379  	fileMod := time.Unix(1000000000, 0).UTC()
   380  	fs := fakeFS{
   381  		"/": &fakeFileInfo{
   382  			dir:     true,
   383  			modtime: dirMod,
   384  			ents: []*fakeFileInfo{
   385  				{
   386  					basename: "b",
   387  					modtime:  fileMod,
   388  					contents: contents,
   389  				},
   390  				{
   391  					basename: "a",
   392  					modtime:  fileMod,
   393  					contents: contents,
   394  				},
   395  			},
   396  		},
   397  	}
   398  
   399  	ts := newClientServerTest(t, mode, FileServer(&fs)).ts
   400  
   401  	res, err := ts.Client().Get(ts.URL)
   402  	if err != nil {
   403  		t.Fatalf("Get: %v", err)
   404  	}
   405  	defer res.Body.Close()
   406  
   407  	b, err := io.ReadAll(res.Body)
   408  	if err != nil {
   409  		t.Fatalf("read Body: %v", err)
   410  	}
   411  	s := string(b)
   412  	if !strings.Contains(s, "<a href=\"a\">a</a>\n<a href=\"b\">b</a>") {
   413  		t.Errorf("output appears to be unsorted:\n%s", s)
   414  	}
   415  }
   416  
   417  func mustRemoveAll(dir string) {
   418  	err := os.RemoveAll(dir)
   419  	if err != nil {
   420  		panic(err)
   421  	}
   422  }
   423  
   424  func TestFileServerImplicitLeadingSlash(t *testing.T) { run(t, testFileServerImplicitLeadingSlash) }
   425  func testFileServerImplicitLeadingSlash(t *testing.T, mode testMode) {
   426  	tempDir := t.TempDir()
   427  	if err := os.WriteFile(filepath.Join(tempDir, "foo.txt"), []byte("Hello world"), 0644); err != nil {
   428  		t.Fatalf("WriteFile: %v", err)
   429  	}
   430  	ts := newClientServerTest(t, mode, StripPrefix("/bar/", FileServer(Dir(tempDir)))).ts
   431  	get := func(suffix string) string {
   432  		res, err := ts.Client().Get(ts.URL + suffix)
   433  		if err != nil {
   434  			t.Fatalf("Get %s: %v", suffix, err)
   435  		}
   436  		b, err := io.ReadAll(res.Body)
   437  		if err != nil {
   438  			t.Fatalf("ReadAll %s: %v", suffix, err)
   439  		}
   440  		res.Body.Close()
   441  		return string(b)
   442  	}
   443  	if s := get("/bar/"); !strings.Contains(s, ">foo.txt<") {
   444  		t.Logf("expected a directory listing with foo.txt, got %q", s)
   445  	}
   446  	if s := get("/bar/foo.txt"); s != "Hello world" {
   447  		t.Logf("expected %q, got %q", "Hello world", s)
   448  	}
   449  }
   450  
   451  func TestDirJoin(t *testing.T) {
   452  	if runtime.GOOS == "windows" {
   453  		t.Skip("skipping test on windows")
   454  	}
   455  	wfi, err := os.Stat("/etc/hosts")
   456  	if err != nil {
   457  		t.Skip("skipping test; no /etc/hosts file")
   458  	}
   459  	test := func(d Dir, name string) {
   460  		f, err := d.Open(name)
   461  		if err != nil {
   462  			t.Fatalf("open of %s: %v", name, err)
   463  		}
   464  		defer f.Close()
   465  		gfi, err := f.Stat()
   466  		if err != nil {
   467  			t.Fatalf("stat of %s: %v", name, err)
   468  		}
   469  		if !os.SameFile(gfi, wfi) {
   470  			t.Errorf("%s got different file", name)
   471  		}
   472  	}
   473  	test(Dir("/etc/"), "/hosts")
   474  	test(Dir("/etc/"), "hosts")
   475  	test(Dir("/etc/"), "../../../../hosts")
   476  	test(Dir("/etc"), "/hosts")
   477  	test(Dir("/etc"), "hosts")
   478  	test(Dir("/etc"), "../../../../hosts")
   479  
   480  	// Not really directories, but since we use this trick in
   481  	// ServeFile, test it:
   482  	test(Dir("/etc/hosts"), "")
   483  	test(Dir("/etc/hosts"), "/")
   484  	test(Dir("/etc/hosts"), "../")
   485  }
   486  
   487  func TestEmptyDirOpenCWD(t *testing.T) {
   488  	test := func(d Dir) {
   489  		name := "fs_test.go"
   490  		f, err := d.Open(name)
   491  		if err != nil {
   492  			t.Fatalf("open of %s: %v", name, err)
   493  		}
   494  		defer f.Close()
   495  	}
   496  	test(Dir(""))
   497  	test(Dir("."))
   498  	test(Dir("./"))
   499  }
   500  
   501  func TestServeFileContentType(t *testing.T) { run(t, testServeFileContentType) }
   502  func testServeFileContentType(t *testing.T, mode testMode) {
   503  	const ctype = "icecream/chocolate"
   504  	ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
   505  		switch r.FormValue("override") {
   506  		case "1":
   507  			w.Header().Set("Content-Type", ctype)
   508  		case "2":
   509  			// Explicitly inhibit sniffing.
   510  			w.Header()["Content-Type"] = []string{}
   511  		}
   512  		ServeFile(w, r, "testdata/file")
   513  	})).ts
   514  	get := func(override string, want []string) {
   515  		resp, err := ts.Client().Get(ts.URL + "?override=" + override)
   516  		if err != nil {
   517  			t.Fatal(err)
   518  		}
   519  		if h := resp.Header["Content-Type"]; !reflect.DeepEqual(h, want) {
   520  			t.Errorf("Content-Type mismatch: got %v, want %v", h, want)
   521  		}
   522  		resp.Body.Close()
   523  	}
   524  	get("0", []string{"text/plain; charset=utf-8"})
   525  	get("1", []string{ctype})
   526  	get("2", nil)
   527  }
   528  
   529  func TestServeFileMimeType(t *testing.T) { run(t, testServeFileMimeType) }
   530  func testServeFileMimeType(t *testing.T, mode testMode) {
   531  	ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
   532  		ServeFile(w, r, "testdata/style.css")
   533  	})).ts
   534  	resp, err := ts.Client().Get(ts.URL)
   535  	if err != nil {
   536  		t.Fatal(err)
   537  	}
   538  	resp.Body.Close()
   539  	want := "text/css; charset=utf-8"
   540  	if h := resp.Header.Get("Content-Type"); h != want {
   541  		t.Errorf("Content-Type mismatch: got %q, want %q", h, want)
   542  	}
   543  }
   544  
   545  func TestServeFileFromCWD(t *testing.T) { run(t, testServeFileFromCWD) }
   546  func testServeFileFromCWD(t *testing.T, mode testMode) {
   547  	ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
   548  		ServeFile(w, r, "fs_test.go")
   549  	})).ts
   550  	r, err := ts.Client().Get(ts.URL)
   551  	if err != nil {
   552  		t.Fatal(err)
   553  	}
   554  	r.Body.Close()
   555  	if r.StatusCode != 200 {
   556  		t.Fatalf("expected 200 OK, got %s", r.Status)
   557  	}
   558  }
   559  
   560  // Issue 13996
   561  func TestServeDirWithoutTrailingSlash(t *testing.T) { run(t, testServeDirWithoutTrailingSlash) }
   562  func testServeDirWithoutTrailingSlash(t *testing.T, mode testMode) {
   563  	e := "/testdata/"
   564  	ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
   565  		ServeFile(w, r, ".")
   566  	})).ts
   567  	r, err := ts.Client().Get(ts.URL + "/testdata")
   568  	if err != nil {
   569  		t.Fatal(err)
   570  	}
   571  	r.Body.Close()
   572  	if g := r.Request.URL.Path; g != e {
   573  		t.Errorf("got %s, want %s", g, e)
   574  	}
   575  }
   576  
   577  // Tests that ServeFile doesn't add a Content-Length if a Content-Encoding is
   578  // specified.
   579  func TestServeFileWithContentEncoding(t *testing.T) { run(t, testServeFileWithContentEncoding) }
   580  func testServeFileWithContentEncoding(t *testing.T, mode testMode) {
   581  	cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
   582  		w.Header().Set("Content-Encoding", "foo")
   583  		ServeFile(w, r, "testdata/file")
   584  
   585  		// Because the testdata is so small, it would fit in
   586  		// both the h1 and h2 Server's write buffers. For h1,
   587  		// sendfile is used, though, forcing a header flush at
   588  		// the io.Copy. http2 doesn't do a header flush so
   589  		// buffers all 11 bytes and then adds its own
   590  		// Content-Length. To prevent the Server's
   591  		// Content-Length and test ServeFile only, flush here.
   592  		w.(Flusher).Flush()
   593  	}))
   594  	resp, err := cst.c.Get(cst.ts.URL)
   595  	if err != nil {
   596  		t.Fatal(err)
   597  	}
   598  	resp.Body.Close()
   599  	if g, e := resp.ContentLength, int64(-1); g != e {
   600  		t.Errorf("Content-Length mismatch: got %d, want %d", g, e)
   601  	}
   602  }
   603  
   604  // Tests that ServeFile does not generate representation metadata when
   605  // file has not been modified, as per RFC 7232 section 4.1.
   606  func TestServeFileNotModified(t *testing.T) { run(t, testServeFileNotModified) }
   607  func testServeFileNotModified(t *testing.T, mode testMode) {
   608  	cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
   609  		w.Header().Set("Content-Type", "application/json")
   610  		w.Header().Set("Content-Encoding", "foo")
   611  		w.Header().Set("Etag", `"123"`)
   612  		ServeFile(w, r, "testdata/file")
   613  
   614  		// Because the testdata is so small, it would fit in
   615  		// both the h1 and h2 Server's write buffers. For h1,
   616  		// sendfile is used, though, forcing a header flush at
   617  		// the io.Copy. http2 doesn't do a header flush so
   618  		// buffers all 11 bytes and then adds its own
   619  		// Content-Length. To prevent the Server's
   620  		// Content-Length and test ServeFile only, flush here.
   621  		w.(Flusher).Flush()
   622  	}))
   623  	req, err := NewRequest("GET", cst.ts.URL, nil)
   624  	if err != nil {
   625  		t.Fatal(err)
   626  	}
   627  	req.Header.Set("If-None-Match", `"123"`)
   628  	resp, err := cst.c.Do(req)
   629  	if err != nil {
   630  		t.Fatal(err)
   631  	}
   632  	b, err := io.ReadAll(resp.Body)
   633  	resp.Body.Close()
   634  	if err != nil {
   635  		t.Fatal("reading Body:", err)
   636  	}
   637  	if len(b) != 0 {
   638  		t.Errorf("non-empty body")
   639  	}
   640  	if g, e := resp.StatusCode, StatusNotModified; g != e {
   641  		t.Errorf("status mismatch: got %d, want %d", g, e)
   642  	}
   643  	// HTTP1 transport sets ContentLength to 0.
   644  	if g, e1, e2 := resp.ContentLength, int64(-1), int64(0); g != e1 && g != e2 {
   645  		t.Errorf("Content-Length mismatch: got %d, want %d or %d", g, e1, e2)
   646  	}
   647  	if resp.Header.Get("Content-Type") != "" {
   648  		t.Errorf("Content-Type present, but it should not be")
   649  	}
   650  	if resp.Header.Get("Content-Encoding") != "" {
   651  		t.Errorf("Content-Encoding present, but it should not be")
   652  	}
   653  }
   654  
   655  func TestServeIndexHtml(t *testing.T) { run(t, testServeIndexHtml) }
   656  func testServeIndexHtml(t *testing.T, mode testMode) {
   657  	for i := 0; i < 2; i++ {
   658  		var h Handler
   659  		var name string
   660  		switch i {
   661  		case 0:
   662  			h = FileServer(Dir("."))
   663  			name = "Dir"
   664  		case 1:
   665  			h = FileServer(FS(os.DirFS(".")))
   666  			name = "DirFS"
   667  		}
   668  		t.Run(name, func(t *testing.T) {
   669  			const want = "index.html says hello\n"
   670  			ts := newClientServerTest(t, mode, h).ts
   671  
   672  			for _, path := range []string{"/testdata/", "/testdata/index.html"} {
   673  				res, err := ts.Client().Get(ts.URL + path)
   674  				if err != nil {
   675  					t.Fatal(err)
   676  				}
   677  				b, err := io.ReadAll(res.Body)
   678  				if err != nil {
   679  					t.Fatal("reading Body:", err)
   680  				}
   681  				if s := string(b); s != want {
   682  					t.Errorf("for path %q got %q, want %q", path, s, want)
   683  				}
   684  				res.Body.Close()
   685  			}
   686  		})
   687  	}
   688  }
   689  
   690  func TestServeIndexHtmlFS(t *testing.T) { run(t, testServeIndexHtmlFS) }
   691  func testServeIndexHtmlFS(t *testing.T, mode testMode) {
   692  	const want = "index.html says hello\n"
   693  	ts := newClientServerTest(t, mode, FileServer(Dir("."))).ts
   694  	defer ts.Close()
   695  
   696  	for _, path := range []string{"/testdata/", "/testdata/index.html"} {
   697  		res, err := ts.Client().Get(ts.URL + path)
   698  		if err != nil {
   699  			t.Fatal(err)
   700  		}
   701  		b, err := io.ReadAll(res.Body)
   702  		if err != nil {
   703  			t.Fatal("reading Body:", err)
   704  		}
   705  		if s := string(b); s != want {
   706  			t.Errorf("for path %q got %q, want %q", path, s, want)
   707  		}
   708  		res.Body.Close()
   709  	}
   710  }
   711  
   712  func TestFileServerZeroByte(t *testing.T) { run(t, testFileServerZeroByte) }
   713  func testFileServerZeroByte(t *testing.T, mode testMode) {
   714  	ts := newClientServerTest(t, mode, FileServer(Dir("."))).ts
   715  
   716  	c, err := net.Dial("tcp", ts.Listener.Addr().String())
   717  	if err != nil {
   718  		t.Fatal(err)
   719  	}
   720  	defer c.Close()
   721  	_, err = fmt.Fprintf(c, "GET /..\x00 HTTP/1.0\r\n\r\n")
   722  	if err != nil {
   723  		t.Fatal(err)
   724  	}
   725  	var got bytes.Buffer
   726  	bufr := bufio.NewReader(io.TeeReader(c, &got))
   727  	res, err := ReadResponse(bufr, nil)
   728  	if err != nil {
   729  		t.Fatal("ReadResponse: ", err)
   730  	}
   731  	if res.StatusCode == 200 {
   732  		t.Errorf("got status 200; want an error. Body is:\n%s", got.Bytes())
   733  	}
   734  }
   735  
   736  func TestFileServerNamesEscape(t *testing.T) { run(t, testFileServerNamesEscape) }
   737  func testFileServerNamesEscape(t *testing.T, mode testMode) {
   738  	ts := newClientServerTest(t, mode, FileServer(Dir("testdata"))).ts
   739  	for _, path := range []string{
   740  		"/../testdata/file",
   741  		"/NUL", // don't read from device files on Windows
   742  	} {
   743  		res, err := ts.Client().Get(ts.URL + path)
   744  		if err != nil {
   745  			t.Fatal(err)
   746  		}
   747  		res.Body.Close()
   748  		if res.StatusCode < 400 || res.StatusCode > 599 {
   749  			t.Errorf("Get(%q): got status %v, want 4xx or 5xx", path, res.StatusCode)
   750  		}
   751  
   752  	}
   753  }
   754  
   755  type fakeFileInfo struct {
   756  	dir      bool
   757  	basename string
   758  	modtime  time.Time
   759  	ents     []*fakeFileInfo
   760  	contents string
   761  	err      error
   762  }
   763  
   764  func (f *fakeFileInfo) Name() string       { return f.basename }
   765  func (f *fakeFileInfo) Sys() any           { return nil }
   766  func (f *fakeFileInfo) ModTime() time.Time { return f.modtime }
   767  func (f *fakeFileInfo) IsDir() bool        { return f.dir }
   768  func (f *fakeFileInfo) Size() int64        { return int64(len(f.contents)) }
   769  func (f *fakeFileInfo) Mode() fs.FileMode {
   770  	if f.dir {
   771  		return 0755 | fs.ModeDir
   772  	}
   773  	return 0644
   774  }
   775  
   776  func (f *fakeFileInfo) String() string {
   777  	return fs.FormatFileInfo(f)
   778  }
   779  
   780  type fakeFile struct {
   781  	io.ReadSeeker
   782  	fi     *fakeFileInfo
   783  	path   string // as opened
   784  	entpos int
   785  }
   786  
   787  func (f *fakeFile) Close() error               { return nil }
   788  func (f *fakeFile) Stat() (fs.FileInfo, error) { return f.fi, nil }
   789  func (f *fakeFile) Readdir(count int) ([]fs.FileInfo, error) {
   790  	if !f.fi.dir {
   791  		return nil, fs.ErrInvalid
   792  	}
   793  	var fis []fs.FileInfo
   794  
   795  	limit := f.entpos + count
   796  	if count <= 0 || limit > len(f.fi.ents) {
   797  		limit = len(f.fi.ents)
   798  	}
   799  	for ; f.entpos < limit; f.entpos++ {
   800  		fis = append(fis, f.fi.ents[f.entpos])
   801  	}
   802  
   803  	if len(fis) == 0 && count > 0 {
   804  		return fis, io.EOF
   805  	} else {
   806  		return fis, nil
   807  	}
   808  }
   809  
   810  type fakeFS map[string]*fakeFileInfo
   811  
   812  func (fsys fakeFS) Open(name string) (File, error) {
   813  	name = path.Clean(name)
   814  	f, ok := fsys[name]
   815  	if !ok {
   816  		return nil, fs.ErrNotExist
   817  	}
   818  	if f.err != nil {
   819  		return nil, f.err
   820  	}
   821  	return &fakeFile{ReadSeeker: strings.NewReader(f.contents), fi: f, path: name}, nil
   822  }
   823  
   824  func TestDirectoryIfNotModified(t *testing.T) { run(t, testDirectoryIfNotModified) }
   825  func testDirectoryIfNotModified(t *testing.T, mode testMode) {
   826  	const indexContents = "I am a fake index.html file"
   827  	fileMod := time.Unix(1000000000, 0).UTC()
   828  	fileModStr := fileMod.Format(TimeFormat)
   829  	dirMod := time.Unix(123, 0).UTC()
   830  	indexFile := &fakeFileInfo{
   831  		basename: "index.html",
   832  		modtime:  fileMod,
   833  		contents: indexContents,
   834  	}
   835  	fs := fakeFS{
   836  		"/": &fakeFileInfo{
   837  			dir:     true,
   838  			modtime: dirMod,
   839  			ents:    []*fakeFileInfo{indexFile},
   840  		},
   841  		"/index.html": indexFile,
   842  	}
   843  
   844  	ts := newClientServerTest(t, mode, FileServer(fs)).ts
   845  
   846  	res, err := ts.Client().Get(ts.URL)
   847  	if err != nil {
   848  		t.Fatal(err)
   849  	}
   850  	b, err := io.ReadAll(res.Body)
   851  	if err != nil {
   852  		t.Fatal(err)
   853  	}
   854  	if string(b) != indexContents {
   855  		t.Fatalf("Got body %q; want %q", b, indexContents)
   856  	}
   857  	res.Body.Close()
   858  
   859  	lastMod := res.Header.Get("Last-Modified")
   860  	if lastMod != fileModStr {
   861  		t.Fatalf("initial Last-Modified = %q; want %q", lastMod, fileModStr)
   862  	}
   863  
   864  	req, _ := NewRequest("GET", ts.URL, nil)
   865  	req.Header.Set("If-Modified-Since", lastMod)
   866  
   867  	c := ts.Client()
   868  	res, err = c.Do(req)
   869  	if err != nil {
   870  		t.Fatal(err)
   871  	}
   872  	if res.StatusCode != 304 {
   873  		t.Fatalf("Code after If-Modified-Since request = %v; want 304", res.StatusCode)
   874  	}
   875  	res.Body.Close()
   876  
   877  	// Advance the index.html file's modtime, but not the directory's.
   878  	indexFile.modtime = indexFile.modtime.Add(1 * time.Hour)
   879  
   880  	res, err = c.Do(req)
   881  	if err != nil {
   882  		t.Fatal(err)
   883  	}
   884  	if res.StatusCode != 200 {
   885  		t.Fatalf("Code after second If-Modified-Since request = %v; want 200; res is %#v", res.StatusCode, res)
   886  	}
   887  	res.Body.Close()
   888  }
   889  
   890  func mustStat(t *testing.T, fileName string) fs.FileInfo {
   891  	fi, err := os.Stat(fileName)
   892  	if err != nil {
   893  		t.Fatal(err)
   894  	}
   895  	return fi
   896  }
   897  
   898  func TestServeContent(t *testing.T) { run(t, testServeContent) }
   899  func testServeContent(t *testing.T, mode testMode) {
   900  	type serveParam struct {
   901  		name        string
   902  		modtime     time.Time
   903  		content     io.ReadSeeker
   904  		contentType string
   905  		etag        string
   906  	}
   907  	servec := make(chan serveParam, 1)
   908  	ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
   909  		p := <-servec
   910  		if p.etag != "" {
   911  			w.Header().Set("ETag", p.etag)
   912  		}
   913  		if p.contentType != "" {
   914  			w.Header().Set("Content-Type", p.contentType)
   915  		}
   916  		ServeContent(w, r, p.name, p.modtime, p.content)
   917  	})).ts
   918  
   919  	type testCase struct {
   920  		// One of file or content must be set:
   921  		file    string
   922  		content io.ReadSeeker
   923  
   924  		modtime          time.Time
   925  		serveETag        string // optional
   926  		serveContentType string // optional
   927  		reqHeader        map[string]string
   928  		wantLastMod      string
   929  		wantContentType  string
   930  		wantContentRange string
   931  		wantStatus       int
   932  	}
   933  	htmlModTime := mustStat(t, "testdata/index.html").ModTime()
   934  	tests := map[string]testCase{
   935  		"no_last_modified": {
   936  			file:            "testdata/style.css",
   937  			wantContentType: "text/css; charset=utf-8",
   938  			wantStatus:      200,
   939  		},
   940  		"with_last_modified": {
   941  			file:            "testdata/index.html",
   942  			wantContentType: "text/html; charset=utf-8",
   943  			modtime:         htmlModTime,
   944  			wantLastMod:     htmlModTime.UTC().Format(TimeFormat),
   945  			wantStatus:      200,
   946  		},
   947  		"not_modified_modtime": {
   948  			file:      "testdata/style.css",
   949  			serveETag: `"foo"`, // Last-Modified sent only when no ETag
   950  			modtime:   htmlModTime,
   951  			reqHeader: map[string]string{
   952  				"If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
   953  			},
   954  			wantStatus: 304,
   955  		},
   956  		"not_modified_modtime_with_contenttype": {
   957  			file:             "testdata/style.css",
   958  			serveContentType: "text/css", // explicit content type
   959  			serveETag:        `"foo"`,    // Last-Modified sent only when no ETag
   960  			modtime:          htmlModTime,
   961  			reqHeader: map[string]string{
   962  				"If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
   963  			},
   964  			wantStatus: 304,
   965  		},
   966  		"not_modified_etag": {
   967  			file:      "testdata/style.css",
   968  			serveETag: `"foo"`,
   969  			reqHeader: map[string]string{
   970  				"If-None-Match": `"foo"`,
   971  			},
   972  			wantStatus: 304,
   973  		},
   974  		"not_modified_etag_no_seek": {
   975  			content:   panicOnSeek{nil}, // should never be called
   976  			serveETag: `W/"foo"`,        // If-None-Match uses weak ETag comparison
   977  			reqHeader: map[string]string{
   978  				"If-None-Match": `"baz", W/"foo"`,
   979  			},
   980  			wantStatus: 304,
   981  		},
   982  		"if_none_match_mismatch": {
   983  			file:      "testdata/style.css",
   984  			serveETag: `"foo"`,
   985  			reqHeader: map[string]string{
   986  				"If-None-Match": `"Foo"`,
   987  			},
   988  			wantStatus:      200,
   989  			wantContentType: "text/css; charset=utf-8",
   990  		},
   991  		"if_none_match_malformed": {
   992  			file:      "testdata/style.css",
   993  			serveETag: `"foo"`,
   994  			reqHeader: map[string]string{
   995  				"If-None-Match": `,`,
   996  			},
   997  			wantStatus:      200,
   998  			wantContentType: "text/css; charset=utf-8",
   999  		},
  1000  		"range_good": {
  1001  			file:      "testdata/style.css",
  1002  			serveETag: `"A"`,
  1003  			reqHeader: map[string]string{
  1004  				"Range": "bytes=0-4",
  1005  			},
  1006  			wantStatus:       StatusPartialContent,
  1007  			wantContentType:  "text/css; charset=utf-8",
  1008  			wantContentRange: "bytes 0-4/8",
  1009  		},
  1010  		"range_match": {
  1011  			file:      "testdata/style.css",
  1012  			serveETag: `"A"`,
  1013  			reqHeader: map[string]string{
  1014  				"Range":    "bytes=0-4",
  1015  				"If-Range": `"A"`,
  1016  			},
  1017  			wantStatus:       StatusPartialContent,
  1018  			wantContentType:  "text/css; charset=utf-8",
  1019  			wantContentRange: "bytes 0-4/8",
  1020  		},
  1021  		"range_match_weak_etag": {
  1022  			file:      "testdata/style.css",
  1023  			serveETag: `W/"A"`,
  1024  			reqHeader: map[string]string{
  1025  				"Range":    "bytes=0-4",
  1026  				"If-Range": `W/"A"`,
  1027  			},
  1028  			wantStatus:      200,
  1029  			wantContentType: "text/css; charset=utf-8",
  1030  		},
  1031  		"range_no_overlap": {
  1032  			file:      "testdata/style.css",
  1033  			serveETag: `"A"`,
  1034  			reqHeader: map[string]string{
  1035  				"Range": "bytes=10-20",
  1036  			},
  1037  			wantStatus:       StatusRequestedRangeNotSatisfiable,
  1038  			wantContentType:  "text/plain; charset=utf-8",
  1039  			wantContentRange: "bytes */8",
  1040  		},
  1041  		// An If-Range resource for entity "A", but entity "B" is now current.
  1042  		// The Range request should be ignored.
  1043  		"range_no_match": {
  1044  			file:      "testdata/style.css",
  1045  			serveETag: `"A"`,
  1046  			reqHeader: map[string]string{
  1047  				"Range":    "bytes=0-4",
  1048  				"If-Range": `"B"`,
  1049  			},
  1050  			wantStatus:      200,
  1051  			wantContentType: "text/css; charset=utf-8",
  1052  		},
  1053  		"range_with_modtime": {
  1054  			file:    "testdata/style.css",
  1055  			modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 /* nanos */, time.UTC),
  1056  			reqHeader: map[string]string{
  1057  				"Range":    "bytes=0-4",
  1058  				"If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
  1059  			},
  1060  			wantStatus:       StatusPartialContent,
  1061  			wantContentType:  "text/css; charset=utf-8",
  1062  			wantContentRange: "bytes 0-4/8",
  1063  			wantLastMod:      "Wed, 25 Jun 2014 17:12:18 GMT",
  1064  		},
  1065  		"range_with_modtime_mismatch": {
  1066  			file:    "testdata/style.css",
  1067  			modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 /* nanos */, time.UTC),
  1068  			reqHeader: map[string]string{
  1069  				"Range":    "bytes=0-4",
  1070  				"If-Range": "Wed, 25 Jun 2014 17:12:19 GMT",
  1071  			},
  1072  			wantStatus:      StatusOK,
  1073  			wantContentType: "text/css; charset=utf-8",
  1074  			wantLastMod:     "Wed, 25 Jun 2014 17:12:18 GMT",
  1075  		},
  1076  		"range_with_modtime_nanos": {
  1077  			file:    "testdata/style.css",
  1078  			modtime: time.Date(2014, 6, 25, 17, 12, 18, 123 /* nanos */, time.UTC),
  1079  			reqHeader: map[string]string{
  1080  				"Range":    "bytes=0-4",
  1081  				"If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
  1082  			},
  1083  			wantStatus:       StatusPartialContent,
  1084  			wantContentType:  "text/css; charset=utf-8",
  1085  			wantContentRange: "bytes 0-4/8",
  1086  			wantLastMod:      "Wed, 25 Jun 2014 17:12:18 GMT",
  1087  		},
  1088  		"unix_zero_modtime": {
  1089  			content:         strings.NewReader("<html>foo"),
  1090  			modtime:         time.Unix(0, 0),
  1091  			wantStatus:      StatusOK,
  1092  			wantContentType: "text/html; charset=utf-8",
  1093  		},
  1094  		"ifmatch_matches": {
  1095  			file:      "testdata/style.css",
  1096  			serveETag: `"A"`,
  1097  			reqHeader: map[string]string{
  1098  				"If-Match": `"Z", "A"`,
  1099  			},
  1100  			wantStatus:      200,
  1101  			wantContentType: "text/css; charset=utf-8",
  1102  		},
  1103  		"ifmatch_star": {
  1104  			file:      "testdata/style.css",
  1105  			serveETag: `"A"`,
  1106  			reqHeader: map[string]string{
  1107  				"If-Match": `*`,
  1108  			},
  1109  			wantStatus:      200,
  1110  			wantContentType: "text/css; charset=utf-8",
  1111  		},
  1112  		"ifmatch_failed": {
  1113  			file:      "testdata/style.css",
  1114  			serveETag: `"A"`,
  1115  			reqHeader: map[string]string{
  1116  				"If-Match": `"B"`,
  1117  			},
  1118  			wantStatus: 412,
  1119  		},
  1120  		"ifmatch_fails_on_weak_etag": {
  1121  			file:      "testdata/style.css",
  1122  			serveETag: `W/"A"`,
  1123  			reqHeader: map[string]string{
  1124  				"If-Match": `W/"A"`,
  1125  			},
  1126  			wantStatus: 412,
  1127  		},
  1128  		"if_unmodified_since_true": {
  1129  			file:    "testdata/style.css",
  1130  			modtime: htmlModTime,
  1131  			reqHeader: map[string]string{
  1132  				"If-Unmodified-Since": htmlModTime.UTC().Format(TimeFormat),
  1133  			},
  1134  			wantStatus:      200,
  1135  			wantContentType: "text/css; charset=utf-8",
  1136  			wantLastMod:     htmlModTime.UTC().Format(TimeFormat),
  1137  		},
  1138  		"if_unmodified_since_false": {
  1139  			file:    "testdata/style.css",
  1140  			modtime: htmlModTime,
  1141  			reqHeader: map[string]string{
  1142  				"If-Unmodified-Since": htmlModTime.Add(-2 * time.Second).UTC().Format(TimeFormat),
  1143  			},
  1144  			wantStatus:  412,
  1145  			wantLastMod: htmlModTime.UTC().Format(TimeFormat),
  1146  		},
  1147  	}
  1148  	for testName, tt := range tests {
  1149  		var content io.ReadSeeker
  1150  		if tt.file != "" {
  1151  			f, err := os.Open(tt.file)
  1152  			if err != nil {
  1153  				t.Fatalf("test %q: %v", testName, err)
  1154  			}
  1155  			defer f.Close()
  1156  			content = f
  1157  		} else {
  1158  			content = tt.content
  1159  		}
  1160  		for _, method := range []string{"GET", "HEAD"} {
  1161  			//restore content in case it is consumed by previous method
  1162  			if content, ok := content.(*strings.Reader); ok {
  1163  				content.Seek(0, io.SeekStart)
  1164  			}
  1165  
  1166  			servec <- serveParam{
  1167  				name:        filepath.Base(tt.file),
  1168  				content:     content,
  1169  				modtime:     tt.modtime,
  1170  				etag:        tt.serveETag,
  1171  				contentType: tt.serveContentType,
  1172  			}
  1173  			req, err := NewRequest(method, ts.URL, nil)
  1174  			if err != nil {
  1175  				t.Fatal(err)
  1176  			}
  1177  			for k, v := range tt.reqHeader {
  1178  				req.Header.Set(k, v)
  1179  			}
  1180  
  1181  			c := ts.Client()
  1182  			res, err := c.Do(req)
  1183  			if err != nil {
  1184  				t.Fatal(err)
  1185  			}
  1186  			io.Copy(io.Discard, res.Body)
  1187  			res.Body.Close()
  1188  			if res.StatusCode != tt.wantStatus {
  1189  				t.Errorf("test %q using %q: got status = %d; want %d", testName, method, res.StatusCode, tt.wantStatus)
  1190  			}
  1191  			if g, e := res.Header.Get("Content-Type"), tt.wantContentType; g != e {
  1192  				t.Errorf("test %q using %q: got content-type = %q, want %q", testName, method, g, e)
  1193  			}
  1194  			if g, e := res.Header.Get("Content-Range"), tt.wantContentRange; g != e {
  1195  				t.Errorf("test %q using %q: got content-range = %q, want %q", testName, method, g, e)
  1196  			}
  1197  			if g, e := res.Header.Get("Last-Modified"), tt.wantLastMod; g != e {
  1198  				t.Errorf("test %q using %q: got last-modified = %q, want %q", testName, method, g, e)
  1199  			}
  1200  		}
  1201  	}
  1202  }
  1203  
  1204  // Issue 12991
  1205  func TestServerFileStatError(t *testing.T) {
  1206  	rec := httptest.NewRecorder()
  1207  	r, _ := NewRequest("GET", "http://foo/", nil)
  1208  	redirect := false
  1209  	name := "file.txt"
  1210  	fs := issue12991FS{}
  1211  	ExportServeFile(rec, r, fs, name, redirect)
  1212  	if body := rec.Body.String(); !strings.Contains(body, "403") || !strings.Contains(body, "Forbidden") {
  1213  		t.Errorf("wanted 403 forbidden message; got: %s", body)
  1214  	}
  1215  }
  1216  
  1217  type issue12991FS struct{}
  1218  
  1219  func (issue12991FS) Open(string) (File, error) { return issue12991File{}, nil }
  1220  
  1221  type issue12991File struct{ File }
  1222  
  1223  func (issue12991File) Stat() (fs.FileInfo, error) { return nil, fs.ErrPermission }
  1224  func (issue12991File) Close() error               { return nil }
  1225  
  1226  func TestFileServerErrorMessages(t *testing.T) { run(t, testFileServerErrorMessages) }
  1227  func testFileServerErrorMessages(t *testing.T, mode testMode) {
  1228  	fs := fakeFS{
  1229  		"/500": &fakeFileInfo{
  1230  			err: errors.New("random error"),
  1231  		},
  1232  		"/403": &fakeFileInfo{
  1233  			err: &fs.PathError{Err: fs.ErrPermission},
  1234  		},
  1235  	}
  1236  	server := FileServer(fs)
  1237  	h := func(w http.ResponseWriter, r *http.Request) {
  1238  		w.Header().Set("Etag", "étude")
  1239  		w.Header().Set("Cache-Control", "yes")
  1240  		w.Header().Set("Content-Type", "awesome")
  1241  		w.Header().Set("Last-Modified", "yesterday")
  1242  		server.ServeHTTP(w, r)
  1243  	}
  1244  	ts := newClientServerTest(t, mode, http.HandlerFunc(h)).ts
  1245  	c := ts.Client()
  1246  	for _, code := range []int{403, 404, 500} {
  1247  		res, err := c.Get(fmt.Sprintf("%s/%d", ts.URL, code))
  1248  		if err != nil {
  1249  			t.Errorf("Error fetching /%d: %v", code, err)
  1250  			continue
  1251  		}
  1252  		res.Body.Close()
  1253  		if res.StatusCode != code {
  1254  			t.Errorf("GET /%d: StatusCode = %d; want %d", code, res.StatusCode, code)
  1255  		}
  1256  		for _, hdr := range []string{"Etag", "Last-Modified", "Cache-Control"} {
  1257  			if v, ok := res.Header[hdr]; ok {
  1258  				t.Errorf("GET /%d: Header[%q] = %q, want not present", code, hdr, v)
  1259  			}
  1260  		}
  1261  	}
  1262  }
  1263  
  1264  // verifies that sendfile is being used on Linux
  1265  func TestLinuxSendfile(t *testing.T) {
  1266  	setParallel(t)
  1267  	defer afterTest(t)
  1268  	if runtime.GOOS != "linux" {
  1269  		t.Skip("skipping; linux-only test")
  1270  	}
  1271  	if _, err := exec.LookPath("strace"); err != nil {
  1272  		t.Skip("skipping; strace not found in path")
  1273  	}
  1274  
  1275  	ln, err := net.Listen("tcp", "127.0.0.1:0")
  1276  	if err != nil {
  1277  		t.Fatal(err)
  1278  	}
  1279  	lnf, err := ln.(*net.TCPListener).File()
  1280  	if err != nil {
  1281  		t.Fatal(err)
  1282  	}
  1283  	defer ln.Close()
  1284  
  1285  	// Attempt to run strace, and skip on failure - this test requires SYS_PTRACE.
  1286  	if err := testenv.Command(t, "strace", "-f", "-q", os.Args[0], "-test.run=^$").Run(); err != nil {
  1287  		t.Skipf("skipping; failed to run strace: %v", err)
  1288  	}
  1289  
  1290  	filename := fmt.Sprintf("1kb-%d", os.Getpid())
  1291  	filepath := path.Join(os.TempDir(), filename)
  1292  
  1293  	if err := os.WriteFile(filepath, bytes.Repeat([]byte{'a'}, 1<<10), 0755); err != nil {
  1294  		t.Fatal(err)
  1295  	}
  1296  	defer os.Remove(filepath)
  1297  
  1298  	var buf strings.Builder
  1299  	child := testenv.Command(t, "strace", "-f", "-q", os.Args[0], "-test.run=^TestLinuxSendfileChild$")
  1300  	child.ExtraFiles = append(child.ExtraFiles, lnf)
  1301  	child.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...)
  1302  	child.Stdout = &buf
  1303  	child.Stderr = &buf
  1304  	if err := child.Start(); err != nil {
  1305  		t.Skipf("skipping; failed to start straced child: %v", err)
  1306  	}
  1307  
  1308  	res, err := Get(fmt.Sprintf("http://%s/%s", ln.Addr(), filename))
  1309  	if err != nil {
  1310  		t.Fatalf("http client error: %v", err)
  1311  	}
  1312  	_, err = io.Copy(io.Discard, res.Body)
  1313  	if err != nil {
  1314  		t.Fatalf("client body read error: %v", err)
  1315  	}
  1316  	res.Body.Close()
  1317  
  1318  	// Force child to exit cleanly.
  1319  	Post(fmt.Sprintf("http://%s/quit", ln.Addr()), "", nil)
  1320  	child.Wait()
  1321  
  1322  	rx := regexp.MustCompile(`\b(n64:)?sendfile(64)?\(`)
  1323  	out := buf.String()
  1324  	if !rx.MatchString(out) {
  1325  		t.Errorf("no sendfile system call found in:\n%s", out)
  1326  	}
  1327  }
  1328  
  1329  func getBody(t *testing.T, testName string, req Request, client *Client) (*Response, []byte) {
  1330  	r, err := client.Do(&req)
  1331  	if err != nil {
  1332  		t.Fatalf("%s: for URL %q, send error: %v", testName, req.URL.String(), err)
  1333  	}
  1334  	b, err := io.ReadAll(r.Body)
  1335  	if err != nil {
  1336  		t.Fatalf("%s: for URL %q, reading body: %v", testName, req.URL.String(), err)
  1337  	}
  1338  	return r, b
  1339  }
  1340  
  1341  // TestLinuxSendfileChild isn't a real test. It's used as a helper process
  1342  // for TestLinuxSendfile.
  1343  func TestLinuxSendfileChild(*testing.T) {
  1344  	if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
  1345  		return
  1346  	}
  1347  	defer os.Exit(0)
  1348  	fd3 := os.NewFile(3, "ephemeral-port-listener")
  1349  	ln, err := net.FileListener(fd3)
  1350  	if err != nil {
  1351  		panic(err)
  1352  	}
  1353  	mux := NewServeMux()
  1354  	mux.Handle("/", FileServer(Dir(os.TempDir())))
  1355  	mux.HandleFunc("/quit", func(ResponseWriter, *Request) {
  1356  		os.Exit(0)
  1357  	})
  1358  	s := &Server{Handler: mux}
  1359  	err = s.Serve(ln)
  1360  	if err != nil {
  1361  		panic(err)
  1362  	}
  1363  }
  1364  
  1365  // Issues 18984, 49552: tests that requests for paths beyond files return not-found errors
  1366  func TestFileServerNotDirError(t *testing.T) {
  1367  	run(t, func(t *testing.T, mode testMode) {
  1368  		t.Run("Dir", func(t *testing.T) {
  1369  			testFileServerNotDirError(t, mode, func(path string) FileSystem { return Dir(path) })
  1370  		})
  1371  		t.Run("FS", func(t *testing.T) {
  1372  			testFileServerNotDirError(t, mode, func(path string) FileSystem { return FS(os.DirFS(path)) })
  1373  		})
  1374  	})
  1375  }
  1376  
  1377  func testFileServerNotDirError(t *testing.T, mode testMode, newfs func(string) FileSystem) {
  1378  	ts := newClientServerTest(t, mode, FileServer(newfs("testdata"))).ts
  1379  
  1380  	res, err := ts.Client().Get(ts.URL + "/index.html/not-a-file")
  1381  	if err != nil {
  1382  		t.Fatal(err)
  1383  	}
  1384  	res.Body.Close()
  1385  	if res.StatusCode != 404 {
  1386  		t.Errorf("StatusCode = %v; want 404", res.StatusCode)
  1387  	}
  1388  
  1389  	test := func(name string, fsys FileSystem) {
  1390  		t.Run(name, func(t *testing.T) {
  1391  			_, err = fsys.Open("/index.html/not-a-file")
  1392  			if err == nil {
  1393  				t.Fatal("err == nil; want != nil")
  1394  			}
  1395  			if !errors.Is(err, fs.ErrNotExist) {
  1396  				t.Errorf("err = %v; errors.Is(err, fs.ErrNotExist) = %v; want true", err,
  1397  					errors.Is(err, fs.ErrNotExist))
  1398  			}
  1399  
  1400  			_, err = fsys.Open("/index.html/not-a-dir/not-a-file")
  1401  			if err == nil {
  1402  				t.Fatal("err == nil; want != nil")
  1403  			}
  1404  			if !errors.Is(err, fs.ErrNotExist) {
  1405  				t.Errorf("err = %v; errors.Is(err, fs.ErrNotExist) = %v; want true", err,
  1406  					errors.Is(err, fs.ErrNotExist))
  1407  			}
  1408  		})
  1409  	}
  1410  
  1411  	absPath, err := filepath.Abs("testdata")
  1412  	if err != nil {
  1413  		t.Fatal("get abs path:", err)
  1414  	}
  1415  
  1416  	test("RelativePath", newfs("testdata"))
  1417  	test("AbsolutePath", newfs(absPath))
  1418  }
  1419  
  1420  func TestFileServerCleanPath(t *testing.T) {
  1421  	tests := []struct {
  1422  		path     string
  1423  		wantCode int
  1424  		wantOpen []string
  1425  	}{
  1426  		{"/", 200, []string{"/", "/index.html"}},
  1427  		{"/dir", 301, []string{"/dir"}},
  1428  		{"/dir/", 200, []string{"/dir", "/dir/index.html"}},
  1429  	}
  1430  	for _, tt := range tests {
  1431  		var log []string
  1432  		rr := httptest.NewRecorder()
  1433  		req, _ := NewRequest("GET", "http://foo.localhost"+tt.path, nil)
  1434  		FileServer(fileServerCleanPathDir{&log}).ServeHTTP(rr, req)
  1435  		if !reflect.DeepEqual(log, tt.wantOpen) {
  1436  			t.Logf("For %s: Opens = %q; want %q", tt.path, log, tt.wantOpen)
  1437  		}
  1438  		if rr.Code != tt.wantCode {
  1439  			t.Logf("For %s: Response code = %d; want %d", tt.path, rr.Code, tt.wantCode)
  1440  		}
  1441  	}
  1442  }
  1443  
  1444  type fileServerCleanPathDir struct {
  1445  	log *[]string
  1446  }
  1447  
  1448  func (d fileServerCleanPathDir) Open(path string) (File, error) {
  1449  	*(d.log) = append(*(d.log), path)
  1450  	if path == "/" || path == "/dir" || path == "/dir/" {
  1451  		// Just return back something that's a directory.
  1452  		return Dir(".").Open(".")
  1453  	}
  1454  	return nil, fs.ErrNotExist
  1455  }
  1456  
  1457  type panicOnSeek struct{ io.ReadSeeker }
  1458  
  1459  func TestScanETag(t *testing.T) {
  1460  	tests := []struct {
  1461  		in         string
  1462  		wantETag   string
  1463  		wantRemain string
  1464  	}{
  1465  		{`W/"etag-1"`, `W/"etag-1"`, ""},
  1466  		{`"etag-2"`, `"etag-2"`, ""},
  1467  		{`"etag-1", "etag-2"`, `"etag-1"`, `, "etag-2"`},
  1468  		{"", "", ""},
  1469  		{"W/", "", ""},
  1470  		{`W/"truc`, "", ""},
  1471  		{`w/"case-sensitive"`, "", ""},
  1472  		{`"spaced etag"`, "", ""},
  1473  	}
  1474  	for _, test := range tests {
  1475  		etag, remain := ExportScanETag(test.in)
  1476  		if etag != test.wantETag || remain != test.wantRemain {
  1477  			t.Errorf("scanETag(%q)=%q %q, want %q %q", test.in, etag, remain, test.wantETag, test.wantRemain)
  1478  		}
  1479  	}
  1480  }
  1481  
  1482  // Issue 40940: Ensure that we only accept non-negative suffix-lengths
  1483  // in "Range": "bytes=-N", and should reject "bytes=--2".
  1484  func TestServeFileRejectsInvalidSuffixLengths(t *testing.T) {
  1485  	run(t, testServeFileRejectsInvalidSuffixLengths, []testMode{http1Mode, https1Mode, http2Mode})
  1486  }
  1487  func testServeFileRejectsInvalidSuffixLengths(t *testing.T, mode testMode) {
  1488  	cst := newClientServerTest(t, mode, FileServer(Dir("testdata"))).ts
  1489  
  1490  	tests := []struct {
  1491  		r        string
  1492  		wantCode int
  1493  		wantBody string
  1494  	}{
  1495  		{"bytes=--6", 416, "invalid range\n"},
  1496  		{"bytes=--0", 416, "invalid range\n"},
  1497  		{"bytes=---0", 416, "invalid range\n"},
  1498  		{"bytes=-6", 206, "hello\n"},
  1499  		{"bytes=6-", 206, "html says hello\n"},
  1500  		{"bytes=-6-", 416, "invalid range\n"},
  1501  		{"bytes=-0", 206, ""},
  1502  		{"bytes=", 200, "index.html says hello\n"},
  1503  	}
  1504  
  1505  	for _, tt := range tests {
  1506  		tt := tt
  1507  		t.Run(tt.r, func(t *testing.T) {
  1508  			req, err := NewRequest("GET", cst.URL+"/index.html", nil)
  1509  			if err != nil {
  1510  				t.Fatal(err)
  1511  			}
  1512  			req.Header.Set("Range", tt.r)
  1513  			res, err := cst.Client().Do(req)
  1514  			if err != nil {
  1515  				t.Fatal(err)
  1516  			}
  1517  			if g, w := res.StatusCode, tt.wantCode; g != w {
  1518  				t.Errorf("StatusCode mismatch: got %d want %d", g, w)
  1519  			}
  1520  			slurp, err := io.ReadAll(res.Body)
  1521  			res.Body.Close()
  1522  			if err != nil {
  1523  				t.Fatal(err)
  1524  			}
  1525  			if g, w := string(slurp), tt.wantBody; g != w {
  1526  				t.Fatalf("Content mismatch:\nGot:  %q\nWant: %q", g, w)
  1527  			}
  1528  		})
  1529  	}
  1530  }
  1531  
  1532  func TestFileServerMethods(t *testing.T) {
  1533  	run(t, testFileServerMethods)
  1534  }
  1535  func testFileServerMethods(t *testing.T, mode testMode) {
  1536  	ts := newClientServerTest(t, mode, FileServer(Dir("testdata"))).ts
  1537  
  1538  	file, err := os.ReadFile(testFile)
  1539  	if err != nil {
  1540  		t.Fatal("reading file:", err)
  1541  	}
  1542  
  1543  	// Get contents via various methods.
  1544  	//
  1545  	// See https://go.dev/issue/59471 for a proposal to limit the set of methods handled.
  1546  	// For now, test the historical behavior.
  1547  	for _, method := range []string{
  1548  		MethodGet,
  1549  		MethodHead,
  1550  		MethodPost,
  1551  		MethodPut,
  1552  		MethodPatch,
  1553  		MethodDelete,
  1554  		MethodOptions,
  1555  		MethodTrace,
  1556  	} {
  1557  		req, _ := NewRequest(method, ts.URL+"/file", nil)
  1558  		t.Log(req.URL)
  1559  		res, err := ts.Client().Do(req)
  1560  		if err != nil {
  1561  			t.Fatal(err)
  1562  		}
  1563  		body, err := io.ReadAll(res.Body)
  1564  		res.Body.Close()
  1565  		if err != nil {
  1566  			t.Fatal(err)
  1567  		}
  1568  		wantBody := file
  1569  		if method == MethodHead {
  1570  			wantBody = nil
  1571  		}
  1572  		if !bytes.Equal(body, wantBody) {
  1573  			t.Fatalf("%v: got body %q, want %q", method, body, wantBody)
  1574  		}
  1575  		if got, want := res.Header.Get("Content-Length"), fmt.Sprint(len(file)); got != want {
  1576  			t.Fatalf("%v: got Content-Length %q, want %q", method, got, want)
  1577  		}
  1578  	}
  1579  }
  1580  
  1581  func TestFileServerFS(t *testing.T) {
  1582  	filename := "index.html"
  1583  	contents := []byte("index.html says hello")
  1584  	fsys := fstest.MapFS{
  1585  		filename: {Data: contents},
  1586  	}
  1587  	ts := newClientServerTest(t, http1Mode, FileServerFS(fsys)).ts
  1588  	defer ts.Close()
  1589  
  1590  	res, err := ts.Client().Get(ts.URL + "/" + filename)
  1591  	if err != nil {
  1592  		t.Fatal(err)
  1593  	}
  1594  	b, err := io.ReadAll(res.Body)
  1595  	if err != nil {
  1596  		t.Fatal("reading Body:", err)
  1597  	}
  1598  	if s := string(b); s != string(contents) {
  1599  		t.Errorf("for path %q got %q, want %q", filename, s, contents)
  1600  	}
  1601  	res.Body.Close()
  1602  }
  1603  
  1604  func TestServeFileFS(t *testing.T) {
  1605  	filename := "index.html"
  1606  	contents := []byte("index.html says hello")
  1607  	fsys := fstest.MapFS{
  1608  		filename: {Data: contents},
  1609  	}
  1610  	ts := newClientServerTest(t, http1Mode, HandlerFunc(func(w ResponseWriter, r *Request) {
  1611  		ServeFileFS(w, r, fsys, filename)
  1612  	})).ts
  1613  	defer ts.Close()
  1614  
  1615  	res, err := ts.Client().Get(ts.URL + "/" + filename)
  1616  	if err != nil {
  1617  		t.Fatal(err)
  1618  	}
  1619  	b, err := io.ReadAll(res.Body)
  1620  	if err != nil {
  1621  		t.Fatal("reading Body:", err)
  1622  	}
  1623  	if s := string(b); s != string(contents) {
  1624  		t.Errorf("for path %q got %q, want %q", filename, s, contents)
  1625  	}
  1626  	res.Body.Close()
  1627  }
  1628  
  1629  func TestServeFileZippingResponseWriter(t *testing.T) {
  1630  	// This test exercises a pattern which is incorrect,
  1631  	// but has been observed enough in the world that we don't want to break it.
  1632  	//
  1633  	// The server is setting "Content-Encoding: gzip",
  1634  	// wrapping the ResponseWriter in an implementation which gzips data written to it,
  1635  	// and passing this ResponseWriter to ServeFile.
  1636  	//
  1637  	// This means ServeFile cannot properly set a Content-Length header, because it
  1638  	// doesn't know what content it is going to send--the ResponseWriter is modifying
  1639  	// the bytes sent.
  1640  	//
  1641  	// Range requests are always going to be broken in this scenario,
  1642  	// but verify that we can serve non-range requests correctly.
  1643  	filename := "index.html"
  1644  	contents := []byte("contents will be sent with Content-Encoding: gzip")
  1645  	fsys := fstest.MapFS{
  1646  		filename: {Data: contents},
  1647  	}
  1648  	ts := newClientServerTest(t, http1Mode, HandlerFunc(func(w ResponseWriter, r *Request) {
  1649  		w.Header().Set("Content-Encoding", "gzip")
  1650  		gzw := gzip.NewWriter(w)
  1651  		defer gzw.Close()
  1652  		ServeFileFS(gzipResponseWriter{w: gzw, ResponseWriter: w}, r, fsys, filename)
  1653  	})).ts
  1654  	defer ts.Close()
  1655  
  1656  	res, err := ts.Client().Get(ts.URL + "/" + filename)
  1657  	if err != nil {
  1658  		t.Fatal(err)
  1659  	}
  1660  	b, err := io.ReadAll(res.Body)
  1661  	if err != nil {
  1662  		t.Fatal("reading Body:", err)
  1663  	}
  1664  	if s := string(b); s != string(contents) {
  1665  		t.Errorf("for path %q got %q, want %q", filename, s, contents)
  1666  	}
  1667  	res.Body.Close()
  1668  }
  1669  
  1670  type gzipResponseWriter struct {
  1671  	ResponseWriter
  1672  	w *gzip.Writer
  1673  }
  1674  
  1675  func (grw gzipResponseWriter) Write(b []byte) (int, error) {
  1676  	return grw.w.Write(b)
  1677  }
  1678  
  1679  func (grw gzipResponseWriter) Flush() {
  1680  	grw.w.Flush()
  1681  	if fw, ok := grw.ResponseWriter.(http.Flusher); ok {
  1682  		fw.Flush()
  1683  	}
  1684  }
  1685  
  1686  // Issue 63769
  1687  func TestFileServerDirWithRootFile(t *testing.T) { run(t, testFileServerDirWithRootFile) }
  1688  func testFileServerDirWithRootFile(t *testing.T, mode testMode) {
  1689  	testDirFile := func(t *testing.T, h Handler) {
  1690  		ts := newClientServerTest(t, mode, h).ts
  1691  		defer ts.Close()
  1692  
  1693  		res, err := ts.Client().Get(ts.URL)
  1694  		if err != nil {
  1695  			t.Fatal(err)
  1696  		}
  1697  		if g, w := res.StatusCode, StatusInternalServerError; g != w {
  1698  			t.Errorf("StatusCode mismatch: got %d, want: %d", g, w)
  1699  		}
  1700  		res.Body.Close()
  1701  	}
  1702  
  1703  	t.Run("FileServer", func(t *testing.T) {
  1704  		testDirFile(t, FileServer(Dir("testdata/index.html")))
  1705  	})
  1706  
  1707  	t.Run("FileServerFS", func(t *testing.T) {
  1708  		testDirFile(t, FileServerFS(os.DirFS("testdata/index.html")))
  1709  	})
  1710  }
  1711  
  1712  func TestServeContentHeadersWithError(t *testing.T) {
  1713  	contents := []byte("content")
  1714  	ts := newClientServerTest(t, http1Mode, HandlerFunc(func(w ResponseWriter, r *Request) {
  1715  		w.Header().Set("Content-Type", "application/octet-stream")
  1716  		w.Header().Set("Content-Length", strconv.Itoa(len(contents)))
  1717  		w.Header().Set("Content-Encoding", "gzip")
  1718  		w.Header().Set("Etag", `"abcdefgh"`)
  1719  		w.Header().Set("Last-Modified", "Wed, 21 Oct 2015 07:28:00 GMT")
  1720  		w.Header().Set("Cache-Control", "immutable")
  1721  		w.Header().Set("Other-Header", "test")
  1722  		ServeContent(w, r, "", time.Time{}, bytes.NewReader(contents))
  1723  	})).ts
  1724  	defer ts.Close()
  1725  
  1726  	req, err := NewRequest("GET", ts.URL, nil)
  1727  	if err != nil {
  1728  		t.Fatal(err)
  1729  	}
  1730  	req.Header.Set("Range", "bytes=100-10000")
  1731  
  1732  	c := ts.Client()
  1733  	res, err := c.Do(req)
  1734  	if err != nil {
  1735  		t.Fatal(err)
  1736  	}
  1737  
  1738  	out, _ := io.ReadAll(res.Body)
  1739  	res.Body.Close()
  1740  
  1741  	if g, e := res.StatusCode, 416; g != e {
  1742  		t.Errorf("got status = %d; want %d", g, e)
  1743  	}
  1744  	if g, e := string(out), "invalid range: failed to overlap\n"; g != e {
  1745  		t.Errorf("got body = %q; want %q", g, e)
  1746  	}
  1747  	if g, e := res.Header.Get("Content-Type"), "text/plain; charset=utf-8"; g != e {
  1748  		t.Errorf("got content-type = %q, want %q", g, e)
  1749  	}
  1750  	if g, e := res.Header.Get("Content-Length"), strconv.Itoa(len(out)); g != e {
  1751  		t.Errorf("got content-length = %q, want %q", g, e)
  1752  	}
  1753  	if g, e := res.Header.Get("Content-Encoding"), ""; g != e {
  1754  		t.Errorf("got content-encoding = %q, want %q", g, e)
  1755  	}
  1756  	if g, e := res.Header.Get("Etag"), ""; g != e {
  1757  		t.Errorf("got etag = %q, want %q", g, e)
  1758  	}
  1759  	if g, e := res.Header.Get("Last-Modified"), ""; g != e {
  1760  		t.Errorf("got last-modified = %q, want %q", g, e)
  1761  	}
  1762  	if g, e := res.Header.Get("Cache-Control"), ""; g != e {
  1763  		t.Errorf("got cache-control = %q, want %q", g, e)
  1764  	}
  1765  	if g, e := res.Header.Get("Content-Range"), "bytes */7"; g != e {
  1766  		t.Errorf("got content-range = %q, want %q", g, e)
  1767  	}
  1768  	if g, e := res.Header.Get("Other-Header"), "test"; g != e {
  1769  		t.Errorf("got other-header = %q, want %q", g, e)
  1770  	}
  1771  }
  1772  

View as plain text