prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Batch requests accept additional attributes
  We should probably forbid them.
  
  ```
  > echo '{"requests": [], "foo": {}}' | http post http://localhost:8888/v1/batch 
  
  HTTP/1.1 200 OK
  Access-Control-Expose-Headers: Retry-After, Alert, Content-Length, Backoff
  Content-Length: 16
  Content-Type: application/json
  Date: Thu, 16 Mar 2017 23:01:01 GMT
  Server: waitress
  X-Content-Type-Options: nosniff
  
  {
      "responses": []
  }
  ```
  
  Edit: I just notice we currently test for batch ignoring additional attributes. Is this a feature?
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `kinto/core/views/batch.py`
  Content:
  ```
  1 import logging
  2 
  3 import colander
  4 from cornice.validators import colander_validator
  5 from pyramid import httpexceptions
  6 from pyramid.security import NO_PERMISSION_REQUIRED
  7 
  8 from kinto.core import errors
  9 from kinto.core import Service
  10 from kinto.core.errors import ErrorSchema
  11 from kinto.core.utils import merge_dicts, build_request, build_response
  12 
  13 
  14 subrequest_logger = logging.getLogger("subrequest.summary")
  15 
  16 valid_http_method = colander.OneOf(('GET', 'HEAD', 'DELETE', 'TRACE',
  17                                     'POST', 'PUT', 'PATCH'))
  18 
  19 
  20 def string_values(node, cstruct):
  21     """Validate that a ``colander.Mapping`` only has strings in its values.
  22 
  23     .. warning::
  24 
  25         Should be associated to a ``colander.Mapping`` schema node.
  26     """
  27     are_strings = [isinstance(v, str) for v in cstruct.values()]
  28     if not all(are_strings):
  29         error_msg = '{} contains non string value'.format(cstruct)
  30         raise colander.Invalid(node, error_msg)
  31 
  32 
  33 class BatchRequestSchema(colander.MappingSchema):
  34     method = colander.SchemaNode(colander.String(),
  35                                  validator=valid_http_method,
  36                                  missing=colander.drop)
  37     path = colander.SchemaNode(colander.String(),
  38                                validator=colander.Regex('^/'))
  39     headers = colander.SchemaNode(colander.Mapping(unknown='preserve'),
  40                                   validator=string_values,
  41                                   missing=colander.drop)
  42     body = colander.SchemaNode(colander.Mapping(unknown='preserve'),
  43                                missing=colander.drop)
  44 
  45 
  46 class BatchPayloadSchema(colander.MappingSchema):
  47     defaults = BatchRequestSchema(missing=colander.drop).clone()
  48     requests = colander.SchemaNode(colander.Sequence(),
  49                                    BatchRequestSchema())
  50 
  51     def __init__(self, *args, **kwargs):
  52         super().__init__(*args, **kwargs)
  53         # On defaults, path is not mandatory.
  54         self.get('defaults').get('path').missing = colander.drop
  55 
  56     def deserialize(self, cstruct=colander.null):
  57         """Preprocess received data to carefully merge defaults.
  58         """
  59         if cstruct is not colander.null:
  60             defaults = cstruct.get('defaults')
  61             requests = cstruct.get('requests')
  62             if isinstance(defaults, dict) and isinstance(requests, list):
  63                 for request in requests:
  64                     if isinstance(request, dict):
  65                         merge_dicts(request, defaults)
  66         return super().deserialize(cstruct)
  67 
  68 
  69 class BatchRequest(colander.MappingSchema):
  70     body = BatchPayloadSchema()
  71 
  72 
  73 class BatchResponseSchema(colander.MappingSchema):
  74     status = colander.SchemaNode(colander.Integer())
  75     path = colander.SchemaNode(colander.String())
  76     headers = colander.SchemaNode(colander.Mapping(unknown='preserve'),
  77                                   validator=string_values,
  78                                   missing=colander.drop)
  79     body = colander.SchemaNode(colander.Mapping(unknown='preserve'),
  80                                missing=colander.drop)
  81 
  82 
  83 class BatchResponseBodySchema(colander.MappingSchema):
  84     responses = colander.SequenceSchema(BatchResponseSchema(missing=colander.drop))
  85 
  86 
  87 class BatchResponse(colander.MappingSchema):
  88     body = BatchResponseBodySchema()
  89 
  90 
  91 class ErrorResponseSchema(colander.MappingSchema):
  92     body = ErrorSchema()
  93 
  94 
  95 batch_responses = {
  96     '200': BatchResponse(description='Return a list of operation responses.'),
  97     '400': ErrorResponseSchema(description='The request was badly formatted.'),
  98     'default': ErrorResponseSchema(description='an unknown error occurred.')
  99 }
  100 
  101 batch = Service(name="batch", path='/batch',
  102                 description="Batch operations")
  103 
  104 
  105 @batch.post(schema=BatchRequest,
  106             validators=(colander_validator,),
  107             permission=NO_PERMISSION_REQUIRED,
  108             tags=['Batch'], operation_id='batch',
  109             response_schemas=batch_responses)
  110 def post_batch(request):
  111     requests = request.validated['body']['requests']
  112 
  113     request.log_context(batch_size=len(requests))
  114 
  115     limit = request.registry.settings['batch_max_requests']
  116     if limit and len(requests) > int(limit):
  117         error_msg = 'Number of requests is limited to {}'.format(limit)
  118         request.errors.add('body', 'requests', error_msg)
  119         return
  120 
  121     if any([batch.path in req['path'] for req in requests]):
  122         error_msg = 'Recursive call on {} endpoint is forbidden.'.format(batch.path)
  123         request.errors.add('body', 'requests', error_msg)
  124         return
  125 
  126     responses = []
  127 
  128     for subrequest_spec in requests:
  129         subrequest = build_request(request, subrequest_spec)
  130 
  131         log_context = {'path': subrequest.path,
  132                        'method': subrequest.method,
  133                        **request.log_context()}
  134         try:
  135             # Invoke subrequest without individual transaction.
  136             resp, subrequest = request.follow_subrequest(subrequest,
  137                                                          use_tweens=False)
  138         except httpexceptions.HTTPException as e:
  139             if e.content_type == 'application/json':
  140                 resp = e
  141             else:
  142                 # JSONify raw Pyramid errors.
  143                 resp = errors.http_error(e)
  144 
  145         subrequest_logger.info('subrequest.summary', extra=log_context)
  146 
  147         dict_resp = build_response(resp, subrequest)
  148         responses.append(dict_resp)
  149 
  150     return {
  151         'responses': responses
  152     }
  153
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: 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
  @@ -42,12 +42,20 @@
       body = colander.SchemaNode(colander.Mapping(unknown='preserve'),
                                  missing=colander.drop)
   
  +    @staticmethod
  +    def schema_type():
  +        return colander.Mapping(unknown='raise')
  +
   
   class BatchPayloadSchema(colander.MappingSchema):
       defaults = BatchRequestSchema(missing=colander.drop).clone()
       requests = colander.SchemaNode(colander.Sequence(),
                                      BatchRequestSchema())
   
  +    @staticmethod
  +    def schema_type():
  +        return colander.Mapping(unknown='raise')
  +
       def __init__(self, *args, **kwargs):
           super().__init__(*args, **kwargs)
           # On defaults, path is not mandatory.
  
verification_info: {"golden_diff": "diff --git a/kinto/core/views/batch.py b/kinto/core/views/batch.py\n--- a/kinto/core/views/batch.py\n+++ b/kinto/core/views/batch.py\n@@ -42,12 +42,20 @@\n     body = colander.SchemaNode(colander.Mapping(unknown='preserve'),\n                                missing=colander.drop)\n \n+    @staticmethod\n+    def schema_type():\n+        return colander.Mapping(unknown='raise')\n+\n \n class BatchPayloadSchema(colander.MappingSchema):\n     defaults = BatchRequestSchema(missing=colander.drop).clone()\n     requests = colander.SchemaNode(colander.Sequence(),\n                                    BatchRequestSchema())\n \n+    @staticmethod\n+    def schema_type():\n+        return colander.Mapping(unknown='raise')\n+\n     def __init__(self, *args, **kwargs):\n         super().__init__(*args, **kwargs)\n         # On defaults, path is not mandatory.\n", "issue": "Batch requests accept additional attributes\nWe should probably forbid them.\r\n\r\n```\r\n> echo '{\"requests\": [], \"foo\": {}}' | http post http://localhost:8888/v1/batch \r\n\r\nHTTP/1.1 200 OK\r\nAccess-Control-Expose-Headers: Retry-After, Alert, Content-Length, Backoff\r\nContent-Length: 16\r\nContent-Type: application/json\r\nDate: Thu, 16 Mar 2017 23:01:01 GMT\r\nServer: waitress\r\nX-Content-Type-Options: nosniff\r\n\r\n{\r\n    \"responses\": []\r\n}\r\n```\r\n\r\nEdit: I just notice we currently test for batch ignoring additional attributes. Is this a feature?\n", "before_files": [{"content": "import logging\n\nimport colander\nfrom cornice.validators import colander_validator\nfrom pyramid import httpexceptions\nfrom pyramid.security import NO_PERMISSION_REQUIRED\n\nfrom kinto.core import errors\nfrom kinto.core import Service\nfrom kinto.core.errors import ErrorSchema\nfrom kinto.core.utils import merge_dicts, build_request, build_response\n\n\nsubrequest_logger = logging.getLogger(\"subrequest.summary\")\n\nvalid_http_method = colander.OneOf(('GET', 'HEAD', 'DELETE', 'TRACE',\n                                    'POST', 'PUT', 'PATCH'))\n\n\ndef string_values(node, cstruct):\n    \"\"\"Validate that a ``colander.Mapping`` only has strings in its values.\n\n    .. warning::\n\n        Should be associated to a ``colander.Mapping`` schema node.\n    \"\"\"\n    are_strings = [isinstance(v, str) for v in cstruct.values()]\n    if not all(are_strings):\n        error_msg = '{} contains non string value'.format(cstruct)\n        raise colander.Invalid(node, error_msg)\n\n\nclass BatchRequestSchema(colander.MappingSchema):\n    method = colander.SchemaNode(colander.String(),\n                                 validator=valid_http_method,\n                                 missing=colander.drop)\n    path = colander.SchemaNode(colander.String(),\n                               validator=colander.Regex('^/'))\n    headers = colander.SchemaNode(colander.Mapping(unknown='preserve'),\n                                  validator=string_values,\n                                  missing=colander.drop)\n    body = colander.SchemaNode(colander.Mapping(unknown='preserve'),\n                               missing=colander.drop)\n\n\nclass BatchPayloadSchema(colander.MappingSchema):\n    defaults = BatchRequestSchema(missing=colander.drop).clone()\n    requests = colander.SchemaNode(colander.Sequence(),\n                                   BatchRequestSchema())\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        # On defaults, path is not mandatory.\n        self.get('defaults').get('path').missing = colander.drop\n\n    def deserialize(self, cstruct=colander.null):\n        \"\"\"Preprocess received data to carefully merge defaults.\n        \"\"\"\n        if cstruct is not colander.null:\n            defaults = cstruct.get('defaults')\n            requests = cstruct.get('requests')\n            if isinstance(defaults, dict) and isinstance(requests, list):\n                for request in requests:\n                    if isinstance(request, dict):\n                        merge_dicts(request, defaults)\n        return super().deserialize(cstruct)\n\n\nclass BatchRequest(colander.MappingSchema):\n    body = BatchPayloadSchema()\n\n\nclass BatchResponseSchema(colander.MappingSchema):\n    status = colander.SchemaNode(colander.Integer())\n    path = colander.SchemaNode(colander.String())\n    headers = colander.SchemaNode(colander.Mapping(unknown='preserve'),\n                                  validator=string_values,\n                                  missing=colander.drop)\n    body = colander.SchemaNode(colander.Mapping(unknown='preserve'),\n                               missing=colander.drop)\n\n\nclass BatchResponseBodySchema(colander.MappingSchema):\n    responses = colander.SequenceSchema(BatchResponseSchema(missing=colander.drop))\n\n\nclass BatchResponse(colander.MappingSchema):\n    body = BatchResponseBodySchema()\n\n\nclass ErrorResponseSchema(colander.MappingSchema):\n    body = ErrorSchema()\n\n\nbatch_responses = {\n    '200': BatchResponse(description='Return a list of operation responses.'),\n    '400': ErrorResponseSchema(description='The request was badly formatted.'),\n    'default': ErrorResponseSchema(description='an unknown error occurred.')\n}\n\nbatch = Service(name=\"batch\", path='/batch',\n                description=\"Batch operations\")\n\n\n@batch.post(schema=BatchRequest,\n            validators=(colander_validator,),\n            permission=NO_PERMISSION_REQUIRED,\n            tags=['Batch'], operation_id='batch',\n            response_schemas=batch_responses)\ndef post_batch(request):\n    requests = request.validated['body']['requests']\n\n    request.log_context(batch_size=len(requests))\n\n    limit = request.registry.settings['batch_max_requests']\n    if limit and len(requests) > int(limit):\n        error_msg = 'Number of requests is limited to {}'.format(limit)\n        request.errors.add('body', 'requests', error_msg)\n        return\n\n    if any([batch.path in req['path'] for req in requests]):\n        error_msg = 'Recursive call on {} endpoint is forbidden.'.format(batch.path)\n        request.errors.add('body', 'requests', error_msg)\n        return\n\n    responses = []\n\n    for subrequest_spec in requests:\n        subrequest = build_request(request, subrequest_spec)\n\n        log_context = {'path': subrequest.path,\n                       'method': subrequest.method,\n                       **request.log_context()}\n        try:\n            # Invoke subrequest without individual transaction.\n            resp, subrequest = request.follow_subrequest(subrequest,\n                                                         use_tweens=False)\n        except httpexceptions.HTTPException as e:\n            if e.content_type == 'application/json':\n                resp = e\n            else:\n                # JSONify raw Pyramid errors.\n                resp = errors.http_error(e)\n\n        subrequest_logger.info('subrequest.summary', extra=log_context)\n\n        dict_resp = build_response(resp, subrequest)\n        responses.append(dict_resp)\n\n    return {\n        'responses': responses\n    }\n", "path": "kinto/core/views/batch.py"}], "after_files": [{"content": "import logging\n\nimport colander\nfrom cornice.validators import colander_validator\nfrom pyramid import httpexceptions\nfrom pyramid.security import NO_PERMISSION_REQUIRED\n\nfrom kinto.core import errors\nfrom kinto.core import Service\nfrom kinto.core.errors import ErrorSchema\nfrom kinto.core.utils import merge_dicts, build_request, build_response\n\n\nsubrequest_logger = logging.getLogger(\"subrequest.summary\")\n\nvalid_http_method = colander.OneOf(('GET', 'HEAD', 'DELETE', 'TRACE',\n                                    'POST', 'PUT', 'PATCH'))\n\n\ndef string_values(node, cstruct):\n    \"\"\"Validate that a ``colander.Mapping`` only has strings in its values.\n\n    .. warning::\n\n        Should be associated to a ``colander.Mapping`` schema node.\n    \"\"\"\n    are_strings = [isinstance(v, str) for v in cstruct.values()]\n    if not all(are_strings):\n        error_msg = '{} contains non string value'.format(cstruct)\n        raise colander.Invalid(node, error_msg)\n\n\nclass BatchRequestSchema(colander.MappingSchema):\n    method = colander.SchemaNode(colander.String(),\n                                 validator=valid_http_method,\n                                 missing=colander.drop)\n    path = colander.SchemaNode(colander.String(),\n                               validator=colander.Regex('^/'))\n    headers = colander.SchemaNode(colander.Mapping(unknown='preserve'),\n                                  validator=string_values,\n                                  missing=colander.drop)\n    body = colander.SchemaNode(colander.Mapping(unknown='preserve'),\n                               missing=colander.drop)\n\n    @staticmethod\n    def schema_type():\n        return colander.Mapping(unknown='raise')\n\n\nclass BatchPayloadSchema(colander.MappingSchema):\n    defaults = BatchRequestSchema(missing=colander.drop).clone()\n    requests = colander.SchemaNode(colander.Sequence(),\n                                   BatchRequestSchema())\n\n    @staticmethod\n    def schema_type():\n        return colander.Mapping(unknown='raise')\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        # On defaults, path is not mandatory.\n        self.get('defaults').get('path').missing = colander.drop\n\n    def deserialize(self, cstruct=colander.null):\n        \"\"\"Preprocess received data to carefully merge defaults.\n        \"\"\"\n        if cstruct is not colander.null:\n            defaults = cstruct.get('defaults')\n            requests = cstruct.get('requests')\n            if isinstance(defaults, dict) and isinstance(requests, list):\n                for request in requests:\n                    if isinstance(request, dict):\n                        merge_dicts(request, defaults)\n        return super().deserialize(cstruct)\n\n\nclass BatchRequest(colander.MappingSchema):\n    body = BatchPayloadSchema()\n\n\nclass BatchResponseSchema(colander.MappingSchema):\n    status = colander.SchemaNode(colander.Integer())\n    path = colander.SchemaNode(colander.String())\n    headers = colander.SchemaNode(colander.Mapping(unknown='preserve'),\n                                  validator=string_values,\n                                  missing=colander.drop)\n    body = colander.SchemaNode(colander.Mapping(unknown='preserve'),\n                               missing=colander.drop)\n\n\nclass BatchResponseBodySchema(colander.MappingSchema):\n    responses = colander.SequenceSchema(BatchResponseSchema(missing=colander.drop))\n\n\nclass BatchResponse(colander.MappingSchema):\n    body = BatchResponseBodySchema()\n\n\nclass ErrorResponseSchema(colander.MappingSchema):\n    body = ErrorSchema()\n\n\nbatch_responses = {\n    '200': BatchResponse(description='Return a list of operation responses.'),\n    '400': ErrorResponseSchema(description='The request was badly formatted.'),\n    'default': ErrorResponseSchema(description='an unknown error occurred.')\n}\n\nbatch = Service(name=\"batch\", path='/batch',\n                description=\"Batch operations\")\n\n\n@batch.post(schema=BatchRequest,\n            validators=(colander_validator,),\n            permission=NO_PERMISSION_REQUIRED,\n            tags=['Batch'], operation_id='batch',\n            response_schemas=batch_responses)\ndef post_batch(request):\n    requests = request.validated['body']['requests']\n\n    request.log_context(batch_size=len(requests))\n\n    limit = request.registry.settings['batch_max_requests']\n    if limit and len(requests) > int(limit):\n        error_msg = 'Number of requests is limited to {}'.format(limit)\n        request.errors.add('body', 'requests', error_msg)\n        return\n\n    if any([batch.path in req['path'] for req in requests]):\n        error_msg = 'Recursive call on {} endpoint is forbidden.'.format(batch.path)\n        request.errors.add('body', 'requests', error_msg)\n        return\n\n    responses = []\n\n    for subrequest_spec in requests:\n        subrequest = build_request(request, subrequest_spec)\n\n        log_context = {'path': subrequest.path,\n                       'method': subrequest.method,\n                       **request.log_context()}\n        try:\n            # Invoke subrequest without individual transaction.\n            resp, subrequest = request.follow_subrequest(subrequest,\n                                                         use_tweens=False)\n        except httpexceptions.HTTPException as e:\n            if e.content_type == 'application/json':\n                resp = e\n            else:\n                # JSONify raw Pyramid errors.\n                resp = errors.http_error(e)\n\n        subrequest_logger.info('subrequest.summary', extra=log_context)\n\n        dict_resp = build_response(resp, subrequest)\n        responses.append(dict_resp)\n\n    return {\n        'responses': responses\n    }\n", "path": "kinto/core/views/batch.py"}]}