mirror of
https://github.com/tarampampam/error-pages.git
synced 2024-08-30 18:22:40 +00:00
Compare commits
50 Commits
v3.2.0
...
v3.0.0-alp
Author | SHA1 | Date | |
---|---|---|---|
05a513898a | |||
3c5b8f6337 | |||
df2163e41e | |||
afaef54ddf | |||
e3499a940c | |||
f3c2646f4e | |||
e91a6f99fd | |||
2664c467c0 | |||
52063654ab | |||
616e4ad7e2 | |||
1563a8c226 | |||
8b29a95bb1 | |||
b7ed431c8f | |||
6766f3f787 | |||
75476a22c0 | |||
4e653f37d3 | |||
4e7abaafbf | |||
9e719c7feb | |||
cbbe2ea631 | |||
4bb567b1d6 | |||
cea34f0475 | |||
86aa4aab93 | |||
7ed6ec414f | |||
14815b4c73 | |||
b7228c3933 | |||
1fa6e1de4f | |||
5976f2903d | |||
63b125e080 | |||
a759504971 | |||
2776c41e0d | |||
42c4e7bf84 | |||
c80229ea05 | |||
f1bcd0d8c4 | |||
3505288e7a | |||
ea7dfe4870 | |||
6326e78cf4 | |||
3f22916cbb | |||
015e686635 | |||
2a1fa5c108 | |||
1682a3513f | |||
65fc5ecc7f | |||
c1eaee0287 | |||
ceeb7f9384 | |||
a52dbde00c | |||
1b94bc367c | |||
669aaf6a1e | |||
b71475fcf7 | |||
71f8cfc162 | |||
e2193cd82e | |||
15d1bcf9c7 |
@ -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"
|
||||
}
|
33
.github/workflows/release.yml
vendored
33
.github/workflows/release.yml
vendored
@ -36,7 +36,7 @@ jobs:
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
- 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'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@ -44,16 +44,6 @@ jobs:
|
||||
path: out/
|
||||
if-no-files-found: error
|
||||
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:
|
||||
name: Update the demo (GitHub Pages)
|
||||
@ -86,19 +76,20 @@ jobs:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: docker/build-push-action@v6
|
||||
- uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||
build-args: "APP_VERSION=${{ steps.slug.outputs.version }}"
|
||||
tags: |
|
||||
tarampampam/error-pages:latest
|
||||
tarampampam/error-pages:${{ steps.slug.outputs.version }}
|
||||
tarampampam/error-pages:${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}
|
||||
tarampampam/error-pages:${{ steps.slug.outputs.version-major }}
|
||||
ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:latest
|
||||
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 }}.${{ steps.slug.outputs.version-minor }}
|
||||
ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version-major }}
|
||||
tags: ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version }}
|
||||
# tags: | # TODO: uncomment after the stable release
|
||||
# tarampampam/error-pages:latest
|
||||
# tarampampam/error-pages:${{ steps.slug.outputs.version }}
|
||||
# tarampampam/error-pages:${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}
|
||||
# tarampampam/error-pages:${{ steps.slug.outputs.version-major }}
|
||||
# ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:latest
|
||||
# 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 }}.${{ steps.slug.outputs.version-minor }}
|
||||
# ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version-major }}
|
||||
|
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@ -83,7 +83,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- {uses: gacts/github-slug@v1, id: slug}
|
||||
- uses: docker/build-push-action@v6
|
||||
- uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
|
25
Dockerfile
25
Dockerfile
@ -3,7 +3,7 @@
|
||||
# -✂- this stage is used to develop and build the application locally -------------------------------------------------
|
||||
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"
|
||||
|
||||
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" .`
|
||||
ARG APP_VERSION="undefined@docker"
|
||||
|
||||
# copy the source code
|
||||
COPY . /src
|
||||
|
||||
RUN set -x \
|
||||
RUN --mount=type=bind,source=.,target=/src set -x \
|
||||
&& go generate ./... \
|
||||
&& 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/ \
|
||||
@ -48,11 +45,10 @@ FROM docker.io/library/alpine:3.20 AS rootfs
|
||||
WORKDIR /tmp/rootfs
|
||||
|
||||
# prepare rootfs for runtime
|
||||
RUN set -x \
|
||||
&& mkdir -p ./etc/ssl/certs ./bin \
|
||||
RUN --mount=type=bind,source=.,target=/src set -x \
|
||||
&& mkdir -p ./etc ./bin \
|
||||
&& echo 'appuser:x:10001:10001::/nonexistent:/sbin/nologin' > ./etc/passwd \
|
||||
&& echo 'appuser:x:10001:' > ./etc/group \
|
||||
&& cp /etc/ssl/certs/ca-certificates.crt ./etc/ssl/certs/
|
||||
&& echo 'appuser:x:10001:' > ./etc/group
|
||||
|
||||
# take the binary from the compile stage
|
||||
COPY --from=compile /tmp/error-pages ./bin/error-pages
|
||||
@ -73,7 +69,7 @@ ARG APP_VERSION="undefined@docker"
|
||||
LABEL \
|
||||
# docs: https://github.com/opencontainers/image-spec/blob/master/annotations.md
|
||||
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.source="https://github.com/tarampampam/error-pages" \
|
||||
org.opencontainers.image.vendor="tarampampam" \
|
||||
@ -91,12 +87,11 @@ WORKDIR /opt
|
||||
# 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
|
||||
|
||||
ENV LOG_LEVEL="warn" \
|
||||
LOG_FORMAT="json"
|
||||
|
||||
# docs: https://docs.docker.com/reference/dockerfile/#healthcheck
|
||||
HEALTHCHECK --interval=10s --start-interval=1s --start-period=5s --timeout=2s CMD ["/bin/error-pages", "healthcheck"]
|
||||
HEALTHCHECK --interval=10s --start-interval=1s --start-period=5s --timeout=2s CMD [\
|
||||
"/bin/error-pages", "--log-format", "json", "healthcheck" \
|
||||
]
|
||||
|
||||
ENTRYPOINT ["/bin/error-pages"]
|
||||
|
||||
CMD ["serve"]
|
||||
CMD ["--log-format", "json", "serve"]
|
||||
|
4
go.mod
4
go.mod
@ -4,10 +4,8 @@ go 1.22
|
||||
|
||||
require (
|
||||
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/v3 v3.0.0-alpha9
|
||||
github.com/valyala/fasthttp v1.55.0
|
||||
go.uber.org/automaxprocs v1.5.3
|
||||
)
|
||||
|
||||
@ -20,8 +18,8 @@ require (
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.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/fasthttp v1.55.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
7
go.sum
7
go.sum
@ -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/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/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/go.mod h1:AIqom6Q60U4tiqHp41i7+/AB2XHgi1WvQ7jOFlccmZ4=
|
||||
github.com/urfave/cli/v3 v3.0.0-alpha9 h1:P0RMy5fQm1AslQS+XCmy9UknDXctOmG/q/FZkUFnJSo=
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
_ "github.com/urfave/cli-docs/v3" // required for `go generate` to work
|
||||
"github.com/urfave/cli/v3"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/appmeta"
|
||||
@ -16,7 +17,7 @@ import (
|
||||
"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.
|
||||
func NewApp(appName string) *cli.Command { //nolint:funlen
|
||||
@ -24,7 +25,7 @@ func NewApp(appName string) *cli.Command { //nolint:funlen
|
||||
logLevelFlag = cli.StringFlag{
|
||||
Name: "log-level",
|
||||
Value: logger.InfoLevel.String(),
|
||||
Usage: "Logging level (" + strings.Join(logger.LevelStrings(), "/") + ")",
|
||||
Usage: "logging level (" + strings.Join(logger.LevelStrings(), "/") + ")",
|
||||
Sources: cli.EnvVars("LOG_LEVEL"),
|
||||
OnlyOnce: true,
|
||||
Config: cli.StringConfig{TrimSpace: true},
|
||||
@ -40,7 +41,7 @@ func NewApp(appName string) *cli.Command { //nolint:funlen
|
||||
logFormatFlag = cli.StringFlag{
|
||||
Name: "log-format",
|
||||
Value: logger.ConsoleFormat.String(),
|
||||
Usage: "Logging format (" + strings.Join(logger.FormatStrings(), "/") + ")",
|
||||
Usage: "logging format (" + strings.Join(logger.FormatStrings(), "/") + ")",
|
||||
Sources: cli.EnvVars("LOG_FORMAT"),
|
||||
OnlyOnce: true,
|
||||
Config: cli.StringConfig{TrimSpace: true},
|
||||
@ -79,7 +80,7 @@ func NewApp(appName string) *cli.Command { //nolint:funlen
|
||||
serve.NewCommand(log),
|
||||
build.NewCommand(log),
|
||||
healthcheck.NewCommand(log, healthcheck.NewHTTPHealthChecker()),
|
||||
perftest.NewCommand(),
|
||||
perftest.NewCommand(log),
|
||||
},
|
||||
Version: fmt.Sprintf("%s (%s)", appmeta.Version(), runtime.Version()),
|
||||
Flags: []cli.Flag{ // global flags
|
||||
|
@ -39,24 +39,21 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit
|
||||
cmd command
|
||||
cfg = config.New()
|
||||
|
||||
addTplFlag = shared.AddTemplatesFlag
|
||||
disableTplFlag = shared.DisableTemplateNamesFlag
|
||||
addCodeFlag = shared.AddHTTPCodesFlag
|
||||
disableL10nFlag = shared.DisableL10nFlag
|
||||
disableMinificationFlag = shared.DisableMinificationFlag
|
||||
createIndexFlag = cli.BoolFlag{
|
||||
Name: "index",
|
||||
Aliases: []string{"i"},
|
||||
Usage: "Generate index.html file with links to all error pages",
|
||||
Category: shared.CategoryBuild,
|
||||
addTplFlag = shared.AddTemplatesFlag
|
||||
disableTplFlag = shared.DisableTemplateNamesFlag
|
||||
addCodeFlag = shared.AddHTTPCodesFlag
|
||||
disableL10nFlag = shared.DisableL10nFlag
|
||||
createIndexFlag = cli.BoolFlag{
|
||||
Name: "index",
|
||||
Aliases: []string{"i"},
|
||||
Usage: "generate index.html file with links to all error pages",
|
||||
}
|
||||
targetDirFlag = cli.StringFlag{
|
||||
Name: "target-dir",
|
||||
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
|
||||
Config: cli.StringConfig{TrimSpace: true},
|
||||
Category: shared.CategoryBuild,
|
||||
OnlyOnce: true,
|
||||
Validator: func(dir string) error {
|
||||
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",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
cfg.L10n.Disable = c.Bool(disableL10nFlag.Name)
|
||||
cfg.DisableMinification = c.Bool(disableMinificationFlag.Name)
|
||||
cmd.opt.createIndex = c.Bool(createIndexFlag.Name)
|
||||
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,
|
||||
&createIndexFlag,
|
||||
&targetDirFlag,
|
||||
&disableMinificationFlag,
|
||||
},
|
||||
}
|
||||
|
||||
return cmd.c
|
||||
}
|
||||
|
||||
func (cmd *command) Run( //nolint:funlen,gocognit
|
||||
func (cmd *command) Run( //nolint:funlen
|
||||
ctx context.Context,
|
||||
log *logger.Logger,
|
||||
cfg *config.Config,
|
||||
@ -175,21 +170,13 @@ func (cmd *command) Run( //nolint:funlen,gocognit
|
||||
|
||||
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),
|
||||
Message: codeDescription.Message,
|
||||
Description: codeDescription.Description,
|
||||
L10nDisabled: cfg.L10n.Disable,
|
||||
ShowRequestDetails: false,
|
||||
}); 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
|
||||
return err
|
||||
}
|
||||
|
@ -1,59 +1,32 @@
|
||||
package perftest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
|
||||
"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.
|
||||
func NewCommand() *cli.Command { //nolint:funlen
|
||||
func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit
|
||||
var (
|
||||
portFlag = shared.ListenPortFlag
|
||||
durationFlag = cli.DurationFlag{
|
||||
Name: "duration",
|
||||
Aliases: []string{"d"},
|
||||
Usage: "Duration of test",
|
||||
Value: 15 * time.Second, //nolint:mnd
|
||||
Usage: "duration of the test",
|
||||
Value: 10 * time.Second, //nolint:mnd
|
||||
Validator: func(d time.Duration) error {
|
||||
if d <= time.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{
|
||||
Name: "threads",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "Number of threads to use",
|
||||
Value: max(2, uint64(math.Round(float64(runtime.NumCPU())/1.3))), //nolint:mnd
|
||||
Usage: "number of threads",
|
||||
Value: max(2, uint64(runtime.NumCPU()/2)), //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
|
||||
},
|
||||
}
|
||||
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
|
||||
@ -96,47 +52,97 @@ func NewCommand() *cli.Command { //nolint:funlen
|
||||
|
||||
return &cli.Command{
|
||||
Name: "perftest",
|
||||
Aliases: []string{"perf", "benchmark", "bench"},
|
||||
Aliases: []string{"perf", "test"},
|
||||
Hidden: true,
|
||||
Usage: "Performance (load) test for the HTTP server (locally installed wrk is required)",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
var wrkBinPath, lErr = exec.LookPath("wrk")
|
||||
if lErr != nil {
|
||||
return fmt.Errorf("seems like wrk (https://github.com/wg/wrk) is not installed: %w", lErr)
|
||||
Usage: "Simple performance (load) test for the HTTP server",
|
||||
Action: func(ctx context.Context, c *cli.Command) error { // TODO: use fasthttp.Client
|
||||
var (
|
||||
perfCtx, cancel = context.WithTimeout(ctx, c.Duration(durationFlag.Name))
|
||||
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 {
|
||||
if stdOut, stdErr, err := wrkRunTest(ctx,
|
||||
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)
|
||||
for i := uint64(0); i < c.Uint(threadsFlag.Name); i++ {
|
||||
wg.Add(1)
|
||||
|
||||
return fmt.Errorf("failed to execute the test: %w (%s)", err, string(errData))
|
||||
} else {
|
||||
var outData, _ = io.ReadAll(stdOut)
|
||||
go func(log *logger.Logger) {
|
||||
defer wg.Done()
|
||||
|
||||
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")
|
||||
|
||||
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
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
},
|
||||
@ -144,51 +150,54 @@ func NewCommand() *cli.Command { //nolint:funlen
|
||||
&portFlag,
|
||||
&durationFlag,
|
||||
&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(
|
||||
ctx context.Context,
|
||||
wrkBinPath string,
|
||||
threadsCount, connectionsCount uint16,
|
||||
duration time.Duration,
|
||||
port uint16,
|
||||
scriptContent string,
|
||||
) (io.Reader, io.Reader, error) {
|
||||
var tmpFile, tErr = os.CreateTemp("", "ep-perf-one-page")
|
||||
if tErr != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create a temporary file: %w", tErr)
|
||||
}
|
||||
|
||||
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),
|
||||
// makeRequest creates a new HTTP request for the performance test.
|
||||
func makeRequest(ctx context.Context, port uint16) (*http.Request, error) {
|
||||
var req, rErr = http.NewRequestWithContext(ctx,
|
||||
http.MethodGet,
|
||||
fmt.Sprintf(
|
||||
"http://127.0.0.1:%d/%d.html?rnd=%d", // for load testing purposes only
|
||||
port,
|
||||
randomIntBetween(400, 418), //nolint:mnd
|
||||
randomIntBetween(1, 999_999_999), //nolint:mnd
|
||||
),
|
||||
http.NoBody,
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
@ -22,9 +22,9 @@ type command struct {
|
||||
|
||||
opt struct {
|
||||
http struct { // our HTTP server
|
||||
addr string
|
||||
port uint16
|
||||
readBufferSize uint
|
||||
addr string
|
||||
port uint16
|
||||
// readBufferSize uint
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -38,37 +38,33 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
|
||||
)
|
||||
|
||||
var (
|
||||
addrFlag = shared.ListenAddrFlag
|
||||
portFlag = shared.ListenPortFlag
|
||||
addTplFlag = shared.AddTemplatesFlag
|
||||
disableTplFlag = shared.DisableTemplateNamesFlag
|
||||
addCodeFlag = shared.AddHTTPCodesFlag
|
||||
disableL10nFlag = shared.DisableL10nFlag
|
||||
disableMinificationFlag = shared.DisableMinificationFlag
|
||||
jsonFormatFlag = cli.StringFlag{
|
||||
addrFlag = shared.ListenAddrFlag
|
||||
portFlag = shared.ListenPortFlag
|
||||
addTplFlag = shared.AddTemplatesFlag
|
||||
disableTplFlag = shared.DisableTemplateNamesFlag
|
||||
addCodeFlag = shared.AddHTTPCodesFlag
|
||||
disableL10nFlag = shared.DisableL10nFlag
|
||||
jsonFormatFlag = cli.StringFlag{
|
||||
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)",
|
||||
Sources: env("RESPONSE_JSON_FORMAT"),
|
||||
Category: shared.CategoryFormats,
|
||||
OnlyOnce: true,
|
||||
Config: trim,
|
||||
}
|
||||
xmlFormatFlag = cli.StringFlag{
|
||||
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)",
|
||||
Sources: env("RESPONSE_XML_FORMAT"),
|
||||
Category: shared.CategoryFormats,
|
||||
OnlyOnce: true,
|
||||
Config: trim,
|
||||
}
|
||||
plainTextFormatFlag = cli.StringFlag{
|
||||
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)",
|
||||
Sources: env("RESPONSE_PLAINTEXT_FORMAT"),
|
||||
Category: shared.CategoryFormats,
|
||||
OnlyOnce: true,
|
||||
Config: trim,
|
||||
}
|
||||
@ -76,19 +72,17 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
|
||||
Name: "template-name",
|
||||
Aliases: []string{"t"},
|
||||
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(), ", ") + ")",
|
||||
Sources: env("TEMPLATE_NAME"),
|
||||
Category: shared.CategoryTemplates,
|
||||
OnlyOnce: true,
|
||||
Config: trim,
|
||||
}
|
||||
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.DefaultCodeToRender),
|
||||
Sources: env("DEFAULT_ERROR_PAGE"),
|
||||
Category: shared.CategoryCodes,
|
||||
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.DefaultCodeToRender),
|
||||
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)
|
||||
@ -100,19 +94,17 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
|
||||
}
|
||||
sendSameHTTPCodeFlag = cli.BoolFlag{
|
||||
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)",
|
||||
Value: cfg.RespondWithSameHTTPCode,
|
||||
Sources: env("SEND_SAME_HTTP_CODE"),
|
||||
Category: shared.CategoryOther,
|
||||
OnlyOnce: true,
|
||||
}
|
||||
showDetailsFlag = cli.BoolFlag{
|
||||
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,
|
||||
Sources: env("SHOW_DETAILS"),
|
||||
Category: shared.CategoryOther,
|
||||
OnlyOnce: true,
|
||||
}
|
||||
proxyHeadersListFlag = cli.StringFlag{
|
||||
@ -130,16 +122,14 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
|
||||
|
||||
return nil
|
||||
},
|
||||
Category: shared.CategoryOther,
|
||||
OnlyOnce: true,
|
||||
Config: trim,
|
||||
}
|
||||
rotationModeFlag = cli.StringFlag{
|
||||
Name: "rotation-mode",
|
||||
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"),
|
||||
Category: shared.CategoryTemplates,
|
||||
OnlyOnce: true,
|
||||
Config: trim,
|
||||
Validator: func(s string) error {
|
||||
@ -150,40 +140,28 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
|
||||
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"),
|
||||
Category: shared.CategoryOther,
|
||||
OnlyOnce: true,
|
||||
}
|
||||
)
|
||||
|
||||
// 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)"
|
||||
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
|
||||
|
||||
cmd.c = &cli.Command{
|
||||
Name: "serve",
|
||||
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,
|
||||
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))
|
||||
cfg.L10n.Disable = c.Bool(disableL10nFlag.Name)
|
||||
cfg.DefaultCodeToRender = uint16(c.Uint(defaultCodeToRenderFlag.Name))
|
||||
cfg.RespondWithSameHTTPCode = c.Bool(sendSameHTTPCodeFlag.Name)
|
||||
cfg.RotationMode, _ = config.ParseRotationMode(c.String(rotationModeFlag.Name))
|
||||
cfg.ShowDetails = c.Bool(showDetailsFlag.Name)
|
||||
cfg.DisableMinification = c.Bool(disableMinificationFlag.Name)
|
||||
|
||||
{ // override default JSON, XML, and PlainText formats
|
||||
if c.IsSet(jsonFormatFlag.Name) {
|
||||
@ -304,8 +282,6 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
|
||||
&showDetailsFlag,
|
||||
&proxyHeadersListFlag,
|
||||
&rotationModeFlag,
|
||||
&readBufferSizeFlag,
|
||||
&disableMinificationFlag,
|
||||
},
|
||||
}
|
||||
|
||||
@ -314,7 +290,7 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
|
||||
|
||||
// Run current command.
|
||||
func (cmd *command) Run(ctx context.Context, log *logger.Logger, cfg *config.Config) error { //nolint:funlen
|
||||
var srv = appHttp.NewServer(log, cmd.opt.http.readBufferSize)
|
||||
var srv = appHttp.NewServer(log)
|
||||
|
||||
if err := srv.Register(cfg); err != nil {
|
||||
return err
|
||||
|
@ -36,7 +36,7 @@ func TestCommand_Run(t *testing.T) {
|
||||
"--add-template", "./testdata/foo-template.html",
|
||||
"--disable-template", "ghost",
|
||||
"--disable-template", "<unknown>",
|
||||
"--add-code", "200=Code/Description",
|
||||
"--add-http-code", "200=Code/Description",
|
||||
"--json-format", "json format",
|
||||
"--xml-format", "xml format",
|
||||
"--plaintext-format", "plaintext format",
|
||||
|
@ -11,15 +11,6 @@ import (
|
||||
"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.
|
||||
// https://github.com/urfave/cli/issues/1926
|
||||
|
||||
@ -29,7 +20,6 @@ var ListenAddrFlag = cli.StringFlag{
|
||||
Usage: "IP (v4 or v6) address to listen on",
|
||||
Value: "0.0.0.0", // bind to all interfaces by default
|
||||
Sources: cli.EnvVars("LISTEN_ADDR"),
|
||||
Category: CategoryHTTP,
|
||||
OnlyOnce: true,
|
||||
Config: cli.StringConfig{TrimSpace: true},
|
||||
Validator: func(ip string) error {
|
||||
@ -51,7 +41,6 @@ var ListenPortFlag = cli.UintFlag{
|
||||
Usage: "TCP port number",
|
||||
Value: 8080, // default port number
|
||||
Sources: cli.EnvVars("LISTEN_PORT"),
|
||||
Category: CategoryHTTP,
|
||||
OnlyOnce: true,
|
||||
Validator: func(port uint64) error {
|
||||
if port == 0 || port > 65535 {
|
||||
@ -64,11 +53,9 @@ var ListenPortFlag = cli.UintFlag{
|
||||
|
||||
var AddTemplatesFlag = cli.StringSliceFlag{
|
||||
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)",
|
||||
Config: cli.StringConfig{TrimSpace: true},
|
||||
Sources: cli.EnvVars("ADD_TEMPLATE"),
|
||||
Category: CategoryTemplates,
|
||||
Config: cli.StringConfig{TrimSpace: true},
|
||||
Validator: func(paths []string) error {
|
||||
for _, path := range paths {
|
||||
if path == "" {
|
||||
@ -85,19 +72,18 @@ var AddTemplatesFlag = cli.StringSliceFlag{
|
||||
}
|
||||
|
||||
var DisableTemplateNamesFlag = cli.StringSliceFlag{
|
||||
Name: "disable-template",
|
||||
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},
|
||||
Category: CategoryTemplates,
|
||||
Name: "disable-template",
|
||||
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},
|
||||
}
|
||||
|
||||
var AddHTTPCodesFlag = cli.StringMapFlag{
|
||||
Name: "add-code",
|
||||
Usage: "To add a new HTTP status code, provide the code and its message/description using this flag (the format " +
|
||||
Name: "add-http-code",
|
||||
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 " +
|
||||
"once, for example, '4**' will cover all 4xx codes unless a more specific code is described previously)",
|
||||
Config: cli.StringConfig{TrimSpace: true},
|
||||
Category: CategoryCodes,
|
||||
Config: cli.StringConfig{TrimSpace: true},
|
||||
Validator: func(codes map[string]string) error {
|
||||
for code, msgAndDesc := range codes {
|
||||
if code == "" {
|
||||
@ -142,16 +128,7 @@ func ParseHTTPCodes(codes map[string]string) map[string]config.CodeDescription {
|
||||
|
||||
var DisableL10nFlag = cli.BoolFlag{
|
||||
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"),
|
||||
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,
|
||||
}
|
||||
|
@ -91,7 +91,6 @@ func TestAddTemplatesFlag(t *testing.T) {
|
||||
var flag = shared.AddTemplatesFlag
|
||||
|
||||
assert.Equal(t, "add-template", flag.Name)
|
||||
assert.Contains(t, flag.Sources.String(), "ADD_TEMPLATE")
|
||||
|
||||
for wantErrMsg, giveValue := range map[string][]string{
|
||||
"missing template path": {""},
|
||||
@ -123,7 +122,7 @@ func TestAddHTTPCodesFlag(t *testing.T) {
|
||||
|
||||
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 {
|
||||
giveValue map[string]string
|
||||
@ -217,12 +216,3 @@ func TestDisableL10nFlag(t *testing.T) {
|
||||
assert.Equal(t, "disable-l10n", flag.Name)
|
||||
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")
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
//go:build generate
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
@ -16,10 +17,8 @@ func main() {
|
||||
if stat, err := os.Stat(readmePath); err == nil && stat.Mode().IsRegular() {
|
||||
if err = cliDocs.ToTabularToFileBetweenTags(cli.NewApp(""), "error-pages", readmePath); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
println("✔ cli docs updated successfully")
|
||||
}
|
||||
} 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())
|
||||
}
|
||||
}
|
@ -56,9 +56,6 @@ type Config struct {
|
||||
// ShowDetails determines whether to show additional details in the error response, extracted from the
|
||||
// incoming request (if supported by the template).
|
||||
ShowDetails bool
|
||||
|
||||
// DisableMinification determines whether to disable minification of the rendered content (e.g., HTML, CSS) or not.
|
||||
DisableMinification bool
|
||||
}
|
||||
|
||||
const defaultJSONFormat string = `{
|
||||
@ -75,7 +72,7 @@ const defaultJSONFormat string = `{
|
||||
"service_name": {{ service_name | json }},
|
||||
"service_port": {{ service_port | json }},
|
||||
"request_id": {{ request_id | json }},
|
||||
"timestamp": {{ nowUnix }}
|
||||
"timestamp": {{ now.Unix }}
|
||||
}{{ end }}
|
||||
}
|
||||
` // an empty line at the end is important for better UX
|
||||
@ -94,7 +91,7 @@ const defaultXMLFormat string = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<serviceName>{{ service_name }}</serviceName>
|
||||
<servicePort>{{ service_port }}</servicePort>
|
||||
<requestID>{{ request_id }}</requestID>
|
||||
<timestamp>{{ nowUnix }}</timestamp>
|
||||
<timestamp>{{ now.Unix }}</timestamp>
|
||||
</details>{{ end }}
|
||||
</error>
|
||||
` // an empty line at the end is important for better UX
|
||||
@ -110,7 +107,7 @@ Ingress Name: {{ ingress_name }}
|
||||
Service Name: {{ service_name }}
|
||||
Service Port: {{ service_port }}
|
||||
Request ID: {{ request_id }}
|
||||
Timestamp: {{ nowUnix }}{{ end }}
|
||||
Timestamp: {{ now.Unix }}{{ end }}
|
||||
` // an empty line at the end is important for better UX
|
||||
|
||||
//nolint:lll
|
||||
|
@ -24,7 +24,6 @@ func TestNew(t *testing.T) {
|
||||
assert.NotEmpty(t, cfg.TemplateName)
|
||||
assert.True(t, cfg.Templates.Has(cfg.TemplateName))
|
||||
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) {
|
||||
|
@ -1,111 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
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
|
||||
}
|
@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@ -16,32 +15,7 @@ import (
|
||||
)
|
||||
|
||||
// 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, 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
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
func New(cfg *config.Config, log *logger.Logger) fasthttp.RequestHandler { //nolint:funlen,gocognit,gocyclo
|
||||
return func(ctx *fasthttp.RequestCtx) {
|
||||
var (
|
||||
reqHeaders = &ctx.Request.Header
|
||||
@ -132,90 +106,57 @@ func New(cfg *config.Config, log *logger.Logger) (_ fasthttp.RequestHandler, clo
|
||||
|
||||
switch {
|
||||
case format == jsonFormat && cfg.Formats.JSON != "":
|
||||
if cached, ok := cache.Get(cfg.Formats.JSON, tplProps); ok { // cache hit
|
||||
write(ctx, log, cached)
|
||||
} else { // cache miss
|
||||
if content, err := template.Render(cfg.Formats.JSON, tplProps); err != nil {
|
||||
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
|
||||
}
|
||||
if content, err := template.Render(cfg.Formats.JSON, tplProps); err != nil {
|
||||
j, _ := json.Marshal(fmt.Sprintf("Failed to render the JSON template: %s", err.Error()))
|
||||
write(ctx, log, j)
|
||||
} else {
|
||||
write(ctx, log, content)
|
||||
}
|
||||
|
||||
case format == xmlFormat && cfg.Formats.XML != "":
|
||||
if cached, ok := cache.Get(cfg.Formats.XML, tplProps); ok { // cache hit
|
||||
write(ctx, log, cached)
|
||||
} else { // cache miss
|
||||
if content, err := template.Render(cfg.Formats.XML, tplProps); err != nil {
|
||||
write(ctx, log, fmt.Sprintf(
|
||||
"<?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)
|
||||
}
|
||||
if content, err := template.Render(cfg.Formats.XML, tplProps); err != nil {
|
||||
write(ctx, log, fmt.Sprintf(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<error>Failed to render the XML template: %s</error>", err.Error(),
|
||||
))
|
||||
} else {
|
||||
write(ctx, log, content)
|
||||
}
|
||||
|
||||
case format == htmlFormat:
|
||||
var templateName = templateToUse(cfg)
|
||||
|
||||
if tpl, found := cfg.Templates.Get(templateName); found { //nolint:nestif
|
||||
if cached, ok := cache.Get(tpl, tplProps); ok { // cache hit
|
||||
write(ctx, log, cached)
|
||||
} else { // cache miss
|
||||
if content, err := template.Render(tpl, tplProps); err != nil {
|
||||
// TODO: add GZIP compression for the HTML content support
|
||||
write(ctx, log, fmt.Sprintf(
|
||||
"<!DOCTYPE html>\n<html><body>Failed to render the HTML template %s: %s</body></html>\n",
|
||||
templateName,
|
||||
err.Error(),
|
||||
))
|
||||
} 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))
|
||||
|
||||
write(ctx, log, content)
|
||||
}
|
||||
if tpl, found := cfg.Templates.Get(templateName); found {
|
||||
if content, err := template.Render(tpl, tplProps); err != nil {
|
||||
// TODO: add GZIP compression for the HTML content support
|
||||
write(ctx, log, fmt.Sprintf(
|
||||
"<!DOCTYPE html>\n<html><body>Failed to render the HTML template %s: %s</body></html>",
|
||||
templateName,
|
||||
err.Error(),
|
||||
))
|
||||
} else {
|
||||
write(ctx, log, content)
|
||||
}
|
||||
} else {
|
||||
write(ctx, log, fmt.Sprintf(
|
||||
"<!DOCTYPE html>\n<html><body>Template %s not found and cannot be used</body></html>\n", templateName,
|
||||
"<!DOCTYPE html>\n<html><body>Template %s not found and cannot be used</body></html>", templateName,
|
||||
))
|
||||
}
|
||||
|
||||
default: // plainTextFormat as default
|
||||
if cfg.Formats.PlainText != "" { //nolint:nestif
|
||||
if cached, ok := cache.Get(cfg.Formats.PlainText, tplProps); ok { // cache hit
|
||||
write(ctx, log, cached)
|
||||
} else { // cache miss
|
||||
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)
|
||||
}
|
||||
if cfg.Formats.PlainText != "" {
|
||||
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 {
|
||||
write(ctx, log, content)
|
||||
}
|
||||
} else {
|
||||
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.
|
||||
|
||||
Supported formats: JSON, XML, HTML, Plain Text
|
||||
`)
|
||||
Supported formats: JSON, XML, HTML, Plain Text`)
|
||||
}
|
||||
}
|
||||
}, func() { stopOnce.Do(func() { close(stopCh) }) }
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -48,7 +48,7 @@ func TestHandler(t *testing.T) {
|
||||
wantStatusCode: http.StatusOK,
|
||||
wantHeaders: map[string]string{"Content-Type": "text/html; charset=utf-8"},
|
||||
wantBodyIncludes: []string{
|
||||
"<!doctype html>",
|
||||
"<!DOCTYPE html>",
|
||||
"<title>407: Proxy Authentication Required",
|
||||
"Proxy Authentication Required",
|
||||
},
|
||||
@ -159,8 +159,7 @@ func TestHandler(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var handler, closeCache = error_page.New(tt.giveConfig(), logger.NewNop())
|
||||
defer closeCache()
|
||||
var handler = error_page.New(tt.giveConfig(), logger.NewNop())
|
||||
|
||||
req, reqErr := http.NewRequest(http.MethodGet, tt.giveUrl, http.NoBody)
|
||||
require.NoError(t, reqErr)
|
||||
@ -203,11 +202,9 @@ func TestRotationModeOnEachRequest(t *testing.T) {
|
||||
lastResponseBody string
|
||||
changedTimes int
|
||||
|
||||
handler, closeCache = error_page.New(&cfg, logger.NewNop())
|
||||
handler = error_page.New(&cfg, logger.NewNop())
|
||||
)
|
||||
|
||||
defer func() { closeCache(); closeCache(); closeCache() }() // multiple calls should not panic
|
||||
|
||||
for range 300 {
|
||||
req, reqErr := http.NewRequest(http.MethodGet, "http://testing/", http.NoBody)
|
||||
require.NoError(t, reqErr)
|
||||
|
@ -23,16 +23,16 @@ import (
|
||||
|
||||
// Server is an HTTP server for serving error pages.
|
||||
type Server struct {
|
||||
log *logger.Logger
|
||||
server *fasthttp.Server
|
||||
beforeStop func()
|
||||
log *logger.Logger
|
||||
server *fasthttp.Server
|
||||
}
|
||||
|
||||
// NewServer creates a new HTTP server.
|
||||
func NewServer(log *logger.Logger, readBufferSize uint) Server {
|
||||
func NewServer(log *logger.Logger) Server {
|
||||
const (
|
||||
readTimeout = 30 * time.Second
|
||||
writeTimeout = readTimeout + 10*time.Second // should be bigger than the read timeout
|
||||
readTimeout = 30 * time.Second
|
||||
writeTimeout = readTimeout + 10*time.Second // should be bigger than the read timeout
|
||||
maxHeaderBytes = (1 << 20) * 5 //nolint:mnd // 5 MB
|
||||
)
|
||||
|
||||
return Server{
|
||||
@ -40,38 +40,33 @@ func NewServer(log *logger.Logger, readBufferSize uint) Server {
|
||||
server: &fasthttp.Server{
|
||||
ReadTimeout: readTimeout,
|
||||
WriteTimeout: writeTimeout,
|
||||
ReadBufferSize: int(readBufferSize),
|
||||
ReadBufferSize: maxHeaderBytes,
|
||||
DisablePreParseMultipartForm: true,
|
||||
NoDefaultServerHeader: true,
|
||||
CloseOnShutdown: true,
|
||||
Logger: logger.NewStdLog(log),
|
||||
},
|
||||
beforeStop: func() {}, // noop
|
||||
}
|
||||
}
|
||||
|
||||
// Register server handlers, middlewares, etc.
|
||||
func (s *Server) Register(cfg *config.Config) error { //nolint:funlen
|
||||
func (s *Server) Register(cfg *config.Config) error {
|
||||
var (
|
||||
liveHandler = live.New()
|
||||
versionHandler = version.New(appmeta.Version())
|
||||
faviconHandler = static.New(static.Favicon)
|
||||
|
||||
errorPagesHandler, closeCache = ep.New(cfg, s.log)
|
||||
liveHandler = live.New()
|
||||
versionHandler = version.New(appmeta.Version())
|
||||
errorPagesHandler = ep.New(cfg, s.log)
|
||||
faviconHandler = static.New(static.Favicon)
|
||||
|
||||
notFound = http.StatusText(http.StatusNotFound) + "\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) {
|
||||
var url, method = string(ctx.Path()), string(ctx.Method())
|
||||
|
||||
switch {
|
||||
// live endpoints
|
||||
case url == "/healthz" || url == "/health/live" || url == "/health" || url == "/live":
|
||||
case url == "/health/live" || url == "/health" || url == "/healthz" || url == "/live":
|
||||
liveHandler(ctx)
|
||||
|
||||
// version endpoint
|
||||
@ -87,9 +82,8 @@ func (s *Server) Register(cfg *config.Config) error { //nolint:funlen
|
||||
// - /{code}.html
|
||||
// - /{code}.htm
|
||||
// - /{code}
|
||||
//
|
||||
// the HTTP method is not limited to GET and HEAD - it can be any
|
||||
case url == "/" || ep.URLContainsCode(url) || ep.HeadersContainCode(&ctx.Request.Header):
|
||||
case method == fasthttp.MethodGet &&
|
||||
(url == "/" || ep.URLContainsCode(url) || ep.HeadersContainCode(&ctx.Request.Header)):
|
||||
errorPagesHandler(ctx)
|
||||
|
||||
// wrong requests handling
|
||||
@ -141,7 +135,5 @@ func (s *Server) Stop(timeout time.Duration) error {
|
||||
var ctx, cancel = context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
s.beforeStop()
|
||||
|
||||
return s.server.ShutdownWithContext(ctx)
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ import (
|
||||
// TestRouting in fact is a test for the whole server, because it tests all the routes and their handlers.
|
||||
func TestRouting(t *testing.T) {
|
||||
var (
|
||||
srv = appHttp.NewServer(logger.NewNop(), 1025*5)
|
||||
srv = appHttp.NewServer(logger.NewNop())
|
||||
cfg = config.New()
|
||||
)
|
||||
|
||||
@ -38,7 +38,7 @@ func TestRouting(t *testing.T) {
|
||||
Service Name: {{ service_name }}
|
||||
Service Port: {{ service_port }}
|
||||
Request ID: {{ request_id }}
|
||||
Timestamp: {{ nowUnix }}
|
||||
Timestamp: {{ now.Unix }}
|
||||
</pre>{{ end }}
|
||||
</html>`))
|
||||
|
||||
@ -221,16 +221,6 @@ func TestRouting(t *testing.T) {
|
||||
assert.Contains(t, string(body), "404: Not Found")
|
||||
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) {
|
||||
@ -274,6 +264,15 @@ func TestRouting(t *testing.T) {
|
||||
assert.Equal(t, http.StatusNotFound, status)
|
||||
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)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -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) }
|
@ -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()
|
||||
}
|
@ -16,9 +16,10 @@ import (
|
||||
)
|
||||
|
||||
var builtInFunctions = template.FuncMap{ //nolint:gochecknoglobals
|
||||
// the current time in unix format (seconds since 1970 UTC):
|
||||
// `{{ nowUnix }}` // `1631610000`
|
||||
"nowUnix": func() int64 { return time.Now().Unix() },
|
||||
// current time:
|
||||
// `{{ now.Unix }}` // `1631610000`
|
||||
// `{{ now.Hour }}:{{ now.Minute }}:{{ now.Second }}` // `15:4:5`
|
||||
"now": time.Now,
|
||||
|
||||
// current hostname:
|
||||
// `{{ hostname }}` // `localhost`
|
||||
|
@ -27,7 +27,7 @@ func TestRender_BuiltInFunction(t *testing.T) {
|
||||
wantErrMsg string
|
||||
}{
|
||||
"now (unix)": {
|
||||
giveTemplate: `{{ nowUnix }}`,
|
||||
giveTemplate: `{{ now.Unix }}`,
|
||||
wantResult: strconv.Itoa(int(time.Now().Unix())),
|
||||
},
|
||||
"hostname": {giveTemplate: `{{ hostname }}`, wantResult: hostname},
|
||||
|
@ -341,7 +341,7 @@
|
||||
<!-- {{- end }}{{ if request_id -}} -->
|
||||
<li><span data-l10n>Request ID</span>: <code>{{ request_id }}</code></li>
|
||||
<!-- {{- end -}} -->
|
||||
<li><span data-l10n>Timestamp</span>: <code>{{ nowUnix }}</code></li>
|
||||
<li><span data-l10n>Timestamp</span>: <code>{{ now.Unix }}</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- {{- end -}} -->
|
||||
|
@ -150,7 +150,7 @@
|
||||
<!-- {{- end -}} -->
|
||||
<tr>
|
||||
<td class="name" data-l10n>Timestamp</td>
|
||||
<td class="value">{{ nowUnix }}</td>
|
||||
<td class="value">{{ now.Unix }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -327,7 +327,7 @@
|
||||
<!-- {{- end }}{{ if request_id -}} -->
|
||||
<li><span data-l10n>Request ID</span>: <code>{{ request_id }}</code></li>
|
||||
<!-- {{- end -}} -->
|
||||
<li><span data-l10n>Timestamp</span>: <code>{{ nowUnix }}</code></li>
|
||||
<li><span data-l10n>Timestamp</span>: <code>{{ now.Unix }}</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- {{- end -}} -->
|
||||
|
@ -235,7 +235,7 @@
|
||||
<!-- {{- end -}} -->
|
||||
<tr>
|
||||
<td class="name" data-l10n>Timestamp</td>
|
||||
<td class="value">{{ nowUnix }}</td>
|
||||
<td class="value">{{ now.Unix }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -174,7 +174,7 @@
|
||||
<!-- {{- end }}{{ if request_id -}} -->
|
||||
<p class="output small"><span data-l10n>Request ID</span>: <code>{{ request_id }}</code></p>
|
||||
<!-- {{- end -}} -->
|
||||
<p class="output small"><span data-l10n>Timestamp</span>: <code>{{ nowUnix }}</code></p>
|
||||
<p class="output small"><span data-l10n>Timestamp</span>: <code>{{ now.Unix }}</code></p>
|
||||
</div>
|
||||
<!-- {{- end -}} -->
|
||||
</main>
|
||||
|
@ -157,7 +157,7 @@
|
||||
<!-- {{- end }}{{ if request_id -}} -->
|
||||
<li class="value">{{ request_id }}</li>
|
||||
<!-- {{- end -}} -->
|
||||
<li class="value">{{ nowUnix }}</li>
|
||||
<li class="value">{{ now.Unix }}</li>
|
||||
</ul>
|
||||
<!-- {{- end -}} -->
|
||||
</div>
|
||||
|
@ -442,7 +442,7 @@
|
||||
<!-- {{- end }}{{ if request_id -}} -->
|
||||
<li><span data-l10n>Request ID</span>: <code>{{ request_id }}</code></li>
|
||||
<!-- {{- end -}} -->
|
||||
<li><span data-l10n>Timestamp</span>: <code>{{ nowUnix }}</code></li>
|
||||
<li><span data-l10n>Timestamp</span>: <code>{{ now.Unix }}</code></li>
|
||||
</ul>
|
||||
<!-- {{- end -}} -->
|
||||
</div>
|
||||
|
@ -9,7 +9,7 @@
|
||||
{{ if service_name }}Service name: {{ service_name }}{{ end }}
|
||||
{{ if service_port }}Service port: {{ service_port }}{{ end }}
|
||||
{{ if request_id }}Request ID: {{ request_id }}{{ end }}
|
||||
Timestamp: {{ nowUnix }}
|
||||
Timestamp: {{ now.Unix }}
|
||||
{{ end }}
|
||||
-->
|
||||
<html lang="en">
|
||||
|
@ -253,7 +253,7 @@
|
||||
<!-- {{- end -}} -->
|
||||
<tr>
|
||||
<td class="name" data-l10n>Timestamp</td>
|
||||
<td class="value">{{ nowUnix }}</td>
|
||||
<td class="value">{{ now.Unix }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -167,7 +167,7 @@
|
||||
<!-- {{- end -}} -->
|
||||
<tr>
|
||||
<td class="name"><span data-l10n>Timestamp</span>:</td>
|
||||
<td class="value">{{ nowUnix }}</td>
|
||||
<td class="value">{{ now.Unix }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- {{- end -}} -->
|
||||
|
Reference in New Issue
Block a user