wip: 🔕 temporary commit

This commit is contained in:
Paramtamtam 2024-06-23 23:48:51 +04:00
parent 1b94bc367c
commit a52dbde00c
No known key found for this signature in database
GPG Key ID: 366371698FAD0A2B
7 changed files with 251 additions and 52 deletions

View File

@ -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`)

View File

@ -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,

View File

@ -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
}

View File

@ -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) {

View File

@ -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
}

View File

@ -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 {

View File

@ -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) {