diff --git a/localstack/aws/forwarder.py b/localstack/aws/forwarder.py index a8e34a3d34bd2..3dbe2f883d0a0 100644 --- a/localstack/aws/forwarder.py +++ b/localstack/aws/forwarder.py @@ -5,7 +5,7 @@ from typing import Any, Callable, Mapping, Optional from urllib.parse import urlsplit -from botocore.awsrequest import AWSPreparedRequest +from botocore.awsrequest import AWSPreparedRequest, prepare_request_dict from botocore.config import Config as BotoConfig from werkzeug.datastructures import Headers @@ -164,9 +164,27 @@ def create_aws_request_context( "has_streaming_input": operation.has_streaming_input, "auth_type": operation.auth_type, } - request_dict = client._convert_to_request_dict(parameters, operation, context=request_context) - aws_request = client._endpoint.create_request(request_dict, operation) + # The endpoint URL is mandatory here, set a dummy if not given (doesn't _need_ to be localstack specific) + if not endpoint_url: + endpoint_url = "http://localhost.localstack.cloud" + request_dict = client._convert_to_request_dict( + parameters, operation, endpoint_url, context=request_context + ) + + if auth_path := request_dict.get("auth_path"): + # botocore >= 1.28 might modify the url path of the request dict (specifically for S3). + # It will then set the original url path as "auth_path". If the auth_path is set, we reset the url_path. + # Afterwards the request needs to be prepared again. + request_dict["url_path"] = auth_path + prepare_request_dict( + request_dict, + endpoint_url=endpoint_url, + user_agent=client._client_config.user_agent, + context=request_context, + ) + + aws_request: AWSPreparedRequest = client._endpoint.create_request(request_dict, operation) context = RequestContext() context.service = service context.operation = operation diff --git a/localstack/aws/protocol/op_router.py b/localstack/aws/protocol/op_router.py index 1f914bc5be0f4..b9ea8a09f1646 100644 --- a/localstack/aws/protocol/op_router.py +++ b/localstack/aws/protocol/op_router.py @@ -28,7 +28,14 @@ class _HttpOperation(NamedTuple): @staticmethod def from_operation(op: OperationModel) -> "_HttpOperation": - uri = op.http.get("requestUri") + # botocore >= 1.28 might modify the internal model (specifically for S3). + # It will modify the request URI and set the original value at "authPath". + # Use authPath if set, otherwise use the regular requestUri. + if auth_path := op.http.get("authPath"): + uri = auth_path.rstrip("/") + else: + uri = op.http.get("requestUri") + method = op.http.get("method") deprecated = op.deprecated diff --git a/setup.cfg b/setup.cfg index dd573ad0b67d5..cc9554f4b8c00 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,7 @@ packages=find: # dependencies that are required for the cli (via pip install localstack) install_requires = - boto3>=1.20,<1.25.0 + boto3>=1.25.0 click>=7.0 cachetools~=5.0.0 #dnspython==1.16.0 @@ -67,7 +67,7 @@ runtime = awscli>=1.22.90 awscrt>=0.13.14 boto>=2.49.0 - botocore>=1.12.13,<1.28.0 + botocore>=1.28.0 cbor2>=5.2.0 crontab>=0.22.6 cryptography diff --git a/tests/unit/aws/protocol/test_parser.py b/tests/unit/aws/protocol/test_parser.py index 913dbe17ea257..6fdd5d44689c8 100644 --- a/tests/unit/aws/protocol/test_parser.py +++ b/tests/unit/aws/protocol/test_parser.py @@ -302,6 +302,12 @@ def _botocore_parser_integration_test( operation_model = service.operation_model(action) serialized_request = serializer.serialize_to_request(kwargs, operation_model) + + # botocore >= 1.28 might modify the url path of the request dict (specifically for S3). + # It will then set the original url path as "auth_path". If the auth_path is set, we reset the url_path. + if auth_path := serialized_request.get("auth_path"): + serialized_request["url_path"] = auth_path + prepare_request_dict(serialized_request, "") split_url = urlsplit(serialized_request.get("url")) path = split_url.path diff --git a/tests/unit/aws/test_service_router.py b/tests/unit/aws/test_service_router.py index d62ef817f6a2f..38e062ab0f14f 100644 --- a/tests/unit/aws/test_service_router.py +++ b/tests/unit/aws/test_service_router.py @@ -30,6 +30,8 @@ def _collect_operations() -> Tuple[ServiceModel, OperationModel]: "chime-sdk-media-pipelines", "chime-sdk-meetings", "chime-sdk-messaging", + "chime-sdk-voice", + "codecatalyst", "connect", "connect-contact-lens", "greengrassv2", @@ -40,6 +42,7 @@ def _collect_operations() -> Tuple[ServiceModel, OperationModel]: "kinesis-video-archived-media", "kinesis-video-media", "kinesis-video-signaling", + "kinesis-video-webrtc-storage", "kinesisvideo", "lex-models", "lex-runtime", @@ -51,9 +54,11 @@ def _collect_operations() -> Tuple[ServiceModel, OperationModel]: "pinpoint-sms-voice", "sagemaker-edge", "sagemaker-featurestore-runtime", + "sagemaker-metrics", "sms-voice", "sso", "sso-oidc", + "workdocs", ]: yield pytest.param( service, @@ -164,7 +169,11 @@ def test_service_router_works_for_every_service( "auth_type": operation.auth_type, } request_args = _create_dummy_request_args(operation) - request_dict = client._convert_to_request_dict(request_args, operation, request_context) + + # The endpoint URL is mandatory here, just set a dummy (doesn't _need_ to be localstack specific) + request_dict = client._convert_to_request_dict( + request_args, operation, "http://localhost.localstack.cloud", request_context + ) request_object = create_request_object(request_dict) client._request_signer.sign(operation.name, request_object) request: Request = _botocore_request_to_localstack_request(request_object)