mirror of
https://github.com/gofiber/fiber.git
synced 2025-02-24 14:44:11 +00:00
commit
0bbce8f988
63
middleware/etag/README.md
Normal file
63
middleware/etag/README.md
Normal 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
137
middleware/etag/etag.go
Normal 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
|
||||
}
|
194
middleware/etag/etag_test.go
Normal file
194
middleware/etag/etag_test.go
Normal 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)))
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user