From 1682a3513f0806e99208e1dbf23f126f67e54376 Mon Sep 17 00:00:00 2001
From: Paramtamtam <7326800+tarampampam@users.noreply.github.com>
Date: Tue, 25 Jun 2024 22:26:34 +0400
Subject: [PATCH] =?UTF-8?q?wip:=20=F0=9F=94=95=20temporary=20commit?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
internal/cli/serve/command.go | 46 ++++--
internal/cli/shared/flags_test.go | 4 +-
internal/config/codes_test.go | 4 +-
internal/config/config.go | 23 ++-
internal/config/config_test.go | 19 +++
internal/http/handlers/error_page/handler.go | 152 ++++++++++++++++---
internal/http/server.go | 2 +-
internal/http/server_test.go | 92 ++++++++---
internal/logger/attr.go | 3 +
internal/logger/attr_test.go | 12 +-
internal/template/props.go | 2 +-
internal/template/props_test.go | 4 +-
internal/template/template_test.go | 18 +--
13 files changed, 292 insertions(+), 89 deletions(-)
diff --git a/internal/cli/serve/command.go b/internal/cli/serve/command.go
index 9434408..988938e 100644
--- a/internal/cli/serve/command.go
+++ b/internal/cli/serve/command.go
@@ -55,6 +55,13 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
OnlyOnce: true,
Config: trim,
}
+ plainTextFormatFlag = cli.StringFlag{
+ Name: "plaintext-format",
+ Usage: "override the default error page response in plain text format (Go templates are supported)",
+ Sources: env("RESPONSE_PLAINTEXT_FORMAT"),
+ OnlyOnce: true,
+ Config: trim,
+ }
templateNameFlag = cli.StringFlag{
Name: "template-name",
Aliases: []string{"t"},
@@ -162,6 +169,23 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
cfg.RotationMode, _ = config.ParseRotationMode(c.String(rotationModeFlag.Name))
cfg.ShowDetails = c.Bool(showDetailsFlag.Name)
+ if add := c.StringSlice(addTplFlag.Name); len(add) > 0 { // add templates from files to the config
+ for _, templatePath := range add {
+ if addedName, err := cfg.Templates.AddFromFile(templatePath); err != nil {
+ return fmt.Errorf("cannot add template from file %s: %w", templatePath, err)
+ } else {
+ log.Info("Template added",
+ logger.String("name", addedName),
+ logger.String("path", templatePath),
+ )
+ }
+ }
+ }
+
+ if !cfg.Templates.Has(cfg.TemplateName) {
+ return fmt.Errorf("template %s not found and cannot be used", cfg.TemplateName)
+ }
+
if c.IsSet(proxyHeadersListFlag.Name) {
var m = make(map[string]struct{}) // map is used to avoid duplicates
@@ -176,19 +200,6 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
}
}
- if add := c.StringSlice(addTplFlag.Name); len(add) > 0 { // add templates from files to the config
- for _, templatePath := range add {
- if addedName, err := cfg.Templates.AddFromFile(templatePath); err != nil {
- return fmt.Errorf("cannot add template from file %s: %w", templatePath, err)
- } else {
- log.Info("Template added",
- logger.String("name", addedName),
- logger.String("path", templatePath),
- )
- }
- }
- }
-
if add := c.StringMap(addCodeFlag.Name); len(add) > 0 { // add custom HTTP codes
for code, msgAndDesc := range add {
var (
@@ -216,11 +227,15 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
{ // override default JSON and XML formats
if c.IsSet(jsonFormatFlag.Name) {
- cfg.Formats.JSON = c.String(jsonFormatFlag.Name)
+ cfg.Formats.JSON = strings.TrimSpace(c.String(jsonFormatFlag.Name))
}
if c.IsSet(xmlFormatFlag.Name) {
- cfg.Formats.XML = c.String(xmlFormatFlag.Name)
+ cfg.Formats.XML = strings.TrimSpace(c.String(xmlFormatFlag.Name))
+ }
+
+ if c.IsSet(plainTextFormatFlag.Name) {
+ cfg.Formats.PlainText = strings.TrimSpace(c.String(plainTextFormatFlag.Name))
}
}
@@ -246,6 +261,7 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
&addCodeFlag,
&jsonFormatFlag,
&xmlFormatFlag,
+ &plainTextFormatFlag,
&templateNameFlag,
&disableL10nFlag,
&defaultCodeToRenderFlag,
diff --git a/internal/cli/shared/flags_test.go b/internal/cli/shared/flags_test.go
index 22e9c62..9e48a63 100644
--- a/internal/cli/shared/flags_test.go
+++ b/internal/cli/shared/flags_test.go
@@ -115,7 +115,7 @@ func TestAddHTTPCodeFlag(t *testing.T) {
assert.Equal(t, "add-http-code", flag.Name)
- for name, _tt := range map[string]struct {
+ for name, tt := range map[string]struct {
giveValue map[string]string
wantErrMsg string
}{
@@ -152,8 +152,6 @@ func TestAddHTTPCodeFlag(t *testing.T) {
wantErrMsg: "missing message for HTTP code [200]",
},
} {
- var tt = _tt
-
t.Run(name, func(t *testing.T) {
if err := flag.Validator(tt.giveValue); tt.wantErrMsg != "" {
assert.ErrorContains(t, err, tt.wantErrMsg)
diff --git a/internal/config/codes_test.go b/internal/config/codes_test.go
index 9727ecf..74297a3 100644
--- a/internal/config/codes_test.go
+++ b/internal/config/codes_test.go
@@ -66,7 +66,7 @@ func TestCodes_Find(t *testing.T) {
"*": {Message: "Single"},
}
- for name, _tt := range map[string]struct {
+ for name, tt := range map[string]struct {
giveCodes config.Codes
giveCode uint16
@@ -114,8 +114,6 @@ func TestCodes_Find(t *testing.T) {
"empty map": {giveCodes: config.Codes{}, giveCode: 404, wantNotFound: true},
"zero code": {giveCodes: common, giveCode: 0, wantNotFound: true},
} {
- var tt = _tt
-
t.Run(name, func(t *testing.T) {
for i := 0; i < 100; i++ { // repeat the test to ensure the function is idempotent
var desc, found = tt.giveCodes.Find(tt.giveCode)
diff --git a/internal/config/config.go b/internal/config/config.go
index b179135..003ba60 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -16,8 +16,9 @@ type Config struct {
// Formats contain alternative response formats (e.g., if a client requests a response in one of these formats,
// we will render the response using the specified format instead of HTML; Go templates are supported).
Formats struct {
- JSON string
- XML string
+ JSON string
+ XML string
+ PlainText string
}
// Codes hold descriptions for HTTP codes (e.g., 404: "Not Found / The server can not find the requested page").
@@ -59,7 +60,7 @@ type Config struct {
const defaultJSONFormat string = `{
"error": true,
- "Code": {{ Code | json }},
+ "code": {{ code | json }},
"message": {{ message | json }},
"description": {{ description | json }}{{ if show_details }},
"details": {
@@ -77,7 +78,7 @@ const defaultJSONFormat string = `{
const defaultXMLFormat string = `
- {{ Code }}
+ {{ code }}
{{ message }}
{{ description }}{{ if show_details }}
@@ -93,6 +94,19 @@ const defaultXMLFormat string = `
{{ end }}
`
+const defaultPlainTextFormat string = `Error {{ code }}: {{ message }}{{ if description }}
+{{ description }}{{ end }}{{ if show_details }}
+
+Host: {{ host }}
+Original URI: {{ original_uri }}
+Forwarded For: {{ forwarded_for }}
+Namespace: {{ namespace }}
+Ingress Name: {{ ingress_name }}
+Service Name: {{ service_name }}
+Service Port: {{ service_port }}
+Request ID: {{ request_id }}
+Timestamp: {{ now.Unix }}{{ end }}`
+
//nolint:lll
var defaultCodes = Codes{ //nolint:gochecknoglobals
"400": {"Bad Request", "The server did not understand the request"},
@@ -134,6 +148,7 @@ func New() Config {
cfg.Formats.JSON = defaultJSONFormat
cfg.Formats.XML = defaultXMLFormat
+ cfg.Formats.PlainText = defaultPlainTextFormat
// add built-in templates
for name, content := range builtinTemplates.BuiltIn() {
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index f12f707..4239fd7 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/config"
+ "gh.tarampamp.am/error-pages/internal/template"
)
func TestNew(t *testing.T) {
@@ -17,6 +18,7 @@ func TestNew(t *testing.T) {
assert.NotEmpty(t, cfg.Formats.XML)
assert.NotEmpty(t, cfg.Formats.JSON)
+ assert.NotEmpty(t, cfg.Formats.PlainText)
assert.True(t, len(cfg.Codes) >= 19)
assert.True(t, len(cfg.Templates) >= 2)
assert.NotEmpty(t, cfg.TemplateName)
@@ -35,4 +37,21 @@ func TestNew(t *testing.T) {
assert.NotEqual(t, cfg1.ProxyHeaders, cfg2.ProxyHeaders)
})
+
+ t.Run("render default format templates", func(t *testing.T) {
+ var cfg = config.New()
+
+ for _, content := range []string{cfg.Formats.JSON, cfg.Formats.XML, cfg.Formats.PlainText} {
+ var result, err = template.Render(content, template.Props{
+ ShowRequestDetails: true,
+ Code: 404,
+ Message: "Not Found",
+ })
+
+ assert.NotEmpty(t, result)
+ assert.NoError(t, err)
+
+ t.Log(result)
+ }
+ })
}
diff --git a/internal/http/handlers/error_page/handler.go b/internal/http/handlers/error_page/handler.go
index 0e9a513..418f7e5 100644
--- a/internal/http/handlers/error_page/handler.go
+++ b/internal/http/handlers/error_page/handler.go
@@ -1,13 +1,19 @@
package error_page
import (
+ "encoding/json"
"fmt"
"net/http"
"gh.tarampamp.am/error-pages/internal/config"
+ "gh.tarampamp.am/error-pages/internal/logger"
+ "gh.tarampamp.am/error-pages/internal/template"
)
-func New(cfg *config.Config) http.Handler {
+const contentTypeHeader = "Content-Type"
+
+// New creates a new handler that returns an error page with the specified status code and format.
+func New(cfg *config.Config, log *logger.Logger) http.Handler { //nolint:funlen,gocognit
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var code uint16
@@ -29,36 +35,132 @@ func New(cfg *config.Config) http.Handler {
var format = detectPreferredFormatForClient(r.Header)
- switch headerName := "Content-Type"; format {
- case jsonFormat:
- w.Header().Set(headerName, "application/json; charset=utf-8")
- case xmlFormat:
- w.Header().Set(headerName, "application/xml; charset=utf-8")
- case htmlFormat:
- w.Header().Set(headerName, "text/html; charset=utf-8")
- case plainTextFormat:
- w.Header().Set(headerName, "text/plain; charset=utf-8")
- default:
- w.Header().Set(headerName, "text/html; charset=utf-8")
- }
+ { // deal with the headers
+ switch format {
+ case jsonFormat:
+ w.Header().Set(contentTypeHeader, "application/json; charset=utf-8")
+ case xmlFormat:
+ w.Header().Set(contentTypeHeader, "application/xml; charset=utf-8")
+ case htmlFormat:
+ w.Header().Set(contentTypeHeader, "text/html; charset=utf-8")
+ default:
+ w.Header().Set(contentTypeHeader, "text/plain; charset=utf-8") // plainTextFormat as default
+ }
- // https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag
- // disallow indexing of the error pages
- w.Header().Set("X-Robots-Tag", "noindex")
+ // https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag
+ // disallow indexing of the error pages
+ w.Header().Set("X-Robots-Tag", "noindex")
- if code >= 500 && code < 600 {
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
- // tell the client (search crawler) to retry the request after 120 seconds, it makes sense for the 5xx errors
- w.Header().Set("Retry-After", "120")
- }
+ if code >= 500 && code < 600 {
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
+ // tell the client (search crawler) to retry the request after 120 seconds, it makes sense for the 5xx errors
+ w.Header().Set("Retry-After", "120")
+ }
- for _, proxyHeader := range cfg.ProxyHeaders {
- if value := r.Header.Get(proxyHeader); value != "" {
- w.Header().Set(proxyHeader, value)
+ // proxy the headers from the incoming request to the error page response if they are defined in the config
+ for _, proxyHeader := range cfg.ProxyHeaders {
+ if value := r.Header.Get(proxyHeader); value != "" {
+ w.Header().Set(proxyHeader, value)
+ }
}
}
w.WriteHeader(httpCode)
- _, _ = w.Write([]byte(fmt.Sprintf("error page for the code %d", code)))
+
+ // prepare the template properties for rendering
+ var tplProps = template.Props{
+ Code: code, // http status code
+ ShowRequestDetails: cfg.ShowDetails, // status message
+ L10nDisabled: cfg.L10n.Disable, // status description
+ }
+
+ //nolint:lll
+ if cfg.ShowDetails { // https://kubernetes.github.io/ingress-nginx/user-guide/custom-errors/
+ tplProps.OriginalURI = r.Header.Get("X-Original-URI") // (ingress-nginx) URI that caused the error
+ tplProps.Namespace = r.Header.Get("X-Namespace") // (ingress-nginx) namespace where the backend Service is located
+ tplProps.IngressName = r.Header.Get("X-Ingress-Name") // (ingress-nginx) name of the Ingress where the backend is defined
+ tplProps.ServiceName = r.Header.Get("X-Service-Name") // (ingress-nginx) name of the Service backing the backend
+ tplProps.ServicePort = r.Header.Get("X-Service-Port") // (ingress-nginx) port number of the Service backing the backend
+ tplProps.RequestID = r.Header.Get("X-Request-Id") // (ingress-nginx) unique ID that identifies the request - same as for backend service
+ tplProps.ForwardedFor = r.Header.Get("X-Forwarded-For") // the value of the `X-Forwarded-For` header
+ tplProps.Host = r.Header.Get("Host") // the value of the `Host` header
+ }
+
+ // try to find the code message and description in the config and if not - use the standard status text or fallback
+ if desc, found := cfg.Codes.Find(code); found {
+ tplProps.Message = desc.Message
+ tplProps.Description = desc.Description
+ } else if stdlibStatusText := http.StatusText(int(code)); stdlibStatusText != "" {
+ tplProps.Message = stdlibStatusText
+ } else {
+ tplProps.Message = "Unknown Status Code" // fallback
+ }
+
+ switch {
+ case format == jsonFormat && cfg.Formats.JSON != "":
+ if content, err := template.Render(cfg.Formats.JSON, tplProps); err != nil {
+ j, _ := json.Marshal(fmt.Sprintf("Failed to render the JSON template: %s", err.Error()))
+ write(w, log, j)
+ } else {
+ write(w, log, content)
+ }
+
+ case format == xmlFormat && cfg.Formats.XML != "":
+ if content, err := template.Render(cfg.Formats.XML, tplProps); err != nil {
+ write(w, log, fmt.Sprintf(
+ "\nFailed to render the XML template: %s", err.Error(),
+ ))
+ } else {
+ write(w, log, content)
+ }
+
+ case format == htmlFormat:
+ if tpl, found := cfg.Templates.Get(cfg.TemplateName); found {
+ if content, err := template.Render(tpl, tplProps); err != nil {
+ write(w, log, fmt.Sprintf(
+ "\n
Failed to render the HTML template %s: %s",
+ cfg.TemplateName,
+ err.Error(),
+ ))
+ } else {
+ write(w, log, content)
+ }
+ } else {
+ write(w, log, fmt.Sprintf(
+ "\nTemplate %s not found and cannot be used", cfg.TemplateName,
+ ))
+ }
+
+ default: // plainTextFormat as default
+ if cfg.Formats.PlainText != "" {
+ if content, err := template.Render(cfg.Formats.PlainText, tplProps); err != nil {
+ write(w, log, fmt.Sprintf("Failed to render the PlainText template: %s", err.Error()))
+ } else {
+ write(w, log, content)
+ }
+ } else {
+ write(w, log, `The requested content format is not supported.
+Please create an issue on the project's GitHub page to request support for it.
+
+Supported formats: JSON, XML, HTML, Plain Text`)
+ }
+ }
})
}
+
+func write[T string | []byte](w http.ResponseWriter, log *logger.Logger, content T) {
+ var data []byte
+
+ if s, ok := any(content).(string); ok {
+ data = []byte(s)
+ } else {
+ data = any(content).([]byte)
+ }
+
+ if _, err := w.Write(data); err != nil && log != nil {
+ log.Error("failed to write the response body",
+ logger.String("content", string(data)),
+ logger.Error(err),
+ )
+ }
+}
diff --git a/internal/http/server.go b/internal/http/server.go
index 7682f17..0265483 100644
--- a/internal/http/server.go
+++ b/internal/http/server.go
@@ -49,7 +49,7 @@ func (s *Server) Register(cfg *config.Config) error {
var (
liveHandler = live.New()
versionHandler = version.New(appmeta.Version())
- errorPagesHandler = ep.New(cfg)
+ errorPagesHandler = ep.New(cfg, s.log)
)
s.server.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
diff --git a/internal/http/server_test.go b/internal/http/server_test.go
index eb88cc8..4218311 100644
--- a/internal/http/server_test.go
+++ b/internal/http/server_test.go
@@ -7,7 +7,6 @@ import (
"io"
"net"
"net/http"
- "strconv"
"testing"
"time"
@@ -19,12 +18,33 @@ import (
"gh.tarampamp.am/error-pages/internal/logger"
)
+// TestRouting in fact is a test for the whole server, because it tests all the routes and their handlers.
func TestRouting(t *testing.T) {
var (
srv = appHttp.NewServer(context.Background(), logger.NewNop())
cfg = config.New()
)
+ assert.NoError(t, cfg.Templates.Add("unit-test", `
+
+ Error {{ code }}: {{ message }}
{{ if description }}
+ {{ description }}
{{ end }}{{ if show_details }}
+
+
+ Host: {{ host }}
+ Original URI: {{ original_uri }}
+ Forwarded For: {{ forwarded_for }}
+ Namespace: {{ namespace }}
+ Ingress Name: {{ ingress_name }}
+ Service Name: {{ service_name }}
+ Service Port: {{ service_port }}
+ Request ID: {{ request_id }}
+ Timestamp: {{ now.Unix }}
+
{{ end }}
+`))
+
+ cfg.TemplateName = "unit-test"
+
require.NoError(t, srv.Register(&cfg))
var baseUrl, stopServer = startServer(t, &srv)
@@ -101,78 +121,106 @@ func TestRouting(t *testing.T) {
t.Run("error page", func(t *testing.T) {
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) {
+ t.Run("index, default (plain text by 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)
+ assert.Contains(t, string(body), "404: Not Found")
+ assert.Contains(t, headers.Get("Content-Type"), "text/plain")
+ })
+
+ t.Run("index, default (json format)", func(t *testing.T) {
+ var status, body, headers = sendRequest(t,
+ http.MethodGet, baseUrl+"/", map[string]string{"Accept": "application/json"},
+ )
+
+ assert.Equal(t, http.StatusOK, status)
+ assert.Contains(t, string(body), `"code": 404`)
+ assert.Contains(t, headers.Get("Content-Type"), "application/json")
+ })
+
+ t.Run("index, default (xml format)", func(t *testing.T) {
+ var status, body, headers = sendRequest(t,
+ http.MethodGet, baseUrl+"/", map[string]string{"Accept": "application/xml"},
+ )
+
+ assert.Equal(t, http.StatusOK, status)
+ assert.Contains(t, string(body), `404
`)
+ assert.Contains(t, headers.Get("Content-Type"), "application/xml")
+ })
+
+ t.Run("index, default (html format)", func(t *testing.T) {
+ var status, body, headers = sendRequest(t,
+ http.MethodGet, baseUrl+"/", map[string]string{"Content-Type": "text/html"},
+ )
+
+ assert.Equal(t, http.StatusOK, status)
+ assert.Contains(t, string(body), `Error 404: Not Found
`)
+ assert.Contains(t, headers.Get("Content-Type"), "text/html")
})
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)
+ assert.Contains(t, string(body), "404: Not Found")
+ assert.Contains(t, headers.Get("Content-Type"), "text/plain")
})
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)
+ assert.Contains(t, string(body), "500: Internal Server Error")
+ assert.Contains(t, headers.Get("Content-Type"), "text/plain")
})
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)
+ assert.Contains(t, string(body), "409: Conflict")
+ assert.Contains(t, headers.Get("Content-Type"), "text/plain")
})
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)
+ assert.Contains(t, string(body), "405: Method Not Allowed")
+ assert.Contains(t, headers.Get("Content-Type"), "text/plain")
})
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)
+ assert.Contains(t, string(body), "405: Method Not Allowed")
+ assert.Contains(t, headers.Get("Content-Type"), "text/plain")
})
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)
+ assert.Contains(t, string(body), "404: Not Found")
+ assert.Contains(t, headers.Get("Content-Type"), "text/plain")
})
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)
+ assert.Contains(t, string(body), "404: Not Found")
+ assert.Contains(t, headers.Get("Content-Type"), "text/plain")
})
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)
+ assert.Contains(t, string(body), "404: Not Found")
+ assert.Contains(t, headers.Get("Content-Type"), "text/plain")
})
})
diff --git a/internal/logger/attr.go b/internal/logger/attr.go
index 4e6593e..8965b5e 100644
--- a/internal/logger/attr.go
+++ b/internal/logger/attr.go
@@ -38,5 +38,8 @@ func Time(key string, v time.Time) Attr { return slog.Time(key, v) }
// Duration returns an Attr for a [time.Duration].
func Duration(key string, v time.Duration) Attr { return slog.Duration(key, v) }
+// Error returns an Attr for an error.
+func Error(err error) Attr { return slog.String("error", err.Error()) }
+
// Any returns an Attr for any value.
func Any(key string, v any) Attr { return slog.Any(key, v) }
diff --git a/internal/logger/attr_test.go b/internal/logger/attr_test.go
index 6d4fc90..81c66f9 100644
--- a/internal/logger/attr_test.go
+++ b/internal/logger/attr_test.go
@@ -1,6 +1,8 @@
package logger_test
import (
+ "errors"
+ "fmt"
"testing"
"time"
@@ -12,9 +14,12 @@ import (
func TestAttrs(t *testing.T) {
t.Parallel()
- var someTime, _ = time.Parse(time.RFC3339, "2021-01-01T00:00:00Z")
+ var (
+ someTime, _ = time.Parse(time.RFC3339, "2021-01-01T00:00:00Z")
+ someErr = fmt.Errorf("foo: %w", errors.New("bar"))
+ )
- for name, _tt := range map[string]struct {
+ for name, tt := range map[string]struct {
giveAttr logger.Attr
wantKey string
@@ -30,10 +35,9 @@ func TestAttrs(t *testing.T) {
"Bool": {logger.Bool("key", true), "key", true},
"Time": {logger.Time("key", someTime), "key", someTime},
"Duration": {logger.Duration("key", time.Second), "key", time.Second},
+ "Error": {logger.Error(someErr), "error", "foo: bar"},
"Any": {logger.Any("key", "value"), "key", "value"},
} {
- tt := _tt
-
t.Run(name, func(t *testing.T) {
t.Parallel()
diff --git a/internal/template/props.go b/internal/template/props.go
index 719ee82..bccd908 100644
--- a/internal/template/props.go
+++ b/internal/template/props.go
@@ -4,7 +4,7 @@ import "reflect"
//nolint:lll
type Props struct {
- Code string `token:"code"` // http status code
+ Code uint16 `token:"code"` // http status code
Message string `token:"message"` // status message
Description string `token:"description"` // status description
OriginalURI string `token:"original_uri"` // (ingress-nginx) URI that caused the error
diff --git a/internal/template/props_test.go b/internal/template/props_test.go
index b073f76..a41eb2f 100644
--- a/internal/template/props_test.go
+++ b/internal/template/props_test.go
@@ -12,7 +12,7 @@ func TestProps_Values(t *testing.T) {
t.Parallel()
assert.Equal(t, template.Props{
- Code: "a",
+ Code: 1,
Message: "b",
Description: "c",
OriginalURI: "d",
@@ -25,7 +25,7 @@ func TestProps_Values(t *testing.T) {
L10nDisabled: true,
ShowRequestDetails: false,
}.Values(), map[string]any{
- "code": "a",
+ "code": uint16(1),
"message": "b",
"description": "c",
"original_uri": "d",
diff --git a/internal/template/template_test.go b/internal/template/template_test.go
index 332b439..14ba215 100644
--- a/internal/template/template_test.go
+++ b/internal/template/template_test.go
@@ -84,33 +84,33 @@ func TestRender(t *testing.T) {
}{
"common case": {
giveTemplate: "{{code}}: {{ message }} {{description}}",
- giveProps: template.Props{Code: "404", Message: "Not found", Description: "Blah"},
+ giveProps: template.Props{Code: 404, Message: "Not found", Description: "Blah"},
wantResult: "404: Not found Blah",
},
"html markup": {
giveTemplate: "{{code}}: {{ message }} {{description}}",
- giveProps: template.Props{Code: "201", Message: "lorem ipsum"},
+ giveProps: template.Props{Code: 201, Message: "lorem ipsum"},
wantResult: "201: lorem ipsum ",
},
"with line breakers": {
- giveTemplate: "\t {{code}}: {{ message }} {{description}}\n",
+ giveTemplate: "\t {{code | json}}: {{ message }} {{description}}\n",
giveProps: template.Props{},
- wantResult: "\t : \n",
+ wantResult: "\t 0: \n",
},
"golang template": {
giveTemplate: "\t {{code}} {{ .Code }}{{ if .Message }} Yeah {{end}}",
- giveProps: template.Props{Code: "201", Message: "lorem ipsum"},
+ giveProps: template.Props{Code: 201, Message: "lorem ipsum"},
wantResult: "\t 201 201 Yeah ",
},
"json common case": {
giveTemplate: `{"code": {{code | json}}, "message": {"here":[ {{ message | json }} ]}, "desc": "{{description}}"}`,
- giveProps: template.Props{Code: `404'"{`, Message: "Not found\t\r\n"},
- wantResult: `{"code": "404'\"{", "message": {"here":[ "Not found\t\r\n" ]}, "desc": ""}`,
+ giveProps: template.Props{Code: 404, Message: "'\"{Not found\t\r\n"},
+ wantResult: `{"code": 404, "message": {"here":[ "'\"{Not found\t\r\n" ]}, "desc": ""}`,
},
"json golang template": {
giveTemplate: `{"code": "{{code}}", "message": {"here":[ "{{ if .Message }} Yeah {{end}}" ]}}`,
- giveProps: template.Props{Code: "201", Message: "lorem ipsum"},
+ giveProps: template.Props{Code: 201, Message: "lorem ipsum"},
wantResult: `{"code": "201", "message": {"here":[ " Yeah " ]}}`,
},
@@ -127,7 +127,7 @@ func TestRender(t *testing.T) {
"complete example with every property and function": {
giveProps: template.Props{
- Code: "404",
+ Code: 404,
Message: "Not found",
Description: "Blah",
OriginalURI: "/test",