diff --git a/httpie/models.py b/httpie/models.py index 9183715e..54db6c1d 100644 --- a/httpie/models.py +++ b/httpie/models.py @@ -82,8 +82,7 @@ class HTTPResponse(HTTPMessage): return self._orig.iter_content(chunk_size=chunk_size) def iter_lines(self, chunk_size): - for line in self._orig.iter_lines(chunk_size): - yield line, b'\n' + return ((line, b'\n') for line in self._orig.iter_lines(chunk_size)) @property def headers(self): @@ -92,9 +91,19 @@ class HTTPResponse(HTTPMessage): version='.'.join(str(original.version)), status=original.status, reason=original.reason - ) - headers = str(original.msg) - return '\n'.join([status_line, headers]).strip() + ) + headers = [status_line] + try: + # `original.msg` is a `http.client.HTTPMessage` on Python 3 + # `_headers` is a 2-tuple + headers.extend( + '%s: %s' % header for header in original.msg._headers) + except AttributeError: + # and a `httplib.HTTPMessage` on Python 2.x + # `headers` is a list of `name: val`. + headers.extend(h.strip() for h in original.msg.headers) + + return '\r\n'.join(headers) @property def encoding(self): @@ -151,7 +160,7 @@ class HTTPRequest(HTTPMessage): headers.insert(0, request_line) - return '\n'.join(headers).strip() + return '\r\n'.join(headers).strip() @property def encoding(self): diff --git a/httpie/output.py b/httpie/output.py index de608596..1b6911ac 100644 --- a/httpie/output.py +++ b/httpie/output.py @@ -85,8 +85,9 @@ def output_stream(args, env, request, response): with_headers=req_h, with_body=req_b)) - if req and resp: - output.append([b'\n\n\n']) + if req_b and resp: + # Request/Response separator. + output.append([b'\n\n']) if resp: output.append(Stream( @@ -94,7 +95,9 @@ def output_stream(args, env, request, response): with_headers=resp_h, with_body=resp_b)) - if env.stdout_isatty: + if env.stdout_isatty and resp_b: + # Ensure a blank line after the response body. + # For terminal output only. output.append([b'\n\n']) return chain(*output) @@ -150,21 +153,12 @@ class BaseStream(object): """Return an iterator over `self.msg`.""" if self.with_headers: yield self._headers() + yield b'\r\n\r\n' if self.with_body: - it = self._body() - try: - if self.with_headers: - # Yield the headers/body separator only if needed. - chunk = next(it) - if chunk: - yield b'\n\n' - yield chunk - - for chunk in it: + for chunk in self._body(): yield chunk - except BinarySuppressedError as e: if self.with_headers: yield b'\n' @@ -433,7 +427,7 @@ class HeadersProcessor(BaseProcessor): def process_headers(self, headers): lines = headers.splitlines() headers = sorted(lines[1:], key=lambda h: h.split(':')[0]) - return '\n'.join(lines[:1] + headers) + return '\r\n'.join(lines[:1] + headers) class OutputProcessor(object): @@ -460,7 +454,7 @@ class OutputProcessor(object): for group in groups: for cls in self.installed_processors[group]: processor = cls(env, **kwargs) - if processor.enable: + if processor.enabled: self.processors.append(processor) def process_headers(self, headers): diff --git a/tests/tests.py b/tests/tests.py index 8c34cc67..45a3cc4c 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -26,6 +26,8 @@ import json import argparse import tempfile import unittest + +CRLF = '\r\n' try: from urllib.request import urlopen except ImportError: @@ -174,7 +176,6 @@ def http(*args, **kwargs): env.stderr.seek(0) output = env.stdout.read() - try: r = StrResponse(output.decode('utf8')) except UnicodeDecodeError: @@ -187,7 +188,7 @@ def http(*args, **kwargs): r.json = json.loads(r) elif r.count('Content-Type:') == 1 and 'application/json' in r: try: - j = r.strip()[r.strip().rindex('\n\n'):] + j = r.strip()[r.strip().rindex('\r\n\r\n'):] except ValueError: pass else: @@ -1005,6 +1006,68 @@ class StreamTest(BaseTestCase): self.assertIn(BIN_FILE_CONTENT, r) +class LineEndingsTest(BaseTestCase): + """Test that CRLF is properly used in headers and + as the headers/body separator.""" + + def _validate_crlf(self, msg): + #noinspection PyUnresolvedReferences + lines = iter(msg.splitlines(True)) + for header in lines: + if header == CRLF: + break + self.assertTrue(header.endswith(CRLF), repr(header)) + else: + self.fail('CRLF between headers and body not found in %r' % msg) + body = ''.join(lines) + self.assertNotIn(CRLF, body) + return body + + def test_CRLF_headers_only(self): + r = http( + '--headers', + 'GET', + httpbin('/get') + ) + body = self._validate_crlf(r) + self.assertFalse(body, 'Garbage after headers: %r' % r) + + def test_CRLF_ugly_response(self): + r = http( + '--ugly', + 'GET', + httpbin('/get') + ) + self._validate_crlf(r) + + def test_CRLF_formatted_response(self): + r = http( + '--format', + 'GET', + httpbin('/get') + ) + self.assertEqual(r.exit_status,0) + self._validate_crlf(r) + + def test_CRLF_ugly_request(self): + r = http( + '--ugly', + '--print=HB', + 'GET', + httpbin('/get') + ) + self._validate_crlf(r) + + def test_CRLF_formatted_request(self): + r = http( + '--format', + '--print=HB', + 'GET', + httpbin('/get') + ) + self._validate_crlf(r) + + ################################################################# # CLI argument parsing related tests. #################################################################