Skip to content
This repository was archived by the owner on Mar 23, 2026. It is now read-only.

Commit b5db830

Browse files
committed
implement RpcV2CBOR parser and serializer + tests
1 parent 8d7a1da commit b5db830

5 files changed

Lines changed: 321 additions & 73 deletions

File tree

localstack-core/localstack/aws/protocol/parser.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1267,17 +1267,31 @@ def _parse_payload(
12671267

12681268

12691269
class BaseRpcV2RequestParser(RequestParser):
1270+
"""
1271+
The ``BaseRpcV2RequestParser`` is the base class for all RPC V2-based AWS service protocols.
1272+
This base class handles the routing of the request, which is specific based on the path.
1273+
The body decoding is done in the respective subclasses.
1274+
"""
1275+
12701276
def __init__(self, service: ServiceModel) -> None:
12711277
super().__init__(service)
12721278
# self.ignore_get_body_errors = False
12731279
self._operation_router = RestServiceOperationRouter(service)
12741280

12751281
@_handle_exceptions
12761282
def parse(self, request: Request) -> tuple[OperationModel, Any]:
1283+
# see https://smithy.io/2.0/additional-specs/protocols/smithy-rpc-v2.html
1284+
headers = request.headers
1285+
if "X-Amz-Target" in headers or "X-Amzn-Target" in headers:
1286+
raise ProtocolParserError(
1287+
"RPC v2 CBOR does not accept 'X-Amz-Target' or 'X-Amzn-Target'. "
1288+
"Such requests are rejected for security reasons."
1289+
)
12771290
# TODO: add this special path handling to the ServiceNameParser to allow RPC v2 service to be properly extracted
12781291
# path = '/service/{service_name}/operation/{operation_name}'
1292+
# The Smithy RPCv2 CBOR protocol will only use the last four segments of the URL when routing requests.
12791293
rpc_v2_params = request.path.lstrip("/").split("/")
1280-
if len(rpc_v2_params) != 4 or not (
1294+
if len(rpc_v2_params) < 4 or not (
12811295
operation := self.service.operation_model(rpc_v2_params[-1])
12821296
):
12831297
raise OperationNotFoundParserError(
@@ -1365,6 +1379,13 @@ def _initial_body_parse(self, request: Request):
13651379

13661380

13671381
class RpcV2CBORRequestParser(BaseRpcV2RequestParser, BaseCBORRequestParser):
1382+
"""
1383+
The ``RpcV2CBORRequestParser`` is responsible for parsing incoming requests for services which use the
1384+
``rpc-v2-cbor`` protocol. The requests for these services encode all of their parameters as CBOR in the
1385+
request body.
1386+
"""
1387+
1388+
# TODO: investigate datetime format for RpcV2CBOR protocol, which might be different than Kinesis CBOR
13681389
def _initial_body_parse(self, request: Request):
13691390
body_contents = request.data
13701391
if body_contents == b"":

localstack-core/localstack/aws/protocol/serializer.py

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1426,6 +1426,8 @@ def _serialize_content_type(
14261426

14271427

14281428
class BaseCBORResponseSerializer(ResponseSerializer):
1429+
SUPPORTED_MIME_TYPES = [APPLICATION_CBOR, APPLICATION_AMZ_CBOR_1_1]
1430+
14291431
UNSIGNED_INT_MAJOR_TYPE = 0
14301432
NEGATIVE_INT_MAJOR_TYPE = 1
14311433
BLOB_MAJOR_TYPE = 2
@@ -1661,8 +1663,6 @@ class CBORResponseSerializer(BaseCBORResponseSerializer):
16611663
from the ``JSONResponseSerializer``
16621664
"""
16631665

1664-
SUPPORTED_MIME_TYPES = [APPLICATION_CBOR, APPLICATION_AMZ_CBOR_1_1]
1665-
16661666
TIMESTAMP_FORMAT = "unixtimestamp"
16671667

16681668
def _serialize_error(
@@ -1737,6 +1737,114 @@ def _prepare_additional_traits_in_response(
17371737
return response
17381738

17391739

1740+
class BaseRpcV2Serializer(ResponseSerializer):
1741+
"""
1742+
The BaseRpcV2Serializer performs the basic logic for the RPC V2 response serialization.
1743+
The only variance between the various RPCv2 protocols is the way the body is serialized for regular responses,
1744+
and the way they will encode exceptions.
1745+
"""
1746+
1747+
def _serialize_response(
1748+
self,
1749+
parameters: dict,
1750+
response: Response,
1751+
shape: Shape | None,
1752+
shape_members: dict,
1753+
operation_model: OperationModel,
1754+
mime_type: str,
1755+
request_id: str,
1756+
) -> None:
1757+
response.content_type = mime_type
1758+
response.set_response(
1759+
self._serialize_body_params(parameters, shape, operation_model, mime_type, request_id)
1760+
)
1761+
1762+
def _serialize_body_params(
1763+
self,
1764+
params: dict,
1765+
shape: Shape,
1766+
operation_model: OperationModel,
1767+
mime_type: str,
1768+
request_id: str,
1769+
) -> bytes | None:
1770+
raise NotImplementedError
1771+
1772+
1773+
class RpcV2CBORSerializer(BaseRpcV2Serializer, BaseCBORResponseSerializer):
1774+
"""
1775+
The RpcV2CBORSerializer implements the CBOR body serialization part for the RPC v2 protocol, and implements the
1776+
specific exception serialization.
1777+
https://smithy.io/2.0/additional-specs/protocols/smithy-rpc-v2.html
1778+
"""
1779+
1780+
# the Smithy spec defines that only `application/cbor` is supported for RPC v2 CBOR
1781+
SUPPORTED_MIME_TYPES = [APPLICATION_CBOR]
1782+
# TODO: check the timestamp format for RpcV2CBOR, which might be different than Kinesis CBOR
1783+
TIMESTAMP_FORMAT = "unixtimestamp"
1784+
1785+
def _serialize_body_params(
1786+
self,
1787+
params: dict,
1788+
shape: Shape,
1789+
operation_model: OperationModel,
1790+
mime_type: str,
1791+
request_id: str,
1792+
) -> bytes | None:
1793+
body = bytearray()
1794+
self._serialize_data_item(body, params, shape)
1795+
return bytes(body)
1796+
1797+
def _serialize_error(
1798+
self,
1799+
error: ServiceException,
1800+
response: Response,
1801+
shape: StructureShape,
1802+
operation_model: OperationModel,
1803+
mime_type: str,
1804+
request_id: str,
1805+
) -> None:
1806+
body = bytearray()
1807+
response.content_type = mime_type # can only be 'application/cbor'
1808+
# TODO: the Botocore parser is able to look at the `x-amzn-query-error` header for the RpcV2 CBOR protocol
1809+
# we'll need to investigate which services need it
1810+
# Responses for the rpcv2Cbor protocol SHOULD NOT contain the X-Amzn-ErrorType header.
1811+
# Type information is always serialized in the payload. This is different than `json` protocol
1812+
1813+
if shape:
1814+
# FIXME: we need to manually add the `__type` field to the shape as it is not part of the specs
1815+
# think about a better way, this is very hacky
1816+
# Error responses in the rpcv2Cbor protocol MUST be serialized identically to standard responses with one
1817+
# additional component to distinguish which error is contained: a body field named __type.
1818+
shape_copy = copy.deepcopy(shape)
1819+
shape_copy.members["__type"] = StringShape(
1820+
shape_name="__type", shape_model={"type": "string"}
1821+
)
1822+
remaining_params = {"__type": error.code}
1823+
1824+
for member_name in shape_copy.members:
1825+
if hasattr(error, member_name):
1826+
remaining_params[member_name] = getattr(error, member_name)
1827+
# Default error message fields can sometimes have different casing in the specs
1828+
elif member_name.lower() in ["code", "message"] and hasattr(
1829+
error, member_name.lower()
1830+
):
1831+
remaining_params[member_name] = getattr(error, member_name.lower())
1832+
1833+
self._serialize_data_item(body, remaining_params, shape_copy, None)
1834+
1835+
response.set_response(bytes(body))
1836+
1837+
def _prepare_additional_traits_in_response(
1838+
self, response: Response, operation_model: OperationModel, request_id: str
1839+
):
1840+
response.headers["x-amzn-requestid"] = request_id
1841+
response.headers["Smithy-Protocol"] = "rpc-v2-cbor"
1842+
response = super()._prepare_additional_traits_in_response(
1843+
response, operation_model, request_id
1844+
)
1845+
return response
1846+
1847+
17401848
class S3ResponseSerializer(RestXMLResponseSerializer):
17411849
"""
17421850
The ``S3ResponseSerializer`` adds some minor logic to handle S3 specific peculiarities with the error response
@@ -2101,7 +2209,10 @@ def create_serializer(
21012209
"rest-json": RestJSONResponseSerializer,
21022210
"rest-xml": RestXMLResponseSerializer,
21032211
"ec2": EC2ResponseSerializer,
2104-
"cbor": CBORResponseSerializer,
2212+
"smithy-rpc-v2-cbor": RpcV2CBORSerializer,
2213+
# TODO: implement multi-protocol support for Kinesis, so that it can uses the `cbor` protocol and remove
2214+
# CBOR handling from JSONResponseSerializer
2215+
# "cbor": CBORResponseSerializer,
21052216
}
21062217
# TODO: do we want to add a check if the user-defined protocol is part of the available ones in the ServiceModel?
21072218
# or should it be checked once

localstack-core/localstack/aws/spec.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
LOG = logging.getLogger(__name__)
2222

2323
ServiceName = str
24-
ProtocolName = Literal["query", "json", "rest-json", "rest-xml", "ec2", "smithy-rpc-v2"]
24+
ProtocolName = Literal["query", "json", "rest-json", "rest-xml", "ec2", "smithy-rpc-v2-cbor"]
2525

2626

2727
class ServiceModelIdentifier(NamedTuple):

tests/unit/aws/protocol/test_parser.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1522,7 +1522,3 @@ def test_smithy_rpc_v2_cbor():
15221522
primaryRegion="string",
15231523
tags={"string": "string"},
15241524
)
1525-
1526-
1527-
# arc-region-switch supports both `json` and `smithy-rpc-v2`. Testing that we can support both
1528-
# @pytest.mark.parametrize("protocol", ["json", "smithy-rpc-v2"])

0 commit comments

Comments
 (0)