mirror of
https://github.com/tarampampam/error-pages.git
synced 2024-08-30 18:22:40 +00:00
wip: 🔕 temporary commit
This commit is contained in:
parent
b71475fcf7
commit
669aaf6a1e
@ -7,10 +7,6 @@ FROM docker.io/library/golang:1.22-bookworm AS develop
|
||||
ENV GOPATH="/var/tmp/go"
|
||||
|
||||
RUN set -x \
|
||||
# renovate: source=github-releases name=abice/go-enum
|
||||
&& ABICE_GOENUM_VERSION="0.6.0" \
|
||||
&& GOBIN=/bin go install "github.com/abice/go-enum@v${ABICE_GOENUM_VERSION}" \
|
||||
&& GOBIN=/bin go install golang.org/x/tools/cmd/goimports@latest \
|
||||
&& GOBIN=/bin go install gotest.tools/gotestsum@latest \
|
||||
&& go clean -cache -modcache \
|
||||
# renovate: source=github-releases name=golangci/golangci-lint
|
||||
|
2
go.mod
2
go.mod
@ -1,6 +1,6 @@
|
||||
module gh.tarampamp.am/error-pages
|
||||
|
||||
go 1.21
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/a8m/envsubst v1.4.2
|
||||
|
@ -2,15 +2,18 @@ package serve
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/cli/shared"
|
||||
"gh.tarampamp.am/error-pages/internal/config"
|
||||
appHttp "gh.tarampamp.am/error-pages/internal/http"
|
||||
)
|
||||
|
||||
type command struct {
|
||||
@ -18,9 +21,9 @@ type command struct {
|
||||
|
||||
opt struct {
|
||||
http struct { // our HTTP server
|
||||
addr string
|
||||
port uint16
|
||||
readBufferSize uint
|
||||
addr string
|
||||
port uint16
|
||||
// readBufferSize uint
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -28,8 +31,9 @@ type command struct {
|
||||
// NewCommand creates `serve` command.
|
||||
func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
|
||||
var (
|
||||
cmd command
|
||||
cfg = config.New()
|
||||
cmd command
|
||||
cfg = config.New()
|
||||
env, trim = cli.EnvVars, cli.StringConfig{TrimSpace: true}
|
||||
)
|
||||
|
||||
var (
|
||||
@ -40,38 +44,38 @@ func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
|
||||
jsonFormatFlag = cli.StringFlag{
|
||||
Name: "json-format",
|
||||
Usage: "override the default error page response in JSON format (Go templates are supported)",
|
||||
Sources: cli.EnvVars("RESPONSE_JSON_FORMAT"),
|
||||
Sources: env("RESPONSE_JSON_FORMAT"),
|
||||
OnlyOnce: true,
|
||||
Config: cli.StringConfig{TrimSpace: true},
|
||||
Config: trim,
|
||||
}
|
||||
xmlFormatFlag = cli.StringFlag{
|
||||
Name: "xml-format",
|
||||
Usage: "override the default error page response in XML format (Go templates are supported)",
|
||||
Sources: cli.EnvVars("RESPONSE_XML_FORMAT"),
|
||||
Sources: env("RESPONSE_XML_FORMAT"),
|
||||
OnlyOnce: true,
|
||||
Config: cli.StringConfig{TrimSpace: true},
|
||||
Config: trim,
|
||||
}
|
||||
templateNameFlag = cli.StringFlag{
|
||||
Name: "template-name",
|
||||
Aliases: []string{"t"},
|
||||
Value: cfg.TemplateName,
|
||||
Usage: "name of the template to use for rendering error pages",
|
||||
Sources: cli.EnvVars("TEMPLATE_NAME"),
|
||||
Sources: env("TEMPLATE_NAME"),
|
||||
OnlyOnce: true,
|
||||
Config: cli.StringConfig{TrimSpace: true},
|
||||
Config: trim,
|
||||
}
|
||||
disableL10nFlag = cli.BoolFlag{
|
||||
Name: "disable-l10n",
|
||||
Usage: "disable localization of error pages (if the template supports localization)",
|
||||
Value: cfg.L10n.Disable,
|
||||
Sources: cli.EnvVars("DISABLE_L10N"),
|
||||
Sources: env("DISABLE_L10N"),
|
||||
OnlyOnce: true,
|
||||
}
|
||||
defaultCodeToRenderFlag = cli.UintFlag{
|
||||
Name: "default-error-page",
|
||||
Usage: "the code of the default (index page, when a code is not specified) error page to render",
|
||||
Value: uint64(cfg.Default.CodeToRender),
|
||||
Sources: cli.EnvVars("DEFAULT_ERROR_PAGE"),
|
||||
Sources: env("DEFAULT_ERROR_PAGE"),
|
||||
Validator: func(code uint64) error {
|
||||
if code > 999 { //nolint:mnd
|
||||
return fmt.Errorf("wrong HTTP code [%d] for the default error page", code)
|
||||
@ -85,7 +89,7 @@ func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
|
||||
Name: "default-http-code",
|
||||
Usage: "the default (index page, when a code is not specified) HTTP response code",
|
||||
Value: uint64(cfg.Default.HttpCode),
|
||||
Sources: cli.EnvVars("DEFAULT_HTTP_CODE"),
|
||||
Sources: env("DEFAULT_HTTP_CODE"),
|
||||
Validator: defaultCodeToRenderFlag.Validator,
|
||||
OnlyOnce: true,
|
||||
}
|
||||
@ -93,7 +97,7 @@ func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
|
||||
Name: "show-details",
|
||||
Usage: "show request details in the error page response (if supported by the template)",
|
||||
Value: cfg.ShowDetails,
|
||||
Sources: cli.EnvVars("SHOW_DETAILS"),
|
||||
Sources: env("SHOW_DETAILS"),
|
||||
OnlyOnce: true,
|
||||
}
|
||||
proxyHeadersListFlag = cli.StringFlag{
|
||||
@ -101,7 +105,7 @@ func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
|
||||
Usage: "listed here HTTP headers will be proxied from the original request to the error page response " +
|
||||
"(comma-separated list)",
|
||||
Value: strings.Join(cfg.ProxyHeaders, ","),
|
||||
Sources: cli.EnvVars("PROXY_HTTP_HEADERS"),
|
||||
Sources: env("PROXY_HTTP_HEADERS"),
|
||||
Validator: func(s string) error {
|
||||
for _, raw := range strings.Split(s, ",") {
|
||||
if clean := strings.TrimSpace(raw); strings.ContainsRune(clean, ' ') {
|
||||
@ -112,17 +116,33 @@ func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
|
||||
return nil
|
||||
},
|
||||
OnlyOnce: true,
|
||||
Config: cli.StringConfig{TrimSpace: true},
|
||||
Config: trim,
|
||||
}
|
||||
readBufferSizeFlag = cli.UintFlag{
|
||||
Name: "read-buffer-size",
|
||||
Usage: "customize the HTTP read buffer size (set per connection for reading requests, also limits the " +
|
||||
"maximum header size; consider increasing it if your clients send multi-KB request URIs or multi-KB " +
|
||||
"headers, such as large cookies)",
|
||||
DefaultText: "not set",
|
||||
Sources: cli.EnvVars("READ_BUFFER_SIZE"),
|
||||
OnlyOnce: true,
|
||||
rotationModeFlag = cli.StringFlag{
|
||||
Name: "rotation-mode",
|
||||
Value: config.RotationModeDisabled.String(),
|
||||
Usage: "templates automatic rotation mode (" + strings.Join(config.RotationModeStrings(), "/") + ")",
|
||||
Sources: env("TEMPLATES_ROTATION_MODE"),
|
||||
OnlyOnce: true,
|
||||
Config: trim,
|
||||
Validator: func(s string) error {
|
||||
if _, err := config.ParseRotationMode(s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// readBufferSizeFlag = cli.UintFlag{
|
||||
// Name: "read-buffer-size",
|
||||
// Usage: "customize the HTTP read buffer size (set per connection for reading requests, also limits the " +
|
||||
// "maximum header size; consider increasing it if your clients send multi-KB request URIs or multi-KB " +
|
||||
// "headers, such as large cookies)",
|
||||
// DefaultText: "not set",
|
||||
// Sources: cli.EnvVars("READ_BUFFER_SIZE"),
|
||||
// OnlyOnce: true,
|
||||
// }
|
||||
)
|
||||
|
||||
cmd.c = &cli.Command{
|
||||
@ -133,12 +153,13 @@ func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
cmd.opt.http.addr = c.String(addrFlag.Name)
|
||||
cmd.opt.http.port = uint16(c.Uint(portFlag.Name))
|
||||
cmd.opt.http.readBufferSize = uint(c.Uint(readBufferSizeFlag.Name))
|
||||
// cmd.opt.http.readBufferSize = uint(c.Uint(readBufferSizeFlag.Name))
|
||||
|
||||
cfg.TemplateName = c.String(templateNameFlag.Name)
|
||||
cfg.L10n.Disable = c.Bool(disableL10nFlag.Name)
|
||||
cfg.Default.CodeToRender = uint16(c.Uint(defaultCodeToRenderFlag.Name))
|
||||
cfg.Default.HttpCode = uint16(c.Uint(defaultHTTPCodeFlag.Name))
|
||||
cfg.RotationMode, _ = config.ParseRotationMode(c.String(rotationModeFlag.Name))
|
||||
cfg.ShowDetails = c.Bool(showDetailsFlag.Name)
|
||||
|
||||
if c.IsSet(proxyHeadersListFlag.Name) {
|
||||
@ -231,7 +252,8 @@ func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
|
||||
&defaultHTTPCodeFlag,
|
||||
&showDetailsFlag,
|
||||
&proxyHeadersListFlag,
|
||||
&readBufferSizeFlag,
|
||||
&rotationModeFlag,
|
||||
// &readBufferSizeFlag,
|
||||
},
|
||||
}
|
||||
|
||||
@ -240,5 +262,47 @@ func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
|
||||
|
||||
// Run current command.
|
||||
func (cmd *command) Run(ctx context.Context, log *zap.Logger, cfg *config.Config) error {
|
||||
return nil // TODO: implement
|
||||
var srv = appHttp.NewServer(ctx, log)
|
||||
|
||||
if err := srv.Register(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var startingErrCh = make(chan error, 1) // channel for server starting error
|
||||
defer close(startingErrCh)
|
||||
|
||||
// start HTTP server in separate goroutine
|
||||
go func(errCh chan<- error) {
|
||||
var now = time.Now()
|
||||
|
||||
defer func() {
|
||||
log.Info("HTTP server stopped", zap.Duration("uptime", time.Since(now).Round(time.Millisecond)))
|
||||
}()
|
||||
|
||||
log.Info("HTTP server starting",
|
||||
zap.String("addr", cmd.opt.http.addr),
|
||||
zap.Uint16("port", cmd.opt.http.port),
|
||||
)
|
||||
|
||||
if err := srv.Start(cmd.opt.http.addr, cmd.opt.http.port); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
errCh <- err
|
||||
}
|
||||
}(startingErrCh)
|
||||
|
||||
// and wait for...
|
||||
select {
|
||||
case err := <-startingErrCh: // ..server starting error
|
||||
return err
|
||||
|
||||
case <-ctx.Done(): // ..or context cancellation
|
||||
const shutdownTimeout = 5 * time.Second
|
||||
|
||||
log.Info("HTTP server stopping", zap.Duration("with timeout", shutdownTimeout))
|
||||
|
||||
if err := srv.Stop(shutdownTimeout); err != nil { //nolint:contextcheck
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -47,6 +47,10 @@ type Config struct {
|
||||
HttpCode uint16
|
||||
}
|
||||
|
||||
// RotationMode allows to set the rotation mode for templates to switch between them automatically on startup,
|
||||
// on each request, daily, hourly and so on.
|
||||
RotationMode RotationMode
|
||||
|
||||
// ShowDetails determines whether to show additional details in the error response, extracted from the
|
||||
// incoming request (if supported by the template).
|
||||
ShowDetails bool
|
||||
|
87
internal/config/rotation_mode.go
Normal file
87
internal/config/rotation_mode.go
Normal file
@ -0,0 +1,87 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RotationMode represents the rotation mode for templates.
|
||||
type RotationMode byte
|
||||
|
||||
const (
|
||||
RotationModeDisabled RotationMode = iota // do not rotate templates, default
|
||||
RotationModeRandomOnStartup // pick a random template on startup
|
||||
RotationModeRandomOnEachRequest // pick a random template on each request
|
||||
RotationModeRandomDaily // once a day switch to a random template
|
||||
RotationModeRandomHourly // once an hour switch to a random template
|
||||
)
|
||||
|
||||
// String returns a human-readable representation of the rotation mode.
|
||||
func (rm RotationMode) String() string {
|
||||
switch rm {
|
||||
case RotationModeDisabled:
|
||||
return "disabled"
|
||||
case RotationModeRandomOnStartup:
|
||||
return "random-on-startup"
|
||||
case RotationModeRandomOnEachRequest:
|
||||
return "random-on-each-request"
|
||||
case RotationModeRandomDaily:
|
||||
return "random-daily"
|
||||
case RotationModeRandomHourly:
|
||||
return "random-hourly"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("RotationMode(%d)", rm)
|
||||
}
|
||||
|
||||
// RotationModes returns a slice of all rotation modes.
|
||||
func RotationModes() []RotationMode {
|
||||
return []RotationMode{
|
||||
RotationModeDisabled,
|
||||
RotationModeRandomOnStartup,
|
||||
RotationModeRandomOnEachRequest,
|
||||
RotationModeRandomDaily,
|
||||
RotationModeRandomHourly,
|
||||
}
|
||||
}
|
||||
|
||||
// RotationModeStrings returns a slice of all rotation modes as strings.
|
||||
func RotationModeStrings() []string {
|
||||
var (
|
||||
modes = RotationModes()
|
||||
result = make([]string, len(modes))
|
||||
)
|
||||
|
||||
for i := range modes {
|
||||
result[i] = modes[i].String()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ParseRotationMode parses a rotation mode (case is ignored) based on the ASCII representation of the rotation mode.
|
||||
// If the provided ASCII representation is invalid an error is returned.
|
||||
func ParseRotationMode[T string | []byte](text T) (RotationMode, error) {
|
||||
var mode string
|
||||
|
||||
if s, ok := any(text).(string); ok {
|
||||
mode = s
|
||||
} else {
|
||||
mode = string(any(text).([]byte))
|
||||
}
|
||||
|
||||
switch strings.ToLower(mode) {
|
||||
case RotationModeDisabled.String(), "":
|
||||
return RotationModeDisabled, nil // the empty string makes sense
|
||||
case RotationModeRandomOnStartup.String():
|
||||
return RotationModeRandomOnStartup, nil
|
||||
case RotationModeRandomOnEachRequest.String():
|
||||
return RotationModeRandomOnEachRequest, nil
|
||||
case RotationModeRandomDaily.String():
|
||||
return RotationModeRandomDaily, nil
|
||||
case RotationModeRandomHourly.String():
|
||||
return RotationModeRandomHourly, nil
|
||||
}
|
||||
|
||||
return RotationMode(0), fmt.Errorf("unrecognized rotation mode: %q", mode)
|
||||
}
|
90
internal/config/rotation_mode_test.go
Normal file
90
internal/config/rotation_mode_test.go
Normal file
@ -0,0 +1,90 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/config"
|
||||
)
|
||||
|
||||
func TestRotationMode_String(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Equal(t, "disabled", config.RotationModeDisabled.String())
|
||||
assert.Equal(t, "random-on-startup", config.RotationModeRandomOnStartup.String())
|
||||
assert.Equal(t, "random-on-each-request", config.RotationModeRandomOnEachRequest.String())
|
||||
assert.Equal(t, "random-daily", config.RotationModeRandomDaily.String())
|
||||
assert.Equal(t, "random-hourly", config.RotationModeRandomHourly.String())
|
||||
|
||||
assert.Equal(t, "RotationMode(255)", config.RotationMode(255).String())
|
||||
}
|
||||
|
||||
func TestRotationModes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Equal(t, []config.RotationMode{
|
||||
config.RotationModeDisabled,
|
||||
config.RotationModeRandomOnStartup,
|
||||
config.RotationModeRandomOnEachRequest,
|
||||
config.RotationModeRandomDaily,
|
||||
config.RotationModeRandomHourly,
|
||||
}, config.RotationModes())
|
||||
}
|
||||
|
||||
func TestRotationModeStrings(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Equal(t, []string{
|
||||
"disabled",
|
||||
"random-on-startup",
|
||||
"random-on-each-request",
|
||||
"random-daily",
|
||||
"random-hourly",
|
||||
}, config.RotationModeStrings())
|
||||
}
|
||||
|
||||
func TestParseRotationMode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for name, _tt := range map[string]struct {
|
||||
giveBytes []byte
|
||||
giveString string
|
||||
wantMode config.RotationMode
|
||||
wantErrorMsg string
|
||||
}{
|
||||
"<empty string>": {giveString: "", wantMode: config.RotationModeDisabled},
|
||||
"<empty bytes>": {giveBytes: []byte(""), wantMode: config.RotationModeDisabled},
|
||||
"disabled": {giveString: "disabled", wantMode: config.RotationModeDisabled},
|
||||
"disabled (bytes)": {giveBytes: []byte("disabled"), wantMode: config.RotationModeDisabled},
|
||||
"random-on-startup": {giveString: "random-on-startup", wantMode: config.RotationModeRandomOnStartup},
|
||||
"random-on-startup (bytes)": {giveBytes: []byte("random-on-startup"), wantMode: config.RotationModeRandomOnStartup},
|
||||
"on-each-request": {giveString: "random-on-each-request", wantMode: config.RotationModeRandomOnEachRequest},
|
||||
"daily": {giveString: "random-daily", wantMode: config.RotationModeRandomDaily},
|
||||
"hourly": {giveString: "random-hourly", wantMode: config.RotationModeRandomHourly},
|
||||
|
||||
"foobar": {giveString: "foobar", wantErrorMsg: "unrecognized rotation mode: \"foobar\""},
|
||||
} {
|
||||
tt := _tt
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var (
|
||||
mode config.RotationMode
|
||||
err error
|
||||
)
|
||||
|
||||
if tt.giveString != "" || tt.giveBytes == nil {
|
||||
mode, err = config.ParseRotationMode(tt.giveString)
|
||||
} else {
|
||||
mode, err = config.ParseRotationMode(tt.giveBytes)
|
||||
}
|
||||
|
||||
if tt.wantErrorMsg == "" {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantMode, mode)
|
||||
} else {
|
||||
assert.ErrorContains(t, err, tt.wantErrorMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
12
internal/http/handlers/error_page/handler.go
Normal file
12
internal/http/handlers/error_page/handler.go
Normal file
@ -0,0 +1,12 @@
|
||||
package error_page
|
||||
|
||||
import "net/http"
|
||||
|
||||
func New() http.Handler {
|
||||
var body = []byte("error page")
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write(body)
|
||||
})
|
||||
}
|
16
internal/http/handlers/live/handler.go
Normal file
16
internal/http/handlers/live/handler.go
Normal file
@ -0,0 +1,16 @@
|
||||
package live
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// New creates a new handler that always returns "OK" with status code 200.
|
||||
func New() http.Handler {
|
||||
var body = []byte("OK")
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(body)
|
||||
})
|
||||
}
|
26
internal/http/handlers/live/handler_test.go
Normal file
26
internal/http/handlers/live/handler_test.go
Normal file
@ -0,0 +1,26 @@
|
||||
package live_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/http/handlers/live"
|
||||
)
|
||||
|
||||
func TestServeHTTP(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
req = httptest.NewRequest(http.MethodGet, "http://testing", http.NoBody)
|
||||
rr = httptest.NewRecorder()
|
||||
)
|
||||
|
||||
live.New().ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, rr.Header().Get("Content-Type"), "text/plain; charset=utf-8")
|
||||
assert.Equal(t, rr.Code, http.StatusOK)
|
||||
assert.Equal(t, rr.Body.String(), "OK")
|
||||
}
|
22
internal/http/handlers/version/handler.go
Normal file
22
internal/http/handlers/version/handler.go
Normal file
@ -0,0 +1,22 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// New creates a handler that returns the version of the service in JSON format.
|
||||
func New(ver string) http.Handler {
|
||||
var body, _ = json.Marshal(struct { //nolint:errchkjson
|
||||
Version string `json:"version"`
|
||||
}{
|
||||
Version: strings.TrimSpace(ver),
|
||||
})
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(body)
|
||||
})
|
||||
}
|
25
internal/http/handlers/version/handler_test.go
Normal file
25
internal/http/handlers/version/handler_test.go
Normal file
@ -0,0 +1,25 @@
|
||||
package version_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/http/handlers/version"
|
||||
)
|
||||
|
||||
func TestServeHTTP(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
req = httptest.NewRequest(http.MethodGet, "http://testing", http.NoBody)
|
||||
rr = httptest.NewRecorder()
|
||||
)
|
||||
|
||||
version.New("\t\n foo@bar ").ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, rr.Code, http.StatusOK)
|
||||
assert.Equal(t, rr.Body.String(), `{"version":"foo@bar"}`)
|
||||
}
|
49
internal/http/middleware/logreq/middleware.go
Normal file
49
internal/http/middleware/logreq/middleware.go
Normal file
@ -0,0 +1,49 @@
|
||||
package logreq
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// New creates a middleware for [http.ServeMux] that logs every incoming request.
|
||||
//
|
||||
// The skipper function should return true if the request should be skipped. It's ok to pass nil.
|
||||
func New(log *zap.Logger, skipper func(*http.Request) bool) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if skipper != nil && skipper(r) {
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var now = time.Now()
|
||||
|
||||
defer func() {
|
||||
var fields = []zap.Field{
|
||||
zap.String("useragent", r.UserAgent()),
|
||||
zap.String("method", r.Method),
|
||||
zap.String("url", r.URL.String()),
|
||||
zap.String("referer", r.Referer()),
|
||||
zap.String("content type", w.Header().Get("Content-Type")),
|
||||
zap.String("remote addr", r.RemoteAddr),
|
||||
zap.String("method", r.Method),
|
||||
zap.Duration("duration", time.Since(now).Round(time.Microsecond)),
|
||||
}
|
||||
|
||||
if log.Level() <= zap.DebugLevel {
|
||||
fields = append(fields,
|
||||
zap.Any("request headers", r.Header.Clone()),
|
||||
zap.Any("response headers", w.Header().Clone()),
|
||||
)
|
||||
}
|
||||
|
||||
log.Info("HTTP request processed", fields...)
|
||||
}()
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
91
internal/http/server.go
Normal file
91
internal/http/server.go
Normal file
@ -0,0 +1,91 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/appmeta"
|
||||
"gh.tarampamp.am/error-pages/internal/config"
|
||||
"gh.tarampamp.am/error-pages/internal/http/handlers/error_page"
|
||||
"gh.tarampamp.am/error-pages/internal/http/handlers/live"
|
||||
"gh.tarampamp.am/error-pages/internal/http/handlers/version"
|
||||
"gh.tarampamp.am/error-pages/internal/http/middleware/logreq"
|
||||
)
|
||||
|
||||
// Server is an HTTP server for serving error pages.
|
||||
type Server struct {
|
||||
log *zap.Logger
|
||||
server *http.Server
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
// NewServer creates a new HTTP server.
|
||||
func NewServer(baseCtx context.Context, log *zap.Logger) Server {
|
||||
const (
|
||||
readTimeout = 30 * time.Second
|
||||
writeTimeout = readTimeout + 10*time.Second // should be bigger than the read timeout
|
||||
maxHeaderBytes = (1 << 20) * 5 //nolint:mnd // 5 MB
|
||||
)
|
||||
|
||||
var (
|
||||
mux = http.NewServeMux()
|
||||
srv = &http.Server{
|
||||
Handler: mux,
|
||||
ReadTimeout: readTimeout,
|
||||
WriteTimeout: writeTimeout,
|
||||
ReadHeaderTimeout: readTimeout,
|
||||
MaxHeaderBytes: maxHeaderBytes,
|
||||
ErrorLog: zap.NewStdLog(log),
|
||||
BaseContext: func(net.Listener) context.Context { return baseCtx },
|
||||
}
|
||||
)
|
||||
|
||||
return Server{log: log, server: srv, mux: mux}
|
||||
}
|
||||
|
||||
// Register server handlers, middlewares, etc.
|
||||
func (s *Server) Register(cfg *config.Config) error {
|
||||
// register middleware
|
||||
s.server.Handler = logreq.New(s.log, func(r *http.Request) bool {
|
||||
// skip logging healthcheck requests
|
||||
return strings.Contains(strings.ToLower(r.UserAgent()), "healthcheck")
|
||||
})(s.server.Handler)
|
||||
|
||||
{ // register handlers (https://go.dev/blog/routing-enhancements)
|
||||
var errorPageHandler = error_page.New()
|
||||
|
||||
s.mux.Handle("/", errorPageHandler)
|
||||
s.mux.Handle("/{any}", errorPageHandler)
|
||||
|
||||
var liveHandler = live.New()
|
||||
|
||||
s.mux.Handle("GET /health/live", liveHandler)
|
||||
s.mux.Handle("GET /healthz", liveHandler)
|
||||
s.mux.Handle("GET /live", liveHandler)
|
||||
|
||||
s.mux.Handle("GET /version", version.New(appmeta.Version()))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start server.
|
||||
func (s *Server) Start(ip string, port uint16) error {
|
||||
s.server.Addr = ip + ":" + strconv.Itoa(int(port))
|
||||
|
||||
return s.server.ListenAndServe()
|
||||
}
|
||||
|
||||
// Stop server gracefully.
|
||||
func (s *Server) Stop(timeout time.Duration) error {
|
||||
var ctx, cancel = context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
return s.server.Shutdown(ctx)
|
||||
}
|
Loading…
Reference in New Issue
Block a user