Codebase list python-webargs / 0b7963f
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
4 changed file(s) with 40 addition(s) and 1 deletion(s). Raw diff Collapse all Expand all
00 Changelog
11 ---------
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.
212
313 6.1.0 (2020-04-05)
414 ******************
6161 return getattr(self.data, name)
6262
6363 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
6571
6672 def __contains__(self, x):
6773 return x in self.data
123123 # the "exclude schema" must be used in this case because WSGI headers may
124124 # be populated with many fields not sent by the caller
125125 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)
126134
127135
128136 @app.route("/echo_cookie")
6666 res = testapp.post_json("/echo_nested_many_data_key", {})
6767 assert res.json == {}
6868
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
6984
7085 @mock.patch("webargs.flaskparser.abort")
7186 def test_abort_called_on_validation_error(mock_abort):