Repository: Kinto/kinto

Issue:
Crash batch list of request has trailing comma
```
echo '{
    "requests": [
        {"path": "/buckets/default/collections/test1/records"},
        {"path": "/buckets/default/collections/test2/records"},
    ]
}' | http POST :8888/v1/batch --auth a:a
```


```
2017-01-18 16:45:58,968 ERROR [kinto.core.views.errors][waitress] "POST  /v1/batch" ? (? ms) '_null' object has no attribute 'get' agent=HTTPie/0.9.2 authn_type=None errno=None exception=Traceback (most recent call last):
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/pyramid/tweens.py", line 22, in excview_tween
    response = handler(request)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/pyramid_tm/__init__.py", line 119, in tm_tween
    reraise(*exc_info)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/pyramid_tm/__init__.py", line 98, in tm_tween
    response = handler(request)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/pyramid/router.py", line 158, in handle_request
    view_name
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/pyramid/view.py", line 547, in _call_view
    response = view_callable(context, request)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/pyramid/config/views.py", line 182,
, in __call__
    return view(context, request)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/pyramid/viewderivers.py", line 393, in attr_view
    return view(context, request)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/pyramid/viewderivers.py", line 371, in predicate_wrapper
    return view(context, request)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/pyramid/viewderivers.py", line 442, in rendered_view
    result = view(context, request)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/pyramid/viewderivers.py", line 147, in _requestonly_view
    response = view(request)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/cornice/service.py", line 484, in wrapper
    validator(request, **args)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/cornice/validators/_colander.py", line 73, in validator
    deserialized = schema.deserialize(cstruct)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/colander/__init__.py", line 2058, in deserialize
    appstruct = self.typ.deserialize(self, cstruct)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/colander/__init__.py", line 719, in deserialize
    return self._impl(node, cstruct, callback)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/colander/__init__.py", line 678, in _impl
    sub_result = callback(subnode, subval)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/colander/__init__.py", line 717, in callback
    return subnode.deserialize(subcstruct)
  File "/home/mathieu/Code/Mozilla/kinto/kinto/core/views/batch.py", line 57, in deserialize
    defaults = cstruct.get('defaults')
AttributeError: '_null' object has no attribute 'get' lang=None uid=None
2017-01-18 16:45:58,969 INFO  [kinto.core.initialization][waitress] "POST  /v1/batch" 500 (5 ms) request.summary agent=HTTPie/0.9.2 authn_type=None errno=999 lang=None time=2017-01-18T16:45:58 uid=None

```
Crash batch list of request has trailing comma
```
echo '{
    "requests": [
        {"path": "/buckets/default/collections/test1/records"},
        {"path": "/buckets/default/collections/test2/records"},
    ]
}' | http POST :8888/v1/batch --auth a:a
```


```
2017-01-18 16:45:58,968 ERROR [kinto.core.views.errors][waitress] "POST  /v1/batch" ? (? ms) '_null' object has no attribute 'get' agent=HTTPie/0.9.2 authn_type=None errno=None exception=Traceback (most recent call last):
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/pyramid/tweens.py", line 22, in excview_tween
    response = handler(request)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/pyramid_tm/__init__.py", line 119, in tm_tween
    reraise(*exc_info)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/pyramid_tm/__init__.py", line 98, in tm_tween
    response = handler(request)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/pyramid/router.py", line 158, in handle_request
    view_name
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/pyramid/view.py", line 547, in _call_view
    response = view_callable(context, request)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/pyramid/config/views.py", line 182,
, in __call__
    return view(context, request)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/pyramid/viewderivers.py", line 393, in attr_view
    return view(context, request)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/pyramid/viewderivers.py", line 371, in predicate_wrapper
    return view(context, request)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/pyramid/viewderivers.py", line 442, in rendered_view
    result = view(context, request)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/pyramid/viewderivers.py", line 147, in _requestonly_view
    response = view(request)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/cornice/service.py", line 484, in wrapper
    validator(request, **args)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/cornice/validators/_colander.py", line 73, in validator
    deserialized = schema.deserialize(cstruct)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/colander/__init__.py", line 2058, in deserialize
    appstruct = self.typ.deserialize(self, cstruct)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/colander/__init__.py", line 719, in deserialize
    return self._impl(node, cstruct, callback)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/colander/__init__.py", line 678, in _impl
    sub_result = callback(subnode, subval)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/local/lib/python2.7/site-packages/colander/__init__.py", line 717, in callback
    return subnode.deserialize(subcstruct)
  File "/home/mathieu/Code/Mozilla/kinto/kinto/core/views/batch.py", line 57, in deserialize
    defaults = cstruct.get('defaults')
AttributeError: '_null' object has no attribute 'get' lang=None uid=None
2017-01-18 16:45:58,969 INFO  [kinto.core.initialization][waitress] "POST  /v1/batch" 500 (5 ms) request.summary agent=HTTPie/0.9.2 authn_type=None errno=999 lang=None time=2017-01-18T16:45:58 uid=None

```


Discussion:

Resolving patch:
diff --git a/kinto/core/views/batch.py b/kinto/core/views/batch.py
--- a/kinto/core/views/batch.py
+++ b/kinto/core/views/batch.py
@@ -54,12 +54,13 @@ def __init__(self, *args, **kwargs):
     def deserialize(self, cstruct=colander.null):
         """Preprocess received data to carefully merge defaults.
         """
-        defaults = cstruct.get('defaults')
-        requests = cstruct.get('requests')
-        if isinstance(defaults, dict) and isinstance(requests, list):
-            for request in requests:
-                if isinstance(request, dict):
-                    merge_dicts(request, defaults)
+        if cstruct is not colander.null:
+            defaults = cstruct.get('defaults')
+            requests = cstruct.get('requests')
+            if isinstance(defaults, dict) and isinstance(requests, list):
+                for request in requests:
+                    if isinstance(request, dict):
+                        merge_dicts(request, defaults)
         return super(BatchPayloadSchema, self).deserialize(cstruct)
 
 


Test patch:
diff --git a/tests/core/test_views_batch.py b/tests/core/test_views_batch.py
--- a/tests/core/test_views_batch.py
+++ b/tests/core/test_views_batch.py
@@ -100,6 +100,12 @@ def test_batch_cannot_be_recursive(self):
         resp = self.app.post_json('/batch', body, status=400)
         self.assertIn('Recursive', resp.json['message'])
 
+    def test_batch_validates_json(self):
+        body = """{"requests": [{"path": "/v0/"},]}"""
+        resp = self.app.post('/batch', body, status=400,
+                             headers={'Content-Type': 'application/json'})
+        self.assertIn('Invalid JSON', resp.json['message'])
+
     def test_responses_are_resolved_with_api_with_prefix(self):
         request = {'path': '/'}
         body = {'requests': [request]}