Repository: boto/botocore
Issue: Unable to use non-string header values with Signature Version 4

When using S3 Signature Version 4 it's not possible to use non-string values in headers, such as Content-Length (which expects a integer/long). When passing a non-string value the following exception will be raised: `AttributeError: 'int' object has no attribute 'strip'`.

Execute script as found on https://gist.github.com/steynovich/bbfe3057ba8ef89bb30d

Error:

``` bash
$ ./upload.py
Traceback (most recent call last):
  File "./upload.py", line 25, in <module>
    ContentLength=size)
  File "/Users/steynovich/.virtualenvs/uploadtest/lib/python2.7/site-packages/botocore/client.py", line 310, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/Users/steynovich/.virtualenvs/uploadtest/lib/python2.7/site-packages/botocore/client.py", line 396, in _make_api_call
    operation_model, request_dict)
  File "/Users/steynovich/.virtualenvs/uploadtest/lib/python2.7/site-packages/botocore/endpoint.py", line 111, in make_request
    return self._send_request(request_dict, operation_model)
  File "/Users/steynovich/.virtualenvs/uploadtest/lib/python2.7/site-packages/botocore/endpoint.py", line 136, in _send_request
    request = self.create_request(request_dict, operation_model)
  File "/Users/steynovich/.virtualenvs/uploadtest/lib/python2.7/site-packages/botocore/endpoint.py", line 120, in create_request
    operation_name=operation_model.name)
  File "/Users/steynovich/.virtualenvs/uploadtest/lib/python2.7/site-packages/botocore/hooks.py", line 226, in emit
    return self._emit(event_name, kwargs)
  File "/Users/steynovich/.virtualenvs/uploadtest/lib/python2.7/site-packages/botocore/hooks.py", line 209, in _emit
    response = handler(**kwargs)
  File "/Users/steynovich/.virtualenvs/uploadtest/lib/python2.7/site-packages/botocore/signers.py", line 84, in handler
    return self.sign(operation_name, request)
  File "/Users/steynovich/.virtualenvs/uploadtest/lib/python2.7/site-packages/botocore/signers.py", line 119, in sign
    signer.add_auth(request=request)
  File "/Users/steynovich/.virtualenvs/uploadtest/lib/python2.7/site-packages/botocore/auth.py", line 315, in add_auth
    canonical_request = self.canonical_request(request)
  File "/Users/steynovich/.virtualenvs/uploadtest/lib/python2.7/site-packages/botocore/auth.py", line 257, in canonical_request
    cr.append(self.canonical_headers(headers_to_sign) + '\\n')
  File "/Users/steynovich/.virtualenvs/uploadtest/lib/python2.7/site-packages/botocore/auth.py", line 224, in canonical_headers
    sorted(headers_to_sign.get_all(key)))
  File "/Users/steynovich/.virtualenvs/uploadtest/lib/python2.7/site-packages/botocore/auth.py", line 223, in <genexpr>
    value = ','.join(v.strip() for v in
AttributeError: 'int' object has no attribute 'strip'
```

Expected behaviour: successful upload. You can test this by disabling the ContentLength parameter when executing upload_part() in the test script.

Used versions:
Python 2.7.10
boto3 (1.2.3)
botocore (1.3.25)


Changed files:
tests/unit/test_serialize.py, tests/functional/test_s3.py, botocore/serialize.py

diff --git a/botocore/serialize.py b/botocore/serialize.py
index 4917264cd..641a73bff 100644
--- a/botocore/serialize.py
+++ b/botocore/serialize.py
@@ -483,7 +483,7 @@ class BaseRestSerializer(Serializer):
         elif location == 'header':
             shape = shape_members[param_name]
             value = self._convert_header_value(shape, param_value)
-            partitioned['headers'][key_name] = value
+            partitioned['headers'][key_name] = str(value)
         elif location == 'headers':
             # 'headers' is a bit of an oddball.  The ``key_name``
             # is actually really a prefix for the header names:
diff --git a/tests/functional/test_s3.py b/tests/functional/test_s3.py
index ddabcc6a6..f7ba03685 100644
--- a/tests/functional/test_s3.py
+++ b/tests/functional/test_s3.py
@@ -206,3 +206,21 @@ class TestPathHostStyle(BaseS3AddressingStyle):
             request_sent = mock_send.call_args[0][0]
             self.assertEqual(
                 'https://foo.amazonaws.com/mybucket/mykey', request_sent.url)
+
+
+class TestCanSendIntegerHeaders(BaseSessionTest):
+
+    def test_int_values_with_sigv4(self):
+        s3 = self.session.create_client(
+            's3', config=Config(signature_version='s3v4'))
+        with mock.patch('botocore.endpoint.Session.send') as mock_send:
+            mock_send.return_value = mock.Mock(status_code=200,
+                                               content=b'',
+                                               headers={})
+            s3.upload_part(Bucket='foo', Key='bar', Body=b'foo',
+                           UploadId='bar', PartNumber=1, ContentLength=3)
+            headers = mock_send.call_args[0][0].headers
+            # Verify that the request integer value of 3 has been converted to
+            # string '3'.  This also means we've made it pass the signer which
+            # expects string values in order to sign properly.
+            self.assertEqual(headers['Content-Length'], '3')
diff --git a/tests/unit/test_serialize.py b/tests/unit/test_serialize.py
index 97cfb0b53..a7399bf43 100644
--- a/tests/unit/test_serialize.py
+++ b/tests/unit/test_serialize.py
@@ -402,3 +402,41 @@ class TestInstanceCreation(unittest.TestCase):
 
         with self.assertRaises(ParamValidationError):
             self.assert_serialize_invalid_parameter(request_serializer)
+
+
+class TestHeaderSerialization(BaseModelWithBlob):
+    def setUp(self):
+        self.model = {
+            'metadata': {'protocol': 'rest-xml', 'apiVersion': '2014-01-01'},
+            'documentation': '',
+            'operations': {
+                'TestOperation': {
+                    'name': 'TestOperation',
+                    'http': {
+                        'method': 'POST',
+                        'requestUri': '/',
+                    },
+                    'input': {'shape': 'InputShape'},
+                }
+            },
+            'shapes': {
+                'InputShape': {
+                    'type': 'structure',
+                    'members': {
+                        'ContentLength': {
+                            'shape': 'Integer',
+                            'location': 'header',
+                            'locationName': 'Content-Length'
+                        },
+                    }
+                },
+                'Integer': {
+                    'type': 'integer'
+                },
+            }
+        }
+        self.service_model = ServiceModel(self.model)
+
+    def test_always_serialized_as_str(self):
+        request = self.serialize_to_request({'ContentLength': 100})
+        self.assertEqual(request['headers']['Content-Length'], '100')