Skip to content
This repository was archived by the owner on Apr 21, 2023. It is now read-only.

Commit bcf63ac

Browse files
committed
Fix flaw in HTTPCache::Find where the fallback value does not get
decompressed even when the cached response is compressed and the request does not have accept-encoding. This was found by observing flakiness in valgrind-system tests. Fix flaw in the version of HTTPCache::Put that takes ResponseHeaders* and mutates it unexpectedly, by adding compression headers. Fix a flaw in InflatingFetch that Reset didn't reset all the cached boolean bits. I don't think this was the cause of anything in production becasue we only use Reset in tests (where we re-use a Fetch object).
1 parent 81c64d2 commit bcf63ac

8 files changed

+196
-45
lines changed

net/instaweb/http/http_cache.cc

+43-16
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@
3131
#include "pagespeed/kernel/base/statistics.h"
3232
#include "pagespeed/kernel/base/string.h"
3333
#include "pagespeed/kernel/base/string_util.h"
34-
#include "pagespeed/kernel/base/string_writer.h"
3534
#include "pagespeed/kernel/base/timer.h"
3635
#include "pagespeed/kernel/cache/cache_interface.h"
36+
#include "pagespeed/kernel/http/content_type.h"
3737
#include "pagespeed/kernel/http/google_url.h"
3838
#include "pagespeed/kernel/http/http_names.h"
3939
#include "pagespeed/kernel/http/response_headers.h"
@@ -229,7 +229,17 @@ class HTTPCacheCallback : public CacheInterface::Callback {
229229
headers->IsProxyCacheable(callback_->req_properties(),
230230
callback_->RespectVaryOnResources(),
231231
ResponseHeaders::kHasValidator)) {
232-
callback_->fallback_http_value()->Link(callback_->http_value());
232+
ResponseHeaders fallback_headers;
233+
if (callback_->request_context()->accepts_gzip() ||
234+
!callback_->http_value()->ExtractHeaders(&fallback_headers,
235+
handler_) ||
236+
!InflatingFetch::UnGzipValueIfCompressed(
237+
*callback_->http_value(), &fallback_headers,
238+
callback_->fallback_http_value(), handler_)) {
239+
// If we don't need to unzip, or can't unzip, then just
240+
// link the value and fallback together.
241+
callback_->fallback_http_value()->Link(callback_->http_value());
242+
}
233243
}
234244
}
235245
}
@@ -249,10 +259,14 @@ class HTTPCacheCallback : public CacheInterface::Callback {
249259
if (result_.status != HTTPCache::kFound) {
250260
headers->Clear();
251261
callback_->http_value()->Clear();
252-
}
253-
if (!callback_->request_context()->accepts_gzip()) {
254-
HTTPValue* http_value = callback_->http_value();
255-
InflatingFetch::UnGzipValueIfCompressed(http_value, headers, handler_);
262+
} else if (!callback_->request_context()->accepts_gzip() &&
263+
headers->IsGzipped()) {
264+
HTTPValue new_value;
265+
GoogleString inflated;
266+
if (InflatingFetch::UnGzipValueIfCompressed(
267+
*callback_->http_value(), headers, &new_value, handler_)) {
268+
callback_->http_value()->Link(&new_value);
269+
}
256270
}
257271
start_ms_ = now_ms;
258272
start_us_ = now_us;
@@ -380,7 +394,8 @@ HTTPValue* HTTPCache::ApplyHeaderChangesForPut(
380394
return value;
381395
}
382396

383-
void HTTPCache::PutInternal(const GoogleString& key,
397+
void HTTPCache::PutInternal(bool preserve_response_headers,
398+
const GoogleString& key,
384399
const GoogleString& fragment, int64 start_us,
385400
HTTPValue* value, ResponseHeaders* response_headers,
386401
MessageHandler* handler) {
@@ -394,13 +409,23 @@ void HTTPCache::PutInternal(const GoogleString& key,
394409
if (!value->Empty() && compression_level_ != 0) {
395410
const ContentType* type = response_headers->DetermineContentType();
396411
if ((type != NULL) && type->IsCompressible() &&
397-
!response_headers->IsGzipped() &&
398-
InflatingFetch::GzipValue(compression_level_, value, &compressed_value,
399-
response_headers, handler)) {
400-
// The resource is text (js, css, html, svg, etc.), and not previously
401-
// compressed, so we'll compress it and stick the new compressed version
402-
// in the cache.
403-
value = &compressed_value;
412+
!response_headers->IsGzipped()) {
413+
ResponseHeaders* headers_to_gzip = response_headers;
414+
ResponseHeaders headers_copy;
415+
if (preserve_response_headers) {
416+
headers_copy.CopyFrom(*response_headers);
417+
headers_to_gzip = &headers_copy;
418+
}
419+
headers_to_gzip->ComputeCaching();
420+
421+
if (InflatingFetch::GzipValue(compression_level_, *value,
422+
&compressed_value, headers_to_gzip,
423+
handler)) {
424+
// The resource is text (js, css, html, svg, etc.), and not previously
425+
// compressed, so we'll compress it and stick the new compressed version
426+
// in the cache.
427+
value = &compressed_value;
428+
}
404429
}
405430
}
406431
// TODO(jcrowell): prevent the unzip-rezip flow when sending compressed data
@@ -442,7 +467,8 @@ void HTTPCache::Put(const GoogleString& key, const GoogleString& fragment,
442467
start_us, NULL, &headers, value, handler);
443468
// Put into underlying cache.
444469
if (new_value != NULL) {
445-
PutInternal(key, fragment, start_us, new_value, &headers, handler);
470+
PutInternal(false /* preserve_response_headers */,
471+
key, fragment, start_us, new_value, &headers, handler);
446472
if (cache_inserts_ != NULL) {
447473
cache_inserts_->Add(1);
448474
}
@@ -478,7 +504,8 @@ void HTTPCache::Put(const GoogleString& key, const GoogleString& fragment,
478504
ApplyHeaderChangesForPut(start_us, &content, headers, NULL, handler));
479505
// Put into underlying cache.
480506
if (value.get() != NULL) {
481-
PutInternal(key, fragment, start_us, value.get(), headers, handler);
507+
PutInternal(true /* preserve_response_headers */,
508+
key, fragment, start_us, value.get(), headers, handler);
482509
if (cache_inserts_ != NULL) {
483510
cache_inserts_->Add(1);
484511
}

net/instaweb/http/http_cache_test.cc

+68-3
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,16 @@ class HTTPCacheTest : public testing::Test {
187187
headers, content, handler);
188188
}
189189

190+
void PopulateGzippedEntry(const char* cache_control,
191+
ResponseHeaders* response_headers) {
192+
InitHeaders(response_headers, cache_control);
193+
response_headers->Add(HttpAttributes::kContentType, "text/css");
194+
static const char kCssText[] = ".a {color:blue;} ";
195+
http_cache_->SetCompressionLevel(9);
196+
response_headers->ComputeCaching();
197+
Put(kUrl, kFragment, response_headers, kCssText, &message_handler_);
198+
}
199+
190200
scoped_ptr<ThreadSystem> thread_system_;
191201
SimpleStats simple_stats_;
192202
MockTimer mock_timer_;
@@ -329,8 +339,10 @@ TEST_F(HTTPCacheTest, StaticInflatingFetch) {
329339
EXPECT_TRUE(value.ExtractHeaders(&response_headers, NULL));
330340
// Check that the InflatingFetch gzip methods work properly when extracting
331341
// data.
332-
InflatingFetch::UnGzipValueIfCompressed(&value, &response_headers, NULL);
333-
ASSERT_TRUE(value.ExtractContents(&contents));
342+
HTTPValue ungzipped;
343+
ASSERT_TRUE(InflatingFetch::UnGzipValueIfCompressed(
344+
value, &response_headers, &ungzipped, &message_handler_));
345+
ASSERT_TRUE(ungzipped.ExtractContents(&contents));
334346
ASSERT_TRUE(meta_data_out.Lookup("name", &values));
335347
ASSERT_EQ(static_cast<size_t>(1), values.size());
336348
EXPECT_EQ(GoogleString("value"), *(values[0]));
@@ -717,7 +729,7 @@ TEST_F(HTTPCacheTest, OverrideCacheTtlMs) {
717729
// Now advance the time by 310 seconds and set override cache TTL to 300
718730
// seconds. The lookup fails.
719731
simple_stats_.Clear();
720-
mock_timer_.AdvanceMs(310 * 1000);
732+
mock_timer_.AdvanceMs(310 * Timer::kSecondMs);
721733
callback.reset(NewCallback());
722734
value.Clear();
723735
meta_data_in.Clear();
@@ -871,6 +883,59 @@ TEST_F(HTTPCacheTest, UpdateVersion) {
871883
Find(kUrl, kFragment, &value, &meta_data_out, &message_handler_));
872884
}
873885

886+
TEST_F(HTTPCacheTest, NoMutateOnPut) {
887+
ResponseHeaders response_headers;
888+
PopulateGzippedEntry("max-age=300", &response_headers);
889+
890+
// The value that we have in the cache is compressed, but that should
891+
// not cause a mutation in the response headers passed into Put.
892+
EXPECT_FALSE(response_headers.HasValue(
893+
HttpAttributes::kContentEncoding, "gzip"));
894+
}
895+
896+
TEST_F(HTTPCacheTest, DecompressFallbackValue) {
897+
ResponseHeaders response_headers;
898+
PopulateGzippedEntry("max-age=300", &response_headers);
899+
900+
mock_timer_.AdvanceMs(310 * Timer::kSecondMs); // Makes entry stale.
901+
response_headers.Clear();
902+
903+
// If we don't have gzip in the request, it should not be in the fallback
904+
// value.
905+
scoped_ptr<Callback> callback(NewCallback());
906+
HTTPValue value;
907+
EXPECT_EQ(kNotFoundResult, FindWithCallback( // Not found because it's stale.
908+
kUrl, kFragment, &value, &response_headers, &message_handler_,
909+
callback.get()));
910+
EXPECT_TRUE(callback->fallback_http_value()->ExtractHeaders(
911+
&response_headers, &message_handler_));
912+
EXPECT_FALSE(response_headers.HasValue(
913+
HttpAttributes::kContentEncoding, "gzip"));
914+
}
915+
916+
TEST_F(HTTPCacheTest, LeaveFallbackCompressed) {
917+
ResponseHeaders response_headers;
918+
PopulateGzippedEntry("max-age=300", &response_headers);
919+
920+
mock_timer_.AdvanceMs(310 * Timer::kSecondMs); // Makes entry stale.
921+
response_headers.Clear();
922+
923+
// When we enable gzip in the request, though, we will get it because the
924+
// value stored in the cache is gzipped.
925+
RequestContextPtr request_context(RequestContext::NewTestRequestContext(
926+
thread_system_.get()));
927+
request_context->SetAcceptsGzip(true);
928+
Callback callback(request_context);
929+
HTTPValue value;
930+
EXPECT_EQ(kNotFoundResult, FindWithCallback(
931+
kUrl, kFragment, &value, &response_headers, &message_handler_,
932+
&callback));
933+
EXPECT_TRUE(callback.fallback_http_value()->ExtractHeaders(
934+
&response_headers, &message_handler_));
935+
EXPECT_TRUE(response_headers.HasValue(
936+
HttpAttributes::kContentEncoding, "gzip"));
937+
}
938+
874939
class HTTPCacheWriteThroughTest : public HTTPCacheTest {
875940
protected:
876941
// Unlike HTTPCacheTest::Callback this can produce different validity for

net/instaweb/http/inflating_fetch.cc

+16-14
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,8 @@
3030
namespace net_instaweb {
3131

3232
InflatingFetch::InflatingFetch(AsyncFetch* fetch)
33-
: SharedAsyncFetch(fetch),
34-
request_checked_for_accept_encoding_(false),
35-
compression_desired_(false),
36-
inflate_failure_(false) {
33+
: SharedAsyncFetch(fetch) {
34+
Reset();
3735
}
3836

3937
InflatingFetch::~InflatingFetch() {
@@ -104,15 +102,16 @@ bool InflatingFetch::HandleWrite(const StringPiece& sp,
104102
return status && !inflate_failure_;
105103
}
106104

107-
// Inflate a HTTPValue, if it was gzip compressed, in place.
108-
void InflatingFetch::UnGzipValueIfCompressed(HTTPValue* http_value,
105+
// Inflate a HTTPValue, if it was gzip compressed.
106+
bool InflatingFetch::UnGzipValueIfCompressed(const HTTPValue& src,
109107
ResponseHeaders* headers,
108+
HTTPValue* dest,
110109
MessageHandler* handler) {
111-
if (!http_value->Empty() && headers->IsGzipped()) {
110+
if (!src.Empty() && headers->IsGzipped()) {
112111
GoogleString inflated;
113112
StringWriter inflate_writer(&inflated);
114113
StringPiece content;
115-
http_value->ExtractContents(&content);
114+
src.ExtractContents(&content);
116115
if (GzipInflater::Inflate(content, GzipInflater::kGzip, &inflate_writer)) {
117116
if (!headers->HasValue(HttpAttributes::HttpAttributes::kVary,
118117
HttpAttributes::kAcceptEncoding)) {
@@ -124,22 +123,23 @@ void InflatingFetch::UnGzipValueIfCompressed(HTTPValue* http_value,
124123
headers->Replace(HttpAttributes::kContentLength,
125124
Integer64ToString(inflated.length()));
126125
content.set(inflated.c_str(), inflated.length());
127-
http_value->Clear();
128-
http_value->Write(content, handler);
129-
http_value->SetHeaders(headers);
126+
dest->Write(content, handler);
127+
dest->SetHeaders(headers);
128+
return true;
130129
}
131130
}
131+
return false;
132132
}
133133

134134
bool InflatingFetch::GzipValue(int compression_level,
135-
const HTTPValue* http_value,
135+
const HTTPValue& http_value,
136136
HTTPValue* compressed_value,
137137
ResponseHeaders* headers,
138138
MessageHandler* handler) {
139139
StringPiece content;
140140
GoogleString deflated;
141141
int64 content_length;
142-
http_value->ExtractContents(&content);
142+
http_value.ExtractContents(&content);
143143
StringWriter deflate_writer(&deflated);
144144
if (!headers->IsGzipped() &&
145145
GzipInflater::Deflate(content, GzipInflater::kGzip, compression_level,
@@ -217,8 +217,10 @@ void InflatingFetch::Reset() {
217217
if (inflater_.get() != NULL) {
218218
inflater_->ShutDown();
219219
inflater_.reset(NULL);
220-
inflate_failure_ = false;
221220
}
221+
request_checked_for_accept_encoding_ = false;
222+
compression_desired_ = false;
223+
inflate_failure_ = false;
222224
SharedAsyncFetch::Reset();
223225
}
224226

net/instaweb/http/inflating_fetch_test.cc

+5-4
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ TEST(StaticInflatingFetchTest, CompressUncompressValue) {
315315
headers.Add(HttpAttributes::kContentType, "text/html");
316316
value.SetHeaders(&headers);
317317
HTTPValue compressed_value;
318-
EXPECT_TRUE(InflatingFetch::GzipValue(9, &value, &compressed_value, &headers,
318+
EXPECT_TRUE(InflatingFetch::GzipValue(9, value, &compressed_value, &headers,
319319
&handler));
320320
StringPiece contents;
321321
compressed_value.ExtractContents(&contents);
@@ -325,9 +325,10 @@ TEST(StaticInflatingFetchTest, CompressUncompressValue) {
325325
EXPECT_STREQ(HttpAttributes::kGzip,
326326
headers.Lookup1(HttpAttributes::kContentEncoding));
327327
compressed_value.ExtractHeaders(&headers, &handler);
328-
InflatingFetch::UnGzipValueIfCompressed(&compressed_value, &headers,
329-
&handler);
330-
compressed_value.ExtractContents(&contents);
328+
HTTPValue uncompressed_value;
329+
ASSERT_TRUE(InflatingFetch::UnGzipValueIfCompressed(
330+
compressed_value, &headers, &uncompressed_value, &handler));
331+
uncompressed_value.ExtractContents(&contents);
331332
// We've unzipped the compressed value, it should now say "hello".
332333
EXPECT_EQ(kHello, contents);
333334
}

net/instaweb/http/public/http_cache.h

+3-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
#include "net/instaweb/http/public/request_context.h"
2626
#include "pagespeed/kernel/base/atomic_bool.h"
2727
#include "pagespeed/kernel/base/basictypes.h"
28-
#include "pagespeed/kernel/base/gtest_prod.h" // for FRIEND_TEST
28+
#include "pagespeed/kernel/base/gtest_prod.h"
2929
#include "pagespeed/kernel/base/ref_counted_ptr.h"
3030
#include "pagespeed/kernel/base/string.h"
3131
#include "pagespeed/kernel/base/string_util.h"
@@ -365,7 +365,8 @@ class HTTPCache {
365365

366366
// If headers is passed as NULL, the response headers will be extracted from
367367
// the HTTPValue. Otherwise, the headers passed in will be used.
368-
void PutInternal(const GoogleString& key,
368+
void PutInternal(bool preserve_response_headers,
369+
const GoogleString& key,
369370
const GoogleString& fragment,
370371
int64 start_us,
371372
HTTPValue* value,

net/instaweb/http/public/inflating_fetch.h

+11-4
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
#define NET_INSTAWEB_HTTP_PUBLIC_INFLATING_FETCH_H_
2121

2222
#include "net/instaweb/http/public/async_fetch.h"
23+
#include "net/instaweb/http/public/http_value.h"
2324
#include "pagespeed/kernel/base/basictypes.h"
2425
#include "pagespeed/kernel/base/scoped_ptr.h"
2526
#include "pagespeed/kernel/base/string_util.h"
27+
#include "pagespeed/kernel/http/response_headers.h"
2628
#include "pagespeed/kernel/util/gzip_inflater.h"
2729

2830
namespace net_instaweb {
@@ -49,15 +51,20 @@ class InflatingFetch : public SharedAsyncFetch {
4951
// or gzip was already in the request then this has no effect.
5052
void EnableGzipFromBackend();
5153

52-
// In-place inflate a GZipped HTTPValue if it has been gzipped-compressed,
53-
// updating the headers to reflect the new state.
54-
static void UnGzipValueIfCompressed(HTTPValue* http_value,
54+
// Inflate a GZipped HTTPValue if it has been gzipped-compressed,
55+
// updating the headers to reflect the new state. Returns false if
56+
// the data was not compressed, leaving dest unmodified.
57+
//
58+
// Notes: dest and src should not be the same object. If the
59+
// unzip fails, you may need to link src into dest.
60+
static bool UnGzipValueIfCompressed(const HTTPValue& src,
5561
ResponseHeaders* headers,
62+
HTTPValue* dest,
5663
MessageHandler* handler);
5764
// GZip compress HTTPValue, updating the headers reflect the new
5865
// state, output to compressed_value. Returns true if the value is
5966
// successfully compressed.
60-
static bool GzipValue(int compression_level, const HTTPValue* http_value,
67+
static bool GzipValue(int compression_level, const HTTPValue& http_value,
6168
HTTPValue* compressed_value, ResponseHeaders* headers,
6269
MessageHandler* handler);
6370

net/instaweb/rewriter/rewrite_context.cc

+5-2
Original file line numberDiff line numberDiff line change
@@ -3121,11 +3121,14 @@ void RewriteContext::FetchFallbackCacheDone(HTTPCache::FindResult result,
31213121
scoped_ptr<HTTPCache::Callback> cleanup_callback(data);
31223122

31233123
StringPiece contents;
3124+
ResponseHeaders* response_headers = data->response_headers();
31243125
if ((result.status == HTTPCache::kFound) &&
31253126
data->http_value()->ExtractContents(&contents) &&
3126-
(data->response_headers()->status_code() == HttpStatus::kOK)) {
3127+
(response_headers->status_code() == HttpStatus::kOK)) {
3128+
DCHECK(!response_headers->IsGzipped() ||
3129+
Driver()->request_context()->accepts_gzip());
31273130
// We want to serve the found result, with short cache lifetime.
3128-
fetch_->FetchFallbackDone(contents, data->response_headers());
3131+
fetch_->FetchFallbackDone(contents, response_headers);
31293132
} else {
31303133
StartFetchReconstruction();
31313134
}

0 commit comments

Comments
 (0)