[Fetch API] Implement abortable fetch
authoryouenn@apple.com <youenn@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 5 Jan 2019 00:01:43 +0000 (00:01 +0000)
committeryouenn@apple.com <youenn@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 5 Jan 2019 00:01:43 +0000 (00:01 +0000)
https://bugs.webkit.org/show_bug.cgi?id=174980
<rdar://problem/46861402>

Reviewed by Chris Dumez.

LayoutTests/imported/w3c:

Fixed tests to run in WebKit CI.
Also fixed a bug in a test where the fetch response body is not actually empty.

* web-platform-tests/fetch/api/abort/cache.https-expected.txt:
* web-platform-tests/fetch/api/abort/general-serviceworker.https-expected.txt:
* web-platform-tests/fetch/api/abort/general.any-expected.txt:
* web-platform-tests/fetch/api/abort/general.any.js:
* web-platform-tests/fetch/api/abort/general.any.worker-expected.txt:
* web-platform-tests/fetch/api/abort/serviceworker-intercepted.https-expected.txt:
* web-platform-tests/fetch/api/response/response-consume-stream-expected.txt:

Source/WebCore:

Add an AbortSignal to FetchRequest.

Add support for AbortSignal algorithm.
The fetch request signal is added an algorithm to abort the fetch.
Update clone algorithm to let signal of the cloned request be following the origin request.

Update ReadableStream error handling to return an exception instead of a string.
This allows passing an AbortError instead of a TypeError as previously done.

Update FetchBodyOwner to store a loading error either as an exception or as a resource error.
The latter is used for passing the error from service worker back to the page.
The former is used to pass it to ReadableStream or body accessors.

Covered by enabled tests.

* Modules/cache/DOMCache.cpp:
(WebCore::DOMCache::put):
* Modules/fetch/FetchBody.cpp:
(WebCore::FetchBody::consumeAsStream):
(WebCore::FetchBody::loadingFailed):
* Modules/fetch/FetchBody.h:
* Modules/fetch/FetchBodyConsumer.cpp:
(WebCore::FetchBodyConsumer::loadingFailed):
* Modules/fetch/FetchBodyConsumer.h:
* Modules/fetch/FetchBodyOwner.cpp:
(WebCore::FetchBodyOwner::arrayBuffer):
(WebCore::FetchBodyOwner::blob):
(WebCore::FetchBodyOwner::cloneBody):
(WebCore::FetchBodyOwner::formData):
(WebCore::FetchBodyOwner::json):
(WebCore::FetchBodyOwner::text):
(WebCore::FetchBodyOwner::loadBlob):
(WebCore::FetchBodyOwner::blobLoadingFailed):
(WebCore::FetchBodyOwner::consumeBodyAsStream):
(WebCore::FetchBodyOwner::setLoadingError):
* Modules/fetch/FetchBodyOwner.h:
(WebCore::FetchBodyOwner::loadingError const):
(WebCore::FetchBodyOwner::loadingException const):
* Modules/fetch/FetchBodySource.cpp:
(WebCore::FetchBodySource::error):
* Modules/fetch/FetchBodySource.h:
* Modules/fetch/FetchRequest.cpp:
(WebCore::FetchRequest::initializeWith):
(WebCore::FetchRequest::clone):
* Modules/fetch/FetchRequest.h:
(WebCore::FetchRequest::FetchRequest):
* Modules/fetch/FetchRequest.idl:
* Modules/fetch/FetchRequestInit.h:
(WebCore::FetchRequestInit::hasMembers const):
* Modules/fetch/FetchRequestInit.idl:
* Modules/fetch/FetchResponse.cpp:
(WebCore::FetchResponse::clone):
(WebCore::FetchResponse::fetch):
(WebCore::FetchResponse::BodyLoader::didFail):
* Modules/fetch/FetchResponse.h:
* bindings/js/ReadableStreamDefaultController.h:
(WebCore::ReadableStreamDefaultController::error):
* dom/AbortSignal.cpp:
(WebCore::AbortSignal::abort):
(WebCore::AbortSignal::follow):
* dom/AbortSignal.h:

LayoutTests:

* TestExpectations: Enable abort tests.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@239644 268f45cc-cd09-0410-ab3c-d52691b4dbfc

32 files changed:
LayoutTests/ChangeLog
LayoutTests/TestExpectations
LayoutTests/imported/w3c/ChangeLog
LayoutTests/imported/w3c/web-platform-tests/fetch/api/abort/cache.https-expected.txt
LayoutTests/imported/w3c/web-platform-tests/fetch/api/abort/general-serviceworker.https-expected.txt
LayoutTests/imported/w3c/web-platform-tests/fetch/api/abort/general.any-expected.txt
LayoutTests/imported/w3c/web-platform-tests/fetch/api/abort/general.any.js
LayoutTests/imported/w3c/web-platform-tests/fetch/api/abort/general.any.worker-expected.txt
LayoutTests/imported/w3c/web-platform-tests/fetch/api/abort/serviceworker-intercepted.https-expected.txt
LayoutTests/imported/w3c/web-platform-tests/fetch/api/response/response-consume-stream-expected.txt
LayoutTests/platform/ios-simulator/TestExpectations
Source/WebCore/ChangeLog
Source/WebCore/Modules/cache/DOMCache.cpp
Source/WebCore/Modules/fetch/FetchBody.cpp
Source/WebCore/Modules/fetch/FetchBody.h
Source/WebCore/Modules/fetch/FetchBodyConsumer.cpp
Source/WebCore/Modules/fetch/FetchBodyConsumer.h
Source/WebCore/Modules/fetch/FetchBodyOwner.cpp
Source/WebCore/Modules/fetch/FetchBodyOwner.h
Source/WebCore/Modules/fetch/FetchBodySource.cpp
Source/WebCore/Modules/fetch/FetchBodySource.h
Source/WebCore/Modules/fetch/FetchRequest.cpp
Source/WebCore/Modules/fetch/FetchRequest.h
Source/WebCore/Modules/fetch/FetchRequest.idl
Source/WebCore/Modules/fetch/FetchRequestInit.h
Source/WebCore/Modules/fetch/FetchRequestInit.idl
Source/WebCore/Modules/fetch/FetchResponse.cpp
Source/WebCore/Modules/fetch/FetchResponse.h
Source/WebCore/bindings/js/ReadableStreamDefaultController.h
Source/WebCore/dom/AbortSignal.cpp
Source/WebCore/dom/AbortSignal.h
Source/WebCore/workers/service/context/ServiceWorkerFetch.cpp

index 1afc066..86df3e4 100644 (file)
@@ -1,3 +1,13 @@
+2019-01-04  Youenn Fablet  <youenn@apple.com>
+
+        [Fetch API] Implement abortable fetch
+        https://bugs.webkit.org/show_bug.cgi?id=174980
+        <rdar://problem/46861402>
+
+        Reviewed by Chris Dumez.
+
+        * TestExpectations: Enable abort tests.
+
 2019-01-04  Brent Fulgham  <bfulgham@apple.com>
 
         Parsed protocol of javascript URLs with embedded newlines and carriage returns do not match parsed protocol in Chrome and Firefox
index 23018bc..4d5c642 100644 (file)
@@ -211,7 +211,6 @@ imported/w3c/web-platform-tests/service-workers/service-worker/worker-client-id.
 imported/w3c/web-platform-tests/cors/remote-origin.htm [ Skip ]
 
 # Skip service worker tests that are timing out.
-imported/w3c/web-platform-tests/fetch/api/abort/general-serviceworker.https.html [ Skip ]
 imported/w3c/web-platform-tests/service-workers/service-worker/performance-timeline.https.html [ Skip ]
 imported/w3c/web-platform-tests/service-workers/service-worker/respond-with-body-accessed-response.https.html [ Skip ]
 imported/w3c/web-platform-tests/service-workers/service-worker/sandboxed-iframe-fetch-event.https.html [ Skip ]
@@ -383,10 +382,6 @@ webkit.org/b/189905 imported/w3c/web-platform-tests/resource-timing/resource_ini
 webkit.org/b/189910 imported/w3c/web-platform-tests/resource-timing/resource_timing_store_and_clear_during_callback.html [ Pass Failure ]
 webkit.org/b/190523 imported/w3c/web-platform-tests/resource-timing/resource_timing_cross_origin_redirect_chain.html [ Pass Failure ]
 
-# The follow two tests change their output each run
-imported/w3c/web-platform-tests/fetch/api/abort/general.any.html [ Skip ]
-imported/w3c/web-platform-tests/fetch/api/abort/general.any.worker.html [ Skip ]
-
 # These tests time out
 imported/w3c/web-platform-tests/fetch/api/request/destination/fetch-destination-no-load-event.https.html [ Skip ]
 imported/w3c/web-platform-tests/fetch/api/request/destination/fetch-destination.https.html [ Skip ]
index 6a43ef6..ce47ba8 100644 (file)
@@ -1,3 +1,22 @@
+2019-01-04  Youenn Fablet  <youenn@apple.com>
+
+        [Fetch API] Implement abortable fetch
+        https://bugs.webkit.org/show_bug.cgi?id=174980
+        <rdar://problem/46861402>
+
+        Reviewed by Chris Dumez.
+
+        Fixed tests to run in WebKit CI.
+        Also fixed a bug in a test where the fetch response body is not actually empty.
+
+        * web-platform-tests/fetch/api/abort/cache.https-expected.txt:
+        * web-platform-tests/fetch/api/abort/general-serviceworker.https-expected.txt:
+        * web-platform-tests/fetch/api/abort/general.any-expected.txt:
+        * web-platform-tests/fetch/api/abort/general.any.js:
+        * web-platform-tests/fetch/api/abort/general.any.worker-expected.txt:
+        * web-platform-tests/fetch/api/abort/serviceworker-intercepted.https-expected.txt:
+        * web-platform-tests/fetch/api/response/response-consume-stream-expected.txt:
+
 2019-01-02  Simon Fraser  <simon.fraser@apple.com>
 
         Support css-color-4 rgb functions
index 6a97708..08a1867 100644 (file)
@@ -1,4 +1,4 @@
 
-FAIL Signals are not stored in the cache API promise_test: Unhandled rejection with value: object "TypeError: undefined is not an object (evaluating 'cachedRequest.signal.aborted')"
-FAIL Signals are not stored in the cache API, even if they're already aborted promise_test: Unhandled rejection with value: object "TypeError: undefined is not an object (evaluating 'cachedRequest.signal.aborted')"
+PASS Signals are not stored in the cache API 
+PASS Signals are not stored in the cache API, even if they're already aborted 
 
index 9ee4d9c..471bc34 100644 (file)
@@ -1,8 +1,6 @@
 
-Harness Error (TIMEOUT), message = null
-
-FAIL Aborting rejects with AbortError assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL Aborting rejects with AbortError - no-cors assert_throws: function "function () { throw e }" threw object "TypeError: A server with the specified hostname could not be found." that is not a DOMException AbortError: property "code" is equal to undefined, expected 20
+PASS Aborting rejects with AbortError 
+PASS Aborting rejects with AbortError - no-cors 
 PASS TypeError from request constructor takes priority - RequestInit's window is not null 
 PASS TypeError from request constructor takes priority - Input URL is not valid 
 PASS TypeError from request constructor takes priority - Input URL has credentials 
@@ -19,34 +17,34 @@ PASS TypeError from request constructor takes priority - Bad mode init parameter
 PASS TypeError from request constructor takes priority - Bad credentials init parameter value 
 PASS TypeError from request constructor takes priority - Bad cache init parameter value 
 PASS TypeError from request constructor takes priority - Bad redirect init parameter value 
-FAIL Request objects have a signal property assert_true: Signal member is present & truthy expected true got false
-FAIL Signal on request object assert_true: Signal member is present & truthy expected true got false
-FAIL Signal on request object created from request object assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL Signal on request object created from request object, with signal on second request assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL Signal on request object created from request object, with signal on second request overriding another assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL Signal retained after unrelated properties are overridden by fetch assert_unreached: Should have rejected: undefined Reached unreachable code
+PASS Request objects have a signal property 
+PASS Signal on request object 
+PASS Signal on request object created from request object 
+PASS Signal on request object created from request object, with signal on second request 
+PASS Signal on request object created from request object, with signal on second request overriding another 
+PASS Signal retained after unrelated properties are overridden by fetch 
 PASS Signal removed by setting to null 
-FAIL Already aborted signal rejects immediately assert_unreached: Fetch must not resolve Reached unreachable code
+PASS Already aborted signal rejects immediately 
 PASS Request is still 'used' if signal is aborted before fetching 
-FAIL response.arrayBuffer() rejects if already aborted assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL response.blob() rejects if already aborted assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL response.formData() rejects if already aborted assert_throws: function "function () { throw e }" threw object "NotSupportedError: The operation is not supported." that is not a DOMException AbortError: property "code" is equal to 9, expected 20
-FAIL response.json() rejects if already aborted assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL response.text() rejects if already aborted assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL Already aborted signal does not make request assert_equals: Request hasn't been made to the server expected (object) null but got (string) "open"
-FAIL Already aborted signal can be used for many fetches assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL Signal can be used to abort other fetches, even if another fetch succeeded before aborting assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL Underlying connection is closed when aborting after receiving response promise_test: Unhandled rejection with value: object "Error: Timed out"
-FAIL Underlying connection is closed when aborting after receiving response - no-cors promise_test: Unhandled rejection with value: object "TypeError: A server with the specified hostname could not be found."
-TIMEOUT Fetch aborted & connection closed when aborted after calling response.arrayBuffer() Test timed out
-NOTRUN Fetch aborted & connection closed when aborted after calling response.blob() 
-NOTRUN Fetch aborted & connection closed when aborted after calling response.formData() 
-NOTRUN Fetch aborted & connection closed when aborted after calling response.json() 
-NOTRUN Fetch aborted & connection closed when aborted after calling response.text() 
-NOTRUN Stream errors once aborted. Underlying connection closed. 
-NOTRUN Stream errors once aborted, after reading. Underlying connection closed. 
-NOTRUN Stream will not error if body is empty. It's closed with an empty queue before it errors. 
-NOTRUN Readable stream synchronously cancels with AbortError if aborted before reading 
-FAIL Signal state is cloned undefined is not an object (evaluating 'request.signal.aborted')
-FAIL Clone aborts with original controller undefined is not an object (evaluating 'request.signal.addEventListener')
+PASS response.arrayBuffer() rejects if already aborted 
+PASS response.blob() rejects if already aborted 
+PASS response.formData() rejects if already aborted 
+PASS response.json() rejects if already aborted 
+PASS response.text() rejects if already aborted 
+PASS Already aborted signal does not make request 
+PASS Already aborted signal can be used for many fetches 
+PASS Signal can be used to abort other fetches, even if another fetch succeeded before aborting 
+PASS Underlying connection is closed when aborting after receiving response 
+PASS Underlying connection is closed when aborting after receiving response - no-cors 
+PASS Fetch aborted & connection closed when aborted after calling response.arrayBuffer() 
+PASS Fetch aborted & connection closed when aborted after calling response.blob() 
+FAIL Fetch aborted & connection closed when aborted after calling response.formData() assert_throws: function "function () { throw e }" threw object "NotSupportedError: The operation is not supported." that is not a DOMException AbortError: property "code" is equal to 9, expected 20
+PASS Fetch aborted & connection closed when aborted after calling response.json() 
+PASS Fetch aborted & connection closed when aborted after calling response.text() 
+PASS Stream errors once aborted. Underlying connection closed. 
+PASS Stream errors once aborted, after reading. Underlying connection closed. 
+PASS Stream will not error if body is empty. It's closed with an empty queue before it errors. 
+FAIL Readable stream synchronously cancels with AbortError if aborted before reading assert_true: Cancel called sync expected true got false
+PASS Signal state is cloned 
+PASS Clone aborts with original controller 
 
index e90ed02..e7fde2e 100644 (file)
@@ -1,12 +1,7 @@
-Blocked access to external URL http://www1.localhost:8800/fetch/api/resources/data.json
-CONSOLE MESSAGE: line 36: Fetch API cannot load http://www1.localhost:8800/fetch/api/resources/data.json due to access control checks.
-Blocked access to external URL http://www1.localhost:8800/fetch/api/resources/infinite-slow-response.py?stateKey=28d5c068-417e-4c81-a0cd-9b8c22aed3c1&abortKey=ef9a1b5a-7afd-4734-b145-f033788c0e6b
-CONSOLE MESSAGE: line 318: Fetch API cannot load http://www1.localhost:8800/fetch/api/resources/infinite-slow-response.py?stateKey=28d5c068-417e-4c81-a0cd-9b8c22aed3c1&abortKey=ef9a1b5a-7afd-4734-b145-f033788c0e6b due to access control checks.
+CONSOLE MESSAGE: Unhandled Promise Rejection: AbortError: Request signal is aborted
 
-Harness Error (TIMEOUT), message = null
-
-FAIL Aborting rejects with AbortError assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL Aborting rejects with AbortError - no-cors assert_throws: function "function () { throw e }" threw object "TypeError: Type error" that is not a DOMException AbortError: property "code" is equal to undefined, expected 20
+PASS Aborting rejects with AbortError 
+PASS Aborting rejects with AbortError - no-cors 
 PASS TypeError from request constructor takes priority - RequestInit's window is not null 
 PASS TypeError from request constructor takes priority - Input URL is not valid 
 PASS TypeError from request constructor takes priority - Input URL has credentials 
@@ -15,7 +10,6 @@ PASS TypeError from request constructor takes priority - RequestInit's referrer
 PASS TypeError from request constructor takes priority - RequestInit's method is invalid 
 PASS TypeError from request constructor takes priority - RequestInit's method is forbidden 
 PASS TypeError from request constructor takes priority - RequestInit's mode is no-cors and method is not simple 
-PASS TypeError from request constructor takes priority - RequestInit's mode is no-cors and integrity is not empty 
 PASS TypeError from request constructor takes priority - RequestInit's cache mode is only-if-cached and mode is not same-origin 
 PASS TypeError from request constructor takes priority - Request with cache mode: only-if-cached and fetch mode cors 
 PASS TypeError from request constructor takes priority - Request with cache mode: only-if-cached and fetch mode no-cors 
@@ -24,34 +18,34 @@ PASS TypeError from request constructor takes priority - Bad mode init parameter
 PASS TypeError from request constructor takes priority - Bad credentials init parameter value 
 PASS TypeError from request constructor takes priority - Bad cache init parameter value 
 PASS TypeError from request constructor takes priority - Bad redirect init parameter value 
-FAIL Request objects have a signal property assert_true: Signal member is present & truthy expected true got false
-FAIL Signal on request object assert_true: Signal member is present & truthy expected true got false
-FAIL Signal on request object created from request object assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL Signal on request object created from request object, with signal on second request assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL Signal on request object created from request object, with signal on second request overriding another assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL Signal retained after unrelated properties are overridden by fetch assert_unreached: Should have rejected: undefined Reached unreachable code
+PASS Request objects have a signal property 
+PASS Signal on request object 
+PASS Signal on request object created from request object 
+PASS Signal on request object created from request object, with signal on second request 
+PASS Signal on request object created from request object, with signal on second request overriding another 
+PASS Signal retained after unrelated properties are overridden by fetch 
 PASS Signal removed by setting to null 
-FAIL Already aborted signal rejects immediately assert_unreached: Fetch must not resolve Reached unreachable code
+PASS Already aborted signal rejects immediately 
 PASS Request is still 'used' if signal is aborted before fetching 
-FAIL response.arrayBuffer() rejects if already aborted assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL response.blob() rejects if already aborted assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL response.formData() rejects if already aborted assert_throws: function "function () { throw e }" threw object "NotSupportedError: The operation is not supported." that is not a DOMException AbortError: property "code" is equal to 9, expected 20
-FAIL response.json() rejects if already aborted assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL response.text() rejects if already aborted assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL Already aborted signal does not make request assert_equals: Request hasn't been made to the server expected (object) null but got (string) "open"
-FAIL Already aborted signal can be used for many fetches assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL Signal can be used to abort other fetches, even if another fetch succeeded before aborting assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL Underlying connection is closed when aborting after receiving response promise_test: Unhandled rejection with value: object "Error: Timed out"
-FAIL Underlying connection is closed when aborting after receiving response - no-cors promise_test: Unhandled rejection with value: object "TypeError: Type error"
-TIMEOUT Fetch aborted & connection closed when aborted after calling response.arrayBuffer() Test timed out
-NOTRUN Fetch aborted & connection closed when aborted after calling response.blob() 
-NOTRUN Fetch aborted & connection closed when aborted after calling response.formData() 
-NOTRUN Fetch aborted & connection closed when aborted after calling response.json() 
-NOTRUN Fetch aborted & connection closed when aborted after calling response.text() 
-NOTRUN Stream errors once aborted. Underlying connection closed. 
-NOTRUN Stream errors once aborted, after reading. Underlying connection closed. 
-NOTRUN Stream will not error if body is empty. It's closed with an empty queue before it errors. 
-NOTRUN Readable stream synchronously cancels with AbortError if aborted before reading 
-FAIL Signal state is cloned undefined is not an object (evaluating 'request.signal.aborted')
-FAIL Clone aborts with original controller undefined is not an object (evaluating 'request.signal.addEventListener')
+PASS response.arrayBuffer() rejects if already aborted 
+PASS response.blob() rejects if already aborted 
+PASS response.formData() rejects if already aborted 
+PASS response.json() rejects if already aborted 
+PASS response.text() rejects if already aborted 
+PASS Already aborted signal does not make request 
+PASS Already aborted signal can be used for many fetches 
+PASS Signal can be used to abort other fetches, even if another fetch succeeded before aborting 
+PASS Underlying connection is closed when aborting after receiving response 
+PASS Underlying connection is closed when aborting after receiving response - no-cors 
+PASS Fetch aborted & connection closed when aborted after calling response.arrayBuffer() 
+PASS Fetch aborted & connection closed when aborted after calling response.blob() 
+FAIL Fetch aborted & connection closed when aborted after calling response.formData() assert_throws: function "function () { throw e }" threw object "NotSupportedError: The operation is not supported." that is not a DOMException AbortError: property "code" is equal to 9, expected 20
+PASS Fetch aborted & connection closed when aborted after calling response.json() 
+PASS Fetch aborted & connection closed when aborted after calling response.text() 
+PASS Stream errors once aborted. Underlying connection closed. 
+PASS Stream errors once aborted, after reading. Underlying connection closed. 
+PASS Stream will not error if body is empty. It's closed with an empty queue before it errors. 
+FAIL Readable stream synchronously cancels with AbortError if aborted before reading assert_true: Cancel called sync expected true got false
+PASS Signal state is cloned 
+PASS Clone aborts with original controller 
 
index eb59797..2b3641b 100644 (file)
@@ -1,4 +1,5 @@
 // META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
 // META: script=../request/request-error.js
 
 const BODY_METHODS = ['arrayBuffer', 'blob', 'formData', 'json', 'text'];
@@ -15,6 +16,9 @@ function abortRequests() {
   );
 }
 
+const hostInfo = get_host_info();
+const urlHostname = hostInfo.REMOTE_HOST;
+
 promise_test(async t => {
   const controller = new AbortController();
   const signal = controller.signal;
@@ -31,7 +35,7 @@ promise_test(async t => {
   controller.abort();
 
   const url = new URL('../resources/data.json', location);
-  url.hostname = 'www1.' + url.hostname;
+  url.hostname = urlHostname;
 
   const fetchPromise = fetch(url, {
     signal,
@@ -314,7 +318,7 @@ promise_test(async t => {
   requestAbortKeys.push(abortKey);
 
   const url = new URL(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, location);
-  url.hostname = 'www1.' + url.hostname;
+  url.hostname = urlHostname;
 
   await fetch(url, {
     signal,
@@ -322,7 +326,7 @@ promise_test(async t => {
   });
 
   const stashTakeURL = new URL(`../resources/stash-take.py?key=${stateKey}`, location);
-  stashTakeURL.hostname = 'www1.' + stashTakeURL.hostname;
+  stashTakeURL.hostname = urlHostname;
 
   const beforeAbortResult = await fetch(stashTakeURL).then(r => r.json());
   assert_equals(beforeAbortResult, "open", "Connection is open");
@@ -440,7 +444,7 @@ promise_test(async t => {
   const controller = new AbortController();
   const signal = controller.signal;
 
-  const response = await fetch(`../resources/empty.txt`, { signal });
+  const response = await fetch(`../resources/method.py`, { signal });
 
   // Read whole response to ensure close signal has sent.
   await response.clone().text();
index 25eee9f..471bc34 100644 (file)
@@ -1,10 +1,6 @@
-Blocked access to external URL http://www1.localhost:8800/fetch/api/resources/data.json
-Blocked access to external URL http://www1.localhost:8800/fetch/api/resources/infinite-slow-response.py?stateKey=7471d98e-1bdb-4254-a315-9489c98b8f59&abortKey=4c631f17-2786-4b07-84f4-0a5760d28f1e
 
-Harness Error (TIMEOUT), message = null
-
-FAIL Aborting rejects with AbortError assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL Aborting rejects with AbortError - no-cors assert_throws: function "function () { throw e }" threw object "TypeError: Type error" that is not a DOMException AbortError: property "code" is equal to undefined, expected 20
+PASS Aborting rejects with AbortError 
+PASS Aborting rejects with AbortError - no-cors 
 PASS TypeError from request constructor takes priority - RequestInit's window is not null 
 PASS TypeError from request constructor takes priority - Input URL is not valid 
 PASS TypeError from request constructor takes priority - Input URL has credentials 
@@ -13,7 +9,6 @@ PASS TypeError from request constructor takes priority - RequestInit's referrer
 PASS TypeError from request constructor takes priority - RequestInit's method is invalid 
 PASS TypeError from request constructor takes priority - RequestInit's method is forbidden 
 PASS TypeError from request constructor takes priority - RequestInit's mode is no-cors and method is not simple 
-PASS TypeError from request constructor takes priority - RequestInit's mode is no-cors and integrity is not empty 
 PASS TypeError from request constructor takes priority - RequestInit's cache mode is only-if-cached and mode is not same-origin 
 PASS TypeError from request constructor takes priority - Request with cache mode: only-if-cached and fetch mode cors 
 PASS TypeError from request constructor takes priority - Request with cache mode: only-if-cached and fetch mode no-cors 
@@ -22,34 +17,34 @@ PASS TypeError from request constructor takes priority - Bad mode init parameter
 PASS TypeError from request constructor takes priority - Bad credentials init parameter value 
 PASS TypeError from request constructor takes priority - Bad cache init parameter value 
 PASS TypeError from request constructor takes priority - Bad redirect init parameter value 
-FAIL Request objects have a signal property assert_true: Signal member is present & truthy expected true got false
-FAIL Signal on request object assert_true: Signal member is present & truthy expected true got false
-FAIL Signal on request object created from request object assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL Signal on request object created from request object, with signal on second request assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL Signal on request object created from request object, with signal on second request overriding another assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL Signal retained after unrelated properties are overridden by fetch assert_unreached: Should have rejected: undefined Reached unreachable code
+PASS Request objects have a signal property 
+PASS Signal on request object 
+PASS Signal on request object created from request object 
+PASS Signal on request object created from request object, with signal on second request 
+PASS Signal on request object created from request object, with signal on second request overriding another 
+PASS Signal retained after unrelated properties are overridden by fetch 
 PASS Signal removed by setting to null 
-FAIL Already aborted signal rejects immediately assert_unreached: Fetch must not resolve Reached unreachable code
+PASS Already aborted signal rejects immediately 
 PASS Request is still 'used' if signal is aborted before fetching 
-FAIL response.arrayBuffer() rejects if already aborted assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL response.blob() rejects if already aborted assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL response.formData() rejects if already aborted assert_throws: function "function () { throw e }" threw object "NotSupportedError: The operation is not supported." that is not a DOMException AbortError: property "code" is equal to 9, expected 20
-FAIL response.json() rejects if already aborted assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL response.text() rejects if already aborted assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL Already aborted signal does not make request assert_equals: Request hasn't been made to the server expected (object) null but got (string) "open"
-FAIL Already aborted signal can be used for many fetches assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL Signal can be used to abort other fetches, even if another fetch succeeded before aborting assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL Underlying connection is closed when aborting after receiving response promise_test: Unhandled rejection with value: object "Error: Timed out"
-FAIL Underlying connection is closed when aborting after receiving response - no-cors promise_test: Unhandled rejection with value: object "TypeError: Type error"
-TIMEOUT Fetch aborted & connection closed when aborted after calling response.arrayBuffer() Test timed out
-NOTRUN Fetch aborted & connection closed when aborted after calling response.blob() 
-NOTRUN Fetch aborted & connection closed when aborted after calling response.formData() 
-NOTRUN Fetch aborted & connection closed when aborted after calling response.json() 
-NOTRUN Fetch aborted & connection closed when aborted after calling response.text() 
-NOTRUN Stream errors once aborted. Underlying connection closed. 
-NOTRUN Stream errors once aborted, after reading. Underlying connection closed. 
-NOTRUN Stream will not error if body is empty. It's closed with an empty queue before it errors. 
-NOTRUN Readable stream synchronously cancels with AbortError if aborted before reading 
-FAIL Signal state is cloned undefined is not an object (evaluating 'request.signal.aborted')
-FAIL Clone aborts with original controller undefined is not an object (evaluating 'request.signal.addEventListener')
+PASS response.arrayBuffer() rejects if already aborted 
+PASS response.blob() rejects if already aborted 
+PASS response.formData() rejects if already aborted 
+PASS response.json() rejects if already aborted 
+PASS response.text() rejects if already aborted 
+PASS Already aborted signal does not make request 
+PASS Already aborted signal can be used for many fetches 
+PASS Signal can be used to abort other fetches, even if another fetch succeeded before aborting 
+PASS Underlying connection is closed when aborting after receiving response 
+PASS Underlying connection is closed when aborting after receiving response - no-cors 
+PASS Fetch aborted & connection closed when aborted after calling response.arrayBuffer() 
+PASS Fetch aborted & connection closed when aborted after calling response.blob() 
+FAIL Fetch aborted & connection closed when aborted after calling response.formData() assert_throws: function "function () { throw e }" threw object "NotSupportedError: The operation is not supported." that is not a DOMException AbortError: property "code" is equal to 9, expected 20
+PASS Fetch aborted & connection closed when aborted after calling response.json() 
+PASS Fetch aborted & connection closed when aborted after calling response.text() 
+PASS Stream errors once aborted. Underlying connection closed. 
+PASS Stream errors once aborted, after reading. Underlying connection closed. 
+PASS Stream will not error if body is empty. It's closed with an empty queue before it errors. 
+FAIL Readable stream synchronously cancels with AbortError if aborted before reading assert_true: Cancel called sync expected true got false
+PASS Signal state is cloned 
+PASS Clone aborts with original controller 
 
index 114cf8a..37d8763 100644 (file)
@@ -1,9 +1,10 @@
 
-FAIL Already aborted request does not land in service worker assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL response.arrayBuffer() rejects if already aborted promise_test: Unhandled rejection with value: object "Error: wait_for_state must be passed a ServiceWorker"
-FAIL response.blob() rejects if already aborted promise_test: Unhandled rejection with value: object "Error: wait_for_state must be passed a ServiceWorker"
-FAIL response.formData() rejects if already aborted promise_test: Unhandled rejection with value: object "Error: wait_for_state must be passed a ServiceWorker"
-FAIL response.json() rejects if already aborted promise_test: Unhandled rejection with value: object "Error: wait_for_state must be passed a ServiceWorker"
-FAIL response.text() rejects if already aborted promise_test: Unhandled rejection with value: object "Error: wait_for_state must be passed a ServiceWorker"
-FAIL Stream errors once aborted. promise_test: Unhandled rejection with value: object "Error: wait_for_state must be passed a ServiceWorker"
+
+PASS Already aborted request does not land in service worker 
+PASS response.arrayBuffer() rejects if already aborted 
+PASS response.blob() rejects if already aborted 
+PASS response.formData() rejects if already aborted 
+PASS response.json() rejects if already aborted 
+PASS response.text() rejects if already aborted 
+FAIL Stream errors once aborted. assert_unreached: Should have rejected: undefined Reached unreachable code
 
index d48ac1b..8e7976d 100644 (file)
@@ -5,7 +5,7 @@ PASS Read blob response's body as readableStream
 PASS Read text response's body as readableStream 
 PASS Read URLSearchParams response's body as readableStream 
 PASS Read array buffer response's body as readableStream 
-FAIL Read form data response's body as readableStream promise_test: Unhandled rejection with value: object "TypeError: not implemented"
+FAIL Read form data response's body as readableStream promise_test: Unhandled rejection with value: object "NotSupportedError: Not implemented"
 PASS Getting an error Response stream 
 PASS Getting a redirect Response stream 
 
index 7e48be0..f535966 100644 (file)
@@ -67,6 +67,7 @@ webgl/1.0.2/conformance/uniforms/uniform-default-values.html [ Failure ]
 # This test requires Skia, which isn't available on iOS.
 webkit.org/b/174079 fast/text/variations/skia-postscript-name.html [ ImageOnlyFailure ]
 
+imported/w3c/web-platform-tests/fetch/api/abort/general-serviceworker.https.html [ Pass Failure ]
 imported/w3c/web-platform-tests/2dcontext/transformations/canvas_transformations_reset_001.html [ ImageOnlyFailure ]
 imported/w3c/web-platform-tests/html/browsers/browsing-the-web/history-traversal/persisted-user-state-restoration/scroll-restoration-fragment-scrolling-cross-origin.html [ Failure ]
 imported/w3c/web-platform-tests/webrtc/RTCPeerConnection-setLocalDescription-offer.html [ Failure ]
index 022aff3..d76382d 100644 (file)
@@ -1,3 +1,73 @@
+2019-01-04  Youenn Fablet  <youenn@apple.com>
+
+        [Fetch API] Implement abortable fetch
+        https://bugs.webkit.org/show_bug.cgi?id=174980
+        <rdar://problem/46861402>
+
+        Reviewed by Chris Dumez.
+
+        Add an AbortSignal to FetchRequest.
+
+        Add support for AbortSignal algorithm.
+        The fetch request signal is added an algorithm to abort the fetch.
+        Update clone algorithm to let signal of the cloned request be following the origin request.
+
+        Update ReadableStream error handling to return an exception instead of a string.
+        This allows passing an AbortError instead of a TypeError as previously done.
+
+        Update FetchBodyOwner to store a loading error either as an exception or as a resource error.
+        The latter is used for passing the error from service worker back to the page.
+        The former is used to pass it to ReadableStream or body accessors.
+
+        Covered by enabled tests.
+
+        * Modules/cache/DOMCache.cpp:
+        (WebCore::DOMCache::put):
+        * Modules/fetch/FetchBody.cpp:
+        (WebCore::FetchBody::consumeAsStream):
+        (WebCore::FetchBody::loadingFailed):
+        * Modules/fetch/FetchBody.h:
+        * Modules/fetch/FetchBodyConsumer.cpp:
+        (WebCore::FetchBodyConsumer::loadingFailed):
+        * Modules/fetch/FetchBodyConsumer.h:
+        * Modules/fetch/FetchBodyOwner.cpp:
+        (WebCore::FetchBodyOwner::arrayBuffer):
+        (WebCore::FetchBodyOwner::blob):
+        (WebCore::FetchBodyOwner::cloneBody):
+        (WebCore::FetchBodyOwner::formData):
+        (WebCore::FetchBodyOwner::json):
+        (WebCore::FetchBodyOwner::text):
+        (WebCore::FetchBodyOwner::loadBlob):
+        (WebCore::FetchBodyOwner::blobLoadingFailed):
+        (WebCore::FetchBodyOwner::consumeBodyAsStream):
+        (WebCore::FetchBodyOwner::setLoadingError):
+        * Modules/fetch/FetchBodyOwner.h:
+        (WebCore::FetchBodyOwner::loadingError const):
+        (WebCore::FetchBodyOwner::loadingException const):
+        * Modules/fetch/FetchBodySource.cpp:
+        (WebCore::FetchBodySource::error):
+        * Modules/fetch/FetchBodySource.h:
+        * Modules/fetch/FetchRequest.cpp:
+        (WebCore::FetchRequest::initializeWith):
+        (WebCore::FetchRequest::clone):
+        * Modules/fetch/FetchRequest.h:
+        (WebCore::FetchRequest::FetchRequest):
+        * Modules/fetch/FetchRequest.idl:
+        * Modules/fetch/FetchRequestInit.h:
+        (WebCore::FetchRequestInit::hasMembers const):
+        * Modules/fetch/FetchRequestInit.idl:
+        * Modules/fetch/FetchResponse.cpp:
+        (WebCore::FetchResponse::clone):
+        (WebCore::FetchResponse::fetch):
+        (WebCore::FetchResponse::BodyLoader::didFail):
+        * Modules/fetch/FetchResponse.h:
+        * bindings/js/ReadableStreamDefaultController.h:
+        (WebCore::ReadableStreamDefaultController::error):
+        * dom/AbortSignal.cpp:
+        (WebCore::AbortSignal::abort):
+        (WebCore::AbortSignal::follow):
+        * dom/AbortSignal.h:
+
 2019-01-04  Brent Fulgham  <bfulgham@apple.com>
 
         Parsed protocol of javascript URLs with embedded newlines and carriage returns do not match parsed protocol in Chrome and Firefox
index 953a1ad..19ba194 100644 (file)
@@ -321,8 +321,8 @@ void DOMCache::put(RequestInfo&& info, Ref<FetchResponse>&& response, DOMPromise
     }
     auto request = requestOrException.releaseReturnValue();
 
-    if (response->loadingError()) {
-        promise.reject(Exception { TypeError, response->loadingError()->localizedDescription() });
+    if (auto exception = response->loadingException()) {
+        promise.reject(*exception);
         return;
     }
 
index da6d4d3..bec22f4 100644 (file)
@@ -186,7 +186,7 @@ void FetchBody::consumeAsStream(FetchBodyOwner& owner, FetchBodySource& source)
         owner.loadBlob(blobBody(), nullptr);
         m_data = nullptr;
     } else if (isFormData())
-        source.error("not implemented"_s);
+        source.error(Exception { NotSupportedError, "Not implemented"_s });
     else if (m_consumer.hasData())
         closeStream = source.enqueue(m_consumer.takeAsArrayBuffer());
     else
@@ -224,9 +224,9 @@ void FetchBody::consumeBlob(FetchBodyOwner& owner, Ref<DeferredPromise>&& promis
     m_data = nullptr;
 }
 
-void FetchBody::loadingFailed()
+void FetchBody::loadingFailed(const Exception& exception)
 {
-    m_consumer.loadingFailed();
+    m_consumer.loadingFailed(exception);
 }
 
 void FetchBody::loadingSucceeded()
index 816c63c..a484f9d 100644 (file)
@@ -60,7 +60,7 @@ public:
 
     WEBCORE_EXPORT static Optional<FetchBody> fromFormData(FormData&);
 
-    void loadingFailed();
+    void loadingFailed(const Exception&);
     void loadingSucceeded();
 
     RefPtr<FormData> bodyAsFormData(ScriptExecutionContext&) const;
index 00f173e..302dff6 100644 (file)
@@ -212,15 +212,15 @@ void FetchBodyConsumer::setSource(Ref<FetchBodySource>&& source)
     }
 }
 
-void FetchBodyConsumer::loadingFailed()
+void FetchBodyConsumer::loadingFailed(const Exception& exception)
 {
     m_isLoading = false;
     if (m_consumePromise) {
-        m_consumePromise->reject();
+        m_consumePromise->reject(exception);
         m_consumePromise = nullptr;
     }
     if (m_source) {
-        m_source->error("Loading failed"_s);
+        m_source->error(exception);
         m_source = nullptr;
     }
 }
index b467db6..1acfe00 100644 (file)
@@ -66,7 +66,7 @@ public:
     void resolve(Ref<DeferredPromise>&&, ReadableStream*);
     void resolveWithData(Ref<DeferredPromise>&&, const unsigned char*, unsigned);
 
-    void loadingFailed();
+    void loadingFailed(const Exception&);
     void loadingSucceeded();
 
     void setConsumePromise(Ref<DeferredPromise>&&);
index 85dc88b..017a4f5 100644 (file)
@@ -99,6 +99,11 @@ bool FetchBodyOwner::isDisturbedOrLocked() const
 
 void FetchBodyOwner::arrayBuffer(Ref<DeferredPromise>&& promise)
 {
+    if (auto exception = loadingException()) {
+        promise->reject(*exception);
+        return;
+    }
+
     if (isBodyNullOrOpaque()) {
         fulfillPromiseWithArrayBuffer(WTFMove(promise), nullptr, 0);
         return;
@@ -113,6 +118,11 @@ void FetchBodyOwner::arrayBuffer(Ref<DeferredPromise>&& promise)
 
 void FetchBodyOwner::blob(Ref<DeferredPromise>&& promise)
 {
+    if (auto exception = loadingException()) {
+        promise->reject(*exception);
+        return;
+    }
+
     if (isBodyNullOrOpaque()) {
         promise->resolve<IDLInterface<Blob>>(Blob::create(Vector<uint8_t> { }, Blob::normalizedContentType(extractMIMETypeFromMediaType(m_contentType))));
         return;
@@ -127,6 +137,8 @@ void FetchBodyOwner::blob(Ref<DeferredPromise>&& promise)
 
 void FetchBodyOwner::cloneBody(FetchBodyOwner& owner)
 {
+    m_loadingError = owner.m_loadingError;
+
     m_contentType = owner.m_contentType;
     if (owner.isBodyNull())
         return;
@@ -161,6 +173,11 @@ void FetchBodyOwner::consumeOnceLoadingFinished(FetchBodyConsumer::Type type, Re
 
 void FetchBodyOwner::formData(Ref<DeferredPromise>&& promise)
 {
+    if (auto exception = loadingException()) {
+        promise->reject(*exception);
+        return;
+    }
+
     if (isBodyNullOrOpaque()) {
         promise->reject();
         return;
@@ -175,6 +192,11 @@ void FetchBodyOwner::formData(Ref<DeferredPromise>&& promise)
 
 void FetchBodyOwner::json(Ref<DeferredPromise>&& promise)
 {
+    if (auto exception = loadingException()) {
+        promise->reject(*exception);
+        return;
+    }
+
     if (isBodyNullOrOpaque()) {
         promise->reject(SyntaxError);
         return;
@@ -189,6 +211,11 @@ void FetchBodyOwner::json(Ref<DeferredPromise>&& promise)
 
 void FetchBodyOwner::text(Ref<DeferredPromise>&& promise)
 {
+    if (auto exception = loadingException()) {
+        promise->reject(*exception);
+        return;
+    }
+
     if (isBodyNullOrOpaque()) {
         promise->resolve<IDLDOMString>({ });
         return;
@@ -208,7 +235,7 @@ void FetchBodyOwner::loadBlob(const Blob& blob, FetchBodyConsumer* consumer)
     ASSERT(!isBodyNull());
 
     if (!scriptExecutionContext()) {
-        m_body->loadingFailed();
+        m_body->loadingFailed(Exception { TypeError, "Blob loading failed"_s});
         return;
     }
 
@@ -217,7 +244,7 @@ void FetchBodyOwner::loadBlob(const Blob& blob, FetchBodyConsumer* consumer)
 
     m_blobLoader->loader->start(*scriptExecutionContext(), blob);
     if (!m_blobLoader->loader->isStarted()) {
-        m_body->loadingFailed();
+        m_body->loadingFailed(Exception { TypeError, "Blob loading failed"_s});
         m_blobLoader = WTF::nullopt;
         return;
     }
@@ -251,11 +278,11 @@ void FetchBodyOwner::blobLoadingFailed()
 #if ENABLE(STREAMS_API)
     if (m_readableStreamSource) {
         if (!m_readableStreamSource->isCancelling())
-            m_readableStreamSource->error("Blob loading failed"_s);
+            m_readableStreamSource->error(Exception { TypeError, "Blob loading failed"_s});
         m_readableStreamSource = nullptr;
     } else
 #endif
-        m_body->loadingFailed();
+        m_body->loadingFailed(Exception { TypeError, "Blob loading failed"_s});
 
     finishBlobLoading();
 }
@@ -318,9 +345,8 @@ void FetchBodyOwner::consumeBodyAsStream()
 {
     ASSERT(m_readableStreamSource);
 
-    if (m_loadingError) {
-        auto errorMessage = m_loadingError->localizedDescription();
-        m_readableStreamSource->error(errorMessage.isEmpty() ? "Loading failed"_s : errorMessage);
+    if (auto exception = loadingException()) {
+        m_readableStreamSource->error(*exception);
         return;
     }
 
@@ -329,4 +355,53 @@ void FetchBodyOwner::consumeBodyAsStream()
         m_readableStreamSource = nullptr;
 }
 
+ResourceError FetchBodyOwner::loadingError() const
+{
+    return WTF::switchOn(m_loadingError, [](const ResourceError& error) {
+        return ResourceError { error };
+    }, [](const Exception& exception) {
+        return ResourceError { errorDomainWebKitInternal, 0, { }, exception.message() };
+    }, [](auto&&) {
+        return ResourceError { };
+    });
+}
+
+Optional<Exception> FetchBodyOwner::loadingException() const
+{
+    return WTF::switchOn(m_loadingError, [](const ResourceError& error) {
+        return Exception { TypeError, error.localizedDescription().isEmpty() ? "Loading failed"_s : error.localizedDescription() };
+    }, [](const Exception& exception) {
+        return Exception { exception };
+    }, [](auto&&) -> Optional<Exception> {
+        return WTF::nullopt;
+    });
+}
+
+bool FetchBodyOwner::hasLoadingError() const
+{
+    return WTF::switchOn(m_loadingError, [](const ResourceError&) {
+        return true;
+    }, [](const Exception&) {
+        return true;
+    }, [](auto&&) {
+        return false;
+    });
+}
+
+void FetchBodyOwner::setLoadingError(Exception&& exception)
+{
+    if (hasLoadingError())
+        return;
+
+    m_loadingError = WTFMove(exception);
+}
+
+void FetchBodyOwner::setLoadingError(ResourceError&& error)
+{
+    if (hasLoadingError())
+        return;
+
+    m_loadingError = WTFMove(error);
+}
+
 } // namespace WebCore
index 45cc67e..4872390 100644 (file)
@@ -66,6 +66,10 @@ public:
     virtual void cancel() { }
 #endif
 
+    bool hasLoadingError() const;
+    ResourceError loadingError() const;
+    Optional<Exception> loadingException() const;
+
 protected:
     const FetchBody& body() const { return *m_body; }
     FetchBody& body() { return *m_body; }
@@ -88,6 +92,9 @@ protected:
     void setBodyAsOpaque() { m_isBodyOpaque = true; }
     bool isBodyOpaque() const { return m_isBodyOpaque; }
 
+    void setLoadingError(Exception&&);
+    void setLoadingError(ResourceError&&);
+
 private:
     // Blob loading routines
     void blobChunk(const char*, size_t);
@@ -116,11 +123,12 @@ protected:
     RefPtr<FetchBodySource> m_readableStreamSource;
 #endif
     Ref<FetchHeaders> m_headers;
-    Optional<ResourceError> m_loadingError;
 
 private:
     Optional<BlobLoader> m_blobLoader;
     bool m_isBodyOpaque { false };
+
+    Variant<std::nullptr_t, Exception, ResourceError> m_loadingError;
 };
 
 } // namespace WebCore
index a285814..6699ef4 100644 (file)
@@ -88,7 +88,7 @@ void FetchBodySource::close()
     m_bodyOwner = nullptr;
 }
 
-void FetchBodySource::error(const String& value)
+void FetchBodySource::error(const Exception& value)
 {
     controller().error(value);
     clean();
index c10fe96..f0e61af 100644 (file)
@@ -44,7 +44,7 @@ public:
 
     bool enqueue(RefPtr<JSC::ArrayBuffer>&& chunk) { return controller().enqueue(WTFMove(chunk)); }
     void close();
-    void error(const String&);
+    void error(const Exception&);
 
     bool isCancelling() const { return m_isCancelling; }
 
index 944451b..c0790bc 100644 (file)
@@ -159,6 +159,9 @@ ExceptionOr<void> FetchRequest::initializeWith(const String& url, Init&& init)
     if (optionsResult.hasException())
         return optionsResult.releaseException();
 
+    if (init.signal && init.signal.value())
+        m_signal->follow(*init.signal.value());
+
     if (init.headers) {
         auto fillResult = m_headers->fill(*init.headers);
         if (fillResult.hasException())
@@ -188,6 +191,12 @@ ExceptionOr<void> FetchRequest::initializeWith(FetchRequest& input, Init&& init)
     if (optionsResult.hasException())
         return optionsResult.releaseException();
 
+    if (init.signal) {
+        if (init.signal.value())
+            m_signal->follow(*init.signal.value());
+    } else
+        m_signal->follow(input.m_signal);
+
     if (init.headers) {
         auto fillResult = m_headers->fill(*init.headers);
         if (fillResult.hasException())
@@ -293,6 +302,7 @@ ExceptionOr<Ref<FetchRequest>> FetchRequest::clone(ScriptExecutionContext& conte
 
     auto clone = adoptRef(*new FetchRequest(context, WTF::nullopt, FetchHeaders::create(m_headers.get()), ResourceRequest { m_request }, FetchOptions { m_options}, String { m_referrer }));
     clone->cloneBody(*this);
+    clone->m_signal->follow(m_signal);
     return WTFMove(clone);
 }
 
index ccdbf86..e85d134 100644 (file)
@@ -28,6 +28,7 @@
 
 #pragma once
 
+#include "AbortSignal.h"
 #include "ExceptionOr.h"
 #include "FetchBodyOwner.h"
 #include "FetchOptions.h"
@@ -68,6 +69,7 @@ public:
     Cache cache() const { return m_options.cache; }
     Redirect redirect() const { return m_options.redirect; }
     bool keepalive() const { return m_options.keepAlive; };
+    AbortSignal& signal() { return m_signal.get(); }
 
     const String& integrity() const { return m_options.integrity; }
 
@@ -96,6 +98,7 @@ private:
     FetchOptions m_options;
     String m_referrer;
     mutable String m_requestURL;
+    Ref<AbortSignal> m_signal;
 };
 
 inline FetchRequest::FetchRequest(ScriptExecutionContext& context, Optional<FetchBody>&& body, Ref<FetchHeaders>&& headers, ResourceRequest&& request, FetchOptions&& options, String&& referrer)
@@ -103,6 +106,7 @@ inline FetchRequest::FetchRequest(ScriptExecutionContext& context, Optional<Fetc
     , m_request(WTFMove(request))
     , m_options(WTFMove(options))
     , m_referrer(WTFMove(referrer))
+    , m_signal(AbortSignal::create(context))
 {
     updateContentType();
 }
index 80a41f8..dd2ec8f 100644 (file)
@@ -55,6 +55,7 @@ typedef (Blob or BufferSource or DOMFormData or URLSearchParams or ReadableStrea
     readonly attribute FetchRequestRedirect redirect;
     readonly attribute DOMString integrity;
     [EnabledAtRuntime=FetchAPIKeepAlive] readonly attribute boolean keepalive;
+    readonly attribute AbortSignal signal;
 
     [CallWith=ScriptExecutionContext, MayThrowException, NewObject] FetchRequest clone();
 };
index d2cb809..a8d374b 100644 (file)
@@ -25,6 +25,7 @@
 
 #pragma once
 
+#include "AbortSignal.h"
 #include "FetchBody.h"
 #include "FetchHeaders.h"
 #include "FetchOptions.h"
@@ -46,9 +47,10 @@ struct FetchRequestInit {
     Optional<FetchOptions::Redirect> redirect;
     String integrity;
     Optional<bool> keepalive;
+    Optional<AbortSignal*> signal;
     JSC::JSValue window;
 
-    bool hasMembers() const { return !method.isEmpty() || headers || body || !referrer.isEmpty() || referrerPolicy || mode || credentials || cache || redirect || !integrity.isEmpty() || keepalive || !window.isUndefined(); }
+    bool hasMembers() const { return !method.isEmpty() || headers || body || !referrer.isEmpty() || referrerPolicy || mode || credentials || cache || redirect || !integrity.isEmpty() || keepalive || !window.isUndefined() || signal; }
 };
 
 }
index d007bdd..cc7ca55 100644 (file)
@@ -39,5 +39,6 @@ dictionary FetchRequestInit {
     FetchRequestRedirect redirect;
     DOMString integrity;
     boolean keepalive;
+    AbortSignal? signal;
     any window; // can only be set to null
 };
index 61fe682..8dafc64 100644 (file)
@@ -179,24 +179,62 @@ ExceptionOr<Ref<FetchResponse>> FetchResponse::clone(ScriptExecutionContext& con
         m_internalResponse.setHTTPHeaderFields(HTTPHeaderMap { headers().internalHeaders() });
 
     auto clone = FetchResponse::create(context, WTF::nullopt, headers().guard(), ResourceResponse { m_internalResponse });
-    clone->m_loadingError = m_loadingError;
     clone->cloneBody(*this);
     clone->m_opaqueLoadIdentifier = m_opaqueLoadIdentifier;
     clone->m_bodySizeWithPadding = m_bodySizeWithPadding;
     return WTFMove(clone);
 }
 
+void FetchResponse::addAbortSteps(Ref<AbortSignal>&& signal)
+{
+    m_abortSignal = WTFMove(signal);
+    m_abortSignal->addAlgorithm([this, weakThis = makeWeakPtr(this)] {
+        // FIXME: Cancel request body if it is a stream.
+        if (!weakThis)
+            return;
+
+        m_abortSignal = nullptr;
+
+        setLoadingError(Exception { AbortError, "Fetch is aborted"_s });
+
+        if (m_bodyLoader) {
+            if (auto callback = m_bodyLoader->takeNotificationCallback())
+                callback(Exception { AbortError, "Fetch is aborted"_s });
+        }
+
+        if (m_readableStreamSource) {
+            if (!m_readableStreamSource->isCancelling())
+                m_readableStreamSource->error(*loadingException());
+            m_readableStreamSource = nullptr;
+        }
+        if (m_body)
+            m_body->loadingFailed(*loadingException());
+
+        if (m_bodyLoader) {
+            m_bodyLoader->stop();
+            m_bodyLoader = WTF::nullopt;
+        }
+    });
+}
+
 void FetchResponse::fetch(ScriptExecutionContext& context, FetchRequest& request, NotificationCallback&& responseCallback)
 {
+    if (request.signal().aborted()) {
+        responseCallback(Exception { AbortError, "Request signal is aborted"_s });
+        // FIXME: Cancel request body if it is a stream.
+        return;
+    }
+
     if (request.hasReadableStreamBody()) {
-        if (responseCallback)
-            responseCallback(Exception { NotSupportedError, "ReadableStream uploading is not supported" });
+        responseCallback(Exception { NotSupportedError, "ReadableStream uploading is not supported"_s });
         return;
     }
     auto response = adoptRef(*new FetchResponse(context, FetchBody { }, FetchHeaders::create(FetchHeaders::Guard::Immutable), { }));
 
     response->body().consumer().setAsLoading();
 
+    response->addAbortSteps(request.signal());
+
     response->m_bodyLoader.emplace(response.get(), WTFMove(responseCallback));
     if (!response->m_bodyLoader->start(context, request))
         response->m_bodyLoader = WTF::nullopt;
@@ -245,7 +283,7 @@ void FetchResponse::BodyLoader::didFail(const ResourceError& error)
 {
     ASSERT(m_response.hasPendingActivity());
 
-    m_response.m_loadingError = error;
+    m_response.setLoadingError(ResourceError { error });
 
     if (auto responseCallback = WTFMove(m_responseCallback))
         responseCallback(Exception { TypeError, error.localizedDescription() });
@@ -256,7 +294,7 @@ void FetchResponse::BodyLoader::didFail(const ResourceError& error)
 #if ENABLE(STREAMS_API)
     if (m_response.m_readableStreamSource) {
         if (!m_response.m_readableStreamSource->isCancelling())
-            m_response.m_readableStreamSource->error(makeString("Loading failed: "_s, error.localizedDescription()));
+            m_response.m_readableStreamSource->error(*m_response.loadingException());
         m_response.m_readableStreamSource = nullptr;
     }
 #endif
index 97f7d3b..9756d5c 100644 (file)
@@ -33,6 +33,7 @@
 #include "ReadableStreamSink.h"
 #include "ResourceResponse.h"
 #include <JavaScriptCore/TypedArrays.h>
+#include <wtf/WeakPtr.h>
 
 namespace JSC {
 class ExecState;
@@ -41,11 +42,12 @@ class JSValue;
 
 namespace WebCore {
 
+class AbortSignal;
 class FetchRequest;
 struct ReadableStreamChunk;
 class ReadableStreamSource;
 
-class FetchResponse final : public FetchBodyOwner {
+class FetchResponse final : public FetchBodyOwner, public CanMakeWeakPtr<FetchResponse> {
 public:
     using Type = ResourceResponse::Type;
 
@@ -107,8 +109,6 @@ public:
 
     void initializeOpaqueLoadIdentifierForTesting() { m_opaqueLoadIdentifier = 1; }
 
-    const Optional<ResourceError>& loadingError() const { return m_loadingError; }
-
     const HTTPHeaderMap& internalResponseHeaders() const { return m_internalResponse.httpHeaderFields(); }
 
 private:
@@ -124,6 +124,8 @@ private:
     void closeStream();
 #endif
 
+    void addAbortSteps(Ref<AbortSignal>&&);
+
     class BodyLoader final : public FetchLoaderClient {
     public:
         BodyLoader(FetchResponse&, NotificationCallback&&);
@@ -137,6 +139,7 @@ private:
 #if ENABLE(STREAMS_API)
         RefPtr<SharedBuffer> startStreaming();
 #endif
+        NotificationCallback takeNotificationCallback() { return WTFMove(m_responseCallback); }
 
     private:
         // FetchLoaderClient API
@@ -158,6 +161,7 @@ private:
     // Opaque responses will padd their body size when used with Cache API.
     uint64_t m_bodySizeWithPadding { 0 };
     uint64_t m_opaqueLoadIdentifier { 0 };
+    RefPtr<AbortSignal> m_abortSignal;
 };
 
 } // namespace WebCore
index 484c8d8..a7e4e7a 100644 (file)
@@ -49,8 +49,7 @@ public:
 
     bool enqueue(RefPtr<JSC::ArrayBuffer>&&);
 
-    template<class ResolveResultType>
-    void error(const ResolveResultType&);
+    void error(const Exception&);
 
     void close() { invoke(*globalObject().globalExec(), jsController(), "close", JSC::jsUndefined()); }
 
@@ -104,12 +103,11 @@ inline bool ReadableStreamDefaultController::enqueue(RefPtr<JSC::ArrayBuffer>&&
     return true;
 }
 
-template<>
-inline void ReadableStreamDefaultController::error<String>(const String& errorMessage)
+inline void ReadableStreamDefaultController::error(const Exception& exception)
 {
     JSC::ExecState& state = globalExec();
     JSC::JSLockHolder locker(&state);
-    error(state, JSC::createTypeError(&state, errorMessage));
+    error(state, createDOMException(&state, exception.code(), exception.message()));
 }
 
 } // namespace WebCore
index 512ecae..cd351fb 100644 (file)
@@ -37,7 +37,6 @@ Ref<AbortSignal> AbortSignal::create(ScriptExecutionContext& context)
     return adoptRef(*new AbortSignal(context));
 }
 
-
 AbortSignal::AbortSignal(ScriptExecutionContext& context)
     : ContextDestructionObserver(&context)
 {
@@ -52,13 +51,32 @@ void AbortSignal::abort()
     
     // 2. Set signal’s aborted flag.
     m_aborted = true;
-    
-    // 3. For each algorithm in signal's abort algorithms: run algorithm.
-    // 4. Empty signal's abort algorithms.
-    // FIXME: Add support for 'abort algorithms' - https://dom.spec.whatwg.org/#abortsignal-abort-algorithms
+
+    auto protectedThis = makeRef(*this);
+    auto algorithms = WTFMove(m_algorithms);
+    for (auto& algorithm : algorithms)
+        algorithm();
 
     // 5. Fire an event named abort at signal.
     dispatchEvent(Event::create(eventNames().abortEvent, Event::CanBubble::No, Event::IsCancelable::No));
 }
 
+// https://dom.spec.whatwg.org/#abortsignal-follow
+void AbortSignal::follow(AbortSignal& signal)
+{
+    if (aborted())
+        return;
+
+    if (signal.aborted()) {
+        abort();
+        return;
+    }
+
+    signal.addAlgorithm([weakThis = makeWeakPtr(this)] {
+        if (!weakThis)
+            return;
+        weakThis->abort();
+    });
+}
+
 }
index d3140ac..a89be55 100644 (file)
 
 #include "ContextDestructionObserver.h"
 #include "EventTarget.h"
+#include <wtf/Function.h>
 #include <wtf/Ref.h>
 #include <wtf/RefCounted.h>
+#include <wtf/WeakPtr.h>
 
 namespace WebCore {
 
 class ScriptExecutionContext;
 
-class AbortSignal final : public RefCounted<AbortSignal>, public EventTargetWithInlineData, private ContextDestructionObserver {
+class AbortSignal final : public RefCounted<AbortSignal>, public EventTargetWithInlineData, public CanMakeWeakPtr<AbortSignal>, private ContextDestructionObserver {
 public:
     static Ref<AbortSignal> create(ScriptExecutionContext&);
 
@@ -45,6 +47,11 @@ public:
     using RefCounted::ref;
     using RefCounted::deref;
 
+    using Algorithm = WTF::Function<void()>;
+    void addAlgorithm(Algorithm&& algorithm) { m_algorithms.append(WTFMove(algorithm)); }
+
+    void follow(AbortSignal&);
+
 private:
     explicit AbortSignal(ScriptExecutionContext&);
 
@@ -54,6 +61,7 @@ private:
     void derefEventTarget() final { deref(); }
     
     bool m_aborted { false };
+    Vector<Algorithm> m_algorithms;
 };
 
 }
index 0e36492..f62959d 100644 (file)
@@ -53,8 +53,9 @@ static void processResponse(Ref<Client>&& client, Expected<Ref<FetchResponse>, R
 
     client->didReceiveResponse(response->resourceResponse());
 
-    if (response->loadingError()) {
-        client->didFail(*response->loadingError());
+    auto loadingError = response->loadingError();
+    if (!loadingError.isNull()) {
+        client->didFail(loadingError);
         return;
     }