├── .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 |
--------------------------------------------------------------------------------