Repository: getmoto/moto
Issue: Second request to presigned URL fails (1.3.14, after c7147b06)

Hello.

Looks like second request to generated presigned URL (using `requests`) fails.

I was able to find with `git bisect` that the offending change was  c7147b06 in #2294.

It looks like `responses` has a bit of weird logic: if there are multiple matches to the given URL, then the first one is popped out from the internal list. So, with requests to presigned URLs, the first (valid) regular expression is popped out, and then second call fails.

Not sure if this affects other intercepted requests (also in other backends than S3).

Responses code: https://github.com/getsentry/responses/blob/c8a1733b099f4b3c1bb3db3b42710c7cf8168142/responses.py#L580-L587

This patch to existing tests reproduces the issue:

```diff
--- a/tests/test_s3/test_s3.py
+++ b/tests/test_s3/test_s3.py
@@ -1017,8 +1017,9 @@ def test_s3_object_in_public_bucket():
     presigned_url = boto3.client("s3").generate_presigned_url(
         "get_object", params, ExpiresIn=900
     )
-    response = requests.get(presigned_url)
-    assert response.status_code == 200
+    for i in range(1, 10):
+        response = requests.get(presigned_url)
+        assert response.status_code == 200, "Failed on req number {}".format(i)

```

Output:
```
FAIL: tests.test_s3.test_s3.test_s3_object_in_public_bucket
AssertionError: Failed on req number 2
    <Response [400]> = <module 'requests' from ...>.get('https://test-bucket.s3.amazonaws.com/file.txt?AWSAccessKeyId=foobar_key&Signature=Gry%2F
OmBZWksaCaoNGVgRazZVxiI%3D&Expires=1573860668')
```

Spotted this from private project test where we do create multiple presigned `PUT` to same bucket but with different key. Second upload using `requests.put()` fails (using session object actually). This used to work with `moto==1.3.13`.

Versions in use:
```
boto3==1.10.16
botocore==1.13.16
requests==2.22.0
responses==0.10.6
moto==1.3.14         # (or latest git revision)
```

Note: recently botocore switched fully away from vendored requests, but that doesn't seem to affect the result.

Changed files:
moto/core/models.py, tests/test_s3/test_s3.py

diff --git a/moto/core/models.py b/moto/core/models.py
index ffb2ffd9f..8ca74d5b5 100644
--- a/moto/core/models.py
+++ b/moto/core/models.py
@@ -7,6 +7,7 @@ import inspect
 import os
 import re
 import six
+import types
 from io import BytesIO
 from collections import defaultdict
 from botocore.handlers import BUILTIN_HANDLERS
@@ -217,12 +218,29 @@ botocore_mock = responses.RequestsMock(
     assert_all_requests_are_fired=False,
     target="botocore.vendored.requests.adapters.HTTPAdapter.send",
 )
+
 responses_mock = responses._default_mock
 # Add passthrough to allow any other requests to work
 # Since this uses .startswith, it applies to http and https requests.
 responses_mock.add_passthru("http")
 
 
+def _find_first_match(self, request):
+    for i, match in enumerate(self._matches):
+        if match.matches(request):
+            return match
+
+    return None
+
+
+# Modify behaviour of the matcher to only/always return the first match
+# Default behaviour is to return subsequent matches for subsequent requests, which leads to https://github.com/spulec/moto/issues/2567
+#  - First request matches on the appropriate S3 URL
+#  - Same request, executed again, will be matched on the subsequent match, which happens to be the catch-all, not-yet-implemented, callback
+# Fix: Always return the first match
+responses_mock._find_match = types.MethodType(_find_first_match, responses_mock)
+
+
 BOTOCORE_HTTP_METHODS = ["GET", "DELETE", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
 
 
diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py
index 2193f8b27..48655ee17 100644
--- a/tests/test_s3/test_s3.py
+++ b/tests/test_s3/test_s3.py
@@ -39,6 +39,7 @@ import moto.s3.models as s3model
 from moto.core.exceptions import InvalidNextTokenException
 from moto.core.utils import py2_strip_unicode_keys
 
+
 if settings.TEST_SERVER_MODE:
     REDUCED_PART_SIZE = s3model.UPLOAD_PART_MIN_SIZE
     EXPECTED_ETAG = '"140f92a6df9f9e415f74a1463bcee9bb-2"'
@@ -1018,12 +1019,23 @@ def test_s3_object_in_public_bucket():
         s3_anonymous.Object(key="file.txt", bucket_name="test-bucket").get()
     exc.exception.response["Error"]["Code"].should.equal("403")
 
+
+@mock_s3
+def test_s3_object_in_public_bucket_using_multiple_presigned_urls():
+    s3 = boto3.resource("s3")
+    bucket = s3.Bucket("test-bucket")
+    bucket.create(
+        ACL="public-read", CreateBucketConfiguration={"LocationConstraint": "us-west-1"}
+    )
+    bucket.put_object(Body=b"ABCD", Key="file.txt")
+
     params = {"Bucket": "test-bucket", "Key": "file.txt"}
     presigned_url = boto3.client("s3").generate_presigned_url(
         "get_object", params, ExpiresIn=900
     )
-    response = requests.get(presigned_url)
-    assert response.status_code == 200
+    for i in range(1, 10):
+        response = requests.get(presigned_url)
+        assert response.status_code == 200, "Failed on req number {}".format(i)
 
 
 @mock_s3