Compare commits

..

8 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
23 changed files with 591 additions and 1103 deletions

View File

@ -1,18 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.base.schema.json",
"name": "default",
"image": "golang:1.22-bookworm",
"features": {
"ghcr.io/guiyomh/features/golangci-lint:0": {},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/sshd:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"streetsidesoftware.code-spell-checker"
]
}
},
"postCreateCommand": "go mod download"
}

View File

@ -36,7 +36,7 @@ jobs:
tag: ${{ github.ref }} tag: ${{ github.ref }}
- if: matrix.os == 'linux' && matrix.arch == 'amd64' - if: matrix.os == 'linux' && matrix.arch == 'amd64'
run: mkdir ./out && ./${{ steps.values.outputs.binary-name }} build --index --disable-minification --target-dir ./out run: mkdir ./out && ./${{ steps.values.outputs.binary-name }} build --index --target-dir ./out
- if: matrix.os == 'linux' && matrix.arch == 'amd64' - if: matrix.os == 'linux' && matrix.arch == 'amd64'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
@ -93,12 +93,13 @@ jobs:
push: true push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8 platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
build-args: "APP_VERSION=${{ steps.slug.outputs.version }}" build-args: "APP_VERSION=${{ steps.slug.outputs.version }}"
tags: | tags: ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version }}
tarampampam/error-pages:latest # tags: | # TODO: uncomment after the stable release
tarampampam/error-pages:${{ steps.slug.outputs.version }} # tarampampam/error-pages:latest
tarampampam/error-pages:${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }} # tarampampam/error-pages:${{ steps.slug.outputs.version }}
tarampampam/error-pages:${{ steps.slug.outputs.version-major }} # tarampampam/error-pages:${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}
ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:latest # tarampampam/error-pages:${{ steps.slug.outputs.version-major }}
ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version }} # ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:latest
ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }} # ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version }}
ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version-major }} # ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}
# ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version-major }}

View File

@ -3,7 +3,7 @@
# -✂- this stage is used to develop and build the application locally ------------------------------------------------- # -✂- this stage is used to develop and build the application locally -------------------------------------------------
FROM docker.io/library/golang:1.22-bookworm AS develop FROM docker.io/library/golang:1.22-bookworm AS develop
# use the /var/tmp/go as the GOPATH to reuse the modules cache # use the /var/tmp as the GOPATH to reuse the modules cache
ENV GOPATH="/var/tmp/go" ENV GOPATH="/var/tmp/go"
RUN set -x \ RUN set -x \
@ -32,10 +32,7 @@ FROM develop AS compile
# can be passed with any prefix (like `v1.2.3@GITHASH`), e.g.: `docker build --build-arg "APP_VERSION=v1.2.3" .` # can be passed with any prefix (like `v1.2.3@GITHASH`), e.g.: `docker build --build-arg "APP_VERSION=v1.2.3" .`
ARG APP_VERSION="undefined@docker" ARG APP_VERSION="undefined@docker"
# copy the source code RUN --mount=type=bind,source=.,target=/src set -x \
COPY . /src
RUN set -x \
&& go generate ./... \ && go generate ./... \
&& CGO_ENABLED=0 LDFLAGS="-s -w -X gh.tarampamp.am/error-pages/internal/appmeta.version=${APP_VERSION}" \ && CGO_ENABLED=0 LDFLAGS="-s -w -X gh.tarampamp.am/error-pages/internal/appmeta.version=${APP_VERSION}" \
go build -trimpath -ldflags "${LDFLAGS}" -o /tmp/error-pages ./cmd/error-pages/ \ go build -trimpath -ldflags "${LDFLAGS}" -o /tmp/error-pages ./cmd/error-pages/ \
@ -48,11 +45,10 @@ FROM docker.io/library/alpine:3.20 AS rootfs
WORKDIR /tmp/rootfs WORKDIR /tmp/rootfs
# prepare rootfs for runtime # prepare rootfs for runtime
RUN set -x \ RUN --mount=type=bind,source=.,target=/src set -x \
&& mkdir -p ./etc/ssl/certs ./bin \ && mkdir -p ./etc ./bin \
&& echo 'appuser:x:10001:10001::/nonexistent:/sbin/nologin' > ./etc/passwd \ && echo 'appuser:x:10001:10001::/nonexistent:/sbin/nologin' > ./etc/passwd \
&& echo 'appuser:x:10001:' > ./etc/group \ && echo 'appuser:x:10001:' > ./etc/group
&& cp /etc/ssl/certs/ca-certificates.crt ./etc/ssl/certs/
# take the binary from the compile stage # take the binary from the compile stage
COPY --from=compile /tmp/error-pages ./bin/error-pages COPY --from=compile /tmp/error-pages ./bin/error-pages
@ -73,7 +69,7 @@ ARG APP_VERSION="undefined@docker"
LABEL \ LABEL \
# docs: https://github.com/opencontainers/image-spec/blob/master/annotations.md # docs: https://github.com/opencontainers/image-spec/blob/master/annotations.md
org.opencontainers.image.title="error-pages" \ org.opencontainers.image.title="error-pages" \
org.opencontainers.image.description="Pretty server's error pages" \ org.opencontainers.image.description="Static server error pages in the docker image" \
org.opencontainers.image.url="https://github.com/tarampampam/error-pages" \ org.opencontainers.image.url="https://github.com/tarampampam/error-pages" \
org.opencontainers.image.source="https://github.com/tarampampam/error-pages" \ org.opencontainers.image.source="https://github.com/tarampampam/error-pages" \
org.opencontainers.image.vendor="tarampampam" \ org.opencontainers.image.vendor="tarampampam" \
@ -91,8 +87,6 @@ WORKDIR /opt
# to find out which environment variables and CLI arguments are supported by the application, run the app # to find out which environment variables and CLI arguments are supported by the application, run the app
# with the `--help` flag or refer to the documentation at https://github.com/tarampampam/error-pages#readme # with the `--help` flag or refer to the documentation at https://github.com/tarampampam/error-pages#readme
ENV LOG_LEVEL="warn"
# docs: https://docs.docker.com/reference/dockerfile/#healthcheck # docs: https://docs.docker.com/reference/dockerfile/#healthcheck
HEALTHCHECK --interval=10s --start-interval=1s --start-period=5s --timeout=2s CMD [\ HEALTHCHECK --interval=10s --start-interval=1s --start-period=5s --timeout=2s CMD [\
"/bin/error-pages", "--log-format", "json", "healthcheck" \ "/bin/error-pages", "--log-format", "json", "healthcheck" \

1045
README.md

File diff suppressed because it is too large Load Diff

2
go.mod
View File

@ -4,7 +4,6 @@ go 1.22
require ( require (
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/tdewolff/minify/v2 v2.20.35
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 github.com/valyala/fasthttp v1.55.0
@ -20,7 +19,6 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
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/tdewolff/parse/v2 v2.7.15 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.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

7
go.sum
View File

@ -26,13 +26,6 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tdewolff/minify/v2 v2.20.35 h1:/Vq/oivpkFyi2PViD25XHZZbJz+eO4OmPSgePex1kBU=
github.com/tdewolff/minify/v2 v2.20.35/go.mod h1:L1VYef/jwKw6Wwyk5A+T0mBjjn3mMPgmjjA688RNsxU=
github.com/tdewolff/parse/v2 v2.7.15 h1:hysDXtdGZIRF5UZXwpfn3ZWRbm+ru4l53/ajBRGpCTw=
github.com/tdewolff/parse/v2 v2.7.15/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/urfave/cli-docs/v3 v3.0.0-alpha5 h1:H1oWnR2/GN0dNm2PVylws+GxSOD6YOwW/jI5l78YfPk= github.com/urfave/cli-docs/v3 v3.0.0-alpha5 h1:H1oWnR2/GN0dNm2PVylws+GxSOD6YOwW/jI5l78YfPk=
github.com/urfave/cli-docs/v3 v3.0.0-alpha5/go.mod h1:AIqom6Q60U4tiqHp41i7+/AB2XHgi1WvQ7jOFlccmZ4= github.com/urfave/cli-docs/v3 v3.0.0-alpha5/go.mod h1:AIqom6Q60U4tiqHp41i7+/AB2XHgi1WvQ7jOFlccmZ4=
github.com/urfave/cli/v3 v3.0.0-alpha9 h1:P0RMy5fQm1AslQS+XCmy9UknDXctOmG/q/FZkUFnJSo= github.com/urfave/cli/v3 v3.0.0-alpha9 h1:P0RMy5fQm1AslQS+XCmy9UknDXctOmG/q/FZkUFnJSo=

View File

@ -6,6 +6,7 @@ import (
"runtime" "runtime"
"strings" "strings"
_ "github.com/urfave/cli-docs/v3" // required for `go generate` to work
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"gh.tarampamp.am/error-pages/internal/appmeta" "gh.tarampamp.am/error-pages/internal/appmeta"
@ -16,7 +17,7 @@ import (
"gh.tarampamp.am/error-pages/internal/logger" "gh.tarampamp.am/error-pages/internal/logger"
) )
//go:generate go run app_generate.go //go:generate go run update_readme.go
// NewApp creates a new console application. // NewApp creates a new console application.
func NewApp(appName string) *cli.Command { //nolint:funlen func NewApp(appName string) *cli.Command { //nolint:funlen
@ -24,7 +25,7 @@ func NewApp(appName string) *cli.Command { //nolint:funlen
logLevelFlag = cli.StringFlag{ logLevelFlag = cli.StringFlag{
Name: "log-level", Name: "log-level",
Value: logger.InfoLevel.String(), Value: logger.InfoLevel.String(),
Usage: "Logging level (" + strings.Join(logger.LevelStrings(), "/") + ")", Usage: "logging level (" + strings.Join(logger.LevelStrings(), "/") + ")",
Sources: cli.EnvVars("LOG_LEVEL"), Sources: cli.EnvVars("LOG_LEVEL"),
OnlyOnce: true, OnlyOnce: true,
Config: cli.StringConfig{TrimSpace: true}, Config: cli.StringConfig{TrimSpace: true},
@ -40,7 +41,7 @@ func NewApp(appName string) *cli.Command { //nolint:funlen
logFormatFlag = cli.StringFlag{ logFormatFlag = cli.StringFlag{
Name: "log-format", Name: "log-format",
Value: logger.ConsoleFormat.String(), Value: logger.ConsoleFormat.String(),
Usage: "Logging format (" + strings.Join(logger.FormatStrings(), "/") + ")", Usage: "logging format (" + strings.Join(logger.FormatStrings(), "/") + ")",
Sources: cli.EnvVars("LOG_FORMAT"), Sources: cli.EnvVars("LOG_FORMAT"),
OnlyOnce: true, OnlyOnce: true,
Config: cli.StringConfig{TrimSpace: true}, Config: cli.StringConfig{TrimSpace: true},
@ -79,7 +80,7 @@ func NewApp(appName string) *cli.Command { //nolint:funlen
serve.NewCommand(log), serve.NewCommand(log),
build.NewCommand(log), build.NewCommand(log),
healthcheck.NewCommand(log, healthcheck.NewHTTPHealthChecker()), healthcheck.NewCommand(log, healthcheck.NewHTTPHealthChecker()),
perftest.NewCommand(), perftest.NewCommand(log),
}, },
Version: fmt.Sprintf("%s (%s)", appmeta.Version(), runtime.Version()), Version: fmt.Sprintf("%s (%s)", appmeta.Version(), runtime.Version()),
Flags: []cli.Flag{ // global flags Flags: []cli.Flag{ // global flags

View File

@ -39,24 +39,21 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit
cmd command cmd command
cfg = config.New() cfg = config.New()
addTplFlag = shared.AddTemplatesFlag addTplFlag = shared.AddTemplatesFlag
disableTplFlag = shared.DisableTemplateNamesFlag disableTplFlag = shared.DisableTemplateNamesFlag
addCodeFlag = shared.AddHTTPCodesFlag addCodeFlag = shared.AddHTTPCodesFlag
disableL10nFlag = shared.DisableL10nFlag disableL10nFlag = shared.DisableL10nFlag
disableMinificationFlag = shared.DisableMinificationFlag createIndexFlag = cli.BoolFlag{
createIndexFlag = cli.BoolFlag{ Name: "index",
Name: "index", Aliases: []string{"i"},
Aliases: []string{"i"}, Usage: "generate index.html file with links to all error pages",
Usage: "Generate index.html file with links to all error pages",
Category: shared.CategoryBuild,
} }
targetDirFlag = cli.StringFlag{ targetDirFlag = cli.StringFlag{
Name: "target-dir", Name: "target-dir",
Aliases: []string{"out", "dir", "o"}, Aliases: []string{"out", "dir", "o"},
Usage: "Directory to put the built error pages into", Usage: "directory to put the built error pages into",
Value: ".", // current directory by default Value: ".", // current directory by default
Config: cli.StringConfig{TrimSpace: true}, Config: cli.StringConfig{TrimSpace: true},
Category: shared.CategoryBuild,
OnlyOnce: true, OnlyOnce: true,
Validator: func(dir string) error { Validator: func(dir string) error {
if dir == "" { if dir == "" {
@ -82,7 +79,6 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit
Usage: "Build the static error pages and put them into a specified directory", Usage: "Build the static error pages and put them into a specified directory",
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
cfg.L10n.Disable = c.Bool(disableL10nFlag.Name) cfg.L10n.Disable = c.Bool(disableL10nFlag.Name)
cfg.DisableMinification = c.Bool(disableMinificationFlag.Name)
cmd.opt.createIndex = c.Bool(createIndexFlag.Name) cmd.opt.createIndex = c.Bool(createIndexFlag.Name)
cmd.opt.targetDirAbsPath, _ = filepath.Abs(c.String(targetDirFlag.Name)) // an error checked by [os.Stat] validator cmd.opt.targetDirAbsPath, _ = filepath.Abs(c.String(targetDirFlag.Name)) // an error checked by [os.Stat] validator
@ -142,14 +138,13 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit
&disableL10nFlag, &disableL10nFlag,
&createIndexFlag, &createIndexFlag,
&targetDirFlag, &targetDirFlag,
&disableMinificationFlag,
}, },
} }
return cmd.c return cmd.c
} }
func (cmd *command) Run( //nolint:funlen,gocognit func (cmd *command) Run( //nolint:funlen
ctx context.Context, ctx context.Context,
log *logger.Logger, log *logger.Logger,
cfg *config.Config, cfg *config.Config,
@ -175,21 +170,13 @@ func (cmd *command) Run( //nolint:funlen,gocognit
var outFilePath = path.Join(cmd.opt.targetDirAbsPath, templateName, code+".html") var outFilePath = path.Join(cmd.opt.targetDirAbsPath, templateName, code+".html")
if content, renderErr := appTemplate.Render(templateContent, appTemplate.Props{ //nolint:nestif if content, renderErr := appTemplate.Render(templateContent, appTemplate.Props{
Code: uint16(codeAsUint), Code: uint16(codeAsUint),
Message: codeDescription.Message, Message: codeDescription.Message,
Description: codeDescription.Description, Description: codeDescription.Description,
L10nDisabled: cfg.L10n.Disable, L10nDisabled: cfg.L10n.Disable,
ShowRequestDetails: false, ShowRequestDetails: false,
}); renderErr == nil { }); renderErr == nil {
if !cfg.DisableMinification {
if mini, minErr := appTemplate.MiniHTML(content); minErr != nil {
log.Warn("Cannot minify the content", logger.Error(minErr))
} else {
content = mini
}
}
if err := os.WriteFile(outFilePath, []byte(content), os.FileMode(0664)); err != nil { //nolint:mnd if err := os.WriteFile(outFilePath, []byte(content), os.FileMode(0664)); err != nil { //nolint:mnd
return err return err
} }

View File

@ -1,59 +1,32 @@
package perftest package perftest
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"math" "math/rand"
"os" "net/http"
"os/exec"
"runtime" "runtime"
"strconv" "sync"
"sync/atomic"
"time" "time"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"gh.tarampamp.am/error-pages/internal/cli/shared" "gh.tarampamp.am/error-pages/internal/cli/shared"
"gh.tarampamp.am/error-pages/internal/logger"
) )
const wrkOneCodeTestLua = `
local formats = { 'application/json', 'application/xml', 'text/html', 'text/plain' }
request = function()
wrk.headers["User-Agent"] = "wrk"
wrk.headers["X-Namespace"] = "NAMESPACE_" .. tostring(math.random(0, 99999999))
wrk.headers["X-Request-ID"] = "REQ_ID_" .. tostring(math.random(0, 99999999))
wrk.headers["Content-Type"] = formats[ math.random( 0, #formats - 1 ) ]
return wrk.format("GET", "/500.html?rnd=" .. tostring(math.random(0, 99999999)), nil, nil)
end
`
//nolint:lll
const bombDifferentCodes = `
local formats = { 'application/json', 'application/xml', 'text/html', 'text/plain' }
request = function()
wrk.headers["User-Agent"] = "wrk"
wrk.headers["X-Namespace"] = "NAMESPACE_" .. tostring(math.random(0, 99999999))
wrk.headers["X-Request-ID"] = "REQ_ID_" .. tostring(math.random(0, 99999999))
wrk.headers["Content-Type"] = formats[ math.random( 0, #formats - 1 ) ]
return wrk.format("GET", "/" .. tostring(math.random(400, 599)) .. ".html?rnd=" .. tostring(math.random(0, 99999999)), nil, nil)
end
`
// NewCommand creates `perftest` command. // NewCommand creates `perftest` command.
func NewCommand() *cli.Command { //nolint:funlen func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit
var ( var (
portFlag = shared.ListenPortFlag portFlag = shared.ListenPortFlag
durationFlag = cli.DurationFlag{ durationFlag = cli.DurationFlag{
Name: "duration", Name: "duration",
Aliases: []string{"d"}, Aliases: []string{"d"},
Usage: "Duration of test", Usage: "duration of the test",
Value: 15 * time.Second, //nolint:mnd Value: 10 * time.Second, //nolint:mnd
Validator: func(d time.Duration) error { Validator: func(d time.Duration) error {
if d <= time.Second { if d <= time.Second {
return errors.New("duration can't be less than 1 second") return errors.New("duration can't be less than 1 second")
@ -65,28 +38,11 @@ func NewCommand() *cli.Command { //nolint:funlen
threadsFlag = cli.UintFlag{ threadsFlag = cli.UintFlag{
Name: "threads", Name: "threads",
Aliases: []string{"t"}, Aliases: []string{"t"},
Usage: "Number of threads to use", Usage: "number of threads",
Value: max(2, uint64(math.Round(float64(runtime.NumCPU())/1.3))), //nolint:mnd Value: max(2, uint64(runtime.NumCPU()/2)), //nolint:mnd
Validator: func(u uint64) error { Validator: func(u uint64) error {
if u == 0 { if u == 0 {
return errors.New("threads number can't be zero") return errors.New("threads number can't be zero")
} else if u > math.MaxUint16 {
return errors.New("threads number can't be greater than 65535")
}
return nil
},
}
connectionsFlag = cli.UintFlag{
Name: "connections",
Aliases: []string{"c"},
Usage: "Number of connections to keep open",
Value: max(16, uint64(runtime.NumCPU()*25)), //nolint:mnd
Validator: func(u uint64) error {
if u == 0 {
return errors.New("threads number can't be zero")
} else if u > math.MaxUint16 {
return errors.New("threads number can't be greater than 65535")
} }
return nil return nil
@ -96,47 +52,97 @@ func NewCommand() *cli.Command { //nolint:funlen
return &cli.Command{ return &cli.Command{
Name: "perftest", Name: "perftest",
Aliases: []string{"perf", "benchmark", "bench"}, Aliases: []string{"perf", "test"},
Hidden: true, Hidden: true,
Usage: "Performance (load) test for the HTTP server (locally installed wrk is required)", Usage: "Simple performance (load) test for the HTTP server",
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error { // TODO: use fasthttp.Client
var wrkBinPath, lErr = exec.LookPath("wrk") var (
if lErr != nil { perfCtx, cancel = context.WithTimeout(ctx, c.Duration(durationFlag.Name))
return fmt.Errorf("seems like wrk (https://github.com/wg/wrk) is not installed: %w", lErr) startedAt = time.Now()
wg sync.WaitGroup
success atomic.Uint64
failed atomic.Uint64
)
defer func() {
cancel()
log.Info("Summary",
logger.Uint64("success", success.Load()),
logger.Uint64("failed", failed.Load()),
logger.Duration("duration", time.Since(startedAt)),
logger.Float64("RPS", float64(success.Load()+failed.Load())/time.Since(startedAt).Seconds()),
logger.Float64("errors rate", float64(failed.Load())/float64(success.Load()+failed.Load())*100), //nolint:mnd
)
}()
log.Info("Running test",
logger.Uint64("threads", c.Uint(threadsFlag.Name)),
logger.Duration("duration", c.Duration(durationFlag.Name)),
)
var httpClient = &http.Client{
Transport: &http.Transport{MaxConnsPerHost: max(2, int(c.Uint(threadsFlag.Name))-1)}, //nolint:mnd
Timeout: c.Duration(durationFlag.Name) + time.Second,
} }
var runTest = func(scriptContent string) error { for i := uint64(0); i < c.Uint(threadsFlag.Name); i++ {
if stdOut, stdErr, err := wrkRunTest(ctx, wg.Add(1)
wrkBinPath,
uint16(c.Uint(threadsFlag.Name)),
uint16(c.Uint(connectionsFlag.Name)),
c.Duration(durationFlag.Name),
uint16(c.Uint(portFlag.Name)),
scriptContent,
); err != nil {
var errData, _ = io.ReadAll(stdErr)
return fmt.Errorf("failed to execute the test: %w (%s)", err, string(errData)) go func(log *logger.Logger) {
} else { defer wg.Done()
var outData, _ = io.ReadAll(stdOut)
printf("Test completed successfully. Here is the output:\n\n%s\n", string(outData)) if perfCtx.Err() != nil {
} return
}
return nil var req, rErr = makeRequest(perfCtx, uint16(c.Uint(portFlag.Name)))
if rErr != nil {
log.Error("Failed to create a new request", logger.Error(rErr))
return
}
for {
var sentAt = time.Now()
var resp, respErr = httpClient.Do(req)
if resp != nil {
if _, err := io.Copy(io.Discard, resp.Body); err != nil && !errIsDone(err) {
log.Error("Failed to read response body", logger.Error(err))
}
if err := resp.Body.Close(); err != nil && !errIsDone(err) {
log.Error("Failed to close response body", logger.Error(err))
}
}
if respErr != nil {
if errIsDone(respErr) {
return
}
log.Error("Request failed", logger.Error(respErr))
failed.Add(1)
continue
}
log.Debug("Response received",
logger.String("status", resp.Status),
logger.Duration("duration", time.Since(sentAt)),
logger.Int64("size", resp.ContentLength),
logger.Uint64("success", success.Load()),
logger.Uint64("failed", failed.Load()),
)
success.Add(1)
}
}(log.Named(fmt.Sprintf("thread-%d", i)))
} }
printf("Starting the test to bomb ONE PAGE (code). Please, be patient...\n") wg.Wait()
if err := runTest(wrkOneCodeTestLua); err != nil {
return err
}
printf("Starting the test to bomb DIFFERENT PAGES (codes). Please, be patient...\n")
if err := runTest(bombDifferentCodes); err != nil {
return err
}
return nil return nil
}, },
@ -144,51 +150,54 @@ func NewCommand() *cli.Command { //nolint:funlen
&portFlag, &portFlag,
&durationFlag, &durationFlag,
&threadsFlag, &threadsFlag,
&connectionsFlag,
}, },
} }
} }
func printf(format string, args ...any) { fmt.Printf(format, args...) } //nolint:forbidigo // randomIntBetween returns a random integer between min and max.
func randomIntBetween(min, max int) int { return min + rand.Intn(max-min) } //nolint:gosec
func wrkRunTest( // makeRequest creates a new HTTP request for the performance test.
ctx context.Context, func makeRequest(ctx context.Context, port uint16) (*http.Request, error) {
wrkBinPath string, var req, rErr = http.NewRequestWithContext(ctx,
threadsCount, connectionsCount uint16, http.MethodGet,
duration time.Duration, fmt.Sprintf(
port uint16, "http://127.0.0.1:%d/%d.html?rnd=%d", // for load testing purposes only
scriptContent string, port,
) (io.Reader, io.Reader, error) { randomIntBetween(400, 418), //nolint:mnd
var tmpFile, tErr = os.CreateTemp("", "ep-perf-one-page") randomIntBetween(1, 999_999_999), //nolint:mnd
if tErr != nil { ),
return nil, nil, fmt.Errorf("failed to create a temporary file: %w", tErr) http.NoBody,
}
defer func() {
_ = tmpFile.Close()
_ = os.Remove(tmpFile.Name())
}()
if _, err := tmpFile.WriteString(scriptContent); err != nil {
return nil, nil, fmt.Errorf("failed to write to a temporary file: %w", err)
}
if err := tmpFile.Close(); err != nil {
return nil, nil, err
}
var stdout, stderr bytes.Buffer
var cmd = exec.CommandContext(ctx, wrkBinPath, //nolint:gosec
"--timeout", "1s",
"--threads", strconv.FormatUint(uint64(threadsCount), 10),
"--connections", strconv.FormatUint(uint64(connectionsCount), 10),
"--duration", duration.String(),
"--script", tmpFile.Name(),
fmt.Sprintf("http://127.0.0.1:%d/", port),
) )
cmd.Stdout, cmd.Stderr = &stdout, &stderr if rErr != nil {
return nil, rErr
}
return &stdout, &stderr, cmd.Run() // execute req.Header.Set("Connection", "keep-alive")
req.Header.Set("User-Agent", "perftest")
req.Header.Set("X-Namespace", fmt.Sprintf("namespace-%d", randomIntBetween(1, 999_999_999))) //nolint:mnd
req.Header.Set("X-Request-ID", fmt.Sprintf("req-id-%d", randomIntBetween(1, 999_999_999))) //nolint:mnd
var contentType string
switch randomIntBetween(1, 4) { //nolint:mnd
case 1:
contentType = "application/json"
case 2: //nolint:mnd
contentType = "application/xml"
case 3: //nolint:mnd
contentType = "text/html"
default:
contentType = "text/plain"
}
req.Header.Set("Content-Type", contentType)
return req, nil
}
// errIsDone checks if the error is a context.DeadlineExceeded or context.Canceled.
func errIsDone(err error) bool {
return errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled)
} }

View File

@ -38,37 +38,33 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
) )
var ( var (
addrFlag = shared.ListenAddrFlag addrFlag = shared.ListenAddrFlag
portFlag = shared.ListenPortFlag portFlag = shared.ListenPortFlag
addTplFlag = shared.AddTemplatesFlag addTplFlag = shared.AddTemplatesFlag
disableTplFlag = shared.DisableTemplateNamesFlag disableTplFlag = shared.DisableTemplateNamesFlag
addCodeFlag = shared.AddHTTPCodesFlag addCodeFlag = shared.AddHTTPCodesFlag
disableL10nFlag = shared.DisableL10nFlag disableL10nFlag = shared.DisableL10nFlag
disableMinificationFlag = shared.DisableMinificationFlag jsonFormatFlag = cli.StringFlag{
jsonFormatFlag = cli.StringFlag{
Name: "json-format", Name: "json-format",
Usage: "Override the default error page response in JSON format (Go templates are supported; the error " + Usage: "override the default error page response in JSON format (Go templates are supported; the error " +
"page will use this template if the client requests JSON content type)", "page will use this template if the client requests JSON content type)",
Sources: env("RESPONSE_JSON_FORMAT"), Sources: env("RESPONSE_JSON_FORMAT"),
Category: shared.CategoryFormats,
OnlyOnce: true, OnlyOnce: true,
Config: trim, Config: trim,
} }
xmlFormatFlag = cli.StringFlag{ xmlFormatFlag = cli.StringFlag{
Name: "xml-format", Name: "xml-format",
Usage: "Override the default error page response in XML format (Go templates are supported; the error " + Usage: "override the default error page response in XML format (Go templates are supported; the error " +
"page will use this template if the client requests XML content type)", "page will use this template if the client requests XML content type)",
Sources: env("RESPONSE_XML_FORMAT"), Sources: env("RESPONSE_XML_FORMAT"),
Category: shared.CategoryFormats,
OnlyOnce: true, OnlyOnce: true,
Config: trim, Config: trim,
} }
plainTextFormatFlag = cli.StringFlag{ plainTextFormatFlag = cli.StringFlag{
Name: "plaintext-format", Name: "plaintext-format",
Usage: "Override the default error page response in plain text format (Go templates are supported; the " + Usage: "override the default error page response in plain text format (Go templates are supported; the " +
"error page will use this template if the client requests plain text content type or does not specify any)", "error page will use this template if the client requests plain text content type or does not specify any)",
Sources: env("RESPONSE_PLAINTEXT_FORMAT"), Sources: env("RESPONSE_PLAINTEXT_FORMAT"),
Category: shared.CategoryFormats,
OnlyOnce: true, OnlyOnce: true,
Config: trim, Config: trim,
} }
@ -76,19 +72,17 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
Name: "template-name", Name: "template-name",
Aliases: []string{"t"}, Aliases: []string{"t"},
Value: cfg.TemplateName, Value: cfg.TemplateName,
Usage: "Name of the template to use for rendering error pages (built-in templates: " + Usage: "name of the template to use for rendering error pages (built-in templates: " +
strings.Join(cfg.Templates.Names(), ", ") + ")", strings.Join(cfg.Templates.Names(), ", ") + ")",
Sources: env("TEMPLATE_NAME"), Sources: env("TEMPLATE_NAME"),
Category: shared.CategoryTemplates,
OnlyOnce: true, OnlyOnce: true,
Config: trim, Config: trim,
} }
defaultCodeToRenderFlag = cli.UintFlag{ defaultCodeToRenderFlag = cli.UintFlag{
Name: "default-error-page", Name: "default-error-page",
Usage: "The code of the default (index page, when a code is not specified) error page to render", Usage: "the code of the default (index page, when a code is not specified) error page to render",
Value: uint64(cfg.DefaultCodeToRender), Value: uint64(cfg.DefaultCodeToRender),
Sources: env("DEFAULT_ERROR_PAGE"), Sources: env("DEFAULT_ERROR_PAGE"),
Category: shared.CategoryCodes,
Validator: func(code uint64) error { Validator: func(code uint64) error {
if code > 999 { //nolint:mnd if code > 999 { //nolint:mnd
return fmt.Errorf("wrong HTTP code [%d] for the default error page", code) return fmt.Errorf("wrong HTTP code [%d] for the default error page", code)
@ -100,19 +94,17 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
} }
sendSameHTTPCodeFlag = cli.BoolFlag{ sendSameHTTPCodeFlag = cli.BoolFlag{
Name: "send-same-http-code", Name: "send-same-http-code",
Usage: "The HTTP response should have the same status code as the requested error page (by default, " + Usage: "the HTTP response should have the same status code as the requested error page (by default, " +
"every response with an error page will have a status code of 200)", "every response with an error page will have a status code of 200)",
Value: cfg.RespondWithSameHTTPCode, Value: cfg.RespondWithSameHTTPCode,
Sources: env("SEND_SAME_HTTP_CODE"), Sources: env("SEND_SAME_HTTP_CODE"),
Category: shared.CategoryOther,
OnlyOnce: true, OnlyOnce: true,
} }
showDetailsFlag = cli.BoolFlag{ showDetailsFlag = cli.BoolFlag{
Name: "show-details", Name: "show-details",
Usage: "Show request details in the error page response (if supported by the template)", Usage: "show request details in the error page response (if supported by the template)",
Value: cfg.ShowDetails, Value: cfg.ShowDetails,
Sources: env("SHOW_DETAILS"), Sources: env("SHOW_DETAILS"),
Category: shared.CategoryOther,
OnlyOnce: true, OnlyOnce: true,
} }
proxyHeadersListFlag = cli.StringFlag{ proxyHeadersListFlag = cli.StringFlag{
@ -130,16 +122,14 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
return nil return nil
}, },
Category: shared.CategoryOther,
OnlyOnce: true, OnlyOnce: true,
Config: trim, Config: trim,
} }
rotationModeFlag = cli.StringFlag{ rotationModeFlag = cli.StringFlag{
Name: "rotation-mode", Name: "rotation-mode",
Value: config.RotationModeDisabled.String(), Value: config.RotationModeDisabled.String(),
Usage: "Templates automatic rotation mode (" + strings.Join(config.RotationModeStrings(), "/") + ")", Usage: "templates automatic rotation mode (" + strings.Join(config.RotationModeStrings(), "/") + ")",
Sources: env("TEMPLATES_ROTATION_MODE"), Sources: env("TEMPLATES_ROTATION_MODE"),
Category: shared.CategoryTemplates,
OnlyOnce: true, OnlyOnce: true,
Config: trim, Config: trim,
Validator: func(s string) error { Validator: func(s string) error {
@ -152,27 +142,26 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
} }
readBufferSizeFlag = cli.UintFlag{ readBufferSizeFlag = cli.UintFlag{
Name: "read-buffer-size", Name: "read-buffer-size",
Usage: "Per-connection buffer size in bytes for reading requests, this also limits the maximum header 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., " + "(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)", "large cookies), note that increasing this value will increase memory consumption)",
Value: 1024 * 5, //nolint:mnd // 5 KB Value: 1024 * 5, //nolint:mnd // 5 KB
Sources: env("READ_BUFFER_SIZE"), Sources: env("READ_BUFFER_SIZE"),
Category: shared.CategoryOther,
OnlyOnce: true, OnlyOnce: true,
} }
) )
// override some flag usage messages // override some flag usage messages
addrFlag.Usage = "The HTTP server will listen on this IP (v4 or v6) address (set 127.0.0.1/::1 for localhost, " + addrFlag.Usage = "the HTTP server will listen on this IP (v4 or v6) address (set 127.0.0.1 for localhost, " +
"0.0.0.0 to listen on all interfaces, or specify a custom IP)" "0.0.0.0 to listen on all interfaces, or specify a custom IP)"
portFlag.Usage = "The TCP port number for the HTTP server to listen on (0-65535)" portFlag.Usage = "the TCP port number for the HTTP server to listen on (0-65535)"
disableL10nFlag.Value = cfg.L10n.Disable // set the default value depending on the configuration disableL10nFlag.Value = cfg.L10n.Disable // set the default value depending on the configuration
cmd.c = &cli.Command{ cmd.c = &cli.Command{
Name: "serve", Name: "serve",
Aliases: []string{"s", "server", "http"}, Aliases: []string{"s", "server", "http"},
Usage: "Please start the HTTP server to serve the error pages. You can configure various options - please RTFM :D", Usage: "Start HTTP server",
Suggest: true, Suggest: true,
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)
@ -183,7 +172,6 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
cfg.RespondWithSameHTTPCode = c.Bool(sendSameHTTPCodeFlag.Name) cfg.RespondWithSameHTTPCode = c.Bool(sendSameHTTPCodeFlag.Name)
cfg.RotationMode, _ = config.ParseRotationMode(c.String(rotationModeFlag.Name)) cfg.RotationMode, _ = config.ParseRotationMode(c.String(rotationModeFlag.Name))
cfg.ShowDetails = c.Bool(showDetailsFlag.Name) cfg.ShowDetails = c.Bool(showDetailsFlag.Name)
cfg.DisableMinification = c.Bool(disableMinificationFlag.Name)
{ // override default JSON, XML, and PlainText formats { // override default JSON, XML, and PlainText formats
if c.IsSet(jsonFormatFlag.Name) { if c.IsSet(jsonFormatFlag.Name) {
@ -305,7 +293,6 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
&proxyHeadersListFlag, &proxyHeadersListFlag,
&rotationModeFlag, &rotationModeFlag,
&readBufferSizeFlag, &readBufferSizeFlag,
&disableMinificationFlag,
}, },
} }

View File

@ -36,7 +36,7 @@ func TestCommand_Run(t *testing.T) {
"--add-template", "./testdata/foo-template.html", "--add-template", "./testdata/foo-template.html",
"--disable-template", "ghost", "--disable-template", "ghost",
"--disable-template", "<unknown>", "--disable-template", "<unknown>",
"--add-code", "200=Code/Description", "--add-http-code", "200=Code/Description",
"--json-format", "json format", "--json-format", "json format",
"--xml-format", "xml format", "--xml-format", "xml format",
"--plaintext-format", "plaintext format", "--plaintext-format", "plaintext format",

View File

@ -11,15 +11,6 @@ import (
"gh.tarampamp.am/error-pages/internal/config" "gh.tarampamp.am/error-pages/internal/config"
) )
const (
CategoryHTTP = "HTTP:"
CategoryTemplates = "TEMPLATES:"
CategoryCodes = "HTTP CODES:"
CategoryFormats = "FORMATS:"
CategoryBuild = "BUILD:"
CategoryOther = "OTHER:"
)
// Note: Don't use pointers for flags, because they have own state which is not thread-safe. // Note: Don't use pointers for flags, because they have own state which is not thread-safe.
// https://github.com/urfave/cli/issues/1926 // https://github.com/urfave/cli/issues/1926
@ -29,7 +20,6 @@ var ListenAddrFlag = cli.StringFlag{
Usage: "IP (v4 or v6) address to listen on", Usage: "IP (v4 or v6) address to listen on",
Value: "0.0.0.0", // bind to all interfaces by default Value: "0.0.0.0", // bind to all interfaces by default
Sources: cli.EnvVars("LISTEN_ADDR"), Sources: cli.EnvVars("LISTEN_ADDR"),
Category: CategoryHTTP,
OnlyOnce: true, OnlyOnce: true,
Config: cli.StringConfig{TrimSpace: true}, Config: cli.StringConfig{TrimSpace: true},
Validator: func(ip string) error { Validator: func(ip string) error {
@ -51,7 +41,6 @@ var ListenPortFlag = cli.UintFlag{
Usage: "TCP port number", Usage: "TCP port number",
Value: 8080, // default port number Value: 8080, // default port number
Sources: cli.EnvVars("LISTEN_PORT"), Sources: cli.EnvVars("LISTEN_PORT"),
Category: CategoryHTTP,
OnlyOnce: true, OnlyOnce: true,
Validator: func(port uint64) error { Validator: func(port uint64) error {
if port == 0 || port > 65535 { if port == 0 || port > 65535 {
@ -64,10 +53,9 @@ var ListenPortFlag = cli.UintFlag{
var AddTemplatesFlag = cli.StringSliceFlag{ var AddTemplatesFlag = cli.StringSliceFlag{
Name: "add-template", Name: "add-template",
Usage: "To add a new template, provide the path to the file using this flag (the filename without the extension " + Usage: "to add a new template, provide the path to the file using this flag (the filename without the extension " +
"will be used as the template name)", "will be used as the template name)",
Config: cli.StringConfig{TrimSpace: true}, Config: cli.StringConfig{TrimSpace: true},
Category: CategoryTemplates,
Validator: func(paths []string) error { Validator: func(paths []string) error {
for _, path := range paths { for _, path := range paths {
if path == "" { if path == "" {
@ -84,19 +72,18 @@ var AddTemplatesFlag = cli.StringSliceFlag{
} }
var DisableTemplateNamesFlag = cli.StringSliceFlag{ var DisableTemplateNamesFlag = cli.StringSliceFlag{
Name: "disable-template", Name: "disable-template",
Usage: "Disable the specified template by its name (useful to disable the built-in templates and use only custom ones)", Usage: "disable the specified template by its name (useful to disable the built-in templates and use only custom ones)",
Config: cli.StringConfig{TrimSpace: true}, Config: cli.StringConfig{TrimSpace: true},
Category: CategoryTemplates,
} }
var AddHTTPCodesFlag = cli.StringMapFlag{ var AddHTTPCodesFlag = cli.StringMapFlag{
Name: "add-code", Name: "add-http-code",
Usage: "To add a new HTTP status code, provide the code and its message/description using this flag (the format " + Aliases: []string{"add-code"},
Usage: "to add a new HTTP status code, provide the code and its message/description using this flag (the format " +
"should be '%code%=%message%/%description%'; the code may contain a wildcard '*' to cover multiple codes at " + "should be '%code%=%message%/%description%'; the code may contain a wildcard '*' to cover multiple codes at " +
"once, for example, '4**' will cover all 4xx codes unless a more specific code is described previously)", "once, for example, '4**' will cover all 4xx codes unless a more specific code is described previously)",
Config: cli.StringConfig{TrimSpace: true}, Config: cli.StringConfig{TrimSpace: true},
Category: CategoryCodes,
Validator: func(codes map[string]string) error { Validator: func(codes map[string]string) error {
for code, msgAndDesc := range codes { for code, msgAndDesc := range codes {
if code == "" { if code == "" {
@ -141,16 +128,7 @@ func ParseHTTPCodes(codes map[string]string) map[string]config.CodeDescription {
var DisableL10nFlag = cli.BoolFlag{ var DisableL10nFlag = cli.BoolFlag{
Name: "disable-l10n", Name: "disable-l10n",
Usage: "Disable localization of error pages (if the template supports localization)", Usage: "disable localization of error pages (if the template supports localization)",
Sources: cli.EnvVars("DISABLE_L10N"), Sources: cli.EnvVars("DISABLE_L10N"),
Category: CategoryOther,
OnlyOnce: true,
}
var DisableMinificationFlag = cli.BoolFlag{
Name: "disable-minification",
Usage: "Disable the minification of HTML pages, including CSS, SVG, and JS (may be useful for debugging)",
Sources: cli.EnvVars("DISABLE_MINIFICATION"),
Category: CategoryOther,
OnlyOnce: true, OnlyOnce: true,
} }

View File

@ -122,7 +122,7 @@ func TestAddHTTPCodesFlag(t *testing.T) {
var flag = shared.AddHTTPCodesFlag var flag = shared.AddHTTPCodesFlag
assert.Equal(t, "add-code", flag.Name) assert.Equal(t, "add-http-code", flag.Name)
for name, tt := range map[string]struct { for name, tt := range map[string]struct {
giveValue map[string]string giveValue map[string]string
@ -216,12 +216,3 @@ func TestDisableL10nFlag(t *testing.T) {
assert.Equal(t, "disable-l10n", flag.Name) assert.Equal(t, "disable-l10n", flag.Name)
assert.Contains(t, flag.Sources.String(), "DISABLE_L10N") assert.Contains(t, flag.Sources.String(), "DISABLE_L10N")
} }
func TestDisableMinificationFlag(t *testing.T) {
t.Parallel()
var flag = shared.DisableMinificationFlag
assert.Equal(t, "disable-minification", flag.Name)
assert.Contains(t, flag.Sources.String(), "DISABLE_MINIFICATION")
}

View File

@ -1,4 +1,5 @@
//go:build generate //go:build ignore
// +build ignore
package main package main
@ -16,10 +17,8 @@ func main() {
if stat, err := os.Stat(readmePath); err == nil && stat.Mode().IsRegular() { if stat, err := os.Stat(readmePath); err == nil && stat.Mode().IsRegular() {
if err = cliDocs.ToTabularToFileBetweenTags(cli.NewApp(""), "error-pages", readmePath); err != nil { if err = cliDocs.ToTabularToFileBetweenTags(cli.NewApp(""), "error-pages", readmePath); err != nil {
panic(err) panic(err)
} else {
println("✔ cli docs updated successfully")
} }
} else if err != nil { } else if err != nil {
println("readme file not found, cli docs not updated:", err.Error()) println("readme file not found, cli docs not updated:", err.Error())
} }
} }

View File

@ -56,9 +56,6 @@ type Config struct {
// ShowDetails determines whether to show additional details in the error response, extracted from the // ShowDetails determines whether to show additional details in the error response, extracted from the
// incoming request (if supported by the template). // incoming request (if supported by the template).
ShowDetails bool ShowDetails bool
// DisableMinification determines whether to disable minification of the rendered content (e.g., HTML, CSS) or not.
DisableMinification bool
} }
const defaultJSONFormat string = `{ const defaultJSONFormat string = `{

View File

@ -24,7 +24,6 @@ func TestNew(t *testing.T) {
assert.NotEmpty(t, cfg.TemplateName) assert.NotEmpty(t, cfg.TemplateName)
assert.True(t, cfg.Templates.Has(cfg.TemplateName)) assert.True(t, cfg.Templates.Has(cfg.TemplateName))
assert.Equal(t, uint16(http.StatusNotFound), cfg.DefaultCodeToRender) assert.Equal(t, uint16(http.StatusNotFound), cfg.DefaultCodeToRender)
assert.False(t, cfg.DisableMinification)
}) })
t.Run("changing cfg1 should not affect cfg2", func(t *testing.T) { t.Run("changing cfg1 should not affect cfg2", func(t *testing.T) {

View File

@ -175,14 +175,6 @@ func New(cfg *config.Config, log *logger.Logger) (_ fasthttp.RequestHandler, clo
err.Error(), err.Error(),
)) ))
} else { } else {
if !cfg.DisableMinification {
if mini, minErr := template.MiniHTML(content); minErr != nil {
log.Warn("HTML minification failed", logger.Error(minErr))
} else {
content = mini
}
}
cache.Put(tpl, tplProps, []byte(content)) cache.Put(tpl, tplProps, []byte(content))
write(ctx, log, content) write(ctx, log, content)

View File

@ -48,7 +48,7 @@ func TestHandler(t *testing.T) {
wantStatusCode: http.StatusOK, wantStatusCode: http.StatusOK,
wantHeaders: map[string]string{"Content-Type": "text/html; charset=utf-8"}, wantHeaders: map[string]string{"Content-Type": "text/html; charset=utf-8"},
wantBodyIncludes: []string{ wantBodyIncludes: []string{
"<!doctype html>", "<!DOCTYPE html>",
"<title>407: Proxy Authentication Required", "<title>407: Proxy Authentication Required",
"Proxy Authentication Required", "Proxy Authentication Required",
}, },

View File

@ -51,7 +51,7 @@ func NewServer(log *logger.Logger, readBufferSize uint) Server {
} }
// Register server handlers, middlewares, etc. // Register server handlers, middlewares, etc.
func (s *Server) Register(cfg *config.Config) error { //nolint:funlen 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())
@ -71,7 +71,7 @@ func (s *Server) Register(cfg *config.Config) error { //nolint:funlen
switch { switch {
// live endpoints // live endpoints
case url == "/healthz" || url == "/health/live" || url == "/health" || url == "/live": case url == "/health/live" || url == "/health" || url == "/healthz" || url == "/live":
liveHandler(ctx) liveHandler(ctx)
// version endpoint // version endpoint
@ -87,9 +87,8 @@ func (s *Server) Register(cfg *config.Config) error { //nolint:funlen
// - /{code}.html // - /{code}.html
// - /{code}.htm // - /{code}.htm
// - /{code} // - /{code}
// case method == fasthttp.MethodGet &&
// the HTTP method is not limited to GET and HEAD - it can be any (url == "/" || ep.URLContainsCode(url) || ep.HeadersContainCode(&ctx.Request.Header)):
case url == "/" || ep.URLContainsCode(url) || ep.HeadersContainCode(&ctx.Request.Header):
errorPagesHandler(ctx) errorPagesHandler(ctx)
// wrong requests handling // wrong requests handling

View File

@ -221,16 +221,6 @@ func TestRouting(t *testing.T) {
assert.Contains(t, string(body), "404: Not Found") assert.Contains(t, string(body), "404: Not Found")
assert.Contains(t, headers.Get("Content-Type"), "text/plain") assert.Contains(t, headers.Get("Content-Type"), "text/plain")
}) })
t.Run("other HTTP methods", func(t *testing.T) {
for _, method := range []string{http.MethodDelete, http.MethodPatch, http.MethodPost, http.MethodPut} {
var status, body, headers = sendRequest(t, method, baseUrl+"/404.html")
assert.Equal(t, http.StatusOK, status)
assert.Contains(t, string(body), "404: Not Found")
assert.Contains(t, headers.Get("Content-Type"), "text/plain")
}
})
}) })
t.Run("failure", func(t *testing.T) { t.Run("failure", func(t *testing.T) {
@ -274,6 +264,15 @@ func TestRouting(t *testing.T) {
assert.Equal(t, http.StatusNotFound, status) assert.Equal(t, http.StatusNotFound, status)
assertIsNotErrorPage(t, body) assertIsNotErrorPage(t, body)
}) })
t.Run("invalid HTTP methods", func(t *testing.T) {
for _, method := range []string{http.MethodDelete, http.MethodPatch, http.MethodPost, http.MethodPut} {
var status, body, _ = sendRequest(t, method, baseUrl+"/404.html")
assert.Equal(t, http.StatusMethodNotAllowed, status)
assertIsNotErrorPage(t, body)
}
})
}) })
}) })

View File

@ -1,23 +0,0 @@
package template
import (
"github.com/tdewolff/minify/v2"
"github.com/tdewolff/minify/v2/css"
"github.com/tdewolff/minify/v2/html"
"github.com/tdewolff/minify/v2/js"
"github.com/tdewolff/minify/v2/svg"
)
var htmlMinify = func() *minify.M { //nolint:gochecknoglobals
var m = minify.New()
m.AddFunc("text/css", css.Minify)
m.Add("text/html", &html.Minifier{KeepDocumentTags: true, KeepEndTags: true, KeepQuotes: true})
m.AddFunc("image/svg+xml", svg.Minify)
m.AddFunc("application/javascript", js.Minify)
return m
}()
// MiniHTML minifies HTML data, including inline CSS, SVG and JS.
func MiniHTML(data string) (string, error) { return htmlMinify.String("text/html", data) }

View File

@ -1,94 +0,0 @@
package template_test
import (
"sync"
"testing"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/template"
)
func TestMiniHTML(t *testing.T) {
t.Parallel()
var wg sync.WaitGroup
for range 100 { // race condition provocation
wg.Add(1)
go func() {
defer wg.Done()
for give, want := range map[string]string{
"": "",
`<!-- Simple HTML page -->
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
</head>
<body>
<h1 align="center">Test</h1>
</body>
</html>`: `<!doctype html><html><head><title>Test</title></head><body><h1 align="center">Test</h1></body></html>`,
`<!-- css styles -->
<html>
<head>
<style>
.foo:hover {
color: #f0a; /* comment */
}
</style>
</head>
<body>
<p style="color: red" class="bar">Text</p>
</body>
</html>`: `<html><head><style>.foo:hover{color:#f0a}</style></head><body><p style="color:red" class="bar">Text</p></body></html>`,
`<!-- svg -->
<svg xmlns="http://www.w3.org/2000/svg">
<g>
<circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
</g>
</svg>`: `<svg><g><circle cx="50" cy="50" r="40" stroke="#000" stroke-width="3" fill="red"/></g></svg>`,
`<!-- js -->
<html>
<body>
<script>
// comment
console.log('Hello, World!');
let foo = 1;
foo++;
</script>
</body>
</html>`: `<html><body><script>console.log("Hello, World!");let foo=1;foo++</script></body></html>`,
`<!-- js module not changed -->
<html>
<body>
<script type="module">
// comment
console.log('Hello, World!');
let foo = 1;
foo++;
</script>
</body>
</html>`: `<html><body><script type="module">
// comment
console.log('Hello, World!');
let foo = 1;
foo++;
</script></body></html>`,
} {
var got, err = template.MiniHTML(give)
assert.NoError(t, err)
assert.Equal(t, want, got)
}
}()
}
wg.Wait()
}