1
0
mirror of https://github.com/gofiber/fiber.git synced 2025-02-24 14:44:11 +00:00

Merge pull request #926 from kiyonlin/etag-mw

Add Etag middleware
This commit is contained in:
Fenny 2020-10-15 10:51:07 +02:00 committed by GitHub
commit 0bbce8f988
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 394 additions and 0 deletions

63
middleware/etag/README.md Normal file
View File

@ -0,0 +1,63 @@
# ETag
ETag middleware for [Fiber](https://github.com/gofiber/fiber) that lets caches be more efficient and save bandwidth, as a web server does not need to resend a full response if the content has not changed.
### Table of Contents
- [Signatures](#signatures)
- [Examples](#examples)
- [Config](#config)
- [Default Config](#default-config)
### Signatures
```go
func New(config ...Config) fiber.Handler
```
### Examples
Import the middleware package that is part of the Fiber web framework
```go
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/etag"
)
```
After you initiate your Fiber app, you can use the following possibilities:
```go
// Default middleware config
app.Use(etag.New())
// Get / receives Etag: "13-1831710635" in response header
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("Hello, World!")
})
```
### Config
```go
// Config defines the config for middleware.
type Config struct {
// Weak indicates that a weak validator is used. Weak etags are easy
// to generate, but are far less useful for comparisons. Strong
// validators are ideal for comparisons but can be very difficult
// to generate efficiently. Weak ETag values of two representations
// of the same resources might be semantically equivalent, but not
// byte-for-byte identical. This means weak etags prevent caching
// when byte range requests are used, but strong etags mean range
// requests can still be cached.
Weak bool
// Next defines a function to skip this middleware when returned true.
//
// Optional. Default: nil
Next func(c *fiber.Ctx) bool
}
```
### Default Config
```go
var ConfigDefault = Config{
Weak: false,
Next: nil,
}
```

137
middleware/etag/etag.go Normal file
View File

@ -0,0 +1,137 @@
package etag
import (
"bytes"
"hash/crc32"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/internal/bytebufferpool"
)
// Config defines the config for middleware.
type Config struct {
// Weak indicates that a weak validator is used. Weak etags are easy
// to generate, but are far less useful for comparisons. Strong
// validators are ideal for comparisons but can be very difficult
// to generate efficiently. Weak ETag values of two representations
// of the same resources might be semantically equivalent, but not
// byte-for-byte identical. This means weak etags prevent caching
// when byte range requests are used, but strong etags mean range
// requests can still be cached.
Weak bool
// Next defines a function to skip this middleware when returned true.
//
// Optional. Default: nil
Next func(c *fiber.Ctx) bool
}
// ConfigDefault is the default config
var ConfigDefault = Config{
Weak: false,
Next: nil,
}
var normalizedHeaderETag = []byte("Etag")
var weakPrefix = []byte("W/")
// New creates a new middleware handler
func New(config ...Config) fiber.Handler {
// Set default config
cfg := ConfigDefault
// Override config if provided
if len(config) > 0 {
cfg = config[0]
}
var crc32q = crc32.MakeTable(0xD5828281)
// Return new handler
return func(c *fiber.Ctx) (err error) {
// Don't execute middleware if Next returns true
if cfg.Next != nil && cfg.Next(c) {
return c.Next()
}
// Return err if next handler returns one
if err = c.Next(); err != nil {
return
}
// Don't generate ETags for invalid responses
if c.Response().StatusCode() != fiber.StatusOK {
return
}
body := c.Response().Body()
// Skips ETag if no response body is present
if len(body) <= 0 {
return
}
// Generate ETag for response
bb := bytebufferpool.Get()
defer bytebufferpool.Put(bb)
// Enable weak tag
if cfg.Weak {
_, _ = bb.Write(weakPrefix)
}
_ = bb.WriteByte('"')
bb.B = appendUint(bb.Bytes(), uint32(len(body)))
_ = bb.WriteByte('-')
bb.B = appendUint(bb.Bytes(), crc32.Checksum(body, crc32q))
_ = bb.WriteByte('"')
etag := bb.Bytes()
// Get ETag header from request
clientEtag := c.Request().Header.Peek(fiber.HeaderIfNoneMatch)
// Check if client's ETag is weak
if bytes.HasPrefix(clientEtag, weakPrefix) {
// Check if server's ETag is weak
if bytes.Equal(clientEtag[2:], etag) || bytes.Equal(clientEtag[2:], etag[2:]) {
// W/1 == 1 || W/1 == W/1
c.Context().ResetBody()
return c.SendStatus(fiber.StatusNotModified)
}
// W/1 != W/2 || W/1 != 2
c.Response().Header.SetCanonical(normalizedHeaderETag, etag)
return
}
if bytes.Contains(clientEtag, etag) {
// 1 == 1
c.Context().ResetBody()
return c.SendStatus(fiber.StatusNotModified)
}
// 1 != 2
c.Response().Header.SetCanonical(normalizedHeaderETag, etag)
return
}
}
// appendUint appends n to dst and returns the extended dst.
func appendUint(dst []byte, n uint32) []byte {
var b [20]byte
buf := b[:]
i := len(buf)
var q uint32
for n >= 10 {
i--
q = n / 10
buf[i] = '0' + byte(n-q*10)
n = q
}
i--
buf[i] = '0' + byte(n)
dst = append(dst, buf[i:]...)
return dst
}

View File

@ -0,0 +1,194 @@
package etag
import (
"io/ioutil"
"net/http/httptest"
"testing"
"github.com/valyala/fasthttp"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/utils"
)
// go test -run Test_ETag_Next
func Test_ETag_Next(t *testing.T) {
app := fiber.New()
app.Use(New(Config{
Next: func(_ *fiber.Ctx) bool {
return true
},
}))
resp, err := app.Test(httptest.NewRequest("GET", "/", nil))
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode)
}
// go test -run Test_ETag_SkipError
func Test_ETag_SkipError(t *testing.T) {
app := fiber.New()
app.Use(New())
app.Get("/", func(c *fiber.Ctx) error {
return fiber.ErrForbidden
})
resp, err := app.Test(httptest.NewRequest("GET", "/", nil))
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, fiber.StatusForbidden, resp.StatusCode)
}
// go test -run Test_ETag_NotStatusOK
func Test_ETag_NotStatusOK(t *testing.T) {
app := fiber.New()
app.Use(New())
app.Get("/", func(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusCreated)
})
resp, err := app.Test(httptest.NewRequest("GET", "/", nil))
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, fiber.StatusCreated, resp.StatusCode)
}
// go test -run Test_ETag_NoBody
func Test_ETag_NoBody(t *testing.T) {
app := fiber.New()
app.Use(New())
app.Get("/", func(c *fiber.Ctx) error {
return nil
})
resp, err := app.Test(httptest.NewRequest("GET", "/", nil))
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode)
}
// go test -run Test_ETag_NewEtag
func Test_ETag_NewEtag(t *testing.T) {
t.Run("without HeaderIfNoneMatch", func(t *testing.T) {
testETagNewEtag(t, false, false)
})
t.Run("with HeaderIfNoneMatch and not matched", func(t *testing.T) {
testETagNewEtag(t, true, false)
})
t.Run("with HeaderIfNoneMatch and matched", func(t *testing.T) {
testETagNewEtag(t, true, true)
})
}
func testETagNewEtag(t *testing.T, headerIfNoneMatch, matched bool) {
app := fiber.New()
app.Use(New())
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("Hello, World!")
})
req := httptest.NewRequest("GET", "/", nil)
if headerIfNoneMatch {
etag := `"non-match"`
if matched {
etag = `"13-1831710635"`
}
req.Header.Set(fiber.HeaderIfNoneMatch, etag)
}
resp, err := app.Test(req)
utils.AssertEqual(t, nil, err)
if !headerIfNoneMatch || !matched {
utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode)
utils.AssertEqual(t, `"13-1831710635"`, resp.Header.Get(fiber.HeaderETag))
return
}
if matched {
utils.AssertEqual(t, fiber.StatusNotModified, resp.StatusCode)
b, err := ioutil.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, 0, len(b))
}
}
// go test -run Test_ETag_WeakEtag
func Test_ETag_WeakEtag(t *testing.T) {
t.Run("without HeaderIfNoneMatch", func(t *testing.T) {
testETagWeakEtag(t, false, false)
})
t.Run("with HeaderIfNoneMatch and not matched", func(t *testing.T) {
testETagWeakEtag(t, true, false)
})
t.Run("with HeaderIfNoneMatch and matched", func(t *testing.T) {
testETagWeakEtag(t, true, true)
})
}
func testETagWeakEtag(t *testing.T, headerIfNoneMatch, matched bool) {
app := fiber.New()
app.Use(New(Config{Weak: true}))
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("Hello, World!")
})
req := httptest.NewRequest("GET", "/", nil)
if headerIfNoneMatch {
etag := `W/"non-match"`
if matched {
etag = `W/"13-1831710635"`
}
req.Header.Set(fiber.HeaderIfNoneMatch, etag)
}
resp, err := app.Test(req)
utils.AssertEqual(t, nil, err)
if !headerIfNoneMatch || !matched {
utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode)
utils.AssertEqual(t, `W/"13-1831710635"`, resp.Header.Get(fiber.HeaderETag))
return
}
if matched {
utils.AssertEqual(t, fiber.StatusNotModified, resp.StatusCode)
b, err := ioutil.ReadAll(resp.Body)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, 0, len(b))
}
}
// go test -v -run=^$ -bench=Benchmark_Etag -benchmem -count=4
func Benchmark_Etag(b *testing.B) {
app := fiber.New()
app.Use(New())
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("Hello, World!")
})
h := app.Handler()
fctx := &fasthttp.RequestCtx{}
fctx.Request.Header.SetMethod("GET")
fctx.Request.SetRequestURI("/")
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
h(fctx)
}
utils.AssertEqual(b, 200, fctx.Response.Header.StatusCode())
utils.AssertEqual(b, `"13-1831710635"`, string(fctx.Response.Header.Peek(fiber.HeaderETag)))
}