• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

localstack / localstack / 16820655284

07 Aug 2025 05:03PM UTC coverage: 86.841% (-0.05%) from 86.892%
16820655284

push

github

web-flow
CFNV2: support CDK bootstrap and deployment (#12967)

32 of 38 new or added lines in 5 files covered. (84.21%)

2013 existing lines in 125 files now uncovered.

66606 of 76699 relevant lines covered (86.84%)

0.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.19
/localstack-core/localstack/aws/protocol/serializer.py
1
"""
2
Response serializers for the different AWS service protocols.
3

4
The module contains classes that take a service's response dict, and
5
given an operation model, serialize the HTTP response according to the
6
specified output shape.
7

8
It can be seen as the counterpart to the ``parse`` module in ``botocore``
9
(which parses the result of these serializer). It has a lot of
10
similarities with the ``serialize`` module in ``botocore``, but
11
serves a different purpose (serializing responses instead of requests).
12

13
The different protocols have many similarities. The class hierarchy is
14
designed such that the serializers share as much logic as possible.
15
The class hierarchy looks as follows:
16
::
17
                                      ┌───────────────────┐
18
                                      │ResponseSerializer │
19
                                      └───────────────────┘
20
                                          ▲    ▲      ▲
21
                   ┌──────────────────────┘    │      └──────────────────┐
22
      ┌────────────┴────────────┐ ┌────────────┴─────────────┐ ┌─────────┴────────────┐
23
      │BaseXMLResponseSerializer│ │BaseRestResponseSerializer│ │JSONResponseSerializer│
24
      └─────────────────────────┘ └──────────────────────────┘ └──────────────────────┘
25
                         ▲    ▲             ▲             ▲              ▲
26
  ┌──────────────────────┴─┐ ┌┴─────────────┴──────────┐ ┌┴──────────────┴──────────┐
27
  │QueryResponseSerializer │ │RestXMLResponseSerializer│ │RestJSONResponseSerializer│
28
  └────────────────────────┘ └─────────────────────────┘ └──────────────────────────┘
29
              ▲
30
   ┌──────────┴──────────┐
31
   │EC2ResponseSerializer│
32
   └─────────────────────┘
33
::
34

35
The ``ResponseSerializer`` contains the logic that is used among all the
36
different protocols (``query``, ``json``, ``rest-json``, ``rest-xml``, and
37
``ec2``).
38
The protocols relate to each other in the following ways:
39

40
* The ``query`` and the ``rest-xml`` protocols both have XML bodies in their
41
  responses which are serialized quite similarly (with some specifics for each
42
  type).
43
* The ``json`` and the ``rest-json`` protocols both have JSON bodies in their
44
  responses which are serialized the same way.
45
* The ``rest-json`` and ``rest-xml`` protocols serialize some metadata in
46
  the HTTP response's header fields
47
* The ``ec2`` protocol is basically similar to the ``query`` protocol with a
48
  specific error response formatting.
49

50
The serializer classes in this module correspond directly to the different
51
protocols. ``#create_serializer`` shows the explicit mapping between the
52
classes and the protocols.
53
The classes are structured as follows:
54

55
* The ``ResponseSerializer`` contains all the basic logic for the
56
  serialization which is shared among all different protocols.
57
* The ``BaseXMLResponseSerializer`` and the ``JSONResponseSerializer``
58
  contain the logic for the XML and the JSON serialization respectively.
59
* The ``BaseRestResponseSerializer`` contains the logic for the REST
60
  protocol specifics (i.e. specific HTTP header serializations).
61
* The ``RestXMLResponseSerializer`` and the ``RestJSONResponseSerializer``
62
  inherit the ReST specific logic from the ``BaseRestResponseSerializer``
63
  and the XML / JSON body serialization from their second super class.
64

65
The services and their protocols are defined by using AWS's Smithy
66
(a language to define services in a - somewhat - protocol-agnostic
67
way). The "peculiarities" in this serializer code usually correspond
68
to certain so-called "traits" in Smithy.
69

70
The result of the serialization methods is the HTTP response which can
71
be sent back to the calling client.
72
"""
73

74
import abc
1✔
75
import base64
1✔
76
import functools
1✔
77
import json
1✔
78
import logging
1✔
79
import string
1✔
80
from abc import ABC
1✔
81
from binascii import crc32
1✔
82
from collections.abc import Iterable, Iterator
1✔
83
from datetime import datetime
1✔
84
from email.utils import formatdate
1✔
85
from struct import pack
1✔
86
from typing import Any
1✔
87
from xml.etree import ElementTree as ETree
1✔
88

89
import xmltodict
1✔
90
from botocore.model import ListShape, MapShape, OperationModel, ServiceModel, Shape, StructureShape
1✔
91
from botocore.serialize import ISO8601, ISO8601_MICRO
1✔
92
from botocore.utils import calculate_md5, is_json_value_header, parse_to_aware_datetime
1✔
93

94
# cbor2: explicitly load from private _encoder module to avoid using the (non-patched) C-version
95
from cbor2._encoder import dumps as cbor2_dumps
1✔
96
from werkzeug import Request as WerkzeugRequest
1✔
97
from werkzeug import Response as WerkzeugResponse
1✔
98
from werkzeug.datastructures import Headers, MIMEAccept
1✔
99
from werkzeug.http import parse_accept_header
1✔
100

101
from localstack.aws.api import CommonServiceException, ServiceException
1✔
102
from localstack.aws.spec import ProtocolName, load_service
1✔
103
from localstack.constants import (
1✔
104
    APPLICATION_AMZ_CBOR_1_1,
105
    APPLICATION_AMZ_JSON_1_0,
106
    APPLICATION_AMZ_JSON_1_1,
107
    APPLICATION_CBOR,
108
    APPLICATION_JSON,
109
    APPLICATION_XML,
110
    TEXT_XML,
111
)
112
from localstack.http import Response
1✔
113
from localstack.utils.common import to_bytes, to_str
1✔
114
from localstack.utils.strings import long_uid
1✔
115
from localstack.utils.xml import strip_xmlns
1✔
116

117
LOG = logging.getLogger(__name__)
1✔
118

119
REQUEST_ID_CHARACTERS = string.digits + string.ascii_uppercase
1✔
120

121

122
class ResponseSerializerError(Exception):
1✔
123
    """
124
    Error which is thrown if the request serialization fails.
125
    Super class of all exceptions raised by the serializer.
126
    """
127

128
    pass
1✔
129

130

131
class UnknownSerializerError(ResponseSerializerError):
1✔
132
    """
133
    Error which indicates that the exception raised by the serializer could be caused by invalid data or by any other
134
    (unknown) issue. Errors like this should be reported and indicate an issue in the serializer itself.
135
    """
136

137
    pass
1✔
138

139

140
class ProtocolSerializerError(ResponseSerializerError):
1✔
141
    """
142
    Error which indicates that the given data is not compliant with the service's specification and cannot be
143
    serialized. This usually results in a response to the client with an HTTP 5xx status code (internal server error).
144
    """
145

146
    pass
1✔
147

148

149
def _handle_exceptions(func):
1✔
150
    """
151
    Decorator which handles the exceptions raised by the serializer. It ensures that all exceptions raised by the public
152
    methods of the parser are instances of ResponseSerializerError.
153
    :param func: to wrap in order to add the exception handling
154
    :return: wrapped function
155
    """
156

157
    @functools.wraps(func)
1✔
158
    def wrapper(*args, **kwargs):
1✔
159
        try:
1✔
160
            return func(*args, **kwargs)
1✔
161
        except ResponseSerializerError:
1✔
162
            raise
1✔
163
        except Exception as e:
1✔
164
            raise UnknownSerializerError(
1✔
165
                "An unknown error occurred when trying to serialize the response."
166
            ) from e
167

168
    return wrapper
1✔
169

170

171
class ResponseSerializer(abc.ABC):
1✔
172
    """
173
    The response serializer is responsible for the serialization of a service implementation's result to an actual
174
    HTTP response (which will be sent to the calling client).
175
    It is the base class of all serializers and therefore contains the basic logic which is used among all of them.
176
    """
177

178
    DEFAULT_ENCODING = "utf-8"
1✔
179
    # The default timestamp format is ISO8601, but this can be overwritten by subclasses.
180
    TIMESTAMP_FORMAT = "iso8601"
1✔
181
    # Event streaming binary data type mapping for type "string"
182
    AWS_BINARY_DATA_TYPE_STRING = 7
1✔
183
    # Defines the supported mime types of the specific serializer. Sorted by priority (preferred / default first).
184
    # Needs to be specified by subclasses.
185
    SUPPORTED_MIME_TYPES: list[str] = []
1✔
186

187
    @_handle_exceptions
1✔
188
    def serialize_to_response(
1✔
189
        self,
190
        response: dict,
191
        operation_model: OperationModel,
192
        headers: dict | Headers | None,
193
        request_id: str,
194
    ) -> Response:
195
        """
196
        Takes a response dict and serializes it to an actual HttpResponse.
197

198
        :param response: to serialize
199
        :param operation_model: specification of the service & operation containing information about the shape of the
200
                                service's output / response
201
        :param headers: the headers of the incoming request this response should be serialized for. This is necessary
202
                        for features like Content-Negotiation (define response content type based on request headers).
203
        :param request_id: autogenerated AWS request ID identifying the original request
204
        :return: Response which can be sent to the calling client
205
        :raises: ResponseSerializerError (either a ProtocolSerializerError or an UnknownSerializerError)
206
        """
207

208
        # determine the preferred mime type (based on the serializer's supported mime types and the Accept header)
209
        mime_type = self._get_mime_type(headers)
1✔
210

211
        # if the operation has a streaming output, handle the serialization differently
212
        if operation_model.has_event_stream_output:
1✔
213
            return self._serialize_event_stream(response, operation_model, mime_type, request_id)
1✔
214

215
        serialized_response = self._create_default_response(operation_model, mime_type)
1✔
216
        shape = operation_model.output_shape
1✔
217
        # The shape can also be none (for empty responses), but it still needs to be serialized (to add some metadata)
218
        shape_members = shape.members if shape is not None else None
1✔
219
        self._serialize_response(
1✔
220
            response,
221
            serialized_response,
222
            shape,
223
            shape_members,
224
            operation_model,
225
            mime_type,
226
            request_id,
227
        )
228
        serialized_response = self._prepare_additional_traits_in_response(
1✔
229
            serialized_response, operation_model, request_id
230
        )
231
        return serialized_response
1✔
232

233
    @_handle_exceptions
1✔
234
    def serialize_error_to_response(
1✔
235
        self,
236
        error: ServiceException,
237
        operation_model: OperationModel,
238
        headers: dict | Headers | None,
239
        request_id: str,
240
    ) -> Response:
241
        """
242
        Takes an error instance and serializes it to an actual HttpResponse.
243
        Therefore, this method is used for errors which should be serialized and transmitted to the calling client.
244

245
        :param error: to serialize
246
        :param operation_model: specification of the service & operation containing information about the shape of the
247
                                service's output / response
248
        :param headers: the headers of the incoming request this response should be serialized for. This is necessary
249
                        for features like Content-Negotiation (define response content type based on request headers).
250
        :param request_id: autogenerated AWS request ID identifying the original request
251
        :return: HttpResponse which can be sent to the calling client
252
        :raises: ResponseSerializerError (either a ProtocolSerializerError or an UnknownSerializerError)
253
        """
254
        # determine the preferred mime type (based on the serializer's supported mime types and the Accept header)
255
        mime_type = self._get_mime_type(headers)
1✔
256

257
        # TODO implement streaming error serialization
258
        serialized_response = self._create_default_response(operation_model, mime_type)
1✔
259
        if not error or not isinstance(error, ServiceException):
1✔
260
            raise ProtocolSerializerError(
1✔
261
                f"Error to serialize ({error.__class__.__name__ if error else None}) is not a ServiceException."
262
            )
263
        shape = operation_model.service_model.shape_for_error_code(error.code)
1✔
264
        serialized_response.status_code = error.status_code
1✔
265

266
        self._serialize_error(
1✔
267
            error, serialized_response, shape, operation_model, mime_type, request_id
268
        )
269
        serialized_response = self._prepare_additional_traits_in_response(
1✔
270
            serialized_response, operation_model, request_id
271
        )
272
        return serialized_response
1✔
273

274
    def _serialize_response(
1✔
275
        self,
276
        parameters: dict,
277
        response: Response,
278
        shape: Shape | None,
279
        shape_members: dict,
280
        operation_model: OperationModel,
281
        mime_type: str,
282
        request_id: str,
283
    ) -> None:
284
        raise NotImplementedError
285

286
    def _serialize_body_params(
1✔
287
        self,
288
        params: dict,
289
        shape: Shape,
290
        operation_model: OperationModel,
291
        mime_type: str,
292
        request_id: str,
293
    ) -> str | None:
294
        """
295
        Actually serializes the given params for the given shape to a string for the transmission in the body of the
296
        response.
297
        :param params: to serialize
298
        :param shape: to know how to serialize the params
299
        :param operation_model: for additional metadata
300
        :param mime_type: Mime type which should be used to encode the payload
301
        :param request_id: autogenerated AWS request ID identifying the original request
302
        :return: string containing the serialized body
303
        """
304
        raise NotImplementedError
305

306
    def _serialize_error(
1✔
307
        self,
308
        error: ServiceException,
309
        response: Response,
310
        shape: StructureShape,
311
        operation_model: OperationModel,
312
        mime_type: str,
313
        request_id: str,
314
    ) -> None:
315
        raise NotImplementedError
316

317
    def _serialize_event_stream(
1✔
318
        self,
319
        response: dict,
320
        operation_model: OperationModel,
321
        mime_type: str,
322
        request_id: str,
323
    ) -> Response:
324
        """
325
        Serializes a given response dict (the return payload of a service implementation) to an _event stream_ using the
326
        given operation model.
327

328
        :param response: dictionary containing the payload for the response
329
        :param operation_model: describing the operation the response dict is being returned by
330
        :param mime_type: Mime type which should be used to encode the payload
331
        :param request_id: autogenerated AWS request ID identifying the original request
332
        :return: Response which can directly be sent to the client (in chunks)
333
        """
334
        event_stream_shape = operation_model.get_event_stream_output()
1✔
335
        event_stream_member_name = operation_model.output_shape.event_stream_name
1✔
336

337
        # wrap the generator in operation specific serialization
338
        def event_stream_serializer() -> Iterable[bytes]:
1✔
339
            yield self._encode_event_payload("initial-response")
1✔
340

341
            # create a default response
342
            serialized_event_response = self._create_default_response(operation_model, mime_type)
1✔
343
            # get the members of the event stream shape
344
            event_stream_shape_members = (
1✔
345
                event_stream_shape.members if event_stream_shape is not None else None
346
            )
347
            # extract the generator from the given response data
348
            event_generator = response.get(event_stream_member_name)
1✔
349
            if not isinstance(event_generator, Iterator):
1✔
UNCOV
350
                raise ProtocolSerializerError(
×
351
                    "Expected iterator for streaming event serialization."
352
                )
353

354
            # yield one event per generated event
355
            for event in event_generator:
1✔
356
                # find the actual event payload (the member with event=true)
357
                event_member_shape = None
1✔
358
                event_member_name = None
1✔
359
                for member_name, member_shape in event_stream_shape_members.items():
1✔
360
                    if member_shape.serialization.get("event") and member_name in event:
1✔
361
                        event_member_shape = member_shape
1✔
362
                        event_member_name = member_name
1✔
363
                        break
1✔
364
                if event_member_shape is None:
1✔
UNCOV
365
                    raise UnknownSerializerError("Couldn't find event shape for serialization.")
×
366

367
                # serialize the part of the response for the event
368
                self._serialize_response(
1✔
369
                    event.get(event_member_name),
370
                    serialized_event_response,
371
                    event_member_shape,
372
                    event_member_shape.members if event_member_shape is not None else None,
373
                    operation_model,
374
                    mime_type,
375
                    request_id,
376
                )
377
                # execute additional response traits (might be modifying the response)
378
                serialized_event_response = self._prepare_additional_traits_in_response(
1✔
379
                    serialized_event_response, operation_model, request_id
380
                )
381
                # encode the event and yield it
382
                yield self._encode_event_payload(
1✔
383
                    event_type=event_member_name, content=serialized_event_response.data
384
                )
385

386
        return Response(
1✔
387
            response=event_stream_serializer(),
388
            status=operation_model.http.get("responseCode", 200),
389
        )
390

391
    def _encode_event_payload(
1✔
392
        self,
393
        event_type: str,
394
        content: str | bytes = "",
395
        error_code: str | None = None,
396
        error_message: str | None = None,
397
    ) -> bytes:
398
        """
399
        Encodes the given event payload according to AWS specific binary event encoding.
400
        A specification of the format can be found in the AWS docs:
401
        https://docs.aws.amazon.com/AmazonS3/latest/API/RESTSelectObjectAppendix.html
402

403
        :param content: string or bytes of the event payload
404
        :param event_type: type of the event. Usually the name of the event shape or specific event types like
405
                            "initial-response".
406
        :param error_code: Optional. Error code if the payload represents an error.
407
        :param error_message: Optional. Error message if the payload represents an error.
408
        :return: bytes with the AWS-specific encoded event payload
409
        """
410

411
        # determine the event type (error if an error message or an error code is set)
412
        if error_message or error_code:
1✔
UNCOV
413
            message_type = "error"
×
414
        else:
415
            message_type = "event"
1✔
416

417
        # set the headers
418
        headers = {":event-type": event_type, ":message-type": message_type}
1✔
419
        if error_message:
1✔
UNCOV
420
            headers[":error-message"] = error_message
×
421
        if error_code:
1✔
UNCOV
422
            headers[":error-code"] = error_code
×
423

424
        # construct headers
425
        header_section = b""
1✔
426
        for key, value in headers.items():
1✔
427
            header_name = key.encode(self.DEFAULT_ENCODING)
1✔
428
            header_value = to_bytes(value)
1✔
429
            header_section += pack("!B", len(header_name))
1✔
430
            header_section += header_name
1✔
431
            header_section += pack("!B", self.AWS_BINARY_DATA_TYPE_STRING)
1✔
432
            header_section += pack("!H", len(header_value))
1✔
433
            header_section += header_value
1✔
434

435
        # construct body
436
        if isinstance(content, str):
1✔
437
            payload = bytes(content, self.DEFAULT_ENCODING)
1✔
438
        else:
439
            payload = content
1✔
440

441
        # calculate lengths
442
        headers_length = len(header_section)
1✔
443
        payload_length = len(payload)
1✔
444

445
        # construct message
446
        # - prelude
447
        result = pack("!I", payload_length + headers_length + 16)
1✔
448
        result += pack("!I", headers_length)
1✔
449
        # - prelude crc
450
        prelude_crc = crc32(result)
1✔
451
        result += pack("!I", prelude_crc)
1✔
452
        # - headers
453
        result += header_section
1✔
454
        # - payload
455
        result += payload
1✔
456
        # - message crc
457
        payload_crc = crc32(result)
1✔
458
        result += pack("!I", payload_crc)
1✔
459

460
        return result
1✔
461

462
    def _create_default_response(self, operation_model: OperationModel, mime_type: str) -> Response:
1✔
463
        """
464
        Creates a boilerplate default response to be used by subclasses as starting points.
465
        Uses the default HTTP response status code defined in the operation model (if defined), otherwise 200.
466

467
        :param operation_model: to extract the default HTTP status code
468
        :param mime_type: Mime type which should be used to encode the payload
469
        :return: boilerplate HTTP response
470
        """
471
        return Response(status=operation_model.http.get("responseCode", 200))
1✔
472

473
    def _get_mime_type(self, headers: dict | Headers | None) -> str:
1✔
474
        """
475
        Extracts the accepted mime type from the request headers and returns a matching, supported mime type for the
476
        serializer or the default mime type of the service if there is no match.
477
        :param headers: to extract the "Accept" header from
478
        :return: preferred mime type to be used by the serializer (if it is not accepted by the client,
479
                 an error is logged)
480
        """
481
        accept_header = None
1✔
482
        if headers and "Accept" in headers and not headers.get("Accept") == "*/*":
1✔
483
            accept_header = headers.get("Accept")
1✔
484
        elif headers and headers.get("Content-Type"):
1✔
485
            # If there is no specific Accept header given, we use the given Content-Type as a fallback.
486
            # i.e. if the request content was JSON encoded and the client doesn't send a specific an Accept header, the
487
            # serializer should prefer JSON encoding.
488
            content_type = headers.get("Content-Type")
1✔
489
            LOG.debug(
1✔
490
                "No accept header given. Using request's Content-Type (%s) as preferred response Content-Type.",
491
                content_type,
492
            )
493
            accept_header = content_type + ", */*"
1✔
494
        mime_accept: MIMEAccept = parse_accept_header(accept_header, MIMEAccept)
1✔
495
        mime_type = mime_accept.best_match(self.SUPPORTED_MIME_TYPES)
1✔
496
        if not mime_type:
1✔
497
            # There is no match between the supported mime types and the requested one(s)
498
            mime_type = self.SUPPORTED_MIME_TYPES[0]
1✔
499
            LOG.debug(
1✔
500
                "Determined accept type (%s) is not supported by this serializer. Using default of this serializer: %s",
501
                accept_header,
502
                mime_type,
503
            )
504
        return mime_type
1✔
505

506
    # Some extra utility methods subclasses can use.
507

508
    @staticmethod
1✔
509
    def _timestamp_iso8601(value: datetime) -> str:
1✔
510
        if value.microsecond > 0:
1✔
511
            timestamp_format = ISO8601_MICRO
1✔
512
        else:
513
            timestamp_format = ISO8601
1✔
514
        return value.strftime(timestamp_format)
1✔
515

516
    @staticmethod
1✔
517
    def _timestamp_unixtimestamp(value: datetime) -> float:
1✔
518
        return value.timestamp()
1✔
519

520
    def _timestamp_rfc822(self, value: datetime) -> str:
1✔
521
        if isinstance(value, datetime):
1✔
522
            value = self._timestamp_unixtimestamp(value)
1✔
523
        return formatdate(value, usegmt=True)
1✔
524

525
    def _convert_timestamp_to_str(self, value: int | str | datetime, timestamp_format=None) -> str:
1✔
526
        if timestamp_format is None:
1✔
527
            timestamp_format = self.TIMESTAMP_FORMAT
1✔
528
        timestamp_format = timestamp_format.lower()
1✔
529
        datetime_obj = parse_to_aware_datetime(value)
1✔
530
        converter = getattr(self, "_timestamp_%s" % timestamp_format)
1✔
531
        final_value = converter(datetime_obj)
1✔
532
        return final_value
1✔
533

534
    @staticmethod
1✔
535
    def _get_serialized_name(shape: Shape, default_name: str) -> str:
1✔
536
        """
537
        Returns the serialized name for the shape if it exists.
538
        Otherwise, it will return the passed in default_name.
539
        """
540
        return shape.serialization.get("name", default_name)
1✔
541

542
    def _get_base64(self, value: str | bytes):
1✔
543
        """
544
        Returns the base64-encoded version of value, handling
545
        both strings and bytes. The returned value is a string
546
        via the default encoding.
547
        """
548
        if isinstance(value, str):
1✔
UNCOV
549
            value = value.encode(self.DEFAULT_ENCODING)
×
550
        return base64.b64encode(value).strip().decode(self.DEFAULT_ENCODING)
1✔
551

552
    def _encode_payload(self, body: bytes | str) -> bytes:
1✔
553
        if isinstance(body, str):
1✔
554
            return body.encode(self.DEFAULT_ENCODING)
1✔
555
        return body
1✔
556

557
    def _prepare_additional_traits_in_response(
1✔
558
        self, response: Response, operation_model: OperationModel, request_id: str
559
    ):
560
        """Applies additional traits on the raw response for a given model or protocol."""
561
        if operation_model.http_checksum_required:
1✔
UNCOV
562
            self._add_md5_header(response)
×
563
        return response
1✔
564

565
    def _has_header(self, header_name: str, headers: dict):
1✔
566
        """Case-insensitive check for header key."""
567
        if header_name is None:
1✔
UNCOV
568
            return False
×
569
        else:
570
            return header_name.lower() in [key.lower() for key in headers.keys()]
1✔
571

572
    def _add_md5_header(self, response: Response):
1✔
573
        """Add a Content-MD5 header if not yet there. Adapted from botocore.utils"""
UNCOV
574
        headers = response.headers
×
575
        body = response.data
×
576
        if body is not None and "Content-MD5" not in headers:
×
577
            md5_digest = calculate_md5(body)
×
578
            headers["Content-MD5"] = md5_digest
×
579

580
    def _get_error_message(self, error: Exception) -> str | None:
1✔
581
        return str(error) if error is not None and str(error) != "None" else None
1✔
582

583

584
class BaseXMLResponseSerializer(ResponseSerializer):
1✔
585
    """
586
    The BaseXMLResponseSerializer performs the basic logic for the XML response serialization.
587
    It is slightly adapted by the QueryResponseSerializer.
588
    While the botocore's RestXMLSerializer is quite similar, there are some subtle differences (since botocore's
589
    implementation handles the serialization of the requests from the client to the service, not the responses from the
590
    service to the client).
591
    """
592

593
    SUPPORTED_MIME_TYPES = [TEXT_XML, APPLICATION_XML, APPLICATION_JSON]
1✔
594

595
    def _serialize_error(
1✔
596
        self,
597
        error: ServiceException,
598
        response: Response,
599
        shape: StructureShape,
600
        operation_model: OperationModel,
601
        mime_type: str,
602
        request_id: str,
603
    ) -> None:
604
        # Check if we need to add a namespace
605
        attr = (
1✔
606
            {"xmlns": operation_model.metadata.get("xmlNamespace")}
607
            if "xmlNamespace" in operation_model.metadata
608
            else {}
609
        )
610
        root = ETree.Element("ErrorResponse", attr)
1✔
611

612
        error_tag = ETree.SubElement(root, "Error")
1✔
613
        self._add_error_tags(error, error_tag, mime_type)
1✔
614
        request_id_element = ETree.SubElement(root, "RequestId")
1✔
615
        request_id_element.text = request_id
1✔
616

617
        self._add_additional_error_tags(vars(error), root, shape, mime_type)
1✔
618

619
        response.set_response(self._encode_payload(self._node_to_string(root, mime_type)))
1✔
620

621
    def _add_error_tags(
1✔
622
        self, error: ServiceException, error_tag: ETree.Element, mime_type: str
623
    ) -> None:
624
        code_tag = ETree.SubElement(error_tag, "Code")
1✔
625
        code_tag.text = error.code
1✔
626
        message = self._get_error_message(error)
1✔
627
        if message:
1✔
628
            self._default_serialize(error_tag, message, None, "Message", mime_type)
1✔
629
        if error.sender_fault:
1✔
630
            # The sender fault is either not set or "Sender"
631
            self._default_serialize(error_tag, "Sender", None, "Type", mime_type)
1✔
632

633
    def _add_additional_error_tags(
1✔
634
        self, parameters: dict, node: ETree, shape: StructureShape, mime_type: str
635
    ):
636
        if shape:
1✔
637
            params = {}
1✔
638
            # TODO add a possibility to serialize simple non-modelled errors (like S3 NoSuchBucket#BucketName)
639
            for member in shape.members:
1✔
640
                # XML protocols do not add modeled default fields to the root node
641
                # (tested for cloudfront, route53, cloudwatch, iam)
642
                if member.lower() not in ["code", "message"] and member in parameters:
1✔
643
                    params[member] = parameters[member]
1✔
644

645
            # If there is an error shape with members which should be set, they need to be added to the node
646
            if params:
1✔
647
                # Serialize the remaining params
648
                root_name = shape.serialization.get("name", shape.name)
1✔
649
                pseudo_root = ETree.Element("")
1✔
650
                self._serialize(shape, params, pseudo_root, root_name, mime_type)
1✔
651
                real_root = list(pseudo_root)[0]
1✔
652
                # Add the child elements to the already created root error element
653
                for child in list(real_root):
1✔
654
                    node.append(child)
1✔
655

656
    def _serialize_body_params(
1✔
657
        self,
658
        params: dict,
659
        shape: Shape,
660
        operation_model: OperationModel,
661
        mime_type: str,
662
        request_id: str,
663
    ) -> str | None:
664
        root = self._serialize_body_params_to_xml(params, shape, operation_model, mime_type)
1✔
665
        self._prepare_additional_traits_in_xml(root, request_id)
1✔
666
        return self._node_to_string(root, mime_type)
1✔
667

668
    def _serialize_body_params_to_xml(
1✔
669
        self, params: dict, shape: Shape, operation_model: OperationModel, mime_type: str
670
    ) -> ETree.Element | None:
671
        if shape is None:
1✔
672
            return
1✔
673
        # The botocore serializer expects `shape.serialization["name"]`, but this isn't always present for responses
674
        root_name = shape.serialization.get("name", shape.name)
1✔
675
        pseudo_root = ETree.Element("")
1✔
676
        self._serialize(shape, params, pseudo_root, root_name, mime_type)
1✔
677
        real_root = list(pseudo_root)[0]
1✔
678
        return real_root
1✔
679

680
    def _serialize(
1✔
681
        self, shape: Shape, params: Any, xmlnode: ETree.Element, name: str, mime_type: str
682
    ) -> None:
683
        """This method dynamically invokes the correct `_serialize_type_*` method for each shape type."""
684
        if shape is None:
1✔
UNCOV
685
            return
×
686
        # Some output shapes define a `resultWrapper` in their serialization spec.
687
        # While the name would imply that the result is _wrapped_, it is actually renamed.
688
        if shape.serialization.get("resultWrapper"):
1✔
689
            name = shape.serialization.get("resultWrapper")
1✔
690

691
        try:
1✔
692
            method = getattr(self, "_serialize_type_%s" % shape.type_name, self._default_serialize)
1✔
693
            method(xmlnode, params, shape, name, mime_type)
1✔
694
        except (TypeError, ValueError, AttributeError) as e:
1✔
695
            raise ProtocolSerializerError(
1✔
696
                f"Invalid type when serializing {shape.name}: '{xmlnode}' cannot be parsed to {shape.type_name}."
697
            ) from e
698

699
    def _serialize_type_structure(
1✔
700
        self, xmlnode: ETree.Element, params: dict, shape: StructureShape, name: str, mime_type
701
    ) -> None:
702
        structure_node = ETree.SubElement(xmlnode, name)
1✔
703

704
        if "xmlNamespace" in shape.serialization:
1✔
705
            namespace_metadata = shape.serialization["xmlNamespace"]
1✔
706
            attribute_name = "xmlns"
1✔
707
            if namespace_metadata.get("prefix"):
1✔
708
                attribute_name += ":%s" % namespace_metadata["prefix"]
1✔
709
            structure_node.attrib[attribute_name] = namespace_metadata["uri"]
1✔
710
        for key, value in params.items():
1✔
711
            if value is None:
1✔
712
                # Don't serialize any param whose value is None.
713
                continue
1✔
714
            try:
1✔
715
                member_shape = shape.members[key]
1✔
716
            except KeyError:
1✔
717
                LOG.warning(
1✔
718
                    "Response object %s contains a member which is not specified: %s",
719
                    shape.name,
720
                    key,
721
                )
722
                continue
1✔
723
            member_name = member_shape.serialization.get("name", key)
1✔
724
            # We need to special case member shapes that are marked as an xmlAttribute.
725
            # Rather than serializing into an XML child node, we instead serialize the shape to
726
            # an XML attribute of the *current* node.
727
            if member_shape.serialization.get("xmlAttribute"):
1✔
728
                # xmlAttributes must have a serialization name.
729
                xml_attribute_name = member_shape.serialization["name"]
1✔
730
                structure_node.attrib[xml_attribute_name] = value
1✔
731
                continue
1✔
732
            self._serialize(member_shape, value, structure_node, member_name, mime_type)
1✔
733

734
    def _serialize_type_list(
1✔
735
        self, xmlnode: ETree.Element, params: list, shape: ListShape, name: str, mime_type: str
736
    ) -> None:
737
        if params is None:
1✔
738
            # Don't serialize any param whose value is None.
UNCOV
739
            return
×
740
        member_shape = shape.member
1✔
741
        if shape.serialization.get("flattened"):
1✔
742
            # If the list is flattened, either take the member's "name" or the name of the usual name for the parent
743
            # element for the children.
744
            element_name = self._get_serialized_name(member_shape, name)
1✔
745
            list_node = xmlnode
1✔
746
        else:
747
            element_name = self._get_serialized_name(member_shape, "member")
1✔
748
            list_node = ETree.SubElement(xmlnode, name)
1✔
749
        for item in params:
1✔
750
            # Don't serialize any item which is None
751
            if item is not None:
1✔
752
                self._serialize(member_shape, item, list_node, element_name, mime_type)
1✔
753

754
    def _serialize_type_map(
1✔
755
        self, xmlnode: ETree.Element, params: dict, shape: MapShape, name: str, mime_type: str
756
    ) -> None:
757
        """
758
        Given the ``name`` of MyMap, an input of {"key1": "val1", "key2": "val2"}, and the ``flattened: False``
759
        we serialize this as:
760
          <MyMap>
761
            <entry>
762
              <key>key1</key>
763
              <value>val1</value>
764
            </entry>
765
            <entry>
766
              <key>key2</key>
767
              <value>val2</value>
768
            </entry>
769
          </MyMap>
770
        If it is flattened, it is serialized as follows:
771
          <MyMap>
772
            <key>key1</key>
773
            <value>val1</value>
774
          </MyMap>
775
          <MyMap>
776
            <key>key2</key>
777
            <value>val2</value>
778
          </MyMap>
779
        """
780
        if params is None:
1✔
781
            # Don't serialize a non-existing map
UNCOV
782
            return
×
783
        if shape.serialization.get("flattened"):
1✔
784
            entries_node = xmlnode
1✔
785
            entry_node_name = name
1✔
786
        else:
787
            entries_node = ETree.SubElement(xmlnode, name)
1✔
788
            entry_node_name = "entry"
1✔
789

790
        for key, value in params.items():
1✔
791
            if value is None:
1✔
792
                # Don't serialize any param whose value is None.
UNCOV
793
                continue
×
794
            entry_node = ETree.SubElement(entries_node, entry_node_name)
1✔
795
            key_name = self._get_serialized_name(shape.key, default_name="key")
1✔
796
            val_name = self._get_serialized_name(shape.value, default_name="value")
1✔
797
            self._serialize(shape.key, key, entry_node, key_name, mime_type)
1✔
798
            self._serialize(shape.value, value, entry_node, val_name, mime_type)
1✔
799

800
    @staticmethod
1✔
801
    def _serialize_type_boolean(xmlnode: ETree.Element, params: bool, _, name: str, __) -> None:
1✔
802
        """
803
        For scalar types, the 'params' attr is actually just a scalar value representing the data
804
        we need to serialize as a boolean. It will either be 'true' or 'false'
805
        """
806
        node = ETree.SubElement(xmlnode, name)
1✔
807
        if params:
1✔
808
            str_value = "true"
1✔
809
        else:
810
            str_value = "false"
1✔
811
        node.text = str_value
1✔
812

813
    def _serialize_type_blob(
1✔
814
        self, xmlnode: ETree.Element, params: str | bytes, _, name: str, __
815
    ) -> None:
816
        node = ETree.SubElement(xmlnode, name)
1✔
817
        node.text = self._get_base64(params)
1✔
818

819
    def _serialize_type_timestamp(
1✔
820
        self, xmlnode: ETree.Element, params: str, shape: Shape, name: str, mime_type: str
821
    ) -> None:
822
        node = ETree.SubElement(xmlnode, name)
1✔
823
        if mime_type != APPLICATION_JSON:
1✔
824
            # Default XML timestamp serialization
825
            node.text = self._convert_timestamp_to_str(
1✔
826
                params, shape.serialization.get("timestampFormat")
827
            )
828
        else:
829
            # For services with XML protocols, where the Accept header is JSON, timestamps are formatted like for JSON
830
            # protocols, but using the int representation instead of the float representation (f.e. requesting JSON
831
            # responses in STS).
832
            node.text = str(
1✔
833
                int(self._convert_timestamp_to_str(params, JSONResponseSerializer.TIMESTAMP_FORMAT))
834
            )
835

836
    def _default_serialize(self, xmlnode: ETree.Element, params: str, _, name: str, __) -> None:
1✔
837
        node = ETree.SubElement(xmlnode, name)
1✔
838
        node.text = str(params)
1✔
839

840
    def _prepare_additional_traits_in_xml(self, root: ETree.Element | None, request_id: str):
1✔
841
        """
842
        Prepares the XML root node before being serialized with additional traits (like the Response ID in the Query
843
        protocol).
844
        For some protocols (like rest-xml), the root can be None.
845
        """
846
        pass
1✔
847

848
    def _create_default_response(self, operation_model: OperationModel, mime_type: str) -> Response:
1✔
849
        response = super()._create_default_response(operation_model, mime_type)
1✔
850
        response.headers["Content-Type"] = mime_type
1✔
851
        return response
1✔
852

853
    def _node_to_string(self, root: ETree.Element | None, mime_type: str) -> str | None:
1✔
854
        """Generates the string representation of the given XML element."""
855
        if root is not None:
1✔
856
            content = ETree.tostring(
1✔
857
                element=root, encoding=self.DEFAULT_ENCODING, xml_declaration=True
858
            )
859
            if mime_type == APPLICATION_JSON:
1✔
860
                # FIXME try to directly convert the ElementTree node to JSON
861
                xml_dict = xmltodict.parse(content)
1✔
862
                xml_dict = strip_xmlns(xml_dict)
1✔
863
                content = json.dumps(xml_dict)
1✔
864
            return content
1✔
865

866

867
class BaseRestResponseSerializer(ResponseSerializer, ABC):
1✔
868
    """
869
    The BaseRestResponseSerializer performs the basic logic for the ReST response serialization.
870
    In our case it basically only adds the request metadata to the HTTP header.
871
    """
872

873
    HEADER_TIMESTAMP_FORMAT = "rfc822"
1✔
874

875
    def _serialize_response(
1✔
876
        self,
877
        parameters: dict,
878
        response: Response,
879
        shape: Shape | None,
880
        shape_members: dict,
881
        operation_model: OperationModel,
882
        mime_type: str,
883
        request_id: str,
884
    ) -> None:
885
        header_params, payload_params = self._partition_members(parameters, shape)
1✔
886
        self._process_header_members(header_params, response, shape)
1✔
887
        # "HEAD" responses are basically "GET" responses without the actual body.
888
        # Do not process the body payload in this case (setting a body could also manipulate the headers)
889
        if operation_model.http.get("method") != "HEAD":
1✔
890
            self._serialize_payload(
1✔
891
                payload_params,
892
                response,
893
                shape,
894
                shape_members,
895
                operation_model,
896
                mime_type,
897
                request_id,
898
            )
899
        self._serialize_content_type(response, shape, shape_members, mime_type)
1✔
900
        self._prepare_additional_traits_in_response(response, operation_model, request_id)
1✔
901

902
    def _serialize_payload(
1✔
903
        self,
904
        parameters: dict,
905
        response: Response,
906
        shape: Shape | None,
907
        shape_members: dict,
908
        operation_model: OperationModel,
909
        mime_type: str,
910
        request_id: str,
911
    ) -> None:
912
        """
913
        Serializes the given payload.
914

915
        :param parameters: The user input params
916
        :param response: The final serialized Response
917
        :param shape: Describes the expected output shape (can be None in case of an "empty" response)
918
        :param shape_members: The members of the output struct shape
919
        :param operation_model: The specification of the operation of which the response is serialized here
920
        :param mime_type: Mime type which should be used to encode the payload
921
        :param request_id: autogenerated AWS request ID identifying the original request
922
        :return: None - the given `serialized` dict is modified
923
        """
924
        if shape is None:
1✔
925
            return
1✔
926

927
        payload_member = shape.serialization.get("payload")
1✔
928
        # If this shape is defined as being an event, we need to search for the payload member
929
        if not payload_member and shape.serialization.get("event"):
1✔
930
            for member_name, member_shape in shape_members.items():
1✔
931
                # Try to find the first shape which is marked as "eventpayload" and is given in the params dict
932
                if member_shape.serialization.get("eventpayload") and parameters.get(member_name):
1✔
933
                    payload_member = member_name
1✔
934
                    break
1✔
935
        if payload_member is not None and shape_members[payload_member].type_name in [
1✔
936
            "blob",
937
            "string",
938
        ]:
939
            # If it's streaming, then the body is just the value of the payload.
940
            body_payload = parameters.get(payload_member, b"")
1✔
941
            body_payload = self._encode_payload(body_payload)
1✔
942
            response.set_response(body_payload)
1✔
943
        elif payload_member is not None:
1✔
944
            # If there's a payload member, we serialized that member to the body.
945
            body_params = parameters.get(payload_member)
1✔
946
            if body_params is not None:
1✔
947
                response.set_response(
1✔
948
                    self._encode_payload(
949
                        self._serialize_body_params(
950
                            body_params,
951
                            shape_members[payload_member],
952
                            operation_model,
953
                            mime_type,
954
                            request_id,
955
                        )
956
                    )
957
                )
958
        else:
959
            # Otherwise, we use the "traditional" way of serializing the whole parameters dict recursively.
960
            response.set_response(
1✔
961
                self._encode_payload(
962
                    self._serialize_body_params(
963
                        parameters, shape, operation_model, mime_type, request_id
964
                    )
965
                )
966
            )
967

968
    def _serialize_content_type(
1✔
969
        self, serialized: Response, shape: Shape, shape_members: dict, mime_type: str
970
    ):
971
        """
972
        Some protocols require varied Content-Type headers depending on user input.
973
        This allows subclasses to apply this conditionally.
974
        """
975
        pass
1✔
976

977
    def _has_streaming_payload(self, payload: str | None, shape_members):
1✔
978
        """Determine if payload is streaming (a blob or string)."""
979
        return payload is not None and shape_members[payload].type_name in ["blob", "string"]
1✔
980

981
    def _prepare_additional_traits_in_response(
1✔
982
        self, response: Response, operation_model: OperationModel, request_id: str
983
    ):
984
        """Adds the request ID to the headers (in contrast to the body - as in the Query protocol)."""
985
        response = super()._prepare_additional_traits_in_response(
1✔
986
            response, operation_model, request_id
987
        )
988
        response.headers["x-amz-request-id"] = request_id
1✔
989
        return response
1✔
990

991
    def _process_header_members(self, parameters: dict, response: Response, shape: Shape):
1✔
992
        shape_members = shape.members if isinstance(shape, StructureShape) else []
1✔
993
        for name in shape_members:
1✔
994
            member_shape = shape_members[name]
1✔
995
            location = member_shape.serialization.get("location")
1✔
996
            if not location:
1✔
997
                continue
1✔
998
            if name not in parameters:
1✔
999
                # ignores optional keys
1000
                continue
1✔
1001
            key = member_shape.serialization.get("name", name)
1✔
1002
            value = parameters[name]
1✔
1003
            if value is None:
1✔
UNCOV
1004
                continue
×
1005
            if location == "header":
1✔
1006
                response.headers[key] = self._serialize_header_value(member_shape, value)
1✔
1007
            elif location == "headers":
1✔
1008
                header_prefix = key
1✔
1009
                self._serialize_header_map(header_prefix, response, value)
1✔
1010
            elif location == "statusCode":
1✔
1011
                response.status_code = int(value)
1✔
1012

1013
    def _serialize_header_map(self, prefix: str, response: Response, params: dict) -> None:
1✔
1014
        """Serializes the header map for the location trait "headers"."""
1015
        for key, val in params.items():
1✔
1016
            actual_key = prefix + key
1✔
1017
            response.headers[actual_key] = val
1✔
1018

1019
    def _serialize_header_value(self, shape: Shape, value: Any):
1✔
1020
        """Serializes a value for the location trait "header"."""
1021
        if shape.type_name == "timestamp":
1✔
1022
            datetime_obj = parse_to_aware_datetime(value)
1✔
1023
            timestamp_format = shape.serialization.get(
1✔
1024
                "timestampFormat", self.HEADER_TIMESTAMP_FORMAT
1025
            )
1026
            return self._convert_timestamp_to_str(datetime_obj, timestamp_format)
1✔
1027
        elif shape.type_name == "list":
1✔
UNCOV
1028
            converted_value = [
×
1029
                self._serialize_header_value(shape.member, v) for v in value if v is not None
1030
            ]
UNCOV
1031
            return ",".join(converted_value)
×
1032
        elif shape.type_name == "boolean":
1✔
1033
            # Set the header value to "true" if the given value is truthy, otherwise set the header value to "false".
1034
            return "true" if value else "false"
1✔
1035
        elif is_json_value_header(shape):
1✔
1036
            # Serialize with no spaces after separators to save space in
1037
            # the header.
UNCOV
1038
            return self._get_base64(json.dumps(value, separators=(",", ":")))
×
1039
        else:
1040
            return value
1✔
1041

1042
    def _partition_members(self, parameters: dict, shape: Shape | None) -> tuple[dict, dict]:
1✔
1043
        """Separates the top-level keys in the given parameters dict into header- and payload-located params."""
1044
        if not isinstance(shape, StructureShape):
1✔
1045
            # If the shape isn't a structure, we default to the whole response being parsed in the body.
1046
            # Non-payload members are only loaded in the top-level hierarchy and those are always structures.
1047
            return {}, parameters
1✔
1048
        header_params = {}
1✔
1049
        payload_params = {}
1✔
1050
        shape_members = shape.members
1✔
1051
        for name in shape_members:
1✔
1052
            member_shape = shape_members[name]
1✔
1053
            if name not in parameters:
1✔
1054
                continue
1✔
1055
            location = member_shape.serialization.get("location")
1✔
1056
            if location:
1✔
1057
                header_params[name] = parameters[name]
1✔
1058
            else:
1059
                payload_params[name] = parameters[name]
1✔
1060
        return header_params, payload_params
1✔
1061

1062

1063
class RestXMLResponseSerializer(BaseRestResponseSerializer, BaseXMLResponseSerializer):
1✔
1064
    """
1065
    The ``RestXMLResponseSerializer`` is responsible for the serialization of responses from services with the
1066
    ``rest-xml`` protocol.
1067
    It combines the ``BaseRestResponseSerializer`` (for the ReST specific logic) with the ``BaseXMLResponseSerializer``
1068
    (for the XML body response serialization).
1069
    """
1070

1071
    pass
1✔
1072

1073

1074
class QueryResponseSerializer(BaseXMLResponseSerializer):
1✔
1075
    """
1076
    The ``QueryResponseSerializer`` is responsible for the serialization of responses from services which use the
1077
    ``query`` protocol. The responses of these services also use XML. It is basically a subset of the features, since it
1078
    does not allow any payload or location traits.
1079
    """
1080

1081
    def _serialize_response(
1✔
1082
        self,
1083
        parameters: dict,
1084
        response: Response,
1085
        shape: Shape | None,
1086
        shape_members: dict,
1087
        operation_model: OperationModel,
1088
        mime_type: str,
1089
        request_id: str,
1090
    ) -> None:
1091
        """
1092
        Serializes the given parameters as XML for the query protocol.
1093

1094
        :param parameters: The user input params
1095
        :param response: The final serialized Response
1096
        :param shape: Describes the expected output shape (can be None in case of an "empty" response)
1097
        :param shape_members: The members of the output struct shape
1098
        :param operation_model: The specification of the operation of which the response is serialized here
1099
        :param mime_type: Mime type which should be used to encode the payload
1100
        :param request_id: autogenerated AWS request ID identifying the original request
1101
        :return: None - the given `serialized` dict is modified
1102
        """
1103
        response.set_response(
1✔
1104
            self._encode_payload(
1105
                self._serialize_body_params(
1106
                    parameters, shape, operation_model, mime_type, request_id
1107
                )
1108
            )
1109
        )
1110

1111
    def _serialize_body_params_to_xml(
1✔
1112
        self, params: dict, shape: Shape, operation_model: OperationModel, mime_type: str
1113
    ) -> ETree.Element:
1114
        # The Query protocol responses have a root element which is not contained in the specification file.
1115
        # Therefore, we first call the super function to perform the normal XML serialization, and afterwards wrap the
1116
        # result in a root element based on the operation name.
1117
        node = super()._serialize_body_params_to_xml(params, shape, operation_model, mime_type)
1✔
1118

1119
        # Check if we need to add a namespace
1120
        attr = (
1✔
1121
            {"xmlns": operation_model.metadata.get("xmlNamespace")}
1122
            if "xmlNamespace" in operation_model.metadata
1123
            else None
1124
        )
1125

1126
        # Create the root element and add the result of the XML serializer as a child node
1127
        root = ETree.Element(f"{operation_model.name}Response", attr)
1✔
1128
        if node is not None:
1✔
1129
            root.append(node)
1✔
1130
        return root
1✔
1131

1132
    def _prepare_additional_traits_in_xml(self, root: ETree.Element | None, request_id: str):
1✔
1133
        # Add the response metadata here (it's not defined in the specs)
1134
        # For the ec2 and the query protocol, the root cannot be None at this time.
1135
        response_metadata = ETree.SubElement(root, "ResponseMetadata")
1✔
1136
        request_id_element = ETree.SubElement(response_metadata, "RequestId")
1✔
1137
        request_id_element.text = request_id
1✔
1138

1139

1140
class EC2ResponseSerializer(QueryResponseSerializer):
1✔
1141
    """
1142
    The ``EC2ResponseSerializer`` is responsible for the serialization of responses from services which use the
1143
    ``ec2`` protocol (basically the EC2 service). This protocol is basically equal to the ``query`` protocol with only
1144
    a few subtle differences.
1145
    """
1146

1147
    def _serialize_error(
1✔
1148
        self,
1149
        error: ServiceException,
1150
        response: Response,
1151
        shape: StructureShape,
1152
        operation_model: OperationModel,
1153
        mime_type: str,
1154
        request_id: str,
1155
    ) -> None:
1156
        # EC2 errors look like:
1157
        # <Response>
1158
        #   <Errors>
1159
        #     <Error>
1160
        #       <Code>InvalidInstanceID.Malformed</Code>
1161
        #       <Message>Invalid id: "1343124"</Message>
1162
        #     </Error>
1163
        #   </Errors>
1164
        #   <RequestID>12345</RequestID>
1165
        # </Response>
1166
        # This is different from QueryParser in that it's RequestID, not RequestId
1167
        # and that the Error tag is in an enclosing Errors tag.
1168
        attr = (
1✔
1169
            {"xmlns": operation_model.metadata.get("xmlNamespace")}
1170
            if "xmlNamespace" in operation_model.metadata
1171
            else None
1172
        )
1173
        root = ETree.Element("Response", attr)
1✔
1174
        errors_tag = ETree.SubElement(root, "Errors")
1✔
1175
        error_tag = ETree.SubElement(errors_tag, "Error")
1✔
1176
        self._add_error_tags(error, error_tag, mime_type)
1✔
1177
        request_id_element = ETree.SubElement(root, "RequestID")
1✔
1178
        request_id_element.text = request_id
1✔
1179
        response.set_response(self._encode_payload(self._node_to_string(root, mime_type)))
1✔
1180

1181
    def _prepare_additional_traits_in_xml(self, root: ETree.Element | None, request_id: str):
1✔
1182
        # The EC2 protocol does not use the root output shape, therefore we need to remove the hierarchy level
1183
        # below the root level
1184
        if len(root) > 0:
1✔
1185
            output_node = root[0]
1✔
1186
            for child in output_node:
1✔
1187
                root.append(child)
1✔
1188
            root.remove(output_node)
1✔
1189

1190
        # Add the requestId here (it's not defined in the specs)
1191
        # For the ec2 and the query protocol, the root cannot be None at this time.
1192
        request_id_element = ETree.SubElement(root, "requestId")
1✔
1193
        request_id_element.text = request_id
1✔
1194

1195

1196
class JSONResponseSerializer(ResponseSerializer):
1✔
1197
    """
1198
    The ``JSONResponseSerializer`` is responsible for the serialization of responses from services with the ``json``
1199
    protocol. It implements the JSON response body serialization, which is also used by the
1200
    ``RestJSONResponseSerializer``.
1201
    """
1202

1203
    JSON_TYPES = [APPLICATION_JSON, APPLICATION_AMZ_JSON_1_0, APPLICATION_AMZ_JSON_1_1]
1✔
1204
    CBOR_TYPES = [APPLICATION_CBOR, APPLICATION_AMZ_CBOR_1_1]
1✔
1205
    SUPPORTED_MIME_TYPES = JSON_TYPES + CBOR_TYPES
1✔
1206

1207
    TIMESTAMP_FORMAT = "unixtimestamp"
1✔
1208

1209
    def _serialize_error(
1✔
1210
        self,
1211
        error: ServiceException,
1212
        response: Response,
1213
        shape: StructureShape,
1214
        operation_model: OperationModel,
1215
        mime_type: str,
1216
        request_id: str,
1217
    ) -> None:
1218
        body = dict()
1✔
1219

1220
        # TODO implement different service-specific serializer configurations
1221
        #   - currently we set both, the `__type` member as well as the `X-Amzn-Errortype` header
1222
        #   - the specification defines that it's either the __type field OR the header
1223
        response.headers["X-Amzn-Errortype"] = error.code
1✔
1224
        body["__type"] = error.code
1✔
1225

1226
        if shape:
1✔
1227
            remaining_params = {}
1✔
1228
            # TODO add a possibility to serialize simple non-modelled errors (like S3 NoSuchBucket#BucketName)
1229
            for member in shape.members:
1✔
1230
                if hasattr(error, member):
1✔
1231
                    remaining_params[member] = getattr(error, member)
1✔
1232
                # Default error message fields can sometimes have different casing in the specs
1233
                elif member.lower() in ["code", "message"] and hasattr(error, member.lower()):
1✔
1234
                    remaining_params[member] = getattr(error, member.lower())
1✔
1235
            self._serialize(body, remaining_params, shape, None, mime_type)
1✔
1236

1237
        # Only set the message if it has not been set with the shape members
1238
        if "message" not in body and "Message" not in body:
1✔
1239
            message = self._get_error_message(error)
1✔
1240
            if message is not None:
1✔
1241
                body["message"] = message
1✔
1242

1243
        if mime_type in self.CBOR_TYPES:
1✔
UNCOV
1244
            response.set_response(cbor2_dumps(body, datetime_as_timestamp=True))
×
1245
            response.content_type = mime_type
×
1246
        else:
1247
            response.set_json(body)
1✔
1248

1249
    def _serialize_response(
1✔
1250
        self,
1251
        parameters: dict,
1252
        response: Response,
1253
        shape: Shape | None,
1254
        shape_members: dict,
1255
        operation_model: OperationModel,
1256
        mime_type: str,
1257
        request_id: str,
1258
    ) -> None:
1259
        if mime_type in self.CBOR_TYPES:
1✔
1260
            response.content_type = mime_type
1✔
1261
        else:
1262
            json_version = operation_model.metadata.get("jsonVersion")
1✔
1263
            if json_version is not None:
1✔
1264
                response.headers["Content-Type"] = "application/x-amz-json-%s" % json_version
1✔
1265
        response.set_response(
1✔
1266
            self._serialize_body_params(parameters, shape, operation_model, mime_type, request_id)
1267
        )
1268

1269
    def _serialize_body_params(
1✔
1270
        self,
1271
        params: dict,
1272
        shape: Shape,
1273
        operation_model: OperationModel,
1274
        mime_type: str,
1275
        request_id: str,
1276
    ) -> str | None:
1277
        body = {}
1✔
1278
        if shape is not None:
1✔
1279
            self._serialize(body, params, shape, None, mime_type)
1✔
1280

1281
        if mime_type in self.CBOR_TYPES:
1✔
1282
            return cbor2_dumps(body, datetime_as_timestamp=True)
1✔
1283
        else:
1284
            return json.dumps(body)
1✔
1285

1286
    def _serialize(self, body: dict, value: Any, shape, key: str | None, mime_type: str):
1✔
1287
        """This method dynamically invokes the correct `_serialize_type_*` method for each shape type."""
1288
        try:
1✔
1289
            method = getattr(self, "_serialize_type_%s" % shape.type_name, self._default_serialize)
1✔
1290
            method(body, value, shape, key, mime_type)
1✔
UNCOV
1291
        except (TypeError, ValueError, AttributeError) as e:
×
1292
            raise ProtocolSerializerError(
×
1293
                f"Invalid type when serializing {shape.name}: '{value}' cannot be parsed to {shape.type_name}."
1294
            ) from e
1295

1296
    def _serialize_type_structure(
1✔
1297
        self, body: dict, value: dict, shape: StructureShape, key: str | None, mime_type: str
1298
    ):
1299
        if value is None:
1✔
UNCOV
1300
            return
×
1301
        if shape.is_document_type:
1✔
UNCOV
1302
            body[key] = value
×
1303
        else:
1304
            if key is not None:
1✔
1305
                # If a key is provided, this is a result of a recursive
1306
                # call, so we need to add a new child dict as the value
1307
                # of the passed in serialized dict.  We'll then add
1308
                # all the structure members as key/vals in the new serialized
1309
                # dictionary we just created.
1310
                new_serialized = {}
1✔
1311
                body[key] = new_serialized
1✔
1312
                body = new_serialized
1✔
1313
            members = shape.members
1✔
1314
            for member_key, member_value in value.items():
1✔
1315
                if member_value is None:
1✔
1316
                    continue
1✔
1317
                try:
1✔
1318
                    member_shape = members[member_key]
1✔
1319
                except KeyError:
1✔
1320
                    LOG.warning(
1✔
1321
                        "Response object %s contains a member which is not specified: %s",
1322
                        shape.name,
1323
                        member_key,
1324
                    )
1325
                    continue
1✔
1326
                if "name" in member_shape.serialization:
1✔
1327
                    member_key = member_shape.serialization["name"]
1✔
1328
                self._serialize(body, member_value, member_shape, member_key, mime_type)
1✔
1329

1330
    def _serialize_type_map(
1✔
1331
        self, body: dict, value: dict, shape: MapShape, key: str, mime_type: str
1332
    ):
1333
        if value is None:
1✔
UNCOV
1334
            return
×
1335
        map_obj = {}
1✔
1336
        body[key] = map_obj
1✔
1337
        for sub_key, sub_value in value.items():
1✔
1338
            if sub_value is not None:
1✔
1339
                self._serialize(map_obj, sub_value, shape.value, sub_key, mime_type)
1✔
1340

1341
    def _serialize_type_list(
1✔
1342
        self, body: dict, value: list, shape: ListShape, key: str, mime_type: str
1343
    ):
1344
        if value is None:
1✔
UNCOV
1345
            return
×
1346
        list_obj = []
1✔
1347
        body[key] = list_obj
1✔
1348
        for list_item in value:
1✔
1349
            if list_item is not None:
1✔
1350
                wrapper = {}
1✔
1351
                # The JSON list serialization is the only case where we aren't
1352
                # setting a key on a dict.  We handle this by using
1353
                # a __current__ key on a wrapper dict to serialize each
1354
                # list item before appending it to the serialized list.
1355
                self._serialize(wrapper, list_item, shape.member, "__current__", mime_type)
1✔
1356
                list_obj.append(wrapper["__current__"])
1✔
1357

1358
    def _default_serialize(self, body: dict, value: Any, _, key: str, __):
1✔
1359
        body[key] = value
1✔
1360

1361
    def _serialize_type_timestamp(
1✔
1362
        self, body: dict, value: Any, shape: Shape, key: str, mime_type: str
1363
    ):
1364
        if mime_type in self.CBOR_TYPES:
1✔
1365
            # CBOR has native support for timestamps
1366
            body[key] = value
1✔
1367
        else:
1368
            timestamp_format = shape.serialization.get("timestampFormat")
1✔
1369
            body[key] = self._convert_timestamp_to_str(value, timestamp_format)
1✔
1370

1371
    def _serialize_type_blob(self, body: dict, value: str | bytes, _, key: str, mime_type: str):
1✔
1372
        if mime_type in self.CBOR_TYPES:
1✔
1373
            body[key] = value
1✔
1374
        else:
1375
            body[key] = self._get_base64(value)
1✔
1376

1377
    def _prepare_additional_traits_in_response(
1✔
1378
        self, response: Response, operation_model: OperationModel, request_id: str
1379
    ):
1380
        response.headers["x-amzn-requestid"] = request_id
1✔
1381
        response = super()._prepare_additional_traits_in_response(
1✔
1382
            response, operation_model, request_id
1383
        )
1384
        return response
1✔
1385

1386

1387
class RestJSONResponseSerializer(BaseRestResponseSerializer, JSONResponseSerializer):
1✔
1388
    """
1389
    The ``RestJSONResponseSerializer`` is responsible for the serialization of responses from services with the
1390
    ``rest-json`` protocol.
1391
    It combines the ``BaseRestResponseSerializer`` (for the ReST specific logic) with the ``JSONResponseSerializer``
1392
    (for the JSOn body response serialization).
1393
    """
1394

1395
    def _serialize_content_type(
1✔
1396
        self, serialized: Response, shape: Shape, shape_members: dict, mime_type: str
1397
    ):
1398
        """Set Content-Type to application/json for all structured bodies."""
1399
        payload = shape.serialization.get("payload") if shape is not None else None
1✔
1400
        if self._has_streaming_payload(payload, shape_members):
1✔
1401
            # Don't apply content-type to streaming bodies
1402
            return
1✔
1403

1404
        has_body = serialized.data != b""
1✔
1405
        has_content_type = self._has_header("Content-Type", serialized.headers)
1✔
1406
        if has_body and not has_content_type:
1✔
UNCOV
1407
            serialized.headers["Content-Type"] = mime_type
×
1408

1409

1410
class S3ResponseSerializer(RestXMLResponseSerializer):
1✔
1411
    """
1412
    The ``S3ResponseSerializer`` adds some minor logic to handle S3 specific peculiarities with the error response
1413
    serialization and the root node tag.
1414
    """
1415

1416
    SUPPORTED_MIME_TYPES = [APPLICATION_XML, TEXT_XML]
1✔
1417
    _RESPONSE_ROOT_TAGS = {
1✔
1418
        "CompleteMultipartUploadOutput": "CompleteMultipartUploadResult",
1419
        "CopyObjectOutput": "CopyObjectResult",
1420
        "CreateMultipartUploadOutput": "InitiateMultipartUploadResult",
1421
        "DeleteObjectsOutput": "DeleteResult",
1422
        "GetBucketAccelerateConfigurationOutput": "AccelerateConfiguration",
1423
        "GetBucketAclOutput": "AccessControlPolicy",
1424
        "GetBucketAnalyticsConfigurationOutput": "AnalyticsConfiguration",
1425
        "GetBucketCorsOutput": "CORSConfiguration",
1426
        "GetBucketEncryptionOutput": "ServerSideEncryptionConfiguration",
1427
        "GetBucketIntelligentTieringConfigurationOutput": "IntelligentTieringConfiguration",
1428
        "GetBucketInventoryConfigurationOutput": "InventoryConfiguration",
1429
        "GetBucketLifecycleOutput": "LifecycleConfiguration",
1430
        "GetBucketLifecycleConfigurationOutput": "LifecycleConfiguration",
1431
        "GetBucketLoggingOutput": "BucketLoggingStatus",
1432
        "GetBucketMetricsConfigurationOutput": "MetricsConfiguration",
1433
        "NotificationConfigurationDeprecated": "NotificationConfiguration",
1434
        "GetBucketOwnershipControlsOutput": "OwnershipControls",
1435
        "GetBucketPolicyStatusOutput": "PolicyStatus",
1436
        "GetBucketReplicationOutput": "ReplicationConfiguration",
1437
        "GetBucketRequestPaymentOutput": "RequestPaymentConfiguration",
1438
        "GetBucketTaggingOutput": "Tagging",
1439
        "GetBucketVersioningOutput": "VersioningConfiguration",
1440
        "GetBucketWebsiteOutput": "WebsiteConfiguration",
1441
        "GetObjectAclOutput": "AccessControlPolicy",
1442
        "GetObjectLegalHoldOutput": "LegalHold",
1443
        "GetObjectLockConfigurationOutput": "ObjectLockConfiguration",
1444
        "GetObjectRetentionOutput": "Retention",
1445
        "GetObjectTaggingOutput": "Tagging",
1446
        "GetObjectAttributesOutput": "GetObjectAttributesResponse",
1447
        "GetPublicAccessBlockOutput": "PublicAccessBlockConfiguration",
1448
        "ListBucketAnalyticsConfigurationsOutput": "ListBucketAnalyticsConfigurationResult",
1449
        "ListBucketInventoryConfigurationsOutput": "ListInventoryConfigurationsResult",
1450
        "ListBucketMetricsConfigurationsOutput": "ListMetricsConfigurationsResult",
1451
        "ListBucketsOutput": "ListAllMyBucketsResult",
1452
        "ListMultipartUploadsOutput": "ListMultipartUploadsResult",
1453
        "ListObjectsOutput": "ListBucketResult",
1454
        "ListObjectsV2Output": "ListBucketResult",
1455
        "ListObjectVersionsOutput": "ListVersionsResult",
1456
        "ListPartsOutput": "ListPartsResult",
1457
        "UploadPartCopyOutput": "CopyPartResult",
1458
    }
1459

1460
    XML_NAMESPACE = "http://s3.amazonaws.com/doc/2006-03-01/"
1✔
1461

1462
    def _serialize_response(
1✔
1463
        self,
1464
        parameters: dict,
1465
        response: Response,
1466
        shape: Shape | None,
1467
        shape_members: dict,
1468
        operation_model: OperationModel,
1469
        mime_type: str,
1470
        request_id: str,
1471
    ) -> None:
1472
        header_params, payload_params = self._partition_members(parameters, shape)
1✔
1473
        self._process_header_members(header_params, response, shape)
1✔
1474
        # "HEAD" responses are basically "GET" responses without the actual body.
1475
        # Do not process the body payload in this case (setting a body could also manipulate the headers)
1476
        # - If the response is a redirection, the body should be empty as well
1477
        # - If the response is from a "PUT" request, the body should be empty except if there's a specific "payload"
1478
        #   field in the serialization (CopyObject and CopyObjectPart)
1479
        http_method = operation_model.http.get("method")
1✔
1480
        if (
1✔
1481
            http_method != "HEAD"
1482
            and not 300 <= response.status_code < 400
1483
            and not (http_method == "PUT" and shape and not shape.serialization.get("payload"))
1484
        ):
1485
            self._serialize_payload(
1✔
1486
                payload_params,
1487
                response,
1488
                shape,
1489
                shape_members,
1490
                operation_model,
1491
                mime_type,
1492
                request_id,
1493
            )
1494
        self._serialize_content_type(response, shape, shape_members, mime_type)
1✔
1495

1496
    def _serialize_error(
1✔
1497
        self,
1498
        error: ServiceException,
1499
        response: Response,
1500
        shape: StructureShape,
1501
        operation_model: OperationModel,
1502
        mime_type: str,
1503
        request_id: str,
1504
    ) -> None:
1505
        attr = (
1✔
1506
            {"xmlns": operation_model.metadata.get("xmlNamespace")}
1507
            if "xmlNamespace" in operation_model.metadata
1508
            else {}
1509
        )
1510
        root = ETree.Element("Error", attr)
1✔
1511
        self._add_error_tags(error, root, mime_type)
1✔
1512
        request_id_element = ETree.SubElement(root, "RequestId")
1✔
1513
        request_id_element.text = request_id
1✔
1514

1515
        header_params, payload_params = self._partition_members(vars(error), shape)
1✔
1516
        self._add_additional_error_tags(payload_params, root, shape, mime_type)
1✔
1517
        self._process_header_members(header_params, response, shape)
1✔
1518

1519
        response.set_response(self._encode_payload(self._node_to_string(root, mime_type)))
1✔
1520

1521
    def _serialize_body_params(
1✔
1522
        self,
1523
        params: dict,
1524
        shape: Shape,
1525
        operation_model: OperationModel,
1526
        mime_type: str,
1527
        request_id: str,
1528
    ) -> str | None:
1529
        root = self._serialize_body_params_to_xml(params, shape, operation_model, mime_type)
1✔
1530
        # S3 does not follow the specs on the root tag name for 41 of 44 operations
1531
        root.tag = self._RESPONSE_ROOT_TAGS.get(root.tag, root.tag)
1✔
1532
        self._prepare_additional_traits_in_xml(root, request_id)
1✔
1533
        return self._node_to_string(root, mime_type)
1✔
1534

1535
    def _prepare_additional_traits_in_response(
1✔
1536
        self, response: Response, operation_model: OperationModel, request_id: str
1537
    ):
1538
        """Adds the request ID to the headers (in contrast to the body - as in the Query protocol)."""
1539
        response = super()._prepare_additional_traits_in_response(
1✔
1540
            response, operation_model, request_id
1541
        )
1542
        # s3 extended Request ID
1543
        # mostly used internally on AWS and corresponds to a HostId
1544
        response.headers["x-amz-id-2"] = (
1✔
1545
            "s9lzHYrFp76ZVxRcpX9+5cjAnEH2ROuNkd2BHfIa6UkFVdtjf5mKR3/eTPFvsiP/XV/VLi31234="
1546
        )
1547
        return response
1✔
1548

1549
    def _add_error_tags(
1✔
1550
        self, error: ServiceException, error_tag: ETree.Element, mime_type: str
1551
    ) -> None:
1552
        code_tag = ETree.SubElement(error_tag, "Code")
1✔
1553
        code_tag.text = error.code
1✔
1554
        message = self._get_error_message(error)
1✔
1555
        if message:
1✔
1556
            self._default_serialize(error_tag, message, None, "Message", mime_type)
1✔
1557
        else:
1558
            # In S3, if there's no message, create an empty node
1559
            self._create_empty_node(error_tag, "Message")
1✔
1560
        if error.sender_fault:
1✔
1561
            # The sender fault is either not set or "Sender"
UNCOV
1562
            self._default_serialize(error_tag, "Sender", None, "Type", mime_type)
×
1563

1564
    @staticmethod
1✔
1565
    def _create_empty_node(xmlnode: ETree.Element, name: str) -> None:
1✔
1566
        ETree.SubElement(xmlnode, name)
1✔
1567

1568
    def _prepare_additional_traits_in_xml(self, root: ETree.Element | None, request_id: str):
1✔
1569
        # some tools (Serverless) require a newline after the "<?xml ...>\n" preamble line, e.g., for LocationConstraint
1570
        if root and not root.tail:
1✔
1571
            root.tail = "\n"
1✔
1572

1573
        root.attrib["xmlns"] = self.XML_NAMESPACE
1✔
1574

1575
    @staticmethod
1✔
1576
    def _timestamp_iso8601(value: datetime) -> str:
1✔
1577
        """
1578
        This is very specific to S3, S3 returns an ISO8601 timestamp but with milliseconds always set to 000
1579
        Some SDKs are very picky about the length
1580
        """
1581
        return value.strftime("%Y-%m-%dT%H:%M:%S.000Z")
1✔
1582

1583

1584
class SqsQueryResponseSerializer(QueryResponseSerializer):
1✔
1585
    """
1586
    Unfortunately, SQS uses a rare interpretation of the XML protocol: It uses HTML entities within XML tag text nodes.
1587
    For example:
1588
    - Normal XML serializers: <Message>No need to escape quotes (like this: ") with HTML entities in XML.</Message>
1589
    - SQS XML serializer: <Message>No need to escape quotes (like this: &quot;) with HTML entities in XML.</Message>
1590

1591
    None of the prominent XML frameworks for python allow HTML entity escapes when serializing XML.
1592
    This serializer implements the following workaround:
1593
    - Escape quotes and \r with their HTML entities (&quot; and &#xD;).
1594
    - Since & is (correctly) escaped in XML, the serialized string contains &amp;quot; and &amp;#xD;
1595
    - These double-escapes are corrected by replacing such strings with their original.
1596
    """
1597

1598
    # those are deleted from the JSON specs, but need to be kept for legacy reason (sent in 'x-amzn-query-error')
1599
    QUERY_PREFIXED_ERRORS = {
1✔
1600
        "BatchEntryIdsNotDistinct",
1601
        "BatchRequestTooLong",
1602
        "EmptyBatchRequest",
1603
        "InvalidBatchEntryId",
1604
        "MessageNotInflight",
1605
        "PurgeQueueInProgress",
1606
        "QueueDeletedRecently",
1607
        "TooManyEntriesInBatchRequest",
1608
        "UnsupportedOperation",
1609
    }
1610

1611
    # Some error code changed between JSON and query, and we need to have a way to map it for legacy reason
1612
    JSON_TO_QUERY_ERROR_CODES = {
1✔
1613
        "InvalidParameterValueException": "InvalidParameterValue",
1614
        "MissingRequiredParameterException": "MissingParameter",
1615
        "AccessDeniedException": "AccessDenied",
1616
        "QueueDoesNotExist": "AWS.SimpleQueueService.NonExistentQueue",
1617
        "QueueNameExists": "QueueAlreadyExists",
1618
    }
1619

1620
    SENDER_FAULT_ERRORS = (
1✔
1621
        QUERY_PREFIXED_ERRORS
1622
        | JSON_TO_QUERY_ERROR_CODES.keys()
1623
        | {"OverLimit", "ResourceNotFoundException"}
1624
    )
1625

1626
    def _default_serialize(self, xmlnode: ETree.Element, params: str, _, name: str, __) -> None:
1✔
1627
        """
1628
        Ensures that we "mark" characters in the node's text which need to be specifically encoded.
1629
        This is necessary to easily identify these specific characters later, after the standard XML serialization is
1630
        done, while not replacing any other occurrences of these characters which might appear in the serialized string.
1631
        """
1632
        node = ETree.SubElement(xmlnode, name)
1✔
1633
        node.text = (
1✔
1634
            str(params)
1635
            .replace('"', '__marker__"__marker__')
1636
            .replace("\r", "__marker__-r__marker__")
1637
        )
1638

1639
    def _node_to_string(self, root: ETree.ElementTree | None, mime_type: str) -> str | None:
1✔
1640
        """Replaces the previously "marked" characters with their encoded value."""
1641
        generated_string = super()._node_to_string(root, mime_type)
1✔
1642
        if generated_string is None:
1✔
UNCOV
1643
            return None
×
1644
        generated_string = to_str(generated_string)
1✔
1645
        # Undo the second escaping of the &
1646
        # Undo the second escaping of the carriage return (\r)
1647
        if mime_type == APPLICATION_JSON:
1✔
1648
            # At this point the json was already dumped and escaped, so we replace directly.
1649
            generated_string = generated_string.replace(r"__marker__\"__marker__", r"\"").replace(
1✔
1650
                "__marker__-r__marker__", r"\r"
1651
            )
1652
        else:
1653
            generated_string = generated_string.replace('__marker__"__marker__', "&quot;").replace(
1✔
1654
                "__marker__-r__marker__", "&#xD;"
1655
            )
1656

1657
        return to_bytes(generated_string)
1✔
1658

1659
    def _add_error_tags(
1✔
1660
        self, error: ServiceException, error_tag: ETree.Element, mime_type: str
1661
    ) -> None:
1662
        """The SQS API stubs is now generated from JSON specs, and some fields have been modified"""
1663
        code_tag = ETree.SubElement(error_tag, "Code")
1✔
1664

1665
        if error.code in self.JSON_TO_QUERY_ERROR_CODES:
1✔
1666
            error_code = self.JSON_TO_QUERY_ERROR_CODES[error.code]
1✔
1667
        elif error.code in self.QUERY_PREFIXED_ERRORS:
1✔
1668
            error_code = f"AWS.SimpleQueueService.{error.code}"
1✔
1669
        else:
1670
            error_code = error.code
1✔
1671
        code_tag.text = error_code
1✔
1672
        message = self._get_error_message(error)
1✔
1673
        if message:
1✔
1674
            self._default_serialize(error_tag, message, None, "Message", mime_type)
1✔
1675
        if error.code in self.SENDER_FAULT_ERRORS or error.sender_fault:
1✔
1676
            # The sender fault is either not set or "Sender"
1677
            self._default_serialize(error_tag, "Sender", None, "Type", mime_type)
1✔
1678

1679

1680
class SqsJsonResponseSerializer(JSONResponseSerializer):
1✔
1681
    # those are deleted from the JSON specs, but need to be kept for legacy reason (sent in 'x-amzn-query-error')
1682
    QUERY_PREFIXED_ERRORS = {
1✔
1683
        "BatchEntryIdsNotDistinct",
1684
        "BatchRequestTooLong",
1685
        "EmptyBatchRequest",
1686
        "InvalidBatchEntryId",
1687
        "MessageNotInflight",
1688
        "PurgeQueueInProgress",
1689
        "QueueDeletedRecently",
1690
        "TooManyEntriesInBatchRequest",
1691
        "UnsupportedOperation",
1692
    }
1693

1694
    # Some error code changed between JSON and query, and we need to have a way to map it for legacy reason
1695
    JSON_TO_QUERY_ERROR_CODES = {
1✔
1696
        "InvalidParameterValueException": "InvalidParameterValue",
1697
        "MissingRequiredParameterException": "MissingParameter",
1698
        "AccessDeniedException": "AccessDenied",
1699
        "QueueDoesNotExist": "AWS.SimpleQueueService.NonExistentQueue",
1700
        "QueueNameExists": "QueueAlreadyExists",
1701
    }
1702

1703
    def _serialize_error(
1✔
1704
        self,
1705
        error: ServiceException,
1706
        response: Response,
1707
        shape: StructureShape,
1708
        operation_model: OperationModel,
1709
        mime_type: str,
1710
        request_id: str,
1711
    ) -> None:
1712
        """
1713
        Overrides _serialize_error as SQS has a special header for query API legacy reason: 'x-amzn-query-error',
1714
        which contained the exception code as well as a Sender field.
1715
        Ex: 'x-amzn-query-error': 'InvalidParameterValue;Sender'
1716
        """
1717
        # TODO: for body["__type"] = error.code, it seems AWS differs from what we send for SQS
1718
        # AWS: "com.amazon.coral.service#InvalidParameterValueException"
1719
        # or AWS: "com.amazonaws.sqs#BatchRequestTooLong"
1720
        # LocalStack: "InvalidParameterValue"
1721
        super()._serialize_error(error, response, shape, operation_model, mime_type, request_id)
1✔
1722
        # We need to add a prefix to certain errors, as they have been deleted in the specs. These will not change
1723
        if error.code in self.JSON_TO_QUERY_ERROR_CODES:
1✔
1724
            code = self.JSON_TO_QUERY_ERROR_CODES[error.code]
1✔
1725
        elif error.code in self.QUERY_PREFIXED_ERRORS:
1✔
1726
            code = f"AWS.SimpleQueueService.{error.code}"
1✔
1727
        else:
1728
            code = error.code
1✔
1729

1730
        response.headers["x-amzn-query-error"] = f"{code};Sender"
1✔
1731

1732

1733
def gen_amzn_requestid():
1✔
1734
    """
1735
    Generate generic AWS request ID.
1736

1737
    3 uses a different format and set of request Ids.
1738

1739
    Examples:
1740
    996d38a0-a4e9-45de-bad4-480cd962d208
1741
    b9260553-df1b-4db6-ae41-97b89a5f85ea
1742
    """
1743
    return long_uid()
1✔
1744

1745

1746
@functools.cache
1✔
1747
def create_serializer(service: ServiceModel) -> ResponseSerializer:
1✔
1748
    """
1749
    Creates the right serializer for the given service model.
1750

1751
    :param service: to create the serializer for
1752
    :return: ResponseSerializer which can handle the protocol of the service
1753
    """
1754

1755
    # Unfortunately, some services show subtle differences in their serialized responses, even though their
1756
    # specification states they implement the same protocol.
1757
    # Since some clients might be stricter / less resilient than others, we need to mimic the serialization of the
1758
    # specific services as close as possible.
1759
    # Therefore, the service-specific serializer implementations (basically the implicit / informally more specific
1760
    # protocol implementation) has precedence over the more general protocol-specific serializers.
1761
    service_specific_serializers = {
1✔
1762
        "sqs": {"json": SqsJsonResponseSerializer, "query": SqsQueryResponseSerializer},
1763
        "s3": {"rest-xml": S3ResponseSerializer},
1764
    }
1765
    protocol_specific_serializers = {
1✔
1766
        "query": QueryResponseSerializer,
1767
        "json": JSONResponseSerializer,
1768
        "rest-json": RestJSONResponseSerializer,
1769
        "rest-xml": RestXMLResponseSerializer,
1770
        "ec2": EC2ResponseSerializer,
1771
    }
1772

1773
    # Try to select a service- and protocol-specific serializer implementation
1774
    if (
1✔
1775
        service.service_name in service_specific_serializers
1776
        and service.protocol in service_specific_serializers[service.service_name]
1777
    ):
1778
        return service_specific_serializers[service.service_name][service.protocol]()
1✔
1779
    else:
1780
        # Otherwise, pick the protocol-specific serializer for the protocol of the service
1781
        return protocol_specific_serializers[service.protocol]()
1✔
1782

1783

1784
def aws_response_serializer(
1✔
1785
    service_name: str, operation: str, protocol: ProtocolName | None = None
1786
):
1787
    """
1788
    A decorator for an HTTP route that can serialize return values or exceptions into AWS responses.
1789
    This can be used to create AWS request handlers in a convenient way. Example usage::
1790

1791
        from localstack.http import route, Request
1792
        from localstack.aws.api.sqs import ListQueuesResult
1793

1794
        @route("/_aws/sqs/queues")
1795
        @aws_response_serializer("sqs", "ListQueues")
1796
        def my_route(request: Request):
1797
            if some_condition_on_request:
1798
                raise CommonServiceError("...")  # <- will be serialized into an error response
1799

1800
            return ListQueuesResult(QueueUrls=...)  # <- object from the SQS API will be serialized
1801

1802
    :param service_name: the AWS service (e.g., "sqs", "lambda")
1803
    :param protocol: the protocol of the AWS service to serialize to. If not set (by default) the default protocol
1804
                    of the service in botocore is used.
1805
    :param operation: the operation name (e.g., "ReceiveMessage", "ListFunctions")
1806
    :returns: a decorator
1807
    """
1808

1809
    def _decorate(fn):
1✔
1810
        service_model = load_service(service_name, protocol=protocol)
1✔
1811
        operation_model = service_model.operation_model(operation)
1✔
1812
        serializer = create_serializer(service_model)
1✔
1813

1814
        def _proxy(*args, **kwargs) -> WerkzeugResponse:
1✔
1815
            # extract request from function invocation (decorator can be used for methods as well as for functions).
1816
            if len(args) > 0 and isinstance(args[0], WerkzeugRequest):
1✔
1817
                # function
1818
                request = args[0]
1✔
1819
            elif len(args) > 1 and isinstance(args[1], WerkzeugRequest):
1✔
1820
                # method (arg[0] == self)
1821
                request = args[1]
1✔
1822
            elif "request" in kwargs:
1✔
1823
                request = kwargs["request"]
1✔
1824
            else:
UNCOV
1825
                raise ValueError(f"could not find Request in signature of function {fn}")
×
1826

1827
            # TODO: we have no context here
1828
            # TODO: maybe try to get the request ID from the headers first before generating a new one
1829
            request_id = gen_amzn_requestid()
1✔
1830

1831
            try:
1✔
1832
                response = fn(*args, **kwargs)
1✔
1833

1834
                if isinstance(response, WerkzeugResponse):
1✔
1835
                    return response
1✔
1836

1837
                return serializer.serialize_to_response(
1✔
1838
                    response, operation_model, request.headers, request_id
1839
                )
1840

1841
            except ServiceException as e:
1✔
1842
                return serializer.serialize_error_to_response(
1✔
1843
                    e, operation_model, request.headers, request_id
1844
                )
1845
            except Exception as e:
1✔
1846
                return serializer.serialize_error_to_response(
1✔
1847
                    CommonServiceException(
1848
                        "InternalError", f"An internal error occurred: {e}", status_code=500
1849
                    ),
1850
                    operation_model,
1851
                    request.headers,
1852
                    request_id,
1853
                )
1854

1855
        return _proxy
1✔
1856

1857
    return _decorate
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc