diff --git a/.changes/next-release/bugfix-endpoints-38658.json b/.changes/next-release/bugfix-endpoints-38658.json new file mode 100644 index 0000000000..1bdfba2953 --- /dev/null +++ b/.changes/next-release/bugfix-endpoints-38658.json @@ -0,0 +1,5 @@ +{ + "type": "bugfix", + "category": "endpoints", + "description": "Include params set in provide-client-param event handlers in dynamic context params for endpoint resolution." +} diff --git a/botocore/client.py b/botocore/client.py index da92f39987..7180779414 100644 --- a/botocore/client.py +++ b/botocore/client.py @@ -819,7 +819,6 @@ def _resolve_signature_version(self, service_name, resolved): class BaseClient: - # This is actually reassigned with the py->op_name mapping # when the client creator creates the subclass. This value is used # because calls such as client.get_paginator('list_objects') use the @@ -913,6 +912,11 @@ def _make_api_call(self, operation_name, api_params): 'has_streaming_input': operation_model.has_streaming_input, 'auth_type': operation_model.auth_type, } + api_params = self._emit_api_params( + api_params=api_params, + operation_model=operation_model, + context=request_context, + ) endpoint_url, additional_headers = self._resolve_endpoint_ruleset( operation_model, api_params, request_context ) @@ -984,9 +988,6 @@ def _convert_to_request_dict( headers=None, set_user_agent_header=True, ): - api_params = self._emit_api_params( - api_params, operation_model, context - ) request_dict = self._serializer.serialize_to_request( api_params, operation_model ) diff --git a/botocore/signers.py b/botocore/signers.py index 42965b41e4..0acbf68463 100644 --- a/botocore/signers.py +++ b/botocore/signers.py @@ -656,6 +656,11 @@ def generate_presigned_url( raise UnknownClientMethodError(method_name=client_method) operation_model = self.meta.service_model.operation_model(operation_name) + params = self._emit_api_params( + api_params=params, + operation_model=operation_model, + context=context, + ) bucket_is_arn = ArnParser.is_arn(params.get('Bucket', '')) endpoint_url, additional_headers = self._resolve_endpoint_ruleset( operation_model, @@ -778,7 +783,11 @@ def generate_presigned_post( # We choose the CreateBucket operation model because its url gets # serialized to what a presign post requires. operation_model = self.meta.service_model.operation_model('CreateBucket') - params = {'Bucket': bucket} + params = self._emit_api_params( + api_params={'Bucket': bucket}, + operation_model=operation_model, + context=context, + ) bucket_is_arn = ArnParser.is_arn(params.get('Bucket', '')) endpoint_url, additional_headers = self._resolve_endpoint_ruleset( operation_model, diff --git a/tests/functional/test_context_params.py b/tests/functional/test_context_params.py index f61e6a18fc..d72b65b265 100644 --- a/tests/functional/test_context_params.py +++ b/tests/functional/test_context_params.py @@ -343,7 +343,7 @@ def test_client_context_param_sent_to_endpoint_resolver( ), ) - # Stub client to prevent a request from getting sent and asceertain that + # Stub client to prevent a request from getting sent and ascertain that # only a single request would get sent. Wrap the EndpointProvider's # resolve_endpoint method for inspecting the arguments it gets called with. with ClientHTTPStubber(client, strict=True) as http_stubber: @@ -488,3 +488,42 @@ def test_dynamic_context_param_sent_to_endpoint_resolver( ) else: mock_resolve_endpoint.assert_called_once_with(Region='us-east-1') + + +def test_dynamic_context_param_from_event_handler_sent_to_endpoint_resolver( + monkeypatch, + patched_session, +): + # patch loader to return fake service model and fake endpoint ruleset + patch_load_service_model( + patched_session, + monkeypatch, + FAKE_MODEL_WITH_DYNAMIC_CONTEXT_PARAM, + FAKE_RULESET_WITH_DYNAMIC_CONTEXT_PARAM, + ) + + # event handler for provide-client-params that modifies the value of the + # MockOpParam parameter + def change_param(params, **kwargs): + params['MockOpParam'] = 'mock-op-param-value-2' + + client = patched_session.create_client( + 'otherservice', region_name='us-east-1' + ) + client.meta.events.register_last( + 'provide-client-params.other-service.*', change_param + ) + + with ClientHTTPStubber(client, strict=True) as http_stubber: + http_stubber.add_response(status=200) + with mock.patch.object( + client._ruleset_resolver._provider, + 'resolve_endpoint', + wraps=client._ruleset_resolver._provider.resolve_endpoint, + ) as mock_resolve_endpoint: + client.mock_operation(MockOpParam='mock-op-param-value-1') + + mock_resolve_endpoint.assert_called_once_with( + Region='us-east-1', + FooDynamicContextParamName='mock-op-param-value-2', + ) diff --git a/tests/unit/test_signers.py b/tests/unit/test_signers.py index 4b0baefe2e..5b05c7fea8 100644 --- a/tests/unit/test_signers.py +++ b/tests/unit/test_signers.py @@ -991,6 +991,37 @@ def test_generate_presign_url_emits_is_presign_in_context(self): 'the following kwargs emitted: %s' % kwargs, ) + def test_context_param_from_event_handler_sent_to_endpoint_resolver(self): + def change_bucket_param(params, **kwargs): + params['Bucket'] = 'mybucket-bar' + + self.client.meta.events.register_last( + 'provide-client-params.s3.*', change_bucket_param + ) + + self.client.generate_presigned_url( + 'get_object', Params={'Bucket': 'mybucket-foo', 'Key': self.key} + ) + + ref_request_dict = { + 'body': b'', + # If the bucket name set in the provide-client-params event handler + # was correctly passed to the endpoint provider as a dynamic context + # parameter, it will appear in the URL and the auth_path: + 'url': 'https://mybucket-bar.s3.amazonaws.com/mykey', + 'headers': {}, + 'auth_path': '/mybucket-bar/mykey', + 'query_string': {}, + 'url_path': '/mykey', + 'method': 'GET', + 'context': mock.ANY, + } + self.generate_url_mock.assert_called_with( + request_dict=ref_request_dict, + expires_in=3600, + operation_name='GetObject', + ) + class TestGeneratePresignedPost(unittest.TestCase): def setUp(self):