diff --git a/djblets/webapi/decorators.py b/djblets/webapi/decorators.py
index 99cd39e687cf7a500a86a32e467af4a7112277ca..1617398ac04f0b9b36929160ab502948cc015f04 100644
--- a/djblets/webapi/decorators.py
+++ b/djblets/webapi/decorators.py
@@ -27,7 +27,6 @@
 
 from django.http import HttpRequest
 
-from djblets.util.decorators import simple_decorator
 from djblets.webapi.core import SPECIAL_PARAMS
 from djblets.webapi.errors import NOT_LOGGED_IN, PERMISSION_DENIED, \
                                   INVALID_FORM_DATA
@@ -45,7 +44,55 @@ def _find_httprequest(args):
     return request
 
 
-@simple_decorator
+def copy_webapi_decorator_data(from_func, to_func):
+    """Copies and merges data from one decorated function to another.
+
+    This will copy over the standard function information (name, docs,
+    and dictionary data), but will also handle intelligently merging
+    together data set by webapi decorators, such as the list of
+    possible errors.
+    """
+    from_errors = getattr(from_func, 'response_errors', set())
+    to_errors = getattr(to_func, 'response_errors', set())
+    from_required_fields = getattr(from_func, 'required_fields', {}).copy()
+    from_optional_fields = getattr(from_func, 'optional_fields', {}).copy()
+    to_required_fields = getattr(to_func, 'required_fields', {}).copy()
+    to_optional_fields = getattr(to_func, 'optional_fields', {}).copy()
+
+    to_func.__name__ = from_func.__name__
+    to_func.__doc__ = from_func.__doc__
+    to_func.__dict__.update(from_func.__dict__)
+
+    # Only copy if one of the two functions had this already.
+    if (hasattr(to_func, 'response_errors') or
+        hasattr(from_func, 'response_errors')):
+        to_func.response_errors = to_errors.union(from_errors)
+
+    if (hasattr(to_func, 'required_fields') or
+        hasattr(from_func, 'required_fields')):
+        to_func.required_fields = from_required_fields
+        to_func.required_fields.update(to_required_fields)
+        to_func.optional_fields = from_optional_fields
+        to_func.optional_fields.update(to_optional_fields)
+
+    return to_func
+
+
+def webapi_decorator(decorator):
+    """Decorator for simple webapi decorators.
+
+    This is meant to be used for other webapi decorators in order to
+    intelligently preserve information, like the possible response
+    errors. It handles merging lists of errors and other information
+    instead of overwriting one list with another, as simple_decorator
+    would do.
+    """
+    return copy_webapi_decorator_data(
+        decorator,
+        lambda f: copy_webapi_decorator_data(f, decorator(f)))
+
+
+@webapi_decorator
 def webapi(view_func):
     """Indicates that a view is a Web API handler."""
     return view_func
@@ -57,30 +104,26 @@ def webapi_response_errors(*errors):
     This can be used for generating documentation or schemas that cover
     the possible error responses of methods on a resource.
     """
+    @webapi_decorator
     def _dec(view_func):
         def _call(*args, **kwargs):
             return view_func(*args, **kwargs)
 
-        _call.__name__ = view_func.__name__
-        _call.__doc__ = view_func.__doc__
-        _call.__dict__.update(view_func.__dict__)
-
-        existing_errors = getattr(view_func, 'response_errors', set())
-        _call.response_errors = existing_errors.union(set(errors))
+        _call.response_errors = set(errors)
 
         return _call
 
     return _dec
 
 
-@webapi_response_errors(NOT_LOGGED_IN)
-@simple_decorator
+@webapi_decorator
 def webapi_login_required(view_func):
     """
     Checks that the user is logged in before invoking the view. If the user
     is not logged in, a NOT_LOGGED_IN error (HTTP 401 Unauthorized) is
     returned.
     """
+    @webapi_response_errors(NOT_LOGGED_IN)
     def _checklogin(*args, **kwargs):
         request = _find_httprequest(args)
 
@@ -89,19 +132,20 @@ def webapi_login_required(view_func):
         else:
             return NOT_LOGGED_IN
 
-    view_func.login_required = True
+    _checklogin.login_required = True
 
     return _checklogin
 
 
-@webapi_response_errors(NOT_LOGGED_IN, PERMISSION_DENIED)
 def webapi_permission_required(perm):
     """
     Checks that the user is logged in and has the appropriate permissions
     to access this view. A PERMISSION_DENIED error is returned if the user
     does not have the proper permissions.
     """
+    @webapi_decorator
     def _dec(view_func):
+        @webapi_response_errors(NOT_LOGGED_IN, PERMISSION_DENIED)
         def _checkpermissions(*args, **kwargs):
             request = _find_httprequest(args)
 
@@ -119,7 +163,6 @@ def webapi_permission_required(perm):
     return _dec
 
 
-@webapi_response_errors(INVALID_FORM_DATA)
 def webapi_request_fields(required={}, optional={}, allow_unknown=False):
     """Validates incoming fields for a request.
 
@@ -150,7 +193,9 @@ def webapi_request_fields(required={}, optional={}, allow_unknown=False):
             }
         })
     """
+    @webapi_decorator
     def _dec(view_func):
+        @webapi_response_errors(INVALID_FORM_DATA)
         def _validate(*args, **kwargs):
             request = _find_httprequest(args)
 
@@ -234,9 +279,6 @@ def webapi_request_fields(required={}, optional={}, allow_unknown=False):
 
             return view_func(*args, **new_kwargs)
 
-        _validate.__name__ = view_func.__name__
-        _validate.__doc__ = view_func.__doc__
-        _validate.__dict__.update(view_func.__dict__)
         _validate.required_fields = required.copy()
         _validate.optional_fields = optional.copy()
 
diff --git a/djblets/webapi/errors.py b/djblets/webapi/errors.py
index 062ac57f1db568b1effb79bf48aa8ad8f59def5b..bb22a7e2f941a8d8f02c749ea5b561ecb676be1f 100644
--- a/djblets/webapi/errors.py
+++ b/djblets/webapi/errors.py
@@ -35,6 +35,10 @@ class WebAPIError(object):
         self.http_status = http_status
         self.headers = headers
 
+    def __repr__(self):
+        return '<API Error %d, HTTP %d: %s>' % (self.code, self.http_status,
+                                                self.msg)
+
     def with_overrides(self, msg=None, headers=None):
         """Overrides the default message and/or headers for an error."""
         if headers is None:
diff --git a/djblets/webapi/tests.py b/djblets/webapi/tests.py
index e4aa9f8cadcda3fbfcc684c836202cd7b309c73e..b6396a5f8f294a4d901677a849560355da0990ab 100644
--- a/djblets/webapi/tests.py
+++ b/djblets/webapi/tests.py
@@ -22,13 +22,445 @@
 # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
+from django.contrib.auth.models import AnonymousUser, User
 from django.test.client import RequestFactory
 
 from djblets.util.testing import TestCase
-from djblets.webapi.errors import WebAPIError
+from djblets.webapi.decorators import (copy_webapi_decorator_data,
+                                       webapi_login_required,
+                                       webapi_permission_required,
+                                       webapi_request_fields,
+                                       webapi_response_errors)
+from djblets.webapi.errors import (DOES_NOT_EXIST, INVALID_FORM_DATA,
+                                   NOT_LOGGED_IN, PERMISSION_DENIED,
+                                   WebAPIError)
 from djblets.webapi.resources import WebAPIResource, unregister_resource
 
 
+class WebAPIDecoratorTests(TestCase):
+    def test_copy_webapi_decorator_data(self):
+        """Testing copy_webapi_decorator_data"""
+        def func1():
+            """Function 1"""
+
+        def func2():
+            """Function 2"""
+
+        func1.test1 = True
+        func1.response_errors = set(['a', 'b'])
+        func2.test2 = True
+        func2.response_errors = set(['c', 'd'])
+
+        result = copy_webapi_decorator_data(func1, func2)
+        self.assertEqual(result, func2)
+
+        self.assertTrue(hasattr(func2, 'test1'))
+        self.assertTrue(hasattr(func2, 'test2'))
+        self.assertTrue(hasattr(func2, 'response_errors'))
+        self.assertTrue(func2.test1)
+        self.assertTrue(func2.test2)
+        self.assertEqual(func2.response_errors, set(['a', 'b', 'c', 'd']))
+        self.assertEqual(func2.__doc__, 'Function 1')
+        self.assertEqual(func2.__name__, 'func1')
+
+        self.assertFalse(hasattr(func1, 'test2'))
+        self.assertEqual(func1.response_errors, set(['a', 'b']))
+
+    def test_webapi_response_errors_state(self):
+        """Testing @webapi_response_errors state"""
+        def orig_func():
+            """Function 1"""
+
+        func = webapi_response_errors(DOES_NOT_EXIST, NOT_LOGGED_IN)(orig_func)
+
+        self.assertFalse(hasattr(orig_func, 'response_errors'))
+
+        self.assertEqual(func.__name__, 'orig_func')
+        self.assertEqual(func.__doc__, 'Function 1')
+        self.assertTrue(hasattr(func, 'response_errors'))
+        self.assertEqual(func.response_errors,
+                         set([DOES_NOT_EXIST, NOT_LOGGED_IN]))
+
+    def test_webapi_response_errors_preserves_state(self):
+        """Testing @webapi_response_errors preserves decorator state"""
+        @webapi_response_errors(DOES_NOT_EXIST)
+        @webapi_response_errors(NOT_LOGGED_IN)
+        def func():
+            """Function 1"""
+
+        self.assertEqual(func.__name__, 'func')
+        self.assertEqual(func.__doc__, 'Function 1')
+        self.assertTrue(hasattr(func, 'response_errors'))
+        self.assertEqual(func.response_errors,
+                         set([DOES_NOT_EXIST, NOT_LOGGED_IN]))
+
+    def test_webapi_response_errors_call(self):
+        """Testing @webapi_response_errors calls original function"""
+        @webapi_response_errors(DOES_NOT_EXIST, NOT_LOGGED_IN)
+        def func():
+            func.seen = True
+
+        func()
+
+        self.assertTrue(hasattr(func, 'seen'))
+
+    def test_webapi_login_required_state(self):
+        """Testing @webapi_login_required state"""
+        def orig_func():
+            """Function 1"""
+
+        func = webapi_login_required(orig_func)
+
+        self.assertFalse(hasattr(orig_func, 'login_required'))
+        self.assertFalse(hasattr(orig_func, 'response_errors'))
+
+        self.assertEqual(func.__name__, 'orig_func')
+        self.assertEqual(func.__doc__, 'Function 1')
+        self.assertTrue(hasattr(func, 'response_errors'))
+        self.assertTrue(hasattr(func, 'login_required'))
+        self.assertTrue(func.login_required)
+        self.assertEqual(func.response_errors, set([NOT_LOGGED_IN]))
+
+    def test_webapi_login_required_preserves_state(self):
+        """Testing @webapi_login_required preserves decorator state"""
+        @webapi_response_errors(DOES_NOT_EXIST)
+        def orig_func():
+            """Function 1"""
+
+        func = webapi_login_required(orig_func)
+
+        self.assertFalse(hasattr(orig_func, 'login_required'))
+
+        self.assertEqual(func.__name__, 'orig_func')
+        self.assertEqual(func.__doc__, 'Function 1')
+        self.assertTrue(hasattr(func, 'response_errors'))
+        self.assertTrue(hasattr(func, 'login_required'))
+        self.assertTrue(func.login_required)
+        self.assertEqual(func.response_errors,
+                         set([DOES_NOT_EXIST, NOT_LOGGED_IN]))
+
+    def test_webapi_login_required_call_when_authenticated(self):
+        """Testing @webapi_login_required calls when authenticated"""
+        @webapi_login_required
+        def func(request):
+            func.seen = True
+
+        request = RequestFactory().request()
+        request.user = User()
+        result = func(request)
+
+        self.assertTrue(hasattr(func, 'seen'))
+        self.assertEqual(result, None)
+
+    def test_webapi_login_required_call_when_anonymous(self):
+        """Testing @webapi_login_required calls when anonymous"""
+        @webapi_login_required
+        def func(request):
+            func.seen = True
+
+        request = RequestFactory().request()
+        request.user = AnonymousUser()
+        result = func(request)
+
+        self.assertFalse(hasattr(func, 'seen'))
+        self.assertEqual(result, NOT_LOGGED_IN)
+
+    def test_webapi_permission_required_state(self):
+        """Testing @webapi_permission_required state"""
+        def orig_func():
+            """Function 1"""
+
+        func = webapi_permission_required('myperm')(orig_func)
+
+        self.assertFalse(hasattr(orig_func, 'response_errors'))
+
+        self.assertEqual(func.__name__, 'orig_func')
+        self.assertEqual(func.__doc__, 'Function 1')
+        self.assertTrue(hasattr(func, 'response_errors'))
+        self.assertEqual(func.response_errors,
+                         set([NOT_LOGGED_IN, PERMISSION_DENIED]))
+
+    def test_webapi_permission_required_preserves_state(self):
+        """Testing @webapi_permission_required preserves decorator state"""
+        @webapi_response_errors(DOES_NOT_EXIST)
+        def orig_func():
+            """Function 1"""
+
+        func = webapi_permission_required('myperm')(orig_func)
+
+        self.assertEqual(func.__name__, 'orig_func')
+        self.assertEqual(func.__doc__, 'Function 1')
+        self.assertTrue(hasattr(func, 'response_errors'))
+        self.assertEqual(func.response_errors,
+                         set([DOES_NOT_EXIST, NOT_LOGGED_IN,
+                              PERMISSION_DENIED]))
+
+    def test_webapi_permission_required_call_when_anonymous(self):
+        """Testing @webapi_permission_required calls when anonymous"""
+        @webapi_permission_required('foo')
+        def func(request):
+            func.seen = True
+
+        request = RequestFactory().request()
+        request.user = AnonymousUser()
+        result = func(request)
+
+        self.assertFalse(hasattr(func, 'seen'))
+        self.assertEqual(result, NOT_LOGGED_IN)
+
+    def test_webapi_permission_required_call_when_has_permission(self):
+        """Testing @webapi_permission_required calls when has permission"""
+        @webapi_permission_required('foo')
+        def func(request):
+            func.seen = True
+
+        request = RequestFactory().request()
+        request.user = User()
+        request.user.has_perm = lambda perm: True
+        result = func(request)
+
+        self.assertTrue(hasattr(func, 'seen'))
+        self.assertEqual(result, None)
+
+    def test_webapi_permission_required_call_when_no_permission(self):
+        """Testing @webapi_permission_required calls when no permission"""
+        @webapi_permission_required('foo')
+        def func(request):
+            func.seen = True
+
+        request = RequestFactory().request()
+        request.user = User()
+        request.user.has_perm = lambda perm: False
+        result = func(request)
+
+        self.assertFalse(hasattr(func, 'seen'))
+        self.assertEqual(result, PERMISSION_DENIED)
+
+    def test_webapi_request_fields_state(self):
+        """Testing @webapi_request_fields state"""
+        def orig_func():
+            """Function 1"""
+
+        required = {
+            'required_param': {
+                'type': bool,
+                'description': 'Required param'
+            },
+        }
+
+        optional = {
+            'optional_param': {
+                'type': bool,
+                'description': 'Optional param'
+            },
+        }
+
+        func = webapi_request_fields(required, optional)(orig_func)
+
+        self.assertFalse(hasattr(orig_func, 'required_fields'))
+        self.assertFalse(hasattr(orig_func, 'optional_fields'))
+        self.assertFalse(hasattr(orig_func, 'response_errors'))
+
+        self.assertEqual(func.__name__, 'orig_func')
+        self.assertEqual(func.__doc__, 'Function 1')
+        self.assertTrue(hasattr(func, 'response_errors'))
+        self.assertTrue(hasattr(func, 'required_fields'))
+        self.assertTrue(hasattr(func, 'optional_fields'))
+        self.assertEqual(func.required_fields, required)
+        self.assertEqual(func.optional_fields, optional)
+        self.assertEqual(func.response_errors, set([INVALID_FORM_DATA]))
+
+    def test_webapi_request_fields_preserves_state(self):
+        """Testing @webapi_request_fields preserves decorator state"""
+        required1 = {
+            'required1': {
+                'type': bool,
+                'description': 'Required param'
+            },
+        }
+
+        optional1 = {
+            'optional1': {
+                'type': bool,
+                'description': 'Optional param'
+            },
+        }
+
+        @webapi_request_fields(required1, optional1)
+        @webapi_response_errors(DOES_NOT_EXIST)
+        def orig_func():
+            """Function 1"""
+
+        required2 = {
+            'required2': {
+                'type': bool,
+                'description': 'Required param'
+            },
+        }
+
+        optional2 = {
+            'optional2': {
+                'type': bool,
+                'description': 'Optional param'
+            },
+        }
+
+        func = webapi_request_fields(required2, optional2)(orig_func)
+
+        expected_required = required1.copy()
+        expected_required.update(required2)
+        expected_optional = optional1.copy()
+        expected_optional.update(optional2)
+
+        self.assertTrue(hasattr(orig_func, 'required_fields'))
+        self.assertTrue(hasattr(orig_func, 'optional_fields'))
+        self.assertTrue(hasattr(orig_func, 'response_errors'))
+
+        self.assertEqual(func.__name__, 'orig_func')
+        self.assertEqual(func.__doc__, 'Function 1')
+        self.assertTrue(hasattr(func, 'response_errors'))
+        self.assertTrue(hasattr(func, 'required_fields'))
+        self.assertTrue(hasattr(func, 'optional_fields'))
+        self.assertEqual(func.required_fields, expected_required)
+        self.assertEqual(func.optional_fields, expected_optional)
+        self.assertEqual(func.response_errors,
+                         set([DOES_NOT_EXIST, INVALID_FORM_DATA]))
+
+    def test_webapi_request_fields_call_normalizes_params(self):
+        """Testing @webapi_request_fields normalizes params to function"""
+        @webapi_request_fields(
+            required={
+                'required_param': {
+                    'type': int,
+                }
+            },
+            optional={
+                'optional_param': {
+                    'type': bool,
+                }
+            },
+        )
+        def func(request, required_param=None, optional_param=None,
+                 extra_fields={}):
+            func.seen = True
+            self.assertTrue(isinstance(required_param, int))
+            self.assertTrue(isinstance(optional_param, bool))
+            self.assertEqual(required_param, 42)
+            self.assertTrue(optional_param)
+            self.assertFalse(extra_fields)
+
+        result = func(RequestFactory().get(
+            path='/',
+            data={
+                'required_param': '42',
+                'optional_param': '1',
+            }
+        ))
+
+        self.assertTrue(hasattr(func, 'seen'))
+
+    def test_webapi_request_fields_call_with_unexpected_arg(self):
+        """Testing @webapi_request_fields with unexpected argument"""
+        @webapi_request_fields(
+            required={
+                'required_param': {
+                    'type': int,
+                }
+            },
+        )
+        def func(request, required_param=None, extra_fields={}):
+            func.seen = True
+
+        result = func(RequestFactory().get(
+            path='/',
+            data={
+                'required_param': '42',
+                'optional_param': '1',
+            }
+        ))
+
+        self.assertFalse(hasattr(func, 'seen'))
+        self.assertEqual(result[0], INVALID_FORM_DATA)
+        self.assertTrue('fields' in result[1])
+        self.assertTrue('optional_param' in result[1]['fields'])
+
+    def test_webapi_request_fields_call_with_allow_unknown(self):
+        """Testing @webapi_request_fields with allow_unknown=True"""
+        @webapi_request_fields(
+            required={
+                'required_param': {
+                    'type': int,
+                }
+            },
+            allow_unknown=True
+        )
+        def func(request, required_param=None, extra_fields={}):
+            func.seen = True
+            self.assertEqual(required_param, 42)
+            self.assertTrue('optional_param' in extra_fields)
+            self.assertEqual(extra_fields['optional_param'], '1')
+
+        result = func(RequestFactory().get(
+            path='/',
+            data={
+                'required_param': '42',
+                'optional_param': '1',
+            }
+        ))
+
+        self.assertTrue(hasattr(func, 'seen'))
+        self.assertEqual(result, None)
+
+    def test_webapi_request_fields_call_filter_special_params(self):
+        """Testing @webapi_request_fields filters special params"""
+        @webapi_request_fields(
+            required={
+                'required_param': {
+                    'type': int,
+                }
+            },
+        )
+        def func(request, required_param=None, extra_fields={}):
+            func.seen = True
+            self.assertTrue(isinstance(required_param, int))
+            self.assertEqual(required_param, 42)
+            self.assertFalse(extra_fields)
+
+        result = func(RequestFactory().get(
+            path='/',
+            data={
+                'required_param': '42',
+                'api_format': 'json',
+            }
+        ))
+
+        self.assertTrue(hasattr(func, 'seen'))
+
+    def test_webapi_request_fields_call_validation_int(self):
+        """Testing @webapi_request_fields with int parameter validation"""
+        @webapi_request_fields(
+            required={
+                'myint': {
+                    'type': int,
+                }
+            }
+        )
+        def func(request, myint=False, extra_fields={}):
+            func.seen = True
+
+        result = func(RequestFactory().get(
+            path='/',
+            data={
+                'myint': 'abc',
+            }
+        ))
+
+        self.assertFalse(hasattr(func, 'seen'))
+        self.assertEqual(result[0], INVALID_FORM_DATA)
+        self.assertTrue('fields' in result[1])
+        self.assertTrue('myint' in result[1]['fields'])
+
+
 class WebAPIErrorTests(TestCase):
     def test_with_message(self):
         """Testing WebAPIError.with_message"""
diff --git a/tests/runtests.py b/tests/runtests.py
index 494dcd1dbdf2af362a0f5ec08cf3dd3b34338413..eb65bc44a0b609c85fb774e3fcc2550b1b086550 100755
--- a/tests/runtests.py
+++ b/tests/runtests.py
@@ -35,7 +35,7 @@ def run_tests(verbosity=1, interactive=False):
         nose_argv.append('--ignore-files=contextmanagers.py')
 
     if len(sys.argv) > 2:
-        node_argv += sys.argv[2:]
+        nose_argv += sys.argv[2:]
 
     nose.main(argv=nose_argv)
 
