Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Error Pages #1675

Merged
merged 17 commits into from
Jun 30, 2017
Merged

Custom Error Pages #1675

merged 17 commits into from
Jun 30, 2017

Conversation

bparli
Copy link
Contributor

@bparli bparli commented May 26, 2017

Description

Follow up to PR 1576 and issue 1634.

Following the above design and discussion:

  • New ErrorPage type to capture frontend error pages configurations
  • New error_pages middleware containing a response recorder
  • Documentation/example
  • Unit tests to ensure middleware is being initialized and rendering correctly
  • Basic integration tests

@nmengin
Copy link
Contributor

nmengin commented Jun 9, 2017

@bparli, your PR seems good for me.

But, I believe the issue 1634 forgets an Use Case : how to specify an error backend for only one error code?
Of course, it's possible to set a range of one code [404-404] but it's a bit ugly. It would be better to allow single host by using configuration as below :

 [frontends]
   [frontends.website]
   backend = "website"
   [errors]
     [error.network]
     status = "404"
     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"

Moreover, the best solution may be to specify a list of single code(s) and range (as printed page into printer windows) and configure it as below :

 [frontends]
   [frontends.website]
   backend = "website"
   [errors]
     [error.network]
     status = "401,404,[500-600]"
     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"

I guess the second suggestion is a little bit harder to develop.
It can be good to allow, at least, configuring a single code in a first time and maybe open a PR to improve the behavior with the list as described in the second solution.

@emilevauge, @bparli, what is your mind about this?

@bparli
Copy link
Contributor Author

bparli commented Jun 9, 2017

Thanks for taking a look @nmengin

The concern you raise did actually cross my mind, but I stuck to the original design and made sure the one-off HTTP code was still covered (albeit not very aesthetically as you point out). Also, I felt the one-off HTTP code may not be a common case; I've usually seen, or configured myself, for a range (or even an entire class) of codes. Of course, you all have a good vantage point so I can work on addressing your concern if you feel its helpful or necessary.

Also, did you mean to ask @emilevauge above instead of that other person?

@emilevauge
Copy link
Member

@containous/traefik do you think we should implement the one-off HTTP code on top of ranges? IMHO, ranges only may be OK at first.

@nmengin
Copy link
Contributor

nmengin commented Jun 12, 2017

@bparli @emilevauge , I am OK with you.

LGTM

Ben Parli and others added 3 commits June 13, 2017 10:31
Copy link
Member

@emilevauge emilevauge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @bparli !
Thanks for this great PR, and sorry for being slow on reviewing.
I made few comments and just wanted to know what you think of adding the possibility to manage variables in query: /{status}.html (like what we proposed in #1634). It may be too restricting not being able to configure the query. WDYT?
But other than these little things, looks very good to me ❤️

log.Debugf("Caught HTTP Status Code %d, returning error page", recorder.Code)
w.WriteHeader(recorder.Code)
if newReq, err := http.NewRequest(http.MethodGet, ep.BackendURL, nil); err != nil {
w.Write([]byte(http.StatusText(recorder.Code)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may log err here

docs/basics.md Outdated
[error.network]
status = ["500-599"]
backend = "error"
query = "/500.html"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/{status}.html ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems I originally misunderstood this point. I like it though and will extend to include the {status} query

c.Assert(err, checker.IsNil)
}

func (ep *ErrorPagesSuite) TestErrorPage(c *check.C) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be even better to test using a non empty query IMHO

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point

bparli added 2 commits June 26, 2017 11:45
* use renamed http recorder

* use renamed http recorder

* updates based on feedback

* grammar
Copy link
Contributor

@nmengin nmengin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many Thanks for this great PR!

LGTM

Copy link
Member

@juliens juliens left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great PR
Thanks

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)
re := regexp.MustCompile("{status}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can allocate the compiled regex in the construction of ErrorPagesHandler ?

Copy link
Contributor

@ldez ldez Jun 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you don't need a regex:

finalURL := strings.Replace(vli, "{status}", "400", -1)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @juliens, good point

and you're right @ldez, strings package will do the trick here

if err != nil {
return nil, err
}
highCode, err := strconv.Atoi(codes[1])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we haven't "-", it panic here with out of range
Can you handle the case when there is no "-" (with a good error)
And add a test for this

thx

Copy link
Contributor Author

@bparli bparli Jun 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are actually two options here, both simple. 1) catch it and log an error or 2) catch it and assume the user means to serve the error page for just the single code. In this case the single entry can still be used to create the correct array for checking at runtime. So, for example if ["503"] (instead of ["503-503"]) is configured, the array ["503", "503"] is created and we continue on. We essentially create the correct configuration on the user's behalf.

This was actually raised by @nmengin above, I should have just addressed it then

WDYT @juliens ?

Copy link
Member

@juliens juliens Jun 30, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that handle the "no range status code" would be better, but a log could be enough


if len(frontend.Errors) > 0 {
for _, errorPage := range frontend.Errors {
if configuration.Backends[errorPage.Backend] != nil && configuration.Backends[errorPage.Backend].Servers["error"].URL != "" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a log if the backend doesn't exist and/or if the URL is empty

bparli added 2 commits June 30, 2017 09:56
* use renamed http recorder

* use renamed http recorder

* updates based on feedback

* grammar

* use string instead of regex, handle single code

* add error message for mis-configured error backend

*Use string instead of regex, handle single code
@bparli
Copy link
Contributor Author

bparli commented Jun 30, 2017

Thanks everyone for the reviews and feedback. I think its all addressed to this point

Copy link
Member

@juliens juliens left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM
Thanks

docs/basics.md Outdated
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"```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you replace ``` by `

@ldez
Copy link
Contributor

ldez commented Jun 30, 2017

@bparli It's time to squash and rebase 😃

Copy link
Contributor

@ldez ldez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@ldez ldez merged commit 121c057 into traefik:master Jun 30, 2017
@timoreimann timoreimann mentioned this pull request Aug 19, 2017
[frontends]
[frontends.website]
backend = "website"
[errors]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bit of documentation looks wrong: the test fixture that resembles this has [frontends.frontend1.errors]
[frontends.frontend1.errors.networks], but the doc here just says [errors] and [errors.networks].

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for noticing! Could you please file a new issue or ideally submit a PR? This one has been closed a while ago.

Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/middleware kind/enhancement a new or improved feature.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants