GHC 2019-04-28

1 comment.

, https://git.io/fjGGQ in graphql-python/flask-graphql
I just encountered this issue myself and looked into it. (My use case: I have a field with two mutually exclusive arguments, and need to return 400 when both are present. Isn't really possible on the schema level, and hence needs to be handled in a resolver.)

The problem apparently is deep down in graphql-core. See `complete_value_catching_error` (https://github.com/graphql-python/graphql-core/blob/d8e9d3abe7c209eb2f51cf001402783bfd480596/graphql/execution/executor.py#L461-L495). An exception raised in a resolver simply isn't treated the same as a schema level error (which causes the `ExecutionResult` to be `invalid`); the exception is logged and the executor moves on with a `None`. The exception ends up in `ExecutionResult.errors` too. But since it's not an uncaught exception, Flask's error handler cannot kick in.

I'm not sure if one can force an `invalid` down at the `complete_value_catching_error` level since I only read a little bit of the code and didn't try to follow the data flow, but that won't help with non-400 status codes anyway. Instead, the solution is higher up, in `graphql_server.format_execution_results` (https://github.com/graphql-python/graphql-server-core/blob/master/graphql_server/__init__.py#L144-L158):

```py
def format_execution_result(
    execution_result,  # type: Optional[ExecutionResult]
    format_error,  # type: Optional[Callable[[Exception], Dict]]
):
    # type: (...) -> GraphQLResponse
    status_code = 200

    if execution_result:
        if execution_result.invalid:
            status_code = 400
        response = execution_result.to_dict(format_error=format_error)
    else:
        response = None

    return GraphQLResponse(response, status_code)
```

Note that you can monkeypatch this function to check for any custom exception, and even deny `data` in the response if you want to. Here's an example (say we want 400 on `BadRequest` and 401 on `Unauthorized`):

```py
import graphql_server
from graphql_server import GraphQLResponse

class BadRequest(Exception):
    pass

class Unauthorized(Exception):
    pass

def format_execution_result(
    execution_result,  # type: Optional[ExecutionResult]
    format_error,  # type: Optional[Callable[[Exception], Dict]]
):
    # type: (...) -> GraphQLResponse
    status_code = 200

    if execution_result:
        if execution_result.invalid:
            status_code = 400
		for e in execution_result.errors:
            if isinstance(e, BadRequest):
                status_code = 400
			if isinstance(e, Unauthorized):
                status_code = 401
		# Deny data entirely on 4XX if you want to.
		if status_code != 200:
            execution_result.invalid = True
        response = execution_result.to_dict(format_error=format_error)
    else:
        response = None

    return GraphQLResponse(response, status_code)

# Monkeypatch
graphql_server.format_execution_result = format_execution_result
```

Personally I've decided to not bother with the status code after all... But maybe my investigation could help someone.