diff --git a/README.md b/README.md index 7b40752..01702d6 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,13 @@ write HTTP servers. It enables: ## Install ```bash -go get -u github.com/zerodha/fastglue +go get -u github.com/zerodha/fastglue/v2 ``` ## Usage ```go -import "github.com/zerodha/fastglue" +import "github.com/zerodha/fastglue/v2" ``` ## Examples diff --git a/custom.go b/custom.go index 280248b..d698e02 100644 --- a/custom.go +++ b/custom.go @@ -31,8 +31,8 @@ type Envelope struct { // NewGlue creates and returns a new instance of Fastglue with custom error // handlers pre-bound. -func NewGlue() *Fastglue { - f := New() +func NewGlue(o Options) *Fastglue { + f := New(o) f.Router.MethodNotAllowed = BadMethodHandler f.Router.NotFound = NotFoundHandler f.Router.SaveMatchedRoutePath = true diff --git a/examples/before-after/main.go b/examples/before-after/main.go index b3216bd..99557ae 100644 --- a/examples/before-after/main.go +++ b/examples/before-after/main.go @@ -8,7 +8,7 @@ import ( "time" "github.com/valyala/fasthttp" - "github.com/zerodha/fastglue" + "github.com/zerodha/fastglue/v2" ) var ( @@ -18,7 +18,7 @@ var ( func main() { flag.Parse() - g := fastglue.New() + g := fastglue.New(fastglue.Options{}) g.Before(setTime) g.After(calculateTime) g.GET("/", handleIndex) diff --git a/examples/decode/main.go b/examples/decode/main.go index 54d6523..56aed97 100644 --- a/examples/decode/main.go +++ b/examples/decode/main.go @@ -7,7 +7,7 @@ import ( "time" "github.com/valyala/fasthttp" - "github.com/zerodha/fastglue" + "github.com/zerodha/fastglue/v2" ) var ( @@ -17,7 +17,7 @@ var ( func main() { flag.Parse() - g := fastglue.New() + g := fastglue.New(fastglue.Options{}) g.GET("/", handleIndex) s := &fasthttp.Server{ diff --git a/examples/example.go b/examples/example.go index 5472461..154d97b 100644 --- a/examples/example.go +++ b/examples/example.go @@ -5,7 +5,7 @@ import ( "time" "github.com/valyala/fasthttp" - "github.com/zerodha/fastglue" + "github.com/zerodha/fastglue/v2" ) // App is the global config "context" that'll be injected into every Request. diff --git a/examples/graceful/main.go b/examples/graceful/main.go index e06abd8..81980f8 100644 --- a/examples/graceful/main.go +++ b/examples/graceful/main.go @@ -9,7 +9,7 @@ import ( "time" "github.com/valyala/fasthttp" - "github.com/zerodha/fastglue" + "github.com/zerodha/fastglue/v2" ) var ( @@ -19,7 +19,7 @@ var ( func main() { flag.Parse() - g := fastglue.New() + g := fastglue.New(fastglue.Options{}) g.ServeStatic("/{filepath:*}", ".", true) s := &fasthttp.Server{ diff --git a/examples/helloworld/main.go b/examples/helloworld/main.go index fca90f4..f8ddae7 100644 --- a/examples/helloworld/main.go +++ b/examples/helloworld/main.go @@ -7,7 +7,7 @@ import ( "time" "github.com/valyala/fasthttp" - "github.com/zerodha/fastglue" + "github.com/zerodha/fastglue/v2" ) var ( @@ -17,7 +17,7 @@ var ( func main() { flag.Parse() - g := fastglue.New() + g := fastglue.New(fastglue.Options{}) g.GET("/", handleHelloWorld) s := &fasthttp.Server{ diff --git a/examples/middleware/main.go b/examples/middleware/main.go index 48c1884..7b1d468 100644 --- a/examples/middleware/main.go +++ b/examples/middleware/main.go @@ -9,7 +9,7 @@ import ( "time" "github.com/valyala/fasthttp" - "github.com/zerodha/fastglue" + "github.com/zerodha/fastglue/v2" ) var ( @@ -19,7 +19,7 @@ var ( func main() { flag.Parse() - g := fastglue.New() + g := fastglue.New(fastglue.Options{}) g.GET("/", auth(validateAll(handleGetAll))) g.PUT("/", auth(fastglue.ReqLenParams(validate(handleMiddleware), map[string]int{"a": 5, "b": 5}))) g.POST("/", auth(fastglue.ReqParams(validate(handleMiddleware), []string{"a", "b"}))) diff --git a/examples/path/main.go b/examples/path/main.go index 2d9b53f..d52195b 100644 --- a/examples/path/main.go +++ b/examples/path/main.go @@ -8,7 +8,7 @@ import ( "time" "github.com/valyala/fasthttp" - "github.com/zerodha/fastglue" + "github.com/zerodha/fastglue/v2" ) var ( @@ -18,7 +18,7 @@ var ( func main() { flag.Parse() - g := fastglue.New() + g := fastglue.New(fastglue.Options{}) g.GET("/", handleIndex) g.GET("/{name:^[a-zA-Z]+$}", handleIndex) diff --git a/examples/singleton/main.go b/examples/singleton/main.go index e2e8712..8a174c2 100644 --- a/examples/singleton/main.go +++ b/examples/singleton/main.go @@ -8,7 +8,7 @@ import ( "time" "github.com/valyala/fasthttp" - "github.com/zerodha/fastglue" + "github.com/zerodha/fastglue/v2" ) var ( @@ -29,7 +29,7 @@ func main() { log: log.New(os.Stdout, "SINGLETON", log.Llongfile), } - g := fastglue.New() + g := fastglue.New(fastglue.Options{}) g.SetContext(app) g.GET("/", handleIndex) diff --git a/examples/static-file/main.go b/examples/static-file/main.go index cf17ad6..9323333 100644 --- a/examples/static-file/main.go +++ b/examples/static-file/main.go @@ -6,7 +6,7 @@ import ( "time" "github.com/valyala/fasthttp" - "github.com/zerodha/fastglue" + "github.com/zerodha/fastglue/v2" ) var ( @@ -16,7 +16,7 @@ var ( func main() { flag.Parse() - g := fastglue.New() + g := fastglue.New(fastglue.Options{}) g.ServeStatic("/{filepath:*}", ".", true) s := &fasthttp.Server{ diff --git a/fastglue.go b/fastglue.go index affa3d4..932bf8a 100644 --- a/fastglue.go +++ b/fastglue.go @@ -38,6 +38,17 @@ var ( authToken = []byte("token") ) +// CompressionType is the type of otuput compression available in fasthttp +// for the response body. +type CompressionType string + +const ( + CompressionZstd CompressionType = "zstd" + CompressionBr CompressionType = "br" + CompressionGzip CompressionType = "gzip" + CompressionDeflate CompressionType = "deflate" +) + // FastRequestHandler is the fastglue HTTP request handler function // that wraps over the fasthttp handler. type FastRequestHandler func(*Request) error @@ -53,21 +64,52 @@ type Request struct { Context interface{} } +// CompressionOpt is the configuration for enabling and controlling compression. +// Enabling this will automatically compress the response based on the client's +// Accept-Encoding header by internally invoking fasthttp.CompressHandler() +// gzip|deflate|br|zstd are the supported compression types. +type CompressionOpt struct { + Enabled bool + + // Type of compression to support (std, gzip, deflate, br). If no type is specified + // then fasthttp's default compression types and its internal order of priority are used. + // + // Important: The first type in the list is the preferred type + // irrespective of the ordering of types in the incoming client's Accept-Encoding header. + // That is because fasthttp's CompressHandler() internally uses arbitrary + // type ordering to compress the response. fastglue thus overrides the + // Accept-Encoding header to only have the first type in this list. + // For instance, if the list here is [zstd, br], and the incoming header is + // [gzip, deflate, br, zstd], fastglue will overwrite the header to [zstd], + // forcing fasthttp to compress the response using the preferred type here. + Types []CompressionType +} + // Fastglue is the "glue" wrapper over fasthttp and fasthttprouter. type Fastglue struct { Router *fasthttprouter.Router Server *fasthttp.Server - context interface{} MatchedRoutePathParam string - before []FastMiddleware - after []FastMiddleware + + context interface{} + before []FastMiddleware + after []FastMiddleware + + opt Options +} + +type Options struct { + CompressionOpt CompressionOpt } // New creates and returns a new instance of Fastglue. -func New() *Fastglue { - return &Fastglue{ +func New(o Options) *Fastglue { + f := &Fastglue{ Router: fasthttprouter.New(), + opt: o, } + + return f } // ListenAndServe is a wrapper for fasthttp.ListenAndServe. It takes a TCP address, @@ -150,7 +192,7 @@ func (f *Fastglue) Shutdown(s *fasthttp.Server, shutdownComplete chan error) { // handler is the "proxy" abstraction that converts a fastglue handler into // a fasthttp handler and passes execution in and out. func (f *Fastglue) handler(h FastRequestHandler) func(*fasthttp.RequestCtx) { - return func(ctx *fasthttp.RequestCtx) { + handler := func(ctx *fasthttp.RequestCtx) { req := &Request{ RequestCtx: ctx, Context: f.context, @@ -172,7 +214,35 @@ func (f *Fastglue) handler(h FastRequestHandler) func(*fasthttp.RequestCtx) { } } + // If compression is enabled, override the response header to + // the preferred type in the config. + if f.opt.CompressionOpt.Enabled { + for _, typ := range f.opt.CompressionOpt.Types { + t := string(typ) + // If the preferred type is in the client's Accept-Encoding header, + // overwrite the request header to only have this type, forcing + // fasthttp.CompressHandler() to compress the response using it. + // This is because fasthttp internally does not respect the order + // in the client header and uses arbitrary ordering to compress the response. + if ctx.Request.Header.HasAcceptEncoding(t) { + ctx.Request.Header.Set("Accept-Encoding", t) + break + } + } + } + } + + // If compression is enabled, wrap the handler with fasthttp's CompressHandler + // which automatically handles the compression logic. + if f.opt.CompressionOpt.Enabled { + // fasthttp's compression handlers are pretty bad. This particular handler + // is the one that supports gzip|br|zstd|deflate. + return fasthttp.CompressHandlerBrotliLevel(handler, + fasthttp.CompressBrotliDefaultCompression, + fasthttp.CompressDefaultCompression) } + + return handler } // Handler returns fastglue's central fasthttp handler that can be registered diff --git a/fastglue_test.go b/fastglue_test.go index de3c22d..16df7a3 100644 --- a/fastglue_test.go +++ b/fastglue_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "io" "io/ioutil" "log" "net" @@ -22,7 +23,12 @@ import ( ) var ( - srv = NewGlue() + srv = NewGlue(Options{ + CompressionOpt: CompressionOpt{ + Enabled: true, + Types: []CompressionType{CompressionZstd, CompressionBr, CompressionGzip, CompressionDeflate}, + }, + }) srvAddress = ":10200" srvRoot = "http://127.0.0.1:10200" sck = "/tmp/fastglue-test.sock" @@ -54,6 +60,7 @@ func init() { srv.Before(getParamMiddleware) srv.GET("/get", myGEThandler) + srv.GET("/compressed", myCompressedHandler) srv.GET("/next", myNextRedirectHandler) srv.GET("/next-uri", myNextRedirectURIHandler) srv.GET("/redirect", myRedirectHandler) @@ -160,6 +167,10 @@ func myGEThandler(r *Request) error { }{"name=" + string(r.RequestCtx.FormValue("name"))}) } +func myCompressedHandler(r *Request) error { + return r.SendBytes(200, "text/plain", make([]byte, 1000)) +} + func myAnyHandler(r *Request) error { // Write the incoming method name to the body. return r.SendBytes(http.StatusOK, "text/plain", r.RequestCtx.Method()) @@ -242,7 +253,7 @@ func myPOSTJsonhandler(r *Request) error { func TestSocketConnection(t *testing.T) { log.Println("Listening on Test Server", sck) go (func() { - log.Fatal(NewGlue().ListenAndServe("", sck, nil)) + log.Fatal(NewGlue(Options{}).ListenAndServe("", sck, nil)) })() time.Sleep(time.Second * 1) @@ -861,7 +872,7 @@ func TestGrace(t *testing.T) { sig := make(chan os.Signal) signal.Notify(sig, os.Interrupt) - g := New() + g := New(Options{}) g.GET("/", func(r *Request) error { time.Sleep(1 * time.Second) @@ -888,3 +899,56 @@ func TestGrace(t *testing.T) { ch <- struct{}{} wg.Wait() } + +func TestCompression(t *testing.T) { + // Uncompressed request. + r := GETrequest(srvRoot+"/compressed?param=123", t) + if r.StatusCode != fasthttp.StatusOK { + // The response body should be the method that was sent. + t.Fatalf("request failed: %d", r.StatusCode) + } + if h := r.Header.Get("Content-Encoding"); h != "" { + t.Fatalf("content encoding should be empty not: %v", h) + } + + // Check the body response size. + { + b, err := io.ReadAll(r.Body) + if err != nil { + t.Fatalf("Couldn't read response body: %v", err) + } + if len(b) != 1000 { + t.Fatalf("content length %d != expected 1000", len(b)) + } + } + + // Compressed request. + c := http.Client{ + Timeout: time.Second * 3, + Transport: &http.Transport{}, + } + + f := func(typ string, length int64) { + req, _ := http.NewRequest(http.MethodGet, srvRoot+"/compressed?param=123", nil) + req.Header.Set("Accept-Encoding", typ) + + resp, err := c.Do(req) + if err != nil || resp.StatusCode != fasthttp.StatusOK { + // The response body should be the method that was sent. + t.Fatalf("request failed: %v: %v", err, resp.StatusCode) + } + + // Verify the compression response headers. + if resp.Header.Get("Content-Encoding") != typ { + t.Fatalf("Expected content encoding %s != %s", typ, resp.Header.Get("Content-Encoding")) + } + if resp.ContentLength != length { + t.Fatalf("content length %d != expected %d", resp.ContentLength, length) + } + } + + f("gzip", 29) + f("deflate", 17) + f("zstd", 16) + f("br", 11) +} diff --git a/go.mod b/go.mod index 858bd2f..b29b84a 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,19 @@ -module github.com/zerodha/fastglue +module github.com/zerodha/fastglue/v2 -go 1.14 +go 1.21 require ( github.com/fasthttp/router v1.4.5 github.com/stretchr/testify v1.6.0 - github.com/valyala/fasthttp v1.34.0 + github.com/valyala/fasthttp v1.58.0 +) + +require ( + github.com/andybalholm/brotli v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/go.sum b/go.sum index e337a74..aeba8ee 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= -github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fasthttp/router v1.4.5 h1:YZonsKCssEwEi3veDMhL6okIx550qegAiuXAK8NnM3Y= @@ -8,8 +8,8 @@ github.com/fasthttp/router v1.4.5/go.mod h1:UYExWhCy7pUmavRZ0XfjEgHwzxyKwyS8uzXh github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= -github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899 h1:Orn7s+r1raRTBKLSc9DmbktTT04sL+vkzsbRD2Q8rOI= @@ -20,26 +20,20 @@ github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.32.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= -github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= -github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= +github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE= +github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=