├── .github ├── dependabot.yml └── workflows │ ├── cifuzz.yml │ ├── lint.yml │ ├── security.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── README.md ├── SECURITY.md ├── TODO ├── allocation_test.go ├── args.go ├── args_test.go ├── args_timing_test.go ├── b2s_new.go ├── b2s_old.go ├── brotli.go ├── brotli_test.go ├── bytesconv.go ├── bytesconv_32.go ├── bytesconv_32_test.go ├── bytesconv_64.go ├── bytesconv_64_test.go ├── bytesconv_table.go ├── bytesconv_table_gen.go ├── bytesconv_test.go ├── bytesconv_timing_test.go ├── client.go ├── client_example_test.go ├── client_test.go ├── client_timing_test.go ├── client_timing_wait_test.go ├── coarsetime.go ├── coarsetime_test.go ├── compress.go ├── compress_test.go ├── cookie.go ├── cookie_test.go ├── cookie_timing_test.go ├── doc.go ├── examples ├── README.md ├── client │ ├── .gitignore │ ├── Makefile │ ├── README.md │ └── client.go ├── fileserver │ ├── .gitignore │ ├── Makefile │ ├── README.md │ ├── fileserver.go │ ├── ssl-cert-snakeoil.key │ └── ssl-cert-snakeoil.pem ├── helloworldserver │ ├── .gitignore │ ├── Makefile │ ├── README.md │ └── helloworldserver.go ├── host_client │ ├── .gitignore │ ├── Makefile │ ├── README.md │ └── hostclient.go ├── letsencrypt │ └── letsencryptserver.go └── multidomain │ ├── Makefile │ ├── README.md │ └── multidomain.go ├── expvarhandler ├── expvar.go └── expvar_test.go ├── fasthttpadaptor ├── adaptor.go ├── adaptor_test.go ├── b2s_new.go ├── b2s_old.go ├── request.go └── request_test.go ├── fasthttpproxy ├── dialer.go ├── dialer_test.go ├── doc.go ├── http.go ├── proxy_env.go └── socks5.go ├── fasthttputil ├── doc.go ├── inmemory_listener.go ├── inmemory_listener_test.go ├── inmemory_listener_timing_test.go ├── pipeconns.go └── pipeconns_test.go ├── fs.go ├── fs_example_test.go ├── fs_fs_test.go ├── fs_handler_example_test.go ├── fs_test.go ├── fuzz_test.go ├── go.mod ├── go.sum ├── header.go ├── header_regression_test.go ├── header_test.go ├── header_timing_test.go ├── headers.go ├── http.go ├── http_test.go ├── http_timing_test.go ├── lbclient.go ├── lbclient_example_test.go ├── methods.go ├── nocopy.go ├── peripconn.go ├── peripconn_test.go ├── pprofhandler └── pprof.go ├── prefork ├── README.md ├── prefork.go └── prefork_test.go ├── requestctx_setbodystreamwriter_example_test.go ├── reuseport ├── LICENSE ├── reuseport.go ├── reuseport_aix.go ├── reuseport_error.go ├── reuseport_example_test.go ├── reuseport_test.go └── reuseport_windows.go ├── round2_32.go ├── round2_32_test.go ├── round2_64.go ├── round2_64_test.go ├── s2b_new.go ├── s2b_old.go ├── server.go ├── server_example_test.go ├── server_race_test.go ├── server_test.go ├── server_timing_test.go ├── stackless ├── doc.go ├── func.go ├── func_test.go ├── func_timing_test.go ├── writer.go └── writer_test.go ├── status.go ├── status_test.go ├── status_timing_test.go ├── stream.go ├── stream_test.go ├── stream_timing_test.go ├── streaming.go ├── streaming_test.go ├── strings.go ├── tcpdialer.go ├── tcplisten ├── README.md ├── socket.go ├── socket_darwin.go ├── socket_other.go ├── socket_zos_s390x.go ├── tcplisten.go ├── tcplisten_js_wasm.go ├── tcplisten_linux.go ├── tcplisten_other.go └── tcplisten_test.go ├── testdata └── test.png ├── timer.go ├── tls.go ├── uri.go ├── uri_test.go ├── uri_timing_test.go ├── uri_unix.go ├── uri_windows.go ├── uri_windows_test.go ├── userdata.go ├── userdata_test.go ├── userdata_timing_test.go ├── workerpool.go ├── workerpool_test.go ├── zstd.go └── zstd_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "gomod" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/cifuzz.yml: -------------------------------------------------------------------------------- 1 | name: CIFuzz 2 | on: [pull_request] 3 | jobs: 4 | Fuzzing: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Build Fuzzers 8 | id: build 9 | uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master 10 | with: 11 | oss-fuzz-project-name: 'fasthttp' 12 | dry-run: false 13 | language: go 14 | - name: Run Fuzzers 15 | uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master 16 | with: 17 | oss-fuzz-project-name: 'fasthttp' 18 | fuzz-seconds: 300 19 | dry-run: false 20 | language: go 21 | - name: Upload Crash 22 | uses: actions/upload-artifact@v4 23 | if: failure() && steps.build.outcome == 'success' 24 | with: 25 | name: artifacts 26 | path: ./out/artifacts 27 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | permissions: 9 | # Required: allow read access to the content for analysis. 10 | contents: read 11 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 12 | pull-requests: read 13 | # Optional: Allow write access to checks to allow the action to annotate code in the PR. 14 | checks: write 15 | 16 | jobs: 17 | lint: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-go@v5 22 | with: 23 | go-version: 1.24.x 24 | - run: go version 25 | - name: Run golangci-lint 26 | uses: golangci/golangci-lint-action@v8 27 | with: 28 | version: v2.1.0 29 | args: --verbose 30 | -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | name: Security 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | go-version: [1.20.x] 12 | platform: [ubuntu-latest] 13 | runs-on: ${{ matrix.platform }} 14 | env: 15 | GO111MODULE: on 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Run Gosec Security Scanner 19 | uses: securego/gosec@v2.22.4 20 | with: 21 | args: '-exclude=G103,G104,G304,G402 ./...' 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | jobs: 8 | test: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | go-version: [1.23.x, 1.24.x] 13 | os: [ubuntu-latest, macos-latest, windows-latest, macos-14] 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-go@v5 18 | with: 19 | go-version: ${{ matrix.go-version }} 20 | 21 | - run: go version 22 | - run: go test -shuffle=on ./... 23 | - run: go test -race -shuffle=on ./... 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tags 2 | *.pprof 3 | *.fasthttp.br 4 | *.fasthttp.zst 5 | *.fasthttp.gz 6 | .idea 7 | .vscode 8 | .DS_Store 9 | vendor/ 10 | testdata/fuzz 11 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: all 4 | disable: 5 | - copyloopvar 6 | - cyclop 7 | - depguard 8 | - dupl 9 | - err113 10 | - errname 11 | - errorlint 12 | - exhaustive 13 | - exhaustruct 14 | - forbidigo 15 | - forcetypeassert 16 | - funcorder 17 | - funlen 18 | - gochecknoglobals 19 | - gocognit 20 | - goconst 21 | - gocyclo 22 | - godox 23 | - gosec 24 | - gosmopolitan 25 | - inamedparam 26 | - intrange 27 | - ireturn 28 | - maintidx 29 | - mnd 30 | - nakedret 31 | - nestif 32 | - nlreturn 33 | - noctx 34 | - nonamedreturns 35 | - paralleltest 36 | - testableexamples 37 | - testpackage 38 | - thelper 39 | - tparallel 40 | - unparam 41 | - usestdlibvars 42 | - varnamelen 43 | - wrapcheck 44 | - wsl 45 | settings: 46 | gocritic: 47 | disabled-checks: 48 | - deferInLoop 49 | - importShadow 50 | - sloppyReassign 51 | - unnamedResult 52 | - whyNoLint 53 | enabled-tags: 54 | - diagnostic 55 | - experimental 56 | - opinionated 57 | - performance 58 | - style 59 | govet: 60 | disable: 61 | - fieldalignment 62 | - shadow 63 | enable-all: true 64 | lll: 65 | line-length: 130 66 | revive: 67 | rules: 68 | - name: indent-error-flow 69 | - name: use-any 70 | staticcheck: 71 | checks: 72 | - -ST1000 73 | - all 74 | exclusions: 75 | generated: lax 76 | presets: 77 | - common-false-positives 78 | - legacy 79 | - std-error-handling 80 | rules: 81 | - linters: 82 | - lll 83 | path: _test\.go 84 | paths: 85 | - third_party$ 86 | - builtin$ 87 | - examples$ 88 | issues: 89 | max-issues-per-linter: 0 90 | max-same-issues: 0 91 | formatters: 92 | enable: 93 | - gci 94 | - gofmt 95 | - gofumpt 96 | - goimports 97 | exclusions: 98 | generated: lax 99 | paths: 100 | - third_party$ 101 | - builtin$ 102 | - examples$ 103 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Aliaksandr Valialkin, VertaMedia, Kirill Danshin, Erik Dubbelboer, FastHTTP Authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ### TL;DR 2 | 3 | We use a simplified version of [Golang Security Policy](https://go.dev/security). 4 | For example, for now we skip CVE assignment. 5 | 6 | ### Reporting a Security Bug 7 | 8 | Please report to us any issues you find. This document explains how to do that and what to expect in return. 9 | 10 | All security bugs in our releases should be reported by email to erik@dubbelboer.com 11 | Your email will be acknowledged within 24 hours, and you'll receive a more detailed response 12 | to your email within 72 hours indicating the next steps in handling your report. 13 | Please use a descriptive subject line for your report email. 14 | 15 | ### Flagging Existing Issues as Security-related 16 | 17 | If you believe that an existing issue is security-related, we ask that you send an email to erik@dubbelboer.com 18 | The email should include the issue ID and a short description of why it should be handled according to this security policy. 19 | 20 | ### Disclosure Process 21 | 22 | Our project uses the following disclosure process: 23 | 24 | - Once the security report is received it is assigned a primary handler. This person coordinates the fix and release process. 25 | - The issue is confirmed and a list of affected software is determined. 26 | - Code is audited to find any potential similar problems. 27 | - Fixes are prepared for the two most recent major releases and the head/master revision. These fixes are not yet committed to the public repository. 28 | - To notify users, a new issue without security details is submitted to our GitHub repository. 29 | - Three working days following this notification, the fixes are applied to the public repository and a new release is issued. 30 | - On the date that the fixes are applied, announcement is published in the issue. 31 | 32 | This process can take some time, especially when coordination is required with maintainers of other projects. 33 | Every effort will be made to handle the bug in as timely a manner as possible, however it's important that we follow 34 | the process described above to ensure that disclosures are handled consistently. 35 | 36 | ### Receiving Security Updates 37 | The best way to receive security announcements is to subscribe ("Watch") to our repository. 38 | Any GitHub issues pertaining to a security issue will be prefixed with [security]. 39 | 40 | ### Comments on This Policy 41 | If you have any suggestions to improve this policy, please send an email to erik@dubbelboer.com for discussion. 42 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - SessionClient with referer and cookies support. 2 | - ProxyHandler similar to FSHandler. 3 | - WebSockets. See https://tools.ietf.org/html/rfc6455 . 4 | - HTTP/2.0. See https://tools.ietf.org/html/rfc7540 . 5 | -------------------------------------------------------------------------------- /allocation_test.go: -------------------------------------------------------------------------------- 1 | //go:build !race 2 | 3 | package fasthttp 4 | 5 | import ( 6 | "net" 7 | "testing" 8 | ) 9 | 10 | func TestAllocationServeConn(t *testing.T) { 11 | s := &Server{ 12 | Handler: func(ctx *RequestCtx) { 13 | }, 14 | } 15 | 16 | rw := &readWriter{} 17 | // Make space for the request and response here so it 18 | // doesn't allocate within the test. 19 | rw.r.Grow(1024) 20 | rw.w.Grow(1024) 21 | 22 | n := testing.AllocsPerRun(100, func() { 23 | rw.r.WriteString("GET / HTTP/1.1\r\nHost: google.com\r\nCookie: foo=bar\r\n\r\n") 24 | if err := s.ServeConn(rw); err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | // Reset the write buffer to make space for the next response. 29 | rw.w.Reset() 30 | }) 31 | 32 | if n != 0 { 33 | t.Fatalf("expected 0 allocations, got %f", n) 34 | } 35 | } 36 | 37 | func TestAllocationClient(t *testing.T) { 38 | ln, err := net.Listen("tcp4", "127.0.0.1:0") 39 | if err != nil { 40 | t.Fatalf("cannot listen: %v", err) 41 | } 42 | defer ln.Close() 43 | 44 | s := &Server{ 45 | Handler: func(ctx *RequestCtx) { 46 | }, 47 | } 48 | go s.Serve(ln) //nolint:errcheck 49 | 50 | c := &Client{} 51 | url := "http://test:test@" + ln.Addr().String() + "/foo?bar=baz" 52 | 53 | n := testing.AllocsPerRun(100, func() { 54 | req := AcquireRequest() 55 | res := AcquireResponse() 56 | 57 | req.SetRequestURI(url) 58 | if err := c.Do(req, res); err != nil { 59 | t.Fatal(err) 60 | } 61 | 62 | ReleaseRequest(req) 63 | ReleaseResponse(res) 64 | }) 65 | 66 | if n != 0 { 67 | t.Fatalf("expected 0 allocations, got %f", n) 68 | } 69 | } 70 | 71 | func TestAllocationURI(t *testing.T) { 72 | uri := []byte("http://username:password@hello.%e4%b8%96%e7%95%8c.com/some/path?foo=bar#test") 73 | 74 | n := testing.AllocsPerRun(100, func() { 75 | u := AcquireURI() 76 | u.Parse(nil, uri) //nolint:errcheck 77 | ReleaseURI(u) 78 | }) 79 | 80 | if n != 0 { 81 | t.Fatalf("expected 0 allocations, got %f", n) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /args_timing_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func BenchmarkArgsParse(b *testing.B) { 9 | s := []byte("foo=bar&baz=qqq&aaaaa=bbbb") 10 | b.RunParallel(func(pb *testing.PB) { 11 | var a Args 12 | for pb.Next() { 13 | a.ParseBytes(s) 14 | } 15 | }) 16 | } 17 | 18 | func BenchmarkArgsPeek(b *testing.B) { 19 | value := []byte("foobarbaz1234") 20 | key := "foobarbaz" 21 | b.RunParallel(func(pb *testing.PB) { 22 | var a Args 23 | a.SetBytesV(key, value) 24 | for pb.Next() { 25 | if !bytes.Equal(a.Peek(key), value) { 26 | b.Fatalf("unexpected arg value %q. Expecting %q", a.Peek(key), value) 27 | } 28 | } 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /b2s_new.go: -------------------------------------------------------------------------------- 1 | //go:build go1.20 2 | 3 | package fasthttp 4 | 5 | import "unsafe" 6 | 7 | // b2s converts byte slice to a string without memory allocation. 8 | // See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ . 9 | func b2s(b []byte) string { 10 | return unsafe.String(unsafe.SliceData(b), len(b)) 11 | } 12 | -------------------------------------------------------------------------------- /b2s_old.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.20 2 | 3 | package fasthttp 4 | 5 | import "unsafe" 6 | 7 | // b2s converts byte slice to a string without memory allocation. 8 | // See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ . 9 | // 10 | // Note it may break if string and/or slice header will change 11 | // in the future go versions. 12 | func b2s(b []byte) string { 13 | return *(*string)(unsafe.Pointer(&b)) 14 | } 15 | -------------------------------------------------------------------------------- /brotli.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "sync" 8 | 9 | "github.com/andybalholm/brotli" 10 | "github.com/valyala/bytebufferpool" 11 | "github.com/valyala/fasthttp/stackless" 12 | ) 13 | 14 | // Supported compression levels. 15 | const ( 16 | CompressBrotliNoCompression = 0 17 | CompressBrotliBestSpeed = brotli.BestSpeed 18 | CompressBrotliBestCompression = brotli.BestCompression 19 | 20 | // Choose a default brotli compression level comparable to 21 | // CompressDefaultCompression (gzip 6) 22 | // See: https://github.com/valyala/fasthttp/issues/798#issuecomment-626293806 23 | CompressBrotliDefaultCompression = 4 24 | ) 25 | 26 | func acquireBrotliReader(r io.Reader) (*brotli.Reader, error) { 27 | v := brotliReaderPool.Get() 28 | if v == nil { 29 | return brotli.NewReader(r), nil 30 | } 31 | zr := v.(*brotli.Reader) 32 | if err := zr.Reset(r); err != nil { 33 | return nil, err 34 | } 35 | return zr, nil 36 | } 37 | 38 | func releaseBrotliReader(zr *brotli.Reader) { 39 | brotliReaderPool.Put(zr) 40 | } 41 | 42 | var brotliReaderPool sync.Pool 43 | 44 | func acquireStacklessBrotliWriter(w io.Writer, level int) stackless.Writer { 45 | nLevel := normalizeBrotliCompressLevel(level) 46 | p := stacklessBrotliWriterPoolMap[nLevel] 47 | v := p.Get() 48 | if v == nil { 49 | return stackless.NewWriter(w, func(w io.Writer) stackless.Writer { 50 | return acquireRealBrotliWriter(w, level) 51 | }) 52 | } 53 | sw := v.(stackless.Writer) 54 | sw.Reset(w) 55 | return sw 56 | } 57 | 58 | func releaseStacklessBrotliWriter(sw stackless.Writer, level int) { 59 | sw.Close() 60 | nLevel := normalizeBrotliCompressLevel(level) 61 | p := stacklessBrotliWriterPoolMap[nLevel] 62 | p.Put(sw) 63 | } 64 | 65 | func acquireRealBrotliWriter(w io.Writer, level int) *brotli.Writer { 66 | nLevel := normalizeBrotliCompressLevel(level) 67 | p := realBrotliWriterPoolMap[nLevel] 68 | v := p.Get() 69 | if v == nil { 70 | zw := brotli.NewWriterLevel(w, level) 71 | return zw 72 | } 73 | zw := v.(*brotli.Writer) 74 | zw.Reset(w) 75 | return zw 76 | } 77 | 78 | func releaseRealBrotliWriter(zw *brotli.Writer, level int) { 79 | zw.Close() 80 | nLevel := normalizeBrotliCompressLevel(level) 81 | p := realBrotliWriterPoolMap[nLevel] 82 | p.Put(zw) 83 | } 84 | 85 | var ( 86 | stacklessBrotliWriterPoolMap = newCompressWriterPoolMap() 87 | realBrotliWriterPoolMap = newCompressWriterPoolMap() 88 | ) 89 | 90 | // AppendBrotliBytesLevel appends brotlied src to dst using the given 91 | // compression level and returns the resulting dst. 92 | // 93 | // Supported compression levels are: 94 | // 95 | // - CompressBrotliNoCompression 96 | // - CompressBrotliBestSpeed 97 | // - CompressBrotliBestCompression 98 | // - CompressBrotliDefaultCompression 99 | func AppendBrotliBytesLevel(dst, src []byte, level int) []byte { 100 | w := &byteSliceWriter{b: dst} 101 | WriteBrotliLevel(w, src, level) //nolint:errcheck 102 | return w.b 103 | } 104 | 105 | // WriteBrotliLevel writes brotlied p to w using the given compression level 106 | // and returns the number of compressed bytes written to w. 107 | // 108 | // Supported compression levels are: 109 | // 110 | // - CompressBrotliNoCompression 111 | // - CompressBrotliBestSpeed 112 | // - CompressBrotliBestCompression 113 | // - CompressBrotliDefaultCompression 114 | func WriteBrotliLevel(w io.Writer, p []byte, level int) (int, error) { 115 | switch w.(type) { 116 | case *byteSliceWriter, 117 | *bytes.Buffer, 118 | *bytebufferpool.ByteBuffer: 119 | // These writers don't block, so we can just use stacklessWriteBrotli 120 | ctx := &compressCtx{ 121 | w: w, 122 | p: p, 123 | level: level, 124 | } 125 | stacklessWriteBrotli(ctx) 126 | return len(p), nil 127 | default: 128 | zw := acquireStacklessBrotliWriter(w, level) 129 | n, err := zw.Write(p) 130 | releaseStacklessBrotliWriter(zw, level) 131 | return n, err 132 | } 133 | } 134 | 135 | var ( 136 | stacklessWriteBrotliOnce sync.Once 137 | stacklessWriteBrotliFunc func(ctx any) bool 138 | ) 139 | 140 | func stacklessWriteBrotli(ctx any) { 141 | stacklessWriteBrotliOnce.Do(func() { 142 | stacklessWriteBrotliFunc = stackless.NewFunc(nonblockingWriteBrotli) 143 | }) 144 | stacklessWriteBrotliFunc(ctx) 145 | } 146 | 147 | func nonblockingWriteBrotli(ctxv any) { 148 | ctx := ctxv.(*compressCtx) 149 | zw := acquireRealBrotliWriter(ctx.w, ctx.level) 150 | 151 | zw.Write(ctx.p) //nolint:errcheck // no way to handle this error anyway 152 | 153 | releaseRealBrotliWriter(zw, ctx.level) 154 | } 155 | 156 | // WriteBrotli writes brotlied p to w and returns the number of compressed 157 | // bytes written to w. 158 | func WriteBrotli(w io.Writer, p []byte) (int, error) { 159 | return WriteBrotliLevel(w, p, CompressBrotliDefaultCompression) 160 | } 161 | 162 | // AppendBrotliBytes appends brotlied src to dst and returns the resulting dst. 163 | func AppendBrotliBytes(dst, src []byte) []byte { 164 | return AppendBrotliBytesLevel(dst, src, CompressBrotliDefaultCompression) 165 | } 166 | 167 | // WriteUnbrotli writes unbrotlied p to w and returns the number of uncompressed 168 | // bytes written to w. 169 | func WriteUnbrotli(w io.Writer, p []byte) (int, error) { 170 | r := &byteSliceReader{b: p} 171 | zr, err := acquireBrotliReader(r) 172 | if err != nil { 173 | return 0, err 174 | } 175 | n, err := copyZeroAlloc(w, zr) 176 | releaseBrotliReader(zr) 177 | nn := int(n) 178 | if int64(nn) != n { 179 | return 0, fmt.Errorf("too much data unbrotlied: %d", n) 180 | } 181 | return nn, err 182 | } 183 | 184 | // AppendUnbrotliBytes appends unbrotlied src to dst and returns the resulting dst. 185 | func AppendUnbrotliBytes(dst, src []byte) ([]byte, error) { 186 | w := &byteSliceWriter{b: dst} 187 | _, err := WriteUnbrotli(w, src) 188 | return w.b, err 189 | } 190 | 191 | // normalizes compression level into [0..11], so it could be used as an index 192 | // in *PoolMap. 193 | func normalizeBrotliCompressLevel(level int) int { 194 | // -2 is the lowest compression level - CompressHuffmanOnly 195 | // 9 is the highest compression level - CompressBestCompression 196 | if level < 0 || level > 11 { 197 | level = CompressBrotliDefaultCompression 198 | } 199 | return level 200 | } 201 | -------------------------------------------------------------------------------- /brotli_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "testing" 9 | ) 10 | 11 | func TestBrotliBytesSerial(t *testing.T) { 12 | t.Parallel() 13 | 14 | if err := testBrotliBytes(); err != nil { 15 | t.Fatal(err) 16 | } 17 | } 18 | 19 | func TestBrotliBytesConcurrent(t *testing.T) { 20 | t.Parallel() 21 | 22 | if err := testConcurrent(10, testBrotliBytes); err != nil { 23 | t.Fatal(err) 24 | } 25 | } 26 | 27 | func testBrotliBytes() error { 28 | for _, s := range compressTestcases { 29 | if err := testBrotliBytesSingleCase(s); err != nil { 30 | return err 31 | } 32 | } 33 | return nil 34 | } 35 | 36 | func testBrotliBytesSingleCase(s string) error { 37 | prefix := []byte("foobar") 38 | brotlipedS := AppendBrotliBytes(prefix, []byte(s)) 39 | if !bytes.Equal(brotlipedS[:len(prefix)], prefix) { 40 | return fmt.Errorf("unexpected prefix when compressing %q: %q. Expecting %q", s, brotlipedS[:len(prefix)], prefix) 41 | } 42 | 43 | unbrotliedS, err := AppendUnbrotliBytes(prefix, brotlipedS[len(prefix):]) 44 | if err != nil { 45 | return fmt.Errorf("unexpected error when uncompressing %q: %w", s, err) 46 | } 47 | if !bytes.Equal(unbrotliedS[:len(prefix)], prefix) { 48 | return fmt.Errorf("unexpected prefix when uncompressing %q: %q. Expecting %q", s, unbrotliedS[:len(prefix)], prefix) 49 | } 50 | unbrotliedS = unbrotliedS[len(prefix):] 51 | if string(unbrotliedS) != s { 52 | return fmt.Errorf("unexpected uncompressed string %q. Expecting %q", unbrotliedS, s) 53 | } 54 | return nil 55 | } 56 | 57 | func TestBrotliCompressSerial(t *testing.T) { 58 | t.Parallel() 59 | 60 | if err := testBrotliCompress(); err != nil { 61 | t.Fatal(err) 62 | } 63 | } 64 | 65 | func TestBrotliCompressConcurrent(t *testing.T) { 66 | t.Parallel() 67 | 68 | if err := testConcurrent(10, testBrotliCompress); err != nil { 69 | t.Fatal(err) 70 | } 71 | } 72 | 73 | func testBrotliCompress() error { 74 | for _, s := range compressTestcases { 75 | if err := testBrotliCompressSingleCase(s); err != nil { 76 | return err 77 | } 78 | } 79 | return nil 80 | } 81 | 82 | func testBrotliCompressSingleCase(s string) error { 83 | var buf bytes.Buffer 84 | zw := acquireStacklessBrotliWriter(&buf, CompressDefaultCompression) 85 | if _, err := zw.Write([]byte(s)); err != nil { 86 | return fmt.Errorf("unexpected error: %w. s=%q", err, s) 87 | } 88 | releaseStacklessBrotliWriter(zw, CompressDefaultCompression) 89 | 90 | zr, err := acquireBrotliReader(&buf) 91 | if err != nil { 92 | return fmt.Errorf("unexpected error: %w. s=%q", err, s) 93 | } 94 | body, err := io.ReadAll(zr) 95 | if err != nil { 96 | return fmt.Errorf("unexpected error: %w. s=%q", err, s) 97 | } 98 | if string(body) != s { 99 | return fmt.Errorf("unexpected string after decompression: %q. Expecting %q", body, s) 100 | } 101 | releaseBrotliReader(zr) 102 | return nil 103 | } 104 | 105 | func TestCompressHandlerBrotliLevel(t *testing.T) { 106 | t.Parallel() 107 | 108 | expectedBody := createFixedBody(2e4) 109 | h := CompressHandlerBrotliLevel(func(ctx *RequestCtx) { 110 | ctx.Write(expectedBody) //nolint:errcheck 111 | }, CompressBrotliDefaultCompression, CompressDefaultCompression) 112 | 113 | var ctx RequestCtx 114 | var resp Response 115 | 116 | // verify uncompressed response 117 | h(&ctx) 118 | s := ctx.Response.String() 119 | br := bufio.NewReader(bytes.NewBufferString(s)) 120 | if err := resp.Read(br); err != nil { 121 | t.Fatalf("unexpected error: %v", err) 122 | } 123 | ce := resp.Header.ContentEncoding() 124 | if len(ce) != 0 { 125 | t.Fatalf("unexpected Content-Encoding: %q. Expecting %q", ce, "") 126 | } 127 | body := resp.Body() 128 | if !bytes.Equal(body, expectedBody) { 129 | t.Fatalf("unexpected body %q. Expecting %q", body, expectedBody) 130 | } 131 | 132 | // verify gzip-compressed response 133 | ctx.Request.Reset() 134 | ctx.Response.Reset() 135 | ctx.Request.Header.Set("Accept-Encoding", "gzip, deflate, sdhc") 136 | 137 | h(&ctx) 138 | s = ctx.Response.String() 139 | br = bufio.NewReader(bytes.NewBufferString(s)) 140 | if err := resp.Read(br); err != nil { 141 | t.Fatalf("unexpected error: %v", err) 142 | } 143 | ce = resp.Header.ContentEncoding() 144 | if string(ce) != "gzip" { 145 | t.Fatalf("unexpected Content-Encoding: %q. Expecting %q", ce, "gzip") 146 | } 147 | body, err := resp.BodyGunzip() 148 | if err != nil { 149 | t.Fatalf("unexpected error: %v", err) 150 | } 151 | if !bytes.Equal(body, expectedBody) { 152 | t.Fatalf("unexpected body %q. Expecting %q", body, expectedBody) 153 | } 154 | 155 | // verify brotli-compressed response 156 | ctx.Request.Reset() 157 | ctx.Response.Reset() 158 | ctx.Request.Header.Set("Accept-Encoding", "gzip, deflate, sdhc, br") 159 | 160 | h(&ctx) 161 | s = ctx.Response.String() 162 | br = bufio.NewReader(bytes.NewBufferString(s)) 163 | if err := resp.Read(br); err != nil { 164 | t.Fatalf("unexpected error: %v", err) 165 | } 166 | ce = resp.Header.ContentEncoding() 167 | if string(ce) != "br" { 168 | t.Fatalf("unexpected Content-Encoding: %q. Expecting %q", ce, "br") 169 | } 170 | body, err = resp.BodyUnbrotli() 171 | if err != nil { 172 | t.Fatalf("unexpected error: %v", err) 173 | } 174 | if !bytes.Equal(body, expectedBody) { 175 | t.Fatalf("unexpected body %q. Expecting %q", body, expectedBody) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /bytesconv_32.go: -------------------------------------------------------------------------------- 1 | //go:build !amd64 && !arm64 && !ppc64 && !ppc64le && !riscv64 && !s390x 2 | 3 | package fasthttp 4 | 5 | const ( 6 | maxHexIntChars = 7 7 | ) 8 | -------------------------------------------------------------------------------- /bytesconv_32_test.go: -------------------------------------------------------------------------------- 1 | //go:build !amd64 && !arm64 && !ppc64 && !ppc64le && !riscv64 && !s390x 2 | 3 | package fasthttp 4 | 5 | import ( 6 | "testing" 7 | ) 8 | 9 | func TestWriteHexInt(t *testing.T) { 10 | t.Parallel() 11 | 12 | testWriteHexInt(t, 0, "0") 13 | testWriteHexInt(t, 1, "1") 14 | testWriteHexInt(t, 0x123, "123") 15 | testWriteHexInt(t, 0x7fffffff, "7fffffff") 16 | } 17 | 18 | func TestAppendUint(t *testing.T) { 19 | t.Parallel() 20 | 21 | testAppendUint(t, 0) 22 | testAppendUint(t, 123) 23 | testAppendUint(t, 0x7fffffff) 24 | 25 | for i := 0; i < 2345; i++ { 26 | testAppendUint(t, i) 27 | } 28 | } 29 | 30 | func TestReadHexIntSuccess(t *testing.T) { 31 | t.Parallel() 32 | 33 | testReadHexIntSuccess(t, "0", 0) 34 | testReadHexIntSuccess(t, "fF", 0xff) 35 | testReadHexIntSuccess(t, "00abc", 0xabc) 36 | testReadHexIntSuccess(t, "7ffffff", 0x7ffffff) 37 | testReadHexIntSuccess(t, "000", 0) 38 | testReadHexIntSuccess(t, "1234ZZZ", 0x1234) 39 | } 40 | 41 | func TestParseUintError32(t *testing.T) { 42 | t.Parallel() 43 | 44 | // Overflow by last digit: 2 ** 32 / 2 * 10 ** n 45 | testParseUintError(t, "2147483648") 46 | testParseUintError(t, "21474836480") 47 | testParseUintError(t, "214748364800") 48 | } 49 | 50 | func TestParseUintSuccess(t *testing.T) { 51 | t.Parallel() 52 | 53 | testParseUintSuccess(t, "0", 0) 54 | testParseUintSuccess(t, "123", 123) 55 | testParseUintSuccess(t, "123456789", 123456789) 56 | 57 | // Max supported value: 2 ** 32 / 2 - 1 58 | testParseUintSuccess(t, "2147483647", 2147483647) 59 | } 60 | -------------------------------------------------------------------------------- /bytesconv_64.go: -------------------------------------------------------------------------------- 1 | //go:build amd64 || arm64 || ppc64 || ppc64le || riscv64 || s390x 2 | 3 | package fasthttp 4 | 5 | const ( 6 | maxHexIntChars = 15 7 | ) 8 | -------------------------------------------------------------------------------- /bytesconv_64_test.go: -------------------------------------------------------------------------------- 1 | //go:build amd64 || arm64 || ppc64 || ppc64le || riscv64 || s390x 2 | 3 | package fasthttp 4 | 5 | import ( 6 | "testing" 7 | ) 8 | 9 | func TestWriteHexInt(t *testing.T) { 10 | t.Parallel() 11 | 12 | testWriteHexInt(t, 0, "0") 13 | testWriteHexInt(t, 1, "1") 14 | testWriteHexInt(t, 0x123, "123") 15 | testWriteHexInt(t, 0x7fffffffffffffff, "7fffffffffffffff") 16 | } 17 | 18 | func TestAppendUint(t *testing.T) { 19 | t.Parallel() 20 | 21 | testAppendUint(t, 0) 22 | testAppendUint(t, 123) 23 | testAppendUint(t, 0x7fffffffffffffff) 24 | 25 | for i := 0; i < 2345; i++ { 26 | testAppendUint(t, i) 27 | } 28 | } 29 | 30 | func TestReadHexIntSuccess(t *testing.T) { 31 | t.Parallel() 32 | 33 | testReadHexIntSuccess(t, "0", 0) 34 | testReadHexIntSuccess(t, "fF", 0xff) 35 | testReadHexIntSuccess(t, "00abc", 0xabc) 36 | testReadHexIntSuccess(t, "7fffffff", 0x7fffffff) 37 | testReadHexIntSuccess(t, "000", 0) 38 | testReadHexIntSuccess(t, "1234ZZZ", 0x1234) 39 | testReadHexIntSuccess(t, "7ffffffffffffff", 0x7ffffffffffffff) 40 | } 41 | 42 | func TestParseUintError64(t *testing.T) { 43 | t.Parallel() 44 | 45 | // Overflow by last digit: 2 ** 64 / 2 * 10 ** n 46 | testParseUintError(t, "9223372036854775808") 47 | testParseUintError(t, "92233720368547758080") 48 | testParseUintError(t, "922337203685477580800") 49 | } 50 | 51 | func TestParseUintSuccess(t *testing.T) { 52 | t.Parallel() 53 | 54 | testParseUintSuccess(t, "0", 0) 55 | testParseUintSuccess(t, "123", 123) 56 | testParseUintSuccess(t, "1234567890", 1234567890) 57 | testParseUintSuccess(t, "123456789012345678", 123456789012345678) 58 | 59 | // Max supported value: 2 ** 64 / 2 - 1 60 | testParseUintSuccess(t, "9223372036854775807", 9223372036854775807) 61 | } 62 | -------------------------------------------------------------------------------- /bytesconv_timing_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "bufio" 5 | "html" 6 | "net" 7 | "testing" 8 | 9 | "github.com/valyala/bytebufferpool" 10 | ) 11 | 12 | func BenchmarkAppendHTMLEscape(b *testing.B) { 13 | sOrig := "foobarbazxxxyyyzzz" 14 | sExpected := string(AppendHTMLEscape(nil, sOrig)) 15 | b.RunParallel(func(pb *testing.PB) { 16 | var buf []byte 17 | for pb.Next() { 18 | for i := 0; i < 10; i++ { 19 | buf = AppendHTMLEscape(buf[:0], sOrig) 20 | if string(buf) != sExpected { 21 | b.Fatalf("unexpected escaped string: %q. Expecting %q", buf, sExpected) 22 | } 23 | } 24 | } 25 | }) 26 | } 27 | 28 | func BenchmarkHTMLEscapeString(b *testing.B) { 29 | sOrig := "foobarbazxxxyyyzzz" 30 | sExpected := html.EscapeString(sOrig) 31 | b.RunParallel(func(pb *testing.PB) { 32 | var s string 33 | for pb.Next() { 34 | for i := 0; i < 10; i++ { 35 | s = html.EscapeString(sOrig) 36 | if s != sExpected { 37 | b.Fatalf("unexpected escaped string: %q. Expecting %q", s, sExpected) 38 | } 39 | } 40 | } 41 | }) 42 | } 43 | 44 | func BenchmarkParseIPv4(b *testing.B) { 45 | ipStr := []byte("123.145.167.189") 46 | b.RunParallel(func(pb *testing.PB) { 47 | var ip net.IP 48 | var err error 49 | for pb.Next() { 50 | ip, err = ParseIPv4(ip, ipStr) 51 | if err != nil { 52 | b.Fatalf("unexpected error: %v", err) 53 | } 54 | } 55 | }) 56 | } 57 | 58 | func BenchmarkAppendIPv4(b *testing.B) { 59 | ip := net.ParseIP("123.145.167.189") 60 | b.RunParallel(func(pb *testing.PB) { 61 | var buf []byte 62 | for pb.Next() { 63 | buf = AppendIPv4(buf[:0], ip) 64 | } 65 | }) 66 | } 67 | 68 | func BenchmarkWriteHexInt(b *testing.B) { 69 | b.RunParallel(func(pb *testing.PB) { 70 | var w bytebufferpool.ByteBuffer 71 | bw := bufio.NewWriter(&w) 72 | i := 0 73 | for pb.Next() { 74 | writeHexInt(bw, i) //nolint:errcheck 75 | i++ 76 | if i > 0x7fffffff { 77 | i = 0 78 | } 79 | w.Reset() 80 | bw.Reset(&w) 81 | } 82 | }) 83 | } 84 | 85 | func BenchmarkParseUint(b *testing.B) { 86 | b.RunParallel(func(pb *testing.PB) { 87 | buf := []byte("1234567") 88 | for pb.Next() { 89 | n, err := ParseUint(buf) 90 | if err != nil { 91 | b.Fatalf("unexpected error: %v", err) 92 | } 93 | if n != 1234567 { 94 | b.Fatalf("unexpected result: %d. Expecting %q", n, buf) 95 | } 96 | } 97 | }) 98 | } 99 | 100 | func BenchmarkAppendUint(b *testing.B) { 101 | b.RunParallel(func(pb *testing.PB) { 102 | var buf []byte 103 | i := 0 104 | for pb.Next() { 105 | buf = AppendUint(buf[:0], i) 106 | i++ 107 | if i > 0x7fffffff { 108 | i = 0 109 | } 110 | } 111 | }) 112 | } 113 | 114 | func BenchmarkLowercaseBytesNoop(b *testing.B) { 115 | src := []byte("foobarbaz_lowercased_all") 116 | b.RunParallel(func(pb *testing.PB) { 117 | s := make([]byte, len(src)) 118 | for pb.Next() { 119 | copy(s, src) 120 | lowercaseBytes(s) 121 | } 122 | }) 123 | } 124 | 125 | func BenchmarkLowercaseBytesAll(b *testing.B) { 126 | src := []byte("FOOBARBAZ_UPPERCASED_ALL") 127 | b.RunParallel(func(pb *testing.PB) { 128 | s := make([]byte, len(src)) 129 | for pb.Next() { 130 | copy(s, src) 131 | lowercaseBytes(s) 132 | } 133 | }) 134 | } 135 | 136 | func BenchmarkLowercaseBytesMixed(b *testing.B) { 137 | src := []byte("Foobarbaz_Uppercased_Mix") 138 | b.RunParallel(func(pb *testing.PB) { 139 | s := make([]byte, len(src)) 140 | for pb.Next() { 141 | copy(s, src) 142 | lowercaseBytes(s) 143 | } 144 | }) 145 | } 146 | 147 | func BenchmarkAppendUnquotedArgFastPath(b *testing.B) { 148 | src := []byte("foobarbaz no quoted chars fdskjsdf jklsdfdfskljd;aflskjdsaf fdsklj fsdkj fsdl kfjsdlk jfsdklj fsdfsdf sdfkflsd") 149 | b.RunParallel(func(pb *testing.PB) { 150 | var dst []byte 151 | for pb.Next() { 152 | dst = AppendUnquotedArg(dst[:0], src) 153 | } 154 | }) 155 | } 156 | 157 | func BenchmarkAppendUnquotedArgSlowPath(b *testing.B) { 158 | src := []byte("D0%B4%20%D0%B0%D0%B2%D0%BB%D0%B4%D1%84%D1%8B%D0%B0%D0%BE%20%D1%84%D0%B2%D0%B6%D0%BB%D0%B4%D1%8B%20%D0%B0%D0%BE") 159 | b.RunParallel(func(pb *testing.PB) { 160 | var dst []byte 161 | for pb.Next() { 162 | dst = AppendUnquotedArg(dst[:0], src) 163 | } 164 | }) 165 | } 166 | 167 | func BenchmarkParseUfloat(b *testing.B) { 168 | src := [][]byte{ 169 | []byte("0"), 170 | []byte("1234566789."), 171 | []byte(".1234556778"), 172 | []byte("123.456"), 173 | []byte("123456789"), 174 | []byte("1234e23"), 175 | []byte("1234E-51"), 176 | []byte("1.234e+32"), 177 | []byte("123456789123456789.987654321"), 178 | } 179 | b.RunParallel(func(pb *testing.PB) { 180 | for pb.Next() { 181 | for i := range src { 182 | _, err := ParseUfloat(src[i]) 183 | if err != nil { 184 | b.Fatalf("unexpected error: %v", err) 185 | } 186 | } 187 | } 188 | }) 189 | } 190 | -------------------------------------------------------------------------------- /client_example_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp_test 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/valyala/fasthttp" 7 | ) 8 | 9 | func ExampleHostClient() { 10 | // Prepare a client, which fetches webpages via HTTP proxy listening 11 | // on the localhost:8080. 12 | c := &fasthttp.HostClient{ 13 | Addr: "localhost:8080", 14 | } 15 | 16 | // Fetch google page via local proxy. 17 | statusCode, body, err := c.Get(nil, "http://google.com/foo/bar") 18 | if err != nil { 19 | log.Fatalf("Error when loading google page through local proxy: %v", err) 20 | } 21 | if statusCode != fasthttp.StatusOK { 22 | log.Fatalf("Unexpected status code: %d. Expecting %d", statusCode, fasthttp.StatusOK) 23 | } 24 | useResponseBody(body) 25 | 26 | // Fetch foobar page via local proxy. Reuse body buffer. 27 | statusCode, body, err = c.Get(body, "http://foobar.com/google/com") 28 | if err != nil { 29 | log.Fatalf("Error when loading foobar page through local proxy: %v", err) 30 | } 31 | if statusCode != fasthttp.StatusOK { 32 | log.Fatalf("Unexpected status code: %d. Expecting %d", statusCode, fasthttp.StatusOK) 33 | } 34 | useResponseBody(body) 35 | } 36 | 37 | func useResponseBody(body []byte) { 38 | // Do something with body :) 39 | } 40 | -------------------------------------------------------------------------------- /client_timing_wait_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "net/http" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/valyala/fasthttp/fasthttputil" 12 | ) 13 | 14 | func newFasthttpSleepEchoHandler(sleep time.Duration) RequestHandler { 15 | return func(ctx *RequestCtx) { 16 | time.Sleep(sleep) 17 | ctx.Success("text/plain", ctx.RequestURI()) 18 | } 19 | } 20 | 21 | func BenchmarkClientGetEndToEndWaitConn1Inmemory(b *testing.B) { 22 | benchmarkClientGetEndToEndWaitConnInmemory(b, 1) 23 | } 24 | 25 | func BenchmarkClientGetEndToEndWaitConn10Inmemory(b *testing.B) { 26 | benchmarkClientGetEndToEndWaitConnInmemory(b, 10) 27 | } 28 | 29 | func BenchmarkClientGetEndToEndWaitConn100Inmemory(b *testing.B) { 30 | benchmarkClientGetEndToEndWaitConnInmemory(b, 100) 31 | } 32 | 33 | func BenchmarkClientGetEndToEndWaitConn1000Inmemory(b *testing.B) { 34 | benchmarkClientGetEndToEndWaitConnInmemory(b, 1000) 35 | } 36 | 37 | func benchmarkClientGetEndToEndWaitConnInmemory(b *testing.B, parallelism int) { 38 | ln := fasthttputil.NewInmemoryListener() 39 | 40 | ch := make(chan struct{}) 41 | sleepDuration := 50 * time.Millisecond 42 | go func() { 43 | if err := Serve(ln, newFasthttpSleepEchoHandler(sleepDuration)); err != nil { 44 | b.Errorf("error when serving requests: %v", err) 45 | } 46 | close(ch) 47 | }() 48 | 49 | c := &Client{ 50 | MaxConnsPerHost: 1, 51 | Dial: func(addr string) (net.Conn, error) { return ln.Dial() }, 52 | MaxConnWaitTimeout: 5 * time.Second, 53 | } 54 | 55 | requestURI := "/foo/bar?baz=123&sleep=10ms" 56 | url := "http://unused.host" + requestURI 57 | b.SetParallelism(parallelism) 58 | b.RunParallel(func(pb *testing.PB) { 59 | var buf []byte 60 | for pb.Next() { 61 | statusCode, body, err := c.Get(buf, url) 62 | if err != nil { 63 | if err != ErrNoFreeConns { 64 | b.Fatalf("unexpected error: %v", err) 65 | } 66 | } else { 67 | if statusCode != StatusOK { 68 | b.Fatalf("unexpected status code: %d. Expecting %d", statusCode, StatusOK) 69 | } 70 | if string(body) != requestURI { 71 | b.Fatalf("unexpected response %q. Expecting %q", body, requestURI) 72 | } 73 | } 74 | buf = body 75 | } 76 | }) 77 | 78 | ln.Close() 79 | select { 80 | case <-ch: 81 | case <-time.After(time.Second): 82 | b.Fatalf("server wasn't stopped") 83 | } 84 | } 85 | 86 | func newNethttpSleepEchoHandler(sleep time.Duration) http.HandlerFunc { 87 | return func(w http.ResponseWriter, r *http.Request) { 88 | time.Sleep(sleep) 89 | w.Header().Set(HeaderContentType, "text/plain") 90 | w.Write([]byte(r.RequestURI)) //nolint:errcheck 91 | } 92 | } 93 | 94 | func BenchmarkNetHTTPClientGetEndToEndWaitConn1Inmemory(b *testing.B) { 95 | benchmarkNetHTTPClientGetEndToEndWaitConnInmemory(b, 1) 96 | } 97 | 98 | func BenchmarkNetHTTPClientGetEndToEndWaitConn10Inmemory(b *testing.B) { 99 | benchmarkNetHTTPClientGetEndToEndWaitConnInmemory(b, 10) 100 | } 101 | 102 | func BenchmarkNetHTTPClientGetEndToEndWaitConn100Inmemory(b *testing.B) { 103 | benchmarkNetHTTPClientGetEndToEndWaitConnInmemory(b, 100) 104 | } 105 | 106 | func BenchmarkNetHTTPClientGetEndToEndWaitConn1000Inmemory(b *testing.B) { 107 | benchmarkNetHTTPClientGetEndToEndWaitConnInmemory(b, 1000) 108 | } 109 | 110 | func benchmarkNetHTTPClientGetEndToEndWaitConnInmemory(b *testing.B, parallelism int) { 111 | ln := fasthttputil.NewInmemoryListener() 112 | 113 | ch := make(chan struct{}) 114 | sleep := 50 * time.Millisecond 115 | go func() { 116 | if err := http.Serve(ln, newNethttpSleepEchoHandler(sleep)); err != nil && !strings.Contains( 117 | err.Error(), "use of closed network connection") { 118 | b.Errorf("error when serving requests: %v", err) 119 | } 120 | close(ch) 121 | }() 122 | 123 | c := &http.Client{ 124 | Transport: &http.Transport{ 125 | Dial: func(_, _ string) (net.Conn, error) { return ln.Dial() }, 126 | MaxConnsPerHost: 1, 127 | }, 128 | Timeout: 5 * time.Second, 129 | } 130 | 131 | requestURI := "/foo/bar?baz=123" 132 | url := "http://unused.host" + requestURI 133 | b.SetParallelism(parallelism) 134 | b.RunParallel(func(pb *testing.PB) { 135 | for pb.Next() { 136 | resp, err := c.Get(url) 137 | if err != nil { 138 | if netErr, ok := err.(net.Error); !ok || !netErr.Timeout() { 139 | b.Fatalf("unexpected error: %v", err) 140 | } 141 | } else { 142 | if resp.StatusCode != http.StatusOK { 143 | b.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode, http.StatusOK) 144 | } 145 | body, err := io.ReadAll(resp.Body) 146 | resp.Body.Close() 147 | if err != nil { 148 | b.Fatalf("unexpected error when reading response body: %v", err) 149 | } 150 | if string(body) != requestURI { 151 | b.Fatalf("unexpected response %q. Expecting %q", body, requestURI) 152 | } 153 | } 154 | } 155 | }) 156 | 157 | ln.Close() 158 | select { 159 | case <-ch: 160 | case <-time.After(time.Second): 161 | b.Fatalf("server wasn't stopped") 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /coarsetime.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // CoarseTimeNow returns the current time truncated to the nearest second. 8 | // 9 | // Deprecated: This is slower than calling time.Now() directly. 10 | // This is now time.Now().Truncate(time.Second) shortcut. 11 | func CoarseTimeNow() time.Time { 12 | return time.Now().Truncate(time.Second) 13 | } 14 | -------------------------------------------------------------------------------- /coarsetime_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "sync/atomic" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func BenchmarkCoarseTimeNow(b *testing.B) { 10 | var zeroTimeCount uint64 11 | b.RunParallel(func(pb *testing.PB) { 12 | for pb.Next() { 13 | t := CoarseTimeNow() 14 | if t.IsZero() { 15 | atomic.AddUint64(&zeroTimeCount, 1) 16 | } 17 | } 18 | }) 19 | if zeroTimeCount > 0 { 20 | b.Fatalf("zeroTimeCount must be zero") 21 | } 22 | } 23 | 24 | func BenchmarkTimeNow(b *testing.B) { 25 | var zeroTimeCount uint64 26 | b.RunParallel(func(pb *testing.PB) { 27 | for pb.Next() { 28 | t := time.Now() 29 | if t.IsZero() { 30 | atomic.AddUint64(&zeroTimeCount, 1) 31 | } 32 | } 33 | }) 34 | if zeroTimeCount > 0 { 35 | b.Fatalf("zeroTimeCount must be zero") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cookie_timing_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func BenchmarkCookieParseMin(b *testing.B) { 8 | var c Cookie 9 | s := []byte("xxx=yyy") 10 | for i := 0; i < b.N; i++ { 11 | if err := c.ParseBytes(s); err != nil { 12 | b.Fatalf("unexpected error when parsing cookies: %v", err) 13 | } 14 | } 15 | } 16 | 17 | func BenchmarkCookieParseNoExpires(b *testing.B) { 18 | var c Cookie 19 | s := []byte("xxx=yyy; domain=foobar.com; path=/a/b") 20 | for i := 0; i < b.N; i++ { 21 | if err := c.ParseBytes(s); err != nil { 22 | b.Fatalf("unexpected error when parsing cookies: %v", err) 23 | } 24 | } 25 | } 26 | 27 | func BenchmarkCookieParseFull(b *testing.B) { 28 | var c Cookie 29 | s := []byte("xxx=yyy; expires=Tue, 10 Nov 2009 23:00:00 GMT; domain=foobar.com; path=/a/b") 30 | for i := 0; i < b.N; i++ { 31 | if err := c.ParseBytes(s); err != nil { 32 | b.Fatalf("unexpected error when parsing cookies: %v", err) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package fasthttp provides fast HTTP server and client API. 3 | 4 | Fasthttp provides the following features: 5 | 6 | 1. Optimized for speed. Easily handles more than 100K qps and more than 1M 7 | concurrent keep-alive connections on modern hardware. 8 | 9 | 2. Optimized for low memory usage. 10 | 11 | 3. Easy 'Connection: Upgrade' support via RequestCtx.Hijack. 12 | 13 | 4. Server provides the following anti-DoS limits: 14 | 15 | - The number of concurrent connections. 16 | 17 | - The number of concurrent connections per client IP. 18 | 19 | - The number of requests per connection. 20 | 21 | - Request read timeout. 22 | 23 | - Response write timeout. 24 | 25 | - Maximum request header size. 26 | 27 | - Maximum request body size. 28 | 29 | - Maximum request execution time. 30 | 31 | - Maximum keep-alive connection lifetime. 32 | 33 | - Early filtering out non-GET requests. 34 | 35 | 5. A lot of additional useful info is exposed to request handler: 36 | 37 | - Server and client address. 38 | 39 | - Per-request logger. 40 | 41 | - Unique request id. 42 | 43 | - Request start time. 44 | 45 | - Connection start time. 46 | 47 | - Request sequence number for the current connection. 48 | 49 | 6. Client supports automatic retry on idempotent requests' failure. 50 | 51 | 7. Fasthttp API is designed with the ability to extend existing client 52 | and server implementations or to write custom client and server 53 | implementations from scratch. 54 | */ 55 | package fasthttp 56 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Code examples 2 | 3 | * [HelloWorld server](helloworldserver) 4 | * [Static file server](fileserver) 5 | -------------------------------------------------------------------------------- /examples/client/.gitignore: -------------------------------------------------------------------------------- 1 | client 2 | -------------------------------------------------------------------------------- /examples/client/Makefile: -------------------------------------------------------------------------------- 1 | client: clean 2 | go get -u github.com/valyala/fasthttp 3 | go build 4 | 5 | clean: 6 | rm -f client 7 | -------------------------------------------------------------------------------- /examples/client/README.md: -------------------------------------------------------------------------------- 1 | # Client Example 2 | 3 | The Client is useful when working with multiple hostnames. 4 | 5 | See the simplest `sendGetRequest()` for GET and more advanced `sendPostRequest()` for a POST request. 6 | 7 | The `sendPostRequest()` also shows: 8 | * Per-request timeout with `DoTimeout()` 9 | * Send a body as bytes slice with `SetBodyRaw()`. This is useful if you generated a request body. Otherwise, prefer `SetBody()` which copies it. 10 | * Parse JSON from response 11 | * Gracefully show error messages i.e. timeouts as warnings and other errors as a failures with detailed error messages. 12 | 13 | ## How to build and run 14 | Start a web server on localhost:8080 then execute: 15 | 16 | make 17 | ./client 18 | 19 | ## Client vs HostClient 20 | Internally the Client creates a dedicated HostClient for each domain/IP address and cleans unused after period of time. 21 | So if you have a single heavily loaded API endpoint it's better to use HostClient. See an example in the [examples/host_client](../host_client/) 22 | -------------------------------------------------------------------------------- /examples/client/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "os" 10 | "reflect" 11 | "time" 12 | 13 | "github.com/valyala/fasthttp" 14 | ) 15 | 16 | var headerContentTypeJSON = []byte("application/json") 17 | 18 | var client *fasthttp.Client 19 | 20 | type Entity struct { 21 | Name string `json:"name"` 22 | ID int `json:"id"` 23 | } 24 | 25 | func main() { 26 | // You may read the timeouts from some config 27 | readTimeout, _ := time.ParseDuration("500ms") 28 | writeTimeout, _ := time.ParseDuration("500ms") 29 | maxIdleConnDuration, _ := time.ParseDuration("1h") 30 | client = &fasthttp.Client{ 31 | ReadTimeout: readTimeout, 32 | WriteTimeout: writeTimeout, 33 | MaxIdleConnDuration: maxIdleConnDuration, 34 | NoDefaultUserAgentHeader: true, // Don't send: User-Agent: fasthttp 35 | DisableHeaderNamesNormalizing: true, // If you set the case on your headers correctly you can enable this 36 | DisablePathNormalizing: true, 37 | // increase DNS cache time to an hour instead of default minute 38 | Dial: (&fasthttp.TCPDialer{ 39 | Concurrency: 4096, 40 | DNSCacheDuration: time.Hour, 41 | }).Dial, 42 | } 43 | sendGetRequest() 44 | sendPostRequest() 45 | } 46 | 47 | func sendGetRequest() { 48 | req := fasthttp.AcquireRequest() 49 | req.SetRequestURI("http://localhost:8080/") 50 | req.Header.SetMethod(fasthttp.MethodGet) 51 | resp := fasthttp.AcquireResponse() 52 | err := client.Do(req, resp) 53 | fasthttp.ReleaseRequest(req) 54 | if err == nil { 55 | fmt.Printf("DEBUG Response: %s\n", resp.Body()) 56 | } else { 57 | fmt.Fprintf(os.Stderr, "ERR Connection error: %v\n", err) 58 | } 59 | fasthttp.ReleaseResponse(resp) 60 | } 61 | 62 | func sendPostRequest() { 63 | // per-request timeout 64 | reqTimeout := time.Duration(100) * time.Millisecond 65 | 66 | reqEntity := &Entity{ 67 | Name: "New entity", 68 | } 69 | reqEntityBytes, _ := json.Marshal(reqEntity) //nolint:errchkjson 70 | 71 | req := fasthttp.AcquireRequest() 72 | req.SetRequestURI("http://localhost:8080/") 73 | req.Header.SetMethod(fasthttp.MethodPost) 74 | req.Header.SetContentTypeBytes(headerContentTypeJSON) 75 | req.SetBodyRaw(reqEntityBytes) 76 | 77 | resp := fasthttp.AcquireResponse() 78 | err := client.DoTimeout(req, resp, reqTimeout) 79 | fasthttp.ReleaseRequest(req) 80 | defer fasthttp.ReleaseResponse(resp) 81 | 82 | if err != nil { 83 | errName, known := httpConnError(err) 84 | if known { 85 | fmt.Fprintf(os.Stderr, "WARN conn error: %v\n", errName) 86 | } else { 87 | fmt.Fprintf(os.Stderr, "ERR conn failure: %v %v\n", errName, err) 88 | } 89 | 90 | return 91 | } 92 | 93 | statusCode := resp.StatusCode() 94 | respBody := resp.Body() 95 | fmt.Printf("DEBUG Response: %s\n", respBody) 96 | 97 | if statusCode != http.StatusOK { 98 | fmt.Fprintf(os.Stderr, "ERR invalid HTTP response code: %d\n", statusCode) 99 | 100 | return 101 | } 102 | 103 | respEntity := &Entity{} 104 | err = json.Unmarshal(respBody, respEntity) 105 | if err == nil || errors.Is(err, io.EOF) { 106 | fmt.Printf("DEBUG Parsed Response: %v\n", respEntity) 107 | } else { 108 | fmt.Fprintf(os.Stderr, "ERR failed to parse response: %v\n", err) 109 | } 110 | } 111 | 112 | func httpConnError(err error) (string, bool) { 113 | var ( 114 | errName string 115 | known = true 116 | ) 117 | 118 | switch { 119 | case errors.Is(err, fasthttp.ErrTimeout): 120 | errName = "timeout" 121 | case errors.Is(err, fasthttp.ErrNoFreeConns): 122 | errName = "conn_limit" 123 | case errors.Is(err, fasthttp.ErrConnectionClosed): 124 | errName = "conn_close" 125 | case reflect.TypeOf(err).String() == "*net.OpError": 126 | errName = "timeout" 127 | default: 128 | known = false 129 | } 130 | 131 | return errName, known 132 | } 133 | -------------------------------------------------------------------------------- /examples/fileserver/.gitignore: -------------------------------------------------------------------------------- 1 | fileserver 2 | -------------------------------------------------------------------------------- /examples/fileserver/Makefile: -------------------------------------------------------------------------------- 1 | fileserver: clean 2 | go get -u github.com/valyala/fasthttp 3 | go get -u github.com/valyala/fasthttp/expvarhandler 4 | go build 5 | 6 | clean: 7 | rm -f fileserver 8 | -------------------------------------------------------------------------------- /examples/fileserver/README.md: -------------------------------------------------------------------------------- 1 | # Static file server example 2 | 3 | * Serves files from the given directory. 4 | * Supports transparent response compression. 5 | * Supports byte range responses. 6 | * Generates directory index pages. 7 | * Supports TLS (aka SSL or HTTPS). 8 | * Supports virtual hosts. 9 | * Exports various stats on /stats path. 10 | 11 | # How to build 12 | 13 | ``` 14 | make 15 | ``` 16 | 17 | # How to run 18 | 19 | ``` 20 | ./fileserver -h 21 | ./fileserver -addr=tcp.addr.to.listen:to -dir=/path/to/directory/to/serve 22 | ``` 23 | 24 | # fileserver vs nginx performance comparison 25 | 26 | Serving default nginx path (`/usr/share/nginx/html` on ubuntu). 27 | 28 | * nginx 29 | 30 | ``` 31 | $ ./wrk -t 4 -c 16 -d 10 http://localhost:80 32 | Running 10s test @ http://localhost:80 33 | 4 threads and 16 connections 34 | Thread Stats Avg Stdev Max +/- Stdev 35 | Latency 397.76us 1.08ms 20.23ms 95.19% 36 | Req/Sec 21.20k 2.49k 31.34k 79.65% 37 | 850220 requests in 10.10s, 695.65MB read 38 | Requests/sec: 84182.71 39 | Transfer/sec: 68.88MB 40 | ``` 41 | 42 | * fileserver 43 | 44 | ``` 45 | $ ./wrk -t 4 -c 16 -d 10 http://localhost:8080 46 | Running 10s test @ http://localhost:8080 47 | 4 threads and 16 connections 48 | Thread Stats Avg Stdev Max +/- Stdev 49 | Latency 447.99us 1.59ms 27.20ms 94.79% 50 | Req/Sec 37.13k 3.99k 47.86k 76.00% 51 | 1478457 requests in 10.02s, 1.03GB read 52 | Requests/sec: 147597.06 53 | Transfer/sec: 105.15MB 54 | ``` 55 | 56 | 8 pipelined requests 57 | 58 | * nginx 59 | 60 | ``` 61 | $ ./wrk -s pipeline.lua -t 4 -c 16 -d 10 http://localhost:80 -- 8 62 | Running 10s test @ http://localhost:80 63 | 4 threads and 16 connections 64 | Thread Stats Avg Stdev Max +/- Stdev 65 | Latency 1.34ms 2.15ms 30.91ms 92.16% 66 | Req/Sec 33.54k 7.36k 108.12k 76.81% 67 | 1339908 requests in 10.10s, 1.07GB read 68 | Requests/sec: 132705.81 69 | Transfer/sec: 108.58MB 70 | ``` 71 | 72 | * fileserver 73 | 74 | ``` 75 | $ ./wrk -s pipeline.lua -t 4 -c 16 -d 10 http://localhost:8080 -- 8 76 | Running 10s test @ http://localhost:8080 77 | 4 threads and 16 connections 78 | Thread Stats Avg Stdev Max +/- Stdev 79 | Latency 2.08ms 6.33ms 88.26ms 92.83% 80 | Req/Sec 116.54k 14.66k 167.98k 69.00% 81 | 4642226 requests in 10.03s, 3.23GB read 82 | Requests/sec: 462769.41 83 | Transfer/sec: 329.67MB 84 | ``` 85 | -------------------------------------------------------------------------------- /examples/fileserver/fileserver.go: -------------------------------------------------------------------------------- 1 | // Example static file server. 2 | // 3 | // Serves static files from the given directory. 4 | // Exports various stats at /stats . 5 | package main 6 | 7 | import ( 8 | "expvar" 9 | "flag" 10 | "log" 11 | 12 | "github.com/valyala/fasthttp" 13 | "github.com/valyala/fasthttp/expvarhandler" 14 | ) 15 | 16 | var ( 17 | addr = flag.String("addr", "localhost:8080", "TCP address to listen to") 18 | addrTLS = flag.String("addrTLS", "", "TCP address to listen to TLS (aka SSL or HTTPS) requests. Leave empty for disabling TLS") //nolint:lll 19 | byteRange = flag.Bool("byteRange", false, "Enables byte range requests if set to true") 20 | certFile = flag.String("certFile", "./ssl-cert.pem", "Path to TLS certificate file") 21 | compress = flag.Bool("compress", false, "Enables transparent response compression if set to true") 22 | dir = flag.String("dir", "/usr/share/nginx/html", "Directory to serve static files from") 23 | generateIndexPages = flag.Bool("generateIndexPages", true, "Whether to generate directory index pages") 24 | keyFile = flag.String("keyFile", "./ssl-cert.key", "Path to TLS key file") 25 | vhost = flag.Bool("vhost", false, "Enables virtual hosting by prepending the requested path with the requested hostname") //nolint:lll 26 | ) 27 | 28 | func main() { 29 | // Parse command-line flags. 30 | flag.Parse() 31 | 32 | // Setup FS handler 33 | fs := &fasthttp.FS{ 34 | Root: *dir, 35 | IndexNames: []string{"index.html"}, 36 | GenerateIndexPages: *generateIndexPages, 37 | Compress: *compress, 38 | AcceptByteRange: *byteRange, 39 | } 40 | if *vhost { 41 | fs.PathRewrite = fasthttp.NewVHostPathRewriter(0) 42 | } 43 | fsHandler := fs.NewRequestHandler() 44 | 45 | // Create RequestHandler serving server stats on /stats and files 46 | // on other requested paths. 47 | // /stats output may be filtered using regexps. For example: 48 | // 49 | // * /stats?r=fs will show only stats (expvars) containing 'fs' 50 | // in their names. 51 | requestHandler := func(ctx *fasthttp.RequestCtx) { 52 | switch string(ctx.Path()) { 53 | case "/stats": 54 | expvarhandler.ExpvarHandler(ctx) 55 | default: 56 | fsHandler(ctx) 57 | updateFSCounters(ctx) 58 | } 59 | } 60 | 61 | // Start HTTP server. 62 | if *addr != "" { 63 | log.Printf("Starting HTTP server on %q", *addr) 64 | go func() { 65 | if err := fasthttp.ListenAndServe(*addr, requestHandler); err != nil { 66 | log.Fatalf("error in ListenAndServe: %v", err) 67 | } 68 | }() 69 | } 70 | 71 | // Start HTTPS server. 72 | if *addrTLS != "" { 73 | log.Printf("Starting HTTPS server on %q", *addrTLS) 74 | go func() { 75 | if err := fasthttp.ListenAndServeTLS(*addrTLS, *certFile, *keyFile, requestHandler); err != nil { 76 | log.Fatalf("error in ListenAndServeTLS: %v", err) 77 | } 78 | }() 79 | } 80 | 81 | log.Printf("Serving files from directory %q", *dir) 82 | log.Printf("See stats at http://%s/stats", *addr) 83 | 84 | // Wait forever. 85 | select {} 86 | } 87 | 88 | func updateFSCounters(ctx *fasthttp.RequestCtx) { 89 | // Increment the number of fsHandler calls. 90 | fsCalls.Add(1) 91 | 92 | // Update other stats counters 93 | resp := &ctx.Response 94 | switch resp.StatusCode() { 95 | case fasthttp.StatusOK: 96 | fsOKResponses.Add(1) 97 | fsResponseBodyBytes.Add(int64(resp.Header.ContentLength())) 98 | case fasthttp.StatusNotModified: 99 | fsNotModifiedResponses.Add(1) 100 | case fasthttp.StatusNotFound: 101 | fsNotFoundResponses.Add(1) 102 | default: 103 | fsOtherResponses.Add(1) 104 | } 105 | } 106 | 107 | // Various counters - see https://pkg.go.dev/expvar for details. 108 | var ( 109 | // Counter for total number of fs calls. 110 | fsCalls = expvar.NewInt("fsCalls") 111 | 112 | // Counters for various response status codes. 113 | fsOKResponses = expvar.NewInt("fsOKResponses") 114 | fsNotModifiedResponses = expvar.NewInt("fsNotModifiedResponses") 115 | fsNotFoundResponses = expvar.NewInt("fsNotFoundResponses") 116 | fsOtherResponses = expvar.NewInt("fsOtherResponses") 117 | 118 | // Total size in bytes for OK response bodies served. 119 | fsResponseBodyBytes = expvar.NewInt("fsResponseBodyBytes") 120 | ) 121 | -------------------------------------------------------------------------------- /examples/fileserver/ssl-cert-snakeoil.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD4IQusAs8PJdnG 3 | 3mURt/AXtgC+ceqLOatJ49JJE1VPTkMAy+oE1f1XvkMrYsHqmDf6GWVzgVXryL4U 4 | wq2/nJSm56ddhN55nI8oSN3dtywUB8/ShelEN73nlN77PeD9tl6NksPwWaKrqxq0 5 | FlabRPZSQCfmgZbhDV8Sa8mfCkFU0G0lit6kLGceCKMvmW+9Bz7ebsYmVdmVMxmf 6 | IJStFD44lWFTdUc65WISKEdW2ELcUefb0zOLw+0PCbXFGJH5x5ktksW8+BBk2Hkg 7 | GeQRL/qPCccthbScO0VgNj3zJ3ZZL0ObSDAbvNDG85joeNjDNq5DT/BAZ0bOSbEF 8 | sh+f9BAzAgMBAAECggEBAJWv2cq7Jw6MVwSRxYca38xuD6TUNBopgBvjREixURW2 9 | sNUaLuMb9Omp7fuOaE2N5rcJ+xnjPGIxh/oeN5MQctz9gwn3zf6vY+15h97pUb4D 10 | uGvYPRDaT8YVGS+X9NMZ4ZCmqW2lpWzKnCFoGHcy8yZLbcaxBsRdvKzwOYGoPiFb 11 | K2QuhXZ/1UPmqK9i2DFKtj40X6vBszTNboFxOVpXrPu0FJwLVSDf2hSZ4fMM0DH3 12 | YqwKcYf5te+hxGKgrqRA3tn0NCWii0in6QIwXMC+kMw1ebg/tZKqyDLMNptAK8J+ 13 | DVw9m5X1seUHS5ehU/g2jrQrtK5WYn7MrFK4lBzlRwECgYEA/d1TeANYECDWRRDk 14 | B0aaRZs87Rwl/J9PsvbsKvtU/bX+OfSOUjOa9iQBqn0LmU8GqusEET/QVUfocVwV 15 | Bggf/5qDLxz100Rj0ags/yE/kNr0Bb31kkkKHFMnCT06YasR7qKllwrAlPJvQv9x 16 | IzBKq+T/Dx08Wep9bCRSFhzRCnsCgYEA+jdeZXTDr/Vz+D2B3nAw1frqYFfGnEVY 17 | wqmoK3VXMDkGuxsloO2rN+SyiUo3JNiQNPDub/t7175GH5pmKtZOlftePANsUjBj 18 | wZ1D0rI5Bxu/71ibIUYIRVmXsTEQkh/ozoh3jXCZ9+bLgYiYx7789IUZZSokFQ3D 19 | FICUT9KJ36kCgYAGoq9Y1rWJjmIrYfqj2guUQC+CfxbbGIrrwZqAsRsSmpwvhZ3m 20 | tiSZxG0quKQB+NfSxdvQW5ulbwC7Xc3K35F+i9pb8+TVBdeaFkw+yu6vaZmxQLrX 21 | fQM/pEjD7A7HmMIaO7QaU5SfEAsqdCTP56Y8AftMuNXn/8IRfo2KuGwaWwKBgFpU 22 | ILzJoVdlad9E/Rw7LjYhZfkv1uBVXIyxyKcfrkEXZSmozDXDdxsvcZCEfVHM6Ipk 23 | K/+7LuMcqp4AFEAEq8wTOdq6daFaHLkpt/FZK6M4TlruhtpFOPkoNc3e45eM83OT 24 | 6mziKINJC1CQ6m65sQHpBtjxlKMRG8rL/D6wx9s5AoGBAMRlqNPMwglT3hvDmsAt 25 | 9Lf9pdmhERUlHhD8bj8mDaBj2Aqv7f6VRJaYZqP403pKKQexuqcn80mtjkSAPFkN 26 | Cj7BVt/RXm5uoxDTnfi26RF9F6yNDEJ7UU9+peBr99aazF/fTgW/1GcMkQnum8uV 27 | c257YgaWmjK9uB0Y2r2VxS0G 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /examples/fileserver/ssl-cert-snakeoil.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICujCCAaKgAwIBAgIJAMbXnKZ/cikUMA0GCSqGSIb3DQEBCwUAMBUxEzARBgNV 3 | BAMTCnVidW50dS5uYW4wHhcNMTUwMjA0MDgwMTM5WhcNMjUwMjAxMDgwMTM5WjAV 4 | MRMwEQYDVQQDEwp1YnVudHUubmFuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 5 | CgKCAQEA+CELrALPDyXZxt5lEbfwF7YAvnHqizmrSePSSRNVT05DAMvqBNX9V75D 6 | K2LB6pg3+hllc4FV68i+FMKtv5yUpuenXYTeeZyPKEjd3bcsFAfP0oXpRDe955Te 7 | +z3g/bZejZLD8Fmiq6satBZWm0T2UkAn5oGW4Q1fEmvJnwpBVNBtJYrepCxnHgij 8 | L5lvvQc+3m7GJlXZlTMZnyCUrRQ+OJVhU3VHOuViEihHVthC3FHn29Mzi8PtDwm1 9 | xRiR+ceZLZLFvPgQZNh5IBnkES/6jwnHLYW0nDtFYDY98yd2WS9Dm0gwG7zQxvOY 10 | 6HjYwzauQ0/wQGdGzkmxBbIfn/QQMwIDAQABow0wCzAJBgNVHRMEAjAAMA0GCSqG 11 | SIb3DQEBCwUAA4IBAQBQjKm/4KN/iTgXbLTL3i7zaxYXFLXsnT1tF+ay4VA8aj98 12 | L3JwRTciZ3A5iy/W4VSCt3eASwOaPWHKqDBB5RTtL73LoAqsWmO3APOGQAbixcQ2 13 | 45GXi05OKeyiYRi1Nvq7Unv9jUkRDHUYVPZVSAjCpsXzPhFkmZoTRxmx5l0ZF7Li 14 | K91lI5h+eFq0dwZwrmlPambyh1vQUi70VHv8DNToVU29kel7YLbxGbuqETfhrcy6 15 | X+Mha6RYITkAn5FqsZcKMsc9eYGEF4l3XV+oS7q6xfTxktYJMFTI18J0lQ2Lv/CI 16 | whdMnYGntDQBE/iFCrJEGNsKGc38796GBOb5j+zd 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /examples/helloworldserver/.gitignore: -------------------------------------------------------------------------------- 1 | helloworldserver 2 | -------------------------------------------------------------------------------- /examples/helloworldserver/Makefile: -------------------------------------------------------------------------------- 1 | helloworldserver: clean 2 | go get -u github.com/valyala/fasthttp 3 | go build 4 | 5 | clean: 6 | rm -f helloworldserver 7 | -------------------------------------------------------------------------------- /examples/helloworldserver/README.md: -------------------------------------------------------------------------------- 1 | # HelloWorld server example 2 | 3 | * Displays various request info. 4 | * Sets response headers and cookies. 5 | * Supports transparent compression. 6 | 7 | # How to build 8 | 9 | ``` 10 | make 11 | ``` 12 | 13 | # How to run 14 | 15 | ``` 16 | ./helloworldserver -addr=tcp.addr.to.listen:to 17 | ``` 18 | -------------------------------------------------------------------------------- /examples/helloworldserver/helloworldserver.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/valyala/fasthttp" 9 | ) 10 | 11 | var ( 12 | addr = flag.String("addr", ":8080", "TCP address to listen to") 13 | compress = flag.Bool("compress", false, "Whether to enable transparent response compression") 14 | ) 15 | 16 | func main() { 17 | flag.Parse() 18 | 19 | h := requestHandler 20 | if *compress { 21 | h = fasthttp.CompressHandler(h) 22 | } 23 | 24 | if err := fasthttp.ListenAndServe(*addr, h); err != nil { 25 | log.Fatalf("Error in ListenAndServe: %v", err) 26 | } 27 | } 28 | 29 | func requestHandler(ctx *fasthttp.RequestCtx) { 30 | fmt.Fprintf(ctx, "Hello, world!\n\n") 31 | 32 | fmt.Fprintf(ctx, "Request method is %q\n", ctx.Method()) 33 | fmt.Fprintf(ctx, "RequestURI is %q\n", ctx.RequestURI()) 34 | fmt.Fprintf(ctx, "Requested path is %q\n", ctx.Path()) 35 | fmt.Fprintf(ctx, "Host is %q\n", ctx.Host()) 36 | fmt.Fprintf(ctx, "Query string is %q\n", ctx.QueryArgs()) 37 | fmt.Fprintf(ctx, "User-Agent is %q\n", ctx.UserAgent()) 38 | fmt.Fprintf(ctx, "Connection has been established at %s\n", ctx.ConnTime()) 39 | fmt.Fprintf(ctx, "Request has been started at %s\n", ctx.Time()) 40 | fmt.Fprintf(ctx, "Serial request number for the current connection is %d\n", ctx.ConnRequestNum()) 41 | fmt.Fprintf(ctx, "Your ip is %q\n\n", ctx.RemoteIP()) 42 | 43 | fmt.Fprintf(ctx, "Raw request is:\n---CUT---\n%s\n---CUT---", &ctx.Request) 44 | 45 | ctx.SetContentType("text/plain; charset=utf8") 46 | 47 | // Set arbitrary headers 48 | ctx.Response.Header.Set("X-My-Header", "my-header-value") 49 | 50 | // Set cookies 51 | var c fasthttp.Cookie 52 | c.SetKey("cookie-name") 53 | c.SetValue("cookie-value") 54 | ctx.Response.Header.SetCookie(&c) 55 | } 56 | -------------------------------------------------------------------------------- /examples/host_client/.gitignore: -------------------------------------------------------------------------------- 1 | hostclient 2 | -------------------------------------------------------------------------------- /examples/host_client/Makefile: -------------------------------------------------------------------------------- 1 | host_client: clean 2 | go get -u github.com/valyala/fasthttp 3 | go build 4 | 5 | clean: 6 | rm -f host_client 7 | -------------------------------------------------------------------------------- /examples/host_client/README.md: -------------------------------------------------------------------------------- 1 | # Host Client Example 2 | 3 | The HostClient is useful when calling an API from a single host. 4 | The example also shows how to use URI. 5 | You may create the parsed URI once and reuse it in many requests. 6 | The URI has a username and password for Basic Auth but you may also set other parts i.e. `SetPath()`, `SetQueryString()`. 7 | 8 | # How to build and run 9 | Start a web server on localhost:8080 then execute: 10 | 11 | make 12 | ./host_client 13 | 14 | -------------------------------------------------------------------------------- /examples/host_client/hostclient.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/valyala/fasthttp" 8 | ) 9 | 10 | func main() { 11 | // Get URI from a pool 12 | url := fasthttp.AcquireURI() 13 | url.Parse(nil, []byte("http://localhost:8080/")) //nolint:errcheck 14 | url.SetUsername("Aladdin") 15 | url.SetPassword("Open Sesame") 16 | 17 | hc := &fasthttp.HostClient{ 18 | Addr: "localhost:8080", // The host address and port must be set explicitly 19 | } 20 | 21 | req := fasthttp.AcquireRequest() 22 | req.SetURI(url) // copy url into request 23 | fasthttp.ReleaseURI(url) // now you may release the URI 24 | 25 | req.Header.SetMethod(fasthttp.MethodGet) 26 | resp := fasthttp.AcquireResponse() 27 | err := hc.Do(req, resp) 28 | fasthttp.ReleaseRequest(req) 29 | if err == nil { 30 | fmt.Printf("Response: %s\n", resp.Body()) 31 | } else { 32 | fmt.Fprintf(os.Stderr, "Connection error: %v\n", err) 33 | } 34 | fasthttp.ReleaseResponse(resp) 35 | } 36 | -------------------------------------------------------------------------------- /examples/letsencrypt/letsencryptserver.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | 7 | "github.com/valyala/fasthttp" 8 | "golang.org/x/crypto/acme" 9 | "golang.org/x/crypto/acme/autocert" 10 | ) 11 | 12 | func requestHandler(ctx *fasthttp.RequestCtx) { 13 | ctx.SetBodyString("hello from https!") 14 | } 15 | 16 | func main() { 17 | m := &autocert.Manager{ 18 | Prompt: autocert.AcceptTOS, 19 | HostPolicy: autocert.HostWhitelist("example.com"), // Replace with your domain. 20 | Cache: autocert.DirCache("./certs"), 21 | } 22 | 23 | cfg := &tls.Config{ 24 | GetCertificate: m.GetCertificate, 25 | NextProtos: []string{ 26 | "http/1.1", acme.ALPNProto, 27 | }, 28 | } 29 | 30 | // Let's Encrypt tls-alpn-01 only works on port 443. 31 | ln, err := net.Listen("tcp4", "0.0.0.0:443") // #nosec G102 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | lnTLS := tls.NewListener(ln, cfg) 37 | 38 | if err := fasthttp.Serve(lnTLS, requestHandler); err != nil { 39 | panic(err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/multidomain/Makefile: -------------------------------------------------------------------------------- 1 | writer: clean 2 | go get -u github.com/valyala/fasthttp 3 | go build 4 | 5 | clean: 6 | rm -f multidomain 7 | -------------------------------------------------------------------------------- /examples/multidomain/README.md: -------------------------------------------------------------------------------- 1 | # Multidomain using SSL certs example 2 | 3 | * Prints two messages depending on visited host. 4 | 5 | # How to build 6 | 7 | ``` 8 | make 9 | ``` 10 | 11 | # How to run 12 | 13 | ``` 14 | ./multidomain 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/multidomain/multidomain.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/valyala/fasthttp" 7 | ) 8 | 9 | var domains = make(map[string]fasthttp.RequestHandler) 10 | 11 | func main() { 12 | server := &fasthttp.Server{ 13 | // You can check the access using openssl command: 14 | // $ openssl s_client -connect localhost:8080 << EOF 15 | // > GET / 16 | // > Host: localhost 17 | // > EOF 18 | // 19 | // $ openssl s_client -connect localhost:8080 << EOF 20 | // > GET / 21 | // > Host: 127.0.0.1:8080 22 | // > EOF 23 | // 24 | Handler: func(ctx *fasthttp.RequestCtx) { 25 | h, ok := domains[string(ctx.Host())] 26 | if !ok { 27 | ctx.NotFound() 28 | return 29 | } 30 | h(ctx) 31 | }, 32 | } 33 | 34 | // preparing first host 35 | cert, priv, err := fasthttp.GenerateTestCertificate("localhost:8080") 36 | if err != nil { 37 | panic(err) 38 | } 39 | domains["localhost:8080"] = func(ctx *fasthttp.RequestCtx) { 40 | ctx.WriteString("You are accessing to localhost:8080\n") //nolint:errcheck 41 | } 42 | 43 | err = server.AppendCertEmbed(cert, priv) 44 | if err != nil { 45 | panic(err) 46 | } 47 | 48 | // preparing second host 49 | cert, priv, err = fasthttp.GenerateTestCertificate("127.0.0.1") 50 | if err != nil { 51 | panic(err) 52 | } 53 | domains["127.0.0.1:8080"] = func(ctx *fasthttp.RequestCtx) { 54 | ctx.WriteString("You are accessing to 127.0.0.1:8080\n") //nolint:errcheck 55 | } 56 | 57 | err = server.AppendCertEmbed(cert, priv) 58 | if err != nil { 59 | panic(err) 60 | } 61 | 62 | fmt.Println(server.ListenAndServeTLS(":8080", "", "")) 63 | } 64 | -------------------------------------------------------------------------------- /expvarhandler/expvar.go: -------------------------------------------------------------------------------- 1 | // Package expvarhandler provides fasthttp-compatible request handler 2 | // serving expvars. 3 | package expvarhandler 4 | 5 | import ( 6 | "expvar" 7 | "fmt" 8 | "regexp" 9 | 10 | "github.com/valyala/fasthttp" 11 | ) 12 | 13 | var ( 14 | expvarHandlerCalls = expvar.NewInt("expvarHandlerCalls") 15 | expvarRegexpErrors = expvar.NewInt("expvarRegexpErrors") 16 | 17 | defaultRE = regexp.MustCompile(".") 18 | ) 19 | 20 | // ExpvarHandler dumps json representation of expvars to http response. 21 | // 22 | // Expvars may be filtered by regexp provided via 'r' query argument. 23 | // 24 | // See https://pkg.go.dev/expvar for details. 25 | func ExpvarHandler(ctx *fasthttp.RequestCtx) { 26 | expvarHandlerCalls.Add(1) 27 | 28 | ctx.Response.Reset() 29 | 30 | r, err := getExpvarRegexp(ctx) 31 | if err != nil { 32 | expvarRegexpErrors.Add(1) 33 | fmt.Fprintf(ctx, "Error when obtaining expvar regexp: %v", err) 34 | ctx.SetStatusCode(fasthttp.StatusBadRequest) 35 | return 36 | } 37 | 38 | fmt.Fprintf(ctx, "{\n") 39 | first := true 40 | expvar.Do(func(kv expvar.KeyValue) { 41 | if r.MatchString(kv.Key) { 42 | if !first { 43 | fmt.Fprintf(ctx, ",\n") 44 | } 45 | first = false 46 | fmt.Fprintf(ctx, "\t%q: %s", kv.Key, kv.Value) 47 | } 48 | }) 49 | fmt.Fprintf(ctx, "\n}\n") 50 | 51 | ctx.SetContentType("application/json; charset=utf-8") 52 | } 53 | 54 | func getExpvarRegexp(ctx *fasthttp.RequestCtx) (*regexp.Regexp, error) { 55 | r := string(ctx.QueryArgs().Peek("r")) 56 | if r == "" { 57 | return defaultRE, nil 58 | } 59 | rr, err := regexp.Compile(r) 60 | if err != nil { 61 | return nil, fmt.Errorf("cannot parse r=%q: %w", r, err) 62 | } 63 | return rr, nil 64 | } 65 | -------------------------------------------------------------------------------- /expvarhandler/expvar_test.go: -------------------------------------------------------------------------------- 1 | package expvarhandler 2 | 3 | import ( 4 | "encoding/json" 5 | "expvar" 6 | "strings" 7 | "sync" 8 | "testing" 9 | 10 | "github.com/valyala/fasthttp" 11 | ) 12 | 13 | var once sync.Once 14 | 15 | func TestExpvarHandlerBasic(t *testing.T) { 16 | t.Parallel() 17 | 18 | // Publish panics if the same var is published more than once, 19 | // which can happen if the test is run with -count 20 | once.Do(func() { 21 | expvar.Publish("customVar", expvar.Func(func() any { 22 | return "foobar" 23 | })) 24 | }) 25 | 26 | var ctx fasthttp.RequestCtx 27 | 28 | expvarHandlerCalls.Set(0) 29 | 30 | ExpvarHandler(&ctx) 31 | 32 | body := ctx.Response.Body() 33 | 34 | var m map[string]any 35 | if err := json.Unmarshal(body, &m); err != nil { 36 | t.Fatalf("unexpected error: %v", err) 37 | } 38 | 39 | if _, ok := m["cmdline"]; !ok { 40 | t.Fatalf("cannot locate cmdline expvar") 41 | } 42 | if _, ok := m["memstats"]; !ok { 43 | t.Fatalf("cannot locate memstats expvar") 44 | } 45 | 46 | v := m["customVar"] 47 | sv, ok := v.(string) 48 | if !ok { 49 | t.Fatalf("unexpected custom var type %T. Expecting string", v) 50 | } 51 | if sv != "foobar" { 52 | t.Fatalf("unexpected custom var value: %q. Expecting %q", v, "foobar") 53 | } 54 | 55 | v = m["expvarHandlerCalls"] 56 | fv, ok := v.(float64) 57 | if !ok { 58 | t.Fatalf("unexpected expvarHandlerCalls type %T. Expecting float64", v) 59 | } 60 | if int(fv) != 1 { 61 | t.Fatalf("unexpected value for expvarHandlerCalls: %v. Expecting %v", fv, 1) 62 | } 63 | } 64 | 65 | func TestExpvarHandlerRegexp(t *testing.T) { 66 | var ctx fasthttp.RequestCtx 67 | ctx.QueryArgs().Set("r", "cmd") 68 | ExpvarHandler(&ctx) 69 | body := string(ctx.Response.Body()) 70 | if !strings.Contains(body, `"cmdline"`) { 71 | t.Fatalf("missing 'cmdline' expvar") 72 | } 73 | if strings.Contains(body, `"memstats"`) { 74 | t.Fatalf("unexpected memstats expvar found") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /fasthttpadaptor/adaptor.go: -------------------------------------------------------------------------------- 1 | // Package fasthttpadaptor provides helper functions for converting net/http 2 | // request handlers to fasthttp request handlers. 3 | package fasthttpadaptor 4 | 5 | import ( 6 | "bufio" 7 | "io" 8 | "net" 9 | "net/http" 10 | "sync" 11 | 12 | "github.com/valyala/fasthttp" 13 | ) 14 | 15 | // NewFastHTTPHandlerFunc wraps net/http handler func to fasthttp 16 | // request handler, so it can be passed to fasthttp server. 17 | // 18 | // While this function may be used for easy switching from net/http to fasthttp, 19 | // it has the following drawbacks comparing to using manually written fasthttp 20 | // request handler: 21 | // 22 | // - A lot of useful functionality provided by fasthttp is missing 23 | // from net/http handler. 24 | // - net/http -> fasthttp handler conversion has some overhead, 25 | // so the returned handler will be always slower than manually written 26 | // fasthttp handler. 27 | // 28 | // So it is advisable using this function only for quick net/http -> fasthttp 29 | // switching. Then manually convert net/http handlers to fasthttp handlers 30 | // according to https://github.com/valyala/fasthttp#switching-from-nethttp-to-fasthttp . 31 | func NewFastHTTPHandlerFunc(h http.HandlerFunc) fasthttp.RequestHandler { 32 | return NewFastHTTPHandler(h) 33 | } 34 | 35 | // NewFastHTTPHandler wraps net/http handler to fasthttp request handler, 36 | // so it can be passed to fasthttp server. 37 | // 38 | // While this function may be used for easy switching from net/http to fasthttp, 39 | // it has the following drawbacks comparing to using manually written fasthttp 40 | // request handler: 41 | // 42 | // - A lot of useful functionality provided by fasthttp is missing 43 | // from net/http handler. 44 | // - net/http -> fasthttp handler conversion has some overhead, 45 | // so the returned handler will be always slower than manually written 46 | // fasthttp handler. 47 | // 48 | // So it is advisable using this function only for quick net/http -> fasthttp 49 | // switching. Then manually convert net/http handlers to fasthttp handlers 50 | // according to https://github.com/valyala/fasthttp#switching-from-nethttp-to-fasthttp . 51 | func NewFastHTTPHandler(h http.Handler) fasthttp.RequestHandler { 52 | return func(ctx *fasthttp.RequestCtx) { 53 | var r http.Request 54 | if err := ConvertRequest(ctx, &r, true); err != nil { 55 | ctx.Logger().Printf("cannot parse requestURI %q: %v", r.RequestURI, err) 56 | ctx.Error("Internal Server Error", fasthttp.StatusInternalServerError) 57 | return 58 | } 59 | w := netHTTPResponseWriter{ 60 | w: ctx.Response.BodyWriter(), 61 | ctx: ctx, 62 | } 63 | h.ServeHTTP(&w, r.WithContext(ctx)) 64 | 65 | ctx.SetStatusCode(w.StatusCode()) 66 | haveContentType := false 67 | for k, vv := range w.Header() { 68 | if k == fasthttp.HeaderContentType { 69 | haveContentType = true 70 | } 71 | 72 | for _, v := range vv { 73 | ctx.Response.Header.Add(k, v) 74 | } 75 | } 76 | if !haveContentType { 77 | // From net/http.ResponseWriter.Write: 78 | // If the Header does not contain a Content-Type line, Write adds a Content-Type set 79 | // to the result of passing the initial 512 bytes of written data to DetectContentType. 80 | l := 512 81 | b := ctx.Response.Body() 82 | if len(b) < 512 { 83 | l = len(b) 84 | } 85 | ctx.Response.Header.Set(fasthttp.HeaderContentType, http.DetectContentType(b[:l])) 86 | } 87 | } 88 | } 89 | 90 | type netHTTPResponseWriter struct { 91 | w io.Writer 92 | h http.Header 93 | ctx *fasthttp.RequestCtx 94 | statusCode int 95 | } 96 | 97 | func (w *netHTTPResponseWriter) StatusCode() int { 98 | if w.statusCode == 0 { 99 | return http.StatusOK 100 | } 101 | return w.statusCode 102 | } 103 | 104 | func (w *netHTTPResponseWriter) Header() http.Header { 105 | if w.h == nil { 106 | w.h = make(http.Header) 107 | } 108 | return w.h 109 | } 110 | 111 | func (w *netHTTPResponseWriter) WriteHeader(statusCode int) { 112 | w.statusCode = statusCode 113 | } 114 | 115 | func (w *netHTTPResponseWriter) Write(p []byte) (int, error) { 116 | return w.w.Write(p) 117 | } 118 | 119 | func (w *netHTTPResponseWriter) Flush() {} 120 | 121 | type wrappedConn struct { 122 | net.Conn 123 | 124 | wg sync.WaitGroup 125 | once sync.Once 126 | } 127 | 128 | func (c *wrappedConn) Close() (err error) { 129 | c.once.Do(func() { 130 | err = c.Conn.Close() 131 | c.wg.Done() 132 | }) 133 | return 134 | } 135 | 136 | func (w *netHTTPResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 137 | // Hijack assumes control of the connection, so we need to prevent fasthttp from closing it or 138 | // doing anything else with it. 139 | w.ctx.HijackSetNoResponse(true) 140 | 141 | conn := &wrappedConn{Conn: w.ctx.Conn()} 142 | conn.wg.Add(1) 143 | w.ctx.Hijack(func(net.Conn) { 144 | conn.wg.Wait() 145 | }) 146 | 147 | bufW := bufio.NewWriter(conn) 148 | 149 | // Write any unflushed body to the hijacked connection buffer. 150 | unflushedBody := w.ctx.Response.Body() 151 | if len(unflushedBody) > 0 { 152 | if _, err := bufW.Write(unflushedBody); err != nil { 153 | conn.Close() 154 | return nil, nil, err 155 | } 156 | } 157 | 158 | return conn, &bufio.ReadWriter{Reader: bufio.NewReader(conn), Writer: bufW}, nil 159 | } 160 | -------------------------------------------------------------------------------- /fasthttpadaptor/b2s_new.go: -------------------------------------------------------------------------------- 1 | //go:build go1.20 2 | 3 | package fasthttpadaptor 4 | 5 | import "unsafe" 6 | 7 | // b2s converts byte slice to a string without memory allocation. 8 | // See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ . 9 | func b2s(b []byte) string { 10 | return unsafe.String(unsafe.SliceData(b), len(b)) 11 | } 12 | -------------------------------------------------------------------------------- /fasthttpadaptor/b2s_old.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.20 2 | 3 | package fasthttpadaptor 4 | 5 | import "unsafe" 6 | 7 | // b2s converts byte slice to a string without memory allocation. 8 | // See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ . 9 | // 10 | // Note it may break if string and/or slice header will change 11 | // in the future go versions. 12 | func b2s(b []byte) string { 13 | return *(*string)(unsafe.Pointer(&b)) 14 | } 15 | -------------------------------------------------------------------------------- /fasthttpadaptor/request.go: -------------------------------------------------------------------------------- 1 | package fasthttpadaptor 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | 10 | "github.com/valyala/fasthttp" 11 | ) 12 | 13 | // ConvertRequest converts a fasthttp.Request to an http.Request. 14 | // forServer should be set to true when the http.Request is going to be passed to a http.Handler. 15 | // 16 | // The http.Request must not be used after the fasthttp handler has returned! 17 | // Memory in use by the http.Request will be reused after your handler has returned! 18 | func ConvertRequest(ctx *fasthttp.RequestCtx, r *http.Request, forServer bool) error { 19 | body := ctx.PostBody() 20 | strRequestURI := b2s(ctx.RequestURI()) 21 | 22 | rURL, err := url.ParseRequestURI(strRequestURI) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | r.Method = b2s(ctx.Method()) 28 | r.Proto = b2s(ctx.Request.Header.Protocol()) 29 | if r.Proto == "HTTP/2" { 30 | r.ProtoMajor = 2 31 | } else { 32 | r.ProtoMajor = 1 33 | } 34 | r.ProtoMinor = 1 35 | r.ContentLength = int64(len(body)) 36 | r.RemoteAddr = ctx.RemoteAddr().String() 37 | r.Host = b2s(ctx.Host()) 38 | r.TLS = ctx.TLSConnectionState() 39 | r.Body = io.NopCloser(bytes.NewReader(body)) 40 | r.URL = rURL 41 | 42 | if forServer { 43 | r.RequestURI = strRequestURI 44 | } 45 | 46 | if r.Header == nil { 47 | r.Header = make(http.Header) 48 | } else if len(r.Header) > 0 { 49 | for k := range r.Header { 50 | delete(r.Header, k) 51 | } 52 | } 53 | 54 | for k, v := range ctx.Request.Header.All() { 55 | sk := b2s(k) 56 | sv := b2s(v) 57 | 58 | switch sk { 59 | case "Transfer-Encoding": 60 | r.TransferEncoding = append(r.TransferEncoding, sv) 61 | default: 62 | if sk == fasthttp.HeaderCookie { 63 | sv = strings.Clone(sv) 64 | } 65 | r.Header.Set(sk, sv) 66 | } 67 | } 68 | 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /fasthttpadaptor/request_test.go: -------------------------------------------------------------------------------- 1 | package fasthttpadaptor 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/valyala/fasthttp" 8 | ) 9 | 10 | func BenchmarkConvertRequest(b *testing.B) { 11 | var httpReq http.Request 12 | 13 | ctx := &fasthttp.RequestCtx{ 14 | Request: fasthttp.Request{ 15 | Header: fasthttp.RequestHeader{}, 16 | UseHostHeader: false, 17 | }, 18 | } 19 | ctx.Request.Header.SetMethod("GET") 20 | ctx.Request.Header.Set("x", "test") 21 | ctx.Request.Header.Set("y", "test") 22 | ctx.Request.SetRequestURI("/test") 23 | ctx.Request.SetHost("test") 24 | b.ResetTimer() 25 | 26 | for i := 0; i < b.N; i++ { 27 | _ = ConvertRequest(ctx, &httpReq, true) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /fasthttpproxy/doc.go: -------------------------------------------------------------------------------- 1 | // Package fasthttpproxy provides SOCKS5 and HTTP proxy support for fasthttp. 2 | package fasthttpproxy 3 | -------------------------------------------------------------------------------- /fasthttpproxy/http.go: -------------------------------------------------------------------------------- 1 | package fasthttpproxy 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/valyala/fasthttp" 7 | "golang.org/x/net/http/httpproxy" 8 | ) 9 | 10 | // FasthttpHTTPDialer returns a fasthttp.DialFunc that dials using 11 | // the provided HTTP proxy. 12 | // 13 | // Example usage: 14 | // 15 | // c := &fasthttp.Client{ 16 | // Dial: fasthttpproxy.FasthttpHTTPDialer("username:password@localhost:9050"), 17 | // } 18 | func FasthttpHTTPDialer(proxy string) fasthttp.DialFunc { 19 | return FasthttpHTTPDialerTimeout(proxy, 0) 20 | } 21 | 22 | // FasthttpHTTPDialerTimeout returns a fasthttp.DialFunc that dials using 23 | // the provided HTTP proxy using the given timeout. 24 | // The timeout parameter determines both the dial timeout and the CONNECT request timeout. 25 | // 26 | // Example usage: 27 | // 28 | // c := &fasthttp.Client{ 29 | // Dial: fasthttpproxy.FasthttpHTTPDialerTimeout("username:password@localhost:9050", time.Second * 2), 30 | // } 31 | func FasthttpHTTPDialerTimeout(proxy string, timeout time.Duration) fasthttp.DialFunc { 32 | d := Dialer{Config: httpproxy.Config{HTTPProxy: proxy, HTTPSProxy: proxy}, Timeout: timeout, ConnectTimeout: timeout} 33 | dialFunc, _ := d.GetDialFunc(false) 34 | return dialFunc 35 | } 36 | 37 | // FasthttpHTTPDialerDualStack returns a fasthttp.DialFunc that dials using 38 | // the provided HTTP proxy with support for both IPv4 and IPv6. 39 | // 40 | // Example usage: 41 | // 42 | // c := &fasthttp.Client{ 43 | // Dial: fasthttpproxy.FasthttpHTTPDialerDualStack("username:password@localhost:9050"), 44 | // } 45 | func FasthttpHTTPDialerDualStack(proxy string) fasthttp.DialFunc { 46 | return FasthttpHTTPDialerDualStackTimeout(proxy, 0) 47 | } 48 | 49 | // FasthttpHTTPDialerDualStackTimeout returns a fasthttp.DialFunc that dials using 50 | // the provided HTTP proxy with support for both IPv4 and IPv6, using the given timeout. 51 | // The timeout parameter determines both the dial timeout and the CONNECT request timeout. 52 | // 53 | // Example usage: 54 | // 55 | // c := &fasthttp.Client{ 56 | // Dial: fasthttpproxy.FasthttpHTTPDialerDualStackTimeout("username:password@localhost:9050", time.Second * 2), 57 | // } 58 | func FasthttpHTTPDialerDualStackTimeout(proxy string, timeout time.Duration) fasthttp.DialFunc { 59 | d := Dialer{ 60 | Config: httpproxy.Config{HTTPProxy: proxy, HTTPSProxy: proxy}, Timeout: timeout, ConnectTimeout: timeout, 61 | DialDualStack: true, 62 | } 63 | dialFunc, _ := d.GetDialFunc(false) 64 | return dialFunc 65 | } 66 | -------------------------------------------------------------------------------- /fasthttpproxy/proxy_env.go: -------------------------------------------------------------------------------- 1 | package fasthttpproxy 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/valyala/fasthttp" 7 | ) 8 | 9 | const ( 10 | httpsScheme = "https" 11 | httpScheme = "http" 12 | ) 13 | 14 | // FasthttpProxyHTTPDialer returns a fasthttp.DialFunc that dials using 15 | // the env(HTTP_PROXY, HTTPS_PROXY and NO_PROXY) configured HTTP proxy. 16 | // 17 | // Example usage: 18 | // 19 | // c := &fasthttp.Client{ 20 | // Dial: fasthttpproxy.FasthttpProxyHTTPDialer(), 21 | // } 22 | func FasthttpProxyHTTPDialer() fasthttp.DialFunc { 23 | return FasthttpProxyHTTPDialerTimeout(0) 24 | } 25 | 26 | // FasthttpProxyHTTPDialerTimeout returns a fasthttp.DialFunc that dials using 27 | // the env(HTTP_PROXY, HTTPS_PROXY and NO_PROXY) configured HTTP proxy using the given timeout. 28 | // The timeout parameter determines both the dial timeout and the CONNECT request timeout. 29 | // 30 | // Example usage: 31 | // 32 | // c := &fasthttp.Client{ 33 | // Dial: fasthttpproxy.FasthttpProxyHTTPDialerTimeout(time.Second * 2), 34 | // } 35 | func FasthttpProxyHTTPDialerTimeout(timeout time.Duration) fasthttp.DialFunc { 36 | d := Dialer{Timeout: timeout, ConnectTimeout: timeout} 37 | dialFunc, _ := d.GetDialFunc(true) 38 | return dialFunc 39 | } 40 | -------------------------------------------------------------------------------- /fasthttpproxy/socks5.go: -------------------------------------------------------------------------------- 1 | package fasthttpproxy 2 | 3 | import ( 4 | "github.com/valyala/fasthttp" 5 | "golang.org/x/net/http/httpproxy" 6 | ) 7 | 8 | // FasthttpSocksDialer returns a fasthttp.DialFunc that dials using 9 | // the provided SOCKS5 proxy. 10 | // 11 | // Example usage: 12 | // 13 | // c := &fasthttp.Client{ 14 | // Dial: fasthttpproxy.FasthttpSocksDialer("socks5://localhost:9050"), 15 | // } 16 | func FasthttpSocksDialer(proxyAddr string) fasthttp.DialFunc { 17 | d := Dialer{Config: httpproxy.Config{HTTPProxy: proxyAddr, HTTPSProxy: proxyAddr}} 18 | dialFunc, _ := d.GetDialFunc(false) 19 | return dialFunc 20 | } 21 | 22 | // FasthttpSocksDialerDualStack returns a fasthttp.DialFunc that dials using 23 | // the provided SOCKS5 proxy with support for both IPv4 and IPv6. 24 | // 25 | // Example usage: 26 | // 27 | // c := &fasthttp.Client{ 28 | // Dial: fasthttpproxy.FasthttpSocksDialerDualStack("socks5://localhost:9050"), 29 | // } 30 | func FasthttpSocksDialerDualStack(proxyAddr string) fasthttp.DialFunc { 31 | d := Dialer{Config: httpproxy.Config{HTTPProxy: proxyAddr, HTTPSProxy: proxyAddr}, DialDualStack: true} 32 | dialFunc, _ := d.GetDialFunc(false) 33 | return dialFunc 34 | } 35 | -------------------------------------------------------------------------------- /fasthttputil/doc.go: -------------------------------------------------------------------------------- 1 | // Package fasthttputil provides utility functions for fasthttp. 2 | package fasthttputil 3 | -------------------------------------------------------------------------------- /fasthttputil/inmemory_listener.go: -------------------------------------------------------------------------------- 1 | package fasthttputil 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "sync" 7 | ) 8 | 9 | // ErrInmemoryListenerClosed indicates that the InmemoryListener is already closed. 10 | var ErrInmemoryListenerClosed = errors.New("InmemoryListener is already closed: use of closed network connection") 11 | 12 | // InmemoryListener provides in-memory dialer<->net.Listener implementation. 13 | // 14 | // It may be used either for fast in-process client<->server communications 15 | // without network stack overhead or for client<->server tests. 16 | type InmemoryListener struct { 17 | listenerAddr net.Addr 18 | conns chan acceptConn 19 | addrLock sync.RWMutex 20 | lock sync.Mutex 21 | closed bool 22 | } 23 | 24 | type acceptConn struct { 25 | conn net.Conn 26 | accepted chan struct{} 27 | } 28 | 29 | // NewInmemoryListener returns new in-memory dialer<->net.Listener. 30 | func NewInmemoryListener() *InmemoryListener { 31 | return &InmemoryListener{ 32 | conns: make(chan acceptConn, 1024), 33 | } 34 | } 35 | 36 | // SetLocalAddr sets the (simulated) local address for the listener. 37 | func (ln *InmemoryListener) SetLocalAddr(localAddr net.Addr) { 38 | ln.addrLock.Lock() 39 | defer ln.addrLock.Unlock() 40 | 41 | ln.listenerAddr = localAddr 42 | } 43 | 44 | // Accept implements net.Listener's Accept. 45 | // 46 | // It is safe calling Accept from concurrently running goroutines. 47 | // 48 | // Accept returns new connection per each Dial call. 49 | func (ln *InmemoryListener) Accept() (net.Conn, error) { 50 | c, ok := <-ln.conns 51 | if !ok { 52 | return nil, ErrInmemoryListenerClosed 53 | } 54 | close(c.accepted) 55 | return c.conn, nil 56 | } 57 | 58 | // Close implements net.Listener's Close. 59 | func (ln *InmemoryListener) Close() error { 60 | var err error 61 | 62 | ln.lock.Lock() 63 | if !ln.closed { 64 | close(ln.conns) 65 | ln.closed = true 66 | } else { 67 | err = ErrInmemoryListenerClosed 68 | } 69 | ln.lock.Unlock() 70 | return err 71 | } 72 | 73 | type inmemoryAddr int 74 | 75 | func (inmemoryAddr) Network() string { 76 | return "inmemory" 77 | } 78 | 79 | func (inmemoryAddr) String() string { 80 | return "InmemoryListener" 81 | } 82 | 83 | // Addr implements net.Listener's Addr. 84 | func (ln *InmemoryListener) Addr() net.Addr { 85 | ln.addrLock.RLock() 86 | defer ln.addrLock.RUnlock() 87 | 88 | if ln.listenerAddr != nil { 89 | return ln.listenerAddr 90 | } 91 | 92 | return inmemoryAddr(0) 93 | } 94 | 95 | // Dial creates new client<->server connection. 96 | // Just like a real Dial it only returns once the server 97 | // has accepted the connection. 98 | // 99 | // It is safe calling Dial from concurrently running goroutines. 100 | func (ln *InmemoryListener) Dial() (net.Conn, error) { 101 | return ln.DialWithLocalAddr(nil) 102 | } 103 | 104 | // DialWithLocalAddr creates new client<->server connection. 105 | // Just like a real Dial it only returns once the server 106 | // has accepted the connection. The local address of the 107 | // client connection can be set with local. 108 | // 109 | // It is safe calling Dial from concurrently running goroutines. 110 | func (ln *InmemoryListener) DialWithLocalAddr(local net.Addr) (net.Conn, error) { 111 | pc := NewPipeConns() 112 | 113 | pc.SetAddresses(local, ln.Addr(), ln.Addr(), local) 114 | 115 | cConn := pc.Conn1() 116 | sConn := pc.Conn2() 117 | ln.lock.Lock() 118 | accepted := make(chan struct{}) 119 | if !ln.closed { 120 | ln.conns <- acceptConn{conn: sConn, accepted: accepted} 121 | // Wait until the connection has been accepted. 122 | <-accepted 123 | } else { 124 | _ = sConn.Close() 125 | _ = cConn.Close() 126 | cConn = nil 127 | } 128 | ln.lock.Unlock() 129 | 130 | if cConn == nil { 131 | return nil, ErrInmemoryListenerClosed 132 | } 133 | return cConn, nil 134 | } 135 | -------------------------------------------------------------------------------- /fasthttputil/inmemory_listener_timing_test.go: -------------------------------------------------------------------------------- 1 | package fasthttputil_test 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | "testing" 7 | 8 | "github.com/valyala/fasthttp" 9 | "github.com/valyala/fasthttp/fasthttputil" 10 | ) 11 | 12 | var ( 13 | certblock = []byte(`-----BEGIN CERTIFICATE----- 14 | MIICujCCAaKgAwIBAgIJAMbXnKZ/cikUMA0GCSqGSIb3DQEBCwUAMBUxEzARBgNV 15 | BAMTCnVidW50dS5uYW4wHhcNMTUwMjA0MDgwMTM5WhcNMjUwMjAxMDgwMTM5WjAV 16 | MRMwEQYDVQQDEwp1YnVudHUubmFuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 17 | CgKCAQEA+CELrALPDyXZxt5lEbfwF7YAvnHqizmrSePSSRNVT05DAMvqBNX9V75D 18 | K2LB6pg3+hllc4FV68i+FMKtv5yUpuenXYTeeZyPKEjd3bcsFAfP0oXpRDe955Te 19 | +z3g/bZejZLD8Fmiq6satBZWm0T2UkAn5oGW4Q1fEmvJnwpBVNBtJYrepCxnHgij 20 | L5lvvQc+3m7GJlXZlTMZnyCUrRQ+OJVhU3VHOuViEihHVthC3FHn29Mzi8PtDwm1 21 | xRiR+ceZLZLFvPgQZNh5IBnkES/6jwnHLYW0nDtFYDY98yd2WS9Dm0gwG7zQxvOY 22 | 6HjYwzauQ0/wQGdGzkmxBbIfn/QQMwIDAQABow0wCzAJBgNVHRMEAjAAMA0GCSqG 23 | SIb3DQEBCwUAA4IBAQBQjKm/4KN/iTgXbLTL3i7zaxYXFLXsnT1tF+ay4VA8aj98 24 | L3JwRTciZ3A5iy/W4VSCt3eASwOaPWHKqDBB5RTtL73LoAqsWmO3APOGQAbixcQ2 25 | 45GXi05OKeyiYRi1Nvq7Unv9jUkRDHUYVPZVSAjCpsXzPhFkmZoTRxmx5l0ZF7Li 26 | K91lI5h+eFq0dwZwrmlPambyh1vQUi70VHv8DNToVU29kel7YLbxGbuqETfhrcy6 27 | X+Mha6RYITkAn5FqsZcKMsc9eYGEF4l3XV+oS7q6xfTxktYJMFTI18J0lQ2Lv/CI 28 | whdMnYGntDQBE/iFCrJEGNsKGc38796GBOb5j+zd 29 | -----END CERTIFICATE----- 30 | `) 31 | keyblock = []byte(`-----BEGIN PRIVATE KEY----- 32 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD4IQusAs8PJdnG 33 | 3mURt/AXtgC+ceqLOatJ49JJE1VPTkMAy+oE1f1XvkMrYsHqmDf6GWVzgVXryL4U 34 | wq2/nJSm56ddhN55nI8oSN3dtywUB8/ShelEN73nlN77PeD9tl6NksPwWaKrqxq0 35 | FlabRPZSQCfmgZbhDV8Sa8mfCkFU0G0lit6kLGceCKMvmW+9Bz7ebsYmVdmVMxmf 36 | IJStFD44lWFTdUc65WISKEdW2ELcUefb0zOLw+0PCbXFGJH5x5ktksW8+BBk2Hkg 37 | GeQRL/qPCccthbScO0VgNj3zJ3ZZL0ObSDAbvNDG85joeNjDNq5DT/BAZ0bOSbEF 38 | sh+f9BAzAgMBAAECggEBAJWv2cq7Jw6MVwSRxYca38xuD6TUNBopgBvjREixURW2 39 | sNUaLuMb9Omp7fuOaE2N5rcJ+xnjPGIxh/oeN5MQctz9gwn3zf6vY+15h97pUb4D 40 | uGvYPRDaT8YVGS+X9NMZ4ZCmqW2lpWzKnCFoGHcy8yZLbcaxBsRdvKzwOYGoPiFb 41 | K2QuhXZ/1UPmqK9i2DFKtj40X6vBszTNboFxOVpXrPu0FJwLVSDf2hSZ4fMM0DH3 42 | YqwKcYf5te+hxGKgrqRA3tn0NCWii0in6QIwXMC+kMw1ebg/tZKqyDLMNptAK8J+ 43 | DVw9m5X1seUHS5ehU/g2jrQrtK5WYn7MrFK4lBzlRwECgYEA/d1TeANYECDWRRDk 44 | B0aaRZs87Rwl/J9PsvbsKvtU/bX+OfSOUjOa9iQBqn0LmU8GqusEET/QVUfocVwV 45 | Bggf/5qDLxz100Rj0ags/yE/kNr0Bb31kkkKHFMnCT06YasR7qKllwrAlPJvQv9x 46 | IzBKq+T/Dx08Wep9bCRSFhzRCnsCgYEA+jdeZXTDr/Vz+D2B3nAw1frqYFfGnEVY 47 | wqmoK3VXMDkGuxsloO2rN+SyiUo3JNiQNPDub/t7175GH5pmKtZOlftePANsUjBj 48 | wZ1D0rI5Bxu/71ibIUYIRVmXsTEQkh/ozoh3jXCZ9+bLgYiYx7789IUZZSokFQ3D 49 | FICUT9KJ36kCgYAGoq9Y1rWJjmIrYfqj2guUQC+CfxbbGIrrwZqAsRsSmpwvhZ3m 50 | tiSZxG0quKQB+NfSxdvQW5ulbwC7Xc3K35F+i9pb8+TVBdeaFkw+yu6vaZmxQLrX 51 | fQM/pEjD7A7HmMIaO7QaU5SfEAsqdCTP56Y8AftMuNXn/8IRfo2KuGwaWwKBgFpU 52 | ILzJoVdlad9E/Rw7LjYhZfkv1uBVXIyxyKcfrkEXZSmozDXDdxsvcZCEfVHM6Ipk 53 | K/+7LuMcqp4AFEAEq8wTOdq6daFaHLkpt/FZK6M4TlruhtpFOPkoNc3e45eM83OT 54 | 6mziKINJC1CQ6m65sQHpBtjxlKMRG8rL/D6wx9s5AoGBAMRlqNPMwglT3hvDmsAt 55 | 9Lf9pdmhERUlHhD8bj8mDaBj2Aqv7f6VRJaYZqP403pKKQexuqcn80mtjkSAPFkN 56 | Cj7BVt/RXm5uoxDTnfi26RF9F6yNDEJ7UU9+peBr99aazF/fTgW/1GcMkQnum8uV 57 | c257YgaWmjK9uB0Y2r2VxS0G 58 | -----END PRIVATE KEY-----`) 59 | ) 60 | 61 | // BenchmarkPlainStreaming measures end-to-end plaintext streaming performance 62 | // for fasthttp client and server. 63 | // 64 | // It issues http requests over a small number of keep-alive connections. 65 | func BenchmarkPlainStreaming(b *testing.B) { 66 | benchmark(b, streamingHandler, false) 67 | } 68 | 69 | // BenchmarkPlainHandshake measures end-to-end plaintext handshake performance 70 | // for fasthttp client and server. 71 | // 72 | // It re-establishes new connection per each http request. 73 | func BenchmarkPlainHandshake(b *testing.B) { 74 | benchmark(b, handshakeHandler, false) 75 | } 76 | 77 | // BenchmarkTLSStreaming measures end-to-end TLS streaming performance 78 | // for fasthttp client and server. 79 | // 80 | // It issues http requests over a small number of TLS keep-alive connections. 81 | func BenchmarkTLSStreaming(b *testing.B) { 82 | benchmark(b, streamingHandler, true) 83 | } 84 | 85 | func benchmark(b *testing.B, h fasthttp.RequestHandler, isTLS bool) { 86 | var serverTLSConfig, clientTLSConfig *tls.Config 87 | if isTLS { 88 | cert, err := tls.X509KeyPair(certblock, keyblock) 89 | if err != nil { 90 | b.Fatalf("cannot load TLS certificate: %v", err) 91 | } 92 | serverTLSConfig = &tls.Config{ 93 | Certificates: []tls.Certificate{cert}, 94 | PreferServerCipherSuites: true, 95 | } 96 | serverTLSConfig.CurvePreferences = []tls.CurveID{} 97 | clientTLSConfig = &tls.Config{ 98 | InsecureSkipVerify: true, 99 | } 100 | } 101 | ln := fasthttputil.NewInmemoryListener() 102 | serverStopCh := make(chan struct{}) 103 | go func() { 104 | serverLn := net.Listener(ln) 105 | if serverTLSConfig != nil { 106 | serverLn = tls.NewListener(serverLn, serverTLSConfig) 107 | } 108 | if err := fasthttp.Serve(serverLn, h); err != nil { 109 | b.Errorf("unexpected error in server: %v", err) 110 | } 111 | close(serverStopCh) 112 | }() 113 | c := &fasthttp.HostClient{ 114 | Dial: func(addr string) (net.Conn, error) { 115 | return ln.Dial() 116 | }, 117 | IsTLS: isTLS, 118 | TLSConfig: clientTLSConfig, 119 | } 120 | 121 | b.RunParallel(func(pb *testing.PB) { 122 | runRequests(b, pb, c, isTLS) 123 | }) 124 | ln.Close() 125 | <-serverStopCh 126 | } 127 | 128 | func streamingHandler(ctx *fasthttp.RequestCtx) { 129 | ctx.WriteString("foobar") //nolint:errcheck 130 | } 131 | 132 | func handshakeHandler(ctx *fasthttp.RequestCtx) { 133 | streamingHandler(ctx) 134 | 135 | // Explicitly close connection after each response. 136 | ctx.SetConnectionClose() 137 | } 138 | 139 | func runRequests(b *testing.B, pb *testing.PB, c *fasthttp.HostClient, isTLS bool) { 140 | var req fasthttp.Request 141 | if isTLS { 142 | req.SetRequestURI("https://foo.bar/baz") 143 | } else { 144 | req.SetRequestURI("http://foo.bar/baz") 145 | } 146 | var resp fasthttp.Response 147 | for pb.Next() { 148 | if err := c.Do(&req, &resp); err != nil { 149 | b.Fatalf("unexpected error: %v", err) 150 | } 151 | if resp.StatusCode() != fasthttp.StatusOK { 152 | b.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), fasthttp.StatusOK) 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /fs_example_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp_test 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/valyala/fasthttp" 7 | ) 8 | 9 | func ExampleFS() { 10 | fs := &fasthttp.FS{ 11 | // Path to directory to serve. 12 | Root: "/var/www/static-site", 13 | 14 | // Generate index pages if client requests directory contents. 15 | GenerateIndexPages: true, 16 | 17 | // Enable transparent compression to save network traffic. 18 | Compress: true, 19 | } 20 | 21 | // Create request handler for serving static files. 22 | h := fs.NewRequestHandler() 23 | 24 | // Start the server. 25 | if err := fasthttp.ListenAndServe(":8080", h); err != nil { 26 | log.Fatalf("error in ListenAndServe: %v", err) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /fs_handler_example_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp_test 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | 7 | "github.com/valyala/fasthttp" 8 | ) 9 | 10 | // Setup file handlers (aka 'file server config'). 11 | var ( 12 | // Handler for serving images from /img/ path, 13 | // i.e. /img/foo/bar.jpg will be served from 14 | // /var/www/images/foo/bar.jpb . 15 | imgPrefix = []byte("/img/") 16 | imgHandler = fasthttp.FSHandler("/var/www/images", 1) 17 | 18 | // Handler for serving css from /static/css/ path, 19 | // i.e. /static/css/foo/bar.css will be served from 20 | // /home/dev/css/foo/bar.css . 21 | cssPrefix = []byte("/static/css/") 22 | cssHandler = fasthttp.FSHandler("/home/dev/css", 2) 23 | 24 | // Handler for serving the rest of requests, 25 | // i.e. /foo/bar/baz.html will be served from 26 | // /var/www/files/foo/bar/baz.html . 27 | filesHandler = fasthttp.FSHandler("/var/www/files", 0) 28 | ) 29 | 30 | // Main request handler. 31 | func requestHandler(ctx *fasthttp.RequestCtx) { 32 | path := ctx.Path() 33 | switch { 34 | case bytes.HasPrefix(path, imgPrefix): 35 | imgHandler(ctx) 36 | case bytes.HasPrefix(path, cssPrefix): 37 | cssHandler(ctx) 38 | default: 39 | filesHandler(ctx) 40 | } 41 | } 42 | 43 | func ExampleFSHandler() { 44 | if err := fasthttp.ListenAndServe(":80", requestHandler); err != nil { 45 | log.Fatalf("Error in server: %v", err) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /fuzz_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/base64" 7 | "encoding/binary" 8 | "net/url" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func FuzzCookieParse(f *testing.F) { 14 | f.Add([]byte(`xxx=yyy`)) 15 | f.Add([]byte(`xxx=yyy; expires=Tue, 10 Nov 2009 23:00:00 GMT; domain=foobar.com; path=/a/b`)) 16 | f.Add([]byte(" \n\t\"")) 17 | 18 | f.Fuzz(func(t *testing.T, cookie []byte) { 19 | var c Cookie 20 | 21 | _ = c.ParseBytes(cookie) 22 | 23 | w := bytes.Buffer{} 24 | if _, err := c.WriteTo(&w); err != nil { 25 | t.Fatalf("unexpected error: %v", err) 26 | } 27 | }) 28 | } 29 | 30 | func FuzzVisitHeaderParams(f *testing.F) { 31 | f.Add([]byte(`application/json; v=1; foo=bar; q=0.938; param=param; param="big fox"; q=0.43`)) 32 | f.Add([]byte(`*/*`)) 33 | f.Add([]byte(`\\`)) 34 | f.Add([]byte(`text/plain; foo="\\\"\'\\''\'"`)) 35 | 36 | f.Fuzz(func(t *testing.T, header []byte) { 37 | VisitHeaderParams(header, func(key, value []byte) bool { 38 | if len(key) == 0 { 39 | t.Errorf("Unexpected length zero parameter, failed input was: %s", header) 40 | } 41 | return true 42 | }) 43 | }) 44 | } 45 | 46 | func FuzzResponseReadLimitBody(f *testing.F) { 47 | f.Add([]byte("HTTP/1.1 200 OK\r\nContent-Type: aa\r\nContent-Length: 10\r\n\r\n9876543210"), 1024) 48 | f.Add([]byte(" 0\nTrAnsfer-EnCoding:0\n\n0\r\n1:0\n 00\n 000\n\n"), 24922) 49 | f.Add([]byte(" 0\n0:\n 0\n :\n"), 1048532) 50 | 51 | // Case found by OSS-Fuzz. 52 | b, err := base64.StdEncoding.DecodeString("oeYAdyAyClRyYW5zZmVyLUVuY29kaW5nOmlka7AKCjANCiA6MAogOgogOgogPgAAAAAAAAAgICAhICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiA6CiA6CiAgOgogOgogYDogCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogOgogOgogIDoKIDoKIGA6IAoKIDoKBSAgOgogOgogOgogOgogIDoKIDoKIGA6IAAgIAA6CiA6CiA6CjoKIDoKIDoWCiAyIOgKIDogugogOjAKIDoKIDoKBSAgOgogOgogOgogOgogIDoKIDoKIGA6IAAgIAAAAAAAAABaYQ==") 53 | if err != nil { 54 | panic(err) 55 | } 56 | f.Add(b[:len(b)-2], int(binary.LittleEndian.Uint16(b[len(b)-2:]))) 57 | 58 | f.Fuzz(func(t *testing.T, body []byte, maxBodySize int) { 59 | if len(body) > 1024*1024 || maxBodySize > 1024*1024 { 60 | return 61 | } 62 | // Only test with a max for the body, otherwise a very large Content-Length will just OOM. 63 | if maxBodySize <= 0 { 64 | return 65 | } 66 | 67 | res := AcquireResponse() 68 | defer ReleaseResponse(res) 69 | 70 | _ = res.ReadLimitBody(bufio.NewReader(bytes.NewReader(body)), maxBodySize) 71 | }) 72 | } 73 | 74 | func FuzzRequestReadLimitBody(f *testing.F) { 75 | f.Add([]byte("POST /a HTTP/1.1\r\nHost: a.com\r\nTransfer-Encoding: chunked\r\nContent-Type: aa\r\n\r\n6\r\nfoobar\r\n3\r\nbaz\r\n0\r\nfoobar\r\n\r\n"), 1024) 76 | 77 | f.Fuzz(func(t *testing.T, body []byte, maxBodySize int) { 78 | if len(body) > 1024*1024 || maxBodySize > 1024*1024 { 79 | return 80 | } 81 | // Only test with a max for the body, otherwise a very large Content-Length will just OOM. 82 | if maxBodySize <= 0 { 83 | return 84 | } 85 | 86 | req := AcquireRequest() 87 | defer ReleaseRequest(req) 88 | 89 | _ = req.ReadLimitBody(bufio.NewReader(bytes.NewReader(body)), maxBodySize) 90 | }) 91 | } 92 | 93 | func FuzzURIUpdateBytes(f *testing.F) { 94 | f.Add([]byte(`http://foobar.com/aaa/bb?cc`)) 95 | f.Add([]byte(`//foobar.com/aaa/bb?cc`)) 96 | f.Add([]byte(`/aaa/bb?cc`)) 97 | f.Add([]byte(`xx?yy=abc`)) 98 | 99 | f.Fuzz(func(t *testing.T, uri []byte) { 100 | var u URI 101 | 102 | u.UpdateBytes(uri) 103 | 104 | w := bytes.Buffer{} 105 | if _, err := u.WriteTo(&w); err != nil { 106 | t.Fatalf("unexpected error: %v", err) 107 | } 108 | }) 109 | } 110 | 111 | func FuzzURIParse(f *testing.F) { 112 | f.Add(`http://foobar.com/aaa/bb?cc#dd`) 113 | f.Add(`http://google.com?github.com`) 114 | f.Add(`http://google.com#@github.com`) 115 | 116 | f.Fuzz(func(t *testing.T, uri string) { 117 | var u URI 118 | 119 | uri = strings.ToLower(uri) 120 | 121 | if !strings.HasPrefix(uri, "http://") && !strings.HasPrefix(uri, "https://") { 122 | return 123 | } 124 | 125 | if u.Parse(nil, []byte(uri)) != nil { 126 | return 127 | } 128 | 129 | nu, err := url.Parse(uri) 130 | if err != nil { 131 | return 132 | } 133 | 134 | if string(u.Host()) != nu.Host { 135 | t.Fatalf("%q: unexpected host: %q. Expecting %q", uri, u.Host(), nu.Host) 136 | } 137 | if string(u.QueryString()) != nu.RawQuery { 138 | t.Fatalf("%q: unexpected query string: %q. Expecting %q", uri, u.QueryString(), nu.RawQuery) 139 | } 140 | }) 141 | } 142 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/valyala/fasthttp 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/andybalholm/brotli v1.1.1 9 | github.com/klauspost/compress v1.18.0 10 | github.com/valyala/bytebufferpool v1.0.0 11 | golang.org/x/crypto v0.38.0 12 | golang.org/x/net v0.40.0 13 | golang.org/x/sys v0.33.0 14 | ) 15 | 16 | require golang.org/x/text v0.25.0 // indirect 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 2 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 3 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 4 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 5 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 6 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 7 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= 8 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 9 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 10 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 11 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 12 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 13 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 14 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 15 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 16 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 17 | -------------------------------------------------------------------------------- /header_regression_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestIssue28ResponseWithoutBodyNoContentType(t *testing.T) { 12 | t.Parallel() 13 | 14 | var r Response 15 | 16 | // Empty response without content-type 17 | s := r.String() 18 | if strings.Contains(s, "Content-Type") { 19 | t.Fatalf("unexpected Content-Type found in response header with empty body: %q", s) 20 | } 21 | 22 | // Explicitly set content-type 23 | r.Header.SetContentType("foo/bar") 24 | s = r.String() 25 | if !strings.Contains(s, "Content-Type: foo/bar\r\n") { 26 | t.Fatalf("missing explicitly set content-type for empty response: %q", s) 27 | } 28 | 29 | // Non-empty response. 30 | r.Reset() 31 | r.SetBodyString("foobar") 32 | s = r.String() 33 | if !strings.Contains(s, fmt.Sprintf("Content-Type: %s\r\n", defaultContentType)) { 34 | t.Fatalf("missing default content-type for non-empty response: %q", s) 35 | } 36 | 37 | // Non-empty response with custom content-type. 38 | r.Header.SetContentType("aaa/bbb") 39 | s = r.String() 40 | if !strings.Contains(s, "Content-Type: aaa/bbb\r\n") { 41 | t.Fatalf("missing custom content-type: %q", s) 42 | } 43 | } 44 | 45 | func TestIssue6RequestHeaderSetContentType(t *testing.T) { 46 | t.Parallel() 47 | 48 | testIssue6RequestHeaderSetContentType(t, MethodGet) 49 | testIssue6RequestHeaderSetContentType(t, MethodPost) 50 | testIssue6RequestHeaderSetContentType(t, MethodPut) 51 | testIssue6RequestHeaderSetContentType(t, MethodPatch) 52 | } 53 | 54 | func testIssue6RequestHeaderSetContentType(t *testing.T, method string) { 55 | contentType := "application/json" 56 | contentLength := 123 57 | 58 | var h RequestHeader 59 | h.SetMethod(method) 60 | h.SetRequestURI("http://localhost/test") 61 | h.SetContentType(contentType) 62 | h.SetContentLength(contentLength) 63 | 64 | issue6VerifyRequestHeader(t, &h, contentType, contentLength, method) 65 | 66 | s := h.String() 67 | 68 | var h1 RequestHeader 69 | 70 | br := bufio.NewReader(bytes.NewBufferString(s)) 71 | if err := h1.Read(br); err != nil { 72 | t.Fatalf("unexpected error: %v", err) 73 | } 74 | issue6VerifyRequestHeader(t, &h1, contentType, contentLength, method) 75 | } 76 | 77 | func issue6VerifyRequestHeader(t *testing.T, h *RequestHeader, contentType string, contentLength int, method string) { 78 | if string(h.ContentType()) != contentType { 79 | t.Fatalf("unexpected content-type: %q. Expecting %q. method=%q", h.ContentType(), contentType, method) 80 | } 81 | if string(h.Method()) != method { 82 | t.Fatalf("unexpected method: %q. Expecting %q", h.Method(), method) 83 | } 84 | if h.ContentLength() != contentLength { 85 | t.Fatalf("unexpected content-length: %d. Expecting %d. method=%q", h.ContentLength(), contentLength, method) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /http_timing_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | "os" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func BenchmarkCopyZeroAllocOSFileToBytesBuffer(b *testing.B) { 13 | r, err := os.Open("./README.md") 14 | if err != nil { 15 | b.Fatal(err) 16 | } 17 | defer r.Close() 18 | 19 | buf := &bytes.Buffer{} 20 | 21 | b.ResetTimer() 22 | for i := 0; i < b.N; i++ { 23 | buf.Reset() 24 | _, err = copyZeroAlloc(buf, r) 25 | if err != nil { 26 | b.Fatal(err) 27 | } 28 | } 29 | } 30 | 31 | func BenchmarkCopyZeroAllocBytesBufferToOSFile(b *testing.B) { 32 | f, err := os.Open("./README.md") 33 | if err != nil { 34 | b.Fatal(err) 35 | } 36 | defer f.Close() 37 | 38 | buf := &bytes.Buffer{} 39 | _, err = io.Copy(buf, f) 40 | if err != nil { 41 | b.Fatal(err) 42 | } 43 | 44 | tmp, err := os.CreateTemp(os.TempDir(), "test_*") 45 | if err != nil { 46 | b.Fatal(err) 47 | } 48 | defer os.Remove(tmp.Name()) 49 | 50 | w, err := os.OpenFile(tmp.Name(), os.O_WRONLY, 0o444) 51 | if err != nil { 52 | b.Fatal(err) 53 | } 54 | defer w.Close() 55 | 56 | b.ResetTimer() 57 | for i := 0; i < b.N; i++ { 58 | _, err := w.Seek(0, 0) 59 | if err != nil { 60 | b.Fatal(err) 61 | } 62 | _, err = copyZeroAlloc(w, buf) 63 | if err != nil { 64 | b.Fatal(err) 65 | } 66 | } 67 | } 68 | 69 | func BenchmarkCopyZeroAllocOSFileToStringsBuilder(b *testing.B) { 70 | r, err := os.Open("./README.md") 71 | if err != nil { 72 | b.Fatalf("Failed to open testing file: %v", err) 73 | } 74 | defer r.Close() 75 | 76 | w := &strings.Builder{} 77 | 78 | b.ResetTimer() 79 | for i := 0; i < b.N; i++ { 80 | w.Reset() 81 | _, err = copyZeroAlloc(w, r) 82 | if err != nil { 83 | b.Fatal(err) 84 | } 85 | } 86 | } 87 | 88 | func BenchmarkCopyZeroAllocIOLimitedReaderToOSFile(b *testing.B) { 89 | f, err := os.Open("./README.md") 90 | if err != nil { 91 | b.Fatal(err) 92 | } 93 | defer f.Close() 94 | 95 | r := io.LimitReader(f, 1024) 96 | 97 | tmp, err := os.CreateTemp(os.TempDir(), "test_*") 98 | if err != nil { 99 | b.Fatal(err) 100 | } 101 | defer os.Remove(tmp.Name()) 102 | 103 | w, err := os.OpenFile(tmp.Name(), os.O_WRONLY, 0o444) 104 | if err != nil { 105 | b.Fatal(err) 106 | } 107 | defer w.Close() 108 | 109 | b.ResetTimer() 110 | for i := 0; i < b.N; i++ { 111 | _, err := w.Seek(0, 0) 112 | if err != nil { 113 | b.Fatal(err) 114 | } 115 | _, err = copyZeroAlloc(w, r) 116 | if err != nil { 117 | b.Fatal(err) 118 | } 119 | } 120 | } 121 | 122 | func BenchmarkCopyZeroAllocOSFileToOSFile(b *testing.B) { 123 | r, err := os.Open("./README.md") 124 | if err != nil { 125 | b.Fatal(err) 126 | } 127 | defer r.Close() 128 | 129 | f, err := os.CreateTemp(os.TempDir(), "test_*") 130 | if err != nil { 131 | b.Fatal(err) 132 | } 133 | defer os.Remove(f.Name()) 134 | 135 | w, err := os.OpenFile(f.Name(), os.O_WRONLY, 0o444) 136 | if err != nil { 137 | b.Fatal(err) 138 | } 139 | defer w.Close() 140 | 141 | b.ResetTimer() 142 | for i := 0; i < b.N; i++ { 143 | _, err := w.Seek(0, 0) 144 | if err != nil { 145 | b.Fatal(err) 146 | } 147 | _, err = copyZeroAlloc(w, r) 148 | if err != nil { 149 | b.Fatal(err) 150 | } 151 | } 152 | } 153 | 154 | func BenchmarkCopyZeroAllocOSFileToNetConn(b *testing.B) { 155 | ln, err := net.Listen("tcp", "127.0.0.1:0") 156 | if err != nil { 157 | b.Fatal(err) 158 | } 159 | 160 | addr := ln.Addr().String() 161 | defer ln.Close() 162 | 163 | done := make(chan struct{}) 164 | defer close(done) 165 | 166 | go func() { 167 | conn, err := ln.Accept() 168 | if err != nil { 169 | b.Error(err) 170 | return 171 | } 172 | defer conn.Close() 173 | for { 174 | select { 175 | case <-done: 176 | return 177 | default: 178 | _, err := io.Copy(io.Discard, conn) 179 | if err != nil { 180 | b.Error(err) 181 | return 182 | } 183 | } 184 | } 185 | }() 186 | 187 | conn, err := net.Dial("tcp", addr) 188 | if err != nil { 189 | b.Fatal(err) 190 | } 191 | defer conn.Close() 192 | 193 | file, err := os.Open("./README.md") 194 | if err != nil { 195 | b.Fatal(err) 196 | } 197 | defer file.Close() 198 | 199 | b.ResetTimer() 200 | for i := 0; i < b.N; i++ { 201 | if _, err := copyZeroAlloc(conn, file); err != nil { 202 | b.Fatal(err) 203 | } 204 | } 205 | } 206 | 207 | func BenchmarkCopyZeroAllocNetConnToOSFile(b *testing.B) { 208 | data, err := os.ReadFile("./README.md") 209 | if err != nil { 210 | b.Fatal(err) 211 | } 212 | 213 | ln, err := net.Listen("tcp", "127.0.0.1:0") 214 | if err != nil { 215 | b.Fatal(err) 216 | } 217 | 218 | addr := ln.Addr().String() 219 | defer ln.Close() 220 | 221 | done := make(chan struct{}) 222 | defer close(done) 223 | 224 | writeDone := make(chan struct{}) 225 | go func() { 226 | for { 227 | select { 228 | case <-done: 229 | return 230 | default: 231 | conn, err := ln.Accept() 232 | if err != nil { 233 | b.Error(err) 234 | return 235 | } 236 | _, err = conn.Write(data) 237 | if err != nil { 238 | b.Error(err) 239 | } 240 | conn.Close() 241 | writeDone <- struct{}{} 242 | } 243 | } 244 | }() 245 | 246 | tmp, err := os.CreateTemp(os.TempDir(), "test_*") 247 | if err != nil { 248 | b.Fatal(err) 249 | } 250 | defer os.Remove(tmp.Name()) 251 | 252 | file, err := os.OpenFile(tmp.Name(), os.O_WRONLY, 0o444) 253 | if err != nil { 254 | b.Fatal(err) 255 | } 256 | defer file.Close() 257 | 258 | conn, err := net.Dial("tcp", addr) 259 | if err != nil { 260 | b.Fatal(err) 261 | } 262 | defer conn.Close() 263 | 264 | b.ResetTimer() 265 | for i := 0; i < b.N; i++ { 266 | b.StopTimer() 267 | <-writeDone 268 | _, err = file.Seek(0, 0) 269 | if err != nil { 270 | b.Fatal(err) 271 | } 272 | b.StartTimer() 273 | _, err = copyZeroAlloc(file, conn) 274 | if err != nil { 275 | b.Fatal(err) 276 | } 277 | b.StopTimer() 278 | conn, err = net.Dial("tcp", addr) 279 | if err != nil { 280 | b.Fatal(err) 281 | } 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /lbclient.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | "time" 7 | ) 8 | 9 | // BalancingClient is the interface for clients, which may be passed 10 | // to LBClient.Clients. 11 | type BalancingClient interface { 12 | DoDeadline(req *Request, resp *Response, deadline time.Time) error 13 | PendingRequests() int 14 | } 15 | 16 | // LBClient balances requests among available LBClient.Clients. 17 | // 18 | // It has the following features: 19 | // 20 | // - Balances load among available clients using 'least loaded' + 'least total' 21 | // hybrid technique. 22 | // - Dynamically decreases load on unhealthy clients. 23 | // 24 | // It is forbidden copying LBClient instances. Create new instances instead. 25 | // 26 | // It is safe calling LBClient methods from concurrently running goroutines. 27 | type LBClient struct { 28 | noCopy noCopy 29 | 30 | // HealthCheck is a callback called after each request. 31 | // 32 | // The request, response and the error returned by the client 33 | // is passed to HealthCheck, so the callback may determine whether 34 | // the client is healthy. 35 | // 36 | // Load on the current client is decreased if HealthCheck returns false. 37 | // 38 | // By default HealthCheck returns false if err != nil. 39 | HealthCheck func(req *Request, resp *Response, err error) bool 40 | 41 | // Clients must contain non-zero clients list. 42 | // Incoming requests are balanced among these clients. 43 | Clients []BalancingClient 44 | 45 | cs []*lbClient 46 | 47 | // Timeout is the request timeout used when calling LBClient.Do. 48 | // 49 | // DefaultLBClientTimeout is used by default. 50 | Timeout time.Duration 51 | 52 | mu sync.RWMutex 53 | 54 | once sync.Once 55 | } 56 | 57 | // DefaultLBClientTimeout is the default request timeout used by LBClient 58 | // when calling LBClient.Do. 59 | // 60 | // The timeout may be overridden via LBClient.Timeout. 61 | const DefaultLBClientTimeout = time.Second 62 | 63 | // DoDeadline calls DoDeadline on the least loaded client. 64 | func (cc *LBClient) DoDeadline(req *Request, resp *Response, deadline time.Time) error { 65 | return cc.get().DoDeadline(req, resp, deadline) 66 | } 67 | 68 | // DoTimeout calculates deadline and calls DoDeadline on the least loaded client. 69 | func (cc *LBClient) DoTimeout(req *Request, resp *Response, timeout time.Duration) error { 70 | deadline := time.Now().Add(timeout) 71 | return cc.get().DoDeadline(req, resp, deadline) 72 | } 73 | 74 | // Do calculates timeout using LBClient.Timeout and calls DoTimeout 75 | // on the least loaded client. 76 | func (cc *LBClient) Do(req *Request, resp *Response) error { 77 | timeout := cc.Timeout 78 | if timeout <= 0 { 79 | timeout = DefaultLBClientTimeout 80 | } 81 | return cc.DoTimeout(req, resp, timeout) 82 | } 83 | 84 | func (cc *LBClient) init() { 85 | cc.mu.Lock() 86 | defer cc.mu.Unlock() 87 | if len(cc.Clients) == 0 { 88 | // developer sanity-check 89 | panic("BUG: LBClient.Clients cannot be empty") 90 | } 91 | for _, c := range cc.Clients { 92 | cc.cs = append(cc.cs, &lbClient{ 93 | c: c, 94 | healthCheck: cc.HealthCheck, 95 | }) 96 | } 97 | } 98 | 99 | // AddClient adds a new client to the balanced clients and 100 | // returns the new total number of clients. 101 | func (cc *LBClient) AddClient(c BalancingClient) int { 102 | cc.mu.Lock() 103 | cc.cs = append(cc.cs, &lbClient{ 104 | c: c, 105 | healthCheck: cc.HealthCheck, 106 | }) 107 | cc.mu.Unlock() 108 | return len(cc.cs) 109 | } 110 | 111 | // RemoveClients removes clients using the provided callback. 112 | // If rc returns true, the passed client will be removed. 113 | // Returns the new total number of clients. 114 | func (cc *LBClient) RemoveClients(rc func(BalancingClient) bool) int { 115 | cc.mu.Lock() 116 | n := 0 117 | for idx, cs := range cc.cs { 118 | cc.cs[idx] = nil 119 | if rc(cs.c) { 120 | continue 121 | } 122 | cc.cs[n] = cs 123 | n++ 124 | } 125 | cc.cs = cc.cs[:n] 126 | 127 | cc.mu.Unlock() 128 | return len(cc.cs) 129 | } 130 | 131 | func (cc *LBClient) get() *lbClient { 132 | cc.once.Do(cc.init) 133 | 134 | cc.mu.RLock() 135 | cs := cc.cs 136 | 137 | minC := cs[0] 138 | minN := minC.PendingRequests() 139 | minT := atomic.LoadUint64(&minC.total) 140 | for _, c := range cs[1:] { 141 | n := c.PendingRequests() 142 | t := atomic.LoadUint64(&c.total) 143 | if n < minN || (n == minN && t < minT) { 144 | minC = c 145 | minN = n 146 | minT = t 147 | } 148 | } 149 | cc.mu.RUnlock() 150 | return minC 151 | } 152 | 153 | type lbClient struct { 154 | c BalancingClient 155 | healthCheck func(req *Request, resp *Response, err error) bool 156 | penalty uint32 157 | 158 | // total amount of requests handled. 159 | total uint64 160 | } 161 | 162 | func (c *lbClient) DoDeadline(req *Request, resp *Response, deadline time.Time) error { 163 | err := c.c.DoDeadline(req, resp, deadline) 164 | if !c.isHealthy(req, resp, err) && c.incPenalty() { 165 | // Penalize the client returning error, so the next requests 166 | // are routed to another clients. 167 | time.AfterFunc(penaltyDuration, c.decPenalty) 168 | } else { 169 | atomic.AddUint64(&c.total, 1) 170 | } 171 | return err 172 | } 173 | 174 | func (c *lbClient) PendingRequests() int { 175 | n := c.c.PendingRequests() 176 | m := atomic.LoadUint32(&c.penalty) 177 | return n + int(m) 178 | } 179 | 180 | func (c *lbClient) isHealthy(req *Request, resp *Response, err error) bool { 181 | if c.healthCheck == nil { 182 | return err == nil 183 | } 184 | return c.healthCheck(req, resp, err) 185 | } 186 | 187 | func (c *lbClient) incPenalty() bool { 188 | m := atomic.AddUint32(&c.penalty, 1) 189 | if m > maxPenalty { 190 | c.decPenalty() 191 | return false 192 | } 193 | return true 194 | } 195 | 196 | func (c *lbClient) decPenalty() { 197 | atomic.AddUint32(&c.penalty, ^uint32(0)) 198 | } 199 | 200 | const ( 201 | maxPenalty = 300 202 | 203 | penaltyDuration = 3 * time.Second 204 | ) 205 | -------------------------------------------------------------------------------- /lbclient_example_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/valyala/fasthttp" 8 | ) 9 | 10 | func ExampleLBClient() { 11 | // Requests will be spread among these servers. 12 | servers := []string{ 13 | "google.com:80", 14 | "foobar.com:8080", 15 | "127.0.0.1:123", 16 | } 17 | 18 | // Prepare clients for each server 19 | var lbc fasthttp.LBClient 20 | for _, addr := range servers { 21 | c := &fasthttp.HostClient{ 22 | Addr: addr, 23 | } 24 | lbc.Clients = append(lbc.Clients, c) 25 | } 26 | 27 | // Send requests to load-balanced servers 28 | var req fasthttp.Request 29 | var resp fasthttp.Response 30 | for i := 0; i < 10; i++ { 31 | url := fmt.Sprintf("http://abcedfg/foo/bar/%d", i) 32 | req.SetRequestURI(url) 33 | if err := lbc.Do(&req, &resp); err != nil { 34 | log.Fatalf("Error when sending request: %v", err) 35 | } 36 | if resp.StatusCode() != fasthttp.StatusOK { 37 | log.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), fasthttp.StatusOK) 38 | } 39 | 40 | useResponseBody(resp.Body()) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /methods.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | // HTTP methods were copied from net/http. 4 | const ( 5 | MethodGet = "GET" // RFC 7231, 4.3.1 6 | MethodHead = "HEAD" // RFC 7231, 4.3.2 7 | MethodPost = "POST" // RFC 7231, 4.3.3 8 | MethodPut = "PUT" // RFC 7231, 4.3.4 9 | MethodPatch = "PATCH" // RFC 5789 10 | MethodDelete = "DELETE" // RFC 7231, 4.3.5 11 | MethodConnect = "CONNECT" // RFC 7231, 4.3.6 12 | MethodOptions = "OPTIONS" // RFC 7231, 4.3.7 13 | MethodTrace = "TRACE" // RFC 7231, 4.3.8 14 | ) 15 | -------------------------------------------------------------------------------- /nocopy.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | // Embed this type into a struct, which mustn't be copied, 4 | // so `go vet` gives a warning if this struct is copied. 5 | // 6 | // See https://github.com/golang/go/issues/8005#issuecomment-190753527 for details. 7 | // and also: https://stackoverflow.com/questions/52494458/nocopy-minimal-example 8 | type noCopy struct{} 9 | 10 | func (*noCopy) Lock() {} 11 | func (*noCopy) Unlock() {} 12 | -------------------------------------------------------------------------------- /peripconn.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | "sync" 7 | ) 8 | 9 | type perIPConnCounter struct { 10 | perIPConnPool sync.Pool 11 | perIPTLSConnPool sync.Pool 12 | m map[uint32]int 13 | lock sync.Mutex 14 | } 15 | 16 | func (cc *perIPConnCounter) Register(ip uint32) int { 17 | cc.lock.Lock() 18 | if cc.m == nil { 19 | cc.m = make(map[uint32]int) 20 | } 21 | n := cc.m[ip] + 1 22 | cc.m[ip] = n 23 | cc.lock.Unlock() 24 | return n 25 | } 26 | 27 | func (cc *perIPConnCounter) Unregister(ip uint32) { 28 | cc.lock.Lock() 29 | defer cc.lock.Unlock() 30 | if cc.m == nil { 31 | // developer safeguard 32 | panic("BUG: perIPConnCounter.Register() wasn't called") 33 | } 34 | n := cc.m[ip] - 1 35 | if n < 0 { 36 | n = 0 37 | } 38 | cc.m[ip] = n 39 | } 40 | 41 | type perIPConn struct { 42 | net.Conn 43 | 44 | perIPConnCounter *perIPConnCounter 45 | 46 | ip uint32 47 | lock sync.Mutex 48 | } 49 | 50 | type perIPTLSConn struct { 51 | *tls.Conn 52 | 53 | perIPConnCounter *perIPConnCounter 54 | 55 | ip uint32 56 | lock sync.Mutex 57 | } 58 | 59 | func acquirePerIPConn(conn net.Conn, ip uint32, counter *perIPConnCounter) net.Conn { 60 | if tlsConn, ok := conn.(*tls.Conn); ok { 61 | v := counter.perIPTLSConnPool.Get() 62 | if v == nil { 63 | return &perIPTLSConn{ 64 | perIPConnCounter: counter, 65 | Conn: tlsConn, 66 | ip: ip, 67 | } 68 | } 69 | c := v.(*perIPTLSConn) 70 | c.Conn = tlsConn 71 | c.ip = ip 72 | return c 73 | } 74 | 75 | v := counter.perIPConnPool.Get() 76 | if v == nil { 77 | return &perIPConn{ 78 | perIPConnCounter: counter, 79 | Conn: conn, 80 | ip: ip, 81 | } 82 | } 83 | c := v.(*perIPConn) 84 | c.Conn = conn 85 | c.ip = ip 86 | return c 87 | } 88 | 89 | func (c *perIPConn) Close() error { 90 | c.lock.Lock() 91 | cc := c.Conn 92 | c.Conn = nil 93 | c.lock.Unlock() 94 | 95 | if cc == nil { 96 | return nil 97 | } 98 | 99 | err := cc.Close() 100 | c.perIPConnCounter.Unregister(c.ip) 101 | c.perIPConnCounter.perIPConnPool.Put(c) 102 | return err 103 | } 104 | 105 | func (c *perIPTLSConn) Close() error { 106 | c.lock.Lock() 107 | cc := c.Conn 108 | c.Conn = nil 109 | c.lock.Unlock() 110 | 111 | if cc == nil { 112 | return nil 113 | } 114 | 115 | err := cc.Close() 116 | c.perIPConnCounter.Unregister(c.ip) 117 | c.perIPConnCounter.perIPTLSConnPool.Put(c) 118 | return err 119 | } 120 | 121 | func getUint32IP(c net.Conn) uint32 { 122 | return ip2uint32(getConnIP4(c)) 123 | } 124 | 125 | func getConnIP4(c net.Conn) net.IP { 126 | addr := c.RemoteAddr() 127 | ipAddr, ok := addr.(*net.TCPAddr) 128 | if !ok { 129 | return net.IPv4zero 130 | } 131 | return ipAddr.IP.To4() 132 | } 133 | 134 | func ip2uint32(ip net.IP) uint32 { 135 | if len(ip) != 4 { 136 | return 0 137 | } 138 | return uint32(ip[0])<<24 | uint32(ip[1])<<16 | uint32(ip[2])<<8 | uint32(ip[3]) 139 | } 140 | 141 | func uint322ip(ip uint32) net.IP { 142 | b := make([]byte, 4) 143 | b[0] = byte(ip >> 24) 144 | b[1] = byte(ip >> 16) 145 | b[2] = byte(ip >> 8) 146 | b[3] = byte(ip) 147 | return b 148 | } 149 | -------------------------------------------------------------------------------- /peripconn_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var _ connTLSer = &perIPTLSConn{} 8 | 9 | func TestIPxUint32(t *testing.T) { 10 | t.Parallel() 11 | 12 | testIPxUint32(t, 0) 13 | testIPxUint32(t, 10) 14 | testIPxUint32(t, 0x12892392) 15 | } 16 | 17 | func testIPxUint32(t *testing.T, n uint32) { 18 | ip := uint322ip(n) 19 | nn := ip2uint32(ip) 20 | if n != nn { 21 | t.Fatalf("Unexpected value=%d for ip=%q. Expected %d", nn, ip, n) 22 | } 23 | } 24 | 25 | func TestPerIPConnCounter(t *testing.T) { 26 | t.Parallel() 27 | 28 | var cc perIPConnCounter 29 | 30 | for i := 1; i < 100; i++ { 31 | if n := cc.Register(123); n != i { 32 | t.Fatalf("Unexpected counter value=%d. Expected %d", n, i) 33 | } 34 | } 35 | 36 | n := cc.Register(456) 37 | if n != 1 { 38 | t.Fatalf("Unexpected counter value=%d. Expected 1", n) 39 | } 40 | 41 | for i := 1; i < 100; i++ { 42 | cc.Unregister(123) 43 | } 44 | cc.Unregister(456) 45 | 46 | n = cc.Register(123) 47 | if n != 1 { 48 | t.Fatalf("Unexpected counter value=%d. Expected 1", n) 49 | } 50 | cc.Unregister(123) 51 | } 52 | -------------------------------------------------------------------------------- /pprofhandler/pprof.go: -------------------------------------------------------------------------------- 1 | // Package pprofhandler provides a fasthttp handler similar to net/http/pprof. 2 | package pprofhandler 3 | 4 | import ( 5 | "bytes" 6 | "net/http/pprof" 7 | rtp "runtime/pprof" 8 | 9 | "github.com/valyala/fasthttp" 10 | "github.com/valyala/fasthttp/fasthttpadaptor" 11 | ) 12 | 13 | var ( 14 | cmdline = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Cmdline) 15 | profile = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Profile) 16 | symbol = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Symbol) 17 | trace = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Trace) 18 | index = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Index) 19 | ) 20 | 21 | // PprofHandler serves server runtime profiling data in the format expected by the pprof visualization tool. 22 | // 23 | // See https://pkg.go.dev/net/http/pprof for details. 24 | func PprofHandler(ctx *fasthttp.RequestCtx) { 25 | ctx.Response.Header.Set("Content-Type", "text/html") 26 | switch { 27 | case bytes.HasPrefix(ctx.Path(), []byte("/debug/pprof/cmdline")): 28 | cmdline(ctx) 29 | case bytes.HasPrefix(ctx.Path(), []byte("/debug/pprof/profile")): 30 | profile(ctx) 31 | case bytes.HasPrefix(ctx.Path(), []byte("/debug/pprof/symbol")): 32 | symbol(ctx) 33 | case bytes.HasPrefix(ctx.Path(), []byte("/debug/pprof/trace")): 34 | trace(ctx) 35 | default: 36 | for _, v := range rtp.Profiles() { 37 | ppName := v.Name() 38 | if bytes.HasPrefix(ctx.Path(), []byte("/debug/pprof/"+ppName)) { 39 | namedHandler := fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler(ppName).ServeHTTP) 40 | namedHandler(ctx) 41 | return 42 | } 43 | } 44 | index(ctx) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /prefork/README.md: -------------------------------------------------------------------------------- 1 | # Prefork 2 | 3 | Server prefork implementation. 4 | 5 | Preforks master process between several child processes increases performance, because Go doesn't have to share and manage memory between cores. 6 | 7 | **WARNING: using prefork prevents the use of any global state!. Things like in-memory caches won't work.** 8 | 9 | - How it works: 10 | 11 | ```go 12 | import ( 13 | "github.com/valyala/fasthttp" 14 | "github.com/valyala/fasthttp/prefork" 15 | ) 16 | 17 | server := &fasthttp.Server{ 18 | // Your configuration 19 | } 20 | 21 | // Wraps the server with prefork 22 | preforkServer := prefork.New(server) 23 | 24 | if err := preforkServer.ListenAndServe(":8080"); err != nil { 25 | panic(err) 26 | } 27 | ``` 28 | 29 | ## Benchmarks 30 | 31 | Environment: 32 | 33 | - Machine: MacBook Pro 13-inch, 2017 34 | - OS: MacOS 10.15.3 35 | - Go: go1.13.6 darwin/amd64 36 | 37 | Handler code: 38 | 39 | ```go 40 | func requestHandler(ctx *fasthttp.RequestCtx) { 41 | // Simulates some hard work 42 | time.Sleep(100 * time.Millisecond) 43 | } 44 | ``` 45 | 46 | Test command: 47 | 48 | ```bash 49 | $ wrk -H 'Host: localhost' -H 'Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7' -H 'Connection: keep-alive' --latency -d 15 -c 512 --timeout 8 -t 4 http://localhost:8080 50 | ``` 51 | 52 | Results: 53 | 54 | - prefork 55 | 56 | ```bash 57 | Running 15s test @ http://localhost:8080 58 | 4 threads and 512 connections 59 | Thread Stats Avg Stdev Max +/- Stdev 60 | Latency 4.75ms 4.27ms 126.24ms 97.45% 61 | Req/Sec 26.46k 4.16k 71.18k 88.72% 62 | Latency Distribution 63 | 50% 4.55ms 64 | 75% 4.82ms 65 | 90% 5.46ms 66 | 99% 15.49ms 67 | 1581916 requests in 15.09s, 140.30MB read 68 | Socket errors: connect 0, read 318, write 0, timeout 0 69 | Requests/sec: 104861.58 70 | Transfer/sec: 9.30MB 71 | ``` 72 | 73 | - **non**-prefork 74 | 75 | ```bash 76 | Running 15s test @ http://localhost:8080 77 | 4 threads and 512 connections 78 | Thread Stats Avg Stdev Max +/- Stdev 79 | Latency 6.42ms 11.83ms 177.19ms 96.42% 80 | Req/Sec 24.96k 5.83k 56.83k 82.93% 81 | Latency Distribution 82 | 50% 4.53ms 83 | 75% 4.93ms 84 | 90% 6.94ms 85 | 99% 74.54ms 86 | 1472441 requests in 15.09s, 130.59MB read 87 | Socket errors: connect 0, read 265, write 0, timeout 0 88 | Requests/sec: 97553.34 89 | Transfer/sec: 8.65MB 90 | ``` 91 | -------------------------------------------------------------------------------- /prefork/prefork_test.go: -------------------------------------------------------------------------------- 1 | package prefork 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "net" 7 | "os" 8 | "reflect" 9 | "runtime" 10 | "testing" 11 | 12 | "github.com/valyala/fasthttp" 13 | ) 14 | 15 | func setUp() { 16 | os.Setenv(preforkChildEnvVariable, "1") 17 | } 18 | 19 | func tearDown() { 20 | os.Unsetenv(preforkChildEnvVariable) 21 | } 22 | 23 | func getAddr() string { 24 | return fmt.Sprintf("127.0.0.1:%d", rand.Intn(9000-3000)+3000) 25 | } 26 | 27 | func Test_IsChild(t *testing.T) { 28 | // This test can't run parallel as it modifies os.Args. 29 | 30 | v := IsChild() 31 | if v { 32 | t.Errorf("IsChild() == %v, want %v", v, false) 33 | } 34 | 35 | setUp() 36 | defer tearDown() 37 | 38 | v = IsChild() 39 | if !v { 40 | t.Errorf("IsChild() == %v, want %v", v, true) 41 | } 42 | } 43 | 44 | func Test_New(t *testing.T) { 45 | t.Parallel() 46 | 47 | s := &fasthttp.Server{} 48 | p := New(s) 49 | 50 | if p.Network != defaultNetwork { 51 | t.Errorf("Prefork.Network == %q, want %q", p.Network, defaultNetwork) 52 | } 53 | 54 | if reflect.ValueOf(p.ServeFunc).Pointer() != reflect.ValueOf(s.Serve).Pointer() { 55 | t.Errorf("Prefork.ServeFunc == %p, want %p", p.ServeFunc, s.Serve) 56 | } 57 | 58 | if reflect.ValueOf(p.ServeTLSFunc).Pointer() != reflect.ValueOf(s.ServeTLS).Pointer() { 59 | t.Errorf("Prefork.ServeTLSFunc == %p, want %p", p.ServeTLSFunc, s.ServeTLS) 60 | } 61 | 62 | if reflect.ValueOf(p.ServeTLSEmbedFunc).Pointer() != reflect.ValueOf(s.ServeTLSEmbed).Pointer() { 63 | t.Errorf("Prefork.ServeTLSFunc == %p, want %p", p.ServeTLSEmbedFunc, s.ServeTLSEmbed) 64 | } 65 | } 66 | 67 | func Test_listen(t *testing.T) { 68 | t.Parallel() 69 | 70 | p := &Prefork{ 71 | Reuseport: true, 72 | } 73 | addr := getAddr() 74 | 75 | ln, err := p.listen(addr) 76 | if err != nil { 77 | t.Fatalf("Unexpected error: %v", err) 78 | } 79 | 80 | ln.Close() 81 | 82 | lnAddr := ln.Addr().String() 83 | if lnAddr != addr { 84 | t.Errorf("Prefork.Addr == %q, want %q", lnAddr, addr) 85 | } 86 | 87 | if p.Network != defaultNetwork { 88 | t.Errorf("Prefork.Network == %q, want %q", p.Network, defaultNetwork) 89 | } 90 | 91 | procs := runtime.GOMAXPROCS(0) 92 | if procs != 1 { 93 | t.Errorf("GOMAXPROCS == %d, want %d", procs, 1) 94 | } 95 | } 96 | 97 | func Test_setTCPListenerFiles(t *testing.T) { 98 | t.Parallel() 99 | 100 | if runtime.GOOS == "windows" { 101 | t.SkipNow() 102 | } 103 | 104 | p := &Prefork{} 105 | addr := getAddr() 106 | 107 | err := p.setTCPListenerFiles(addr) 108 | if err != nil { 109 | t.Fatalf("Unexpected error: %v", err) 110 | } 111 | 112 | if p.ln == nil { 113 | t.Fatal("Prefork.ln is nil") 114 | } 115 | 116 | p.ln.Close() 117 | 118 | lnAddr := p.ln.Addr().String() 119 | if lnAddr != addr { 120 | t.Errorf("Prefork.Addr == %q, want %q", lnAddr, addr) 121 | } 122 | 123 | if p.Network != defaultNetwork { 124 | t.Errorf("Prefork.Network == %q, want %q", p.Network, defaultNetwork) 125 | } 126 | 127 | if len(p.files) != 1 { 128 | t.Errorf("Prefork.files == %d, want %d", len(p.files), 1) 129 | } 130 | } 131 | 132 | func Test_ListenAndServe(t *testing.T) { 133 | // This test can't run parallel as it modifies os.Args. 134 | 135 | setUp() 136 | defer tearDown() 137 | 138 | s := &fasthttp.Server{} 139 | p := New(s) 140 | p.Reuseport = true 141 | p.ServeFunc = func(ln net.Listener) error { 142 | return nil 143 | } 144 | 145 | addr := getAddr() 146 | 147 | err := p.ListenAndServe(addr) 148 | if err != nil { 149 | t.Errorf("Unexpected error: %v", err) 150 | } 151 | 152 | p.ln.Close() 153 | 154 | lnAddr := p.ln.Addr().String() 155 | if lnAddr != addr { 156 | t.Errorf("Prefork.Addr == %q, want %q", lnAddr, addr) 157 | } 158 | 159 | if p.ln == nil { 160 | t.Error("Prefork.ln is nil") 161 | } 162 | } 163 | 164 | func Test_ListenAndServeTLS(t *testing.T) { 165 | // This test can't run parallel as it modifies os.Args. 166 | 167 | setUp() 168 | defer tearDown() 169 | 170 | s := &fasthttp.Server{} 171 | p := New(s) 172 | p.Reuseport = true 173 | p.ServeTLSFunc = func(ln net.Listener, certFile, keyFile string) error { 174 | return nil 175 | } 176 | 177 | addr := getAddr() 178 | 179 | err := p.ListenAndServeTLS(addr, "./key", "./cert") 180 | if err != nil { 181 | t.Errorf("Unexpected error: %v", err) 182 | } 183 | 184 | p.ln.Close() 185 | 186 | lnAddr := p.ln.Addr().String() 187 | if lnAddr != addr { 188 | t.Errorf("Prefork.Addr == %q, want %q", lnAddr, addr) 189 | } 190 | 191 | if p.ln == nil { 192 | t.Error("Prefork.ln is nil") 193 | } 194 | } 195 | 196 | func Test_ListenAndServeTLSEmbed(t *testing.T) { 197 | // This test can't run parallel as it modifies os.Args. 198 | 199 | setUp() 200 | defer tearDown() 201 | 202 | s := &fasthttp.Server{} 203 | p := New(s) 204 | p.Reuseport = true 205 | p.ServeTLSEmbedFunc = func(ln net.Listener, certData, keyData []byte) error { 206 | return nil 207 | } 208 | 209 | addr := getAddr() 210 | 211 | err := p.ListenAndServeTLSEmbed(addr, []byte("key"), []byte("cert")) 212 | if err != nil { 213 | t.Errorf("Unexpected error: %v", err) 214 | } 215 | 216 | p.ln.Close() 217 | 218 | lnAddr := p.ln.Addr().String() 219 | if lnAddr != addr { 220 | t.Errorf("Prefork.Addr == %q, want %q", lnAddr, addr) 221 | } 222 | 223 | if p.ln == nil { 224 | t.Error("Prefork.ln is nil") 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /requestctx_setbodystreamwriter_example_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp_test 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/valyala/fasthttp" 10 | ) 11 | 12 | func ExampleRequestCtx_SetBodyStreamWriter() { 13 | // Start fasthttp server for streaming responses. 14 | if err := fasthttp.ListenAndServe(":8080", responseStreamHandler); err != nil { 15 | log.Fatalf("unexpected error in server: %v", err) 16 | } 17 | } 18 | 19 | func responseStreamHandler(ctx *fasthttp.RequestCtx) { 20 | // Send the response in chunks and wait for a second between each chunk. 21 | ctx.SetBodyStreamWriter(func(w *bufio.Writer) { 22 | for i := 0; i < 10; i++ { 23 | fmt.Fprintf(w, "this is a message number %d", i) 24 | 25 | // Do not forget flushing streamed data to the client. 26 | if err := w.Flush(); err != nil { 27 | return 28 | } 29 | time.Sleep(time.Second) 30 | } 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /reuseport/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Max Riveiro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /reuseport/reuseport.go: -------------------------------------------------------------------------------- 1 | //go:build !windows && !aix 2 | 3 | // Package reuseport provides TCP net.Listener with SO_REUSEPORT support. 4 | // 5 | // SO_REUSEPORT allows linear scaling server performance on multi-CPU servers. 6 | // See https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/ for more details :) 7 | // 8 | // The package is based on https://github.com/kavu/go_reuseport . 9 | package reuseport 10 | 11 | import ( 12 | "net" 13 | "strings" 14 | 15 | "github.com/valyala/fasthttp/tcplisten" 16 | ) 17 | 18 | // Listen returns TCP listener with SO_REUSEPORT option set. 19 | // 20 | // The returned listener tries enabling the following TCP options, which usually 21 | // have positive impact on performance: 22 | // 23 | // - TCP_DEFER_ACCEPT. This option expects that the server reads from accepted 24 | // connections before writing to them. 25 | // 26 | // - TCP_FASTOPEN. See https://lwn.net/Articles/508865/ for details. 27 | // 28 | // Only tcp4 and tcp6 networks are supported. 29 | // 30 | // ErrNoReusePort error is returned if the system doesn't support SO_REUSEPORT. 31 | func Listen(network, addr string) (net.Listener, error) { 32 | ln, err := cfg.NewListener(network, addr) 33 | if err != nil && strings.Contains(err.Error(), "SO_REUSEPORT") { 34 | return nil, &ErrNoReusePort{err: err} 35 | } 36 | return ln, err 37 | } 38 | 39 | var cfg = &tcplisten.Config{ 40 | ReusePort: true, 41 | DeferAccept: true, 42 | FastOpen: true, 43 | } 44 | -------------------------------------------------------------------------------- /reuseport/reuseport_aix.go: -------------------------------------------------------------------------------- 1 | package reuseport 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "syscall" 7 | 8 | "golang.org/x/sys/unix" 9 | ) 10 | 11 | var listenConfig = net.ListenConfig{ 12 | Control: func(network, address string, c syscall.RawConn) (err error) { 13 | return c.Control(func(fd uintptr) { 14 | err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) 15 | if err == nil { 16 | err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) 17 | } 18 | }) 19 | }, 20 | } 21 | 22 | // Listen returns a TCP listener with the SO_REUSEADDR and SO_REUSEPORT options set. 23 | func Listen(network, addr string) (net.Listener, error) { 24 | return listenConfig.Listen(context.Background(), network, addr) 25 | } 26 | -------------------------------------------------------------------------------- /reuseport/reuseport_error.go: -------------------------------------------------------------------------------- 1 | package reuseport 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // ErrNoReusePort is returned if the OS doesn't support SO_REUSEPORT. 8 | type ErrNoReusePort struct { 9 | err error 10 | } 11 | 12 | // Error implements error interface. 13 | func (e *ErrNoReusePort) Error() string { 14 | return fmt.Sprintf("The OS doesn't support SO_REUSEPORT: %v", e.err) 15 | } 16 | -------------------------------------------------------------------------------- /reuseport/reuseport_example_test.go: -------------------------------------------------------------------------------- 1 | package reuseport_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/valyala/fasthttp" 8 | "github.com/valyala/fasthttp/reuseport" 9 | ) 10 | 11 | func ExampleListen() { 12 | ln, err := reuseport.Listen("tcp4", "localhost:12345") 13 | if err != nil { 14 | log.Fatalf("error in reuseport listener: %v", err) 15 | } 16 | 17 | if err = fasthttp.Serve(ln, requestHandler); err != nil { 18 | log.Fatalf("error in fasthttp Server: %v", err) 19 | } 20 | } 21 | 22 | func requestHandler(ctx *fasthttp.RequestCtx) { 23 | fmt.Fprintf(ctx, "Hello, world!") 24 | } 25 | -------------------------------------------------------------------------------- /reuseport/reuseport_test.go: -------------------------------------------------------------------------------- 1 | package reuseport 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | ) 7 | 8 | func TestTCP4(t *testing.T) { 9 | t.Parallel() 10 | 11 | testNewListener(t, "tcp4", "localhost:10081") 12 | } 13 | 14 | func TestTCP6(t *testing.T) { 15 | t.Parallel() 16 | 17 | // Run this test only if tcp6 interface exists. 18 | if hasLocalIPv6(t) { 19 | testNewListener(t, "tcp6", "[::1]:10082") 20 | } 21 | } 22 | 23 | func hasLocalIPv6(t *testing.T) bool { 24 | addrs, err := net.InterfaceAddrs() 25 | if err != nil { 26 | t.Fatalf("cannot obtain local interfaces: %v", err) 27 | } 28 | for _, a := range addrs { 29 | if a.String() == "::1/128" { 30 | return true 31 | } 32 | } 33 | return false 34 | } 35 | 36 | func testNewListener(t *testing.T, network, addr string) { 37 | ln1, err := Listen(network, addr) 38 | if err != nil { 39 | t.Fatalf("cannot create listener %v", err) 40 | } 41 | 42 | ln2, err := Listen(network, addr) 43 | if err != nil { 44 | t.Fatalf("cannot create listener %v", err) 45 | } 46 | 47 | _ = ln1.Close() 48 | _ = ln2.Close() 49 | } 50 | -------------------------------------------------------------------------------- /reuseport/reuseport_windows.go: -------------------------------------------------------------------------------- 1 | package reuseport 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "syscall" 7 | 8 | "golang.org/x/sys/windows" 9 | ) 10 | 11 | var listenConfig = net.ListenConfig{ 12 | Control: func(network, address string, c syscall.RawConn) (err error) { 13 | return c.Control(func(fd uintptr) { 14 | err = windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1) 15 | }) 16 | }, 17 | } 18 | 19 | // Listen returns TCP listener with SO_REUSEADDR option set, SO_REUSEPORT is not supported on Windows, so it uses 20 | // SO_REUSEADDR as an alternative to achieve the same effect. 21 | func Listen(network, addr string) (net.Listener, error) { 22 | return listenConfig.Listen(context.Background(), network, addr) 23 | } 24 | -------------------------------------------------------------------------------- /round2_32.go: -------------------------------------------------------------------------------- 1 | //go:build !amd64 && !arm64 && !ppc64 && !ppc64le && !riscv64 && !s390x 2 | 3 | package fasthttp 4 | 5 | import "math" 6 | 7 | func roundUpForSliceCap(n int) int { 8 | if n <= 0 { 9 | return 0 10 | } 11 | 12 | // Above 100MB, we don't round up as the overhead is too large. 13 | if n > 100*1024*1024 { 14 | return n 15 | } 16 | 17 | x := uint32(n - 1) 18 | x |= x >> 1 19 | x |= x >> 2 20 | x |= x >> 4 21 | x |= x >> 8 22 | x |= x >> 16 23 | 24 | // Make sure we don't return 0 due to overflow, even on 32 bit systems 25 | if x >= uint32(math.MaxInt32) { 26 | return math.MaxInt32 27 | } 28 | 29 | return int(x + 1) 30 | } 31 | -------------------------------------------------------------------------------- /round2_32_test.go: -------------------------------------------------------------------------------- 1 | //go:build !amd64 && !arm64 && !ppc64 && !ppc64le && !riscv64 && !s390x 2 | 3 | package fasthttp 4 | 5 | import ( 6 | "math" 7 | "testing" 8 | ) 9 | 10 | func TestRound2ForSliceCap(t *testing.T) { 11 | t.Parallel() 12 | 13 | testRound2ForSliceCap(t, 0, 0) 14 | testRound2ForSliceCap(t, 1, 1) 15 | testRound2ForSliceCap(t, 2, 2) 16 | testRound2ForSliceCap(t, 3, 4) 17 | testRound2ForSliceCap(t, 4, 4) 18 | testRound2ForSliceCap(t, 5, 8) 19 | testRound2ForSliceCap(t, 7, 8) 20 | testRound2ForSliceCap(t, 8, 8) 21 | testRound2ForSliceCap(t, 9, 16) 22 | testRound2ForSliceCap(t, 0x10001, 0x20000) 23 | 24 | testRound2ForSliceCap(t, math.MaxInt32-1, math.MaxInt32-1) 25 | } 26 | 27 | func testRound2ForSliceCap(t *testing.T, n, expectedRound2 int) { 28 | if roundUpForSliceCap(n) != expectedRound2 { 29 | t.Fatalf("Unexpected round2(%d)=%d. Expected %d", n, roundUpForSliceCap(n), expectedRound2) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /round2_64.go: -------------------------------------------------------------------------------- 1 | //go:build amd64 || arm64 || ppc64 || ppc64le || riscv64 || s390x 2 | 3 | package fasthttp 4 | 5 | func roundUpForSliceCap(n int) int { 6 | if n <= 0 { 7 | return 0 8 | } 9 | 10 | // Above 100MB, we don't round up as the overhead is too large. 11 | if n > 100*1024*1024 { 12 | return n 13 | } 14 | 15 | x := uint64(n - 1) // #nosec G115 16 | x |= x >> 1 17 | x |= x >> 2 18 | x |= x >> 4 19 | x |= x >> 8 20 | x |= x >> 16 21 | 22 | return int(x + 1) // #nosec G115 23 | } 24 | -------------------------------------------------------------------------------- /round2_64_test.go: -------------------------------------------------------------------------------- 1 | //go:build amd64 || arm64 || ppc64 || ppc64le || riscv64 || s390x 2 | 3 | package fasthttp 4 | 5 | import ( 6 | "math" 7 | "testing" 8 | ) 9 | 10 | func TestRound2ForSliceCap(t *testing.T) { 11 | t.Parallel() 12 | 13 | testRound2ForSliceCap(t, 0, 0) 14 | testRound2ForSliceCap(t, 1, 1) 15 | testRound2ForSliceCap(t, 2, 2) 16 | testRound2ForSliceCap(t, 3, 4) 17 | testRound2ForSliceCap(t, 4, 4) 18 | testRound2ForSliceCap(t, 5, 8) 19 | testRound2ForSliceCap(t, 7, 8) 20 | testRound2ForSliceCap(t, 8, 8) 21 | testRound2ForSliceCap(t, 9, 16) 22 | testRound2ForSliceCap(t, 0x10001, 0x20000) 23 | 24 | testRound2ForSliceCap(t, math.MaxInt32, math.MaxInt32) 25 | testRound2ForSliceCap(t, math.MaxInt64-1, math.MaxInt64-1) 26 | } 27 | 28 | func testRound2ForSliceCap(t *testing.T, n, expectedRound2 int) { 29 | if roundUpForSliceCap(n) != expectedRound2 { 30 | t.Fatalf("Unexpected round2(%d)=%d. Expected %d", n, roundUpForSliceCap(n), expectedRound2) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /s2b_new.go: -------------------------------------------------------------------------------- 1 | //go:build go1.20 2 | 3 | package fasthttp 4 | 5 | import "unsafe" 6 | 7 | // s2b converts string to a byte slice without memory allocation. 8 | func s2b(s string) []byte { 9 | return unsafe.Slice(unsafe.StringData(s), len(s)) 10 | } 11 | -------------------------------------------------------------------------------- /s2b_old.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.20 2 | 3 | package fasthttp 4 | 5 | import ( 6 | "reflect" 7 | "unsafe" 8 | ) 9 | 10 | // s2b converts string to a byte slice without memory allocation. 11 | // 12 | // Note it may break if string and/or slice header will change 13 | // in the future go versions. 14 | func s2b(s string) (b []byte) { 15 | bh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) 16 | sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) 17 | bh.Data = sh.Data 18 | bh.Cap = sh.Len 19 | bh.Len = sh.Len 20 | return b 21 | } 22 | -------------------------------------------------------------------------------- /server_race_test.go: -------------------------------------------------------------------------------- 1 | //go:build race 2 | 3 | package fasthttp 4 | 5 | import ( 6 | "context" 7 | "github.com/valyala/fasthttp/fasthttputil" 8 | "math" 9 | "testing" 10 | ) 11 | 12 | func TestServerDoneRace(t *testing.T) { 13 | t.Parallel() 14 | 15 | s := &Server{ 16 | Handler: func(ctx *RequestCtx) { 17 | for i := 0; i < math.MaxInt; i++ { 18 | ctx.Done() 19 | } 20 | }, 21 | } 22 | 23 | ln := fasthttputil.NewInmemoryListener() 24 | defer ln.Close() 25 | 26 | go func() { 27 | if err := s.Serve(ln); err != nil { 28 | t.Errorf("unexpected error: %v", err) 29 | } 30 | }() 31 | 32 | c, err := ln.Dial() 33 | if err != nil { 34 | t.Fatalf("unexpected error: %v", err) 35 | } 36 | defer c.Close() 37 | if _, err = c.Write([]byte("POST / HTTP/1.1\r\nHost: go.dev\r\nContent-Length: 3\r\n\r\nABC" + 38 | "\r\n\r\n" + // <-- this stuff is bogus, but we'll ignore it 39 | "GET / HTTP/1.1\r\nHost: go.dev\r\n\r\n")); err != nil { 40 | t.Fatal(err) 41 | } 42 | ctx, cancelFunc := context.WithCancel(context.Background()) 43 | cancelFunc() 44 | 45 | s.ShutdownWithContext(ctx) 46 | } 47 | -------------------------------------------------------------------------------- /stackless/doc.go: -------------------------------------------------------------------------------- 1 | // Package stackless provides functionality that may save stack space 2 | // for high number of concurrently running goroutines. 3 | package stackless 4 | -------------------------------------------------------------------------------- /stackless/func.go: -------------------------------------------------------------------------------- 1 | package stackless 2 | 3 | import ( 4 | "runtime" 5 | "sync" 6 | ) 7 | 8 | // NewFunc returns stackless wrapper for the function f. 9 | // 10 | // Unlike f, the returned stackless wrapper doesn't use stack space 11 | // on the goroutine that calls it. 12 | // The wrapper may save a lot of stack space if the following conditions 13 | // are met: 14 | // 15 | // - f doesn't contain blocking calls on network, I/O or channels; 16 | // - f uses a lot of stack space; 17 | // - the wrapper is called from high number of concurrent goroutines. 18 | // 19 | // The stackless wrapper returns false if the call cannot be processed 20 | // at the moment due to high load. 21 | func NewFunc(f func(ctx any)) func(ctx any) bool { 22 | if f == nil { 23 | // developer sanity-check 24 | panic("BUG: f cannot be nil") 25 | } 26 | 27 | funcWorkCh := make(chan *funcWork, runtime.GOMAXPROCS(-1)*2048) 28 | onceInit := func() { 29 | n := runtime.GOMAXPROCS(-1) 30 | for i := 0; i < n; i++ { 31 | go funcWorker(funcWorkCh, f) 32 | } 33 | } 34 | var once sync.Once 35 | 36 | return func(ctx any) bool { 37 | once.Do(onceInit) 38 | fw := getFuncWork() 39 | fw.ctx = ctx 40 | 41 | select { 42 | case funcWorkCh <- fw: 43 | default: 44 | putFuncWork(fw) 45 | return false 46 | } 47 | <-fw.done 48 | putFuncWork(fw) 49 | return true 50 | } 51 | } 52 | 53 | func funcWorker(funcWorkCh <-chan *funcWork, f func(ctx any)) { 54 | for fw := range funcWorkCh { 55 | f(fw.ctx) 56 | fw.done <- struct{}{} 57 | } 58 | } 59 | 60 | func getFuncWork() *funcWork { 61 | v := funcWorkPool.Get() 62 | if v == nil { 63 | v = &funcWork{ 64 | done: make(chan struct{}, 1), 65 | } 66 | } 67 | return v.(*funcWork) 68 | } 69 | 70 | func putFuncWork(fw *funcWork) { 71 | fw.ctx = nil 72 | funcWorkPool.Put(fw) 73 | } 74 | 75 | var funcWorkPool sync.Pool 76 | 77 | type funcWork struct { 78 | ctx any 79 | done chan struct{} 80 | } 81 | -------------------------------------------------------------------------------- /stackless/func_test.go: -------------------------------------------------------------------------------- 1 | package stackless 2 | 3 | import ( 4 | "errors" 5 | "sync/atomic" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestNewFuncSimple(t *testing.T) { 11 | t.Parallel() 12 | 13 | var n uint64 14 | f := NewFunc(func(ctx any) { 15 | atomic.AddUint64(&n, uint64(ctx.(int))) 16 | }) 17 | 18 | iterations := 4 * 1024 19 | for i := 0; i < iterations; i++ { 20 | if !f(2) { 21 | t.Fatalf("f mustn't return false") 22 | } 23 | } 24 | if n != uint64(2*iterations) { 25 | t.Fatalf("Unexpected n: %d. Expecting %d", n, 2*iterations) 26 | } 27 | } 28 | 29 | func TestNewFuncMulti(t *testing.T) { 30 | t.Parallel() 31 | 32 | var n1, n2 uint64 33 | f1 := NewFunc(func(ctx any) { 34 | atomic.AddUint64(&n1, uint64(ctx.(int))) 35 | }) 36 | f2 := NewFunc(func(ctx any) { 37 | atomic.AddUint64(&n2, uint64(ctx.(int))) 38 | }) 39 | 40 | iterations := 4 * 1024 41 | 42 | f1Done := make(chan error, 1) 43 | go func() { 44 | var err error 45 | for i := 0; i < iterations; i++ { 46 | if !f1(3) { 47 | err = errors.New("f1 mustn't return false") 48 | break 49 | } 50 | } 51 | f1Done <- err 52 | }() 53 | 54 | f2Done := make(chan error, 1) 55 | go func() { 56 | var err error 57 | for i := 0; i < iterations; i++ { 58 | if !f2(5) { 59 | err = errors.New("f2 mustn't return false") 60 | break 61 | } 62 | } 63 | f2Done <- err 64 | }() 65 | 66 | select { 67 | case err := <-f1Done: 68 | if err != nil { 69 | t.Fatalf("unexpected error: %v", err) 70 | } 71 | case <-time.After(time.Second): 72 | t.Fatalf("timeout") 73 | } 74 | 75 | select { 76 | case err := <-f2Done: 77 | if err != nil { 78 | t.Fatalf("unexpected error: %v", err) 79 | } 80 | case <-time.After(time.Second): 81 | t.Fatalf("timeout") 82 | } 83 | 84 | if n1 != uint64(3*iterations) { 85 | t.Fatalf("unexpected n1: %d. Expecting %d", n1, 3*iterations) 86 | } 87 | if n2 != uint64(5*iterations) { 88 | t.Fatalf("unexpected n2: %d. Expecting %d", n2, 5*iterations) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /stackless/func_timing_test.go: -------------------------------------------------------------------------------- 1 | package stackless 2 | 3 | import ( 4 | "sync/atomic" 5 | "testing" 6 | ) 7 | 8 | func BenchmarkFuncOverhead(b *testing.B) { 9 | var n uint64 10 | f := NewFunc(func(ctx any) { 11 | atomic.AddUint64(&n, *(ctx.(*uint64))) 12 | }) 13 | b.RunParallel(func(pb *testing.PB) { 14 | x := uint64(1) 15 | for pb.Next() { 16 | if !f(&x) { 17 | b.Fatalf("f mustn't return false") 18 | } 19 | } 20 | }) 21 | if n != uint64(b.N) { 22 | b.Fatalf("unexpected n: %d. Expecting %d", n, b.N) 23 | } 24 | } 25 | 26 | func BenchmarkFuncPure(b *testing.B) { 27 | var n uint64 28 | f := func(x *uint64) { 29 | atomic.AddUint64(&n, *x) 30 | } 31 | b.RunParallel(func(pb *testing.PB) { 32 | x := uint64(1) 33 | for pb.Next() { 34 | f(&x) 35 | } 36 | }) 37 | if n != uint64(b.N) { 38 | b.Fatalf("unexpected n: %d. Expecting %d", n, b.N) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /stackless/writer.go: -------------------------------------------------------------------------------- 1 | package stackless 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "sync" 8 | 9 | "github.com/valyala/bytebufferpool" 10 | ) 11 | 12 | // Writer is an interface stackless writer must conform to. 13 | // 14 | // The interface contains common subset for Writers from compress/* packages. 15 | type Writer interface { 16 | Write(p []byte) (int, error) 17 | Flush() error 18 | Close() error 19 | Reset(w io.Writer) 20 | } 21 | 22 | // NewWriterFunc must return new writer that will be wrapped into 23 | // stackless writer. 24 | type NewWriterFunc func(w io.Writer) Writer 25 | 26 | // NewWriter creates a stackless writer around a writer returned 27 | // from newWriter. 28 | // 29 | // The returned writer writes data to dstW. 30 | // 31 | // Writers that use a lot of stack space may be wrapped into stackless writer, 32 | // thus saving stack space for high number of concurrently running goroutines. 33 | func NewWriter(dstW io.Writer, newWriter NewWriterFunc) Writer { 34 | w := &writer{ 35 | dstW: dstW, 36 | } 37 | w.zw = newWriter(&w.xw) 38 | return w 39 | } 40 | 41 | type writer struct { 42 | dstW io.Writer 43 | zw Writer 44 | 45 | err error 46 | xw xWriter 47 | 48 | p []byte 49 | n int 50 | 51 | op op 52 | } 53 | 54 | type op int 55 | 56 | const ( 57 | opWrite op = iota 58 | opFlush 59 | opClose 60 | opReset 61 | ) 62 | 63 | func (w *writer) Write(p []byte) (int, error) { 64 | w.p = p 65 | err := w.do(opWrite) 66 | w.p = nil 67 | return w.n, err 68 | } 69 | 70 | func (w *writer) Flush() error { 71 | return w.do(opFlush) 72 | } 73 | 74 | func (w *writer) Close() error { 75 | return w.do(opClose) 76 | } 77 | 78 | func (w *writer) Reset(dstW io.Writer) { 79 | w.xw.Reset() 80 | w.do(opReset) //nolint:errcheck 81 | w.dstW = dstW 82 | } 83 | 84 | func (w *writer) do(op op) error { 85 | w.op = op 86 | if !stacklessWriterFunc(w) { 87 | return errHighLoad 88 | } 89 | err := w.err 90 | if err != nil { 91 | return err 92 | } 93 | if w.xw.bb != nil && len(w.xw.bb.B) > 0 { 94 | _, err = w.dstW.Write(w.xw.bb.B) 95 | } 96 | w.xw.Reset() 97 | 98 | return err 99 | } 100 | 101 | var errHighLoad = errors.New("cannot compress data due to high load") 102 | 103 | var ( 104 | stacklessWriterFuncOnce sync.Once 105 | stacklessWriterFuncFunc func(ctx any) bool 106 | ) 107 | 108 | func stacklessWriterFunc(ctx any) bool { 109 | stacklessWriterFuncOnce.Do(func() { 110 | stacklessWriterFuncFunc = NewFunc(writerFunc) 111 | }) 112 | return stacklessWriterFuncFunc(ctx) 113 | } 114 | 115 | func writerFunc(ctx any) { 116 | w := ctx.(*writer) 117 | switch w.op { 118 | case opWrite: 119 | w.n, w.err = w.zw.Write(w.p) 120 | case opFlush: 121 | w.err = w.zw.Flush() 122 | case opClose: 123 | w.err = w.zw.Close() 124 | case opReset: 125 | w.zw.Reset(&w.xw) 126 | w.err = nil 127 | default: 128 | panic(fmt.Sprintf("BUG: unexpected op: %d", w.op)) 129 | } 130 | } 131 | 132 | type xWriter struct { 133 | bb *bytebufferpool.ByteBuffer 134 | } 135 | 136 | func (w *xWriter) Write(p []byte) (int, error) { 137 | if w.bb == nil { 138 | w.bb = bufferPool.Get() 139 | } 140 | return w.bb.Write(p) 141 | } 142 | 143 | func (w *xWriter) Reset() { 144 | if w.bb != nil { 145 | bufferPool.Put(w.bb) 146 | w.bb = nil 147 | } 148 | } 149 | 150 | var bufferPool bytebufferpool.Pool 151 | -------------------------------------------------------------------------------- /stackless/writer_test.go: -------------------------------------------------------------------------------- 1 | package stackless 2 | 3 | import ( 4 | "bytes" 5 | "compress/flate" 6 | "compress/gzip" 7 | "fmt" 8 | "io" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestCompressFlateSerial(t *testing.T) { 14 | t.Parallel() 15 | 16 | if err := testCompressFlate(); err != nil { 17 | t.Fatalf("unexpected error: %v", err) 18 | } 19 | } 20 | 21 | func TestCompressFlateConcurrent(t *testing.T) { 22 | t.Parallel() 23 | 24 | if err := testConcurrent(testCompressFlate, 10); err != nil { 25 | t.Fatalf("unexpected error: %v", err) 26 | } 27 | } 28 | 29 | func testCompressFlate() error { 30 | return testWriter(func(w io.Writer) Writer { 31 | zw, err := flate.NewWriter(w, flate.DefaultCompression) 32 | if err != nil { 33 | panic(fmt.Sprintf("BUG: unexpected error: %v", err)) 34 | } 35 | return zw 36 | }, func(r io.Reader) io.Reader { 37 | return flate.NewReader(r) 38 | }) 39 | } 40 | 41 | func TestCompressGzipSerial(t *testing.T) { 42 | t.Parallel() 43 | 44 | if err := testCompressGzip(); err != nil { 45 | t.Fatalf("unexpected error: %v", err) 46 | } 47 | } 48 | 49 | func TestCompressGzipConcurrent(t *testing.T) { 50 | t.Parallel() 51 | 52 | if err := testConcurrent(testCompressGzip, 10); err != nil { 53 | t.Fatalf("unexpected error: %v", err) 54 | } 55 | } 56 | 57 | func testCompressGzip() error { 58 | return testWriter(func(w io.Writer) Writer { 59 | return gzip.NewWriter(w) 60 | }, func(r io.Reader) io.Reader { 61 | zr, err := gzip.NewReader(r) 62 | if err != nil { 63 | panic(fmt.Sprintf("BUG: cannot create gzip reader: %v", err)) 64 | } 65 | return zr 66 | }) 67 | } 68 | 69 | func testWriter(newWriter NewWriterFunc, newReader func(io.Reader) io.Reader) error { 70 | dstW := &bytes.Buffer{} 71 | w := NewWriter(dstW, newWriter) 72 | 73 | for i := 0; i < 5; i++ { 74 | if err := testWriterReuse(w, dstW, newReader); err != nil { 75 | return fmt.Errorf("unexpected error when re-using writer on iteration %d: %w", i, err) 76 | } 77 | dstW = &bytes.Buffer{} 78 | w.Reset(dstW) 79 | } 80 | 81 | return nil 82 | } 83 | 84 | func testWriterReuse(w Writer, r io.Reader, newReader func(io.Reader) io.Reader) error { 85 | wantW := &bytes.Buffer{} 86 | mw := io.MultiWriter(w, wantW) 87 | for i := 0; i < 30; i++ { 88 | fmt.Fprintf(mw, "foobar %d\n", i) 89 | if i%13 == 0 { 90 | if err := w.Flush(); err != nil { 91 | return fmt.Errorf("error on flush: %w", err) 92 | } 93 | } 94 | } 95 | w.Close() 96 | 97 | zr := newReader(r) 98 | data, err := io.ReadAll(zr) 99 | if err != nil { 100 | return fmt.Errorf("unexpected error: %w, data=%q", err, data) 101 | } 102 | 103 | wantData := wantW.Bytes() 104 | if !bytes.Equal(data, wantData) { 105 | return fmt.Errorf("unexpected data: %q. Expecting %q", data, wantData) 106 | } 107 | 108 | return nil 109 | } 110 | 111 | func testConcurrent(testFunc func() error, concurrency int) error { 112 | ch := make(chan error, concurrency) 113 | for i := 0; i < concurrency; i++ { 114 | go func() { 115 | ch <- testFunc() 116 | }() 117 | } 118 | for i := 0; i < concurrency; i++ { 119 | select { 120 | case err := <-ch: 121 | if err != nil { 122 | return fmt.Errorf("unexpected error on goroutine %d: %w", i, err) 123 | } 124 | case <-time.After(time.Second): 125 | return fmt.Errorf("timeout on goroutine %d", i) 126 | } 127 | } 128 | return nil 129 | } 130 | -------------------------------------------------------------------------------- /status_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestStatusLine(t *testing.T) { 9 | t.Parallel() 10 | 11 | testStatusLine(t, -1, []byte("HTTP/1.1 -1 Unknown Status Code\r\n")) 12 | testStatusLine(t, 99, []byte("HTTP/1.1 99 Unknown Status Code\r\n")) 13 | testStatusLine(t, 200, []byte("HTTP/1.1 200 OK\r\n")) 14 | testStatusLine(t, 512, []byte("HTTP/1.1 512 Unknown Status Code\r\n")) 15 | testStatusLine(t, 512, []byte("HTTP/1.1 512 Unknown Status Code\r\n")) 16 | testStatusLine(t, 520, []byte("HTTP/1.1 520 Unknown Status Code\r\n")) 17 | } 18 | 19 | func testStatusLine(t *testing.T, statusCode int, expected []byte) { 20 | line := formatStatusLine(nil, strHTTP11, statusCode, s2b(StatusMessage(statusCode))) 21 | if !bytes.Equal(expected, line) { 22 | t.Fatalf("unexpected status line %q. Expecting %q", string(line), string(expected)) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /status_timing_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func BenchmarkStatusLine99(b *testing.B) { 9 | benchmarkStatusLine(b, 99, []byte("HTTP/1.1 99 Unknown Status Code\r\n")) 10 | } 11 | 12 | func BenchmarkStatusLine200(b *testing.B) { 13 | benchmarkStatusLine(b, 200, []byte("HTTP/1.1 200 OK\r\n")) 14 | } 15 | 16 | func BenchmarkStatusLine512(b *testing.B) { 17 | benchmarkStatusLine(b, 512, []byte("HTTP/1.1 512 Unknown Status Code\r\n")) 18 | } 19 | 20 | func benchmarkStatusLine(b *testing.B, statusCode int, expected []byte) { 21 | b.RunParallel(func(pb *testing.PB) { 22 | for pb.Next() { 23 | line := formatStatusLine(nil, strHTTP11, statusCode, s2b(StatusMessage(statusCode))) 24 | if !bytes.Equal(expected, line) { 25 | b.Fatalf("unexpected status line %q. Expecting %q", string(line), string(expected)) 26 | } 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /stream.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "sync" 7 | 8 | "github.com/valyala/fasthttp/fasthttputil" 9 | ) 10 | 11 | // StreamWriter must write data to w. 12 | // 13 | // Usually StreamWriter writes data to w in a loop (aka 'data streaming'). 14 | // 15 | // StreamWriter must return immediately if w returns error. 16 | // 17 | // Since the written data is buffered, do not forget calling w.Flush 18 | // when the data must be propagated to reader. 19 | type StreamWriter func(w *bufio.Writer) 20 | 21 | // NewStreamReader returns a reader, which replays all the data generated by sw. 22 | // 23 | // The returned reader may be passed to Response.SetBodyStream. 24 | // 25 | // Close must be called on the returned reader after all the required data 26 | // has been read. Otherwise goroutine leak may occur. 27 | // 28 | // See also Response.SetBodyStreamWriter. 29 | func NewStreamReader(sw StreamWriter) io.ReadCloser { 30 | pc := fasthttputil.NewPipeConns() 31 | pw := pc.Conn1() 32 | pr := pc.Conn2() 33 | 34 | var bw *bufio.Writer 35 | v := streamWriterBufPool.Get() 36 | if v == nil { 37 | bw = bufio.NewWriter(pw) 38 | } else { 39 | bw = v.(*bufio.Writer) 40 | bw.Reset(pw) 41 | } 42 | 43 | go func() { 44 | sw(bw) 45 | bw.Flush() 46 | pw.Close() 47 | 48 | streamWriterBufPool.Put(bw) 49 | }() 50 | 51 | return pr 52 | } 53 | 54 | var streamWriterBufPool sync.Pool 55 | -------------------------------------------------------------------------------- /stream_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestNewStreamReader(t *testing.T) { 13 | t.Parallel() 14 | 15 | ch := make(chan struct{}) 16 | r := NewStreamReader(func(w *bufio.Writer) { 17 | fmt.Fprintf(w, "Hello, world\n") 18 | fmt.Fprintf(w, "Line #2\n") 19 | close(ch) 20 | }) 21 | 22 | data, err := io.ReadAll(r) 23 | if err != nil { 24 | t.Fatalf("unexpected error: %v", err) 25 | } 26 | expectedData := "Hello, world\nLine #2\n" 27 | if string(data) != expectedData { 28 | t.Fatalf("unexpected data %q. Expecting %q", data, expectedData) 29 | } 30 | 31 | if err = r.Close(); err != nil { 32 | t.Fatalf("unexpected error") 33 | } 34 | 35 | select { 36 | case <-ch: 37 | case <-time.After(time.Second): 38 | t.Fatalf("timeout") 39 | } 40 | } 41 | 42 | func TestStreamReaderClose(t *testing.T) { 43 | t.Parallel() 44 | 45 | firstLine := "the first line must pass" 46 | ch := make(chan error, 1) 47 | r := NewStreamReader(func(w *bufio.Writer) { 48 | fmt.Fprintf(w, "%s", firstLine) 49 | if err := w.Flush(); err != nil { 50 | ch <- fmt.Errorf("unexpected error on first flush: %w", err) 51 | return 52 | } 53 | 54 | data := createFixedBody(4000) 55 | for i := 0; i < 100; i++ { 56 | w.Write(data) //nolint:errcheck 57 | } 58 | if err := w.Flush(); err == nil { 59 | ch <- errors.New("expecting error on the second flush") 60 | } 61 | ch <- nil 62 | }) 63 | 64 | buf := make([]byte, len(firstLine)) 65 | n, err := io.ReadFull(r, buf) 66 | if err != nil { 67 | t.Fatalf("unexpected error: %v", err) 68 | } 69 | if n != len(buf) { 70 | t.Fatalf("unexpected number of bytes read: %d. Expecting %d", n, len(buf)) 71 | } 72 | if string(buf) != firstLine { 73 | t.Fatalf("unexpected result: %q. Expecting %q", buf, firstLine) 74 | } 75 | 76 | if err := r.Close(); err != nil { 77 | t.Fatalf("unexpected error: %v", err) 78 | } 79 | 80 | select { 81 | case err := <-ch: 82 | if err != nil { 83 | t.Fatalf("error returned from stream reader: %v", err) 84 | } 85 | case <-time.After(time.Second): 86 | t.Fatalf("timeout when waiting for stream reader") 87 | } 88 | 89 | // read trailing data 90 | go func() { 91 | if _, err := io.ReadAll(r); err != nil { 92 | ch <- fmt.Errorf("unexpected error when reading trailing data: %w", err) 93 | return 94 | } 95 | ch <- nil 96 | }() 97 | 98 | select { 99 | case err := <-ch: 100 | if err != nil { 101 | t.Fatalf("error returned when reading tail data: %v", err) 102 | } 103 | case <-time.After(time.Second): 104 | t.Fatalf("timeout when reading tail data") 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /stream_timing_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func BenchmarkStreamReader1(b *testing.B) { 11 | benchmarkStreamReader(b, 1) 12 | } 13 | 14 | func BenchmarkStreamReader10(b *testing.B) { 15 | benchmarkStreamReader(b, 10) 16 | } 17 | 18 | func BenchmarkStreamReader100(b *testing.B) { 19 | benchmarkStreamReader(b, 100) 20 | } 21 | 22 | func BenchmarkStreamReader1K(b *testing.B) { 23 | benchmarkStreamReader(b, 1000) 24 | } 25 | 26 | func BenchmarkStreamReader10K(b *testing.B) { 27 | benchmarkStreamReader(b, 10000) 28 | } 29 | 30 | func benchmarkStreamReader(b *testing.B, size int) { 31 | src := createFixedBody(size) 32 | b.SetBytes(int64(size)) 33 | 34 | b.RunParallel(func(pb *testing.PB) { 35 | dst := make([]byte, size) 36 | ch := make(chan error, 1) 37 | sr := NewStreamReader(func(w *bufio.Writer) { 38 | for pb.Next() { 39 | if _, err := w.Write(src); err != nil { 40 | ch <- err 41 | return 42 | } 43 | if err := w.Flush(); err != nil { 44 | ch <- err 45 | return 46 | } 47 | } 48 | ch <- nil 49 | }) 50 | for { 51 | if _, err := sr.Read(dst); err != nil { 52 | if err == io.EOF { 53 | break 54 | } 55 | b.Fatalf("unexpected error when reading from stream reader: %v", err) 56 | } 57 | } 58 | if err := sr.Close(); err != nil { 59 | b.Fatalf("unexpected error when closing stream reader: %v", err) 60 | } 61 | select { 62 | case err := <-ch: 63 | if err != nil { 64 | b.Fatalf("unexpected error from stream reader: %v", err) 65 | } 66 | case <-time.After(time.Second): 67 | b.Fatalf("timeout") 68 | } 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /streaming.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "sync" 8 | 9 | "github.com/valyala/bytebufferpool" 10 | ) 11 | 12 | type headerInterface interface { 13 | ContentLength() int 14 | ReadTrailer(r *bufio.Reader) error 15 | } 16 | 17 | type requestStream struct { 18 | header headerInterface 19 | prefetchedBytes *bytes.Reader 20 | reader *bufio.Reader 21 | totalBytesRead int 22 | chunkLeft int 23 | } 24 | 25 | func (rs *requestStream) Read(p []byte) (int, error) { 26 | var ( 27 | n int 28 | err error 29 | ) 30 | if rs.header.ContentLength() == -1 { 31 | if rs.chunkLeft == 0 { 32 | chunkSize, err := parseChunkSize(rs.reader) 33 | if err != nil { 34 | return 0, err 35 | } 36 | if chunkSize == 0 { 37 | err = rs.header.ReadTrailer(rs.reader) 38 | if err != nil && err != io.EOF { 39 | return 0, err 40 | } 41 | return 0, io.EOF 42 | } 43 | rs.chunkLeft = chunkSize 44 | } 45 | bytesToRead := len(p) 46 | if rs.chunkLeft < len(p) { 47 | bytesToRead = rs.chunkLeft 48 | } 49 | n, err = rs.reader.Read(p[:bytesToRead]) 50 | rs.totalBytesRead += n 51 | rs.chunkLeft -= n 52 | if err == io.EOF { 53 | err = io.ErrUnexpectedEOF 54 | } 55 | if err == nil && rs.chunkLeft == 0 { 56 | err = readCrLf(rs.reader) 57 | } 58 | return n, err 59 | } 60 | if rs.totalBytesRead == rs.header.ContentLength() { 61 | return 0, io.EOF 62 | } 63 | prefetchedSize := int(rs.prefetchedBytes.Size()) 64 | if prefetchedSize > rs.totalBytesRead { 65 | left := prefetchedSize - rs.totalBytesRead 66 | if len(p) > left { 67 | p = p[:left] 68 | } 69 | n, err := rs.prefetchedBytes.Read(p) 70 | rs.totalBytesRead += n 71 | if n == rs.header.ContentLength() { 72 | return n, io.EOF 73 | } 74 | return n, err 75 | } 76 | left := rs.header.ContentLength() - rs.totalBytesRead 77 | if left > 0 && len(p) > left { 78 | p = p[:left] 79 | } 80 | n, err = rs.reader.Read(p) 81 | rs.totalBytesRead += n 82 | if err != nil { 83 | return n, err 84 | } 85 | 86 | if rs.totalBytesRead == rs.header.ContentLength() { 87 | err = io.EOF 88 | } 89 | return n, err 90 | } 91 | 92 | func acquireRequestStream(b *bytebufferpool.ByteBuffer, r *bufio.Reader, h headerInterface) *requestStream { 93 | rs := requestStreamPool.Get().(*requestStream) 94 | rs.prefetchedBytes = bytes.NewReader(b.B) 95 | rs.reader = r 96 | rs.header = h 97 | return rs 98 | } 99 | 100 | func releaseRequestStream(rs *requestStream) { 101 | rs.prefetchedBytes = nil 102 | rs.totalBytesRead = 0 103 | rs.chunkLeft = 0 104 | rs.reader = nil 105 | rs.header = nil 106 | requestStreamPool.Put(rs) 107 | } 108 | 109 | var requestStreamPool = sync.Pool{ 110 | New: func() any { 111 | return &requestStream{} 112 | }, 113 | } 114 | -------------------------------------------------------------------------------- /strings.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | var ( 4 | defaultServerName = "fasthttp" 5 | defaultUserAgent = "fasthttp" 6 | defaultContentType = []byte("text/plain; charset=utf-8") 7 | ) 8 | 9 | var ( 10 | strSlash = []byte("/") 11 | strSlashSlash = []byte("//") 12 | strSlashDotDot = []byte("/..") 13 | strSlashDotSlash = []byte("/./") 14 | strSlashDotDotSlash = []byte("/../") 15 | strBackSlashDotDot = []byte(`\..`) 16 | strBackSlashDotBackSlash = []byte(`\.\`) 17 | strSlashDotDotBackSlash = []byte(`/..\`) 18 | strBackSlashDotDotBackSlash = []byte(`\..\`) 19 | strCRLF = []byte("\r\n") 20 | strHTTP = []byte("http") 21 | strHTTPS = []byte("https") 22 | strHTTP11 = []byte("HTTP/1.1") 23 | strColon = []byte(":") 24 | strColonSlashSlash = []byte("://") 25 | strColonSpace = []byte(": ") 26 | strCommaSpace = []byte(", ") 27 | strGMT = []byte("GMT") 28 | strSpace = []byte(" ") 29 | 30 | strResponseContinue = []byte("HTTP/1.1 100 Continue\r\n\r\n") 31 | strEarlyHints = []byte("HTTP/1.1 103 Early Hints\r\n") 32 | 33 | strExpect = []byte(HeaderExpect) 34 | strConnection = []byte(HeaderConnection) 35 | strContentLength = []byte(HeaderContentLength) 36 | strContentType = []byte(HeaderContentType) 37 | strDate = []byte(HeaderDate) 38 | strHost = []byte(HeaderHost) 39 | strReferer = []byte(HeaderReferer) 40 | strServer = []byte(HeaderServer) 41 | strTransferEncoding = []byte(HeaderTransferEncoding) 42 | strContentEncoding = []byte(HeaderContentEncoding) 43 | strAcceptEncoding = []byte(HeaderAcceptEncoding) 44 | strUserAgent = []byte(HeaderUserAgent) 45 | strCookie = []byte(HeaderCookie) 46 | strSetCookie = []byte(HeaderSetCookie) 47 | strLocation = []byte(HeaderLocation) 48 | strIfModifiedSince = []byte(HeaderIfModifiedSince) 49 | strLastModified = []byte(HeaderLastModified) 50 | strAcceptRanges = []byte(HeaderAcceptRanges) 51 | strRange = []byte(HeaderRange) 52 | strContentRange = []byte(HeaderContentRange) 53 | strAuthorization = []byte(HeaderAuthorization) 54 | strTE = []byte(HeaderTE) 55 | strTrailer = []byte(HeaderTrailer) 56 | strMaxForwards = []byte(HeaderMaxForwards) 57 | strProxyConnection = []byte(HeaderProxyConnection) 58 | strProxyAuthenticate = []byte(HeaderProxyAuthenticate) 59 | strProxyAuthorization = []byte(HeaderProxyAuthorization) 60 | strWWWAuthenticate = []byte(HeaderWWWAuthenticate) 61 | strVary = []byte(HeaderVary) 62 | 63 | strCookieExpires = []byte("expires") 64 | strCookieDomain = []byte("domain") 65 | strCookiePath = []byte("path") 66 | strCookieHTTPOnly = []byte("HttpOnly") 67 | strCookieSecure = []byte("secure") 68 | strCookiePartitioned = []byte("Partitioned") 69 | strCookieMaxAge = []byte("max-age") 70 | strCookieSameSite = []byte("SameSite") 71 | strCookieSameSiteLax = []byte("Lax") 72 | strCookieSameSiteStrict = []byte("Strict") 73 | strCookieSameSiteNone = []byte("None") 74 | 75 | strClose = []byte("close") 76 | strGzip = []byte("gzip") 77 | strBr = []byte("br") 78 | strZstd = []byte("zstd") 79 | strDeflate = []byte("deflate") 80 | strKeepAlive = []byte("keep-alive") 81 | strUpgrade = []byte("Upgrade") 82 | strChunked = []byte("chunked") 83 | strIdentity = []byte("identity") 84 | str100Continue = []byte("100-continue") 85 | strPostArgsContentType = []byte("application/x-www-form-urlencoded") 86 | strDefaultContentType = []byte("application/octet-stream") 87 | strMultipartFormData = []byte("multipart/form-data") 88 | strBoundary = []byte("boundary") 89 | strBytes = []byte("bytes") 90 | strBasicSpace = []byte("Basic ") 91 | strLink = []byte("Link") 92 | 93 | strApplicationSlash = []byte("application/") 94 | strImageSVG = []byte("image/svg") 95 | strImageIcon = []byte("image/x-icon") 96 | strFontSlash = []byte("font/") 97 | strMultipartSlash = []byte("multipart/") 98 | strTextSlash = []byte("text/") 99 | ) 100 | -------------------------------------------------------------------------------- /tcplisten/README.md: -------------------------------------------------------------------------------- 1 | # TCPListen 2 | 3 | Package tcplisten provides customizable TCP net.Listener with various 4 | performance-related options: 5 | 6 | * SO_REUSEPORT. This option allows linear scaling server performance 7 | on multi-CPU servers. 8 | See https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/ for details. 9 | 10 | * TCP_DEFER_ACCEPT. This option expects the server reads from the accepted 11 | connection before writing to them. 12 | 13 | * TCP_FASTOPEN. See https://lwn.net/Articles/508865/ for details. 14 | 15 | The package is derived from [go_reuseport](https://github.com/valyala/tcplisten). 16 | -------------------------------------------------------------------------------- /tcplisten/socket.go: -------------------------------------------------------------------------------- 1 | //go:build !js && !wasm && (linux || darwin || dragonfly || freebsd || netbsd || openbsd || rumprun || (zos && s390x)) 2 | 3 | package tcplisten 4 | 5 | import ( 6 | "fmt" 7 | "syscall" 8 | 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | func newSocketCloexecOld(domain, typ, proto int) (int, error) { 13 | syscall.ForkLock.RLock() 14 | fd, err := unix.Socket(domain, typ, proto) 15 | if err == nil { 16 | unix.CloseOnExec(fd) 17 | } 18 | syscall.ForkLock.RUnlock() 19 | if err != nil { 20 | return -1, fmt.Errorf("cannot create listening socket: %w", err) 21 | } 22 | if err = unix.SetNonblock(fd, true); err != nil { 23 | unix.Close(fd) 24 | return -1, fmt.Errorf("cannot make non-blocked listening socket: %w", err) 25 | } 26 | return fd, nil 27 | } 28 | -------------------------------------------------------------------------------- /tcplisten/socket_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | 3 | package tcplisten 4 | 5 | var newSocketCloexec = newSocketCloexecOld 6 | -------------------------------------------------------------------------------- /tcplisten/socket_other.go: -------------------------------------------------------------------------------- 1 | //go:build !js && !wasm && (linux || dragonfly || freebsd || netbsd || openbsd || rumprun) 2 | 3 | package tcplisten 4 | 5 | import ( 6 | "fmt" 7 | 8 | "golang.org/x/sys/unix" 9 | ) 10 | 11 | func newSocketCloexec(domain, typ, proto int) (int, error) { 12 | fd, err := unix.Socket(domain, typ|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, proto) 13 | if err == nil { 14 | return fd, nil 15 | } 16 | 17 | if err == unix.EPROTONOSUPPORT || err == unix.EINVAL { 18 | return newSocketCloexecOld(domain, typ, proto) 19 | } 20 | 21 | return -1, fmt.Errorf("cannot create listening unblocked socket: %w", err) 22 | } 23 | -------------------------------------------------------------------------------- /tcplisten/socket_zos_s390x.go: -------------------------------------------------------------------------------- 1 | //go:build zos && s390x 2 | 3 | package tcplisten 4 | 5 | import ( 6 | "fmt" 7 | 8 | "golang.org/x/sys/unix" 9 | ) 10 | 11 | func newSocketCloexec(domain, typ, proto int) (int, error) { 12 | fd, err := unix.Socket(domain, typ, proto) 13 | _, err = unix.FcntlInt(uintptr(fd), unix.F_SETFD, unix.FD_CLOEXEC) 14 | _, err = unix.FcntlInt(uintptr(fd), unix.F_SETFL, unix.O_NONBLOCK) 15 | if err == nil { 16 | return fd, nil 17 | } 18 | return -1, fmt.Errorf("cannot create listening unblocked socket: %s", err) 19 | } 20 | -------------------------------------------------------------------------------- /tcplisten/tcplisten.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin || dragonfly || freebsd || netbsd || openbsd || rumprun || (zos && s390x) 2 | 3 | // Package tcplisten provides customizable TCP net.Listener with various 4 | // performance-related options: 5 | // 6 | // - SO_REUSEPORT. This option allows linear scaling server performance 7 | // on multi-CPU servers. 8 | // See https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/ for details. 9 | // 10 | // - TCP_DEFER_ACCEPT. This option expects the server reads from the accepted 11 | // connection before writing to them. 12 | // 13 | // - TCP_FASTOPEN. See https://lwn.net/Articles/508865/ for details. 14 | // 15 | // The package is derived from https://github.com/valyala/tcplisten 16 | package tcplisten 17 | 18 | import ( 19 | "errors" 20 | "fmt" 21 | "math" 22 | "net" 23 | "os" 24 | 25 | "golang.org/x/sys/unix" 26 | ) 27 | 28 | // Config provides options to enable on the returned listener. 29 | type Config struct { 30 | // ReusePort enables SO_REUSEPORT. 31 | ReusePort bool 32 | 33 | // DeferAccept enables TCP_DEFER_ACCEPT. 34 | DeferAccept bool 35 | 36 | // FastOpen enables TCP_FASTOPEN. 37 | FastOpen bool 38 | 39 | // Backlog is the maximum number of pending TCP connections the listener 40 | // may queue before passing them to Accept. 41 | // See man 2 listen for details. 42 | // 43 | // By default system-level backlog value is used. 44 | Backlog int 45 | } 46 | 47 | // NewListener returns TCP listener with options set in the Config. 48 | // 49 | // The function may be called many times for creating distinct listeners 50 | // with the given config. 51 | // 52 | // Only tcp4 and tcp6 networks are supported. 53 | func (cfg *Config) NewListener(network, addr string) (net.Listener, error) { 54 | sa, soType, err := getSockaddr(network, addr) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | fd, err := newSocketCloexec(soType, unix.SOCK_STREAM, unix.IPPROTO_TCP) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | if err = cfg.fdSetup(fd, sa, addr); err != nil { 65 | unix.Close(fd) 66 | return nil, err 67 | } 68 | 69 | name := fmt.Sprintf("reuseport.%d.%s.%s", os.Getpid(), network, addr) 70 | file := os.NewFile(uintptr(fd), name) 71 | ln, err := net.FileListener(file) 72 | if err != nil { 73 | file.Close() 74 | return nil, err 75 | } 76 | 77 | if err = file.Close(); err != nil { 78 | ln.Close() 79 | return nil, err 80 | } 81 | 82 | return ln, nil 83 | } 84 | 85 | func (cfg *Config) fdSetup(fd int, sa unix.Sockaddr, addr string) error { 86 | var err error 87 | 88 | if err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEADDR, 1); err != nil { 89 | return fmt.Errorf("cannot enable SO_REUSEADDR: %w", err) 90 | } 91 | 92 | // This should disable Nagle's algorithm in all accepted sockets by default. 93 | // Users may enable it with net.TCPConn.SetNoDelay(false). 94 | if err = unix.SetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_NODELAY, 1); err != nil { 95 | return fmt.Errorf("cannot disable Nagle's algorithm: %w", err) 96 | } 97 | 98 | if cfg.ReusePort { 99 | if err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, soReusePort, 1); err != nil { 100 | return fmt.Errorf("cannot enable SO_REUSEPORT: %w", err) 101 | } 102 | } 103 | 104 | if cfg.DeferAccept { 105 | if err = enableDeferAccept(fd); err != nil { 106 | return err 107 | } 108 | } 109 | 110 | if cfg.FastOpen { 111 | if err = enableFastOpen(fd); err != nil { 112 | return err 113 | } 114 | } 115 | 116 | if err = unix.Bind(fd, sa); err != nil { 117 | return fmt.Errorf("cannot bind to %q: %w", addr, err) 118 | } 119 | 120 | backlog := cfg.Backlog 121 | if backlog <= 0 { 122 | if backlog, err = soMaxConn(); err != nil { 123 | return fmt.Errorf("cannot determine backlog to pass to listen(2): %w", err) 124 | } 125 | } 126 | if err = unix.Listen(fd, backlog); err != nil { 127 | return fmt.Errorf("cannot listen on %q: %w", addr, err) 128 | } 129 | 130 | return nil 131 | } 132 | 133 | func getSockaddr(network, addr string) (sa unix.Sockaddr, soType int, err error) { 134 | tcpAddr, err := net.ResolveTCPAddr(network, addr) 135 | if err != nil { 136 | return nil, -1, err 137 | } 138 | 139 | switch network { 140 | case "tcp4": 141 | var sa4 unix.SockaddrInet4 142 | sa4.Port = tcpAddr.Port 143 | copy(sa4.Addr[:], tcpAddr.IP.To4()) 144 | return &sa4, unix.AF_INET, nil 145 | case "tcp6": 146 | var sa6 unix.SockaddrInet6 147 | sa6.Port = tcpAddr.Port 148 | copy(sa6.Addr[:], tcpAddr.IP.To16()) 149 | if tcpAddr.Zone != "" { 150 | ifi, err := net.InterfaceByName(tcpAddr.Zone) 151 | if err != nil { 152 | return nil, -1, err 153 | } 154 | sa6.ZoneId, err = safeIntToUint32(ifi.Index) 155 | if err != nil { 156 | return nil, -1, fmt.Errorf("unexpected convert net interface index int to uint32: %w", err) 157 | } 158 | } 159 | return &sa6, unix.AF_INET6, nil 160 | case "tcp": 161 | var sa6 unix.SockaddrInet6 162 | sa6.Port = tcpAddr.Port 163 | if tcpAddr.IP == nil { 164 | tcpAddr.IP = net.IPv4(0, 0, 0, 0) 165 | } 166 | copy(sa6.Addr[:], tcpAddr.IP.To16()) 167 | if tcpAddr.Zone != "" { 168 | ifi, err := net.InterfaceByName(tcpAddr.Zone) 169 | if err != nil { 170 | return nil, -1, err 171 | } 172 | sa6.ZoneId, err = safeIntToUint32(ifi.Index) 173 | if err != nil { 174 | return nil, -1, fmt.Errorf("unexpected convert net interface index int to uint32: %w", err) 175 | } 176 | } 177 | return &sa6, unix.AF_INET6, nil 178 | default: 179 | return nil, -1, errors.New("only tcp, tcp4, or tcp6 is supported " + network) 180 | } 181 | } 182 | 183 | func safeIntToUint32(i int) (uint32, error) { 184 | if i < 0 { 185 | return 0, errors.New("value is negative, cannot convert to uint32") 186 | } 187 | ui := uint64(i) 188 | if ui > math.MaxUint32 { 189 | return 0, errors.New("value exceeds uint32 max value") 190 | } 191 | return uint32(ui), nil 192 | } 193 | -------------------------------------------------------------------------------- /tcplisten/tcplisten_js_wasm.go: -------------------------------------------------------------------------------- 1 | package tcplisten 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | // A dummy implementation for js,wasm 8 | type Config struct { 9 | ReusePort bool 10 | DeferAccept bool 11 | FastOpen bool 12 | Backlog int 13 | } 14 | 15 | func (cfg *Config) NewListener(network, addr string) (net.Listener, error) { 16 | return net.Listen(network, addr) 17 | } 18 | -------------------------------------------------------------------------------- /tcplisten/tcplisten_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package tcplisten 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "strconv" 9 | "strings" 10 | 11 | "golang.org/x/sys/unix" 12 | ) 13 | 14 | const ( 15 | soReusePort = 0x0F 16 | tcpFastOpen = 0x17 17 | ) 18 | 19 | func enableDeferAccept(fd int) error { 20 | if err := unix.SetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_DEFER_ACCEPT, 1); err != nil { 21 | return fmt.Errorf("cannot enable TCP_DEFER_ACCEPT: %s", err) 22 | } 23 | return nil 24 | } 25 | 26 | func enableFastOpen(fd int) error { 27 | if err := unix.SetsockoptInt(fd, unix.SOL_TCP, tcpFastOpen, fastOpenQlen); err != nil { 28 | return fmt.Errorf("cannot enable TCP_FASTOPEN(qlen=%d): %s", fastOpenQlen, err) 29 | } 30 | return nil 31 | } 32 | 33 | const fastOpenQlen = 16 * 1024 34 | 35 | func soMaxConn() (int, error) { 36 | data, err := os.ReadFile(soMaxConnFilePath) 37 | if err != nil { 38 | // This error may trigger on travis build. Just use SOMAXCONN 39 | if os.IsNotExist(err) { 40 | return unix.SOMAXCONN, nil 41 | } 42 | return -1, err 43 | } 44 | s := strings.TrimSpace(string(data)) 45 | n, err := strconv.Atoi(s) 46 | if err != nil || n <= 0 { 47 | return -1, fmt.Errorf("cannot parse somaxconn %q read from %s: %s", s, soMaxConnFilePath, err) 48 | } 49 | 50 | // Linux stores the backlog in a uint16. 51 | // Truncate number to avoid wrapping. 52 | // See https://github.com/golang/go/issues/5030 . 53 | if n > 1<<16-1 { 54 | n = maxAckBacklog(n) 55 | } 56 | return n, nil 57 | } 58 | 59 | func kernelVersion() (major, minor int) { 60 | var uname unix.Utsname 61 | if err := unix.Uname(&uname); err != nil { 62 | return 63 | } 64 | 65 | rl := uname.Release 66 | var values [2]int 67 | vi := 0 68 | value := 0 69 | for _, c := range rl { 70 | if c >= '0' && c <= '9' { 71 | value = (value * 10) + int(c-'0') 72 | } else { 73 | // Note that we're assuming N.N.N here. If we see anything else we are likely to 74 | // mis-parse it. 75 | values[vi] = value 76 | vi++ 77 | if vi >= len(values) { 78 | break 79 | } 80 | } 81 | } 82 | switch vi { 83 | case 0: 84 | return 0, 0 85 | case 1: 86 | return values[0], 0 87 | case 2: 88 | return values[0], values[1] 89 | } 90 | return 91 | } 92 | 93 | // Linux stores the backlog as: 94 | // 95 | // - uint16 in kernel version < 4.1, 96 | // - uint32 in kernel version >= 4.1 97 | // 98 | // Truncate number to avoid wrapping. 99 | // 100 | // See issue 5 or 101 | // https://github.com/golang/go/issues/5030. 102 | // https://github.com/golang/go/issues/41470. 103 | func maxAckBacklog(n int) int { 104 | major, minor := kernelVersion() 105 | size := 16 106 | if major > 4 || (major == 4 && minor >= 1) { 107 | size = 32 108 | } 109 | 110 | u := 1< u { 112 | n = u 113 | } 114 | return n 115 | } 116 | 117 | const soMaxConnFilePath = "/proc/sys/net/core/somaxconn" 118 | -------------------------------------------------------------------------------- /tcplisten/tcplisten_other.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly || freebsd || netbsd || openbsd || rumprun || (zos && s390x) 2 | 3 | package tcplisten 4 | 5 | import "golang.org/x/sys/unix" 6 | 7 | const soReusePort = unix.SO_REUSEPORT 8 | 9 | func enableDeferAccept(fd int) error { 10 | // TODO: implement SO_ACCEPTFILTER:dataready here 11 | return nil 12 | } 13 | 14 | func enableFastOpen(fd int) error { 15 | // TODO: implement TCP_FASTOPEN when it will be ready 16 | return nil 17 | } 18 | 19 | func soMaxConn() (int, error) { 20 | // TODO: properly implement it 21 | return unix.SOMAXCONN, nil 22 | } 23 | -------------------------------------------------------------------------------- /tcplisten/tcplisten_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin || dragonfly || freebsd || netbsd || openbsd || rumprun 2 | 3 | package tcplisten 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | "net" 9 | "runtime" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestConfigDeferAccept(t *testing.T) { 15 | if runtime.GOOS != "linux" { 16 | t.Skip() 17 | } 18 | testConfig(t, Config{DeferAccept: true}) 19 | } 20 | 21 | func TestConfigReusePort(t *testing.T) { 22 | testConfig(t, Config{ReusePort: true}) 23 | } 24 | 25 | func TestConfigFastOpen(t *testing.T) { 26 | if runtime.GOOS != "linux" { 27 | t.Skip() 28 | } 29 | testConfig(t, Config{FastOpen: true}) 30 | } 31 | 32 | func TestConfigAll(t *testing.T) { 33 | if runtime.GOOS != "linux" { 34 | t.Skip() 35 | } 36 | cfg := Config{ 37 | ReusePort: true, 38 | DeferAccept: true, 39 | FastOpen: true, 40 | } 41 | testConfig(t, cfg) 42 | } 43 | 44 | func TestConfigBacklog(t *testing.T) { 45 | cfg := Config{ 46 | Backlog: 32, 47 | } 48 | testConfig(t, cfg) 49 | } 50 | 51 | func testConfig(t *testing.T, cfg Config) { 52 | testConfigV(t, cfg, "tcp", "localhost:10083") 53 | testConfigV(t, cfg, "tcp", "[::1]:10083") 54 | testConfigV(t, cfg, "tcp4", "localhost:10083") 55 | testConfigV(t, cfg, "tcp6", "[::1]:10083") 56 | } 57 | 58 | func testConfigV(t *testing.T, cfg Config, network, addr string) { 59 | const requestsCount = 1000 60 | serversCount := 1 61 | if cfg.ReusePort { 62 | serversCount = 10 63 | } 64 | doneCh := make(chan struct{}, serversCount) 65 | 66 | var lns []net.Listener 67 | for i := 0; i < serversCount; i++ { 68 | ln, err := cfg.NewListener(network, addr) 69 | if err != nil { 70 | t.Fatalf("cannot create listener %d using Config %#v: %s", i, &cfg, err) 71 | } 72 | go func() { 73 | serveEcho(t, ln) 74 | doneCh <- struct{}{} 75 | }() 76 | lns = append(lns, ln) 77 | } 78 | 79 | for i := 0; i < requestsCount; i++ { 80 | c, err := net.Dial(network, addr) 81 | if err != nil { 82 | t.Fatalf("%d. unexpected error when dialing: %s", i, err) 83 | } 84 | req := fmt.Sprintf("request number %d", i) 85 | if _, err = c.Write([]byte(req)); err != nil { 86 | t.Fatalf("%d. unexpected error when writing request: %s", i, err) 87 | } 88 | if err = c.(*net.TCPConn).CloseWrite(); err != nil { 89 | t.Fatalf("%d. unexpected error when closing write end of the connection: %s", i, err) 90 | } 91 | 92 | var resp []byte 93 | ch := make(chan error) 94 | go func() { 95 | resp, err = io.ReadAll(c) 96 | ch <- err 97 | close(ch) 98 | }() 99 | select { 100 | case err := <-ch: 101 | if err != nil { 102 | t.Fatalf("%d. unexpected error when reading response: %s", i, err) 103 | } 104 | case <-time.After(200 * time.Millisecond): 105 | t.Fatalf("%d. timeout when waiting for response: %s", i, err) 106 | } 107 | 108 | if string(resp) != req { 109 | t.Fatalf("%d. unexpected response %q. Expecting %q", i, resp, req) 110 | } 111 | if err = c.Close(); err != nil { 112 | t.Fatalf("%d. unexpected error when closing connection: %s", i, err) 113 | } 114 | } 115 | 116 | for _, ln := range lns { 117 | if err := ln.Close(); err != nil { 118 | t.Fatalf("unexpected error when closing listener: %s", err) 119 | } 120 | } 121 | 122 | for i := 0; i < serversCount; i++ { 123 | select { 124 | case <-doneCh: 125 | case <-time.After(time.Second): 126 | t.Fatalf("timeout when waiting for servers to be closed") 127 | } 128 | } 129 | } 130 | 131 | func serveEcho(t *testing.T, ln net.Listener) { 132 | for { 133 | c, err := ln.Accept() 134 | if err != nil { 135 | break 136 | } 137 | req, err := io.ReadAll(c) 138 | if err != nil { 139 | t.Fatalf("unexpected error when reading request: %s", err) 140 | } 141 | if _, err = c.Write(req); err != nil { 142 | t.Fatalf("unexpected error when writing response: %s", err) 143 | } 144 | if err = c.Close(); err != nil { 145 | t.Fatalf("unexpected error when closing connection: %s", err) 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /testdata/test.png: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /timer.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | func initTimer(t *time.Timer, timeout time.Duration) *time.Timer { 9 | if t == nil { 10 | return time.NewTimer(timeout) 11 | } 12 | if t.Reset(timeout) { 13 | // developer sanity-check 14 | panic("BUG: active timer trapped into initTimer()") 15 | } 16 | return t 17 | } 18 | 19 | func stopTimer(t *time.Timer) { 20 | if !t.Stop() { 21 | // Collect possibly added time from the channel 22 | // if timer has been stopped and nobody collected its value. 23 | select { 24 | case <-t.C: 25 | default: 26 | } 27 | } 28 | } 29 | 30 | // AcquireTimer returns a time.Timer from the pool and updates it to 31 | // send the current time on its channel after at least timeout. 32 | // 33 | // The returned Timer may be returned to the pool with ReleaseTimer 34 | // when no longer needed. This allows reducing GC load. 35 | func AcquireTimer(timeout time.Duration) *time.Timer { 36 | v := timerPool.Get() 37 | if v == nil { 38 | return time.NewTimer(timeout) 39 | } 40 | t := v.(*time.Timer) 41 | initTimer(t, timeout) 42 | return t 43 | } 44 | 45 | // ReleaseTimer returns the time.Timer acquired via AcquireTimer to the pool 46 | // and prevents the Timer from firing. 47 | // 48 | // Do not access the released time.Timer or read from its channel otherwise 49 | // data races may occur. 50 | func ReleaseTimer(t *time.Timer) { 51 | stopTimer(t) 52 | timerPool.Put(t) 53 | } 54 | 55 | var timerPool sync.Pool 56 | -------------------------------------------------------------------------------- /tls.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "crypto/x509/pkix" 8 | "encoding/pem" 9 | "math/big" 10 | "time" 11 | ) 12 | 13 | // GenerateTestCertificate generates a test certificate and private key based on the given host. 14 | func GenerateTestCertificate(host string) ([]byte, []byte, error) { 15 | priv, err := rsa.GenerateKey(rand.Reader, 2048) 16 | if err != nil { 17 | return nil, nil, err 18 | } 19 | 20 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 21 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 22 | if err != nil { 23 | return nil, nil, err 24 | } 25 | 26 | cert := &x509.Certificate{ 27 | SerialNumber: serialNumber, 28 | Subject: pkix.Name{ 29 | Organization: []string{"fasthttp test"}, 30 | }, 31 | NotBefore: time.Now(), 32 | NotAfter: time.Now().Add(365 * 24 * time.Hour), 33 | KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, 34 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, 35 | SignatureAlgorithm: x509.SHA256WithRSA, 36 | DNSNames: []string{host}, 37 | BasicConstraintsValid: true, 38 | IsCA: true, 39 | } 40 | 41 | certBytes, err := x509.CreateCertificate( 42 | rand.Reader, cert, cert, &priv.PublicKey, priv, 43 | ) 44 | 45 | p := pem.EncodeToMemory( 46 | &pem.Block{ 47 | Type: "PRIVATE KEY", 48 | Bytes: x509.MarshalPKCS1PrivateKey(priv), 49 | }, 50 | ) 51 | 52 | b := pem.EncodeToMemory( 53 | &pem.Block{ 54 | Type: "CERTIFICATE", 55 | Bytes: certBytes, 56 | }, 57 | ) 58 | 59 | return b, p, err 60 | } 61 | -------------------------------------------------------------------------------- /uri_timing_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func BenchmarkURIParsePath(b *testing.B) { 8 | benchmarkURIParse(b, "google.com", "/foo/bar") 9 | } 10 | 11 | func BenchmarkURIParsePathQueryString(b *testing.B) { 12 | benchmarkURIParse(b, "google.com", "/foo/bar?query=string&other=value") 13 | } 14 | 15 | func BenchmarkURIParsePathQueryStringHash(b *testing.B) { 16 | benchmarkURIParse(b, "google.com", "/foo/bar?query=string&other=value#hashstring") 17 | } 18 | 19 | func BenchmarkURIParseHostname(b *testing.B) { 20 | benchmarkURIParse(b, "google.com", "http://foobar.com/foo/bar?query=string&other=value#hashstring") 21 | } 22 | 23 | func BenchmarkURIFullURI(b *testing.B) { 24 | host := []byte("foobar.com") 25 | requestURI := []byte("/foobar/baz?aaa=bbb&ccc=ddd") 26 | uriLen := len(host) + len(requestURI) + 7 27 | 28 | b.RunParallel(func(pb *testing.PB) { 29 | var u URI 30 | u.Parse(host, requestURI) //nolint:errcheck 31 | for pb.Next() { 32 | uri := u.FullURI() 33 | if len(uri) != uriLen { 34 | b.Fatalf("unexpected uri len %d. Expecting %d", len(uri), uriLen) 35 | } 36 | } 37 | }) 38 | } 39 | 40 | func benchmarkURIParse(b *testing.B, host, uri string) { 41 | strHost, strURI := []byte(host), []byte(uri) 42 | 43 | b.RunParallel(func(pb *testing.PB) { 44 | var u URI 45 | for pb.Next() { 46 | u.Parse(strHost, strURI) //nolint:errcheck 47 | } 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /uri_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package fasthttp 4 | 5 | func addLeadingSlash(dst, src []byte) []byte { 6 | // add leading slash for unix paths 7 | if len(src) == 0 || src[0] != '/' { 8 | dst = append(dst, '/') 9 | } 10 | 11 | return dst 12 | } 13 | -------------------------------------------------------------------------------- /uri_windows.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | func addLeadingSlash(dst, src []byte) []byte { 4 | // zero length 、"C:/" and "a" case 5 | isDisk := len(src) > 2 && src[1] == ':' 6 | if len(src) == 0 || (!isDisk && src[0] != '/') { 7 | dst = append(dst, '/') 8 | } 9 | 10 | return dst 11 | } 12 | -------------------------------------------------------------------------------- /uri_windows_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import "testing" 4 | 5 | func TestURIPathNormalizeIssue86(t *testing.T) { 6 | t.Parallel() 7 | 8 | // see https://github.com/valyala/fasthttp/issues/86 9 | var u URI 10 | 11 | testURIPathNormalize(t, &u, `C:\a\b\c\fs.go`, `C:\a\b\c\fs.go`) 12 | 13 | testURIPathNormalize(t, &u, `a`, `/a`) 14 | 15 | testURIPathNormalize(t, &u, "/../../../../../foo", "/foo") 16 | 17 | testURIPathNormalize(t, &u, "/..\\..\\..\\..\\..\\", "/") 18 | 19 | testURIPathNormalize(t, &u, "/..%5c..%5cfoo", "/foo") 20 | } 21 | -------------------------------------------------------------------------------- /userdata.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | type userDataKV struct { 8 | key any 9 | value any 10 | } 11 | 12 | type userData []userDataKV 13 | 14 | func (d *userData) Set(key, value any) { 15 | if b, ok := key.([]byte); ok { 16 | key = string(b) 17 | } 18 | args := *d 19 | n := len(args) 20 | for i := 0; i < n; i++ { 21 | kv := &args[i] 22 | if kv.key == key { 23 | kv.value = value 24 | return 25 | } 26 | } 27 | 28 | if value == nil { 29 | return 30 | } 31 | 32 | c := cap(args) 33 | if c > n { 34 | args = args[:n+1] 35 | kv := &args[n] 36 | kv.key = key 37 | kv.value = value 38 | *d = args 39 | return 40 | } 41 | 42 | kv := userDataKV{} 43 | kv.key = key 44 | kv.value = value 45 | args = append(args, kv) 46 | *d = args 47 | } 48 | 49 | func (d *userData) SetBytes(key []byte, value any) { 50 | d.Set(key, value) 51 | } 52 | 53 | func (d *userData) Get(key any) any { 54 | if b, ok := key.([]byte); ok { 55 | key = b2s(b) 56 | } 57 | args := *d 58 | n := len(args) 59 | for i := 0; i < n; i++ { 60 | kv := &args[i] 61 | if kv.key == key { 62 | return kv.value 63 | } 64 | } 65 | return nil 66 | } 67 | 68 | func (d *userData) GetBytes(key []byte) any { 69 | return d.Get(key) 70 | } 71 | 72 | func (d *userData) Reset() { 73 | args := *d 74 | n := len(args) 75 | for i := 0; i < n; i++ { 76 | v := args[i].value 77 | if vc, ok := v.(io.Closer); ok { 78 | vc.Close() 79 | } 80 | (*d)[i].value = nil 81 | (*d)[i].key = nil 82 | } 83 | *d = (*d)[:0] 84 | } 85 | 86 | func (d *userData) Remove(key any) { 87 | if b, ok := key.([]byte); ok { 88 | key = b2s(b) 89 | } 90 | args := *d 91 | n := len(args) 92 | for i := 0; i < n; i++ { 93 | kv := &args[i] 94 | if kv.key == key { 95 | n-- 96 | args[i], args[n] = args[n], args[i] 97 | args[n].key = nil 98 | args[n].value = nil 99 | args = args[:n] 100 | *d = args 101 | return 102 | } 103 | } 104 | } 105 | 106 | func (d *userData) RemoveBytes(key []byte) { 107 | d.Remove(key) 108 | } 109 | -------------------------------------------------------------------------------- /userdata_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "runtime" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestUserData(t *testing.T) { 12 | t.Parallel() 13 | 14 | var u userData 15 | 16 | for i := 0; i < 10; i++ { 17 | key := []byte(fmt.Sprintf("key_%d", i)) 18 | u.SetBytes(key, i+5) 19 | testUserDataGet(t, &u, key, i+5) 20 | u.SetBytes(key, i) 21 | testUserDataGet(t, &u, key, i) 22 | } 23 | 24 | for i := 0; i < 10; i++ { 25 | key := []byte(fmt.Sprintf("key_%d", i)) 26 | testUserDataGet(t, &u, key, i) 27 | } 28 | 29 | u.Reset() 30 | 31 | for i := 0; i < 10; i++ { 32 | key := []byte(fmt.Sprintf("key_%d", i)) 33 | testUserDataGet(t, &u, key, nil) 34 | } 35 | } 36 | 37 | func testUserDataGet(t *testing.T, u *userData, key []byte, value any) { 38 | v := u.GetBytes(key) 39 | if v == nil && value != nil { 40 | t.Fatalf("cannot obtain value for key=%q", key) 41 | } 42 | if !reflect.DeepEqual(v, value) { 43 | t.Fatalf("unexpected value for key=%q: %d. Expecting %d", key, v, value) 44 | } 45 | } 46 | 47 | func TestUserDataValueClose(t *testing.T) { 48 | t.Parallel() 49 | 50 | var u userData 51 | 52 | closeCalls := 0 53 | 54 | // store values implementing io.Closer 55 | for i := 0; i < 5; i++ { 56 | key := fmt.Sprintf("key_%d", i) 57 | u.Set(key, &closerValue{closeCalls: &closeCalls}) 58 | } 59 | 60 | // store values without io.Closer 61 | for i := 0; i < 10; i++ { 62 | key := fmt.Sprintf("key_noclose_%d", i) 63 | u.Set(key, i) 64 | } 65 | 66 | u.Reset() 67 | 68 | if closeCalls != 5 { 69 | t.Fatalf("unexpected number of Close calls: %d. Expecting 10", closeCalls) 70 | } 71 | } 72 | 73 | type closerValue struct { 74 | closeCalls *int 75 | } 76 | 77 | func (cv *closerValue) Close() error { 78 | (*cv.closeCalls)++ 79 | return nil 80 | } 81 | 82 | func TestUserDataDelete(t *testing.T) { 83 | t.Parallel() 84 | 85 | var u userData 86 | 87 | for i := 0; i < 10; i++ { 88 | key := fmt.Sprintf("key_%d", i) 89 | u.Set(key, i) 90 | testUserDataGet(t, &u, []byte(key), i) 91 | } 92 | 93 | for i := 0; i < 10; i += 2 { 94 | k := fmt.Sprintf("key_%d", i) 95 | u.Remove(k) 96 | if val := u.Get(k); val != nil { 97 | t.Fatalf("unexpected key= %q, value =%v ,Expecting key= %q, value = nil", k, val, k) 98 | } 99 | kk := fmt.Sprintf("key_%d", i+1) 100 | testUserDataGet(t, &u, []byte(kk), i+1) 101 | } 102 | for i := 0; i < 10; i++ { 103 | key := fmt.Sprintf("key_new_%d", i) 104 | u.Set(key, i) 105 | testUserDataGet(t, &u, []byte(key), i) 106 | } 107 | } 108 | 109 | func TestUserDataSetAndRemove(t *testing.T) { 110 | var ( 111 | u userData 112 | shortKey = "[]" 113 | longKey = "[ ]" 114 | ) 115 | 116 | u.Set(shortKey, "") 117 | u.Set(longKey, "") 118 | u.Remove(shortKey) 119 | u.Set(shortKey, "") 120 | testUserDataGet(t, &u, []byte(shortKey), "") 121 | testUserDataGet(t, &u, []byte(longKey), "") 122 | } 123 | 124 | func TestUserData_GC(t *testing.T) { 125 | t.Parallel() 126 | 127 | var u userData 128 | key := "foo" 129 | final := make(chan struct{}) 130 | 131 | func() { 132 | val := &RequestHeader{} 133 | runtime.SetFinalizer(val, func(v *RequestHeader) { 134 | close(final) 135 | }) 136 | 137 | u.Set(key, val) 138 | }() 139 | 140 | u.Reset() 141 | runtime.GC() 142 | 143 | select { 144 | case <-final: 145 | case <-time.After(time.Second): 146 | t.Fatalf("value is garbage collected") 147 | } 148 | 149 | // Keep u alive, otherwise val will always get garbage collected. 150 | u.Set("bar", 1) 151 | } 152 | -------------------------------------------------------------------------------- /userdata_timing_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func BenchmarkUserDataCustom(b *testing.B) { 8 | keys := []string{"foobar", "baz", "aaa", "bsdfs"} 9 | b.RunParallel(func(pb *testing.PB) { 10 | var u userData 11 | var v any = u 12 | for pb.Next() { 13 | for _, key := range keys { 14 | u.Set(key, v) 15 | } 16 | for _, key := range keys { 17 | vv := u.Get(key) 18 | if _, ok := vv.(userData); !ok { 19 | b.Fatalf("unexpected value %v for key %q", vv, key) 20 | } 21 | } 22 | u.Reset() 23 | } 24 | }) 25 | } 26 | 27 | func BenchmarkUserDataStdMap(b *testing.B) { 28 | keys := []string{"foobar", "baz", "aaa", "bsdfs"} 29 | b.RunParallel(func(pb *testing.PB) { 30 | u := make(map[string]any) 31 | var v any = u 32 | for pb.Next() { 33 | for _, key := range keys { 34 | u[key] = v 35 | } 36 | for _, key := range keys { 37 | vv := u[key] 38 | if _, ok := vv.(map[string]any); !ok { 39 | b.Fatalf("unexpected value %v for key %q", vv, key) 40 | } 41 | } 42 | 43 | for k := range u { 44 | delete(u, k) 45 | } 46 | } 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /workerpool_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "testing" 7 | "time" 8 | 9 | "github.com/valyala/fasthttp/fasthttputil" 10 | ) 11 | 12 | func TestWorkerPoolStartStopSerial(t *testing.T) { 13 | t.Parallel() 14 | 15 | testWorkerPoolStartStop() 16 | } 17 | 18 | func TestWorkerPoolStartStopConcurrent(t *testing.T) { 19 | t.Parallel() 20 | 21 | concurrency := 10 22 | ch := make(chan struct{}, concurrency) 23 | for i := 0; i < concurrency; i++ { 24 | go func() { 25 | testWorkerPoolStartStop() 26 | ch <- struct{}{} 27 | }() 28 | } 29 | for i := 0; i < concurrency; i++ { 30 | select { 31 | case <-ch: 32 | case <-time.After(time.Second): 33 | t.Fatalf("timeout") 34 | } 35 | } 36 | } 37 | 38 | func testWorkerPoolStartStop() { 39 | wp := &workerPool{ 40 | WorkerFunc: func(conn net.Conn) error { return nil }, 41 | MaxWorkersCount: 10, 42 | Logger: defaultLogger, 43 | } 44 | for i := 0; i < 10; i++ { 45 | wp.Start() 46 | wp.Stop() 47 | } 48 | } 49 | 50 | func TestWorkerPoolMaxWorkersCountSerial(t *testing.T) { 51 | t.Parallel() 52 | 53 | testWorkerPoolMaxWorkersCountMulti(t) 54 | } 55 | 56 | func TestWorkerPoolMaxWorkersCountConcurrent(t *testing.T) { 57 | t.Parallel() 58 | 59 | concurrency := 4 60 | ch := make(chan struct{}, concurrency) 61 | for i := 0; i < concurrency; i++ { 62 | go func() { 63 | testWorkerPoolMaxWorkersCountMulti(t) 64 | ch <- struct{}{} 65 | }() 66 | } 67 | for i := 0; i < concurrency; i++ { 68 | select { 69 | case <-ch: 70 | case <-time.After(time.Second * 2): 71 | t.Fatalf("timeout") 72 | } 73 | } 74 | } 75 | 76 | func testWorkerPoolMaxWorkersCountMulti(t *testing.T) { 77 | for i := 0; i < 5; i++ { 78 | testWorkerPoolMaxWorkersCount(t) 79 | } 80 | } 81 | 82 | func testWorkerPoolMaxWorkersCount(t *testing.T) { 83 | ready := make(chan struct{}) 84 | wp := &workerPool{ 85 | WorkerFunc: func(conn net.Conn) error { 86 | buf := make([]byte, 100) 87 | n, err := conn.Read(buf) 88 | if err != nil { 89 | t.Errorf("unexpected error: %v", err) 90 | } 91 | buf = buf[:n] 92 | if string(buf) != "foobar" { 93 | t.Errorf("unexpected data read: %q. Expecting %q", buf, "foobar") 94 | } 95 | if _, err = conn.Write([]byte("baz")); err != nil { 96 | t.Errorf("unexpected error: %v", err) 97 | } 98 | 99 | <-ready 100 | 101 | return nil 102 | }, 103 | MaxWorkersCount: 10, 104 | Logger: defaultLogger, 105 | connState: func(net.Conn, ConnState) {}, 106 | } 107 | wp.Start() 108 | 109 | ln := fasthttputil.NewInmemoryListener() 110 | 111 | clientCh := make(chan struct{}, wp.MaxWorkersCount) 112 | for i := 0; i < wp.MaxWorkersCount; i++ { 113 | go func() { 114 | conn, err := ln.Dial() 115 | if err != nil { 116 | t.Errorf("unexpected error: %v", err) 117 | } 118 | if _, err = conn.Write([]byte("foobar")); err != nil { 119 | t.Errorf("unexpected error: %v", err) 120 | } 121 | data, err := io.ReadAll(conn) 122 | if err != nil { 123 | t.Errorf("unexpected error: %v", err) 124 | } 125 | if string(data) != "baz" { 126 | t.Errorf("unexpected value read: %q. Expecting %q", data, "baz") 127 | } 128 | if err = conn.Close(); err != nil { 129 | t.Errorf("unexpected error: %v", err) 130 | } 131 | clientCh <- struct{}{} 132 | }() 133 | } 134 | 135 | for i := 0; i < wp.MaxWorkersCount; i++ { 136 | conn, err := ln.Accept() 137 | if err != nil { 138 | t.Fatalf("unexpected error: %v", err) 139 | } 140 | if !wp.Serve(conn) { 141 | t.Fatalf("worker pool must have enough workers to serve the conn") 142 | } 143 | } 144 | 145 | go func() { 146 | if _, err := ln.Dial(); err != nil { 147 | t.Errorf("unexpected error: %v", err) 148 | } 149 | }() 150 | conn, err := ln.Accept() 151 | if err != nil { 152 | t.Fatalf("unexpected error: %v", err) 153 | } 154 | for i := 0; i < 5; i++ { 155 | if wp.Serve(conn) { 156 | t.Fatalf("worker pool must be full") 157 | } 158 | } 159 | if err = conn.Close(); err != nil { 160 | t.Fatalf("unexpected error: %v", err) 161 | } 162 | 163 | close(ready) 164 | 165 | for i := 0; i < wp.MaxWorkersCount; i++ { 166 | select { 167 | case <-clientCh: 168 | case <-time.After(time.Second): 169 | t.Fatalf("timeout") 170 | } 171 | } 172 | 173 | if err := ln.Close(); err != nil { 174 | t.Fatalf("unexpected error: %v", err) 175 | } 176 | wp.Stop() 177 | } 178 | -------------------------------------------------------------------------------- /zstd.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "sync" 8 | 9 | "github.com/klauspost/compress/zstd" 10 | "github.com/valyala/bytebufferpool" 11 | "github.com/valyala/fasthttp/stackless" 12 | ) 13 | 14 | const ( 15 | CompressZstdSpeedNotSet = iota 16 | CompressZstdBestSpeed 17 | CompressZstdDefault 18 | CompressZstdSpeedBetter 19 | CompressZstdBestCompression 20 | ) 21 | 22 | var ( 23 | zstdDecoderPool sync.Pool 24 | zstdEncoderPool sync.Pool 25 | realZstdWriterPoolMap = newCompressWriterPoolMap() 26 | stacklessZstdWriterPoolMap = newCompressWriterPoolMap() 27 | ) 28 | 29 | func acquireZstdReader(r io.Reader) (*zstd.Decoder, error) { 30 | v := zstdDecoderPool.Get() 31 | if v == nil { 32 | return zstd.NewReader(r) 33 | } 34 | zr := v.(*zstd.Decoder) 35 | if err := zr.Reset(r); err != nil { 36 | return nil, err 37 | } 38 | return zr, nil 39 | } 40 | 41 | func releaseZstdReader(zr *zstd.Decoder) { 42 | zstdDecoderPool.Put(zr) 43 | } 44 | 45 | func acquireZstdWriter(w io.Writer, level int) (*zstd.Encoder, error) { 46 | v := zstdEncoderPool.Get() 47 | if v == nil { 48 | return zstd.NewWriter(w, zstd.WithEncoderLevel(zstd.EncoderLevel(level))) 49 | } 50 | zw := v.(*zstd.Encoder) 51 | zw.Reset(w) 52 | return zw, nil 53 | } 54 | 55 | func releaseZstdWriter(zw *zstd.Encoder) { //nolint:unused 56 | zw.Close() 57 | zstdEncoderPool.Put(zw) 58 | } 59 | 60 | func acquireStacklessZstdWriter(w io.Writer, compressLevel int) stackless.Writer { 61 | nLevel := normalizeZstdCompressLevel(compressLevel) 62 | p := stacklessZstdWriterPoolMap[nLevel] 63 | v := p.Get() 64 | if v == nil { 65 | return stackless.NewWriter(w, func(w io.Writer) stackless.Writer { 66 | return acquireRealZstdWriter(w, compressLevel) 67 | }) 68 | } 69 | sw := v.(stackless.Writer) 70 | sw.Reset(w) 71 | return sw 72 | } 73 | 74 | func releaseStacklessZstdWriter(zf stackless.Writer, zstdDefault int) { 75 | zf.Close() 76 | nLevel := normalizeZstdCompressLevel(zstdDefault) 77 | p := stacklessZstdWriterPoolMap[nLevel] 78 | p.Put(zf) 79 | } 80 | 81 | func acquireRealZstdWriter(w io.Writer, level int) *zstd.Encoder { 82 | nLevel := normalizeZstdCompressLevel(level) 83 | p := realZstdWriterPoolMap[nLevel] 84 | v := p.Get() 85 | if v == nil { 86 | zw, err := acquireZstdWriter(w, level) 87 | if err != nil { 88 | panic(err) 89 | } 90 | return zw 91 | } 92 | zw := v.(*zstd.Encoder) 93 | zw.Reset(w) 94 | return zw 95 | } 96 | 97 | func releaseRealZstdWrter(zw *zstd.Encoder, level int) { 98 | zw.Close() 99 | nLevel := normalizeZstdCompressLevel(level) 100 | p := realZstdWriterPoolMap[nLevel] 101 | p.Put(zw) 102 | } 103 | 104 | func AppendZstdBytesLevel(dst, src []byte, level int) []byte { 105 | w := &byteSliceWriter{b: dst} 106 | WriteZstdLevel(w, src, level) //nolint:errcheck 107 | return w.b 108 | } 109 | 110 | func WriteZstdLevel(w io.Writer, p []byte, level int) (int, error) { 111 | level = normalizeZstdCompressLevel(level) 112 | switch w.(type) { 113 | case *byteSliceWriter, 114 | *bytes.Buffer, 115 | *bytebufferpool.ByteBuffer: 116 | ctx := &compressCtx{ 117 | w: w, 118 | p: p, 119 | level: level, 120 | } 121 | stacklessWriteZstd(ctx) 122 | return len(p), nil 123 | default: 124 | zw := acquireStacklessZstdWriter(w, level) 125 | n, err := zw.Write(p) 126 | releaseStacklessZstdWriter(zw, level) 127 | return n, err 128 | } 129 | } 130 | 131 | var ( 132 | stacklessWriteZstdOnce sync.Once 133 | stacklessWriteZstdFunc func(ctx any) bool 134 | ) 135 | 136 | func stacklessWriteZstd(ctx any) { 137 | stacklessWriteZstdOnce.Do(func() { 138 | stacklessWriteZstdFunc = stackless.NewFunc(nonblockingWriteZstd) 139 | }) 140 | stacklessWriteZstdFunc(ctx) 141 | } 142 | 143 | func nonblockingWriteZstd(ctxv any) { 144 | ctx := ctxv.(*compressCtx) 145 | zw := acquireRealZstdWriter(ctx.w, ctx.level) 146 | zw.Write(ctx.p) //nolint:errcheck 147 | releaseRealZstdWrter(zw, ctx.level) 148 | } 149 | 150 | // AppendZstdBytes appends zstd src to dst and returns the resulting dst. 151 | func AppendZstdBytes(dst, src []byte) []byte { 152 | return AppendZstdBytesLevel(dst, src, CompressZstdDefault) 153 | } 154 | 155 | // WriteUnzstd writes unzstd p to w and returns the number of uncompressed 156 | // bytes written to w. 157 | func WriteUnzstd(w io.Writer, p []byte) (int, error) { 158 | r := &byteSliceReader{b: p} 159 | zr, err := acquireZstdReader(r) 160 | if err != nil { 161 | return 0, err 162 | } 163 | n, err := copyZeroAlloc(w, zr) 164 | releaseZstdReader(zr) 165 | nn := int(n) 166 | if int64(nn) != n { 167 | return 0, fmt.Errorf("too much data unzstd: %d", n) 168 | } 169 | return nn, err 170 | } 171 | 172 | // AppendUnzstdBytes appends unzstd src to dst and returns the resulting dst. 173 | func AppendUnzstdBytes(dst, src []byte) ([]byte, error) { 174 | w := &byteSliceWriter{b: dst} 175 | _, err := WriteUnzstd(w, src) 176 | return w.b, err 177 | } 178 | 179 | // normalizes compression level into [0..7], so it could be used as an index 180 | // in *PoolMap. 181 | func normalizeZstdCompressLevel(level int) int { 182 | if level < CompressZstdSpeedNotSet || level > CompressZstdBestCompression { 183 | level = CompressZstdDefault 184 | } 185 | return level 186 | } 187 | -------------------------------------------------------------------------------- /zstd_test.go: -------------------------------------------------------------------------------- 1 | package fasthttp 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "testing" 8 | ) 9 | 10 | func TestZstdBytesSerial(t *testing.T) { 11 | t.Parallel() 12 | 13 | if err := testZstdBytes(); err != nil { 14 | t.Fatal(err) 15 | } 16 | } 17 | 18 | func TestZstdBytesConcurrent(t *testing.T) { 19 | t.Parallel() 20 | 21 | if err := testConcurrent(10, testZstdBytes); err != nil { 22 | t.Fatal(err) 23 | } 24 | } 25 | 26 | func testZstdBytes() error { 27 | for _, s := range compressTestcases { 28 | if err := testZstdBytesSingleCase(s); err != nil { 29 | return err 30 | } 31 | } 32 | return nil 33 | } 34 | 35 | func testZstdBytesSingleCase(s string) error { 36 | prefix := []byte("foobar") 37 | ZstdpedS := AppendZstdBytes(prefix, []byte(s)) 38 | if !bytes.Equal(ZstdpedS[:len(prefix)], prefix) { 39 | return fmt.Errorf("unexpected prefix when compressing %q: %q. Expecting %q", s, ZstdpedS[:len(prefix)], prefix) 40 | } 41 | 42 | unZstdedS, err := AppendUnzstdBytes(prefix, ZstdpedS[len(prefix):]) 43 | if err != nil { 44 | return fmt.Errorf("unexpected error when uncompressing %q: %w", s, err) 45 | } 46 | if !bytes.Equal(unZstdedS[:len(prefix)], prefix) { 47 | return fmt.Errorf("unexpected prefix when uncompressing %q: %q. Expecting %q", s, unZstdedS[:len(prefix)], prefix) 48 | } 49 | unZstdedS = unZstdedS[len(prefix):] 50 | if string(unZstdedS) != s { 51 | return fmt.Errorf("unexpected uncompressed string %q. Expecting %q", unZstdedS, s) 52 | } 53 | return nil 54 | } 55 | 56 | func TestZstdCompressSerial(t *testing.T) { 57 | t.Parallel() 58 | 59 | if err := testZstdCompress(); err != nil { 60 | t.Fatal(err) 61 | } 62 | } 63 | 64 | func TestZstdCompressConcurrent(t *testing.T) { 65 | t.Parallel() 66 | 67 | if err := testConcurrent(10, testZstdCompress); err != nil { 68 | t.Fatal(err) 69 | } 70 | } 71 | 72 | func testZstdCompress() error { 73 | for _, s := range compressTestcases { 74 | if err := testZstdCompressSingleCase(s); err != nil { 75 | return err 76 | } 77 | } 78 | return nil 79 | } 80 | 81 | func testZstdCompressSingleCase(s string) error { 82 | var buf bytes.Buffer 83 | zw := acquireStacklessZstdWriter(&buf, CompressZstdDefault) 84 | if _, err := zw.Write([]byte(s)); err != nil { 85 | return fmt.Errorf("unexpected error: %w. s=%q", err, s) 86 | } 87 | releaseStacklessZstdWriter(zw, CompressZstdDefault) 88 | 89 | zr, err := acquireZstdReader(&buf) 90 | if err != nil { 91 | return fmt.Errorf("unexpected error: %w. s=%q", err, s) 92 | } 93 | body, err := io.ReadAll(zr) 94 | if err != nil { 95 | return fmt.Errorf("unexpected error: %w. s=%q", err, s) 96 | } 97 | if string(body) != s { 98 | return fmt.Errorf("unexpected string after decompression: %q. Expecting %q", body, s) 99 | } 100 | releaseZstdReader(zr) 101 | return nil 102 | } 103 | --------------------------------------------------------------------------------