Provide a RFC 7231 compliant user agent string

The current default of "keystoneauth1" doesn't convey enough
information, and additionally when the user of a Session supplies their
own user agent, it stomps on any notion of keystoneauth1 being there.

Per RFC 7231 Section 5.5.3
(, user agents should
basically be a space-delimited list of product/version pairs in
decreasing order of importance. This change makes the default user agent
something like the following:

keystoneauth1/2.1.1 python-requests/2.8.1 CPython/3.4.1+

Due to the decreasing order of importance, when a user creates a Session
with something like Session(user_agent="my-product/1.0"),
'my-product/1.0' is then prepended to the above list. The only time this
is not the case is if a user agent is provided directly to
Session.request. In that case, the User-Agent header is set to whatever
the provided argument is, verbatim.

This was a change we had originally made to the Transport class in
python-openstacksdk (I80ca26fff3f2522b8232472676396abb86166f91), but
upon moving to keystoneauth instead of our own implementation, it was
noticed that we lost this, and keystoneauth is a better place for this
than for us to re-implement it inside of python-openstacksdk.

Change-Id: I46f336f25fac5b524547bb13e4f5438ebf1d4320
This commit is contained in:
briancurtin 2015-12-10 10:42:37 -06:00
parent 30f9a02fa2
commit bbd85fded7
4 changed files with 39 additions and 7 deletions

View File

@ -0,0 +1,16 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import pbr.version
__version__ = pbr.version.VersionInfo('keystoneauth1').version_string()

View File

@ -15,6 +15,7 @@ import functools
import hashlib
import json
import logging
import platform
import socket
import time
import uuid
@ -23,6 +24,7 @@ import requests
import six
from six.moves import urllib
import keystoneauth1
from keystoneauth1 import _utils as utils
from keystoneauth1 import exceptions
@ -36,7 +38,9 @@ try:
except ImportError:
osprofiler_web = None
USER_AGENT = 'keystoneauth1'
DEFAULT_USER_AGENT = 'keystoneauth1/%s %s %s/%s' % (
keystoneauth1.__version__, requests.utils.default_user_agent(),
platform.python_implementation(), platform.python_version())
_logger = utils.get_logger(__name__)
@ -95,8 +99,12 @@ class Session(object):
of seconds or 0 for no timeout. (optional, defaults
to 0)
:param string user_agent: A User-Agent header string to use for the
request. If not provided a default is used.
(optional, defaults to 'keystoneauth1')
request. If not provided, a default of
is used, which contains the keystoneauth1 version
as well as those of the requests library and
which Python is being used. When a non-None value
is passed, it will be prepended to the default.
:param int/bool redirect: Controls the maximum number of redirections that
can be followed by a request. Either an integer
for a specific count or True/False for
@ -127,7 +135,11 @@ class Session(object):
# don't override the class variable if none provided
if user_agent is not None:
self.user_agent = user_agent
# Per RFC 7231 Section 5.5.3, identifiers in a user-agent
# should be ordered by decreasing significance.
# If a user sets their product, we prepend it to the KSA
# version, requests version, and then the Python version.
self.user_agent = "%s %s" % (user_agent, DEFAULT_USER_AGENT)
self._json = _JSONEncoder()
@ -367,7 +379,7 @@ class Session(object):
elif self.user_agent:
user_agent = headers.setdefault('User-Agent', self.user_agent)
user_agent = headers.setdefault('User-Agent', USER_AGENT)
user_agent = headers.setdefault('User-Agent', DEFAULT_USER_AGENT)
if self.original_ip:

View File

@ -89,12 +89,15 @@ class SessionTests(utils.TestCase):
self.assertRequestBodyIs(json={'hello': 'world'})
def test_user_agent(self):
session = client_session.Session(user_agent='test-agent')
custom_agent = 'custom-agent/1.0'
session = client_session.Session(user_agent=custom_agent)
self.stub_url('GET', text='response')
resp = session.get(self.TEST_URL)
self.assertRequestHeaderEqual('User-Agent', 'test-agent')
'%s %s' % (custom_agent, client_session.DEFAULT_USER_AGENT))
resp = session.get(self.TEST_URL, headers={'User-Agent': 'new-agent'})

View File

@ -2,6 +2,7 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.