iter(MultiDictProxy) checks if contents are tuples
Specifically in order to handle headers more gracefully,
MultiDictProxy needs to be careful about how it exposes __iter__.
Marshmallow gets a list of keys in the data it's loading by calling
`set(data)`, which implicitly calls `iter()`. And MultiDictProxy was
naively proxying that method to the underlying object's `__iter__`.
However, for Flask Headers and potentially other specialized request
objects, `iter()` does not produce an iterator over keys, but over
some other type. In the case of Flask Headers, the type produced is a
tuple of (key, value).
Rather than trying to make MultiDictProxy aware of all of the
dict-like types it might be handling, simply wrap the `iter()` call in
a loop which checks if objects are tuples. If they are, pop the first
element. In the case of headers, at least, that is the key.
This can definitely be made to fail on oddly-constructed underlying
data. e.g. If you define a type where iter yields an empty tuple,
this will blow up. However, no known real data from frameworks is
shaped that way.
fixes #500
Stephen Rosen
3 years ago
0 | 0 |
Changelog
|
1 | 1 |
---------
|
|
2 |
|
|
3 |
6.1.1 (Unreleased)
|
|
4 |
******************
|
|
5 |
|
|
6 |
Bug fixes:
|
|
7 |
|
|
8 |
* Failure to validate flask headers would produce error data which contained
|
|
9 |
tuples as keys, and was therefore not JSON-serializable. (:issue:`500`)
|
|
10 |
These errors will now extract the headername as the key correctly.
|
|
11 |
Thanks to :user:`shughes-uk` for reporting.
|
2 | 12 |
|
3 | 13 |
6.1.0 (2020-04-05)
|
4 | 14 |
******************
|
61 | 61 |
return getattr(self.data, name)
|
62 | 62 |
|
63 | 63 |
def __iter__(self):
|
64 | |
return iter(self.data)
|
|
64 |
for x in iter(self.data):
|
|
65 |
# special case for header dicts which produce an iterator of tuples
|
|
66 |
# instead of an iterator of strings
|
|
67 |
if isinstance(x, tuple):
|
|
68 |
yield x[0]
|
|
69 |
else:
|
|
70 |
yield x
|
65 | 71 |
|
66 | 72 |
def __contains__(self, x):
|
67 | 73 |
return x in self.data
|
123 | 123 |
# the "exclude schema" must be used in this case because WSGI headers may
|
124 | 124 |
# be populated with many fields not sent by the caller
|
125 | 125 |
return J(parser.parse(hello_exclude_schema, location="headers"))
|
|
126 |
|
|
127 |
|
|
128 |
@app.route("/echo_headers_raising")
|
|
129 |
@use_args(HelloSchema(**strict_kwargs), location="headers")
|
|
130 |
def echo_headers_raising(args):
|
|
131 |
# as above, but in this case, don't use the exclude schema (so unexpected
|
|
132 |
# headers will raise errors)
|
|
133 |
return J(args)
|
126 | 134 |
|
127 | 135 |
|
128 | 136 |
@app.route("/echo_cookie")
|
66 | 66 |
res = testapp.post_json("/echo_nested_many_data_key", {})
|
67 | 67 |
assert res.json == {}
|
68 | 68 |
|
|
69 |
# regression test for
|
|
70 |
# https://github.com/marshmallow-code/webargs/issues/500
|
|
71 |
def test_parsing_unexpected_headers_when_raising(self, testapp):
|
|
72 |
res = testapp.get(
|
|
73 |
"/echo_headers_raising", expect_errors=True, headers={"X-Unexpected": "foo"}
|
|
74 |
)
|
|
75 |
# under marshmallow 2 this is allowed and works
|
|
76 |
if MARSHMALLOW_VERSION_INFO[0] < 3:
|
|
77 |
assert res.json == {"name": "World"}
|
|
78 |
# but on ma3 it's supposed to be a validation error
|
|
79 |
else:
|
|
80 |
assert res.status_code == 422
|
|
81 |
assert "headers" in res.json
|
|
82 |
assert "X-Unexpected" in set(res.json["headers"].keys())
|
|
83 |
|
69 | 84 |
|
70 | 85 |
@mock.patch("webargs.flaskparser.abort")
|
71 | 86 |
def test_abort_called_on_validation_error(mock_abort):
|