mirror of
https://github.com/tarampampam/error-pages.git
synced 2024-08-30 18:22:40 +00:00
wip: 🔕 temporary commit
This commit is contained in:
parent
1b94bc367c
commit
a52dbde00c
@ -28,7 +28,7 @@ $ error-pages [GLOBAL FLAGS] serve [COMMAND FLAGS] [ARGUMENTS...]
|
||||
The following flags are supported:
|
||||
|
||||
| Name | Description | Default value | Environment variables |
|
||||
|------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------:|:----------------------:|
|
||||
|------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------:|:-------------------------:|
|
||||
| `--listen="…"` (`-l`) | IP (v4 or v6) address to listen on | `0.0.0.0` | `LISTEN_ADDR` |
|
||||
| `--port="…"` (`-p`) | TCP port number | `8080` | `LISTEN_PORT` |
|
||||
| `--add-template="…"` | to add a new template, provide the path to the file using this flag (the filename without the extension will be used as the template name) | `[]` | *none* |
|
||||
@ -38,10 +38,10 @@ The following flags are supported:
|
||||
| `--template-name="…"` (`-t`) | name of the template to use for rendering error pages | `template-1` | `TEMPLATE_NAME` |
|
||||
| `--disable-l10n` | disable localization of error pages (if the template supports localization) | `false` | `DISABLE_L10N` |
|
||||
| `--default-error-page="…"` | the code of the default (index page, when a code is not specified) error page to render | `404` | `DEFAULT_ERROR_PAGE` |
|
||||
| `--default-http-code="…"` | the default (index page, when a code is not specified) HTTP response code | `404` | `DEFAULT_HTTP_CODE` |
|
||||
| `--send-same-http-code` | the HTTP response should have the same status code as the requested error page (by default, every response with an error page will have a status code of 200) | `false` | `SEND_SAME_HTTP_CODE` |
|
||||
| `--show-details` | show request details in the error page response (if supported by the template) | `false` | `SHOW_DETAILS` |
|
||||
| `--proxy-headers="…"` | listed here HTTP headers will be proxied from the original request to the error page response (comma-separated list) | `X-Request-Id,X-Trace-Id,X-Amzn-Trace-Id` | `PROXY_HTTP_HEADERS` |
|
||||
| `--read-buffer-size="…"` | customize the HTTP read buffer size (set per connection for reading requests, also limits the maximum header size; consider increasing it if your clients send multi-KB request URIs or multi-KB headers, such as large cookies) | `0` | `READ_BUFFER_SIZE` |
|
||||
| `--rotation-mode="…"` | templates automatic rotation mode (disabled/random-on-startup/random-on-each-request/random-daily/random-hourly) | `disabled` | `TEMPLATES_ROTATION_MODE` |
|
||||
|
||||
### `healthcheck` command (aliases: `chk`, `health`, `check`)
|
||||
|
||||
|
@ -74,7 +74,7 @@ func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
|
||||
defaultCodeToRenderFlag = cli.UintFlag{
|
||||
Name: "default-error-page",
|
||||
Usage: "the code of the default (index page, when a code is not specified) error page to render",
|
||||
Value: uint64(cfg.Default.CodeToRender),
|
||||
Value: uint64(cfg.DefaultCodeToRender),
|
||||
Sources: env("DEFAULT_ERROR_PAGE"),
|
||||
Validator: func(code uint64) error {
|
||||
if code > 999 { //nolint:mnd
|
||||
@ -85,12 +85,12 @@ func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
|
||||
},
|
||||
OnlyOnce: true,
|
||||
}
|
||||
defaultHTTPCodeFlag = cli.UintFlag{
|
||||
Name: "default-http-code",
|
||||
Usage: "the default (index page, when a code is not specified) HTTP response code",
|
||||
Value: uint64(cfg.Default.HttpCode),
|
||||
Sources: env("DEFAULT_HTTP_CODE"),
|
||||
Validator: defaultCodeToRenderFlag.Validator,
|
||||
sendSameHTTPCodeFlag = cli.BoolFlag{
|
||||
Name: "send-same-http-code",
|
||||
Usage: "the HTTP response should have the same status code as the requested error page (by default, " +
|
||||
"every response with an error page will have a status code of 200)",
|
||||
Value: cfg.RespondWithSameHTTPCode,
|
||||
Sources: env("SEND_SAME_HTTP_CODE"),
|
||||
OnlyOnce: true,
|
||||
}
|
||||
showDetailsFlag = cli.BoolFlag{
|
||||
@ -157,8 +157,8 @@ func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
|
||||
|
||||
cfg.TemplateName = c.String(templateNameFlag.Name)
|
||||
cfg.L10n.Disable = c.Bool(disableL10nFlag.Name)
|
||||
cfg.Default.CodeToRender = uint16(c.Uint(defaultCodeToRenderFlag.Name))
|
||||
cfg.Default.HttpCode = uint16(c.Uint(defaultHTTPCodeFlag.Name))
|
||||
cfg.DefaultCodeToRender = uint16(c.Uint(defaultCodeToRenderFlag.Name))
|
||||
cfg.RespondWithSameHTTPCode = c.Bool(sendSameHTTPCodeFlag.Name)
|
||||
cfg.RotationMode, _ = config.ParseRotationMode(c.String(rotationModeFlag.Name))
|
||||
cfg.ShowDetails = c.Bool(showDetailsFlag.Name)
|
||||
|
||||
@ -231,8 +231,8 @@ func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
|
||||
zap.String("XML format", cfg.Formats.XML),
|
||||
zap.String("template name", cfg.TemplateName),
|
||||
zap.Bool("disable localization", cfg.L10n.Disable),
|
||||
zap.Uint16("default code to render", cfg.Default.CodeToRender),
|
||||
zap.Uint16("default HTTP code", cfg.Default.HttpCode),
|
||||
zap.Uint16("default code to render", cfg.DefaultCodeToRender),
|
||||
zap.Bool("respond with the same HTTP code", cfg.RespondWithSameHTTPCode),
|
||||
zap.Bool("show details", cfg.ShowDetails),
|
||||
zap.Strings("proxy HTTP headers", cfg.ProxyHeaders),
|
||||
)
|
||||
@ -249,7 +249,7 @@ func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
|
||||
&templateNameFlag,
|
||||
&disableL10nFlag,
|
||||
&defaultCodeToRenderFlag,
|
||||
&defaultHTTPCodeFlag,
|
||||
&sendSameHTTPCodeFlag,
|
||||
&showDetailsFlag,
|
||||
&proxyHeadersListFlag,
|
||||
&rotationModeFlag,
|
||||
|
@ -37,15 +37,16 @@ type Config struct {
|
||||
Disable bool
|
||||
}
|
||||
|
||||
// Default contains default settings.
|
||||
Default struct {
|
||||
// CodeToRender is the code for the default error page to be displayed. It is used when the requested
|
||||
// DefaultCodeToRender is the code for the default error page to be displayed. It is used when the requested
|
||||
// code is not defined in the incoming request (i.e., the code to render as the index page).
|
||||
CodeToRender uint16
|
||||
DefaultCodeToRender uint16
|
||||
|
||||
// HTTPCode is the HTTP code to return when the requested code is not defined in the incoming request.
|
||||
HttpCode uint16
|
||||
}
|
||||
// RespondWithSameHTTPCode determines whether the response should have the same HTTP status code as the requested
|
||||
// error page.
|
||||
// In other words, if set to true and the requested error page has a code of 404, the HTTP response will also have
|
||||
// a status code of 404. If set to false, the HTTP response will have a status code of 200 regardless of the
|
||||
// requested error page's status code.
|
||||
RespondWithSameHTTPCode bool
|
||||
|
||||
// RotationMode allows to set the rotation mode for templates to switch between them automatically on startup,
|
||||
// on each request, daily, hourly and so on.
|
||||
@ -150,8 +151,7 @@ func New() Config {
|
||||
cfg.ProxyHeaders = slices.Clone(defaultProxyHeaders)
|
||||
|
||||
// set defaults
|
||||
cfg.Default.CodeToRender = 404
|
||||
cfg.Default.HttpCode = http.StatusNotFound
|
||||
cfg.DefaultCodeToRender = http.StatusNotFound
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -20,8 +21,7 @@ func TestNew(t *testing.T) {
|
||||
assert.True(t, len(cfg.Templates) >= 2)
|
||||
assert.NotEmpty(t, cfg.TemplateName)
|
||||
assert.True(t, cfg.Templates.Has(cfg.TemplateName))
|
||||
assert.Equal(t, uint16(404), cfg.Default.CodeToRender)
|
||||
assert.Equal(t, uint16(404), cfg.Default.HttpCode)
|
||||
assert.Equal(t, uint16(http.StatusNotFound), cfg.DefaultCodeToRender)
|
||||
})
|
||||
|
||||
t.Run("changing cfg1 should not affect cfg2", func(t *testing.T) {
|
||||
|
@ -1,12 +1,84 @@
|
||||
package error_page
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
func New() http.Handler {
|
||||
var body = []byte("error page")
|
||||
"gh.tarampamp.am/error-pages/internal/config"
|
||||
)
|
||||
|
||||
func New(cfg *config.Config) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write(body)
|
||||
var code uint16
|
||||
|
||||
if fromUrl, okUrl := ExtractCodeFromURL(r.URL.Path); okUrl {
|
||||
code = fromUrl
|
||||
} else if fromHeader, okHeaders := ExtractCodeFromHeaders(r.Header); okHeaders {
|
||||
code = fromHeader
|
||||
} else {
|
||||
code = cfg.DefaultCodeToRender
|
||||
}
|
||||
|
||||
var httpCode int
|
||||
|
||||
if cfg.RespondWithSameHTTPCode {
|
||||
httpCode = int(code)
|
||||
} else {
|
||||
httpCode = http.StatusOK
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8") // TODO: should depends on requested type
|
||||
w.WriteHeader(httpCode)
|
||||
_, _ = w.Write([]byte(fmt.Sprintf("<html>error page for the code %d</html>", code)))
|
||||
})
|
||||
}
|
||||
|
||||
// ExtractCodeFromURL extracts the error code from the given URL.
|
||||
func ExtractCodeFromURL(url string) (uint16, bool) {
|
||||
var parts = strings.SplitN(strings.TrimLeft(url, "/"), "/", 1)
|
||||
|
||||
if len(parts) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
var (
|
||||
fileName = parts[0]
|
||||
ext = strings.ToLower(filepath.Ext(fileName)) // ".html", ".htm", ".%something%" or an empty string
|
||||
)
|
||||
|
||||
if ext != "" && ext != ".html" && ext != ".htm" {
|
||||
return 0, false
|
||||
} else if ext != "" {
|
||||
fileName = strings.TrimSuffix(fileName, ext)
|
||||
}
|
||||
|
||||
if code, err := strconv.ParseUint(fileName, 10, 16); err == nil && code > 0 && code < 999 {
|
||||
return uint16(code), true
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// URLContainsCode checks if the given URL contains an error code.
|
||||
func URLContainsCode(url string) (ok bool) { _, ok = ExtractCodeFromURL(url); return } //nolint:nlreturn
|
||||
|
||||
// ExtractCodeFromHeaders extracts the error code from the given headers.
|
||||
func ExtractCodeFromHeaders(headers http.Header) (uint16, bool) {
|
||||
if value := headers.Get("X-Code"); len(value) > 0 && len(value) <= 3 {
|
||||
if code, err := strconv.ParseUint(value, 10, 16); err == nil && code > 0 && code < 999 {
|
||||
return uint16(code), true
|
||||
}
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// HeadersContainCode checks if the given headers contain an error code.
|
||||
func HeadersContainCode(headers http.Header) (ok bool) {
|
||||
_, ok = ExtractCodeFromHeaders(headers)
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -13,7 +12,7 @@ import (
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/appmeta"
|
||||
"gh.tarampamp.am/error-pages/internal/config"
|
||||
"gh.tarampamp.am/error-pages/internal/http/handlers/error_page"
|
||||
ep "gh.tarampamp.am/error-pages/internal/http/handlers/error_page"
|
||||
"gh.tarampamp.am/error-pages/internal/http/handlers/live"
|
||||
"gh.tarampamp.am/error-pages/internal/http/handlers/version"
|
||||
"gh.tarampamp.am/error-pages/internal/http/middleware/logreq"
|
||||
@ -51,9 +50,7 @@ func (s *Server) Register(cfg *config.Config) error {
|
||||
var (
|
||||
liveHandler = live.New()
|
||||
versionHandler = version.New(appmeta.Version())
|
||||
errorPagesHandler = error_page.New()
|
||||
|
||||
errorPageRegex = regexp.MustCompile(`^/(\d{3})(?:\.html|\.htm)?$`) // TODO: rewrite to function
|
||||
errorPagesHandler = ep.New(cfg)
|
||||
)
|
||||
|
||||
s.server.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@ -63,16 +60,19 @@ func (s *Server) Register(cfg *config.Config) error {
|
||||
// live endpoints
|
||||
case url == "/health/live" || url == "/health" || url == "/healthz" || url == "/live":
|
||||
liveHandler.ServeHTTP(w, r)
|
||||
|
||||
// version endpoint
|
||||
case url == "/version":
|
||||
versionHandler.ServeHTTP(w, r)
|
||||
|
||||
// error pages endpoints:
|
||||
// - /
|
||||
// - /{code}.html
|
||||
// - /{code}.htm
|
||||
// - /{code}
|
||||
case method == http.MethodGet && (url == "/" || errorPageRegex.MatchString(url)):
|
||||
case method == http.MethodGet && (url == "/" || ep.URLContainsCode(url) || ep.HeadersContainCode(r.Header)):
|
||||
errorPagesHandler.ServeHTTP(w, r)
|
||||
|
||||
// wrong requests handling
|
||||
default:
|
||||
switch {
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -99,7 +100,133 @@ func TestRouting(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("error page", func(t *testing.T) {
|
||||
t.Skip("not implemented")
|
||||
t.Run("success", func(t *testing.T) {
|
||||
var assertErrorPage = func(t *testing.T, wantErrorPageCode int, body []byte, headers http.Header) {
|
||||
t.Helper()
|
||||
|
||||
var bodyStr = string(body)
|
||||
|
||||
assert.NotEmpty(t, bodyStr)
|
||||
assert.Contains(t, bodyStr, "error page") // FIXME
|
||||
assert.Contains(t, bodyStr, strconv.Itoa(wantErrorPageCode)) // FIXME?
|
||||
assert.Contains(t, headers.Get("Content-Type"), "text/html") // FIXME
|
||||
}
|
||||
|
||||
t.Run("index, default", func(t *testing.T) {
|
||||
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/")
|
||||
|
||||
assert.Equal(t, http.StatusOK, status)
|
||||
assertErrorPage(t, int(cfg.DefaultCodeToRender), body, headers)
|
||||
})
|
||||
|
||||
t.Run("index, code in HTTP header", func(t *testing.T) {
|
||||
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/", map[string]string{"X-Code": "404"})
|
||||
|
||||
assert.Equal(t, http.StatusOK, status) // because of [cfg.RespondWithSameHTTPCode] is false by default
|
||||
assertErrorPage(t, 404, body, headers)
|
||||
})
|
||||
|
||||
t.Run("code in URL, .html", func(t *testing.T) {
|
||||
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/500.html")
|
||||
|
||||
assert.Equal(t, http.StatusOK, status)
|
||||
assertErrorPage(t, 500, body, headers)
|
||||
})
|
||||
|
||||
t.Run("code in URL, .htm", func(t *testing.T) {
|
||||
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/409.htm")
|
||||
|
||||
assert.Equal(t, http.StatusOK, status)
|
||||
assertErrorPage(t, 409, body, headers)
|
||||
})
|
||||
|
||||
t.Run("code in URL, without extension", func(t *testing.T) {
|
||||
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/405")
|
||||
|
||||
assert.Equal(t, http.StatusOK, status)
|
||||
assertErrorPage(t, 405, body, headers)
|
||||
})
|
||||
|
||||
t.Run("code in the URL have higher priority than in the headers", func(t *testing.T) {
|
||||
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/405", map[string]string{"X-Code": "404"})
|
||||
|
||||
assert.Equal(t, http.StatusOK, status)
|
||||
assertErrorPage(t, 405, body, headers)
|
||||
})
|
||||
|
||||
t.Run("invalid code in HTTP header (with a string)", func(t *testing.T) {
|
||||
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/", map[string]string{"X-Code": "foobar"})
|
||||
|
||||
assert.Equal(t, http.StatusOK, status)
|
||||
assertErrorPage(t, int(cfg.DefaultCodeToRender), body, headers)
|
||||
})
|
||||
|
||||
t.Run("invalid code in HTTP header (too small)", func(t *testing.T) {
|
||||
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/", map[string]string{"X-Code": "0"})
|
||||
|
||||
assert.Equal(t, http.StatusOK, status)
|
||||
assertErrorPage(t, int(cfg.DefaultCodeToRender), body, headers)
|
||||
})
|
||||
|
||||
t.Run("invalid code in HTTP header (too big)", func(t *testing.T) {
|
||||
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/", map[string]string{"X-Code": "1000"})
|
||||
|
||||
assert.Equal(t, http.StatusOK, status)
|
||||
assertErrorPage(t, int(cfg.DefaultCodeToRender), body, headers)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("failure", func(t *testing.T) {
|
||||
var assertIsNotErrorPage = func(t *testing.T, body []byte) {
|
||||
t.Helper()
|
||||
|
||||
assert.NotContains(t, string(body), "error page") // FIXME
|
||||
}
|
||||
|
||||
t.Run("invalid code in URL (too small)", func(t *testing.T) {
|
||||
var status, body, _ = sendRequest(t, http.MethodGet, baseUrl+"/0.html")
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, status)
|
||||
assertIsNotErrorPage(t, body)
|
||||
})
|
||||
|
||||
t.Run("invalid code in URL (too big)", func(t *testing.T) {
|
||||
var status, body, _ = sendRequest(t, http.MethodGet, baseUrl+"/1000.html")
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, status)
|
||||
assertIsNotErrorPage(t, body)
|
||||
})
|
||||
|
||||
t.Run("invalid code in URL (with a string suffix)", func(t *testing.T) {
|
||||
var status, body, _ = sendRequest(t, http.MethodGet, baseUrl+"/404foobar.html")
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, status)
|
||||
assertIsNotErrorPage(t, body)
|
||||
})
|
||||
|
||||
t.Run("invalid code in URL (with a string prefix)", func(t *testing.T) {
|
||||
var status, body, _ = sendRequest(t, http.MethodGet, baseUrl+"/foobar404.html")
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, status)
|
||||
assertIsNotErrorPage(t, body)
|
||||
})
|
||||
|
||||
t.Run("invalid code in URL (with a string)", func(t *testing.T) {
|
||||
var status, body, _ = sendRequest(t, http.MethodGet, baseUrl+"/foobar.html")
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, status)
|
||||
assertIsNotErrorPage(t, body)
|
||||
})
|
||||
|
||||
t.Run("invalid HTTP methods", func(t *testing.T) {
|
||||
for _, method := range []string{http.MethodDelete, http.MethodPatch, http.MethodPost, http.MethodPut} {
|
||||
var status, body, _ = sendRequest(t, method, baseUrl+"/404.html")
|
||||
|
||||
assert.Equal(t, http.StatusMethodNotAllowed, status)
|
||||
assertIsNotErrorPage(t, body)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("errors handling", func(t *testing.T) {
|
||||
|
Loading…
Reference in New Issue
Block a user