Friday, December 14, 2012

Appengine: Custom Error Handlers using webapp2 - Python 2.7

Google Developers has a writeup on Custom Error Responses using app.yaml

Here is a sample app.yaml

app.yaml

application: appname
version: 1
runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /static
  static_dir: static
  http_headers:
    Vary: Accept-Encoding

- url: /articles/.*
  script: articles.app


On the Development Environment, I get the following error when I visit http://localhost:8080/ i.e the home page
Not found error: / did not match any patterns in application configuration.

On Appengine, a standard 404 Error is shown

Error: Not Found
The requested URL / was not found on this server.

This is because the app.yaml file does not specify any handler for "/" Here is the articles.py file

articles.py

import webapp2

class HomePage(webapp2.RequestHandler):
  def get(self):
    self.response.write.out("Hello Birds!")

app = webapp2.WSGIApplication(
   [  (r'/articles/', HomePage)
      ],
   debug=True)

Specifying Custom Error Pages in app.yaml

Let us use the advice on this Google Developers page under Custom Error Responses section
 
error_handlers:
  - file: default_error.html

  - error_code: over_quota
    file: over_quota.html

The page further goes on to say, that error_code can be
1. over_quota
2. dos_api_denial
3. timeout

Whether this error_handler can handle 404 or 500 errors is not mentioned.

The error_code part is optional. So we edit app.yaml as follows app.yaml

app.yaml

application: appname
version: 1
runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /static
  static_dir: static
  http_headers:
    Vary: Accept-Encoding

- url: /articles/.*
  script: articles.app

error_handlers:
  - file: default_error.html

On testing on both the development server and live environments, the Custom error page, default_error.html is not served for 404 errors. We get the same 404 error as before.

I could not come up with something that would raise a 500 error without having to use a script.

Additionally, I read this comment on this Google Code page
"Over quota error pages are only displayed if your entire app is over quota (eg, out of instance hours or bandwidth). If you've run out of quota for a specific API, then an exception is thrown..."
Out of the three error codes supported, timeout is one which we can try to reproduce. Any request that takes more than 30 seconds will produce a timeout error. Change articles.py to the following

articles.py

import webapp2,time

class HomePage(webapp2.RequestHandler):
  def get(self):
    time.sleep(140)
    self.response.write("Hello Birds!")

app = webapp2.WSGIApplication(
   [  (r'/articles/', HomePage)
      ],
   debug=True)


The script sleeps for 140 seconds, enough to produce a timeout.

On visiting /articles/ on the dev_server, I do not get an error. But the live environment gives me the custom error page.

The Developer page says
"Warning!: Make sure that the path to the error response file does not overlap with static file handler paths. " 
But editing app.yaml, and moving the custom error file to a static directory, still gives me the custom error page on the live environment.

error_handlers:
  - file: static/default_error.html

Note that I have only checked this for timeout. It hopefully should work for dos_api_denial and over_quota too.

However, it does not work for 404 errors and I have my doubts about 500 errors being handled.

On the other hand, 500 errors, will most likely be encountered in scripts, and there is a simpler way to handle 404, 500 and other exceptions within a script.

The example below is for webapp2. I haven't tried the same on webapp.

Error handling using handle_exception():

http://webapp-improved.appspot.com/guide/exceptions.html describes the handle_exception method

handle_exception is a method of webapp2.RequestHandler. Any uncaught exceptions are passed on to this method. All this method does by default is raise the error to WSGIApplication.handle_exception().

To use it, we extend webapp2.RequestHandler.

articles.py is edited as follows
(code adapted from webapp-improved.appspot.com/guide/exceptions.html)

articles.py

import webapp2

class BaseHandler(webapp2.RequestHandler):
  def handle_exception(self, exception, debug):
    # Set a custom message.
    self.response.write('Custom Error Message')

    # If the exception is a HTTPException, use its error code.
    # Otherwise use a generic 500 error code.
    if isinstance(exception, webapp2.HTTPException):
      self.response.set_status(exception.code)
    else:
      self.response.set_status(500)

class HomePage(BaseHandler):
  def get(self):
    self.session.pop("h")
    self.response.write("Hello Birds!")

app = webapp2.WSGIApplication(
   [  (r'/articles/', HomePage)
      ],
   debug=True)


In the HomePage class, I am trying to pop a session when I have no session defined, so that a 500 error is raised.

On visiting /articles/ I get a 500 error,  with our custom text "Custom Error Message"

On visiting /articles/blah ,which I am not mapping to anything, I get a generic 404 error. This is because, all that the articles module is used for is the url /articles/ and nothing else.

Changed articles.py as follows

articles.py

import webapp2

class BaseHandler(webapp2.RequestHandler):
  def handle_exception(self, exception, debug):
    # Set a custom message.
    self.response.write('An error occurred.')

    # If the exception is a HTTPException, use its error code.
    # Otherwise use a generic 500 error code.
    if isinstance(exception, webapp2.HTTPException):
      self.response.set_status(exception.code)
    else:
      self.response.set_status(500)

class MissingPage(BaseHandler):
  def get(self):
    self.response.set_status(404)
    self.response.write("404 Custom Error!")
    
class HomePage(BaseHandler):
  def get(self):
    self.response.write("Hello Birds!")

app = webapp2.WSGIApplication(
   [  (r'/articles/', HomePage),
      (r'/articles/.*', MissingPage)
      ],
   debug=True)

Now, when I visit /articles/blah I get my 404 Custom error message. Instead of self.response.set_status, you can use self.error as well

class MissingPage(BaseHandler):
  def get(self):
    self.error(404)
    self.response.write("404 Custom Error!")

error in self.error is a method of webapp2.RequestHandler, which does the following
(code from http://code.google.com/codesearch#Qx8E-7HUBTk/trunk/python/lib/webapp2/webapp2.py Apache2 License)

self.response.status = HTTP_ERROR_CODE
self.response.clear()

How to handle errors out of url path /articles/.* ? 

Map everything else to a script, which does the error handling as done in articles.py

app.yaml

application: appname
version: 1
runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /static
  static_dir: static
  http_headers:
    Vary: Accept-Encoding

- url: /articles/.*
  script: articles.app

- url: /.*
  script: others.app

error_handlers:
- file: error.html

others.py

import webapp2

class BaseHandler(webapp2.RequestHandler):
  def handle_exception(self, exception, debug):
    self.response.write('An error occurred.')

    if isinstance(exception, webapp2.HTTPException):
      self.response.set_status(exception.code)
    else:
      self.response.set_status(500)

class MissingPage(BaseHandler):
  def get(self):
    self.response.set_status(404)
    self.response.write("404 Custom!")

app = webapp2.WSGIApplication(
   [  (r'/.*', MissingPage)
      ],
   debug=True)


This will map everything other than /articles/ and your static files as mentioned in app.yaml to our custom error page.

tl;dr :

Custom Error Handling in appengine:

 1. over_quota
 2. dos_api_denial
 3. timeout

for the above three use, error handler in app.yaml

error_handlers:
  - file: default_error.html

  - error_code: over_quota
    file: over_quota.html

For 404, 500 and other errors in appengine:

extend webapp2.RequestHandler
override handle_exception() method and set your own custom message.

I haven't described template usage to keep things simple for myself. Additionally, you should go through http://webapp-improved.appspot.com/guide/exceptions.html. I have skipped the logging part of handling exceptions, which you might want to include. 

Sources:
Google Developers - https://developers.google.com/appengine/docs/python/config/appconfig#Custom_Error_Responses

Webapp2 Docs - webapp-improved.appspot.com/guide/exceptions.html


Google Code Search - License Apache 2 - http://code.google.com/codesearch#Qx8E-7HUBTk/trunk/python/lib/webapp2/webapp2.py

No comments:

Post a Comment