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( + "\nFailed 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",