Repository: falconry/falcon
Pull request #1655 (2020-01-25 19:13:40)

Fix: Make error initializers more consistent
# Summary of Changes

Normalized the interface of the http errors with common title and description kwargs.
Compared to the issue I've left the `headers` kw as is, since it's used in all errors

# Related Issues

Fixes:  #777

# Pull Request Checklist

This is just a reminder about the most common mistakes.  Please make sure that you tick all *appropriate* boxes.  But please read our [contribution guide](https://github.com/falconry/falcon/blob/master/CONTRIBUTING.md) at least once; it will save you a few review cycles!

If an item doesn't apply to your pull request, **check it anyway** to make it apparent that there's nothing to do.

- [x] Added **tests** for changed code.
- [x] Prefixed code comments with GitHub nick and an appropriate prefix.
- [x] Coding style is consistent with the rest of the framework.
- [x] Updated **documentation** for changed code.
    - [x] Added docstrings for any new classes, functions, or modules.
    - [x] Updated docstrings for any modifications to existing code.
    - [x] Added references to new classes, functions, or modules to the relevant RST file under `docs/`.
    - [x] Updated all relevant supporting documentation files under `docs/`.
    - [x] A copyright notice is included at the top of any new modules (using your own name or the name of your organization).
    - [x] Changed/added classes/methods/functions have appropriate `versionadded`, `versionchanged`, or `deprecated` [directives](http://www.sphinx-doc.org/en/stable/usage/restructuredtext/directives.html?highlight=versionadded#directive-versionadded).
- [x] Changes (and possible deprecations) have [towncrier](https://pypi.org/project/towncrier/) news fragments under `docs/_newsfragments/`. (Run `towncrier --draft` to ensure it renders correctly.)

If you have *any* questions to *any* of the points above, just **submit and ask**! This checklist is here to *help* you, not to deter you from contributing!

*PR template inspired by the attrs project.*


codecov[bot]: # [Codecov](https://codecov.io/gh/falconry/falcon/pull/1655?src=pr&el=h1) Report
> Merging [#1655](https://codecov.io/gh/falconry/falcon/pull/1655?src=pr&el=desc) into [master](https://codecov.io/gh/falconry/falcon/commit/ddda42fed8e883924bc4b16940f73f0764f524ab?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/falconry/falcon/pull/1655/graphs/tree.svg?width=650&token=zOtkfNQ3h6&height=150&src=pr)](https://codecov.io/gh/falconry/falcon/pull/1655?src=pr&el=tree)

```diff
@@          Coverage Diff           @@
##           master   #1655   +/-   ##
======================================
  Coverage     100%    100%           
======================================
  Files          45      45           
  Lines        3115    3110    -5     
  Branches      480     480           
======================================
- Hits         3115    3110    -5
```


| [Impacted Files](https://codecov.io/gh/falconry/falcon/pull/1655?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [falcon/errors.py](https://codecov.io/gh/falconry/falcon/pull/1655/diff?src=pr&el=tree#diff-ZmFsY29uL2Vycm9ycy5weQ==) | `100% <100%> (ø)` | :arrow_up: |
| [falcon/http\_error.py](https://codecov.io/gh/falconry/falcon/pull/1655/diff?src=pr&el=tree#diff-ZmFsY29uL2h0dHBfZXJyb3IucHk=) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/falconry/falcon/pull/1655?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/falconry/falcon/pull/1655?src=pr&el=footer). Last update [ddda42f...c98c7f3](https://codecov.io/gh/falconry/falcon/pull/1655?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

CaselIT: I've a couple of questions:

- should the kwargument be kw only? Using `def __init__(self, *, title=None, ...)`?
- some types have additional keyword args, that are placed in `headers`, like `challenges` in `HTTPUnauthorized`. I've not changed this. Should we change it?

kgriffs: > should the kw argument be kw only? Using PEP 3102 def __init__(self, *, title=None, ...)?

Yes, I think that would be a Good Thing™. As an aside, we should consider doing this in other places as well so we don't have to worry about a simple reordering of kwargs down the road causing a breaking change. @vytas7 thoughts?


kgriffs: > some types have additional keyword args, that are placed in headers, like challenges in HTTPUnauthorized. I've not changed this. Should we change it?

It would not hurt to normalize the ordering of extra params such that they always follow _headers_ (although it would only be cosmetic if PEP 3102 is used). Regardless, we will need to make sure we have test coverage for things like that overriding any values also set in _headers_.
CaselIT: > we will need to make sure we have test coverage for things like that overriding any values also set in headers.

I think the current implementation overrides in all of them. I can check if that is the case and check if there are tests for it. Sadly the code coverage does not help in this case
CaselIT: I'm changing to kwonly.

There seem to be a lot of changes required, both in code and in documentations.
Just letting you know
CaselIT: I've notices that for the headers it says: `headers (dict or list)`

But the list case is not handled by all the errors that add something to the headers, like `HTTPUnauthorized`:

https://github.com/falconry/falcon/blob/c817a42d183b89250e633969a1b9a3e23d95aef5/falcon/errors.py#L165-L170




CaselIT: should be almost done other than https://github.com/falconry/falcon/pull/1655#issuecomment-578503132
CaselIT: > should the kw argument be kw only? Using PEP 3102 def __init__(self, *, title=None, ...)?

I know I'm the one who suggested it, and I've already implemented it, but thinking a bit more about it and also judging by the amount of refactoring that this has required in falcon, I'm not sure if it's worth the trouble. By it I mead that it will probably cause a lot of refactoring on the users who use falcon, without a clear improvement.
I think we can keep the changes in falcon, but I would revert the actual requirement for kw_only. Or at least move the `*` after the `title` and `description`.

What do you thing @kgriffs @vytas7 
vytas7: > I know I'm the one who suggested it, and I've already implemented it, but thinking a bit more about it and also judging by the amount of refactoring that this has required in falcon, I'm not sure if it's worth the trouble. By it I mead that it will probably cause a lot of refactoring on the users who use falcon, without a clear improvement.

I'd need to read through signatures of existing `HTTPError`s & `HTTPStatus`es more carefully to provide a more informed opinion.

A quick reaction off the top of my head would be that it indeed sounds like a bit too abrupt breaking change without any compatibility shims or deprecation warnings, even given that 3.0 will be a major release.
kgriffs: Good points. It does look like that will cause a bigger burden on app developers than we want to impose this time around. We may have to save this for a 4.0, since there are already enough other things in 3.0 for people to worry about.
CaselIT: I agree.
Should I actually deprecate them with a warning, or will a line in the documentation suffice?
Regarding the deprecation, I'm not sure if there is something better than something like

```py
def deprecate_arg(allowed=0, fn):
  def wraps(self, *args, **kwargs):
    if len(args)> allowed:
      # warn here
    return fn(self, *args, **kwargs)
  return wraps

class HttpBadRequest(...):
  @deprecate_arg
  def __init__(self, title=None, description=None, headers=None, **kwargs):
    ...
```
CaselIT: The only thing missing is to decide if we want to raise a warning regarding the kwonly args, or if we should just add a note in the docs. (or maybe nothing at all)
kgriffs: IMO we should at least add a warning section to the relevant docstrings. As for outputting a deprecation warning, I'm inclined to say yes as well, since this raising errors is typically not super performance sensitive, and the extra nudge to the app developer can't hurt. @vytas7 thoughts?
vytas7: Agreed re the deprecation warning, not many are going to care or even notice about a line or two in the docs. It is ideal to emit them if possible, and agreed re the performance impact being lesser for exceptions.
CaselIT: ok, so something like the `deprecate_arg` in the snippet
CaselIT: Ok I've replaced the kwonly with a warning and a note in the documents. (not sure if it should be a warning)

I think it's ready for review
CaselIT: I've noticed that the changelog needs updating.
kgriffs: > I've noticed that the changelog needs updating.

Nice catch! I think we are all trying to get used to making news fragments part of the PRs.

Patch:
diff --git a/README.rst b/README.rst
index 34e09fe..5c62a59 100644
--- a/README.rst
+++ b/README.rst
@@ -455,8 +455,8 @@ bodies.
                            'database. It worked on my box.')
 
             raise falcon.HTTPError(falcon.HTTP_725,
-                                   'Database Error',
-                                   description)
+                                   title='Database Error',
+                                   description=description)
 
 
     class SinkAdapter:
@@ -488,18 +488,18 @@ bodies.
                 description = ('Please provide an auth token '
                                'as part of the request.')
 
-                raise falcon.HTTPUnauthorized('Auth token required',
-                                              description,
-                                              challenges,
+                raise falcon.HTTPUnauthorized(title='Auth token required',
+                                              description=description,
+                                              challenges=challenges,
                                               href='http://docs.example.com/auth')
 
             if not self._token_is_valid(token, account_id):
                 description = ('The provided auth token is not valid. '
                                'Please request a new token and try again.')
 
-                raise falcon.HTTPUnauthorized('Authentication required',
-                                              description,
-                                              challenges,
+                raise falcon.HTTPUnauthorized(title='Authentication required',
+                                              description=description,
+                                              challenges=challenges,
                                               href='http://docs.example.com/auth')
 
         def _token_is_valid(self, token, account_id):
@@ -511,13 +511,13 @@ bodies.
         def process_request(self, req, resp):
             if not req.client_accepts_json:
                 raise falcon.HTTPNotAcceptable(
-                    'This API only supports responses encoded as JSON.',
+                    title='This API only supports responses encoded as JSON.',
                     href='http://docs.examples.com/api/json')
 
             if req.method in ('POST', 'PUT'):
                 if 'application/json' not in req.content_type:
                     raise falcon.HTTPUnsupportedMediaType(
-                        'This API only supports requests encoded as JSON.',
+                        title='This API only supports requests encoded as JSON.',
                         href='http://docs.examples.com/api/json')
 
 
@@ -537,16 +537,16 @@ bodies.
 
             body = req.stream.read()
             if not body:
-                raise falcon.HTTPBadRequest('Empty request body',
-                                            'A valid JSON document is required.')
+                raise falcon.HTTPBadRequest(title='Empty request body',
+                                            description='A valid JSON document is required.')
 
             try:
                 req.context.doc = json.loads(body.decode('utf-8'))
 
             except (ValueError, UnicodeDecodeError):
                 raise falcon.HTTPError(falcon.HTTP_753,
-                                       'Malformed JSON',
-                                       'Could not decode the request body. The '
+                                       title='Malformed JSON',
+                                       description='Could not decode the request body. The '
                                        'JSON was incorrect or not encoded as '
                                        'UTF-8.')
 
@@ -566,7 +566,7 @@ bodies.
                        'exceed ' + str(limit) + ' bytes in length.')
 
                 raise falcon.HTTPPayloadTooLarge(
-                    'Request body is too large', msg)
+                    title='Request body is too large', description=msg)
 
         return hook
 
@@ -591,9 +591,9 @@ bodies.
                                'We appreciate your patience.')
 
                 raise falcon.HTTPServiceUnavailable(
-                    'Service Outage',
-                    description,
-                    30)
+                    title='Service Outage',
+                    description=description,
+                    retry_after=30)
 
             # NOTE: Normally you would use resp.media for this sort of thing;
             # this example serves only to demonstrate how the context can be
@@ -610,8 +610,8 @@ bodies.
                 doc = req.context.doc
             except AttributeError:
                 raise falcon.HTTPBadRequest(
-                    'Missing thing',
-                    'A thing must be submitted in the request body.')
+                    title='Missing thing',
+                    description='A thing must be submitted in the request body.')
 
             proper_thing = self.db.add_thing(doc)
 
diff --git a/docs/_newsfragments/777.feature.rst b/docs/_newsfragments/777.feature.rst
new file mode 100644
index 0000000..a43fa5c
--- /dev/null
+++ b/docs/_newsfragments/777.feature.rst
@@ -0,0 +1,3 @@
+The error classes in ``falcon.errors`` were updated to have the ``title`` and
+``description`` keyword arguments and to correctly handle headers passed as
+list of tuples
\ No newline at end of file
diff --git a/docs/_newsfragments/777.misc.rst b/docs/_newsfragments/777.misc.rst
new file mode 100644
index 0000000..0eff343
--- /dev/null
+++ b/docs/_newsfragments/777.misc.rst
@@ -0,0 +1,2 @@
+Deprecate the use of positional arguments for the optional kw args of
+the HTTPError subclasses
\ No newline at end of file
diff --git a/docs/_newsfragments/777.removal.rst b/docs/_newsfragments/777.removal.rst
new file mode 100644
index 0000000..cf1c45e
--- /dev/null
+++ b/docs/_newsfragments/777.removal.rst
@@ -0,0 +1,2 @@
+The class :class:`~.falcon.http_error.NoRepresentation` was removed since
+all :class:`~.falcon.HTTPError` have optional representation.
\ No newline at end of file
diff --git a/docs/api/errors.rst b/docs/api/errors.rst
index a2b959d..448c45c 100644
--- a/docs/api/errors.rst
+++ b/docs/api/errors.rst
@@ -54,8 +54,8 @@ All classes are available directly in the ``falcon`` package namespace::
             # ...
 
             raise falcon.HTTPBadRequest(
-                "TTL Out of Range",
-                "The message's TTL must be between 60 and 300 seconds, inclusive."
+                title="TTL Out of Range",
+                description="The message's TTL must be between 60 and 300 seconds, inclusive."
             )
 
             # ...
@@ -79,7 +79,7 @@ Base Class
 Mixins
 ------
 
-.. autoclass:: falcon.http_error.NoRepresentation
+.. autoclass:: falcon.http_error.OptionalRepresentation
     :members:
 
 .. _predefined_errors:
diff --git a/docs/api/hooks.rst b/docs/api/hooks.rst
index 4a4c797..20cfa00 100644
--- a/docs/api/hooks.rst
+++ b/docs/api/hooks.rst
@@ -15,7 +15,7 @@ an image resource:
     def validate_image_type(req, resp, resource, params):
         if req.content_type not in ALLOWED_IMAGE_TYPES:
             msg = 'Image type not allowed. Must be PNG, JPEG, or GIF'
-            raise falcon.HTTPBadRequest('Bad request', msg)
+            raise falcon.HTTPBadRequest(title='Bad request', description=msg)
 
 You would attach this hook to an ``on_post`` responder like so:
 
@@ -56,7 +56,7 @@ as needed:
     def validate_image_type(req, resp, resource, params, allowed_types):
         if req.content_type not in allowed_types:
             msg = 'Image type not allowed.'
-            raise falcon.HTTPBadRequest('Bad request', msg)
+            raise falcon.HTTPBadRequest(title='Bad request', description=msg)
 
     @falcon.before(validate_image_type, ['image/png'])
     def on_post(self, req, resp):
diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst
index 8433706..a54d91b 100644
--- a/docs/user/quickstart.rst
+++ b/docs/user/quickstart.rst
@@ -112,8 +112,8 @@ parameters, handling errors, and working with request and response bodies.
                            'database. It worked on my box.')
 
             raise falcon.HTTPError(falcon.HTTP_725,
-                                   'Database Error',
-                                   description)
+                                   title='Database Error',
+                                   description=description)
 
 
     class SinkAdapter:
@@ -145,18 +145,18 @@ parameters, handling errors, and working with request and response bodies.
                 description = ('Please provide an auth token '
                                'as part of the request.')
 
-                raise falcon.HTTPUnauthorized('Auth token required',
-                                              description,
-                                              challenges,
+                raise falcon.HTTPUnauthorized(title='Auth token required',
+                                              description=description,
+                                              challenges=challenges,
                                               href='http://docs.example.com/auth')
 
             if not self._token_is_valid(token, account_id):
                 description = ('The provided auth token is not valid. '
                                'Please request a new token and try again.')
 
-                raise falcon.HTTPUnauthorized('Authentication required',
-                                              description,
-                                              challenges,
+                raise falcon.HTTPUnauthorized(title='Authentication required',
+                                              description=description,
+                                              challenges=challenges,
                                               href='http://docs.example.com/auth')
 
         def _token_is_valid(self, token, account_id):
@@ -168,13 +168,13 @@ parameters, handling errors, and working with request and response bodies.
         def process_request(self, req, resp):
             if not req.client_accepts_json:
                 raise falcon.HTTPNotAcceptable(
-                    'This API only supports responses encoded as JSON.',
+                    title='This API only supports responses encoded as JSON.',
                     href='http://docs.examples.com/api/json')
 
             if req.method in ('POST', 'PUT'):
                 if 'application/json' not in req.content_type:
                     raise falcon.HTTPUnsupportedMediaType(
-                        'This API only supports requests encoded as JSON.',
+                        title='This API only supports requests encoded as JSON.',
                         href='http://docs.examples.com/api/json')
 
 
@@ -194,16 +194,16 @@ parameters, handling errors, and working with request and response bodies.
 
             body = req.stream.read()
             if not body:
-                raise falcon.HTTPBadRequest('Empty request body',
-                                            'A valid JSON document is required.')
+                raise falcon.HTTPBadRequest(title='Empty request body',
+                                            description='A valid JSON document is required.')
 
             try:
                 req.context.doc = json.loads(body.decode('utf-8'))
 
             except (ValueError, UnicodeDecodeError):
                 raise falcon.HTTPError(falcon.HTTP_753,
-                                       'Malformed JSON',
-                                       'Could not decode the request body. The '
+                                       title='Malformed JSON',
+                                       description='Could not decode the request body. The '
                                        'JSON was incorrect or not encoded as '
                                        'UTF-8.')
 
@@ -223,7 +223,7 @@ parameters, handling errors, and working with request and response bodies.
                        'exceed ' + str(limit) + ' bytes in length.')
 
                 raise falcon.HTTPPayloadTooLarge(
-                    'Request body is too large', msg)
+                    title='Request body is too large', description=msg)
 
         return hook
 
@@ -248,9 +248,9 @@ parameters, handling errors, and working with request and response bodies.
                                'We appreciate your patience.')
 
                 raise falcon.HTTPServiceUnavailable(
-                    'Service Outage',
-                    description,
-                    30)
+                    title='Service Outage',
+                    description=description,
+                    retry_after=30)
 
             # NOTE: Normally you would use resp.media for this sort of thing;
             # this example serves only to demonstrate how the context can be
@@ -267,8 +267,8 @@ parameters, handling errors, and working with request and response bodies.
                 doc = req.context.doc
             except AttributeError:
                 raise falcon.HTTPBadRequest(
-                    'Missing thing',
-                    'A thing must be submitted in the request body.')
+                    title='Missing thing',
+                    description='A thing must be submitted in the request body.')
 
             proper_thing = self.db.add_thing(doc)
 
diff --git a/docs/user/tutorial.rst b/docs/user/tutorial.rst
index 278541f..db7380c 100644
--- a/docs/user/tutorial.rst
+++ b/docs/user/tutorial.rst
@@ -1293,7 +1293,7 @@ message. Add this method below the definition of ``ALLOWED_IMAGE_TYPES``:
     def validate_image_type(req, resp, resource, params):
         if req.content_type not in ALLOWED_IMAGE_TYPES:
             msg = 'Image type not allowed. Must be PNG, JPEG, or GIF'
-            raise falcon.HTTPBadRequest('Bad request', msg)
+            raise falcon.HTTPBadRequest(title='Bad request', description=msg)
 
 And then attach the hook to the ``on_post()`` responder:
 
diff --git a/examples/things_advanced.py b/examples/things_advanced.py
index ecf6b10..ea7eaff 100644
--- a/examples/things_advanced.py
+++ b/examples/things_advanced.py
@@ -26,8 +26,8 @@ class StorageError(Exception):
                        'database. It worked on my box.')
 
         raise falcon.HTTPError(falcon.HTTP_725,
-                               'Database Error',
-                               description)
+                               title='Database Error',
+                               description=description)
 
 
 class SinkAdapter:
@@ -59,18 +59,18 @@ class AuthMiddleware:
             description = ('Please provide an auth token '
                            'as part of the request.')
 
-            raise falcon.HTTPUnauthorized('Auth token required',
-                                          description,
-                                          challenges,
+            raise falcon.HTTPUnauthorized(title='Auth token required',
+                                          description=description,
+                                          challenges=challenges,
                                           href='http://docs.example.com/auth')
 
         if not self._token_is_valid(token, account_id):
             description = ('The provided auth token is not valid. '
                            'Please request a new token and try again.')
 
-            raise falcon.HTTPUnauthorized('Authentication required',
-                                          description,
-                                          challenges,
+            raise falcon.HTTPUnauthorized(title='Authentication required',
+                                          description=description,
+                                          challenges=challenges,
                                           href='http://docs.example.com/auth')
 
     def _token_is_valid(self, token, account_id):
@@ -82,13 +82,13 @@ class RequireJSON:
     def process_request(self, req, resp):
         if not req.client_accepts_json:
             raise falcon.HTTPNotAcceptable(
-                'This API only supports responses encoded as JSON.',
+                title='This API only supports responses encoded as JSON.',
                 href='http://docs.examples.com/api/json')
 
         if req.method in ('POST', 'PUT'):
             if 'application/json' not in req.content_type:
                 raise falcon.HTTPUnsupportedMediaType(
-                    'This API only supports requests encoded as JSON.',
+                    title='This API only supports requests encoded as JSON.',
                     href='http://docs.examples.com/api/json')
 
 
@@ -108,16 +108,16 @@ class JSONTranslator:
 
         body = req.stream.read()
         if not body:
-            raise falcon.HTTPBadRequest('Empty request body',
-                                        'A valid JSON document is required.')
+            raise falcon.HTTPBadRequest(title='Empty request body',
+                                        description='A valid JSON document is required.')
 
         try:
             req.context.doc = json.loads(body.decode('utf-8'))
 
         except (ValueError, UnicodeDecodeError):
             raise falcon.HTTPError(falcon.HTTP_753,
-                                   'Malformed JSON',
-                                   'Could not decode the request body. The '
+                                   title='Malformed JSON',
+                                   description='Could not decode the request body. The '
                                    'JSON was incorrect or not encoded as '
                                    'UTF-8.')
 
@@ -137,7 +137,7 @@ def max_body(limit):
                    'exceed ' + str(limit) + ' bytes in length.')
 
             raise falcon.HTTPPayloadTooLarge(
-                'Request body is too large', msg)
+                title='Request body is too large', description=msg)
 
     return hook
 
@@ -162,9 +162,9 @@ class ThingsResource:
                            'We appreciate your patience.')
 
             raise falcon.HTTPServiceUnavailable(
-                'Service Outage',
-                description,
-                30)
+                title='Service Outage',
+                description=description,
+                retry_after=30)
 
         # NOTE: Normally you would use resp.media for this sort of thing;
         # this example serves only to demonstrate how the context can be
@@ -181,8 +181,8 @@ class ThingsResource:
             doc = req.context.doc
         except AttributeError:
             raise falcon.HTTPBadRequest(
-                'Missing thing',
-                'A thing must be submitted in the request body.')
+                title='Missing thing',
+                description='A thing must be submitted in the request body.')
 
         proper_thing = self.db.add_thing(doc)
 
diff --git a/falcon/app.py b/falcon/app.py
index 3b3a138..401f2f3 100644
--- a/falcon/app.py
+++ b/falcon/app.py
@@ -732,7 +732,8 @@ class App:
             The default serializer will not render any response body for
             :class:`~.HTTPError` instances where the `has_representation`
             property evaluates to ``False`` (such as in the case of types
-            that subclass :class:`falcon.http_error.NoRepresentation`).
+            that subclass :class:`falcon.http_error.OptionalRepresentation`
+            and do not provide a description).
             However a custom serializer will be called regardless of the
             property value, and it may choose to override the
             representation logic.
diff --git a/falcon/errors.py b/falcon/errors.py
index b14e6f9..b55d25a 100644
--- a/falcon/errors.py
+++ b/falcon/errors.py
@@ -27,8 +27,8 @@ package namespace::
             # ...
 
             raise falcon.HTTPBadRequest(
-                'TTL Out of Range',
-                'The message's TTL must be between 60 and 300 seconds, inclusive.'
+                title='TTL Out of Range',
+                description='The message's TTL must be between 60 and 300 seconds, inclusive.'
             )
 
             # ...
@@ -38,8 +38,7 @@ package namespace::
 from datetime import datetime
 
 from falcon import util
-from falcon.http_error import HTTPError, NoRepresentation, \
-    OptionalRepresentation
+from falcon.http_error import HTTPError, OptionalRepresentation
 import falcon.status_codes as status
 
 
@@ -69,6 +68,11 @@ class HTTPBadRequest(HTTPError):
 
     (See also: RFC 7231, Section 6.5.1)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '400 Bad Request').
         description (str): Human-friendly description of the error, along with
@@ -99,9 +103,15 @@ class HTTPBadRequest(HTTPError):
             base articles related to this error (default ``None``).
     """
 
+    @util.deprecated_args(allowed_positional=0)
     def __init__(self, title=None, description=None, headers=None, **kwargs):
-        super(HTTPBadRequest, self).__init__(status.HTTP_400, title,
-                                             description, headers, **kwargs)
+        super().__init__(
+            status.HTTP_400,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPUnauthorized(HTTPError):
@@ -125,16 +135,15 @@ class HTTPUnauthorized(HTTPError):
 
     (See also: RFC 7235, Section 3.1)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '401 Unauthorized').
         description (str): Human-friendly description of the error, along with
             a helpful suggestion or two.
-        challenges (iterable of str): One or more authentication
-            challenges to use as the value of the WWW-Authenticate header in
-            the response.
-
-            (See also: RFC 7235, Section 2.1)
-
         headers (dict or list): A ``dict`` of header names and values
             to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and
             *value* must be of type ``str`` or ``StringType``, and only
@@ -151,6 +160,15 @@ class HTTPUnauthorized(HTTPError):
             Note:
                 Falcon can process a list of ``tuple`` slightly faster
                 than a ``dict``.
+        challenges (iterable of str): One or more authentication
+            challenges to use as the value of the WWW-Authenticate header in
+            the response.
+
+            Note:
+                The existing value of the WWW-Authenticate in headers will be
+                overridden by this value
+
+            (See also: RFC 7235, Section 2.1)
         href (str): A URL someone can visit to find out more information
             (default ``None``). Unicode characters are percent-encoded.
         href_text (str): If href is given, use this as the friendly
@@ -162,15 +180,19 @@ class HTTPUnauthorized(HTTPError):
 
     """
 
-    def __init__(self, title=None, description=None, challenges=None, headers=None, **kwargs):
-        if headers is None:
-            headers = {}
-
+    @util.deprecated_args(allowed_positional=0)
+    def __init__(self, title=None, description=None, headers=None, challenges=None, **kwargs):
         if challenges:
+            headers = _load_headers(headers)
             headers['WWW-Authenticate'] = ', '.join(challenges)
 
-        super(HTTPUnauthorized, self).__init__(status.HTTP_401, title,
-                                               description, headers, **kwargs)
+        super().__init__(
+            status.HTTP_401,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPForbidden(HTTPError):
@@ -194,6 +216,11 @@ class HTTPForbidden(HTTPError):
 
     (See also: RFC 7231, Section 6.5.4)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '403 Forbidden').
         description (str): Human-friendly description of the error, along with
@@ -224,9 +251,15 @@ class HTTPForbidden(HTTPError):
             base articles related to this error (default ``None``).
     """
 
+    @util.deprecated_args(allowed_positional=0)
     def __init__(self, title=None, description=None, headers=None, **kwargs):
-        super(HTTPForbidden, self).__init__(status.HTTP_403, title,
-                                            description, headers, **kwargs)
+        super().__init__(
+            status.HTTP_403,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPNotFound(OptionalRepresentation, HTTPError):
@@ -246,6 +279,11 @@ class HTTPNotFound(OptionalRepresentation, HTTPError):
 
     (See also: RFC 7231, Section 6.5.3)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Human-friendly error title. If not provided, and
             `description` is also not provided, no body will be included
@@ -278,8 +316,15 @@ class HTTPNotFound(OptionalRepresentation, HTTPError):
             base articles related to this error (default ``None``).
     """
 
-    def __init__(self, headers=None, **kwargs):
-        super(HTTPNotFound, self).__init__(status.HTTP_404, headers=headers, **kwargs)
+    @util.deprecated_args(allowed_positional=0)
+    def __init__(self, title=None, description=None, headers=None, **kwargs):
+        super().__init__(
+            status.HTTP_404,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPRouteNotFound(HTTPNotFound):
@@ -292,6 +337,11 @@ class HTTPRouteNotFound(HTTPNotFound):
     behavior can be customized by registering a custom error handler for
     :class:`~.HTTPRouteNotFound`.
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Human-friendly error title. If not provided, and
             `description` is also not provided, no body will be included
@@ -323,7 +373,6 @@ class HTTPRouteNotFound(HTTPNotFound):
             support request or to help them when searching for knowledge
             base articles related to this error (default ``None``).
     """
-    pass
 
 
 class HTTPMethodNotAllowed(OptionalRepresentation, HTTPError):
@@ -341,10 +390,20 @@ class HTTPMethodNotAllowed(OptionalRepresentation, HTTPError):
 
     (See also: RFC 7231, Section 6.5.5)
 
+    Note:
+        ``allowed_methods`` is the only positional argument allowed, the other
+        arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Args:
         allowed_methods (list of str): Allowed HTTP methods for this
             resource (e.g., ``['GET', 'POST', 'HEAD']``).
 
+            Note:
+                The existing valuessss of the Allow in headers will be
+                overridden by this value.
+
     Keyword Args:
         title (str): Human-friendly error title. If not provided, and
             `description` is also not provided, no body will be included
@@ -377,15 +436,17 @@ class HTTPMethodNotAllowed(OptionalRepresentation, HTTPError):
             base articles related to this error (default ``None``).
     """
 
-    def __init__(self, allowed_methods, headers=None, **kwargs):
-        new_headers = {'Allow': ', '.join(allowed_methods)}
-        super(HTTPMethodNotAllowed, self).__init__(status.HTTP_405,
-                                                   headers=headers,
-                                                   **kwargs)
-        if not self.headers:
-            self.headers = {}
-
-        self.headers.update(new_headers)
+    @util.deprecated_args(allowed_positional=1)
+    def __init__(self, allowed_methods, title=None, description=None, headers=None, **kwargs):
+        headers = _load_headers(headers)
+        headers['Allow'] = ', '.join(allowed_methods)
+        super().__init__(
+            status.HTTP_405,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPNotAcceptable(HTTPError):
@@ -406,6 +467,11 @@ class HTTPNotAcceptable(HTTPError):
 
     (See also: RFC 7231, Section 6.5.6)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         description (str): Human-friendly description of the error, along with
             a helpful suggestion or two.
@@ -435,11 +501,15 @@ class HTTPNotAcceptable(HTTPError):
             base articles related to this error (default ``None``).
     """
 
+    @util.deprecated_args(allowed_positional=0)
     def __init__(self, title=None, description=None, headers=None, **kwargs):
-        super(HTTPNotAcceptable, self).__init__(status.HTTP_406,
-                                                title,
-                                                description, headers,
-                                                **kwargs)
+        super().__init__(
+            status.HTTP_406,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPConflict(HTTPError):
@@ -463,6 +533,11 @@ class HTTPConflict(HTTPError):
 
     (See also: RFC 7231, Section 6.5.8)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '409 Conflict').
         description (str): Human-friendly description of the error, along with
@@ -493,10 +568,15 @@ class HTTPConflict(HTTPError):
             base articles related to this error (default ``None``).
     """
 
+    @util.deprecated_args(allowed_positional=0)
     def __init__(self, title=None, description=None, headers=None, **kwargs):
-        super(HTTPConflict, self).__init__(status.HTTP_409, title,
-                                           description, headers,
-                                           **kwargs)
+        super().__init__(
+            status.HTTP_409,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPGone(OptionalRepresentation, HTTPError):
@@ -524,6 +604,11 @@ class HTTPGone(OptionalRepresentation, HTTPError):
 
     (See also: RFC 7231, Section 6.5.9)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Human-friendly error title. If not provided, and
             `description` is also not provided, no body will be included
@@ -556,8 +641,15 @@ class HTTPGone(OptionalRepresentation, HTTPError):
             base articles related to this error (default ``None``).
     """
 
-    def __init__(self, headers=None, **kwargs):
-        super(HTTPGone, self).__init__(status.HTTP_410, headers=headers, **kwargs)
+    @util.deprecated_args(allowed_positional=0)
+    def __init__(self, title=None, description=None, headers=None, **kwargs):
+        super().__init__(
+            status.HTTP_410,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPLengthRequired(HTTPError):
@@ -572,6 +664,11 @@ class HTTPLengthRequired(HTTPError):
 
     (See also: RFC 7231, Section 6.5.10)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '411 Length Required').
         description (str): Human-friendly description of the error, along with
@@ -601,10 +698,16 @@ class HTTPLengthRequired(HTTPError):
             support request or to help them when searching for knowledge
             base articles related to this error (default ``None``).
     """
+
+    @util.deprecated_args(allowed_positional=0)
     def __init__(self, title=None, description=None, headers=None, **kwargs):
-        super(HTTPLengthRequired, self).__init__(status.HTTP_411,
-                                                 title, description,
-                                                 headers, **kwargs)
+        super().__init__(
+            status.HTTP_411,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPPreconditionFailed(HTTPError):
@@ -620,6 +723,11 @@ class HTTPPreconditionFailed(HTTPError):
 
     (See also: RFC 7232, Section 4.2)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '412 Precondition Failed').
         description (str): Human-friendly description of the error, along with
@@ -650,10 +758,15 @@ class HTTPPreconditionFailed(HTTPError):
             base articles related to this error (default ``None``).
     """
 
+    @util.deprecated_args(allowed_positional=0)
     def __init__(self, title=None, description=None, headers=None, **kwargs):
-        super(HTTPPreconditionFailed, self).__init__(status.HTTP_412, title,
-                                                     description, headers,
-                                                     **kwargs)
+        super().__init__(
+            status.HTTP_412,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPPayloadTooLarge(HTTPError):
@@ -671,16 +784,15 @@ class HTTPPayloadTooLarge(HTTPError):
 
     (See also: RFC 7231, Section 6.5.11)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '413 Payload Too Large').
-
         description (str): Human-friendly description of the error, along with
             a helpful suggestion or two.
-
-        retry_after (datetime or int): Value for the Retry-After
-            header. If a ``datetime`` object, will serialize as an HTTP date.
-            Otherwise, a non-negative ``int`` is expected, representing the
-            number of seconds to wait.
         headers (dict or list): A ``dict`` of header names and values
             to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and
             *value* must be of type ``str`` or ``StringType``, and only
@@ -697,6 +809,14 @@ class HTTPPayloadTooLarge(HTTPError):
             Note:
                 Falcon can process a list of ``tuple`` slightly faster
                 than a ``dict``.
+        retry_after (datetime or int): Value for the Retry-After
+            header. If a ``datetime`` object, will serialize as an HTTP date.
+            Otherwise, a non-negative ``int`` is expected, representing the
+            number of seconds to wait.
+
+            Note:
+                The existing value of the Retry-After in headers will be
+                overridden by this value
         href (str): A URL someone can visit to find out more information
             (default ``None``). Unicode characters are percent-encoded.
         href_text (str): If href is given, use this as the friendly
@@ -707,20 +827,15 @@ class HTTPPayloadTooLarge(HTTPError):
             base articles related to this error (default ``None``).
     """
 
+    @util.deprecated_args(allowed_positional=0)
     def __init__(self, title=None, description=None, retry_after=None, headers=None, **kwargs):
-        if headers is None:
-            headers = {}
-
-        if isinstance(retry_after, datetime):
-            headers['Retry-After'] = util.dt_to_http(retry_after)
-        elif retry_after is not None:
-            headers['Retry-After'] = str(retry_after)
-
-        super(HTTPPayloadTooLarge, self).__init__(status.HTTP_413,
-                                                  title,
-                                                  description,
-                                                  headers,
-                                                  **kwargs)
+        super().__init__(
+            status.HTTP_413,
+            title=title,
+            description=description,
+            headers=_parse_retry_after(headers, retry_after),
+            **kwargs
+        )
 
 
 class HTTPUriTooLong(HTTPError):
@@ -741,6 +856,11 @@ class HTTPUriTooLong(HTTPError):
 
     (See also: RFC 7231, Section 6.5.12)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '414 URI Too Long').
         description (str): Human-friendly description of the error, along with
@@ -771,10 +891,15 @@ class HTTPUriTooLong(HTTPError):
             base articles related to this error (default ``None``).
     """
 
+    @util.deprecated_args(allowed_positional=0)
     def __init__(self, title=None, description=None, headers=None, **kwargs):
-        super(HTTPUriTooLong, self).__init__(status.HTTP_414, title,
-                                             description, headers,
-                                             **kwargs)
+        super().__init__(
+            status.HTTP_414,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPUnsupportedMediaType(HTTPError):
@@ -790,7 +915,13 @@ class HTTPUnsupportedMediaType(HTTPError):
 
     (See also: RFC 7231, Section 6.5.13)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
+        title (str): Error title (default '415 Unsupported Media Type').
         description (str): Human-friendly description of the error, along with
             a helpful suggestion or two.
         headers (dict or list): A ``dict`` of header names and values
@@ -819,13 +950,18 @@ class HTTPUnsupportedMediaType(HTTPError):
             base articles related to this error (default ``None``).
     """
 
+    @util.deprecated_args(allowed_positional=0)
     def __init__(self, title=None, description=None, headers=None, **kwargs):
-        super(HTTPUnsupportedMediaType, self).__init__(
-            status.HTTP_415, title,
-            description, headers, **kwargs)
+        super().__init__(
+            status.HTTP_415,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
-class HTTPRangeNotSatisfiable(NoRepresentation, HTTPError):
+class HTTPRangeNotSatisfiable(OptionalRepresentation, HTTPError):
     """416 Range Not Satisfiable.
 
     None of the ranges in the request's Range header field overlap the
@@ -842,18 +978,62 @@ class HTTPRangeNotSatisfiable(NoRepresentation, HTTPError):
 
     (See also: RFC 7233, Section 4.4)
 
+    Note:
+        ``resource_length`` is the only positional argument allowed, the other
+        arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Args:
         resource_length: The maximum value for the last-byte-pos of a range
             request. Used to set the Content-Range header.
+
+            Note:
+                The existing value of the Content-Range in headers will be
+                overridden by this value
+
+    Keyword Args:
+        title (str): Error title (default '416 Range Not Satisfiable').
+        description (str): Human-friendly description of the error, along with
+            a helpful suggestion or two.
+        headers (dict or list): A ``dict`` of header names and values
+            to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and
+            *value* must be of type ``str`` or ``StringType``, and only
+            character values 0x00 through 0xFF may be used on platforms that
+            use wide characters.
+
+            Note:
+                The Content-Type header, if present, will be overridden. If
+                you wish to return custom error messages, you can create
+                your own HTTP error class, and install an error handler
+                to convert it into an appropriate HTTP response for the
+                client
+
+            Note:
+                Falcon can process a list of ``tuple`` slightly faster
+                than a ``dict``.
+        href (str): A URL someone can visit to find out more information
+            (default ``None``). Unicode characters are percent-encoded.
+        href_text (str): If href is given, use this as the friendly
+            title/description for the link (default 'API documentation
+            for this error').
+        code (int): An internal code that customers can reference in their
+            support request or to help them when searching for knowledge
+            base articles related to this error (default ``None``).
     """
 
-    def __init__(self, resource_length, headers=None):
-        if headers is None:
-            headers = {}
-        headers.setdefault('Content-Range', 'bytes */' + str(resource_length))
+    @util.deprecated_args(allowed_positional=1)
+    def __init__(self, resource_length, title=None, description=None, headers=None, **kwargs):
+        headers = _load_headers(headers)
+        headers['Content-Range'] = 'bytes */' + str(resource_length)
 
-        super(HTTPRangeNotSatisfiable, self).__init__(status.HTTP_416,
-                                                      headers=headers)
+        super().__init__(
+            status.HTTP_416,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPUnprocessableEntity(HTTPError):
@@ -871,6 +1051,11 @@ class HTTPUnprocessableEntity(HTTPError):
 
     (See also: RFC 4918, Section 11.2)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '422 Unprocessable Entity').
         description (str): Human-friendly description of the error, along with
@@ -901,9 +1086,15 @@ class HTTPUnprocessableEntity(HTTPError):
             base articles related to this error (default ``None``).
     """
 
+    @util.deprecated_args(allowed_positional=0)
     def __init__(self, title=None, description=None, headers=None, **kwargs):
-        super(HTTPUnprocessableEntity, self).__init__(status.HTTP_422, title,
-                                                      description, headers, **kwargs)
+        super().__init__(
+            status.HTTP_422,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPLocked(OptionalRepresentation, HTTPError):
@@ -916,6 +1107,11 @@ class HTTPLocked(OptionalRepresentation, HTTPError):
 
     (See also: RFC 4918, Section 11.3)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '423 Locked').
         description (str): Human-friendly description of the error, along with
@@ -946,10 +1142,15 @@ class HTTPLocked(OptionalRepresentation, HTTPError):
             base articles related to this error (default ``None``).
     """
 
+    @util.deprecated_args(allowed_positional=0)
     def __init__(self, title=None, description=None, headers=None, **kwargs):
-        super(HTTPLocked, self).__init__(status.HTTP_423, title,
-                                         description, headers,
-                                         **kwargs)
+        super().__init__(
+            status.HTTP_423,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPFailedDependency(OptionalRepresentation, HTTPError):
@@ -961,6 +1162,11 @@ class HTTPFailedDependency(OptionalRepresentation, HTTPError):
 
     (See also: RFC 4918, Section 11.4)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '424 Failed Dependency').
         description (str): Human-friendly description of the error, along with
@@ -991,10 +1197,15 @@ class HTTPFailedDependency(OptionalRepresentation, HTTPError):
             base articles related to this error (default ``None``).
     """
 
+    @util.deprecated_args(allowed_positional=0)
     def __init__(self, title=None, description=None, headers=None, **kwargs):
-        super(HTTPFailedDependency, self).__init__(status.HTTP_424, title,
-                                                   description, headers,
-                                                   **kwargs)
+        super().__init__(
+            status.HTTP_424,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPPreconditionRequired(HTTPError):
@@ -1014,6 +1225,11 @@ class HTTPPreconditionRequired(HTTPError):
 
     (See also: RFC 6585, Section 3)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '428 Precondition Required').
         description (str): Human-friendly description of the error, along with
@@ -1043,10 +1259,16 @@ class HTTPPreconditionRequired(HTTPError):
             support request or to help them when searching for knowledge
             base articles related to this error (default ``None``).
     """
+
+    @util.deprecated_args(allowed_positional=0)
     def __init__(self, title=None, description=None, headers=None, **kwargs):
-        super(HTTPPreconditionRequired, self).__init__(status.HTTP_428, title,
-                                                       description, headers,
-                                                       **kwargs)
+        super().__init__(
+            status.HTTP_428,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPTooManyRequests(HTTPError):
@@ -1063,14 +1285,15 @@ class HTTPTooManyRequests(HTTPError):
 
     (See also: RFC 6585, Section 4)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '429 Too Many Requests').
         description (str): Human-friendly description of the rate limit that
             was exceeded.
-        retry_after (datetime or int): Value for the Retry-After
-            header. If a ``datetime`` object, will serialize as an HTTP date.
-            Otherwise, a non-negative ``int`` is expected, representing the
-            number of seconds to wait.
         headers (dict or list): A ``dict`` of header names and values
             to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and
             *value* must be of type ``str`` or ``StringType``, and only
@@ -1087,6 +1310,14 @@ class HTTPTooManyRequests(HTTPError):
             Note:
                 Falcon can process a list of ``tuple`` slightly faster
                 than a ``dict``.
+        retry_after (datetime or int): Value for the Retry-After
+            header. If a ``datetime`` object, will serialize as an HTTP date.
+            Otherwise, a non-negative ``int`` is expected, representing the
+            number of seconds to wait.
+
+            Note:
+                The existing value of the Retry-After in headers will be
+                overridden by this value
         href (str): A URL someone can visit to find out more information
             (default ``None``). Unicode characters are percent-encoded.
         href_text (str): If href is given, use this as the friendly
@@ -1097,20 +1328,15 @@ class HTTPTooManyRequests(HTTPError):
             base articles related to this error (default ``None``).
     """
 
-    def __init__(self, title=None, description=None, retry_after=None, headers=None, **kwargs):
-        if headers is None:
-            headers = {}
-
-        if isinstance(retry_after, datetime):
-            headers['Retry-After'] = util.dt_to_http(retry_after)
-        elif retry_after is not None:
-            headers['Retry-After'] = str(retry_after)
-
-        super(HTTPTooManyRequests, self).__init__(status.HTTP_429,
-                                                  title,
-                                                  description,
-                                                  headers,
-                                                  **kwargs)
+    @util.deprecated_args(allowed_positional=0)
+    def __init__(self, title=None, description=None, headers=None, retry_after=None, **kwargs):
+        super().__init__(
+            status.HTTP_429,
+            title=title,
+            description=description,
+            headers=_parse_retry_after(headers, retry_after),
+            **kwargs
+        )
 
 
 class HTTPRequestHeaderFieldsTooLarge(HTTPError):
@@ -1129,6 +1355,11 @@ class HTTPRequestHeaderFieldsTooLarge(HTTPError):
 
     (See also: RFC 6585, Section 5)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '431 Request Header Fields Too Large').
         description (str): Human-friendly description of the rate limit that
@@ -1159,12 +1390,15 @@ class HTTPRequestHeaderFieldsTooLarge(HTTPError):
             base articles related to this error (default ``None``).
     """
 
+    @util.deprecated_args(allowed_positional=0)
     def __init__(self, title=None, description=None, headers=None, **kwargs):
-        super(HTTPRequestHeaderFieldsTooLarge, self).__init__(status.HTTP_431,
-                                                              title,
-                                                              description,
-                                                              headers,
-                                                              **kwargs)
+        super().__init__(
+            status.HTTP_431,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPUnavailableForLegalReasons(OptionalRepresentation, HTTPError):
@@ -1190,6 +1424,11 @@ class HTTPUnavailableForLegalReasons(OptionalRepresentation, HTTPError):
 
     (See also: RFC 7725, Section 3)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '451 Unavailable For Legal Reasons').
         description (str): Human-friendly description of the error, along with
@@ -1220,11 +1459,15 @@ class HTTPUnavailableForLegalReasons(OptionalRepresentation, HTTPError):
             base articles related to this error (default ``None``).
     """
 
-    def __init__(self, title=None, headers=None, **kwargs):
-        super(HTTPUnavailableForLegalReasons, self).__init__(status.HTTP_451,
-                                                             title,
-                                                             headers=headers,
-                                                             **kwargs)
+    @util.deprecated_args(allowed_positional=0)
+    def __init__(self, title=None, description=None, headers=None, **kwargs):
+        super().__init__(
+            status.HTTP_451,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPInternalServerError(HTTPError):
@@ -1235,6 +1478,11 @@ class HTTPInternalServerError(HTTPError):
 
     (See also: RFC 7231, Section 6.6.1)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '500 Internal Server Error').
         description (str): Human-friendly description of the error, along with
@@ -1266,10 +1514,15 @@ class HTTPInternalServerError(HTTPError):
 
     """
 
+    @util.deprecated_args(allowed_positional=0)
     def __init__(self, title=None, description=None, headers=None, **kwargs):
-        super(HTTPInternalServerError, self).__init__(status.HTTP_500, title,
-                                                      description, headers,
-                                                      **kwargs)
+        super().__init__(
+            status.HTTP_500,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPNotImplemented(HTTPError):
@@ -1286,6 +1539,11 @@ class HTTPNotImplemented(HTTPError):
 
     (See also: RFC 7231, Section 6.6.2)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '500 Internal Server Error').
         description (str): Human-friendly description of the error, along with
@@ -1318,10 +1576,15 @@ class HTTPNotImplemented(HTTPError):
 
     """
 
+    @util.deprecated_args(allowed_positional=0)
     def __init__(self, title=None, description=None, headers=None, **kwargs):
-        super(HTTPNotImplemented, self).__init__(status.HTTP_501, title,
-                                                 description, headers,
-                                                 **kwargs)
+        super().__init__(
+            status.HTTP_501,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPBadGateway(HTTPError):
@@ -1333,6 +1596,11 @@ class HTTPBadGateway(HTTPError):
 
     (See also: RFC 7231, Section 6.6.3)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '502 Bad Gateway').
         description (str): Human-friendly description of the error, along with
@@ -1363,10 +1631,15 @@ class HTTPBadGateway(HTTPError):
             base articles related to this error (default ``None``).
     """
 
+    @util.deprecated_args(allowed_positional=0)
     def __init__(self, title=None, description=None, headers=None, **kwargs):
-        super(HTTPBadGateway, self).__init__(status.HTTP_502, title,
-                                             description, headers,
-                                             **kwargs)
+        super().__init__(
+            status.HTTP_502,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPServiceUnavailable(HTTPError):
@@ -1386,14 +1659,15 @@ class HTTPServiceUnavailable(HTTPError):
 
     (See also: RFC 7231, Section 6.6.4)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '503 Service Unavailable').
         description (str): Human-friendly description of the error, along with
             a helpful suggestion or two.
-        retry_after (datetime or int): Value for the Retry-After header. If a
-            ``datetime`` object, will serialize as an HTTP date. Otherwise,
-            a non-negative ``int`` is expected, representing the number of
-            seconds to wait.
         headers (dict or list): A ``dict`` of header names and values
             to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and
             *value* must be of type ``str`` or ``StringType``, and only
@@ -1410,6 +1684,14 @@ class HTTPServiceUnavailable(HTTPError):
             Note:
                 Falcon can process a list of ``tuple`` slightly faster
                 than a ``dict``.
+        retry_after (datetime or int): Value for the Retry-After header. If a
+            ``datetime`` object, will serialize as an HTTP date. Otherwise,
+            a non-negative ``int`` is expected, representing the number of
+            seconds to wait.
+
+            Note:
+                The existing value of the Retry-After in headers will be
+                overridden by this value
         href (str): A URL someone can visit to find out more information
             (default ``None``). Unicode characters are percent-encoded.
         href_text (str): If href is given, use this as the friendly
@@ -1420,20 +1702,15 @@ class HTTPServiceUnavailable(HTTPError):
             base articles related to this error (default ``None``).
     """
 
-    def __init__(self, title=None, description=None, retry_after=None, headers=None, **kwargs):
-        if headers is None:
-            headers = {}
-
-        if isinstance(retry_after, datetime):
-            headers['Retry-After'] = util.dt_to_http(retry_after)
-        elif retry_after is not None:
-            headers['Retry-After'] = str(retry_after)
-
-        super(HTTPServiceUnavailable, self).__init__(status.HTTP_503,
-                                                     title,
-                                                     description,
-                                                     headers,
-                                                     **kwargs)
+    @util.deprecated_args(allowed_positional=0)
+    def __init__(self, title=None, description=None, headers=None, retry_after=None, **kwargs):
+        super().__init__(
+            status.HTTP_503,
+            title=title,
+            description=description,
+            headers=_parse_retry_after(headers, retry_after),
+            **kwargs
+        )
 
 
 class HTTPGatewayTimeout(HTTPError):
@@ -1446,6 +1723,11 @@ class HTTPGatewayTimeout(HTTPError):
 
     (See also: RFC 7231, Section 6.6.5)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '503 Service Unavailable').
         description (str): Human-friendly description of the error, along with
@@ -1466,7 +1748,6 @@ class HTTPGatewayTimeout(HTTPError):
             Note:
                 Falcon can process a list of ``tuple`` slightly faster
                 than a ``dict``.
-
         href (str): A URL someone can visit to find out more information
             (default ``None``). Unicode characters are percent-encoded.
         href_text (str): If href is given, use this as the friendly
@@ -1477,10 +1758,15 @@ class HTTPGatewayTimeout(HTTPError):
             base articles related to this error (default ``None``).
     """
 
+    @util.deprecated_args(allowed_positional=0)
     def __init__(self, title=None, description=None, headers=None, **kwargs):
-        super(HTTPGatewayTimeout, self).__init__(status.HTTP_504, title,
-                                                 description, headers,
-                                                 **kwargs)
+        super().__init__(
+            status.HTTP_504,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPVersionNotSupported(HTTPError):
@@ -1498,6 +1784,11 @@ class HTTPVersionNotSupported(HTTPError):
 
     (See also: RFC 7231, Section 6.6.6)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '503 Service Unavailable').
         description (str): Human-friendly description of the error, along with
@@ -1518,7 +1809,6 @@ class HTTPVersionNotSupported(HTTPError):
             Note:
                 Falcon can process a list of ``tuple`` slightly faster
                 than a ``dict``.
-
         href (str): A URL someone can visit to find out more information
             (default ``None``). Unicode characters are percent-encoded.
         href_text (str): If href is given, use this as the friendly
@@ -1529,10 +1819,15 @@ class HTTPVersionNotSupported(HTTPError):
             base articles related to this error (default ``None``).
     """
 
+    @util.deprecated_args(allowed_positional=0)
     def __init__(self, title=None, description=None, headers=None, **kwargs):
-        super(HTTPVersionNotSupported, self).__init__(status.HTTP_505, title,
-                                                      description, headers,
-                                                      **kwargs)
+        super().__init__(
+            status.HTTP_505,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPInsufficientStorage(HTTPError):
@@ -1548,6 +1843,11 @@ class HTTPInsufficientStorage(HTTPError):
 
     (See also: RFC 4918, Section 11.5)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '507 Insufficient Storage').
         description (str): Human-friendly description of the error, along with
@@ -1578,10 +1878,15 @@ class HTTPInsufficientStorage(HTTPError):
             base articles related to this error (default ``None``).
     """
 
+    @util.deprecated_args(allowed_positional=0)
     def __init__(self, title=None, description=None, headers=None, **kwargs):
-        super(HTTPInsufficientStorage, self).__init__(status.HTTP_507, title,
-                                                      description, headers,
-                                                      **kwargs)
+        super().__init__(
+            status.HTTP_507,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPLoopDetected(HTTPError):
@@ -1594,6 +1899,11 @@ class HTTPLoopDetected(HTTPError):
 
     (See also: RFC 5842, Section 7.2)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '508 Loop Detected').
         description (str): Human-friendly description of the error, along with
@@ -1624,10 +1934,15 @@ class HTTPLoopDetected(HTTPError):
             base articles related to this error (default ``None``).
     """
 
+    @util.deprecated_args(allowed_positional=0)
     def __init__(self, title=None, description=None, headers=None, **kwargs):
-        super(HTTPLoopDetected, self).__init__(status.HTTP_508, title,
-                                               description, headers,
-                                               **kwargs)
+        super().__init__(
+            status.HTTP_508,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPNetworkAuthenticationRequired(HTTPError):
@@ -1652,6 +1967,11 @@ class HTTPNetworkAuthenticationRequired(HTTPError):
 
     (See also: RFC 6585, Section 6)
 
+    Note:
+        All the arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Keyword Args:
         title (str): Error title (default '511 Network Authentication Required').
         description (str): Human-friendly description of the error, along with
@@ -1682,12 +2002,15 @@ class HTTPNetworkAuthenticationRequired(HTTPError):
             base articles related to this error (default ``None``).
     """
 
+    @util.deprecated_args(allowed_positional=0)
     def __init__(self, title=None, description=None, headers=None, **kwargs):
-        super(HTTPNetworkAuthenticationRequired, self).__init__(status.HTTP_511,
-                                                                title,
-                                                                description,
-                                                                headers,
-                                                                **kwargs)
+        super().__init__(
+            status.HTTP_511,
+            title=title,
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPInvalidHeader(HTTPBadRequest):
@@ -1695,6 +2018,12 @@ class HTTPInvalidHeader(HTTPBadRequest):
 
     One of the headers in the request is invalid.
 
+    Note:
+        ``msg`` and ``header_name`` are the only positional argument allowed,
+        the other arguments should be used as keyword only. Using them as
+        positional arguments will raise a deprecation warning and will result
+        in an error in a future version of falcon.
+
     Args:
         msg (str): A description of why the value is invalid.
         header_name (str): The name of the invalid header.
@@ -1726,14 +2055,17 @@ class HTTPInvalidHeader(HTTPBadRequest):
             base articles related to this error (default ``None``).
     """
 
+    @util.deprecated_args(allowed_positional=2)
     def __init__(self, msg, header_name, headers=None, **kwargs):
-        description = ('The value provided for the {0} header is '
-                       'invalid. {1}')
+        description = 'The value provided for the "{0}" header is invalid. {1}'
         description = description.format(header_name, msg)
 
-        super(HTTPInvalidHeader, self).__init__('Invalid header value',
-                                                description, headers,
-                                                **kwargs)
+        super().__init__(
+            title='Invalid header value',
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPMissingHeader(HTTPBadRequest):
@@ -1741,6 +2073,12 @@ class HTTPMissingHeader(HTTPBadRequest):
 
     A header is missing from the request.
 
+    Note:
+        ``header_name`` is the only positional argument allowed, the other
+        arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Args:
         header_name (str): The name of the missing header.
 
@@ -1771,13 +2109,17 @@ class HTTPMissingHeader(HTTPBadRequest):
             base articles related to this error (default ``None``).
     """
 
+    @util.deprecated_args(allowed_positional=1)
     def __init__(self, header_name, headers=None, **kwargs):
-        description = 'The {0} header is required.'
+        description = 'The "{0}" header is required.'
         description = description.format(header_name)
 
-        super(HTTPMissingHeader, self).__init__('Missing header value',
-                                                description, headers,
-                                                **kwargs)
+        super().__init__(
+            title='Missing header value',
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPInvalidParam(HTTPBadRequest):
@@ -1787,6 +2129,12 @@ class HTTPInvalidParam(HTTPBadRequest):
     parameter in a query string, form, or document that was submitted
     with the request.
 
+    Note:
+        ``msg`` and ``param_name`` are the only positional argument allowed,
+        the other arguments should be used as keyword only. Using them as
+        positional arguments will raise a deprecation warning and will result
+        in an error in a future version of falcon.
+
     Args:
         msg (str): A description of the invalid parameter.
         param_name (str): The name of the parameter.
@@ -1818,13 +2166,17 @@ class HTTPInvalidParam(HTTPBadRequest):
             base articles related to this error (default ``None``).
     """
 
+    @util.deprecated_args(allowed_positional=2)
     def __init__(self, msg, param_name, headers=None, **kwargs):
         description = 'The "{0}" parameter is invalid. {1}'
         description = description.format(param_name, msg)
 
-        super(HTTPInvalidParam, self).__init__('Invalid parameter',
-                                               description, headers,
-                                               **kwargs)
+        super().__init__(
+            title='Invalid parameter',
+            description=description,
+            headers=headers,
+            **kwargs
+        )
 
 
 class HTTPMissingParam(HTTPBadRequest):
@@ -1834,6 +2186,12 @@ class HTTPMissingParam(HTTPBadRequest):
     parameter in a query string, form, or document that was submitted
     with the request.
 
+    Note:
+        ``param_name`` is the only positional argument allowed, the other
+        arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Args:
         param_name (str): The name of the missing parameter.
 
@@ -1864,10 +2222,39 @@ class HTTPMissingParam(HTTPBadRequest):
             base articles related to this error (default ``None``).
     """
 
+    @util.deprecated_args(allowed_positional=1)
     def __init__(self, param_name, headers=None, **kwargs):
         description = 'The "{0}" parameter is required.'
         description = description.format(param_name)
 
-        super(HTTPMissingParam, self).__init__('Missing parameter',
-                                               description, headers,
-                                               **kwargs)
+        super().__init__(
+            title='Missing parameter',
+            description=description,
+            headers=headers,
+            **kwargs
+        )
+
+# -----------------------------------------------------------------------------
+# Helpers
+# -----------------------------------------------------------------------------
+
+
+def _load_headers(headers):
+    """Transforms the headers to dict"""
+    if headers is None:
+        return {}
+    if isinstance(headers, dict):
+        return headers
+    return dict(headers)
+
+
+def _parse_retry_after(headers, retry_after):
+    """Sets the Retry-After to the headers when required"""
+    if retry_after is None:
+        return headers
+    headers = _load_headers(headers)
+    if isinstance(retry_after, datetime):
+        headers['Retry-After'] = util.dt_to_http(retry_after)
+    else:
+        headers['Retry-After'] = str(retry_after)
+    return headers
diff --git a/falcon/hooks.py b/falcon/hooks.py
index 79f1b12..24189ff 100644
--- a/falcon/hooks.py
+++ b/falcon/hooks.py
@@ -40,8 +40,8 @@ def before(action, *args, is_async=False, **kwargs):
             try:
                 params['id'] = int(params['id'])
             except ValueError:
-                raise falcon.HTTPBadRequest('Invalid ID',
-                                            'ID was not valid.')
+                raise falcon.HTTPBadRequest(title='Invalid ID',
+                                            description='ID was not valid.')
 
             params['answer'] = 42
 
diff --git a/falcon/http_error.py b/falcon/http_error.py
index e32c5ed..305ba05 100644
--- a/falcon/http_error.py
+++ b/falcon/http_error.py
@@ -17,7 +17,7 @@
 from collections import OrderedDict
 import xml.etree.ElementTree as et
 
-from falcon.util import json, uri
+from falcon.util import deprecated_args, json, uri
 
 
 class HTTPError(Exception):
@@ -37,6 +37,12 @@ class HTTPError(Exception):
     is implemented via ``to_dict()``). To also support XML, override
     the ``to_xml()`` method.
 
+    Note:
+        ``status`` is the only positional argument allowed, the other
+        arguments should be used as keyword only. Using them as positional
+        arguments will raise a deprecation warning and will result in an
+        error in a future version of falcon.
+
     Args:
         status (str): HTTP status code and text, such as "400 Bad Request"
 
@@ -79,7 +85,7 @@ class HTTPError(Exception):
             returns ``True``, but child classes may override it
             in order to return ``False`` when an empty HTTP body is desired.
 
-            (See also: :class:`falcon.http_error.NoRepresentation`)
+            (See also: :class:`falcon.http_error.OptionalRepresentation`)
 
             Note:
                 A custom error serializer
@@ -104,6 +110,7 @@ class HTTPError(Exception):
         'code',
     )
 
+    @deprecated_args(allowed_positional=1)
     def __init__(self, status, title=None, description=None, headers=None,
                  href=None, href_text=None, code=None):
         self.status = status
@@ -203,30 +210,6 @@ class HTTPError(Exception):
                 et.tostring(error_element, encoding='utf-8'))
 
 
-class NoRepresentation:
-    """Mixin for ``HTTPError`` child classes that have no representation.
-
-    This class can be mixed in when inheriting from ``HTTPError``, in order
-    to override the `has_representation` property such that it always
-    returns ``False``. This, in turn, will cause Falcon to return an empty
-    response body to the client.
-
-    You can use this mixin when defining errors that either should not have
-    a body (as dictated by HTTP standards or common practice), or in the
-    case that a detailed error response may leak information to an attacker.
-
-    Note:
-        This mixin class must appear before ``HTTPError`` in the base class
-        list when defining the child; otherwise, it will not override the
-        `has_representation` property as expected.
-
-    """
-
-    @property
-    def has_representation(self):
-        return False
-
-
 class OptionalRepresentation:
     """Mixin for ``HTTPError`` child classes that may have a representation.
 
@@ -247,4 +230,4 @@ class OptionalRepresentation:
     """
     @property
     def has_representation(self):
-        return super(OptionalRepresentation, self).description is not None
+        return super().description is not None
diff --git a/falcon/media/json.py b/falcon/media/json.py
index ef59e46..375b9a9 100644
--- a/falcon/media/json.py
+++ b/falcon/media/json.py
@@ -74,8 +74,8 @@ class JSONHandler(BaseHandler):
             return self.loads(stream.read().decode('utf-8'))
         except ValueError as err:
             raise errors.HTTPBadRequest(
-                'Invalid JSON',
-                'Could not parse JSON body - {0}'.format(err)
+                title='Invalid JSON',
+                description='Could not parse JSON body - {0}'.format(err)
             )
 
     async def deserialize_async(self, stream, content_type, content_length):
@@ -85,8 +85,8 @@ class JSONHandler(BaseHandler):
             return self.loads(data.decode('utf-8'))
         except ValueError as err:
             raise errors.HTTPBadRequest(
-                'Invalid JSON',
-                'Could not parse JSON body - {0}'.format(err)
+                title='Invalid JSON',
+                description='Could not parse JSON body - {0}'.format(err)
             )
 
     def serialize(self, media, content_type):
diff --git a/falcon/media/msgpack.py b/falcon/media/msgpack.py
index 8c87b0e..d88b7a7 100644
--- a/falcon/media/msgpack.py
+++ b/falcon/media/msgpack.py
@@ -37,8 +37,8 @@ class MessagePackHandler(BaseHandler):
             return self.msgpack.unpackb(stream.read(), raw=False)
         except ValueError as err:
             raise errors.HTTPBadRequest(
-                'Invalid MessagePack',
-                'Could not parse MessagePack body - {0}'.format(err)
+                title='Invalid MessagePack',
+                description='Could not parse MessagePack body - {0}'.format(err)
             )
 
     async def deserialize_async(self, stream, content_type, content_length):
@@ -50,8 +50,8 @@ class MessagePackHandler(BaseHandler):
             return self.msgpack.unpackb(data, raw=False)
         except ValueError as err:
             raise errors.HTTPBadRequest(
-                'Invalid MessagePack',
-                'Could not parse MessagePack body - {0}'.format(err)
+                title='Invalid MessagePack',
+                description='Could not parse MessagePack body - {0}'.format(err)
             )
 
     def serialize(self, media, content_type):
diff --git a/falcon/media/validators/jsonschema.py b/falcon/media/validators/jsonschema.py
index 1a3863e..62db1cd 100644
--- a/falcon/media/validators/jsonschema.py
+++ b/falcon/media/validators/jsonschema.py
@@ -80,7 +80,7 @@ def _validate(func, req_schema=None, resp_schema=None):
                 )
             except jsonschema.ValidationError as e:
                 raise falcon.HTTPBadRequest(
-                    'Request data failed validation',
+                    title='Request data failed validation',
                     description=e.message
                 )
 
@@ -94,7 +94,7 @@ def _validate(func, req_schema=None, resp_schema=None):
                 )
             except jsonschema.ValidationError:
                 raise falcon.HTTPInternalServerError(
-                    'Response data failed validation'
+                    title='Response data failed validation'
                     # Do not return 'e.message' in the response to
                     # prevent info about possible internal response
                     # formatting bugs from leaking out to users.
@@ -118,7 +118,7 @@ def _validate_async(func, req_schema=None, resp_schema=None):
                 )
             except jsonschema.ValidationError as e:
                 raise falcon.HTTPBadRequest(
-                    'Request data failed validation',
+                    title='Request data failed validation',
                     description=e.message
                 )
 
@@ -132,7 +132,7 @@ def _validate_async(func, req_schema=None, resp_schema=None):
                 )
             except jsonschema.ValidationError:
                 raise falcon.HTTPInternalServerError(
-                    'Response data failed validation'
+                    title='Response data failed validation'
                     # Do not return 'e.message' in the response to
                     # prevent info about possible internal response
                     # formatting bugs from leaking out to users.
diff --git a/falcon/responders.py b/falcon/responders.py
index 516bf0f..d01ab0b 100644
--- a/falcon/responders.py
+++ b/falcon/responders.py
@@ -32,12 +32,12 @@ async def path_not_found_async(req, resp, **kwargs):
 
 def bad_request(req, resp, **kwargs):
     """Raise 400 HTTPBadRequest error"""
-    raise HTTPBadRequest('Bad request', 'Invalid HTTP method')
+    raise HTTPBadRequest(title='Bad request', description='Invalid HTTP method')
 
 
 async def bad_request_async(req, resp, **kwargs):
     """Raise 400 HTTPBadRequest error"""
-    raise HTTPBadRequest('Bad request', 'Invalid HTTP method')
+    raise HTTPBadRequest(title='Bad request', description='Invalid HTTP method')
 
 
 def create_method_not_allowed(allowed_methods, asgi=False):
diff --git a/falcon/util/misc.py b/falcon/util/misc.py
index 5cf4699..de48c4f 100644
--- a/falcon/util/misc.py
+++ b/falcon/util/misc.py
@@ -48,6 +48,7 @@ __all__ = (
     'get_http_status',
     'http_status_to_code',
     'code_to_http_status',
+    'deprecated_args',
     'secure_filename',
 )
 
@@ -472,3 +473,34 @@ def code_to_http_status(code):
         return getattr(status_codes, 'HTTP_' + str(code))
     except AttributeError:
         return str(code)
+
+
+def deprecated_args(*, allowed_positional, is_method=True):
+    """Flags a method call with positional args as deprecated
+
+    Keyword Args:
+        allowed_positional (int): Number of allowed positional arguments
+        is_method (bool, optional): The decorated function is a method. Will
+          add one to the number of allowed positional args to account for
+          ``self``. Defaults to True.
+    """
+
+    template = (
+        'Calls with{} positional args are deprecated.'
+        ' Please specify them as keyword arguments instead.'
+    )
+    text = ' more than {}'.format(allowed_positional) if allowed_positional else ''
+    warn_text = template.format(text)
+    if is_method:
+        allowed_positional += 1
+
+    def deprecated_args(fn):
+        @functools.wraps(fn)
+        def wraps(*args, **kwargs):
+            if len(args) > allowed_positional:
+                warnings.warn(warn_text, DeprecatedWarning, stacklevel=2)
+            return fn(*args, **kwargs)
+
+        return wraps
+
+    return deprecated_args


Test patch:
diff --git a/tests/test_after_hooks.py b/tests/test_after_hooks.py
index f332898..0905047 100644
--- a/tests/test_after_hooks.py
+++ b/tests/test_after_hooks.py
@@ -36,7 +36,7 @@ def client(asgi):
 
 def validate_output(req, resp, resource):
     assert resource
-    raise falcon.HTTPError(falcon.HTTP_723, 'Tricky')
+    raise falcon.HTTPError(falcon.HTTP_723, title='Tricky')
 
 
 def serialize_body(req, resp, resource):
diff --git a/tests/test_before_hooks.py b/tests/test_before_hooks.py
index bbe2d0a..8a8aa0f 100644
--- a/tests/test_before_hooks.py
+++ b/tests/test_before_hooks.py
@@ -12,7 +12,7 @@ from _util import create_app, create_resp, disable_asgi_non_coroutine_wrapping
 
 def validate(req, resp, resource, params):
     assert resource
-    raise falcon.HTTPBadRequest('Invalid thing', 'Your thing was not '
+    raise falcon.HTTPBadRequest(title='Invalid thing', description='Your thing was not '
                                 'formatted correctly.')
 
 
@@ -22,7 +22,7 @@ def validate_param(req, resp, resource, params, param_name, maxval=100):
     limit = req.get_param_as_int(param_name)
     if limit and int(limit) > maxval:
         msg = '{0} must be <= {1}'.format(param_name, maxval)
-        raise falcon.HTTPBadRequest('Out of Range', msg)
+        raise falcon.HTTPBadRequest(title='Out of Range', description=msg)
 
 
 async def validate_param_async(*args, **kwargs):
@@ -456,7 +456,7 @@ class PiggybackingCollection:
     @falcon.before(header_hook)
     def on_delete_collection(self, req, resp, fish):
         if fish != 'wet':
-            raise falcon.HTTPUnavailableForLegalReasons('fish must be wet')
+            raise falcon.HTTPUnavailableForLegalReasons(title='fish must be wet')
         self._items = {}
         resp.status = falcon.HTTP_NO_CONTENT
 
diff --git a/tests/test_error.py b/tests/test_error.py
index ea33ae1..d7ef36f 100644
--- a/tests/test_error.py
+++ b/tests/test_error.py
@@ -1,17 +1,25 @@
 import pytest
 
 import falcon
+import falcon.errors as errors
 import falcon.status_codes as status
+from falcon.util.misc import DeprecatedWarning
 
 
 @pytest.mark.parametrize('err, title', [
     (falcon.HTTPBadRequest, status.HTTP_400),
+    (falcon.HTTPUnauthorized, status.HTTP_401),
     (falcon.HTTPForbidden, status.HTTP_403),
+    (falcon.HTTPNotFound, status.HTTP_404),
+    (errors.HTTPRouteNotFound, status.HTTP_404),
+    (falcon.HTTPNotAcceptable, status.HTTP_406),
     (falcon.HTTPConflict, status.HTTP_409),
+    (falcon.HTTPGone, status.HTTP_410),
     (falcon.HTTPLengthRequired, status.HTTP_411),
     (falcon.HTTPPreconditionFailed, status.HTTP_412),
     (falcon.HTTPPayloadTooLarge, status.HTTP_413),
     (falcon.HTTPUriTooLong, status.HTTP_414),
+    (falcon.HTTPUnsupportedMediaType, status.HTTP_415),
     (falcon.HTTPUnprocessableEntity, status.HTTP_422),
     (falcon.HTTPLocked, status.HTTP_423),
     (falcon.HTTPFailedDependency, status.HTTP_424),
@@ -40,17 +48,40 @@ def test_with_default_title_and_desc(err, title):
         assert 'Retry-After' not in e.value.headers
 
 
+@pytest.mark.parametrize('err, title, args', (
+    (falcon.HTTPMethodNotAllowed, status.HTTP_405, (['GET'], )),
+    (falcon.HTTPRangeNotSatisfiable, status.HTTP_416, (11,)),
+))
+def test_with_default_title_and_desc_args(err, title, args):
+    with pytest.raises(err) as e:
+        raise err(*args)
+
+    assert e.value.title == title
+    assert e.value.description is None
+
+    if e.value.headers:
+        assert 'Retry-After' not in e.value.headers
+
+
 @pytest.mark.parametrize('err', [
     falcon.HTTPBadRequest,
+    falcon.HTTPUnauthorized,
     falcon.HTTPForbidden,
+    falcon.HTTPNotFound,
+    errors.HTTPRouteNotFound,
+    falcon.HTTPNotAcceptable,
     falcon.HTTPConflict,
+    falcon.HTTPGone,
     falcon.HTTPLengthRequired,
     falcon.HTTPPreconditionFailed,
-    falcon.HTTPPreconditionRequired,
+    falcon.HTTPPayloadTooLarge,
     falcon.HTTPUriTooLong,
+    falcon.HTTPUnsupportedMediaType,
     falcon.HTTPUnprocessableEntity,
     falcon.HTTPLocked,
     falcon.HTTPFailedDependency,
+    falcon.HTTPPreconditionRequired,
+    falcon.HTTPTooManyRequests,
     falcon.HTTPRequestHeaderFieldsTooLarge,
     falcon.HTTPUnavailableForLegalReasons,
     falcon.HTTPInternalServerError,
@@ -76,6 +107,78 @@ def test_with_title_desc_and_headers(err):
     assert e.value.headers['foo'] == 'bar'
 
 
+@pytest.mark.parametrize('err', [
+    falcon.HTTPBadRequest,
+    falcon.HTTPUnauthorized,
+    falcon.HTTPForbidden,
+    falcon.HTTPNotFound,
+    errors.HTTPRouteNotFound,
+    falcon.HTTPNotAcceptable,
+    falcon.HTTPConflict,
+    falcon.HTTPGone,
+    falcon.HTTPLengthRequired,
+    falcon.HTTPPreconditionFailed,
+    falcon.HTTPPayloadTooLarge,
+    falcon.HTTPUriTooLong,
+    falcon.HTTPUnsupportedMediaType,
+    falcon.HTTPUnprocessableEntity,
+    falcon.HTTPLocked,
+    falcon.HTTPFailedDependency,
+    falcon.HTTPPreconditionRequired,
+    falcon.HTTPTooManyRequests,
+    falcon.HTTPRequestHeaderFieldsTooLarge,
+    falcon.HTTPUnavailableForLegalReasons,
+    falcon.HTTPInternalServerError,
+    falcon.HTTPNotImplemented,
+    falcon.HTTPBadGateway,
+    falcon.HTTPServiceUnavailable,
+    falcon.HTTPGatewayTimeout,
+    falcon.HTTPVersionNotSupported,
+    falcon.HTTPInsufficientStorage,
+    falcon.HTTPLoopDetected,
+    falcon.HTTPNetworkAuthenticationRequired,
+])
+def test_kw_only(err):
+    # only deprecated for now
+    # with pytest.raises(TypeError, match='positional argument'):
+    #     err('foo', 'bar')
+    with pytest.warns(DeprecatedWarning, match='positional args are deprecated'):
+        err('foo', 'bar')
+
+
+@pytest.mark.parametrize('err, args', (
+    (falcon.HTTPMethodNotAllowed, (['GET'], )),
+    (falcon.HTTPRangeNotSatisfiable, (11,)),
+))
+def test_with_title_desc_and_headers_args(err, args):
+    title = 'trace'
+    desc = 'boom'
+    headers = {'foo': 'bar'}
+
+    with pytest.raises(err) as e:
+        raise err(*args, title=title, description=desc, headers=headers)
+
+    assert e.value.title == title
+    assert e.value.description == desc
+    assert e.value.headers['foo'] == 'bar'
+
+
+@pytest.mark.parametrize('err, args', (
+    (falcon.HTTPMethodNotAllowed, (['GET'], )),
+    (falcon.HTTPRangeNotSatisfiable, (11,)),
+    (falcon.HTTPInvalidHeader, ('foo', 'bar')),
+    (falcon.HTTPMissingHeader, ('foo',)),
+    (falcon.HTTPInvalidParam, ('foo', 'bar')),
+    (falcon.HTTPMissingParam, ('foo',)),
+))
+def test_args_kw_only(err, args):
+    # only deprecated for now
+    # with pytest.raises(TypeError, match='positional argument'):
+    #     err(*args, 'bar')
+    with pytest.warns(DeprecatedWarning, match='positional args are deprecated'):
+        err(*args, 'bar')
+
+
 @pytest.mark.parametrize('err', [
     falcon.HTTPServiceUnavailable,
     falcon.HTTPTooManyRequests,
@@ -101,80 +204,78 @@ def test_with_retry_after_and_headers(err):
     assert e.value.headers['foo'] == 'bar'
 
 
-def test_http_range_not_satisfiable_with_headers():
-    try:
-        raise falcon.HTTPRangeNotSatisfiable(resource_length=0, headers={'foo': 'bar'})
-    except falcon.HTTPRangeNotSatisfiable as e:
-        assert e.headers['foo'] == 'bar'
-
-
-def test_http_unauthorized_no_title_and_desc_and_challenges_and_headers():
-    try:
-        raise falcon.HTTPUnauthorized()
-    except falcon.HTTPUnauthorized as e:
-        assert status.HTTP_401 == e.title
-        assert e.description is None
-        assert 'WWW-Authenticate' not in e.headers
-
-
-def test_http_unauthorized_with_title_and_desc_and_challenges_and_headers():
-    try:
-        raise falcon.HTTPUnauthorized(
-            title='Test',
-            description='Testdescription',
-            challenges=['Testch'],
-            headers={'foo': 'bar'}
-        )
-    except falcon.HTTPUnauthorized as e:
-        assert 'Test' == e.title
-        assert 'Testdescription' == e.description
-        assert 'Testch' == e.headers['WWW-Authenticate']
-        assert 'bar' == e.headers['foo']
-
-
-def test_http_not_acceptable_no_title_and_desc_and_challenges():
-    try:
-        raise falcon.HTTPNotAcceptable()
-    except falcon.HTTPNotAcceptable as e:
-        assert e.description is None
-
-
-def test_http_not_acceptable_with_title():
-    try:
-        raise falcon.HTTPNotAcceptable(title='test title')
-    except falcon.HTTPNotAcceptable as e:
-        assert e.title == 'test title'
-
-
-def test_http_not_acceptable_with_title_and_desc_and_challenges():
-    try:
-        raise falcon.HTTPNotAcceptable(description='Testdescription')
-    except falcon.HTTPNotAcceptable as e:
-        assert 'Testdescription' == e.description
-
-
-def test_http_unsupported_media_type_no_title_and_desc_and_challenges():
-    try:
-        raise falcon.HTTPUnsupportedMediaType()
-    except falcon.HTTPUnsupportedMediaType as e:
-        assert e.description is None
-
+def test_http_error_repr():
+    error = falcon.HTTPBadRequest()
+    _repr = '<%s: %s>' % (error.__class__.__name__, error.status)
+    assert error.__repr__() == _repr
 
-def test_http_unsupported_media_type_with_title():
-    try:
-        raise falcon.HTTPUnsupportedMediaType(title='test title')
-    except falcon.HTTPUnsupportedMediaType as e:
-        assert e.title == 'test title'
 
+@pytest.mark.parametrize('err, args, title, desc', (
+    (falcon.HTTPInvalidHeader, ('foo', 'bar'), 'Invalid header value',
+     'The value provided for the "bar" header is invalid. foo'),
+    (falcon.HTTPMissingHeader, ('foo',), 'Missing header value', 'The "foo" header is required.'),
+    (falcon.HTTPInvalidParam, ('foo', 'bar'), 'Invalid parameter',
+     'The "bar" parameter is invalid. foo'),
+    (falcon.HTTPMissingParam, ('foo',), 'Missing parameter', 'The "foo" parameter is required.'),
+))
+def test_custom_400(err, args, title, desc):
+    with pytest.raises(err) as e:
+        raise err(*args)
 
-def test_http_unsupported_media_type_with_title_and_desc_and_challenges():
-    try:
-        raise falcon.HTTPUnsupportedMediaType(description='boom')
-    except falcon.HTTPUnsupportedMediaType as e:
-        assert e.description == 'boom'
+    assert e.value.title == title
+    assert e.value.description == desc
 
 
-def test_http_error_repr():
-    error = falcon.HTTPBadRequest()
-    _repr = '<%s: %s>' % (error.__class__.__name__, error.status)
-    assert error.__repr__() == _repr
+@pytest.mark.parametrize('err, header_name, kw_name, args, res, kw_required', (
+    (falcon.HTTPUnauthorized, 'WWW-Authenticate', 'challenges', ('a', 'b'), 'a, b', False),
+    (falcon.HTTPMethodNotAllowed, 'Allow', 'allowed_methods', ('a', 'b'), 'a, b', True),
+    (falcon.HTTPPayloadTooLarge, 'Retry-After', 'retry_after', 123, '123', False),
+    (falcon.HTTPRangeNotSatisfiable, 'Content-Range', 'resource_length', 123, 'bytes */123', True),
+    (falcon.HTTPTooManyRequests, 'Retry-After', 'retry_after', 123, '123', False),
+    (falcon.HTTPServiceUnavailable, 'Retry-After', 'retry_after', 123, '123', False),
+))
+class TestErrorsWithHeadersKW:
+    def test_no_header(self, err, header_name, kw_name, args, res, kw_required):
+        if not kw_required:
+            value = err()
+
+            if value.headers:
+                assert header_name not in value.headers
+
+    def test_other_header(self, err, header_name, kw_name, args, res, kw_required):
+        headers = {'foo bar': 'baz'}
+        kw = {kw_name: args}
+        value = err(**kw, headers=headers)
+
+        assert value.headers['foo bar'] == 'baz'
+        assert header_name in value.headers
+        assert value.headers[header_name] == res
+
+    def test_override_header(self, err, header_name, kw_name, args, res, kw_required):
+        headers = {'foo bar': 'baz', header_name: 'other'}
+        kw = {kw_name: args}
+        value = err(**kw, headers=headers)
+
+        assert value.headers['foo bar'] == 'baz'
+        assert header_name in value.headers
+        assert value.headers[header_name] == res
+
+    def test_other_header_list(self, err, header_name, kw_name, args, res, kw_required):
+        headers = [('foo bar', 'baz')]
+        kw = {kw_name: args}
+        value = err(**kw, headers=headers)
+
+        assert value.headers['foo bar'] == 'baz'
+        assert header_name in value.headers
+        assert isinstance(value.headers, dict)
+        assert value.headers[header_name] == res
+
+    def test_override_header_list(self, err, header_name, kw_name, args, res, kw_required):
+        headers = [('foo bar', 'baz'), (header_name, 'other')]
+        kw = {kw_name: args}
+        value = err(**kw, headers=headers)
+
+        assert value.headers['foo bar'] == 'baz'
+        assert header_name in value.headers
+        assert isinstance(value.headers, dict)
+        assert value.headers[header_name] == res
diff --git a/tests/test_error_handlers.py b/tests/test_error_handlers.py
index 504840b..55d6a7c 100644
--- a/tests/test_error_handlers.py
+++ b/tests/test_error_handlers.py
@@ -30,8 +30,8 @@ class CustomException(CustomBaseException):
     def handle(req, resp, ex, params):
         raise falcon.HTTPError(
             falcon.HTTP_792,
-            'Internet crashed!',
-            'Catastrophic weather event',
+            title='Internet crashed!',
+            description='Catastrophic weather event',
             href='http://example.com/api/inconvenient-truth',
             href_text='Drill, baby drill!')
 
diff --git a/tests/test_example.py b/tests/test_example.py
index 675664b..caf46c2 100644
--- a/tests/test_example.py
+++ b/tests/test_example.py
@@ -26,8 +26,8 @@ class StorageError(Exception):
                        'database. It worked on my box.')
 
         raise falcon.HTTPError(falcon.HTTP_725,
-                               'Database Error',
-                               description)
+                               title='Database Error',
+                               description=description)
 
 
 class SinkAdapter:
@@ -59,18 +59,18 @@ class AuthMiddleware:
             description = ('Please provide an auth token '
                            'as part of the request.')
 
-            raise falcon.HTTPUnauthorized('Auth token required',
-                                          description,
-                                          challenges,
+            raise falcon.HTTPUnauthorized(title='Auth token required',
+                                          description=description,
+                                          challenges=challenges,
                                           href='http://docs.example.com/auth')
 
         if not self._token_is_valid(token, account_id):
             description = ('The provided auth token is not valid. '
                            'Please request a new token and try again.')
 
-            raise falcon.HTTPUnauthorized('Authentication required',
-                                          description,
-                                          challenges,
+            raise falcon.HTTPUnauthorized(title='Authentication required',
+                                          description=description,
+                                          challenges=challenges,
                                           href='http://docs.example.com/auth')
 
     def _token_is_valid(self, token, account_id):
@@ -107,16 +107,16 @@ class JSONTranslator:
 
         body = req.bounded_stream.read()
         if not body:
-            raise falcon.HTTPBadRequest('Empty request body',
-                                        'A valid JSON document is required.')
+            raise falcon.HTTPBadRequest(title='Empty request body',
+                                        description='A valid JSON document is required.')
 
         try:
             req.context.doc = json.loads(body.decode('utf-8'))
 
         except (ValueError, UnicodeDecodeError):
             raise falcon.HTTPError(falcon.HTTP_753,
-                                   'Malformed JSON',
-                                   'Could not decode the request body. The '
+                                   title='Malformed JSON',
+                                   description='Could not decode the request body. The '
                                    'JSON was incorrect or not encoded as '
                                    'UTF-8.')
 
@@ -136,7 +136,7 @@ def max_body(limit):
                    'exceed ' + str(limit) + ' bytes in length.')
 
             raise falcon.HTTPPayloadTooLarge(
-                'Request body is too large', msg)
+                title='Request body is too large', description=msg)
 
     return hook
 
@@ -161,9 +161,9 @@ class ThingsResource:
                            'We appreciate your patience.')
 
             raise falcon.HTTPServiceUnavailable(
-                'Service Outage',
-                description,
-                30)
+                title='Service Outage',
+                description=description,
+                retry_after=30)
 
         # An alternative way of doing DRY serialization would be to
         # create a custom class that inherits from falcon.Request. This
@@ -180,8 +180,8 @@ class ThingsResource:
             doc = req.context.doc
         except AttributeError:
             raise falcon.HTTPBadRequest(
-                'Missing thing',
-                'A thing must be submitted in the request body.')
+                title='Missing thing',
+                description='A thing must be submitted in the request body.')
 
         proper_thing = self.db.add_thing(doc)
 
diff --git a/tests/test_headers.py b/tests/test_headers.py
index 5d40d2d..6df03be 100644
--- a/tests/test_headers.py
+++ b/tests/test_headers.py
@@ -349,7 +349,7 @@ class TestHeaders:
         except falcon.HTTPMissingHeader as ex:
             assert isinstance(ex, falcon.HTTPBadRequest)
             assert ex.title == 'Missing header value'
-            expected_desc = 'The X-Not-Found header is required.'
+            expected_desc = 'The "X-Not-Found" header is required.'
             assert ex.description == expected_desc
 
     @pytest.mark.parametrize('status', (falcon.HTTP_204, falcon.HTTP_304))
diff --git a/tests/test_httperror.py b/tests/test_httperror.py
index 198edc3..3546438 100644
--- a/tests/test_httperror.py
+++ b/tests/test_httperror.py
@@ -9,7 +9,7 @@ import yaml
 
 import falcon
 import falcon.testing as testing
-from falcon.util import json
+from falcon.util import json, misc
 
 from _util import create_app  # NOQA
 
@@ -32,19 +32,19 @@ class FaultyResource:
         description = req.get_header('X-Error-Description')
         code = 10042
 
-        raise falcon.HTTPError(status, title, description, code=code)
+        raise falcon.HTTPError(status, title=title, description=description, code=code)
 
     def on_post(self, req, resp):
         raise falcon.HTTPForbidden(
-            'Request denied',
-            'You do not have write permissions for this queue.',
+            title='Request denied',
+            description='You do not have write permissions for this queue.',
             href='http://example.com/api/rbac')
 
     def on_put(self, req, resp):
         raise falcon.HTTPError(
             falcon.HTTP_792,
-            'Internet crashed',
-            'Catastrophic weather event due to climate change.',
+            title='Internet crashed',
+            description='Catastrophic weather event due to climate change.',
             href='http://example.com/api/climate',
             href_text='Drill baby drill!',
             code=8733224)
@@ -62,8 +62,8 @@ class UnicodeFaultyResource:
         self.called = True
         raise falcon.HTTPError(
             falcon.HTTP_792,
-            'Internet \xe7rashed!',
-            '\xc7atastrophic weather event',
+            title='Internet \xe7rashed!',
+            description='\xc7atastrophic weather event',
             href='http://example.com/api/\xe7limate',
             href_text='Drill b\xe1by drill!')
 
@@ -76,27 +76,27 @@ class MiscErrorsResource:
 
     def on_get(self, req, resp):
         if self.needs_title:
-            raise self._exception('Excuse Us', 'Something went boink!')
+            raise self._exception(title='Excuse Us', description='Something went boink!')
         else:
-            raise self._exception('Something went boink!')
+            raise self._exception(title='Something went boink!')
 
 
 class UnauthorizedResource:
 
     def on_get(self, req, resp):
-        raise falcon.HTTPUnauthorized('Authentication Required',
-                                      'Missing or invalid authorization.',
-                                      ['Basic realm="simple"'])
+        raise falcon.HTTPUnauthorized(title='Authentication Required',
+                                      description='Missing or invalid authorization.',
+                                      challenges=['Basic realm="simple"'])
 
     def on_post(self, req, resp):
-        raise falcon.HTTPUnauthorized('Authentication Required',
-                                      'Missing or invalid authorization.',
-                                      ['Newauth realm="apps"',
-                                       'Basic realm="simple"'])
+        raise falcon.HTTPUnauthorized(title='Authentication Required',
+                                      description='Missing or invalid authorization.',
+                                      challenges=['Newauth realm="apps"', 'Basic realm="simple"'])
 
     def on_put(self, req, resp):
-        raise falcon.HTTPUnauthorized('Authentication Required',
-                                      'Missing or invalid authorization.', [])
+        raise falcon.HTTPUnauthorized(title='Authentication Required',
+                                      description='Missing or invalid authorization.',
+                                      challenges=[])
 
 
 class NotFoundResource:
@@ -156,14 +156,14 @@ class MethodNotAllowedResourceWithBody:
 class LengthRequiredResource:
 
     def on_get(self, req, resp):
-        raise falcon.HTTPLengthRequired('title', 'description')
+        raise falcon.HTTPLengthRequired(title='title', description='description')
 
 
 class RequestEntityTooLongResource:
 
     def on_get(self, req, resp):
-        raise falcon.HTTPPayloadTooLarge('Request Rejected',
-                                         'Request Body Too Large')
+        raise falcon.HTTPPayloadTooLarge(title='Request Rejected',
+                                         description='Request Body Too Large')
 
 
 class TemporaryRequestEntityTooLongResource:
@@ -172,8 +172,8 @@ class TemporaryRequestEntityTooLongResource:
         self.retry_after = retry_after
 
     def on_get(self, req, resp):
-        raise falcon.HTTPPayloadTooLarge('Request Rejected',
-                                         'Request Body Too Large',
+        raise falcon.HTTPPayloadTooLarge(title='Request Rejected',
+                                         description='Request Body Too Large',
                                          retry_after=self.retry_after)
 
 
@@ -185,8 +185,8 @@ class UriTooLongResource:
         self.code = code
 
     def on_get(self, req, resp):
-        raise falcon.HTTPUriTooLong(self.title,
-                                    self.description,
+        raise falcon.HTTPUriTooLong(title=self.title,
+                                    description=self.description,
                                     code=self.code)
 
 
@@ -202,8 +202,8 @@ class TooManyRequestsResource:
         self.retry_after = retry_after
 
     def on_get(self, req, resp):
-        raise falcon.HTTPTooManyRequests('Too many requests',
-                                         '1 per minute',
+        raise falcon.HTTPTooManyRequests(title='Too many requests',
+                                         description='1 per minute',
                                          retry_after=self.retry_after)
 
 
@@ -213,8 +213,8 @@ class ServiceUnavailableResource:
         self.retry_after = retry_after
 
     def on_get(self, req, resp):
-        raise falcon.HTTPServiceUnavailable('Oops',
-                                            'Stand by...',
+        raise falcon.HTTPServiceUnavailable(title='Oops',
+                                            description='Stand by...',
                                             retry_after=self.retry_after)
 
 
@@ -782,7 +782,7 @@ class TestHTTPError:
         client.app.add_route('/400', InvalidHeaderResource())
         response = client.simulate_request(path='/400')
 
-        expected_desc = ('The value provided for the X-Auth-Token '
+        expected_desc = ('The value provided for the "X-Auth-Token" '
                          'header is invalid. Please provide a valid token.')
 
         expected_body = {
@@ -800,7 +800,7 @@ class TestHTTPError:
 
         expected_body = {
             'title': 'Missing header value',
-            'description': 'The X-Auth-Token header is required.',
+            'description': 'The "X-Auth-Token" header is required.',
         }
 
         assert response.status == falcon.HTTP_400
@@ -857,3 +857,11 @@ class TestHTTPError:
 
         assert response.status == headers['X-Error-Status']
         assert response.json['title'] == headers['X-Error-Status']
+
+
+def test_kw_only():
+    # only deprecated for now
+    # with pytest.raises(TypeError, match='positional argument'):
+    #     falcon.HTTPError(falcon.HTTP_BAD_REQUEST, 'foo', 'bar')
+    with pytest.warns(misc.DeprecatedWarning, match='positional args are deprecated'):
+        falcon.HTTPError(falcon.HTTP_BAD_REQUEST, 'foo', 'bar')
diff --git a/tests/test_middleware.py b/tests/test_middleware.py
index 174df18..e2a20de 100644
--- a/tests/test_middleware.py
+++ b/tests/test_middleware.py
@@ -157,7 +157,7 @@ class MiddlewareClassResource:
         resp.body = json.dumps(_EXPECTED_BODY)
 
     def on_post(self, req, resp):
-        raise falcon.HTTPForbidden(falcon.HTTP_403, 'Setec Astronomy')
+        raise falcon.HTTPForbidden(title=falcon.HTTP_403, description='Setec Astronomy')
 
 
 class EmptySignatureMiddleware:
diff --git a/tests/test_request_attrs.py b/tests/test_request_attrs.py
index c0acd27..825dbea 100644
--- a/tests/test_request_attrs.py
+++ b/tests/test_request_attrs.py
@@ -520,7 +520,7 @@ class TestRequestAttributes:
             req.range
 
         headers = {'Range': 'bytes=-'}
-        expected_desc = ('The value provided for the Range header is '
+        expected_desc = ('The value provided for the "Range" header is '
                          'invalid. The range offsets are missing.')
         self._test_error_details(headers, 'range',
                                  falcon.HTTPInvalidHeader,
@@ -583,7 +583,7 @@ class TestRequestAttributes:
             req.range
 
         headers = {'Range': 'bytes=x-y'}
-        expected_desc = ('The value provided for the Range header is '
+        expected_desc = ('The value provided for the "Range" header is '
                          'invalid. It must be a range formatted '
                          'according to RFC 7233.')
         self._test_error_details(headers, 'range',
@@ -592,7 +592,7 @@ class TestRequestAttributes:
                                  asgi)
 
         headers = {'Range': 'bytes=0-0,-1'}
-        expected_desc = ('The value provided for the Range '
+        expected_desc = ('The value provided for the "Range" '
                          'header is invalid. The value must be a '
                          'continuous range.')
         self._test_error_details(headers, 'range',
@@ -601,7 +601,7 @@ class TestRequestAttributes:
                                  asgi)
 
         headers = {'Range': '10-'}
-        expected_desc = ('The value provided for the Range '
+        expected_desc = ('The value provided for the "Range" '
                          'header is invalid. The value must be '
                          "prefixed with a range unit, e.g. 'bytes='")
         self._test_error_details(headers, 'range',
@@ -628,7 +628,7 @@ class TestRequestAttributes:
     def test_bogus_content_length_nan(self, asgi):
         headers = {'content-length': 'fuzzy-bunnies'}
         expected_desc = ('The value provided for the '
-                         'Content-Length header is invalid. The value '
+                         '"Content-Length" header is invalid. The value '
                          'of the header must be a number.')
         self._test_error_details(headers, 'content_length',
                                  falcon.HTTPInvalidHeader,
@@ -637,7 +637,7 @@ class TestRequestAttributes:
 
     def test_bogus_content_length_neg(self, asgi):
         headers = {'content-length': '-1'}
-        expected_desc = ('The value provided for the Content-Length '
+        expected_desc = ('The value provided for the "Content-Length" '
                          'header is invalid. The value of the header '
                          'must be a positive number.')
         self._test_error_details(headers, 'content_length',
@@ -667,7 +667,7 @@ class TestRequestAttributes:
 
         # Date formats don't conform to RFC 1123
         headers = {header: 'Thu, 04 Apr 2013'}
-        expected_desc = ('The value provided for the {} '
+        expected_desc = ('The value provided for the "{}" '
                          'header is invalid. It must be formatted '
                          'according to RFC 7231, Section 7.1.1.1')
 
diff --git a/tests/test_utils.py b/tests/test_utils.py
index c813257..d393a09 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1001,3 +1001,26 @@ class TestContextType:
         assert set(ctx.keys()) == {1, 2, 3, 4}
         assert set(ctx.values()) == {1, 4, 9, 16}
         assert set(ctx.items()) == {(1, 1), (2, 4), (3, 9), (4, 16)}
+
+
+class TestDeprecatedArgs:
+    def test_method(self, recwarn):
+        class C:
+            @misc.deprecated_args(allowed_positional=0)
+            def a_method(self, a=1, b=2):
+                pass
+
+        C().a_method(a=1, b=2)
+        assert len(recwarn) == 0
+        C().a_method(1, b=2)
+        assert len(recwarn) == 1
+
+    def test_function(self, recwarn):
+        @misc.deprecated_args(allowed_positional=0, is_method=False)
+        def a_function(a=1, b=2):
+            pass
+
+        a_function(a=1, b=2)
+        assert len(recwarn) == 0
+        a_function(1, b=2)
+        assert len(recwarn) == 1