import colander
import logging
from pyramid import httpexceptions
from enum import Enum

from kinto.core.schema import Any
from kinto.core.utils import json, reapply_cors


class ERRORS(Enum):
    """Predefined errors as specified by the API.

    +-------------+-------+------------------------------------------------+
    | Status code | Errno | Description                                    |
    +=============+=======+================================================+
    | 401         | 104   | Missing Authorization Token                    |
    +-------------+-------+------------------------------------------------+
    | 401         | 105   | Invalid Authorization Token                    |
    +-------------+-------+------------------------------------------------+
    | 400         | 106   | request body was not valid JSON                |
    +-------------+-------+------------------------------------------------+
    | 400         | 107   | invalid request parameter                      |
    +-------------+-------+------------------------------------------------+
    | 400         | 108   | missing request parameter                      |
    +-------------+-------+------------------------------------------------+
    | 400         | 109   | invalid posted data                            |
    +-------------+-------+------------------------------------------------+
    | 404         | 110   | Invalid Token / id                             |
    +-------------+-------+------------------------------------------------+
    | 404         | 111   | Missing Token / id                             |
    +-------------+-------+------------------------------------------------+
    | 411         | 112   | Content-Length header was not provided         |
    +-------------+-------+------------------------------------------------+
    | 413         | 113   | Request body too large                         |
    +-------------+-------+------------------------------------------------+
    | 412         | 114   | Resource was modified meanwhile                |
    +-------------+-------+------------------------------------------------+
    | 405         | 115   | Method not allowed on this end point           |
    +-------------+-------+------------------------------------------------+
    | 404         | 116   | Requested version not available on this server |
    +-------------+-------+------------------------------------------------+
    | 429         | 117   | Client has sent too many requests              |
    +-------------+-------+------------------------------------------------+
    | 403         | 121   | Resource's access forbidden for this user      |
    +-------------+-------+------------------------------------------------+
    | 409         | 122   | Another resource violates constraint           |
    +-------------+-------+------------------------------------------------+
    | 500         | 999   | Internal Server Error                          |
    +-------------+-------+------------------------------------------------+
    | 503         | 201   | Service Temporary unavailable due to high load |
    +-------------+-------+------------------------------------------------+
    | 410         | 202   | Service deprecated                             |
    +-------------+-------+------------------------------------------------+
    """

    MISSING_AUTH_TOKEN = 104
    INVALID_AUTH_TOKEN = 105
    BADJSON = 106
    INVALID_PARAMETERS = 107
    MISSING_PARAMETERS = 108
    INVALID_POSTED_DATA = 109
    INVALID_RESOURCE_ID = 110
    MISSING_RESOURCE = 111
    MISSING_CONTENT_LENGTH = 112
    REQUEST_TOO_LARGE = 113
    MODIFIED_MEANWHILE = 114
    METHOD_NOT_ALLOWED = 115
    VERSION_NOT_AVAILABLE = 116
    CLIENT_REACHED_CAPACITY = 117
    FORBIDDEN = 121
    CONSTRAINT_VIOLATED = 122
    UNDEFINED = 999
    BACKEND = 201
    SERVICE_DEPRECATED = 202


class ErrorSchema(colander.MappingSchema):
    """Payload schema for Kinto errors."""

    code = colander.SchemaNode(colander.Integer())
    errno = colander.SchemaNode(colander.Integer())
    error = colander.SchemaNode(colander.String())
    message = colander.SchemaNode(colander.String(), missing=colander.drop)
    info = colander.SchemaNode(colander.String(), missing=colander.drop)
    details = colander.SchemaNode(Any(), missing=colander.drop)


def http_error(
    httpexception, errno=None, code=None, error=None, message=None, info=None, details=None
):
    """Return a JSON formated response matching the error HTTP API.

    :param httpexception: Instance of :mod:`~pyramid:pyramid.httpexceptions`
    :param errno: stable application-level error number (e.g. 109)
    :param code: matches the HTTP status code (e.g 400)
    :param error: string description of error type (e.g. "Bad request")
    :param message: context information (e.g. "Invalid request parameters")
    :param info: information about error (e.g. URL to troubleshooting)
    :param details: additional structured details (conflicting object)
    :returns: the formatted response object
    :rtype: pyramid.httpexceptions.HTTPException
    """
    errno = errno or ERRORS.UNDEFINED

    if isinstance(errno, Enum):
        errno = errno.value

    body = {
        "code": code or httpexception.code,
        "errno": errno,
        "error": error or httpexception.title,
        "message": message,
        "info": info,
        "details": details or colander.drop,
    }

    response = httpexception
    response.errno = errno
    response.json = ErrorSchema().deserialize(body)
    response.content_type = "application/json"
    return response


def json_error_handler(request):
    """Cornice JSON error handler, returning consistant JSON formatted errors
    from schema validation errors.

    This is meant to be used is custom services in your applications.

    .. code-block:: python

        upload = Service(name="upload", path='/upload',
                         error_handler=errors.json_error_handler)

    .. warning::

        Only the first error of the list is formatted in the response.
        (c.f. HTTP API).
    """
    errors = request.errors
    sorted_errors = sorted(errors, key=lambda x: str(x["name"]))
    # In Cornice, we call error handler if at least one error was set.
    error = sorted_errors[0]
    name = error["name"]
    description = error["description"]

    if isinstance(description, bytes):
        description = error["description"].decode("utf-8")

    if name is not None:
        if name in description:
            message = description
        else:
            message = "{name} in {location}: {description}".format_map(error)
    else:
        message = "{location}: {description}".format_map(error)

    response = http_error(
        httpexceptions.HTTPBadRequest(),
        code=errors.status,
        errno=ERRORS.INVALID_PARAMETERS.value,
        error="Invalid parameters",
        message=message,
        details=errors,
    )
    response.status = errors.status
    response = reapply_cors(request, response)
    return response


def raise_invalid(request, location="body", name=None, description=None, **kwargs):
    """Helper to raise a validation error.

    :param location: location in request (e.g. ``'querystring'``)
    :param name: field name
    :param description: detailed description of validation error

    :raises: :class:`~pyramid:pyramid.httpexceptions.HTTPBadRequest`
    """
    request.errors.add(location, name, description, **kwargs)
    response = json_error_handler(request)
    raise response


def send_alert(request, message=None, url=None, code="soft-eol"):
    """Helper to add an Alert header to the response.

    :param code: The type of error 'soft-eol', 'hard-eol'
    :param message: The description message.
    :param url: The URL for more information, default to the documentation url.
    """
    if url is None:
        url = request.registry.settings["project_docs"]

    request.response.headers["Alert"] = json.dumps({"code": code, "message": message, "url": url})


def request_GET(request):
    """Catches a UnicodeDecode error in request.GET in case a wrong request was received.
    Fixing a webob long term issue: https://github.com/Pylons/webob/issues/161
    """
    try:
        return request.GET
    except UnicodeDecodeError:
        querystring = request.environ.get("QUERY_STRING", "")
        logger = logging.getLogger(__name__)
        logger.warning("Error decoding QUERY_STRING: %s" % request.environ)
        raise http_error(
            httpexceptions.HTTPBadRequest(),
            errno=ERRORS.INVALID_PARAMETERS,
            message="A request with an incorrect encoding in the querystring was"
            "received. Please make sure your requests are encoded in UTF-8: %s" % querystring,
        )

kinto/core/errors.py
import colander
import logging
from pyramid import httpexceptions
from enum import Enum

from kinto.core.schema import Any
from kinto.core.utils import json, reapply_cors


class ERRORS(Enum):
    """Predefined errors as specified by the API.

    +-------------+-------+------------------------------------------------+
    | Status code | Errno | Description                                    |
    +=============+=======+================================================+
    | 401         | 104   | Missing Authorization Token                    |
    +-------------+-------+------------------------------------------------+
    | 401         | 105   | Invalid Authorization Token                    |
    +-------------+-------+------------------------------------------------+
    | 400         | 106   | request body was not valid JSON                |
    +-------------+-------+------------------------------------------------+
    | 400         | 107   | invalid request parameter                      |
    +-------------+-------+------------------------------------------------+
    | 400         | 108   | missing request parameter                      |
    +-------------+-------+------------------------------------------------+
    | 400         | 109   | invalid posted data                            |
    +-------------+-------+------------------------------------------------+
    | 404         | 110   | Invalid Token / id                             |
    +-------------+-------+------------------------------------------------+
    | 404         | 111   | Missing Token / id                             |
    +-------------+-------+------------------------------------------------+
    | 411         | 112   | Content-Length header was not provided         |
    +-------------+-------+------------------------------------------------+
    | 413         | 113   | Request body too large                         |
    +-------------+-------+------------------------------------------------+
    | 412         | 114   | Resource was modified meanwhile                |
    +-------------+-------+------------------------------------------------+
    | 405         | 115   | Method not allowed on this end point           |
    +-------------+-------+------------------------------------------------+
    | 404         | 116   | Requested version not available on this server |
    +-------------+-------+------------------------------------------------+
    | 429         | 117   | Client has sent too many requests              |
    +-------------+-------+------------------------------------------------+
    | 403         | 121   | Resource's access forbidden for this user      |
    +-------------+-------+------------------------------------------------+
    | 409         | 122   | Another resource violates constraint           |
    +-------------+-------+------------------------------------------------+
    | 500         | 999   | Internal Server Error                          |
    +-------------+-------+------------------------------------------------+
    | 503         | 201   | Service Temporary unavailable due to high load |
    +-------------+-------+------------------------------------------------+
    | 410         | 202   | Service deprecated                             |
    +-------------+-------+------------------------------------------------+
    """

    MISSING_AUTH_TOKEN = 104
    INVALID_AUTH_TOKEN = 105
    BADJSON = 106
    INVALID_PARAMETERS = 107
    MISSING_PARAMETERS = 108
    INVALID_POSTED_DATA = 109
    INVALID_RESOURCE_ID = 110
    MISSING_RESOURCE = 111
    MISSING_CONTENT_LENGTH = 112
    REQUEST_TOO_LARGE = 113
    MODIFIED_MEANWHILE = 114
    METHOD_NOT_ALLOWED = 115
    VERSION_NOT_AVAILABLE = 116
    CLIENT_REACHED_CAPACITY = 117
    FORBIDDEN = 121
    CONSTRAINT_VIOLATED = 122
    UNDEFINED = 999
    BACKEND = 201
    SERVICE_DEPRECATED = 202


class ErrorSchema(colander.MappingSchema):
    """Payload schema for Kinto errors."""

    code = colander.SchemaNode(colander.Integer())
    errno = colander.SchemaNode(colander.Integer())
    error = colander.SchemaNode(colander.String())
    message = colander.SchemaNode(colander.String(), missing=colander.drop)
    info = colander.SchemaNode(colander.String(), missing=colander.drop)
    details = colander.SchemaNode(Any(), missing=colander.drop)


def http_error(
    httpexception, errno=None, code=None, error=None, message=None, info=None, details=None
):
    """Return a JSON formated response matching the error HTTP API.

    :param httpexception: Instance of :mod:`~pyramid:pyramid.httpexceptions`
    :param errno: stable application-level error number (e.g. 109)
    :param code: matches the HTTP status code (e.g 400)
    :param error: string description of error type (e.g. "Bad request")
    :param message: context information (e.g. "Invalid request parameters")
    :param info: information about error (e.g. URL to troubleshooting)
    :param details: additional structured details (conflicting object)
    :returns: the formatted response object
    :rtype: pyramid.httpexceptions.HTTPException
    """
    errno = errno or ERRORS.UNDEFINED

    if isinstance(errno, Enum):
        errno = errno.value

    body = {
        "code": code or httpexception.code,
        "errno": errno,
        "error": error or httpexception.title,
        "message": message,
        "info": info,
        "details": details or colander.drop,
    }

    response = httpexception
    response.errno = errno
    response.json = ErrorSchema().deserialize(body)
    response.content_type = "application/json"
    return response


def json_error_handler(request):
    """Cornice JSON error handler, returning consistant JSON formatted errors
    from schema validation errors.

    This is meant to be used is custom services in your applications.

    .. code-block:: python

        upload = Service(name="upload", path='/upload',
                         error_handler=errors.json_error_handler)

    .. warning::

        Only the first error of the list is formatted in the response.
        (c.f. HTTP API).
    """
    errors = request.errors
    sorted_errors = sorted(errors, key=lambda x: str(x["name"]))
    # In Cornice, we call error handler if at least one error was set.
    error = sorted_errors[0]
    name = error["name"]
    description = error["description"]

    if isinstance(description, bytes):
        description = error["description"].decode("utf-8")

    if name is not None:
        if str(name) in description:
            message = description
        else:
            message = "{name} in {location}: {description}".format_map(error)
    else:
        message = "{location}: {description}".format_map(error)

    response = http_error(
        httpexceptions.HTTPBadRequest(),
        code=errors.status,
        errno=ERRORS.INVALID_PARAMETERS.value,
        error="Invalid parameters",
        message=message,
        details=errors,
    )
    response.status = errors.status
    response = reapply_cors(request, response)
    return response


def raise_invalid(request, location="body", name=None, description=None, **kwargs):
    """Helper to raise a validation error.

    :param location: location in request (e.g. ``'querystring'``)
    :param name: field name
    :param description: detailed description of validation error

    :raises: :class:`~pyramid:pyramid.httpexceptions.HTTPBadRequest`
    """
    request.errors.add(location, name, description, **kwargs)
    response = json_error_handler(request)
    raise response


def send_alert(request, message=None, url=None, code="soft-eol"):
    """Helper to add an Alert header to the response.

    :param code: The type of error 'soft-eol', 'hard-eol'
    :param message: The description message.
    :param url: The URL for more information, default to the documentation url.
    """
    if url is None:
        url = request.registry.settings["project_docs"]

    request.response.headers["Alert"] = json.dumps({"code": code, "message": message, "url": url})


def request_GET(request):
    """Catches a UnicodeDecode error in request.GET in case a wrong request was received.
    Fixing a webob long term issue: https://github.com/Pylons/webob/issues/161
    """
    try:
        return request.GET
    except UnicodeDecodeError:
        querystring = request.environ.get("QUERY_STRING", "")
        logger = logging.getLogger(__name__)
        logger.warning("Error decoding QUERY_STRING: %s" % request.environ)
        raise http_error(
            httpexceptions.HTTPBadRequest(),
            errno=ERRORS.INVALID_PARAMETERS,
            message="A request with an incorrect encoding in the querystring was"
            "received. Please make sure your requests are encoded in UTF-8: %s" % querystring,
        )

kinto/core/errors.py
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 74d43eb6a..5c05159ab 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -9,6 +9,7 @@ This document describes changes between each past release.
 **Bug fixes**
 
 - Fixed two potential bugs relating to mutable default values.
+- Fix crash on validating records with errors in arrays (#1508)
 - Fix crash on deleting multiple accounts (#2009)
 
 **Internal changes**
diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst
index f71f3ae2f..8ef5c37f3 100644
--- a/CONTRIBUTORS.rst
+++ b/CONTRIBUTORS.rst
@@ -29,6 +29,7 @@ Contributors
 * Éric Lemoine <eric.lemoine@gmail.com>
 * Ethan Glasser-Camp <ethan@betacantrips.com>
 * Étienne <@Étienne>
+* Eugene Kulak <kulak.eugene@gmail.com>
 * Fil <fil@rezo.net>
 * FooBarQuaxx
 * Francisco J. Piedrahita <@fpiedrah>
diff --git a/kinto/core/errors.py b/kinto/core/errors.py
index 55f92029f..6e79327cd 100644
--- a/kinto/core/errors.py
+++ b/kinto/core/errors.py
@@ -148,7 +148,7 @@ def json_error_handler(request):
         description = error["description"].decode("utf-8")
 
     if name is not None:
-        if name in description:
+        if str(name) in description:
             message = description
         else:
             message = "{name} in {location}: {description}".format_map(error)
diff --git a/tests/test_views_schema_record.py b/tests/test_views_schema_record.py
index 058b065d6..7e20290e3 100644
--- a/tests/test_views_schema_record.py
+++ b/tests/test_views_schema_record.py
@@ -19,6 +19,7 @@
             "type": "object",
             "properties": {"size": {"type": "number"}, "name": {"type": "string"}},
         },
+        "tags": {"type": "array", "items": {"type": "string"}},
     },
     "required": ["title"],
 }
@@ -125,6 +126,11 @@ def test_records_are_invalid_if_do_not_match_schema(self):
             status=400,
         )
 
+    def test_records_are_invalid_if_do_not_match_schema_in_array(self):
+        self.app.post_json(
+            RECORDS_URL, {"data": {**VALID_RECORD, "tags": [0]}}, headers=self.headers, status=400
+        )
+
     def test_records_are_validated_on_patch(self):
         resp = self.app.post_json(
             RECORDS_URL, {"data": VALID_RECORD}, headers=self.headers, status=201

Crash with "in <string>' requires string as left operand, not int"
```
ValidationError: 'minVersion' is a required property

Failed validating 'required' in schema['properties']['versionRange']['items']['properties']['targetApplication']['items']:
    {'additionalProperties': False,
     'description': 'Target application',
     'properties': {'guid': {'description': 'The application unique '
                                            'identifier.',
                             'enum': ['{ec8030f7-c20a-464f-9b0e-13a3a9e97384}',
                                      '{3550f703-e582-4d05-9a08-453d09bdfdc6}',
                                      '{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}',
                                      '{aa3c5121-dab2-40e2-81ca-7ea25febc110}'],
                             'enumNames': ['Firefox',
                                           'Thunderbird',
                                           'Seamonkey',
                                           'Android'],
                             'title': 'Application id',
                             'type': 'string'},
                    'maxVersion': {'$ref': '#/definitions/maxVersion'},
                    'minVersion': {'$ref': '#/definitions/minVersion'}},
     'required': ['guid', 'minVersion', 'maxVersion'],
     'title': 'Target application',
     'type': 'object'}

On instance['versionRange'][0]['targetApplication'][0]:
    {'guid': 'ec8030f7-c20a-464f-9b0e-13a3a9e97384', 'maxVersion': '57.0.*'}
  File "kinto/views/records.py", line 73, in process_record
    jsonschema.validate(data, schema)
  File "jsonschema/validators.py", line 541, in validate
    cls(schema, *args, **kwargs).validate(instance)
  File "jsonschema/validators.py", line 130, in validate
    raise error

TypeError: 'in <string>' requires string as left operand, not int
(11 additional frame(s) were not displayed)
...
  File "cornice/service.py", line 494, in wrapper
    response = view_()
  File "kinto/core/resource/__init__.py", line 463, in put
    new_record = self.process_record(post_record, old=existing)
  File "kinto/views/records.py", line 81, in process_record
    raise_invalid(self.request, name=field, description=e.message)
  File "kinto/core/errors.py", line 178, in raise_invalid
    response = json_error_handler(request)
  File "kinto/core/errors.py", line 149, in json_error_handler
    if name in description:
```
