New issue
Advanced search Search tips

Issue 820599 link

Starred by 2 users

Issue metadata

Status: Verified
Owner:
Closed: Mar 2018
Cc:
Components:
EstimatedDays: ----
NextAction: ----
OS: Linux , Android , Windows , Chrome , Mac , Fuchsia
Pri: 1
Type: Bug-Regression



Sign in to add a comment

HTTP range request is served a mismatching response from cache

Project Member Reported by rob@robwu.nl, Mar 9 2018

Issue description

If a range request is sent while another request is pending,
Chrome responds with a cached entry from the pending request,
even if.the cached entry does not match the requested range for the range request.

Chrome versions:
- 61.0.3130.0 (earliest affected - 8061c420676998bda77caa74581ea8061860f438)
- 65.0.3325.146 (stable)
- 67.0.3367.0 (canary)

What steps will reproduce the problem?
1. Download the attached files server.js and testcase.html
2. Start the Node.js script: node server.js 8080
3. Visit the http://localhost:8080 and press the button.

What is the expected result?
PASS: Expected 65536, got 65536 bytes. (fetch with cache)

What happens instead of that?
FAIL: Expected 65536, got 10000 bytes. (fetch with cache)


Please provide any additional information below. Attach a screenshot if
possible.

The test case does the following:
1. Client sends request to server.
2. Server sends first part of response (10000 bytes) and waits before sending the response.
3. Client sends a HTTP range request to the server (requesting 65536 bytes).
4. Server responds to the range request (while the first request is pending).
5. Client checks whether the response from the range request matches with the requested byte range.
(6. Server eventually sends back the rest of the response for both requests; this is not interesting)

This bug happens for both fetch and XMLHttpRequest, so it's likely an issue in the network stack.


Alternative test case (not minimal): https://github.com/mozilla/pdf.js/issues/9544#issuecomment-371887594


Bisect result:
You are probably looking for a change made after 479202 (known good), but no later than 479207 (first known bad).
CHANGELOG URL:
  https://chromium.googlesource.com/chromium/src/+log/3a8a4c87d1120f517aefbcd2a44e81dcbdd6f4ee..0bb515d00576c78a0b74b6154f665f1ed509fee5

Suspect: 8061c420676998bda77caa74581ea8061860f438
 

Comment 1 by rob@robwu.nl, Mar 9 2018

Files for test case. Note that server.js is a Node.js script, and testcase.html should be placed in the same directory.
server.js
2.0 KB View Download
testcase.html
2.2 KB View Download

Comment 2 by rob@robwu.nl, Mar 9 2018

The cache seems very unreliable when range requests are used.
Additional tests below show that the responses for the same request can be different, and that seemingly unrelated byte range requests affect the response of other requests.

I ran more tests, and reused the test framework from the report.
3. Open the DevTools before executing step 3 of the report.
4. When the test finishes, go to the Network tab and copy the URL.
In the console, run the following code (with the URL replaced):

await doFetch('http://localhost:8080/bigfile?1520634101835', new Headers({'range':'bytes=0-50000'}));

Expected: "50001" (the content-length of the response).
Actual  : "10000"

I have repeated the above multiple times (after waiting for the response, to make sure that there are no concurrent requests).

Then I change the start offset by one:
await doFetch('http://localhost:8080/bigfile?1520634101835', new Headers({'range':'bytes=1-50000'}));

Expected: "50000"
Actual  : "9999" (this is one lower than the previous bad case, but still not 50000...)
(I also repeated this multiple times to see if there would be any difference)

Then I repeated the original range request again (starting from 0):
await doFetch('http://localhost:8080/bigfile?1520634101835', new Headers({'range':'bytes=0-50000'}));

Expected: "50001" (the content-length of the response).
Actual  : "9999" (This is different)



PS. If someone starts working on this bug, consider looking at bug 770694 too. Perhaps the two bugs are related.
Cc: mmenke@chromium.org morlovich@chromium.org
I'm not terribly familiar with the cache, but it's been suggested in the past that we stop caching range request responses - we're the only browser that does (Or at least we were at one point, not sure if it's still the case), and it adds a lot of complexity to our cache implementation.  We could also consider just disabling it until this current set of issues is resolved.
Labels: Needs-Feedback
@reporter: Can you please provide the net internals logs for the issue as per the following instructions:
https://sites.google.com/a/chromium.org/dev/for-testers/providing-network-details

Comment 5 by rob@robwu.nl, Mar 11 2018

See attachment. I followed the STR from the report, but opened the devtools before executing step 3.
chrome-net-export-log.json
68.3 KB View Download
re: comment #3: they are rather important for things like short looping videos 
(or at least multiple people complained about backend size limits breaking caching for their not-quite-short videos); though I definitely agree with you on complexity impact.
Also I think our PDF plugin might use it, much like pdf.js, which it sounds like what this was discovered with?

re: comment #5: so I peeked at the log to see if the backend screws up, but it's uninteresting in that way; dumping what I noticed here in case it saves Shivani some time. The first requests is a regular GET, with no range funkyness, though we only get partway through before we doom the cache entry due to the 2nd request 
coming in (note that our story with concurrent range request is not great even discounting outright bugs, I think):

t= 11 [st= 10]    SIMPLE_CACHE_ENTRY_DOOM_CALL
t= 11 [st= 10]    SIMPLE_CACHE_ENTRY_DOOM_BEGIN
t= 12 [st= 11]    SIMPLE_CACHE_ENTRY_DOOM_END
t=105 [st=104]    SIMPLE_CACHE_ENTRY_WRITE_CALL
                  --> buf_len = 10000
                  --> index = 1
                  --> offset = 10000
                  --> truncate = true

(10K, this may be important)

Then on second request:

t=10 [st=1]      HTTP_CACHE_CALLER_REQUEST_HEADERS
                 --> X-DevTools-Emulate-Network-Conditions-Client-Id: C93601E469E7CA04FBA40CFA3F2BF19E
                     User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3367.0 Safari/537.36
                     range: bytes=0-65535
                     Accept: */*
                     Referer: http://localhost:8080/
                     Accept-Encoding: gzip, deflate, br
                     Accept-Language: en-US,en;q=0.9
                 --> line = ""
t=11 [st=2]      HTTP_CACHE_GET_BACKEND  [dt=0]
t=11 [st=2]      HTTP_CACHE_OPEN_ENTRY  [dt=0]
t=11 [st=2]      HTTP_CACHE_ADD_TO_ENTRY  [dt=0]
t=11 [st=2]      HTTP_CACHE_READ_INFO  [dt=0]
t=11 [st=2]      HTTP_CACHE_RESTART_PARTIAL_REQUEST
t=11 [st=2]      HTTP_CACHE_CREATE_ENTRY  [dt=0]
t=11 [st=2]      HTTP_CACHE_ADD_TO_ENTRY  [dt=1]
t=12 [st=3]     +HTTP_STREAM_REQUEST  [dt=2]
t=12 [st=3]        HTTP_STREAM_JOB_CONTROLLER_BOUND
                   --> source_dependency = 386 (HTTP_STREAM_JOB_CONTROLLER)
t=14 [st=5]        HTTP_STREAM_REQUEST_BOUND_TO_JOB
                   --> source_dependency = 387 (HTTP_STREAM_JOB)
t=14 [st=5]     -HTTP_STREAM_REQUEST
t=14 [st=5]     +HTTP_TRANSACTION_SEND_REQUEST  [dt=0]
t=14 [st=5]        HTTP_TRANSACTION_SEND_REQUEST_HEADERS
                   --> GET /bigfile?1520764865483 HTTP/1.1
                       Host: localhost:8080
                       Connection: keep-alive
                       User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3367.0 Safari/537.36
                       Accept: */*
                       Referer: http://localhost:8080/
                       Accept-Encoding: gzip, deflate, br
                       Accept-Language: en-US,en;q=0.9
                       Range: bytes=0-9999

... Notice the change in range.



Initial investigation reveals this:
While the 1st request is still pending completion, the 2nd request comes in, due to parallel validation , starts looking at the headers written in the cache by the 1st request and hits this !ConditionalizeRequest() condition (https://cs.chromium.org/chromium/src/net/http/http_cache_transaction.cc?dr&l=2470). This leads to dooming the existing entry. This seems wrong to doom the existing entry.

Also noticed while debugging that if there is a timing gap between the 2 requests, the second succeeds. It might help to understand how does it get past the !ConditionalizeRequest() condition in that case?
Created a CL for the fix here: https://chromium-review.googlesource.com/c/chromium/src/+/959450

The issue is this:
HttpCache::Transaction::ResetPartialState dooms the current entry, and
goes back to creating a new entry for this request. In case of parallel
validation, if the first request is a full request and has written some
part of the response to the cache, then this second transaction would
create the range header with as many bytes as the written response even
on dooming the current entry which is incorrect.
This CL fixes it by restoring the original range header on this path.

Comment 9 by mattm@chromium.org, Mar 13 2018

Labels: -Needs-Feedback
(Removing needs-feedback label, looks like it was provided.)
Project Member

Comment 10 by bugdroid1@chromium.org, Mar 15 2018

The following revision refers to this bug:
  https://chromium.googlesource.com/chromium/src.git/+/4fe20e106bcb130bea7cbffdca6bc6a192e7b1fa

commit 4fe20e106bcb130bea7cbffdca6bc6a192e7b1fa
Author: Shivani Sharma <shivanisha@chromium.org>
Date: Thu Mar 15 22:36:08 2018

[Http Cache]: Reset the range headers to original when restarting

HttpCache::Transaction::ResetPartialState dooms the current entry, and
goes back to creating a new entry for this request. In case of parallel
validation, if the first request is a full request and has written some
part of the response to the cache, then this second transaction would
create the range header with as many bytes as the written response even
on dooming the current entry which is incorrect.
This CL fixes it by restoring the original range header on this path.

RangeGET_ParallelValidationCouldntConditionalize
Also manually verified the test case reported in the bug.

TEST: net_unittests --gtest_filter=HttpCache.
Bug:  820599 
Change-Id: I4e57003193873e433c3b503b9fc20a843f28c4da
Reviewed-on: https://chromium-review.googlesource.com/959450
Reviewed-by: Maks Orlovich <morlovich@chromium.org>
Commit-Queue: Shivani Sharma <shivanisha@chromium.org>
Cr-Commit-Position: refs/heads/master@{#543521}
[modify] https://crrev.com/4fe20e106bcb130bea7cbffdca6bc6a192e7b1fa/net/http/http_cache_transaction.cc
[modify] https://crrev.com/4fe20e106bcb130bea7cbffdca6bc6a192e7b1fa/net/http/http_cache_unittest.cc

Status: Fixed (was: Assigned)

Comment 12 by rob@robwu.nl, Mar 19 2018

Status: Verified (was: Fixed)
Verified fixed in 67.0.3376.0 with the test case from my initial report.

Sign in to add a comment