Repository: getmoto/moto

Issue:
S3 not found error differs from AWS response
When trying to access a key that is not present, moto returns this:

``` python
{
    'ResponseMetadata': {
        'HTTPStatusCode': 404, 'HostId': '', 'RequestId': ''
    },
    'Error': {
        'Message': 'Not Found',
        'Code': '404'
    }
}
```

vs AWS S3 returning this:

``` python
{
    'ResponseMetadata': {
        'HTTPStatusCode': 404, 'HostId': 'my-host-id', 'RequestId': 'some-request-id'
    },
    'Error': {
        'Message': 'The specified key does not exist.',
        'Code': 'NoSuchKey',
        'Key': 'my-s3-prefix/object-id'
    }
}
```

`Error.Code` is were the important difference lies. Handling of '404' vs 'NoSuchKey' will be very different. moto's '404'  also doesn't let you differentiate between bucket not found and key not found.



Discussion:
Thanks!

Thanks for fixing!

I've just updated moto to 1.0.0 from 0.4.31 and there is an issue with `404` vs `NoSuchKey` exists response.
I have a following call to check for S3 object existence:

```
    try:
        boto3.session.Session(
            region_name=current_app.config['AWS_REGION'],
        ).resource('s3').Object(
            current_app.config['AWS_S3_BUCKET'],
            key=key,
        ).load()
        return True
    except BotoClientError as e:
        if e.response['Error']['Code'] == "404":
            return False
        else:
            raise
```

It worked before with `e.response['Error']['Code'] == "404"` both with moto and real AWS S3.
After moto update, the mocked `e.response` looks this way:

```
{
    "ResponseMetadata": {
        "HTTPStatusCode": 404,
        "RetryAttempts": 0,
        "HTTPHeaders": {
            "Content-Type": "text/plain"
        }
    },
    "Error": {
        "Message": "The specified key does not exist.",
        "Code": "NoSuchKey",
        "RequestID": "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE"
    }
}
```

While the real AWS response looks like this:

```
{
    "Error": {
        "Message": "Not Found",
        "Code": "404"
    },
    "ResponseMetadata": {
        "RequestId": "0DE3423DC5314379",
        "RetryAttempts": 0,
        "HTTPStatusCode": 404,
        "HostId": "...",
        "HTTPHeaders": {
            "content-type": "application/xml",
            "server": "AmazonS3",
            "date": "Mon, 15 May 2017 13:10:51 GMT",
            "transfer-encoding": "chunked",
            "x-amz-request-id": "...",
            "x-amz-id-2": "..."
        }
    }
}
```

So I have the reverse situation to the original issue here - AWS responds with `Error.Code = 404` and moto responds with `Error.Code = NoSuchKey`.

Probably, the AWS response differs for different operations. Above I am using [Object.load](https://boto3.readthedocs.io/en/latest/reference/services/s3.html#S3.Object.load) which only sends `HEAD` request without actual object retrieval.

Resolving patch:
diff --git a/moto/s3/models.py b/moto/s3/models.py
--- a/moto/s3/models.py
+++ b/moto/s3/models.py
@@ -11,7 +11,7 @@
 from bisect import insort
 from moto.core import BaseBackend, BaseModel
 from moto.core.utils import iso_8601_datetime_with_milliseconds, rfc_1123_datetime
-from .exceptions import BucketAlreadyExists, MissingBucket, MissingKey, InvalidPart, EntityTooSmall
+from .exceptions import BucketAlreadyExists, MissingBucket, InvalidPart, EntityTooSmall
 from .utils import clean_key_name, _VersionedKeyStore
 
 UPLOAD_ID_BYTES = 43
@@ -473,7 +473,7 @@ def get_key(self, bucket_name, key_name, version_id=None):
         if isinstance(key, FakeKey):
             return key
         else:
-            raise MissingKey(key_name=key_name)
+            return None
 
     def initiate_multipart(self, bucket_name, key_name, metadata):
         bucket = self.get_bucket(bucket_name)
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -12,7 +12,7 @@
 from moto.s3bucket_path.utils import bucket_name_from_url as bucketpath_bucket_name_from_url, parse_key_name as bucketpath_parse_key_name, is_delete_keys as bucketpath_is_delete_keys
 
 
-from .exceptions import BucketAlreadyExists, S3ClientError, InvalidPartOrder
+from .exceptions import BucketAlreadyExists, S3ClientError, MissingKey, InvalidPartOrder
 from .models import s3_backend, get_canned_acl, FakeGrantee, FakeGrant, FakeAcl, FakeKey
 from .utils import bucket_name_from_url, metadata_from_headers
 from xml.dom import minidom
@@ -508,6 +508,8 @@ def _key_response_get(self, bucket_name, query, key_name, headers):
         version_id = query.get('versionId', [None])[0]
         key = self.backend.get_key(
             bucket_name, key_name, version_id=version_id)
+        if key is None:
+            raise MissingKey(key_name)
         if 'acl' in query:
             template = self.response_template(S3_OBJECT_ACL_RESPONSE)
             return 200, response_headers, template.render(obj=key)


Test patch:
diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py
--- a/tests/test_s3/test_s3.py
+++ b/tests/test_s3/test_s3.py
@@ -1223,9 +1223,10 @@ def test_boto3_head_object():
     s3.Object('blah', 'hello.txt').meta.client.head_object(
         Bucket='blah', Key='hello.txt')
 
-    with assert_raises(ClientError):
+    with assert_raises(ClientError) as e:
         s3.Object('blah', 'hello2.txt').meta.client.head_object(
             Bucket='blah', Key='hello_bad.txt')
+    e.exception.response['Error']['Code'].should.equal('404')
 
 
 @mock_s3
@@ -1353,7 +1354,7 @@ def test_boto3_delete_markers():
             Bucket=bucket_name,
             Key=key
         )
-        e.response['Error']['Code'].should.equal('NoSuchKey')
+        e.response['Error']['Code'].should.equal('404')
 
     s3.delete_object(
         Bucket=bucket_name,