Repository: localstack/localstack

Issue:
bug: SQS ChangeMessageVisibility Error Response Does Not Match AWS and Locks Up .NET Client
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

When using the image localstack/localstack:0.13.3

1. When calling SQS **ChangeMessageVisibility** with a receipt handle that is not properly formatted, the error response is `ReceiptHandleIsInvalid`. (_This is correct behavior_.)
2. When calling SQS **ChangeMessageVisibility** with a receipt handle that is properly formatted but does not actually exist, the error response is `ReceiptHandleIsInvalid`.
3. When using the AWS .NET SDK with either of the above, the SDK code goes into an infinite loop trying to parse the error response.



### Expected Behavior

1. When calling SQS **ChangeMessageVisibility** with a receipt handle that is not properly formatted, the error response should be `ReceiptHandleIsInvalid`.
2. When calling SQS **ChangeMessageVisibility** with a receipt handle that is properly formatted but does not actually exist, the error response should be `InvalidParameterValue`.
3. When using the AWS .NET SDK with either of the above, the SDK code should not go into an infinite loop trying to parse the error response.

### How are you starting LocalStack?

Custom (please describe below)

### Steps To Reproduce

#### How are you starting localstack (e.g., `bin/localstack` command, arguments, or `docker-compose.yml`)

If used the [Ductus.FluentDocker](https://github.com/mariotoffia/FluentDocker) library to create the container. I believe in the end it uses `docker run`. The actual image that was used is localstack/localstack:0.13.3

#### Client commands (e.g., AWS SDK code snippet, or sequence of "awslocal" commands)

(I did not use `awslocal` but rather `aws --endpoint-url=...`. I hope that's OK. :-)

Here is the repro with LocalStack:

```
C:\>aws --endpoint-url="http://localhost:4566" sqs create-queue --queue-name MyQueue
{
    "QueueUrl": "http://localhost:4566/000000000000/MyQueue"
}

C:\>aws --endpoint-url="http://localhost:4566" sqs send-message --queue-url http://localhost:4566/000000000000/MyQueue --message-body "{}"
{
    "MD5OfMessageBody": "99914b932bd37a50b983c5e7c90ae93b",
    "MessageId": "e1a53d04-fc0e-4a87-fa0c-85291ab017ac"
}

C:\>aws --endpoint-url="http://localhost:4566" sqs receive-message --queue-url http://localhost:4566/000000000000/MyQueue
{
    "Messages": [
        {
            "MessageId": "e1a53d04-fc0e-4a87-fa0c-85291ab017ac",
            "ReceiptHandle": "pjkzjurnqzvcrvusgkcnxclzkszvipcfifkaukrltjsjcfsjicagkqbxvwmzrresvdvqanbbqueqxelfvvfuubuhnutgtyyyseswmgobmkdejggkmxmskmmhjqxywhscfecrspsncaimlumjxvcbhksudipxqvwkqgkfrrnypdcdbdldoejonkpvg",
            "MD5OfBody": "99914b932bd37a50b983c5e7c90ae93b",
            "Body": "{}"
        }
    ]
}

C:\>aws --endpoint-url="http://localhost:4566" sqs delete-message --queue-url http://localhost:4566/000000000000/MyQueue --receipt-handle pjkzjurnqzvcrvusgkcnxclzkszvipcfifkaukrltjsjcfsjicagkqbxvwmzrresvdvqanbbqueqxelfvvfuubuhnutgtyyyseswmgobmkdejggkmxmskmmhjqxywhscfecrspsncaimlumjxvcbhksudipxqvwkqgkfrrnypdcdbdldoejonkpvg

C:\>aws --endpoint-url="http://localhost:4566" sqs change-message-visibility --queue-url http://localhost:4566/000000000000/MyQueue --receipt-handle pjkzjurnqzvcrvusgkcnxclzkszvipcfifkaukrltjsjcfsjicagkqbxvwmzrresvdvqanbbqueqxelfvvfuubuhnutgtyyyseswmgobmkdejggkmxmskmmhjqxywhscfecrspsncaimlumjxvcbhksudipxqvwkqgkfrrnypdcdbdldoejonkpvg --visibility-timeout 30

An error occurred (ReceiptHandleIsInvalid) when calling the ChangeMessageVisibility operation: The input receipt handle is invalid.

C:\>aws --endpoint-url="http://localhost:4566" sqs change-message-visibility --queue-url http://localhost:4566/000000000000/MyQueue --receipt-handle garbage --visibility-timeout 30

An error occurred (ReceiptHandleIsInvalid) when calling the ChangeMessageVisibility operation: The input receipt handle is invalid.
```

Here is the repro with real AWS. My queue was created with the AWS console. 
```
C:\>aws sqs send-message --queue-url <my-queue-url> --message-body "{}"
{
    "MD5OfMessageBody": "99914b932bd37a50b983c5e7c90ae93b",
    "MessageId": "645cb14e-6cc2-40e8-a934-bf5b82bf8169"
}

C:\>aws sqs receive-message --queue-url <my-queue-url>
{
    "Messages": [
        {
            "MessageId": "645cb14e-6cc2-40e8-a934-bf5b82bf8169",
            "ReceiptHandle": "AQEBRv9C815p34ggarmRAxrMMpvTA4U9e5GjQmqOQTWnO9QfCzzJaJ6sCzEfpyrXBTpdSTBbrBbKduvyonWS6TPbNagBwKMjIBKtA7sKv4P+/vJI0/0mX/Fc4jyPjzOrYaxs7IMACKmotNWgP1nrFQ3MUHIMzRVG3QNqmb3awA0g6S22QJDH85gSUqgGUNUt8IiTwW7wVdS1T4vVRnLtKhl0IMzvEJfsPHjbQU7uTrD8OmzbleegHjb4/Q4ibnxemoEtRutRlxGuP6j+FtLsrR6m8x2H6rdL1jeZBf831QQCFr1lH8qfkVhaz6AJpbYB0R1L/JRkbWxpbyIItSV1rA0I3WipGStr/5EGGM+Fmq4rJf2m/krcrWZVV20oJJNc9nNH19RB+xxN2cLBjXG/02OHmm8Kg8tfYD2D5FQ951Pgi0s=",
            "MD5OfBody": "99914b932bd37a50b983c5e7c90ae93b",
            "Body": "{}"
        }
    ]
}

C:\>aws sqs delete-message --queue-url <my-queue-url> --receipt-handle AQEBRv9C815p34ggarmRAxrMMpvTA4U9e5GjQmqOQTWnO9QfCzzJaJ6sCzEfpyrXBTpdSTBbrBbKduvyonWS6TPbNagBwKMjIBKtA7sKv4P+/vJI0/0mX/Fc4jyPjzOrYaxs7IMACKmotNWgP1nrFQ3MUHIMzRVG3QNqmb3awA0g6S22QJDH85gSUqgGUNUt8IiTwW7wVdS1T4vVRnLtKhl0IMzvEJfsPHjbQU7uTrD8OmzbleegHjb4/Q4ibnxemoEtRutRlxGuP6j+FtLsrR6m8x2H6rdL1jeZBf831QQCFr1lH8qfkVhaz6AJpbYB0R1L/JRkbWxpbyIItSV1rA0I3WipGStr/5EGGM+Fmq4rJf2m/krcrWZVV20oJJNc9nNH19RB+xxN2cLBjXG/02OHmm8Kg8tfYD2D5FQ951Pgi0s=

C:\>aws sqs change-message-visibility --queue-url <my-queue-url> --receipt-handle AQEBRv9C815p34ggarmRAxrMMpvTA4U9e5GjQmqOQTWnO9QfCzzJaJ6sCzEfpyrXBTpdSTBbrBbKduvyonWS6TPbNagBwKMjIBKtA7sKv4P+/vJI0/0mX/Fc4jyPjzOrYaxs7IMACKmotNWgP1nrFQ3MUHIMzRVG3QNqmb3awA0g6S22QJDH85gSUqgGUNUt8IiTwW7wVdS1T4vVRnLtKhl0IMzvEJfsPHjbQU7uTrD8OmzbleegHjb4/Q4ibnxemoEtRutRlxGuP6j+FtLsrR6m8x2H6rdL1jeZBf831QQCFr1lH8qfkVhaz6AJpbYB0R1L/JRkbWxpbyIItSV1rA0I3WipGStr/5EGGM+Fmq4rJf2m/krcrWZVV20oJJNc9nNH19RB+xxN2cLBjXG/02OHmm8Kg8tfYD2D5FQ951Pgi0s= --visibility-timeout 30

An error occurred (InvalidParameterValue) when calling the ChangeMessageVisibility operation: Value AQEBRv9C815p34ggarmRAxrMMpvTA4U9e5GjQmqOQTWnO9QfCzzJaJ6sCzEfpyrXBTpdSTBbrBbKduvyonWS6TPbNagBwKMjIBKtA7sKv4P+/vJI0/0mX/Fc4jyPjzOrYaxs7IMACKmotNWgP1nrFQ3MUHIMzRVG3QNqmb3awA0g6S22QJDH85gSUqgGUNUt8IiTwW7wVdS1T4vVRnLtKhl0IMzvEJfsPHjbQU7uTrD8OmzbleegHjb4/Q4ibnxemoEtRutRlxGuP6j+FtLsrR6m8x2H6rdL1jeZBf831QQCFr1lH8qfkVhaz6AJpbYB0R1L/JRkbWxpbyIItSV1rA0I3WipGStr/5EGGM+Fmq4rJf2m/krcrWZVV20oJJNc9nNH19RB+xxN2cLBjXG/02OHmm8Kg8tfYD2D5FQ951Pgi0s= for parameter ReceiptHandle is invalid. Reason: Message does not exist or is not available for visibility timeout change.

C:\>aws sqs change-message-visibility --queue-url <my-queue-url> --receipt-handle garbage --visibility-timeout 30

An error occurred (ReceiptHandleIsInvalid) when calling the ChangeMessageVisibility operation: The input receipt handle "garbage" is not a valid receipt handle.
```

### Environment

```markdown
- OS: Windows 10.0.19044 Build 19044
- LocalStack: 0.13.3
```

Note that the "infinite loop" behavior I'm referring to seems to have been introduced with localstack 0.12.19. localstack 0.12.18 does not exhibit the "infinite loop" behavior. The "not matching AWS" behavior is in all the versions I tested.

### Anything else?

There are two main parts to this:

### Incorrect Error Response

This [AWS Java SDK issue](https://github.com/aws/aws-sdk-java/issues/1628) explains the two different error responses for **ChangeMessageVisibility**. "Invalid" is a tricky word here. I've referred to the first error as "not properly formatted". What I mean is that receipt handles are normally really long strings (for real AWS they appear to be base64 strings). So we know a string like "garbage" is not a real receipt handle. That's what AWS means by "invalid". But you could have a "properly formatted" receipt handle that's still not a real receipt handle. For example, the receipt handle for a message that's previously been deleted. That's how I reproduce this issue. 

We have some production code that depends on this behavior. We have a race condition between code that deletes messages and code that changes the visibility on messages. This bug in LocalStack means I cannot integration test this code.

### Infinite Loop in .NET SDK

In the AWS .NET SDK, error responses are unmarshalled by two key classes for our purposes: [ErrorResponseUnmarshaller](https://github.com/aws/aws-sdk-net/blob/475822dec5e87954b7a47ac65995714ae1f1b115/sdk/src/Core/Amazon.Runtime/Internal/Transform/ErrorResponseUnmarshaller.cs) and [XmlUnmarshallerContext](https://github.com/aws/aws-sdk-net/blob/475822dec5e87954b7a47ac65995714ae1f1b115/sdk/src/Core/Amazon.Runtime/Internal/Transform/UnmarshallerContext.cs). 

The code expects the XML response to be formatted in a certain way. In particular, it does not seem to deal with whitespace between elements. If whitespace is present in the wrong place in the XML, it goes into an infinite loop. I found this by cloning the AWS .NET SDK and fiddling their SQS unit tests and stepping through the client code.

Here is the response XML from real AWS:
```
<?xml version="1.0"?><ErrorResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/"><Error><Type>Sender</Type><Code>ReceiptHandleIsInvalid</Code><Message>The input receipt handle &quot;garbage&quot; is not a valid receipt handle.</Message><Detail/></Error><RequestId>bd34a56b-d17a-552a-b343-ff0b22169609</RequestId></ErrorResponse>
```

Note how there are no newlines or spaces.

Here is the response XML from LocalStack:
```
<?xml version="1.0" encoding="UTF-8"?>
  <ErrorResponse>
    <Errors>
      <Error>
        <Code>ReceiptHandleIsInvalid</Code>
        <Message><![CDATA[The input receipt handle is invalid.]]></Message>
        
      </Error>
    </Errors>
  <RequestId>03D34ALJ80TB0D5YUQ9IBUAOTFSYZN74Y5S20JRFIPICJ0SNSW7T</RequestId>
</ErrorResponse>
```

Note how there are newlines and indenting. What looks like a blank line actually had eight spaces on it. The .NET code seems to fail due to that line with the eight spaces.





Discussion:
hi Tom @twinter-patriot! thanks for this really detailed bug report! I wish all of them had this much information to work with!

we've recently introduced a completely new SQS provider that's still a bit experimental but may solve your problem. it would be great if you could give it a try while I try to replicate your issue.

to active the provider, you can set the environment variable `PROVIDER_OVERRIDE_SQS=asf`
All behavior is the same with `PROVIDER_OVERRIDE_SQS=asf`, though I did not look into the XML returned by LocalStack.
@thrau we had similar issue where .NET AWS SDK entered an infinite loop #4783. It seems that .NET SDK doesn't handle `CDATA` wrapper well instead it uses `HTML encoding`. I see the `HTML encoding` in the XML response from real AWS. Please take a look here #4853 , it might be helpful to you to investigate this issue. I'm not familiar with Java SDK.
that is indeed very helpful @KrishnanRanjithkumar, thanks! the message encoding may indeed be the problem.

with the default moto SQS provider, the `CDATA` wrapper is added into the Message response, and with the ASF provider, we're not doing HTML encoding correctly. i started a PR #5298 to address that.
The CDATA could be it. When I was stepping through the AWS .NET SDK code, it went into the infinite loop right after or right when reading the `<Message>` element. The code is a little hard to follow so I'm not exactly sure where it was. 😄 
we've deployed a fix for the XML encoding issue for the `PROVIDER_OVERRIDE_SQS=asf` SQS provider #5298 which is now in latest. it would be great if you could give that a try!

the parity issue that a `ReceiptHandleIsInvalid` is raised instead of a `InvalidParameterValue` remains, and we can fix that in a second step, but it's not obvious to me why a client would stop working because of that. those are both valid exceptions for the method.
> the parity issue that a `ReceiptHandleIsInvalid` is raised instead of a `InvalidParameterValue` remains, and we can fix that in a second step, but it's not obvious to me why a client would stop working because of that. those are both valid exceptions for the method.

The incorrect error code affects us due to...long story. In our code that receives SQS messages we have a timer-based method that automatically extends the visibility timeout for us every so often, so the main code doesn't need to worry about that. But then we have the main code deleting the SQS message when it's done processing. So we have a race condition between deleting and changing the visibility. If we delete the message and then the timer goes off before we can stop it, the timer method will try change visibility on a deleted message, which causes AWS to send back the `InvalidParameterValue`. This isn't a big deal to us since things aren't really "broken" (and so no motivation to fix the race condition 😉). We know if this kind of thing happens a lot we just need to tweak our visibility extending amounts so this doesn't happen. So the key part here is that when this `InvalidParameterValue` error happens (with the right message) we just log a warning about this situation. All other errors from SQS we log as errors. All this works fine with AWS. But when we want to integration test this code with LocalStack it fails. We want to test that we're logging a warning in this case instead of an error. But LocalStack returns the wrong error code, so we log an error instead of a warning, and the integration test fails. 

Fun extra part: We used to base our "determine if we log a warning or an error" logic on what LocalStack returned. We had never actually tried it out on AWS. 😲 So our code basically looked for the `ReceiptHandleIsInvalid` and (incorrectly) logged that as a warning. It was only when the LocalStack update came out that caused the XML encoding issue and the infinite loop in the AWS client that our integration test started to fail (since **nothing** ever got logged because the visibility changed call never returned). It was only while investigating that issue that we actually tried things out to see what AWS itself returned and realized that LocalStack did not match it. Fun times. 😃 
thanks again for the detailed explanation, makes sense to me. and i apologize for being dismissive in my previous comment. i went back to read your original post where you in fact stated clearly that you are relying on this behavior.

we'll get on it :-)
I can confirm that this discrepancy between LocalStack and AWS in reporting of invalid receipt handle error still stands for LocalStack 0.14.2 and can be reproduced with this code snippet (using AWSSDK.SQS .NET v3.7.2.42):
````
static async Task TestChangeVisibility()
{
    using (AmazonSQSClient SQS = new AmazonSQSClient("ignore", "ignore", new AmazonSQSConfig { ServiceURL = "http://localhost:4566" }))
    //using (AmazonSQSClient SQS = new AmazonSQSClient(RegionEndpoint.USEast1))
    {
        CreateQueueResponse createQueueResp = await SQS.CreateQueueAsync("testQueue");
        Console.WriteLine($"[{DateTime.Now:T}] Queue created.");

        SendMessageRequest sendMessageRequest = new SendMessageRequest(createQueueResp.QueueUrl, "ABCD");
        SendMessageResponse msgResp = await SQS.SendMessageAsync(sendMessageRequest);
        Console.WriteLine($"[{DateTime.Now:T}] Message sent.");
        await Task.Delay(1000);

        ReceiveMessageResponse receiveMessageResp = await SQS.ReceiveMessageAsync(new ReceiveMessageRequest(createQueueResp.QueueUrl) { WaitTimeSeconds = 5 });
        if (receiveMessageResp.Messages.Count > 0)
        {
            Console.WriteLine($"[{DateTime.Now:T}] Message received.");
            Message message = receiveMessageResp.Messages[0];

            await SQS.DeleteMessageAsync(createQueueResp.QueueUrl, message.ReceiptHandle);
            Console.WriteLine($"[{DateTime.Now:T}] Message deleted");

            try
            {
                await SQS.ChangeMessageVisibilityAsync(createQueueResp.QueueUrl, message.ReceiptHandle, 10);
            }
            catch (ReceiptHandleIsInvalidException)
            {
                Console.WriteLine($"[{DateTime.Now:T}] Changing visibility: {typeof(ReceiptHandleIsInvalidException).Name}");
            }
            catch (AmazonSQSException ex)
            {
                Console.WriteLine($"[{DateTime.Now:T}] Changing visibility: {typeof(AmazonSQSException).Name} - {ex.Message}");
            }
        }

        await SQS.DeleteQueueAsync(createQueueResp.QueueUrl);
        Console.WriteLine($"[{DateTime.Now:T}] Queue deleted.");
    }
}
````
For AWS `AmazonSQSException` is thrown:
```
[10:52:15] Queue created.
[10:52:15] Message sent.
[10:52:16] Message received.
[10:52:16] Message deleted
[10:52:16] Changing visibility: AmazonSQSException - Value AQEBWQnMrnqmYpsWdvd3LD9JiYfUqKE7/zQEZJ/l53MVQtxq93OGZYhzmlploseUEIvBfGQZQlmruXQTFh4kMAGDCSS+gDbMN0FjuzU+Y1KzHFPJRmNjE9NEyfCDXg3Xa8mlm2IguO2a1sQk8uNOtWVXh/SqueCVtejiY03PAApZDkKcKf7r60B1H4LEP+YRsYIV8U2CTmzHTlBjFXJwqiIx9NtYwPRd+0hLz+WDElY21B6FjiPSH+Ua32BRoVa+3n9SwtHQudQWiFmOcWI60H99DUfib1VSJornUm7j+9DFJiKhEpb53+lcXF/IvQcfRASiaRWDE+SU05kO1wVlExBEepyxMPCW6lW92e8qkxz1COdBc8XnHvpOYN/hUrKfq/ZgqIXW4IL6tKNsc6DxjS+YUByh1J8ZVOFs7HEExm6BPNHALFxEK6LVFodEusUwVMGW for parameter ReceiptHandle is invalid. Reason: Message does not exist or is not available for visibility timeout change.
[10:52:16] Queue deleted.
```
For LocalStack with `PROVIDER_OVERRIDE_SQS=asf` `ReceiptHandleIsInvalidException` is thrown:
```
[10:48:30] Queue created.
[10:48:30] Message sent.
[10:48:31] Message received.
[10:48:31] Message deleted
[10:48:31] Changing visibility: ReceiptHandleIsInvalidException
[10:48:31] Queue deleted.
```
For LocalStack with default SQS provider execution gets stuck on ChangeMessageVisibility() call:
```
[10:50:59] Queue created.
[10:50:59] Message sent.
[10:51:00] Message received.
[10:51:00] Message deleted
...
```

Resolving patch:
diff --git a/localstack/services/sqs/provider.py b/localstack/services/sqs/provider.py
--- a/localstack/services/sqs/provider.py
+++ b/localstack/services/sqs/provider.py
@@ -85,15 +85,6 @@
 DEDUPLICATION_INTERVAL_IN_SEC = 5 * 60
 
 
-def generate_message_id():
-    return long_uid()
-
-
-def generate_receipt_handle():
-    # http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/ImportantIdentifiers.html#ImportantIdentifiers-receipt-handles
-    return "".join(random.choices(string.ascii_letters + string.digits, k=172)) + "="
-
-
 class InvalidParameterValue(CommonServiceException):
     def __init__(self, message):
         super().__init__("InvalidParameterValue", message, 400, True)
@@ -109,6 +100,10 @@ def __init__(self, message):
         super().__init__("MissingParameter", message, 400, True)
 
 
+def generate_message_id():
+    return long_uid()
+
+
 def assert_queue_name(queue_name: str, fifo: bool = False):
     if queue_name.endswith(".fifo"):
         if not fifo:
@@ -133,6 +128,27 @@ def check_message_content(message_body: str):
         raise InvalidMessageContents(error)
 
 
+def encode_receipt_handle(queue_arn, message: "SqsMessage") -> str:
+    # http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/ImportantIdentifiers.html#ImportantIdentifiers-receipt-handles
+    # encode the queue arn in the receipt handle, so we can later check if it belongs to the queue
+    # but also add some randomness s.t. the generated receipt handles look like the ones from AWS
+    handle = f"{long_uid()} {queue_arn} {message.message.get('MessageId')} {message.last_received}"
+    encoded = base64.b64encode(handle.encode("utf-8"))
+    return encoded.decode("utf-8")
+
+
+def decode_receipt_handle(receipt_handle: str) -> str:
+    try:
+        handle = base64.b64decode(receipt_handle).decode("utf-8")
+        _, queue_arn, message_id, last_received = handle.split(" ")
+        parse_arn(queue_arn)  # raises a ValueError if it is not an arn
+        return queue_arn
+    except (IndexError, ValueError):
+        raise ReceiptHandleIsInvalid(
+            f'The input receipt handle "{receipt_handle}" is not a valid receipt handle.'
+        )
+
+
 class Permission(NamedTuple):
     # TODO: just a placeholder for real policies
     label: str
@@ -302,12 +318,22 @@ def url(self, context: RequestContext) -> str:
     def visibility_timeout(self) -> int:
         return int(self.attributes[QueueAttributeName.VisibilityTimeout])
 
+    def validate_receipt_handle(self, receipt_handle: str):
+        if self.arn != decode_receipt_handle(receipt_handle):
+            raise ReceiptHandleIsInvalid(
+                f'The input receipt handle "{receipt_handle}" is not a valid receipt handle.'
+            )
+
     def update_visibility_timeout(self, receipt_handle: str, visibility_timeout: int):
         with self.mutex:
+            self.validate_receipt_handle(receipt_handle)
+
             if receipt_handle not in self.receipts:
-                raise ReceiptHandleIsInvalid(
-                    f'The input receipt handle "{receipt_handle}" is not a valid receipt handle.'
+                raise InvalidParameterValue(
+                    f"Value {receipt_handle} for parameter ReceiptHandle is invalid. Reason: Message does not exist "
+                    f"or is not available for visibility timeout change."
                 )
+
             standard_message = self.receipts[receipt_handle]
 
             if standard_message not in self.inflight:
@@ -327,6 +353,8 @@ def update_visibility_timeout(self, receipt_handle: str, visibility_timeout: int
 
     def remove(self, receipt_handle: str):
         with self.mutex:
+            self.validate_receipt_handle(receipt_handle)
+
             if receipt_handle not in self.receipts:
                 LOG.debug(
                     "no in-flight message found for receipt handle %s in queue %s",
@@ -394,7 +422,7 @@ def get(self, block=True, timeout=None, visibility_timeout: int = None) -> SqsMe
                     standard_message.first_received = standard_message.last_received
 
                 # create and manage receipt handle
-                receipt_handle = generate_receipt_handle()
+                receipt_handle = self.create_receipt_handle(standard_message)
                 standard_message.receipt_handles.add(receipt_handle)
                 self.receipts[receipt_handle] = standard_message
 
@@ -415,6 +443,9 @@ def get(self, block=True, timeout=None, visibility_timeout: int = None) -> SqsMe
 
             return copied_message
 
+    def create_receipt_handle(self, message: SqsMessage) -> str:
+        return encode_receipt_handle(self.arn, message)
+
     def requeue_inflight_messages(self):
         if not self.inflight:
             return


Test patch:
diff --git a/tests/integration/test_sqs.py b/tests/integration/test_sqs.py
--- a/tests/integration/test_sqs.py
+++ b/tests/integration/test_sqs.py
@@ -1836,6 +1836,56 @@ def receive_message(attribute_names, message_attribute_names=None):
         response = receive_message(["SenderId", "SequenceNumber"])
         assert snapshot.match("multiple_attributes", response)
 
+    @pytest.mark.aws_validated
+    def test_change_visibility_on_deleted_message_raises_invalid_parameter_value(
+        self, sqs_client, sqs_queue
+    ):
+        # prepare the fixture
+        sqs_client.send_message(QueueUrl=sqs_queue, MessageBody="foo")
+        response = sqs_client.receive_message(QueueUrl=sqs_queue, WaitTimeSeconds=5)
+        handle = response["Messages"][0]["ReceiptHandle"]
+
+        # check that it works as expected
+        sqs_client.change_message_visibility(
+            QueueUrl=sqs_queue, ReceiptHandle=handle, VisibilityTimeout=42
+        )
+
+        # delete the message, the handle becomes invalid
+        sqs_client.delete_message(QueueUrl=sqs_queue, ReceiptHandle=handle)
+
+        with pytest.raises(ClientError) as e:
+            sqs_client.change_message_visibility(
+                QueueUrl=sqs_queue, ReceiptHandle=handle, VisibilityTimeout=42
+            )
+
+        err = e.value.response["Error"]
+        assert err["Code"] == "InvalidParameterValue"
+        assert (
+            err["Message"]
+            == f"Value {handle} for parameter ReceiptHandle is invalid. Reason: Message does not exist or is not "
+            f"available for visibility timeout change."
+        )
+
+    @pytest.mark.aws_validated
+    def test_delete_message_with_illegal_receipt_handle(self, sqs_client, sqs_queue):
+        with pytest.raises(ClientError) as e:
+            sqs_client.delete_message(QueueUrl=sqs_queue, ReceiptHandle="garbage")
+
+        err = e.value.response["Error"]
+        assert err["Code"] == "ReceiptHandleIsInvalid"
+        assert err["Message"] == 'The input receipt handle "garbage" is not a valid receipt handle.'
+
+    @pytest.mark.aws_validated
+    def test_delete_message_with_deleted_receipt_handle(self, sqs_client, sqs_queue):
+        sqs_client.send_message(QueueUrl=sqs_queue, MessageBody="foo")
+        response = sqs_client.receive_message(QueueUrl=sqs_queue, WaitTimeSeconds=5)
+        handle = response["Messages"][0]["ReceiptHandle"]
+
+        # does not raise errors even after successive calls
+        sqs_client.delete_message(QueueUrl=sqs_queue, ReceiptHandle=handle)
+        sqs_client.delete_message(QueueUrl=sqs_queue, ReceiptHandle=handle)
+        sqs_client.delete_message(QueueUrl=sqs_queue, ReceiptHandle=handle)
+
 
 def get_region():
     return os.environ.get("AWS_DEFAULT_REGION") or TEST_REGION