diff --git a/docs/api.md b/docs/api.md
index 7432767..ae54c7c 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -309,9 +309,9 @@ respx.post("https://example.org/", content__contains="bar")
 ```
 
 ### Data
-Matches request *form data*, using [eq](#eq) as default lookup.
+Matches request *form data*, excluding files, using [eq](#eq) as default lookup.
 > Key: `data`  
-> Lookups: [eq](#eq)
+> Lookups: [eq](#eq), [contains](#contains)
 ``` python
 respx.post("https://example.org/", data={"foo": "bar"})
 ```
diff --git a/respx/patterns.py b/respx/patterns.py
index 3bf8d9b..75cb815 100644
--- a/respx/patterns.py
+++ b/respx/patterns.py
@@ -25,6 +25,8 @@ from urllib.parse import urljoin
 
 import httpx
 
+from respx.utils import MultiItems, decode_data
+
 from .types import (
     URL as RawURL,
     CookieTypes,
@@ -536,14 +538,16 @@ class JSON(ContentMixin, PathPattern):
         return jsonlib.dumps(value, sort_keys=True)
 
 
-class Data(ContentMixin, Pattern):
-    lookups = (Lookup.EQUAL,)
+class Data(MultiItemsMixin, Pattern):
+    lookups = (Lookup.EQUAL, Lookup.CONTAINS)
     key = "data"
-    value: bytes
+    value: MultiItems
+
+    def clean(self, value: Dict) -> MultiItems:
+        return MultiItems(value)
 
-    def clean(self, value: Dict) -> bytes:
-        request = httpx.Request("POST", "/", data=value)
-        data = request.read()
+    def parse(self, request: httpx.Request) -> Any:
+        data, _ = decode_data(request)
         return data
 
 
diff --git a/respx/utils.py b/respx/utils.py
new file mode 100644
index 0000000..434c30d
--- /dev/null
+++ b/respx/utils.py
@@ -0,0 +1,73 @@
+import email
+from email.message import Message
+from typing import List, Tuple, cast
+from urllib.parse import parse_qsl
+
+import httpx
+
+
+class MultiItems(dict):
+    def get_list(self, key: str) -> List[str]:
+        try:
+            return [self[key]]
+        except KeyError:  # pragma: no cover
+            return []
+
+    def multi_items(self) -> List[Tuple[str, str]]:
+        return list(self.items())
+
+
+def _parse_multipart_form_data(
+    content: bytes, *, content_type: str, encoding: str
+) -> Tuple[MultiItems, MultiItems]:
+    form_data = b"\r\n".join(
+        (
+            b"MIME-Version: 1.0",
+            b"Content-Type: " + content_type.encode(encoding),
+            b"\r\n" + content,
+        )
+    )
+    data = MultiItems()
+    files = MultiItems()
+    for payload in email.message_from_bytes(form_data).get_payload():
+        payload = cast(Message, payload)
+        name = payload.get_param("name", header="Content-Disposition")
+        filename = payload.get_filename()
+        content_type = payload.get_content_type()
+        value = payload.get_payload(decode=True)
+        assert isinstance(value, bytes)
+        if content_type.startswith("text/") and filename is None:
+            # Text field
+            data[name] = value.decode(payload.get_content_charset() or "utf-8")
+        else:
+            # File field
+            files[name] = filename, value
+
+    return data, files
+
+
+def _parse_urlencoded_data(content: bytes, *, encoding: str) -> MultiItems:
+    return MultiItems(
+        (key, value)
+        for key, value in parse_qsl(content.decode(encoding), keep_blank_values=True)
+    )
+
+
+def decode_data(request: httpx.Request) -> Tuple[MultiItems, MultiItems]:
+    content = request.read()
+    content_type = request.headers.get("Content-Type", "")
+
+    if content_type.startswith("multipart/form-data"):
+        data, files = _parse_multipart_form_data(
+            content,
+            content_type=content_type,
+            encoding=request.headers.encoding,
+        )
+    else:
+        data = _parse_urlencoded_data(
+            content,
+            encoding=request.headers.encoding,
+        )
+        files = MultiItems()
+
+    return data, files

diff --git a/tests/test_api.py b/tests/test_api.py
index c126408..597c589 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -263,6 +263,15 @@ def test_json_post_body():
         assert get_route.called
 
 
+def test_data_post_body():
+    with respx.mock:
+        url = "https://foo.bar/"
+        route = respx.post(url, data={"foo": "bar"}) % 201
+        response = httpx.post(url, data={"foo": "bar"}, files={"file": b"..."})
+        assert response.status_code == 201
+        assert route.called
+
+
 async def test_raising_content(client):
     async with MockRouter() as respx_mock:
         url = "https://foo.bar/"
diff --git a/tests/test_patterns.py b/tests/test_patterns.py
index e704b40..f492307 100644
--- a/tests/test_patterns.py
+++ b/tests/test_patterns.py
@@ -323,14 +323,69 @@ def test_content_pattern(lookup, content, expected):
 
 
 @pytest.mark.parametrize(
-    ("lookup", "data", "expected"),
+    ("lookup", "data", "request_data", "expected"),
     [
-        (Lookup.EQUAL, {"foo": "bar", "ham": "spam"}, True),
+        (
+            Lookup.EQUAL,
+            {"foo": "bar", "ham": "spam"},
+            None,
+            True,
+        ),
+        (
+            Lookup.EQUAL,
+            {"foo": "bar", "ham": "spam"},
+            {"ham": "spam", "foo": "bar"},
+            True,
+        ),
+        (
+            Lookup.EQUAL,
+            {"uni": "äpple", "mixed": "Geh&#xE4;usegröße"},
+            None,
+            True,
+        ),
+        (
+            Lookup.EQUAL,
+            {"blank_value": ""},
+            None,
+            True,
+        ),
+        (
+            Lookup.EQUAL,
+            {"x": "a"},
+            {"x": "b"},
+            False,
+        ),
+        (
+            Lookup.EQUAL,
+            {"foo": "bar"},
+            {"foo": "bar", "ham": "spam"},
+            False,
+        ),
+        (
+            Lookup.CONTAINS,
+            {"foo": "bar"},
+            {"foo": "bar", "ham": "spam"},
+            True,
+        ),
     ],
 )
-def test_data_pattern(lookup, data, expected):
-    request = httpx.Request("POST", "https://foo.bar/", data=data)
-    match = Data(data, lookup=lookup).match(request)
+def test_data_pattern(lookup, data, request_data, expected):
+    request_with_data = httpx.Request(
+        "POST",
+        "https://foo.bar/",
+        data=request_data or data,
+    )
+    request_with_data_and_files = httpx.Request(
+        "POST",
+        "https://foo.bar/",
+        data=request_data or data,
+        files={"upload-file": ("report.xls", b"<...>", "application/vnd.ms-excel")},
+    )
+
+    match = Data(data, lookup=lookup).match(request_with_data)
+    assert bool(match) is expected
+
+    match = Data(data, lookup=lookup).match(request_with_data_and_files)
     assert bool(match) is expected
 
 

Support multipart encoding in Data pattern
`Data` pattern only handles urlencoded data, not multipart encoded. This is troublesome because by addition of `files` to the request, the encoding changes and `Data` pattern fails to match the request.

### MWE
```python
import httpx
import respx

with respx.mock() as rsps:
    rsps.post('http://example.org/', data={'answer': '42'})
    httpx.post('http://example.org', data={'answer': '42'})  # OK

with respx.mock() as rsps:
    rsps.post('http://example.org/', data={'answer': '42'})
    httpx.post('http://example.org', data={'answer': '42'}, files={'file': b'content'})
    # >>> respx.models.AllMockedAssertionError: RESPX: <Request('POST', 'http://example.org/')> not mocked!
```

Related to #115
tests/test_api.py::test_data_post_body tests/test_patterns.py::test_data_pattern[Lookup.EQUAL-data0-None-True] tests/test_patterns.py::test_data_pattern[Lookup.EQUAL-data1-request_data1-True] tests/test_patterns.py::test_data_pattern[Lookup.EQUAL-data2-None-True] tests/test_patterns.py::test_data_pattern[Lookup.EQUAL-data3-None-True] tests/test_patterns.py::test_data_pattern[Lookup.CONTAINS-data6-request_data6-True]
tests/test_api.py::test_http_methods tests/test_api.py::test_url_match[https://foo.bar-https://foo.bar] tests/test_api.py::test_url_match[https://foo.bar/baz/-None] tests/test_api.py::test_url_match[https://foo.bar/baz/-] tests/test_api.py::test_url_match[https://foo.bar/baz/-https://foo.bar/baz/] tests/test_api.py::test_url_match[https://foo.bar/baz/-^https://foo.bar/\\w+/$] tests/test_api.py::test_url_match[https://foo.bar/baz/-pattern5] tests/test_api.py::test_url_match[https://foo.bar:443/baz/-pattern6] tests/test_api.py::test_url_match[https://foo.bar/%08-https://foo.bar/%08] tests/test_api.py::test_invalid_url_pattern tests/test_api.py::test_repeated_pattern tests/test_api.py::test_status_code tests/test_api.py::test_headers[headers0-None-expected0] tests/test_api.py::test_headers[headers1-None-expected1] tests/test_api.py::test_headers[headers2-ham/spam-expected2] tests/test_api.py::test_text_encoding[eldr\xc3\xa4v-eldr\xe4v] tests/test_api.py::test_text_encoding[\xe4pple-\xe4pple] tests/test_api.py::test_text_encoding[Geh&#xE4;usegr\xf6\xdfe-Geh&#xE4;usegr\xf6\xdfe] tests/test_api.py::test_content_variants[content-foobar-None0] tests/test_api.py::test_content_variants[content-foobar-None1] tests/test_api.py::test_content_variants[json-value2-application/json] tests/test_api.py::test_content_variants[json-value3-application/json] tests/test_api.py::test_content_variants[text-foobar-text/plain; tests/test_api.py::test_content_variants[html-<strong>foobar</strong>-text/html; tests/test_api.py::test_json_content[content0-headers0-expected_headers0] tests/test_api.py::test_json_content[content1-headers1-expected_headers1] tests/test_api.py::test_json_post_body tests/test_api.py::test_raising_content tests/test_api.py::test_callable_content tests/test_api.py::test_request_callback tests/test_api.py::test_pass_through[httpcore-route0-True] tests/test_api.py::test_pass_through[httpx-route1-True] tests/test_api.py::test_pass_through[httpcore-route2-False] tests/test_api.py::test_pass_through[httpcore-route3-True] tests/test_api.py::test_parallel_requests tests/test_api.py::test_method_case[DELETE-delete] tests/test_api.py::test_method_case[delete-delete] tests/test_api.py::test_method_case[GET-get] tests/test_api.py::test_method_case[get-get] tests/test_api.py::test_method_case[HEAD-head] tests/test_api.py::test_method_case[head-head] tests/test_api.py::test_method_case[OPTIONS-options] tests/test_api.py::test_method_case[options-options] tests/test_api.py::test_method_case[PATCH-patch] tests/test_api.py::test_method_case[patch-patch] tests/test_api.py::test_method_case[POST-post] tests/test_api.py::test_method_case[post-post] tests/test_api.py::test_method_case[PUT-put] tests/test_api.py::test_method_case[put-put] tests/test_api.py::test_pop tests/test_api.py::test_params_match[https://foo/-foo=bar-https://foo/-foo=bar0] tests/test_api.py::test_params_match[https://foo/-foo=bar-https://foo/-foo=bar1] tests/test_api.py::test_params_match[https://foo/-params2-https://foo/-call_params2] tests/test_api.py::test_params_match[https://foo/-params3-https://foo/-call_params3] tests/test_api.py::test_params_match[https://foo/-params4-https://foo/-call_params4] tests/test_api.py::test_params_match[https://foo?foo=bar-baz=qux-https://foo?foo=bar-baz=qux] tests/test_api.py::test_params_match[https://foo?foo=bar-baz=qux-https://foo?foo=bar&baz=qux-None] tests/test_api.py::test_params_match[https://foo/(\\w+)/-foo=bar-https://foo/bar/-foo=bar] tests/test_api.py::test_params_match[url8-foo=bar-https://foo/-foo=bar] tests/test_api.py::test_params_match[url9-baz=qux-https://foo?foo=bar&baz=qux-None] tests/test_api.py::test_build_url_base[None-https://foo.bar/baz/] tests/test_api.py::test_build_url_base[-https://foo.bar/baz/] tests/test_api.py::test_build_url_base[https://foo.bar-baz/] tests/test_api.py::test_build_url_base[https://foo.bar/-baz/] tests/test_api.py::test_build_url_base[https://foo.bar/-/baz/] tests/test_api.py::test_build_url_base[https://foo.bar/baz/-None] tests/test_api.py::test_build_url_base[https://foo.bar/-/(\\w+)/] tests/test_api.py::test_add tests/test_api.py::test_respond tests/test_api.py::test_async_post_content[kwargs0] tests/test_api.py::test_async_post_content[kwargs1] tests/test_api.py::test_async_post_content[kwargs2] tests/test_api.py::test_async_post_content[kwargs3] tests/test_api.py::test_async_post_content[kwargs4] tests/test_patterns.py::test_bitwise_and tests/test_patterns.py::test_bitwise_operators[GET-https://foo.bar/-True] tests/test_patterns.py::test_bitwise_operators[GET-https://foo.bar/baz/-False] tests/test_patterns.py::test_bitwise_operators[POST-https://foo.bar/-True] tests/test_patterns.py::test_bitwise_operators[POST-https://ham.spam/-True] tests/test_patterns.py::test_bitwise_operators[PATCH-https://foo.bar/-True] tests/test_patterns.py::test_bitwise_operators[PUT-https://foo.bar/-False] tests/test_patterns.py::test_match_context tests/test_patterns.py::test_noop_pattern tests/test_patterns.py::test_m_pattern[kwargs0-https://foo.bar/-True] tests/test_patterns.py::test_m_pattern[kwargs1-https://foo.bar/?x=y-False] tests/test_patterns.py::test_m_pattern[kwargs2-https://foo.bar/?x=y-True] tests/test_patterns.py::test_method_pattern[Lookup.EQUAL-GET-True] tests/test_patterns.py::test_method_pattern[Lookup.EQUAL-get-True] tests/test_patterns.py::test_method_pattern[Lookup.EQUAL-POST-False] tests/test_patterns.py::test_method_pattern[Lookup.IN-value3-True] tests/test_patterns.py::test_method_pattern[Lookup.IN-value4-False] tests/test_patterns.py::test_headers_pattern[Lookup.CONTAINS-headers0-request_headers0-True] tests/test_patterns.py::test_headers_pattern[Lookup.CONTAINS-headers1--False] tests/test_patterns.py::test_headers_pattern_hash tests/test_patterns.py::test_cookies_pattern[Lookup.CONTAINS-cookies0-request_cookies0-True] tests/test_patterns.py::test_cookies_pattern[Lookup.CONTAINS-cookies1-request_cookies1-False] tests/test_patterns.py::test_cookies_pattern[Lookup.EQUAL-cookies2-request_cookies2-True] tests/test_patterns.py::test_cookies_pattern[Lookup.EQUAL-cookies3-request_cookies3-True] tests/test_patterns.py::test_cookies_pattern[Lookup.EQUAL-cookies4-request_cookies4-True] tests/test_patterns.py::test_cookies_pattern[Lookup.EQUAL-cookies5-None-True] tests/test_patterns.py::test_cookies_pattern[Lookup.EQUAL-cookies6-request_cookies6-False] tests/test_patterns.py::test_cookies_pattern__hash tests/test_patterns.py::test_scheme_pattern[Lookup.EQUAL-https-True] tests/test_patterns.py::test_scheme_pattern[Lookup.EQUAL-HTTPS-True] tests/test_patterns.py::test_scheme_pattern[Lookup.EQUAL-http-False] tests/test_patterns.py::test_scheme_pattern[Lookup.IN-scheme3-True] tests/test_patterns.py::test_host_pattern[Lookup.EQUAL-foo.bar-True] tests/test_patterns.py::test_host_pattern[Lookup.EQUAL-ham.spam-False] tests/test_patterns.py::test_host_pattern[Lookup.REGEX-.+\\.bar-True] tests/test_patterns.py::test_port_pattern[Lookup.EQUAL-443-https://foo.bar/-True] tests/test_patterns.py::test_port_pattern[Lookup.EQUAL-80-https://foo.bar/-False] tests/test_patterns.py::test_port_pattern[Lookup.EQUAL-80-http://foo.bar/-True] tests/test_patterns.py::test_port_pattern[Lookup.EQUAL-8080-https://foo.bar:8080/baz/-True] tests/test_patterns.py::test_port_pattern[Lookup.EQUAL-8080-https://foo.bar/baz/-False] tests/test_patterns.py::test_port_pattern[Lookup.EQUAL-22-//foo.bar:22/baz/-True] tests/test_patterns.py::test_port_pattern[Lookup.EQUAL-None-//foo.bar/-True] tests/test_patterns.py::test_port_pattern[Lookup.IN-port7-http://foo.bar/-True] tests/test_patterns.py::test_port_pattern[Lookup.IN-port8-https://foo.bar/-True] tests/test_patterns.py::test_port_pattern[Lookup.IN-port9-https://foo.bar:8080/-False] tests/test_patterns.py::test_path_pattern tests/test_patterns.py::test_params_pattern[Lookup.CONTAINS--https://foo.bar/-True] tests/test_patterns.py::test_params_pattern[Lookup.CONTAINS-x=1-https://foo.bar/?x=1-True] tests/test_patterns.py::test_params_pattern[Lookup.CONTAINS-x=-https://foo.bar/?x=1-False] tests/test_patterns.py::test_params_pattern[Lookup.CONTAINS-x=-https://foo.bar/?x=-True] tests/test_patterns.py::test_params_pattern[Lookup.CONTAINS-y=2-https://foo.bar/?x=1-False] tests/test_patterns.py::test_params_pattern[Lookup.CONTAINS-params5-https://foo.bar/?x=1-True] tests/test_patterns.py::test_params_pattern[Lookup.CONTAINS-params6-https://foo.bar/?x=1-True] tests/test_patterns.py::test_params_pattern[Lookup.CONTAINS-params7-https://foo.bar/?x=1-False] tests/test_patterns.py::test_params_pattern[Lookup.CONTAINS-params8-https://foo.bar/?x=1&y=2-True] tests/test_patterns.py::test_params_pattern[Lookup.CONTAINS-params9-https://foo.bar/?x=1-False] tests/test_patterns.py::test_params_pattern[Lookup.CONTAINS-params10-https://foo.bar/?x=1&x=2-True] tests/test_patterns.py::test_params_pattern[Lookup.CONTAINS-params11-https://foo.bar/?x=2&x=3-False] tests/test_patterns.py::test_params_pattern[Lookup.CONTAINS-x=1&y=2-https://foo.bar/?x=1-False] tests/test_patterns.py::test_params_pattern[Lookup.EQUAL--https://foo.bar/-True] tests/test_patterns.py::test_params_pattern[Lookup.EQUAL-x-https://foo.bar/?x-True] tests/test_patterns.py::test_params_pattern[Lookup.EQUAL-x=-https://foo.bar/?x=-True] tests/test_patterns.py::test_params_pattern[Lookup.EQUAL-x=1-https://foo.bar/?x=1-True] tests/test_patterns.py::test_params_pattern[Lookup.EQUAL-y=2-https://foo.bar/?x=1-False] tests/test_patterns.py::test_params_pattern[Lookup.EQUAL-params18-https://foo.bar/?x=1-True] tests/test_patterns.py::test_params_pattern[Lookup.EQUAL-params19-https://foo.bar/?x=1-False] tests/test_patterns.py::test_params_pattern[Lookup.EQUAL-params20-https://foo.bar/?x=1-False] tests/test_patterns.py::test_params_pattern[Lookup.EQUAL-params21-https://foo.bar/-True] tests/test_patterns.py::test_params_pattern[Lookup.EQUAL-x=1&y=2-https://foo.bar/?x=1-False] tests/test_patterns.py::test_params_pattern[Lookup.EQUAL-y=2&x=1-https://foo.bar/?x=1&y=2-True] tests/test_patterns.py::test_params_pattern[Lookup.EQUAL-y=3&x=2&x=1-https://foo.bar/?x=1&x=2&y=3-False] tests/test_patterns.py::test_params_pattern[Lookup.EQUAL-y=3&x=1&x=2-https://foo.bar/?x=1&x=2&y=3-True] tests/test_patterns.py::test_params_pattern[Lookup.CONTAINS-x=2&x=1-https://foo.bar/?x=1&x=2&y=3-False] tests/test_patterns.py::test_params_pattern[Lookup.CONTAINS-x=1&x=2-https://foo.bar/?x=1&x=2&x=3-False] tests/test_patterns.py::test_params_pattern[Lookup.CONTAINS-x=1&x=2-https://foo.bar/?x=1&x=2&y=3-True] tests/test_patterns.py::test_params_pattern_hash tests/test_patterns.py::test_url_pattern[Lookup.REGEX-https?://a.b/(?P<c>\\w+)/-context0-http://a.b/c/-True] tests/test_patterns.py::test_url_pattern[Lookup.REGEX-^https://a.b/.+$-context1-https://a.b/c/-True] tests/test_patterns.py::test_url_pattern[Lookup.REGEX-https://a.b/c/-context2-https://x.y/c/-False] tests/test_patterns.py::test_url_pattern[Lookup.EQUAL-https://a.b/c/-context3-https://a.b/c/-True] tests/test_patterns.py::test_url_pattern[Lookup.EQUAL-https://a.b/x/-context4-https://a.b/c/-False] tests/test_patterns.py::test_url_pattern[Lookup.EQUAL-https://a.b?x=y-context5-https://a.b/?x=y-True] tests/test_patterns.py::test_url_pattern[Lookup.EQUAL-https://a.b/?x=y-context6-https://a.b?x=y-True] tests/test_patterns.py::test_url_pattern[Lookup.STARTS_WITH-https://a.b/b-context7-https://a.b/baz/-True] tests/test_patterns.py::test_url_pattern[Lookup.STARTS_WITH-http://a.b/baz/-context8-https://a.b/baz/-False] tests/test_patterns.py::test_url_pattern[Lookup.EQUAL-value9-context9-https://[FE80::1]-True] tests/test_patterns.py::test_url_pattern_invalid tests/test_patterns.py::test_url_pattern_hash tests/test_patterns.py::test_content_pattern[Lookup.EQUAL-foobar-True0] tests/test_patterns.py::test_content_pattern[Lookup.EQUAL-foobar-True1] tests/test_patterns.py::test_content_pattern[Lookup.CONTAINS-bar-True0] tests/test_patterns.py::test_content_pattern[Lookup.CONTAINS-bar-True1] tests/test_patterns.py::test_content_pattern[Lookup.CONTAINS-baz-False] tests/test_patterns.py::test_data_pattern[Lookup.EQUAL-data4-request_data4-False] tests/test_patterns.py::test_data_pattern[Lookup.EQUAL-data5-request_data5-False] tests/test_patterns.py::test_json_pattern[Lookup.EQUAL-value0-json0-True] tests/test_patterns.py::test_json_pattern[Lookup.EQUAL-value1-json1-False] tests/test_patterns.py::test_json_pattern[Lookup.EQUAL-value2-json2-True] tests/test_patterns.py::test_json_pattern[Lookup.EQUAL-value3-json3-False] tests/test_patterns.py::test_json_pattern[Lookup.EQUAL-json-string-json-string-True] tests/test_patterns.py::test_json_pattern_path[json0-foo__bar-baz-True] tests/test_patterns.py::test_json_pattern_path[json1-x-value1-True] tests/test_patterns.py::test_json_pattern_path[json2-ham__1__egg-yolk-True] tests/test_patterns.py::test_json_pattern_path[json3-0__name-jonas-True] tests/test_patterns.py::test_json_pattern_path[json4-pk-123-True] tests/test_patterns.py::test_json_pattern_path[json5-foo__ham-spam-False] tests/test_patterns.py::test_json_pattern_path[json6-1__name-lundberg-False] tests/test_patterns.py::test_invalid_pattern tests/test_patterns.py::test_iter_pattern tests/test_patterns.py::test_parse_url_patterns tests/test_patterns.py::test_merge_patterns tests/test_patterns.py::test_unique_pattern_key