From 5e71b5e11f056fc082e61e0e3a4515fcf1b9948f Mon Sep 17 00:00:00 2001 From: Fei Long Wang Date: Thu, 11 Jul 2013 21:17:07 +0800 Subject: [PATCH] Fixes image-download error of v2 When trying to retrieve a Glance image using the v2 api, user will get an error: 'utf8' codec can't decode byte 0xcc in position 1217: invalid continuation byte This is because Glance client expects a response of type content-type: application/octet-stream but it's getting a Content-Type: text/html; charset=UTF-8 Transfer-Encoding: chunked The root cause is Glance will return the image data from cache if there is. However, in method _process_v2_request, the content type has never been configured. See: https://github.com/openstack/glance/blob/master/glance/api/middleware/cache.py#L162 Fixes bug 1193310 Change-Id: Icab5e28db63455358ab3919bd94c484411987e39 --- glance/api/middleware/cache.py | 12 ++++++ glance/tests/unit/test_cache_middleware.py | 47 ++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/glance/api/middleware/cache.py b/glance/api/middleware/cache.py index 691679bbde..8f68a71939 100644 --- a/glance/api/middleware/cache.py +++ b/glance/api/middleware/cache.py @@ -175,6 +175,18 @@ class CacheFilter(wsgi.Middleware): image_meta['size'], image_iterator, notifier.Notifier()) + # NOTE (flwang): Set the content-type, content-md5 and content-length + # explicitly to be consistent with the non-cache scenario. + # Besides, it's not worth the candle to invoke the "download" method + # of ResponseSerializer under image_data. Because method "download" + # will reset the app_iter. Then we have to call method + # "size_checked_iter" to avoid missing any notification. But after + # call "size_checked_iter", we will lose the content-md5 and + # content-length got by the method "download" because of this issue: + # https://github.com/Pylons/webob/issues/86 + response.headers['Content-Type'] = 'application/octet-stream' + response.headers['Content-MD5'] = image.checksum + response.headers['Content-Length'] = str(image.size) return response def process_response(self, resp): diff --git a/glance/tests/unit/test_cache_middleware.py b/glance/tests/unit/test_cache_middleware.py index 4c171fada1..fe52be2c30 100644 --- a/glance/tests/unit/test_cache_middleware.py +++ b/glance/tests/unit/test_cache_middleware.py @@ -20,6 +20,7 @@ import webob import glance.api.middleware.cache from glance.common import exception from glance import context +import glance.db.sqlalchemy.api as db import glance.registry.client.v1.api as registry from glance.tests import utils @@ -234,6 +235,52 @@ class TestCacheMiddlewareProcessRequest(utils.BaseTestCase): cache_filter._verify_metadata(image_meta) self.assertTrue(image_meta['size'] == image_size) + def test_v2_process_request_response_headers(self): + def dummy_img_iterator(): + for i in range(3): + yield i + + def fake_image_get(self, image_id): + return { + 'id': 'test1', + 'name': 'fake_image', + 'status': 'Active', + 'created_at': '', + 'min_disk': '10G', + 'min_ram': '1024M', + 'protected': False, + 'locations': '', + 'checksum': 'c352f4e7121c6eae958bc1570324f17e', + 'owner': '', + 'disk_format': 'raw', + 'container_format': 'bare', + 'size': '123456789', + 'is_public': 'public', + 'deleted': False, + 'updated_at': '', + 'properties': {}, + } + + def fake_image_tag_get_all(context, image_id, session=None): + return None + + image_id = 'test1' + request = webob.Request.blank('/v2/images/test1/file') + request.context = context.RequestContext() + + self.stubs.Set(db, 'image_get', fake_image_get) + self.stubs.Set(db, 'image_tag_get_all', fake_image_tag_get_all) + + cache_filter = ProcessRequestTestCacheFilter() + response = cache_filter._process_v2_request( + request, image_id, dummy_img_iterator) + self.assertEqual(response.headers['Content-Type'], + 'application/octet-stream') + self.assertEqual(response.headers['Content-MD5'], + 'c352f4e7121c6eae958bc1570324f17e') + self.assertEqual(response.headers['Content-Length'], + '123456789') + class TestProcessResponse(utils.BaseTestCase): def setUp(self):