graphql-python / gql

A GraphQL client in Python
https://gql.readthedocs.io
MIT License
1.56k stars 180 forks source link

DSL does not provide expected argument validation #355

Open brhubbar opened 2 years ago

brhubbar commented 2 years ago

Describe the bug

I've been able to recreate this using the countries api used in the docs.

To Reproduce

Jump to step 5 to see the actual improper behavior.

  1. Set up the transport/client.

    import json
    
    import gql
    from gql.transport.requests import RequestsHTTPTransport as Transport
    from gql import dsl
    
    url = "https://countries.trevorblades.com/"
    
    transport = Transport(url=url)
    client = gql.Client(transport=transport, fetch_schema_from_transport=True)
    
    # Fetch the schema (lemme know if there's a recommended approach for this).
    client.connect_sync()
    client.close_sync()
    ds = dsl.DSLSchema(client.schema)
  2. Run a good query using strings.

    good_query_str = gql.gql(
        """
        query {
            continents (filter:{code:{eq:"AN"}}) {
                code
                name
            }
        }
        """
    )
    result = client.execute(good_query_str)
    print(json.dumps(result, indent=2))

    Result:

    {
    "continents": [
        {
        "code": "AN",
        "name": "Antarctica"
        }
    ]
    }
  3. Run a bad query using strings. The only change here is using 'AN' directly as an argument to code, instead of providing the eq directive.

    bad_query_str = gql.gql(
        """
        query {
            continents (filter:{code:"AN"}) {
                code
                name
            }
        }
        """
    )
    result = client.execute(bad_query_str)
    print(json.dumps(result, indent=2))

    Result:

    GraphQLError: Expected value of type 'StringQueryOperatorInput', found "AN".
    
    GraphQL request:3:34
    2 |     query {
    3 |         continents (filter:{code:"AN"}) {
    |                                  ^
    4 |             code
  4. Run a good query using DSL.

    good_query_dsl = dsl.dsl_gql(
        dsl.DSLQuery(
            ds.Query.continents(
                filter={
                    'code': {'eq': 'AN'}
                }
            ).select(
                ds.Continent.code,
                ds.Continent.name,
            )
        )
    )
    result = client.execute(good_query_dsl)
    print(json.dumps(result, indent=2))

    Result:

    {
    "continents": [
        {
        "code": "AN",
        "name": "Antarctica"
        }
    ]
    }
  5. Run a bad query using DSL. Same deal, just remove the 'eq' level of filter specification. Note that the result is an unfiltered response.

    bad_query_dsl = dsl.dsl_gql(
        dsl.DSLQuery(
            ds.Query.continents(
                filter={
                    'code': 'AN'
                }
            ).select(
                ds.Continent.code,
                ds.Continent.name,
            )
        )
    )
    result = client.execute(bad_query_dsl)
    print(json.dumps(result, indent=2))

    Result:

    {
    "continents": [
        {
        "code": "AF",
        "name": "Africa"
        },
        {
        "code": "AN",
        "name": "Antarctica"
        },
        {
        "code": "AS",
        "name": "Asia"
        },
        {
        "code": "EU",
        "name": "Europe"
        },
        {
        "code": "NA",
        "name": "North America"
        },
        {
        "code": "OC",
        "name": "Oceania"
        },
        {
        "code": "SA",
        "name": "South America"
        }
    ]
    }

Expected behavior

Step 5 should raise an equivalent exception to step 3.

System info (please complete the following information):

brhubbar commented 2 years ago

Poking at this a bit more (after finding gql.dsl.print_ast), it looks like DSL silently cleanses your query of anything it doesn't recognize, so the example above:

from gql.dsl import print_ast

bad_query_dsl = dsl.dsl_gql(
    dsl.DSLQuery(
        ds.Query.continents(
            filter={
                'code': 'AN'
            }
        ).select(
            ds.Continent.code,
            ds.Continent.name,
        )
    )
)
print(print_ast(bad_query_dsl))

Results in this query:

{
  continents(filter: {}) {
    code
    name
  }
}

This also happens in some (but not all) cases where an incorrect type is provided as a value. I've found that passing a list where a string is expected returns a very long traceback, ending with this:

in serialize_string(output_value)
    174 # do not serialize builtin types as strings, but allow serialization of custom    175 # types via their `__str__` method
    176 if type(output_value).__module__ == "builtins":
--> 177     raise GraphQLError("String cannot represent value: " + inspect(output_value))
    178 return str(output_value)

GraphQLError: String cannot represent value: ['AN']

However, passing a string or a dict where a list of dict is expected results in an empty list in the query. Differently again, passing a string where a list of string is expected is accepted. I don't know if this is a gql quirk or a GraphQL quirk.