Repository: falconry/falcon

Issue:
Create InvalidHeader and InvalidParam HTTP exceptions
These should inherit from HTTPBadRequest. Use request.InvalidHeaderValueError and request.InvalidParamValueError as starting points. It would be great to somehow encourage the mention of the problematic header/param in the description.



Discussion:
I'll take on this one.

thanks! If you have any questions, come find me in #falconframework on freenode.

IIRC, the idea with this work item was to relocate those two classes from the request module to the exceptions module. We will need to add nice docstrings in the process. Also, I was thinking that rather than just have a simple 'msg' arg, we might make the interface more structured. I am imagining a templated description, e.g.:

``` python
'The value provided for the "{header_name}" header is invalid. {info}'
```

and

``` python
'The value provided for the "{param}" query parameter is invalid. {info}'
```

Feel free to play around with the wording and such to make it beautifarous.


Resolving patch:
diff --git a/falcon/exceptions.py b/falcon/exceptions.py
--- a/falcon/exceptions.py
+++ b/falcon/exceptions.py
@@ -382,3 +382,37 @@ def __init__(self, title, description, retry_after, **kwargs):
         headers = kwargs.setdefault('headers', {})
         headers['Retry-After'] = str(retry_after)
         HTTPError.__init__(self, status.HTTP_503, title, description, **kwargs)
+
+
+class InvalidHeader(HTTPBadRequest):
+    """HTTP header is invalid.
+
+    Args:
+        msg (str): A description of why the value is invalid.
+        value (str): The value that is given to the header.
+        header_name (str): The name of the header.
+        kwargs (optional): Optional args to include more information.
+        """
+
+    def __init__(self, msg, value, header_name, **kwargs):
+        description = ('The value "{0}" given for the {1} header is invalid. '
+                       '{2}').format(value, header_name, msg)
+
+        super(InvalidHeader, self).__init__('Invalid header value',
+                                            description, **kwargs)
+
+
+class InvalidParam(HTTPBadRequest):
+    """HTTP parameter is invalid
+
+    Args:
+        msg (str): A description of the invalid parameter.
+        param_name (str): The name of the paramameter.
+        kwargs (optional): Optional args to include more information.
+        """
+
+    def __init__(self, msg, param_name, **kwargs):
+        description = 'The {0} parameter is invalid. {1}'.format(param_name,
+                                                                 msg)
+        super(InvalidParam, self).__init__('Invalid query parameter',
+                                           description, **kwargs)
diff --git a/falcon/request.py b/falcon/request.py
--- a/falcon/request.py
+++ b/falcon/request.py
@@ -33,7 +33,7 @@
 import mimeparse
 import six
 
-from falcon.exceptions import HTTPBadRequest
+from falcon.exceptions import HTTPBadRequest, InvalidHeader, InvalidParam
 from falcon import util
 from falcon.util import uri
 from falcon import request_helpers as helpers
@@ -46,18 +46,6 @@
 FALSE_STRINGS = ('false', 'False', 'no')
 
 
-class InvalidHeaderValueError(HTTPBadRequest):
-    def __init__(self, msg, href=None, href_text=None):
-        super(InvalidHeaderValueError, self).__init__(
-            'Invalid header value', msg, href=href, href_text=None)
-
-
-class InvalidParamValueError(HTTPBadRequest):
-    def __init__(self, msg, href=None, href_text=None):
-        super(InvalidParamValueError, self).__init__(
-            'Invalid query parameter', msg, href=href, href_text=None)
-
-
 class Request(object):
     """Represents a client's HTTP request
 
@@ -268,18 +256,17 @@ def content_length(self):
                 formatted correctly or was a negative number.
         """
         value = self._get_header_by_wsgi_name('HTTP_CONTENT_LENGTH')
+
         if value:
             try:
                 value_as_int = int(value)
             except ValueError:
-                msg = ('The value of the content-length header must be '
-                       'a number.')
-                raise InvalidHeaderValueError(msg)
+                msg = 'The value of the header must be a number.'
+                raise InvalidHeader(msg, value, 'content-length')
 
             if value_as_int < 0:
-                msg = ('The value of the content-length header must be '
-                       'a positive number.')
-                raise InvalidHeaderValueError(msg)
+                msg = 'The value of the header must be a positive number.'
+                raise InvalidHeader(msg, value, 'content-length')
             else:
                 return value_as_int
 
@@ -309,9 +296,9 @@ def date(self):
         try:
             return util.http_date_to_dt(http_date)
         except ValueError:
-            msg = ('The value of the Date header could not be parsed. It '
-                   'must be formatted according to RFC 1123.')
-            raise InvalidHeaderValueError(msg)
+            msg = ('The value could not be parsed. It must be formatted '
+                   'according to RFC 1123.')
+            raise InvalidHeader(msg, http_date, 'date')
 
     @property
     def expect(self):
@@ -372,8 +359,8 @@ def range(self):
 
         if value:
             if ',' in value:
-                raise InvalidHeaderValueError('Only continuous byte ranges '
-                                              'are supported.')
+                msg = 'The value must be a continuous byte range.'
+                raise InvalidHeader(msg, value, 'range')
 
             try:
                 first, last = value.split('-')
@@ -383,16 +370,16 @@ def range(self):
                 elif last:
                     return (-int(last), -1)
                 else:
-                    raise InvalidHeaderValueError(
-                        'Range value is missing offsets')
+                    msg = 'The value is missing offsets.'
+                    raise InvalidHeader(msg, value, 'range')
 
             except ValueError:
                 href = 'http://goo.gl/zZ6Ey'
                 href_text = 'HTTP/1.1 Range Requests'
-                raise InvalidHeaderValueError('Range string must be formatted '
-                                              'according to RFC 2616.',
-                                              href=href,
-                                              href_text=href_text)
+                msg = ('The string given could not be parsed. It must be '
+                       'formatted according to RFC 2616.')
+                raise InvalidHeader(msg, value, 'range', href=href,
+                                    href_text=href_text)
 
      # implicit return None
 
@@ -573,19 +560,16 @@ def get_param_as_int(self, name,
             try:
                 val = int(val)
             except ValueError:
-                description = ('The value of the "' + name + '" query '
-                               'parameter must be an integer.')
-                raise InvalidParamValueError(description)
+                msg = 'The value must be an integer.'
+                raise InvalidParam(msg, name)
 
             if min is not None and val < min:
-                description = ('The value of the "' + name + '" query '
-                               'parameter must be at least %d') % min
-                raise InvalidHeaderValueError(description)
+                msg = 'The value must be at least ' + str(min)
+                raise InvalidParam(msg, name)
 
             if max is not None and max < val:
-                description = ('The value of the "' + name + '" query '
-                               'parameter may not exceed %d') % max
-                raise InvalidHeaderValueError(description)
+                msg = 'The value may not exceed ' + str(max)
+                raise InvalidParam(msg, name)
 
             if store is not None:
                 store[name] = val
@@ -636,9 +620,8 @@ def get_param_as_bool(self, name, required=False, store=None):
             elif val in FALSE_STRINGS:
                 val = False
             else:
-                description = ('The value of the "' + name + '" query '
-                               'parameter must be "true" or "false".')
-                raise InvalidParamValueError(description)
+                msg = 'The value of the parameter must be "true" or "false".'
+                raise InvalidParam(msg, name)
 
             if store is not None:
                 store[name] = val
@@ -712,9 +695,8 @@ def get_param_as_list(self, name,
                     items = [transform(i) if i != '' else None
                              for i in items]
                 except ValueError:
-                    desc = ('The value of the "' + name + '" query parameter '
-                            'is not formatted correctly.')
-                    raise InvalidParamValueError(desc)
+                    msg = 'The value is not formatted correctly.'
+                    raise InvalidParam(msg, name)
 
             if store is not None:
                 store[name] = items


Test patch: