# Communications between layers of the Application

THe current boilerplate shows the model, logic and handler layers communicating with each other using response objects, ie an owsresponse.

This pattern is not suitable for handling errors and creates confusion and overhead in passing data around between the layers.

This document will illustrate a simple exception class for handling errors and removing the need to pass responses between the applications layers.

## Current Global Exception Handler

In the current boilerplate, all uncaught exceptions result in a single fatal response. This works well when the application crashes in the following scenarios:

* database connection error
* service misconfiguration in config.py \(although our current fargate deployment protects against that\)
* CPU or memory issues
* microservice is misconfigured in Terraform: for example it cant access Dynamodb and make owsrequest, or cant connect to  S3 buckets.

```python
@app.errorhandler(500)
def exception_handler(error):
    """Handle error when uncaught exception is raised.
    Default exception handler.
    Note: Exception will also be sent to Sentry if config.SENTRY is set.
    Returns:
        flask.Response: A 500 response with JSON 'code' & 'message' payload.
    """
    message = (
        'The server encountered an internal error '
        'and was unable to complete your request.')
    g.log.exception(error)
    return flaskify(response.create_fatal_response(message))
```

If you encounter validation errors, API errors from calling other microservices, you will need to construct response objects and send them upstream.

## OwsError

The following is a generic exception class that you can use in the model and logic layers whenever you encounter the following

* microservice API calls in the model layer that result in errors
* validation errors of input data
* business logic errors

```python
class OwsError(Exception):
    """Exception to create OwsException from str message."""

    def __init__(self, message, status=500):
        """Initialize exception."""
        super().__init__()
        self.message = message
        self.status = status

    @classmethod
    def not_found(cls, message=None):
        """Return not found error."""
        return OwsError(message, 404)

    @classmethod
    def forbidden(cls, message='Access Forbidden'):
        """Return not found error."""
        return OwsError(message, 403)

    @classmethod
    def bad_request(cls, message=None):
        """Return bad request error."""
        return OwsError(message, 400)

    def __str__(self):
        """Turn this exception into a string."""
        if not self.message:
            return str(self.status)
        return 'Status %d: %s' % (self.status, self.message)
```

This exception class can be used as follows with an updated global exception handler

```python
@app.errorhandler(Exception)
def exception_handler(error):
    """Handle error when uncaught exception is raised.
    Default exception handler.
    Note: Exception will also be sent to Sentry if config.SENTRY is set.
    Returns:
        flask.Response: A 500 response with JSON 'code' & 'message' payload.
    """
    if isinstance(error, OwsError):
        if error.status == status.INTERNAL_ERROR:
            g.log.exception(error)

        return flaskify(response.Response(
            message={'message': error.message},
            status=error.status
        ), encoder=Encoder)

    message = (
        'The server encountered an internal error '
        'and was unable to complete your request.')
    g.log.exception(error)
    if isinstance(error, HTTPException):
        return flaskify(response.create_error_response(
            code=error.name,
            message=error.description,
            status=error.code
        ))
    return flaskify(response.create_fatal_response(message), encoder=Encoder)
```

## Examples

Model API call that fails

```python
def _get_assets_from_oat(object_ids, object_type, cover=None):
    oat_response = request.get(
        OWS_ASSET_TRANSCODER, '/assets-by-ids-and-types', params={
            'object_ids': object_ids, 'object_types': [object_type] * len(object_ids)
        },
        headers=get_ows_headers()
    )

    if oat_response.status_code != 200:
        if 'message' in oat_response.json():
            raise OwsError(oat_response.json()['message'])
        else:
            raise OwsError(
                'ows-asset-transcader assets-by-ids-and-types failed with {}'.format(
                    oat_response.status_code
                )
            )
    assets_items = oat_response.json()['items']
    return _convert_to_flat_list(assets_items, cover)
```

S3 File Check Error in a utility

```python
def check_s3_file_exists(bucket_name, file_key):
    """Check the file exists in provided s3 bucket.
    Args:
        bucket_name (str): name of bucket where file is stored.
        file_key (str): path to file for which url should be generated.
    Returns:
        Response: Response with success or error.
    """
    try:
        s3_client = s3.get_s3_client()
        s3_client.head_object(Bucket=bucket_name, Key=file_key)
    except ClientError as e:
        error_response = e.response
        error_message = error_response['Error']['Message']
        error_status = error_response['ResponseMetadata']['HTTPStatusCode']
        if error_status == status.NOT_FOUND:
            error_message = '{key} not found in bucket {bucket}!'.format(key=file_key, bucket=bucket_name)
        raise OwsError(error_message, error_status)
```

Input Validation Error

```python
def validate_request_data(schema, partial=False):
    """Decorate requests' input data validation.
    Args:
        schema (marshmallow.Schema()): schema instance to apply
        partial (bool): validate only derived fields
    """
    def validator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            data = request.get_json()
            try:
                validated = schema.load(data, partial=partial)
            except ValidationError as err:
                raise OwsError.bad_request(err.messages)
            else:
                return func(*args, data=validated, **kwargs)
        return wrapper
    return validator
```

