Skip to content

Commit bfd3e2d

Browse files
committed
Limit errors in get_variable_values()
Replicates graphql/graphql-js@14f260b
1 parent 14f21aa commit bfd3e2d

File tree

15 files changed

+672
-505
lines changed

15 files changed

+672
-505
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The current version 3.0.0a2 of GraphQL-core is up-to-date
1616
with GraphQL.js version 14.4.2.
1717

1818
All parts of the API are covered by an extensive test suite
19-
of currently 1918 unit tests.
19+
of currently 1925 unit tests.
2020

2121

2222
## Documentation

docs/modules/pyutils.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ PyUtils
2828
.. autoclass:: FrozenDict
2929
.. autoclass:: Path
3030
:members:
31+
.. autofunction:: print_path_list

docs/modules/utilities.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ type system:
6868

6969
Coerce a Python value to a GraphQL type, or produce errors:
7070

71+
.. autofunction:: coerce_input_value
72+
73+
Deprecated, use :func:`coerce_input_value`:
74+
7175
.. autofunction:: coerce_value
7276

7377
Concatenate multiple ASTs together:

src/graphql/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,8 @@
360360
# GraphQL type system.
361361
TypeInfo,
362362
# Coerce a Python value to a GraphQL type, or produce errors.
363+
coerce_input_value,
364+
# Deprecated: use coerce_input_value
363365
coerce_value,
364366
# Concatenates multiple ASTs together.
365367
concat_ast,
@@ -648,6 +650,7 @@
648650
"value_from_ast_untyped",
649651
"ast_from_value",
650652
"TypeInfo",
653+
"coerce_input_value",
651654
"coerce_value",
652655
"concat_ast",
653656
"separate_operations",

src/graphql/execution/execute.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ def build(
276276
schema,
277277
operation.variable_definitions or FrozenList(),
278278
raw_variable_values or {},
279+
max_errors=50,
279280
)
280281

281282
if isinstance(coerced_variable_values, list):

src/graphql/execution/values.py

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Dict, List, Optional, Union, cast
1+
from typing import Any, Callable, Dict, List, Optional, Union, cast
22

33
from ..error import GraphQLError, INVALID
44
from ..language import (
@@ -14,7 +14,7 @@
1414
VariableNode,
1515
print_ast,
1616
)
17-
from ..pyutils import inspect, FrozenList
17+
from ..pyutils import inspect, print_path_list, FrozenList
1818
from ..type import (
1919
GraphQLDirective,
2020
GraphQLField,
@@ -23,7 +23,7 @@
2323
is_input_type,
2424
is_non_null_type,
2525
)
26-
from ..utilities import coerce_value, type_from_ast, value_from_ast
26+
from ..utilities import coerce_input_value, type_from_ast, value_from_ast
2727

2828
__all__ = ["get_variable_values", "get_argument_values", "get_directive_values"]
2929

@@ -35,6 +35,7 @@ def get_variable_values(
3535
schema: GraphQLSchema,
3636
var_def_nodes: FrozenList[VariableDefinitionNode],
3737
inputs: Dict[str, Any],
38+
max_errors: int = None,
3839
) -> CoercedVariableValues:
3940
"""Get coerced variable values based on provided definitions.
4041
@@ -43,6 +44,31 @@ def get_variable_values(
4344
the variable definitions, a GraphQLError will be thrown.
4445
"""
4546
errors: List[GraphQLError] = []
47+
48+
def on_error(error: GraphQLError):
49+
if max_errors is not None and len(errors) >= max_errors:
50+
raise GraphQLError(
51+
"Too many errors processing variables,"
52+
" error limit reached. Execution aborted."
53+
)
54+
errors.append(error)
55+
56+
try:
57+
coerced = coerce_variable_values(schema, var_def_nodes, inputs, on_error)
58+
if not errors:
59+
return coerced
60+
except GraphQLError as e:
61+
errors.append(e)
62+
63+
return errors
64+
65+
66+
def coerce_variable_values(
67+
schema: GraphQLSchema,
68+
var_def_nodes: FrozenList[VariableDefinitionNode],
69+
inputs: Dict[str, Any],
70+
on_error: Callable[[GraphQLError], None],
71+
):
4672
coerced_values: Dict[str, Any] = {}
4773
for var_def_node in var_def_nodes:
4874
var_name = var_def_node.variable.name.value
@@ -51,7 +77,7 @@ def get_variable_values(
5177
# Must use input types for variables. This should be caught during
5278
# validation, however is checked again here for safety.
5379
var_type_str = print_ast(var_def_node.type)
54-
errors.append(
80+
on_error(
5581
GraphQLError(
5682
f"Variable '${var_name}' expected value of type '{var_type_str}'"
5783
" which cannot be used as an input type.",
@@ -69,7 +95,7 @@ def get_variable_values(
6995

7096
if is_non_null_type(var_type):
7197
var_type_str = inspect(var_type)
72-
errors.append(
98+
on_error(
7399
GraphQLError(
74100
f"Variable '${var_name}' of required type '{var_type_str}'"
75101
" was not provided.",
@@ -81,7 +107,7 @@ def get_variable_values(
81107
value = inputs[var_name]
82108
if value is None and is_non_null_type(var_type):
83109
var_type_str = inspect(var_type)
84-
errors.append(
110+
on_error(
85111
GraphQLError(
86112
f"Variable '${var_name}' of non-null type '{var_type_str}'"
87113
" must not be null.",
@@ -90,20 +116,26 @@ def get_variable_values(
90116
)
91117
continue
92118

93-
coerced = coerce_value(value, var_type, var_def_node)
94-
coercion_errors = coerced.errors
95-
if coercion_errors:
96-
for error in coercion_errors:
97-
error.message = (
98-
f"Variable '${var_name}' got invalid"
99-
f" value {inspect(value)}; {error.message}"
119+
def on_input_value_error(
120+
path: List[Union[str, int]], invalid_value: Any, error: GraphQLError
121+
):
122+
invalid_str = inspect(invalid_value)
123+
prefix = f"Variable '${var_name}' got invalid value {invalid_str}"
124+
if path:
125+
prefix += f" at '{var_name}{print_path_list(path)}'"
126+
on_error(
127+
GraphQLError(
128+
prefix + "; " + error.message,
129+
var_def_node,
130+
original_error=error.original_error,
100131
)
101-
errors.extend(coercion_errors)
102-
continue
132+
)
103133

104-
coerced_values[var_name] = coerced.value
134+
coerced_values[var_name] = coerce_input_value(
135+
value, var_type, on_input_value_error
136+
)
105137

106-
return errors or coerced_values
138+
return coerced_values
107139

108140

109141
def get_argument_values(

src/graphql/pyutils/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from .frozen_list import FrozenList
2626
from .frozen_dict import FrozenDict
2727
from .path import Path
28+
from .print_path_list import print_path_list
2829

2930
__all__ = [
3031
"camel_to_snake",
@@ -46,4 +47,5 @@
4647
"FrozenList",
4748
"FrozenDict",
4849
"Path",
50+
"print_path_list",
4951
]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from typing import Sequence, Union
2+
3+
4+
def print_path_list(path: Sequence[Union[str, int]]):
5+
"""Build a string describing the path."""
6+
return "".join(f"[{key}]" if isinstance(key, int) else f".{key}" for key in path)

src/graphql/utilities/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@
5353
from .type_info import TypeInfo
5454

5555
# Coerce a Python value to a GraphQL type, or produce errors.
56+
from .coerce_input_value import coerce_input_value
57+
58+
# deprecated: use coerce_input_value
5659
from .coerce_value import coerce_value
5760

5861
# Concatenate multiple ASTs together.
@@ -95,6 +98,7 @@
9598
"build_ast_schema",
9699
"build_client_schema",
97100
"build_schema",
101+
"coerce_input_value",
98102
"coerce_value",
99103
"concat_ast",
100104
"do_types_overlap",
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
from typing import Any, Callable, Dict, Iterable, List, Union, cast
2+
3+
4+
from ..error import GraphQLError, INVALID
5+
from ..pyutils import Path, did_you_mean, inspect, print_path_list, suggestion_list
6+
from ..type import (
7+
GraphQLEnumType,
8+
GraphQLInputObjectType,
9+
GraphQLInputType,
10+
GraphQLList,
11+
GraphQLScalarType,
12+
is_enum_type,
13+
is_input_object_type,
14+
is_list_type,
15+
is_non_null_type,
16+
is_scalar_type,
17+
GraphQLNonNull,
18+
)
19+
20+
__all__ = ["coerce_input_value"]
21+
22+
23+
OnErrorCB = Callable[[List[Union[str, int]], Any, GraphQLError], None]
24+
25+
26+
def default_on_error(
27+
path: List[Union[str, int]], invalid_value: Any, error: GraphQLError
28+
) -> None:
29+
error_prefix = "Invalid value " + inspect(invalid_value)
30+
if path:
31+
error_prefix += f" at 'value{print_path_list(path)}': "
32+
error.message = error_prefix + ": " + error.message
33+
raise error
34+
35+
36+
def coerce_input_value(
37+
input_value: Any,
38+
type_: GraphQLInputType,
39+
on_error: OnErrorCB = default_on_error,
40+
path: Path = None,
41+
) -> Any:
42+
"""Coerce a Python value given a GraphQL Input Type."""
43+
if is_non_null_type(type_):
44+
if input_value is not None and input_value is not INVALID:
45+
type_ = cast(GraphQLNonNull, type_)
46+
return coerce_input_value(input_value, type_.of_type, on_error, path)
47+
on_error(
48+
path.as_list() if path else [],
49+
input_value,
50+
GraphQLError(
51+
f"Expected non-nullable type {inspect(type_)} not to be None."
52+
),
53+
)
54+
return INVALID
55+
56+
if input_value is None or input_value is INVALID:
57+
# Explicitly return the value null.
58+
return None
59+
60+
if is_list_type(type_):
61+
type_ = cast(GraphQLList, type_)
62+
item_type = type_.of_type
63+
if isinstance(input_value, Iterable) and not isinstance(input_value, str):
64+
coerced_list: List[Any] = []
65+
append_item = coerced_list.append
66+
for index, item_value in enumerate(input_value):
67+
append_item(
68+
coerce_input_value(
69+
item_value, item_type, on_error, Path(path, index)
70+
)
71+
)
72+
return coerced_list
73+
# Lists accept a non-list value as a list of one.
74+
return [coerce_input_value(input_value, item_type, on_error, path)]
75+
76+
if is_input_object_type(type_):
77+
type_ = cast(GraphQLInputObjectType, type_)
78+
if not isinstance(input_value, dict):
79+
on_error(
80+
path.as_list() if path else [],
81+
input_value,
82+
GraphQLError(f"Expected type {type_.name} to be a dict."),
83+
)
84+
return INVALID
85+
86+
coerced_dict: Dict[str, Any] = {}
87+
fields = type_.fields
88+
89+
for field_name, field in fields.items():
90+
field_value = input_value.get(field_name, INVALID)
91+
92+
if field_value is INVALID:
93+
if field.default_value is not INVALID:
94+
# Use out name as name if it exists (extension of GraphQL.js).
95+
coerced_dict[field.out_name or field_name] = field.default_value
96+
elif is_non_null_type(field.type):
97+
type_str = inspect(field.type)
98+
on_error(
99+
path.as_list() if path else [],
100+
input_value,
101+
GraphQLError(
102+
f"Field {field_name} of required type {type_str}"
103+
" was not provided."
104+
),
105+
)
106+
continue
107+
108+
coerced_dict[field.out_name or field_name] = coerce_input_value(
109+
field_value, field.type, on_error, Path(path, field_name)
110+
)
111+
112+
# Ensure every provided field is defined.
113+
for field_name in input_value:
114+
if field_name not in fields:
115+
suggestions = suggestion_list(field_name, fields)
116+
on_error(
117+
path.as_list() if path else [],
118+
input_value,
119+
GraphQLError(
120+
f"Field '{field_name}' is not defined by type {type_.name}."
121+
+ did_you_mean(suggestions)
122+
),
123+
)
124+
return type_.out_type(coerced_dict)
125+
126+
if is_scalar_type(type_):
127+
# Scalars determine if a value is valid via `parse_value()`, which can throw to
128+
# indicate failure. If it throws, maintain a reference to the original error.
129+
type_ = cast(GraphQLScalarType, type_)
130+
try:
131+
parse_result = type_.parse_value(input_value)
132+
except (TypeError, ValueError) as error:
133+
on_error(
134+
path.as_list() if path else [],
135+
input_value,
136+
GraphQLError(
137+
f"Expected type {type_.name}. {error}", original_error=error
138+
),
139+
)
140+
return INVALID
141+
if parse_result is INVALID:
142+
on_error(
143+
path.as_list() if path else [],
144+
input_value,
145+
GraphQLError(f"Expected type {type_.name}."),
146+
)
147+
return parse_result
148+
149+
if is_enum_type(type_):
150+
type_ = cast(GraphQLEnumType, type_)
151+
values = type_.values
152+
if isinstance(input_value, str):
153+
enum_value = values.get(input_value)
154+
if enum_value:
155+
return enum_value.value
156+
suggestions = suggestion_list(str(input_value), values)
157+
on_error(
158+
path.as_list() if path else [],
159+
input_value,
160+
GraphQLError(f"Expected type {type_.name}." + did_you_mean(suggestions)),
161+
)
162+
return INVALID
163+
164+
# Not reachable. All possible input types have been considered.
165+
raise TypeError(f"Unexpected input type: '{inspect(type_)}'.") # pragma: no cover

0 commit comments

Comments
 (0)