diff --git a/README.rst b/README.rst index 188116f0..d740b296 100644 --- a/README.rst +++ b/README.rst @@ -1246,6 +1246,7 @@ Changelog * Added ``--cert`` and ``--certkey`` parameters to specify a client side certificate and private key for SSL * Improved unicode support. + * Fixed ``User-Agent`` overwriting when used within a session. * Switched from ``unittest`` to ``pytest``. * Various test suite improvements. * Added `CONTRIBUTING`_. diff --git a/httpie/client.py b/httpie/client.py index d4c88052..a17408e5 100644 --- a/httpie/client.py +++ b/httpie/client.py @@ -18,26 +18,27 @@ DEFAULT_UA = 'HTTPie/%s' % __version__ def get_response(args, config_dir): """Send the request and return a `request.Response`.""" - requests_kwargs = get_requests_kwargs(args) - - if args.debug: - sys.stderr.write('\n>>> requests.request(%s)\n\n' - % pformat(requests_kwargs)) - if not args.session and not args.session_read_only: + requests_kwargs = get_requests_kwargs(args) + if args.debug: + dump_request(requests_kwargs) response = requests.request(**requests_kwargs) else: response = sessions.get_response( args=args, config_dir=config_dir, session_name=args.session or args.session_read_only, - requests_kwargs=requests_kwargs, read_only=bool(args.session_read_only), ) return response +def dump_request(kwargs): + sys.stderr.write('\n>>> requests.request(%s)\n\n' + % pformat(kwargs)) + + def encode_headers(headers): # This allows for unicode headers which is non-standard but practical. # See: https://github.com/jakubroztocil/httpie/issues/212 @@ -47,36 +48,47 @@ def encode_headers(headers): ) -def get_requests_kwargs(args): - """Translate our `args` into `requests.request` keyword arguments.""" - - implicit_headers = { +def get_default_headers(args): + default_headers = { 'User-Agent': DEFAULT_UA } auto_json = args.data and not args.form # FIXME: Accept is set to JSON with `http url @./file.txt`. if args.json or auto_json: - implicit_headers['Accept'] = 'application/json' + default_headers['Accept'] = 'application/json' if args.json or (auto_json and args.data): - implicit_headers['Content-Type'] = JSON - - if isinstance(args.data, dict): - if args.data: - args.data = json.dumps(args.data) - else: - # We need to set data to an empty string to prevent requests - # from assigning an empty list to `response.request.data`. - args.data = '' + default_headers['Content-Type'] = JSON elif args.form and not args.files: # If sending files, `requests` will set # the `Content-Type` for us. - implicit_headers['Content-Type'] = FORM + default_headers['Content-Type'] = FORM + return default_headers - for name, value in implicit_headers.items(): - if name not in args.headers: - args.headers[name] = value + +def get_requests_kwargs(args, base_headers=None): + """ + Translate our `args` into `requests.request` keyword arguments. + + """ + # Serialize JSON data, if needed. + data = args.data + auto_json = data and not args.form + if args.json or auto_json and isinstance(data, dict): + if data: + data = json.dumps(data) + else: + # We need to set data to an empty string to prevent requests + # from assigning an empty list to `response.request.data`. + data = '' + + # Finalize headers. + headers = get_default_headers(args) + if base_headers: + headers.update(base_headers) + headers.update(args.headers) + headers = encode_headers(headers) credentials = None if args.auth: @@ -87,14 +99,14 @@ def get_requests_kwargs(args): if args.cert: cert = args.cert if args.certkey: - cert = (cert, args.certkey) + cert = cert, args.certkey kwargs = { 'stream': True, 'method': args.method.lower(), 'url': args.url, - 'headers': encode_headers(args.headers), - 'data': args.data, + 'headers': headers, + 'data': data, 'verify': { 'yes': True, 'no': False diff --git a/httpie/sessions.py b/httpie/sessions.py index 4fe4589e..795b8577 100644 --- a/httpie/sessions.py +++ b/httpie/sessions.py @@ -21,21 +21,17 @@ VALID_SESSION_NAME_PATTERN = re.compile('^[a-zA-Z0-9_.-]+$') SESSION_IGNORED_HEADER_PREFIXES = ['Content-', 'If-'] -def get_response(session_name, requests_kwargs, config_dir, args, - read_only=False): +def get_response(session_name, config_dir, args, read_only=False): """Like `client.get_response`, but applies permanent aspects of the session to the request. """ - from .client import encode_headers + from .client import get_requests_kwargs, dump_request if os.path.sep in session_name: path = os.path.expanduser(session_name) else: - hostname = ( - requests_kwargs['headers'].get('Host', None) - or urlsplit(requests_kwargs['url']).netloc.split('@')[-1] - ) - + hostname = (args.headers.get('Host', None) + or urlsplit(args.url).netloc.split('@')[-1]) assert re.match('^[a-zA-Z0-9_.:-]+$', hostname) # host:port => host_port @@ -48,13 +44,10 @@ def get_response(session_name, requests_kwargs, config_dir, args, session = Session(path) session.load() - request_headers = requests_kwargs.get('headers', {}) - - merged_headers = dict(session.headers) - merged_headers.update(request_headers) - requests_kwargs['headers'] = encode_headers(merged_headers) - - session.update_headers(request_headers) + requests_kwargs = get_requests_kwargs(args, base_headers=session.headers) + if args.debug: + dump_request(requests_kwargs) + session.update_headers(requests_kwargs['headers']) if args.auth: session.auth = { diff --git a/tests/test_sessions.py b/tests/test_sessions.py index 77ab6ca2..63ea7b06 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -137,3 +137,14 @@ class TestSession(SessionTestBase): assert (r2.json['headers']['Authorization'] == HTTPBasicAuth.make_header(u'test', UNICODE)) assert r2.json['headers']['Test'] == UNICODE + + def test_session_default_header_value_overwritten(self): + # https://github.com/jakubroztocil/httpie/issues/180 + r1 = http('--session=test', httpbin('/headers'), 'User-Agent:custom', + env=self.env()) + assert HTTP_OK in r1 + assert r1.json['headers']['User-Agent'] == 'custom' + + r2 = http('--session=test', httpbin('/headers'), env=self.env()) + assert HTTP_OK in r2 + assert r2.json['headers']['User-Agent'] == 'custom'