From bb17027cc95b638b462dd1d0bbb7f57fb712a826 Mon Sep 17 00:00:00 2001
From: Paramtamtam <7326800+tarampampam@users.noreply.github.com>
Date: Wed, 6 Oct 2021 22:38:00 +0500
Subject: [PATCH] Allow to set default error page (#30)
---
.github/workflows/tests.yml | 3 +-
CHANGELOG.md | 12 ++
Dockerfile | 3 +-
README.md | 70 ++++++---
internal/breaker/os_signal.go | 4 +-
internal/cli/build/command.go | 45 +++---
internal/cli/serve/command.go | 103 ++++++++-----
internal/cli/serve/flags.go | 30 +++-
internal/env/env.go | 9 +-
internal/env/env_test.go | 2 +
internal/http/common/handlers.go | 35 -----
internal/http/common/middlewares.go | 1 +
internal/http/handlers/errorpage/handler.go | 81 +++--------
internal/http/handlers/index/handler.go | 36 +++++
.../index/handler_test.go} | 2 +-
internal/http/handlers/notfound/handler.go | 14 ++
.../http/handlers/notfound/handler_test.go | 7 +
internal/http/server.go | 71 +++------
internal/pick/strings_slice.go | 64 +++++++++
internal/pick/strings_slice_test.go | 102 +++++++++++++
internal/tpl/error_pages.go | 136 ++++++++++++++++++
internal/tpl/error_pages_test.go | 132 +++++++++++++++++
internal/tpl/errors.go | 102 -------------
internal/tpl/errors_test.go | 106 --------------
internal/tpl/replace.go | 47 ------
internal/tpl/replace_test.go | 66 ---------
26 files changed, 721 insertions(+), 562 deletions(-)
delete mode 100644 internal/http/common/handlers.go
create mode 100644 internal/http/handlers/index/handler.go
rename internal/http/{common/handlers_test.go => handlers/index/handler_test.go} (85%)
create mode 100644 internal/http/handlers/notfound/handler.go
create mode 100644 internal/http/handlers/notfound/handler_test.go
create mode 100644 internal/pick/strings_slice.go
create mode 100644 internal/pick/strings_slice_test.go
create mode 100644 internal/tpl/error_pages.go
create mode 100644 internal/tpl/error_pages_test.go
delete mode 100644 internal/tpl/errors.go
delete mode 100644 internal/tpl/errors_test.go
delete mode 100644 internal/tpl/replace.go
delete mode 100644 internal/tpl/replace_test.go
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index a9e2293..747c4f1 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -200,10 +200,11 @@ jobs: # Docs:
- name: Wait for container "healthy" state
run: until [[ "`docker inspect -f {{.State.Health.Status}} app`" == "healthy" ]]; do echo "wait 1 sec.."; sleep 1; done
+ - run: curl --fail http://127.0.0.1:8080/
- run: curl --fail http://127.0.0.1:8080/500.html
- run: curl --fail http://127.0.0.1:8080/400.html
- run: curl --fail http://127.0.0.1:8080/health/live
- - run: test $(curl --write-out %{http_code} --silent --output /dev/null http://127.0.0.1:8080/) -eq 404
+ - run: test $(curl --write-out %{http_code} --silent --output /dev/null http://127.0.0.1:8080/foobar) -eq 404
- name: Stop the container
if: always()
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dbc794a..295a147 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,18 @@ All notable changes to this package will be documented in this file.
The format is based on [Keep a Changelog][keepachangelog] and this project adheres to [Semantic Versioning][semver].
+## UNRELEASED
+
+### Added
+
+- `referer` field in access log records
+- Flag `--default-error-page` for the `serve` subcommand (`404` is used by default, environment name `DEFAULT_ERROR_PAGE`)
+
+### Changed
+
+- The source code has been refactored
+- The index page (`/`) now returns the error page with a code, declared using `--default-error-page` flag (HTTP code 200, when a page code exists)
+
## v2.0.0
### Changed
diff --git a/Dockerfile b/Dockerfile
index f1911e7..833355e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -64,7 +64,8 @@ USER appuser:appuser
WORKDIR /opt
ENV LISTEN_PORT="8080" \
- TEMPLATE_NAME="ghost"
+ TEMPLATE_NAME="ghost" \
+ DEFAULT_ERROR_PAGE="404"
# Docs:
HEALTHCHECK --interval=7s --timeout=2s CMD [ \
diff --git a/README.md b/README.md
index 5157a53..a7bf819 100644
--- a/README.md
+++ b/README.md
@@ -30,8 +30,6 @@ Also, this project can be used for the [**Traefik** error pages customization](h
Download the latest binary file for your os/arch from the [releases page][link_releases] or use our docker image:
-[![image stats](https://dockeri.co/image/tarampampam/error-pages)][link_docker_hub]
-
Registry | Image
-------------------------------------- | -----
[Docker Hub][link_docker_hub] | `tarampampam/error-pages`
@@ -56,13 +54,25 @@ $ docker run --rm -it \
+## Templates
+Name | Preview
+:---------------: | :-----:
+`ghost` | [![ghost](https://hsto.org/webt/oj/cl/4k/ojcl4ko_cvusy5xuki6efffzsyo.gif)](https://tarampampam.github.io/error-pages/ghost/404.html)
+`l7-light` | [![l7-light](https://hsto.org/webt/xc/iq/vt/xciqvty-aoj-rchfarsjhutpjny.png)](https://tarampampam.github.io/error-pages/l7-light/404.html)
+`l7-dark` | [![l7-dark](https://hsto.org/webt/s1/ih/yr/s1ihyrqs_y-sgraoimfhk6ypney.png)](https://tarampampam.github.io/error-pages/l7-dark/404.html)
+`shuffle` | [![shuffle](https://hsto.org/webt/7w/rk/3m/7wrk3mrzz3y8qfqwovmuvacu-bs.gif)](https://tarampampam.github.io/error-pages/shuffle/404.html)
+`noise` | [![noise](https://hsto.org/webt/42/oq/8y/42oq8yok_i-arrafjt6hds_7ahy.gif)](https://tarampampam.github.io/error-pages/noise/404.html)
+`hacker-terminal` | [![hacker-terminal](https://hsto.org/webt/5s/l0/p1/5sl0p1_ud_nalzjzsj5slz6dfda.gif)](https://tarampampam.github.io/error-pages/hacker-terminal/404.html)
+
+> Note: `noise` template highly uses the CPU, be careful
## Usage
All of the examples below will use a docker image with the application, but you can also use a binary. By the way, our docker image uses the **unleveled user** by default and **distroless**.
-### HTTP server
+
+ HTTP server
As mentioned above - our application can be run as an HTTP server. It only needs to specify the path to the configuration file (it does not need statically generated error pages). The server uses [FastHTTP][fasthttp] and stores all necessary data in memory - so it does not use the file system and very fast. Oh yes, the image with the app also contains a configured **healthcheck** and **logs in JSON** format :)
@@ -86,8 +96,10 @@ To see the help run the following command:
```bash
$ docker run --rm tarampampam/error-pages serve --help
```
+
-### Generator
+
+ Generator
Create a config file (`error-pages.yml`) with the following content:
@@ -158,13 +170,15 @@ To see the usage help run the following command:
```bash
$ docker run --rm tarampampam/error-pages build --help
```
+
-### Static error pages
+
+ Static error pages
You may want to use the generated error pages somewhere else, and you can simply extract them from the docker image to your local directory for this purpose:
```bash
-$ docker create --name error-pages tarampampam/error-pages:2.0.0-rc2
+$ docker create --name error-pages tarampampam/error-pages
$ docker cp error-pages:/opt/html ./out
$ docker rm -f error-pages
$ ls ./out
@@ -187,7 +201,35 @@ $ tree
...
```
-### Custom error pages for your image with [nginx][link_nginx]
+Or inside another docker image:
+
+```dockerfile
+FROM alpine:latest
+
+COPY --from=tarampampam/error-pages /opt/html /error-pages
+
+RUN ls -l /error-pages
+```
+
+```bash
+$ docker build --rm .
+
+...
+Step 3/3 : RUN ls -l /error-pages
+ ---> Running in 30095dc344a9
+total 12
+drwxr-xr-x 2 root root 326 Sep 29 15:44 ghost
+drwxr-xr-x 2 root root 326 Sep 29 15:44 hacker-terminal
+-rw-r--r-- 1 root root 11241 Sep 29 15:44 index.html
+drwxr-xr-x 2 root root 326 Sep 29 15:44 l7-dark
+drwxr-xr-x 2 root root 326 Sep 29 15:44 l7-light
+drwxr-xr-x 2 root root 326 Sep 29 15:44 noise
+drwxr-xr-x 2 root root 326 Sep 29 15:44 shuffle
+```
+
+
+
+ Custom error pages for your image with nginx
You can build your own docker image with `nginx` and our error pages:
@@ -234,19 +276,7 @@ $ docker build --tag your-nginx:local -f ./Dockerfile .
```
> More info about `error_page` directive can be [found here](http://nginx.org/en/docs/http/ngx_http_core_module.html#error_page).
-
-## Templates
-
-Name | Preview
-:---------------: | :-----:
-`ghost` | [![ghost](https://hsto.org/webt/oj/cl/4k/ojcl4ko_cvusy5xuki6efffzsyo.gif)](https://tarampampam.github.io/error-pages/ghost/404.html)
-`l7-light` | [![l7-light](https://hsto.org/webt/xc/iq/vt/xciqvty-aoj-rchfarsjhutpjny.png)](https://tarampampam.github.io/error-pages/l7-light/404.html)
-`l7-dark` | [![l7-dark](https://hsto.org/webt/s1/ih/yr/s1ihyrqs_y-sgraoimfhk6ypney.png)](https://tarampampam.github.io/error-pages/l7-dark/404.html)
-`shuffle` | [![shuffle](https://hsto.org/webt/7w/rk/3m/7wrk3mrzz3y8qfqwovmuvacu-bs.gif)](https://tarampampam.github.io/error-pages/shuffle/404.html)
-`noise` | [![noise](https://hsto.org/webt/42/oq/8y/42oq8yok_i-arrafjt6hds_7ahy.gif)](https://tarampampam.github.io/error-pages/noise/404.html)
-`hacker-terminal` | [![hacker-terminal](https://hsto.org/webt/5s/l0/p1/5sl0p1_ud_nalzjzsj5slz6dfda.gif)](https://tarampampam.github.io/error-pages/hacker-terminal/404.html)
-
-> Note: `noise` template highly uses the CPU, be careful
+
## Custom error pages for [Traefik][link_traefik]
diff --git a/internal/breaker/os_signal.go b/internal/breaker/os_signal.go
index 41d532a..ede2886 100644
--- a/internal/breaker/os_signal.go
+++ b/internal/breaker/os_signal.go
@@ -8,7 +8,7 @@ import (
"syscall"
)
-// OSSignals allows to subscribe for system signals.
+// OSSignals allows subscribing for system signals.
type OSSignals struct {
ctx context.Context
ch chan os.Signal
@@ -22,7 +22,7 @@ func NewOSSignals(ctx context.Context) OSSignals {
}
}
-// Subscribe for some of system signals (call Stop for stopping).
+// 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
diff --git a/internal/cli/build/command.go b/internal/cli/build/command.go
index 98f613e..46ffd88 100644
--- a/internal/cli/build/command.go
+++ b/internal/cli/build/command.go
@@ -19,7 +19,7 @@ type historyItem struct {
}
// NewCommand creates `build` command.
-func NewCommand(log *zap.Logger, configFile *string) *cobra.Command { //nolint:funlen,gocognit
+func NewCommand(log *zap.Logger, configFile *string) *cobra.Command { //nolint:funlen,gocognit,gocyclo
var (
generateIndex bool
cfg *config.Config
@@ -52,33 +52,34 @@ func NewCommand(log *zap.Logger, configFile *string) *cobra.Command { //nolint:f
return errors.New("wrong arguments count")
}
- log.Info("loading templates")
+ errorPages := tpl.NewErrorPages()
- templates, err := cfg.LoadTemplates()
- if err != nil {
+ log.Info("loading templates")
+ if templates, err := cfg.LoadTemplates(); err == nil {
+ if len(templates) > 0 {
+ for templateName, content := range templates {
+ errorPages.AddTemplate(templateName, content)
+ }
+
+ for code, desc := range cfg.Pages {
+ errorPages.AddPage(code, desc.Message, desc.Description)
+ }
+ } else {
+ return errors.New("no loaded templates")
+ }
+ } else {
return err
- } else if len(templates) == 0 {
- return errors.New("no loaded templates")
}
log.Debug("the output directory preparing", zap.String("Path", args[0]))
-
- if err = createDirectory(args[0]); err != nil {
+ if err := createDirectory(args[0]); err != nil {
return errors.Wrap(err, "cannot prepare output directory")
}
- codes := make(map[string]tpl.Annotator)
-
- for code, desc := range cfg.Pages {
- codes[code] = tpl.Annotator{Message: desc.Message, Description: desc.Description}
- }
-
- history := make(map[string][]historyItem, len(templates))
+ history, startedAt := make(map[string][]historyItem), time.Now()
log.Info("saving the error pages")
- startedAt := time.Now()
-
- if err = tpl.NewErrors(templates, codes).VisitAll(func(template, code string, content []byte) error {
+ if err := errorPages.IteratePages(func(template, code string, content []byte) error {
if e := createDirectory(path.Join(args[0], template)); e != nil {
return e
}
@@ -90,18 +91,18 @@ func NewCommand(log *zap.Logger, configFile *string) *cobra.Command { //nolint:f
}
if _, ok := history[template]; !ok {
- history[template] = make([]historyItem, 0, len(codes))
+ history[template] = make([]historyItem, 0, len(cfg.Pages))
}
history[template] = append(history[template], historyItem{
Code: code,
- Message: codes[code].Message,
+ Message: cfg.Pages[code].Message,
Path: path.Join(template, fileName),
})
return nil
}); err != nil {
- return nil
+ return err
}
log.Debug("saved", zap.Duration("duration", time.Since(startedAt)))
@@ -110,7 +111,7 @@ func NewCommand(log *zap.Logger, configFile *string) *cobra.Command { //nolint:f
log.Info("index file generation")
startedAt = time.Now()
- if err = writeIndexFile(path.Join(args[0], "index.html"), history); err != nil {
+ if err := writeIndexFile(path.Join(args[0], "index.html"), history); err != nil {
return err
}
diff --git a/internal/cli/serve/command.go b/internal/cli/serve/command.go
index d38fc0a..a757f67 100644
--- a/internal/cli/serve/command.go
+++ b/internal/cli/serve/command.go
@@ -4,16 +4,16 @@ import (
"context"
"errors"
"os"
+ "sort"
"time"
- "github.com/tarampampam/error-pages/internal/http/handlers/errorpage"
- "github.com/tarampampam/error-pages/internal/tpl"
- "go.uber.org/zap"
-
"github.com/spf13/cobra"
"github.com/tarampampam/error-pages/internal/breaker"
"github.com/tarampampam/error-pages/internal/config"
appHttp "github.com/tarampampam/error-pages/internal/http"
+ "github.com/tarampampam/error-pages/internal/pick"
+ "github.com/tarampampam/error-pages/internal/tpl"
+ "go.uber.org/zap"
)
// NewCommand creates `serve` command.
@@ -56,8 +56,6 @@ func NewCommand(ctx context.Context, log *zap.Logger, configFile *string) *cobra
return cmd
}
-const serverShutdownTimeout = 15 * time.Second
-
// run current command.
func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config) error { //nolint:funlen
var (
@@ -77,35 +75,78 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config
oss.Stop() // stop system signals listening
}()
- // load templates content
- templates, loadingErr := cfg.LoadTemplates()
- if loadingErr != nil {
- return loadingErr
- } else if len(templates) == 0 {
- return errors.New("no loaded templates")
- }
+ var (
+ errorPages = tpl.NewErrorPages()
+ templateNames = make([]string, 0) // slice with all possible template names
+ )
- if f.template.name != "" && f.template.name != errorpage.UseRandom && f.template.name != errorpage.UseRandomOnEachRequest { //nolint:lll
- if _, found := templates[f.template.name]; !found {
- return errors.New("requested nonexistent template: " + f.template.name) // requested unknown template
+ log.Debug("Loading templates")
+
+ if templates, err := cfg.LoadTemplates(); err == nil {
+ if len(templates) > 0 {
+ for templateName, content := range templates {
+ errorPages.AddTemplate(templateName, content)
+ templateNames = append(templateNames, templateName)
+ }
+
+ for code, desc := range cfg.Pages {
+ errorPages.AddPage(code, desc.Message, desc.Description)
+ }
+
+ log.Info("Templates loaded", zap.Int("templates", len(templates)), zap.Int("pages", len(cfg.Pages)))
+ } else {
+ return errors.New("no loaded templates")
}
+ } else {
+ return err
}
- // burn the error codes map
- codes := make(map[string]tpl.Annotator)
- for code, desc := range cfg.Pages {
- codes[code] = tpl.Annotator{Message: desc.Message, Description: desc.Description}
+ sort.Strings(templateNames) // sorting is important for the first template picking
+
+ var picker *pick.StringsSlice
+
+ switch f.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 "":
+ log.Info("The first template (ordered by name) will be used")
+
+ picker = pick.NewStringsSlice(templateNames, pick.First)
+
+ default:
+ var found bool
+
+ for i := 0; i < len(templateNames); i++ {
+ if templateNames[i] == f.template.name {
+ found = true
+
+ break
+ }
+ }
+
+ if !found {
+ return errors.New("requested nonexistent template: " + f.template.name)
+ }
+
+ log.Info("We will use the requested template", zap.String("name", f.template.name))
+ picker = pick.NewStringsSlice([]string{f.template.name}, pick.First)
}
// create HTTP server
server := appHttp.NewServer(log)
// register server routes, middlewares, etc.
- if err := server.Register(f.template.name, templates, codes); err != nil {
- return err
- }
+ server.Register(&errorPages, picker, f.defaultErrorPage)
- startingErrCh := make(chan error, 1) // channel for server starting error
+ startedAt, startingErrCh := time.Now(), make(chan error, 1) // channel for server starting error
// start HTTP server in separate goroutine
go func(errCh chan<- error) {
@@ -114,7 +155,7 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config
log.Info("Server starting",
zap.String("addr", f.listen.ip),
zap.Uint16("port", f.listen.port),
- zap.String("template name", f.template.name),
+ zap.String("default error page", f.defaultErrorPage),
)
if err := server.Start(f.listen.ip, f.listen.port); err != nil {
@@ -128,20 +169,12 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config
return err
case <-ctx.Done(): // ..or context cancellation
- log.Info("Gracefully server stopping")
-
- stoppedAt := time.Now()
+ log.Info("Gracefully server stopping", zap.Duration("uptime", time.Since(startedAt)))
// stop the server using created context above
- if err := server.Stop(serverShutdownTimeout); err != nil {
- if errors.Is(err, context.DeadlineExceeded) {
- log.Error("Server stopping timeout exceeded", zap.Duration("timeout", serverShutdownTimeout))
- }
-
+ if err := server.Stop(); err != nil {
return err
}
-
- log.Debug("Server stopped", zap.Duration("stopping duration", time.Since(stoppedAt)))
}
return nil
diff --git a/internal/cli/serve/flags.go b/internal/cli/serve/flags.go
index 6994156..7117ea9 100644
--- a/internal/cli/serve/flags.go
+++ b/internal/cli/serve/flags.go
@@ -6,8 +6,6 @@ import (
"strconv"
"strings"
- "github.com/tarampampam/error-pages/internal/http/handlers/errorpage"
-
"github.com/spf13/pflag"
"github.com/tarampampam/error-pages/internal/env"
)
@@ -20,12 +18,19 @@ type flags struct {
template struct {
name string
}
+ defaultErrorPage string
}
const (
- listenFlagName = "listen"
- portFlagName = "port"
- templateNameFlagName = "template-name"
+ listenFlagName = "listen"
+ portFlagName = "port"
+ templateNameFlagName = "template-name"
+ defaultErrorPageFlagName = "default-error-page"
+)
+
+const (
+ useRandomTemplate = "random"
+ useRandomTemplateOnEachRequest = "i-said-random"
)
func (f *flags) init(flagSet *pflag.FlagSet) {
@@ -46,10 +51,16 @@ func (f *flags) init(flagSet *pflag.FlagSet) {
templateNameFlagName, "t",
"",
fmt.Sprintf(
- "template name (set \"%s\" to use the randomized or \"%s\" to use the randomized template on each request) [$%s]", //nolint:lll
- errorpage.UseRandom, errorpage.UseRandomOnEachRequest, env.TemplateName,
+ "template name (set \"%s\" to use a randomized or \"%s\" to use a randomized template on each request) [$%s]", //nolint:lll
+ useRandomTemplate, useRandomTemplateOnEachRequest, env.TemplateName,
),
)
+ flagSet.StringVarP(
+ &f.defaultErrorPage,
+ defaultErrorPageFlagName, "",
+ "404",
+ fmt.Sprintf("default error page [$%s]", env.DefaultErrorPage),
+ )
}
func (f *flags) overrideUsingEnv(flagSet *pflag.FlagSet) (lastErr error) {
@@ -75,6 +86,11 @@ func (f *flags) overrideUsingEnv(flagSet *pflag.FlagSet) (lastErr error) {
if envVar, exists := env.TemplateName.Lookup(); exists {
f.template.name = strings.TrimSpace(envVar)
}
+
+ case defaultErrorPageFlagName:
+ if envVar, exists := env.DefaultErrorPage.Lookup(); exists {
+ f.defaultErrorPage = strings.TrimSpace(envVar)
+ }
}
}
})
diff --git a/internal/env/env.go b/internal/env/env.go
index a3245fd..6c638f5 100644
--- a/internal/env/env.go
+++ b/internal/env/env.go
@@ -6,10 +6,11 @@ import "os"
type envVariable string
const (
- 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
+ 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)
)
// String returns environment variable name in the string representation.
diff --git a/internal/env/env_test.go b/internal/env/env_test.go
index 5200b7e..e249f99 100644
--- a/internal/env/env_test.go
+++ b/internal/env/env_test.go
@@ -12,6 +12,7 @@ func TestConstants(t *testing.T) {
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))
}
func TestEnvVariable_Lookup(t *testing.T) {
@@ -22,6 +23,7 @@ func TestEnvVariable_Lookup(t *testing.T) {
{giveEnv: ListenPort},
{giveEnv: TemplateName},
{giveEnv: ConfigFilePath},
+ {giveEnv: DefaultErrorPage},
}
for _, tt := range cases {
diff --git a/internal/http/common/handlers.go b/internal/http/common/handlers.go
deleted file mode 100644
index 4cae565..0000000
--- a/internal/http/common/handlers.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package common
-
-import (
- "strings"
-
- "github.com/valyala/fasthttp"
-)
-
-const internalErrorPattern = `
-
-
-
-
-
- Internal error occurred
-
-
-
-
-
-
{{ message }}
-
-
-`
-
-func HandleInternalHTTPError(ctx *fasthttp.RequestCtx, statusCode int, message string) {
- ctx.SetStatusCode(statusCode)
- ctx.SetContentType("text/html; charset=UTF-8")
-
- _, _ = ctx.WriteString(strings.ReplaceAll(internalErrorPattern, "{{ message }}", message))
-}
diff --git a/internal/http/common/middlewares.go b/internal/http/common/middlewares.go
index 8043889..2c0daba 100644
--- a/internal/http/common/middlewares.go
+++ b/internal/http/common/middlewares.go
@@ -25,6 +25,7 @@ func LogRequest(h fasthttp.RequestHandler, log *zap.Logger) fasthttp.RequestHand
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.Bool("connection_close", ctx.Response.ConnectionClose()),
zap.Duration("duration", time.Since(startedAt)),
diff --git a/internal/http/handlers/errorpage/handler.go b/internal/http/handlers/errorpage/handler.go
index 3b07f13..a38fd37 100644
--- a/internal/http/handlers/errorpage/handler.go
+++ b/internal/http/handlers/errorpage/handler.go
@@ -1,85 +1,38 @@
package errorpage
import (
- "math/rand"
- "sort"
- "time"
-
- "github.com/pkg/errors"
- "github.com/tarampampam/error-pages/internal/http/common"
- "github.com/tarampampam/error-pages/internal/tpl"
"github.com/valyala/fasthttp"
)
-const (
- UseRandom = "random"
- UseRandomOnEachRequest = "i-said-random"
+type (
+ errorsPager interface {
+ // GetPage with passed template name and error code.
+ GetPage(templateName, code string) ([]byte, error)
+ }
+
+ templatePicker interface {
+ // Pick the template name for responding.
+ Pick() string
+ }
)
// NewHandler creates handler for error pages serving.
-func NewHandler(
- templateName string,
- templates map[string][]byte,
- codes map[string]tpl.Annotator,
-) (fasthttp.RequestHandler, error) {
- if len(templates) == 0 {
- return nil, errors.New("empty templates map")
- }
-
- var (
- rnd = rand.New(rand.NewSource(time.Now().UnixNano())) //nolint:gosec
- templateNames = templateTames(templates)
- )
-
- if templateName == "" { // on empty template name
- templateName = templateNames[0] // pick the first
- } else if templateName == UseRandom { // on "random" template name
- templateName = templateNames[rnd.Intn(len(templateNames))] // pick the randomized
- }
-
- if _, found := templates[templateName]; !found && templateName != UseRandomOnEachRequest {
- return nil, errors.New("wrong template name passed")
- }
-
- var pages = tpl.NewErrors(templates, codes)
-
+func NewHandler(e errorsPager, p templatePicker) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
- var useTemplate = templateName // default
-
- if templateName == UseRandomOnEachRequest {
- useTemplate = templateNames[rnd.Intn(len(templateNames))] // pick the randomized
- }
+ ctx.SetContentType("text/plain; charset=utf-8") // default content type
if code, ok := ctx.UserValue("code").(string); ok {
- if content, err := pages.Get(useTemplate, code); err == nil {
+ if content, err := e.GetPage(p.Pick(), code); err == nil {
ctx.SetStatusCode(fasthttp.StatusOK)
ctx.SetContentType("text/html; charset=utf-8")
_, _ = ctx.Write(content)
} else {
- common.HandleInternalHTTPError(
- ctx,
- fasthttp.StatusNotFound,
- "requested code not available: "+err.Error(),
- )
+ ctx.SetStatusCode(fasthttp.StatusNotFound)
+ _, _ = ctx.WriteString("requested code not available: " + err.Error()) // TODO customize the output?
}
} else { // will never happen
- common.HandleInternalHTTPError(
- ctx,
- fasthttp.StatusInternalServerError,
- "cannot extract requested code from the request",
- )
+ ctx.SetStatusCode(fasthttp.StatusInternalServerError)
+ _, _ = ctx.WriteString("cannot extract requested code from the request") // TODO customize the output?
}
- }, nil
-}
-
-func templateTames(templates map[string][]byte) []string {
- var templateNames = make([]string, 0, len(templates))
-
- for name := range templates {
- templateNames = append(templateNames, name)
}
-
- sort.Strings(templateNames)
-
- return templateNames
}
diff --git a/internal/http/handlers/index/handler.go b/internal/http/handlers/index/handler.go
new file mode 100644
index 0000000..fceff42
--- /dev/null
+++ b/internal/http/handlers/index/handler.go
@@ -0,0 +1,36 @@
+package index
+
+import (
+ "github.com/valyala/fasthttp"
+)
+
+type (
+ errorsPager interface {
+ // GetPage with passed template name and error code.
+ GetPage(templateName, code string) ([]byte, error)
+ }
+
+ templatePicker interface {
+ // Pick the template name for responding.
+ Pick() string
+ }
+)
+
+// NewHandler creates handler for the index page serving.
+func NewHandler(e errorsPager, p templatePicker, defaultPageCode string) fasthttp.RequestHandler {
+ return func(ctx *fasthttp.RequestCtx) {
+ content, err := e.GetPage(p.Pick(), defaultPageCode)
+
+ if err == nil {
+ ctx.SetContentType("text/html; charset=utf-8")
+ ctx.SetStatusCode(fasthttp.StatusOK)
+ _, _ = ctx.Write(content)
+
+ return
+ }
+
+ ctx.SetContentType("text/plain; charset=utf-8")
+ ctx.SetStatusCode(fasthttp.StatusNotAcceptable)
+ _, _ = ctx.WriteString("default page code " + defaultPageCode + " is not available: " + err.Error())
+ }
+}
diff --git a/internal/http/common/handlers_test.go b/internal/http/handlers/index/handler_test.go
similarity index 85%
rename from internal/http/common/handlers_test.go
rename to internal/http/handlers/index/handler_test.go
index 56140ff..4543537 100644
--- a/internal/http/common/handlers_test.go
+++ b/internal/http/handlers/index/handler_test.go
@@ -1,4 +1,4 @@
-package common_test
+package index_test
import "testing"
diff --git a/internal/http/handlers/notfound/handler.go b/internal/http/handlers/notfound/handler.go
new file mode 100644
index 0000000..d531cfa
--- /dev/null
+++ b/internal/http/handlers/notfound/handler.go
@@ -0,0 +1,14 @@
+package notfound
+
+import (
+ "github.com/valyala/fasthttp"
+)
+
+// NewHandler creates handler missing requests handling.
+func NewHandler() fasthttp.RequestHandler {
+ return func(ctx *fasthttp.RequestCtx) {
+ ctx.SetContentType("text/plain; charset=utf-8")
+ ctx.SetStatusCode(fasthttp.StatusNotFound)
+ _, _ = ctx.WriteString("Wrong request URL. Error pages are available at the following URLs: /{code}.html")
+ }
+}
diff --git a/internal/http/handlers/notfound/handler_test.go b/internal/http/handlers/notfound/handler_test.go
new file mode 100644
index 0000000..3daed4d
--- /dev/null
+++ b/internal/http/handlers/notfound/handler_test.go
@@ -0,0 +1,7 @@
+package notfound_test
+
+import "testing"
+
+func TestNothing(t *testing.T) {
+ t.Skip("tests for this package have not been implemented yet")
+}
diff --git a/internal/http/server.go b/internal/http/server.go
index f3f945f..5b1da05 100644
--- a/internal/http/server.go
+++ b/internal/http/server.go
@@ -1,7 +1,6 @@
package http
import (
- "context"
"strconv"
"time"
@@ -10,8 +9,9 @@ import (
"github.com/tarampampam/error-pages/internal/http/common"
errorpageHandler "github.com/tarampampam/error-pages/internal/http/handlers/errorpage"
healthzHandler "github.com/tarampampam/error-pages/internal/http/handlers/healthz"
+ indexHandler "github.com/tarampampam/error-pages/internal/http/handlers/index"
+ notfoundHandler "github.com/tarampampam/error-pages/internal/http/handlers/notfound"
versionHandler "github.com/tarampampam/error-pages/internal/http/handlers/version"
- "github.com/tarampampam/error-pages/internal/tpl"
"github.com/tarampampam/error-pages/internal/version"
"github.com/valyala/fasthttp"
"go.uber.org/zap"
@@ -23,9 +23,9 @@ type Server struct {
}
const (
- defaultWriteTimeout = time.Second * 7
- defaultReadTimeout = time.Second * 7
- defaultIdleTimeout = time.Second * 15
+ defaultWriteTimeout = time.Second * 4
+ defaultReadTimeout = time.Second * 4
+ defaultIdleTimeout = time.Second * 6
)
func NewServer(log *zap.Logger) Server {
@@ -52,55 +52,28 @@ func (s *Server) Start(ip string, port uint16) error {
return s.fast.ListenAndServe(ip + ":" + strconv.Itoa(int(port)))
}
+type (
+ errorsPager interface {
+ // GetPage with passed template name and error code.
+ GetPage(templateName, code string) ([]byte, error)
+ }
+
+ templatePicker interface {
+ // Pick the template name for responding.
+ Pick() string
+ }
+)
+
// Register server routes, middlewares, etc.
// Router docs:
-func (s *Server) Register(
- templateName string,
- templates map[string][]byte,
- codes map[string]tpl.Annotator,
-) error {
- s.router.GET("/", func(ctx *fasthttp.RequestCtx) {
- common.HandleInternalHTTPError(
- ctx,
- fasthttp.StatusNotFound,
- "Hi there! Error pages are available at the following URLs: /{code}.html",
- )
- })
-
- s.router.NotFound = func(ctx *fasthttp.RequestCtx) {
- common.HandleInternalHTTPError(
- ctx,
- fasthttp.StatusNotFound,
- "Wrong request URL. Error pages are available at the following URLs: /{code}.html",
- )
- }
-
+func (s *Server) Register(errorsPager errorsPager, templatePicker templatePicker, defaultPageCode string) {
+ s.router.GET("/", indexHandler.NewHandler(errorsPager, templatePicker, defaultPageCode))
s.router.GET("/version", versionHandler.NewHandler(version.Version()))
s.router.ANY("/health/live", healthzHandler.NewHandler(checkers.NewLiveChecker()))
+ s.router.GET("/{code}.html", errorpageHandler.NewHandler(errorsPager, templatePicker))
- if h, err := errorpageHandler.NewHandler(templateName, templates, codes); err != nil {
- return err
- } else {
- s.router.GET("/{code}.html", h)
- }
-
- return nil
+ s.router.NotFound = notfoundHandler.NewHandler()
}
// Stop server.
-func (s *Server) Stop(timeout time.Duration) error {
- ctx, cancel := context.WithTimeout(context.Background(), timeout) // TODO replace with simple time.After
- defer cancel()
-
- ch := make(chan error, 1) // channel for server stopping error
-
- go func() { defer close(ch); ch <- s.fast.Shutdown() }()
-
- select {
- case err := <-ch:
- return err
-
- case <-ctx.Done():
- return ctx.Err()
- }
-}
+func (s *Server) Stop() error { return s.fast.Shutdown() }
diff --git a/internal/pick/strings_slice.go b/internal/pick/strings_slice.go
new file mode 100644
index 0000000..2226e1b
--- /dev/null
+++ b/internal/pick/strings_slice.go
@@ -0,0 +1,64 @@
+package pick
+
+import (
+ "math/rand"
+ "time"
+)
+
+type pickMode byte
+
+const (
+ First pickMode = 1 + iota // Always pick the first element
+ RandomOnce // Pick random element once (any future Pick calls will return the same element)
+ RandomEveryTime // Always Pick the random element
+)
+
+type StringsSlice struct {
+ items []string
+ mode pickMode
+ lastUsedIdx int // -1 when unset, needed for RandomOnce mode
+ rnd *rand.Rand // will be nil for the First mode
+}
+
+// NewStringsSlice creates new StringsSlice.
+func NewStringsSlice(items []string, mode pickMode) *StringsSlice {
+ var rnd *rand.Rand
+
+ if mode != First {
+ rnd = rand.New(rand.NewSource(time.Now().UnixNano())) //nolint:gosec
+ }
+
+ return &StringsSlice{
+ items: items,
+ mode: mode,
+ lastUsedIdx: -1,
+ rnd: rnd,
+ }
+}
+
+// Pick an element from the strings slice.
+func (s *StringsSlice) Pick() string {
+ if l := len(s.items); l == 0 {
+ return ""
+ } else if l == 1 {
+ return s.items[0]
+ }
+
+ switch s.mode {
+ case First:
+ return s.items[0]
+
+ case RandomOnce:
+ if s.lastUsedIdx == -1 {
+ s.lastUsedIdx = s.rnd.Intn(len(s.items))
+ }
+
+ return s.items[s.lastUsedIdx]
+
+ case RandomEveryTime:
+ return s.items[s.rnd.Intn(len(s.items))]
+
+ default:
+ panic("pick: unsupported mode")
+ }
+}
diff --git a/internal/pick/strings_slice_test.go b/internal/pick/strings_slice_test.go
new file mode 100644
index 0000000..7b15d34
--- /dev/null
+++ b/internal/pick/strings_slice_test.go
@@ -0,0 +1,102 @@
+package pick_test
+
+import (
+ "math/rand"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/tarampampam/error-pages/internal/pick"
+)
+
+func TestStringsSlice_Pick_First(t *testing.T) {
+ for name, items := range map[string][]string{
+ "0 item": {},
+ "1 item": {"foo"},
+ "3 items": {"foo", "bar", "baz"},
+ } {
+ t.Run(name, func(t *testing.T) {
+ p := pick.NewStringsSlice(items, pick.First)
+
+ for i := 0; i < 100; i++ {
+ if len(items) == 0 {
+ assert.Equal(t, "", p.Pick())
+ } else {
+ assert.Equal(t, "foo", p.Pick())
+ }
+ }
+ })
+ }
+}
+
+func TestStringsSlice_Pick_RandomOnce(t *testing.T) {
+ p := pick.NewStringsSlice([]string{}, pick.RandomOnce)
+ assert.Equal(t, "", p.Pick())
+
+ p = pick.NewStringsSlice([]string{"foo"}, pick.RandomOnce)
+ assert.Equal(t, "foo", p.Pick())
+
+ dataSet := randomStringsSlice(t, 2048) // if this test will fail - Increase this value
+ p = pick.NewStringsSlice(dataSet, pick.RandomOnce)
+ picked := p.Pick()
+
+ assert.NotEqual(t, dataSet[0], p.Pick())
+
+ for i := 0; i < 32; i++ {
+ assert.Equal(t, picked, p.Pick())
+ }
+}
+
+func TestStringsSlice_Pick_RandomEveryTime(t *testing.T) {
+ p := pick.NewStringsSlice([]string{}, pick.RandomEveryTime)
+ assert.Equal(t, "", p.Pick())
+
+ p = pick.NewStringsSlice([]string{"foo"}, pick.RandomEveryTime)
+ assert.Equal(t, "foo", p.Pick())
+
+ dataSet := randomStringsSlice(t, 2048) // if this test will fail - Increase this value
+ p = pick.NewStringsSlice(dataSet, pick.RandomEveryTime)
+
+ lastPicked := p.Pick()
+
+ for i := 0; i < 32; i++ {
+ picked := p.Pick()
+
+ assert.NotEqual(t, lastPicked, picked)
+ lastPicked = picked
+ }
+}
+
+func TestStringsSlice_Pick_UnsupportedMode(t *testing.T) {
+ p := pick.NewStringsSlice([]string{}, 255)
+ assert.Equal(t, "", p.Pick())
+
+ p = pick.NewStringsSlice([]string{"foo"}, 255)
+ assert.Equal(t, "foo", p.Pick())
+
+ p = pick.NewStringsSlice([]string{"foo", "bar"}, 255)
+
+ assert.Panics(t, func() { p.Pick() })
+}
+
+func randomStringsSlice(t *testing.T, itemsCount int) []string {
+ t.Helper()
+
+ var (
+ rnd = rand.New(rand.NewSource(time.Now().UnixNano())) //nolint:gosec
+ items = make([]string, itemsCount)
+ letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-+=")
+ )
+
+ for i := 0; i < len(items); i++ {
+ b := make([]rune, 32)
+
+ for j := range b {
+ b[j] = letters[rnd.Intn(len(letters))]
+ }
+
+ items[i] = string(b)
+ }
+
+ return items
+}
diff --git a/internal/tpl/error_pages.go b/internal/tpl/error_pages.go
new file mode 100644
index 0000000..c15d93a
--- /dev/null
+++ b/internal/tpl/error_pages.go
@@ -0,0 +1,136 @@
+package tpl
+
+import (
+ "bytes"
+ "sync"
+
+ "github.com/pkg/errors"
+)
+
+type (
+ // ErrorPages is a error page templates generator.
+ ErrorPages struct {
+ mu sync.RWMutex
+ templates map[string][]byte // map[template_name]raw_content
+ pages map[string]*pageProperties // map[page_code]props
+ state map[string]map[string][]byte // map[template_name]map[page_code]content
+ }
+
+ pageProperties struct {
+ message, description string
+ }
+)
+
+var (
+ ErrUnknownTemplate = errors.New("unknown template") // unknown template
+ ErrUnknownPageCode = errors.New("unknown page code") // unknown page code
+)
+
+// NewErrorPages creates ErrorPages templates generator.
+func NewErrorPages() ErrorPages {
+ return ErrorPages{
+ templates: make(map[string][]byte),
+ pages: make(map[string]*pageProperties),
+ state: make(map[string]map[string][]byte),
+ }
+}
+
+// AddTemplate to the generator. Template can contain the special placeholders for the error code, message and
+// description:
+// {{ code }} - for the code
+// {{ message }} - for the message
+// {{ description }} - for the description
+func (e *ErrorPages) AddTemplate(templateName string, content []byte) {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+
+ e.templates[templateName] = content
+ e.state[templateName] = make(map[string][]byte)
+
+ for code, props := range e.pages { // update the state
+ e.state[templateName][code] = e.makeReplaces(content, code, props.message, props.description)
+ }
+}
+
+// AddPage with the passed code, message and description. This page will ba available for the all templates.
+func (e *ErrorPages) AddPage(code, message, description string) {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+
+ e.pages[code] = &pageProperties{message, description}
+
+ for templateName, content := range e.templates { // update the state
+ e.state[templateName][code] = e.makeReplaces(content, code, message, description)
+ }
+}
+
+// GetPage with passed template name and error code.
+func (e *ErrorPages) GetPage(templateName, code string) (content []byte, err error) {
+ e.mu.RLock()
+ defer e.mu.RUnlock()
+
+ if pages, templateExists := e.state[templateName]; templateExists {
+ if c, pageExists := pages[code]; pageExists {
+ content = c
+ } else {
+ err = ErrUnknownPageCode
+ }
+ } else {
+ err = ErrUnknownTemplate
+ }
+
+ return
+}
+
+// IteratePages will call the passed function for each page and template.
+func (e *ErrorPages) IteratePages(fn func(template, code string, content []byte) error) error {
+ e.mu.RLock()
+ defer e.mu.RUnlock()
+
+ for tplName, codes := range e.state {
+ for code, content := range codes {
+ if err := fn(tplName, code, content); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+const (
+ tknCode byte = iota + 1
+ tknMessage
+ tknDescription
+)
+
+var tknSets = map[byte][][]byte{ //nolint:gochecknoglobals
+ tknCode: {[]byte("{{code}}"), []byte("{{ code }}")},
+ tknMessage: {[]byte("{{message}}"), []byte("{{ message }}")},
+ tknDescription: {[]byte("{{description}}"), []byte("{{ description }}")},
+}
+
+func (e *ErrorPages) makeReplaces(where []byte, code, message, description string) []byte {
+ for tkn, set := range tknSets {
+ var replaceWith []byte
+
+ switch tkn {
+ case tknCode:
+ replaceWith = []byte(code)
+ case tknMessage:
+ replaceWith = []byte(message)
+ case tknDescription:
+ replaceWith = []byte(description)
+ default:
+ panic("tpl: unsupported token") // this is like a fuse, will never occur during normal usage
+ }
+
+ if len(replaceWith) > 0 {
+ for i := 0; i < len(set); i++ {
+ where = bytes.ReplaceAll(where, set[i], replaceWith)
+ }
+ }
+ }
+
+ return where
+}
diff --git a/internal/tpl/error_pages_test.go b/internal/tpl/error_pages_test.go
new file mode 100644
index 0000000..810c506
--- /dev/null
+++ b/internal/tpl/error_pages_test.go
@@ -0,0 +1,132 @@
+package tpl_test
+
+import (
+ "errors"
+ "sync"
+ "testing"
+
+ "github.com/tarampampam/error-pages/internal/tpl"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestErrorPages_GetPage(t *testing.T) {
+ e := tpl.NewErrorPages()
+
+ e.AddTemplate("foo", []byte("{{code}}: {{ message }} {{description}}"))
+ e.AddPage("200", "ok", "all is ok")
+ e.AddTemplate("bar", []byte("{{ code }} _ {{message}} ({{ description }})"))
+ e.AddPage("201", "lorem", "ipsum")
+
+ content, err := e.GetPage("foo", "200")
+ assert.NoError(t, err)
+ assert.Equal(t, "200: ok all is ok", string(content))
+
+ content, err = e.GetPage("foo", "201")
+ assert.NoError(t, err)
+ assert.Equal(t, "201: lorem ipsum", string(content))
+
+ content, err = e.GetPage("bar", "200")
+ assert.NoError(t, err)
+ assert.Equal(t, "200 _ ok (all is ok)", string(content))
+
+ content, err = e.GetPage("bar", "201")
+ assert.NoError(t, err)
+ assert.Equal(t, "201 _ lorem (ipsum)", string(content))
+
+ content, err = e.GetPage("foo", "666")
+ assert.ErrorIs(t, err, tpl.ErrUnknownPageCode)
+ assert.Nil(t, content)
+
+ content, err = e.GetPage("baz", "200")
+ assert.ErrorIs(t, err, tpl.ErrUnknownTemplate)
+ assert.Nil(t, content)
+}
+
+func TestErrorPages_GetPage_Concurrent(t *testing.T) {
+ e := tpl.NewErrorPages()
+
+ init := func() {
+ e.AddTemplate("foo", []byte("{{ code }}: {{ message }} {{ description }}"))
+ e.AddPage("200", "ok", "all is ok")
+ e.AddPage("201", "lorem", "ipsum")
+ }
+
+ var wg sync.WaitGroup
+
+ init()
+
+ for i := 0; i < 1234; i++ {
+ wg.Add(2)
+
+ go func() {
+ defer wg.Done()
+
+ init() // make re-initialization
+ }()
+
+ go func() {
+ defer wg.Done()
+
+ content, err := e.GetPage("foo", "200")
+ assert.NoError(t, err)
+ assert.Equal(t, "200: ok all is ok", string(content))
+
+ content, err = e.GetPage("foo", "201")
+ assert.NoError(t, err)
+ assert.Equal(t, "201: lorem ipsum", string(content))
+
+ content, err = e.GetPage("foo", "666")
+ assert.Error(t, err)
+ assert.Nil(t, content)
+
+ content, err = e.GetPage("bar", "200")
+ assert.Error(t, err)
+ assert.Nil(t, content)
+ }()
+ }
+
+ wg.Wait()
+}
+
+func TestErrorPages_IteratePages(t *testing.T) {
+ e := tpl.NewErrorPages()
+
+ e.AddTemplate("foo", []byte("{{ code }}: {{ message }} {{ description }}"))
+ e.AddTemplate("bar", []byte("{{ code }}: {{ message }} {{ description }}"))
+ e.AddPage("200", "ok", "all is ok")
+ e.AddPage("400", "Bad Request", "")
+
+ visited := make(map[string]map[string]bool) // map[template]codes
+
+ assert.NoError(t, e.IteratePages(func(template, code string, content []byte) error {
+ if _, ok := visited[template]; !ok {
+ visited[template] = make(map[string]bool)
+ }
+
+ visited[template][code] = true
+
+ assert.NotNil(t, content)
+
+ return nil
+ }))
+
+ assert.Len(t, visited, 2)
+ assert.Len(t, visited["foo"], 2)
+ assert.True(t, visited["foo"]["200"])
+ assert.True(t, visited["foo"]["400"])
+ assert.Len(t, visited["bar"], 2)
+ assert.True(t, visited["bar"]["200"])
+ assert.True(t, visited["bar"]["400"])
+}
+
+func TestErrorPages_IteratePages_WillReturnTheError(t *testing.T) {
+ e := tpl.NewErrorPages()
+
+ e.AddTemplate("foo", []byte("{{ code }}: {{ message }} {{ description }}"))
+ e.AddPage("200", "ok", "all is ok")
+
+ assert.EqualError(t, e.IteratePages(func(template, code string, content []byte) error {
+ return errors.New("foo error")
+ }), "foo error")
+}
diff --git a/internal/tpl/errors.go b/internal/tpl/errors.go
deleted file mode 100644
index 0413332..0000000
--- a/internal/tpl/errors.go
+++ /dev/null
@@ -1,102 +0,0 @@
-package tpl
-
-import (
- "errors"
- "sync"
-)
-
-// Annotator allows to annotate error code.
-type Annotator struct {
- Message string
- Description string
-}
-
-// Errors is a "cached storage" for the rendered error pages for the different templates and codes.
-type Errors struct {
- templates map[string][]byte
- codes map[string]Annotator
-
- cacheMu sync.RWMutex
- cache map[string]map[string][]byte // map[template]map[code]content
-}
-
-// NewErrors creates new Errors.
-func NewErrors(templates map[string][]byte, codes map[string]Annotator) *Errors {
- return &Errors{
- templates: templates,
- codes: codes,
- cache: make(map[string]map[string][]byte),
- }
-}
-
-func (e *Errors) existsInCache(template, code string) ([]byte, bool) {
- e.cacheMu.RLock()
- defer e.cacheMu.RUnlock()
-
- if codes, tplOk := e.cache[template]; tplOk {
- if content, codeOk := codes[code]; codeOk {
- return content, true
- }
- }
-
- return nil, false
-}
-
-func (e *Errors) putInCache(template, code string) error {
- if _, ok := e.templates[template]; !ok {
- return errors.New("template \"" + template + "\" does not exists")
- }
-
- if _, ok := e.codes[code]; !ok {
- return errors.New("code \"" + code + "\" does not exists")
- }
-
- e.cacheMu.Lock()
- defer e.cacheMu.Unlock()
-
- if _, ok := e.cache[template]; !ok {
- e.cache[template] = make(map[string][]byte)
- }
-
- e.cache[template][code] = Replace(e.templates[template], Replaces{
- Code: code,
- Message: e.codes[code].Message,
- Description: e.codes[code].Description,
- })
-
- return nil
-}
-
-// Get the rendered error page content.
-func (e *Errors) Get(template, code string) ([]byte, error) {
- if content, ok := e.existsInCache(template, code); ok {
- return content, nil
- }
-
- if err := e.putInCache(template, code); err != nil {
- return nil, err
- }
-
- e.cacheMu.RLock()
- defer e.cacheMu.RUnlock()
-
- return e.cache[template][code], nil
-}
-
-// VisitAll allows to iterate all possible error pages and templates.
-func (e *Errors) VisitAll(fn func(template, code string, content []byte) error) error {
- for tpl := range e.templates {
- for code := range e.codes {
- content, err := e.Get(tpl, code)
- if err != nil {
- return err // will never happen
- }
-
- if err = fn(tpl, code, content); err != nil {
- return err
- }
- }
- }
-
- return nil
-}
diff --git a/internal/tpl/errors_test.go b/internal/tpl/errors_test.go
deleted file mode 100644
index 681d58f..0000000
--- a/internal/tpl/errors_test.go
+++ /dev/null
@@ -1,106 +0,0 @@
-package tpl_test
-
-import (
- "errors"
- "sync"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/tarampampam/error-pages/internal/tpl"
-)
-
-func TestErrors_Get(t *testing.T) {
- e := tpl.NewErrors(
- map[string][]byte{"foo": []byte("{{ code }}: {{ message }} {{ description }}")},
- map[string]tpl.Annotator{"200": {"ok", "all is ok"}},
- )
-
- content, err := e.Get("foo", "200")
- assert.NoError(t, err)
- assert.Equal(t, "200: ok all is ok", string(content))
-
- content, err = e.Get("foo", "666")
- assert.EqualError(t, err, "code \"666\" does not exists")
- assert.Nil(t, content)
-
- content, err = e.Get("bar", "200")
- assert.EqualError(t, err, "template \"bar\" does not exists")
- assert.Nil(t, content)
-}
-
-func TestErrors_GetConcurrent(t *testing.T) {
- e := tpl.NewErrors(
- map[string][]byte{"foo": []byte("{{ code }}: {{ message }} {{ description }}")},
- map[string]tpl.Annotator{"200": {"ok", "all is ok"}},
- )
-
- var wg sync.WaitGroup
-
- for i := 0; i < 1234; i++ {
- wg.Add(1)
-
- go func() {
- defer wg.Done()
-
- content, err := e.Get("foo", "200")
- assert.NoError(t, err)
- assert.Equal(t, "200: ok all is ok", string(content))
-
- content, err = e.Get("foo", "666")
- assert.Error(t, err)
- assert.Nil(t, content)
- }()
- }
-
- wg.Wait()
-}
-
-func TestErrors_VisitAll(t *testing.T) {
- e := tpl.NewErrors(
- map[string][]byte{
- "foo": []byte("{{ code }}: {{ message }} {{ description }}"),
- "bar": []byte("{{ code }}: {{ message }} {{ description }}"),
- },
- map[string]tpl.Annotator{
- "200": {"ok", "all is ok"},
- "400": {"Bad Request", "The server did not understand the request"},
- },
- )
-
- visited := make(map[string]map[string]bool) // map[template]codes
-
- assert.NoError(t, e.VisitAll(func(template, code string, content []byte) error {
- if _, ok := visited[template]; !ok {
- visited[template] = make(map[string]bool)
- }
-
- visited[template][code] = true
-
- assert.NotNil(t, content)
-
- return nil
- }))
-
- assert.Len(t, visited, 2)
- assert.Len(t, visited["foo"], 2)
- assert.True(t, visited["foo"]["200"])
- assert.True(t, visited["foo"]["400"])
- assert.Len(t, visited["bar"], 2)
- assert.True(t, visited["bar"]["200"])
- assert.True(t, visited["bar"]["400"])
-}
-
-func TestErrors_VisitAllWillReturnTheError(t *testing.T) {
- e := tpl.NewErrors(
- map[string][]byte{
- "foo": []byte("{{ code }}: {{ message }} {{ description }}"),
- },
- map[string]tpl.Annotator{
- "200": {"ok", "all is ok"},
- },
- )
-
- assert.EqualError(t, e.VisitAll(func(template, code string, content []byte) error {
- return errors.New("foo error")
- }), "foo error")
-}
diff --git a/internal/tpl/replace.go b/internal/tpl/replace.go
deleted file mode 100644
index 704b707..0000000
--- a/internal/tpl/replace.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package tpl
-
-import "bytes"
-
-type Replaces struct {
- Code string
- Message string
- Description string
-}
-
-const (
- tknCode byte = iota + 1
- tknMessage
- tknDescription
-)
-
-var tknSets = map[byte][][]byte{ //nolint:gochecknoglobals
- tknCode: {[]byte("{{code}}"), []byte("{{ code }}")},
- tknMessage: {[]byte("{{message}}"), []byte("{{ message }}")},
- tknDescription: {[]byte("{{description}}"), []byte("{{ description }}")},
-}
-
-// Replace found tokens in the incoming slice with passed tokens.
-func Replace(in []byte, re Replaces) []byte {
- for tkn, set := range tknSets {
- var replaceWith []byte
-
- switch tkn {
- case tknCode:
- replaceWith = []byte(re.Code)
- case tknMessage:
- replaceWith = []byte(re.Message)
- case tknDescription:
- replaceWith = []byte(re.Description)
- default:
- panic("tpl: unsupported token")
- }
-
- if len(replaceWith) > 0 {
- for i := 0; i < len(set); i++ {
- in = bytes.ReplaceAll(in, set[i], replaceWith)
- }
- }
- }
-
- return in
-}
diff --git a/internal/tpl/replace_test.go b/internal/tpl/replace_test.go
deleted file mode 100644
index e25bc05..0000000
--- a/internal/tpl/replace_test.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package tpl_test
-
-import (
- "fmt"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/tarampampam/error-pages/internal/tpl"
-)
-
-func ExampleReplace() {
- var in = []byte("{{ code }}: {{message}} ({{ description }})")
-
- fmt.Println(string(tpl.Replace(in, tpl.Replaces{
- Code: "400",
- Message: "Bad Request",
- Description: "The server did not understand the request",
- })))
-
- // Output:
- // 400: Bad Request (The server did not understand the request)
-}
-
-func TestReplace(t *testing.T) {
- for name, tt := range map[string]struct {
- giveIn []byte
- giveRe tpl.Replaces
- wantResult []byte
- }{
- "common": {
- giveIn: []byte("-- {{ code }} {{code}} __ {{message}} {{ description }} "),
- giveRe: tpl.Replaces{
- Code: "123",
- Message: "message",
- Description: "desc",
- },
- wantResult: []byte("-- 123 123 __ message desc "),
- },
- "alpha and underline in the code": {
- giveIn: []byte("\t{{ code }}\t"),
- giveRe: tpl.Replaces{
- Code: " qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_ ",
- },
- wantResult: []byte("\t qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_ \t"),
- },
- } {
- tt := tt
-
- t.Run(name, func(t *testing.T) {
- assert.Equal(t, tt.wantResult, tpl.Replace(tt.giveIn, tt.giveRe))
- })
- }
-}
-
-func BenchmarkReplace(b *testing.B) {
- b.ReportAllocs()
- b.ResetTimer()
-
- for i := 0; i < b.N; i++ {
- tpl.Replace([]byte("-- {{ code }} {{code}} __ {{message}} {{ description }} "), tpl.Replaces{
- Code: "123",
- Message: "message",
- Description: "desc",
- })
- }
-}