Repository: aio-libs/aiohttp

Issue:
request.post() should raise HTTPRequestEntityTooLarge same as request.read()
`request.read()`

https://github.com/aio-libs/aiohttp/blob/0bc7d52ccdcc8014293130bb5be0a7075cf1c8bc/aiohttp/web_request.py#L526-L528

`request.post()`

https://github.com/aio-libs/aiohttp/blob/0bc7d52ccdcc8014293130bb5be0a7075cf1c8bc/aiohttp/web_request.py#L583-L585

Seems inconsistent to me that the two methods behave differently. `Request.post()` should raise `HTTPRequestEntityTooLarge` too. 

Or am I missing something?

Discussion:
samuelcolvin: by the way, when I run `make test`, I'm getting

```
tests/test_helpers.py:319: in <module>
    class TestCReify(ReifyMixin):
tests/test_helpers.py:320: in TestCReify
    reify = helpers.reify_c
E   AttributeError: module 'aiohttp.helpers' has no attribute 'reify_c'
```

This seems to be new, I wasn't getting it before.
asvetlov: Perhaps you didn't compile cython extensions.
`make test` should do it.
Also please don't forget `git submodule init` / `git submodule update`
samuelcolvin: `git submodule init` / `git submodule update` was what I needed.
codecov-io: # [Codecov](https://codecov.io/gh/aio-libs/aiohttp/pull/3093?src=pr&el=h1) Report
> Merging [#3093](https://codecov.io/gh/aio-libs/aiohttp/pull/3093?src=pr&el=desc) into [master](https://codecov.io/gh/aio-libs/aiohttp/commit/69f5c560b6a515c83b4498d799d0c8b4ed56bd8e?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/aio-libs/aiohttp/pull/3093/graphs/tree.svg?width=650&token=LJn22G3rd7&height=150&src=pr)](https://codecov.io/gh/aio-libs/aiohttp/pull/3093?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master    #3093   +/-   ##
=======================================
  Coverage   98.01%   98.01%           
=======================================
  Files          43       43           
  Lines        7807     7807           
  Branches     1352     1352           
=======================================
  Hits         7652     7652           
  Misses         56       56           
  Partials       99       99
```


| [Impacted Files](https://codecov.io/gh/aio-libs/aiohttp/pull/3093?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [aiohttp/web\_request.py](https://codecov.io/gh/aio-libs/aiohttp/pull/3093/diff?src=pr&el=tree#diff-YWlvaHR0cC93ZWJfcmVxdWVzdC5weQ==) | `98.67% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/aio-libs/aiohttp/pull/3093?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/aio-libs/aiohttp/pull/3093?src=pr&el=footer). Last update [69f5c56...ccd9dc5](https://codecov.io/gh/aio-libs/aiohttp/pull/3093?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/CHANGES/3087.bugfix b/CHANGES/3087.bugfix
new file mode 100644
index 000000000..617d5d55e
--- /dev/null
+++ b/CHANGES/3087.bugfix
@@ -0,0 +1,2 @@
+raise 413 "Payload Too Large" rather than raising ValueError in request.post()
+Add helpful debug message to 413 responses
diff --git a/CHANGES/3092.feature b/CHANGES/3092.feature
new file mode 100644
index 000000000..6a08d2849
--- /dev/null
+++ b/CHANGES/3092.feature
@@ -0,0 +1,1 @@
+Add type hints to HTTP client
\ No newline at end of file
diff --git a/aiohttp/client.py b/aiohttp/client.py
index 28d0f1142..7e5236dd4 100644
--- a/aiohttp/client.py
+++ b/aiohttp/client.py
@@ -8,7 +8,8 @@ import os
 import sys
 import traceback
 import warnings
-from collections.abc import Coroutine
+from collections.abc import Coroutine as CoroutineABC
+from typing import Any, Generator, Optional, Tuple
 
 import attr
 from multidict import CIMultiDict, MultiDict, MultiDictProxy, istr
@@ -25,7 +26,7 @@ from .client_reqrep import *  # noqa
 from .client_reqrep import ClientRequest, ClientResponse, _merge_ssl_params
 from .client_ws import ClientWebSocketResponse
 from .connector import *  # noqa
-from .connector import TCPConnector
+from .connector import BaseConnector, TCPConnector
 from .cookiejar import CookieJar
 from .helpers import (DEBUG, PY_36, CeilTimeout, TimeoutHandle,
                       proxies_from_env, sentinel, strip_auth_from_url)
@@ -34,6 +35,7 @@ from .http_websocket import WSHandshakeError, ws_ext_gen, ws_ext_parse
 from .streams import FlowControlDataQueue
 from .tcp_helpers import tcp_cork, tcp_nodelay
 from .tracing import Trace
+from .typedefs import StrOrURL
 
 
 __all__ = (client_exceptions.__all__ +  # noqa
@@ -137,7 +139,7 @@ class ClientSession:
         if cookies is not None:
             self._cookie_jar.update_cookies(cookies)
 
-        self._connector = connector
+        self._connector = connector  # type: BaseConnector
         self._connector_owner = connector_owner
         self._default_auth = auth
         self._version = version
@@ -216,7 +218,10 @@ class ClientSession:
                 context['source_traceback'] = self._source_traceback
             self._loop.call_exception_handler(context)
 
-    def request(self, method, url, **kwargs):
+    def request(self,
+                method: str,
+                url: StrOrURL,
+                **kwargs) -> '_RequestContextManager':
         """Perform HTTP request."""
         return _RequestContextManager(self._request(method, url, **kwargs))
 
@@ -506,7 +511,7 @@ class ClientSession:
                 )
             raise
 
-    def ws_connect(self, url, *,
+    def ws_connect(self, url: StrOrURL, *,
                    protocols=(),
                    timeout=10.0,
                    receive_timeout=None,
@@ -713,66 +718,72 @@ class ClientSession:
                     added_names.add(key)
         return result
 
-    def get(self, url, *, allow_redirects=True, **kwargs):
+    def get(self, url: StrOrURL, *, allow_redirects: bool=True,
+            **kwargs) -> '_RequestContextManager':
         """Perform HTTP GET request."""
         return _RequestContextManager(
             self._request(hdrs.METH_GET, url,
                           allow_redirects=allow_redirects,
                           **kwargs))
 
-    def options(self, url, *, allow_redirects=True, **kwargs):
+    def options(self, url: StrOrURL, *, allow_redirects: bool=True,
+                **kwargs) -> '_RequestContextManager':
         """Perform HTTP OPTIONS request."""
         return _RequestContextManager(
             self._request(hdrs.METH_OPTIONS, url,
                           allow_redirects=allow_redirects,
                           **kwargs))
 
-    def head(self, url, *, allow_redirects=False, **kwargs):
+    def head(self, url: StrOrURL, *, allow_redirects: bool=False,
+             **kwargs) -> '_RequestContextManager':
         """Perform HTTP HEAD request."""
         return _RequestContextManager(
             self._request(hdrs.METH_HEAD, url,
                           allow_redirects=allow_redirects,
                           **kwargs))
 
-    def post(self, url, *, data=None, **kwargs):
+    def post(self, url: StrOrURL,
+             *, data: Any=None, **kwargs) -> '_RequestContextManager':
         """Perform HTTP POST request."""
         return _RequestContextManager(
             self._request(hdrs.METH_POST, url,
                           data=data,
                           **kwargs))
 
-    def put(self, url, *, data=None, **kwargs):
+    def put(self, url: StrOrURL,
+            *, data: Any=None, **kwargs) -> '_RequestContextManager':
         """Perform HTTP PUT request."""
         return _RequestContextManager(
             self._request(hdrs.METH_PUT, url,
                           data=data,
                           **kwargs))
 
-    def patch(self, url, *, data=None, **kwargs):
+    def patch(self, url: StrOrURL,
+              *, data: Any=None, **kwargs) -> '_RequestContextManager':
         """Perform HTTP PATCH request."""
         return _RequestContextManager(
             self._request(hdrs.METH_PATCH, url,
                           data=data,
                           **kwargs))
 
-    def delete(self, url, **kwargs):
+    def delete(self, url: StrOrURL, **kwargs) -> '_RequestContextManager':
         """Perform HTTP DELETE request."""
         return _RequestContextManager(
             self._request(hdrs.METH_DELETE, url,
                           **kwargs))
 
-    async def close(self):
+    async def close(self) -> None:
         """Close underlying connector.
 
         Release all acquired resources.
         """
         if not self.closed:
-            if self._connector_owner:
+            if self._connector is not None and self._connector_owner:
                 self._connector.close()
             self._connector = None
 
     @property
-    def closed(self):
+    def closed(self) -> bool:
         """Is client session closed.
 
         A readonly property.
@@ -780,26 +791,26 @@ class ClientSession:
         return self._connector is None or self._connector.closed
 
     @property
-    def connector(self):
+    def connector(self) -> Optional[BaseConnector]:
         """Connector instance used for the session."""
         return self._connector
 
     @property
-    def cookie_jar(self):
+    def cookie_jar(self) -> CookieJar:
         """The session cookies."""
         return self._cookie_jar
 
     @property
-    def version(self):
+    def version(self) -> Tuple[int, int]:
         """The session HTTP protocol version."""
         return self._version
 
     @property
-    def loop(self):
+    def loop(self) -> asyncio.AbstractEventLoop:
         """Session's loop."""
         return self._loop
 
-    def detach(self):
+    def detach(self) -> None:
         """Detach connector from session without closing the former.
 
         Session is switched to closed state anyway.
@@ -813,14 +824,14 @@ class ClientSession:
         # __exit__ should exist in pair with __enter__ but never executed
         pass  # pragma: no cover
 
-    async def __aenter__(self):
+    async def __aenter__(self) -> 'ClientSession':
         return self
 
     async def __aexit__(self, exc_type, exc_val, exc_tb):
         await self.close()
 
 
-class _BaseRequestContextManager(Coroutine):
+class _BaseRequestContextManager(CoroutineABC):
 
     __slots__ = ('_coro', '_resp')
 
@@ -836,14 +847,14 @@ class _BaseRequestContextManager(Coroutine):
     def close(self):
         return self._coro.close()
 
-    def __await__(self):
+    def __await__(self) -> Generator[Any, None, ClientResponse]:
         ret = self._coro.__await__()
         return ret
 
     def __iter__(self):
         return self.__await__()
 
-    async def __aenter__(self):
+    async def __aenter__(self) -> ClientResponse:
         self._resp = await self._coro
         return self._resp
 
@@ -872,7 +883,7 @@ class _SessionRequestContextManager:
         self._resp = None
         self._session = session
 
-    async def __aenter__(self):
+    async def __aenter__(self) -> ClientResponse:
         self._resp = await self._coro
         return self._resp
 
@@ -899,7 +910,7 @@ def request(method, url, *,
             loop=None,
             read_until_eof=True,
             proxy=None,
-            proxy_auth=None):
+            proxy_auth=None) -> _SessionRequestContextManager:
     """Constructs and sends a request. Returns response object.
     method - HTTP method
     url - request url
diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py
index 6d1ff7bfc..f9c6674c4 100644
--- a/aiohttp/client_reqrep.py
+++ b/aiohttp/client_reqrep.py
@@ -1,7 +1,6 @@
 import asyncio
 import codecs
 import io
-import json
 import re
 import sys
 import traceback
@@ -9,6 +8,7 @@ import warnings
 from hashlib import md5, sha1, sha256
 from http.cookies import CookieError, Morsel, SimpleCookie
 from types import MappingProxyType
+from typing import Any, Optional, Tuple
 
 import attr
 from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy
@@ -22,6 +22,8 @@ from .formdata import FormData
 from .helpers import PY_36, HeadersMixin, TimerNoop, noop, reify, set_result
 from .http import SERVER_SOFTWARE, HttpVersion10, HttpVersion11, StreamWriter
 from .log import client_logger
+from .streams import StreamReader  # noqa
+from .typedefs import DEFAULT_JSON_DECODER, JSONDecoder, RawHeaders
 
 
 try:
@@ -578,15 +580,14 @@ class ClientResponse(HeadersMixin):
 
     # from the Status-Line of the response
     version = None  # HTTP-Version
-    status = None   # Status-Code
+    status = None   # type: int  # Status-Code
     reason = None   # Reason-Phrase
 
-    content = None  # Payload stream
-    _headers = None  # Response headers, CIMultiDictProxy
-    _raw_headers = None  # Response raw headers, a sequence of pairs
+    content = None  # type: StreamReader  # Payload stream
+    _headers = None  # type: CIMultiDictProxy  # Response headers
+    _raw_headers = None  # type: RawHeaders  # Response raw headers
 
     _connection = None  # current connection
-    _reader = None     # input stream
     _source_traceback = None
     # setted up by ClientRequest after ClientResponse object creation
     # post-init stage allows to not change ctor signature
@@ -604,7 +605,7 @@ class ClientResponse(HeadersMixin):
 
         self._real_url = url
         self._url = url.with_fragment(None)
-        self._body = None
+        self._body = None  # type: bytes
         self._writer = writer
         self._continue = continue100  # None by default
         self._closed = True
@@ -619,37 +620,37 @@ class ClientResponse(HeadersMixin):
             self._source_traceback = traceback.extract_stack(sys._getframe(1))
 
     @reify
-    def url(self):
+    def url(self) -> URL:
         return self._url
 
     @reify
-    def url_obj(self):
+    def url_obj(self) -> URL:
         warnings.warn(
             "Deprecated, use .url #1654", DeprecationWarning, stacklevel=2)
         return self._url
 
     @reify
-    def real_url(self):
+    def real_url(self) -> URL:
         return self._real_url
 
     @reify
-    def host(self):
+    def host(self) -> str:
         return self._url.host
 
     @reify
-    def headers(self):
+    def headers(self) -> CIMultiDictProxy:
         return self._headers
 
     @reify
-    def raw_headers(self):
+    def raw_headers(self) -> RawHeaders:
         return self._raw_headers
 
     @reify
-    def request_info(self):
+    def request_info(self) -> RequestInfo:
         return self._request_info
 
     @reify
-    def content_disposition(self):
+    def content_disposition(self) -> Optional[ContentDisposition]:
         raw = self._headers.get(hdrs.CONTENT_DISPOSITION)
         if raw is None:
             return None
@@ -700,30 +701,38 @@ class ClientResponse(HeadersMixin):
         return self._connection
 
     @reify
-    def history(self):
+    def history(self) -> Tuple['ClientResponse', ...]:
         """A sequence of of responses, if redirects occurred."""
         return self._history
 
     @reify
-    def links(self):
+    def links(self) -> MultiDictProxy:
         links_str = ", ".join(self.headers.getall("link", []))
 
-        links = MultiDict()
+        links = MultiDict()  # type: MultiDict
 
         if not links_str:
             return MultiDictProxy(links)
 
         for val in re.split(r",(?=\s*<)", links_str):
-            url, params = re.match(r"\s*<(.*)>(.*)", val).groups()
-            params = params.split(";")[1:]
+            match = re.match(r"\s*<(.*)>(.*)", val)
+            if match is None:  # pragma: no cover
+                # the check exists to suppress mypy error
+                continue
+            url, params_str = match.groups()
+            params = params_str.split(";")[1:]
 
-            link = MultiDict()
+            link = MultiDict()  # type: MultiDict
 
             for param in params:
-                key, _, value, _ = re.match(
+                match = re.match(
                     r"^\s*(\S*)\s*=\s*(['\"]?)(.*?)(\2)\s*$",
                     param, re.M
-                ).groups()
+                )
+                if match is None:  # pragma: no cover
+                    # the check exists to suppress mypy error
+                    continue
+                key, _, value, _ = match.groups()
 
                 link.add(key, value)
 
@@ -802,10 +811,10 @@ class ClientResponse(HeadersMixin):
         self._cleanup_writer()
 
     @property
-    def closed(self):
+    def closed(self) -> bool:
         return self._closed
 
-    def close(self):
+    def close(self) -> None:
         if not self._released:
             self._notify_content()
         if self._closed:
@@ -820,7 +829,7 @@ class ClientResponse(HeadersMixin):
             self._connection = None
         self._cleanup_writer()
 
-    def release(self):
+    def release(self) -> Any:
         if not self._released:
             self._notify_content()
         if self._closed:
@@ -834,7 +843,7 @@ class ClientResponse(HeadersMixin):
         self._cleanup_writer()
         return noop()
 
-    def raise_for_status(self):
+    def raise_for_status(self) -> None:
         if 400 <= self.status:
             raise ClientResponseError(
                 self.request_info,
@@ -856,7 +865,7 @@ class ClientResponse(HeadersMixin):
                 ClientConnectionError('Connection closed'))
         self._released = True
 
-    async def wait_for_close(self):
+    async def wait_for_close(self) -> None:
         if self._writer is not None:
             try:
                 await self._writer
@@ -864,7 +873,7 @@ class ClientResponse(HeadersMixin):
                 self._writer = None
         self.release()
 
-    async def read(self):
+    async def read(self) -> bytes:
         """Read response payload."""
         if self._body is None:
             try:
@@ -879,7 +888,7 @@ class ClientResponse(HeadersMixin):
 
         return self._body
 
-    def get_encoding(self):
+    def get_encoding(self) -> str:
         ctype = self.headers.get(hdrs.CONTENT_TYPE, '').lower()
         mimetype = helpers.parse_mimetype(ctype)
 
@@ -900,7 +909,8 @@ class ClientResponse(HeadersMixin):
 
         return encoding
 
-    async def text(self, encoding=None, errors='strict'):
+    async def text(self,
+                   encoding: Optional[str]=None, errors: str='strict') -> str:
         """Read response payload and decode."""
         if self._body is None:
             await self.read()
@@ -910,8 +920,9 @@ class ClientResponse(HeadersMixin):
 
         return self._body.decode(encoding, errors=errors)
 
-    async def json(self, *, encoding=None, loads=json.loads,
-                   content_type='application/json'):
+    async def json(self, *, encoding: str=None,
+                   loads: JSONDecoder=DEFAULT_JSON_DECODER,
+                   content_type: Optional[str]='application/json') -> Any:
         """Read and decodes JSON response."""
         if self._body is None:
             await self.read()
@@ -935,7 +946,7 @@ class ClientResponse(HeadersMixin):
 
         return loads(stripped.decode(encoding))
 
-    async def __aenter__(self):
+    async def __aenter__(self) -> 'ClientResponse':
         return self
 
     async def __aexit__(self, exc_type, exc_val, exc_tb):
diff --git a/aiohttp/client_ws.py b/aiohttp/client_ws.py
index 3ce3221d2..7a9a0f9ce 100644
--- a/aiohttp/client_ws.py
+++ b/aiohttp/client_ws.py
@@ -1,7 +1,7 @@
 """WebSocket client for asyncio."""
 
 import asyncio
-import json
+from typing import Any, Optional
 
 import async_timeout
 
@@ -10,6 +10,8 @@ from .helpers import call_later, set_result
 from .http import (WS_CLOSED_MESSAGE, WS_CLOSING_MESSAGE, WebSocketError,
                    WSMessage, WSMsgType)
 from .streams import EofStream
+from .typedefs import (DEFAULT_JSON_DECODER, DEFAULT_JSON_ENCODER, Byteish,
+                       JSONDecoder, JSONEncoder)
 
 
 class ClientWebSocketResponse:
@@ -80,11 +82,11 @@ class ClientWebSocketResponse:
             self._response.close()
 
     @property
-    def closed(self):
+    def closed(self) -> bool:
         return self._closed
 
     @property
-    def close_code(self):
+    def close_code(self) -> Optional[int]:
         return self._close_code
 
     @property
@@ -92,14 +94,14 @@ class ClientWebSocketResponse:
         return self._protocol
 
     @property
-    def compress(self):
+    def compress(self) -> int:
         return self._compress
 
     @property
-    def client_notakeover(self):
+    def client_notakeover(self) -> bool:
         return self._client_notakeover
 
-    def get_extra_info(self, name, default=None):
+    def get_extra_info(self, name: str, default: Any=None):
         """extra info from connection transport"""
         try:
             return self._response.connection.transport.get_extra_info(
@@ -107,30 +109,34 @@ class ClientWebSocketResponse:
         except Exception:
             return default
 
-    def exception(self):
+    def exception(self) -> Optional[BaseException]:
         return self._exception
 
-    async def ping(self, message='b'):
+    async def ping(self, message: bytes=b'') -> None:
         await self._writer.ping(message)
 
-    async def pong(self, message='b'):
+    async def pong(self, message: bytes=b'') -> None:
         await self._writer.pong(message)
 
-    async def send_str(self, data, compress=None):
+    async def send_str(self, data: str,
+                       compress: Optional[int]=None) -> None:
         if not isinstance(data, str):
             raise TypeError('data argument must be str (%r)' % type(data))
         await self._writer.send(data, binary=False, compress=compress)
 
-    async def send_bytes(self, data, compress=None):
+    async def send_bytes(self, data: Byteish,
+                         compress: Optional[int]=None) -> None:
         if not isinstance(data, (bytes, bytearray, memoryview)):
             raise TypeError('data argument must be byte-ish (%r)' %
                             type(data))
         await self._writer.send(data, binary=True, compress=compress)
 
-    async def send_json(self, data, compress=None, *, dumps=json.dumps):
+    async def send_json(self, data: Any,
+                        compress: Optional[int]=None,
+                        *, dumps: JSONEncoder=DEFAULT_JSON_ENCODER) -> None:
         await self.send_str(dumps(data), compress=compress)
 
-    async def close(self, *, code=1000, message=b''):
+    async def close(self, *, code: int=1000, message: bytes=b''):
         # we need to break `receive()` cycle first,
         # `close()` may be called from different task
         if self._waiting is not None and not self._closed:
@@ -177,7 +183,7 @@ class ClientWebSocketResponse:
         else:
             return False
 
-    async def receive(self, timeout=None):
+    async def receive(self, timeout: Optional[float]=None) -> WSMessage:
         while True:
             if self._waiting is not None:
                 raise RuntimeError(
@@ -238,7 +244,7 @@ class ClientWebSocketResponse:
 
             return msg
 
-    async def receive_str(self, *, timeout=None):
+    async def receive_str(self, *, timeout: Optional[float]=None) -> str:
         msg = await self.receive(timeout)
         if msg.type != WSMsgType.TEXT:
             raise TypeError(
@@ -246,7 +252,7 @@ class ClientWebSocketResponse:
                                                              msg.data))
         return msg.data
 
-    async def receive_bytes(self, *, timeout=None):
+    async def receive_bytes(self, *, timeout: Optional[float]=None) -> bytes:
         msg = await self.receive(timeout)
         if msg.type != WSMsgType.BINARY:
             raise TypeError(
@@ -254,14 +260,16 @@ class ClientWebSocketResponse:
                                                                msg.data))
         return msg.data
 
-    async def receive_json(self, *, loads=json.loads, timeout=None):
+    async def receive_json(self,
+                           *, loads: JSONDecoder=DEFAULT_JSON_DECODER,
+                           timeout: Optional[float]=None) -> Any:
         data = await self.receive_str(timeout=timeout)
         return loads(data)
 
-    def __aiter__(self):
+    def __aiter__(self) -> 'ClientWebSocketResponse':
         return self
 
-    async def __anext__(self):
+    async def __anext__(self) -> WSMessage:
         msg = await self.receive()
         if msg.type in (WSMsgType.CLOSE,
                         WSMsgType.CLOSING,
diff --git a/aiohttp/typedefs.py b/aiohttp/typedefs.py
index 3a620e49e..921ee17c0 100644
--- a/aiohttp/typedefs.py
+++ b/aiohttp/typedefs.py
@@ -1,11 +1,15 @@
+import json
 from typing import Any, Callable, Mapping, Tuple, Union  # noqa
 
 from multidict import CIMultiDict, CIMultiDictProxy
 from yarl import URL
 
 
-#  type helpers
+DEFAULT_JSON_ENCODER = json.dumps
+DEFAULT_JSON_DECODER = json.loads
+
 Byteish = Union[bytes, bytearray, memoryview]
+JSONEncoder = Callable[[Any], str]
 JSONDecoder = Callable[[str], Any]
 LooseHeaders = Union[Mapping, CIMultiDict, CIMultiDictProxy]
 RawHeaders = Tuple[Tuple[bytes, bytes], ...]
diff --git a/aiohttp/web_exceptions.py b/aiohttp/web_exceptions.py
index 4d948f4ee..d8a04a3f0 100644
--- a/aiohttp/web_exceptions.py
+++ b/aiohttp/web_exceptions.py
@@ -253,6 +253,14 @@ class HTTPPreconditionFailed(HTTPClientError):
 class HTTPRequestEntityTooLarge(HTTPClientError):
     status_code = 413
 
+    def __init__(self, max_size, actual_size, **kwargs):
+        kwargs.setdefault(
+            'text',
+            'Maximum request body size {} exceeded, '
+            'actual body size {}'.format(max_size, actual_size)
+        )
+        super().__init__(**kwargs)
+
 
 class HTTPRequestURITooLong(HTTPClientError):
     status_code = 414
diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py
index d3f341bb9..a5b213ca1 100644
--- a/aiohttp/web_request.py
+++ b/aiohttp/web_request.py
@@ -2,7 +2,6 @@ import asyncio
 import collections
 import datetime
 import io
-import json
 import re
 import socket
 import string
@@ -23,7 +22,8 @@ from . import hdrs
 from .helpers import DEBUG, ChainMapProxy, HeadersMixin, reify, sentinel
 from .multipart import MultipartReader
 from .streams import EmptyStreamReader, StreamReader
-from .typedefs import JSONDecoder, LooseHeaders, RawHeaders, StrOrURL
+from .typedefs import (DEFAULT_JSON_DECODER, JSONDecoder, LooseHeaders,
+                       RawHeaders, StrOrURL)
 from .web_exceptions import HTTPRequestEntityTooLarge
 from .web_urldispatcher import UrlMappingMatchInfo
 
@@ -31,9 +31,6 @@ from .web_urldispatcher import UrlMappingMatchInfo
 __all__ = ('BaseRequest', 'FileField', 'Request')
 
 
-DEFAULT_JSON_DECODER = json.loads
-
-
 @attr.s(frozen=True, slots=True)
 class FileField:
     name = attr.ib(type=str)
@@ -530,9 +527,13 @@ class BaseRequest(collections.MutableMapping, HeadersMixin):
             while True:
                 chunk = await self._payload.readany()
                 body.extend(chunk)
-                if self._client_max_size \
-                        and len(body) >= self._client_max_size:
-                    raise HTTPRequestEntityTooLarge
+                if self._client_max_size:
+                    body_size = len(body)
+                    if body_size >= self._client_max_size:
+                        raise HTTPRequestEntityTooLarge(
+                            max_size=self._client_max_size,
+                            actual_size=body_size
+                        )
                 if not chunk:
                     break
             self._read_bytes = bytes(body)
@@ -572,11 +573,11 @@ class BaseRequest(collections.MutableMapping, HeadersMixin):
 
         if content_type == 'multipart/form-data':
             multipart = await self.multipart()
+            max_size = self._client_max_size
 
             field = await multipart.next()
             while field is not None:
                 size = 0
-                max_size = self._client_max_size
                 content_type = field.headers.get(hdrs.CONTENT_TYPE)
 
                 if field.filename:
@@ -588,8 +589,10 @@ class BaseRequest(collections.MutableMapping, HeadersMixin):
                         tmp.write(chunk)
                         size += len(chunk)
                         if 0 < max_size < size:
-                            raise ValueError(
-                                'Maximum request body size exceeded')
+                            raise HTTPRequestEntityTooLarge(
+                                max_size=max_size,
+                                actual_size=size
+                            )
                         chunk = await field.read_chunk(size=2**16)
                     tmp.seek(0)
 
@@ -606,8 +609,10 @@ class BaseRequest(collections.MutableMapping, HeadersMixin):
                     out.add(field.name, value)
                     size += len(value)
                     if 0 < max_size < size:
-                        raise ValueError(
-                            'Maximum request body size exceeded')
+                        raise HTTPRequestEntityTooLarge(
+                            max_size=max_size,
+                            actual_size=size
+                        )
 
                 field = await multipart.next()
         else:


Test patch:
diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py
index 0e973bb4d..3651bb602 100644
--- a/tests/test_web_functional.py
+++ b/tests/test_web_functional.py
@@ -1451,7 +1451,8 @@ async def test_app_max_client_size(aiohttp_client):
         resp = await client.post('/', data=data)
     assert 413 == resp.status
     resp_text = await resp.text()
-    assert 'Request Entity Too Large' in resp_text
+    assert 'Maximum request body size 1048576 exceeded, ' \
+           'actual body size 1048591' in resp_text
 
 
 async def test_app_max_client_size_adjusted(aiohttp_client):
@@ -1476,7 +1477,8 @@ async def test_app_max_client_size_adjusted(aiohttp_client):
         resp = await client.post('/', data=too_large_data)
     assert 413 == resp.status
     resp_text = await resp.text()
-    assert 'Request Entity Too Large' in resp_text
+    assert 'Maximum request body size 2097152 exceeded, ' \
+           'actual body size 2097166' in resp_text
 
 
 async def test_app_max_client_size_none(aiohttp_client):
@@ -1507,30 +1509,27 @@ async def test_app_max_client_size_none(aiohttp_client):
 async def test_post_max_client_size(aiohttp_client):
 
     async def handler(request):
-        try:
-            await request.post()
-        except ValueError:
-            return web.Response()
-        raise web.HTTPBadRequest()
+        await request.post()
+        return web.Response()
 
     app = web.Application(client_max_size=10)
     app.router.add_post('/', handler)
     client = await aiohttp_client(app)
 
-    data = {"long_string": 1024 * 'x', 'file': io.BytesIO(b'test')}
+    data = {'long_string': 1024 * 'x', 'file': io.BytesIO(b'test')}
     resp = await client.post('/', data=data)
 
-    assert 200 == resp.status
+    assert 413 == resp.status
+    resp_text = await resp.text()
+    assert 'Maximum request body size 10 exceeded, ' \
+           'actual body size 1024' in resp_text
 
 
 async def test_post_max_client_size_for_file(aiohttp_client):
 
     async def handler(request):
-        try:
-            await request.post()
-        except ValueError:
-            return web.Response()
-        raise web.HTTPBadRequest()
+        await request.post()
+        return web.Response()
 
     app = web.Application(client_max_size=2)
     app.router.add_post('/', handler)
@@ -1539,7 +1538,7 @@ async def test_post_max_client_size_for_file(aiohttp_client):
     data = {'file': io.BytesIO(b'test')}
     resp = await client.post('/', data=data)
 
-    assert 200 == resp.status
+    assert 413 == resp.status
 
 
 async def test_response_with_bodypart(aiohttp_client):