Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User Agent 2.0 #2955

Merged
merged 13 commits into from
Jun 27, 2023
5 changes: 5 additions & 0 deletions .changes/next-release/enhancement-Useragent-93485.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "feature",
"category": "Useragent",
"description": "Update User-Agent header format"
}
46 changes: 35 additions & 11 deletions botocore/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from botocore.regions import EndpointResolverBuiltins as EPRBuiltins
from botocore.regions import EndpointRulesetResolver
from botocore.signers import RequestSigner
from botocore.useragent import UserAgentString
from botocore.utils import ensure_boolean, is_s3_accelerate_url

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -55,6 +56,9 @@
'us-west-1',
'us-west-2',
]
# Maximum allowed length of the ``user_agent_appid`` config field. Longer
# values result in a warning-level log message.
USERAGENT_APPID_MAXLEN = 50


class ClientArgsCreator:
Expand All @@ -66,13 +70,17 @@ def __init__(
loader,
exceptions_factory,
config_store,
user_agent_creator=None,
):
self._event_emitter = event_emitter
self._user_agent = user_agent
self._response_parser_factory = response_parser_factory
self._loader = loader
self._exceptions_factory = exceptions_factory
self._config_store = config_store
if user_agent_creator is None:
self._session_ua_creator = UserAgentString.from_environment()
else:
self._session_ua_creator = user_agent_creator

def get_client_args(
self,
Expand Down Expand Up @@ -159,6 +167,11 @@ def get_client_args(
event_emitter,
)

# Copy the session's user agent factory and adds client configuration.
client_ua_creator = self._session_ua_creator.with_client_config(
new_config
)

return {
'serializer': serializer,
'endpoint': endpoint,
Expand All @@ -171,6 +184,7 @@ def get_client_args(
'partition': partition,
'exceptions_factory': self._exceptions_factory,
'endpoint_ruleset_resolver': ruleset_resolver,
'user_agent_creator': client_ua_creator,
}

def compute_client_args(
Expand All @@ -193,14 +207,6 @@ def compute_client_args(
if raw_value is not None:
parameter_validation = ensure_boolean(raw_value)

# Override the user agent if specified in the client config.
user_agent = self._user_agent
if client_config is not None:
if client_config.user_agent is not None:
user_agent = client_config.user_agent
if client_config.user_agent_extra is not None:
user_agent += ' %s' % client_config.user_agent_extra

s3_config = self.compute_s3_config(client_config)
endpoint_config = self._compute_endpoint_config(
service_name=service_name,
Expand All @@ -217,7 +223,6 @@ def compute_client_args(
config_kwargs = dict(
region_name=endpoint_config['region_name'],
signature_version=endpoint_config['signature_version'],
user_agent=user_agent,
)
if 'dualstack' in endpoint_variant_tags:
config_kwargs.update(use_dualstack_endpoint=True)
Expand All @@ -234,9 +239,13 @@ def compute_client_args(
client_cert=client_config.client_cert,
inject_host_prefix=client_config.inject_host_prefix,
tcp_keepalive=client_config.tcp_keepalive,
user_agent=client_config.user_agent,
user_agent_extra=client_config.user_agent_extra,
user_agent_appid=client_config.user_agent_appid,
)
self._compute_retry_config(config_kwargs)
self._compute_connect_timeout(config_kwargs)
self._compute_user_agent_appid_config(config_kwargs)
s3_config = self.compute_s3_config(client_config)

is_s3_service = self._is_s3_service(service_name)
Expand All @@ -249,7 +258,6 @@ def compute_client_args(
return {
'service_name': service_name,
'parameter_validation': parameter_validation,
'user_agent': user_agent,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may also be problematic, I don't immediately know how we do this cleanly though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can bring back this line together with lines 196-203 above where its value gets computed using the legacy method. There is no harm in returning the user_agent key in the dict, it simply won't get used by botocore code. Do you think that's preferable? I was worried the confusion caused by the dead code existing and an unused value being returned might outweigh any benefit to any third-party that may be relying on the user_agent dict key being present.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at other libraries that may have issues with this, I can't find one that relies on user_agent. I agree this may cause unnecessary confusion, so let's opt to remove it.

'endpoint_config': endpoint_config,
'protocol': protocol,
'config_kwargs': config_kwargs,
Expand Down Expand Up @@ -646,3 +654,19 @@ def compute_endpoint_resolver_builtin_defaults(
),
EPRBuiltins.SDK_ENDPOINT: given_endpoint,
}

def _compute_user_agent_appid_config(self, config_kwargs):
user_agent_appid = config_kwargs.get('user_agent_appid')
if user_agent_appid is None:
user_agent_appid = self._config_store.get_config_variable(
'user_agent_appid'
)
if (
user_agent_appid is not None
and len(user_agent_appid) > USERAGENT_APPID_MAXLEN
):
logger.warning(
'The configured value for user_agent_appid exceeds the '
f'maximum length of {USERAGENT_APPID_MAXLEN} characters.'
)
config_kwargs['user_agent_appid'] = user_agent_appid
nateprewitt marked this conversation as resolved.
Show resolved Hide resolved
14 changes: 13 additions & 1 deletion botocore/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from botocore.model import ServiceModel
from botocore.paginate import Paginator
from botocore.retries import adaptive, standard
from botocore.useragent import UserAgentString
from botocore.utils import (
CachedProperty,
EventbridgeSignerSetter,
Expand Down Expand Up @@ -91,6 +92,7 @@ def __init__(
response_parser_factory=None,
exceptions_factory=None,
config_store=None,
user_agent_creator=None,
):
self._loader = loader
self._endpoint_resolver = endpoint_resolver
Expand All @@ -105,6 +107,7 @@ def __init__(
# config and environment variables (and potentially more in the
# future).
self._config_store = config_store
self._user_agent_creator = user_agent_creator

def create_client(
self,
Expand Down Expand Up @@ -481,6 +484,7 @@ def _get_client_args(
self._loader,
self._exceptions_factory,
config_store=self._config_store,
user_agent_creator=self._user_agent_creator,
)
return args_creator.get_client_args(
service_model,
Expand Down Expand Up @@ -840,6 +844,7 @@ def __init__(
partition,
exceptions_factory,
endpoint_ruleset_resolver=None,
user_agent_creator=None,
):
self._serializer = serializer
self._endpoint = endpoint
Expand All @@ -859,6 +864,13 @@ def __init__(
)
self._exceptions_factory = exceptions_factory
self._exceptions = None
self._user_agent_creator = user_agent_creator
if self._user_agent_creator is None:
self._user_agent_creator = (
UserAgentString.from_environment().with_client_config(
self._client_config
)
)
self._register_handlers()

def __getattr__(self, item):
Expand Down Expand Up @@ -996,7 +1008,7 @@ def _convert_to_request_dict(
if headers is not None:
request_dict['headers'].update(headers)
if set_user_agent_header:
user_agent = self._client_config.user_agent
user_agent = self._user_agent_creator.to_string()
else:
user_agent = None
prepare_request_dict(
Expand Down
7 changes: 7 additions & 0 deletions botocore/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ class Config:
:param user_agent_extra: The value to append to the current User-Agent
header value.

:type user_agent_appid: str
:param user_agent_appid: A value that gets included in the User-Agent
string in the format "app/<user_agent_appid>". Allowed characters are
ASCII alphanumerics and ``!$%&'*+-.^_`|~``. All other characters will
be replaced by a ``-``.

:type connect_timeout: float or int
:param connect_timeout: The time in seconds till a timeout exception is
thrown when attempting to make a connection. The default is 60
Expand Down Expand Up @@ -201,6 +207,7 @@ class Config:
('signature_version', None),
('user_agent', None),
('user_agent_extra', None),
('user_agent_appid', None),
('connect_timeout', DEFAULT_TIMEOUT),
('read_timeout', DEFAULT_TIMEOUT),
('parameter_validation', True),
Expand Down
1 change: 1 addition & 0 deletions botocore/configprovider.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
# We can't have a default here for v1 because we need to defer to
# whatever the defaults are in _retry.json.
'max_attempts': ('max_attempts', 'AWS_MAX_ATTEMPTS', None, int),
'user_agent_appid': ('sdk_ua_app_id', 'AWS_SDK_UA_APP_ID', None, None),
}
# A mapping for the s3 specific configuration vars. These are the configuration
# vars that typically go in the s3 section of the config file. This mapping
Expand Down
27 changes: 21 additions & 6 deletions botocore/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,16 @@
from botocore.model import ServiceModel
from botocore.parsers import ResponseParserFactory
from botocore.regions import EndpointResolver
from botocore.useragent import UserAgentString
from botocore.utils import (
EVENT_ALIASES,
IMDSRegionProvider,
validate_region_name,
)

from botocore.compat import HAS_CRT # noqa


logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -165,6 +169,7 @@ def _register_components(self):
self._register_monitor()
self._register_default_config_resolver()
self._register_smart_defaults_factory()
self._register_user_agent_creator()

def _register_event_emitter(self):
self._components.register_component('event_emitter', self._events)
Expand Down Expand Up @@ -263,6 +268,10 @@ def _register_monitor(self):
'monitor', self._create_csm_monitor
)

def _register_user_agent_creator(self):
uas = UserAgentString.from_environment()
self._components.register_component('user_agent_creator', uas)

def _create_csm_monitor(self):
if self.get_config_variable('csm_enabled'):
client_id = self.get_config_variable('csm_client_id')
Expand All @@ -283,12 +292,8 @@ def _create_csm_monitor(self):
return None

def _get_crt_version(self):
try:
import awscrt

return awscrt.__version__
except AttributeError:
return "Unknown"
user_agent_creator = self.get_component('user_agent_creator')
return user_agent_creator._crt_version or 'Unknown'

@property
def available_profiles(self):
Expand Down Expand Up @@ -953,6 +958,15 @@ def create_client(
endpoint_resolver = self._get_internal_component('endpoint_resolver')
exceptions_factory = self._get_internal_component('exceptions_factory')
config_store = self.get_component('config_store')
user_agent_creator = self.get_component('user_agent_creator')
# Session configuration values for the user agent string are applied
# just before each client creation because they may have been modified
# at any time between session creation and client creation.
user_agent_creator.set_session_config(
session_user_agent_name=self.user_agent_name,
session_user_agent_version=self.user_agent_version,
session_user_agent_extra=self.user_agent_extra,
)
defaults_mode = self._resolve_defaults_mode(config, config_store)
if defaults_mode != 'legacy':
smart_defaults_factory = self._get_internal_component(
Expand All @@ -972,6 +986,7 @@ def create_client(
response_parser_factory,
exceptions_factory,
config_store,
user_agent_creator=user_agent_creator,
)
client = client_creator.create_client(
service_name=service_name,
Expand Down
Loading