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

Add text and json fallback error handlers #1937

Merged
merged 7 commits into from Sep 30, 2020
Merged

Conversation

ahopkins
Copy link
Member

@ahopkins ahopkins commented Sep 29, 2020

Replaces #1905

PR #1768 added HTML default exception pages. This PR adds two alternative fallback exception pages: text and JSON.

This is NOT meant to replace custom error handlers, but is meant to be for fallback.

This is configurable with:

app.config.FALLBACK_ERROR_FORMAT = "text"
# or
app.config.FALLBACK_ERROR_FORMAT = "json"

The default will still be HTML. And, the HTML page is untouched (except for some fixes to make valid HTML.

Just like in HTML, there are two handlers: one for debuggable exception responses, the other for minimal responses.

Text

⚠️ 500 — Internal Server Error
==============================
foobar

Exception: foobar while handling path /hello
Traceback of __BASE__ (most recent call last):

  Exception: foobar
    File /path/to/sanic/app.py, line 938, in handle_request
    response = await response

    File server.py, line 12, in foo
    raise Exception("foobar")
⚠️ 500 — Internal Server Error
==============================
The server encountered an internal error and cannot complete your request.

JSON

{
  "description": "Internal Server Error",
  "status": 500,
  "message": "foobar",
  "path": "/hello",
  "args": {
    "name": [
      "world"
    ]
  },
  "exceptions": [
    {
      "type": "Exception",
      "exception": "foobar",
      "frames": [
        {
          "file": "/path/to/sanic/app.py",
          "line": 938,
          "name": "handle_request",
          "src": "response = await response"
        },
        {
          "file": "server.py",
          "line": 12,
          "name": "foo",
          "src": "raise Exception(\"foobar\")"
        }
      ]
    }
  ]
}
{
  "description": "Internal Server Error",
  "status": 500,
  "message": "The server encountered an internal error and cannot complete your request."
}

@ahopkins
Copy link
Member Author

I am still working on the tests, but I wanted to get this out there for some feedback ASAP so we can try and include it in upcoming release. It is not a breaking change, and a relatively minor change.

Copy link
Member

@ashleysommer ashleysommer left a comment

Choose a reason for hiding this comment

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

Looks great!

FWIW, the old HTML responses are actually valid HTML5.
The <!DOCTYPE html> hint tag puts the document in HTML5-mode. In HTML5 the <html> document wrapper tag is not needed (though most people put it in anyway for html4 backward compatibility). Also the <head> tag is technically optional, you can just put your meta, script, title, and style tags before your content. Along the same lines, it doesn't need a <body> tag if there's no head before it.
<p> tags never need to be closed (some linters even generate an error for a closing </p> tag) and finally </body> and </html> closing tags don't need to be present at the end of the file (they are assumed present if the HTML just ends).

@ahopkins
Copy link
Member Author

Looks great!

FWIW, the old HTML responses are actually valid HTML5.

The <!DOCTYPE html> hint tag puts the document in HTML5-mode. In HTML5 the <html> document wrapper tag is not needed (though most people put it in anyway for html4 backward compatibility). Also the <head> tag is technically optional, you can just put your meta, script, title, and style tags before your content. Along the same lines, it doesn't need a <body> tag if there's no head before it.

<p> tags never need to be closed (some linters even generate an error for a closing </p> tag) and finally </body> and </html> closing tags don't need to be present at the end of the file (they are assumed present if the HTML just ends).

I put it in a linter and it yelled at me 🤷‍♂️

@vltr
Copy link
Member

vltr commented Sep 29, 2020

long time no see, but I really like this PR. I was just wondering: what if we read the Content-Type header from the request (in case it exists) and provide the error properly? let's say, text/plain would return the text error, application/json would return the JSON error and anything else not declared would return the default fallback? it's just an initial thought, it's open to ideas 😅

@ahopkins
Copy link
Member Author

ahopkins commented Sep 29, 2020

long time no see, but I really like this PR. I was just wondering: what if we read the Content-Type header from the request (in case it exists) and provide the error properly? let's say, text/plain would return the text error, application/json would return the JSON error and anything else not declared would return the default fallback? it's just an initial thought, it's open to ideas 😅

I like this idea. Maybe this can be the behavior for

app.config.FALLBACK_ERROR_FORMAT = "auto"

I would suggest that would be a sensible default. But since that would be a breaking change, maybe we don't make that default until after LTS.

@vltr
Copy link
Member

vltr commented Sep 29, 2020

app.config.FALLBACK_ERROR_FORMAT = "auto"

hey, I like that as well! 😎 I don't know if this will add more complexity to the code (after all, "gotta go fast"), but this is just in case of an error, which is not what any developer is looking on their app (except during development, sure), so ... but, even so, I'm pretty sure that could be passed to a constructor / partial to eliminate any unnecessary ifs and / or elses from the code (not that I'm concerned, but I had to bring this up before this could become a concern).

@ahopkins
Copy link
Member Author

@vltr, I think the main usage of this is really for development purposes. They should be safe enough to not leak in production, but this is not intended to be a production-ready replacement for error handling. Primarily because (as mentioned in the other thread), there are too many considerations for us to be able to account for. With that said, I will keep this in mind.

@vltr
Copy link
Member

vltr commented Sep 29, 2020

@ahopkins yeap, being just for development I think it's fine ... I just mentioned about performance because, well, never mind, that should not be the case 👍

@ahopkins
Copy link
Member Author

Looks great!

FWIW, the old HTML responses are actually valid HTML5.
The <!DOCTYPE html> hint tag puts the document in HTML5-mode. In HTML5 the <html> document wrapper tag is not needed (though most people put it in anyway for html4 backward compatibility). Also the <head> tag is technically optional, you can just put your meta, script, title, and style tags before your content. Along the same lines, it doesn't need a <body> tag if there's no head before it.
<p> tags never need to be closed (some linters even generate an error for a closing </p> tag) and finally </body> and </html> closing tags don't need to be present at the end of the file (they are assumed present if the HTML just ends).

I discovered the issue. Needed a single closing </div>.

@vltr
Copy link
Member

vltr commented Sep 29, 2020

I discovered the issue. Needed a single closing </div>.

"The missing </div>". Sorry, but I had to ... 🤣

ashleysommer
ashleysommer previously approved these changes Sep 30, 2020
@ashleysommer
Copy link
Member

Ok, I think we can still push this out in this release, with default format as "html", to keep compatibility. But we'll change it to default to "auto" in v21.03.

@ashleysommer
Copy link
Member

ashleysommer commented Sep 30, 2020

There are still some type-hinting mypy errors

sanic/errorpages.py:310: error: Incompatible types in assignment (expression has type "Type[HTMLRenderer]", variable has type "Optional[BaseRenderer]")

sanic/errorpages.py:315: error: Incompatible types in assignment (expression has type "Union[Type[JSONRenderer], Type[HTMLRenderer]]", variable has type "Optional[BaseRenderer]")

sanic/errorpages.py:317: error: Incompatible types in assignment (expression has type "Type[HTMLRenderer]", variable has type "Optional[BaseRenderer]")

sanic/errorpages.py:322: error: Incompatible types in assignment (expression has type "Union[Type[BaseRenderer], BaseRenderer, None]", variable has type "Optional[BaseRenderer]")

sanic/errorpages.py:327: error: Incompatible types in assignment (expression has type "Union[Type[BaseRenderer], BaseRenderer, None]", variable has type "Optional[BaseRenderer]")

sanic/errorpages.py:329: error: "BaseRenderer" not callable

sanic/errorpages.py:329: error: "None" not callable

@codecov
Copy link

codecov bot commented Sep 30, 2020

Codecov Report

Merging #1937 into master will increase coverage by 0.28%.
The diff coverage is 99.24%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1937      +/-   ##
==========================================
+ Coverage   92.45%   92.73%   +0.28%     
==========================================
  Files          27       27              
  Lines        3100     3192      +92     
  Branches      555      563       +8     
==========================================
+ Hits         2866     2960      +94     
+ Misses        157      155       -2     
  Partials       77       77              
Impacted Files Coverage Δ
sanic/config.py 100.00% <ø> (ø)
sanic/errorpages.py 99.25% <99.24%> (-0.75%) ⬇️
sanic/server.py 81.61% <0.00%> (+0.56%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 0c4a9b1...fdfc129. Read the comment docs.

Copy link
Member

@ashleysommer ashleysommer left a comment

Choose a reason for hiding this comment

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

Go ahead and merge

@ahopkins ahopkins merged commit 066df2c into master Sep 30, 2020
@ahopkins ahopkins deleted the default-error-handlers branch September 30, 2020 12:11
@ahopkins ahopkins mentioned this pull request Sep 30, 2020
@Tronic
Copy link
Member

Tronic commented Oct 4, 2020

If I understood the discussion and the code correctly, only the request Content-Type header is used for determining which format to return in auto mode. Content-Type on request specifies the format of the request itself, and is normally not provided in GET requests which carry no body.

It might be a good idea to use the Accept request header if available, and see if it explicitly lists text/html (I think all browsers put that at the very beginning). Curl and wget by default send Accept: */* in which case a text response is more appropriate. Some JSON APIs only work when Accept: application/json is specified on the request, so despite the widespread misuse of Content-Type for this, it might be enough to only examine the Accept header.

@ahopkins
Copy link
Member Author

ahopkins commented Oct 4, 2020

I thought about that, but as you said, most browsers list a bunch of different options in Accept headers.

Before this becomes default, we need a much smarter logic switch on this.

@Tronic
Copy link
Member

Tronic commented Oct 5, 2020

I just tested Chrome, Firefox, Safari, Edge, IE11, Android-Chrome, Android-Firefox, iOS-Safari and Opera Mini, which all can be handled by accept.startswith("text/html"), really fast with no need for further parsing. The fetch API uses */* by default, as do wget and curl. (except for IE11 which doesn't support fetch and Opera Mini which uses */*;image/webp for whatever the reason).

The results and a quickly hacked testing page at https://browser.zi.fi/

@ahopkins
Copy link
Member Author

ahopkins commented Oct 5, 2020

Nice. @Tronic can you move this to a new issue for further development?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants