wip: 🔕 temporary commit

This commit is contained in:
Paramtamtam 2024-06-21 17:13:27 +04:00
parent 15d1bcf9c7
commit e2193cd82e
No known key found for this signature in database
GPG Key ID: 366371698FAD0A2B
102 changed files with 581 additions and 6396 deletions

1
.gitignore vendored
View File

@ -8,6 +8,7 @@
## Temp dirs & trash
/temp
/tmp
/*-old
/cmd/test*
.DS_Store
/go.work*

View File

@ -117,6 +117,10 @@ issues:
- tmp
- temp
- testdata
- cmd-old # TODO: remove
- internal-old # TODO: remove
- templates-old # TODO: remove
- test-old # TODO: remove
exclude-rules:
- {path: flags\.go, linters: [gochecknoglobals, lll, mnd, dupl]}
- {path: env\.go, linters: [lll, gosec]}

View File

@ -4,7 +4,7 @@
FROM docker.io/library/golang:1.22-bookworm AS develop
# use the /var/tmp as the GOPATH to reuse the modules cache
ENV GOPATH="/var/tmp"
ENV GOPATH="/var/tmp/go"
RUN set -x \
# renovate: source=github-releases name=abice/go-enum
@ -28,7 +28,9 @@ WORKDIR /src
RUN \
--mount=type=bind,source=go.mod,target=/src/go.mod \
--mount=type=bind,source=go.sum,target=/src/go.sum \
go mod download -x
go mod download -x \
&& find "${GOPATH}" -type d -exec chmod 0777 {} \; \
&& find "${GOPATH}" -type f -exec chmod 0666 {} \;
# -✂- this stage is used to compile the application -------------------------------------------------------------------
FROM develop AS compile
@ -38,7 +40,7 @@ ARG APP_VERSION="undefined@docker"
RUN --mount=type=bind,source=.,target=/src set -x \
&& go generate ./... \
&& CGO_ENABLED=0 LDFLAGS="-s -w -X gh.tarampamp.am/error-pages/internal/version.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/ \
&& /tmp/error-pages --version \
&& /tmp/error-pages -h
@ -90,13 +92,13 @@ USER 10001:10001
WORKDIR /opt
ENV LISTEN_PORT="8080" \
TEMPLATE_NAME="ghost" \
DEFAULT_ERROR_PAGE="404" \
DEFAULT_HTTP_CODE="404" \
SHOW_DETAILS="false" \
DISABLE_L10N="false" \
READ_BUFFER_SIZE="2048"
#ENV LISTEN_PORT="8080" \
# TEMPLATE_NAME="ghost" \
# DEFAULT_ERROR_PAGE="404" \
# DEFAULT_HTTP_CODE="404" \
# SHOW_DETAILS="false" \
# DISABLE_L10N="false" \
# READ_BUFFER_SIZE="2048"
# docs: https://docs.docker.com/reference/dockerfile/#healthcheck
HEALTHCHECK --interval=10s --start-interval=1s --start-period=5s --timeout=2s CMD [\

View File

@ -39,64 +39,3 @@ gen: ## Generate code
hurl: ## Run integration tests using hurl
docker compose run $(DC_RUN_ARGS) hurl --color --test --fail-at-end --variable host=web --variable port=8080 ./test/hurl/*.hurl
#SHELL = /bin/sh
#LDFLAGS = "-s -w -X gh.tarampamp.am/error-pages/internal/version.version=$(shell git rev-parse HEAD)"
#
#DC_RUN_ARGS = --rm --user "$(shell id -u):$(shell id -g)"
#APP_NAME = $(notdir $(CURDIR))
#
#.PHONY : help \
# image dive build fmt lint gotest int-test test shell \
# up down restart \
# clean
#.DEFAULT_GOAL : help
#.SILENT : lint gotest
#
## This will output the help for each task. thanks to https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
#help: ## Show this help
# @printf "\033[33m%s:\033[0m\n" 'Available commands'
# @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " \033[32m%-11s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
#
#image: ## Build docker image with app
# docker build -f ./Dockerfile -t $(APP_NAME):local .
# docker run --rm $(APP_NAME):local version
# @printf "\n \e[30;42m %s \033[0m\n\n" 'Now you can use image like `docker run --rm -p "8080:8080/tcp" $(APP_NAME):local ...`';
#
#dive: image ## Explore the docker image
# docker run --rm -it -v "/var/run/docker.sock:/var/run/docker.sock:ro" wagoodman/dive:latest $(APP_NAME):local
#
#build: ## Build app binary file
# docker-compose run $(DC_RUN_ARGS) -e "CGO_ENABLED=0" --no-deps app go build -trimpath -ldflags $(LDFLAGS) -o ./error-pages ./cmd/error-pages/
#
#fmt: ## Run source code formatter tools
# docker-compose run $(DC_RUN_ARGS) -e "GO111MODULE=off" --no-deps app sh -c 'go get golang.org/x/tools/cmd/goimports && $$GOPATH/bin/goimports -d -w .'
# docker-compose run $(DC_RUN_ARGS) --no-deps app gofmt -s -w -d .
# docker-compose run $(DC_RUN_ARGS) --no-deps app go mod tidy
#
#lint: ## Run app linters
# docker-compose run --rm --no-deps golint golangci-lint run
#
#gotest: ## Run app tests
# docker-compose run $(DC_RUN_ARGS) --no-deps app go test -v -race -timeout 10s ./...
#
#int-test: ## Run integration tests (docs: https://hurl.dev/docs/man-page.html#options)
# docker-compose run --rm hurl --color --test --fail-at-end --variable host=web --variable port=8080 ./test/hurl/*.hurl
#
#test: lint gotest int-test ## Run app tests and linters
#
#shell: ## Start shell into container with golang
# docker-compose run $(DC_RUN_ARGS) app bash
#
#up: ## Create and start containers
# docker-compose up --detach web
# @printf "\n \e[30;42m %s \033[0m\n\n" 'Navigate your browser to ⇒ http://127.0.0.1:8080';
#
#down: ## Stop all services
# docker-compose down -t 5
#
#restart: down up ## Restart all containers
#
#clean: ## Make clean
# docker-compose down -v -t 1
# -docker rmi $(APP_NAME):local -f

View File

@ -1,3 +1,57 @@
<!--GENERATED:CLI_DOCS-->
<!-- Documentation inside this block generated by github.com/urfave/cli; DO NOT EDIT -->
## CLI interface
Usage:
```bash
$ error-pages [GLOBAL FLAGS] [COMMAND] [COMMAND FLAGS] [ARGUMENTS...]
```
Global flags:
| Name | Description | Default value | Environment variables |
|--------------------|---------------------------------------------|:-------------:|:---------------------:|
| `--log-level="…"` | logging level (debug/info/warn/error/fatal) | `info` | `LOG_LEVEL` |
| `--log-format="…"` | logging format (console/json) | `console` | `LOG_FORMAT` |
<!--/GENERATED:CLI_DOCS-->
<!--
<p align="center">
<a href="https://github.com/tarampampam/error-pages#readme"><img src="https://socialify.git.ci/tarampampam/error-pages/image?description=1&font=Raleway&forks=1&issues=1&logo=https%3A%2F%2Fhsto.org%2Fwebt%2Frm%2F9y%2Fww%2Frm9ywwx3gjv9agwkcmllhsuyo7k.png&owner=1&pulls=1&pattern=Solid&stargazers=1&theme=Dark" alt="banner" width="100%" /></a>
</p>
@ -203,3 +257,5 @@ This is open-sourced software licensed under the [MIT License][license].
[preview-demo]:https://tarampampam.github.io/error-pages/
[traefik]:https://github.com/traefik/traefik
[ingress-nginx]:https://github.com/kubernetes/ingress-nginx/tree/main/charts/ingress-nginx
-->

View File

@ -1,8 +1,11 @@
package main
import (
"context"
"os"
"os/signal"
"path/filepath"
"syscall"
"github.com/fatih/color"
"go.uber.org/automaxprocs/maxprocs"
@ -10,23 +13,23 @@ import (
"gh.tarampamp.am/error-pages/internal/cli"
)
// set GOMAXPROCS to match Linux container CPU quota.
var _, _ = maxprocs.Set(maxprocs.Min(1), maxprocs.Logger(func(_ string, _ ...any) {}))
// exitFn is a function for application exiting.
var exitFn = os.Exit //nolint:gochecknoglobals
// main CLI application entrypoint.
func main() { exitFn(run()) }
func main() {
// automatically set GOMAXPROCS to match Linux container CPU quota
_, _ = maxprocs.Set(maxprocs.Min(1), maxprocs.Logger(func(_ string, _ ...any) {}))
// run this CLI application.
// Exit codes documentation: <https://tldp.org/LDP/abs/html/exitcodes.html>
func run() int {
if err := (cli.NewApp(filepath.Base(os.Args[0]))).Run(os.Args); err != nil {
if err := run(); err != nil {
_, _ = color.New(color.FgHiRed, color.Bold).Fprintln(os.Stderr, err.Error())
return 1
os.Exit(1)
}
return 0
}
// run this CLI application.
func run() error {
// create a context that is canceled when the user interrupts the program
var ctx, cancel = signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
return (cli.NewApp(filepath.Base(os.Args[0]))).Run(ctx, os.Args)
}

View File

@ -1,21 +0,0 @@
package main
import (
"os"
"testing"
"github.com/kami-zh/go-capturer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_MainHelp(t *testing.T) {
os.Args = []string{"", "--help"}
exitFn = func(code int) { require.Equal(t, 0, code) }
output := capturer.CaptureStdout(main)
assert.Contains(t, output, "USAGE:")
assert.Contains(t, output, "COMMANDS:")
assert.Contains(t, output, "GLOBAL OPTIONS:")
}

4
go.mod
View File

@ -11,7 +11,9 @@ require (
github.com/prometheus/client_golang v1.19.1
github.com/prometheus/client_model v0.6.1
github.com/stretchr/testify v1.9.0
github.com/urfave/cli-docs/v3 v3.0.0-alpha5
github.com/urfave/cli/v2 v2.27.2
github.com/urfave/cli/v3 v3.0.0-alpha9
github.com/valyala/fasthttp v1.54.0
go.uber.org/automaxprocs v1.5.3
go.uber.org/goleak v1.3.0
@ -34,7 +36,7 @@ require (
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sys v0.18.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect

8
go.sum
View File

@ -51,14 +51,18 @@ github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 h1:KanIMPX0QdEdB4R3
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
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/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/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
github.com/urfave/cli/v3 v3.0.0-alpha9 h1:P0RMy5fQm1AslQS+XCmy9UknDXctOmG/q/FZkUFnJSo=
github.com/urfave/cli/v3 v3.0.0-alpha9/go.mod h1:0kK/RUFHyh+yIKSfWxwheGndfnrvYSmYFVeKCh03ZUc=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.54.0 h1:cCL+ZZR3z3HPLMVfEYVUMtJqVaui0+gu7Lx63unHwS0=
github.com/valyala/fasthttp v1.54.0/go.mod h1:6dt4/8olwq9QARP/TDuPmWyWcl4byhpvTJ4AAtcz+QM=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=

2
internal/appmeta/doc.go Normal file
View File

@ -0,0 +1,2 @@
// Package appmeta provides the application metadata, such as version.
package appmeta

View File

@ -1,5 +1,4 @@
// Package version is used as a place, where application version defined.
package version
package appmeta
import "strings"
@ -8,7 +7,7 @@ var version = "v0.0.0@undefined"
// Version returns version value (without `v` prefix).
func Version() string {
v := strings.TrimSpace(version)
var v = strings.TrimSpace(version)
if len(v) > 1 && ((v[0] == 'v' || v[0] == 'V') && (v[1] >= '0' && v[1] <= '9')) {
return v[1:]

View File

@ -1,8 +1,6 @@
package version
package appmeta
import (
"testing"
)
import "testing"
func TestVersion(t *testing.T) {
for give, want := range map[string]string{

View File

@ -1,54 +0,0 @@
// Package breaker provides OSSignals struct for OS signals handling (with context).
package breaker
import (
"context"
"os"
"os/signal"
"syscall"
)
// OSSignals allows subscribing for system signals.
type OSSignals struct {
ctx context.Context
ch chan os.Signal
}
// NewOSSignals creates new subscriber for system signals.
func NewOSSignals(ctx context.Context) OSSignals {
return OSSignals{
ctx: ctx,
ch: make(chan os.Signal, 1),
}
}
// Subscribe for some system signals (call Stop for stopping).
func (oss *OSSignals) Subscribe(onSignal func(os.Signal), signals ...os.Signal) {
if len(signals) == 0 {
signals = []os.Signal{os.Interrupt, syscall.SIGINT, syscall.SIGTERM} // default signals
}
signal.Notify(oss.ch, signals...)
go func(ch <-chan os.Signal) {
select {
case <-oss.ctx.Done():
break
case sig, opened := <-ch:
if oss.ctx.Err() != nil {
break
}
if opened && sig != nil {
onSignal(sig)
}
}
}(oss.ch)
}
// Stop system signals listening.
func (oss *OSSignals) Stop() {
signal.Stop(oss.ch)
close(oss.ch)
}

View File

@ -1,57 +0,0 @@
package breaker_test
import (
"context"
"os"
"syscall"
"testing"
"time"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/breaker"
)
func TestNewOSSignals(t *testing.T) {
oss := breaker.NewOSSignals(context.Background())
gotSignal := make(chan os.Signal, 1)
oss.Subscribe(func(signal os.Signal) {
gotSignal <- signal
}, syscall.SIGUSR2)
defer oss.Stop()
proc, err := os.FindProcess(os.Getpid())
assert.NoError(t, err)
assert.NoError(t, proc.Signal(syscall.SIGUSR2)) // send the signal
time.Sleep(time.Millisecond * 5)
assert.Equal(t, syscall.SIGUSR2, <-gotSignal)
}
func TestNewOSSignalCtxCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
oss := breaker.NewOSSignals(ctx)
gotSignal := make(chan os.Signal, 1)
oss.Subscribe(func(signal os.Signal) {
gotSignal <- signal
}, syscall.SIGUSR2)
defer oss.Stop()
proc, err := os.FindProcess(os.Getpid())
assert.NoError(t, err)
cancel()
assert.NoError(t, proc.Signal(syscall.SIGUSR2)) // send the signal
assert.Empty(t, gotSignal)
}

View File

@ -1,56 +0,0 @@
package checkers
import (
"context"
"fmt"
"net/http"
"time"
)
type httpClient interface {
Do(*http.Request) (*http.Response, error)
}
// HealthChecker is a heals checker.
type HealthChecker struct {
ctx context.Context
httpClient httpClient
}
const defaultHTTPClientTimeout = time.Second * 3
// NewHealthChecker creates heals checker.
func NewHealthChecker(ctx context.Context, client ...httpClient) *HealthChecker {
var c httpClient
if len(client) == 1 {
c = client[0]
} else {
c = &http.Client{Timeout: defaultHTTPClientTimeout} // default
}
return &HealthChecker{ctx: ctx, httpClient: c}
}
// Check application using liveness probe.
func (c *HealthChecker) Check(port uint16) error {
req, err := http.NewRequestWithContext(c.ctx, http.MethodGet, fmt.Sprintf("http://127.0.0.1:%d/healthz", port), nil) //nolint:lll
if err != nil {
return err
}
req.Header.Set("User-Agent", "HealthChecker/internal")
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
_ = resp.Body.Close()
if code := resp.StatusCode; code != http.StatusOK {
return fmt.Errorf("wrong status code [%d] from live endpoint", code)
}
return nil
}

View File

@ -1,49 +0,0 @@
package checkers_test
import (
"bytes"
"context"
"io"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/checkers"
)
type httpClientFunc func(*http.Request) (*http.Response, error)
func (f httpClientFunc) Do(req *http.Request) (*http.Response, error) { return f(req) }
func TestHealthChecker_CheckSuccess(t *testing.T) {
var httpMock httpClientFunc = func(req *http.Request) (*http.Response, error) {
assert.Equal(t, http.MethodGet, req.Method)
assert.Equal(t, "http://127.0.0.1:123/healthz", req.URL.String())
assert.Equal(t, "HealthChecker/internal", req.Header.Get("User-Agent"))
return &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte{})),
StatusCode: http.StatusOK,
}, nil
}
checker := checkers.NewHealthChecker(context.Background(), httpMock)
assert.NoError(t, checker.Check(123))
}
func TestHealthChecker_CheckFail(t *testing.T) {
var httpMock httpClientFunc = func(req *http.Request) (*http.Response, error) {
return &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte{})),
StatusCode: http.StatusBadGateway,
}, nil
}
checker := checkers.NewHealthChecker(context.Background(), httpMock)
err := checker.Check(123)
assert.Error(t, err)
assert.Contains(t, err.Error(), "wrong status code")
}

View File

@ -1,10 +0,0 @@
package checkers
// LiveChecker is a liveness checker.
type LiveChecker struct{}
// NewLiveChecker creates liveness checker.
func NewLiveChecker() *LiveChecker { return &LiveChecker{} }
// Check application is alive?
func (*LiveChecker) Check() error { return nil }

View File

@ -1,13 +0,0 @@
package checkers_test
import (
"testing"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/checkers"
)
func TestLiveChecker_Check(t *testing.T) {
assert.NoError(t, checkers.NewLiveChecker().Check())
}

View File

@ -6,98 +6,79 @@ import (
"runtime"
"strings"
"github.com/urfave/cli/v2"
_ "github.com/urfave/cli-docs/v3" // required for `go generate` to work
"github.com/urfave/cli/v3"
"gh.tarampamp.am/error-pages/internal/checkers"
"gh.tarampamp.am/error-pages/internal/cli/build"
"gh.tarampamp.am/error-pages/internal/cli/healthcheck"
"gh.tarampamp.am/error-pages/internal/cli/serve"
"gh.tarampamp.am/error-pages/internal/env"
"gh.tarampamp.am/error-pages/internal-old/env"
"gh.tarampamp.am/error-pages/internal/appmeta"
"gh.tarampamp.am/error-pages/internal/logger"
"gh.tarampamp.am/error-pages/internal/version"
)
// NewApp creates new console application.
func NewApp(appName string) *cli.App { //nolint:funlen
const (
logLevelFlagName = "log-level"
logFormatFlagName = "log-format"
verboseFlagName = "verbose"
debugFlagName = "debug"
logJSONFlagName = "log-json"
//go:generate go run update_readme.go
defaultLogLevel = logger.InfoLevel
defaultLogFormat = logger.ConsoleFormat
// NewApp creates a new console application.
func NewApp(appName string) *cli.Command { //nolint:funlen
var (
logLevelFlag = cli.StringFlag{
Name: "log-level",
Value: logger.InfoLevel.String(),
Usage: "logging level (" + strings.Join(logger.LevelStrings(), "/") + ")",
Sources: cli.EnvVars(env.LogLevel.String()),
OnlyOnce: true,
Config: cli.StringConfig{TrimSpace: true},
Validator: func(s string) error {
if _, err := logger.ParseLevel(s); err != nil {
return err
}
return nil
},
}
logFormatFlag = cli.StringFlag{
Name: "log-format",
Value: logger.ConsoleFormat.String(),
Usage: "logging format (" + strings.Join(logger.FormatStrings(), "/") + ")",
Sources: cli.EnvVars(env.LogFormat.String()),
OnlyOnce: true,
Config: cli.StringConfig{TrimSpace: true},
Validator: func(s string) error {
if _, err := logger.ParseFormat(s); err != nil {
return err
}
return nil
},
}
)
// create "default" logger (will be overwritten later with customized)
var log, _ = logger.New(defaultLogLevel, defaultLogFormat) // error will never occurs
// create a "default" logger (will be swapped later with customized)
var log, _ = logger.New(logger.InfoLevel, logger.ConsoleFormat) // error will never occur
return &cli.App{
Usage: appName,
Before: func(c *cli.Context) (err error) {
return &cli.Command{
Usage: appName,
Suggest: true,
Before: func(ctx context.Context, c *cli.Command) error {
_ = log.Sync() // sync previous logger instance
var logLevel, logFormat = defaultLogLevel, defaultLogFormat //nolint:ineffassign
var (
logLevel, _ = logger.ParseLevel(c.String(logLevelFlag.Name)) // error ignored because the flag validates itself
logFormat, _ = logger.ParseFormat(c.String(logFormatFlag.Name)) // --//--
)
if c.Bool(verboseFlagName) || c.Bool(debugFlagName) {
logLevel = logger.DebugLevel
} else {
// parse logging level
if logLevel, err = logger.ParseLevel(c.String(logLevelFlagName)); err != nil {
return err
}
}
if c.Bool(logJSONFlagName) {
logFormat = logger.JSONFormat
} else {
// parse logging format
if logFormat, err = logger.ParseFormat(c.String(logFormatFlagName)); err != nil {
return err
}
}
configured, err := logger.New(logLevel, logFormat) // create new logger instance
configured, err := logger.New(logLevel, logFormat) // create a new logger instance
if err != nil {
return err
}
*log = *configured // replace "default" logger with customized
*log = *configured // swap the "default" logger with customized
return nil
},
Commands: []*cli.Command{
healthcheck.NewCommand(checkers.NewHealthChecker(context.TODO())),
build.NewCommand(log),
serve.NewCommand(log),
},
Version: fmt.Sprintf("%s (%s)", version.Version(), runtime.Version()),
Version: fmt.Sprintf("%s (%s)", appmeta.Version(), runtime.Version()),
Flags: []cli.Flag{ // global flags
&cli.BoolFlag{ // kept for backward compatibility
Name: verboseFlagName,
Usage: "verbose output (DEPRECATED FLAG)",
},
&cli.BoolFlag{ // kept for backward compatibility
Name: debugFlagName,
Usage: "debug output (DEPRECATED FLAG)",
},
&cli.BoolFlag{ // kept for backward compatibility
Name: logJSONFlagName,
Usage: "logs in JSON format (DEPRECATED FLAG)",
},
&cli.StringFlag{
Name: logLevelFlagName,
Value: defaultLogLevel.String(),
Usage: "logging level (`" + strings.Join(logger.LevelStrings(), "/") + "`)",
EnvVars: []string{env.LogLevel.String()},
},
&cli.StringFlag{
Name: logFormatFlagName,
Value: defaultLogFormat.String(),
Usage: "logging format (`" + strings.Join(logger.FormatStrings(), "/") + "`)",
EnvVars: []string{env.LogFormat.String()},
},
&logLevelFlag,
&logFormatFlag,
},
}
}

View File

@ -1,19 +1,25 @@
package cli_test
import (
"context"
"testing"
"github.com/kami-zh/go-capturer"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/cli"
)
func TestNewCommand(t *testing.T) {
func TestNewApp(t *testing.T) {
t.Parallel()
app := cli.NewApp("app")
app := cli.NewApp("appName")
assert.NotEmpty(t, app.Flags)
assert.NoError(t, app.Run([]string{"", "--log-level", "debug", "--log-format", "json"}))
output := capturer.CaptureStdout(func() {
assert.NoError(t, app.Run(context.Background(), []string{""}))
})
assert.NotEmpty(t, output)
}

View File

@ -1,155 +0,0 @@
package build
import (
"os"
"path"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"go.uber.org/zap"
"gh.tarampamp.am/error-pages/internal/cli/shared"
"gh.tarampamp.am/error-pages/internal/config"
"gh.tarampamp.am/error-pages/internal/tpl"
)
type command struct {
c *cli.Command
}
// NewCommand creates `build` command.
func NewCommand(log *zap.Logger) *cli.Command {
var cmd = command{}
const (
generateIndexFlagName = "index"
disableL10nFlagName = "disable-l10n"
)
cmd.c = &cli.Command{
Name: "build",
Aliases: []string{"b"},
Usage: "build <output-directory>",
Description: "Build the error pages",
Action: func(c *cli.Context) error {
cfg, cfgErr := config.FromYamlFile(c.String(shared.ConfigFileFlag.Name))
if cfgErr != nil {
return cfgErr
}
if c.Args().Len() != 1 {
return errors.New("wrong arguments count")
}
return cmd.Run(log, cfg, c.Args().First(), c.Bool(generateIndexFlagName), c.Bool(disableL10nFlagName))
},
Flags: []cli.Flag{ // global flags
&cli.BoolFlag{
Name: generateIndexFlagName,
Aliases: []string{"i"},
Usage: "generate index page",
},
&cli.BoolFlag{
Name: disableL10nFlagName,
Usage: "disable error pages localization",
},
shared.ConfigFileFlag,
},
}
return cmd.c
}
const (
outHTMLFileExt = ".html"
outIndexFileName = "index"
outFilePerm = os.FileMode(0664)
outDirPerm = os.FileMode(0775)
)
func (cmd *command) Run(log *zap.Logger, cfg *config.Config, outDirectoryPath string, generateIndex, disableL10n bool) error { //nolint:funlen,lll
if len(cfg.Templates) == 0 {
return errors.New("no loaded templates")
}
log.Info("output directory preparing", zap.String("path", outDirectoryPath))
if err := cmd.createDirectory(outDirectoryPath, outDirPerm); err != nil {
return errors.Wrap(err, "cannot prepare output directory")
}
history, renderer := newBuildingHistory(), tpl.NewTemplateRenderer()
defer func() { _ = renderer.Close() }()
for _, template := range cfg.Templates {
log.Debug("template processing", zap.String("name", template.Name()))
for _, page := range cfg.Pages {
if err := cmd.createDirectory(path.Join(outDirectoryPath, template.Name()), outDirPerm); err != nil {
return err
}
var (
fileName = page.Code() + outHTMLFileExt
filePath = path.Join(outDirectoryPath, template.Name(), fileName)
)
content, renderingErr := renderer.Render(template.Content(), tpl.Properties{
Code: page.Code(),
Message: page.Message(),
Description: page.Description(),
ShowRequestDetails: false,
L10nDisabled: disableL10n,
})
if renderingErr != nil {
return renderingErr
}
if err := os.WriteFile(filePath, content, outFilePerm); err != nil {
return err
}
log.Debug("page rendered", zap.String("path", filePath))
if generateIndex {
history.Append(
template.Name(),
page.Code(),
page.Message(),
path.Join(template.Name(), fileName),
)
}
}
}
if generateIndex {
var filepath = path.Join(outDirectoryPath, outIndexFileName+outHTMLFileExt)
log.Info("index file generation", zap.String("path", filepath))
if err := history.WriteIndexFile(filepath, outFilePerm); err != nil {
return err
}
}
log.Info("job is done")
return nil
}
func (cmd *command) createDirectory(path string, perm os.FileMode) error {
stat, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return os.MkdirAll(path, perm)
}
return err
}
if !stat.IsDir() {
return errors.New("is not a directory")
}
return nil
}

View File

@ -1,26 +0,0 @@
package build_test
import (
"flag"
"testing"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2"
"go.uber.org/goleak"
"go.uber.org/zap"
"gh.tarampamp.am/error-pages/internal/cli/build"
)
func TestNewCommand(t *testing.T) {
defer goleak.VerifyNone(t)
cmd := build.NewCommand(zap.NewNop())
assert.NotEmpty(t, cmd.Flags)
assert.Error(t, cmd.Run(
cli.NewContext(cli.NewApp(), &flag.FlagSet{}, nil),
"",
), "should fail because of missing external services")
}

View File

@ -1,59 +0,0 @@
package build
import (
"bytes"
_ "embed"
"os"
"sort"
"text/template"
)
type (
buildingHistory struct {
items map[string][]historyItem
}
historyItem struct {
Code, Message, Path string
}
)
func newBuildingHistory() buildingHistory {
return buildingHistory{items: make(map[string][]historyItem)}
}
func (bh *buildingHistory) Append(templateName, pageCode, message, path string) {
if _, ok := bh.items[templateName]; !ok {
bh.items[templateName] = make([]historyItem, 0)
}
bh.items[templateName] = append(bh.items[templateName], historyItem{
Code: pageCode,
Message: message,
Path: path,
})
sort.Slice(bh.items[templateName], func(i, j int) bool { // keep history items sorted
return bh.items[templateName][i].Code < bh.items[templateName][j].Code
})
}
//go:embed index.tpl.html
var indexPageTemplate string
func (bh *buildingHistory) WriteIndexFile(path string, perm os.FileMode) error {
t, err := template.New("index").Parse(indexPageTemplate)
if err != nil {
return err
}
var buf bytes.Buffer
if err = t.Execute(&buf, bh.items); err != nil {
return err
}
defer buf.Reset() // optimization (is needed here?)
return os.WriteFile(path, buf.Bytes(), perm)
}

View File

@ -1,41 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Error pages list</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.1.3/css/bootstrap.min.css"
integrity="sha512-GQGU0fMMi238uA+a/bdWJfpUGKUkBdgfFdgBm72SUQ6BeyWjoY/ton0tEjH+OSH9iP4Dfh+7HM0I9f5eR0L/4w=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
@media (prefers-color-scheme:dark){
:root {--bs-light:#212529;--bs-light-rgb:33,37,41;--bs-body-color:#eee}a{color:#91b4e8}a:hover{color:#a2bfec}
}
</style>
</head>
<body class="bg-light">
<div class="container">
<main>
<div class="py-5 text-center">
<img class="d-block mx-auto mb-4" src="https://hsto.org/webt/rm/9y/ww/rm9ywwx3gjv9agwkcmllhsuyo7k.png"
alt="" width="94">
<h2>Error pages index</h2>
</div>
{{- range $template, $item := . -}}
<h2 class="mb-3">Template name: <Code>{{ $template }}</Code></h2>
<ul class="mb-5">
{{ range $item -}}
<li><a href="{{ .Path }}"><strong>{{ .Code }}</strong>: {{ .Message }}</a></li>
{{ end -}}
</ul>
{{ end }}
</main>
</div>
<footer class="footer">
<div class="container text-center text-muted mt-3 mb-3">
For online documentation and support please refer to the
<a href="https://gh.tarampamp.am/error-pages">project repository</a>.
</div>
</footer>
</body>
</html>

View File

@ -0,0 +1,89 @@
package healthcheck
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"strings"
"time"
"gh.tarampamp.am/error-pages/internal/appmeta"
)
type (
httpClient interface {
Do(*http.Request) (*http.Response, error)
}
// HealthCheckerOption allows you to change some settings of the checker.
HealthCheckerOption func(*HTTPHealthChecker)
)
// WithHttpClient allows to set http client.
func WithHttpClient(c httpClient) HealthCheckerOption {
return func(hc *HTTPHealthChecker) { hc.httpClient = c }
}
// WithLiveEndpoint set the endpoint to check.
func WithLiveEndpoint(endpoint string) HealthCheckerOption {
if len(endpoint) > 0 && endpoint[0] != '/' {
endpoint = "/" + endpoint
}
return func(hc *HTTPHealthChecker) { hc.liveEndpoint = endpoint }
}
// HTTPHealthChecker is HTTP probe checker.
type HTTPHealthChecker struct {
httpClient httpClient
liveEndpoint string
}
var _ checker = (*HTTPHealthChecker)(nil) // ensure that HTTPHealthChecker implements checker interface
func NewHTTPHealthChecker(opts ...HealthCheckerOption) *HTTPHealthChecker {
const (
httpClientTimeout = 3 * time.Second
liveRoute = "/healthz"
)
var c = HTTPHealthChecker{
httpClient: &http.Client{
Timeout: httpClientTimeout,
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}, //nolint:gosec
},
liveEndpoint: liveRoute,
}
for _, opt := range opts {
opt(&c)
}
return &c
}
// Check performs HTTP get request.
func (c *HTTPHealthChecker) Check(ctx context.Context, baseURL string) error {
var endpoint = strings.TrimRight(strings.TrimSpace(baseURL), "/") + c.liveEndpoint
var req, err = http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody)
if err != nil {
return err
}
req.Header.Set("User-Agent", fmt.Sprintf("ErrorPages/%s (HealthCheck)", appmeta.Version()))
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
_ = resp.Body.Close()
if code := resp.StatusCode; code != http.StatusOK && code != http.StatusNoContent {
return fmt.Errorf("wrong status code [%d] from the live endpoint (%s)", code, endpoint)
}
return nil
}

View File

@ -0,0 +1,130 @@
package healthcheck_test
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gh.tarampamp.am/error-pages/internal/appmeta"
"gh.tarampamp.am/error-pages/internal/cli/healthcheck"
)
type httpClientFunc func(*http.Request) (*http.Response, error)
func (f httpClientFunc) Do(req *http.Request) (*http.Response, error) { return f(req) }
func TestHealthChecker_CheckSuccess(t *testing.T) {
t.Parallel()
var httpMock httpClientFunc = func(req *http.Request) (*http.Response, error) {
assert.Equal(t, http.MethodGet, req.Method)
assert.Equal(t, "foobar:123/healthz", req.URL.String())
assert.Equal(t, fmt.Sprintf("ErrorPages/%s (HealthCheck)", appmeta.Version()), req.Header.Get("User-Agent"))
return &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte("ok"))),
StatusCode: http.StatusOK,
}, nil
}
assert.NoError(t, healthcheck.NewHTTPHealthChecker(
healthcheck.WithHttpClient(httpMock),
).Check(context.Background(), "foobar:123"))
}
func TestHealthChecker_CheckFail(t *testing.T) {
t.Parallel()
var httpMock httpClientFunc = func(req *http.Request) (*http.Response, error) {
assert.Equal(t, "foobar:123/foo", req.URL.String())
return &http.Response{
Body: http.NoBody,
StatusCode: http.StatusBadGateway,
}, nil
}
var err = healthcheck.NewHTTPHealthChecker(
healthcheck.WithHttpClient(httpMock),
healthcheck.WithLiveEndpoint("foo"),
).Check(context.Background(), "foobar:123")
assert.Error(t, err)
assert.Contains(t, err.Error(), "wrong status code [502]")
}
func TestHealthChecker_ClientDoError(t *testing.T) {
t.Parallel()
var httpMock httpClientFunc = func(req *http.Request) (*http.Response, error) {
return nil, assert.AnError
}
var err = healthcheck.NewHTTPHealthChecker(
healthcheck.WithHttpClient(httpMock),
healthcheck.WithLiveEndpoint("foo"),
).Check(context.Background(), "foobar:123")
assert.ErrorIs(t, err, assert.AnError)
}
func TestHTTPHealthChecker_CheckNormalize(t *testing.T) {
t.Parallel()
for name, _tc := range map[string]struct {
giveBaseURL string
giveLive string
wantURL string
}{
"no-live": {
giveBaseURL: "foobar:123",
wantURL: "foobar:123",
},
"live with slash": {
giveBaseURL: "foobar:123",
giveLive: "/foo",
wantURL: "foobar:123/foo",
},
"live without slash": {
giveBaseURL: "foobar:123",
giveLive: "foo",
wantURL: "foobar:123/foo",
},
"base with slash": {
giveBaseURL: "foobar:123/",
giveLive: "foo",
wantURL: "foobar:123/foo",
},
"all of slashes": {
giveBaseURL: "foobar:123/",
giveLive: "/foo",
wantURL: "foobar:123/foo",
},
} {
tc := _tc
t.Run(name, func(t *testing.T) {
t.Parallel()
var httpMock httpClientFunc = func(req *http.Request) (*http.Response, error) {
assert.Equal(t, tc.wantURL, req.URL.String())
return &http.Response{
Body: http.NoBody,
StatusCode: http.StatusOK,
}, nil
}
require.NoError(t, healthcheck.NewHTTPHealthChecker(
healthcheck.WithHttpClient(httpMock),
healthcheck.WithLiveEndpoint(tc.giveLive),
).Check(context.Background(), tc.giveBaseURL))
})
}
}

View File

@ -1,36 +1,32 @@
// Package healthcheck contains CLI `healthcheck` command implementation.
package healthcheck
import (
"errors"
"math"
"context"
"fmt"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
"go.uber.org/zap"
"gh.tarampamp.am/error-pages/internal/cli/shared"
)
type checker interface {
Check(port uint16) error
Check(ctx context.Context, baseURL string) error
}
// NewCommand creates `healthcheck` command.
func NewCommand(checker checker) *cli.Command {
func NewCommand(_ *zap.Logger, checker checker) *cli.Command {
var portFlag = shared.ListenPortFlag
return &cli.Command{
Name: "healthcheck",
Aliases: []string{"chk", "health", "check"},
Usage: "Health checker for the HTTP server. Use case - docker healthcheck",
Action: func(c *cli.Context) error {
var port = c.Uint(shared.ListenPortFlag.Name)
if port <= 0 || port > math.MaxUint16 {
return errors.New("port value out of range")
}
return checker.Check(uint16(port))
Usage: "Health checker for the HTTP server. The use case - docker health check",
Action: func(ctx context.Context, c *cli.Command) error {
return checker.Check(ctx, fmt.Sprintf("http://127.0.0.1:%d", c.Uint(portFlag.Name)))
},
Flags: []cli.Flag{
shared.ListenPortFlag,
&portFlag,
},
}
}

View File

@ -1,47 +1,59 @@
package healthcheck_test
import (
"errors"
"flag"
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"gh.tarampamp.am/error-pages/internal/cli/healthcheck"
)
type fakeChecker struct{ err error }
func TestNewCommand(t *testing.T) {
t.Parallel()
func (c *fakeChecker) Check(port uint16) error { return c.err }
func TestProperties(t *testing.T) {
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
var cmd = healthcheck.NewCommand(zap.NewNop(), nil)
assert.Equal(t, "healthcheck", cmd.Name)
assert.ElementsMatch(t, []string{"chk", "health", "check"}, cmd.Aliases)
assert.NotNil(t, cmd.Action)
assert.Equal(t, []string{"chk", "health", "check"}, cmd.Aliases)
}
func TestCommandRun(t *testing.T) {
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
assert.NoError(t, cmd.Run(cli.NewContext(cli.NewApp(), &flag.FlagSet{}, nil)))
type fakeHealthChecker struct {
t *testing.T
wantAddress string
giveErr error
}
func TestCommandRunFailed(t *testing.T) {
cmd := healthcheck.NewCommand(&fakeChecker{err: errors.New("foo err")})
func (m *fakeHealthChecker) Check(_ context.Context, addr string) error {
assert.Equal(m.t, m.wantAddress, addr)
assert.ErrorContains(t, cmd.Run(cli.NewContext(cli.NewApp(), &flag.FlagSet{}, nil)), "foo err")
return m.giveErr
}
func TestPortFlagWrongArgument(t *testing.T) {
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
func TestCommand_RunSuccess(t *testing.T) {
t.Parallel()
err := cmd.Run(
cli.NewContext(cli.NewApp(), &flag.FlagSet{}, nil),
"", "-p", "65536",
var cmd = healthcheck.NewCommand(zap.NewNop(), &fakeHealthChecker{
t: t,
wantAddress: "http://127.0.0.1:1234",
})
require.NoError(t, cmd.Run(context.Background(), []string{"", "--port", "1234"}))
}
func TestCommand_RunFail(t *testing.T) {
t.Parallel()
cmd := healthcheck.NewCommand(zap.NewNop(), &fakeHealthChecker{
t: t,
wantAddress: "http://127.0.0.1:4321",
giveErr: assert.AnError,
})
assert.ErrorIs(t,
cmd.Run(context.Background(), []string{"", "--port", "4321"}),
assert.AnError,
)
assert.ErrorContains(t, err, "port value out of range")
}

View File

@ -1,297 +0,0 @@
package serve
import (
"context"
"errors"
"fmt"
"net"
"os"
"strings"
"time"
"github.com/urfave/cli/v2"
"go.uber.org/zap"
"gh.tarampamp.am/error-pages/internal/breaker"
"gh.tarampamp.am/error-pages/internal/cli/shared"
"gh.tarampamp.am/error-pages/internal/config"
"gh.tarampamp.am/error-pages/internal/env"
appHttp "gh.tarampamp.am/error-pages/internal/http"
"gh.tarampamp.am/error-pages/internal/options"
"gh.tarampamp.am/error-pages/internal/pick"
)
type command struct {
c *cli.Command
}
const (
templateNameFlagName = "template-name"
defaultErrorPageFlagName = "default-error-page"
defaultHTTPCodeFlagName = "default-http-code"
showDetailsFlagName = "show-details"
proxyHTTPHeadersFlagName = "proxy-headers"
disableL10nFlagName = "disable-l10n"
catchAllFlagName = "catch-all"
readBufferSizeFlagName = "read-buffer-size"
)
const (
useRandomTemplate = "random"
useRandomTemplateOnEachRequest = "i-said-random"
useRandomTemplateDaily = "random-daily"
useRandomTemplateHourly = "random-hourly"
)
// NewCommand creates `serve` command.
func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen
var cmd = command{}
cmd.c = &cli.Command{
Name: "serve",
Aliases: []string{"s", "server"},
Usage: "Start HTTP server",
Action: func(c *cli.Context) error {
var cfg *config.Config
if configPath := c.String(shared.ConfigFileFlag.Name); configPath == "" { // load config from file
return errors.New("path to the config file is required for this command")
} else if loadedCfg, err := config.FromYamlFile(c.String(shared.ConfigFileFlag.Name)); err != nil {
return err
} else {
cfg = loadedCfg
}
var (
ip = c.String(shared.ListenAddrFlag.Name)
port = uint16(c.Uint(shared.ListenPortFlag.Name))
o options.ErrorPage
)
if net.ParseIP(ip) == nil {
return fmt.Errorf("wrong IP address [%s] for listening", ip)
}
{ // fill options
o.Template.Name = c.String(templateNameFlagName)
o.L10n.Disabled = c.Bool(disableL10nFlagName)
o.Default.PageCode = c.String(defaultErrorPageFlagName)
o.Default.HTTPCode = uint16(c.Uint(defaultHTTPCodeFlagName))
o.ShowDetails = c.Bool(showDetailsFlagName)
o.CatchAll = c.Bool(catchAllFlagName)
if headers := c.String(proxyHTTPHeadersFlagName); headers != "" { //nolint:nestif
var m = make(map[string]struct{})
// make unique and ignore empty strings
for _, header := range strings.Split(headers, ",") {
if h := strings.TrimSpace(header); h != "" {
if strings.ContainsRune(h, ' ') {
return fmt.Errorf("whitespaces in the HTTP headers for proxying [%s] are not allowed", header)
}
if _, ok := m[h]; !ok {
m[h] = struct{}{}
}
}
}
// convert map into slice
o.ProxyHTTPHeaders = make([]string, 0, len(m))
for h := range m {
o.ProxyHTTPHeaders = append(o.ProxyHTTPHeaders, h)
}
}
}
if o.Default.HTTPCode > 599 { //nolint:gomnd
return fmt.Errorf("wrong default HTTP response code [%d]", o.Default.HTTPCode)
}
return cmd.Run(c.Context, log, cfg, ip, port, c.Uint(readBufferSizeFlagName), o)
},
Flags: []cli.Flag{
shared.ConfigFileFlag,
shared.ListenPortFlag,
shared.ListenAddrFlag,
&cli.StringFlag{
Name: templateNameFlagName,
Aliases: []string{"t"},
Usage: fmt.Sprintf(
"template name (set \"%s\" to use a randomized or \"%s\" to use a randomized template on "+
"each request or \"%s/%s\" daily/hourly randomized)",
useRandomTemplate,
useRandomTemplateOnEachRequest,
useRandomTemplateDaily,
useRandomTemplateHourly,
),
EnvVars: []string{env.TemplateName.String()},
},
&cli.StringFlag{
Name: defaultErrorPageFlagName,
Value: "404",
Usage: "default error page",
EnvVars: []string{env.DefaultErrorPage.String()},
},
&cli.UintFlag{
Name: defaultHTTPCodeFlagName,
Value: 404, //nolint:gomnd
Usage: "default HTTP response code",
EnvVars: []string{env.DefaultHTTPCode.String()},
},
&cli.BoolFlag{
Name: showDetailsFlagName,
Usage: "show request details in response",
EnvVars: []string{env.ShowDetails.String()},
},
&cli.StringFlag{
Name: proxyHTTPHeadersFlagName,
Usage: "proxy HTTP request headers list (comma-separated)",
EnvVars: []string{env.ProxyHTTPHeaders.String()},
},
&cli.BoolFlag{
Name: disableL10nFlagName,
Usage: "disable error pages localization",
EnvVars: []string{env.DisableL10n.String()},
},
&cli.BoolFlag{
Name: catchAllFlagName,
Usage: "catch all pages",
EnvVars: []string{env.CatchAll.String()},
},
&cli.UintFlag{
Name: readBufferSizeFlagName,
Usage: "read buffer size (0 = use default value)",
EnvVars: []string{env.ReadBufferSize.String()},
},
},
}
return cmd.c
}
// Run current command.
func (cmd *command) Run( //nolint:funlen
parentCtx context.Context,
log *zap.Logger,
cfg *config.Config,
ip string,
port uint16,
readBufferSize uint,
opt options.ErrorPage,
) error {
var (
ctx, cancel = context.WithCancel(parentCtx) // serve context creation
oss = breaker.NewOSSignals(ctx) // OS signals listener
)
// subscribe for system signals
oss.Subscribe(func(sig os.Signal) {
log.Warn("Stopping by OS signal..", zap.String("signal", sig.String()))
cancel()
})
defer func() {
cancel() // call the cancellation function after all
oss.Stop() // stop system signals listening
}()
var (
templateNames = cfg.TemplateNames()
picker interface{ Pick() string }
)
switch opt.Template.Name {
case useRandomTemplate:
log.Info("A random template will be used")
picker = pick.NewStringsSlice(templateNames, pick.RandomOnce)
case useRandomTemplateOnEachRequest:
log.Info("A random template on EACH request will be used")
picker = pick.NewStringsSlice(templateNames, pick.RandomEveryTime)
case useRandomTemplateDaily:
log.Info("A random template will be used and changed once a day")
picker = pick.NewStringsSliceWithInterval(templateNames, pick.RandomEveryTime, time.Hour*24) //nolint:gomnd
case useRandomTemplateHourly:
log.Info("A random template will be used and changed hourly")
picker = pick.NewStringsSliceWithInterval(templateNames, pick.RandomEveryTime, time.Hour)
case "":
log.Info("The first template (ordered by name) will be used")
picker = pick.NewStringsSlice(templateNames, pick.First)
default:
if t, found := cfg.Template(opt.Template.Name); found {
log.Info("We will use the requested template", zap.String("name", t.Name()))
picker = pick.NewStringsSlice([]string{t.Name()}, pick.First)
} else {
return errors.New("requested nonexistent template: " + opt.Template.Name)
}
}
// create HTTP server
server := appHttp.NewServer(log, readBufferSize)
// register server routes, middlewares, etc.
if err := server.Register(cfg, picker, opt); err != nil {
return err
}
startedAt, startingErrCh := time.Now(), make(chan error, 1) // channel for server starting error
// start HTTP server in separate goroutine
go func(errCh chan<- error) {
defer close(errCh)
var fields = []zap.Field{
zap.String("addr", ip),
zap.Uint16("port", port),
zap.String("default error page", opt.Default.PageCode),
zap.Uint16("default HTTP response code", opt.Default.HTTPCode),
zap.Strings("proxy headers", opt.ProxyHTTPHeaders),
zap.Bool("show request details", opt.ShowDetails),
zap.Bool("localization disabled", opt.L10n.Disabled),
zap.Bool("catch all enabled", opt.CatchAll),
}
if readBufferSize > 0 {
fields = append(fields, zap.Uint("read buffer size", readBufferSize))
}
log.Info("Server starting", fields...)
if err := server.Start(ip, port); err != nil {
errCh <- err
}
}(startingErrCh)
// and wait for...
select {
case err := <-startingErrCh: // ..server starting error
return err
case <-ctx.Done(): // ..or context cancellation
log.Info("Gracefully server stopping", zap.Duration("uptime", time.Since(startedAt)))
if p, ok := picker.(interface{ Close() error }); ok {
if err := p.Close(); err != nil {
return err
}
}
// stop the server using created context above
if err := server.Stop(); err != nil {
return err
}
}
return nil
}

View File

@ -1,7 +0,0 @@
package serve_test
import "testing"
func TestNothing(t *testing.T) {
t.Skip("tests for this package have not been implemented yet")
}

View File

@ -1,31 +1,47 @@
package shared
import (
"github.com/urfave/cli/v2"
"fmt"
"net"
"gh.tarampamp.am/error-pages/internal/env"
"github.com/urfave/cli/v3"
)
var ConfigFileFlag = &cli.StringFlag{ //nolint:gochecknoglobals
Name: "config-file",
Aliases: []string{"c"},
Usage: "path to the config file (yaml)",
Value: "./error-pages.yml",
EnvVars: []string{env.ConfigFilePath.String()},
var ListenAddrFlag = cli.StringFlag{
Name: "listen",
Aliases: []string{"l"},
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"),
OnlyOnce: true,
Required: true,
Config: cli.StringConfig{TrimSpace: true},
Validator: func(ip string) error {
if ip == "" {
return fmt.Errorf("missing IP address")
}
if net.ParseIP(ip) == nil {
return fmt.Errorf("wrong IP address [%s] for listening", ip)
}
return nil
},
}
var ListenAddrFlag = &cli.StringFlag{ //nolint:gochecknoglobals
Name: "listen",
Aliases: []string{"l"},
Usage: "IP (v4 or v6) address to Listen on",
Value: "0.0.0.0",
EnvVars: []string{env.ListenAddr.String()},
}
var ListenPortFlag = cli.UintFlag{
Name: "port",
Aliases: []string{"p"},
Usage: "TCP port number",
Value: 8080, // default port number
Sources: cli.EnvVars("LISTEN_PORT"),
OnlyOnce: true,
Required: true,
Validator: func(port uint64) error {
if port == 0 || port > 65535 {
return fmt.Errorf("wrong TCP port number [%d]", port)
}
var ListenPortFlag = &cli.UintFlag{ //nolint:gochecknoglobals
Name: "port",
Aliases: []string{"p"},
Usage: "TCP port number",
Value: 8080, //nolint:gomnd
EnvVars: []string{env.ListenPort.String()},
return nil
},
}

View File

@ -0,0 +1,85 @@
package shared_test
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/cli/shared"
)
func TestListenAddrFlag(t *testing.T) {
t.Parallel()
var flag = shared.ListenAddrFlag
assert.Equal(t, "listen", flag.Name)
assert.Equal(t, "0.0.0.0", flag.Value)
assert.Contains(t, flag.Sources.String(), "LISTEN_ADDR")
for giveValue, wantErrMsg := range map[string]string{
flag.Value: "", // default value
// ipv4
"0.0.0.0": "",
"127.0.0.1": "",
"255.255.255.255": "",
// ipv6
"::": "",
"::1": "",
"2001:0db8:85a3:0000:0000:8a2e:0370:7334": "",
"2001:db8:85a3:0:0:8a2e:370:7334": "",
"2001:db8:85a3::8a2e:370:7334": "",
"2001:db8::8a2e:370:7334": "",
"2001:db8::7334": "",
"2001:db8::": "",
"2001:db8:0:0:1::1": "",
"2001:db8:0:0:1::": "",
// invalid
"": "missing IP address",
"255.255.255.256": "wrong IP address [255.255.255.256] for listening",
"example.com": "wrong IP address [example.com] for listening",
"123.123.abc.123": "wrong IP address [123.123.abc.123] for listening",
"foo:123:321": "wrong IP address [foo:123:321] for listening",
"2001:db8:0:0:1:": "wrong IP address [2001:db8:0:0:1:] for listening",
} {
t.Run(fmt.Sprintf("%s: %s", giveValue, wantErrMsg), func(t *testing.T) {
if err := flag.Validator(giveValue); wantErrMsg != "" {
assert.ErrorContains(t, err, wantErrMsg)
} else {
assert.NoError(t, err)
}
})
}
}
func TestListenPortFlag(t *testing.T) {
t.Parallel()
var flag = shared.ListenPortFlag
assert.Equal(t, "port", flag.Name)
assert.Equal(t, uint64(8080), flag.Value)
assert.Contains(t, flag.Sources.String(), "LISTEN_PORT")
for giveValue, wantErrMsg := range map[uint64]string{
flag.Value: "", // default value
1: "",
8080: "",
65535: "",
0: "wrong TCP port number [0]",
65536: "wrong TCP port number [65536]",
} {
t.Run(fmt.Sprintf("%d: %s", giveValue, wantErrMsg), func(t *testing.T) {
if err := flag.Validator(giveValue); wantErrMsg != "" {
assert.ErrorContains(t, err, wantErrMsg)
} else {
assert.NoError(t, err)
}
})
}
}

View File

@ -0,0 +1,24 @@
//go:build ignore
// +build ignore
package main
import (
"os"
cliDocs "github.com/urfave/cli-docs/v3"
"gh.tarampamp.am/error-pages/internal/cli"
)
func main() {
const readmePath = "../../README.md"
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 if err != nil {
println("readme file not found, cli docs not updated:", err.Error())
}
}

View File

@ -1,255 +0,0 @@
package config
import (
"os"
"path"
"path/filepath"
"strconv"
"strings"
"github.com/a8m/envsubst"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)
// Config is a main (exportable) config struct.
type Config struct {
Templates []Template
Pages map[string]Page // map key is a page code
Formats map[string]Format // map key is a format name
}
// Template returns a Template with the passes name.
func (c *Config) Template(name string) (*Template, bool) {
for i := 0; i < len(c.Templates); i++ {
if c.Templates[i].name == name {
return &c.Templates[i], true
}
}
return &Template{}, false
}
func (c *Config) JSONFormat() (*Format, bool) { return c.format("json") }
func (c *Config) XMLFormat() (*Format, bool) { return c.format("xml") }
func (c *Config) format(name string) (*Format, bool) {
if f, ok := c.Formats[name]; ok {
if len(f.content) > 0 {
return &f, true
}
}
return &Format{}, false
}
// TemplateNames returns all template names.
func (c *Config) TemplateNames() []string {
n := make([]string, len(c.Templates))
for i, t := range c.Templates {
n[i] = t.name
}
return n
}
// Template describes HTTP error page template.
type Template struct {
name string
content []byte
}
// Name returns the name of the template.
func (t Template) Name() string { return t.name }
// Content returns the template content.
func (t Template) Content() []byte { return t.content }
func (t *Template) loadContentFromFile(filePath string) (err error) {
if t.content, err = os.ReadFile(filePath); err != nil {
return errors.Wrap(err, "cannot load content for the template "+t.Name()+" from file "+filePath)
}
return
}
// Page describes error page.
type Page struct {
code string
message string
description string
}
// Code returns the code of the Page.
func (p Page) Code() string { return p.code }
// Message returns the message of the Page.
func (p Page) Message() string { return p.message }
// Description returns the description of the Page.
func (p Page) Description() string { return p.description }
// Format describes different response formats.
type Format struct {
name string
content []byte
}
// Name returns the name of the format.
func (f Format) Name() string { return f.name }
// Content returns the format content.
func (f Format) Content() []byte { return f.content }
// config is internal struct for marshaling/unmarshaling configuration file content.
type config struct {
Templates []struct {
Path string `yaml:"path"`
Name string `yaml:"name"`
Content string `yaml:"content"`
} `yaml:"templates"`
Formats map[string]struct {
Content string `yaml:"content"`
} `yaml:"formats"`
Pages map[string]struct {
Message string `yaml:"message"`
Description string `yaml:"description"`
} `yaml:"pages"`
}
// Validate the config struct and return an error if something is wrong.
func (c config) Validate() error {
if len(c.Templates) == 0 {
return errors.New("empty templates list")
} else {
for i := 0; i < len(c.Templates); i++ {
if c.Templates[i].Name == "" && c.Templates[i].Path == "" {
return errors.New("empty path and name with index " + strconv.Itoa(i))
}
if c.Templates[i].Path == "" && c.Templates[i].Content == "" {
return errors.New("empty path and template content with index " + strconv.Itoa(i))
}
}
}
if len(c.Pages) == 0 {
return errors.New("empty pages list")
} else {
for code := range c.Pages {
if code == "" {
return errors.New("empty page code")
}
if strings.ContainsRune(code, ' ') {
return errors.New("code should not contain whitespaces")
}
}
}
if len(c.Formats) > 0 {
for name := range c.Formats {
if name == "" {
return errors.New("empty format name")
}
if strings.ContainsRune(name, ' ') {
return errors.New("format should not contain whitespaces")
}
}
}
return nil
}
// Export the config struct into Config.
func (c *config) Export() (*Config, error) {
cfg := &Config{}
cfg.Templates = make([]Template, 0, len(c.Templates))
for i := 0; i < len(c.Templates); i++ {
tpl := Template{name: c.Templates[i].Name}
if c.Templates[i].Content == "" {
if c.Templates[i].Path == "" {
return nil, errors.New("path to the template " + c.Templates[i].Name + " not provided")
}
if err := tpl.loadContentFromFile(c.Templates[i].Path); err != nil {
return nil, err
}
} else {
tpl.content = []byte(c.Templates[i].Content)
}
cfg.Templates = append(cfg.Templates, tpl)
}
cfg.Pages = make(map[string]Page, len(c.Pages))
for code, p := range c.Pages {
cfg.Pages[code] = Page{code: code, message: p.Message, description: p.Description}
}
cfg.Formats = make(map[string]Format, len(c.Formats))
for name, f := range c.Formats {
cfg.Formats[name] = Format{name: name, content: []byte(strings.TrimSpace(f.Content))}
}
return cfg, nil
}
// FromYaml creates new Config instance using YAML-structured content.
func FromYaml(in []byte) (_ *Config, err error) {
in, err = envsubst.Bytes(in)
if err != nil {
return nil, err
}
c := &config{}
if err = yaml.Unmarshal(in, c); err != nil {
return nil, errors.Wrap(err, "cannot parse configuration file")
}
var basename string
for i := 0; i < len(c.Templates); i++ {
if c.Templates[i].Name == "" { // set the template name from file path
basename = filepath.Base(c.Templates[i].Path)
c.Templates[i].Name = strings.TrimSuffix(basename, filepath.Ext(basename))
}
}
if err = c.Validate(); err != nil {
return nil, err
}
return c.Export()
}
// FromYamlFile creates new Config instance using YAML file.
func FromYamlFile(filepath string) (*Config, error) {
bytes, err := os.ReadFile(filepath)
if err != nil {
return nil, errors.Wrap(err, "cannot read configuration file")
}
// the following code makes it possible to use the relative links in the config file (`.` means "directory with
// the config file")
cwd, err := os.Getwd()
if err == nil {
if err = os.Chdir(path.Dir(filepath)); err != nil {
return nil, err
}
defer func() { _ = os.Chdir(cwd) }()
}
return FromYaml(bytes)
}

View File

@ -1,196 +0,0 @@
package config_test
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/config"
)
func TestFromYaml(t *testing.T) {
var cases = map[string]struct { //nolint:maligned
giveYaml []byte
giveEnv map[string]string
wantErr bool
checkResultFn func(*testing.T, *config.Config)
}{
"with all possible values": {
giveEnv: map[string]string{
"__FOO_TPL_PATH": "./testdata/foo-tpl.html",
"__FOO_TPL_NAME": "Foo Template",
},
giveYaml: []byte(`
templates:
- path: ${__FOO_TPL_PATH}
name: ${__FOO_TPL_NAME:-default_value} # name is optional
- path: ./testdata/bar-tpl.html
- name: Baz
content: |
Some content {{ code }}
New line
formats:
json:
content: |
{"code": "{{code}}"}
Avada_Kedavra:
content: "{{ message }}"
pages:
400:
message: Bad Request
description: The server did not understand the request
401:
message: Unauthorized
description: The requested page needs a username and a password
`),
wantErr: false,
checkResultFn: func(t *testing.T, cfg *config.Config) {
assert.Len(t, cfg.Templates, 3)
tpl, found := cfg.Template("Foo Template")
assert.True(t, found)
assert.Equal(t, "Foo Template", tpl.Name())
assert.Equal(t, "<html><body>foo {{ code }}</body></html>\n", string(tpl.Content()))
tpl, found = cfg.Template("bar-tpl")
assert.True(t, found)
assert.Equal(t, "bar-tpl", tpl.Name())
assert.Equal(t, "<html><body>bar {{ code }}</body></html>\n", string(tpl.Content()))
tpl, found = cfg.Template("Baz")
assert.True(t, found)
assert.Equal(t, "Baz", tpl.Name())
assert.Equal(t, "Some content {{ code }}\nNew line\n", string(tpl.Content()))
tpl, found = cfg.Template("NonExists")
assert.False(t, found)
assert.Equal(t, "", tpl.Name())
assert.Equal(t, "", string(tpl.Content()))
assert.Len(t, cfg.Formats, 2)
format, found := cfg.Formats["json"]
assert.True(t, found)
assert.Equal(t, `{"code": "{{code}}"}`, string(format.Content()))
format, found = cfg.Formats["Avada_Kedavra"]
assert.True(t, found)
assert.Equal(t, "{{ message }}", string(format.Content()))
assert.Len(t, cfg.Pages, 2)
errPage, found := cfg.Pages["400"]
assert.True(t, found)
assert.Equal(t, "400", errPage.Code())
assert.Equal(t, "Bad Request", errPage.Message())
assert.Equal(t, "The server did not understand the request", errPage.Description())
errPage, found = cfg.Pages["401"]
assert.True(t, found)
assert.Equal(t, "401", errPage.Code())
assert.Equal(t, "Unauthorized", errPage.Message())
assert.Equal(t, "The requested page needs a username and a password", errPage.Description())
errPage, found = cfg.Pages["666"]
assert.False(t, found)
assert.Equal(t, "", errPage.Message())
assert.Equal(t, "", errPage.Code())
assert.Equal(t, "", errPage.Description())
},
},
"broken yaml": {
giveYaml: []byte(`foo bar`),
wantErr: true,
},
}
for name, tt := range cases {
t.Run(name, func(t *testing.T) {
if tt.giveEnv != nil {
for key, value := range tt.giveEnv {
assert.NoError(t, os.Setenv(key, value))
}
}
conf, err := config.FromYaml(tt.giveYaml)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.Nil(t, err)
tt.checkResultFn(t, conf)
}
if tt.giveEnv != nil {
for key := range tt.giveEnv {
assert.NoError(t, os.Unsetenv(key))
}
}
})
}
}
func TestFromYamlFile(t *testing.T) {
var cases = map[string]struct { //nolint:maligned
giveYamlFilePath string
wantErr bool
checkResultFn func(*testing.T, *config.Config)
}{
"with all possible values": {
giveYamlFilePath: "./testdata/simple.yml",
wantErr: false,
checkResultFn: func(t *testing.T, cfg *config.Config) {
assert.Len(t, cfg.Templates, 2)
tpl, found := cfg.Template("ghost")
assert.True(t, found)
assert.Equal(t, "ghost", tpl.Name())
assert.Equal(t, "<html><body>foo {{ code }}</body></html>\n", string(tpl.Content()))
tpl, found = cfg.Template("bar-tpl")
assert.True(t, found)
assert.Equal(t, "bar-tpl", tpl.Name())
assert.Equal(t, "<html><body>bar {{ code }}</body></html>\n", string(tpl.Content()))
assert.Len(t, cfg.Pages, 2)
errPage, found := cfg.Pages["400"]
assert.True(t, found)
assert.Equal(t, "400", errPage.Code())
assert.Equal(t, "Bad Request", errPage.Message())
assert.Equal(t, "The server did not understand the request", errPage.Description())
errPage, found = cfg.Pages["401"]
assert.True(t, found)
assert.Equal(t, "401", errPage.Code())
assert.Equal(t, "Unauthorized", errPage.Message())
assert.Equal(t, "The requested page needs a username and a password", errPage.Description())
},
},
"broken yaml": {
giveYamlFilePath: "./testdata/broken.yml",
wantErr: true,
},
"wrong file path": {
giveYamlFilePath: "foo bar",
wantErr: true,
},
}
for name, tt := range cases {
t.Run(name, func(t *testing.T) {
conf, err := config.FromYamlFile(tt.giveYamlFilePath)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.Nil(t, err)
tt.checkResultFn(t, conf)
}
})
}
}

View File

@ -1 +0,0 @@
<html><body>bar {{ code }}</body></html>

View File

@ -1 +0,0 @@
foo bar

View File

@ -1 +0,0 @@
<html><body>foo {{ code }}</body></html>

View File

@ -1,13 +0,0 @@
templates:
- path: ./foo-tpl.html
name: ghost # name is optional
- path: ./bar-tpl.html
pages:
400:
message: Bad Request
description: The server did not understand the request
401:
message: Unauthorized
description: The requested page needs a username and a password

31
internal/env/env.go vendored
View File

@ -1,31 +0,0 @@
// Package env contains all about environment variables, that can be used by current application.
package env
import "os"
type envVariable string
const (
LogLevel envVariable = "LOG_LEVEL" // logging level
LogFormat envVariable = "LOG_FORMAT" // logging format (json|console)
ListenAddr envVariable = "LISTEN_ADDR" // IP address for listening
ListenPort envVariable = "LISTEN_PORT" // port number for listening
TemplateName envVariable = "TEMPLATE_NAME" // template name
ConfigFilePath envVariable = "CONFIG_FILE" // path to the config file
DefaultErrorPage envVariable = "DEFAULT_ERROR_PAGE" // default error page (code)
DefaultHTTPCode envVariable = "DEFAULT_HTTP_CODE" // default HTTP response code
ShowDetails envVariable = "SHOW_DETAILS" // show request details in response
ProxyHTTPHeaders envVariable = "PROXY_HTTP_HEADERS" // proxy HTTP request headers list (request -> response)
DisableL10n envVariable = "DISABLE_L10N" // disable pages localization
CatchAll envVariable = "CATCH_ALL" // catch all pages
ReadBufferSize envVariable = "READ_BUFFER_SIZE" // https://github.com/tarampampam/error-pages/issues/238
)
// String returns environment variable name in the string representation.
func (e envVariable) String() string { return string(e) }
// Lookup retrieves the value of the environment variable. If the variable is present in the environment the value
// (which may be empty) is returned and the boolean is true. Otherwise the returned value will be empty and the
// boolean will be false.
func (e envVariable) Lookup() (string, bool) { return os.LookupEnv(string(e)) }

View File

@ -1,59 +0,0 @@
package env
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestConstants(t *testing.T) {
assert.Equal(t, "LISTEN_ADDR", string(ListenAddr))
assert.Equal(t, "LISTEN_PORT", string(ListenPort))
assert.Equal(t, "TEMPLATE_NAME", string(TemplateName))
assert.Equal(t, "CONFIG_FILE", string(ConfigFilePath))
assert.Equal(t, "DEFAULT_ERROR_PAGE", string(DefaultErrorPage))
assert.Equal(t, "DEFAULT_HTTP_CODE", string(DefaultHTTPCode))
assert.Equal(t, "SHOW_DETAILS", string(ShowDetails))
assert.Equal(t, "PROXY_HTTP_HEADERS", string(ProxyHTTPHeaders))
assert.Equal(t, "DISABLE_L10N", string(DisableL10n))
assert.Equal(t, "CATCH_ALL", string(CatchAll))
assert.Equal(t, "READ_BUFFER_SIZE", string(ReadBufferSize))
}
func TestEnvVariable_Lookup(t *testing.T) {
cases := []struct {
giveEnv envVariable
}{
{giveEnv: ListenAddr},
{giveEnv: ListenPort},
{giveEnv: TemplateName},
{giveEnv: ConfigFilePath},
{giveEnv: DefaultErrorPage},
{giveEnv: DefaultHTTPCode},
{giveEnv: ShowDetails},
{giveEnv: ProxyHTTPHeaders},
{giveEnv: DisableL10n},
{giveEnv: CatchAll},
{giveEnv: ReadBufferSize},
}
for _, tt := range cases {
tt := tt
t.Run(tt.giveEnv.String(), func(t *testing.T) {
assert.NoError(t, os.Unsetenv(tt.giveEnv.String())) // make sure that env is unset for test
defer func() { assert.NoError(t, os.Unsetenv(tt.giveEnv.String())) }()
value, exists := tt.giveEnv.Lookup()
assert.False(t, exists)
assert.Empty(t, value)
assert.NoError(t, os.Setenv(tt.giveEnv.String(), "foo"))
value, exists = tt.giveEnv.Lookup()
assert.True(t, exists)
assert.Equal(t, "foo", value)
})
}
}

View File

@ -1,68 +0,0 @@
package common
import (
"strings"
"time"
"github.com/valyala/fasthttp"
"go.uber.org/zap"
)
func LogRequest(h fasthttp.RequestHandler, log *zap.Logger) fasthttp.RequestHandler {
const headersSeparator = ": "
return func(ctx *fasthttp.RequestCtx) {
var ua = string(ctx.UserAgent())
if strings.Contains(strings.ToLower(ua), "healthcheck") { // skip healthcheck requests logging
h(ctx)
return
}
var reqHeaders = make([]string, 0, 24) //nolint:gomnd
ctx.Request.Header.VisitAll(func(key, value []byte) {
reqHeaders = append(reqHeaders, string(key)+headersSeparator+string(value))
})
var startedAt = time.Now()
h(ctx)
var respHeaders = make([]string, 0, 16) //nolint:gomnd
ctx.Response.Header.VisitAll(func(key, value []byte) {
respHeaders = append(respHeaders, string(key)+headersSeparator+string(value))
})
log.Info("HTTP request processed",
zap.String("useragent", ua),
zap.String("method", string(ctx.Method())),
zap.String("url", string(ctx.RequestURI())),
zap.String("referer", string(ctx.Referer())),
zap.Int("status_code", ctx.Response.StatusCode()),
zap.String("content_type", string(ctx.Response.Header.ContentType())),
zap.Bool("connection_close", ctx.Response.ConnectionClose()),
zap.Duration("duration", time.Since(startedAt)),
zap.Strings("request_headers", reqHeaders),
zap.Strings("response_headers", respHeaders),
)
}
}
type metrics interface {
IncrementTotalRequests()
ObserveRequestDuration(t time.Duration)
}
func DurationMetrics(h fasthttp.RequestHandler, m metrics) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
var startedAt = time.Now()
h(ctx)
m.IncrementTotalRequests()
m.ObserveRequestDuration(time.Since(startedAt))
}
}

View File

@ -1,7 +0,0 @@
package common_test
import "testing"
func TestNothing2(t *testing.T) {
t.Skip("tests for this package have not been implemented yet")
}

View File

@ -1,127 +0,0 @@
package core
import (
"strconv"
"github.com/valyala/fasthttp"
"gh.tarampamp.am/error-pages/internal/config"
"gh.tarampamp.am/error-pages/internal/options"
"gh.tarampamp.am/error-pages/internal/tpl"
)
type templatePicker interface {
// Pick the template name for responding.
Pick() string
}
type renderer interface {
Render(content []byte, props tpl.Properties) ([]byte, error)
}
func RespondWithErrorPage( //nolint:funlen,gocyclo
ctx *fasthttp.RequestCtx,
cfg *config.Config,
p templatePicker,
rdr renderer,
pageCode string,
httpCode int,
opt options.ErrorPage,
) {
ctx.Response.Header.Set("X-Robots-Tag", "noindex") // block Search indexing
var (
clientWant = ClientWantFormat(ctx)
json, canJSON = cfg.JSONFormat()
xml, canXML = cfg.XMLFormat()
props = tpl.Properties{
Code: pageCode,
ShowRequestDetails: opt.ShowDetails,
L10nDisabled: opt.L10n.Disabled,
}
)
if opt.ShowDetails {
props.OriginalURI = string(ctx.Request.Header.Peek(OriginalURI))
props.Namespace = string(ctx.Request.Header.Peek(Namespace))
props.IngressName = string(ctx.Request.Header.Peek(IngressName))
props.ServiceName = string(ctx.Request.Header.Peek(ServiceName))
props.ServicePort = string(ctx.Request.Header.Peek(ServicePort))
props.RequestID = string(ctx.Request.Header.Peek(RequestID))
props.ForwardedFor = string(ctx.Request.Header.Peek(ForwardedFor))
props.Host = string(ctx.Request.Header.Peek(Host))
}
if page, exists := cfg.Pages[pageCode]; exists {
props.Message = page.Message()
props.Description = page.Description()
} else if c, err := strconv.Atoi(pageCode); err == nil {
if s := fasthttp.StatusMessage(c); s != "Unknown Status Code" { // as a fallback
props.Message = s
}
}
SetClientFormat(ctx, PlainTextContentType) // set default content type
if props.Message == "" {
ctx.SetStatusCode(fasthttp.StatusNotFound)
_, _ = ctx.WriteString("requested pageCode (" + pageCode + ") not available")
return
}
// proxy required HTTP headers from the request to the response
for _, headerToProxy := range opt.ProxyHTTPHeaders {
if reqHeader := ctx.Request.Header.Peek(headerToProxy); len(reqHeader) > 0 {
ctx.Response.Header.SetBytesV(headerToProxy, reqHeader)
}
}
switch {
case clientWant == JSONContentType && canJSON: // JSON
{
SetClientFormat(ctx, JSONContentType)
if content, err := rdr.Render(json.Content(), props); err == nil {
ctx.SetStatusCode(httpCode)
_, _ = ctx.Write(content)
} else {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
_, _ = ctx.WriteString("cannot render JSON template: " + err.Error())
}
}
case clientWant == XMLContentType && canXML: // XML
{
SetClientFormat(ctx, XMLContentType)
if content, err := rdr.Render(xml.Content(), props); err == nil {
ctx.SetStatusCode(httpCode)
_, _ = ctx.Write(content)
} else {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
_, _ = ctx.WriteString("cannot render XML template: " + err.Error())
}
}
default: // HTML
{
SetClientFormat(ctx, HTMLContentType)
var templateName = p.Pick()
if template, exists := cfg.Template(templateName); exists {
if content, err := rdr.Render(template.Content(), props); err == nil {
ctx.SetStatusCode(httpCode)
_, _ = ctx.Write(content)
} else {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
_, _ = ctx.WriteString("cannot render HTML template: " + err.Error())
}
} else {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
_, _ = ctx.WriteString("template " + templateName + " not exists")
}
}
}
}

View File

@ -1,102 +0,0 @@
package core
import (
"bytes"
"sort"
"strconv"
"github.com/valyala/fasthttp"
)
type ContentType = byte
const (
UnknownContentType ContentType = iota // should be first
JSONContentType
XMLContentType
HTMLContentType
PlainTextContentType
)
func ClientWantFormat(ctx *fasthttp.RequestCtx) ContentType {
// parse "Content-Type" header (e.g.: `application/json;charset=UTF-8`)
if ct := bytes.ToLower(ctx.Request.Header.ContentType()); len(ct) > 4 { //nolint:gomnd
return mimeTypeToContentType(ct)
}
// parse `X-Format` header (aka `Accept`) for the Ingress support
// e.g.: `text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8`
if h := bytes.ToLower(bytes.TrimSpace(ctx.Request.Header.Peek(FormatHeader))); len(h) > 2 { //nolint:gomnd,nestif
type format struct {
mimeType []byte
weight float32
}
var formats = make([]format, 0, 8) //nolint:gomnd
for _, b := range bytes.FieldsFunc(h, func(r rune) bool { return r == ',' }) {
if idx := bytes.Index(b, []byte(";q=")); idx > 0 && idx < len(b) {
f := format{b[0:idx], 0}
if len(b) > idx+3 {
if weight, err := strconv.ParseFloat(string(b[idx+3:]), 32); err == nil {
f.weight = float32(weight)
}
}
formats = append(formats, f)
} else {
formats = append(formats, format{b, 1})
}
}
switch l := len(formats); {
case l == 0:
return UnknownContentType
case l == 1:
return mimeTypeToContentType(formats[0].mimeType)
default:
sort.SliceStable(formats, func(i, j int) bool { return formats[i].weight > formats[j].weight })
return mimeTypeToContentType(formats[0].mimeType)
}
}
return UnknownContentType
}
func mimeTypeToContentType(mimeType []byte) ContentType {
switch {
case bytes.Contains(mimeType, []byte("application/json")), bytes.Contains(mimeType, []byte("text/json")):
return JSONContentType
case bytes.Contains(mimeType, []byte("application/xml")), bytes.Contains(mimeType, []byte("text/xml")):
return XMLContentType
case bytes.Contains(mimeType, []byte("text/html")):
return HTMLContentType
case bytes.Contains(mimeType, []byte("text/plain")):
return PlainTextContentType
}
return UnknownContentType
}
func SetClientFormat(ctx *fasthttp.RequestCtx, t ContentType) {
switch t {
case JSONContentType:
ctx.SetContentType("application/json; charset=utf-8")
case XMLContentType:
ctx.SetContentType("application/xml; charset=utf-8")
case HTMLContentType:
ctx.SetContentType("text/html; charset=utf-8")
case PlainTextContentType:
ctx.SetContentType("text/plain; charset=utf-8")
}
}

View File

@ -1,118 +0,0 @@
package core_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/valyala/fasthttp"
"gh.tarampamp.am/error-pages/internal/http/core"
)
func TestClientWantFormat(t *testing.T) {
for name, tt := range map[string]struct {
giveContentTypeHeader string
giveFormatHeader string
giveReqCtx func() *fasthttp.RequestCtx
wantFormat core.ContentType
}{
"priority": {
giveFormatHeader: "application/xml",
giveContentTypeHeader: "text/plain",
wantFormat: core.PlainTextContentType,
},
"format respects weight": {
giveFormatHeader: "text/html;q=0.5,application/xhtml+xml;q=0.9,application/xml;q=1,*/*;q=0.8",
wantFormat: core.XMLContentType,
},
"wrong format value": {
giveFormatHeader: ";q=foobar,bar/baz;;;;;application/xml",
wantFormat: core.UnknownContentType,
},
"content type - application/json": {
giveContentTypeHeader: "application/jsoN; charset=utf-8", wantFormat: core.JSONContentType,
},
"content type - text/json": {
giveContentTypeHeader: "text/Json; charset=utf-8", wantFormat: core.JSONContentType,
},
"format - json": {
giveFormatHeader: "application/jsoN,*/*;q=0.8", wantFormat: core.JSONContentType,
},
"content type - application/xml": {
giveContentTypeHeader: "application/xmL; charset=utf-8", wantFormat: core.XMLContentType,
},
"content type - text/xml": {
giveContentTypeHeader: "text/Xml; charset=utf-8", wantFormat: core.XMLContentType,
},
"format - xml": {
giveFormatHeader: "text/Xml", wantFormat: core.XMLContentType,
},
"content type - text/html": {
giveContentTypeHeader: "text/htMl; charset=utf-8", wantFormat: core.HTMLContentType,
},
"format - html": {
giveFormatHeader: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
wantFormat: core.HTMLContentType,
},
"content type - text/plain": {
giveContentTypeHeader: "text/plaiN; charset=utf-8", wantFormat: core.PlainTextContentType,
},
"format - plain": {
giveFormatHeader: "text/plaiN,text/html,application/xml;q=0.9,,,*/*;q=0.8", wantFormat: core.PlainTextContentType,
},
"unknown on empty": {
wantFormat: core.UnknownContentType,
},
"unknown on foo/bar": {
giveContentTypeHeader: "foo/bar; charset=utf-8",
giveFormatHeader: "foo/bar; charset=utf-8",
wantFormat: core.UnknownContentType,
},
} {
t.Run(name, func(t *testing.T) {
h := &fasthttp.RequestHeader{}
h.Set(fasthttp.HeaderContentType, tt.giveContentTypeHeader)
h.Set(core.FormatHeader, tt.giveFormatHeader)
ctx := &fasthttp.RequestCtx{
Request: fasthttp.Request{
Header: *h, //nolint:govet
},
}
assert.Equal(t, tt.wantFormat, core.ClientWantFormat(ctx))
})
}
}
func TestSetClientFormat(t *testing.T) {
for name, tt := range map[string]struct {
giveContentType core.ContentType
wantHeaderValue string
}{
"plain on unknown": {giveContentType: core.UnknownContentType, wantHeaderValue: "text/plain; charset=utf-8"},
"json": {giveContentType: core.JSONContentType, wantHeaderValue: "application/json; charset=utf-8"},
"xml": {giveContentType: core.XMLContentType, wantHeaderValue: "application/xml; charset=utf-8"},
"html": {giveContentType: core.HTMLContentType, wantHeaderValue: "text/html; charset=utf-8"},
"plain": {giveContentType: core.PlainTextContentType, wantHeaderValue: "text/plain; charset=utf-8"},
} {
t.Run(name, func(t *testing.T) {
ctx := &fasthttp.RequestCtx{
Response: fasthttp.Response{
Header: fasthttp.ResponseHeader{},
},
}
assert.Empty(t, "", ctx.Response.Header.Peek(fasthttp.HeaderContentType))
core.SetClientFormat(ctx, tt.giveContentType)
assert.Equal(t, tt.wantHeaderValue, string(ctx.Response.Header.Peek(fasthttp.HeaderContentType)))
})
}
}

View File

@ -1,33 +0,0 @@
package core
const (
// FormatHeader name of the header used to extract the format
FormatHeader = "X-Format"
// CodeHeader name of the header used as source of the HTTP status code to return
CodeHeader = "X-Code"
// OriginalURI name of the header with the original URL from NGINX
OriginalURI = "X-Original-URI"
// Namespace name of the header that contains information about the Ingress namespace
Namespace = "X-Namespace"
// IngressName name of the header that contains the matched Ingress
IngressName = "X-Ingress-Name"
// ServiceName name of the header that contains the matched Service in the Ingress
ServiceName = "X-Service-Name"
// ServicePort name of the header that contains the matched Service port in the Ingress
ServicePort = "X-Service-Port"
// RequestID is a unique ID that identifies the request - same as for backend service
RequestID = "X-Request-ID"
// ForwardedFor identifies the user of this session
ForwardedFor = "X-Forwarded-For"
// Host identifies the hosts origin
Host = "Host"
)

View File

@ -1,35 +0,0 @@
package errorpage
import (
"github.com/valyala/fasthttp"
"gh.tarampamp.am/error-pages/internal/config"
"gh.tarampamp.am/error-pages/internal/http/core"
"gh.tarampamp.am/error-pages/internal/options"
"gh.tarampamp.am/error-pages/internal/tpl"
)
type (
templatePicker interface {
// Pick the template name for responding.
Pick() string
}
renderer interface {
Render(content []byte, props tpl.Properties) ([]byte, error)
}
)
// NewHandler creates handler for error pages serving.
func NewHandler(cfg *config.Config, p templatePicker, rdr renderer, opt options.ErrorPage) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
core.SetClientFormat(ctx, core.PlainTextContentType) // default content type
if code, ok := ctx.UserValue("code").(string); ok {
core.RespondWithErrorPage(ctx, cfg, p, rdr, code, fasthttp.StatusOK, opt)
} else { // will never occur
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
_, _ = ctx.WriteString("cannot extract requested code from the request")
}
}
}

View File

@ -1,7 +0,0 @@
package errorpage_test
import "testing"
func TestNothing(t *testing.T) {
t.Skip("tests for this package have not been implemented yet")
}

View File

@ -1,24 +0,0 @@
package healthz
import "github.com/valyala/fasthttp"
// checker allows to check some service part.
type checker interface {
// Check makes a check and return error only if something is wrong.
Check() error
}
// NewHandler creates healthcheck handler.
func NewHandler(checker checker) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
if err := checker.Check(); err != nil {
ctx.SetStatusCode(fasthttp.StatusServiceUnavailable)
_, _ = ctx.WriteString(err.Error())
return
}
ctx.SetStatusCode(fasthttp.StatusOK)
_, _ = ctx.WriteString("OK")
}
}

View File

@ -1,7 +0,0 @@
package healthz_test
import "testing"
func TestNothing(t *testing.T) {
t.Skip("tests for this package have not been implemented yet")
}

View File

@ -1,50 +0,0 @@
package index
import (
"strconv"
"github.com/valyala/fasthttp"
"gh.tarampamp.am/error-pages/internal/config"
"gh.tarampamp.am/error-pages/internal/http/core"
"gh.tarampamp.am/error-pages/internal/options"
"gh.tarampamp.am/error-pages/internal/tpl"
)
type (
templatePicker interface {
// Pick the template name for responding.
Pick() string
}
renderer interface {
Render(content []byte, props tpl.Properties) ([]byte, error)
}
)
// NewHandler creates handler for the index page serving.
func NewHandler(cfg *config.Config, p templatePicker, rdr renderer, opt options.ErrorPage) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
pageCode, httpCode := opt.Default.PageCode, int(opt.Default.HTTPCode)
if returnCode, ok := extractCodeToReturn(ctx); ok {
pageCode, httpCode = strconv.Itoa(returnCode), returnCode
}
core.RespondWithErrorPage(ctx, cfg, p, rdr, pageCode, httpCode, opt)
}
}
func extractCodeToReturn(ctx *fasthttp.RequestCtx) (int, bool) { // for the Ingress support
var ch = ctx.Request.Header.Peek(core.CodeHeader)
if len(ch) > 0 && len(ch) <= 3 {
if code, err := strconv.Atoi(string(ch)); err == nil {
if code > 0 && code <= 599 {
return code, true
}
}
}
return 0, false
}

View File

@ -1,7 +0,0 @@
package index_test
import "testing"
func TestNothing(t *testing.T) {
t.Skip("tests for this package have not been implemented yet")
}

View File

@ -1,16 +0,0 @@
// Package metrics contains HTTP handler for application metrics (prometheus format) generation.
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/fasthttpadaptor"
)
// NewHandler creates metrics handler.
func NewHandler(registry prometheus.Gatherer) fasthttp.RequestHandler {
return fasthttpadaptor.NewFastHTTPHandler(
promhttp.HandlerFor(registry, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError}),
)
}

View File

@ -1,7 +0,0 @@
package metrics_test
import "testing"
func TestNothing(t *testing.T) {
t.Skip("tests for this package have not been implemented yet")
}

View File

@ -1,28 +0,0 @@
package notfound
import (
"github.com/valyala/fasthttp"
"gh.tarampamp.am/error-pages/internal/config"
"gh.tarampamp.am/error-pages/internal/http/core"
"gh.tarampamp.am/error-pages/internal/options"
"gh.tarampamp.am/error-pages/internal/tpl"
)
type (
templatePicker interface {
// Pick the template name for responding.
Pick() string
}
renderer interface {
Render(content []byte, props tpl.Properties) ([]byte, error)
}
)
// NewHandler creates handler missing requests handling.
func NewHandler(cfg *config.Config, p templatePicker, rdr renderer, opt options.ErrorPage) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
core.RespondWithErrorPage(ctx, cfg, p, rdr, "404", fasthttp.StatusNotFound, opt)
}
}

View File

@ -1,7 +0,0 @@
package notfound_test
import "testing"
func TestNothing(t *testing.T) {
t.Skip("tests for this package have not been implemented yet")
}

View File

@ -1,26 +0,0 @@
package version
import (
"encoding/json"
"github.com/valyala/fasthttp"
)
// NewHandler creates version handler.
func NewHandler(ver string) fasthttp.RequestHandler {
var cache []byte
return func(ctx *fasthttp.RequestCtx) {
if cache == nil {
cache, _ = json.Marshal(struct {
Version string `json:"version"`
}{
Version: ver,
})
}
ctx.SetContentType("application/json")
ctx.SetStatusCode(fasthttp.StatusOK)
_, _ = ctx.Write(cache)
}
}

View File

@ -1,7 +0,0 @@
package version_test
import "testing"
func TestNothing(t *testing.T) {
t.Skip("tests for this package have not been implemented yet")
}

View File

@ -1,131 +0,0 @@
package http
import (
"errors"
"fmt"
"net"
"strings"
"time"
"github.com/fasthttp/router"
"github.com/valyala/fasthttp"
"go.uber.org/zap"
"gh.tarampamp.am/error-pages/internal/checkers"
"gh.tarampamp.am/error-pages/internal/config"
"gh.tarampamp.am/error-pages/internal/http/common"
errorpageHandler "gh.tarampamp.am/error-pages/internal/http/handlers/errorpage"
healthzHandler "gh.tarampamp.am/error-pages/internal/http/handlers/healthz"
indexHandler "gh.tarampamp.am/error-pages/internal/http/handlers/index"
metricsHandler "gh.tarampamp.am/error-pages/internal/http/handlers/metrics"
notfoundHandler "gh.tarampamp.am/error-pages/internal/http/handlers/notfound"
versionHandler "gh.tarampamp.am/error-pages/internal/http/handlers/version"
"gh.tarampamp.am/error-pages/internal/metrics"
"gh.tarampamp.am/error-pages/internal/options"
"gh.tarampamp.am/error-pages/internal/tpl"
"gh.tarampamp.am/error-pages/internal/version"
)
type Server struct {
log *zap.Logger
fast *fasthttp.Server
router *router.Router
rdr *tpl.TemplateRenderer
}
const (
defaultWriteTimeout = time.Second * 4
defaultReadTimeout = time.Second * 4
defaultIdleTimeout = time.Second * 6
)
func NewServer(log *zap.Logger, readBufferSize uint) Server {
rdr := tpl.NewTemplateRenderer()
return Server{
// fasthttp docs: <https://github.com/valyala/fasthttp>
fast: &fasthttp.Server{
WriteTimeout: defaultWriteTimeout,
ReadBufferSize: int(readBufferSize),
ReadTimeout: defaultReadTimeout,
IdleTimeout: defaultIdleTimeout,
NoDefaultServerHeader: true,
ReduceMemoryUsage: true,
CloseOnShutdown: true,
Logger: zap.NewStdLog(log),
},
router: router.New(),
log: log,
rdr: rdr,
}
}
// Start server.
func (s *Server) Start(ip string, port uint16) (err error) {
if net.ParseIP(ip) == nil {
return errors.New("invalid IP address")
}
var ln net.Listener
if strings.Count(ip, ":") >= 2 { //nolint:gomnd // ipv6
if ln, err = net.Listen("tcp6", fmt.Sprintf("[%s]:%d", ip, port)); err != nil {
return err
}
} else { // ipv4
if ln, err = net.Listen("tcp4", fmt.Sprintf("%s:%d", ip, port)); err != nil {
return err
}
}
return s.fast.Serve(ln)
}
type templatePicker interface {
// Pick the template name for responding.
Pick() string
}
// Register server routes, middlewares, etc.
// Router docs: <https://github.com/fasthttp/router>
func (s *Server) Register(cfg *config.Config, templatePicker templatePicker, opt options.ErrorPage) error {
reg, m := metrics.NewRegistry(), metrics.NewMetrics()
if err := m.Register(reg); err != nil {
return err
}
s.fast.Handler = common.DurationMetrics(common.LogRequest(s.router.Handler, s.log), &m)
s.router.GET("/", indexHandler.NewHandler(cfg, templatePicker, s.rdr, opt))
s.router.GET("/{code}.html", errorpageHandler.NewHandler(cfg, templatePicker, s.rdr, opt))
s.router.GET("/version", versionHandler.NewHandler(version.Version()))
liveHandler := healthzHandler.NewHandler(checkers.NewLiveChecker())
s.router.ANY("/healthz", liveHandler)
s.router.ANY("/health/live", liveHandler) // deprecated
s.router.GET("/metrics", metricsHandler.NewHandler(reg))
// use index handler to catch all paths? Uses DEFAULT_ERROR_PAGE
if opt.CatchAll {
s.router.NotFound = indexHandler.NewHandler(cfg, templatePicker, s.rdr, opt)
} else {
// use default not found handler
s.router.NotFound = notfoundHandler.NewHandler(cfg, templatePicker, s.rdr, opt)
}
return nil
}
// Stop server.
func (s *Server) Stop() error {
if err := s.rdr.Close(); err != nil {
defer func() { _ = s.fast.Shutdown() }()
return err
}
return s.fast.Shutdown()
}

View File

@ -1,7 +0,0 @@
package http
import "testing"
func TestNothing(t *testing.T) {
t.Skip("tests for this package have not been implemented yet")
}

View File

@ -1,4 +1,3 @@
// Package logger contains functions for a working with application logging.
package logger
import (

View File

@ -1,52 +0,0 @@
package metrics
import (
"time"
"github.com/prometheus/client_golang/prometheus"
)
type Metrics struct {
total prometheus.Counter
duration prometheus.Histogram
}
// NewMetrics creates new Metrics collector.
func NewMetrics() Metrics {
const namespace, subsystem = "http", "requests"
return Metrics{
total: prometheus.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "total_count",
Help: "Counter of HTTP requests made.",
}),
duration: prometheus.NewHistogram(prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "duration_milliseconds",
Help: "Histogram of the time (in milliseconds) each request took.",
Buckets: append([]float64{.001, .003}, prometheus.DefBuckets...),
}),
}
}
// IncrementTotalRequests increments total requests counter.
func (w *Metrics) IncrementTotalRequests() { w.total.Inc() }
// ObserveRequestDuration observer requests duration histogram.
func (w *Metrics) ObserveRequestDuration(t time.Duration) { w.duration.Observe(t.Seconds()) }
// Register metrics with registerer.
func (w *Metrics) Register(reg prometheus.Registerer) error {
if err := reg.Register(w.total); err != nil {
return err
}
if err := reg.Register(w.duration); err != nil {
return err
}
return nil
}

View File

@ -1,71 +0,0 @@
package metrics_test
import (
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/metrics"
)
func TestMetrics_Register(t *testing.T) {
var (
registry = prometheus.NewRegistry()
m = metrics.NewMetrics()
)
assert.NoError(t, m.Register(registry))
count, err := testutil.GatherAndCount(registry,
"http_requests_total_count",
"http_requests_duration_milliseconds",
)
assert.NoError(t, err)
assert.Equal(t, 2, count)
}
func TestMetrics_IncrementTotalRequests(t *testing.T) {
p := metrics.NewMetrics()
p.IncrementTotalRequests()
metric := getMetric(t, &p, "http_requests_total_count")
assert.Equal(t, float64(1), metric.Counter.GetValue())
}
func TestMetrics_ObserveRequestDuration(t *testing.T) {
p := metrics.NewMetrics()
p.ObserveRequestDuration(time.Second)
metric := getMetric(t, &p, "http_requests_duration_milliseconds")
assert.Equal(t, float64(1), metric.Histogram.GetSampleSum())
}
type registerer interface {
Register(prometheus.Registerer) error
}
func getMetric(t *testing.T, reg registerer, name string) *dto.Metric {
t.Helper()
registry := prometheus.NewRegistry()
_ = reg.Register(registry)
families, _ := registry.Gather()
for _, family := range families {
if family.GetName() == name {
return family.Metric[0]
}
}
assert.FailNowf(t, "cannot resolve metric for: %s", name)
return nil
}

View File

@ -1,20 +0,0 @@
// Package metrics contains custom prometheus metrics and registry factories.
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
)
// NewRegistry creates new prometheus registry with pre-registered common collectors.
func NewRegistry() *prometheus.Registry {
registry := prometheus.NewRegistry()
// register common metric collectors
registry.MustRegister(
// collectors.NewGoCollector(),
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
)
return registry
}

View File

@ -1,19 +0,0 @@
package metrics_test
import (
"testing"
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/metrics"
)
func TestNewRegistry(t *testing.T) {
registry := metrics.NewRegistry()
count, err := testutil.GatherAndCount(registry)
assert.NoError(t, err)
assert.True(t, count >= 6, "not enough common metrics")
}

View File

@ -1,17 +0,0 @@
package options
type ErrorPage struct {
Default struct {
PageCode string // default error page code
HTTPCode uint16 // default HTTP response code
}
L10n struct {
Disabled bool // disable error pages localization
}
Template struct {
Name string // template name
}
ShowDetails bool // show request details in response
ProxyHTTPHeaders []string // proxy HTTP request headers list
CatchAll bool // catch all pages
}

View File

@ -1,88 +0,0 @@
package pick
import (
"math/rand"
"sync"
"time"
)
type pickMode = byte
const (
First pickMode = 1 + iota // Always pick the first element (index = 0)
RandomOnce // Pick random element once (any future Pick calls will return the same element)
RandomEveryTime // Always Pick the random element
)
type picker struct {
mode pickMode
rand *rand.Rand // will be nil for the First pick mode
maxIdx uint32
mu sync.Mutex
lastIdx uint32
}
const unsetIdx uint32 = 4294967295
func NewPicker(maxIdx uint32, mode pickMode) *picker {
var p = &picker{
maxIdx: maxIdx,
mode: mode,
lastIdx: unsetIdx,
}
if mode != First {
p.rand = rand.New(rand.NewSource(time.Now().UnixNano())) //nolint:gosec
}
return p
}
// NextIndex returns an index for the next element (based on pickMode).
func (p *picker) NextIndex() uint32 {
if p.maxIdx == 0 {
return 0
}
switch p.mode {
case First:
return 0
case RandomOnce:
if p.lastIdx == unsetIdx {
return p.randomizeNext()
}
return p.lastIdx
case RandomEveryTime:
return p.randomizeNext()
default:
panic("picker.NextIndex(): unsupported mode")
}
}
func (p *picker) randomizeNext() uint32 {
var idx = uint32(p.rand.Intn(int(p.maxIdx + 1)))
p.mu.Lock()
defer p.mu.Unlock()
if idx == p.lastIdx {
p.lastIdx++
} else {
p.lastIdx = idx
}
if p.lastIdx > p.maxIdx { // overflow?
p.lastIdx = 0
}
if p.lastIdx == unsetIdx {
p.lastIdx--
}
return p.lastIdx
}

View File

@ -1,58 +0,0 @@
package pick_test
import (
"testing"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/pick"
)
func TestPicker_NextIndex_First(t *testing.T) {
for i := uint32(0); i < 100; i++ {
p := pick.NewPicker(i, pick.First)
for j := uint8(0); j < 100; j++ {
assert.Equal(t, uint32(0), p.NextIndex())
}
}
}
func TestPicker_NextIndex_RandomOnce(t *testing.T) {
for i := uint8(0); i < 10; i++ {
assert.Equal(t, uint32(0), pick.NewPicker(0, pick.RandomOnce).NextIndex())
}
for i := uint8(10); i < 100; i++ {
p := pick.NewPicker(uint32(i), pick.RandomOnce)
next := p.NextIndex()
assert.LessOrEqual(t, next, uint32(i))
for j := uint8(0); j < 100; j++ {
assert.Equal(t, next, p.NextIndex())
}
}
}
func TestPicker_NextIndex_RandomEveryTime(t *testing.T) {
for i := uint8(0); i < 10; i++ {
assert.Equal(t, uint32(0), pick.NewPicker(0, pick.RandomEveryTime).NextIndex())
}
for i := uint8(1); i < 100; i++ {
p := pick.NewPicker(uint32(i), pick.RandomEveryTime)
for j := uint8(0); j < 100; j++ {
one, two := p.NextIndex(), p.NextIndex()
assert.LessOrEqual(t, one, uint32(i))
assert.LessOrEqual(t, two, uint32(i))
assert.NotEqual(t, one, two)
}
}
}
func TestPicker_NextIndex_Unsupported(t *testing.T) {
assert.Panics(t, func() { pick.NewPicker(1, 255).NextIndex() })
}

View File

@ -1,135 +0,0 @@
package pick
import (
"errors"
"sync"
"time"
)
type StringsSlice struct {
s []string
p *picker
}
// NewStringsSlice creates new StringsSlice.
func NewStringsSlice(items []string, mode pickMode) *StringsSlice {
maxIdx := len(items) - 1
if maxIdx < 0 {
maxIdx = 0
}
return &StringsSlice{s: items, p: NewPicker(uint32(maxIdx), mode)}
}
// Pick an element from the strings slice.
func (s *StringsSlice) Pick() string {
if len(s.s) == 0 {
return ""
}
return s.s[s.p.NextIndex()]
}
type StringsSliceWithInterval struct {
s []string
p *picker
d time.Duration
idxMu sync.RWMutex
idx uint32
close chan struct{}
closedMu sync.RWMutex
closed bool
}
// NewStringsSliceWithInterval creates new StringsSliceWithInterval.
func NewStringsSliceWithInterval(items []string, mode pickMode, interval time.Duration) *StringsSliceWithInterval {
maxIdx := len(items) - 1
if maxIdx < 0 {
maxIdx = 0
}
if interval <= time.Duration(0) {
panic("NewStringsSliceWithInterval: wrong interval")
}
s := &StringsSliceWithInterval{
s: items,
p: NewPicker(uint32(maxIdx), mode),
d: interval,
close: make(chan struct{}, 1),
}
s.next()
go s.rotate()
return s
}
func (s *StringsSliceWithInterval) rotate() {
defer close(s.close)
timer := time.NewTimer(s.d)
defer timer.Stop()
for {
select {
case <-s.close:
return
case <-timer.C:
s.next()
timer.Reset(s.d)
}
}
}
func (s *StringsSliceWithInterval) next() {
idx := s.p.NextIndex()
s.idxMu.Lock()
s.idx = idx
s.idxMu.Unlock()
}
// Pick an element from the strings slice.
func (s *StringsSliceWithInterval) Pick() string {
if s.isClosed() {
panic("StringsSliceWithInterval.Pick(): closed")
}
if len(s.s) == 0 {
return ""
}
s.idxMu.RLock()
defer s.idxMu.RUnlock()
return s.s[s.idx]
}
func (s *StringsSliceWithInterval) isClosed() (closed bool) {
s.closedMu.RLock()
closed = s.closed
s.closedMu.RUnlock()
return
}
func (s *StringsSliceWithInterval) Close() error {
if s.isClosed() {
return errors.New("closed")
}
s.closedMu.Lock()
s.closed = true
s.closedMu.Unlock()
s.close <- struct{}{}
return nil
}

View File

@ -1,131 +0,0 @@
package pick_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/pick"
)
func TestStringsSlice_Pick(t *testing.T) {
t.Run("first", func(t *testing.T) {
for i := uint8(0); i < 100; i++ {
assert.Equal(t, "", pick.NewStringsSlice([]string{}, pick.First).Pick())
}
p := pick.NewStringsSlice([]string{"foo", "bar", "baz"}, pick.First)
for i := uint8(0); i < 100; i++ {
assert.Equal(t, "foo", p.Pick())
}
})
t.Run("random once", func(t *testing.T) {
for i := uint8(0); i < 100; i++ {
assert.Equal(t, "", pick.NewStringsSlice([]string{}, pick.RandomOnce).Pick())
}
var (
p = pick.NewStringsSlice([]string{"foo", "bar", "baz"}, pick.RandomOnce)
picked = p.Pick()
)
for i := uint8(0); i < 100; i++ {
assert.Equal(t, picked, p.Pick())
}
})
t.Run("random every time", func(t *testing.T) {
for i := uint8(0); i < 100; i++ {
assert.Equal(t, "", pick.NewStringsSlice([]string{}, pick.RandomEveryTime).Pick())
}
for i := uint8(0); i < 100; i++ {
p := pick.NewStringsSlice([]string{"foo", "bar", "baz"}, pick.RandomEveryTime)
assert.NotEqual(t, p.Pick(), p.Pick())
}
})
}
func TestNewStringsSliceWithInterval_Pick(t *testing.T) {
t.Run("first", func(t *testing.T) {
for i := uint8(0); i < 50; i++ {
p := pick.NewStringsSliceWithInterval([]string{}, pick.First, time.Millisecond)
assert.Equal(t, "", p.Pick())
assert.NoError(t, p.Close())
assert.Panics(t, func() { p.Pick() })
}
p := pick.NewStringsSliceWithInterval([]string{"foo", "bar", "baz"}, pick.First, time.Millisecond)
for i := uint8(0); i < 50; i++ {
assert.Equal(t, "foo", p.Pick())
<-time.After(time.Millisecond * 2)
}
assert.NoError(t, p.Close())
assert.Error(t, p.Close())
assert.Panics(t, func() { p.Pick() })
})
t.Run("random once", func(t *testing.T) {
for i := uint8(0); i < 50; i++ {
p := pick.NewStringsSliceWithInterval([]string{}, pick.RandomOnce, time.Millisecond)
assert.Equal(t, "", p.Pick())
assert.NoError(t, p.Close())
assert.Panics(t, func() { p.Pick() })
}
var (
p = pick.NewStringsSliceWithInterval([]string{"foo", "bar", "baz"}, pick.RandomOnce, time.Millisecond)
picked = p.Pick()
)
for i := uint8(0); i < 50; i++ {
assert.Equal(t, picked, p.Pick())
<-time.After(time.Millisecond * 2)
}
assert.NoError(t, p.Close())
assert.Error(t, p.Close())
assert.Panics(t, func() { p.Pick() })
})
t.Run("random every time", func(t *testing.T) {
for i := uint8(0); i < 50; i++ {
p := pick.NewStringsSliceWithInterval([]string{}, pick.RandomEveryTime, time.Millisecond)
assert.Equal(t, "", p.Pick())
assert.NoError(t, p.Close())
assert.Panics(t, func() { p.Pick() })
}
var changed int
for i := uint8(0); i < 50; i++ {
p := pick.NewStringsSliceWithInterval([]string{"foo", "bar", "baz"}, pick.RandomEveryTime, time.Millisecond) //nolint:lll
one, two := p.Pick(), p.Pick()
assert.Equal(t, one, two)
<-time.After(time.Millisecond * 2)
three, four := p.Pick(), p.Pick()
assert.Equal(t, three, four)
if one != three {
changed++
}
assert.NoError(t, p.Close())
assert.Error(t, p.Close())
assert.Panics(t, func() { p.Pick() })
}
assert.GreaterOrEqual(t, changed, 25)
})
}

View File

@ -1,25 +0,0 @@
package tpl
import (
"bytes"
"crypto/md5" //nolint:gosec
"encoding/gob"
)
const hashLength = 16 // md5 hash length
type Hash [hashLength]byte
func HashStruct(s interface{}) (Hash, error) {
var b bytes.Buffer
if err := gob.NewEncoder(&b).Encode(s); err != nil {
return Hash{}, err
}
return md5.Sum(b.Bytes()), nil //nolint:gosec
}
func HashBytes(b []byte) Hash {
return md5.Sum(b) //nolint:gosec
}

View File

@ -1,36 +0,0 @@
package tpl_test
import (
"testing"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/tpl"
)
func TestHashBytes(t *testing.T) {
assert.NotEqual(t, tpl.HashBytes([]byte{1}), tpl.HashBytes([]byte{2}))
}
func TestHashStruct(t *testing.T) {
type s struct {
S string
I int
B bool
}
h1, err1 := tpl.HashStruct(s{S: "foo", I: 1, B: false})
assert.NoError(t, err1)
h2, err2 := tpl.HashStruct(s{S: "bar", I: 2, B: true})
assert.NoError(t, err2)
assert.NotEqual(t, h1, h2)
type p struct { // no exported fields
any string
}
_, err := tpl.HashStruct(p{any: "foo"})
assert.Error(t, err)
}

View File

@ -1,40 +0,0 @@
package tpl
import (
"reflect"
)
type Properties struct { // only string properties with a "token" tag, please
Code string `token:"code"`
Message string `token:"message"`
Description string `token:"description"`
OriginalURI string `token:"original_uri"`
Namespace string `token:"namespace"`
IngressName string `token:"ingress_name"`
ServiceName string `token:"service_name"`
ServicePort string `token:"service_port"`
RequestID string `token:"request_id"`
ForwardedFor string `token:"forwarded_for"`
Host string `token:"host"`
L10nDisabled bool
ShowRequestDetails bool
}
// Replaces return a map with strings for the replacing, where the map key is a token.
func (p *Properties) Replaces() map[string]string {
var replaces = make(map[string]string, reflect.ValueOf(*p).NumField())
for i, v := 0, reflect.ValueOf(*p); i < v.NumField(); i++ {
if keyword, tagExists := v.Type().Field(i).Tag.Lookup("token"); tagExists {
if sv, isString := v.Field(i).Interface().(string); isString && len(sv) > 0 {
replaces[keyword] = sv
} else {
replaces[keyword] = ""
}
}
}
return replaces
}
func (p *Properties) Hash() (Hash, error) { return HashStruct(p) }

View File

@ -1,67 +0,0 @@
package tpl_test
import (
"testing"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/tpl"
)
func TestProperties_Replaces(t *testing.T) {
props := tpl.Properties{
Code: "foo",
Message: "bar",
Description: "baz",
OriginalURI: "aaa",
Namespace: "bbb",
IngressName: "ccc",
ServiceName: "ddd",
ServicePort: "eee",
RequestID: "fff",
ForwardedFor: "ggg",
Host: "hhh",
}
r := props.Replaces()
assert.Equal(t, "foo", r["code"])
assert.Equal(t, "bar", r["message"])
assert.Equal(t, "baz", r["description"])
assert.Equal(t, "aaa", r["original_uri"])
assert.Equal(t, "bbb", r["namespace"])
assert.Equal(t, "ccc", r["ingress_name"])
assert.Equal(t, "ddd", r["service_name"])
assert.Equal(t, "eee", r["service_port"])
assert.Equal(t, "fff", r["request_id"])
assert.Equal(t, "ggg", r["forwarded_for"])
assert.Equal(t, "hhh", r["host"])
props.Code, props.Message, props.Description = "", "", ""
r = props.Replaces()
assert.Equal(t, "", r["code"])
assert.Equal(t, "", r["message"])
assert.Equal(t, "", r["description"])
}
func TestProperties_Hash(t *testing.T) {
props1 := tpl.Properties{Code: "123"}
props2 := tpl.Properties{Code: "123"}
hash1, err := props1.Hash()
assert.NoError(t, err)
hash2, err := props2.Hash()
assert.NoError(t, err)
assert.Equal(t, hash1, hash2)
props2.Code = "321"
hash2, err = props2.Hash()
assert.NoError(t, err)
assert.NotEqual(t, hash1, hash2)
}

View File

@ -1,220 +0,0 @@
package tpl
import (
"bytes"
"encoding/json"
"os"
"strconv"
"sync"
"text/template"
"time"
"github.com/pkg/errors"
"gh.tarampamp.am/error-pages/internal/version"
)
// These functions are always allowed in the templates.
var tplFnMap = template.FuncMap{ //nolint:gochecknoglobals
"now": time.Now,
"hostname": os.Hostname,
"json": func(v interface{}) string { b, _ := json.Marshal(v); return string(b) }, //nolint:nlreturn
"version": version.Version,
"int": func(v interface{}) int {
if s, ok := v.(string); ok {
if i, err := strconv.Atoi(s); err == nil {
return i
}
} else if i, ok := v.(int); ok {
return i
}
return 0
},
"env": os.Getenv,
}
var ErrClosed = errors.New("closed")
type TemplateRenderer struct {
cacheMu sync.RWMutex
cache map[cacheEntryHash]cacheItem // map key is a unique hash
cacheCleanupInterval time.Duration
cacheItemLifetime time.Duration
close chan struct{}
closedMu sync.RWMutex
closed bool
}
type (
cacheEntryHash = [hashLength * 2]byte // two md5 hashes
cacheItem struct {
data []byte
expiresAtNano int64
}
)
const (
cacheCleanupInterval = time.Second
cacheItemLifetime = time.Second * 2
)
// NewTemplateRenderer returns new template renderer. Don't forget to call Close() function!
func NewTemplateRenderer() *TemplateRenderer {
tr := &TemplateRenderer{
cache: make(map[cacheEntryHash]cacheItem),
cacheCleanupInterval: cacheCleanupInterval,
cacheItemLifetime: cacheItemLifetime,
close: make(chan struct{}, 1),
}
go tr.cleanup()
return tr
}
func (tr *TemplateRenderer) cleanup() {
defer close(tr.close)
timer := time.NewTimer(tr.cacheCleanupInterval)
defer timer.Stop()
for {
select {
case <-tr.close:
tr.cacheMu.Lock()
for hash := range tr.cache {
delete(tr.cache, hash)
}
tr.cacheMu.Unlock()
return
case <-timer.C:
tr.cacheMu.Lock()
var now = time.Now().UnixNano()
for hash, item := range tr.cache {
if now > item.expiresAtNano {
delete(tr.cache, hash)
}
}
tr.cacheMu.Unlock()
timer.Reset(tr.cacheCleanupInterval)
}
}
}
func (tr *TemplateRenderer) Render(content []byte, props Properties) ([]byte, error) { //nolint:funlen
if tr.isClosed() {
return nil, ErrClosed
}
if len(content) == 0 {
return content, nil
}
var (
cacheKey cacheEntryHash
cacheKeyInit bool
)
if propsHash, err := props.Hash(); err == nil {
cacheKeyInit, cacheKey = true, tr.mixHashes(propsHash, HashBytes(content))
tr.cacheMu.RLock()
item, hit := tr.cache[cacheKey]
tr.cacheMu.RUnlock()
if hit {
// cache item has been expired?
if time.Now().UnixNano() > item.expiresAtNano {
tr.cacheMu.Lock()
delete(tr.cache, cacheKey)
tr.cacheMu.Unlock()
} else {
return item.data, nil
}
}
}
var funcMap = template.FuncMap{
"show_details": func() bool { return props.ShowRequestDetails },
"hide_details": func() bool { return !props.ShowRequestDetails },
"l10n_disabled": func() bool { return props.L10nDisabled },
"l10n_enabled": func() bool { return !props.L10nDisabled },
}
// make a copy of template functions map
for s, i := range tplFnMap {
funcMap[s] = i
}
// and allow the direct calling of Properties tokens, e.g. `{{ code | json }}`
for what, with := range props.Replaces() {
var n, s = what, with
funcMap[n] = func() string { return s }
}
t, err := template.New("").Funcs(funcMap).Parse(string(content))
if err != nil {
return nil, err
}
var buf bytes.Buffer
if err = t.Execute(&buf, props); err != nil {
return nil, err
}
b := buf.Bytes()
if cacheKeyInit {
tr.cacheMu.Lock()
tr.cache[cacheKey] = cacheItem{
data: b,
expiresAtNano: time.Now().UnixNano() + tr.cacheItemLifetime.Nanoseconds(),
}
tr.cacheMu.Unlock()
}
return b, nil
}
func (tr *TemplateRenderer) isClosed() (closed bool) {
tr.closedMu.RLock()
closed = tr.closed
tr.closedMu.RUnlock()
return
}
func (tr *TemplateRenderer) Close() error {
if tr.isClosed() {
return ErrClosed
}
tr.closedMu.Lock()
tr.closed = true
tr.closedMu.Unlock()
tr.close <- struct{}{}
return nil
}
func (tr *TemplateRenderer) mixHashes(a, b Hash) (result cacheEntryHash) {
for i := 0; i < len(a); i++ { //nolint:gosimple
result[i] = a[i]
}
for i := 0; i < len(b); i++ {
result[i+len(a)] = b[i]
}
return
}

View File

@ -1,142 +0,0 @@
package tpl_test
import (
"math/rand"
"os"
"strconv"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gh.tarampamp.am/error-pages/internal/tpl"
)
func Test_Render(t *testing.T) {
renderer := tpl.NewTemplateRenderer()
defer func() { _ = renderer.Close() }()
require.NoError(t, os.Setenv("TEST_ENV_VAR", "unit-test"))
defer func() { require.NoError(t, os.Unsetenv("TEST_ENV_VAR")) }()
for name, tt := range map[string]struct {
giveContent string
giveProps tpl.Properties
wantContent string
wantError bool
}{
"common case": {
giveContent: "{{code}}: {{ message }} {{description}}",
giveProps: tpl.Properties{Code: "404", Message: "Not found", Description: "Blah"},
wantContent: "404: Not found Blah",
},
"html markup": {
giveContent: "<!-- comment --><html><body>{{code}}: {{ message }} {{description}}</body></html>",
giveProps: tpl.Properties{Code: "201", Message: "lorem ipsum"},
wantContent: "<!-- comment --><html><body>201: lorem ipsum </body></html>",
},
"with line breakers": {
giveContent: "\t {{code}}: {{ message }} {{description}}\n",
giveProps: tpl.Properties{},
wantContent: "\t : \n",
},
"golang template": {
giveContent: "\t {{code}} {{ .Code }}{{ if .Message }} Yeah {{end}}",
giveProps: tpl.Properties{Code: "201", Message: "lorem ipsum"},
wantContent: "\t 201 201 Yeah ",
},
"wrong golang template": {
giveContent: "{{ if foo() }} Test {{ end }}",
giveProps: tpl.Properties{},
wantError: true,
},
"json common case": {
giveContent: `{"code": {{code | json}}, "message": {"here":[ {{ message | json }} ]}, "desc": "{{description}}"}`,
giveProps: tpl.Properties{Code: `404'"{`, Message: "Not found\t\r\n"},
wantContent: `{"code": "404'\"{", "message": {"here":[ "Not found\t\r\n" ]}, "desc": ""}`,
},
"json golang template": {
giveContent: `{"code": "{{code}}", "message": {"here":[ "{{ if .Message }} Yeah {{end}}" ]}}`,
giveProps: tpl.Properties{Code: "201", Message: "lorem ipsum"},
wantContent: `{"code": "201", "message": {"here":[ " Yeah " ]}}`,
},
"fn l10n_enabled": {
giveContent: "{{ if l10n_enabled }}Y{{ else }}N{{ end }}",
giveProps: tpl.Properties{L10nDisabled: true},
wantContent: "N",
},
"fn l10n_disabled": {
giveContent: "{{ if l10n_disabled }}Y{{ else }}N{{ end }}",
giveProps: tpl.Properties{L10nDisabled: true},
wantContent: "Y",
},
"env": {
giveContent: `{{ env "TEST_ENV_VAR" }}`,
wantContent: "unit-test",
},
} {
t.Run(name, func(t *testing.T) {
content, err := renderer.Render([]byte(tt.giveContent), tt.giveProps)
if tt.wantError == true {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.wantContent, string(content))
}
})
}
}
func TestTemplateRenderer_Render_Concurrent(t *testing.T) {
renderer := tpl.NewTemplateRenderer()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
props := tpl.Properties{
Code: strconv.Itoa(rand.Intn(599-300+1) + 300), //nolint:gosec
Message: "Not found",
Description: "Blah",
}
content, err := renderer.Render([]byte("{{code}}: {{ message }} {{description}}"), props)
assert.NoError(t, err)
assert.NotEmpty(t, content)
}()
}
wg.Wait()
assert.NoError(t, renderer.Close())
assert.EqualError(t, renderer.Close(), tpl.ErrClosed.Error())
content, err := renderer.Render([]byte{}, tpl.Properties{})
assert.Nil(t, content)
assert.EqualError(t, err, tpl.ErrClosed.Error())
}
func BenchmarkRenderHTML(b *testing.B) {
b.ReportAllocs()
renderer := tpl.NewTemplateRenderer()
defer func() { _ = renderer.Close() }()
for i := 0; i < b.N; i++ {
_, _ = renderer.Render(
[]byte("{{code}}: {{ message }} {{description}}"),
tpl.Properties{Code: "404", Message: "Not found", Description: "Blah"},
)
}
}

View File

@ -1,244 +0,0 @@
<!DOCTYPE html>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="robots" content="noindex, nofollow"/>
<title>{{ message }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="preconnect" href="https://fonts.bunny.net" crossorigin>
<link rel="dns-prefetch" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>
:root{--color-bg-primary:#fff;--color-bg-secondary:#eef6fa;--color-bg-sign:#fff;--color-text-primary:#333;--color-text-secondary:#777;--color-img-details:#f62f37;--color-img-primary:#7990a1;--color-img-secondary:#00baff;--font-size-small:13px;--font-size-normal:16px;--font-size-large:45px}
@media (prefers-color-scheme:dark){
:root{--color-bg-primary:#222526;--color-bg-secondary:#292e2f;--color-bg-sign:#262828;--color-text-primary:#fff;--color-text-secondary:#999;--color-img-details:#c72d34;--color-img-primary:#adacac;--color-img-secondary:#86d3ff}
}
body,html{background-color:var(--color-bg-primary);color:var(--color-text-primary);font-family:Roboto,Helvetica,sans-serif;font-size:0;margin:0;padding:0;height:100vh;overflow-x:hidden}
body{align-items:center;display:flex;justify-content:center;height:100vh}
main{width:100%;max-width:1024px;padding:0 40px;display:flex;justify-content:space-between}
.content,.picture{box-sizing:border-box}
.content{display:flex;flex-direction:column;flex-shrink:0;justify-content:space-around;width:45%;z-index:1}
a,p,ul li{font-size:var(--font-size-normal)}
.title{line-height:1.2;font-size:var(--font-size-large);margin:0 0 30px;width:130%}
.subtitle{display:flex;flex-direction:column;justify-content:center;margin:16px 0}
ul{padding:0;list-style:none;line-height:24px}
ul li::before{content:'•';padding-right:7px;color:var(--color-img-secondary)}
/* {{ if show_details }} */
.details{margin:0 0 16px 0}
.details p{font-size:var(--font-size-small)}
.details ul{line-height:0}
.details ul li{padding-top:calc(var(--font-size-small) * 1.5)}
.details ul li:first-child{padding-top:calc(var(--font-size-small) * .6)}
.details code,.details span,.details ul li::before{font-size:var(--font-size-small);font-weight:400}
.details code{padding-left:7px}
/* {{ end }} */
a{text-decoration:none;color:var(--color-img-secondary)}
.hidden{display:none}
.picture{display:flex;align-items:center;justify-content:center;width:55%;user-select:none;z-index:0}
.picture svg{width:100%}
.picture svg .st10,.picture svg .st11,.picture svg .st12,.picture svg .st13,.picture svg .st14,.picture svg .st15,.picture svg .st16,.picture svg .st17,.picture svg .st3,.picture svg .st6,.picture svg .st9{stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10}
.picture svg .st0{fill:var(--color-bg-primary)}
.picture svg .st1{fill:url(#svg-background-gradient)}
.picture svg .st2{fill:var(--color-bg-secondary)}
.picture svg .st3{fill:var(--color-bg-primary);stroke:var(--color-img-primary);stroke-width:3.5}
.picture svg .st4{fill:var(--color-img-secondary)}
.picture svg .st5{fill:none;stroke:var(--color-img-secondary);stroke-width:4;stroke-linejoin:round;stroke-miterlimit:10}
.picture svg .st6{fill:var(--color-bg-primary);stroke:var(--color-img-primary);stroke-width:3}
.picture svg .st7{fill:var(--color-img-primary)}
.picture svg .st8{fill:none;stroke:var(--color-img-primary);stroke-width:2.5;stroke-linecap:round;stroke-miterlimit:10}
.picture svg .st9{fill:none;stroke:var(--color-img-primary);stroke-width:3}
.picture svg .st10{fill:none;stroke:var(--color-img-primary);stroke-width:3.5}
.picture svg .st11{fill:none;stroke:var(--color-img-secondary);stroke-width:4}
.picture svg .st12{fill:var(--color-bg-primary);stroke:var(--color-img-primary);stroke-width:4}
.picture svg .st13{fill:none;stroke:var(--color-img-primary);stroke-width:4}
.picture svg .st14{fill:none;stroke:var(--color-img-secondary);stroke-width:4.5}
.picture svg .st15{fill:none;stroke:var(--color-img-secondary);stroke-width:5}
.picture svg .st16{fill:none;stroke:var(--color-img-primary);stroke-width:5}
.picture svg .st17{fill:var(--color-bg-primary);stroke:var(--color-img-details);stroke-width:3.5}
.picture svg .st19{fill:none;stroke:var(--color-img-details);stroke-width:2.5;stroke-linecap:round;stroke-miterlimit:10}
.picture svg .error-code{font:bold 40px sans-serif;fill:var(--color-img-details)}
@media (max-width:1024px){
:root{--font-size-small:11px;--font-size-normal:14px;--font-size-large:35px}
main{display:block;position:relative;padding-top:40px}
.content,.picture{width:100%}
.content{position:relative;margin:0 auto;z-index:1}
.title{width:100%}
.picture{position:absolute;top:0;left:0;z-index:0;opacity:.2;width:100%;height:100%;padding:0}
.picture svg{max-width:70%}
}
@media (max-width:600px){
.picture svg{max-width:100%}
}
</style>
</head>
<body>
<main>
<div class="content">
<h2 class="title" data-l10n>{{ message }}</h2>
<p data-l10n>{{ description }}</p>
<div class="subtitle if-not-found hidden">
<p><span data-l10n>Here's what might have happened</span>:</p>
<ul>
<li data-l10n>You may have mistyped the URL</li>
<li data-l10n>The site was moved</li>
<li data-l10n>It was never here</li>
</ul>
</div>
<p class="if-maybe-wrong-uri"><span data-l10n>Double-check the URL</span>. <a class="go-back hidden" data-l10n>Alternatively, go back</a></p>
{{ if show_details }}
<div class="details">
<p><span data-l10n>Request details</span>:</p>
<ul>
{{- if host }}<li><span><span data-l10n>Host</span>:</span> <code>{{ host }}</code></li>{{ end -}}
{{- if original_uri }}<li><span><span data-l10n>Original URI</span>:</span> <code>{{ original_uri }}</code></li>{{ end -}}
{{- if forwarded_for }}<li><span><span data-l10n>Forwarded for</span>:</span> <code>{{ forwarded_for }}</code></li>{{ end -}}
{{- if namespace }}<li><span><span data-l10n>Namespace</span>:</span> <code>{{ namespace }}</code></li>{{ end -}}
{{- if ingress_name }}<li><span><span data-l10n>Ingress name</span>:</span> <code>{{ ingress_name }}</code></li>{{ end -}}
{{- if service_name }}<li><span><span data-l10n>Service name</span>:</span> <code>{{ service_name }}</code></li>{{ end -}}
{{- if service_port }}<li><span><span data-l10n>Service port</span>:</span> <code>{{ service_port }}</code></li>{{ end -}}
{{- if request_id }}<li><span><span data-l10n>Request ID</span>:</span> <code>{{ request_id }}</code></li>{{ end -}}
<li><span><span data-l10n>Timestamp</span>:</span> <code>{{ now.Unix }}</code></li>
</ul>
</div>
{{ end }}
</div>
<div class="picture">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 480" x="0px" y="0px" xml:space="preserve">
<rect y="0" class="st0" width="600" height="480"></rect>
<radialgradient id="svg-background-gradient" cx="328.1394" cy="306.3561" r="219.5134" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:var(--color-bg-secondary)"></stop>
<stop offset="0.5002" style="stop-color:var(--color-bg-secondary)"></stop>
<stop offset="1" style="stop-color:var(--color-bg-primary)"></stop>
</radialgradient>
<rect x="95.2" y="35.7" class="st1" width="460" height="271.4"></rect>
<ellipse class="st2" cx="289.7" cy="352.3" rx="69.5" ry="13.9"></ellipse>
<ellipse class="st2" cx="180.5" cy="396.3" rx="51.2" ry="9.5"></ellipse>
<ellipse class="st2" cx="381.3" cy="418.3" rx="40.8" ry="6.4"></ellipse>
<path class="st3" d="M551.1,285.8H527c-2.3,0-4.1-1.8-4.1-4.1v-30c0-2.3,1.8-4.1,4.1-4.1h24.1c2.3,0,4.1,1.8,4.1,4.1v30 C555.2,284,553.4,285.8,551.1,285.8z"></path>
<circle class="st3" cx="539.1" cy="266.7" r="10.3"></circle>
<path class="st4" d="M265.6,343.3c-5,0-9,4-9,9h18C274.6,347.3,270.6,343.3,265.6,343.3z"></path>
<line class="st5" x1="272.7" y1="328.1" x2="272.7" y2="352.3"></line>
<path class="st4" d="M307,343.3c-5,0-9,4-9,9h18C316,347.3,311.9,343.3,307,343.3z"></path>
<line class="st5" x1="314.1" y1="328.1" x2="314.1" y2="352.3"></line>
<path class="st6" d="M380.7,422.6l-37.6-6.4c-1.5-0.3-2.5-1.5-2.2-2.9l4.6-26.8c0.2-1.4,1.6-2.2,3-2l37.6,6.4 c1.5,0.3,2.5,1.5,2.2,2.9l-4.6,26.8C383.6,422,382.2,422.9,380.7,422.6z"></path>
<path class="st6" d="M344.6,391.5l0.8-4.5c0.3-1.7,1.6-2.8,3.1-2.5l37.6,6.4c1.5,0.3,2.4,1.7,2.1,3.4l-0.8,4.5L344.6,391.5z"></path>
<circle class="st7" cx="349" cy="388.4" r="1"></circle>
<circle class="st7" cx="353.1" cy="389.1" r="1"></circle>
<circle class="st7" cx="357.1" cy="389.8" r="1"></circle>
<line class="st8" x1="360.4" y1="402.8" x2="367.4" y2="412.7"></line>
<line class="st8" x1="368.8" y1="404.3" x2="359" y2="411.2"></line>
<path class="st6" d="M166.4,401.4l-36.6-10.8c-1.5-0.4-2.3-1.8-1.9-3.1l7.7-26.1c0.4-1.3,1.8-2,3.3-1.6l36.6,10.8 c1.5,0.4,2.3,1.8,1.9,3.1l-7.7,26.1C169.3,401.1,167.9,401.8,166.4,401.4z"></path>
<path class="st6" d="M134.2,366.2l1.3-4.4c0.5-1.6,2-2.6,3.4-2.1l36.6,10.8c1.5,0.4,2.2,2,1.7,3.6l-1.3,4.4L134.2,366.2z"></path>
<circle class="st7" cx="138.9" cy="363.7" r="1"></circle>
<circle class="st7" cx="142.9" cy="364.8" r="1"></circle>
<circle class="st7" cx="146.9" cy="366" r="1"></circle>
<path class="st6" d="M220.9,399.3l-38-3.9c-1.5-0.2-2.5-1.3-2.4-2.7l2.8-27.1c0.1-1.4,1.4-2.3,2.9-2.2l38,3.9 c1.5,0.2,2.5,1.3,2.4,2.7l-2.8,27.1C223.6,398.5,222.4,399.5,220.9,399.3z"></path>
<path class="st6" d="M188.6,400.9l-38.1,2.8c-1.5,0.1-2.7-0.9-2.8-2.3l-2-27.1c-0.1-1.4,1-2.6,2.5-2.7l38.1-2.8 c1.5-0.1,2.7,0.9,2.8,2.3l2,27.1C191.2,399.6,190.1,400.8,188.6,400.9z"></path>
<path class="st9" d="M146.1,379.4l-0.3-4.5c-0.1-1.7,0.9-3.1,2.4-3.2l38.1-2.8c1.5-0.1,2.8,1.1,2.9,2.8l0.3,4.5L146.1,379.4z"></path>
<circle class="st7" cx="149.6" cy="375.3" r="1"></circle>
<circle class="st7" cx="153.7" cy="375" r="1"></circle>
<circle class="st7" cx="157.8" cy="374.7" r="1"></circle>
<line class="st8" x1="164.1" y1="386.6" x2="173.3" y2="394.4"></line>
<line class="st8" x1="172.7" y1="385.9" x2="164.8" y2="395.1"></line>
<path class="st10" d="M539.1,267.8c0,96.1-51.7,97.6-67.6,98.6c-28.1,1.8-76.3-14.4-63-25.6c13.3-11.2,53.8-10.3,59.3-4.3 c4,4.3,6.1,16.6-49.9,15.8c-29.4-0.4-51-8.4-60.8-32.1"></path>
<path class="st11" d="M184.1,262.5c17.8,9,28.4-2.4,28.4-2.4"></path>
<ellipse class="st0" cx="289.7" cy="170.7" rx="77.1" ry="21.7"></ellipse>
<path class="st12" d="M366.8,308.7c0,12.1-34.5,21.8-77.1,21.8c-42.6,0-77.1-9.8-77.1-21.8V170.7c0,12.1,34.5,21.8,77.1,21.8 c42.6,0,77.1-9.8,77.1-21.8V308.7z"></path>
<path class="st13" d="M212.6,170.7c0-12.1,34.5-21.8,77.1-21.8c42.6,0,77.1,9.8,77.1,21.8"></path>
<path class="st13" d="M366.8,216.7c0,12.1-34.5,21.8-77.1,21.8c-42.6,0-77.1-9.8-77.1-21.8"></path>
<path class="st13" d="M366.8,262.7c0,12.1-34.5,21.8-77.1,21.8c-42.6,0-77.1-9.8-77.1-21.8"></path>
<path class="st11" d="M384.2,279.8c-6.2-18.9-25.1-18.7-25.1-18.7"></path>
<path class="st14" d="M378,288.7c0,0,0-6.3,5.6-8.8c0,0,1.6,0.5,3.3,1.3"></path>
<path class="st15" d="M384.2,279.8"></path>
<circle class="st4" cx="319" cy="254.8" r="4.2"></circle>
<circle class="st4" cx="257.2" cy="255.4" r="4.2"></circle>
<line class="st16" x1="182.4" y1="284.4" x2="179" y2="229.2"></line>
<polygon class="st17" points="191.3,144 153.6,146.3 128.7,174.8 131,212.7 159.3,238 196.9,235.6 221.8,207.2 219.5,169.2" style="fill:var(--color-bg-sign)"></polygon>
<text class="error-code" x="125" y="220" transform="rotate(-5)">{{ code }}</text>
<line class="st14" x1="183.2" y1="255.9" x2="175.9" y2="258.8"></line>
<line class="st14" x1="184.7" y1="260.4" x2="175.8" y2="263"></line>
<line class="st14" x1="185.4" y1="265.4" x2="176.9" y2="267.2"></line>
<ellipse class="st11" cx="287.7" cy="269" rx="4.4" ry="6.7"></ellipse>
<path class="st6" d="M405.5,316l-37.8,5.5c-1.5,0.2-2.8-0.7-3-2.1l-3.9-26.9c-0.2-1.4,0.8-2.6,2.3-2.8l37.8-5.5 c1.5-0.2,2.8,0.7,3,2.1l3.9,26.9C407.9,314.5,407,315.7,405.5,316z"></path>
<path class="st6" d="M361.5,297.6l-0.7-4.5c-0.2-1.7,0.7-3.1,2.2-3.4l37.8-5.5c1.5-0.2,2.8,0.9,3.1,2.6l0.7,4.5L361.5,297.6z"></path>
<circle class="st7" cx="364.7" cy="293.3" r="1"></circle>
<circle class="st7" cx="368.8" cy="292.7" r="1"></circle>
<circle class="st7" cx="372.9" cy="292.1" r="1"></circle>
<line class="st19" x1="380" y1="303.4" x2="389.7" y2="310.6"></line>
<line class="st19" x1="388.5" y1="302.2" x2="381.3" y2="311.9"></line>
<path class="st6" d="M204.8,355.2l-28.4,25.5c-1.1,1-2.7,1-3.6-0.1l-18.2-20.3c-0.9-1-0.8-2.6,0.3-3.6l28.4-25.5 c1.1-1,2.7-1,3.6,0.1l18.2,20.3C206.1,352.6,205.9,354.2,204.8,355.2z"></path>
<path class="st9" d="M158,364.1l-3-3.4c-1.1-1.3-1.1-3,0-4l28.4-25.5c1.1-1,2.9-0.8,4,0.5l3,3.4L158,364.1z"></path>
<circle class="st7" cx="158.3" cy="358.7" r="1"></circle>
<circle class="st7" cx="161.3" cy="356" r="1"></circle>
<circle class="st7" cx="164.4" cy="353.2" r="1"></circle>
<line class="st8" x1="176.7" y1="358.8" x2="188.7" y2="359.4"></line>
<line class="st8" x1="183" y1="353.1" x2="182.4" y2="365.1"></line>
<path class="st6" d="M219.9,344l14.8,35.2c0.6,1.4,0,2.9-1.2,3.4l-25.1,10.5c-1.3,0.5-2.7-0.1-3.3-1.5l-14.8-35.2 c-0.6-1.4,0-2.9,1.2-3.4l25.1-10.5C217.8,341.9,219.3,342.6,219.9,344z"></path>
<path class="st9" d="M213,391.1l-4.2,1.8c-1.6,0.7-3.2,0.1-3.8-1.3l-14.8-35.2c-0.6-1.4,0.2-3,1.7-3.6l4.2-1.8L213,391.1z"></path>
<circle class="st7" cx="208" cy="389.1" r="1"></circle>
<circle class="st7" cx="206.4" cy="385.3" r="1"></circle>
<circle class="st7" cx="204.8" cy="381.5" r="1"></circle>
<line class="st8" x1="214.1" y1="371.7" x2="218.6" y2="360.6"></line>
<line class="st8" x1="210.8" y1="363.9" x2="221.9" y2="368.4"></line>
<path class="st14" d="M394.1,287.1c-0.7-1.6-3.9-4.5-7.2-5.9"></path>
<path class="st6" d="M419.7,413.7l-37.8,5.2c-1.5,0.2-2.8-0.7-3-2.1l-3.7-27c-0.2-1.4,0.8-2.6,2.3-2.8l37.8-5.2 c1.5-0.2,2.8,0.7,3,2.1l3.7,27C422.2,412.2,421.2,413.5,419.7,413.7z"></path>
<path class="st6" d="M375.9,394.8l-0.6-4.5c-0.2-1.7,0.7-3.1,2.2-3.3l37.8-5.2c1.5-0.2,2.8,0.9,3.1,2.6l0.6,4.5L375.9,394.8z"></path>
<circle class="st7" cx="379.2" cy="390.6" r="1"></circle>
<circle class="st7" cx="383.3" cy="390" r="1"></circle>
<circle class="st7" cx="387.4" cy="389.5" r="1"></circle>
<line class="st8" x1="394.4" y1="400.9" x2="404" y2="408.2"></line>
<line class="st8" x1="402.9" y1="399.7" x2="395.6" y2="409.4"></line>
<polygon class="st17" points="361,62.2 346.5,104.9 364.7,107.8 347.6,141.8 382,99.7 363.5,93.5 385,63.8 "></polygon>
<polygon class="st17" points="396.5,101.6 374.8,122.8 384.1,130.2 363.6,145.4 396.4,130.6 388,121.2 409.5,109.9 "></polygon>
<line class="st14" x1="384.7" y1="281.7" x2="386" y2="290.6"></line>
</svg>
</div>
</main>
<script>
Array.prototype.forEach.call(document.getElementsByClassName('if-not-found'), function ($el) {
$el.style.display = "{{ code }}" === "404" ? 'block' : 'none';
});
Array.prototype.forEach.call(document.getElementsByClassName('if-maybe-wrong-uri'), function ($el) {
$el.style.display = ["401", "403", "404", "418", "505"].includes("{{ code }}") ? 'block' : 'none';
});
Array.prototype.forEach.call(document.getElementsByClassName('go-back'), function ($el) {
if (document.referrer !== '' || history.length > 1) {
$el.setAttribute('href', '#back-to-the-future');
$el.addEventListener('click', function (event) {
history.back();
event.preventDefault();
return false;
}, false);
$el.style.display = 'inline-block'; // show the element
} else {
$el.style.display = 'none'; // hide the element
}
});
// {{ if l10n_enabled }}
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
s.async = s.defer = true;
s.addEventListener('load', () => p.removeChild(s));
p.appendChild(s);
})(document.createElement('script'), document.body);
}
// {{ end }}
</script>
</body>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
</html>

View File

@ -1,123 +0,0 @@
<!DOCTYPE html>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow"/>
<title>{{ message }}</title>
<style>
html, body {
margin: 0;
height: 100vh;
background-color: #000;
color: #ddd;
font-family: 'Open Sans', sans-serif;
font-size: 0;
}
.centered {
height: 100vh;
align-items: center;
display: flex;
justify-content: center
}
.centered img {
max-width: 750px;
width: 100%;
}
/* {{ if show_details }} */
.details table {
width: 100%;
}
.details td {
font-size: 11px;
}
.details td.name {
text-align: right;
padding-right: .6em;
width: 50%;
}
.details td.value {
text-align: left;
padding-left: .6em;
font-family: 'Lucida Console', 'Courier New', monospace;
}
/* {{ end }} */
</style>
</head>
<body>
<div class="centered">
<!-- Pictures provider: <https://http.cat/> -->
<div>
<img src="https://http.cat/{{ code }}.jpg" alt="{{ message }}">
{{ if show_details }}
<div class="details">
<table>
{{- if host }}<tr>
<td class="name" data-l10n>Host</td>
<td class="value">{{ host }}</td>
</tr>{{ end -}}
{{- if original_uri }}<tr>
<td class="name" data-l10n>Original URI</td>
<td class="value">{{ original_uri }}</td>
</tr>{{ end -}}
{{- if forwarded_for }}<tr>
<td class="name" data-l10n>Forwarded for</td>
<td class="value">{{ forwarded_for }}</td>
</tr>{{ end -}}
{{- if namespace }}<tr>
<td class="name" data-l10n>Namespace</td>
<td class="value">{{ namespace }}</td>
</tr>{{ end -}}
{{- if ingress_name }}<tr>
<td class="name" data-l10n>Ingress name</td>
<td class="value">{{ ingress_name }}</td>
</tr>{{ end -}}
{{- if service_name }}<tr>
<td class="name" data-l10n>Service name</td>
<td class="value">{{ service_name }}</td>
</tr>{{ end -}}
{{- if service_port }}<tr>
<td class="name" data-l10n>Service port</td>
<td class="value">{{ service_port }}</td>
</tr>{{ end -}}
{{- if request_id }}<tr>
<td class="name" data-l10n>Request ID</td>
<td class="value">{{ request_id }}</td>
</tr>{{ end -}}
<tr>
<td class="name" data-l10n>Timestamp</td>
<td class="value">{{ now.Unix }}</td>
</tr>
</table>
</div>
{{ end }}
</div>
</div>
<script>
// {{ if l10n_enabled }}
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
s.async = s.defer = true;
s.addEventListener('load', () => p.removeChild(s));
p.appendChild(s);
})(document.createElement('script'), document.body);
}
// {{ end }}
</script>
</body>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
</html>

View File

@ -1,273 +0,0 @@
<!DOCTYPE html>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="robots" content="noindex, nofollow"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title>{{ code }} | {{ message }}</title>
<link rel="preconnect" href="https://fonts.bunny.net" crossorigin>
<link rel="dns-prefetch" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css2?family=Red+Hat+Display:wght@500&family=Fira+Mono&family=Ubuntu&display=swap" rel="stylesheet"/>
<style>
/** Idea author: https://github.com/186526/CloudflareCustomErrorPage */
:root{--color-bg-primary:#fff;--color-text-primary:#000;--color-text-secondary:#575958;--font-size-primary:56px;--font-size-secondary:20px;--ui-card-color-bg:#f2f2f2;--color-text-ok:#137333;--color-bg-ok:#e6f4ea;--color-text-error:#c5221f;--color-bg-error:#fce8e6;--color-text-warning:#b05a00;--color-bg-warning:#fef7e0;--icon-size:48px}
@media (prefers-color-scheme:dark){
:root{--color-bg-primary:#111;--color-text-primary:rgba(255, 255, 255, 0.86);--color-text-secondary:rgba(255, 255, 255, 0.4);--ui-card-color-bg:rgba(40, 40, 40, 0.73);--color-bg-ok:#07220f;--color-bg-error:#270501;--color-bg-warning:#392605}
}
body{margin:2rem 2rem;font-family:'Red Hat Display',Ubuntu,Roboto,'Noto Sans SC',sans-serif;color:var(--color-text-primary);background-color:var(--color-bg-primary)}
header{margin-left:1rem}
header .error-code{font-size:var(--font-size-primary);line-height:var(--font-size-primary);font-family:'Fira Mono',Ubuntu,monospace;font-weight:400;margin:0 0 0 10px}
header .error-description{font-family:Ubuntu,Roboto,'Noto Sans SC',sans-serif;font-size:var(--font-size-secondary);color:var(--color-text-secondary);margin:0 0 0 10px}
code{font-family:'Fira Mono',monospace}
.status{margin-top:2.5rem;display:flex;flex-direction:row;flex-wrap:wrap;justify-content:center;align-items:center}
.card{background-color:var(--ui-card-color-bg);padding:2rem;margin:1rem 1rem;min-height:3rem;border-radius:9px;flex-grow:1}
.arrows svg{fill:var(--color-text-secondary)}
.icon svg{width:var(--icon-size);height:auto;fill:var(--color-text-primary)}
.card.ok{background-color:var(--color-bg-ok)}.card.ok .status-text{color:var(--color-text-ok)}.card.ok svg{fill:var(--color-text-ok)}
.card.error{background-color:var(--color-bg-error)}.card.error .status-text{color:var(--color-text-error)}.card.error svg{fill:var(--color-text-error)}
.card.warning{background-color:var(--color-bg-warning)}.card.warning .status-text{color:var(--color-text-warning)}.card.warning svg{fill:var(--color-text-warning)}
.card main{font-size:calc(var(--font-size-secondary) + .1rem)}
.card .status-text,.reason p{margin:0;font-family:Ubuntu,Roboto,Noto Sans SC,sans-serif}
.reason p{line-height:125%}
a{text-decoration:none;color:#1967d2}
.reason{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:baseline}
.reason>*{display:block;margin:1rem;flex-grow:1;max-width:40%}
.reason h2{font-size:calc(var(--font-size-secondary) + .2rem);margin:0 0 .6em 0;font-weight:550}
footer{margin:1rem;color:var(--color-text-secondary);font-size:0}
/* {{ if show_details }} */
footer .details{margin-top:20px}
footer .details ul{padding:0}
footer .details code,footer .details span{font-size:calc(var(--font-size-secondary) - .6rem)}
footer .details code{padding-left:7px}
/* {{ end }} */
@media screen and (max-width:820px){
.arrows{display:none}
}
@media screen and (max-width:480px){
.reason>*{max-width:100%}
/* {{ if show_details }} */
footer .details code,footer .details span{font-size:calc(var(--font-size-secondary) - .3rem)}
/* {{ end }} */
}
@media screen and (min-width:768px){
body{margin:8% 10%}
header>*{display:inline-block;margin-left:1%}
}
</style>
</head>
<body>
<header>
<h1 class="error-code">{{ code }}</h1>
<p class="error-description">{{ message }}</p>
</header>
<div class="status">
<div class="card warning" id="client-status-card">
<i class="icon">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
<path d="M0 0h24v24H0V0z" fill="none"/>
<path d="M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.89-2-2-2zm0 14H5V8h14v10z"/>
</svg>
</i>
<main data-l10n>Your Client</main>
<p class="status-text" data-l10n>Unknown</p>
</div>
<div class="arrows">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" width="24px" fill="#000000">
<defs>
<symbol id="arrows-horizontal" viewBox="0 0 24 24">
<rect fill="none" height="24" width="24" x="0"/>
<polygon points="7.41,13.41 6,12 2,16 6,20 7.41,18.59 5.83,17 21,17 21,15 5.83,15"/>
<polygon points="16.59,10.59 18,12 22,8 18,4 16.59,5.41 18.17,7 3,7 3,9 18.17,9"/>
</symbol>
</defs>
<use href="#arrows-horizontal"/>
</svg>
</div>
<div class="card ok" id="network-status-card">
<i class="icon">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
<path d="M0 0h24v24H0V0z" fill="none"/>
<path d="M12 6c2.62 0 4.88 1.86 5.39 4.43l.3 1.5 1.53.11c1.56.1 2.78 1.41 2.78 2.96 0 1.65-1.35 3-3 3H6c-2.21 0-4-1.79-4-4 0-2.05 1.53-3.76 3.56-3.97l1.07-.11.5-.95C8.08 7.14 9.94 6 12 6m0-2C9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96C18.67 6.59 15.64 4 12 4z"/>
</svg>
</i>
<main data-l10n>Network</main>
<p class="status-text" data-l10n>Working</p>
</div>
<div class="arrows">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" width="24px" fill="#000000">
<use href="#arrows-horizontal" />
</svg>
</div>
<div class="card warning" id="server-status-card">
<i class="icon">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
<path d="M0 0h24v24H0V0z" fill="none"/>
<path d="M19 15v4H5v-4h14m1-2H4c-.55 0-1 .45-1 1v6c0 .55.45 1 1 1h16c.55 0 1-.45 1-1v-6c0-.55-.45-1-1-1zM7 18.5c-.82 0-1.5-.67-1.5-1.5s.68-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM19 5v4H5V5h14m1-2H4c-.55 0-1 .45-1 1v6c0 .55.45 1 1 1h16c.55 0 1-.45 1-1V4c0-.55-.45-1-1-1zM7 8.5c-.82 0-1.5-.67-1.5-1.5S6.18 5.5 7 5.5s1.5.68 1.5 1.5S7.83 8.5 7 8.5z"/>
</svg>
</i>
<main data-l10n>Web Server</main>
<p class="status-text" data-l10n>Unknown</p>
</div>
</div>
<div class="reason">
<div class="what-happened">
<h2 data-l10n>What happened?</h2>
<p class="description" data-l10n>{{ description }}</p>
</div>
<div class="what-can-i-do">
<h2 data-l10n>What can I do?</h2>
<p class="description" data-l10n>Please try again in a few minutes</p>
</div>
</div>
</body>
<footer>
{{ if show_details }}
<div class="details">
<ul>
{{- if host }}<li><span><span data-l10n>Host</span>:</span> <code>{{ host }}</code></li>{{ end -}}
{{- if original_uri }}<li><span><span data-l10n>Original URI</span>:</span> <code>{{ original_uri }}</code></li>{{ end -}}
{{- if forwarded_for }}<li><span><span data-l10n>Forwarded for</span>:</span> <code>{{ forwarded_for }}</code></li>{{ end -}}
{{- if namespace }}<li><span><span data-l10n>Namespace</span>:</span> <code>{{ namespace }}</code></li>{{ end -}}
{{- if ingress_name }}<li><span><span data-l10n>Ingress name</span>:</span> <code>{{ ingress_name }}</code></li>{{ end -}}
{{- if service_name }}<li><span><span data-l10n>Service name</span>:</span> <code>{{ service_name }}</code></li>{{ end -}}
{{- if service_port }}<li><span><span data-l10n>Service port</span>:</span> <code>{{ service_port }}</code></li>{{ end -}}
{{- if request_id }}<li><span><span data-l10n>Request ID</span>:</span> <code>{{ request_id }}</code></li>{{ end -}}
<li><span><span data-l10n>Timestamp</span>:</span> <code>{{ now.Unix }}</code></li>
</ul>
</div>
{{ end }}
</footer>
<script>
const errorCode = parseInt(`{{ code }}`, 10);
if (typeof errorCode !== 'undefined' && !isNaN(errorCode)) {
/**
* @param {HTMLElement} $card
* @param { {isOk?: boolean, isWarning?: boolean, isError?: boolean} } state
* @param {string} statusText
*/
const setCardState = function ($card, state, statusText) {
const okClass = 'ok', warnClass = 'warning', errClass = 'error',
$statusText = $card.querySelectorAll('.status-text');
switch (true) {
case state.isOk === true:
$card.classList.remove(errClass, warnClass);
$card.classList.add(okClass);
$statusText.forEach(($statusText) => $statusText.innerText = statusText);
break;
case state.isWarning === true:
$card.classList.remove(okClass, errClass);
$card.classList.add(warnClass);
$statusText.forEach(($statusText) => $statusText.innerText = statusText);
break;
case state.isError === true:
$card.classList.remove(okClass, warnClass);
$card.classList.add(errClass);
$statusText.forEach(($statusText) => $statusText.innerText = statusText);
break;
}
};
/**
* @param { {whatHappened?: string, whatToDo?: string} } reasons
*/
const setReasons = function (reasons) {
const descSelector = '.description';
Array.prototype.forEach.call(document.getElementsByClassName('what-happened'), ($el) => {
if (typeof reasons.whatHappened === 'string' && reasons.whatHappened.length > 0) {
Array.prototype.forEach.call($el.querySelectorAll(descSelector), ($desc) => $desc.innerText = reasons.whatHappened);
} else {
$el.remove();
}
});
Array.prototype.forEach.call(document.getElementsByClassName('what-can-i-do'), ($el) => {
if (typeof reasons.whatToDo === 'string' && reasons.whatToDo.length > 0) {
Array.prototype.forEach.call($el.querySelectorAll(descSelector), ($desc) => $desc.innerText = reasons.whatToDo);
} else {
$el.remove();
}
});
};
/**
* @param {string} text
*/
const setErrorDescription = function (text) {
Array.prototype.forEach.call(document.getElementsByClassName('error-description'), function ($el) {
$el.innerHTML = text;
});
};
const message = `{{ message }}`.trim(), cards = {
$client: document.getElementById('client-status-card'),
$network: document.getElementById('network-status-card'),
$server: document.getElementById('server-status-card'),
};
let whatToDo = 'Please try again in a few minutes';
switch (true) {
case errorCode >= 400 && errorCode <= 499:
switch (errorCode) {
case 400: case 405: case 411: case 413: whatToDo = 'Please try to change the request method, headers, payload, or URL'; break;
case 401: case 403: case 407: whatToDo = 'Please check your authorization data'; break;
case 404: whatToDo = 'Please double-check the URL and try again'; break;
case 409: case 410: case 418: whatToDo = '¯\\_(ツ)_/¯'; break;
}
setErrorDescription(`<span data-l10n>${message}</span> (<span data-l10n>client-side error</span>)`);
setCardState(cards.$client, {isError: true}, message)
setCardState(cards.$network, {isOk: true}, 'Working')
setCardState(cards.$server, {isOk: true}, 'Working')
break;
case errorCode >= 500 && errorCode <= 599:
setErrorDescription(`<span data-l10n>${message}</span> (<span data-l10n>server-side error</span>)`);
setCardState(cards.$client, {isOk: true}, 'Working')
setCardState(cards.$network, {isOk: true}, 'Working')
setCardState(cards.$server, {isError: true}, message)
break;
default:
setErrorDescription(message);
setCardState(cards.$client, {isWarning: true}, 'Unknown')
setCardState(cards.$network, {isOk: true}, 'Working')
setCardState(cards.$server, {isWarning: true}, 'Unknown')
break;
}
setReasons({whatHappened: `{{ description }}`.trim(), whatToDo: whatToDo.trim()});
} else {
console.warn('Cannot parse the error code:', errorCode);
}
// {{ if l10n_enabled }}
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
s.async = s.defer = true;
s.addEventListener('load', () => p.removeChild(s));
p.appendChild(s);
})(document.createElement('script'), document.body);
}
// {{ end }}
</script>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
</html>

View File

@ -1,118 +0,0 @@
<!DOCTYPE html>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="robots" content="noindex, nofollow" />
<title>{{ code }}: {{ message }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<link rel="preconnect" href="https://fonts.bunny.net" crossorigin>
<link rel="dns-prefetch" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css2?family=Open+Sans:wght@400;700&display=swap" rel="stylesheet">
<style>
html,body {background-color:#1a1a1a;color:#fff;font-family:'Open Sans',sans-serif;height:100vh;margin:0;font-size:0}
.container {height:100vh;align-items:center;display:flex;justify-content:center;position:relative}
.wrap {text-align:center}
.ghost {animation:float 3s ease-out infinite}
@keyframes float { 50% {transform:translate(0,20px)}}
.shadowFrame {width:130px;margin: 10px auto 0 auto}
.shadow {animation:shrink 3s ease-out infinite;transform-origin:center center}
@keyframes shrink {0%{width:90%;margin:0 5%} 50% {width:60%;margin:0 18%} 100% {width:90%;margin:0 5%}}
h3 {font-size:17px;text-transform: uppercase;margin:0.3em auto}
.description {font-size:13px;color:#aaa}
/* {{ if show_details }} */
.details {color:#999;width:100%}
.details table {width:100%}
.details td {white-space:nowrap;font-size:11px}
.details .name {text-align:right;padding-right:.6em;width:50%}
.details .value {text-align:left;padding-left:.6em;font-family:'Lucida Console','Courier New',monospace}
/* {{ end }} */
</style>
</head>
<body>
<div class="container">
<div class="wrap">
<svg class="ghost" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="127.433px" height="132.743px" viewBox="0 0 127.433 132.743" xml:space="preserve">
<path fill="#FFF6F4" d="M116.223,125.064c1.032-1.183,1.323-2.73,1.391-3.747V54.76c0,0-4.625-34.875-36.125-44.375 s-66,6.625-72.125,44l-0.781,63.219c0.062,4.197,1.105,6.177,1.808,7.006c1.94,1.811,5.408,3.465,10.099-0.6 c7.5-6.5,8.375-10,12.75-6.875s5.875,9.75,13.625,9.25s12.75-9,13.75-9.625s4.375-1.875,7,1.25s5.375,8.25,12.875,7.875 s12.625-8.375,12.625-8.375s2.25-3.875,7.25,0.375s7.625,9.75,14.375,8.125C114.739,126.01,115.412,125.902,116.223,125.064z"></path>
<circle fill="#1a1a1a" cx="86.238" cy="57.885" r="6.667"></circle>
<circle fill="#1a1a1a" cx="40.072" cy="57.885" r="6.667"></circle>
<path fill="#1a1a1a" d="M71.916,62.782c0.05-1.108-0.809-2.046-1.917-2.095c-0.673-0.03-1.28,0.279-1.667,0.771 c-0.758,0.766-2.483,2.235-4.696,2.358c-1.696,0.094-3.438-0.625-5.191-2.137c-0.003-0.003-0.007-0.006-0.011-0.009l0.002,0.005 c-0.332-0.294-0.757-0.488-1.235-0.509c-1.108-0.049-2.046,0.809-2.095,1.917c-0.032,0.724,0.327,1.37,0.887,1.749 c-0.001,0-0.002-0.001-0.003-0.001c2.221,1.871,4.536,2.88,6.912,2.986c0.333,0.014,0.67,0.012,1.007-0.01 c3.163-0.191,5.572-1.942,6.888-3.166l0.452-0.453c0.021-0.019,0.04-0.041,0.06-0.061l0.034-0.034 c-0.007,0.007-0.015,0.014-0.021,0.02C71.666,63.771,71.892,63.307,71.916,62.782z"></path>
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="18.614" cy="99.426" r="3.292"></circle>
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="95.364" cy="28.676" r="3.291"></circle>
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="24.739" cy="93.551" r="2.667"></circle>
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="101.489" cy="33.051" r="2.666"></circle>
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="18.738" cy="87.717" r="2.833"></circle>
<path fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" d="M116.279,55.814c-0.021-0.286-2.323-28.744-30.221-41.012 c-7.806-3.433-15.777-5.173-23.691-5.173c-16.889,0-30.283,7.783-37.187,15.067c-9.229,9.736-13.84,26.712-14.191,30.259 l-0.748,62.332c0.149,2.133,1.389,6.167,5.019,6.167c1.891,0,4.074-1.083,6.672-3.311c4.96-4.251,7.424-6.295,9.226-6.295 c1.339,0,2.712,1.213,5.102,3.762c4.121,4.396,7.461,6.355,10.833,6.355c2.713,0,5.311-1.296,7.942-3.962 c3.104-3.145,5.701-5.239,8.285-5.239c2.116,0,4.441,1.421,7.317,4.473c2.638,2.8,5.674,4.219,9.022,4.219 c4.835,0,8.991-2.959,11.27-5.728l0.086-0.104c1.809-2.2,3.237-3.938,5.312-3.938c2.208,0,5.271,1.942,9.359,5.936 c0.54,0.743,3.552,4.674,6.86,4.674c1.37,0,2.559-0.65,3.531-1.932l0.203-0.268L116.279,55.814z M114.281,121.405 c-0.526,0.599-1.096,0.891-1.734,0.891c-2.053,0-4.51-2.82-5.283-3.907l-0.116-0.136c-4.638-4.541-7.975-6.566-10.82-6.566 c-3.021,0-4.884,2.267-6.857,4.667l-0.086,0.104c-1.896,2.307-5.582,4.999-9.725,4.999c-2.775,0-5.322-1.208-7.567-3.59 c-3.325-3.528-6.03-5.102-8.772-5.102c-3.278,0-6.251,2.332-9.708,5.835c-2.236,2.265-4.368,3.366-6.518,3.366 c-2.772,0-5.664-1.765-9.374-5.723c-2.488-2.654-4.29-4.395-6.561-4.395c-2.515,0-5.045,2.077-10.527,6.777 c-2.727,2.337-4.426,2.828-5.37,2.828c-2.662,0-3.017-4.225-3.021-4.225l0.745-62.163c0.332-3.321,4.767-19.625,13.647-28.995 c3.893-4.106,10.387-8.632,18.602-11.504c-0.458,0.503-0.744,1.165-0.744,1.898c0,1.565,1.269,2.833,2.833,2.833 c1.564,0,2.833-1.269,2.833-2.833c0-1.355-0.954-2.485-2.226-2.764c4.419-1.285,9.269-2.074,14.437-2.074 c7.636,0,15.336,1.684,22.887,5.004c26.766,11.771,29.011,39.047,29.027,39.251V121.405z"></path>
</svg>
<p class="shadowFrame">
<svg class="shadow" xmlns="http://www.w3.org/2000/svg" x="61px" y="20px" width="122.436px" height="39.744px" viewBox="0 0 122.436 39.744" xml:space="preserve">
<ellipse fill="#262626" cx="61.128" cy="19.872" rx="49.25" ry="8.916"></ellipse>
</svg>
</p>
<h3><span data-l10n>Error</span> {{ code }}</h3>
<p class="description" data-l10n>{{ description }}</p>
{{ if show_details }}
<div class="details">
<table>
{{- if host }}<tr>
<td class="name" data-l10n>Host</td>
<td class="value">{{ host }}</td>
</tr>{{ end -}}
{{- if original_uri }}<tr>
<td class="name" data-l10n>Original URI</td>
<td class="value">{{ original_uri }}</td>
</tr>{{ end -}}
{{- if forwarded_for }}<tr>
<td class="name" data-l10n>Forwarded for</td>
<td class="value">{{ forwarded_for }}</td>
</tr>{{ end -}}
{{- if namespace }}<tr>
<td class="name" data-l10n>Namespace</td>
<td class="value">{{ namespace }}</td>
</tr>{{ end -}}
{{- if ingress_name }}<tr>
<td class="name" data-l10n>Ingress name</td>
<td class="value">{{ ingress_name }}</td>
</tr>{{ end -}}
{{- if service_name }}<tr>
<td class="name" data-l10n>Service name</td>
<td class="value">{{ service_name }}</td>
</tr>{{ end -}}
{{- if service_port }}<tr>
<td class="name" data-l10n>Service port</td>
<td class="value">{{ service_port }}</td>
</tr>{{ end -}}
{{- if request_id }}<tr>
<td class="name" data-l10n>Request ID</td>
<td class="value">{{ request_id }}</td>
</tr>{{ end -}}
<tr>
<td class="name" data-l10n>Timestamp</td>
<td class="value">{{ now.Unix }}</td>
</tr>
</table>
</div>
{{ end }}
</div>
</div>
<script>
// {{ if l10n_enabled }}
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
s.async = s.defer = true;
s.addEventListener('load', () => p.removeChild(s));
p.appendChild(s);
})(document.createElement('script'), document.body);
}
// {{ end }}
</script>
</body>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
</html>

View File

@ -1,183 +0,0 @@
<!DOCTYPE html>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow" />
<title>{{ message }}</title>
<link rel="preconnect" href="https://fonts.bunny.net" crossorigin>
<link rel="dns-prefetch" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css2?family=Inconsolata:wght@400;700&display=swap" rel="stylesheet">
<style>
/** Idea author: https://codepen.io/robinselmer */
html, body {
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
font-size: 0;
}
body {
box-sizing: border-box;
background-color: #000000;
background-image: radial-gradient(#11581E, #041607);
background-repeat: no-repeat;
background-size: cover;
font-family: 'Inconsolata', Helvetica, sans-serif;
color: rgba(128, 255, 128, 0.8);
text-shadow:
0 0 11px rgba(51, 255, 51, 1),
0 0 2px rgba(255, 255, 255, 0.8);
}
.overlay {
pointer-events: none;
position: absolute;
width: 100%;
height: 100%;
background:
repeating-linear-gradient(
180deg,
rgba(0, 0, 0, 0) 0,
rgba(0, 0, 0, 0.3) 50%,
rgba(0, 0, 0, 0) 100%);
background-size: auto 4px;
z-index: 1;
}
.overlay::before {
content: "";
pointer-events: none;
position: absolute;
display: block;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
background-image: linear-gradient(
0deg,
transparent 0%,
rgba(32, 128, 32, 0.2) 2%,
rgba(32, 128, 32, 0.8) 3%,
rgba(32, 128, 32, 0.2) 3%,
transparent 100%);
background-repeat: no-repeat;
animation: scan 7.5s linear 0s infinite;
}
@keyframes scan {
0% { background-position: 0 -100vh; }
35%, 100% { background-position: 0 100vh; }
}
.terminal {
box-sizing: inherit;
position: absolute;
height: 100%;
width: 1000px;
max-width: 100%;
padding: 64px;
text-transform: uppercase;
}
h1 {
font-size: 48px;
}
p {
font-size: 24px;
}
.output {
color: rgba(128, 255, 128, 0.8);
text-shadow:
0 0 1px rgba(51, 255, 51, 0.4),
0 0 2px rgba(255, 255, 255, 0.8);
}
.output::before {
content: "> ";
}
a {
color: #fff;
text-decoration: none;
}
a::before {
content: "[";
}
a::after {
content: "]";
}
.error_code {
color: white;
}
/* {{ if show_details }} */
.details p {
margin-top: .5em;
margin-bottom: .5em;
}
.details * {
font-size: 15px;
}
.details p::before {
content: "$ ";
}
.details code {
font-size: 0.9em;
}
/* {{ end }} */
</style>
</head>
<body>
<div class="overlay"></div>
<div class="terminal">
<h1><span data-l10n>Error</span> <span class="error_code">{{ code }}</span></h1>
<p class="output" data-l10n>{{ description }}.</p>
<p class="output"><span data-l10n>Good luck</span>.</p>
{{ if show_details }}
<div class="details">
{{- if host }}<p class="output small"><span data-l10n>Host</span>: <code>{{ host }}</code></p>{{ end -}}
{{- if original_uri }}<p class="output small"><span data-l10n>Original URI</span>: <code>{{ original_uri }}</code></p>{{ end -}}
{{- if forwarded_for }}<p class="output small"><span data-l10n>Forwarded for</span>: <code>{{ forwarded_for }}</code></p>{{ end -}}
{{- if namespace }}<p class="output small"><span data-l10n>Namespace</span>: <code>{{ namespace }}</code></p>{{ end -}}
{{- if ingress_name }}<p class="output small"><span data-l10n>Ingress name</span>: <code>{{ ingress_name }}</code></p>{{ end -}}
{{- if service_name }}<p class="output small"><span data-l10n>Service name</span>: <code>{{ service_name }}</code></p>{{ end -}}
{{- if service_port }}<p class="output small"><span data-l10n>Service port</span>: <code>{{ service_port }}</code></p>{{ 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>{{ now.Unix }}</code></p>
</div>
{{ end }}
</div>
<script>
// {{ if l10n_enabled }}
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
s.async = s.defer = true;
s.addEventListener('load', () => p.removeChild(s));
p.appendChild(s);
})(document.createElement('script'), document.body);
}
// {{ end }}
</script>
</body>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
</html>

View File

@ -1,102 +0,0 @@
<!DOCTYPE html>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow" />
<title>{{ message }}</title>
<link rel="preconnect" href="https://fonts.bunny.net" crossorigin>
<link rel="dns-prefetch" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=Nunito" rel="stylesheet">
<style>
html,body {background-color:#222526;color:#fff;font-family:'Nunito',sans-serif;font-weight:100;height:100vh;margin:0;font-size:0}
.full-height {height:100vh}
.flex-center {align-items:center;display:flex;justify-content:center}
.position-ref {position:relative}
.code {border-right:2px solid;font-size:26px;padding:0 10px 0 15px;text-align:center}
.message {font-size:18px;text-align:center;padding:10px}
/* {{ if show_details }} */
.details table {width:100%;border-collapse:collapse;box-sizing:border-box;margin-top:20px}
.details td {font-size:11px;color:#999}
.details td.name {text-align:right;padding-right:.6em;width:50%;border-right:2px solid;border-color:#777}
.details td.value {text-align:left;padding-left:.6em;font-family:'Lucida Console','Courier New',monospace}
/* {{ end }} */
</style>
</head>
<body>
<div class="flex-center position-ref full-height">
<div>
<div class="flex-center">
<div class="code">
{{ code }}
</div>
<div class="message" data-l10n>
{{ message }}
</div>
</div>
{{ if show_details }}
<div class="details">
<table>
{{- if host }}<tr>
<td class="name" data-l10n>Host</td>
<td class="value">{{ host }}</td>
</tr>{{ end -}}
{{- if original_uri }}<tr>
<td class="name" data-l10n>Original URI</td>
<td class="value">{{ original_uri }}</td>
</tr>{{ end -}}
{{- if forwarded_for }}<tr>
<td class="name" data-l10n>Forwarded for</td>
<td class="value">{{ forwarded_for }}</td>
</tr>{{ end -}}
{{- if namespace }}<tr>
<td class="name" data-l10n>Namespace</td>
<td class="value">{{ namespace }}</td>
</tr>{{ end -}}
{{- if ingress_name }}<tr>
<td class="name" data-l10n>Ingress name</td>
<td class="value">{{ ingress_name }}</td>
</tr>{{ end -}}
{{- if service_name }}<tr>
<td class="name" data-l10n>Service name</td>
<td class="value">{{ service_name }}</td>
</tr>{{ end -}}
{{- if service_port }}<tr>
<td class="name" data-l10n>Service port</td>
<td class="value">{{ service_port }}</td>
</tr>{{ end -}}
{{- if request_id }}<tr>
<td class="name" data-l10n>Request ID</td>
<td class="value">{{ request_id }}</td>
</tr>{{ end -}}
<tr>
<td class="name" data-l10n>Timestamp</td>
<td class="value">{{ now.Unix }}</td>
</tr>
</table>
</div>
{{ end }}
</div>
</div>
<script>
// {{ if l10n_enabled }}
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
s.async = s.defer = true;
s.addEventListener('load', () => p.removeChild(s));
p.appendChild(s);
})(document.createElement('script'), document.body);
}
// {{ end }}
</script>
</body>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
</html>

View File

@ -1,104 +0,0 @@
<!DOCTYPE html>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow" />
<title>{{ message }}</title>
<link rel="preconnect" href="https://fonts.bunny.net" crossorigin>
<link rel="dns-prefetch" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=Nunito" rel="stylesheet">
<style>
:root {--color-bg-primary:#fff;--color-text-primary:#636b6f;--color-text-secondary:#777}
@media (prefers-color-scheme: dark) {:root {--color-bg-primary:#222526;--color-text-primary:#fff;--color-text-secondary:#999}}
html,body {background-color:var(--color-bg-primary);color:var(--color-text-primary);font-family:'Nunito',sans-serif;font-weight:100;height:100vh;margin:0;font-size:0}
.full-height {height:100vh}
.flex-center {align-items:center;display:flex;justify-content:center}
.position-ref {position:relative}
.code {border-right:2px solid;font-size:26px;padding:0 10px 0 15px;text-align:center}
.message {font-size:18px;text-align:center;padding:10px}
/* {{ if show_details }} */
.details table {width:100%;border-collapse:collapse;box-sizing:border-box;margin-top:20px}
.details td {font-size:11px;color:var(--color-text-secondary)}
.details td.name {text-align:right;padding-right:.6em;width:50%;border-right:2px solid;border-color:#aaa}
.details td.value {text-align:left;padding-left:.6em;font-family:'Lucida Console','Courier New',monospace}
/* {{ end }} */
</style>
</head>
<body>
<div class="flex-center position-ref full-height">
<div>
<div class="flex-center">
<div class="code">
{{ code }}
</div>
<div class="message" data-l10n>
{{ message }}
</div>
</div>
{{ if show_details }}
<div class="details">
<table>
{{- if host }}<tr>
<td class="name" data-l10n>Host</td>
<td class="value">{{ host }}</td>
</tr>{{ end -}}
{{- if original_uri }}<tr>
<td class="name" data-l10n>Original URI</td>
<td class="value">{{ original_uri }}</td>
</tr>{{ end -}}
{{- if forwarded_for }}<tr>
<td class="name" data-l10n>Forwarded for</td>
<td class="value">{{ forwarded_for }}</td>
</tr>{{ end -}}
{{- if namespace }}<tr>
<td class="name" data-l10n>Namespace</td>
<td class="value">{{ namespace }}</td>
</tr>{{ end -}}
{{- if ingress_name }}<tr>
<td class="name" data-l10n>Ingress name</td>
<td class="value">{{ ingress_name }}</td>
</tr>{{ end -}}
{{- if service_name }}<tr>
<td class="name" data-l10n>Service name</td>
<td class="value">{{ service_name }}</td>
</tr>{{ end -}}
{{- if service_port }}<tr>
<td class="name" data-l10n>Service port</td>
<td class="value">{{ service_port }}</td>
</tr>{{ end -}}
{{- if request_id }}<tr>
<td class="name" data-l10n>Request ID</td>
<td class="value">{{ request_id }}</td>
</tr>{{ end -}}
<tr>
<td class="name" data-l10n>Timestamp</td>
<td class="value">{{ now.Unix }}</td>
</tr>
</table>
</div>
{{ end }}
</div>
</div>
<script>
// {{ if l10n_enabled }}
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
s.async = s.defer = true;
s.addEventListener('load', () => p.removeChild(s));
p.appendChild(s);
})(document.createElement('script'), document.body);
}
// {{ end }}
</script>
</body>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
</html>

View File

@ -1,399 +0,0 @@
<!DOCTYPE html>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow"/>
<title>{{ message }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="preconnect" href="https://fonts.bunny.net" crossorigin>
<link rel="dns-prefetch" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=Nunito+Sans" rel="stylesheet">
<style>
/** Codepen: https://codepen.io/kdbkapsere/pen/oNXLbqQ */
:root {
--color-bg-primary: #fff;
--color-text-primary: #0e0620;
--color-ui-bg-primary: #0e0620;
--color-ui-bg-inverted: #fff;
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg-primary: #212121;
--color-text-primary: #fafafa;
--color-ui-bg-primary: #fafafa;
--color-ui-bg-inverted: #212121;
}
}
html,
body {
height: 100%;
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
font-family: 'Nunito Sans', sans-serif;
margin: 0;
padding: 0;
font-size: 0;
}
body {
align-items: center;
display: flex;
justify-content: center;
height: 100vh;
}
main {
width: 100%;
max-width: 1140px;
display: flex;
justify-content: space-between;
}
.picture, .content {
box-sizing: border-box;
width: 50%;
}
.content {
padding: 0 40px;
}
svg .dark {
stroke: var(--color-ui-bg-primary);
}
svg .fill-dark {
fill: var(--color-ui-bg-primary);
}
svg .fill-light {
fill: var(--color-ui-bg-inverted);
}
h1 {
font-size: 150px;
margin: 15px 0;
font-weight: bold;
}
h2 {
font-size: 32px;
font-weight: bold;
}
p {
font-size: 16px;
}
/* {{ if show_details }} */
.details {
list-style: none;
padding-left: 0;
opacity: .7;
}
.details li span {
font-size: 14px;
font-weight: bold;
}
.details li code {
font-size: 14px;
font-weight: normal;
padding-left: 7px;
}
/* {{ end }} */
@media screen and (max-width: 768px) {
main {
display: block;
}
.picture, .content {
width: 100%;
text-align: center;
}
.content {
padding: 0 20px;
}
.picture svg {
max-width: 60%;
}
}
</style>
</head>
<body>
<main>
<div class="picture">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600">
<g>
<defs>
<clipPath id="GlassClip">
<path
d="M380.857,346.164c-1.247,4.651-4.668,8.421-9.196,10.06c-9.332,3.377-26.2,7.817-42.301,3.5 s-28.485-16.599-34.877-24.192c-3.101-3.684-4.177-8.66-2.93-13.311l7.453-27.798c0.756-2.82,3.181-4.868,6.088-5.13 c6.755-0.61,20.546-0.608,41.785,5.087s33.181,12.591,38.725,16.498c2.387,1.682,3.461,4.668,2.705,7.488L380.857,346.164z"/>
</clipPath>
<clipPath id="cordClip">
<rect width="800" height="600"/>
</clipPath>
</defs>
<g id="planet">
<circle fill="none" stroke-width="3" stroke-miterlimit="10" cx="572.859" cy="108.803"
r="90.788" class="dark" />
<circle id="craterBig" fill="none" stroke-width="3" stroke-miterlimit="10" cx="548.891"
cy="62.319" r="13.074" class="dark"/>
<circle id="craterSmall" fill="none" stroke-width="3" stroke-miterlimit="10" cx="591.743"
cy="158.918" r="7.989" class="dark"/>
<path id="ring" fill="none" stroke-width="3" stroke-linecap="round" stroke-miterlimit="10"
class="dark"
d="M476.562,101.461c-30.404,2.164-49.691,4.221-49.691,8.007c0,6.853,63.166,12.408,141.085,12.408s141.085-5.555,141.085-12.408c0-3.378-15.347-4.988-40.243-7.225"/>
<path id="ringShadow" opacity="0.5" fill="none" class="dark" stroke-width="3"
stroke-linecap="round" stroke-miterlimit="10"
d="M483.985,127.43c23.462,1.531,52.515,2.436,83.972,2.436c36.069,0,68.978-1.19,93.922-3.149"/>
</g>
<g id="stars">
<g id="starsBig">
<g>
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" x1="518.07" y1="245.375" x2="518.07" y2="266.581"/>
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" x1="508.129" y1="255.978" x2="528.01" y2="255.978"/>
</g>
<g>
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" x1="154.55" y1="231.391" x2="154.55" y2="252.598"/>
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" x1="144.609" y1="241.995" x2="164.49" y2="241.995"/>
</g>
<g>
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" x1="320.135" y1="132.746" x2="320.135" y2="153.952"/>
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" x1="310.194" y1="143.349" x2="330.075" y2="143.349"/>
</g>
<g>
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" x1="200.67" y1="483.11" x2="200.67" y2="504.316"/>
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" x1="210.611" y1="493.713" x2="190.73" y2="493.713"/>
</g>
</g>
<g id="starsSmall">
<g>
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" x1="432.173" y1="380.52" x2="432.173" y2="391.83"/>
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" x1="426.871" y1="386.175" x2="437.474" y2="386.175"/>
</g>
<g>
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" x1="489.555" y1="299.765" x2="489.555" y2="308.124"/>
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" x1="485.636" y1="303.945" x2="493.473" y2="303.945"/>
</g>
<g>
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" x1="231.468" y1="291.009" x2="231.468" y2="299.369"/>
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" x1="227.55" y1="295.189" x2="235.387" y2="295.189"/>
</g>
<g>
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" x1="244.032" y1="547.539" x2="244.032" y2="555.898"/>
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" x1="247.95" y1="551.719" x2="240.113" y2="551.719"/>
</g>
<g>
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" x1="186.359" y1="406.967" x2="186.359" y2="415.326"/>
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" x1="190.277" y1="411.146" x2="182.44" y2="411.146"/>
</g>
<g>
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" x1="480.296" y1="406.967" x2="480.296" y2="415.326"/>
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" x1="484.215" y1="411.146" x2="476.378" y2="411.146"/>
</g>
</g>
<g id="circlesBig">
<circle fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" cx="588.977" cy="255.978" r="7.952"/>
<circle fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" cx="450.066" cy="320.259" r="7.952"/>
<circle fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" cx="168.303" cy="353.753" r="7.952"/>
<circle fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" cx="429.522" cy="201.185" r="7.952"/>
<circle fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" cx="200.67" cy="176.313" r="7.952"/>
<circle fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" cx="133.343" cy="477.014" r="7.952"/>
<circle fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" cx="283.521" cy="568.033" r="7.952"/>
<circle fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-miterlimit="10" cx="413.618" cy="482.387" r="7.952"/>
</g>
<g id="circlesSmall">
<circle class="fill-dark" cx="549.879" cy="296.402" r="2.651"/>
<circle class="fill-dark" cx="253.29" cy="229.24" r="2.651"/>
<circle class="fill-dark" cx="434.824" cy="263.931" r="2.651"/>
<circle class="fill-dark" cx="183.708" cy="544.176" r="2.651"/>
<circle class="fill-dark" cx="382.515" cy="530.923" r="2.651"/>
<circle class="fill-dark" cx="130.693" cy="305.608" r="2.651"/>
<circle class="fill-dark" cx="480.296" cy="477.014" r="2.651"/>
</g>
</g>
<g id="spaceman" clip-path="url(cordClip)">
<path id="cord" fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round" stroke-miterlimit="10"
d="M273.813,410.969c0,0-54.527,39.501-115.34,38.218c-2.28-0.048-4.926-0.241-7.841-0.548c-68.038-7.178-134.288-43.963-167.33-103.87c-0.908-1.646-1.793-3.3-2.654-4.964c-18.395-35.511-37.259-83.385-32.075-118.817"/>
<path id="backpack" class="dark fill-light" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round" stroke-miterlimit="10"
d="M338.164,454.689l-64.726-17.353c-11.086-2.972-17.664-14.369-14.692-25.455l15.694-58.537c3.889-14.504,18.799-23.11,33.303-19.221l52.349,14.035c14.504,3.889,23.11,18.799,19.221,33.303l-15.694,58.537C360.647,451.083,349.251,457.661,338.164,454.689z"/>
<g id="antenna">
<line class="dark fill-light" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round" stroke-miterlimit="10" x1="323.396" y1="236.625"
x2="295.285" y2="353.753"/>
<circle class="dark fill-light" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round" stroke-miterlimit="10" cx="323.666" cy="235.617"
r="6.375"/>
</g>
<g id="armR">
<path class="dark fill-light" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round" stroke-miterlimit="10"
d="M360.633,363.039c1.352,1.061,4.91,5.056,5.824,6.634l27.874,47.634c3.855,6.649,1.59,15.164-5.059,19.02l0,0c-6.649,3.855-15.164,1.59-19.02-5.059l-5.603-9.663"/>
<path class="dark fill-light" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round" stroke-miterlimit="10"
d="M388.762,434.677c5.234-3.039,7.731-8.966,6.678-14.594c2.344,1.343,4.383,3.289,5.837,5.793c4.411,7.596,1.829,17.33-5.767,21.741c-7.596,4.411-17.33,1.829-21.741-5.767c-1.754-3.021-2.817-5.818-2.484-9.046C375.625,437.355,383.087,437.973,388.762,434.677z"/>
</g>
<g id="armL">
<path class="dark fill-light" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round" stroke-miterlimit="10"
d="M301.301,347.66c-1.702,0.242-5.91,1.627-7.492,2.536l-47.965,27.301c-6.664,3.829-8.963,12.335-5.134,18.999h0c3.829,6.664,12.335,8.963,18.999,5.134l9.685-5.564"/>
<path class="dark fill-light" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round" stroke-miterlimit="10"
d="M241.978,395.324c-3.012-5.25-2.209-11.631,1.518-15.977c-2.701-0.009-5.44,0.656-7.952,2.096c-7.619,4.371-10.253,14.09-5.883,21.71c4.371,7.619,14.09,10.253,21.709,5.883c3.03-1.738,5.35-3.628,6.676-6.59C252.013,404.214,245.243,401.017,241.978,395.324z"/>
</g>
<g id="body">
<path class="dark fill-light" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round" stroke-miterlimit="10"
d="M353.351,365.387c-7.948,1.263-16.249,0.929-24.48-1.278c-8.232-2.207-15.586-6.07-21.836-11.14c-17.004,4.207-31.269,17.289-36.128,35.411l-1.374,5.123c-7.112,26.525,8.617,53.791,35.13,60.899l0,0c26.513,7.108,53.771-8.632,60.883-35.158l1.374-5.123C371.778,395.999,365.971,377.536,353.351,365.387z"/>
<path fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round" stroke-miterlimit="10"
d="M269.678,394.912L269.678,394.912c26.3,20.643,59.654,29.585,93.106,25.724l2.419-0.114"/>
</g>
<g id="legs">
<g id="legR">
<path class="dark fill-light" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round" stroke-miterlimit="10"
d="M312.957,456.734l-14.315,53.395c-1.896,7.07,2.299,14.338,9.37,16.234l0,0c7.07,1.896,14.338-2.299,16.234-9.37l17.838-66.534C333.451,455.886,323.526,457.387,312.957,456.734z"/>
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round" stroke-miterlimit="10" x1="304.883" y1="486.849"
x2="330.487" y2="493.713"/>
</g>
<g id="legL">
<path class="dark fill-light" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round" stroke-miterlimit="10"
d="M296.315,452.273L282,505.667c-1.896,7.07-9.164,11.265-16.234,9.37l0,0c-7.07-1.896-11.265-9.164-9.37-16.234l17.838-66.534C278.993,441.286,286.836,447.55,296.315,452.273z"/>
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round" stroke-miterlimit="10" x1="262.638" y1="475.522"
x2="288.241" y2="482.387"/>
</g>
</g>
<g id="head">
<ellipse transform="matrix(0.259 -0.9659 0.9659 0.259 -51.5445 563.2371)"
class="dark fill-light" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round" stroke-miterlimit="10" cx="341.295" cy="315.211"
rx="61.961" ry="60.305"/>
<path id="headStripe" fill="none" class="dark" stroke-width="3"
stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
d="M330.868,261.338c-7.929,1.72-15.381,5.246-21.799,10.246"/>
<path class="dark fill-light" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round" stroke-miterlimit="10"
d="M380.857,346.164c-1.247,4.651-4.668,8.421-9.196,10.06c-9.332,3.377-26.2,7.817-42.301,3.5s-28.485-16.599-34.877-24.192c-3.101-3.684-4.177-8.66-2.93-13.311l7.453-27.798c0.756-2.82,3.181-4.868,6.088-5.13c6.755-0.61,20.546-0.608,41.785,5.087s33.181,12.591,38.725,16.498c2.387,1.682,3.461,4.668,2.705,7.488L380.857,346.164z"/>
<g clip-path="url(#GlassClip)">
<polygon id="glassShine" fill="none" class="dark" stroke-width="3"
stroke-miterlimit="10"
points="278.436,375.599 383.003,264.076 364.393,251.618 264.807,364.928 "/>
</g>
</g>
</g>
</g>
</svg>
</div>
<div class="content">
<h1>{{ code }}</h1>
<h2><span data-l10n>UH OH</span>! <span data-l10n>{{ message }}</span></h2>
<p data-l10n>{{ description }}</p>
{{ if show_details }}
<ul class="details">
{{- if host }}<li><span><span data-l10n>Host</span>:</span> <code>{{ host }}</code></li>{{ end -}}
{{- if original_uri }}<li><span><span data-l10n>Original URI</span>:</span> <code>{{ original_uri }}</code></li>{{ end -}}
{{- if forwarded_for }}<li><span><span data-l10n>Forwarded for</span>:</span> <code>{{ forwarded_for }}</code></li>{{ end -}}
{{- if namespace }}<li><span><span data-l10n>Namespace</span>:</span> <code>{{ namespace }}</code></li>{{ end -}}
{{- if ingress_name }}<li><span><span data-l10n>Ingress name</span>:</span> <code>{{ ingress_name }}</code></li>{{ end -}}
{{- if service_name }}<li><span><span data-l10n>Service name</span>:</span> <code>{{ service_name }}</code></li>{{ end -}}
{{- if service_port }}<li><span><span data-l10n>Service port</span>:</span> <code>{{ service_port }}</code></li>{{ end -}}
{{- if request_id }}<li><span><span data-l10n>Request ID</span>:</span> <code>{{ request_id }}</code></li>{{ end -}}
<li><span><span data-l10n>Timestamp</span>:</span> <code>{{ now.Unix }}</code></li>
</ul>
{{ end }}
</div>
</main>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"
integrity="sha512-H6cPm97FAsgIKmlBA4s774vqoN24V5gSQL4yBTDOY2su2DeXZVhQPxFK4P6GPdnZqM9fg1G3cMv5wD7e6cFLZQ=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
/** @var {object} gsap is a library for building high-performance animations */
if (typeof gsap === 'object') {
gsap.set("svg", {visibility: "visible"});
gsap.to("#headStripe", {y: 0.5, rotation: 1, yoyo: true, repeat: -1, ease: "sine.inOut", duration: 1});
gsap.to("#spaceman", {y: 0.5, rotation: 1, yoyo: true, repeat: -1, ease: "sine.inOut", duration: 1});
gsap.to("#craterSmall", {x: -3, yoyo: true, repeat: -1, duration: 1, ease: "sine.inOut"});
gsap.to("#craterBig", {x: 3, yoyo: true, repeat: -1, duration: 1, ease: "sine.inOut"});
gsap.to("#planet", {rotation: -2, yoyo: true, repeat: -1, duration: 1, ease: "sine.inOut", transformOrigin: "50% 50%"});
gsap.to("#starsBig g", {rotation: "random(-30,30)", transformOrigin: "50% 50%", yoyo: true, repeat: -1, ease: "sine.inOut"});
gsap.fromTo("#starsSmall g", {scale: 0, transformOrigin: "50% 50%"}, {scale: 1, transformOrigin: "50% 50%", yoyo: true, repeat: -1, stagger: 0.1});
gsap.to("#circlesSmall circle", {y: -4, yoyo: true, duration: 1, ease: "sine.inOut", repeat: -1});
gsap.to("#circlesBig circle", {y: -2, yoyo: true, duration: 1, ease: "sine.inOut", repeat: -1});
gsap.set("#glassShine", {x: -68});
gsap.to("#glassShine", {x: 80, duration: 2, rotation: -30, ease: "expo.inOut", transformOrigin: "50% 50%", repeat: -1, repeatDelay: 8, delay: 2});
} else {
console.warn('gsap library is not initialized (network error?)')
}
// {{ if l10n_enabled }}
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
s.async = s.defer = true;
s.addEventListener('load', () => p.removeChild(s));
p.appendChild(s);
})(document.createElement('script'), document.body);
}
// {{ end }}
</script>
</body>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
</html>

View File

@ -1,282 +0,0 @@
<!DOCTYPE html>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{ message }} ({{ code }})</title>
<link rel="preconnect" href="https://fonts.bunny.net" crossorigin>
<link rel="dns-prefetch" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css2?family=Inconsolata:wght@400;700&display=swap" rel="stylesheet">
<style>
:root{--matrix-glyph-size:15px;--matrix-glyph-font-size:15px;--matrix-glyph-front-color:rgba(255, 255, 255, 0.8);--matrix-glyph-tail-color:#0f0;--matrix-overlay-color:rgba(0, 0, 0, 0.12)}
body,html{margin:0;padding:0;background-color:#000;height:100vh}
#matrix{display:block;position:fixed;width:100vw;height:100vh}
.container{align-items:center;display:flex;justify-content:center;position:absolute;top:0;left:0;width:100vw;height:100vh;z-index:1}
.container .message{background-color:rgba(0,0,0,.85);border:2px solid var(--matrix-glyph-tail-color);padding:15px 20px;margin:0 20px;font-family:Inconsolata,Helvetica,sans-serif;text-align:center;font-size:0;color:var(--matrix-glyph-tail-color);text-shadow:1px 0 2px var(--matrix-glyph-tail-color),-1px 0 2px var(--matrix-glyph-tail-color);box-shadow:1px 0 5px var(--matrix-glyph-tail-color),-1px 0 2px var(--matrix-glyph-tail-color);max-width:640px}
.container .message h1{margin:0;font-size:52px}
.container .message p{margin:.3em 0 0 0;font-size:17px;color:var(--matrix-glyph-front-color)}
/* {{ if show_details }} */
.container .details{margin-top:20px}
.container .details ul{padding:0}
.container .details code,.container .details span{font-size:11px}
.container .details code{padding-left:7px}
/* {{ end }} */
.hidden {display:none}
@media screen and (max-width:820px){
:root{--matrix-glyph-size:10px;--matrix-glyph-font-size:10px}
.container .message h1{font-size:38px}
.container .message p{font-size:13px}
/* {{ if show_details }} */
.container .details code,.container .details span{font-size:11px}
/* {{ end }} */
}
</style>
</head>
<body>
<div class="container">
<ul id="matrix-words" class="hidden">
<li>{{ code }}</li>
<li>{{ message }}</li>
<li>{{ description }}</li>
<li>{{ code }} {{ message }}</li>
</ul>
<div class="message">
<h1>{{ code }}: <span data-l10n>{{ message }}</span></h1>
<p data-l10n>{{ description }}</p>
{{ if show_details }}
<div class="details">
<ul>
{{- if host }}<li><span><span data-l10n>Host</span>:</span> <code>{{ host }}</code></li>{{ end -}}
{{- if original_uri }}<li><span><span data-l10n>Original URI</span>:</span> <code>{{ original_uri }}</code></li>{{ end -}}
{{- if forwarded_for }}<li><span><span data-l10n>Forwarded for</span>:</span> <code>{{ forwarded_for }}</code></li>{{ end -}}
{{- if namespace }}<li><span><span data-l10n>Namespace</span>:</span> <code>{{ namespace }}</code></li>{{ end -}}
{{- if ingress_name }}<li><span><span data-l10n>Ingress name</span>:</span> <code>{{ ingress_name }}</code></li>{{ end -}}
{{- if service_name }}<li><span><span data-l10n>Service name</span>:</span> <code>{{ service_name }}</code></li>{{ end -}}
{{- if service_port }}<li><span><span data-l10n>Service port</span>:</span> <code>{{ service_port }}</code></li>{{ end -}}
{{- if request_id }}<li><span><span data-l10n>Request ID</span>:</span> <code>{{ request_id }}</code></li>{{ end -}}
<li><span><span data-l10n>Timestamp</span>:</span> <code>{{ now.Unix }}</code></li>
</ul>
</div>
{{ end }}
</div>
</div>
<canvas id="matrix"></canvas>
<script>
'use strict';
/**
* @param {HTMLCanvasElement} $canvas
* @constructor
*/
const Matrix = function ($canvas) {
const symbols = 'ラドクリフマラソンわたしワタシんょンョたばこタバコとうきょうトウキョウ '.split('');
/**
* @return {string}
*/
const getRandomSymbol = function () {
return symbols[Math.floor(Math.random() * symbols.length)];
}
const ctx = $canvas.getContext('2d');
ctx.globalCompositeOperation = 'lighter'; // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
this.redrawInterval = 90; // fade oud speed
this.glyphSize = 0;
this.rowsCapacity = 0;
this.columnsCapacity = 0;
/**
* @return {void}
*/
this.init = () => {
$canvas.width = $canvas.clientWidth;
$canvas.height = $canvas.clientHeight;
this.glyphSize = parseInt(getComputedStyle($canvas).getPropertyValue('--matrix-glyph-size'), 10);
this.rowsCapacity = Math.ceil($canvas.clientHeight / this.glyphSize);
this.columnsCapacity = Math.ceil($canvas.clientWidth / this.glyphSize);
};
/**
* @param {string} symbol
* @param {number} row
* @param {number} column
* @param {string} color
*/
const drawSymbol = (symbol, row, column, color) => {
if (row > this.rowsCapacity || column > this.columnsCapacity) {
return;
}
ctx.fillStyle = color;
ctx.font = getComputedStyle($canvas).getPropertyValue('--matrix-glyph-font-size') + ' monospace';
if (symbol.length > 1) {
symbol = symbol.charAt(0); // only one char is allowed
}
let xOffset = 0, charCode = symbol.charCodeAt(0);
if (charCode >= 33 && charCode <= 126) { // is ascii
xOffset = this.glyphSize / 5;
}
ctx.fillText(symbol, (column * this.glyphSize) + xOffset, row * this.glyphSize);
};
/**
* @param {number} column
* @param {number} speed Lowest = fastest, largest = slowest
* @param {string?} text
* @param {number?} offset
*/
const drawLine = (column, speed, text, offset) => {
let cursor = 0;
const tailColor = getComputedStyle($canvas).getPropertyValue('--matrix-glyph-tail-color'),
frontColor = getComputedStyle($canvas).getPropertyValue('--matrix-glyph-front-color');
const handler = window.setInterval(() => {
if (column > this.columnsCapacity) {
return window.clearInterval(handler);
}
if (cursor <= this.rowsCapacity) {
let symbol = getRandomSymbol();
if (typeof text === 'string' && typeof offset === 'number') {
if (cursor >= offset && text.length >= cursor - offset) {
symbol = text.charAt(cursor - offset);
}
}
if (typeof symbol === 'string' && symbol !== ' ') {
const prev = cursor;
window.setTimeout(() => { // redraw with a green color
drawSymbol(symbol, prev, column, tailColor);
}, speed / 1.3);
drawSymbol(symbol, cursor, column, frontColor); // white color first
}
cursor++;
} else {
window.clearInterval(handler);
}
}, speed);
};
/**
* @return {void}
*/
this.redraw = () => {
ctx.fillStyle = getComputedStyle($canvas).getPropertyValue('--matrix-overlay-color');
ctx.fillRect(0, 0, $canvas.clientWidth, $canvas.clientHeight);
};
let redrawIntervalHandler = undefined, dropsIntervalHandler = undefined;
/**
* @param {HTMLUListElement?} $linesList
*/
this.run = ($linesList) => {
if (redrawIntervalHandler === undefined) {
redrawIntervalHandler = window.setInterval(this.redraw, this.redrawInterval);
}
if (dropsIntervalHandler === undefined) {
const fn = () => {
const randomColumn = Math.floor(Math.random() * (this.columnsCapacity + 1)),
minSpeed = 200, maxSpeed = 50,
randomSpeed = Math.floor(Math.random() * (maxSpeed - minSpeed + 1)) + minSpeed;
const list = [];
let line = undefined, offset = undefined;
if ($linesList !== undefined) {
Array.prototype.forEach.call($linesList.querySelectorAll('li'), $li => {
const text = $li.innerText.trim();
if (text.length > 0) {
list.push(text);
}
});
if (list.length > 0 && Math.random() > 0.4) {
line = list[Math.floor(Math.random() * list.length)];
offset = Math.floor(Math.random() * line.length);
if (offset <= 5) {
offset *= 3;
}
}
}
drawLine(randomColumn, randomSpeed, line, offset);
if (dropsIntervalHandler !== undefined) {
window.clearInterval(dropsIntervalHandler);
dropsIntervalHandler = undefined;
}
dropsIntervalHandler = window.setInterval(fn, ((minSpeed + maxSpeed) / 2 * this.rowsCapacity) / this.columnsCapacity / 0.5);
};
fn();
}
};
/**
* @return {void}
*/
this.stop = () => {
if (redrawIntervalHandler !== undefined) {
window.clearInterval(redrawIntervalHandler);
redrawIntervalHandler = undefined;
}
if (dropsIntervalHandler !== undefined) {
window.clearInterval(dropsIntervalHandler);
dropsIntervalHandler = undefined;
}
};
if (typeof ResizeObserver === 'function') {
(new ResizeObserver(this.init)).observe($canvas);
} else {
this.init();
}
};
(new Matrix(document.getElementById('matrix'))).run(document.getElementById('matrix-words'));
// {{ if l10n_enabled }}
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
s.async = s.defer = true;
s.addEventListener('load', () => p.removeChild(s));
p.appendChild(s);
})(document.createElement('script'), document.body);
}
// {{ end }}
</script>
</body>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
</html>

View File

@ -1,194 +0,0 @@
<!DOCTYPE html>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
{{ if show_details }}
{{ if host }}Host: {{ host }}{{ end }}
{{ if original_uri }}Original URI: {{ original_uri }}{{ end }}
{{ if forwarded_for }}Forwarded for: {{ forwarded_for }}{{ end }}
{{ if namespace }}Namespace: {{ namespace }}{{ end }}
{{ if ingress_name }}Ingress name: {{ ingress_name }}{{ end }}
{{ 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: {{ now.Unix }}
{{ end }}
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{ code }}: {{ message }}</title>
<style>
html, body {
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
}
body {
font: 20px Hack, Helvetica, sans-serif;
color: #333;
}
canvas {
z-index: 1;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.frame {
z-index: 3;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: radial-gradient(ellipse at center, rgba(0, 0, 0, .1) 0%, rgba(0, 0, 0, .2) 19%, rgba(0, 0, 0, .9) 100%);
}
.frame div {
position: absolute;
left: 0;
top: -20%;
width: 100%;
height: 20%;
background-color: rgba(0, 0, 0, .12);
box-shadow: 0 0 30px rgba(0, 0, 0, .25);
animation: horizontalLine 12s linear infinite;
}
.frame div:nth-child(1) {
animation-delay: 0ms;
}
.frame div:nth-child(2) {
animation-delay: 4s;
}
.frame div:nth-child(3) {
animation-delay: 8s;
}
@keyframes horizontalLine {
0% {top: -20%}
100% {top: 100%}
}
.container-center {
height: 100%;
align-items: center;
display: flex;
justify-content: center;
}
.container-center div {
z-index: 2;
}
h1, h2 {
text-align: center;
color: transparent;
text-shadow: 0 0 10px rgba(0, 0, 0, .6);
}
h1 {
font: bold 13em Arial, sans-serif;
animation: codeText 2s linear infinite;
margin: 0;
}
@keyframes codeText {
0% {text-shadow: 0 0 15px rgba(0, 0, 0, .3)}
33% {text-shadow: 0 0 5px rgba(0, 0, 0, .2)}
66% {text-shadow: 0 0 10px rgba(0, 0, 0, .1)}
100% {text-shadow: 0 0 15px rgba(0, 0, 0, .3)}
}
h2 {
font: bold 2.5em Arial, sans-serif;
animation: descriptionText 4s linear infinite;
margin-bottom: 0;
}
@keyframes descriptionText {
0% {text-shadow: 0 0 10px rgba(0, 0, 0, .5)}
33% {text-shadow: 0 0 5px rgba(0, 0, 0, .1)}
66% {text-shadow: 0 0 5px rgba(0, 0, 0, .25)}
100% {text-shadow: 0 0 10px rgba(0, 0, 0, .5)}
}
</style>
</head>
<body>
<div class="container-center">
<div>
<h1>{{ code }}</h1>
<h2 data-l10n>{{ description }}</h2>
</div>
</div>
<div class="frame">
<div></div>
<div></div>
<div></div>
</div>
<canvas id="canvas"></canvas>
<script>
// main idea author: https://codepen.io/moklick
const $canvas = document.getElementById('canvas'),
width = Math.max(800, document.body.clientWidth),
height = Math.max(600, document.body.clientHeight);
$canvas.width = width;
$canvas.height = height;
const ctx = $canvas.getContext('2d');
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, width, height);
ctx.fill();
const imgData = ctx.getImageData(0, 0, width, height), pix = imgData.data;
const flickerInterval = window.setInterval(function () {
for (let i = 0; i < pix.length; i += 4) {
let color = (Math.random() * 255) + 50;
pix[i] = color;
pix[i + 1] = color;
pix[i + 2] = color;
}
ctx.putImageData(imgData, 0, 0);
}, 45);
window.addEventListener('beforeunload', function (/** @param BeforeUnloadEvent event */ event) {
window.clearInterval(flickerInterval);
});
// {{ if l10n_enabled }}
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
s.async = s.defer = true;
s.addEventListener('load', () => p.removeChild(s));
p.appendChild(s);
})(document.createElement('script'), document.body);
}
// {{ end }}
</script>
</body>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
</html>

File diff suppressed because one or more lines are too long

View File

@ -1,39 +0,0 @@
# Templates
Creating templates is a very simple operation, even for those who know nothing at all about [Go Template](https://pkg.go.dev/text/template). All you should know is:
- The template should be one page. Without additional `css` or `js` files (but you can load them from the CDN or another GitHub repositories using [jsdelivr.com](https://www.jsdelivr.com/), for example)
- Don't forget to include `<meta name="robots" content="noindex, nofollow" />` tag in the header
- You can use a special "placeholders" (wrapped in `{{` and `}}`) for the rendering error code, message and others (see details below)
## Supported signatures
### Error page & request data
| Signature | Description | Example |
|--------------------------------------|---------------------------------------------------------------|----------------------------------------------|
| `{{ code }}` | Error page code | `404` |
| `{{ message }}` | Error code message | `Not found` |
| `{{ description }}` | Error code description | `The server can not find the requested page` |
| `{{ original_uri }}` | `X-Original-URI` header value | `/foo1/bar2` |
| `{{ namespace }}` | `X-Namespace` header value | `foo` |
| `{{ ingress_name }}` | `X-Ingress-Name` header value | `bar` |
| `{{ service_name }}` | `X-Service-Name` header value | `baz` |
| `{{ service_port }}` | `X-Service-Port` header value | `8080` |
| `{{ request_id }}` | `X-Request-ID` header value | `12AB34CD56EF78` |
| `{{ forwarded_for }}` | `X-Forwarded-For` header value | `203.0.113.195, 70.41.3.18` |
| `{{ host }}` | `Host` header value | `example.com` |
| `{{ now.Unix }}` | Current timestamp (e.g. in Unix format) | `1643621927` |
| `{{ hostname }}` | OS hostname | `ab12cd34ef56` |
| `{{ version }}` | Application version | `2.5.0` |
| `{{ if show_details }}...{{ end }}` | Logical operator (server started with "show details" option?) | |
| `{{ if hide_details }}...{{ end }}` | Same as above, but inverted | |
| `{{ if l10n_enabled }}...{{ end }}` | Logical operator (l10n is enabled?) | |
| `{{ if l10n_disabled }}...{{ end }}` | Same as above, but inverted | |
### Modifiers
| Signature | Description | Example |
|------------------------------------|--------------------------------|-------------------------------------|
| <code>{{ ... &#124; json }}</code> | Convert value into json-string | <code>{{ code &#124; json }}</code> |
| <code>{{ ... &#124; int }}</code> | Convert value into integer | <code>{{ code &#124; int }}</code> |

View File

@ -1,234 +0,0 @@
<!DOCTYPE html>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{ code }} - {{ message }}</title>
<style>
html, body {
margin: 0;
background-color: #222;
color: #aaa;
font-family: 'Hack', monospace;
font-size: 0;
}
.full-height {
height: 100vh;
}
.flex-center {
align-items: center;
display: flex;
justify-content: center;
}
#error_text {
font-size: 32px;
}
/* {{ if show_details }} */
#details table {
width: 100%;
border-collapse: collapse;
box-sizing: border-box;
margin-top: 20px;
}
#details.hidden td {
opacity: 0;
font-size: 0;
color: #222;
}
#details td {
font-size: 11px;
color: #999;
padding-top: .5em;
transition: opacity 1.4s, font-size .3s, color 1.2s;
opacity: 1;
}
#details td.name {
text-align: right;
padding-right: .3em;
width: 50%;
}
#details td.value {
text-align: left;
padding-left: .3em;
font-family: 'Lucida Console', 'Courier New', monospace;
}
/* {{ end }} */
</style>
</head>
<body>
<div class="flex-center full-height">
<div>
<div id="error_text">
<span class="source">{{ code }}: <span data-l10n>{{ message }}</span></span>
<span class="target"></span>
</div>
{{ if show_details }}
<div class="hidden" id="details">
<table>
{{- if host }}
<tr>
<td class="name"><span data-l10n>Host</span>:</td>
<td class="value">{{ host }}</td>
</tr>
{{ end -}}
{{- if original_uri }}
<tr>
<td class="name"><span data-l10n>Original URI</span>:</td>
<td class="value">{{ original_uri }}</td>
</tr>
{{ end -}}
{{- if forwarded_for }}
<tr>
<td class="name"><span data-l10n>Forwarded for</span>:</td>
<td class="value">{{ forwarded_for }}</td>
</tr>
{{ end -}}
{{- if namespace }}
<tr>
<td class="name"><span data-l10n>Namespace</span>:</td>
<td class="value">{{ namespace }}</td>
</tr>
{{ end -}}
{{- if ingress_name }}
<tr>
<td class="name"><span data-l10n>Ingress name</span>:</td>
<td class="value">{{ ingress_name }}</td>
</tr>
{{ end -}}
{{- if service_name }}
<tr>
<td class="name"><span data-l10n>Service name</span>:</td>
<td class="value">{{ service_name }}</td>
</tr>
{{ end -}}
{{- if service_port }}
<tr>
<td class="name"><span data-l10n>Service port</span>:</td>
<td class="value">{{ service_port }}</td>
</tr>
{{ end -}}
{{- if request_id }}
<tr>
<td class="name"><span data-l10n>Request ID</span>:</td>
<td class="value">{{ request_id }}</td>
</tr>
{{ end -}}
<tr>
<td class="name"><span data-l10n>Timestamp</span>:</td>
<td class="value">{{ now.Unix }}</td>
</tr>
</table>
</div>
{{ end }}
</div>
</div>
<script>
'use strict';
/**
* @param {HTMLElement} $el
*/
const Shuffle = function ($el) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-=+<>,./?[{()}]!@#$%^&*~`\|'.split(''),
$source = $el.querySelector('.source'), $target = $el.querySelector('.target');
let cursor = 0, scrambleInterval = undefined, cursorDelayInterval = undefined, cursorInterval = undefined;
/**
* @param {Number} len
* @return {string}
*/
const getRandomizedString = function (len) {
let s = '';
for (let i = 0; i < len; i++) {
s += chars[Math.floor(Math.random() * chars.length)];
}
return s;
};
this.start = function () {
$source.style.display = 'none';
$target.style.display = 'block';
scrambleInterval = window.setInterval(() => {
if (cursor <= $source.innerText.length) {
$target.innerText = $source.innerText.substring(0, cursor) + getRandomizedString($source.innerText.length - cursor);
}
}, 450 / 30);
cursorDelayInterval = window.setTimeout(() => {
cursorInterval = window.setInterval(() => {
if (cursor > $source.innerText.length - 1) {
this.stop();
}
cursor++;
}, 70);
}, 350);
};
this.stop = function () {
$source.style.display = 'block';
$target.style.display = 'none';
$target.innerText = '';
cursor = 0;
if (scrambleInterval !== undefined) {
window.clearInterval(scrambleInterval);
scrambleInterval = undefined;
}
if (cursorInterval !== undefined) {
window.clearInterval(cursorInterval);
cursorInterval = undefined;
}
if (cursorDelayInterval !== undefined) {
window.clearInterval(cursorDelayInterval);
cursorDelayInterval = undefined;
}
};
};
(new Shuffle(document.getElementById('error_text'))).start();
// {{ if show_details }}
window.setTimeout(function () {
document.getElementById('details').classList.remove('hidden');
}, 550);
// {{ end }}
// {{ if l10n_enabled }}
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
s.async = s.defer = true;
s.addEventListener('load', () => p.removeChild(s));
p.appendChild(s);
})(document.createElement('script'), document.body);
}
// {{ end }}
</script>
</body>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
</html>

View File

@ -1,7 +0,0 @@
GET http://{{ host }}:{{ port }}/not-found
HTTP 404
[Asserts]
header "Content-Type" contains "text/html"
body contains "The server can not find the requested page"

View File

@ -1,10 +0,0 @@
GET http://{{ host }}:{{ port }}/502.html
HTTP 200
[Asserts]
header "Content-Type" contains "text/html"
header "X-Robots-Tag" == "noindex"
body contains "502"
body contains "Bad Gateway"
body contains "The server received an invalid response from the upstream server"

View File

@ -1,43 +0,0 @@
# The common request
GET http://{{ host }}:{{ port }}/502.html
Content-Type: application/json;charset=UTF-8
X-Original-URI: foo
X-Namespace: bar
X-Ingress-Name: baz
X-Service-Name: aaa
X-Service-Port: bbb
X-Request-ID: ccc
X-Forwarded-For: ddd
Host: eee
HTTP 200
[Asserts]
header "Content-Type" contains "application/json"
header "X-Robots-Tag" == "noindex"
jsonpath "$.error" == true
jsonpath "$.code" == "502"
jsonpath "$.message" == "Bad Gateway"
jsonpath "$.description" == "The server received an invalid response from the upstream server"
jsonpath "$.details.original_uri" == "foo"
jsonpath "$.details.namespace" == "bar"
jsonpath "$.details.ingress_name" == "baz"
jsonpath "$.details.service_name" == "aaa"
jsonpath "$.details.service_port" == "bbb"
jsonpath "$.details.request_id" == "ccc"
jsonpath "$.details.forwarded_for" == "ddd"
jsonpath "$.details.host" == "eee"
jsonpath "$.details.timestamp" isInteger
# X-Format in the action
GET http://{{ host }}:{{ port }}/502.html
X-Format: text/json
HTTP 200
[Asserts]
header "Content-Type" contains "application/json"
header "X-Robots-Tag" == "noindex"
jsonpath "$.error" == true
jsonpath "$.code" == "502"
jsonpath "$.message" == "Bad Gateway"

View File

@ -1,42 +0,0 @@
# The common request
GET http://{{ host }}:{{ port }}/502.html
Content-Type: application/xml;charset=UTF-8
X-Original-URI: foo
X-Namespace: bar
X-Ingress-Name: baz
X-Service-Name: aaa
X-Service-Port: bbb
X-Request-ID: ccc
X-Forwarded-For: ddd
Host: eee
HTTP 200
[Asserts]
header "Content-Type" contains "application/xml"
header "X-Robots-Tag" == "noindex"
xpath "string(//error/code)" == "502"
xpath "string(//error/message)" == "Bad Gateway"
xpath "string(//error/description)" == "The server received an invalid response from the upstream server"
xpath "string(//error/details/originalURI)" == "foo"
xpath "string(//error/details/namespace)" == "bar"
xpath "string(//error/details/ingressName)" == "baz"
xpath "string(//error/details/serviceName)" == "aaa"
xpath "string(//error/details/servicePort)" == "bbb"
xpath "string(//error/details/requestID)" == "ccc"
xpath "string(//error/details/forwardedFor)" == "ddd"
xpath "string(//error/details/host)" == "eee"
xpath "string(//error/details/timestamp)" exists
# X-Format in the action
GET http://{{ host }}:{{ port }}/502.html
X-Format: text/xml
HTTP 200
[Asserts]
header "Content-Type" contains "application/xml"
header "X-Robots-Tag" == "noindex"
xpath "string(//error/code)" == "502"
xpath "string(//error/message)" == "Bad Gateway"
xpath "string(//error/description)" == "The server received an invalid response from the upstream server"

View File

@ -1,12 +0,0 @@
GET http://{{ host }}:{{ port }}/healthz
HTTP 200
`OK`
# Next endpoint marked as deprecated
GET http://{{ host }}:{{ port }}/health/live
HTTP 200
`OK`

View File

@ -1,34 +0,0 @@
# HTML content
GET http://{{ host }}:{{ port }}/
HTTP 404
[Asserts]
header "Content-Type" contains "text/html"
body contains "404"
body contains "Not Found"
# JSON content
GET http://{{ host }}:{{ port }}/
Content-Type: text/json
HTTP 404
[Asserts]
header "Content-Type" contains "application/json"
header "X-Robots-Tag" == "noindex"
jsonpath "$.error" == true
jsonpath "$.code" == "404"
jsonpath "$.message" == "Not Found"
# XML content
GET http://{{ host }}:{{ port }}/
Content-Type: application/xml
HTTP 404
[Asserts]
header "Content-Type" contains "application/xml"
header "X-Robots-Tag" == "noindex"
xpath "string(//error/code)" == "404"
xpath "string(//error/message)" == "Not Found"

View File

@ -1,10 +0,0 @@
# disabled until https://github.com/Orange-OpenSource/hurl/issues/2540 is not fixed
#GET http://{{ host }}:{{ port }}/metrics
#
#HTTP 200
#
#[Asserts]
#header "Content-Type" contains "text/plain"
#body contains "http_requests_duration_millisecond"
#body contains "http_requests_total_count"

View File

@ -1,13 +0,0 @@
GET http://{{ host }}:{{ port }}/502.html
X-Foo: foo
bar: BAR
Baz_blah: baz Baz
NonEx: skip
HTTP 200
[Asserts]
header "X-Foo" == "foo"
header "Bar" == "BAR"
header "Baz_blah" == "baz Baz"
header "NonEx" not exists

View File

@ -1,27 +0,0 @@
# Hurl
Hurl is a command line tool that runs **HTTP requests** defined in a simple **plain text format**.
## How to use
It can perform requests, capture values and evaluate queries on headers and body response. Hurl is very versatile: it can be used for both fetching data and testing HTTP sessions.
```hurl
# Get home:
GET https://example.net
HTTP 200
[Captures]
csrf_token: xpath "string(//meta[@name='_csrf_token']/@content)"
# Do login!
POST https://example.net/login?user=toto&password=1234
X-CSRF-TOKEN: {{csrf_token}}
HTTP 302
```
### Links:
- [Official website](https://hurl.dev/)
- [GitHub](https://github.com/Orange-OpenSource/hurl)

View File

@ -1,8 +0,0 @@
GET http://{{ host }}:{{ port }}/version
HTTP 200
[Asserts]
header "Content-Type" == "application/json"
jsonpath "$.version" exists
jsonpath "$.version" isString

Some files were not shown because too many files have changed in this diff Show More