parent
2c976227dd
commit
121c057b90
10 changed files with 411 additions and 9 deletions
|
@ -396,6 +396,39 @@ Here is an example of backends and servers definition:
|
|||
- `backend2` will forward the traffic to two servers: `http://172.17.0.4:80"` with weight `1` and `http://172.17.0.5:80` with weight `2` using `drr` load-balancing strategy.
|
||||
- a circuit breaker is added on `backend1` using the expression `NetworkErrorRatio() > 0.5`: watch error ratio over 10 second sliding window
|
||||
|
||||
## Custom Error pages
|
||||
|
||||
Custom error pages can be returned, in lieu of the default, according to frontend-configured ranges of HTTP Status codes.
|
||||
In the example below, if a 503 status is returned from the frontend "website", the custom error page at http://2.3.4.5/503.html is returned with the actual status code set in the HTTP header.
|
||||
Note, the 503.html page itself is not hosted on traefik, but some other infrastructure.
|
||||
|
||||
```toml
|
||||
[frontends]
|
||||
[frontends.website]
|
||||
backend = "website"
|
||||
[errors]
|
||||
[error.network]
|
||||
status = ["500-599"]
|
||||
backend = "error"
|
||||
query = "/{status}.html"
|
||||
[frontends.website.routes.website]
|
||||
rule = "Host: website.mydomain.com"
|
||||
|
||||
[backends]
|
||||
[backends.website]
|
||||
[backends.website.servers.website]
|
||||
url = "https://1.2.3.4"
|
||||
[backends.error]
|
||||
[backends.error.servers.error]
|
||||
url = "http://2.3.4.5"
|
||||
```
|
||||
|
||||
In the above example, the error page rendered was based on the status code.
|
||||
Instead, the query parameter can also be set to some generic error page like so: `query = "/500s.html"`
|
||||
|
||||
Now the 500s.html error page is returned for the configured code range.
|
||||
The configured status code ranges are inclusive; that is, in the above example, the 500s.html page will be returned for status codes 500 through, and including, 599.
|
||||
|
||||
# Configuration
|
||||
|
||||
Træfik's configuration has two parts:
|
||||
|
|
69
integration/error_pages_test.go
Normal file
69
integration/error_pages_test.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/containous/traefik/integration/try"
|
||||
"github.com/go-check/check"
|
||||
checker "github.com/vdemeester/shakers"
|
||||
)
|
||||
|
||||
// ErrorPagesSuite test suites (using libcompose)
|
||||
type ErrorPagesSuite struct{ BaseSuite }
|
||||
|
||||
func (ep *ErrorPagesSuite) SetUpSuite(c *check.C) {
|
||||
ep.createComposeProject(c, "error_pages")
|
||||
ep.composeProject.Start(c)
|
||||
}
|
||||
|
||||
func (ep *ErrorPagesSuite) TestSimpleConfiguration(c *check.C) {
|
||||
|
||||
errorPageHost := ep.composeProject.Container(c, "nginx2").NetworkSettings.IPAddress
|
||||
backendHost := ep.composeProject.Container(c, "nginx1").NetworkSettings.IPAddress
|
||||
|
||||
file := ep.adaptFile(c, "fixtures/error_pages/simple.toml", struct {
|
||||
Server1 string
|
||||
Server2 string
|
||||
}{backendHost, errorPageHost})
|
||||
defer os.Remove(file)
|
||||
cmd := exec.Command(traefikBinary, "--configFile="+file)
|
||||
|
||||
err := cmd.Start()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cmd.Process.Kill()
|
||||
|
||||
frontendReq, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:80", nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
frontendReq.Host = "test.local"
|
||||
|
||||
err = try.Request(frontendReq, 2*time.Second, try.BodyContains("nginx"))
|
||||
c.Assert(err, checker.IsNil)
|
||||
}
|
||||
|
||||
func (ep *ErrorPagesSuite) TestErrorPage(c *check.C) {
|
||||
|
||||
errorPageHost := ep.composeProject.Container(c, "nginx2").NetworkSettings.IPAddress
|
||||
backendHost := ep.composeProject.Container(c, "nginx1").NetworkSettings.IPAddress
|
||||
|
||||
//error.toml contains a mis-configuration of the backend host
|
||||
file := ep.adaptFile(c, "fixtures/error_pages/error.toml", struct {
|
||||
Server1 string
|
||||
Server2 string
|
||||
}{backendHost, errorPageHost})
|
||||
defer os.Remove(file)
|
||||
cmd := exec.Command(traefikBinary, "--configFile="+file)
|
||||
|
||||
err := cmd.Start()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cmd.Process.Kill()
|
||||
|
||||
frontendReq, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:80", nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
frontendReq.Host = "test.local"
|
||||
|
||||
err = try.Request(frontendReq, 2*time.Second, try.BodyContains("An error occurred."))
|
||||
c.Assert(err, checker.IsNil)
|
||||
}
|
27
integration/fixtures/error_pages/error.toml
Normal file
27
integration/fixtures/error_pages/error.toml
Normal file
|
@ -0,0 +1,27 @@
|
|||
defaultEntryPoints = ["http"]
|
||||
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":80"
|
||||
|
||||
[file]
|
||||
[backends]
|
||||
[backends.backend1]
|
||||
[backends.backend1.servers.server1]
|
||||
url = "http://{{.Server1}}:8989474"
|
||||
[backends.error]
|
||||
[backends.error.servers.error]
|
||||
url = "http://{{.Server2}}:80"
|
||||
[frontends]
|
||||
[frontends.frontend1]
|
||||
passHostHeader = true
|
||||
backend = "backend1"
|
||||
[frontends.frontend1.routes.test_1]
|
||||
rule = "Host:test.local"
|
||||
[frontends.frontend1.errors]
|
||||
[frontends.frontend1.errors.networks]
|
||||
status = ["500-502", "503-599"]
|
||||
backend = "error"
|
||||
query = "/50x.html"
|
27
integration/fixtures/error_pages/simple.toml
Normal file
27
integration/fixtures/error_pages/simple.toml
Normal file
|
@ -0,0 +1,27 @@
|
|||
defaultEntryPoints = ["http"]
|
||||
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":80"
|
||||
|
||||
[file]
|
||||
[backends]
|
||||
[backends.backend1]
|
||||
[backends.backend1.servers.server1]
|
||||
url = "http://{{.Server1}}:80"
|
||||
[backends.error]
|
||||
[backends.error.servers.error]
|
||||
url = "http://{{.Server2}}:80"
|
||||
[frontends]
|
||||
[frontends.frontend1]
|
||||
passHostHeader = true
|
||||
backend = "backend1"
|
||||
[frontends.frontend1.routes.test_1]
|
||||
rule = "Host:test.local"
|
||||
[frontends.frontend1.errors]
|
||||
[frontends.frontend1.errors.networks]
|
||||
status = ["500-502", "503-599"]
|
||||
backend = "error"
|
||||
query = "/50x.html"
|
|
@ -38,6 +38,7 @@ func init() {
|
|||
check.Suite(&EurekaSuite{})
|
||||
check.Suite(&AcmeSuite{})
|
||||
check.Suite(&DynamoDBSuite{})
|
||||
check.Suite(&ErrorPagesSuite{})
|
||||
}
|
||||
|
||||
var traefikBinary = "../dist/traefik"
|
||||
|
|
4
integration/resources/compose/error_pages.yml
Normal file
4
integration/resources/compose/error_pages.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
nginx1:
|
||||
image: nginx:alpine
|
||||
nginx2:
|
||||
image: nginx:alpine
|
77
middlewares/error_pages.go
Normal file
77
middlewares/error_pages.go
Normal file
|
@ -0,0 +1,77 @@
|
|||
package middlewares
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/containous/traefik/log"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/vulcand/oxy/forward"
|
||||
"github.com/vulcand/oxy/utils"
|
||||
)
|
||||
|
||||
//ErrorPagesHandler is a middleware that provides the custom error pages
|
||||
type ErrorPagesHandler struct {
|
||||
HTTPCodeRanges [][2]int
|
||||
BackendURL string
|
||||
errorPageForwarder *forward.Forwarder
|
||||
}
|
||||
|
||||
//NewErrorPagesHandler initializes the utils.ErrorHandler for the custom error pages
|
||||
func NewErrorPagesHandler(errorPage types.ErrorPage, backendURL string) (*ErrorPagesHandler, error) {
|
||||
fwd, err := forward.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//Break out the http status code ranges into a low int and high int
|
||||
//for ease of use at runtime
|
||||
var blocks [][2]int
|
||||
for _, block := range errorPage.Status {
|
||||
codes := strings.Split(block, "-")
|
||||
//if only a single HTTP code was configured, assume the best and create the correct configuration on the user's behalf
|
||||
if len(codes) == 1 {
|
||||
codes = append(codes, codes[0])
|
||||
}
|
||||
lowCode, err := strconv.Atoi(codes[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
highCode, err := strconv.Atoi(codes[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blocks = append(blocks, [2]int{lowCode, highCode})
|
||||
}
|
||||
return &ErrorPagesHandler{
|
||||
HTTPCodeRanges: blocks,
|
||||
BackendURL: backendURL + errorPage.Query,
|
||||
errorPageForwarder: fwd},
|
||||
nil
|
||||
}
|
||||
|
||||
func (ep *ErrorPagesHandler) ServeHTTP(w http.ResponseWriter, req *http.Request, next http.HandlerFunc) {
|
||||
recorder := newRetryResponseRecorder()
|
||||
recorder.responseWriter = w
|
||||
next.ServeHTTP(recorder, req)
|
||||
|
||||
w.WriteHeader(recorder.Code)
|
||||
//check the recorder code against the configured http status code ranges
|
||||
for _, block := range ep.HTTPCodeRanges {
|
||||
if recorder.Code >= block[0] && recorder.Code <= block[1] {
|
||||
log.Errorf("Caught HTTP Status Code %d, returning error page", recorder.Code)
|
||||
finalURL := strings.Replace(ep.BackendURL, "{status}", strconv.Itoa(recorder.Code), -1)
|
||||
if newReq, err := http.NewRequest(http.MethodGet, finalURL, nil); err != nil {
|
||||
w.Write([]byte(http.StatusText(recorder.Code)))
|
||||
} else {
|
||||
ep.errorPageForwarder.ServeHTTP(w, newReq)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//did not catch a configured status code so proceed with the request
|
||||
utils.CopyHeaders(w.Header(), recorder.Header())
|
||||
w.Write(recorder.Body.Bytes())
|
||||
}
|
140
middlewares/error_pages_test.go
Normal file
140
middlewares/error_pages_test.go
Normal file
|
@ -0,0 +1,140 @@
|
|||
package middlewares
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/codegangsta/negroni"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestErrorPage(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, "Test Server")
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
testErrorPage := &types.ErrorPage{Backend: "error", Query: "/test", Status: []string{"500-501", "503-599"}}
|
||||
testHandler, err := NewErrorPagesHandler(*testErrorPage, ts.URL)
|
||||
|
||||
assert.Equal(t, nil, err, "Should be no error")
|
||||
assert.Equal(t, testHandler.BackendURL, ts.URL+"/test", "Should be equal")
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", ts.URL+"/test", nil)
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, "traefik")
|
||||
})
|
||||
n := negroni.New()
|
||||
n.Use(testHandler)
|
||||
n.UseHandler(handler)
|
||||
|
||||
n.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, recorder.Code, "HTTP statusOK")
|
||||
assert.Contains(t, recorder.Body.String(), "traefik")
|
||||
|
||||
handler500 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(500)
|
||||
fmt.Fprintln(w, "oops")
|
||||
})
|
||||
recorder500 := httptest.NewRecorder()
|
||||
n500 := negroni.New()
|
||||
n500.Use(testHandler)
|
||||
n500.UseHandler(handler500)
|
||||
|
||||
n500.ServeHTTP(recorder500, req)
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, recorder500.Code, "HTTP status Internal Server Error")
|
||||
assert.Contains(t, recorder500.Body.String(), "Test Server")
|
||||
assert.NotContains(t, recorder500.Body.String(), "oops", "Should not return the oops page")
|
||||
|
||||
handler502 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(502)
|
||||
fmt.Fprintln(w, "oops")
|
||||
})
|
||||
recorder502 := httptest.NewRecorder()
|
||||
n502 := negroni.New()
|
||||
n502.Use(testHandler)
|
||||
n502.UseHandler(handler502)
|
||||
|
||||
n502.ServeHTTP(recorder502, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadGateway, recorder502.Code, "HTTP status Bad Gateway")
|
||||
assert.Contains(t, recorder502.Body.String(), "oops")
|
||||
assert.NotContains(t, recorder502.Body.String(), "Test Server", "Should return the oops page since we have not configured the 502 code")
|
||||
|
||||
}
|
||||
|
||||
func TestErrorPageQuery(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.RequestURI() == "/"+strconv.Itoa(503) {
|
||||
fmt.Fprintln(w, "503 Test Server")
|
||||
} else {
|
||||
fmt.Fprintln(w, "Failed")
|
||||
}
|
||||
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
testErrorPage := &types.ErrorPage{Backend: "error", Query: "/{status}", Status: []string{"503-503"}}
|
||||
testHandler, err := NewErrorPagesHandler(*testErrorPage, ts.URL)
|
||||
assert.Equal(t, nil, err, "Should be no error")
|
||||
assert.Equal(t, testHandler.BackendURL, ts.URL+"/{status}", "Should be equal")
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(503)
|
||||
fmt.Fprintln(w, "oops")
|
||||
})
|
||||
recorder := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", ts.URL+"/test", nil)
|
||||
n := negroni.New()
|
||||
n.Use(testHandler)
|
||||
n.UseHandler(handler)
|
||||
|
||||
n.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusServiceUnavailable, recorder.Code, "HTTP status Service Unavailable")
|
||||
assert.Contains(t, recorder.Body.String(), "503 Test Server")
|
||||
assert.NotContains(t, recorder.Body.String(), "oops", "Should not return the oops page")
|
||||
|
||||
}
|
||||
|
||||
func TestErrorPageSingleCode(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.RequestURI() == "/"+strconv.Itoa(503) {
|
||||
fmt.Fprintln(w, "503 Test Server")
|
||||
} else {
|
||||
fmt.Fprintln(w, "Failed")
|
||||
}
|
||||
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
testErrorPage := &types.ErrorPage{Backend: "error", Query: "/{status}", Status: []string{"503"}}
|
||||
testHandler, err := NewErrorPagesHandler(*testErrorPage, ts.URL)
|
||||
assert.Equal(t, nil, err, "Should be no error")
|
||||
assert.Equal(t, testHandler.BackendURL, ts.URL+"/{status}", "Should be equal")
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(503)
|
||||
fmt.Fprintln(w, "oops")
|
||||
})
|
||||
recorder := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", ts.URL+"/test", nil)
|
||||
n := negroni.New()
|
||||
n.Use(testHandler)
|
||||
n.UseHandler(handler)
|
||||
|
||||
n.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, http.StatusServiceUnavailable, recorder.Code, "HTTP status Service Unavailable")
|
||||
assert.Contains(t, recorder.Body.String(), "503 Test Server")
|
||||
assert.NotContains(t, recorder.Body.String(), "oops", "Should not return the oops page")
|
||||
|
||||
}
|
|
@ -772,6 +772,22 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
|
|||
backendsHealthcheck[frontend.Backend] = healthcheck.NewBackendHealthCheck(*hcOpts)
|
||||
}
|
||||
}
|
||||
|
||||
if len(frontend.Errors) > 0 {
|
||||
for _, errorPage := range frontend.Errors {
|
||||
if configuration.Backends[errorPage.Backend] != nil && configuration.Backends[errorPage.Backend].Servers["error"].URL != "" {
|
||||
errorPageHandler, err := middlewares.NewErrorPagesHandler(errorPage, configuration.Backends[errorPage.Backend].Servers["error"].URL)
|
||||
if err != nil {
|
||||
log.Errorf("Error creating custom error page middleware, %v", err)
|
||||
} else {
|
||||
negroni.Use(errorPageHandler)
|
||||
}
|
||||
} else {
|
||||
log.Errorf("Error Page is configured for Frontend %s, but either Backend %s is not set or Backend URL is missing", frontendName, errorPage.Backend)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
maxConns := configuration.Backends[frontend.Backend].MaxConn
|
||||
if maxConns != nil && maxConns.Amount != 0 {
|
||||
extractFunc, err := utils.NewExtractor(maxConns.ExtractorFunc)
|
||||
|
|
|
@ -54,6 +54,13 @@ type Route struct {
|
|||
Rule string `json:"rule,omitempty"`
|
||||
}
|
||||
|
||||
//ErrorPage holds custom error page configuration
|
||||
type ErrorPage struct {
|
||||
Status []string `json:"status,omitempty"`
|
||||
Backend string `json:"backend,omitempty"`
|
||||
Query string `json:"query,omitempty"`
|
||||
}
|
||||
|
||||
// Headers holds the custom header configuration
|
||||
type Headers struct {
|
||||
CustomRequestHeaders map[string]string `json:"customRequestHeaders,omitempty"`
|
||||
|
@ -108,15 +115,16 @@ func (h Headers) HasSecureHeadersDefined() bool {
|
|||
|
||||
// Frontend holds frontend configuration.
|
||||
type Frontend struct {
|
||||
EntryPoints []string `json:"entryPoints,omitempty"`
|
||||
Backend string `json:"backend,omitempty"`
|
||||
Routes map[string]Route `json:"routes,omitempty"`
|
||||
PassHostHeader bool `json:"passHostHeader,omitempty"`
|
||||
PassTLSCert bool `json:"passTLSCert,omitempty"`
|
||||
Priority int `json:"priority"`
|
||||
BasicAuth []string `json:"basicAuth"`
|
||||
WhitelistSourceRange []string `json:"whitelistSourceRange,omitempty"`
|
||||
Headers Headers `json:"headers,omitempty"`
|
||||
EntryPoints []string `json:"entryPoints,omitempty"`
|
||||
Backend string `json:"backend,omitempty"`
|
||||
Routes map[string]Route `json:"routes,omitempty"`
|
||||
PassHostHeader bool `json:"passHostHeader,omitempty"`
|
||||
PassTLSCert bool `json:"passTLSCert,omitempty"`
|
||||
Priority int `json:"priority"`
|
||||
BasicAuth []string `json:"basicAuth"`
|
||||
WhitelistSourceRange []string `json:"whitelistSourceRange,omitempty"`
|
||||
Headers Headers `json:"headers,omitempty"`
|
||||
Errors map[string]ErrorPage `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
// LoadBalancerMethod holds the method of load balancing to use.
|
||||
|
|
Loading…
Reference in a new issue