Support a crossorigin= attribute for webundle subresource loading
There are several changes in this CL. Let me summarize at first.
The current behavior is:
- No support crossorigin= attribute
- request's mode is set to "cors" in default.
- request's creditial-mode is "omit"; UA never sends a credential.
This CL changes the behavior as follows:
- If crossorigin= attribute is not specified,
- request's mode is set to "no-cors"
- request's credential-mode is set to "include"
- If crossorigin= attribute is "anonymous" or empty (See [1] for details)
- request's mode is set to "cors"
- request's credential-mode is set to "same-origin"
- If crossorigin= attribute is "use-credential",
- request's mode is set to "cors"
- request's credential-mode is set to "include"
Note: In the current implementation, subresources can be loaded from a
cross-origin bundle which the server returns without a valid
"Access-Control-Allow-Origin" response header. That should be considered
as a bug. This Cl changes request's mode to no-cors correctly in
default.
Regarding tests,
A general behavior of a crossorigin attribute is well tested in existing
tests, such as fast/dom/HTMLLinkElement/link-crossOrigin.htm.
This CL tests only the effects of crossorign= attributes for a bundle
as follows:
1. Test for credentials:
subresource-loading-credential.tentative.sub.html
2. Test for CORS behavior:
subresource-loading-cors.tentative.html
3. Test for CORS behavior (error case):
subresource-loading-cors-error.tentative.html
The existing test, subresource-loading-cross-origin.tentative.html is
rewritten and split into 2 and 3.
Due to https://crbug.com/1168449, the test 3 is marked as TIMEOUT.
This bug will be fixed as another CL.
[1]: https://html.spec.whatwg.org/multipage/#cors-settings-attribute
BUG=1149816, 1168449
Change-Id: I354e203a3a536bc829ef7ba6a97d4edd6a6340a0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2670486
Reviewed-by: Tsuyoshi Horo <horo@chromium.org>
Reviewed-by: Kinuko Yasuda <kinuko@chromium.org>
Reviewed-by: Kunihiko Sakamoto <ksakamoto@chromium.org>
Commit-Queue: Hayato Ito <hayato@chromium.org>
Cr-Commit-Position: refs/heads/master@{#853942}
diff --git a/web-bundle/resources/cross-origin-no-cors.har b/web-bundle/resources/cross-origin-no-cors.har
index 96d0b4e..983024b 100644
--- a/web-bundle/resources/cross-origin-no-cors.har
+++ b/web-bundle/resources/cross-origin-no-cors.har
@@ -4,7 +4,7 @@
{
"request": {
"method": "GET",
- "url": "https://web-platform.test:8444/web-bundle/resources/wbn/no-cors/resource.cors.json",
+ "url": "https://web-platform.test:8444/web-bundle/resources/wbn/no-cors/resource.cors.js",
"headers": []
},
"response": {
@@ -12,7 +12,7 @@
"headers": [
{
"name": "Content-type",
- "value": "application/json"
+ "value": "text/javascript"
},
{
"name": "Access-Control-Allow-Origin",
@@ -20,14 +20,14 @@
}
],
"content": {
- "text": "{ cors: 1 }"
+ "text": "scriptLoaded('resource.cors.js');"
}
}
},
{
"request": {
"method": "GET",
- "url": "https://web-platform.test:8444/web-bundle/resources/wbn/no-cors/resource.no-cors.json",
+ "url": "https://web-platform.test:8444/web-bundle/resources/wbn/no-cors/resource.no-cors.js",
"headers": []
},
"response": {
@@ -35,11 +35,11 @@
"headers": [
{
"name": "Content-type",
- "value": "application/json"
+ "value": "text/javascript"
}
],
"content": {
- "text": "{ no_cors: 1 }"
+ "text": "scriptLoaded('resource.no-cors.js');"
}
}
}
diff --git a/web-bundle/resources/cross-origin.har b/web-bundle/resources/cross-origin.har
index 7435393..2197dea 100644
--- a/web-bundle/resources/cross-origin.har
+++ b/web-bundle/resources/cross-origin.har
@@ -4,7 +4,7 @@
{
"request": {
"method": "GET",
- "url": "https://web-platform.test:8444/web-bundle/resources/wbn/cors/resource.cors.json",
+ "url": "https://web-platform.test:8444/web-bundle/resources/wbn/cors/resource.cors.js",
"headers": []
},
"response": {
@@ -12,7 +12,7 @@
"headers": [
{
"name": "Content-type",
- "value": "application/json"
+ "value": "text/javascript"
},
{
"name": "Access-Control-Allow-Origin",
@@ -20,14 +20,14 @@
}
],
"content": {
- "text": "{ cors: 1 }"
+ "text": "scriptLoaded('resource.cors.js');"
}
}
},
{
"request": {
"method": "GET",
- "url": "https://web-platform.test:8444/web-bundle/resources/wbn/cors/resource.no-cors.json",
+ "url": "https://web-platform.test:8444/web-bundle/resources/wbn/cors/resource.no-cors.js",
"headers": []
},
"response": {
@@ -35,11 +35,11 @@
"headers": [
{
"name": "Content-type",
- "value": "application/json"
+ "value": "text/javascript"
}
],
"content": {
- "text": "{ no_cors: 1}"
+ "text": "scriptLoaded('resource.no-cors.js');"
}
}
}
diff --git a/web-bundle/resources/generate-test-wbns.sh b/web-bundle/resources/generate-test-wbns.sh
index 5a205c9..36a8078 100755
--- a/web-bundle/resources/generate-test-wbns.sh
+++ b/web-bundle/resources/generate-test-wbns.sh
@@ -81,11 +81,11 @@
gen-bundle \
-version b1 \
-har cross-origin.har \
- -primaryURL $wpt_test_https_origin/web-bundle/resources/wbn/cors/resource.cors.json \
+ -primaryURL $wpt_test_https_origin/web-bundle/resources/wbn/cors/resource.cors.js \
-o wbn/cors/cross-origin.wbn
gen-bundle \
-version b1 \
-har cross-origin-no-cors.har \
- -primaryURL $wpt_test_https_origin/web-bundle/resources/wbn/no-cors/resource.cors.json \
+ -primaryURL $wpt_test_https_origin/web-bundle/resources/wbn/no-cors/resource.cors.js \
-o wbn/no-cors/cross-origin.wbn
diff --git a/web-bundle/resources/test-helpers.js b/web-bundle/resources/test-helpers.js
new file mode 100644
index 0000000..d4f420e
--- /dev/null
+++ b/web-bundle/resources/test-helpers.js
@@ -0,0 +1,73 @@
+// Helper functions used in web-bundle tests.
+
+function addElementAndWaitForLoad(element) {
+ return new Promise((resolve, reject) => {
+ element.onload = resolve;
+ element.onerror = reject;
+ document.body.appendChild(element);
+ });
+}
+
+function addElementAndWaitForError(element) {
+ return new Promise((resolve, reject) => {
+ element.onload = reject;
+ element.onerror = resolve;
+ document.body.appendChild(element);
+ });
+}
+
+function fetchAndWaitForReject(url) {
+ return new Promise((resolve, reject) => {
+ fetch(url)
+ .then(() => {
+ reject();
+ })
+ .catch(() => {
+ resolve();
+ });
+ });
+}
+
+function addLinkAndWaitForLoad(url, resources, crossorigin) {
+ return new Promise((resolve, reject) => {
+ const link = document.createElement("link");
+ link.rel = "webbundle";
+ link.href = url;
+ if (crossorigin) {
+ link.crossOrigin = crossorigin;
+ }
+ for (const resource of resources) {
+ link.resources.add(resource);
+ }
+ link.onload = () => resolve(link);
+ link.onerror = () => reject(link);
+ document.body.appendChild(link);
+ });
+}
+
+function addLinkAndWaitForError(url, resources, crossorigin) {
+ return new Promise((resolve, reject) => {
+ const link = document.createElement("link");
+ link.rel = "webbundle";
+ link.href = url;
+ if (crossorigin) {
+ link.crossOrigin = crossorigin;
+ }
+ for (const resource of resources) {
+ link.resources.add(resource);
+ }
+ link.onload = () => reject(link);
+ link.onerror = () => resolve(link);
+ document.body.appendChild(link);
+ });
+}
+
+function addScriptAndWaitForError(url) {
+ return new Promise((resolve, reject) => {
+ const script = document.createElement("script");
+ script.src = url;
+ script.onload = reject;
+ script.onerror = resolve;
+ document.body.appendChild(script);
+ });
+}
diff --git a/web-bundle/resources/wbn/cors/cross-origin.wbn b/web-bundle/resources/wbn/cors/cross-origin.wbn
index bed9bf2..8043ba7 100644
--- a/web-bundle/resources/wbn/cors/cross-origin.wbn
+++ b/web-bundle/resources/wbn/cors/cross-origin.wbn
Binary files differ
diff --git a/web-bundle/resources/wbn/no-cors/cross-origin.wbn b/web-bundle/resources/wbn/no-cors/cross-origin.wbn
index b2adba6..7004673 100644
--- a/web-bundle/resources/wbn/no-cors/cross-origin.wbn
+++ b/web-bundle/resources/wbn/no-cors/cross-origin.wbn
Binary files differ
diff --git a/web-bundle/subresource-loading/check-cookie-and-return-bundle.py b/web-bundle/subresource-loading/check-cookie-and-return-bundle.py
new file mode 100644
index 0000000..32765e8
--- /dev/null
+++ b/web-bundle/subresource-loading/check-cookie-and-return-bundle.py
@@ -0,0 +1,25 @@
+import os
+
+
+def main(request, response):
+ origin = request.headers.get(b"origin")
+
+ if origin is not None:
+ response.headers.set(b"Access-Control-Allow-Origin", origin)
+ response.headers.set(b"Access-Control-Allow-Methods", b"GET")
+ response.headers.set(b"Access-Control-Allow-Credentials", b"true")
+
+ headers = [
+ (b"Content-Type", b"application/webbundle"),
+ (b"X-Content-Type-Options", b"nosniff"),
+ ]
+
+ cookie = request.cookies.first(b"milk", None)
+ if (cookie is not None) and cookie.value == b"1":
+ with open(
+ os.path.join(os.path.dirname(__file__), "../resources/wbn/subresource.wbn"),
+ "rb",
+ ) as f:
+ return (200, headers, f.read())
+ else:
+ return (400, [], "")
diff --git a/web-bundle/subresource-loading/subresource-loading-cors-error.tentative.html b/web-bundle/subresource-loading/subresource-loading-cors-error.tentative.html
new file mode 100644
index 0000000..31ad66e
--- /dev/null
+++ b/web-bundle/subresource-loading/subresource-loading-cors-error.tentative.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<title>Cross origin WebBundle subresource loading (error case)</title>
+<link
+ rel="help"
+ href="https://github.com/WICG/webpackage/blob/master/explainers/subresource-loading.md"
+/>
+<link
+ rel="help"
+ href="https://html.spec.whatwg.org/multipage/#cors-settings-attribute"
+/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.js"></script>
+<body>
+ <!--
+ This wpt should run on an origin different from https://web-platform.test:8444/,
+ from where cross-orign WebBundles are served.
+
+ This test uses a cross-origin WebBundle,
+ https://web-platform.test:8444/web-bundle/resources/wbn/no-cors/cross-origin.wbn,
+ which is served *without* an Access-Control-Allow-Origin response header.
+
+ `cross-origin.wbn` includes two subresources:
+ a. `resource.cors.js`, which includes an Access-Control-Allow-Origin response header.
+ b. `resource.no-cors.js`, which doesn't include an Access-Control-Allow-Origin response header.
+ -->
+ <script>
+ promise_test(async () => {
+ const prefix =
+ "https://web-platform.test:8444/web-bundle/resources/wbn/no-cors/";
+ const resources = [
+ prefix + "resource.cors.js",
+ prefix + "resource.no-cors.js",
+ ];
+ for (const crossorigin_attribute_value of [
+ "anonymous",
+ "use-credential",
+ ]) {
+ const link = await addLinkAndWaitForError(
+ prefix + "cross-origin.wbn",
+ resources,
+ crossorigin_attribute_value
+ );
+
+ // A subresource in the bundle can not be used in any case.
+ for (const resource of resources) {
+ await fetchAndWaitForReject(resource);
+ await addScriptAndWaitForError(resource);
+ }
+ link.remove();
+ }
+ }, "Use CORS if crossorigin=anonymous or crossorigin=use-credential is specified. A cross origin bundle must not be loaded unless a server returns a valid Access-Control-Allow-Origin header.");
+ </script>
+</body>
diff --git a/web-bundle/subresource-loading/subresource-loading-cors.tentative.html b/web-bundle/subresource-loading/subresource-loading-cors.tentative.html
new file mode 100644
index 0000000..b1b2595
--- /dev/null
+++ b/web-bundle/subresource-loading/subresource-loading-cors.tentative.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<title>Cross origin WebBundle subresource loading</title>
+<link
+ rel="help"
+ href="https://github.com/WICG/webpackage/blob/master/explainers/subresource-loading.md"
+/>
+<link
+ rel="help"
+ href="https://html.spec.whatwg.org/multipage/#cors-settings-attribute"
+/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.js"></script>
+<body>
+ <!--
+ This wpt should run on an origin different from https://web-platform.test:8444/,
+ from where cross-orign WebBundles are served.
+
+ This test uses the two cross-origin WebBundles:
+
+ 1. https://web-platform.test:8444/web-bundle/resources/wbn/cors/cross-origin.wbn,
+ which is served with an Access-Control-Allow-Origin response header.
+ 2. https://web-platform.test:8444/web-bundle/resources/wbn/no-cors/cross-origin.wbn,
+ which is served *without* an Access-Control-Allow-Origin response header.
+
+ Each `cross-origin.wbn` includes two subresources:
+ a. `resource.cors.json`, which includes an Access-Control-Allow-Origin response header.
+ b. `resource.no-cors.json`, which doesn't include an Access-Control-Allow-Origin response header.
+ -->
+ <script>
+ promise_test(async () => {
+ for (const prefix of [
+ "https://web-platform.test:8444/web-bundle/resources/wbn/cors/",
+ "https://web-platform.test:8444/web-bundle/resources/wbn/no-cors/",
+ ]) {
+ const resources = [
+ prefix + "resource.cors.js",
+ prefix + "resource.no-cors.js",
+ ];
+ const link = await addLinkAndWaitForLoad(
+ prefix + "cross-origin.wbn",
+ resources
+ );
+
+ // Can fetch a subresource which has a valid Access-Control-Allow-Origin response header.
+ const response = await fetch(prefix + "resource.cors.js");
+ assert_true(response.ok);
+ const text = await response.text();
+ assert_equals(text, "scriptLoaded('resource.cors.js');");
+
+ // Can not fetch a subresource which does NOT have a valid
+ // Access-Control-Allow-Origin response header.
+ await fetchAndWaitForReject(prefix + "resource.no-cors.js");
+
+ // Both subresource js can be loaded via a <script> element, which doesn't use cors.
+ for (const resource of resources) {
+ const scriptEvaluted = new Promise((resolve, reject) => {
+ window.scriptLoaded = resolve;
+ });
+ const script = document.createElement("script");
+ script.src = resource;
+ document.body.appendChild(script);
+ await scriptEvaluted;
+ }
+ link.remove();
+ }
+ }, "Use no-cors if crossorigin=attribute is not specified");
+
+ promise_test(async () => {
+ const prefix =
+ "https://web-platform.test:8444/web-bundle/resources/wbn/cors/";
+ const resources = [
+ prefix + "resource.cors.js",
+ prefix + "resource.no-cors.js",
+ ];
+ for (const crossorigin_attribute_value of [
+ "anonymous",
+ "use-credential",
+ ]) {
+ const link = await addLinkAndWaitForLoad(
+ prefix + "cross-origin.wbn",
+ resources,
+ crossorigin_attribute_value
+ );
+
+ // Can fetch a subresource which has a valid Access-Control-Allow-Origin response header.
+ const response = await fetch(prefix + "resource.cors.js");
+ assert_true(response.ok);
+ const text = await response.text();
+ assert_equals(text, "scriptLoaded('resource.cors.js');");
+
+ // Can not fetch a subresource which does NOT have a valid
+ // Access-Control-Allow-Origin response header.
+ await fetchAndWaitForReject(prefix + "resource.no-cors.js");
+
+ // Both subresource js can be loaded via a <script> element, which doesn't use cors.
+ for (const resource of resources) {
+ const scriptEvaluted = new Promise((resolve, reject) => {
+ window.scriptLoaded = resolve;
+ });
+ const script = document.createElement("script");
+ script.src = resource;
+ document.body.appendChild(script);
+ await scriptEvaluted;
+ }
+ link.remove();
+ }
+ }, "Use CORS if crossorigin=anonymous or crossorigin=use-credential is specified. A server should return a valid Access-Control-Allow-Origin header if a bundle is a cross origin bundle.");
+ </script>
+</body>
diff --git a/web-bundle/subresource-loading/subresource-loading-credential.tentative.sub.html b/web-bundle/subresource-loading/subresource-loading-credential.tentative.sub.html
new file mode 100644
index 0000000..d7d8e52
--- /dev/null
+++ b/web-bundle/subresource-loading/subresource-loading-credential.tentative.sub.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<title>
+ crossorigin= attribute and credentials in WebBundle subresource loading
+</title>
+<link
+ rel="help"
+ href="https://github.com/WICG/webpackage/blob/master/explainers/subresource-loading.md"
+/>
+<link
+ rel="help"
+ href="https://html.spec.whatwg.org/multipage/#cors-settings-attribute"
+/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/test-helpers.js"></script>
+<body>
+ <script>
+ // In this wpt, we only test request's credential mode, which controls
+ // whether UA sends a credential or not.
+ // We assume that a <link> element fires a load event correctly if
+ // check-cookie-and-return-bundle.py returns a valid format webbundle. That
+ // happens only when UA sends a credential. We don't care of the contents of
+ // a bundle. That's out of scope of this wpt.
+
+ // See subresoruce-loading-cors{-error}.tentative.html, where we test subresource
+ // loading with crossorigin= attribute, in terms of request's mode (cors or no-cors).
+
+ document.cookie = "milk=1";
+
+ // Make sure to set a cookie for a cross-origin domain from where a cross
+ // origin bundle is served.
+ const setCookiePromise = fetch(
+ "http://{{domains[www2]}}:{{ports[http][0]}}/cookies/resources/set-cookie.py?name=milk&path=/web-bundle/subresource-loading/",
+ {
+ mode: "no-cors",
+ credentials: "include",
+ }
+ );
+
+ const same_origin_bundle = "./check-cookie-and-return-bundle.py";
+ const cross_origin_bundle = "http://{{domains[www2]}}:{{ports[http][0]}}/web-bundle/subresource-loading/check-cookie-and-return-bundle.py";
+
+ promise_test(async () => {
+ const link = document.createElement("link");
+ link.rel = "webbundle";
+ link.href = same_origin_bundle;
+ await addElementAndWaitForLoad(link);
+ link.remove()
+ }, "'no crossorigin attribute' should send a credential to a same origin bundle");
+
+ promise_test(async () => {
+ await setCookiePromise;
+ const link = document.createElement("link");
+ link.rel = "webbundle";
+ link.href = cross_origin_bundle;
+ await addElementAndWaitForLoad(link);
+ link.remove()
+ }, "'no crossorigin attribute' should send a credential to a cross origin bundle");
+
+ promise_test(async () => {
+ const link = document.createElement("link");
+ link.rel = "webbundle";
+ link.href = same_origin_bundle;
+ link.crossOrigin = "anonymous";
+ await addElementAndWaitForLoad(link);
+ link.remove()
+ }, "'anonymous' should send a credential to a same origin bundle");
+
+ promise_test(async () => {
+ await setCookiePromise;
+ const link = document.createElement("link");
+ link.rel = "webbundle";
+ link.href = cross_origin_bundle;
+ link.crossOrigin = "anonymous";
+ await addElementAndWaitForError(link);
+ link.remove()
+ }, "'anonymous' should not send a credential to a cross origin bundle");
+
+ promise_test(async () => {
+ const link = document.createElement("link");
+ link.rel = "webbundle";
+ link.href = same_origin_bundle;
+ link.crossOrigin = "use-credentials";
+ await addElementAndWaitForLoad(link);
+ link.remove()
+ }, "'use-credentials' should send a credential to a same origin bundle");
+
+ promise_test(async () => {
+ await setCookiePromise;
+ const link = document.createElement("link");
+ link.rel = "webbundle";
+ link.href = cross_origin_bundle;
+ link.crossOrigin = "use-credentials";
+ await addElementAndWaitForLoad(link);
+ link.remove()
+ }, "'use-credentials' should send a credential to a cross origin bundle");
+ </script>
+</body>
diff --git a/web-bundle/subresource-loading/subresource-loading-cross-origin.tentative.html b/web-bundle/subresource-loading/subresource-loading-cross-origin.tentative.html
deleted file mode 100644
index b0072dd..0000000
--- a/web-bundle/subresource-loading/subresource-loading-cross-origin.tentative.html
+++ /dev/null
@@ -1,93 +0,0 @@
-<!DOCTYPE html>
-<title>Cross-origin WebBundle subresource loading</title>
-<link
- rel="help"
- href="https://github.com/WICG/webpackage/blob/master/explainers/subresource-loading.md"
-/>
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<body>
- <!--
- This wpt should run on an origin different from https://web-platform.test:8444/,
- from where cross-orign WebBundles are served.
-
- This test uses the two cross-origin WebBundles:
-
- 1. https://web-platform.test:8444/web-bundle/resources/wbn/cors/cross-origin.wbn,
- which is served with an Access-Control-Allow-Origin response header.
- 2. http://web-platform.test:8444/web-bundle/resources/wbn/no-cors/cross-origin.wbn,
- which is served *without* an Access-Control-Allow-Origin response header.
-
- Each `cross-origin.wbn` includes two subresources:
- a. `resource.cors.json`, which includes an Access-Control-Allow-Origin response header.
- b. `resource.no-cors.json`, which doesn't include an Access-Control-Allow-Origin response header.
- -->
- <link
- rel="webbundle"
- href="https://web-platform.test:8444/web-bundle/resources/wbn/cors/cross-origin.wbn"
- resources="https://web-platform.test:8444/web-bundle/resources/wbn/cors/resource.cors.json
- https://web-platform.test:8444/web-bundle/resources/wbn/cors/resource.no-cors.json"
- />
- <script>
- promise_test(async () => {
- const response = await fetch(
- "https://web-platform.test:8444/web-bundle/resources/wbn/cors/resource.cors.json"
- );
- assert_true(response.ok);
- const text = await response.text();
- assert_equals(text, "{ cors: 1 }");
- }, "A subresource which includes an Access-Control-Allow-Origin response header can be fetched");
-
- promise_test(async (t) => {
- return promise_rejects_js(
- t,
- TypeError,
- fetch(
- "https://web-platform.test:8444/web-bundle/resources/wbn/cors/resource.no-cors.json"
- )
- );
- }, "A subresource which does not include an Access-Control-Allow-Origin response header can not be fetched");
-
- promise_test(async (t) => {
- const prefix =
- "http://web-platform.test:8444/web-bundle/resources/wbn/no-cors/";
- const resources = [
- prefix + "resource.cors.json",
- prefix + "resource.no-cors.json",
- ]
- // Should fire an error event on loading webbundle.
- await addLinkAndWaitForError(prefix + "cross-origin.wbn", resources);
- // A fetch should fail for any subresource specified in resources attribute.
- for (const url of resources) {
- await fetchAndWaitForReject(url);
- }
- }, "A cross-origin WebBundle which does not include an Access-Control-Allow-Origin response header should fire an error event on load, and a fetch should fail for any subresource");
-
- function addLinkAndWaitForError(url, resources) {
- return new Promise((resolve, reject) => {
- const link = document.createElement("link");
- link.rel = "webbundle";
- link.href = url;
- for (const resource of resources) {
- link.resources.add(resource);
- }
- link.onload = reject;
- link.onerror = () => resolve(link);
- document.body.appendChild(link);
- });
- }
-
- function fetchAndWaitForReject(url) {
- return new Promise((resolve, reject) => {
- fetch(url)
- .then(() => {
- reject();
- })
- .catch(() => {
- resolve();
- });
- });
- }
-
- </script>
-</body>