djexcept is a module that brings flexible exception handling to the Django web framework.
A weakness of the great Django web framework are its poor builtin
error handling capabilities. In plain Django, you can define custom
error handlers for 4 different exceptions (SuspiciousOperation
,
PermissionDenied
, Http404
and a fallback handler for runtime
errors which doesn't even get the exception passed to decide what to
do best).
That is where djexcept hooks in. For every type of exception you like - even custom ones - it lets you decide how to handle it. It provides a default exception handler that makes additional information about the exception available in a template context and then renders a template of your choice. You can define the template to use, the HTTP status code to send and even change the exception handler on a per exception basis. However, sensible defaults are provided for you to get up and running as quick as possible.
Note: djexcept is still a young pice of software that I use in my Django projects. There may be bugs I haven't found yet. However, the codebase is reasonably small without lots of magic in it, hence things shouldn't go terribly wrong after all. Any bug reports and code review are highly appreciated.
It's as easy as this:
pip install djexcept
djexcept is tested to work with Django 1.10+, but it should also work
with earlier versions without problems. The contained middleware
is compatible with both the new MIDDLEWARE
as well as the pre 1.10
MIDDLEWARE_CLASSES
mechanism.
-
Add
djexcept.middleware.ExceptionHandlingMiddleware
to yourMIDDLEWARE
insettings.py
. No modification ofINSTALLED_APPS
is needed. -
Add the
@djexcept.register()
decorator to every custom exception you'd like to be handled by djexcept.import djexcept @djexcept.register() class MyCustomException(Exception): ...
-
For exceptions you haven't defined yourself, you can call the
register()
function e.g. from inside yoururls.py
file like so:import djexcept djexcept.register(ValueError)
-
Create a template called
exception.html
which might contain the following:<h1>{{ exc_modname }}</h1> <p>An exception of type {{ exc_name }} from {{ exc_module }} occured.</p> <p>Exception message: {{ exc }}</p> <p>The HTTP status code sent with this page is {{ status }}.</p>
-
Raise an exception you have just registered from within your view and watch what happens.
For every type of exception you register, you may specify several
parameters that influence how exceptions of that type are handled. The
following parameters may be passed to djexcept.register()
as keyword
arguments:
handler
: exception handler (callable or string of typepath.to.module.function
)handle_subtypes
: boolean that controls whether unregistered subclasses of the exception class being registered should be handled in the same way as their ancestor
All other keyword arguments are passed through straight to the exception handler. The following are those that the built-in exception handler understands:
template_name
: name of the template to use for rendering the error pagestatus
: HTTP status code for the error responseinclude_request
whether to include the request in template contextcontext
dictionary of which a copy is used as starting point for the template context; values in that context won't be overwritten
If some of these keyword arguments are not specified, default values
for them will automatically be inserted according to the DJEXCEPT_*
settings (see below) before the handler is called.
It is at the sole discretion of the chosen exception handler to interpret these keyword arguments as desired.
djexcept's built-in exception handler understands all of
the keyword arguments listed above and will create either
a django.template.response.SimpleTemplateResponse
or a
django.template.response.TemplateResponse
, depending on the setting
for DJEXCEPT_INCLUDE_REQUEST
.
It uses djexcept.util.populate_context()
to populate the context
with some handy values regarding the exception that you can use in your
template. Please see the API reference for details about these values.
There is nothing stopping you from writing your own exception handler, as long as it follows some guidelines.
An exception handler has to be a callable that accepts as positional
arguments the request, the exception object and at least the keyword
arguments listed in the previous section, because these, if unspecified
at time of registration, will be filled with default values. It must
return either a django.http.response.HttpResponse
object or None
,
in which case the exception isn't handled by djexcept and Django's
regular exception handling sets in.
If your custom handler doesn't care about some of the mandatory keyword
arguments, you could insert a **kwargs
at the end of its argument
list to catch any extra keyword arguments and have it working even when
new ones are added to djexcept in the future.
Note: Please keep in mind that exceptions raised from inside exception handlers are not handled by djexcept. to prevent creating an infinite loop.
Here is a simple example that populates the context with some value and then calls djexcept's built-in handler to construct the response. Please don't forget to create a copy of the context object before altering it, because dictionaries are mutable and you might otherwise change the context of subsequent exceptions.
import time
import djexcept
from djexcept.handlers import handle_exception
def my_handler(request, exc, ctime=True, localtime=True, context=None, **kwargs):
context = dict(context or {})
if localtime:
context.setdefault("localtime", time.localtime())
if ctime:
context.setdefault("ctime", time.ctime())
return handle_exception(request, exc, context=context, **kwargs)
# Use the handler like so:
djexcept.register(SomeException, handler=my_handler)
# or to include just ctime:
djexcept.register(OtherException, handler=my_handler, localtime=False)
djexcept has the ability to automatically handle any sub-type
of a registered exception type in the same way as their
registered ancestor. This behaviour is controlled by the
DJEXCEPT_HANDLE_SUBTYPES
setting and is enabled by default.
In practice, that allows you to write your own hirarchy of meaningful exception types that you can use within your view logic. Consider the following example:
import djexcept
@djexcept.register(template_name="exceptions/business_logic_violation.html")
class BusinessRuleViolation(Exception):
pass
class NegativeAccountBalance(BusinessRuleViolation):
pass
class OfferExpired(BusinessRuleViolation):
pass
Using this example, raising either NegativeAccountBalance
or
OfferExpired
will be handled as it was a BusinessRuleViolation
,
what it in fact is.
In your template, you could now introduce some conditional content:
{% if exc_name == "NegativeAccountBalance" %}
...
{% elif exc_name == "OfferExpired" %}
...
{% endif %}
In theory, you could even catch all possible sub-types of Exception
,
however doing so is not recommended because it will hide potential bugs
that might occur at runtime:
import djexcept
djexcept.register(Exception)
Sub-type handling can also be disabled per type by passing
handle_subtypes = False
to djexcept.register()
.
djexcept introduces some new settings that may be used in settings.py
to customize its behaviour. Neither of them are required for djexcept
to work, because all have sensible default values that should be just
fine for most users.
(default: exception.html
)
Name of the default template to use.
(default: 400
)
Default HTTP status code for exception pages.
(default: djexcept.handlers.handle_exception
)
Default exception handler. Please specify it as a string of the form
path.to.module.function
, as known from Django's MIDDLEWARE
list.
(default: True
)
Whether to treat unregistered subclasses of registered exception types in the same way as their ancestor.
(default: True
)
Whether to include the request
object into the template context.
(default: False
)
Whether to disable djexcept's exception handling when Django's debug mode is enabled. You might find this useful to see full tracebacks instead of your custom exception pages while developing your project.
The public API methods of the djexcept.registration
submodule are
also directly available in djexcept
for convenience.
Registers the given Exception subclass for error handling with djexcept.
The additional keyword arguments are treated as follows:
handler
: an exception handler to overwrite the default onehandle_subtypes
: may be used to overwrite theDJEXCEPT_HANDLE_SUBTYPES
setting on a per exception basis
All other keyword arguments are passed directly to the handler function when there is an exception to handle.
This function may also be used as a class decorator when defining custom exceptions.
djexcept.exceptions.RegistrationError
is raised if the class was
already registered.
Unregisters the given exception class from djexcept.
djexcept.exceptions.RegistrationError
is raised if the class wasn't
registered.
Checks whether the given Exception subclass is registered for use with djexcept.
Checks whether the given exception class is handled by djexcept.
If DJEXCEPT_HANDLE_SUBTYPES
setting is disabled and not overwritten
at registration stage, this function returns the same result as
djexcept.is_registered()
.
djexcept.handler.handle_exception(request, exc, template_name=None, status=None, include_request=None, context=None)
This is djexcept's default exception handler.
It uses djexcept.util.populate_context()
to populate the context
with some handy values regarding the exception that you can use in your
template.
A django.template.response.SimpleTemplateResponse
or
django.template.response.TemplateResponse
is returned.
Populates the given context dictionary with djexcept's handy default values. The dictionary is altered in-place, but values that are already present won't be overwritten.
The following values are added to the context:
exc
: the exception objectexc_name
: the name of the exception type (e.g.PermissionDenied
orValueError
)exc_module
: the module name of the exception's type (e.g.django.core.exceptions
orbuiltins
)exc_modname
: both concatenated, separated by a period (e.g.django.core.exceptions.PermissionDenied
orbuiltins.ValueError
)status
: the HTTP status code used (only added if notNone
)
Is raised when something went wrong at settings parsing.
Is raised when an illegal call to djexcept.register()
or
djexcept.unregister()
is made.
Contributions are always welcome. Please use issues and pull requests on GitHub.