mirror of
https://github.com/tarampampam/error-pages.git
synced 2024-08-30 18:22:40 +00:00
Allow to set default error page (#30)
This commit is contained in:
parent
6b17d3eb7d
commit
bb17027cc9
3
.github/workflows/tests.yml
vendored
3
.github/workflows/tests.yml
vendored
@ -200,10 +200,11 @@ jobs: # Docs: <https://git.io/JvxXE>
|
||||
- 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()
|
||||
|
12
CHANGELOG.md
12
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
|
||||
|
@ -64,7 +64,8 @@ USER appuser:appuser
|
||||
WORKDIR /opt
|
||||
|
||||
ENV LISTEN_PORT="8080" \
|
||||
TEMPLATE_NAME="ghost"
|
||||
TEMPLATE_NAME="ghost" \
|
||||
DEFAULT_ERROR_PAGE="404"
|
||||
|
||||
# Docs: <https://docs.docker.com/engine/reference/builder/#healthcheck>
|
||||
HEALTHCHECK --interval=7s --timeout=2s CMD [ \
|
||||
|
70
README.md
70
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 \
|
||||
</p>
|
||||
</details>
|
||||
|
||||
## 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
|
||||
<details>
|
||||
<summary><strong>HTTP server</strong></summary>
|
||||
|
||||
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
|
||||
```
|
||||
</details>
|
||||
|
||||
### Generator
|
||||
<details>
|
||||
<summary><strong>Generator</strong></summary>
|
||||
|
||||
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
|
||||
```
|
||||
</details>
|
||||
|
||||
### Static error pages
|
||||
<details>
|
||||
<summary><strong>Static error pages</strong></summary>
|
||||
|
||||
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
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Custom error pages for your image with nginx</strong></summary>
|
||||
|
||||
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
|
||||
</details>
|
||||
|
||||
## Custom error pages for [Traefik][link_traefik]
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
return err
|
||||
} else if len(templates) == 0 {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
var (
|
||||
errorPages = tpl.NewErrorPages()
|
||||
templateNames = make([]string, 0) // slice with all possible template names
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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}
|
||||
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
|
||||
|
@ -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"
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
1
internal/env/env.go
vendored
1
internal/env/env.go
vendored
@ -10,6 +10,7 @@ const (
|
||||
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.
|
||||
|
2
internal/env/env_test.go
vendored
2
internal/env/env_test.go
vendored
@ -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 {
|
||||
|
@ -1,35 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
const internalErrorPattern = `<!DOCTYPE html>
|
||||
<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>Internal error occurred</title>
|
||||
<style>
|
||||
html,body {background-color: #0e0e0e;color:#fff;font-family:'Nunito',sans-serif;height:100%;margin:0}
|
||||
.message {height:100%;align-items:center;display:flex;justify-content:center;position:relative;font-size:1.4em}
|
||||
img {padding-right: .4em}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="message">
|
||||
<img src="https://hsto.org/webt/fs/sx/gt/fssxgtssfg689qxboqvjil5yz8g.png" alt="logo" height="32">
|
||||
<p>{{ message }}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
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))
|
||||
}
|
@ -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)),
|
||||
|
@ -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
|
||||
}
|
||||
|
36
internal/http/handlers/index/handler.go
Normal file
36
internal/http/handlers/index/handler.go
Normal file
@ -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())
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package common_test
|
||||
package index_test
|
||||
|
||||
import "testing"
|
||||
|
14
internal/http/handlers/notfound/handler.go
Normal file
14
internal/http/handlers/notfound/handler.go
Normal file
@ -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")
|
||||
}
|
||||
}
|
7
internal/http/handlers/notfound/handler_test.go
Normal file
7
internal/http/handlers/notfound/handler_test.go
Normal file
@ -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")
|
||||
}
|
@ -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: <https://github.com/fasthttp/router>
|
||||
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() }
|
||||
|
64
internal/pick/strings_slice.go
Normal file
64
internal/pick/strings_slice.go
Normal file
@ -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")
|
||||
}
|
||||
}
|
102
internal/pick/strings_slice_test.go
Normal file
102
internal/pick/strings_slice_test.go
Normal file
@ -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
|
||||
}
|
136
internal/tpl/error_pages.go
Normal file
136
internal/tpl/error_pages.go
Normal file
@ -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
|
||||
}
|
132
internal/tpl/error_pages_test.go
Normal file
132
internal/tpl/error_pages_test.go
Normal file
@ -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")
|
||||
}
|
@ -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
|
||||
}
|
@ -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")
|
||||
}
|
@ -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
|
||||
}
|
@ -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",
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user