Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion client_src/test/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,6 @@ def test_save_docs_unknown_coll(self):
{
'error': {
'message': 'Not found',
'status': 404,
'details': "Collection 'xyz123' does not exist.",
'name': 'xyz123',
}
Expand Down
2 changes: 1 addition & 1 deletion dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ bandit==1.5.1
mccabe==0.6.1
flake8==3.5.0
grequests==0.3.0
coverage==4.5.1
coverage==5.2.1
21 changes: 18 additions & 3 deletions relation_engine_server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ The API is a small, rest-ish service where all data is in JSON format. Replace t
* Staging: `https://ci.kbase.us/services/relation_engine_api`
* App-dev: `https://appdev.kbase.us/services/relation_engine_api`

### Error responses

The majority of errors returned from the server have explanatory information in the response content in the following format:

```json

{
"error": {
"message": "A brief message explaining the error",
}
}
```

Specific errors may have other fields giving more details, e.g. JSON parsing errors have `source_json`, `pos`, `lineno`, and `colno` describing the error; ArangoDB errors have an `arango_message` field.

### GET /

Returns server status info
Expand Down Expand Up @@ -168,7 +183,7 @@ _Response JSON schema_

If you try to update a collection and it fails validation against a JSON schema found in the [relation engine spec](spec/), then you will get a JSON error response with the following fields:

* `"error"` - Human readable message explaining the error
* `"message"` - Human readable message explaining the error
* `"failed_validator"` - The name of the validator that failed (eg. "required")
* `"value"` - The (possibly nested) value in your data that failed validation
* `"path"` - The path into your data where you can find the value that failed validation
Expand Down Expand Up @@ -428,9 +443,9 @@ curl -X PUT -H "Authorization: <mytoken>" \

## Deprecated Endpoints

#### GET `/api/v1/specs/schemas` (replaced by `/api/v1/specs/schemas`)
#### GET `/api/v1/specs/schemas` (replaced by `/api/v1/specs/collections`)

This endpoint has been deprecated; queries should use `/api/v1/specs/schemas` instead.
This endpoint has been deprecated; queries should use `/api/v1/specs/collections` instead.


## Development
Expand Down
2 changes: 1 addition & 1 deletion relation_engine_server/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def __str__(self):


class UnauthorizedAccess(Exception):
"Authentication failed for an authorization header."""
"""Authentication failed for an authorization header."""

def __init__(self, auth_url, response):
self.auth_url = auth_url
Expand Down
95 changes: 53 additions & 42 deletions relation_engine_server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,23 @@
app.register_blueprint(api_v1, url_prefix='/api/v1')


def return_error(error_dict, code):
"""return the appropriate error structure and code

Errors returned by the server have the basic format

'error': {
'message': <text explanation of error>,
}

The 'error' dictionary may have extra keys if there is additional information.

This helper wraps the whole structure in an extra dict under the key 'error'.

"""
return (flask.jsonify({'error': error_dict}), code)


@app.route('/', methods=['GET'])
def root():
"""Server status."""
Expand All @@ -40,29 +57,32 @@ def root():
def json_decode_error(err):
"""A problem parsing json."""
resp = {
'error': 'Unable to parse JSON',
'message': 'Unable to parse JSON',
'source_json': err.doc,
'pos': err.pos,
'lineno': err.lineno,
'colno': err.colno
'colno': err.colno,
}
return (flask.jsonify(resp), 400)
return return_error(resp, 400)


@app.errorhandler(arango_client.ArangoServerError)
def arango_server_error(err):
resp = {
'error': str(err),
'arango_message': err.resp_json['errorMessage']
'message': str(err),
'arango_message': err.resp_json['errorMessage'],
}
return (flask.jsonify(resp), 400)
return return_error(resp, 400)


# Invalid request body json params or missing headers
@app.errorhandler(MissingHeader)
@app.errorhandler(InvalidParameters)
def invalid_params(err):
"""Invalid request body json params."""
resp = {'error': str(err)}
return (flask.jsonify(resp), 400)
def generic_400(err):
resp = {
'message': str(err),
}
return return_error(resp, 400)


@app.errorhandler(ValidationError)
Expand All @@ -71,63 +91,52 @@ def validation_error(err):
# Refer to the documentation on jsonschema.exceptions.ValidationError:
# https://python-jsonschema.readthedocs.io/en/stable/errors/
resp = {
'error': err.message,
'message': err.message,
'failed_validator': err.validator,
'value': err.instance,
'path': list(err.absolute_path),
}
return (flask.jsonify(resp), 400)
return return_error(resp, 400)


@app.errorhandler(UnauthorizedAccess)
def unauthorized_access(err):
resp = {
'error': {
'status': 403,
'message': 'Unauthorized',
'auth_url': err.auth_url,
'auth_response': err.response,
},
'message': 'Unauthorized',
'auth_url': err.auth_url,
'auth_response': err.response,
}
return (flask.jsonify(resp), 403)
return return_error(resp, 403)


@app.errorhandler(SchemaNonexistent)
def schema_does_not_exist(err):
"""General error cases."""
resp = {
'error': {
'message': 'Not found',
'status': 404,
'details': str(err),
'name': err.name,
}
'message': 'Not found',
'details': str(err),
'name': err.name,
}
return (flask.jsonify(resp), 404)
return return_error(resp, 404)


@app.errorhandler(NotFound)
@app.errorhandler(404)
def page_not_found(err):
resp = {
'error': {
'message': 'Not found',
'status': 404,
}
'message': 'Not found',
}
if hasattr(err, 'details'):
resp['error']['details'] = err.details
return (flask.jsonify(resp), 404)
resp['details'] = err.details
return return_error(resp, 404)


@app.errorhandler(405)
def method_not_allowed(err):
return (flask.jsonify({'error': {'message': 'Method not allowed', 'status': 405}}), 405)


@app.errorhandler(MissingHeader)
def generic_400(err):
return (flask.jsonify({'error': {'message': str(err), 'status': 400}}), 400)
resp = {
'message': 'Method not allowed',
}
return return_error(resp, 405)


# Any other unhandled exceptions -> 500
Expand All @@ -139,11 +148,13 @@ def server_error(err):
print('-' * 80)
traceback.print_exc()
print('=' * 80)
resp = {'error': {'status': 500, 'message': 'Unexpected server error'}}
resp = {
'message': 'Unexpected server error'
}
# TODO only set below two fields in dev mode
resp['error']['class'] = err.__class__.__name__
resp['error']['details'] = str(err)
return (flask.jsonify(resp), 500)
resp['class'] = err.__class__.__name__
resp['details'] = str(err)
return return_error(resp, 500)


@app.after_request
Expand Down
Empty file.
Loading