diff --git a/httpie/client.py b/httpie/client.py index 9407181d..e7c8267d 100644 --- a/httpie/client.py +++ b/httpie/client.py @@ -283,7 +283,8 @@ def make_request_kwargs( body=data, body_read_callback=request_body_read_callback, chunked=args.chunked, - content_length_header_value=headers.get('Content-Length') + offline=args.offline, + content_length_header_value=headers.get('Content-Length'), ), 'auth': args.auth, 'params': args.params.items(), diff --git a/httpie/core.py b/httpie/core.py index e0db9c2f..a316d4ea 100644 --- a/httpie/core.py +++ b/httpie/core.py @@ -201,7 +201,7 @@ def program( if is_request: if not initial_request: initial_request = message - is_streamed_upload = not args.offline and not isinstance( + is_streamed_upload = not isinstance( message.body, (str, bytes)) if with_body: with_body = not is_streamed_upload diff --git a/httpie/uploads.py b/httpie/uploads.py index f7dc0f5e..c65bfd33 100644 --- a/httpie/uploads.py +++ b/httpie/uploads.py @@ -39,11 +39,20 @@ def prepare_request_body( body_read_callback: Callable[[bytes], bytes], content_length_header_value: int = None, chunked=False, + offline=False, ) -> Union[str, bytes, IO, MultipartEncoder, ChunkedUploadStream]: + + is_file_like = hasattr(body, 'read') + if isinstance(body, RequestDataDict): body = urlencode(body, doseq=True) - if not hasattr(body, 'read'): + if offline: + if is_file_like: + return body.read() + return body + + if not is_file_like: if chunked: body = ChunkedUploadStream( # Pass the entire body as one chunk. diff --git a/tests/test_httpie.py b/tests/test_httpie.py index f4fe1b37..02208793 100644 --- a/tests/test_httpie.py +++ b/tests/test_httpie.py @@ -10,7 +10,7 @@ from httpie.context import Environment from httpie.status import ExitStatus from httpie.cli.exceptions import ParseError from utils import MockEnvironment, StdinBytesIO, http, HTTP_OK -from fixtures import FILE_PATH, FILE_CONTENT +from fixtures import FILE_PATH, FILE_CONTENT, FILE_PATH_ARG import httpie @@ -193,6 +193,48 @@ def test_offline(): assert 'GET /foo' in r +def test_offline_form(): + r = http( + '--offline', + '--form', + 'https://this-should.never-resolve/foo', + 'foo=bar' + ) + assert 'POST /foo' in r + assert 'foo=bar' in r + + +def test_offline_json(): + r = http( + '--offline', + 'https://this-should.never-resolve/foo', + 'foo=bar' + ) + assert 'POST /foo' in r + assert r.json == {'foo': 'bar'} + + +def test_offline_multipart(): + r = http( + '--offline', + '--multipart', + 'https://this-should.never-resolve/foo', + 'foo=bar' + ) + assert 'POST /foo' in r + assert 'name="foo"' in r + + +def test_offline_from_file(): + r = http( + '--offline', + 'https://this-should.never-resolve/foo', + f'@{FILE_PATH_ARG}' + ) + assert 'POST /foo' in r + assert FILE_CONTENT in r + + def test_offline_download(): """Absence of response should be handled gracefully with --download""" r = http( diff --git a/tests/test_uploads.py b/tests/test_uploads.py index 7dbf1716..7e3d2a88 100644 --- a/tests/test_uploads.py +++ b/tests/test_uploads.py @@ -170,7 +170,6 @@ class TestMultipartFormDataFileUpload: '--verbose', '--multipart', '--chunked', - # '--offline', HTTPBIN_WITH_CHUNKED_SUPPORT + '/post', 'AAA=AAA', ) @@ -179,6 +178,25 @@ class TestMultipartFormDataFileUpload: assert 'name="AAA"' in r # in request assert '"AAA": "AAA"', r # in response + def test_multipart_preserve_order(self, httpbin): + r = http( + '--form', + '--offline', + httpbin + '/post', + 'text_field=foo', + f'file_field@{FILE_PATH_ARG}', + ) + assert r.index('text_field') < r.index('file_field') + + r = http( + '--form', + '--offline', + httpbin + '/post', + f'file_field@{FILE_PATH_ARG}', + 'text_field=foo', + ) + assert r.index('text_field') > r.index('file_field') + class TestRequestBodyFromFilePath: """ diff --git a/tests/utils.py b/tests/utils.py index b84805e2..4359a47e 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -144,10 +144,9 @@ class StrCLIResponse(str, BaseCLIResponse): elif self.strip().startswith('{'): # Looks like JSON body. self._json = json.loads(self) - elif (self.count('Content-Type:') == 1 - and 'application/json' in self): - # Looks like a whole JSON HTTP message, - # try to extract its body. + elif self.count('Content-Type:') == 1: + # Looks like a HTTP message, + # try to extract JSON from its body. try: j = self.strip()[self.strip().rindex('\r\n\r\n'):] except ValueError: