Files
fasthttp/fs_fs_test.go
T
Erik Dubbelboer d238e60fed Add ServeFileLiteral, ServeFSLiteral and SendFileLiteral (#2163)
ServeFile and ServeFS interpret the path as a URI, so percent-encoded
sequences are decoded and characters like '?' and '#' act as URI
delimiters. This makes it impossible to serve files whose names
contain those characters.

Changing this behavior would be backwards incompatible. So instead the
new ServeFileLiteral, ServeFSLiteral and SendFileLiteral are added.

The new Literal variants percent-encode the path before setting it as
the request URI, preserving every byte of the original filesystem path.

Thanks to @thesmartshadow for reporting this issue.
2026-03-23 03:21:36 +01:00

1072 lines
28 KiB
Go

package fasthttp
import (
"bufio"
"bytes"
"embed"
"io/fs"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"testing/fstest"
"time"
)
//go:embed fasthttputil fs.go README.md testdata examples
var fsTestFilesystem embed.FS
func TestFSServeFileHead(t *testing.T) {
t.Parallel()
var ctx RequestCtx
var req Request
req.Header.SetMethod(MethodHead)
req.SetRequestURI("http://foobar.com/baz")
ctx.Init(&req, nil, nil)
ServeFS(&ctx, fsTestFilesystem, "fs.go")
var resp Response
resp.SkipBody = true
s := ctx.Response.String()
br := bufio.NewReader(bytes.NewBufferString(s))
if err := resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
ce := resp.Header.ContentEncoding()
if len(ce) > 0 {
t.Fatalf("Unexpected 'Content-Encoding' %q", ce)
}
body := resp.Body()
if len(body) > 0 {
t.Fatalf("unexpected response body %q. Expecting empty body", body)
}
expectedBody, err := getFileContents("/fs.go")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
contentLength := resp.Header.ContentLength()
if contentLength != len(expectedBody) {
t.Fatalf("unexpected Content-Length: %d. expecting %d", contentLength, len(expectedBody))
}
}
func TestServeFSLiteral(t *testing.T) {
t.Parallel()
testFS := fstest.MapFS{
"space name.txt": {Data: []byte("space")},
"hash#name.txt": {Data: []byte("hash")},
"percent%61name.txt": {Data: []byte("percent")},
"query?name.txt": {Data: []byte("query")},
}
for name, file := range testFS {
t.Run(name, func(t *testing.T) {
var ctx RequestCtx
var req Request
req.SetRequestURI("http://foobar.com/original")
ctx.Init(&req, nil, nil)
ServeFSLiteral(&ctx, testFS, name)
resp := readResponseFromCtx(t, &ctx, false)
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code %d. expecting %d", resp.StatusCode(), StatusOK)
}
if !bytes.Equal(resp.Body(), file.Data) {
t.Fatalf("unexpected body %q. expecting %q", resp.Body(), file.Data)
}
})
}
}
func TestServeFSSpecialCharsNotLiteral(t *testing.T) {
t.Parallel()
testFS := fstest.MapFS{
"hash#name.txt": {Data: []byte("hash")},
"percent%61name.txt": {Data: []byte("percent")},
"query?name.txt": {Data: []byte("query")},
}
for name := range testFS {
t.Run(name, func(t *testing.T) {
var ctx RequestCtx
var req Request
req.SetRequestURI("http://foobar.com/original")
ctx.Init(&req, nil, nil)
ServeFS(&ctx, testFS, name)
resp := readResponseFromCtx(t, &ctx, false)
if resp.StatusCode() == StatusOK {
t.Fatalf("ServeFS should fail for special-character path %q without literal mode, but got 200", name)
}
})
}
}
func TestFSServeFileCompressed(t *testing.T) {
t.Parallel()
var ctx RequestCtx
ctx.Init(&Request{}, nil, nil)
var resp Response
expectedBody, err := getFileContents("/fs.go")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// should prefer brotli over zstd, gzip and ignore unknown encoding
ctx.Request.SetRequestURI("http://foobar.com/baz")
ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, br, wompwomp")
ServeFS(&ctx, fsTestFilesystem, "fs.go")
s := ctx.Response.String()
br := bufio.NewReader(bytes.NewBufferString(s))
if err = resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
}
ce := resp.Header.ContentEncoding()
if string(ce) != "br" {
t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "br")
}
vary := resp.Header.PeekBytes(strVary)
if !bytes.Equal(vary, strAcceptEncoding) {
t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
}
body, err := resp.BodyUnbrotli()
if err != nil {
t.Fatalf("unexpected error on unbrotli response body: %v", err)
}
if !bytes.Equal(body, expectedBody) {
t.Fatalf("unexpected body: len=%d. Expected len=%d", len(body), len(expectedBody))
}
// should prefer zstd over gzip and ignore unknown encoding
ctx.Request.Reset()
ctx.Request.SetRequestURI("http://foobar.com/baz")
ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, wompwomp")
ServeFS(&ctx, fsTestFilesystem, "fs.go")
s = ctx.Response.String()
br = bufio.NewReader(bytes.NewBufferString(s))
if err = resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
}
ce = resp.Header.ContentEncoding()
if string(ce) != "zstd" {
t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "zstd")
}
vary = resp.Header.PeekBytes(strVary)
if !bytes.Equal(vary, strAcceptEncoding) {
t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
}
body, err = resp.BodyUnzstd()
if err != nil {
t.Fatalf("unexpected error on unzstd response body: %v", err)
}
if !bytes.Equal(body, expectedBody) {
t.Fatalf("unexpected body: len=%d. Expected len=%d", len(body), len(expectedBody))
}
// should prefer gzip and ignore unknown encoding
ctx.Request.Reset()
ctx.Request.SetRequestURI("http://foobar.com/baz")
ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, wompwomp")
ServeFS(&ctx, fsTestFilesystem, "fs.go")
s = ctx.Response.String()
br = bufio.NewReader(bytes.NewBufferString(s))
if err = resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
}
ce = resp.Header.ContentEncoding()
if string(ce) != "gzip" {
t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "gzip")
}
vary = resp.Header.PeekBytes(strVary)
if !bytes.Equal(vary, strAcceptEncoding) {
t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
}
body, err = resp.BodyGunzip()
if err != nil {
t.Fatalf("unexpected error on gunzip response body: %v", err)
}
if !bytes.Equal(body, expectedBody) {
t.Fatalf("unexpected body: len=%d. Expected len=%d", len(body), len(expectedBody))
}
}
func TestFSServeFileUncompressed(t *testing.T) {
t.Parallel()
var ctx RequestCtx
ctx.Init(&Request{}, nil, nil)
var resp Response
expectedBody, err := getFileContents("/fs.go")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ctx.Request.SetRequestURI("http://foobar.com/baz")
ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, br, wompwomp")
ServeFileUncompressed(&ctx, "fs.go")
s := ctx.Response.String()
br := bufio.NewReader(bytes.NewBufferString(s))
if err = resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
}
ce := resp.Header.ContentEncoding()
if len(ce) > 0 {
t.Fatalf("unexpected 'Content-Encoding': %q. Expecting \"\"", string(ce))
}
vary := resp.Header.PeekBytes(strVary)
if len(vary) > 0 {
t.Fatalf("unexpected 'Vary': %q. Expecting \"\"", string(vary))
}
body := resp.Body()
if !bytes.Equal(body, expectedBody) {
t.Fatalf("unexpected body: len=%d. Expecting len=%d", len(body), len(expectedBody))
}
}
func TestFSFSByteRangeConcurrent(t *testing.T) {
t.Parallel()
stop := make(chan struct{})
defer close(stop)
fs := &FS{
FS: fsTestFilesystem,
Root: "",
AcceptByteRange: true,
CleanStop: stop,
}
h := fs.NewRequestHandler()
concurrency := 10
ch := make(chan struct{}, concurrency)
for range concurrency {
go func() {
for range 5 {
testFSByteRange(t, h, "/fs.go")
testFSByteRange(t, h, "/README.md")
}
ch <- struct{}{}
}()
}
for range concurrency {
select {
case <-time.After(time.Second):
t.Fatalf("timeout")
case <-ch:
}
}
}
func TestFSFSByteRangeSingleThread(t *testing.T) {
t.Parallel()
stop := make(chan struct{})
defer close(stop)
fs := &FS{
FS: fsTestFilesystem,
Root: ".",
AcceptByteRange: true,
CleanStop: stop,
}
h := fs.NewRequestHandler()
testFSByteRange(t, h, "/fs.go")
testFSByteRange(t, h, "/README.md")
}
func TestFSFSCompressConcurrent(t *testing.T) {
t.Parallel()
// go 1.16 timeout may occur
if strings.HasPrefix(runtime.Version(), "go1.16") {
t.SkipNow()
}
stop := make(chan struct{})
defer close(stop)
fs := &FS{
FS: fsTestFilesystem,
Root: ".",
GenerateIndexPages: true,
Compress: true,
CompressBrotli: true,
CompressZstd: true,
CleanStop: stop,
}
h := fs.NewRequestHandler()
concurrency := 4
ch := make(chan struct{}, concurrency)
for range concurrency {
go func() {
for range 5 {
testFSFSCompress(t, h, "/fs.go")
testFSFSCompress(t, h, "/examples/")
testFSFSCompress(t, h, "/README.md")
}
ch <- struct{}{}
}()
}
for range concurrency {
select {
case <-ch:
case <-time.After(time.Second * 4):
t.Fatalf("timeout")
}
}
}
func TestFSFSCompressSingleThread(t *testing.T) {
t.Parallel()
stop := make(chan struct{})
defer close(stop)
fs := &FS{
FS: fsTestFilesystem,
Root: ".",
GenerateIndexPages: true,
Compress: true,
CompressBrotli: true,
CompressZstd: true,
CleanStop: stop,
}
h := fs.NewRequestHandler()
testFSFSCompress(t, h, "/fs.go")
testFSFSCompress(t, h, "/examples/")
testFSFSCompress(t, h, "/README.md")
}
func testFSFSCompress(t *testing.T, h RequestHandler, filePath string) {
var ctx RequestCtx
ctx.Init(&Request{}, nil, nil)
var resp Response
// get uncompressed
ctx.Request.SetRequestURI(filePath)
h(&ctx)
s := ctx.Response.String()
br := bufio.NewReader(bytes.NewBufferString(s))
if err := resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
}
ce := resp.Header.ContentEncoding()
if len(ce) > 0 {
t.Fatalf("unexpected 'Content-Encoding': %q. Expecting \"\"", string(ce))
}
vary := resp.Header.PeekBytes(strVary)
if len(vary) > 0 {
t.Fatalf("unexpected 'Vary': %q. Expecting \"\"", string(vary))
}
expectedBody := bytes.Clone(resp.Body())
// should prefer brotli over zstd, gzip and ignore unknown encoding
ctx.Request.Reset()
ctx.Request.SetRequestURI(filePath)
ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, br, wompwomp")
h(&ctx)
s = ctx.Response.String()
br = bufio.NewReader(bytes.NewBufferString(s))
if err := resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v. filePath=%q", err, filePath)
}
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
}
ce = resp.Header.ContentEncoding()
if string(ce) != "br" {
t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q. filePath=%q", string(ce), "br", filePath)
}
vary = resp.Header.PeekBytes(strVary)
if !bytes.Equal(vary, strAcceptEncoding) {
t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
}
body, err := resp.BodyUnbrotli()
if err != nil {
t.Fatalf("unexpected error on unbrotli response body: %v. filePath=%q", err, filePath)
}
if !bytes.Equal(body, expectedBody) {
t.Fatalf("unexpected body: len=%d. Expecting len=%d. filePath=%q", len(body), len(expectedBody), filePath)
}
// should prefer zstd over gzip and ignore unknown encoding
ctx.Request.Reset()
ctx.Request.SetRequestURI(filePath)
ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, wompwomp")
h(&ctx)
s = ctx.Response.String()
br = bufio.NewReader(bytes.NewBufferString(s))
if err = resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v. filePath=%q", err, filePath)
}
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
}
ce = resp.Header.ContentEncoding()
if string(ce) != "zstd" {
t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q. filePath=%q", string(ce), "zstd", filePath)
}
vary = resp.Header.PeekBytes(strVary)
if !bytes.Equal(vary, strAcceptEncoding) {
t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
}
body, err = resp.BodyUnzstd()
if err != nil {
t.Fatalf("unexpected error on unzstd response body: %v. filePath=%q", err, filePath)
}
if !bytes.Equal(body, expectedBody) {
t.Fatalf("unexpected body: len=%d. Expecting len=%d. filePath=%q", len(body), len(expectedBody), filePath)
}
// should prefer gzip and ignore unknown encoding
ctx.Request.Reset()
ctx.Request.SetRequestURI(filePath)
ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, wompwomp")
h(&ctx)
s = ctx.Response.String()
br = bufio.NewReader(bytes.NewBufferString(s))
if err = resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v. filePath=%q", err, filePath)
}
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
}
ce = resp.Header.ContentEncoding()
if string(ce) != "gzip" {
t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q. filePath=%q", string(ce), "gzip", filePath)
}
vary = resp.Header.PeekBytes(strVary)
if !bytes.Equal(vary, strAcceptEncoding) {
t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
}
body, err = resp.BodyGunzip()
if err != nil {
t.Fatalf("unexpected error on gunzip response body: %v. filePath=%q", err, filePath)
}
if !bytes.Equal(body, expectedBody) {
t.Fatalf("unexpected body: len=%d. Expecting len=%d. filePath=%q", len(body), len(expectedBody), filePath)
}
}
func TestFSServeFileContentType(t *testing.T) {
t.Parallel()
var ctx RequestCtx
var req Request
req.Header.SetMethod(MethodGet)
req.SetRequestURI("http://foobar.com/baz")
ctx.Init(&req, nil, nil)
ServeFS(&ctx, fsTestFilesystem, "testdata/test.png")
var resp Response
s := ctx.Response.String()
br := bufio.NewReader(bytes.NewBufferString(s))
if err := resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := []byte("image/png")
if !bytes.Equal(resp.Header.ContentType(), expected) {
t.Fatalf("Unexpected Content-Type, expected: %q got %q", expected, resp.Header.ContentType())
}
}
func TestFSServeFileDirectoryRedirect(t *testing.T) {
t.Parallel()
var ctx RequestCtx
var req Request
req.SetRequestURI("http://foobar.com")
ctx.Init(&req, nil, nil)
ctx.Request.Reset()
ctx.Response.Reset()
ServeFS(&ctx, fsTestFilesystem, "fasthttputil")
if ctx.Response.StatusCode() != StatusFound {
t.Fatalf("Unexpected status code %d for directory '/fasthttputil' without trailing slash. Expecting %d.", ctx.Response.StatusCode(), StatusFound)
}
ctx.Request.Reset()
ctx.Response.Reset()
ServeFS(&ctx, fsTestFilesystem, "fasthttputil/")
if ctx.Response.StatusCode() != StatusOK {
t.Fatalf("Unexpected status code %d for directory '/fasthttputil/' with trailing slash. Expecting %d.", ctx.Response.StatusCode(), StatusOK)
}
ctx.Request.Reset()
ctx.Response.Reset()
ServeFS(&ctx, fsTestFilesystem, "fs.go")
if ctx.Response.StatusCode() != StatusOK {
t.Fatalf("Unexpected status code %d for file '/fs.go'. Expecting %d.", ctx.Response.StatusCode(), StatusOK)
}
}
var dirTestFilesystem = os.DirFS(".")
func TestDirFSServeFileHead(t *testing.T) {
t.Parallel()
var ctx RequestCtx
var req Request
req.Header.SetMethod(MethodHead)
req.SetRequestURI("http://foobar.com/baz")
ctx.Init(&req, nil, nil)
ServeFS(&ctx, dirTestFilesystem, "fs.go")
var resp Response
resp.SkipBody = true
s := ctx.Response.String()
br := bufio.NewReader(bytes.NewBufferString(s))
if err := resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
ce := resp.Header.ContentEncoding()
if len(ce) > 0 {
t.Fatalf("Unexpected 'Content-Encoding' %q", ce)
}
body := resp.Body()
if len(body) > 0 {
t.Fatalf("unexpected response body %q. Expecting empty body", body)
}
expectedBody, err := getFileContents("/fs.go")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
contentLength := resp.Header.ContentLength()
if contentLength != len(expectedBody) {
t.Fatalf("unexpected Content-Length: %d. expecting %d", contentLength, len(expectedBody))
}
}
func TestDirFSServeFileCompressed(t *testing.T) {
t.Parallel()
var ctx RequestCtx
ctx.Init(&Request{}, nil, nil)
var resp Response
expectedBody, err := getFileContents("/fs.go")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// should prefer brotli over zstd, gzip and ignore unknown encoding
ctx.Request.SetRequestURI("http://foobar.com/baz")
ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, br, wompwomp")
ServeFS(&ctx, dirTestFilesystem, "fs.go")
s := ctx.Response.String()
br := bufio.NewReader(bytes.NewBufferString(s))
if err = resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
}
ce := resp.Header.ContentEncoding()
if string(ce) != "br" {
t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "br")
}
vary := resp.Header.PeekBytes(strVary)
if !bytes.Equal(vary, strAcceptEncoding) {
t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
}
body, err := resp.BodyUnbrotli()
if err != nil {
t.Fatalf("unexpected error on unbrotli response body: %v", err)
}
if !bytes.Equal(body, expectedBody) {
t.Fatalf("unexpected body: len=%d. Expecting len=%d", len(body), len(expectedBody))
}
// should prefer zstd over gzip and ignore unknown encoding
ctx.Request.Reset()
ctx.Request.SetRequestURI("http://foobar.com/baz")
ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, wompwomp")
ServeFS(&ctx, dirTestFilesystem, "fs.go")
s = ctx.Response.String()
br = bufio.NewReader(bytes.NewBufferString(s))
if err = resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
}
ce = resp.Header.ContentEncoding()
if string(ce) != "zstd" {
t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "zstd")
}
vary = resp.Header.PeekBytes(strVary)
if !bytes.Equal(vary, strAcceptEncoding) {
t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
}
body, err = resp.BodyUnzstd()
if err != nil {
t.Fatalf("unexpected error on unzstd response body: %v", err)
}
if !bytes.Equal(body, expectedBody) {
t.Fatalf("unexpected body: len=%d. Expecting len=%d", len(body), len(expectedBody))
}
// should prefer gzip and ignore unknown encoding
ctx.Request.Reset()
ctx.Request.SetRequestURI("http://foobar.com/baz")
ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, wompwomp")
ServeFS(&ctx, dirTestFilesystem, "fs.go")
s = ctx.Response.String()
br = bufio.NewReader(bytes.NewBufferString(s))
if err = resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.StatusCode() != StatusOK {
t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
}
ce = resp.Header.ContentEncoding()
if string(ce) != "gzip" {
t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "gzip")
}
vary = resp.Header.PeekBytes(strVary)
if !bytes.Equal(vary, strAcceptEncoding) {
t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
}
body, err = resp.BodyGunzip()
if err != nil {
t.Fatalf("unexpected error on gunzip response body: %v", err)
}
if !bytes.Equal(body, expectedBody) {
t.Fatalf("unexpected body: len=%d. Expecting len=%d", len(body), len(expectedBody))
}
}
func TestDirFSFSByteRangeConcurrent(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip()
}
t.Parallel()
stop := make(chan struct{})
defer close(stop)
fs := &FS{
FS: dirTestFilesystem,
Root: "",
AcceptByteRange: true,
CleanStop: stop,
}
h := fs.NewRequestHandler()
concurrency := 10
ch := make(chan struct{}, concurrency)
for range concurrency {
go func() {
for range 5 {
testFSByteRange(t, h, "/fs.go")
testFSByteRange(t, h, "/README.md")
}
ch <- struct{}{}
}()
}
for range concurrency {
select {
case <-time.After(time.Second):
t.Fatalf("timeout")
case <-ch:
}
}
}
func TestDirFSFSByteRangeSingleThread(t *testing.T) {
t.Parallel()
stop := make(chan struct{})
defer close(stop)
fs := &FS{
FS: dirTestFilesystem,
Root: ".",
AcceptByteRange: true,
CleanStop: stop,
}
h := fs.NewRequestHandler()
testFSByteRange(t, h, "/fs.go")
testFSByteRange(t, h, "/README.md")
}
func TestDirFSFSCompressConcurrent(t *testing.T) {
t.Parallel()
stop := make(chan struct{})
defer close(stop)
fs := &FS{
FS: dirTestFilesystem,
Root: ".",
GenerateIndexPages: true,
Compress: true,
CompressBrotli: true,
CompressZstd: true,
CleanStop: stop,
}
h := fs.NewRequestHandler()
concurrency := 4
ch := make(chan struct{}, concurrency)
for range concurrency {
go func() {
for range 5 {
testFSFSCompress(t, h, "/fs.go")
testFSFSCompress(t, h, "/examples/")
testFSFSCompress(t, h, "/README.md")
}
ch <- struct{}{}
}()
}
for range concurrency {
select {
case <-ch:
case <-time.After(time.Second * 2):
t.Fatalf("timeout")
}
}
}
func TestDirFSFSCompressSingleThread(t *testing.T) {
t.Parallel()
stop := make(chan struct{})
defer close(stop)
fs := &FS{
FS: dirTestFilesystem,
Root: ".",
GenerateIndexPages: true,
Compress: true,
CompressBrotli: true,
CompressZstd: true,
CleanStop: stop,
}
h := fs.NewRequestHandler()
testFSFSCompress(t, h, "/fs.go")
testFSFSCompress(t, h, "/examples/")
testFSFSCompress(t, h, "/README.md")
}
func TestDirFSServeFileContentType(t *testing.T) {
t.Parallel()
var ctx RequestCtx
var req Request
req.Header.SetMethod(MethodGet)
req.SetRequestURI("http://foobar.com/baz")
ctx.Init(&req, nil, nil)
ServeFS(&ctx, dirTestFilesystem, "testdata/test.png")
var resp Response
s := ctx.Response.String()
br := bufio.NewReader(bytes.NewBufferString(s))
if err := resp.Read(br); err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := []byte("image/png")
if !bytes.Equal(resp.Header.ContentType(), expected) {
t.Fatalf("Unexpected Content-Type, expected: %q got %q", expected, resp.Header.ContentType())
}
}
func TestDirFSServeFileDirectoryRedirect(t *testing.T) {
t.Parallel()
var ctx RequestCtx
var req Request
req.SetRequestURI("http://foobar.com")
ctx.Init(&req, nil, nil)
ctx.Request.Reset()
ctx.Response.Reset()
ServeFS(&ctx, dirTestFilesystem, "fasthttputil")
if ctx.Response.StatusCode() != StatusFound {
t.Fatalf("Unexpected status code %d for directory '/fasthttputil' without trailing slash. Expecting %d.", ctx.Response.StatusCode(), StatusFound)
}
ctx.Request.Reset()
ctx.Response.Reset()
ServeFS(&ctx, dirTestFilesystem, "fasthttputil/")
if ctx.Response.StatusCode() != StatusOK {
t.Fatalf("Unexpected status code %d for directory '/fasthttputil/' with trailing slash. Expecting %d.", ctx.Response.StatusCode(), StatusOK)
}
ctx.Request.Reset()
ctx.Response.Reset()
ServeFS(&ctx, dirTestFilesystem, "fs.go")
if ctx.Response.StatusCode() != StatusOK {
t.Fatalf("Unexpected status code %d for file '/fs.go'. Expecting %d.", ctx.Response.StatusCode(), StatusOK)
}
}
func TestFSFSGenerateIndexOsDirFS(t *testing.T) {
t.Parallel()
t.Run("dirFS", func(t *testing.T) {
t.Parallel()
fs := &FS{
FS: dirTestFilesystem,
Root: ".",
GenerateIndexPages: true,
}
h := fs.NewRequestHandler()
var ctx RequestCtx
var req Request
ctx.Init(&req, nil, nil)
h(&ctx)
cases := []string{"/", "//", ""}
for _, c := range cases {
ctx.Request.Reset()
ctx.Response.Reset()
req.Header.SetMethod(MethodGet)
req.SetRequestURI("http://foobar.com" + c)
h(&ctx)
if ctx.Response.StatusCode() != StatusOK {
t.Fatalf("unexpected status code %d for path %q. Expecting %d", ctx.Response.StatusCode(), ctx.Response.StatusCode(), StatusOK)
}
if !bytes.Contains(ctx.Response.Body(), []byte("fasthttputil")) {
t.Fatalf("unexpected body %q. Expecting to contain %q", ctx.Response.Body(), "fasthttputil")
}
if !bytes.Contains(ctx.Response.Body(), []byte("fs.go")) {
t.Fatalf("unexpected body %q. Expecting to contain %q", ctx.Response.Body(), "fs.go")
}
}
})
t.Run("embedFS", func(t *testing.T) {
t.Parallel()
fs := &FS{
FS: fsTestFilesystem,
Root: ".",
GenerateIndexPages: true,
}
h := fs.NewRequestHandler()
var ctx RequestCtx
var req Request
ctx.Init(&req, nil, nil)
h(&ctx)
cases := []string{"/", "//", ""}
for _, c := range cases {
ctx.Request.Reset()
ctx.Response.Reset()
req.Header.SetMethod(MethodGet)
req.SetRequestURI("http://foobar.com" + c)
h(&ctx)
if ctx.Response.StatusCode() != StatusOK {
t.Fatalf("unexpected status code %d for path %q. Expecting %d", ctx.Response.StatusCode(), ctx.Response.StatusCode(), StatusOK)
}
if !bytes.Contains(ctx.Response.Body(), []byte("fasthttputil")) {
t.Fatalf("unexpected body %q. Expecting to contain %q", ctx.Response.Body(), "fasthttputil")
}
if !bytes.Contains(ctx.Response.Body(), []byte("fs.go")) {
t.Fatalf("unexpected body %q. Expecting to contain %q", ctx.Response.Body(), "fs.go")
}
}
})
}
func TestFSRootEnforcement(t *testing.T) {
t.Parallel()
memFS := fstest.MapFS{
"public/index.html": {Data: []byte("<h1>Public</h1>")},
"secret/admin.json": {Data: []byte(`{"admin": true, "key": "s3cret"}`)},
"public/nested/info": {Data: []byte("nested")},
}
// Skip dirfs subtests on Windows: the FS handler pools open file handles
// (via bigFileReader) which prevents t.TempDir cleanup.
// The mapfs subtests exercise the same root enforcement logic.
var tmpDir string
if runtime.GOOS != "windows" {
tmpDir = t.TempDir()
if err := os.MkdirAll(filepath.Join(tmpDir, "public"), 0o755); err != nil {
t.Fatalf("cannot create public dir: %v", err)
}
if err := os.MkdirAll(filepath.Join(tmpDir, "secret"), 0o755); err != nil {
t.Fatalf("cannot create secret dir: %v", err)
}
if err := os.WriteFile(filepath.Join(tmpDir, "public", "index.html"), []byte("<h1>Public</h1>"), 0o644); err != nil {
t.Fatalf("cannot create public index: %v", err)
}
if err := os.WriteFile(filepath.Join(tmpDir, "secret", "admin.json"), []byte(`{"admin": true, "key": "s3cret"}`), 0o644); err != nil {
t.Fatalf("cannot create secret admin file: %v", err)
}
}
type testCase struct {
name string
root string
filesystem fs.FS
pathRewrite PathRewriteFunc
}
cases := make([]testCase, 0, 9)
for _, root := range []string{"public", "public/", "./public", "/public"} {
cases = append(cases, testCase{
name: "mapfs/" + root,
root: root,
filesystem: memFS,
})
if tmpDir != "" {
cases = append(cases, testCase{
name: "dirfs/" + root,
root: root,
filesystem: os.DirFS(tmpDir),
})
}
}
cases = append(cases, testCase{
name: "mapfs/pathrewrite-no-leading-slash",
root: "./public/",
filesystem: memFS,
pathRewrite: func(ctx *RequestCtx) []byte {
return bytes.TrimPrefix(ctx.Path(), []byte("/"))
},
})
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
stop := make(chan struct{})
defer close(stop)
fs := &FS{
Root: tc.root,
FS: tc.filesystem,
AllowEmptyRoot: true,
CleanStop: stop,
PathRewrite: tc.pathRewrite,
}
h := fs.NewRequestHandler()
var ctx RequestCtx
ctx.Init(&Request{}, nil, TestLogger{t: t})
checkStatus := func(uri string, expected int) {
ctx.Request.Reset()
ctx.Response.Reset()
ctx.Request.SetRequestURI(uri)
h(&ctx)
if ctx.Response.StatusCode() != expected {
t.Fatalf("unexpected status code for %s: %d. Expecting %d", uri, ctx.Response.StatusCode(), expected)
}
}
checkStatus("http://localhost/index.html", StatusOK)
checkStatus("http://localhost/secret/admin.json", StatusNotFound)
})
}
}