diff --git a/httpie/uploads.py b/httpie/uploads.py index 4a993b3a25..55e18de31b 100644 --- a/httpie/uploads.py +++ b/httpie/uploads.py @@ -188,6 +188,39 @@ def _prepare_file_for_upload( return file +def _normalize_content_length_header( + value: Optional[Union[int, bytes, str]] +) -> Optional[int]: + if value is None: + return None + + if isinstance(value, int): + if value < 0: + raise ValueError('Invalid Content-Length header value') + return value + + if isinstance(value, bytes): + try: + value = value.decode('ascii') + except UnicodeDecodeError as exc: + raise ValueError('Invalid Content-Length header value') from exc + + if isinstance(value, str): + value = value.strip() + if value == '': + raise ValueError('Invalid Content-Length header value') + + try: + normalized = int(value) + except (TypeError, ValueError) as exc: + raise ValueError('Invalid Content-Length header value') from exc + + if normalized < 0: + raise ValueError('Invalid Content-Length header value') + + return normalized + + def prepare_request_body( env: Environment, raw_body: Union[str, bytes, IO, 'MultipartEncoder', RequestDataDict], @@ -210,6 +243,10 @@ def prepare_request_body( else: return body + content_length_header_value = _normalize_content_length_header( + content_length_header_value + ) + if is_file_like: return _prepare_file_for_upload( env, diff --git a/tests/test_uploads.py b/tests/test_uploads.py index e6bb80ac70..e16df2e3e9 100644 --- a/tests/test_uploads.py +++ b/tests/test_uploads.py @@ -12,6 +12,7 @@ from httpie.client import FORM_CONTENT_TYPE from httpie.compat import is_windows from httpie.status import ExitStatus +from httpie.uploads import prepare_request_body from .utils import ( MockEnvironment, StdinBytesIO, http, HTTP_OK, @@ -91,6 +92,34 @@ def test_chunked_raw(httpbin_with_chunked_support): assert 'Transfer-Encoding: chunked' in r +def test_content_length_header_normalization(monkeypatch): + env = MockEnvironment(stdin_isatty=False) + captured = {} + + def fake_prepare(env_arg, file, callback, chunked=False, content_length_header_value=None): + captured['value'] = content_length_header_value + return b'' + + monkeypatch.setattr('httpie.uploads._prepare_file_for_upload', fake_prepare) + + prepare_request_body( + env, + StdinBytesIO(b'data'), + lambda chunk: chunk, + content_length_header_value=b'4', + ) + assert captured['value'] == 4 + + with pytest.raises(ValueError): + prepare_request_body( + env, + StdinBytesIO(b'data'), + lambda chunk: chunk, + content_length_header_value=b'invalid', + ) + assert captured['value'] == 4 + + @contextlib.contextmanager def stdin_processes(httpbin, *args, warn_threshold=0.1): process_1 = subprocess.Popen(