Chapter 17 · The utility layer

17.12 Django’s exceptions: django.core.exceptions

django/core/exceptions.py holds the exceptions its docstring calls global, many raised in one part of Django and caught in another: the bases of each model’s Model.DoesNotExist and Model.MultipleObjectsReturned, SuspiciousOperation and its subclasses, PermissionDenied, BadRequest, ImproperlyConfigured, ValidationError with NON_FIELD_ERRORS, the compiler’s EmptyResultSet and FullResultSet, and SynchronousOnlyOperation, which async_unsafe raises.

The code that raises an exception and the code that catches it are often in different parts of Django: the ORM raises a model’s Model.DoesNotExist and the template engine catches it; the request raises DisallowedHost and the handler answers it. The module imports nothing of Django but make_hashable, so any part can import it. Most of its classes are a name, a base and a docstring.

Who raises each, and what becomes of it

Table 17.3 A 400 or a 403 is the handler’s answer when the exception leaves a view or a middleware. A cell that says “it goes up” means only that the handler does not name that exception: a caller may catch it, and one that leaves a view or a middleware is a 500, like any the handler does not name, unless settings.DEBUG_PROPAGATE_EXCEPTIONS is true (How an exception becomes a response); the result sets are told in The where clause: build_filter, add_q and WhereNode.
exception base raised by what becomes of it
ObjectDoesNotExist base Exception raised by a model’s Model.DoesNotExist, from QuerySet.get (QuerySet.get django/db/models/query.py) what becomes of it get_object_or_404 raises Http404 instead (get_object_or_404 django/shortcuts.py); a template renders "string_if_invalid", empty by default
MultipleObjectsReturned base Exception raised by a model’s Model.MultipleObjectsReturned, from QuerySet.get what becomes of it it goes up
ObjectNotUpdated base Exception raised by a model’s Model.NotUpdated, from a forced update that changed no row (Model._save_table django/db/models/base.py) what becomes of it it goes up
FieldDoesNotExist base Exception raised by Options.get_field (Options.get_field django/db/models/options.py) what becomes of it the code that asked; a query raises FieldError instead
FieldError base Exception raised by a query’s keyword that names no field (Query.names_to_path django/db/models/sql/query.py) what becomes of it it goes up
FieldFetchBlocked base FieldError raised by FETCH_RAISE, when a field not loaded is read (FetchRaise.fetch django/db/models/fetch_modes.py) what becomes of it it goes up (Deferred fields and fetch modes in a queryset)
ValidationError base Exception raised by validators (validators.py django/core/) what becomes of it a form’s or a model’s errors
SuspiciousOperation base Exception raised by its subclasses; markup strip_tags gives up on (strip_tags django/utils/html.py) what becomes of it a 400, logged under "django.security" (response_for_exception django/core/handlers/exception.py)
DisallowedHost base SuspiciousOperation raised by a host ALLOWED_HOSTS refuses (HttpRequest.get_host django/http/request.py) what becomes of it a 400
DisallowedRedirect base SuspiciousOperation raised by a redirect too long or to a scheme not allowed (HttpResponseRedirectBase.__init__ django/http/response.py) what becomes of it a 400
RequestDataTooBig base SuspiciousOperation raised by a body over DATA_UPLOAD_MAX_MEMORY_SIZE (HttpRequest._check_data_too_big django/http/request.py) what becomes of it a 400
TooManyFieldsSent base SuspiciousOperation raised by fields over DATA_UPLOAD_MAX_NUMBER_FIELDS (QueryDict.__init__ django/http/request.py) what becomes of it a 400
TooManyFilesSent base SuspiciousOperation raised by files over DATA_UPLOAD_MAX_NUMBER_FILES (MultiPartParser._parse django/http/multipartparser.py) what becomes of it a 400
SuspiciousMultipartForm base SuspiciousOperation raised by a multipart body on which the parser seems to loop (LazyStream._update_unget_history django/http/multipartparser.py) what becomes of it a 400
SuspiciousFileOperation base SuspiciousOperation raised by a file name reaching outside its directory (safe_join django/utils/_os.py) what becomes of it a 400
BadRequest base Exception raised by a urlencoded form whose encoding is not UTF-8 (HttpRequest._load_post_and_files django/http/request.py) what becomes of it a 400
PermissionDenied base Exception raised by a permission check (AccessMixin.handle_no_permission django/contrib/auth/mixins.py) what becomes of it a 403; from a backend, authenticate tries no other (authenticate django/contrib/auth/__init__.py)
RequestAborted base Exception raised by a client gone (ASGIHandler.read_body django/core/handlers/asgi.py) what becomes of it ASGIHandler.handle sends nothing
ImproperlyConfigured base Exception raised by a setting read before any are configured (LazySettings._setup django/conf/__init__.py), and other wrong configurations what becomes of it it goes up
AppRegistryNotReady base Exception raised by the registry asked before it is ready (Apps.check_apps_ready django/apps/registry.py) what becomes of it it goes up
MiddlewareNotUsed base Exception raised by a middleware’s factory what becomes of it the layer is left out (Building the middleware chain)
ViewDoesNotExist base Exception raised by a dotted path that names no view (get_callable django/urls/utils.py) what becomes of it the URL checks report it
EmptyResultSet base Exception raised by a condition that can match no row (WhereNode.as_sql django/db/models/sql/where.py) what becomes of it the compiler sends no statement, or writes a false condition
FullResultSet base Exception raised by a condition that matches every row what becomes of it the compiler writes no "WHERE"
SynchronousOnlyOperation base Exception raised by async_unsafe what becomes of it it goes up

ValidationError: one error, a list, or a dictionary

ValidationError.__init__ gives the error one of three shapes, chosen by its first argument:

Given a ValidationError, it takes that error’s dictionary, its list, or its message, code and params, which win over a code and params passed beside it.

A single or list-shaped error iterates as its messages, each with its params put in by %; a dictionary-shaped one as pairs of a name and its messages. ValidationError.messages is a flat list of strings for any shape; ValidationError.message_dict is the dictionary of them, and raises AttributeError for the other shapes. ValidationError.update_error_dict adds the errors to a dictionary it is given, by name, or under NON_FIELD_ERRORS, "__all__"; Model.full_clean gathers its steps’ errors with it, and BaseForm.add_error files a form’s errors in the same shape (Validation: full_clean, cleaned_data and the errors).

ValidationError.__eq__ compares hashes. ValidationError.__hash__ hashes a single error’s message, code and params; a dictionary-shaped error’s dictionary through make_hashable, which sorts the names and keeps each list’s order; a list-shaped error’s errors sorted by message. So a list’s order does not count, the order within a dictionary’s list does, and one error is not equal to a list of it.

# a run: a ship's errors
small  = ValidationError("Tonnage %(tons)s is too small.", code="min_tonnage", params={"tons": 0})
both   = ValidationError([small, "A ship needs a home port."])
byname = ValidationError({"tonnage": small, "home": ["A ship needs a home port."]})
home   = both.error_list[1]

list(small)                             ['Tonnage 0 is too small.']
both.messages                           ['Tonnage 0 is too small.', 'A ship needs a home port.']
both.update_error_dict({})              {'__all__': [ValidationError(['Tonnage 0 is too small.']), ValidationError(['A ship needs a home port.'])]}
dict(byname)                            {'tonnage': ['Tonnage 0 is too small.'], 'home': ['A ship needs a home port.']}
ValidationError([byname]).messages      ['Tonnage 0 is too small.', 'A ship needs a home port.']
both == ValidationError([home, small])  True
small == ValidationError([small])       False

A model’s exceptions and the template engine

ModelBase makes each model that is not abstract its own Model.DoesNotExist, Model.MultipleObjectsReturned and Model.NotUpdated from ObjectDoesNotExist, MultipleObjectsReturned and ObjectNotUpdated, or from the parent model’s classes; Model.NotUpdated derives from DatabaseError too, for backward compatibility (ModelBase: a class statement becomes a model). An except can name one model’s error or every model’s.

ObjectDoesNotExist.silent_variable_failure is true. When a template variable’s lookup raises an exception with that attribute true, Variable._resolve_lookup gives the engine’s "string_if_invalid", empty by default, in its place; any other exception goes on up (Variables and filters: FilterExpression and Variable).

# a run: Valletta has two ships, Piraeus none
Port.objects.get(name="Rhodes")          raises Port.DoesNotExist: Port matching query does not exist.
  caught by except ObjectDoesNotExist    Port.DoesNotExist
[{{ port.ships.get }}], port Piraeus     '[]'
[{{ port.ships.get }}], port Valletta    raises Ship.MultipleObjectsReturned: get() returned more than one Ship -- it returned 2!

SynchronousOnlyOperation and async_unsafe

The documentation says some of Django relies on thread-safety and does not work under coroutines sharing a thread; async_unsafe marks such code. Its wrapper asks asyncio.get_running_loop whether a loop runs in the calling thread; if one does, and the environment variable "DJANGO_ALLOW_ASYNC_UNSAFE" is empty or unset, it raises SynchronousOnlyOperation, and otherwise calls the function. It asks about the thread, not the caller: a plain function called from a coroutine is refused too, and the same call through sync_to_async, on a thread with no loop, is not. The variable is read at each call.

Given a function, async_unsafe wraps it with a message of its own; given a message, it returns a decorator that raises with that one. BaseDatabaseWrapper marks the methods that open and close a connection, make a cursor, commit, roll back and keep savepoints, so a query run in a coroutine fails (The asynchronous methods of a queryset). A deployment check reports the variable set.

# harbour/office/notices.py
from django.utils.asyncio import async_unsafe


@async_unsafe
def post_notice(text):
    return f"posted: {text}"


@async_unsafe("The notice board is written from a thread.")
def pin_notice(text):
    return f"pinned: {text}"
# a run of the two functions
in a coroutine:
  post_notice("Beagle sails")                  raises SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.
  pin_notice("Beagle sails")                   raises SynchronousOnlyOperation: The notice board is written from a thread.
  the same, DJANGO_ALLOW_ASYNC_UNSAFE set       'pinned: Beagle sails'
  await sync_to_async(pin_notice)("Beagle sails")  'pinned: Beagle sails'