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
65fc5ecc7f
commit
1682a3513f
@ -55,6 +55,13 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
|
|||||||
OnlyOnce: true,
|
OnlyOnce: true,
|
||||||
Config: trim,
|
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{
|
templateNameFlag = cli.StringFlag{
|
||||||
Name: "template-name",
|
Name: "template-name",
|
||||||
Aliases: []string{"t"},
|
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.RotationMode, _ = config.ParseRotationMode(c.String(rotationModeFlag.Name))
|
||||||
cfg.ShowDetails = c.Bool(showDetailsFlag.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) {
|
if c.IsSet(proxyHeadersListFlag.Name) {
|
||||||
var m = make(map[string]struct{}) // map is used to avoid duplicates
|
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
|
if add := c.StringMap(addCodeFlag.Name); len(add) > 0 { // add custom HTTP codes
|
||||||
for code, msgAndDesc := range add {
|
for code, msgAndDesc := range add {
|
||||||
var (
|
var (
|
||||||
@ -216,11 +227,15 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
|
|||||||
|
|
||||||
{ // override default JSON and XML formats
|
{ // override default JSON and XML formats
|
||||||
if c.IsSet(jsonFormatFlag.Name) {
|
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) {
|
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,
|
&addCodeFlag,
|
||||||
&jsonFormatFlag,
|
&jsonFormatFlag,
|
||||||
&xmlFormatFlag,
|
&xmlFormatFlag,
|
||||||
|
&plainTextFormatFlag,
|
||||||
&templateNameFlag,
|
&templateNameFlag,
|
||||||
&disableL10nFlag,
|
&disableL10nFlag,
|
||||||
&defaultCodeToRenderFlag,
|
&defaultCodeToRenderFlag,
|
||||||
|
@ -115,7 +115,7 @@ func TestAddHTTPCodeFlag(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, "add-http-code", flag.Name)
|
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
|
giveValue map[string]string
|
||||||
wantErrMsg string
|
wantErrMsg string
|
||||||
}{
|
}{
|
||||||
@ -152,8 +152,6 @@ func TestAddHTTPCodeFlag(t *testing.T) {
|
|||||||
wantErrMsg: "missing message for HTTP code [200]",
|
wantErrMsg: "missing message for HTTP code [200]",
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
var tt = _tt
|
|
||||||
|
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
if err := flag.Validator(tt.giveValue); tt.wantErrMsg != "" {
|
if err := flag.Validator(tt.giveValue); tt.wantErrMsg != "" {
|
||||||
assert.ErrorContains(t, err, tt.wantErrMsg)
|
assert.ErrorContains(t, err, tt.wantErrMsg)
|
||||||
|
@ -66,7 +66,7 @@ func TestCodes_Find(t *testing.T) {
|
|||||||
"*": {Message: "Single"},
|
"*": {Message: "Single"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, _tt := range map[string]struct {
|
for name, tt := range map[string]struct {
|
||||||
giveCodes config.Codes
|
giveCodes config.Codes
|
||||||
giveCode uint16
|
giveCode uint16
|
||||||
|
|
||||||
@ -114,8 +114,6 @@ func TestCodes_Find(t *testing.T) {
|
|||||||
"empty map": {giveCodes: config.Codes{}, giveCode: 404, wantNotFound: true},
|
"empty map": {giveCodes: config.Codes{}, giveCode: 404, wantNotFound: true},
|
||||||
"zero code": {giveCodes: common, giveCode: 0, wantNotFound: true},
|
"zero code": {giveCodes: common, giveCode: 0, wantNotFound: true},
|
||||||
} {
|
} {
|
||||||
var tt = _tt
|
|
||||||
|
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
for i := 0; i < 100; i++ { // repeat the test to ensure the function is idempotent
|
for i := 0; i < 100; i++ { // repeat the test to ensure the function is idempotent
|
||||||
var desc, found = tt.giveCodes.Find(tt.giveCode)
|
var desc, found = tt.giveCodes.Find(tt.giveCode)
|
||||||
|
@ -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,
|
// 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).
|
// we will render the response using the specified format instead of HTML; Go templates are supported).
|
||||||
Formats struct {
|
Formats struct {
|
||||||
JSON string
|
JSON string
|
||||||
XML string
|
XML string
|
||||||
|
PlainText string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Codes hold descriptions for HTTP codes (e.g., 404: "Not Found / The server can not find the requested page").
|
// 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 = `{
|
const defaultJSONFormat string = `{
|
||||||
"error": true,
|
"error": true,
|
||||||
"Code": {{ Code | json }},
|
"code": {{ code | json }},
|
||||||
"message": {{ message | json }},
|
"message": {{ message | json }},
|
||||||
"description": {{ description | json }}{{ if show_details }},
|
"description": {{ description | json }}{{ if show_details }},
|
||||||
"details": {
|
"details": {
|
||||||
@ -77,7 +78,7 @@ const defaultJSONFormat string = `{
|
|||||||
|
|
||||||
const defaultXMLFormat string = `<?xml version="1.0" encoding="utf-8"?>
|
const defaultXMLFormat string = `<?xml version="1.0" encoding="utf-8"?>
|
||||||
<error>
|
<error>
|
||||||
<Code>{{ Code }}</Code>
|
<code>{{ code }}</code>
|
||||||
<message>{{ message }}</message>
|
<message>{{ message }}</message>
|
||||||
<description>{{ description }}</description>{{ if show_details }}
|
<description>{{ description }}</description>{{ if show_details }}
|
||||||
<details>
|
<details>
|
||||||
@ -93,6 +94,19 @@ const defaultXMLFormat string = `<?xml version="1.0" encoding="utf-8"?>
|
|||||||
</details>{{ end }}
|
</details>{{ end }}
|
||||||
</error>`
|
</error>`
|
||||||
|
|
||||||
|
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
|
//nolint:lll
|
||||||
var defaultCodes = Codes{ //nolint:gochecknoglobals
|
var defaultCodes = Codes{ //nolint:gochecknoglobals
|
||||||
"400": {"Bad Request", "The server did not understand the request"},
|
"400": {"Bad Request", "The server did not understand the request"},
|
||||||
@ -134,6 +148,7 @@ func New() Config {
|
|||||||
|
|
||||||
cfg.Formats.JSON = defaultJSONFormat
|
cfg.Formats.JSON = defaultJSONFormat
|
||||||
cfg.Formats.XML = defaultXMLFormat
|
cfg.Formats.XML = defaultXMLFormat
|
||||||
|
cfg.Formats.PlainText = defaultPlainTextFormat
|
||||||
|
|
||||||
// add built-in templates
|
// add built-in templates
|
||||||
for name, content := range builtinTemplates.BuiltIn() {
|
for name, content := range builtinTemplates.BuiltIn() {
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"gh.tarampamp.am/error-pages/internal/config"
|
"gh.tarampamp.am/error-pages/internal/config"
|
||||||
|
"gh.tarampamp.am/error-pages/internal/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
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.XML)
|
||||||
assert.NotEmpty(t, cfg.Formats.JSON)
|
assert.NotEmpty(t, cfg.Formats.JSON)
|
||||||
|
assert.NotEmpty(t, cfg.Formats.PlainText)
|
||||||
assert.True(t, len(cfg.Codes) >= 19)
|
assert.True(t, len(cfg.Codes) >= 19)
|
||||||
assert.True(t, len(cfg.Templates) >= 2)
|
assert.True(t, len(cfg.Templates) >= 2)
|
||||||
assert.NotEmpty(t, cfg.TemplateName)
|
assert.NotEmpty(t, cfg.TemplateName)
|
||||||
@ -35,4 +37,21 @@ func TestNew(t *testing.T) {
|
|||||||
|
|
||||||
assert.NotEqual(t, cfg1.ProxyHeaders, cfg2.ProxyHeaders)
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
package error_page
|
package error_page
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"gh.tarampamp.am/error-pages/internal/config"
|
"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) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
var code uint16
|
var code uint16
|
||||||
|
|
||||||
@ -29,36 +35,132 @@ func New(cfg *config.Config) http.Handler {
|
|||||||
|
|
||||||
var format = detectPreferredFormatForClient(r.Header)
|
var format = detectPreferredFormatForClient(r.Header)
|
||||||
|
|
||||||
switch headerName := "Content-Type"; format {
|
{ // deal with the headers
|
||||||
case jsonFormat:
|
switch format {
|
||||||
w.Header().Set(headerName, "application/json; charset=utf-8")
|
case jsonFormat:
|
||||||
case xmlFormat:
|
w.Header().Set(contentTypeHeader, "application/json; charset=utf-8")
|
||||||
w.Header().Set(headerName, "application/xml; charset=utf-8")
|
case xmlFormat:
|
||||||
case htmlFormat:
|
w.Header().Set(contentTypeHeader, "application/xml; charset=utf-8")
|
||||||
w.Header().Set(headerName, "text/html; charset=utf-8")
|
case htmlFormat:
|
||||||
case plainTextFormat:
|
w.Header().Set(contentTypeHeader, "text/html; charset=utf-8")
|
||||||
w.Header().Set(headerName, "text/plain; charset=utf-8")
|
default:
|
||||||
default:
|
w.Header().Set(contentTypeHeader, "text/plain; charset=utf-8") // plainTextFormat as default
|
||||||
w.Header().Set(headerName, "text/html; charset=utf-8")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag
|
// https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag
|
||||||
// disallow indexing of the error pages
|
// disallow indexing of the error pages
|
||||||
w.Header().Set("X-Robots-Tag", "noindex")
|
w.Header().Set("X-Robots-Tag", "noindex")
|
||||||
|
|
||||||
if code >= 500 && code < 600 {
|
if code >= 500 && code < 600 {
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
// 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
|
// 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")
|
w.Header().Set("Retry-After", "120")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, proxyHeader := range cfg.ProxyHeaders {
|
// proxy the headers from the incoming request to the error page response if they are defined in the config
|
||||||
if value := r.Header.Get(proxyHeader); value != "" {
|
for _, proxyHeader := range cfg.ProxyHeaders {
|
||||||
w.Header().Set(proxyHeader, value)
|
if value := r.Header.Get(proxyHeader); value != "" {
|
||||||
|
w.Header().Set(proxyHeader, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(httpCode)
|
w.WriteHeader(httpCode)
|
||||||
_, _ = w.Write([]byte(fmt.Sprintf("<html>error page for the code %d</html>", 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(
|
||||||
|
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<error>Failed to render the XML template: %s</error>", 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(
|
||||||
|
"<!DOCTYPE html>\n<html><body>Failed to render the HTML template %s: %s</body></html>",
|
||||||
|
cfg.TemplateName,
|
||||||
|
err.Error(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
write(w, log, content)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
write(w, log, fmt.Sprintf(
|
||||||
|
"<!DOCTYPE html>\n<html><body>Template %s not found and cannot be used</body></html>", 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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -49,7 +49,7 @@ func (s *Server) Register(cfg *config.Config) error {
|
|||||||
var (
|
var (
|
||||||
liveHandler = live.New()
|
liveHandler = live.New()
|
||||||
versionHandler = version.New(appmeta.Version())
|
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) {
|
s.server.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -19,12 +18,33 @@ import (
|
|||||||
"gh.tarampamp.am/error-pages/internal/logger"
|
"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) {
|
func TestRouting(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
srv = appHttp.NewServer(context.Background(), logger.NewNop())
|
srv = appHttp.NewServer(context.Background(), logger.NewNop())
|
||||||
cfg = config.New()
|
cfg = config.New()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert.NoError(t, cfg.Templates.Add("unit-test", `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<h1>Error {{ code }}: {{ message }}</h1>{{ if description }}
|
||||||
|
<h2>{{ description }}</h2>{{ end }}{{ if show_details }}
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
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 }}
|
||||||
|
</pre>{{ end }}
|
||||||
|
</html>`))
|
||||||
|
|
||||||
|
cfg.TemplateName = "unit-test"
|
||||||
|
|
||||||
require.NoError(t, srv.Register(&cfg))
|
require.NoError(t, srv.Register(&cfg))
|
||||||
|
|
||||||
var baseUrl, stopServer = startServer(t, &srv)
|
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("error page", func(t *testing.T) {
|
||||||
t.Run("success", 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.Run("index, default (plain text by default)", func(t *testing.T) {
|
||||||
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+"/")
|
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/")
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, status)
|
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), `<code>404</code>`)
|
||||||
|
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), `<h1>Error 404: Not Found</h1>`)
|
||||||
|
assert.Contains(t, headers.Get("Content-Type"), "text/html")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("index, code in HTTP header", func(t *testing.T) {
|
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"})
|
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
|
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) {
|
t.Run("code in URL, .html", func(t *testing.T) {
|
||||||
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/500.html")
|
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/500.html")
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, status)
|
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) {
|
t.Run("code in URL, .htm", func(t *testing.T) {
|
||||||
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/409.htm")
|
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/409.htm")
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, status)
|
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) {
|
t.Run("code in URL, without extension", func(t *testing.T) {
|
||||||
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/405")
|
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/405")
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, status)
|
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) {
|
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"})
|
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/405", map[string]string{"X-Code": "404"})
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, status)
|
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) {
|
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"})
|
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/", map[string]string{"X-Code": "foobar"})
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, status)
|
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) {
|
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"})
|
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/", map[string]string{"X-Code": "0"})
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, status)
|
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) {
|
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"})
|
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/", map[string]string{"X-Code": "1000"})
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, status)
|
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")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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].
|
// Duration returns an Attr for a [time.Duration].
|
||||||
func Duration(key string, v time.Duration) Attr { return slog.Duration(key, v) }
|
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.
|
// Any returns an Attr for any value.
|
||||||
func Any(key string, v any) Attr { return slog.Any(key, v) }
|
func Any(key string, v any) Attr { return slog.Any(key, v) }
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package logger_test
|
package logger_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -12,9 +14,12 @@ import (
|
|||||||
func TestAttrs(t *testing.T) {
|
func TestAttrs(t *testing.T) {
|
||||||
t.Parallel()
|
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
|
giveAttr logger.Attr
|
||||||
|
|
||||||
wantKey string
|
wantKey string
|
||||||
@ -30,10 +35,9 @@ func TestAttrs(t *testing.T) {
|
|||||||
"Bool": {logger.Bool("key", true), "key", true},
|
"Bool": {logger.Bool("key", true), "key", true},
|
||||||
"Time": {logger.Time("key", someTime), "key", someTime},
|
"Time": {logger.Time("key", someTime), "key", someTime},
|
||||||
"Duration": {logger.Duration("key", time.Second), "key", time.Second},
|
"Duration": {logger.Duration("key", time.Second), "key", time.Second},
|
||||||
|
"Error": {logger.Error(someErr), "error", "foo: bar"},
|
||||||
"Any": {logger.Any("key", "value"), "key", "value"},
|
"Any": {logger.Any("key", "value"), "key", "value"},
|
||||||
} {
|
} {
|
||||||
tt := _tt
|
|
||||||
|
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import "reflect"
|
|||||||
|
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
type Props struct {
|
type Props struct {
|
||||||
Code string `token:"code"` // http status code
|
Code uint16 `token:"code"` // http status code
|
||||||
Message string `token:"message"` // status message
|
Message string `token:"message"` // status message
|
||||||
Description string `token:"description"` // status description
|
Description string `token:"description"` // status description
|
||||||
OriginalURI string `token:"original_uri"` // (ingress-nginx) URI that caused the error
|
OriginalURI string `token:"original_uri"` // (ingress-nginx) URI that caused the error
|
||||||
|
@ -12,7 +12,7 @@ func TestProps_Values(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
assert.Equal(t, template.Props{
|
assert.Equal(t, template.Props{
|
||||||
Code: "a",
|
Code: 1,
|
||||||
Message: "b",
|
Message: "b",
|
||||||
Description: "c",
|
Description: "c",
|
||||||
OriginalURI: "d",
|
OriginalURI: "d",
|
||||||
@ -25,7 +25,7 @@ func TestProps_Values(t *testing.T) {
|
|||||||
L10nDisabled: true,
|
L10nDisabled: true,
|
||||||
ShowRequestDetails: false,
|
ShowRequestDetails: false,
|
||||||
}.Values(), map[string]any{
|
}.Values(), map[string]any{
|
||||||
"code": "a",
|
"code": uint16(1),
|
||||||
"message": "b",
|
"message": "b",
|
||||||
"description": "c",
|
"description": "c",
|
||||||
"original_uri": "d",
|
"original_uri": "d",
|
||||||
|
@ -84,33 +84,33 @@ func TestRender(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
"common case": {
|
"common case": {
|
||||||
giveTemplate: "{{code}}: {{ message }} {{description}}",
|
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",
|
wantResult: "404: Not found Blah",
|
||||||
},
|
},
|
||||||
"html markup": {
|
"html markup": {
|
||||||
giveTemplate: "<!-- comment --><html><body>{{code}}: {{ message }} {{description}}</body></html>",
|
giveTemplate: "<!-- comment --><html><body>{{code}}: {{ message }} {{description}}</body></html>",
|
||||||
giveProps: template.Props{Code: "201", Message: "lorem ipsum"},
|
giveProps: template.Props{Code: 201, Message: "lorem ipsum"},
|
||||||
wantResult: "<!-- comment --><html><body>201: lorem ipsum </body></html>",
|
wantResult: "<!-- comment --><html><body>201: lorem ipsum </body></html>",
|
||||||
},
|
},
|
||||||
"with line breakers": {
|
"with line breakers": {
|
||||||
giveTemplate: "\t {{code}}: {{ message }} {{description}}\n",
|
giveTemplate: "\t {{code | json}}: {{ message }} {{description}}\n",
|
||||||
giveProps: template.Props{},
|
giveProps: template.Props{},
|
||||||
wantResult: "\t : \n",
|
wantResult: "\t 0: \n",
|
||||||
},
|
},
|
||||||
"golang template": {
|
"golang template": {
|
||||||
giveTemplate: "\t {{code}} {{ .Code }}{{ if .Message }} Yeah {{end}}",
|
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 ",
|
wantResult: "\t 201 201 Yeah ",
|
||||||
},
|
},
|
||||||
|
|
||||||
"json common case": {
|
"json common case": {
|
||||||
giveTemplate: `{"code": {{code | json}}, "message": {"here":[ {{ message | json }} ]}, "desc": "{{description}}"}`,
|
giveTemplate: `{"code": {{code | json}}, "message": {"here":[ {{ message | json }} ]}, "desc": "{{description}}"}`,
|
||||||
giveProps: template.Props{Code: `404'"{`, Message: "Not found\t\r\n"},
|
giveProps: template.Props{Code: 404, Message: "'\"{Not found\t\r\n"},
|
||||||
wantResult: `{"code": "404'\"{", "message": {"here":[ "Not found\t\r\n" ]}, "desc": ""}`,
|
wantResult: `{"code": 404, "message": {"here":[ "'\"{Not found\t\r\n" ]}, "desc": ""}`,
|
||||||
},
|
},
|
||||||
"json golang template": {
|
"json golang template": {
|
||||||
giveTemplate: `{"code": "{{code}}", "message": {"here":[ "{{ if .Message }} Yeah {{end}}" ]}}`,
|
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 " ]}}`,
|
wantResult: `{"code": "201", "message": {"here":[ " Yeah " ]}}`,
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -127,7 +127,7 @@ func TestRender(t *testing.T) {
|
|||||||
|
|
||||||
"complete example with every property and function": {
|
"complete example with every property and function": {
|
||||||
giveProps: template.Props{
|
giveProps: template.Props{
|
||||||
Code: "404",
|
Code: 404,
|
||||||
Message: "Not found",
|
Message: "Not found",
|
||||||
Description: "Blah",
|
Description: "Blah",
|
||||||
OriginalURI: "/test",
|
OriginalURI: "/test",
|
||||||
|
Loading…
Reference in New Issue
Block a user