Compare commits

..

10 Commits

Author SHA1 Message Date
746ce05ae7 add static html pages into releases 2024-07-02 23:57:47 +04:00
5698daa36c feat: templates caching 2024-07-02 23:42:09 +04:00
075bc8f57c chore: move --read-buffer-size back 2024-07-02 15:01:56 +04:00
5a2e678d33 Update README.md (#291)
Add note that the cat template is the only one that fetches external resources.

Closes: #274
2024-07-02 13:16:37 +04:00
ae73819644 go mod tidy 2024-07-02 11:51:54 +04:00
ed427d614f Merge branch 'master' into v3 2024-07-02 00:51:28 -07:00
0f221d4016 docker/build-push-action action updated 2024-07-02 11:48:49 +04:00
d5a51e22e2 the prototype is done 2024-07-02 11:47:35 +04:00
d4b2b5ef96 Merge pull request #290 from tarampampam/dependabot/go_modules/gomod-bdfb6ece4e
build(deps): bump github.com/valyala/fasthttp from 1.54.0 to 1.55.0 in the gomod group
2024-07-01 22:58:34 +00:00
f7bbaf97f0 build(deps): bump github.com/valyala/fasthttp in the gomod group
Bumps the gomod group with 1 update: [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp).


Updates `github.com/valyala/fasthttp` from 1.54.0 to 1.55.0
- [Release notes](https://github.com/valyala/fasthttp/releases)
- [Commits](https://github.com/valyala/fasthttp/compare/1.54.0...v1.55.0)

---
updated-dependencies:
- dependency-name: github.com/valyala/fasthttp
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 22:58:20 +00:00
24 changed files with 353 additions and 70 deletions

View File

@ -44,6 +44,16 @@ jobs:
path: out/ path: out/
if-no-files-found: error if-no-files-found: error
retention-days: 1 retention-days: 1
- if: matrix.os == 'linux' && matrix.arch == 'amd64'
working-directory: ./out
run: zip -r ./../templates.zip .
- if: matrix.os == 'linux' && matrix.arch == 'amd64'
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: templates.zip
asset_name: error-pages-static.zip
tag: ${{ github.ref }}
demo: demo:
name: Update the demo (GitHub Pages) name: Update the demo (GitHub Pages)
@ -76,7 +86,7 @@ jobs:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5 - uses: docker/build-push-action@v6
with: with:
context: . context: .
file: Dockerfile file: Dockerfile

View File

@ -83,7 +83,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- {uses: gacts/github-slug@v1, id: slug} - {uses: gacts/github-slug@v1, id: slug}
- uses: docker/build-push-action@v5 - uses: docker/build-push-action@v6
with: with:
context: . context: .
file: Dockerfile file: Dockerfile

View File

@ -44,6 +44,7 @@ The following flags are supported:
| `--show-details` | show request details in the error page response (if supported by the template) | `false` | `SHOW_DETAILS` | | `--show-details` | show request details in the error page response (if supported by the template) | `false` | `SHOW_DETAILS` |
| `--proxy-headers="…"` | HTTP headers listed here will be proxied from the original request to the error page response (comma-separated list) | `X-Request-Id,X-Trace-Id,X-Amzn-Trace-Id` | `PROXY_HTTP_HEADERS` | | `--proxy-headers="…"` | HTTP headers listed here will be proxied from the original request to the error page response (comma-separated list) | `X-Request-Id,X-Trace-Id,X-Amzn-Trace-Id` | `PROXY_HTTP_HEADERS` |
| `--rotation-mode="…"` | templates automatic rotation mode (disabled/random-on-startup/random-on-each-request/random-hourly/random-daily) | `disabled` | `TEMPLATES_ROTATION_MODE` | | `--rotation-mode="…"` | templates automatic rotation mode (disabled/random-on-startup/random-on-each-request/random-hourly/random-daily) | `disabled` | `TEMPLATES_ROTATION_MODE` |
| `--read-buffer-size="…"` | per-connection buffer size in bytes for reading requests, this also limits the maximum header size (increase this buffer if your clients send multi-KB Request URIs and/or multi-KB headers (e.g., large cookies), note that increasing this value will increase memory consumption) | `5120` | `READ_BUFFER_SIZE` |
### `build` command (aliases: `b`) ### `build` command (aliases: `b`)
@ -88,6 +89,10 @@ The following flags are supported:
The following templates are built-in and available for use without any additional setup: The following templates are built-in and available for use without any additional setup:
> [!NOTE]
> The `cat` template is the only one of those that fetches resources (the actual cat pictures)
> from external servers - all other templates are self-contained.
<table> <table>
<thead> <thead>
<tr> <tr>

2
go.mod
View File

@ -6,6 +6,7 @@ require (
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/urfave/cli-docs/v3 v3.0.0-alpha5 github.com/urfave/cli-docs/v3 v3.0.0-alpha5
github.com/urfave/cli/v3 v3.0.0-alpha9 github.com/urfave/cli/v3 v3.0.0-alpha9
github.com/valyala/fasthttp v1.55.0
go.uber.org/automaxprocs v1.5.3 go.uber.org/automaxprocs v1.5.3
) )
@ -19,7 +20,6 @@ require (
github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.55.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

View File

@ -22,9 +22,9 @@ type command struct {
opt struct { opt struct {
http struct { // our HTTP server http struct { // our HTTP server
addr string addr string
port uint16 port uint16
// readBufferSize uint readBufferSize uint
} }
} }
} }
@ -140,6 +140,15 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
return nil return nil
}, },
} }
readBufferSizeFlag = cli.UintFlag{
Name: "read-buffer-size",
Usage: "per-connection buffer size in bytes for reading requests, this also limits the maximum header size " +
"(increase this buffer if your clients send multi-KB Request URIs and/or multi-KB headers (e.g., " +
"large cookies), note that increasing this value will increase memory consumption)",
Value: 1024 * 5, //nolint:mnd // 5 KB
Sources: env("READ_BUFFER_SIZE"),
OnlyOnce: true,
}
) )
// override some flag usage messages // override some flag usage messages
@ -157,6 +166,7 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
cmd.opt.http.addr = c.String(addrFlag.Name) cmd.opt.http.addr = c.String(addrFlag.Name)
cmd.opt.http.port = uint16(c.Uint(portFlag.Name)) cmd.opt.http.port = uint16(c.Uint(portFlag.Name))
cmd.opt.http.readBufferSize = uint(c.Uint(readBufferSizeFlag.Name))
cfg.L10n.Disable = c.Bool(disableL10nFlag.Name) cfg.L10n.Disable = c.Bool(disableL10nFlag.Name)
cfg.DefaultCodeToRender = uint16(c.Uint(defaultCodeToRenderFlag.Name)) cfg.DefaultCodeToRender = uint16(c.Uint(defaultCodeToRenderFlag.Name))
cfg.RespondWithSameHTTPCode = c.Bool(sendSameHTTPCodeFlag.Name) cfg.RespondWithSameHTTPCode = c.Bool(sendSameHTTPCodeFlag.Name)
@ -282,6 +292,7 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
&showDetailsFlag, &showDetailsFlag,
&proxyHeadersListFlag, &proxyHeadersListFlag,
&rotationModeFlag, &rotationModeFlag,
&readBufferSizeFlag,
}, },
} }
@ -290,7 +301,7 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
// Run current command. // Run current command.
func (cmd *command) Run(ctx context.Context, log *logger.Logger, cfg *config.Config) error { //nolint:funlen func (cmd *command) Run(ctx context.Context, log *logger.Logger, cfg *config.Config) error { //nolint:funlen
var srv = appHttp.NewServer(log) var srv = appHttp.NewServer(log, cmd.opt.http.readBufferSize)
if err := srv.Register(cfg); err != nil { if err := srv.Register(cfg); err != nil {
return err return err

View File

@ -72,7 +72,7 @@ const defaultJSONFormat string = `{
"service_name": {{ service_name | json }}, "service_name": {{ service_name | json }},
"service_port": {{ service_port | json }}, "service_port": {{ service_port | json }},
"request_id": {{ request_id | json }}, "request_id": {{ request_id | json }},
"timestamp": {{ now.Unix }} "timestamp": {{ nowUnix }}
}{{ end }} }{{ end }}
} }
` // an empty line at the end is important for better UX ` // an empty line at the end is important for better UX
@ -91,7 +91,7 @@ const defaultXMLFormat string = `<?xml version="1.0" encoding="utf-8"?>
<serviceName>{{ service_name }}</serviceName> <serviceName>{{ service_name }}</serviceName>
<servicePort>{{ service_port }}</servicePort> <servicePort>{{ service_port }}</servicePort>
<requestID>{{ request_id }}</requestID> <requestID>{{ request_id }}</requestID>
<timestamp>{{ now.Unix }}</timestamp> <timestamp>{{ nowUnix }}</timestamp>
</details>{{ end }} </details>{{ end }}
</error> </error>
` // an empty line at the end is important for better UX ` // an empty line at the end is important for better UX
@ -107,7 +107,7 @@ Ingress Name: {{ ingress_name }}
Service Name: {{ service_name }} Service Name: {{ service_name }}
Service Port: {{ service_port }} Service Port: {{ service_port }}
Request ID: {{ request_id }} Request ID: {{ request_id }}
Timestamp: {{ now.Unix }}{{ end }} Timestamp: {{ nowUnix }}{{ end }}
` // an empty line at the end is important for better UX ` // an empty line at the end is important for better UX
//nolint:lll //nolint:lll

View File

@ -0,0 +1,111 @@
package error_page
import (
"bytes"
"crypto/md5" //nolint:gosec
"encoding/gob"
"sync"
"time"
"gh.tarampamp.am/error-pages/internal/template"
)
type (
// RenderedCache is a cache for rendered error pages. It's safe for concurrent use.
// It uses a hash of the template and props as a key.
//
// To remove expired items, call ClearExpired method periodically (a bit more often than the ttl).
RenderedCache struct {
ttl time.Duration
mu sync.RWMutex
items map[[32]byte]cacheItem // map[template_hash[0:15];props_hash[16:32]]cache_item
}
cacheItem struct {
content []byte
addedAtNano int64
}
)
// NewRenderedCache creates a new RenderedCache with the specified ttl.
func NewRenderedCache(ttl time.Duration) *RenderedCache {
return &RenderedCache{ttl: ttl, items: make(map[[32]byte]cacheItem)}
}
// genKey generates a key for the cache item by hashing the template and props.
func (rc *RenderedCache) genKey(template string, props template.Props) [32]byte {
var (
key [32]byte
th, ph = hash(template), hash(props) // template hash, props hash
)
copy(key[:16], th[:]) // first 16 bytes for the template hash
copy(key[16:], ph[:]) // last 16 bytes for the props hash
return key
}
// Has checks if the cache has an item with the specified template and props.
func (rc *RenderedCache) Has(template string, props template.Props) bool {
var key = rc.genKey(template, props)
rc.mu.RLock()
_, ok := rc.items[key]
rc.mu.RUnlock()
return ok
}
// Put adds a new item to the cache with the specified template, props, and content.
func (rc *RenderedCache) Put(template string, props template.Props, content []byte) {
var key = rc.genKey(template, props)
rc.mu.Lock()
rc.items[key] = cacheItem{content: content, addedAtNano: time.Now().UnixNano()}
rc.mu.Unlock()
}
// Get returns the content of the item with the specified template and props.
func (rc *RenderedCache) Get(template string, props template.Props) ([]byte, bool) {
var key = rc.genKey(template, props)
rc.mu.RLock()
item, ok := rc.items[key]
rc.mu.RUnlock()
return item.content, ok
}
// ClearExpired removes all expired items from the cache.
func (rc *RenderedCache) ClearExpired() {
rc.mu.Lock()
var now = time.Now().UnixNano()
for key, item := range rc.items {
if now-item.addedAtNano > rc.ttl.Nanoseconds() {
delete(rc.items, key)
}
}
rc.mu.Unlock()
}
// Clear removes all items from the cache.
func (rc *RenderedCache) Clear() {
rc.mu.Lock()
clear(rc.items)
rc.mu.Unlock()
}
// hash returns an MD5 hash of the provided value (it may be any built-in type).
func hash(in any) [16]byte {
var b bytes.Buffer
if err := gob.NewEncoder(&b).Encode(in); err != nil {
return [16]byte{} // never happens because we encode only built-in types
}
return md5.Sum(b.Bytes()) //nolint:gosec
}

View File

@ -0,0 +1,86 @@
package error_page_test
import (
"strconv"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/http/handlers/error_page"
"gh.tarampamp.am/error-pages/internal/template"
)
func TestRenderedCache_CRUD(t *testing.T) {
t.Parallel()
var cache = error_page.NewRenderedCache(time.Millisecond)
t.Run("has", func(t *testing.T) {
assert.False(t, cache.Has("template", template.Props{}))
cache.Put("template", template.Props{}, []byte("content"))
assert.True(t, cache.Has("template", template.Props{}))
assert.False(t, cache.Has("template", template.Props{Code: 1}))
assert.False(t, cache.Has("foo", template.Props{Code: 1}))
})
t.Run("exists", func(t *testing.T) {
var got, ok = cache.Get("template", template.Props{})
assert.True(t, ok)
assert.Equal(t, []byte("content"), got)
cache.Clear()
assert.False(t, cache.Has("template", template.Props{}))
})
t.Run("not exists", func(t *testing.T) {
var got, ok = cache.Get("template", template.Props{Code: 2})
assert.False(t, ok)
assert.Nil(t, got)
})
t.Run("race condition provocation", func(t *testing.T) {
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(2)
go func(i int) {
defer wg.Done()
cache.Get("template", template.Props{})
cache.Put("template"+strconv.Itoa(i), template.Props{}, []byte("content"))
cache.Has("template", template.Props{})
}(i)
go func() {
defer wg.Done()
cache.ClearExpired()
}()
}
wg.Wait()
})
}
func TestRenderedCache_Expiring(t *testing.T) {
t.Parallel()
var cache = error_page.NewRenderedCache(10 * time.Millisecond)
cache.Put("template", template.Props{}, []byte("content"))
cache.ClearExpired()
assert.True(t, cache.Has("template", template.Props{}))
<-time.After(10 * time.Millisecond)
assert.True(t, cache.Has("template", template.Props{})) // expired, but not cleared yet
cache.ClearExpired()
assert.False(t, cache.Has("template", template.Props{})) // cleared
}

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"sync"
"sync/atomic" "sync/atomic"
"time" "time"
@ -15,7 +16,32 @@ import (
) )
// New creates a new handler that returns an error page with the specified status code and format. // New creates a new handler that returns an error page with the specified status code and format.
func New(cfg *config.Config, log *logger.Logger) fasthttp.RequestHandler { //nolint:funlen,gocognit,gocyclo func New(cfg *config.Config, log *logger.Logger) (_ fasthttp.RequestHandler, closeCache func()) { //nolint:funlen,gocognit,gocyclo,lll
// if the ttl will be bigger than 1 second, the template functions like `nowUnix` will not work as expected
const cacheTtl = 900 * time.Millisecond // the cache TTL
var (
cache, stopCh = NewRenderedCache(cacheTtl), make(chan struct{})
stopOnce sync.Once
)
// run a goroutine that will clear the cache from expired items. to stop the goroutine - close the stop channel
// or call the closeCache
go func() {
var timer = time.NewTimer(cacheTtl)
defer func() { timer.Stop(); cache.Clear() }()
for {
select {
case <-timer.C:
cache.ClearExpired()
timer.Reset(cacheTtl)
case <-stopCh:
return
}
}
}()
return func(ctx *fasthttp.RequestCtx) { return func(ctx *fasthttp.RequestCtx) {
var ( var (
reqHeaders = &ctx.Request.Header reqHeaders = &ctx.Request.Header
@ -106,57 +132,82 @@ func New(cfg *config.Config, log *logger.Logger) fasthttp.RequestHandler { //nol
switch { switch {
case format == jsonFormat && cfg.Formats.JSON != "": case format == jsonFormat && cfg.Formats.JSON != "":
if content, err := template.Render(cfg.Formats.JSON, tplProps); err != nil { if cached, ok := cache.Get(cfg.Formats.JSON, tplProps); ok { // cache hit
j, _ := json.Marshal(fmt.Sprintf("Failed to render the JSON template: %s", err.Error())) write(ctx, log, cached)
write(ctx, log, j) } else { // cache miss
} else { if content, err := template.Render(cfg.Formats.JSON, tplProps); err != nil {
write(ctx, log, content) errAsJson, _ := json.Marshal(fmt.Sprintf("Failed to render the JSON template: %s", err.Error()))
write(ctx, log, errAsJson) // error during rendering
} else {
cache.Put(cfg.Formats.JSON, tplProps, []byte(content))
write(ctx, log, content) // rendered successfully
}
} }
case format == xmlFormat && cfg.Formats.XML != "": case format == xmlFormat && cfg.Formats.XML != "":
if content, err := template.Render(cfg.Formats.XML, tplProps); err != nil { if cached, ok := cache.Get(cfg.Formats.XML, tplProps); ok { // cache hit
write(ctx, log, fmt.Sprintf( write(ctx, log, cached)
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<error>Failed to render the XML template: %s</error>", err.Error(), } else { // cache miss
)) if content, err := template.Render(cfg.Formats.XML, tplProps); err != nil {
} else { write(ctx, log, fmt.Sprintf(
write(ctx, log, content) "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<error>Failed to render the XML template: %s</error>\n", err.Error(),
))
} else {
cache.Put(cfg.Formats.XML, tplProps, []byte(content))
write(ctx, log, content)
}
} }
case format == htmlFormat: case format == htmlFormat:
var templateName = templateToUse(cfg) var templateName = templateToUse(cfg)
if tpl, found := cfg.Templates.Get(templateName); found { if tpl, found := cfg.Templates.Get(templateName); found { //nolint:nestif
if content, err := template.Render(tpl, tplProps); err != nil { if cached, ok := cache.Get(tpl, tplProps); ok { // cache hit
// TODO: add GZIP compression for the HTML content support write(ctx, log, cached)
write(ctx, log, fmt.Sprintf( } else { // cache miss
"<!DOCTYPE html>\n<html><body>Failed to render the HTML template %s: %s</body></html>", if content, err := template.Render(tpl, tplProps); err != nil {
templateName, // TODO: add GZIP compression for the HTML content support
err.Error(), write(ctx, log, fmt.Sprintf(
)) "<!DOCTYPE html>\n<html><body>Failed to render the HTML template %s: %s</body></html>\n",
} else { templateName,
write(ctx, log, content) err.Error(),
))
} else {
cache.Put(tpl, tplProps, []byte(content))
write(ctx, log, content)
}
} }
} else { } else {
write(ctx, log, fmt.Sprintf( write(ctx, log, fmt.Sprintf(
"<!DOCTYPE html>\n<html><body>Template %s not found and cannot be used</body></html>", templateName, "<!DOCTYPE html>\n<html><body>Template %s not found and cannot be used</body></html>\n", templateName,
)) ))
} }
default: // plainTextFormat as default default: // plainTextFormat as default
if cfg.Formats.PlainText != "" { if cfg.Formats.PlainText != "" { //nolint:nestif
if content, err := template.Render(cfg.Formats.PlainText, tplProps); err != nil { if cached, ok := cache.Get(cfg.Formats.PlainText, tplProps); ok { // cache hit
write(ctx, log, fmt.Sprintf("Failed to render the PlainText template: %s", err.Error())) write(ctx, log, cached)
} else { } else { // cache miss
write(ctx, log, content) if content, err := template.Render(cfg.Formats.PlainText, tplProps); err != nil {
write(ctx, log, fmt.Sprintf("Failed to render the PlainText template: %s", err.Error()))
} else {
cache.Put(cfg.Formats.PlainText, tplProps, []byte(content))
write(ctx, log, content)
}
} }
} else { } else {
write(ctx, log, `The requested content format is not supported. write(ctx, log, `The requested content format is not supported.
Please create an issue on the project's GitHub page to request support for this format. Please create an issue on the project's GitHub page to request support for this format.
Supported formats: JSON, XML, HTML, Plain Text`) Supported formats: JSON, XML, HTML, Plain Text
`)
} }
} }
} }, func() { stopOnce.Do(func() { close(stopCh) }) }
} }
var ( var (

View File

@ -159,7 +159,8 @@ func TestHandler(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
t.Parallel() t.Parallel()
var handler = error_page.New(tt.giveConfig(), logger.NewNop()) var handler, closeCache = error_page.New(tt.giveConfig(), logger.NewNop())
defer closeCache()
req, reqErr := http.NewRequest(http.MethodGet, tt.giveUrl, http.NoBody) req, reqErr := http.NewRequest(http.MethodGet, tt.giveUrl, http.NoBody)
require.NoError(t, reqErr) require.NoError(t, reqErr)
@ -202,9 +203,11 @@ func TestRotationModeOnEachRequest(t *testing.T) {
lastResponseBody string lastResponseBody string
changedTimes int changedTimes int
handler = error_page.New(&cfg, logger.NewNop()) handler, closeCache = error_page.New(&cfg, logger.NewNop())
) )
defer func() { closeCache(); closeCache(); closeCache() }() // multiple calls should not panic
for range 300 { for range 300 {
req, reqErr := http.NewRequest(http.MethodGet, "http://testing/", http.NoBody) req, reqErr := http.NewRequest(http.MethodGet, "http://testing/", http.NoBody)
require.NoError(t, reqErr) require.NoError(t, reqErr)

View File

@ -23,16 +23,16 @@ import (
// Server is an HTTP server for serving error pages. // Server is an HTTP server for serving error pages.
type Server struct { type Server struct {
log *logger.Logger log *logger.Logger
server *fasthttp.Server server *fasthttp.Server
beforeStop func()
} }
// NewServer creates a new HTTP server. // NewServer creates a new HTTP server.
func NewServer(log *logger.Logger) Server { func NewServer(log *logger.Logger, readBufferSize uint) Server {
const ( const (
readTimeout = 30 * time.Second readTimeout = 30 * time.Second
writeTimeout = readTimeout + 10*time.Second // should be bigger than the read timeout writeTimeout = readTimeout + 10*time.Second // should be bigger than the read timeout
maxHeaderBytes = (1 << 20) * 5 //nolint:mnd // 5 MB
) )
return Server{ return Server{
@ -40,27 +40,32 @@ func NewServer(log *logger.Logger) Server {
server: &fasthttp.Server{ server: &fasthttp.Server{
ReadTimeout: readTimeout, ReadTimeout: readTimeout,
WriteTimeout: writeTimeout, WriteTimeout: writeTimeout,
ReadBufferSize: maxHeaderBytes, ReadBufferSize: int(readBufferSize),
DisablePreParseMultipartForm: true, DisablePreParseMultipartForm: true,
NoDefaultServerHeader: true, NoDefaultServerHeader: true,
CloseOnShutdown: true, CloseOnShutdown: true,
Logger: logger.NewStdLog(log), Logger: logger.NewStdLog(log),
}, },
beforeStop: func() {}, // noop
} }
} }
// Register server handlers, middlewares, etc. // Register server handlers, middlewares, etc.
func (s *Server) Register(cfg *config.Config) error { func (s *Server) Register(cfg *config.Config) error {
var ( var (
liveHandler = live.New() liveHandler = live.New()
versionHandler = version.New(appmeta.Version()) versionHandler = version.New(appmeta.Version())
errorPagesHandler = ep.New(cfg, s.log) faviconHandler = static.New(static.Favicon)
faviconHandler = static.New(static.Favicon)
errorPagesHandler, closeCache = ep.New(cfg, s.log)
notFound = http.StatusText(http.StatusNotFound) + "\n" notFound = http.StatusText(http.StatusNotFound) + "\n"
notAllowed = http.StatusText(http.StatusMethodNotAllowed) + "\n" notAllowed = http.StatusText(http.StatusMethodNotAllowed) + "\n"
) )
// wrap the before shutdown function to close the cache
s.beforeStop = closeCache
s.server.Handler = func(ctx *fasthttp.RequestCtx) { s.server.Handler = func(ctx *fasthttp.RequestCtx) {
var url, method = string(ctx.Path()), string(ctx.Method()) var url, method = string(ctx.Path()), string(ctx.Method())
@ -135,5 +140,7 @@ func (s *Server) Stop(timeout time.Duration) error {
var ctx, cancel = context.WithTimeout(context.Background(), timeout) var ctx, cancel = context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
s.beforeStop()
return s.server.ShutdownWithContext(ctx) return s.server.ShutdownWithContext(ctx)
} }

View File

@ -20,7 +20,7 @@ import (
// TestRouting in fact is a test for the whole server, because it tests all the routes and their handlers. // TestRouting in fact is a test for the whole server, because it tests all the routes and their handlers.
func TestRouting(t *testing.T) { func TestRouting(t *testing.T) {
var ( var (
srv = appHttp.NewServer(logger.NewNop()) srv = appHttp.NewServer(logger.NewNop(), 1025*5)
cfg = config.New() cfg = config.New()
) )
@ -38,7 +38,7 @@ func TestRouting(t *testing.T) {
Service Name: {{ service_name }} Service Name: {{ service_name }}
Service Port: {{ service_port }} Service Port: {{ service_port }}
Request ID: {{ request_id }} Request ID: {{ request_id }}
Timestamp: {{ now.Unix }} Timestamp: {{ nowUnix }}
</pre>{{ end }} </pre>{{ end }}
</html>`)) </html>`))

View File

@ -16,10 +16,9 @@ import (
) )
var builtInFunctions = template.FuncMap{ //nolint:gochecknoglobals var builtInFunctions = template.FuncMap{ //nolint:gochecknoglobals
// current time: // the current time in unix format (seconds since 1970 UTC):
// `{{ now.Unix }}` // `1631610000` // `{{ nowUnix }}` // `1631610000`
// `{{ now.Hour }}:{{ now.Minute }}:{{ now.Second }}` // `15:4:5` "nowUnix": func() int64 { return time.Now().Unix() },
"now": time.Now,
// current hostname: // current hostname:
// `{{ hostname }}` // `localhost` // `{{ hostname }}` // `localhost`

View File

@ -27,7 +27,7 @@ func TestRender_BuiltInFunction(t *testing.T) {
wantErrMsg string wantErrMsg string
}{ }{
"now (unix)": { "now (unix)": {
giveTemplate: `{{ now.Unix }}`, giveTemplate: `{{ nowUnix }}`,
wantResult: strconv.Itoa(int(time.Now().Unix())), wantResult: strconv.Itoa(int(time.Now().Unix())),
}, },
"hostname": {giveTemplate: `{{ hostname }}`, wantResult: hostname}, "hostname": {giveTemplate: `{{ hostname }}`, wantResult: hostname},

View File

@ -341,7 +341,7 @@
<!-- {{- end }}{{ if request_id -}} --> <!-- {{- end }}{{ if request_id -}} -->
<li><span data-l10n>Request ID</span>: <code>{{ request_id }}</code></li> <li><span data-l10n>Request ID</span>: <code>{{ request_id }}</code></li>
<!-- {{- end -}} --> <!-- {{- end -}} -->
<li><span data-l10n>Timestamp</span>: <code>{{ now.Unix }}</code></li> <li><span data-l10n>Timestamp</span>: <code>{{ nowUnix }}</code></li>
</ul> </ul>
</div> </div>
<!-- {{- end -}} --> <!-- {{- end -}} -->

View File

@ -150,7 +150,7 @@
<!-- {{- end -}} --> <!-- {{- end -}} -->
<tr> <tr>
<td class="name" data-l10n>Timestamp</td> <td class="name" data-l10n>Timestamp</td>
<td class="value">{{ now.Unix }}</td> <td class="value">{{ nowUnix }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -327,7 +327,7 @@
<!-- {{- end }}{{ if request_id -}} --> <!-- {{- end }}{{ if request_id -}} -->
<li><span data-l10n>Request ID</span>: <code>{{ request_id }}</code></li> <li><span data-l10n>Request ID</span>: <code>{{ request_id }}</code></li>
<!-- {{- end -}} --> <!-- {{- end -}} -->
<li><span data-l10n>Timestamp</span>: <code>{{ now.Unix }}</code></li> <li><span data-l10n>Timestamp</span>: <code>{{ nowUnix }}</code></li>
</ul> </ul>
</div> </div>
<!-- {{- end -}} --> <!-- {{- end -}} -->

View File

@ -235,7 +235,7 @@
<!-- {{- end -}} --> <!-- {{- end -}} -->
<tr> <tr>
<td class="name" data-l10n>Timestamp</td> <td class="name" data-l10n>Timestamp</td>
<td class="value">{{ now.Unix }}</td> <td class="value">{{ nowUnix }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -174,7 +174,7 @@
<!-- {{- end }}{{ if request_id -}} --> <!-- {{- end }}{{ if request_id -}} -->
<p class="output small"><span data-l10n>Request ID</span>: <code>{{ request_id }}</code></p> <p class="output small"><span data-l10n>Request ID</span>: <code>{{ request_id }}</code></p>
<!-- {{- end -}} --> <!-- {{- end -}} -->
<p class="output small"><span data-l10n>Timestamp</span>: <code>{{ now.Unix }}</code></p> <p class="output small"><span data-l10n>Timestamp</span>: <code>{{ nowUnix }}</code></p>
</div> </div>
<!-- {{- end -}} --> <!-- {{- end -}} -->
</main> </main>

View File

@ -157,7 +157,7 @@
<!-- {{- end }}{{ if request_id -}} --> <!-- {{- end }}{{ if request_id -}} -->
<li class="value">{{ request_id }}</li> <li class="value">{{ request_id }}</li>
<!-- {{- end -}} --> <!-- {{- end -}} -->
<li class="value">{{ now.Unix }}</li> <li class="value">{{ nowUnix }}</li>
</ul> </ul>
<!-- {{- end -}} --> <!-- {{- end -}} -->
</div> </div>

View File

@ -442,7 +442,7 @@
<!-- {{- end }}{{ if request_id -}} --> <!-- {{- end }}{{ if request_id -}} -->
<li><span data-l10n>Request ID</span>: <code>{{ request_id }}</code></li> <li><span data-l10n>Request ID</span>: <code>{{ request_id }}</code></li>
<!-- {{- end -}} --> <!-- {{- end -}} -->
<li><span data-l10n>Timestamp</span>: <code>{{ now.Unix }}</code></li> <li><span data-l10n>Timestamp</span>: <code>{{ nowUnix }}</code></li>
</ul> </ul>
<!-- {{- end -}} --> <!-- {{- end -}} -->
</div> </div>

View File

@ -9,7 +9,7 @@
{{ if service_name }}Service name: {{ service_name }}{{ end }} {{ if service_name }}Service name: {{ service_name }}{{ end }}
{{ if service_port }}Service port: {{ service_port }}{{ end }} {{ if service_port }}Service port: {{ service_port }}{{ end }}
{{ if request_id }}Request ID: {{ request_id }}{{ end }} {{ if request_id }}Request ID: {{ request_id }}{{ end }}
Timestamp: {{ now.Unix }} Timestamp: {{ nowUnix }}
{{ end }} {{ end }}
--> -->
<html lang="en"> <html lang="en">

View File

@ -253,7 +253,7 @@
<!-- {{- end -}} --> <!-- {{- end -}} -->
<tr> <tr>
<td class="name" data-l10n>Timestamp</td> <td class="name" data-l10n>Timestamp</td>
<td class="value">{{ now.Unix }}</td> <td class="value">{{ nowUnix }}</td>
</tr> </tr>
</table> </table>
</div> </div>

View File

@ -167,7 +167,7 @@
<!-- {{- end -}} --> <!-- {{- end -}} -->
<tr> <tr>
<td class="name"><span data-l10n>Timestamp</span>:</td> <td class="name"><span data-l10n>Timestamp</span>:</td>
<td class="value">{{ now.Unix }}</td> <td class="value">{{ nowUnix }}</td>
</tr> </tr>
</table> </table>
<!-- {{- end -}} --> <!-- {{- end -}} -->