Commit 9f32e9c0 authored by Matt Falkenhagen's avatar Matt Falkenhagen Committed by Commit Bot

S13nServiceWorker: Add tests for network fallback for navigations with request bodies

Originally I planned to clone the request body for main resource requests,
similar to subresource requests, but it looks unnecessary for now because
main resource request bodies don't have data pipe getter elements. They
are only created by the renderer when converting from a Blob, which doesn't
happen for navigations.

So this patch:
- Adds a DCHECK to the main resource request handling code that there are
no data pipe elements.
- Adds a WPT test for network fallback with a request body (using just strings
since we can't test uploading a file with WPT as far as a I can tell, it needs
user interaction or a special test harness flag).
- Adds a http/tests/local/ test case for file upload with network fallback.
This test file was already passing, so the failing expectation is also removed.

R=kinuko, shimazu

Bug: 778878
Cq-Include-Trybots: master.tryserver.chromium.linux:linux_mojo
Change-Id: I7a5f7a72ef9ac2ca3ffbe54549739ca6bcc8d071
Reviewed-on: https://chromium-review.googlesource.com/885684
Commit-Queue: Matt Falkenhagen <falken@chromium.org>
Reviewed-by: default avatarKinuko Yasuda <kinuko@chromium.org>
Reviewed-by: default avatarMakoto Shimazu <shimazu@chromium.org>
Cr-Commit-Position: refs/heads/master@{#532121}
parent 89760234
...@@ -19,6 +19,20 @@ ...@@ -19,6 +19,20 @@
namespace content { namespace content {
namespace {
bool BodyHasNoDataPipeGetters(const network::ResourceRequestBody* body) {
if (!body)
return true;
for (const auto& elem : *body->elements()) {
if (elem.type() == network::DataElement::TYPE_DATA_PIPE)
return false;
}
return true;
}
} // namespace
// This class waits for completion of a stream response from the service worker. // This class waits for completion of a stream response from the service worker.
// It calls ServiceWorkerURLLoader::CommitComplete() upon completion of the // It calls ServiceWorkerURLLoader::CommitComplete() upon completion of the
// response. // response.
...@@ -154,10 +168,25 @@ void ServiceWorkerURLLoaderJob::StartRequest() { ...@@ -154,10 +168,25 @@ void ServiceWorkerURLLoaderJob::StartRequest() {
return; return;
} }
// ServiceWorkerFetchDispatcher requires a std::unique_ptr<ResourceRequest>
// so make one here.
// TODO(crbug.com/803125): Try to eliminate unnecessary copying?
auto request = std::make_unique<network::ResourceRequest>(resource_request_);
// Passing the request body over Mojo moves out the DataPipeGetter elements,
// which would mean we should clone the body like
// ServiceWorkerSubresourceLoader does. But we don't expect DataPipeGetters
// here yet: they are only created by the renderer when converting from a
// Blob, which doesn't happen for navigations. In interest of speed, just
// don't clone until proven necessary.
DCHECK(BodyHasNoDataPipeGetters(request->request_body.get()))
<< "We assumed there would be no data pipe getter elements here, but "
"there are. Add code here to clone the body before proceeding.";
// Dispatch the fetch event. // Dispatch the fetch event.
fetch_dispatcher_ = std::make_unique<ServiceWorkerFetchDispatcher>( fetch_dispatcher_ = std::make_unique<ServiceWorkerFetchDispatcher>(
std::make_unique<network::ResourceRequest>(resource_request_), std::move(request), active_worker,
active_worker, net::NetLogWithSource() /* TODO(scottmg): net log? */, net::NetLogWithSource() /* TODO(scottmg): net log? */,
base::BindOnce(&ServiceWorkerURLLoaderJob::DidPrepareFetchEvent, base::BindOnce(&ServiceWorkerURLLoaderJob::DidPrepareFetchEvent,
weak_factory_.GetWeakPtr(), weak_factory_.GetWeakPtr(),
base::WrapRefCounted(active_worker)), base::WrapRefCounted(active_worker)),
......
...@@ -42,7 +42,6 @@ Bug(none) external/wpt/service-workers/service-worker/ServiceWorkerGlobalScope/u ...@@ -42,7 +42,6 @@ Bug(none) external/wpt/service-workers/service-worker/ServiceWorkerGlobalScope/u
Bug(none) external/wpt/service-workers/service-worker/appcache-ordering-main.https.html [ Pass Failure ] Bug(none) external/wpt/service-workers/service-worker/appcache-ordering-main.https.html [ Pass Failure ]
Bug(none) external/wpt/service-workers/service-worker/ready.https.html [ Pass Crash Failure ] Bug(none) external/wpt/service-workers/service-worker/ready.https.html [ Pass Crash Failure ]
Bug(none) external/wpt/service-workers/service-worker/skip-waiting-using-registration.https.html [ Pass Timeout ] Bug(none) external/wpt/service-workers/service-worker/skip-waiting-using-registration.https.html [ Pass Timeout ]
Bug(none) http/tests/local/serviceworker/fetch-request-body-file.html [ Pass Crash Failure ]
Bug(none) http/tests/serviceworker/chromium/stop-worker-during-respond-with.html [ Pass Failure ] Bug(none) http/tests/serviceworker/chromium/stop-worker-during-respond-with.html [ Pass Failure ]
Bug(none) http/tests/appcache/top-frame-3.html [ Pass Timeout ] Bug(none) http/tests/appcache/top-frame-3.html [ Pass Timeout ]
Bug(none) http/tests/appcache/top-frame-4.html [ Pass Timeout ] Bug(none) http/tests/appcache/top-frame-4.html [ Pass Timeout ]
......
...@@ -8,6 +8,7 @@ PASS Service Worker does not respond to fetch event ...@@ -8,6 +8,7 @@ PASS Service Worker does not respond to fetch event
PASS Service Worker responds to fetch event with null response body PASS Service Worker responds to fetch event with null response body
PASS Service Worker fetches other file in fetch event PASS Service Worker fetches other file in fetch event
PASS Service Worker responds to fetch event with POST form PASS Service Worker responds to fetch event with POST form
PASS Service Worker falls back to network in fetch event with POST form
PASS Multiple calls of respondWith must throw InvalidStateErrors PASS Multiple calls of respondWith must throw InvalidStateErrors
PASS Service Worker event.respondWith must set the used flag PASS Service Worker event.respondWith must set the used flag
PASS Service Worker should expose FetchEvent URL fragments. PASS Service Worker should expose FetchEvent URL fragments.
......
...@@ -174,50 +174,72 @@ async_test(function(t) { ...@@ -174,50 +174,72 @@ async_test(function(t) {
.catch(unreached_rejection(t)); .catch(unreached_rejection(t));
}, 'Service Worker fetches other file in fetch event'); }, 'Service Worker fetches other file in fetch event');
async_test(function(t) { // Creates a form and an iframe and does a form submission that navigates the
var scope = 'resources/simple.html?form-post'; // frame to |action_url|. Returns the frame after navigation.
var frame_name = 'xhr-post-frame'; function submit_form(action_url) {
service_worker_unregister_and_register(t, worker, scope) return new Promise(resolve => {
.then(function(reg) { const frame = document.createElement('iframe');
frame.name = 'post-frame';
document.body.appendChild(frame);
const form = document.createElement('form');
form.target = frame.name;
form.action = action_url;
form.method = 'post';
const input1 = document.createElement('input');
input1.type = 'text';
input1.value = 'testValue1';
input1.name = 'testName1'
form.appendChild(input1);
const input2 = document.createElement('input');
input2.type = 'text';
input2.value = 'testValue2';
input2.name = 'testName2'
form.appendChild(input2);
document.body.appendChild(form);
frame.onload = function() {
document.body.removeChild(form);
resolve(frame);
};
form.submit();
});
}
promise_test(t => {
const scope = 'resources/simple.html?form-post';
return service_worker_unregister_and_register(t, worker, scope)
.then(reg => {
add_result_callback(() => { reg.unregister(); });
return wait_for_state(t, reg.installing, 'activated'); return wait_for_state(t, reg.installing, 'activated');
}) })
.then(function(sw) { .then(() => {
return new Promise(function(resolve) { return submit_form(scope);
var frame = document.createElement('iframe');
frame.name = frame_name;
document.body.appendChild(frame);
var form = document.createElement('form');
form.target = frame_name;
form.action = scope;
form.method = 'post';
var input1 = document.createElement('input');
input1.type = 'text';
input1.value = 'testValue1';
input1.name = 'testName1'
form.appendChild(input1);
var input2 = document.createElement('input');
input2.type = 'text';
input2.value = 'testValue2';
input2.name = 'testName2'
form.appendChild(input2);
document.body.appendChild(form);
frame.onload = function() {
document.body.removeChild(form);
resolve(frame);
};
form.submit();
});
}) })
.then(function(frame) { .then(frame => {
assert_equals(frame.contentDocument.body.textContent, assert_equals(frame.contentDocument.body.textContent,
'POST:application/x-www-form-urlencoded:' + 'POST:application/x-www-form-urlencoded:' +
'testName1=testValue1&testName2=testValue2'); 'testName1=testValue1&testName2=testValue2');
frame.remove(); frame.remove();
return service_worker_unregister_and_done(t, scope); });
})
.catch(unreached_rejection(t));
}, 'Service Worker responds to fetch event with POST form'); }, 'Service Worker responds to fetch event with POST form');
promise_test(t => {
// Add '?ignore' to scope so the service worker falls back to network.
const scope = 'resources/echo-content.py?ignore';
return service_worker_unregister_and_register(t, worker, scope)
.then(reg => {
add_result_callback(() => { reg.unregister(); });
return wait_for_state(t, reg.installing, 'activated');
})
.then(() => {
return submit_form(scope);
})
.then(frame => {
assert_equals(frame.contentDocument.body.textContent,
'testName1=testValue1&testName2=testValue2');
frame.remove();
});
}, 'Service Worker falls back to network in fetch event with POST form');
async_test(function(t) { async_test(function(t) {
var scope = 'resources/simple.html?multiple-respond-with'; var scope = 'resources/simple.html?multiple-respond-with';
service_worker_unregister_and_register(t, worker, scope) service_worker_unregister_and_register(t, worker, scope)
......
# This is a copy of fetch/api/resources/echo-content.py since it's more
# convenient in this directory due to service worker's path restriction.
def main(request, response):
headers = [("X-Request-Method", request.method),
("X-Request-Content-Length", request.headers.get("Content-Length", "NO")),
("X-Request-Content-Type", request.headers.get("Content-Type", "NO"))]
content = request.body
return headers, content
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
<script> <script>
var ORIGIN = get_host_info()['HTTP_ORIGIN']; var ORIGIN = get_host_info()['HTTP_ORIGIN'];
var IFRAME_BASE_URL = ORIGIN + '/local/serviceworker/resources/fetch-request-body-file-iframe.html'; var IFRAME_BASE_URL = ORIGIN + '/local/serviceworker/resources/fetch-request-body-file-iframe.html';
var SCOPE = ORIGIN + '/local/serviceworker/resources/fetch-request-body-file-test'; var SCOPE = ORIGIN + '/local/serviceworker/resources/fetch-request-body-file-test.php';
function register() { function register() {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
...@@ -134,12 +134,22 @@ function as_blob_test_frame_check(frame) { ...@@ -134,12 +134,22 @@ function as_blob_test_frame_check(frame) {
assert_equals(data.body_size, expected_body.length); assert_equals(data.body_size, expected_body.length);
} }
function network_fallback_test_frame_check(frame) {
const expected =
'testName1=testValue1&' +
'testName2=testValue2&' +
'testFile=file-for-drag-to-send.txt:text/plain:1234567890'
assert_equals(frame.contentDocument.body.textContent, expected);
}
async_test(function(t) { async_test(function(t) {
register() register()
.then(function() { return send_post_frame_promise('asText'); }) .then(function() { return send_post_frame_promise('asText'); })
.then(as_text_test_frame_check) .then(as_text_test_frame_check)
.then(function() { return send_post_frame_promise('asBlob'); }) .then(function() { return send_post_frame_promise('asBlob'); })
.then(as_blob_test_frame_check) .then(as_blob_test_frame_check)
.then(function() { return send_post_frame_promise('NetworkFallback'); })
.then(network_fallback_test_frame_check)
.then(function() { return unregister_and_done(t); }) .then(function() { return unregister_and_done(t); })
.catch(unreached_rejection(t)); .catch(unreached_rejection(t));
}, 'Service Worker fetch request body with file.'); }, 'Service Worker fetch request body with file.');
......
<?php
// This file is a copy of xmlhttprequest/resources/multipart-post-echo.php
// Not sure if it's possible to reuse the same file, when falken@ tried it
// seemed like http/tests/local tests run in a weird server configuration
// without access to the other files.
if (strpos($_SERVER['CONTENT_TYPE'], 'multipart/form-data; boundary=') != 0) {
echo 'Invalid Content-Types.';
return;
}
$values = array();
foreach ($_POST as $key => $value) {
$values[] = "$key=$value";
}
foreach ($_FILES as $key => $value) {
$file = $_FILES[$key];
if ($file['error']) {
echo 'Upload file error: ' . $file['error'];
return;
} else {
$fp = fopen($file['tmp_name'], 'r');
if ($fp) {
$content = $file['size'] > 0 ? fread($fp, $file['size']) : "";
fclose($fp);
}
$values[] = $key . '=' . $file['name'] . ':' . $file['type'] . ':' . $content;
}
}
echo join('&', $values);
?>
self.addEventListener('fetch', function(event) { self.addEventListener('fetch', function(event) {
if (event.request.url.indexOf('NetworkFallback') != -1)
return;
event.respondWith(new Promise(function(resolve) { event.respondWith(new Promise(function(resolve) {
var headers = []; var headers = [];
for (var header of event.request.headers) { for (var header of event.request.headers) {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment