Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

Ticket #733: 400_404_arg_diff.txt

Line 
1 Index: test/test_objectmapping.py
2 ===================================================================
3 --- test/test_objectmapping.py  (revision 2029)
4 +++ test/test_objectmapping.py  (working copy)
5 @@ -268,11 +268,10 @@
6          self.getPage("/dir1/dir2/5/3/sir")
7          self.assertBody("default for dir1, param is:('dir2', '5', '3', 'sir')")
8          
9 -        # test that extra positional args raises an error.
10 -        # 500 for now, maybe 404 in the future.
11 +        # test that extra positional args raises an 404 Not Found
12          # See http://www.cherrypy.org/ticket/733.
13          self.getPage("/dir1/dir2/script_name/extra/stuff")
14 -        self.assertStatus(500)
15 +        self.assertStatus(404)
16      
17      def testExpose(self):
18          # Test the cherrypy.expose function/decorator
19 Index: test/test_core.py
20 ===================================================================
21 --- test/test_core.py   (revision 2029)
22 +++ test/test_core.py   (working copy)
23 @@ -98,7 +98,41 @@
24          def default(self, *args, **kwargs):
25              return "args: %s kwargs: %s" % (args, kwargs)
26  
27 +    class ParamErrors(Test):
28  
29 +        def one_positional(self, param1):
30 +            return "data"
31 +        one_positional.exposed = True
32 +
33 +        def one_positional_args(self, param1, *args):
34 +            return "data"
35 +        one_positional_args.exposed = True
36 +
37 +        def one_positional_args_kwargs(self, param1, *args, **kwargs):
38 +            return "data"
39 +        one_positional_args_kwargs.exposed = True
40 +
41 +        def one_positional_kwargs(self, param1, **kwargs):
42 +            return "data"
43 +        one_positional_kwargs.exposed = True
44 +
45 +        def no_positional(self):
46 +            return "data"
47 +        no_positional.exposed = True
48 +
49 +        def no_positional_args(self, *args):
50 +            return "data"
51 +        no_positional_args.exposed = True
52 +
53 +        def no_positional_args_kwargs(self, *args, **kwargs):
54 +            return "data"
55 +        no_positional_args_kwargs.exposed = True
56 +
57 +        def no_positional_kwargs(self, **kwargs):
58 +            return "data"
59 +        no_positional_kwargs.exposed = True
60 +
61 +
62      class Status(Test):
63          
64          def index(self):
65 @@ -444,15 +478,6 @@
66          self.getPage("/params/?thing=a&thing=b&thing=c")
67          self.assertBody("['a', 'b', 'c']")
68          
69 -        # Test friendly error message when given params are not accepted.
70 -        ignore = helper.webtest.ignored_exceptions
71 -        ignore.append(TypeError)
72 -        try:
73 -            self.getPage("/params/?notathing=meeting")
74 -            self.assertInBody("index() got an unexpected keyword argument 'notathing'")
75 -        finally:
76 -            ignore.pop()
77 -       
78          # Test "% HEX HEX"-encoded URL, param keys, and values
79          self.getPage("/params/%d4%20%e3/cheese?Gruy%E8re=Bulgn%e9ville")
80          self.assertBody(r"args: ('\xd4 \xe3', 'cheese') "
81 @@ -466,7 +491,82 @@
82          # Test coordinates sent by <img ismap>
83          self.getPage("/params/ismap?223,114")
84          self.assertBody("Coordinates: 223, 114")
85 -   
86 +
87 +    def testParamErrors(self):
88 +
89 +        # test that all of the handlers work when given
90 +        # the correct parameters in order to ensure that the
91 +        # errors below aren't coming from some other source.
92 +        for uri in (
93 +                '/paramerrors/one_positional?param1=foo',
94 +                '/paramerrors/one_positional_args?param1=foo',
95 +                '/paramerrors/one_positional_args/foo',
96 +                '/paramerrors/one_positional_args/foo/bar/baz',
97 +                '/paramerrors/one_positional_args_kwargs?param1=foo&param2=bar',
98 +                '/paramerrors/one_positional_args_kwargs/foo?param2=bar&param3=baz',
99 +                '/paramerrors/one_positional_args_kwargs/foo/bar/baz?param2=bar&param3=baz',
100 +                '/paramerrors/one_positional_kwargs?param1=foo&param2=bar&param3=baz',
101 +                '/paramerrors/one_positional_kwargs/foo?param4=foo&param2=bar&param3=baz',
102 +                '/paramerrors/no_positional',
103 +                '/paramerrors/no_positional_args/foo',
104 +                '/paramerrors/no_positional_args/foo/bar/baz',
105 +                '/paramerrors/no_positional_args_kwargs?param1=foo&param2=bar',
106 +                '/paramerrors/no_positional_args_kwargs/foo?param2=bar',
107 +                '/paramerrors/no_positional_args_kwargs/foo/bar/baz?param2=bar&param3=baz',
108 +                '/paramerrors/no_positional_kwargs?param1=foo&param2=bar',
109 +            ):
110 +            self.getPage(uri)
111 +            self.assertStatus(200)
112 +
113 +        # query string parameters are part of the URI, so if they are wrong
114 +        # for a particular handler, the status MUST be a 404.
115 +        for uri in (
116 +                '/paramerrors/one_positional',
117 +                '/paramerrors/one_positional?foo=foo',
118 +                '/paramerrors/one_positional/foo/bar/baz',
119 +                '/paramerrors/one_positional/foo?param1=foo',
120 +                '/paramerrors/one_positional/foo?param1=foo&param2=foo',
121 +                '/paramerrors/one_positional_args/foo?param1=foo&param2=foo',
122 +                '/paramerrors/one_positional_args/foo/bar/baz?param2=foo',
123 +                '/paramerrors/one_positional_args_kwargs/foo/bar/baz?param1=bar&param3=baz',
124 +                '/paramerrors/one_positional_kwargs/foo?param1=foo&param2=bar&param3=baz',
125 +                '/paramerrors/no_positional/boo',
126 +                '/paramerrors/no_positional?param1=foo',
127 +                '/paramerrors/no_positional_args/boo?param1=foo',
128 +                '/paramerrors/no_positional_kwargs/boo?param1=foo',
129 +            ):
130 +            self.getPage(uri)
131 +            self.assertStatus(404)
132 +
133 +        # if body parameters are wrong, a 400 must be returned.
134 +        for uri, body in (
135 +                ('/paramerrors/one_positional/foo', 'param1=foo',),
136 +                ('/paramerrors/one_positional/foo', 'param1=foo&param2=foo',),
137 +                ('/paramerrors/one_positional_args/foo', 'param1=foo&param2=foo',),
138 +                ('/paramerrors/one_positional_args/foo/bar/baz', 'param2=foo',),
139 +                ('/paramerrors/one_positional_args_kwargs/foo/bar/baz', 'param1=bar&param3=baz',),
140 +                ('/paramerrors/one_positional_kwargs/foo', 'param1=foo&param2=bar&param3=baz',),
141 +                ('/paramerrors/no_positional', 'param1=foo',),
142 +                ('/paramerrors/no_positional_args/boo', 'param1=foo',),
143 +            ):
144 +            self.getPage(uri, method='POST', body=body)
145 +            self.assertStatus(400)
146 +
147 +
148 +        # even if body parameters are wrong, if we get the uri wrong, then
149 +        # it's a 404
150 +        for uri, body in (
151 +                ('/paramerrors/one_positional?param2=foo', 'param1=foo',),
152 +                ('/paramerrors/one_positional/foo/bar', 'param2=foo',),
153 +                ('/paramerrors/one_positional_args/foo/bar?param2=foo', 'param3=foo',),
154 +                ('/paramerrors/one_positional_kwargs/foo/bar', 'param2=bar&param3=baz',),
155 +                ('/paramerrors/no_positional?param1=foo', 'param2=foo',),
156 +                ('/paramerrors/no_positional_args/boo?param2=foo', 'param1=foo',),
157 +            ):
158 +            self.getPage(uri, method='POST', body=body)
159 +            self.assertStatus(404)
160 +
161 +
162      def testStatus(self):
163          self.getPage("/status/")
164          self.assertBody('normal')
165 Index: _cpdispatch.py
166 ===================================================================
167 --- _cpdispatch.py      (revision 2029)
168 +++ _cpdispatch.py      (working copy)
169 @@ -21,9 +21,129 @@
170          self.kwargs = kwargs
171      
172      def __call__(self):
173 -        return self.callable(*self.args, **self.kwargs)
174 +        try:
175 +            return self.callable(*self.args, **self.kwargs)
176 +        except TypeError, x:
177 +            test_callable_spec(self.callable, self.args, self.kwargs)
178 +            raise
179  
180 +def test_callable_spec(callable, callable_args, callable_kwargs):
181 +    """
182 +    Inspect callable and test to see if the given args are suitable for it.
183  
184 +    When an error occurs during the handler's invoking stage there are 2
185 +    erroneous cases:
186 +    1.  Too many parameters passed to a function which doesn't define
187 +        one of *args or **kwargs.
188 +    2.  Too little parameters are passed to the function.
189 +
190 +    There are 3 sources of parameters to a cherrypy handler.
191 +    1.  query string parameters are passed as keyword parameters to the handler.
192 +    2.  body parameters are also passed as keyword parameters.
193 +    3.  when partial matching occurs, the final path atoms are passed as
194 +        positional args.
195 +    Both the query string and path atoms are part of the URI.  If they are
196 +    incorrect, then a 404 Not Found should be raised. Conversely the body
197 +    parameters are part of the request; if they are invalid a 400 Bad Request.
198 +    """
199 +    (args, varargs, varkw, defaults) = inspect.getargspec(callable)
200 +
201 +    if args and args[0] == 'self':
202 +        args = args[1:]
203 +
204 +    arg_usage = dict([(arg, 0,) for arg in args])
205 +    vararg_usage = 0
206 +    varkw_usage = 0
207 +    extra_kwargs = set()
208 +
209 +    for i, value in enumerate(callable_args):
210 +        try:
211 +            arg_usage[args[i]] += 1
212 +        except IndexError:
213 +            vararg_usage += 1
214 +
215 +    for key in callable_kwargs.keys():
216 +        try:
217 +            arg_usage[key] += 1
218 +        except KeyError:
219 +            varkw_usage += 1
220 +            extra_kwargs.add(key)
221 +
222 +    for i, val in enumerate(defaults or []):
223 +        # Defaults take effect only when the arg hasn't been used yet.
224 +        if arg_usage[args[i]] == 0:
225 +            arg_usage[args[i]] += 1
226 +
227 +    missing_args = []
228 +    multiple_args = []
229 +    for key, usage in arg_usage.iteritems():
230 +        if usage == 0:
231 +            missing_args.append(key)
232 +        elif usage > 1:
233 +            multiple_args.append(key)
234 +
235 +    if missing_args:
236 +        # In the case where the method allows body arguments
237 +        # there are 3 potential errors:
238 +        # 1. not enough query string parameters -> 404
239 +        # 2. not enough body parameters -> 400
240 +        # 3. not enough path parts (partial matches) -> 404
241 +        #
242 +        # We can't actually tell which case it is,
243 +        # so I'm raising a 404 because that covers 2/3 of the
244 +        # possibilities
245 +        #
246 +        # In the case where the method does not allow body
247 +        # arguments it's definitely a 404.
248 +        raise cherrypy.HTTPError(404,
249 +                message="Missing parameters: %s" % ",".join(missing_args))
250 +
251 +    # the extra positional arguments come from the path - 404 Not Found
252 +    if not varargs and vararg_usage > 0:
253 +        raise cherrypy.HTTPError(404)
254 +
255 +    body_params = cherrypy.request.body_params or {}
256 +    body_params = set(body_params.keys())
257 +    qs_params = set(callable_kwargs.keys()) - body_params
258 +
259 +    if multiple_args:
260 +
261 +        if qs_params.intersection(set(multiple_args)):
262 +            # If any of the multiple parameters came from the query string then
263 +            # it's a 404 Not Found
264 +            error = 404
265 +        else:
266 +            # Otherwise it's a 400 Bad Request
267 +            error = 400
268 +
269 +        raise cherrypy.HTTPError(error,
270 +                message="Multiple values for parameters: "\
271 +                        "%s" % ",".join(multiple_args))
272 +
273 +    if not varkw and varkw_usage > 0:
274 +
275 +        # If there were extra query string parameters, it's a 404 Not Found
276 +        extra_qs_params = set(qs_params).intersection(extra_kwargs)
277 +        if extra_qs_params:
278 +            raise cherrypy.HTTPError(404,
279 +                message="Unexpected query string "\
280 +                        "parameters: %s" % ", ".join(extra_qs_params))
281 +
282 +        # If there were any extra body parameters, it's a 400 Not Found
283 +        extra_body_params = set(body_params).intersection(extra_kwargs)
284 +        if extra_body_params:
285 +            raise cherrypy.HTTPError(400,
286 +                message="Unexpected body parameters: "\
287 +                        "%s" % ", ".join(extra_body_params))
288 +
289 +
290 +try:
291 +    import inspect
292 +except ImportError:
293 +    test_callable_spec = lambda callable, args, kwargs: None
294 +
295 +
296 +
297  class LateParamPageHandler(PageHandler):
298      """When passing cherrypy.request.params to the page handler, we do not
299      want to capture that dict too early; we want to give tools like the

Hosted by WebFaction

Log in as guest/cpguest to create tickets