diff --git a/httpie/__main__.py b/httpie/__main__.py index 07429195..c617266d 100644 --- a/httpie/__main__.py +++ b/httpie/__main__.py @@ -16,23 +16,29 @@ TYPE_FORM = 'application/x-www-form-urlencoded; charset=utf-8' TYPE_JSON = 'application/json; charset=utf-8' -def _get_response(parser, args, stdin, stdin_isatty): +def _get_response(args): - if args.json or (not args.form and args.data): + auto_json = args.data and not args.form + if args.json or auto_json: # JSON - if not args.files and ( - 'Content-Type' not in args.headers - and (args.data or args.json)): - args.headers['Content-Type'] = TYPE_JSON - if isinstance(args.data, dict): - # Serialize the data dict parsed from arguments. - args.data = json.dumps(args.data) + if 'Content-Type' not in args.headers: + args.headers['Content-Type'] = TYPE_JSON + if 'Accept' not in args.headers: # Default Accept to JSON as well. args.headers['Accept'] = 'application/json' - elif not args.files and 'Content-Type' not in args.headers: + + if isinstance(args.data, dict): + # If not empty, serialize the data `dict` parsed from arguments. + # Otherwise set it to `None` avoid sending "{}". + args.data = json.dumps(args.data) if args.data else None + + elif args.form: # Form - args.headers['Content-Type'] = TYPE_FORM + if not args.files and 'Content-Type' not in args.headers: + # If sending files, `requests` will set + # the `Content-Type` for us. + args.headers['Content-Type'] = TYPE_FORM # Fire the request. try: @@ -113,7 +119,7 @@ def main(args=None, stdin=stdin, stdin_isatty=stdin_isatty ) - response = _get_response(parser, args, stdin, stdin_isatty) + response = _get_response(args) output = _get_output(args, stdout_isatty, response) output_bytes = output.encode('utf8') f = (stdout.buffer if hasattr(stdout, 'buffer') else stdout) diff --git a/httpie/cliparse.py b/httpie/cliparse.py index 8f3b67d3..b183dba5 100644 --- a/httpie/cliparse.py +++ b/httpie/cliparse.py @@ -146,7 +146,6 @@ class Parser(argparse.ArgumentParser): content_type = '%s; charset=%s' % (mime, encoding) args.headers['Content-Type'] = content_type - def _validate_output_options(self, args): unknown_output_options = set(args.output_options) - set(OUTPUT_OPTIONS) if unknown_output_options: diff --git a/tests/tests.py b/tests/tests.py index cba8ac42..0259086a 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -80,16 +80,6 @@ class HTTPieTest(BaseTestCase): self.assertIn('HTTP/1.1 200', r) self.assertIn('"foo": "bar"', r) - def test_GET_JSON_implicit_accept(self): - r = http('-j', 'GET', 'http://httpbin.org/headers') - self.assertIn('HTTP/1.1 200', r) - self.assertIn('"Accept": "application/json"', r) - - def test_GET_JSON_explicit_accept(self): - r = http('-j', 'GET', 'http://httpbin.org/headers', 'Accept:application/xml') - self.assertIn('HTTP/1.1 200', r) - self.assertIn('"Accept": "application/xml"', r) - def test_POST_form(self): r = http('--form', 'POST', 'http://httpbin.org/post', 'foo=bar') self.assertIn('HTTP/1.1 200', r) @@ -108,6 +98,69 @@ class HTTPieTest(BaseTestCase): self.assertIn('"Foo": "bar"', r) +class AutoContentTypeAndAcceptHeadersTest(BaseTestCase): + """ + Test that Accept and Content-Type correctly defaults to JSON, + but can still be overridden. The same with Content-Type when --form + -f is used. + + """ + def test_GET_no_data_no_auto_headers(self): + # https://github.com/jkbr/httpie/issues/62 + r = http('GET', 'http://httpbin.org/headers') + self.assertIn('HTTP/1.1 200', r) + self.assertIn('"Accept": "*/*"', r) + # Although an empty header is present in the response from httpbin, + # it's not included in the request. + self.assertIn('"Content-Type": ""', r) + + def test_POST_no_data_no_auto_headers(self): + # JSON headers shouldn't be automatically set for POST with no data. + r = http('POST', 'http://httpbin.org/post') + self.assertIn('HTTP/1.1 200', r) + self.assertIn('"Accept": "*/*"', r) + # Although an empty header is present in the response from httpbin, + # it's not included in the request. + self.assertIn(' "Content-Type": ""', r) + + def test_POST_with_data_auto_JSON_headers(self): + r = http('POST', 'http://httpbin.org/post', 'a=b') + self.assertIn('HTTP/1.1 200', r) + self.assertIn('"Accept": "application/json"', r) + self.assertIn('"Content-Type": "application/json; charset=utf-8', r) + + def test_GET_with_data_auto_JSON_headers(self): + # JSON headers should automatically be set also for GET with data. + r = http('POST', 'http://httpbin.org/post', 'a=b') + self.assertIn('HTTP/1.1 200', r) + self.assertIn('"Accept": "application/json"', r) + self.assertIn('"Content-Type": "application/json; charset=utf-8', r) + + def test_POST_explicit_JSON_auto_JSON_headers(self): + r = http('-j', 'POST', 'http://httpbin.org/post') + self.assertIn('HTTP/1.1 200', r) + self.assertIn('"Accept": "application/json"', r) + self.assertIn('"Content-Type": "application/json; charset=utf-8', r) + + def test_GET_explicit_JSON_explicit_headers(self): + r = http('-j', 'GET', 'http://httpbin.org/headers', + 'Accept:application/xml', + 'Content-Type:application/xml') + self.assertIn('HTTP/1.1 200', r) + self.assertIn('"Accept": "application/xml"', r) + self.assertIn('"Content-Type": "application/xml"', r) + + def test_POST_form_auto_Content_Type(self): + r = http('-f', 'POST', 'http://httpbin.org/post') + self.assertIn('HTTP/1.1 200', r) + self.assertIn('"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"', r) + + def test_POST_form_Content_Type_override(self): + r = http('-f', 'POST', 'http://httpbin.org/post', 'Content-Type:application/xml') + self.assertIn('HTTP/1.1 200', r) + self.assertIn('"Content-Type": "application/xml"', r) + + class ImplicitHTTPMethodTest(BaseTestCase): def test_implicit_GET(self): @@ -209,7 +262,7 @@ class RequestBodyFromFilePathTest(BaseTestCase): '@' + TEST_FILE_PATH, '@' + TEST_FILE2_PATH)) - def test_request_body_from_file_by_path_only_no_data_items_allowed(self): + def test_request_body_from_file_by_path_no_data_items_allowed(self): self.assertRaises(SystemExit, lambda: http( 'POST', 'http://httpbin.org/post',