summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/fetch/api
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/fetch/api
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/fetch/api')
-rw-r--r--testing/web-platform/tests/fetch/api/abort/cache.https.any.js47
-rw-r--r--testing/web-platform/tests/fetch/api/abort/destroyed-context.html27
-rw-r--r--testing/web-platform/tests/fetch/api/abort/general.any.js572
-rw-r--r--testing/web-platform/tests/fetch/api/abort/keepalive.html85
-rw-r--r--testing/web-platform/tests/fetch/api/abort/request.any.js85
-rw-r--r--testing/web-platform/tests/fetch/api/abort/serviceworker-intercepted.https.html212
-rw-r--r--testing/web-platform/tests/fetch/api/basic/accept-header.any.js34
-rw-r--r--testing/web-platform/tests/fetch/api/basic/block-mime-as-script.html43
-rw-r--r--testing/web-platform/tests/fetch/api/basic/conditional-get.any.js38
-rw-r--r--testing/web-platform/tests/fetch/api/basic/error-after-response.any.js24
-rw-r--r--testing/web-platform/tests/fetch/api/basic/header-value-combining.any.js15
-rw-r--r--testing/web-platform/tests/fetch/api/basic/header-value-null-byte.any.js5
-rw-r--r--testing/web-platform/tests/fetch/api/basic/historical.any.js17
-rw-r--r--testing/web-platform/tests/fetch/api/basic/http-response-code.any.js14
-rw-r--r--testing/web-platform/tests/fetch/api/basic/integrity.sub.any.js87
-rw-r--r--testing/web-platform/tests/fetch/api/basic/keepalive.any.js42
-rw-r--r--testing/web-platform/tests/fetch/api/basic/mediasource.window.js5
-rw-r--r--testing/web-platform/tests/fetch/api/basic/mode-no-cors.sub.any.js29
-rw-r--r--testing/web-platform/tests/fetch/api/basic/mode-same-origin.any.js28
-rw-r--r--testing/web-platform/tests/fetch/api/basic/referrer.any.js29
-rw-r--r--testing/web-platform/tests/fetch/api/basic/request-forbidden-headers.any.js82
-rw-r--r--testing/web-platform/tests/fetch/api/basic/request-head.any.js6
-rw-r--r--testing/web-platform/tests/fetch/api/basic/request-headers-case.any.js13
-rw-r--r--testing/web-platform/tests/fetch/api/basic/request-headers-nonascii.any.js29
-rw-r--r--testing/web-platform/tests/fetch/api/basic/request-headers.any.js82
-rw-r--r--testing/web-platform/tests/fetch/api/basic/request-private-network-headers.tentative.any.js18
-rw-r--r--testing/web-platform/tests/fetch/api/basic/request-referrer-redirected-worker.html17
-rw-r--r--testing/web-platform/tests/fetch/api/basic/request-referrer.any.js24
-rw-r--r--testing/web-platform/tests/fetch/api/basic/request-upload.any.js135
-rw-r--r--testing/web-platform/tests/fetch/api/basic/request-upload.h2.any.js186
-rw-r--r--testing/web-platform/tests/fetch/api/basic/response-null-body.any.js38
-rw-r--r--testing/web-platform/tests/fetch/api/basic/response-url.sub.any.js16
-rw-r--r--testing/web-platform/tests/fetch/api/basic/scheme-about.any.js26
-rw-r--r--testing/web-platform/tests/fetch/api/basic/scheme-blob.sub.any.js125
-rw-r--r--testing/web-platform/tests/fetch/api/basic/scheme-data.any.js43
-rw-r--r--testing/web-platform/tests/fetch/api/basic/scheme-others.sub.any.js31
-rw-r--r--testing/web-platform/tests/fetch/api/basic/status.h2.any.js17
-rw-r--r--testing/web-platform/tests/fetch/api/basic/stream-response.any.js40
-rw-r--r--testing/web-platform/tests/fetch/api/basic/stream-safe-creation.any.js54
-rw-r--r--testing/web-platform/tests/fetch/api/basic/text-utf8.any.js74
-rw-r--r--testing/web-platform/tests/fetch/api/basic/url-parsing.sub.html33
-rw-r--r--testing/web-platform/tests/fetch/api/body/cloned-any.js50
-rw-r--r--testing/web-platform/tests/fetch/api/body/formdata.any.js14
-rw-r--r--testing/web-platform/tests/fetch/api/body/mime-type.any.js127
-rw-r--r--testing/web-platform/tests/fetch/api/cors/cors-basic.any.js43
-rw-r--r--testing/web-platform/tests/fetch/api/cors/cors-cookies-redirect.any.js49
-rw-r--r--testing/web-platform/tests/fetch/api/cors/cors-cookies.any.js56
-rw-r--r--testing/web-platform/tests/fetch/api/cors/cors-expose-star.sub.any.js41
-rw-r--r--testing/web-platform/tests/fetch/api/cors/cors-filtering.sub.any.js69
-rw-r--r--testing/web-platform/tests/fetch/api/cors/cors-keepalive.any.js116
-rw-r--r--testing/web-platform/tests/fetch/api/cors/cors-multiple-origins.sub.any.js22
-rw-r--r--testing/web-platform/tests/fetch/api/cors/cors-no-preflight.any.js41
-rw-r--r--testing/web-platform/tests/fetch/api/cors/cors-origin.any.js51
-rw-r--r--testing/web-platform/tests/fetch/api/cors/cors-preflight-cache.any.js46
-rw-r--r--testing/web-platform/tests/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js19
-rw-r--r--testing/web-platform/tests/fetch/api/cors/cors-preflight-redirect.any.js37
-rw-r--r--testing/web-platform/tests/fetch/api/cors/cors-preflight-referrer.any.js51
-rw-r--r--testing/web-platform/tests/fetch/api/cors/cors-preflight-response-validation.any.js33
-rw-r--r--testing/web-platform/tests/fetch/api/cors/cors-preflight-star.any.js86
-rw-r--r--testing/web-platform/tests/fetch/api/cors/cors-preflight-status.any.js37
-rw-r--r--testing/web-platform/tests/fetch/api/cors/cors-preflight.any.js62
-rw-r--r--testing/web-platform/tests/fetch/api/cors/cors-redirect-credentials.any.js52
-rw-r--r--testing/web-platform/tests/fetch/api/cors/cors-redirect-preflight.any.js46
-rw-r--r--testing/web-platform/tests/fetch/api/cors/cors-redirect.any.js42
-rw-r--r--testing/web-platform/tests/fetch/api/cors/data-url-iframe.html58
-rw-r--r--testing/web-platform/tests/fetch/api/cors/data-url-shared-worker.html53
-rw-r--r--testing/web-platform/tests/fetch/api/cors/data-url-worker.html50
-rw-r--r--testing/web-platform/tests/fetch/api/cors/resources/corspreflight.js58
-rw-r--r--testing/web-platform/tests/fetch/api/cors/resources/not-cors-safelisted.json13
-rw-r--r--testing/web-platform/tests/fetch/api/cors/sandboxed-iframe.html14
-rw-r--r--testing/web-platform/tests/fetch/api/crashtests/aborted-fetch-response.https.html11
-rw-r--r--testing/web-platform/tests/fetch/api/crashtests/body-window-destroy.html11
-rw-r--r--testing/web-platform/tests/fetch/api/crashtests/request.html8
-rw-r--r--testing/web-platform/tests/fetch/api/credentials/authentication-basic.any.js17
-rw-r--r--testing/web-platform/tests/fetch/api/credentials/authentication-redirection.any.js29
-rw-r--r--testing/web-platform/tests/fetch/api/credentials/cookies.any.js49
-rw-r--r--testing/web-platform/tests/fetch/api/headers/header-setcookie.any.js266
-rw-r--r--testing/web-platform/tests/fetch/api/headers/header-values-normalize.any.js72
-rw-r--r--testing/web-platform/tests/fetch/api/headers/header-values.any.js63
-rw-r--r--testing/web-platform/tests/fetch/api/headers/headers-basic.any.js275
-rw-r--r--testing/web-platform/tests/fetch/api/headers/headers-casing.any.js54
-rw-r--r--testing/web-platform/tests/fetch/api/headers/headers-combine.any.js66
-rw-r--r--testing/web-platform/tests/fetch/api/headers/headers-errors.any.js96
-rw-r--r--testing/web-platform/tests/fetch/api/headers/headers-no-cors.any.js59
-rw-r--r--testing/web-platform/tests/fetch/api/headers/headers-normalize.any.js56
-rw-r--r--testing/web-platform/tests/fetch/api/headers/headers-record.any.js357
-rw-r--r--testing/web-platform/tests/fetch/api/headers/headers-structure.any.js20
-rw-r--r--testing/web-platform/tests/fetch/api/idlharness.any.js21
-rw-r--r--testing/web-platform/tests/fetch/api/policies/csp-blocked-worker.html16
-rw-r--r--testing/web-platform/tests/fetch/api/policies/csp-blocked.html15
-rw-r--r--testing/web-platform/tests/fetch/api/policies/csp-blocked.html.headers1
-rw-r--r--testing/web-platform/tests/fetch/api/policies/csp-blocked.js13
-rw-r--r--testing/web-platform/tests/fetch/api/policies/csp-blocked.js.headers1
-rw-r--r--testing/web-platform/tests/fetch/api/policies/nested-policy.js1
-rw-r--r--testing/web-platform/tests/fetch/api/policies/nested-policy.js.headers1
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-no-referrer-service-worker.https.html18
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-no-referrer-worker.html17
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.html15
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.html.headers1
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.js19
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.js.headers1
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-origin-service-worker.https.html18
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html17
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin-worker.html16
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.html16
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.html.headers1
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.js21
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.js.headers1
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-origin-worker.html17
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-origin.html16
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-origin.html.headers1
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-origin.js30
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-origin.js.headers1
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url-service-worker.https.html18
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url-worker.html17
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.html16
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.html.headers1
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.js21
-rw-r--r--testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.js.headers1
-rw-r--r--testing/web-platform/tests/fetch/api/redirect/redirect-back-to-original-origin.any.js38
-rw-r--r--testing/web-platform/tests/fetch/api/redirect/redirect-count.any.js51
-rw-r--r--testing/web-platform/tests/fetch/api/redirect/redirect-empty-location.any.js21
-rw-r--r--testing/web-platform/tests/fetch/api/redirect/redirect-keepalive.any.js35
-rw-r--r--testing/web-platform/tests/fetch/api/redirect/redirect-keepalive.https.any.js18
-rw-r--r--testing/web-platform/tests/fetch/api/redirect/redirect-location-escape.tentative.any.js46
-rw-r--r--testing/web-platform/tests/fetch/api/redirect/redirect-location.any.js73
-rw-r--r--testing/web-platform/tests/fetch/api/redirect/redirect-method.any.js112
-rw-r--r--testing/web-platform/tests/fetch/api/redirect/redirect-mode.any.js59
-rw-r--r--testing/web-platform/tests/fetch/api/redirect/redirect-origin.any.js68
-rw-r--r--testing/web-platform/tests/fetch/api/redirect/redirect-referrer-override.any.js104
-rw-r--r--testing/web-platform/tests/fetch/api/redirect/redirect-referrer.any.js66
-rw-r--r--testing/web-platform/tests/fetch/api/redirect/redirect-schemes.any.js19
-rw-r--r--testing/web-platform/tests/fetch/api/redirect/redirect-to-dataurl.any.js28
-rw-r--r--testing/web-platform/tests/fetch/api/redirect/redirect-upload.h2.any.js33
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/fetch-destination-frame.https.html51
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/fetch-destination-iframe.https.html51
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/fetch-destination-no-load-event.https.html124
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/fetch-destination-prefetch.https.html46
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/fetch-destination-worker.https.html60
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/fetch-destination.https.html485
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/resources/dummy0
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/resources/dummy.css0
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/resources/dummy.es0
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/resources/dummy.es.headers1
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/resources/dummy.html0
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/resources/dummy.json1
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/resources/dummy.pngbin0 -> 18299 bytes
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/resources/dummy.ttfbin0 -> 2528 bytes
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/resources/dummy_audio.mp3bin0 -> 20498 bytes
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/resources/dummy_audio.ogabin0 -> 18541 bytes
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/resources/dummy_video.mp4bin0 -> 67369 bytes
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/resources/dummy_video.ogvbin0 -> 94372 bytes
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/resources/dummy_video.webmbin0 -> 96902 bytes
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/resources/empty.https.html0
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker-frame.js20
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js20
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js20
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker.js12
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/resources/import-declaration-type-css.js1
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/resources/import-declaration-type-json.js1
-rw-r--r--testing/web-platform/tests/fetch/api/request/destination/resources/importer.js1
-rw-r--r--testing/web-platform/tests/fetch/api/request/forbidden-method.any.js13
-rw-r--r--testing/web-platform/tests/fetch/api/request/multi-globals/construct-in-detached-frame.window.js11
-rw-r--r--testing/web-platform/tests/fetch/api/request/multi-globals/current/current.html3
-rw-r--r--testing/web-platform/tests/fetch/api/request/multi-globals/incumbent/incumbent.html14
-rw-r--r--testing/web-platform/tests/fetch/api/request/multi-globals/url-parsing.html27
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-bad-port.any.js92
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-cache-default-conditional.any.js170
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-cache-default.any.js39
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-cache-force-cache.any.js67
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-cache-no-cache.any.js25
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-cache-no-store.any.js37
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-cache-only-if-cached.any.js66
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-cache-reload.any.js51
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-cache.js223
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-clone.sub.html63
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-constructor-init-body-override.any.js21
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-consume-empty.any.js101
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-consume.any.js145
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-disturbed.any.js109
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-error.any.js56
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-error.js57
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-headers.any.js177
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-init-001.sub.html112
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-init-002.any.js60
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-init-003.sub.html84
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-init-contenttype.any.js141
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-init-priority.any.js26
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-init-stream.any.js147
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-keepalive-quota.html97
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-keepalive.any.js17
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-reset-attributes.https.html96
-rw-r--r--testing/web-platform/tests/fetch/api/request/request-structure.any.js143
-rw-r--r--testing/web-platform/tests/fetch/api/request/resources/cache.py67
-rw-r--r--testing/web-platform/tests/fetch/api/request/resources/hello.txt1
-rw-r--r--testing/web-platform/tests/fetch/api/request/resources/request-reset-attributes-worker.js19
-rw-r--r--testing/web-platform/tests/fetch/api/request/url-encoding.html25
-rw-r--r--testing/web-platform/tests/fetch/api/resources/authentication.py14
-rw-r--r--testing/web-platform/tests/fetch/api/resources/bad-chunk-encoding.py13
-rw-r--r--testing/web-platform/tests/fetch/api/resources/basic.html5
-rw-r--r--testing/web-platform/tests/fetch/api/resources/cache.py18
-rw-r--r--testing/web-platform/tests/fetch/api/resources/clean-stash.py6
-rw-r--r--testing/web-platform/tests/fetch/api/resources/cors-top.txt1
-rw-r--r--testing/web-platform/tests/fetch/api/resources/cors-top.txt.headers1
-rw-r--r--testing/web-platform/tests/fetch/api/resources/data.json1
-rw-r--r--testing/web-platform/tests/fetch/api/resources/dump-authorization-header.py14
-rw-r--r--testing/web-platform/tests/fetch/api/resources/echo-content.h2.py7
-rw-r--r--testing/web-platform/tests/fetch/api/resources/echo-content.py12
-rw-r--r--testing/web-platform/tests/fetch/api/resources/empty.txt0
-rw-r--r--testing/web-platform/tests/fetch/api/resources/infinite-slow-response.py35
-rw-r--r--testing/web-platform/tests/fetch/api/resources/inspect-headers.py24
-rw-r--r--testing/web-platform/tests/fetch/api/resources/keepalive-helper.js176
-rw-r--r--testing/web-platform/tests/fetch/api/resources/keepalive-iframe.html22
-rw-r--r--testing/web-platform/tests/fetch/api/resources/keepalive-redirect-iframe.html23
-rw-r--r--testing/web-platform/tests/fetch/api/resources/keepalive-redirect-window.html42
-rw-r--r--testing/web-platform/tests/fetch/api/resources/method.py18
-rw-r--r--testing/web-platform/tests/fetch/api/resources/preflight.py78
-rw-r--r--testing/web-platform/tests/fetch/api/resources/redirect-empty-location.py3
-rw-r--r--testing/web-platform/tests/fetch/api/resources/redirect.h2.py14
-rw-r--r--testing/web-platform/tests/fetch/api/resources/redirect.py73
-rw-r--r--testing/web-platform/tests/fetch/api/resources/sandboxed-iframe.html34
-rw-r--r--testing/web-platform/tests/fetch/api/resources/script-with-header.py7
-rw-r--r--testing/web-platform/tests/fetch/api/resources/stash-put.py41
-rw-r--r--testing/web-platform/tests/fetch/api/resources/stash-take.py9
-rw-r--r--testing/web-platform/tests/fetch/api/resources/status.py11
-rw-r--r--testing/web-platform/tests/fetch/api/resources/sw-intercept-abort.js19
-rw-r--r--testing/web-platform/tests/fetch/api/resources/sw-intercept.js10
-rw-r--r--testing/web-platform/tests/fetch/api/resources/top.txt1
-rw-r--r--testing/web-platform/tests/fetch/api/resources/trickle.py15
-rw-r--r--testing/web-platform/tests/fetch/api/resources/utils.js120
-rw-r--r--testing/web-platform/tests/fetch/api/response/json.any.js14
-rw-r--r--testing/web-platform/tests/fetch/api/response/many-empty-chunks-crash.html14
-rw-r--r--testing/web-platform/tests/fetch/api/response/multi-globals/current/current.html3
-rw-r--r--testing/web-platform/tests/fetch/api/response/multi-globals/incumbent/incumbent.html16
-rw-r--r--testing/web-platform/tests/fetch/api/response/multi-globals/relevant/relevant.html2
-rw-r--r--testing/web-platform/tests/fetch/api/response/multi-globals/url-parsing.html27
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-body-read-task-handling.html86
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-cancel-stream.any.js64
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-clone-iframe.window.js32
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-clone.any.js140
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-consume-empty.any.js99
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-consume-stream.any.js80
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-consume.html317
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-error-from-stream.any.js59
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-error.any.js27
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-from-stream.any.js23
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-headers-guard.any.js8
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-init-001.any.js64
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-init-002.any.js61
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-init-contenttype.any.js125
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-static-error.any.js22
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-static-json.any.js96
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-static-redirect.any.js40
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-stream-bad-chunk.any.js24
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-stream-disturbed-1.any.js44
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-stream-disturbed-2.any.js35
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-stream-disturbed-3.any.js36
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-stream-disturbed-4.any.js35
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-stream-disturbed-5.any.js19
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-stream-disturbed-6.any.js76
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-stream-disturbed-by-pipe.any.js17
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-stream-disturbed-util.js17
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-stream-with-broken-then.any.js117
263 files changed, 12814 insertions, 0 deletions
diff --git a/testing/web-platform/tests/fetch/api/abort/cache.https.any.js b/testing/web-platform/tests/fetch/api/abort/cache.https.any.js
new file mode 100644
index 0000000000..bdaf0e69e5
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/abort/cache.https.any.js
@@ -0,0 +1,47 @@
+// META: title=Request signals &amp; the cache API
+// META: global=window,worker
+
+promise_test(async () => {
+ await caches.delete('test');
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request('../resources/data.json', { signal });
+
+ const cache = await caches.open('test');
+ await cache.put(request, new Response(''));
+
+ const requests = await cache.keys();
+
+ assert_equals(requests.length, 1, 'Ensuring cleanup worked');
+
+ const [cachedRequest] = requests;
+
+ controller.abort();
+
+ assert_false(cachedRequest.signal.aborted, "Request from cache shouldn't be aborted");
+
+ const data = await fetch(cachedRequest).then(r => r.json());
+ assert_equals(data.key, 'value', 'Fetch fully completes');
+}, "Signals are not stored in the cache API");
+
+promise_test(async () => {
+ await caches.delete('test');
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request('../resources/data.json', { signal });
+ controller.abort();
+
+ const cache = await caches.open('test');
+ await cache.put(request, new Response(''));
+
+ const requests = await cache.keys();
+
+ assert_equals(requests.length, 1, 'Ensuring cleanup worked');
+
+ const [cachedRequest] = requests;
+
+ assert_false(cachedRequest.signal.aborted, "Request from cache shouldn't be aborted");
+
+ const data = await fetch(cachedRequest).then(r => r.json());
+ assert_equals(data.key, 'value', 'Fetch fully completes');
+}, "Signals are not stored in the cache API, even if they're already aborted");
diff --git a/testing/web-platform/tests/fetch/api/abort/destroyed-context.html b/testing/web-platform/tests/fetch/api/abort/destroyed-context.html
new file mode 100644
index 0000000000..161d39bd9c
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/abort/destroyed-context.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+// This is a regression test for crbug.com/860063.
+window.controller = new AbortController();
+async_test(t => {
+ onmessage = t.step_func(event => {
+ assert_equals(event.data, 'started');
+ const iframe = document.querySelector('iframe');
+ document.body.removeChild(iframe);
+ controller.abort();
+ t.done();
+ });
+}, 'aborting a fetch in a destroyed context should not crash');
+</script>
+<iframe srcdoc="
+ <!DOCTYPE html>
+ <meta charset=utf-8>
+ <script>
+ fetch('../resources/infinite-slow-response.py', { signal: parent.controller.signal }).then(() => {
+ parent.postMessage('started', '*');
+ });
+ </script>
+ ">
+</iframe>
diff --git a/testing/web-platform/tests/fetch/api/abort/general.any.js b/testing/web-platform/tests/fetch/api/abort/general.any.js
new file mode 100644
index 0000000000..3727bb42af
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/abort/general.any.js
@@ -0,0 +1,572 @@
+// META: timeout=long
+// META: global=window,worker
+// 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'];
+
+const error1 = new Error('error1');
+error1.name = 'error1';
+
+// This is used to close connections that weren't correctly closed during the tests,
+// otherwise you can end up running out of HTTP connections.
+let requestAbortKeys = [];
+
+function abortRequests() {
+ const keys = requestAbortKeys;
+ requestAbortKeys = [];
+ return Promise.all(
+ keys.map(key => fetch(`../resources/stash-put.py?key=${key}&value=close`))
+ );
+}
+
+const hostInfo = get_host_info();
+const urlHostname = hostInfo.REMOTE_HOST;
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const fetchPromise = fetch('../resources/data.json', { signal });
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Aborting rejects with AbortError");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort(error1);
+
+ const fetchPromise = fetch('../resources/data.json', { signal });
+
+ await promise_rejects_exactly(t, error1, fetchPromise, 'fetch() should reject with abort reason');
+}, "Aborting rejects with abort reason");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const url = new URL('../resources/data.json', location);
+ url.hostname = urlHostname;
+
+ const fetchPromise = fetch(url, {
+ signal,
+ mode: 'no-cors'
+ });
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Aborting rejects with AbortError - no-cors");
+
+// Test that errors thrown from the request constructor take priority over abort errors.
+// badRequestArgTests is from response-error.js
+for (const { args, testName } of badRequestArgTests) {
+ promise_test(async t => {
+ try {
+ // If this doesn't throw, we'll effectively skip the test.
+ // It'll fail properly in ../request/request-error.html
+ new Request(...args);
+ }
+ catch (err) {
+ const controller = new AbortController();
+ controller.abort();
+
+ // Add signal to 2nd arg
+ args[1] = args[1] || {};
+ args[1].signal = controller.signal;
+ await promise_rejects_js(t, TypeError, fetch(...args));
+ }
+ }, `TypeError from request constructor takes priority - ${testName}`);
+}
+
+test(() => {
+ const request = new Request('');
+ assert_true(Boolean(request.signal), "Signal member is present & truthy");
+ assert_equals(request.signal.constructor, AbortSignal);
+}, "Request objects have a signal property");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', { signal });
+
+ assert_true(Boolean(request.signal), "Signal member is present & truthy");
+ assert_equals(request.signal.constructor, AbortSignal);
+ assert_not_equals(request.signal, signal, 'Request has a new signal, not a reference');
+ assert_true(request.signal.aborted, `Request's signal has aborted`);
+
+ const fetchPromise = fetch(request);
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Signal on request object");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort(error1);
+
+ const request = new Request('../resources/data.json', { signal });
+
+ assert_not_equals(request.signal, signal, 'Request has a new signal, not a reference');
+ assert_true(request.signal.aborted, `Request's signal has aborted`);
+ assert_equals(request.signal.reason, error1, `Request's signal's abort reason is error1`);
+
+ const fetchPromise = fetch(request);
+
+ await promise_rejects_exactly(t, error1, fetchPromise, "fetch() should reject with abort reason");
+}, "Signal on request object should also have abort reason");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', { signal });
+ const requestFromRequest = new Request(request);
+
+ const fetchPromise = fetch(requestFromRequest);
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Signal on request object created from request object");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json');
+ const requestFromRequest = new Request(request, { signal });
+
+ const fetchPromise = fetch(requestFromRequest);
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Signal on request object created from request object, with signal on second request");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', { signal: new AbortController().signal });
+ const requestFromRequest = new Request(request, { signal });
+
+ const fetchPromise = fetch(requestFromRequest);
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Signal on request object created from request object, with signal on second request overriding another");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', { signal });
+
+ const fetchPromise = fetch(request, {method: 'POST'});
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+}, "Signal retained after unrelated properties are overridden by fetch");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', { signal });
+
+ const data = await fetch(request, { signal: null }).then(r => r.json());
+ assert_equals(data.key, 'value', 'Fetch fully completes');
+}, "Signal removed by setting to null");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const log = [];
+
+ await Promise.all([
+ fetch('../resources/data.json', { signal }).then(
+ () => assert_unreached("Fetch must not resolve"),
+ () => log.push('fetch-reject')
+ ),
+ Promise.resolve().then(() => log.push('next-microtask'))
+ ]);
+
+ assert_array_equals(log, ['fetch-reject', 'next-microtask']);
+}, "Already aborted signal rejects immediately");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('../resources/data.json', {
+ signal,
+ method: 'POST',
+ body: 'foo',
+ headers: { 'Content-Type': 'text/plain' }
+ });
+
+ await fetch(request).catch(() => {});
+
+ assert_true(request.bodyUsed, "Body has been used");
+}, "Request is still 'used' if signal is aborted before fetching");
+
+for (const bodyMethod of BODY_METHODS) {
+ promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ const log = [];
+ const response = await fetch('../resources/data.json', { signal });
+
+ controller.abort();
+
+ const bodyPromise = response[bodyMethod]();
+
+ await Promise.all([
+ bodyPromise.catch(() => log.push(`${bodyMethod}-reject`)),
+ Promise.resolve().then(() => log.push('next-microtask'))
+ ]);
+
+ await promise_rejects_dom(t, "AbortError", bodyPromise);
+
+ assert_array_equals(log, [`${bodyMethod}-reject`, 'next-microtask']);
+ }, `response.${bodyMethod}() rejects if already aborted`);
+}
+
+promise_test(async (t) => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ const res = await fetch('../resources/data.json', { signal });
+ controller.abort();
+
+ await promise_rejects_dom(t, 'AbortError', res.text());
+ await promise_rejects_dom(t, 'AbortError', res.text());
+}, 'Call text() twice on aborted response');
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+ controller.abort();
+
+ await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal }).catch(() => {});
+
+ // I'm hoping this will give the browser enough time to (incorrectly) make the request
+ // above, if it intends to.
+ await fetch('../resources/data.json').then(r => r.json());
+
+ const response = await fetch(`../resources/stash-take.py?key=${stateKey}`);
+ const data = await response.json();
+
+ assert_equals(data, null, "Request hasn't been made to the server");
+}, "Already aborted signal does not make request");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const fetches = [];
+
+ for (let i = 0; i < 3; i++) {
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ fetches.push(
+ fetch(`../resources/infinite-slow-response.py?${i}&abortKey=${abortKey}`, { signal })
+ );
+ }
+
+ for (const fetchPromise of fetches) {
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+ }
+}, "Already aborted signal can be used for many fetches");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ await fetch('../resources/data.json', { signal }).then(r => r.json());
+
+ controller.abort();
+
+ const fetches = [];
+
+ for (let i = 0; i < 3; i++) {
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ fetches.push(
+ fetch(`../resources/infinite-slow-response.py?${i}&abortKey=${abortKey}`, { signal })
+ );
+ }
+
+ for (const fetchPromise of fetches) {
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+ }
+}, "Signal can be used to abort other fetches, even if another fetch succeeded before aborting");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal });
+
+ const beforeAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ assert_equals(beforeAbortResult, "open", "Connection is open");
+
+ controller.abort();
+
+ // The connection won't close immediately, but it should close at some point:
+ const start = Date.now();
+
+ while (true) {
+ // Stop spinning if 10 seconds have passed
+ if (Date.now() - start > 10000) throw Error('Timed out');
+
+ const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ if (afterAbortResult == 'closed') break;
+ }
+}, "Underlying connection is closed when aborting after receiving response");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ const url = new URL(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, location);
+ url.hostname = urlHostname;
+
+ await fetch(url, {
+ signal,
+ mode: 'no-cors'
+ });
+
+ const stashTakeURL = new URL(`../resources/stash-take.py?key=${stateKey}`, location);
+ stashTakeURL.hostname = urlHostname;
+
+ const beforeAbortResult = await fetch(stashTakeURL).then(r => r.json());
+ assert_equals(beforeAbortResult, "open", "Connection is open");
+
+ controller.abort();
+
+ // The connection won't close immediately, but it should close at some point:
+ const start = Date.now();
+
+ while (true) {
+ // Stop spinning if 10 seconds have passed
+ if (Date.now() - start > 10000) throw Error('Timed out');
+
+ const afterAbortResult = await fetch(stashTakeURL).then(r => r.json());
+ if (afterAbortResult == 'closed') break;
+ }
+}, "Underlying connection is closed when aborting after receiving response - no-cors");
+
+for (const bodyMethod of BODY_METHODS) {
+ promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal });
+
+ const beforeAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ assert_equals(beforeAbortResult, "open", "Connection is open");
+
+ const bodyPromise = response[bodyMethod]();
+
+ controller.abort();
+
+ await promise_rejects_dom(t, "AbortError", bodyPromise);
+
+ const start = Date.now();
+
+ while (true) {
+ // Stop spinning if 10 seconds have passed
+ if (Date.now() - start > 10000) throw Error('Timed out');
+
+ const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ if (afterAbortResult == 'closed') break;
+ }
+ }, `Fetch aborted & connection closed when aborted after calling response.${bodyMethod}()`);
+}
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal });
+ const reader = response.body.getReader();
+
+ controller.abort();
+
+ await promise_rejects_dom(t, "AbortError", reader.read());
+ await promise_rejects_dom(t, "AbortError", reader.closed);
+
+ // The connection won't close immediately, but it should close at some point:
+ const start = Date.now();
+
+ while (true) {
+ // Stop spinning if 10 seconds have passed
+ if (Date.now() - start > 10000) throw Error('Timed out');
+
+ const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ if (afterAbortResult == 'closed') break;
+ }
+}, "Stream errors once aborted. Underlying connection closed.");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const stateKey = token();
+ const abortKey = token();
+ requestAbortKeys.push(abortKey);
+
+ const response = await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, { signal });
+ const reader = response.body.getReader();
+
+ await reader.read();
+
+ controller.abort();
+
+ await promise_rejects_dom(t, "AbortError", reader.read());
+ await promise_rejects_dom(t, "AbortError", reader.closed);
+
+ // The connection won't close immediately, but it should close at some point:
+ const start = Date.now();
+
+ while (true) {
+ // Stop spinning if 10 seconds have passed
+ if (Date.now() - start > 10000) throw Error('Timed out');
+
+ const afterAbortResult = await fetch(`../resources/stash-take.py?key=${stateKey}`).then(r => r.json());
+ if (afterAbortResult == 'closed') break;
+ }
+}, "Stream errors once aborted, after reading. Underlying connection closed.");
+
+promise_test(async t => {
+ await abortRequests();
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ const response = await fetch(`../resources/empty.txt`, { signal });
+
+ // Read whole response to ensure close signal has sent.
+ await response.clone().text();
+
+ const reader = response.body.getReader();
+
+ controller.abort();
+
+ const item = await reader.read();
+
+ assert_true(item.done, "Stream is done");
+}, "Stream will not error if body is empty. It's closed with an empty queue before it errors.");
+
+promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ let cancelReason;
+
+ const body = new ReadableStream({
+ pull(controller) {
+ controller.enqueue(new Uint8Array([42]));
+ },
+ cancel(reason) {
+ cancelReason = reason;
+ }
+ });
+
+ const fetchPromise = fetch('../resources/empty.txt', {
+ body, signal,
+ method: 'POST',
+ duplex: 'half',
+ headers: {
+ 'Content-Type': 'text/plain'
+ }
+ });
+
+ assert_true(!!cancelReason, 'Cancel called sync');
+ assert_equals(cancelReason.constructor, DOMException);
+ assert_equals(cancelReason.name, 'AbortError');
+
+ await promise_rejects_dom(t, "AbortError", fetchPromise);
+
+ const fetchErr = await fetchPromise.catch(e => e);
+
+ assert_equals(cancelReason, fetchErr, "Fetch rejects with same error instance");
+}, "Readable stream synchronously cancels with AbortError if aborted before reading");
+
+test(() => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const request = new Request('.', { signal });
+ const requestSignal = request.signal;
+
+ const clonedRequest = request.clone();
+
+ assert_equals(requestSignal, request.signal, "Original request signal the same after cloning");
+ assert_true(request.signal.aborted, "Original request signal aborted");
+ assert_not_equals(clonedRequest.signal, request.signal, "Cloned request has different signal");
+ assert_true(clonedRequest.signal.aborted, "Cloned request signal aborted");
+}, "Signal state is cloned");
+
+test(() => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ const request = new Request('.', { signal });
+ const clonedRequest = request.clone();
+
+ const log = [];
+
+ request.signal.addEventListener('abort', () => log.push('original-aborted'));
+ clonedRequest.signal.addEventListener('abort', () => log.push('clone-aborted'));
+
+ controller.abort();
+
+ assert_array_equals(log, ['original-aborted', 'clone-aborted'], "Abort events fired in correct order");
+ assert_true(request.signal.aborted, 'Signal aborted');
+ assert_true(clonedRequest.signal.aborted, 'Signal aborted');
+}, "Clone aborts with original controller");
diff --git a/testing/web-platform/tests/fetch/api/abort/keepalive.html b/testing/web-platform/tests/fetch/api/abort/keepalive.html
new file mode 100644
index 0000000000..db12df0d28
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/abort/keepalive.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script>
+// This controller must be on the window so it is visible to the iframe.
+window.sharedController = new AbortController();
+
+async function fetchJson(url) {
+ const response = await fetch(url);
+ assert_true(response.ok, 'response should be ok');
+ return response.json();
+}
+
+promise_test(async () => {
+ const stateKey = token();
+ const controller = new AbortController();
+ await fetch(`../resources/infinite-slow-response.py?stateKey=${stateKey}`,
+ {
+ signal: controller.signal,
+ keepalive: true
+ });
+ const before = await fetchJson(`../resources/stash-take.py?key=${stateKey}`);
+ assert_equals(before, 'open', 'connection should be open');
+
+ controller.abort();
+
+ // Spin until the abort completes.
+ while (true) {
+ const after = await fetchJson(`../resources/stash-take.py?key=${stateKey}`);
+ if (after) {
+ // stateKey='open' was removed from the dictionary by the first fetch of
+ // stash-take.py, so we should only ever see the value 'closed' here.
+ assert_equals(after, 'closed', 'connection should have closed');
+ break;
+ }
+ }
+}, 'aborting a keepalive fetch should work');
+
+promise_test(async t => {
+ const key = token();
+ const iframeEl = document.querySelector('iframe');
+
+ // Tell the iframe to start the fetch, and wait until it says it has.
+ await new Promise(resolve => {
+ onmessage = t.step_func(event => {
+ assert_equals(event.data, 'started', 'event data should be "started"');
+ resolve();
+ });
+ iframeEl.contentWindow.postMessage(key, '*');
+ });
+
+ // Detach the context of the fetch.
+ iframeEl.remove();
+
+ sharedController.abort();
+
+ // The abort should not do anything. The connection should stay open. Wait 1
+ // second to give time for the fetch to complete.
+ await new Promise(resolve => t.step_timeout(resolve, 1000));
+
+ const after = await fetchJson(`../resources/stash-take.py?key=${key}`);
+ assert_equals(after, 'on', 'fetch should have completed');
+}, 'aborting a detached keepalive fetch should not do anything');
+</script>
+
+<iframe srcdoc="
+ <!DOCTYPE html>
+ <meta charset=utf-8>
+ <script>
+ onmessage = async event => {
+ const key = event.data;
+ await fetch(
+ `../resources/redirect.py?delay=500&amp;location=` +
+ `../resources/stash-put.py%3fkey=${key}%26value=on`,
+ {
+ signal: parent.sharedController.signal,
+ keepalive: true
+ });
+ parent.postMessage('started', '*');
+ };
+ </script>
+ ">
+</iframe>
diff --git a/testing/web-platform/tests/fetch/api/abort/request.any.js b/testing/web-platform/tests/fetch/api/abort/request.any.js
new file mode 100644
index 0000000000..dcc7803abe
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/abort/request.any.js
@@ -0,0 +1,85 @@
+// META: timeout=long
+// META: global=window,worker
+
+const BODY_FUNCTION_AND_DATA = {
+ arrayBuffer: null,
+ blob: null,
+ formData: new FormData(),
+ json: new Blob(["{}"]),
+ text: null,
+};
+
+for (const [bodyFunction, body] of Object.entries(BODY_FUNCTION_AND_DATA)) {
+ promise_test(async () => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request("../resources/data.json", {
+ method: "post",
+ signal,
+ body,
+ });
+
+ controller.abort();
+ await request[bodyFunction]();
+ assert_true(
+ true,
+ `An aborted request should still be able to run ${bodyFunction}()`
+ );
+ }, `Calling ${bodyFunction}() on an aborted request`);
+
+ promise_test(async () => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request("../resources/data.json", {
+ method: "post",
+ signal,
+ body,
+ });
+
+ const p = request[bodyFunction]();
+ controller.abort();
+ await p;
+ assert_true(
+ true,
+ `An aborted request should still be able to run ${bodyFunction}()`
+ );
+ }, `Aborting a request after calling ${bodyFunction}()`);
+
+ if (!body) {
+ promise_test(async () => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request("../resources/data.json", {
+ method: "post",
+ signal,
+ body,
+ });
+
+ // consuming happens synchronously, so don't wait
+ fetch(request).catch(() => {});
+
+ controller.abort();
+ await request[bodyFunction]();
+ assert_true(
+ true,
+ `An aborted consumed request should still be able to run ${bodyFunction}() when empty`
+ );
+ }, `Calling ${bodyFunction}() on an aborted consumed empty request`);
+ }
+
+ promise_test(async t => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ const request = new Request("../resources/data.json", {
+ method: "post",
+ signal,
+ body: body || new Blob(["foo"]),
+ });
+
+ // consuming happens synchronously, so don't wait
+ fetch(request).catch(() => {});
+
+ controller.abort();
+ await promise_rejects_js(t, TypeError, request[bodyFunction]());
+ }, `Calling ${bodyFunction}() on an aborted consumed nonempty request`);
+}
diff --git a/testing/web-platform/tests/fetch/api/abort/serviceworker-intercepted.https.html b/testing/web-platform/tests/fetch/api/abort/serviceworker-intercepted.https.html
new file mode 100644
index 0000000000..ed9bc973e8
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/abort/serviceworker-intercepted.https.html
@@ -0,0 +1,212 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Aborting fetch when intercepted by a service worker</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../../../service-workers/service-worker/resources/test-helpers.sub.js"></script>
+</head>
+<body>
+<script>
+ // Duplicating this resource to make service worker scoping simpler.
+ const SCOPE = '../resources/basic.html';
+ const BODY_METHODS = ['arrayBuffer', 'blob', 'formData', 'json', 'text'];
+
+ const error1 = new Error('error1');
+ error1.name = 'error1';
+
+ async function setupRegistration(t, scope, service_worker) {
+ const reg = await navigator.serviceWorker.register(service_worker, { scope });
+ await wait_for_state(t, reg.installing, 'activated');
+ add_completion_callback(_ => reg.unregister());
+ return reg;
+ }
+
+ promise_test(async t => {
+ const suffix = "?q=aborted-not-intercepted";
+ const scope = SCOPE + suffix;
+ await setupRegistration(t, scope, '../resources/sw-intercept.js');
+ const iframe = await with_iframe(scope);
+ add_completion_callback(_ => iframe.remove());
+ const w = iframe.contentWindow;
+
+ const controller = new w.AbortController();
+ const signal = controller.signal;
+ controller.abort();
+
+ const nextData = new Promise(resolve => {
+ w.navigator.serviceWorker.addEventListener('message', function once(event) {
+ // The message triggered by the iframe's document's fetch
+ // request cannot get dispatched by the time we add the event
+ // listener, so we have to guard against it.
+ if (!event.data.endsWith(suffix)) {
+ w.navigator.serviceWorker.removeEventListener('message', once);
+ resolve(event.data);
+ }
+ })
+ });
+
+ const fetchPromise = w.fetch('data.json', { signal });
+
+ await promise_rejects_dom(t, "AbortError", w.DOMException, fetchPromise);
+
+ await w.fetch('data.json?no-abort');
+
+ assert_true((await nextData).endsWith('?no-abort'), "Aborted request does not go through service worker");
+ }, "Already aborted request does not land in service worker");
+
+ for (const bodyMethod of BODY_METHODS) {
+ promise_test(async t => {
+ const scope = SCOPE + "?q=aborted-" + bodyMethod + "-rejects";
+ await setupRegistration(t, scope, '../resources/sw-intercept.js');
+ const iframe = await with_iframe(scope);
+ add_completion_callback(_ => iframe.remove());
+ const w = iframe.contentWindow;
+
+ const controller = new w.AbortController();
+ const signal = controller.signal;
+
+ const log = [];
+ const response = await w.fetch('data.json', { signal });
+
+ controller.abort();
+
+ const bodyPromise = response[bodyMethod]();
+
+ await Promise.all([
+ bodyPromise.catch(() => log.push(`${bodyMethod}-reject`)),
+ Promise.resolve().then(() => log.push('next-microtask'))
+ ]);
+
+ await promise_rejects_dom(t, "AbortError", w.DOMException, bodyPromise);
+
+ assert_array_equals(log, [`${bodyMethod}-reject`, 'next-microtask']);
+ }, `response.${bodyMethod}() rejects if already aborted`);
+ }
+
+ promise_test(async t => {
+ const scope = SCOPE + "?q=aborted-stream-errors";
+ await setupRegistration(t, scope, '../resources/sw-intercept.js');
+ const iframe = await with_iframe(scope);
+ add_completion_callback(_ => iframe.remove());
+ const w = iframe.contentWindow;
+
+ const controller = new w.AbortController();
+ const signal = controller.signal;
+
+ const response = await w.fetch('data.json', { signal });
+ const reader = response.body.getReader();
+
+ controller.abort();
+
+ await promise_rejects_dom(t, "AbortError", w.DOMException, reader.read());
+ await promise_rejects_dom(t, "AbortError", w.DOMException, reader.closed);
+ }, "Stream errors once aborted.");
+
+ promise_test(async t => {
+ const scope = SCOPE + "?q=aborted-with-abort-reason";
+ await setupRegistration(t, scope, '../resources/sw-intercept.js');
+ const iframe = await with_iframe(scope);
+ add_completion_callback(_ => iframe.remove());
+ const w = iframe.contentWindow;
+
+ const controller = new w.AbortController();
+ const signal = controller.signal;
+
+ const fetchPromise = w.fetch('data.json', { signal });
+
+ controller.abort(error1);
+
+ await promise_rejects_exactly(t, error1, fetchPromise);
+ }, "fetch() rejects with abort reason");
+
+
+ promise_test(async t => {
+ const scope = SCOPE + "?q=aborted-with-abort-reason-in-body";
+ await setupRegistration(t, scope, '../resources/sw-intercept.js');
+ const iframe = await with_iframe(scope);
+ add_completion_callback(_ => iframe.remove());
+ const w = iframe.contentWindow;
+
+ const controller = new w.AbortController();
+ const signal = controller.signal;
+
+ const fetchResponse = await w.fetch('data.json', { signal });
+ const bodyPromise = fetchResponse.body.getReader().read();
+ controller.abort(error1);
+
+ await promise_rejects_exactly(t, error1, bodyPromise);
+ }, "fetch() response body has abort reason");
+
+ promise_test(async t => {
+ const scope = SCOPE + "?q=service-worker-observes-abort-reason";
+ await setupRegistration(t, scope, '../resources/sw-intercept-abort.js');
+ const iframe = await with_iframe(scope);
+ add_completion_callback(_ => iframe.remove());
+ const w = iframe.contentWindow;
+
+ const controller = new w.AbortController();
+ const signal = controller.signal;
+
+ const fetchPromise = w.fetch('data.json', { signal });
+
+ await new Promise(resolve => {
+ w.navigator.serviceWorker.addEventListener('message', t.step_func(event => {
+ assert_equals(event.data, "fetch event has arrived");
+ resolve();
+ }), {once: true});
+ });
+
+ controller.abort(error1);
+
+ await new Promise(resolve => {
+ w.navigator.serviceWorker.addEventListener('message', t.step_func(event => {
+ assert_equals(event.data.message, error1.message);
+ resolve();
+ }), {once: true});
+ });
+
+ await promise_rejects_exactly(t, error1, fetchPromise);
+ }, "Service Worker can observe the fetch abort and associated abort reason");
+
+ promise_test(async t => {
+ let incrementing_error = new Error('error1');
+ incrementing_error.name = 'error1';
+
+ const scope = SCOPE + "?q=serialization-on-abort";
+ await setupRegistration(t, scope, '../resources/sw-intercept-abort.js');
+ const iframe = await with_iframe(scope);
+ add_completion_callback(_ => iframe.remove());
+ const w = iframe.contentWindow;
+
+ const controller = new w.AbortController();
+ const signal = controller.signal;
+
+ const fetchPromise = w.fetch('data.json', { signal });
+
+ await new Promise(resolve => {
+ w.navigator.serviceWorker.addEventListener('message', t.step_func(event => {
+ assert_equals(event.data, "fetch event has arrived");
+ resolve();
+ }), {once: true});
+ });
+
+ controller.abort(incrementing_error);
+
+ const original_error_name = incrementing_error.name;
+
+ incrementing_error.name = 'error2';
+
+ await new Promise(resolve => {
+ w.navigator.serviceWorker.addEventListener('message', t.step_func(event => {
+ assert_equals(event.data.name, original_error_name);
+ resolve();
+ }), {once: true});
+ });
+
+ await promise_rejects_exactly(t, incrementing_error, fetchPromise);
+ }, "Abort reason serialization happens on abort");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/fetch/api/basic/accept-header.any.js b/testing/web-platform/tests/fetch/api/basic/accept-header.any.js
new file mode 100644
index 0000000000..cd54cf2a03
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/accept-header.any.js
@@ -0,0 +1,34 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+promise_test(function() {
+ return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept").then(function(response) {
+ assert_equals(response.status, 200, "HTTP status is 200");
+ assert_equals(response.type , "basic", "Response's type is basic");
+ assert_equals(response.headers.get("x-request-accept"), "*/*", "Request has accept header with value '*/*'");
+ });
+}, "Request through fetch should have 'accept' header with value '*/*'");
+
+promise_test(function() {
+ return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept", {"headers": [["Accept", "custom/*"]]}).then(function(response) {
+ assert_equals(response.status, 200, "HTTP status is 200");
+ assert_equals(response.type , "basic", "Response's type is basic");
+ assert_equals(response.headers.get("x-request-accept"), "custom/*", "Request has accept header with value 'custom/*'");
+ });
+}, "Request through fetch should have 'accept' header with value 'custom/*'");
+
+promise_test(function() {
+ return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept-Language").then(function(response) {
+ assert_equals(response.status, 200, "HTTP status is 200");
+ assert_equals(response.type , "basic", "Response's type is basic");
+ assert_true(response.headers.has("x-request-accept-language"));
+ });
+}, "Request through fetch should have a 'accept-language' header");
+
+promise_test(function() {
+ return fetch(RESOURCES_DIR + "inspect-headers.py?headers=Accept-Language", {"headers": [["Accept-Language", "bzh"]]}).then(function(response) {
+ assert_equals(response.status, 200, "HTTP status is 200");
+ assert_equals(response.type , "basic", "Response's type is basic");
+ assert_equals(response.headers.get("x-request-accept-language"), "bzh", "Request has accept header with value 'bzh'");
+ });
+}, "Request through fetch should have 'accept-language' header with value 'bzh'");
diff --git a/testing/web-platform/tests/fetch/api/basic/block-mime-as-script.html b/testing/web-platform/tests/fetch/api/basic/block-mime-as-script.html
new file mode 100644
index 0000000000..afc2bbbafb
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/block-mime-as-script.html
@@ -0,0 +1,43 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>Block mime type as script</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div></div>
+<script>
+ var noop = function() {};
+
+ ["non-empty", "empty"].forEach(function(content) {
+ ["text/csv",
+ "audio/aiff",
+ "audio/midi",
+ "audio/whatever",
+ "video/avi",
+ "video/fli",
+ "video/whatever",
+ "image/jpeg",
+ "image/gif",
+ "image/whatever"].forEach(function(test_case) {
+ async_test(function(t) {
+ var script = document.createElement("script");
+ script.onerror = t.step_func_done(noop);
+ script.onload = t.unreached_func("Unexpected load event");
+ script.src = "../resources/script-with-header.py?content=" + content +
+ "&mime=" + test_case;
+ document.body.appendChild(script);
+ }, "Should fail loading " + content + " script with " + test_case +
+ " MIME type");
+ });
+ });
+
+ ["html", "plain"].forEach(function(test_case) {
+ async_test(function(t) {
+ var script = document.createElement("script");
+ script.onerror = t.unreached_func("Unexpected error event");
+ script.onload = t.step_func_done(noop);
+ script.src = "../resources/script-with-header.py?mime=text/" + test_case;
+ document.body.appendChild(script);
+ }, "Should load script with text/" + test_case + " MIME type");
+ });
+
+</script>
diff --git a/testing/web-platform/tests/fetch/api/basic/conditional-get.any.js b/testing/web-platform/tests/fetch/api/basic/conditional-get.any.js
new file mode 100644
index 0000000000..2f9fa81c02
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/conditional-get.any.js
@@ -0,0 +1,38 @@
+// META: title=Request ETag
+// META: global=window,worker
+// META: script=/common/utils.js
+
+promise_test(function() {
+ var cacheBuster = token(); // ensures first request is uncached
+ var url = "../resources/cache.py?v=" + cacheBuster;
+ var etag;
+
+ // make the first request
+ return fetch(url).then(function(response) {
+ // ensure we're getting the regular, uncached response
+ assert_equals(response.status, 200);
+ assert_equals(response.headers.get("X-HTTP-STATUS"), null)
+
+ return response.text(); // consuming the body, just to be safe
+ }).then(function(body) {
+ // make a second request
+ return fetch(url);
+ }).then(function(response) {
+ // while the server responds with 304 if our browser sent the correct
+ // If-None-Match request header, at the JavaScript level this surfaces
+ // as 200
+ assert_equals(response.status, 200);
+ assert_equals(response.headers.get("X-HTTP-STATUS"), "304")
+
+ etag = response.headers.get("ETag")
+
+ return response.text(); // consuming the body, just to be safe
+ }).then(function(body) {
+ // make a third request, explicitly setting If-None-Match request header
+ var headers = { "If-None-Match": etag }
+ return fetch(url, { headers: headers })
+ }).then(function(response) {
+ // 304 now surfaces thanks to the explicit If-None-Match request header
+ assert_equals(response.status, 304);
+ });
+}, "Testing conditional GET with ETags");
diff --git a/testing/web-platform/tests/fetch/api/basic/error-after-response.any.js b/testing/web-platform/tests/fetch/api/basic/error-after-response.any.js
new file mode 100644
index 0000000000..f7114425f9
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/error-after-response.any.js
@@ -0,0 +1,24 @@
+// META: title=Fetch: network timeout after receiving the HTTP response headers
+// META: global=window,worker
+// META: timeout=long
+// META: script=../resources/utils.js
+
+function checkReader(test, reader, promiseToTest)
+{
+ return reader.read().then((value) => {
+ validateBufferFromString(value.value, "TEST_CHUNK", "Should receive first chunk");
+ return promise_rejects_js(test, TypeError, promiseToTest(reader));
+ });
+}
+
+promise_test((test) => {
+ return fetch("../resources/bad-chunk-encoding.py?count=1").then((response) => {
+ return checkReader(test, response.body.getReader(), reader => reader.read());
+ });
+}, "Response reader read() promise should reject after a network error happening after resolving fetch promise");
+
+promise_test((test) => {
+ return fetch("../resources/bad-chunk-encoding.py?count=1").then((response) => {
+ return checkReader(test, response.body.getReader(), reader => reader.closed);
+ });
+}, "Response reader closed promise should reject after a network error happening after resolving fetch promise");
diff --git a/testing/web-platform/tests/fetch/api/basic/header-value-combining.any.js b/testing/web-platform/tests/fetch/api/basic/header-value-combining.any.js
new file mode 100644
index 0000000000..bb70d87d25
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/header-value-combining.any.js
@@ -0,0 +1,15 @@
+// META: global=window,worker
+
+[
+ ["content-length", "0", "header-content-length"],
+ ["content-length", "0, 0", "header-content-length-twice"],
+ ["double-trouble", ", ", "headers-double-empty"],
+ ["foo-test", "1, 2, 3", "headers-basic"],
+ ["heya", ", \u000B\u000C, 1, , , 2", "headers-some-are-empty"],
+ ["www-authenticate", "1, 2, 3, 4", "headers-www-authenticate"],
+].forEach(testValues => {
+ promise_test(async t => {
+ const response = await fetch("../../../xhr/resources/" + testValues[2] + ".asis");
+ assert_equals(response.headers.get(testValues[0]), testValues[1]);
+ }, "response.headers.get('" + testValues[0] + "') expects " + testValues[1]);
+});
diff --git a/testing/web-platform/tests/fetch/api/basic/header-value-null-byte.any.js b/testing/web-platform/tests/fetch/api/basic/header-value-null-byte.any.js
new file mode 100644
index 0000000000..741d83bf7a
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/header-value-null-byte.any.js
@@ -0,0 +1,5 @@
+// META: global=window,worker
+
+promise_test(t => {
+ return promise_rejects_js(t, TypeError, fetch("../../../xhr/resources/parse-headers.py?my-custom-header="+encodeURIComponent("x\0x")));
+}, "Ensure fetch() rejects null bytes in headers");
diff --git a/testing/web-platform/tests/fetch/api/basic/historical.any.js b/testing/web-platform/tests/fetch/api/basic/historical.any.js
new file mode 100644
index 0000000000..c808126216
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/historical.any.js
@@ -0,0 +1,17 @@
+// META: global=window,worker
+
+test(() => {
+ assert_false("getAll" in new Headers());
+ assert_false("getAll" in Headers.prototype);
+}, "Headers object no longer has a getAll() method");
+
+test(() => {
+ assert_false("type" in new Request("about:blank"));
+ assert_false("type" in Request.prototype);
+}, "'type' getter should not exist on Request objects");
+
+// See https://github.com/whatwg/fetch/pull/979 for the removal
+test(() => {
+ assert_false("trailer" in new Response());
+ assert_false("trailer" in Response.prototype);
+}, "Response object no longer has a trailer getter");
diff --git a/testing/web-platform/tests/fetch/api/basic/http-response-code.any.js b/testing/web-platform/tests/fetch/api/basic/http-response-code.any.js
new file mode 100644
index 0000000000..1fd312a3e9
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/http-response-code.any.js
@@ -0,0 +1,14 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+
+promise_test(async (test) => {
+ const resp = await fetch(
+ "/fetch/connection-pool/resources/network-partition-key.py?"
+ + `status=425&uuid=${token()}&partition_id=${get_host_info().ORIGIN}`
+ + `&dispatch=check_partition&addcounter=true`);
+ assert_equals(resp.status, 425);
+ const text = await resp.text();
+ assert_equals(text, "ok. Request was sent 1 times. 1 connections were created.");
+}, "Fetch on 425 response should not be retried for non TLS early data.");
diff --git a/testing/web-platform/tests/fetch/api/basic/integrity.sub.any.js b/testing/web-platform/tests/fetch/api/basic/integrity.sub.any.js
new file mode 100644
index 0000000000..e3cfd1b2f6
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/integrity.sub.any.js
@@ -0,0 +1,87 @@
+// META: global=window,dedicatedworker,sharedworker
+// META: script=../resources/utils.js
+
+function integrity(desc, url, integrity, initRequestMode, shouldPass) {
+ var fetchRequestInit = {'integrity': integrity}
+ if (!!initRequestMode && initRequestMode !== "") {
+ fetchRequestInit.mode = initRequestMode;
+ }
+
+ if (shouldPass) {
+ promise_test(function(test) {
+ return fetch(url, fetchRequestInit).then(function(resp) {
+ if (initRequestMode !== "no-cors") {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ } else {
+ assert_equals(resp.status, 0, "Opaque response's status is 0");
+ assert_equals(resp.type, "opaque");
+ }
+ });
+ }, desc);
+ } else {
+ promise_test(function(test) {
+ return promise_rejects_js(test, TypeError, fetch(url, fetchRequestInit));
+ }, desc);
+ }
+}
+
+const topSha256 = "sha256-KHIDZcXnR2oBHk9DrAA+5fFiR6JjudYjqoXtMR1zvzk=";
+const topSha384 = "sha384-MgZYnnAzPM/MjhqfOIMfQK5qcFvGZsGLzx4Phd7/A8fHTqqLqXqKo8cNzY3xEPTL";
+const topSha512 = "sha512-D6yns0qxG0E7+TwkevZ4Jt5t7Iy3ugmAajG/dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg==";
+const topSha512wrongpadding = "sha512-D6yns0qxG0E7+TwkevZ4Jt5t7Iy3ugmAajG/dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg";
+const topSha512base64url = "sha512-D6yns0qxG0E7-TwkevZ4Jt5t7Iy3ugmAajG_dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg==";
+const topSha512base64url_nopadding = "sha512-D6yns0qxG0E7-TwkevZ4Jt5t7Iy3ugmAajG_dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg";
+const invalidSha256 = "sha256-dKUcPOn/AlUjWIwcHeHNqYXPlvyGiq+2dWOdFcE+24I=";
+const invalidSha512 = "sha512-oUceBRNxPxnY60g/VtPCj2syT4wo4EZh2CgYdWy9veW8+OsReTXoh7dizMGZafvx9+QhMS39L/gIkxnPIn41Zg==";
+
+const path = dirname(location.pathname) + RESOURCES_DIR + "top.txt";
+const url = path;
+const corsUrl =
+ `http://{{host}}:{{ports[http][1]}}${path}?pipe=header(Access-Control-Allow-Origin,*)`;
+const corsUrl2 = `https://{{host}}:{{ports[https][0]}}${path}`
+
+integrity("Empty string integrity", url, "", /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("SHA-256 integrity", url, topSha256, /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("SHA-384 integrity", url, topSha384, /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("SHA-512 integrity", url, topSha512, /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("SHA-512 integrity with missing padding", url, topSha512wrongpadding,
+ /* initRequestMode */ undefined, /* shouldPass */ true);
+integrity("SHA-512 integrity base64url encoded", url, topSha512base64url,
+ /* initRequestMode */ undefined, /* shouldPass */ true);
+integrity("SHA-512 integrity base64url encoded with missing padding", url,
+ topSha512base64url_nopadding, /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("Invalid integrity", url, invalidSha256,
+ /* initRequestMode */ undefined, /* shouldPass */ false);
+integrity("Multiple integrities: valid stronger than invalid", url,
+ invalidSha256 + " " + topSha384, /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("Multiple integrities: invalid stronger than valid",
+ url, invalidSha512 + " " + topSha384, /* initRequestMode */ undefined,
+ /* shouldPass */ false);
+integrity("Multiple integrities: invalid as strong as valid", url,
+ invalidSha512 + " " + topSha512, /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("Multiple integrities: both are valid", url,
+ topSha384 + " " + topSha512, /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("Multiple integrities: both are invalid", url,
+ invalidSha256 + " " + invalidSha512, /* initRequestMode */ undefined,
+ /* shouldPass */ false);
+integrity("CORS empty integrity", corsUrl, "", /* initRequestMode */ undefined,
+ /* shouldPass */ true);
+integrity("CORS SHA-512 integrity", corsUrl, topSha512,
+ /* initRequestMode */ undefined, /* shouldPass */ true);
+integrity("CORS invalid integrity", corsUrl, invalidSha512,
+ /* initRequestMode */ undefined, /* shouldPass */ false);
+
+integrity("Empty string integrity for opaque response", corsUrl2, "",
+ /* initRequestMode */ "no-cors", /* shouldPass */ true);
+integrity("SHA-* integrity for opaque response", corsUrl2, topSha512,
+ /* initRequestMode */ "no-cors", /* shouldPass */ false);
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/basic/keepalive.any.js b/testing/web-platform/tests/fetch/api/basic/keepalive.any.js
new file mode 100644
index 0000000000..d6ec1f6792
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/keepalive.any.js
@@ -0,0 +1,42 @@
+// META: global=window
+// META: timeout=long
+// META: title=Fetch API: keepalive handling
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=../resources/keepalive-helper.js
+
+'use strict';
+
+const {
+ HTTP_NOTSAMESITE_ORIGIN,
+ HTTP_REMOTE_ORIGIN,
+ HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT
+} = get_host_info();
+
+/**
+ * In a different-site iframe, test to fetch a keepalive URL on the specified
+ * document event.
+ */
+function keepaliveSimpleRequestTest(method) {
+ for (const evt of ['load', 'pagehide', 'unload']) {
+ const desc =
+ `[keepalive] simple ${method} request on '${evt}' [no payload]`;
+ promise_test(async (test) => {
+ const token1 = token();
+ const iframe = document.createElement('iframe');
+ iframe.src = getKeepAliveIframeUrl(token1, method, {sendOn: evt});
+ document.body.appendChild(iframe);
+ await iframeLoaded(iframe);
+ if (evt != 'load') {
+ iframe.remove();
+ }
+ assert_equals(await getTokenFromMessage(), token1);
+
+ assertStashedTokenAsync(desc, token1);
+ }, `${desc}; setting up`);
+ }
+}
+
+for (const method of ['GET', 'POST']) {
+ keepaliveSimpleRequestTest(method);
+}
diff --git a/testing/web-platform/tests/fetch/api/basic/mediasource.window.js b/testing/web-platform/tests/fetch/api/basic/mediasource.window.js
new file mode 100644
index 0000000000..1f89595393
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/mediasource.window.js
@@ -0,0 +1,5 @@
+promise_test(t => {
+ const mediaSource = new MediaSource(),
+ mediaSourceURL = URL.createObjectURL(mediaSource);
+ return promise_rejects_js(t, TypeError, fetch(mediaSourceURL));
+}, "Cannot fetch blob: URL from a MediaSource");
diff --git a/testing/web-platform/tests/fetch/api/basic/mode-no-cors.sub.any.js b/testing/web-platform/tests/fetch/api/basic/mode-no-cors.sub.any.js
new file mode 100644
index 0000000000..a4abcac55f
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/mode-no-cors.sub.any.js
@@ -0,0 +1,29 @@
+// META: script=../resources/utils.js
+
+function fetchNoCors(url, isOpaqueFiltered) {
+ var urlQuery = "?pipe=header(x-is-filtered,value)"
+ promise_test(function(test) {
+ if (isOpaqueFiltered)
+ return fetch(url + urlQuery, {"mode": "no-cors"}).then(function(resp) {
+ assert_equals(resp.status, 0, "Opaque filter: status is 0");
+ assert_equals(resp.statusText, "", "Opaque filter: statusText is \"\"");
+ assert_equals(resp.url, "", "Opaque filter: url is \"\"");
+ assert_equals(resp.type , "opaque", "Opaque filter: response's type is opaque");
+ assert_equals(resp.headers.get("x-is-filtered"), null, "Header x-is-filtered is filtered");
+ });
+ else
+ return fetch(url + urlQuery, {"mode": "no-cors"}).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ assert_equals(resp.headers.get("x-is-filtered"), "value", "Header x-is-filtered is not filtered");
+ });
+ }, "Fetch "+ url + " with no-cors mode");
+}
+
+fetchNoCors(RESOURCES_DIR + "top.txt", false);
+fetchNoCors("http://{{host}}:{{ports[http][0]}}/fetch/api/resources/top.txt", false);
+fetchNoCors("https://{{host}}:{{ports[https][0]}}/fetch/api/resources/top.txt", true);
+fetchNoCors("http://{{host}}:{{ports[http][1]}}/fetch/api/resources/top.txt", true);
+
+done();
+
diff --git a/testing/web-platform/tests/fetch/api/basic/mode-same-origin.any.js b/testing/web-platform/tests/fetch/api/basic/mode-same-origin.any.js
new file mode 100644
index 0000000000..1457702f1b
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/mode-same-origin.any.js
@@ -0,0 +1,28 @@
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function fetchSameOrigin(url, shouldPass) {
+ promise_test(function(test) {
+ if (shouldPass)
+ return fetch(url , {"mode": "same-origin"}).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type, "basic", "response type is basic");
+ });
+ else
+ return promise_rejects_js(test, TypeError, fetch(url, {mode: "same-origin"}));
+ }, "Fetch "+ url + " with same-origin mode");
+}
+
+var host_info = get_host_info();
+
+fetchSameOrigin(RESOURCES_DIR + "top.txt", true);
+fetchSameOrigin(host_info.HTTP_ORIGIN + "/fetch/api/resources/top.txt", true);
+fetchSameOrigin(host_info.HTTPS_ORIGIN + "/fetch/api/resources/top.txt", false);
+fetchSameOrigin(host_info.HTTP_REMOTE_ORIGIN + "/fetch/api/resources/top.txt", false);
+
+var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py?location=";
+
+fetchSameOrigin(redirPath + RESOURCES_DIR + "top.txt", true);
+fetchSameOrigin(redirPath + host_info.HTTP_ORIGIN + "/fetch/api/resources/top.txt", true);
+fetchSameOrigin(redirPath + host_info.HTTPS_ORIGIN + "/fetch/api/resources/top.txt", false);
+fetchSameOrigin(redirPath + host_info.HTTP_REMOTE_ORIGIN + "/fetch/api/resources/top.txt", false);
diff --git a/testing/web-platform/tests/fetch/api/basic/referrer.any.js b/testing/web-platform/tests/fetch/api/basic/referrer.any.js
new file mode 100644
index 0000000000..85745e692a
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/referrer.any.js
@@ -0,0 +1,29 @@
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function runTest(url, init, expectedReferrer, title) {
+ promise_test(function(test) {
+ url += (url.indexOf('?') !== -1 ? '&' : '?') + "headers=referer&cors";
+
+ return fetch(url , init).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.headers.get("x-request-referer"), expectedReferrer, "Request's referrer is correct");
+ });
+ }, title);
+}
+
+var fetchedUrl = RESOURCES_DIR + "inspect-headers.py";
+var corsFetchedUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py";
+var redirectUrl = RESOURCES_DIR + "redirect.py?location=" ;
+var corsRedirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py?location=";
+
+runTest(fetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, location.toString(), "origin-when-cross-origin policy on a same-origin URL");
+runTest(corsFetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, get_host_info().HTTP_ORIGIN + "/", "origin-when-cross-origin policy on a cross-origin URL");
+runTest(redirectUrl + corsFetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, get_host_info().HTTP_ORIGIN + "/", "origin-when-cross-origin policy on a cross-origin URL after same-origin redirection");
+runTest(corsRedirectUrl + fetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, get_host_info().HTTP_ORIGIN + "/", "origin-when-cross-origin policy on a same-origin URL after cross-origin redirection");
+
+
+var referrerUrlWithCredentials = get_host_info().HTTP_ORIGIN.replace("http://", "http://username:password@");
+runTest(fetchedUrl, {referrer: referrerUrlWithCredentials}, get_host_info().HTTP_ORIGIN + "/", "Referrer with credentials should be stripped");
+var referrerUrlWithFragmentIdentifier = get_host_info().HTTP_ORIGIN + "#fragmentIdentifier";
+runTest(fetchedUrl, {referrer: referrerUrlWithFragmentIdentifier}, get_host_info().HTTP_ORIGIN + "/", "Referrer with fragment ID should be stripped");
diff --git a/testing/web-platform/tests/fetch/api/basic/request-forbidden-headers.any.js b/testing/web-platform/tests/fetch/api/basic/request-forbidden-headers.any.js
new file mode 100644
index 0000000000..d7560f03a2
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/request-forbidden-headers.any.js
@@ -0,0 +1,82 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function requestValidOverrideHeaders(desc, validHeaders) {
+ var url = RESOURCES_DIR + "inspect-headers.py";
+ var requestInit = {"headers": validHeaders}
+ var urlParameters = "?headers=" + Object.keys(validHeaders).join("|");
+
+ promise_test(function(test){
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ for (var header in validHeaders)
+ assert_equals(resp.headers.get("x-request-" + header), validHeaders[header], header + "is not skipped for non-forbidden methods");
+ });
+ }, desc);
+}
+
+requestForbiddenHeaders("Accept-Charset is a forbidden request header", {"Accept-Charset": "utf-8"});
+requestForbiddenHeaders("Accept-Encoding is a forbidden request header", {"Accept-Encoding": ""});
+
+requestForbiddenHeaders("Access-Control-Request-Headers is a forbidden request header", {"Access-Control-Request-Headers": ""});
+requestForbiddenHeaders("Access-Control-Request-Method is a forbidden request header", {"Access-Control-Request-Method": ""});
+requestForbiddenHeaders("Connection is a forbidden request header", {"Connection": "close"});
+requestForbiddenHeaders("Content-Length is a forbidden request header", {"Content-Length": "42"});
+requestForbiddenHeaders("Cookie is a forbidden request header", {"Cookie": "cookie=none"});
+requestForbiddenHeaders("Cookie2 is a forbidden request header", {"Cookie2": "cookie2=none"});
+requestForbiddenHeaders("Date is a forbidden request header", {"Date": "Wed, 04 May 1988 22:22:22 GMT"});
+requestForbiddenHeaders("DNT is a forbidden request header", {"DNT": "4"});
+requestForbiddenHeaders("Expect is a forbidden request header", {"Expect": "100-continue"});
+requestForbiddenHeaders("Host is a forbidden request header", {"Host": "http://wrong-host.com"});
+requestForbiddenHeaders("Keep-Alive is a forbidden request header", {"Keep-Alive": "timeout=15"});
+requestForbiddenHeaders("Origin is a forbidden request header", {"Origin": "http://wrong-origin.com"});
+requestForbiddenHeaders("Referer is a forbidden request header", {"Referer": "http://wrong-referer.com"});
+requestForbiddenHeaders("TE is a forbidden request header", {"TE": "trailers"});
+requestForbiddenHeaders("Trailer is a forbidden request header", {"Trailer": "Accept"});
+requestForbiddenHeaders("Transfer-Encoding is a forbidden request header", {"Transfer-Encoding": "chunked"});
+requestForbiddenHeaders("Upgrade is a forbidden request header", {"Upgrade": "HTTP/2.0"});
+requestForbiddenHeaders("Via is a forbidden request header", {"Via": "1.1 nowhere.com"});
+requestForbiddenHeaders("Proxy- is a forbidden request header", {"Proxy-": "value"});
+requestForbiddenHeaders("Proxy-Test is a forbidden request header", {"Proxy-Test": "value"});
+requestForbiddenHeaders("Sec- is a forbidden request header", {"Sec-": "value"});
+requestForbiddenHeaders("Sec-Test is a forbidden request header", {"Sec-Test": "value"});
+
+let forbiddenMethods = [
+ "TRACE",
+ "TRACK",
+ "CONNECT",
+ "trace",
+ "track",
+ "connect",
+ "trace,",
+ "GET,track ",
+ " connect",
+];
+
+let overrideHeaders = [
+ "x-http-method-override",
+ "x-http-method",
+ "x-method-override",
+ "X-HTTP-METHOD-OVERRIDE",
+ "X-HTTP-METHOD",
+ "X-METHOD-OVERRIDE",
+];
+
+for (forbiddenMethod of forbiddenMethods) {
+ for (overrideHeader of overrideHeaders) {
+ requestForbiddenHeaders(`header ${overrideHeader} is forbidden to use value ${forbiddenMethod}`, {[overrideHeader]: forbiddenMethod});
+ }
+}
+
+let permittedValues = [
+ "GETTRACE",
+ "GET",
+ "\",TRACE\",",
+];
+
+for (permittedValue of permittedValues) {
+ for (overrideHeader of overrideHeaders) {
+ requestValidOverrideHeaders(`header ${overrideHeader} is allowed to use value ${permittedValue}`, {[overrideHeader]: permittedValue});
+ }
+}
diff --git a/testing/web-platform/tests/fetch/api/basic/request-head.any.js b/testing/web-platform/tests/fetch/api/basic/request-head.any.js
new file mode 100644
index 0000000000..e0b6afa079
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/request-head.any.js
@@ -0,0 +1,6 @@
+// META: global=window,worker
+
+promise_test(function(test) {
+ var requestInit = {"method": "HEAD", "body": "test"};
+ return promise_rejects_js(test, TypeError, fetch(".", requestInit));
+}, "Fetch with HEAD with body");
diff --git a/testing/web-platform/tests/fetch/api/basic/request-headers-case.any.js b/testing/web-platform/tests/fetch/api/basic/request-headers-case.any.js
new file mode 100644
index 0000000000..4c10e717f8
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/request-headers-case.any.js
@@ -0,0 +1,13 @@
+// META: global=window,worker
+
+promise_test(() => {
+ return fetch("/xhr/resources/echo-headers.py", {headers: [["THIS-is-A-test", 1], ["THIS-IS-A-TEST", 2]] }).then(res => res.text()).then(body => {
+ assert_regexp_match(body, /THIS-is-A-test: 1, 2/)
+ })
+}, "Multiple headers with the same name, different case (THIS-is-A-test first)")
+
+promise_test(() => {
+ return fetch("/xhr/resources/echo-headers.py", {headers: [["THIS-IS-A-TEST", 1], ["THIS-is-A-test", 2]] }).then(res => res.text()).then(body => {
+ assert_regexp_match(body, /THIS-IS-A-TEST: 1, 2/)
+ })
+}, "Multiple headers with the same name, different case (THIS-IS-A-TEST first)")
diff --git a/testing/web-platform/tests/fetch/api/basic/request-headers-nonascii.any.js b/testing/web-platform/tests/fetch/api/basic/request-headers-nonascii.any.js
new file mode 100644
index 0000000000..4a9a801138
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/request-headers-nonascii.any.js
@@ -0,0 +1,29 @@
+// META: global=window,worker
+
+// This tests characters that are not
+// https://infra.spec.whatwg.org/#ascii-code-point
+// but are still
+// https://infra.spec.whatwg.org/#byte-value
+// in request header values.
+// Such request header values are valid and thus sent to servers.
+// Characters outside the #byte-value range are tested e.g. in
+// fetch/api/headers/headers-errors.html.
+
+promise_test(() => {
+ return fetch(
+ "../resources/inspect-headers.py?headers=accept|x-test",
+ {headers: {
+ "Accept": "before-æøå-after",
+ "X-Test": "before-ß-after"
+ }})
+ .then(res => {
+ assert_equals(
+ res.headers.get("x-request-accept"),
+ "before-æøå-after",
+ "Accept Header");
+ assert_equals(
+ res.headers.get("x-request-x-test"),
+ "before-ß-after",
+ "X-Test Header");
+ });
+}, "Non-ascii bytes in request headers");
diff --git a/testing/web-platform/tests/fetch/api/basic/request-headers.any.js b/testing/web-platform/tests/fetch/api/basic/request-headers.any.js
new file mode 100644
index 0000000000..ac54256e4c
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/request-headers.any.js
@@ -0,0 +1,82 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function checkContentType(contentType, body)
+{
+ if (self.FormData && body instanceof self.FormData) {
+ assert_true(contentType.startsWith("multipart/form-data; boundary="), "Request should have header content-type starting with multipart/form-data; boundary=, but got " + contentType);
+ return;
+ }
+
+ var expectedContentType = "text/plain;charset=UTF-8";
+ if(body === null || body instanceof ArrayBuffer || body.buffer instanceof ArrayBuffer)
+ expectedContentType = null;
+ else if (body instanceof Blob)
+ expectedContentType = body.type ? body.type : null;
+ else if (body instanceof URLSearchParams)
+ expectedContentType = "application/x-www-form-urlencoded;charset=UTF-8";
+
+ assert_equals(contentType , expectedContentType, "Request should have header content-type: " + expectedContentType);
+}
+
+function requestHeaders(desc, url, method, body, expectedOrigin, expectedContentLength) {
+ var urlParameters = "?headers=origin|user-agent|accept-charset|content-length|content-type";
+ var requestInit = {"method": method}
+ promise_test(function(test){
+ if (typeof body === "function")
+ body = body();
+ if (body)
+ requestInit["body"] = body;
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ assert_true(resp.headers.has("x-request-user-agent"), "Request has header user-agent");
+ assert_false(resp.headers.has("accept-charset"), "Request has header accept-charset");
+ assert_equals(resp.headers.get("x-request-origin") , expectedOrigin, "Request should have header origin: " + expectedOrigin);
+ if (expectedContentLength !== undefined)
+ assert_equals(resp.headers.get("x-request-content-length") , expectedContentLength, "Request should have header content-length: " + expectedContentLength);
+ checkContentType(resp.headers.get("x-request-content-type"), body);
+ });
+ }, desc);
+}
+
+var url = RESOURCES_DIR + "inspect-headers.py"
+
+requestHeaders("Fetch with GET", url, "GET", null, null, null);
+requestHeaders("Fetch with HEAD", url, "HEAD", null, null, null);
+requestHeaders("Fetch with PUT without body", url, "POST", null, location.origin, "0");
+requestHeaders("Fetch with PUT with body", url, "PUT", "Request's body", location.origin, "14");
+requestHeaders("Fetch with POST without body", url, "POST", null, location.origin, "0");
+requestHeaders("Fetch with POST with text body", url, "POST", "Request's body", location.origin, "14");
+requestHeaders("Fetch with POST with FormData body", url, "POST", function() { return new FormData(); }, location.origin);
+requestHeaders("Fetch with POST with URLSearchParams body", url, "POST", function() { return new URLSearchParams("name=value"); }, location.origin, "10");
+requestHeaders("Fetch with POST with Blob body", url, "POST", new Blob(["Test"]), location.origin, "4");
+requestHeaders("Fetch with POST with ArrayBuffer body", url, "POST", new ArrayBuffer(4), location.origin, "4");
+requestHeaders("Fetch with POST with Uint8Array body", url, "POST", new Uint8Array(4), location.origin, "4");
+requestHeaders("Fetch with POST with Int8Array body", url, "POST", new Int8Array(4), location.origin, "4");
+requestHeaders("Fetch with POST with Float32Array body", url, "POST", new Float32Array(1), location.origin, "4");
+requestHeaders("Fetch with POST with Float64Array body", url, "POST", new Float64Array(1), location.origin, "8");
+requestHeaders("Fetch with POST with DataView body", url, "POST", new DataView(new ArrayBuffer(8), 0, 4), location.origin, "4");
+requestHeaders("Fetch with POST with Blob body with mime type", url, "POST", new Blob(["Test"], { type: "text/maybe" }), location.origin, "4");
+requestHeaders("Fetch with Chicken", url, "Chicken", null, location.origin, null);
+requestHeaders("Fetch with Chicken with body", url, "Chicken", "Request's body", location.origin, "14");
+
+function requestOriginHeader(method, mode, needsOrigin) {
+ promise_test(function(test){
+ return fetch(url + "?headers=origin", {method:method, mode:mode}).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ if(needsOrigin)
+ assert_equals(resp.headers.get("x-request-origin") , location.origin, "Request should have an Origin header with origin: " + location.origin);
+ else
+ assert_equals(resp.headers.get("x-request-origin"), null, "Request should not have an Origin header")
+ });
+ }, "Fetch with " + method + " and mode \"" + mode + "\" " + (needsOrigin ? "needs" : "does not need") + " an Origin header");
+}
+
+requestOriginHeader("GET", "cors", false);
+requestOriginHeader("POST", "same-origin", true);
+requestOriginHeader("POST", "no-cors", true);
+requestOriginHeader("PUT", "same-origin", true);
+requestOriginHeader("TacO", "same-origin", true);
+requestOriginHeader("TacO", "cors", true);
diff --git a/testing/web-platform/tests/fetch/api/basic/request-private-network-headers.tentative.any.js b/testing/web-platform/tests/fetch/api/basic/request-private-network-headers.tentative.any.js
new file mode 100644
index 0000000000..9662a91c17
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/request-private-network-headers.tentative.any.js
@@ -0,0 +1,18 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+requestForbiddenHeaders(
+ 'Access-Control-Request-Private-Network is a forbidden request header',
+ {'Access-Control-Request-Private-Network': ''});
+
+var invalidRequestHeaders = [
+ ["Access-Control-Request-Private-Network", "KO"],
+];
+
+invalidRequestHeaders.forEach(function(header) {
+ test(function() {
+ var request = new Request("");
+ request.headers.set(header[0], header[1]);
+ assert_equals(request.headers.get(header[0]), null);
+ }, "Adding invalid request header \"" + header[0] + ": " + header[1] + "\"");
+});
diff --git a/testing/web-platform/tests/fetch/api/basic/request-referrer-redirected-worker.html b/testing/web-platform/tests/fetch/api/basic/request-referrer-redirected-worker.html
new file mode 100644
index 0000000000..bdea1e1853
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/request-referrer-redirected-worker.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch in worker: referrer header</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ let finalURL = "/fetch/api/basic/request-referrer.any.worker.js";
+ let url = "/fetch/api/resources/redirect.py?location=" +
+ encodeURIComponent(finalURL);
+ fetch_tests_from_worker(new Worker(url));
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/fetch/api/basic/request-referrer.any.js b/testing/web-platform/tests/fetch/api/basic/request-referrer.any.js
new file mode 100644
index 0000000000..0c3357642d
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/request-referrer.any.js
@@ -0,0 +1,24 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function testReferrer(referrer, expected, desc) {
+ promise_test(function(test) {
+ var url = RESOURCES_DIR + "inspect-headers.py?headers=referer"
+ var req = new Request(url, { referrer: referrer });
+ return fetch(req).then(function(resp) {
+ var actual = resp.headers.get("x-request-referer");
+ if (expected) {
+ assert_equals(actual, expected, "request's referer should be: " + expected);
+ return;
+ }
+ if (actual) {
+ assert_equals(actual, "", "request's referer should be empty");
+ }
+ });
+ }, desc);
+}
+
+testReferrer("about:client", self.location.href, 'about:client referrer');
+
+var fooURL = new URL("./foo", self.location).href;
+testReferrer(fooURL, fooURL, 'url referrer');
diff --git a/testing/web-platform/tests/fetch/api/basic/request-upload.any.js b/testing/web-platform/tests/fetch/api/basic/request-upload.any.js
new file mode 100644
index 0000000000..9168aa1154
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/request-upload.any.js
@@ -0,0 +1,135 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function testUpload(desc, url, method, createBody, expectedBody) {
+ const requestInit = {method};
+ promise_test(function(test){
+ const body = createBody();
+ if (body) {
+ requestInit["body"] = body;
+ requestInit.duplex = "half";
+ }
+ return fetch(url, requestInit).then(function(resp) {
+ return resp.text().then((text)=> {
+ assert_equals(text, expectedBody);
+ });
+ });
+ }, desc);
+}
+
+function testUploadFailure(desc, url, method, createBody) {
+ const requestInit = {method};
+ promise_test(t => {
+ const body = createBody();
+ if (body) {
+ requestInit["body"] = body;
+ }
+ return promise_rejects_js(t, TypeError, fetch(url, requestInit));
+ }, desc);
+}
+
+const url = RESOURCES_DIR + "echo-content.py"
+
+testUpload("Fetch with PUT with body", url,
+ "PUT",
+ () => "Request's body",
+ "Request's body");
+testUpload("Fetch with POST with text body", url,
+ "POST",
+ () => "Request's body",
+ "Request's body");
+testUpload("Fetch with POST with URLSearchParams body", url,
+ "POST",
+ () => new URLSearchParams("name=value"),
+ "name=value");
+testUpload("Fetch with POST with Blob body", url,
+ "POST",
+ () => new Blob(["Test"]),
+ "Test");
+testUpload("Fetch with POST with ArrayBuffer body", url,
+ "POST",
+ () => new ArrayBuffer(4),
+ "\0\0\0\0");
+testUpload("Fetch with POST with Uint8Array body", url,
+ "POST",
+ () => new Uint8Array(4),
+ "\0\0\0\0");
+testUpload("Fetch with POST with Int8Array body", url,
+ "POST",
+ () => new Int8Array(4),
+ "\0\0\0\0");
+testUpload("Fetch with POST with Float32Array body", url,
+ "POST",
+ () => new Float32Array(1),
+ "\0\0\0\0");
+testUpload("Fetch with POST with Float64Array body", url,
+ "POST",
+ () => new Float64Array(1),
+ "\0\0\0\0\0\0\0\0");
+testUpload("Fetch with POST with DataView body", url,
+ "POST",
+ () => new DataView(new ArrayBuffer(8), 0, 4),
+ "\0\0\0\0");
+testUpload("Fetch with POST with Blob body with mime type", url,
+ "POST",
+ () => new Blob(["Test"], { type: "text/maybe" }),
+ "Test");
+
+testUploadFailure("Fetch with POST with ReadableStream containing String", url,
+ "POST",
+ () => {
+ return new ReadableStream({start: controller => {
+ controller.enqueue("Test");
+ controller.close();
+ }})
+ });
+testUploadFailure("Fetch with POST with ReadableStream containing null", url,
+ "POST",
+ () => {
+ return new ReadableStream({start: controller => {
+ controller.enqueue(null);
+ controller.close();
+ }})
+ });
+testUploadFailure("Fetch with POST with ReadableStream containing number", url,
+ "POST",
+ () => {
+ return new ReadableStream({start: controller => {
+ controller.enqueue(99);
+ controller.close();
+ }})
+ });
+testUploadFailure("Fetch with POST with ReadableStream containing ArrayBuffer", url,
+ "POST",
+ () => {
+ return new ReadableStream({start: controller => {
+ controller.enqueue(new ArrayBuffer());
+ controller.close();
+ }})
+ });
+testUploadFailure("Fetch with POST with ReadableStream containing Blob", url,
+ "POST",
+ () => {
+ return new ReadableStream({start: controller => {
+ controller.enqueue(new Blob());
+ controller.close();
+ }})
+ });
+
+promise_test(async (test) => {
+ const resp = await fetch(
+ "/fetch/connection-pool/resources/network-partition-key.py?"
+ + `status=421&uuid=${token()}&partition_id=${get_host_info().ORIGIN}`
+ + `&dispatch=check_partition&addcounter=true`,
+ {method: "POST", body: "foobar"});
+ assert_equals(resp.status, 421);
+ const text = await resp.text();
+ assert_equals(text, "ok. Request was sent 2 times. 2 connections were created.");
+}, "Fetch with POST with text body on 421 response should be retried once on new connection.");
+
+promise_test(async (test) => {
+ const body = new ReadableStream({start: c => c.close()});
+ await promise_rejects_js(test, TypeError, fetch('/', {method: 'POST', body}));
+}, "Streaming upload shouldn't work on Http/1.1.");
diff --git a/testing/web-platform/tests/fetch/api/basic/request-upload.h2.any.js b/testing/web-platform/tests/fetch/api/basic/request-upload.h2.any.js
new file mode 100644
index 0000000000..eedc2bf6a7
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/request-upload.h2.any.js
@@ -0,0 +1,186 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+
+const duplex = "half";
+
+async function assertUpload(url, method, createBody, expectedBody) {
+ const requestInit = {method};
+ const body = createBody();
+ if (body) {
+ requestInit["body"] = body;
+ requestInit.duplex = "half";
+ }
+ const resp = await fetch(url, requestInit);
+ const text = await resp.text();
+ assert_equals(text, expectedBody);
+}
+
+function testUpload(desc, url, method, createBody, expectedBody) {
+ promise_test(async () => {
+ await assertUpload(url, method, createBody, expectedBody);
+ }, desc);
+}
+
+function createStream(chunks) {
+ return new ReadableStream({
+ start: (controller) => {
+ for (const chunk of chunks) {
+ controller.enqueue(chunk);
+ }
+ controller.close();
+ }
+ });
+}
+
+const url = RESOURCES_DIR + "echo-content.h2.py"
+
+testUpload("Fetch with POST with empty ReadableStream", url,
+ "POST",
+ () => {
+ return new ReadableStream({start: controller => {
+ controller.close();
+ }})
+ },
+ "");
+
+testUpload("Fetch with POST with ReadableStream", url,
+ "POST",
+ () => {
+ return new ReadableStream({start: controller => {
+ const encoder = new TextEncoder();
+ controller.enqueue(encoder.encode("Test"));
+ controller.close();
+ }})
+ },
+ "Test");
+
+promise_test(async (test) => {
+ const body = new ReadableStream({start: controller => {
+ const encoder = new TextEncoder();
+ controller.enqueue(encoder.encode("Test"));
+ controller.close();
+ }});
+ const resp = await fetch(
+ "/fetch/connection-pool/resources/network-partition-key.py?"
+ + `status=421&uuid=${token()}&partition_id=${self.origin}`
+ + `&dispatch=check_partition&addcounter=true`,
+ {method: "POST", body: body, duplex});
+ assert_equals(resp.status, 421);
+ const text = await resp.text();
+ assert_equals(text, "ok. Request was sent 1 times. 1 connections were created.");
+}, "Fetch with POST with ReadableStream on 421 response should return the response and not retry.");
+
+promise_test(async (test) => {
+ const request = new Request('', {
+ body: new ReadableStream(),
+ method: 'POST',
+ duplex,
+ });
+
+ assert_equals(request.headers.get('Content-Type'), null, `Request should not have a content-type set`);
+
+ const response = await fetch('data:a/a;charset=utf-8,test', {
+ method: 'POST',
+ body: new ReadableStream(),
+ duplex,
+ });
+
+ assert_equals(await response.text(), 'test', `Response has correct body`);
+}, "Feature detect for POST with ReadableStream");
+
+promise_test(async (test) => {
+ const request = new Request('data:a/a;charset=utf-8,test', {
+ body: new ReadableStream(),
+ method: 'POST',
+ duplex,
+ });
+
+ assert_equals(request.headers.get('Content-Type'), null, `Request should not have a content-type set`);
+ const response = await fetch(request);
+ assert_equals(await response.text(), 'test', `Response has correct body`);
+}, "Feature detect for POST with ReadableStream, using request object");
+
+test(() => {
+ let duplexAccessed = false;
+
+ const request = new Request("", {
+ body: new ReadableStream(),
+ method: "POST",
+ get duplex() {
+ duplexAccessed = true;
+ return "half";
+ },
+ });
+
+ assert_equals(
+ request.headers.get("Content-Type"),
+ null,
+ `Request should not have a content-type set`
+ );
+ assert_true(duplexAccessed, `duplex dictionary property should be accessed`);
+}, "Synchronous feature detect");
+
+// The asserts the synchronousFeatureDetect isn't broken by a partial implementation.
+// An earlier feature detect was broken by Safari implementing streaming bodies as part of Request,
+// but it failed when passed to fetch().
+// This tests ensures that UAs must not implement RequestInit.duplex and streaming request bodies without also implementing the fetch() parts.
+promise_test(async () => {
+ let duplexAccessed = false;
+
+ const request = new Request("", {
+ body: new ReadableStream(),
+ method: "POST",
+ get duplex() {
+ duplexAccessed = true;
+ return "half";
+ },
+ });
+
+ const supported =
+ request.headers.get("Content-Type") === null && duplexAccessed;
+
+ // If the feature detect fails, assume the browser is being truthful (other tests pick up broken cases here)
+ if (!supported) return false;
+
+ await assertUpload(
+ url,
+ "POST",
+ () =>
+ new ReadableStream({
+ start: (controller) => {
+ const encoder = new TextEncoder();
+ controller.enqueue(encoder.encode("Test"));
+ controller.close();
+ },
+ }),
+ "Test"
+ );
+}, "Synchronous feature detect fails if feature unsupported");
+
+promise_test(async (t) => {
+ const body = createStream(["hello"]);
+ const method = "POST";
+ await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex }));
+}, "Streaming upload with body containing a String");
+
+promise_test(async (t) => {
+ const body = createStream([null]);
+ const method = "POST";
+ await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex }));
+}, "Streaming upload with body containing null");
+
+promise_test(async (t) => {
+ const body = createStream([33]);
+ const method = "POST";
+ await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex }));
+}, "Streaming upload with body containing a number");
+
+promise_test(async (t) => {
+ const url = "/fetch/api/resources/authentication.py?realm=test";
+ const body = createStream([]);
+ const method = "POST";
+ await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex }));
+}, "Streaming upload should fail on a 401 response");
+
diff --git a/testing/web-platform/tests/fetch/api/basic/response-null-body.any.js b/testing/web-platform/tests/fetch/api/basic/response-null-body.any.js
new file mode 100644
index 0000000000..bb05892657
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/response-null-body.any.js
@@ -0,0 +1,38 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+const nullBodyStatus = [204, 205, 304];
+const methods = ["GET", "POST", "OPTIONS"];
+
+for (const status of nullBodyStatus) {
+ for (const method of methods) {
+ promise_test(
+ async () => {
+ const url =
+ `${RESOURCES_DIR}status.py?code=${status}&content=hello-world`;
+ const resp = await fetch(url, { method });
+ assert_equals(resp.status, status);
+ assert_equals(resp.body, null, "the body should be null");
+ const text = await resp.text();
+ assert_equals(text, "", "null bodies result in empty text");
+ },
+ `Response.body is null for responses with status=${status} (method=${method})`,
+ );
+ }
+}
+
+promise_test(async () => {
+ const url = `${RESOURCES_DIR}status.py?code=200&content=hello-world`;
+ const resp = await fetch(url, { method: "HEAD" });
+ assert_equals(resp.status, 200);
+ assert_equals(resp.body, null, "the body should be null");
+ const text = await resp.text();
+ assert_equals(text, "", "null bodies result in empty text");
+}, `Response.body is null for responses with method=HEAD`);
+
+promise_test(async (t) => {
+ const integrity = "sha384-UT6f7WCFp32YJnp1is4l/ZYnOeQKpE8xjmdkLOwZ3nIP+tmT2aMRFQGJomjVf5cE";
+ const url = `${RESOURCES_DIR}status.py?code=204&content=hello-world`;
+ const promise = fetch(url, { method: "GET", integrity });
+ promise_rejects_js(t, TypeError, promise);
+}, "Null body status with subresource integrity should abort");
diff --git a/testing/web-platform/tests/fetch/api/basic/response-url.sub.any.js b/testing/web-platform/tests/fetch/api/basic/response-url.sub.any.js
new file mode 100644
index 0000000000..0d123c4294
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/response-url.sub.any.js
@@ -0,0 +1,16 @@
+function checkResponseURL(fetchedURL, expectedURL)
+{
+ promise_test(function() {
+ return fetch(fetchedURL).then(function(response) {
+ assert_equals(response.url, expectedURL);
+ });
+ }, "Testing response url getter with " +fetchedURL);
+}
+
+var baseURL = "http://{{host}}:{{ports[http][0]}}";
+checkResponseURL(baseURL + "/ada", baseURL + "/ada");
+checkResponseURL(baseURL + "/#", baseURL + "/");
+checkResponseURL(baseURL + "/#ada", baseURL + "/");
+checkResponseURL(baseURL + "#ada", baseURL + "/");
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/basic/scheme-about.any.js b/testing/web-platform/tests/fetch/api/basic/scheme-about.any.js
new file mode 100644
index 0000000000..9ef44183c1
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/scheme-about.any.js
@@ -0,0 +1,26 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function checkNetworkError(url, method) {
+ method = method || "GET";
+ const desc = "Fetching " + url.substring(0, 45) + " with method " + method + " is KO"
+ promise_test(function(test) {
+ var promise = fetch(url, { method: method });
+ return promise_rejects_js(test, TypeError, promise);
+ }, desc);
+}
+
+checkNetworkError("about:blank", "GET");
+checkNetworkError("about:blank", "PUT");
+checkNetworkError("about:blank", "POST");
+checkNetworkError("about:invalid.com");
+checkNetworkError("about:config");
+checkNetworkError("about:unicorn");
+
+promise_test(function(test) {
+ var promise = fetch("about:blank", {
+ "method": "GET",
+ "Range": "bytes=1-10"
+ });
+ return promise_rejects_js(test, TypeError, promise);
+}, "Fetching about:blank with range header does not affect behavior");
diff --git a/testing/web-platform/tests/fetch/api/basic/scheme-blob.sub.any.js b/testing/web-platform/tests/fetch/api/basic/scheme-blob.sub.any.js
new file mode 100644
index 0000000000..8afdc033c9
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/scheme-blob.sub.any.js
@@ -0,0 +1,125 @@
+// META: script=../resources/utils.js
+
+function checkFetchResponse(url, data, mime, size, desc) {
+ promise_test(function(test) {
+ size = size.toString();
+ return fetch(url).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type, "basic", "response type is basic");
+ assert_equals(resp.headers.get("Content-Type"), mime, "Content-Type is " + resp.headers.get("Content-Type"));
+ assert_equals(resp.headers.get("Content-Length"), size, "Content-Length is " + resp.headers.get("Content-Length"));
+ return resp.text();
+ }).then(function(bodyAsText) {
+ assert_equals(bodyAsText, data, "Response's body is " + data);
+ });
+ }, desc);
+}
+
+var blob = new Blob(["Blob's data"], { "type" : "text/plain" });
+checkFetchResponse(URL.createObjectURL(blob), "Blob's data", "text/plain", blob.size,
+ "Fetching [GET] URL.createObjectURL(blob) is OK");
+
+function checkKoUrl(url, method, desc) {
+ promise_test(function(test) {
+ var promise = fetch(url, {"method": method});
+ return promise_rejects_js(test, TypeError, promise);
+ }, desc);
+}
+
+var blob2 = new Blob(["Blob's data"], { "type" : "text/plain" });
+checkKoUrl("blob:http://{{domains[www]}}:{{ports[http][0]}}/", "GET",
+ "Fetching [GET] blob:http://{{domains[www]}}:{{ports[http][0]}}/ is KO");
+
+var invalidRequestMethods = [
+ "POST",
+ "OPTIONS",
+ "HEAD",
+ "PUT",
+ "DELETE",
+ "INVALID",
+];
+invalidRequestMethods.forEach(function(method) {
+ checkKoUrl(URL.createObjectURL(blob2), method, "Fetching [" + method + "] URL.createObjectURL(blob) is KO");
+});
+
+checkKoUrl("blob:not-backed-by-a-blob/", "GET",
+ "Fetching [GET] blob:not-backed-by-a-blob/ is KO");
+
+let empty_blob = new Blob([]);
+checkFetchResponse(URL.createObjectURL(empty_blob), "", "", 0,
+ "Fetching URL.createObjectURL(empty_blob) is OK");
+
+let empty_type_blob = new Blob([], {type: ""});
+checkFetchResponse(URL.createObjectURL(empty_type_blob), "", "", 0,
+ "Fetching URL.createObjectURL(empty_type_blob) is OK");
+
+let empty_data_blob = new Blob([], {type: "text/plain"});
+checkFetchResponse(URL.createObjectURL(empty_data_blob), "", "text/plain", 0,
+ "Fetching URL.createObjectURL(empty_data_blob) is OK");
+
+let invalid_type_blob = new Blob([], {type: "invalid"});
+checkFetchResponse(URL.createObjectURL(invalid_type_blob), "", "", 0,
+ "Fetching URL.createObjectURL(invalid_type_blob) is OK");
+
+promise_test(function(test) {
+ return fetch("/images/blue.png").then(function(resp) {
+ return resp.arrayBuffer();
+ }).then(function(image_buffer) {
+ let blob = new Blob([image_buffer]);
+ return fetch(URL.createObjectURL(blob)).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type, "basic", "response type is basic");
+ assert_equals(resp.headers.get("Content-Type"), "", "Content-Type is " + resp.headers.get("Content-Type"));
+ })
+ });
+}, "Blob content is not sniffed for a content type [image/png]");
+
+let simple_xml_string = '<?xml version="1.0" encoding="UTF-8"?><x></x>';
+let xml_blob_no_type = new Blob([simple_xml_string]);
+checkFetchResponse(URL.createObjectURL(xml_blob_no_type), simple_xml_string, "", 45,
+ "Blob content is not sniffed for a content type [text/xml]");
+
+let simple_text_string = 'Hello, World!';
+promise_test(function(test) {
+ let blob = new Blob([simple_text_string], {"type": "text/plain"});
+ let slice = blob.slice(7, simple_text_string.length, "\0");
+ return fetch(URL.createObjectURL(slice)).then(function (resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type, "basic", "response type is basic");
+ assert_equals(resp.headers.get("Content-Type"), "");
+ assert_equals(resp.headers.get("Content-Length"), "6");
+ return resp.text();
+ }).then(function(bodyAsText) {
+ assert_equals(bodyAsText, "World!");
+ });
+}, "Set content type to the empty string for slice with invalid content type");
+
+promise_test(function(test) {
+ let blob = new Blob([simple_text_string], {"type": "text/plain"});
+ let slice = blob.slice(7, simple_text_string.length, "\0");
+ return fetch(URL.createObjectURL(slice)).then(function (resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type, "basic", "response type is basic");
+ assert_equals(resp.headers.get("Content-Type"), "");
+ assert_equals(resp.headers.get("Content-Length"), "6");
+ return resp.text();
+ }).then(function(bodyAsText) {
+ assert_equals(bodyAsText, "World!");
+ });
+}, "Set content type to the empty string for slice with no content type ");
+
+promise_test(function(test) {
+ let blob = new Blob([simple_xml_string]);
+ let slice = blob.slice(0, 38);
+ return fetch(URL.createObjectURL(slice)).then(function (resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type, "basic", "response type is basic");
+ assert_equals(resp.headers.get("Content-Type"), "");
+ assert_equals(resp.headers.get("Content-Length"), "38");
+ return resp.text();
+ }).then(function(bodyAsText) {
+ assert_equals(bodyAsText, '<?xml version="1.0" encoding="UTF-8"?>');
+ });
+}, "Blob.slice should not sniff the content for a content type");
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/basic/scheme-data.any.js b/testing/web-platform/tests/fetch/api/basic/scheme-data.any.js
new file mode 100644
index 0000000000..55df43bd50
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/scheme-data.any.js
@@ -0,0 +1,43 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function checkFetchResponse(url, data, mime, fetchMode, method) {
+ var cut = (url.length >= 40) ? "[...]" : "";
+ var desc = "Fetching " + (method ? "[" + method + "] " : "") + url.substring(0, 40) + cut + " is OK";
+ var init = {"method": method || "GET"};
+ if (fetchMode) {
+ init.mode = fetchMode;
+ desc += " (" + fetchMode + ")";
+ }
+ promise_test(function(test) {
+ return fetch(url, init).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.statusText, "OK", "HTTP statusText is OK");
+ assert_equals(resp.type, "basic", "response type is basic");
+ assert_equals(resp.headers.get("Content-Type"), mime, "Content-Type is " + resp.headers.get("Content-Type"));
+ return resp.text();
+ }).then(function(body) {
+ assert_equals(body, data, "Response's body is correct");
+ });
+ }, desc);
+}
+
+checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII");
+checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII", "same-origin");
+checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII", "cors");
+checkFetchResponse("data:text/plain;base64,cmVzcG9uc2UncyBib2R5", "response's body", "text/plain");
+checkFetchResponse("data:image/png;base64,cmVzcG9uc2UncyBib2R5",
+ "response's body",
+ "image/png");
+checkFetchResponse("data:,response%27s%20body", "response's body", "text/plain;charset=US-ASCII", null, "POST");
+checkFetchResponse("data:,response%27s%20body", "", "text/plain;charset=US-ASCII", null, "HEAD");
+
+function checkKoUrl(url, method, desc) {
+ var cut = (url.length >= 40) ? "[...]" : "";
+ desc = "Fetching [" + method + "] " + url.substring(0, 45) + cut + " is KO"
+ promise_test(function(test) {
+ return promise_rejects_js(test, TypeError, fetch(url, {"method": method}));
+ }, desc);
+}
+
+checkKoUrl("data:notAdataUrl.com", "GET");
diff --git a/testing/web-platform/tests/fetch/api/basic/scheme-others.sub.any.js b/testing/web-platform/tests/fetch/api/basic/scheme-others.sub.any.js
new file mode 100644
index 0000000000..550f69c41b
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/scheme-others.sub.any.js
@@ -0,0 +1,31 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function checkKoUrl(url, desc) {
+ if (!desc)
+ desc = "Fetching " + url.substring(0, 45) + " is KO"
+ promise_test(function(test) {
+ var promise = fetch(url);
+ return promise_rejects_js(test, TypeError, promise);
+ }, desc);
+}
+
+var urlWithoutScheme = "://{{host}}:{{ports[http][0]}}/";
+checkKoUrl("aaa" + urlWithoutScheme);
+checkKoUrl("cap" + urlWithoutScheme);
+checkKoUrl("cid" + urlWithoutScheme);
+checkKoUrl("dav" + urlWithoutScheme);
+checkKoUrl("dict" + urlWithoutScheme);
+checkKoUrl("dns" + urlWithoutScheme);
+checkKoUrl("geo" + urlWithoutScheme);
+checkKoUrl("im" + urlWithoutScheme);
+checkKoUrl("imap" + urlWithoutScheme);
+checkKoUrl("ipp" + urlWithoutScheme);
+checkKoUrl("ldap" + urlWithoutScheme);
+checkKoUrl("mailto" + urlWithoutScheme);
+checkKoUrl("nfs" + urlWithoutScheme);
+checkKoUrl("pop" + urlWithoutScheme);
+checkKoUrl("rtsp" + urlWithoutScheme);
+checkKoUrl("snmp" + urlWithoutScheme);
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/basic/status.h2.any.js b/testing/web-platform/tests/fetch/api/basic/status.h2.any.js
new file mode 100644
index 0000000000..99fec88f50
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/status.h2.any.js
@@ -0,0 +1,17 @@
+// See also /xhr/status.h2.window.js
+
+[
+ 200,
+ 210,
+ 400,
+ 404,
+ 410,
+ 500,
+ 502
+].forEach(status => {
+ promise_test(async t => {
+ const response = await fetch("/xhr/resources/status.py?code=" + status);
+ assert_equals(response.status, status, "status should be " + status);
+ assert_equals(response.statusText, "", "statusText should be the empty string");
+ }, "statusText over H2 for status " + status + " should be the empty string");
+});
diff --git a/testing/web-platform/tests/fetch/api/basic/stream-response.any.js b/testing/web-platform/tests/fetch/api/basic/stream-response.any.js
new file mode 100644
index 0000000000..d964dda717
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/stream-response.any.js
@@ -0,0 +1,40 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function streamBody(reader, test, count = 0) {
+ return reader.read().then(function(data) {
+ if (!data.done && count < 2) {
+ count += 1;
+ return streamBody(reader, test, count);
+ } else {
+ test.step(function() {
+ assert_true(count >= 2, "Retrieve body progressively");
+ });
+ }
+ });
+}
+
+//simulate streaming:
+//count is large enough to let the UA deliver the body before it is completely retrieved
+promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "trickle.py?ms=30&count=100").then(function(resp) {
+ if (resp.body)
+ return streamBody(resp.body.getReader(), test);
+ else
+ test.step(function() {
+ assert_unreached( "Body does not exist in response");
+ });
+ });
+}, "Stream response's body when content-type is present");
+
+// This test makes sure that the response body is not buffered if no content type is provided.
+promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "trickle.py?ms=300&count=10&notype=true").then(function(resp) {
+ if (resp.body)
+ return streamBody(resp.body.getReader(), test);
+ else
+ test.step(function() {
+ assert_unreached( "Body does not exist in response");
+ });
+ });
+}, "Stream response's body when content-type is not present");
diff --git a/testing/web-platform/tests/fetch/api/basic/stream-safe-creation.any.js b/testing/web-platform/tests/fetch/api/basic/stream-safe-creation.any.js
new file mode 100644
index 0000000000..382efc1a8b
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/stream-safe-creation.any.js
@@ -0,0 +1,54 @@
+// META: global=window,worker
+
+// These tests verify that stream creation is not affected by changes to
+// Object.prototype.
+
+const creationCases = {
+ fetch: async () => fetch(location.href),
+ request: () => new Request(location.href, {method: 'POST', body: 'hi'}),
+ response: () => new Response('bye'),
+ consumeEmptyResponse: () => new Response().text(),
+ consumeNonEmptyResponse: () => new Response(new Uint8Array([64])).text(),
+ consumeEmptyRequest: () => new Request(location.href).text(),
+ consumeNonEmptyRequest: () => new Request(location.href,
+ {method: 'POST', body: 'yes'}).arrayBuffer(),
+};
+
+for (const creationCase of Object.keys(creationCases)) {
+ for (const accessorName of ['start', 'type', 'size', 'highWaterMark']) {
+ promise_test(async t => {
+ Object.defineProperty(Object.prototype, accessorName, {
+ get() { throw Error(`Object.prototype.${accessorName} was accessed`); },
+ configurable: true
+ });
+ t.add_cleanup(() => {
+ delete Object.prototype[accessorName];
+ return Promise.resolve();
+ });
+ await creationCases[creationCase]();
+ }, `throwing Object.prototype.${accessorName} accessor should not affect ` +
+ `stream creation by '${creationCase}'`);
+
+ promise_test(async t => {
+ // -1 is a convenient value which is invalid, and should cause the
+ // constructor to throw, for all four fields.
+ Object.prototype[accessorName] = -1;
+ t.add_cleanup(() => {
+ delete Object.prototype[accessorName];
+ return Promise.resolve();
+ });
+ await creationCases[creationCase]();
+ }, `Object.prototype.${accessorName} accessor returning invalid value ` +
+ `should not affect stream creation by '${creationCase}'`);
+ }
+
+ promise_test(async t => {
+ Object.prototype.start = controller => controller.error(new Error('start'));
+ t.add_cleanup(() => {
+ delete Object.prototype.start;
+ return Promise.resolve();
+ });
+ await creationCases[creationCase]();
+ }, `Object.prototype.start function which errors the stream should not ` +
+ `affect stream creation by '${creationCase}'`);
+}
diff --git a/testing/web-platform/tests/fetch/api/basic/text-utf8.any.js b/testing/web-platform/tests/fetch/api/basic/text-utf8.any.js
new file mode 100644
index 0000000000..05c8c88825
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/text-utf8.any.js
@@ -0,0 +1,74 @@
+// META: title=Fetch: Request and Response text() should decode as UTF-8
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function testTextDecoding(body, expectedText, urlParameter, title)
+{
+ var arrayBuffer = stringToArray(body);
+
+ promise_test(function(test) {
+ var request = new Request("", {method: "POST", body: arrayBuffer});
+ return request.text().then(function(value) {
+ assert_equals(value, expectedText, "Request.text() should decode data as UTF-8");
+ });
+ }, title + " with Request.text()");
+
+ promise_test(function(test) {
+ var response = new Response(arrayBuffer);
+ return response.text().then(function(value) {
+ assert_equals(value, expectedText, "Response.text() should decode data as UTF-8");
+ });
+ }, title + " with Response.text()");
+
+ promise_test(function(test) {
+ return fetch("../resources/status.py?code=200&type=text%2Fplain%3Bcharset%3DUTF-8&content=" + urlParameter).then(function(response) {
+ return response.text().then(function(value) {
+ assert_equals(value, expectedText, "Fetched Response.text() should decode data as UTF-8");
+ });
+ });
+ }, title + " with fetched data (UTF-8 charset)");
+
+ promise_test(function(test) {
+ return fetch("../resources/status.py?code=200&type=text%2Fplain%3Bcharset%3DUTF-16&content=" + urlParameter).then(function(response) {
+ return response.text().then(function(value) {
+ assert_equals(value, expectedText, "Fetched Response.text() should decode data as UTF-8");
+ });
+ });
+ }, title + " with fetched data (UTF-16 charset)");
+
+ promise_test(function(test) {
+ return new Response(body).arrayBuffer().then(function(buffer) {
+ assert_array_equals(new Uint8Array(buffer), encode_utf8(body), "Response.arrayBuffer() should contain data encoded as UTF-8");
+ });
+ }, title + " (Response object)");
+
+ promise_test(function(test) {
+ return new Request("", {method: "POST", body: body}).arrayBuffer().then(function(buffer) {
+ assert_array_equals(new Uint8Array(buffer), encode_utf8(body), "Request.arrayBuffer() should contain data encoded as UTF-8");
+ });
+ }, title + " (Request object)");
+
+}
+
+var utf8WithBOM = "\xef\xbb\xbf\xe4\xb8\x89\xe6\x9d\x91\xe3\x81\x8b\xe3\x81\xaa\xe5\xad\x90";
+var utf8WithBOMAsURLParameter = "%EF%BB%BF%E4%B8%89%E6%9D%91%E3%81%8B%E3%81%AA%E5%AD%90";
+var utf8WithoutBOM = "\xe4\xb8\x89\xe6\x9d\x91\xe3\x81\x8b\xe3\x81\xaa\xe5\xad\x90";
+var utf8WithoutBOMAsURLParameter = "%E4%B8%89%E6%9D%91%E3%81%8B%E3%81%AA%E5%AD%90";
+var utf8Decoded = "三村かな子";
+testTextDecoding(utf8WithBOM, utf8Decoded, utf8WithBOMAsURLParameter, "UTF-8 with BOM");
+testTextDecoding(utf8WithoutBOM, utf8Decoded, utf8WithoutBOMAsURLParameter, "UTF-8 without BOM");
+
+var utf16BEWithBOM = "\xfe\xff\x4e\x09\x67\x51\x30\x4b\x30\x6a\x5b\x50";
+var utf16BEWithBOMAsURLParameter = "%fe%ff%4e%09%67%51%30%4b%30%6a%5b%50";
+var utf16BEWithBOMDecodedAsUTF8 = "��N\tgQ0K0j[P";
+testTextDecoding(utf16BEWithBOM, utf16BEWithBOMDecodedAsUTF8, utf16BEWithBOMAsURLParameter, "UTF-16BE with BOM decoded as UTF-8");
+
+var utf16LEWithBOM = "\xff\xfe\x09\x4e\x51\x67\x4b\x30\x6a\x30\x50\x5b";
+var utf16LEWithBOMAsURLParameter = "%ff%fe%09%4e%51%67%4b%30%6a%30%50%5b";
+var utf16LEWithBOMDecodedAsUTF8 = "��\tNQgK0j0P[";
+testTextDecoding(utf16LEWithBOM, utf16LEWithBOMDecodedAsUTF8, utf16LEWithBOMAsURLParameter, "UTF-16LE with BOM decoded as UTF-8");
+
+var utf16WithoutBOM = "\xe6\x00\xf8\x00\xe5\x00\x0a\x00\xc6\x30\xb9\x30\xc8\x30\x0a\x00";
+var utf16WithoutBOMAsURLParameter = "%E6%00%F8%00%E5%00%0A%00%C6%30%B9%30%C8%30%0A%00";
+var utf16WithoutBOMDecoded = "\ufffd\u0000\ufffd\u0000\ufffd\u0000\u000a\u0000\ufffd\u0030\ufffd\u0030\ufffd\u0030\u000a\u0000";
+testTextDecoding(utf16WithoutBOM, utf16WithoutBOMDecoded, utf16WithoutBOMAsURLParameter, "UTF-16 without BOM decoded as UTF-8");
diff --git a/testing/web-platform/tests/fetch/api/basic/url-parsing.sub.html b/testing/web-platform/tests/fetch/api/basic/url-parsing.sub.html
new file mode 100644
index 0000000000..fa47b29473
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/basic/url-parsing.sub.html
@@ -0,0 +1,33 @@
+<!-- Based on /html/infrastructure/urls/resolving-urls/query-encoding/location.sub.html -->
+<!doctype html>
+<meta charset={{GET[encoding]}}> <!-- ends up as <meta charset> by default which is windows-1252 -->
+<meta name=variant content="?encoding=windows-1252">
+<meta name=variant content="?encoding=x-cp1251">
+<meta name=variant content="?encoding=utf8">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+function expected(encoding) {
+ return {
+ "UTF-8": "%C3%BF",
+ "windows-1251": "%26%23255%3B",
+ "windows-1252": "%FF"
+ }[encoding];
+}
+
+test(() => {
+ const request = new Request("?\u00FF");
+ assert_equals(request.url.split("?")[1], expected("UTF-8"));
+}, "Request uses the UTF-8 URL parser");
+
+test(() => {
+ const request = new Request("about:blank", { referrer: "?\u00FF" });
+ assert_equals(request.referrer.split("?")[1], expected("UTF-8"));
+}, "Request's referrer uses the UTF-8 URL parser");
+
+test(() => {
+ const response = Response.redirect("?\u00FF");
+ assert_equals(response.headers.get("Location").split("?")[1], expected("UTF-8"));
+}, "Response.redirect() uses the UTF-8 URL parser");
+</script>
diff --git a/testing/web-platform/tests/fetch/api/body/cloned-any.js b/testing/web-platform/tests/fetch/api/body/cloned-any.js
new file mode 100644
index 0000000000..2bca96c704
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/body/cloned-any.js
@@ -0,0 +1,50 @@
+// Changing the body after it have been passed to Response/Request
+// should not change the outcome of the consumed body
+
+const url = 'http://a';
+const method = 'post';
+
+promise_test(async t => {
+ const body = new FormData();
+ body.set('a', '1');
+ const res = new Response(body);
+ const req = new Request(url, { method, body });
+ body.set('a', '2');
+ assert_true((await res.formData()).get('a') === '1');
+ assert_true((await req.formData()).get('a') === '1');
+}, 'FormData is cloned');
+
+promise_test(async t => {
+ const body = new URLSearchParams({a: '1'});
+ const res = new Response(body);
+ const req = new Request(url, { method, body });
+ body.set('a', '2');
+ assert_true((await res.formData()).get('a') === '1');
+ assert_true((await req.formData()).get('a') === '1');
+}, 'URLSearchParams is cloned');
+
+promise_test(async t => {
+ const body = new Uint8Array([97]); // a
+ const res = new Response(body);
+ const req = new Request(url, { method, body });
+ body[0] = 98; // b
+ assert_true(await res.text() === 'a');
+ assert_true(await req.text() === 'a');
+}, 'TypedArray is cloned');
+
+promise_test(async t => {
+ const body = new Uint8Array([97]); // a
+ const res = new Response(body.buffer);
+ const req = new Request(url, { method, body: body.buffer });
+ body[0] = 98; // b
+ assert_true(await res.text() === 'a');
+ assert_true(await req.text() === 'a');
+}, 'ArrayBuffer is cloned');
+
+promise_test(async t => {
+ const body = new Blob(['a']);
+ const res = new Response(body);
+ const req = new Request(url, { method, body });
+ assert_true(await res.blob() !== body);
+ assert_true(await req.blob() !== body);
+}, 'Blob is cloned');
diff --git a/testing/web-platform/tests/fetch/api/body/formdata.any.js b/testing/web-platform/tests/fetch/api/body/formdata.any.js
new file mode 100644
index 0000000000..e25035923c
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/body/formdata.any.js
@@ -0,0 +1,14 @@
+promise_test(async t => {
+ const res = new Response(new FormData());
+ const fd = await res.formData();
+ assert_true(fd instanceof FormData);
+}, 'Consume empty response.formData() as FormData');
+
+promise_test(async t => {
+ const req = new Request('about:blank', {
+ method: 'POST',
+ body: new FormData()
+ });
+ const fd = await req.formData();
+ assert_true(fd instanceof FormData);
+}, 'Consume empty request.formData() as FormData');
diff --git a/testing/web-platform/tests/fetch/api/body/mime-type.any.js b/testing/web-platform/tests/fetch/api/body/mime-type.any.js
new file mode 100644
index 0000000000..67c9af7da2
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/body/mime-type.any.js
@@ -0,0 +1,127 @@
+[
+ () => new Request("about:blank", { headers: { "Content-Type": "text/plain" } }),
+ () => new Response("", { headers: { "Content-Type": "text/plain" } })
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ assert_equals(bodyContainer.headers.get("Content-Type"), "text/plain");
+ const newMIMEType = "test/test";
+ bodyContainer.headers.set("Content-Type", newMIMEType);
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, newMIMEType);
+ }, `${bodyContainer.constructor.name}: overriding explicit Content-Type`);
+});
+
+[
+ () => new Request("about:blank", { body: new URLSearchParams(), method: "POST" }),
+ () => new Response(new URLSearchParams()),
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ assert_equals(bodyContainer.headers.get("Content-Type"), "application/x-www-form-urlencoded;charset=UTF-8");
+ bodyContainer.headers.delete("Content-Type");
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, "");
+ }, `${bodyContainer.constructor.name}: removing implicit Content-Type`);
+});
+
+[
+ () => new Request("about:blank", { body: new ArrayBuffer(), method: "POST" }),
+ () => new Response(new ArrayBuffer()),
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ assert_equals(bodyContainer.headers.get("Content-Type"), null);
+ const newMIMEType = "test/test";
+ bodyContainer.headers.set("Content-Type", newMIMEType);
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, newMIMEType);
+ }, `${bodyContainer.constructor.name}: setting missing Content-Type`);
+});
+
+[
+ () => new Request("about:blank", { method: "POST" }),
+ () => new Response(),
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, "");
+ }, `${bodyContainer.constructor.name}: MIME type for Blob from empty body`);
+});
+
+[
+ () => new Request("about:blank", { method: "POST", headers: [["Content-Type", "Mytext/Plain"]] }),
+ () => new Response("", { headers: [["Content-Type", "Mytext/Plain"]] })
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, 'mytext/plain');
+ }, `${bodyContainer.constructor.name}: MIME type for Blob from empty body with Content-Type`);
+});
+
+[
+ () => new Request("about:blank", { body: new Blob([""]), method: "POST" }),
+ () => new Response(new Blob([""]))
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, "");
+ assert_equals(bodyContainer.headers.get("Content-Type"), null);
+ }, `${bodyContainer.constructor.name}: MIME type for Blob`);
+});
+
+[
+ () => new Request("about:blank", { body: new Blob([""], { type: "Text/Plain" }), method: "POST" }),
+ () => new Response(new Blob([""], { type: "Text/Plain" }))
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, "text/plain");
+ assert_equals(bodyContainer.headers.get("Content-Type"), "text/plain");
+ }, `${bodyContainer.constructor.name}: MIME type for Blob with non-empty type`);
+});
+
+[
+ () => new Request("about:blank", { method: "POST", body: new Blob([""], { type: "Text/Plain" }), headers: [["Content-Type", "Text/Html"]] }),
+ () => new Response(new Blob([""], { type: "Text/Plain" }, { headers: [["Content-Type", "Text/Html"]] }))
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ const cloned = bodyContainer.clone();
+ promise_test(async t => {
+ const blobs = [await bodyContainer.blob(), await cloned.blob()];
+ assert_equals(blobs[0].type, "text/html");
+ assert_equals(blobs[1].type, "text/html");
+ assert_equals(bodyContainer.headers.get("Content-Type"), "Text/Html");
+ assert_equals(cloned.headers.get("Content-Type"), "Text/Html");
+ }, `${bodyContainer.constructor.name}: Extract a MIME type with clone`);
+});
+
+[
+ () => new Request("about:blank", { body: new Blob([], { type: "text/plain" }), method: "POST", headers: [["Content-Type", "text/html"]] }),
+ () => new Response(new Blob([], { type: "text/plain" }), { headers: [["Content-Type", "text/html"]] }),
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ assert_equals(bodyContainer.headers.get("Content-Type"), "text/html");
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, "text/html");
+ }, `${bodyContainer.constructor.name}: Content-Type in headers wins Blob"s type`);
+});
+
+[
+ () => new Request("about:blank", { body: new Blob([], { type: "text/plain" }), method: "POST" }),
+ () => new Response(new Blob([], { type: "text/plain" })),
+].forEach(bodyContainerCreator => {
+ const bodyContainer = bodyContainerCreator();
+ promise_test(async t => {
+ assert_equals(bodyContainer.headers.get("Content-Type"), "text/plain");
+ const newMIMEType = "text/html";
+ bodyContainer.headers.set("Content-Type", newMIMEType);
+ const blob = await bodyContainer.blob();
+ assert_equals(blob.type, newMIMEType);
+ }, `${bodyContainer.constructor.name}: setting missing Content-Type in headers and it wins Blob"s type`);
+});
diff --git a/testing/web-platform/tests/fetch/api/cors/cors-basic.any.js b/testing/web-platform/tests/fetch/api/cors/cors-basic.any.js
new file mode 100644
index 0000000000..95de0af2d8
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/cors-basic.any.js
@@ -0,0 +1,43 @@
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+const {
+ HTTPS_ORIGIN,
+ HTTP_ORIGIN_WITH_DIFFERENT_PORT,
+ HTTP_REMOTE_ORIGIN,
+ HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT,
+ HTTPS_REMOTE_ORIGIN,
+} = get_host_info();
+
+function cors(desc, origin) {
+ const url = `${origin}${dirname(location.pathname)}${RESOURCES_DIR}top.txt`;
+ const urlAllowCors = `${url}?pipe=header(Access-Control-Allow-Origin,*)`;
+
+ promise_test((test) => {
+ return fetch(urlAllowCors, {'mode': 'no-cors'}).then((resp) => {
+ assert_equals(resp.status, 0, "Opaque filter: status is 0");
+ assert_equals(resp.statusText, "", "Opaque filter: statusText is \"\"");
+ assert_equals(resp.type , "opaque", "Opaque filter: response's type is opaque");
+ return resp.text().then((value) => {
+ assert_equals(value, "", "Opaque response should have an empty body");
+ });
+ });
+ }, `${desc} [no-cors mode]`);
+
+ promise_test((test) => {
+ return promise_rejects_js(test, TypeError, fetch(url, {'mode': 'cors'}));
+ }, `${desc} [server forbid CORS]`);
+
+ promise_test((test) => {
+ return fetch(urlAllowCors, {'mode': 'cors'}).then((resp) => {
+ assert_equals(resp.status, 200, "Fetch's response's status is 200");
+ assert_equals(resp.type , "cors", "CORS response's type is cors");
+ });
+ }, `${desc} [cors mode]`);
+}
+
+cors('Same domain different port', HTTP_ORIGIN_WITH_DIFFERENT_PORT);
+cors('Same domain different protocol different port', HTTPS_ORIGIN);
+cors('Cross domain basic usage', HTTP_REMOTE_ORIGIN);
+cors('Cross domain different port', HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT);
+cors('Cross domain different protocol', HTTPS_REMOTE_ORIGIN);
diff --git a/testing/web-platform/tests/fetch/api/cors/cors-cookies-redirect.any.js b/testing/web-platform/tests/fetch/api/cors/cors-cookies-redirect.any.js
new file mode 100644
index 0000000000..f5217b4246
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/cors-cookies-redirect.any.js
@@ -0,0 +1,49 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+var redirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py";
+var urlSetCookies1 = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "top.txt";
+var urlSetCookies2 = get_host_info().HTTP_ORIGIN_WITH_DIFFERENT_PORT + dirname(location.pathname) + RESOURCES_DIR + "top.txt";
+var urlCheckCookies = get_host_info().HTTP_ORIGIN_WITH_DIFFERENT_PORT + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=cookie";
+
+var urlSetCookiesParameters = "?pipe=header(Access-Control-Allow-Origin," + location.origin + ")";
+urlSetCookiesParameters += "|header(Access-Control-Allow-Credentials,true)";
+
+urlSetCookiesParameters1 = urlSetCookiesParameters + "|header(Set-Cookie,a=1)";
+urlSetCookiesParameters2 = urlSetCookiesParameters + "|header(Set-Cookie,a=2)";
+
+urlClearCookiesParameters1 = urlSetCookiesParameters + "|header(Set-Cookie,a=1%3B%20max-age=0)";
+urlClearCookiesParameters2 = urlSetCookiesParameters + "|header(Set-Cookie,a=2%3B%20max-age=0)";
+
+promise_test(async (test) => {
+ await fetch(urlSetCookies1 + urlSetCookiesParameters1, {"credentials": "include", "mode": "cors"});
+ await fetch(urlSetCookies2 + urlSetCookiesParameters2, {"credentials": "include", "mode": "cors"});
+}, "Set cookies");
+
+function doTest(usePreflight) {
+ promise_test(async (test) => {
+ var url = redirectUrl;
+ var uuid_token = token();
+ var urlParameters = "?token=" + uuid_token + "&max_age=0";
+ urlParameters += "&redirect_status=301";
+ urlParameters += "&location=" + encodeURIComponent(urlCheckCookies);
+ urlParameters += "&allow_headers=a&headers=Cookie";
+ headers = [];
+ if (usePreflight)
+ headers.push(["a", "b"]);
+
+ var requestInit = {"credentials": "include", "mode": "cors", "headers": headers};
+ var response = await fetch(url + urlParameters, requestInit);
+
+ assert_equals(response.headers.get("x-request-cookie") , "a=2", "Request includes cookie(s)");
+ }, "Testing credentials after cross-origin redirection with CORS and " + (usePreflight ? "" : "no ") + "preflight");
+}
+
+doTest(false);
+doTest(true);
+
+promise_test(async (test) => {
+ await fetch(urlSetCookies1 + urlClearCookiesParameters1, {"credentials": "include", "mode": "cors"});
+ await fetch(urlSetCookies2 + urlClearCookiesParameters2, {"credentials": "include", "mode": "cors"});
+}, "Clean cookies");
diff --git a/testing/web-platform/tests/fetch/api/cors/cors-cookies.any.js b/testing/web-platform/tests/fetch/api/cors/cors-cookies.any.js
new file mode 100644
index 0000000000..8c666e4782
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/cors-cookies.any.js
@@ -0,0 +1,56 @@
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsCookies(desc, baseURL1, baseURL2, credentialsMode, cookies) {
+ var urlSetCookie = baseURL1 + dirname(location.pathname) + RESOURCES_DIR + "top.txt";
+ var urlCheckCookies = baseURL2 + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=cookie";
+ //enable cors with credentials
+ var urlParameters = "?pipe=header(Access-Control-Allow-Origin," + location.origin + ")";
+ urlParameters += "|header(Access-Control-Allow-Credentials,true)";
+
+ var urlCleanParameters = "?pipe=header(Access-Control-Allow-Origin," + location.origin + ")";
+ urlCleanParameters += "|header(Access-Control-Allow-Credentials,true)";
+ if (cookies) {
+ urlParameters += "|header(Set-Cookie,";
+ urlParameters += cookies.join(",True)|header(Set-Cookie,") + ",True)";
+ urlCleanParameters += "|header(Set-Cookie,";
+ urlCleanParameters += cookies.join("%3B%20max-age=0,True)|header(Set-Cookie,") + "%3B%20max-age=0,True)";
+ }
+
+ var requestInit = {"credentials": credentialsMode, "mode": "cors"};
+
+ promise_test(function(test){
+ return fetch(urlSetCookie + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ //check cookies sent
+ return fetch(urlCheckCookies, requestInit);
+ }).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_false(resp.headers.has("Cookie") , "Cookie header is not exposed in response");
+ if (credentialsMode === "include" && baseURL1 === baseURL2) {
+ assert_equals(resp.headers.get("x-request-cookie") , cookies.join("; "), "Request includes cookie(s)");
+ }
+ else {
+ assert_false(resp.headers.has("x-request-cookie") , "Request should have no cookie");
+ }
+ //clean cookies
+ return fetch(urlSetCookie + urlCleanParameters, {"credentials": "include"});
+ }).catch(function(e) {
+ return fetch(urlSetCookie + urlCleanParameters, {"credentials": "include"}).then(function(resp) {
+ throw e;
+ })
+ });
+ }, desc);
+}
+
+var local = get_host_info().HTTP_ORIGIN;
+var remote = get_host_info().HTTP_REMOTE_ORIGIN;
+// FIXME: otherRemote might not be accessible on some test environments.
+var otherRemote = local.replace("http://", "http://www.");
+
+corsCookies("Omit mode: no cookie sent", local, local, "omit", ["g=7"]);
+corsCookies("Include mode: 1 cookie", remote, remote, "include", ["a=1"]);
+corsCookies("Include mode: local cookies are not sent with remote request", local, remote, "include", ["c=3"]);
+corsCookies("Include mode: remote cookies are not sent with local request", remote, local, "include", ["d=4"]);
+corsCookies("Same-origin mode: cookies are discarded in cors request", remote, remote, "same-origin", ["f=6"]);
+corsCookies("Include mode: remote cookies are not sent with other remote request", remote, otherRemote, "include", ["e=5"]);
diff --git a/testing/web-platform/tests/fetch/api/cors/cors-expose-star.sub.any.js b/testing/web-platform/tests/fetch/api/cors/cors-expose-star.sub.any.js
new file mode 100644
index 0000000000..340e99ab5f
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/cors-expose-star.sub.any.js
@@ -0,0 +1,41 @@
+// META: script=../resources/utils.js
+
+const url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "top.txt",
+ sharedHeaders = "?pipe=header(Access-Control-Expose-Headers,*)|header(Test,X)|header(Set-Cookie,X)|header(*,whoa)|"
+
+promise_test(() => {
+ const headers = "header(Access-Control-Allow-Origin,*)"
+ return fetch(url + sharedHeaders + headers).then(resp => {
+ assert_equals(resp.status, 200)
+ assert_equals(resp.type , "cors")
+ assert_equals(resp.headers.get("test"), "X")
+ assert_equals(resp.headers.get("set-cookie"), null)
+ assert_equals(resp.headers.get("*"), "whoa")
+ })
+}, "Basic Access-Control-Expose-Headers: * support")
+
+promise_test(() => {
+ const origin = location.origin, // assuming an ASCII origin
+ headers = "header(Access-Control-Allow-Origin," + origin + ")|header(Access-Control-Allow-Credentials,true)"
+ return fetch(url + sharedHeaders + headers, { credentials:"include" }).then(resp => {
+ assert_equals(resp.status, 200)
+ assert_equals(resp.type , "cors")
+ assert_equals(resp.headers.get("content-type"), "text/plain") // safelisted
+ assert_equals(resp.headers.get("test"), null)
+ assert_equals(resp.headers.get("set-cookie"), null)
+ assert_equals(resp.headers.get("*"), "whoa")
+ })
+}, "* for credentialed fetches only matches literally")
+
+promise_test(() => {
+ const headers = "header(Access-Control-Allow-Origin,*)|header(Access-Control-Expose-Headers,set-cookie\\,*)"
+ return fetch(url + sharedHeaders + headers).then(resp => {
+ assert_equals(resp.status, 200)
+ assert_equals(resp.type , "cors")
+ assert_equals(resp.headers.get("test"), "X")
+ assert_equals(resp.headers.get("set-cookie"), null)
+ assert_equals(resp.headers.get("*"), "whoa")
+ })
+}, "* can be one of several values")
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/cors/cors-filtering.sub.any.js b/testing/web-platform/tests/fetch/api/cors/cors-filtering.sub.any.js
new file mode 100644
index 0000000000..a26eaccf2a
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/cors-filtering.sub.any.js
@@ -0,0 +1,69 @@
+// META: script=../resources/utils.js
+
+function corsFilter(corsUrl, headerName, headerValue, isFiltered) {
+ var url = corsUrl + "?pipe=header(" + headerName + "," + encodeURIComponent(headerValue) +")|header(Access-Control-Allow-Origin,*)";
+ promise_test(function(test) {
+ return fetch(url).then(function(resp) {
+ assert_equals(resp.status, 200, "Fetch success with code 200");
+ assert_equals(resp.type , "cors", "CORS fetch's response has cors type");
+ if (!isFiltered) {
+ assert_equals(resp.headers.get(headerName), headerValue,
+ headerName + " header should be included in response with value: " + headerValue);
+ } else {
+ assert_false(resp.headers.has(headerName), "UA should exclude " + headerName + " header from response");
+ }
+ test.done();
+ });
+ }, "CORS filter on " + headerName + " header");
+}
+
+function corsExposeFilter(corsUrl, headerName, headerValue, isForbidden, withCredentials) {
+ var url = corsUrl + "?pipe=header(" + headerName + "," + encodeURIComponent(headerValue) +")|" +
+ "header(Access-Control-Allow-Origin, http://{{host}}:{{ports[http][0]}})" +
+ "header(Access-Control-Allow-Credentials, true)" +
+ "header(Access-Control-Expose-Headers," + headerName + ")";
+
+ var title = "CORS filter on " + headerName + " header, header is " + (isForbidden ? "forbidden" : "exposed");
+ if (withCredentials)
+ title+= "(credentials = include)";
+ promise_test(function(test) {
+ return fetch(new Request(url, { credentials: withCredentials ? "include" : "omit" })).then(function(resp) {
+ assert_equals(resp.status, 200, "Fetch success with code 200");
+ assert_equals(resp.type , "cors", "CORS fetch's response has cors type");
+ if (!isForbidden) {
+ assert_equals(resp.headers.get(headerName), headerValue,
+ headerName + " header should be included in response with value: " + headerValue);
+ } else {
+ assert_false(resp.headers.has(headerName), "UA should exclude " + headerName + " header from response");
+ }
+ test.done();
+ });
+ }, title);
+}
+
+var url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "top.txt";
+
+corsFilter(url, "Cache-Control", "no-cache", false);
+corsFilter(url, "Content-Language", "fr", false);
+corsFilter(url, "Content-Type", "text/html", false);
+corsFilter(url, "Expires","04 May 1988 22:22:22 GMT" , false);
+corsFilter(url, "Last-Modified", "04 May 1988 22:22:22 GMT", false);
+corsFilter(url, "Pragma", "no-cache", false);
+corsFilter(url, "Content-Length", "3" , false); // top.txt contains "top"
+
+corsFilter(url, "Age", "27", true);
+corsFilter(url, "Server", "wptServe" , true);
+corsFilter(url, "Warning", "Mind the gap" , true);
+corsFilter(url, "Set-Cookie", "name=value" , true);
+corsFilter(url, "Set-Cookie2", "name=value" , true);
+
+corsExposeFilter(url, "Age", "27", false);
+corsExposeFilter(url, "Server", "wptServe" , false);
+corsExposeFilter(url, "Warning", "Mind the gap" , false);
+
+corsExposeFilter(url, "Set-Cookie", "name=value" , true);
+corsExposeFilter(url, "Set-Cookie2", "name=value" , true);
+corsExposeFilter(url, "Set-Cookie", "name=value" , true, true);
+corsExposeFilter(url, "Set-Cookie2", "name=value" , true, true);
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/cors/cors-keepalive.any.js b/testing/web-platform/tests/fetch/api/cors/cors-keepalive.any.js
new file mode 100644
index 0000000000..f54bf4f9b6
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/cors-keepalive.any.js
@@ -0,0 +1,116 @@
+// META: global=window
+// META: timeout=long
+// META: title=Fetch API: keepalive handling
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=../resources/keepalive-helper.js
+// META: script=../resources/utils.js
+
+'use strict';
+
+const {
+ HTTP_NOTSAMESITE_ORIGIN,
+ HTTPS_ORIGIN,
+ HTTP_ORIGIN_WITH_DIFFERENT_PORT,
+ HTTP_REMOTE_ORIGIN,
+ HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT,
+ HTTPS_REMOTE_ORIGIN,
+} = get_host_info();
+
+/**
+ * Tests to cover the basic behaviors of keepalive + cors/no-cors mode requests
+ * to different `origin` when the initiator document is still alive. They should
+ * behave the same as without setting keepalive.
+ */
+function keepaliveCorsBasicTest(desc, origin) {
+ const url = `${origin}${dirname(location.pathname)}${RESOURCES_DIR}top.txt`;
+ const urlAllowCors = `${url}?pipe=header(Access-Control-Allow-Origin,*)`;
+
+ promise_test((test) => {
+ return fetch(urlAllowCors, {keepalive: true, 'mode': 'no-cors'})
+ .then((resp) => {
+ assert_equals(resp.status, 0, 'Opaque filter: status is 0');
+ assert_equals(resp.statusText, '', 'Opaque filter: statusText is ""');
+ assert_equals(
+ resp.type, 'opaque', 'Opaque filter: response\'s type is opaque');
+ return resp.text().then((value) => {
+ assert_equals(
+ value, '', 'Opaque response should have an empty body');
+ });
+ });
+ }, `${desc} [no-cors mode]`);
+
+ promise_test((test) => {
+ return promise_rejects_js(
+ test, TypeError, fetch(url, {keepalive: true, 'mode': 'cors'}));
+ }, `${desc} [cors mode, server forbid CORS]`);
+
+ promise_test((test) => {
+ return fetch(urlAllowCors, {keepalive: true, 'mode': 'cors'})
+ .then((resp) => {
+ assert_equals(resp.status, 200, 'Fetch\'s response\'s status is 200');
+ assert_equals(resp.type, 'cors', 'CORS response\'s type is cors');
+ });
+ }, `${desc} [cors mode]`);
+}
+
+keepaliveCorsBasicTest(
+ `[keepalive] Same domain different port`, HTTP_ORIGIN_WITH_DIFFERENT_PORT);
+keepaliveCorsBasicTest(
+ `[keepalive] Same domain different protocol different port`, HTTPS_ORIGIN);
+keepaliveCorsBasicTest(
+ `[keepalive] Cross domain basic usage`, HTTP_REMOTE_ORIGIN);
+keepaliveCorsBasicTest(
+ `[keepalive] Cross domain different port`,
+ HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT);
+keepaliveCorsBasicTest(
+ `[keepalive] Cross domain different protocol`, HTTPS_REMOTE_ORIGIN);
+
+/**
+ * In a same-site iframe, and in `unload` event handler, test to fetch
+ * a keepalive URL that involves in different cors modes.
+ */
+function keepaliveCorsInUnloadTest(description, origin, method) {
+ const evt = 'unload';
+ for (const mode of ['no-cors', 'cors']) {
+ for (const disallowCrossOrigin of [false, true]) {
+ const desc = `${description} ${method} request in ${evt} [${mode} mode` +
+ (disallowCrossOrigin ? ']' : ', server forbid CORS]');
+ const expectTokenExist = !disallowCrossOrigin || mode === 'no-cors';
+ promise_test(async (test) => {
+ const token1 = token();
+ const iframe = document.createElement('iframe');
+ iframe.src = getKeepAliveIframeUrl(token1, method, {
+ frameOrigin: '',
+ requestOrigin: origin,
+ sendOn: evt,
+ mode: mode,
+ disallowCrossOrigin
+ });
+ document.body.appendChild(iframe);
+ await iframeLoaded(iframe);
+ iframe.remove();
+ assert_equals(await getTokenFromMessage(), token1);
+
+ assertStashedTokenAsync(desc, token1, {expectTokenExist});
+ }, `${desc}; setting up`);
+ }
+ }
+}
+
+for (const method of ['GET', 'POST']) {
+ keepaliveCorsInUnloadTest(
+ '[keepalive] Same domain different port', HTTP_ORIGIN_WITH_DIFFERENT_PORT,
+ method);
+ keepaliveCorsInUnloadTest(
+ `[keepalive] Same domain different protocol different port`, HTTPS_ORIGIN,
+ method);
+ keepaliveCorsInUnloadTest(
+ `[keepalive] Cross domain basic usage`, HTTP_REMOTE_ORIGIN, method);
+ keepaliveCorsInUnloadTest(
+ `[keepalive] Cross domain different port`,
+ HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, method);
+ keepaliveCorsInUnloadTest(
+ `[keepalive] Cross domain different protocol`, HTTPS_REMOTE_ORIGIN,
+ method);
+}
diff --git a/testing/web-platform/tests/fetch/api/cors/cors-multiple-origins.sub.any.js b/testing/web-platform/tests/fetch/api/cors/cors-multiple-origins.sub.any.js
new file mode 100644
index 0000000000..b3abb92284
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/cors-multiple-origins.sub.any.js
@@ -0,0 +1,22 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function corsMultipleOrigins(originList) {
+ var urlParameters = "?origin=" + encodeURIComponent(originList.join(", "));
+ var url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+
+ promise_test(function(test) {
+ return promise_rejects_js(test, TypeError, fetch(url + urlParameters));
+ }, "Listing multiple origins is illegal: " + originList);
+}
+/* Actual origin */
+var origin = "http://{{host}}:{{ports[http][0]}}";
+
+corsMultipleOrigins(["\"\"", "http://example.com", origin]);
+corsMultipleOrigins(["\"\"", "http://example.com", "*"]);
+corsMultipleOrigins(["\"\"", origin, origin]);
+corsMultipleOrigins(["*", "http://example.com", "*"]);
+corsMultipleOrigins(["*", "http://example.com", origin]);
+corsMultipleOrigins(["", "http://example.com", "https://example2.com"]);
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/cors/cors-no-preflight.any.js b/testing/web-platform/tests/fetch/api/cors/cors-no-preflight.any.js
new file mode 100644
index 0000000000..7a0269aae4
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/cors-no-preflight.any.js
@@ -0,0 +1,41 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsNoPreflight(desc, baseURL, method, headerName, headerValue) {
+
+ var uuid_token = token();
+ var url = baseURL + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+ var urlParameters = "?token=" + uuid_token + "&max_age=0";
+ var requestInit = {"mode": "cors", "method": method, "headers":{}};
+ if (headerName)
+ requestInit["headers"][headerName] = headerValue;
+
+ promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ assert_equals(resp.status, 200, "Clean stash response's status is 200");
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "0", "No preflight request has been made");
+ });
+ });
+ }, desc);
+}
+
+var host_info = get_host_info();
+
+corsNoPreflight("Cross domain basic usage [GET]", host_info.HTTP_REMOTE_ORIGIN, "GET");
+corsNoPreflight("Same domain different port [GET]", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT, "GET");
+corsNoPreflight("Cross domain different port [GET]", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, "GET");
+corsNoPreflight("Cross domain different protocol [GET]", host_info.HTTPS_REMOTE_ORIGIN, "GET");
+corsNoPreflight("Same domain different protocol different port [GET]", host_info.HTTPS_ORIGIN, "GET");
+corsNoPreflight("Cross domain [POST]", host_info.HTTP_REMOTE_ORIGIN, "POST");
+corsNoPreflight("Cross domain [HEAD]", host_info.HTTP_REMOTE_ORIGIN, "HEAD");
+corsNoPreflight("Cross domain [GET] [Accept: */*]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Accept", "*/*");
+corsNoPreflight("Cross domain [GET] [Accept-Language: fr]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Accept-Language", "fr");
+corsNoPreflight("Cross domain [GET] [Content-Language: fr]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Language", "fr");
+corsNoPreflight("Cross domain [GET] [Content-Type: application/x-www-form-urlencoded]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "application/x-www-form-urlencoded");
+corsNoPreflight("Cross domain [GET] [Content-Type: multipart/form-data]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "multipart/form-data");
+corsNoPreflight("Cross domain [GET] [Content-Type: text/plain]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "text/plain");
+corsNoPreflight("Cross domain [GET] [Content-Type: text/plain;charset=utf-8]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "text/plain;charset=utf-8");
+corsNoPreflight("Cross domain [GET] [Content-Type: Text/Plain;charset=utf-8]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "Text/Plain;charset=utf-8");
diff --git a/testing/web-platform/tests/fetch/api/cors/cors-origin.any.js b/testing/web-platform/tests/fetch/api/cors/cors-origin.any.js
new file mode 100644
index 0000000000..30a02d910f
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/cors-origin.any.js
@@ -0,0 +1,51 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+/* If origin is undefined, it is set to fetched url's origin*/
+function corsOrigin(desc, baseURL, method, origin, shouldPass) {
+ if (!origin)
+ origin = baseURL;
+
+ var uuid_token = token();
+ var urlParameters = "?token=" + uuid_token + "&max_age=0&origin=" + encodeURIComponent(origin) + "&allow_methods=" + method;
+ var url = baseURL + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+ var requestInit = {"mode": "cors", "method": method};
+
+ promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ assert_equals(resp.status, 200, "Clean stash response's status is 200");
+ if (shouldPass) {
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ });
+ } else {
+ return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit));
+ }
+ });
+ }, desc);
+
+}
+
+var host_info = get_host_info();
+
+/* Actual origin */
+var origin = host_info.HTTP_ORIGIN;
+
+corsOrigin("Cross domain different subdomain [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "GET", origin, true);
+corsOrigin("Cross domain different subdomain [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "GET", undefined, false);
+corsOrigin("Same domain different port [origin OK]", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT, "GET", origin, true);
+corsOrigin("Same domain different port [origin KO]", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT, "GET", undefined, false);
+corsOrigin("Cross domain different port [origin OK]", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, "GET", origin, true);
+corsOrigin("Cross domain different port [origin KO]", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, "GET", undefined, false);
+corsOrigin("Cross domain different protocol [origin OK]", host_info.HTTPS_REMOTE_ORIGIN, "GET", origin, true);
+corsOrigin("Cross domain different protocol [origin KO]", host_info.HTTPS_REMOTE_ORIGIN, "GET", undefined, false);
+corsOrigin("Same domain different protocol different port [origin OK]", host_info.HTTPS_ORIGIN, "GET", origin, true);
+corsOrigin("Same domain different protocol different port [origin KO]", host_info.HTTPS_ORIGIN, "GET", undefined, false);
+corsOrigin("Cross domain [POST] [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "POST", origin, true);
+corsOrigin("Cross domain [POST] [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "POST", undefined, false);
+corsOrigin("Cross domain [HEAD] [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "HEAD", origin, true);
+corsOrigin("Cross domain [HEAD] [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "HEAD", undefined, false);
+corsOrigin("CORS preflight [PUT] [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "PUT", origin, true);
+corsOrigin("CORS preflight [PUT] [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "PUT", undefined, false);
+corsOrigin("Allowed origin: \"\" [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "GET", "" , false);
diff --git a/testing/web-platform/tests/fetch/api/cors/cors-preflight-cache.any.js b/testing/web-platform/tests/fetch/api/cors/cors-preflight-cache.any.js
new file mode 100644
index 0000000000..ce6a169d81
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/cors-preflight-cache.any.js
@@ -0,0 +1,46 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+var cors_url = get_host_info().HTTP_REMOTE_ORIGIN +
+ dirname(location.pathname) +
+ RESOURCES_DIR +
+ "preflight.py";
+
+promise_test((test) => {
+ var uuid_token = token();
+ var request_url =
+ cors_url + "?token=" + uuid_token + "&max_age=12000&allow_methods=POST" +
+ "&allow_headers=x-test-header";
+ return fetch(cors_url + "?token=" + uuid_token + "&clear-stash")
+ .then(() => {
+ return fetch(
+ new Request(request_url,
+ {
+ mode: "cors",
+ method: "POST",
+ headers: [["x-test-header", "test1"]]
+ }));
+ })
+ .then((resp) => {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made");
+ return fetch(cors_url + "?token=" + uuid_token + "&clear-stash");
+ })
+ .then((res) => res.text())
+ .then((txt) => {
+ assert_equals(txt, "1", "Server stash must be cleared.");
+ return fetch(
+ new Request(request_url,
+ {
+ mode: "cors",
+ method: "POST",
+ headers: [["x-test-header", "test2"]]
+ }));
+ })
+ .then((resp) => {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "0", "Preflight request has not been made");
+ return fetch(cors_url + "?token=" + uuid_token + "&clear-stash");
+ });
+});
diff --git a/testing/web-platform/tests/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js b/testing/web-platform/tests/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js
new file mode 100644
index 0000000000..b2747ccd5b
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js
@@ -0,0 +1,19 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=resources/corspreflight.js
+
+const corsURL = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+
+promise_test(() => fetch("resources/not-cors-safelisted.json").then(res => res.json().then(runTests)), "Loading data…");
+
+function runTests(testArray) {
+ testArray.forEach(testItem => {
+ const [headerName, headerValue] = testItem;
+ corsPreflight("Need CORS-preflight for " + headerName + "/" + headerValue + " header",
+ corsURL,
+ "GET",
+ true,
+ [[headerName, headerValue]]);
+ });
+}
diff --git a/testing/web-platform/tests/fetch/api/cors/cors-preflight-redirect.any.js b/testing/web-platform/tests/fetch/api/cors/cors-preflight-redirect.any.js
new file mode 100644
index 0000000000..15f7659abd
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/cors-preflight-redirect.any.js
@@ -0,0 +1,37 @@
+// META: global=window,worker
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsPreflightRedirect(desc, redirectUrl, redirectLocation, redirectStatus, redirectPreflight) {
+ var uuid_token = token();
+ var url = redirectUrl;
+ var urlParameters = "?token=" + uuid_token + "&max_age=0";
+ urlParameters += "&redirect_status=" + redirectStatus;
+ urlParameters += "&location=" + encodeURIComponent(redirectLocation);
+
+ if (redirectPreflight)
+ urlParameters += "&redirect_preflight";
+ var requestInit = {"mode": "cors", "redirect": "follow"};
+
+ /* Force preflight */
+ requestInit["headers"] = {"x-force-preflight": ""};
+ urlParameters += "&allow_headers=x-force-preflight";
+
+ promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ assert_equals(resp.status, 200, "Clean stash response's status is 200");
+ return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit));
+ });
+ }, desc);
+}
+
+var redirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py";
+var locationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+
+for (var code of [301, 302, 303, 307, 308]) {
+ /* preflight should not follow the redirection */
+ corsPreflightRedirect("Redirection " + code + " on preflight failed", redirectUrl, locationUrl, code, true);
+ /* preflight is done before redirection: preflight force redirect to error */
+ corsPreflightRedirect("Redirection " + code + " after preflight failed", redirectUrl, locationUrl, code, false);
+}
diff --git a/testing/web-platform/tests/fetch/api/cors/cors-preflight-referrer.any.js b/testing/web-platform/tests/fetch/api/cors/cors-preflight-referrer.any.js
new file mode 100644
index 0000000000..5df9fcf142
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/cors-preflight-referrer.any.js
@@ -0,0 +1,51 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsPreflightReferrer(desc, corsUrl, referrerPolicy, referrer, expectedReferrer) {
+ var uuid_token = token();
+ var url = corsUrl;
+ var urlParameters = "?token=" + uuid_token + "&max_age=0";
+ var requestInit = {"mode": "cors", "referrerPolicy": referrerPolicy};
+
+ if (referrer)
+ requestInit.referrer = referrer;
+
+ /* Force preflight */
+ requestInit["headers"] = {"x-force-preflight": ""};
+ urlParameters += "&allow_headers=x-force-preflight";
+
+ promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ assert_equals(resp.status, 200, "Clean stash response's status is 200");
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made");
+ assert_equals(resp.headers.get("x-preflight-referrer"), expectedReferrer, "Preflight's referrer is correct");
+ assert_equals(resp.headers.get("x-referrer"), expectedReferrer, "Request's referrer is correct");
+ assert_equals(resp.headers.get("x-control-request-headers"), "", "Access-Control-Allow-Headers value");
+ });
+ });
+ }, desc + " and referrer: " + (referrer ? "'" + referrer + "'" : "default"));
+}
+
+var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+var origin = get_host_info().HTTP_ORIGIN + "/";
+
+corsPreflightReferrer("Referrer policy: no-referrer", corsUrl, "no-referrer", undefined, "");
+corsPreflightReferrer("Referrer policy: no-referrer", corsUrl, "no-referrer", "myreferrer", "");
+
+corsPreflightReferrer("Referrer policy: \"\"", corsUrl, "", undefined, origin);
+corsPreflightReferrer("Referrer policy: \"\"", corsUrl, "", "myreferrer", origin);
+
+corsPreflightReferrer("Referrer policy: no-referrer-when-downgrade", corsUrl, "no-referrer-when-downgrade", undefined, location.toString())
+corsPreflightReferrer("Referrer policy: no-referrer-when-downgrade", corsUrl, "no-referrer-when-downgrade", "myreferrer", new URL("myreferrer", location).toString());
+
+corsPreflightReferrer("Referrer policy: origin", corsUrl, "origin", undefined, origin);
+corsPreflightReferrer("Referrer policy: origin", corsUrl, "origin", "myreferrer", origin);
+
+corsPreflightReferrer("Referrer policy: origin-when-cross-origin", corsUrl, "origin-when-cross-origin", undefined, origin);
+corsPreflightReferrer("Referrer policy: origin-when-cross-origin", corsUrl, "origin-when-cross-origin", "myreferrer", origin);
+
+corsPreflightReferrer("Referrer policy: unsafe-url", corsUrl, "unsafe-url", undefined, location.toString());
+corsPreflightReferrer("Referrer policy: unsafe-url", corsUrl, "unsafe-url", "myreferrer", new URL("myreferrer", location).toString());
diff --git a/testing/web-platform/tests/fetch/api/cors/cors-preflight-response-validation.any.js b/testing/web-platform/tests/fetch/api/cors/cors-preflight-response-validation.any.js
new file mode 100644
index 0000000000..718e351c1d
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/cors-preflight-response-validation.any.js
@@ -0,0 +1,33 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsPreflightResponseValidation(desc, corsUrl, allowHeaders, allowMethods) {
+ var uuid_token = token();
+ var url = corsUrl;
+ var requestInit = {"mode": "cors"};
+ /* Force preflight */
+ requestInit["headers"] = {"x-force-preflight": ""};
+
+ var urlParameters = "?token=" + uuid_token + "&max_age=0";
+ urlParameters += "&allow_headers=x-force-preflight";
+ if (allowHeaders)
+ urlParameters += "," + allowHeaders;
+ if (allowMethods)
+ urlParameters += "&allow_methods="+ allowMethods;
+
+ promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(async function(resp) {
+ assert_equals(resp.status, 200, "Clean stash response's status is 200");
+ await promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit));
+
+ return fetch(url + urlParameters).then(function(resp) {
+ assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made");
+ });
+ });
+ }, desc);
+}
+
+var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+corsPreflightResponseValidation("Preflight response with a bad Access-Control-Allow-Headers", corsUrl, "Bad value", null);
+corsPreflightResponseValidation("Preflight response with a bad Access-Control-Allow-Methods", corsUrl, null, "Bad value");
diff --git a/testing/web-platform/tests/fetch/api/cors/cors-preflight-star.any.js b/testing/web-platform/tests/fetch/api/cors/cors-preflight-star.any.js
new file mode 100644
index 0000000000..f9fb20469c
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/cors-preflight-star.any.js
@@ -0,0 +1,86 @@
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+const url = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py",
+ origin = location.origin // assuming an ASCII origin
+
+function preflightTest(succeeds, withCredentials, allowMethod, allowHeader, useMethod, useHeader) {
+ return promise_test(t => {
+ let testURL = url + "?",
+ requestInit = {}
+ if (withCredentials) {
+ testURL += "origin=" + origin + "&"
+ testURL += "credentials&"
+ requestInit.credentials = "include"
+ }
+ if (useMethod) {
+ requestInit.method = useMethod
+ }
+ if (useHeader.length > 0) {
+ requestInit.headers = [useHeader]
+ }
+ testURL += "allow_methods=" + allowMethod + "&"
+ testURL += "allow_headers=" + allowHeader + "&"
+
+ if (succeeds) {
+ return fetch(testURL, requestInit).then(resp => {
+ assert_equals(resp.headers.get("x-origin"), origin)
+ })
+ } else {
+ return promise_rejects_js(t, TypeError, fetch(testURL, requestInit))
+ }
+ }, "CORS that " + (succeeds ? "succeeds" : "fails") + " with credentials: " + withCredentials + "; method: " + useMethod + " (allowed: " + allowMethod + "); header: " + useHeader + " (allowed: " + allowHeader + ")")
+}
+
+// "GET" does not pass the case-sensitive method check, but in the safe list.
+preflightTest(true, false, "get", "x-test", "GET", ["X-Test", "1"])
+// Headers check is case-insensitive, and "*" works as any for method.
+preflightTest(true, false, "*", "x-test", "SUPER", ["X-Test", "1"])
+// "*" works as any only without credentials.
+preflightTest(true, false, "*", "*", "OK", ["X-Test", "1"])
+preflightTest(false, true, "*", "*", "OK", ["X-Test", "1"])
+preflightTest(false, true, "*", "", "PUT", [])
+preflightTest(false, true, "get", "*", "GET", ["X-Test", "1"])
+preflightTest(false, true, "*", "*", "GET", ["X-Test", "1"])
+// Exact character match works even for "*" with credentials.
+preflightTest(true, true, "*", "*", "*", ["*", "1"])
+
+// The following methods are upper-cased for init["method"] by
+// https://fetch.spec.whatwg.org/#concept-method-normalize
+// but not in Access-Control-Allow-Methods response.
+// But they are https://fetch.spec.whatwg.org/#cors-safelisted-method,
+// CORS anyway passes regardless of the cases.
+for (const METHOD of ['GET', 'HEAD', 'POST']) {
+ const method = METHOD.toLowerCase();
+ preflightTest(true, true, METHOD, "*", METHOD, [])
+ preflightTest(true, true, METHOD, "*", method, [])
+ preflightTest(true, true, method, "*", METHOD, [])
+ preflightTest(true, true, method, "*", method, [])
+}
+
+// The following methods are upper-cased for init["method"] by
+// https://fetch.spec.whatwg.org/#concept-method-normalize
+// but not in Access-Control-Allow-Methods response.
+// As they are not https://fetch.spec.whatwg.org/#cors-safelisted-method,
+// Access-Control-Allow-Methods should contain upper-cased methods,
+// while init["method"] can be either in upper or lower case.
+for (const METHOD of ['DELETE', 'PUT']) {
+ const method = METHOD.toLowerCase();
+ preflightTest(true, true, METHOD, "*", METHOD, [])
+ preflightTest(true, true, METHOD, "*", method, [])
+ preflightTest(false, true, method, "*", METHOD, [])
+ preflightTest(false, true, method, "*", method, [])
+}
+
+// "PATCH" is NOT upper-cased in both places because it is not listed in
+// https://fetch.spec.whatwg.org/#concept-method-normalize.
+// So Access-Control-Allow-Methods value and init["method"] should match
+// case-sensitively.
+preflightTest(true, true, "PATCH", "*", "PATCH", [])
+preflightTest(false, true, "PATCH", "*", "patch", [])
+preflightTest(false, true, "patch", "*", "PATCH", [])
+preflightTest(true, true, "patch", "*", "patch", [])
+
+// "Authorization" header can't be wildcarded.
+preflightTest(false, false, "*", "*", "POST", ["Authorization", "123"])
+preflightTest(true, false, "*", "*, Authorization", "POST", ["Authorization", "123"])
diff --git a/testing/web-platform/tests/fetch/api/cors/cors-preflight-status.any.js b/testing/web-platform/tests/fetch/api/cors/cors-preflight-status.any.js
new file mode 100644
index 0000000000..a4467a6087
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/cors-preflight-status.any.js
@@ -0,0 +1,37 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+/* Check preflight is ok if status is ok status (200 to 299)*/
+function corsPreflightStatus(desc, corsUrl, preflightStatus) {
+ var uuid_token = token();
+ var url = corsUrl;
+ var requestInit = {"mode": "cors"};
+ /* Force preflight */
+ requestInit["headers"] = {"x-force-preflight": ""};
+
+ var urlParameters = "?token=" + uuid_token + "&max_age=0";
+ urlParameters += "&allow_headers=x-force-preflight";
+ urlParameters += "&preflight_status=" + preflightStatus;
+
+ promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ assert_equals(resp.status, 200, "Clean stash response's status is 200");
+ if (200 <= preflightStatus && 299 >= preflightStatus) {
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made");
+ });
+ } else {
+ return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit));
+ }
+ });
+ }, desc);
+}
+
+var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+for (status of [200, 201, 202, 203, 204, 205, 206,
+ 300, 301, 302, 303, 304, 305, 306, 307, 308,
+ 400, 401, 402, 403, 404, 405,
+ 501, 502, 503, 504, 505])
+ corsPreflightStatus("Preflight answered with status " + status, corsUrl, status);
diff --git a/testing/web-platform/tests/fetch/api/cors/cors-preflight.any.js b/testing/web-platform/tests/fetch/api/cors/cors-preflight.any.js
new file mode 100644
index 0000000000..045422f40b
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/cors-preflight.any.js
@@ -0,0 +1,62 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=resources/corspreflight.js
+
+var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+
+corsPreflight("CORS [DELETE], server allows", corsUrl, "DELETE", true);
+corsPreflight("CORS [DELETE], server refuses", corsUrl, "DELETE", false);
+corsPreflight("CORS [PUT], server allows", corsUrl, "PUT", true);
+corsPreflight("CORS [PUT], server allows, check preflight has user agent", corsUrl + "?checkUserAgentHeaderInPreflight", "PUT", true);
+corsPreflight("CORS [PUT], server refuses", corsUrl, "PUT", false);
+corsPreflight("CORS [PATCH], server allows", corsUrl, "PATCH", true);
+corsPreflight("CORS [PATCH], server refuses", corsUrl, "PATCH", false);
+corsPreflight("CORS [patcH], server allows", corsUrl, "patcH", true);
+corsPreflight("CORS [patcH], server refuses", corsUrl, "patcH", false);
+corsPreflight("CORS [NEW], server allows", corsUrl, "NEW", true);
+corsPreflight("CORS [NEW], server refuses", corsUrl, "NEW", false);
+corsPreflight("CORS [chicken], server allows", corsUrl, "chicken", true);
+corsPreflight("CORS [chicken], server refuses", corsUrl, "chicken", false);
+
+corsPreflight("CORS [GET] [x-test-header: allowed], server allows", corsUrl, "GET", true, [["x-test-header1", "allowed"]]);
+corsPreflight("CORS [GET] [x-test-header: refused], server refuses", corsUrl, "GET", false, [["x-test-header1", "refused"]]);
+
+var headers = [
+ ["x-test-header1", "allowedOrRefused"],
+ ["x-test-header2", "allowedOrRefused"],
+ ["X-test-header3", "allowedOrRefused"],
+ ["x-test-header-b", "allowedOrRefused"],
+ ["x-test-header-D", "allowedOrRefused"],
+ ["x-test-header-C", "allowedOrRefused"],
+ ["x-test-header-a", "allowedOrRefused"],
+ ["Content-Type", "allowedOrRefused"],
+];
+var safeHeaders= [
+ ["Accept", "*"],
+ ["Accept-Language", "bzh"],
+ ["Content-Language", "eu"],
+];
+
+corsPreflight("CORS [GET] [several headers], server allows", corsUrl, "GET", true, headers, safeHeaders);
+corsPreflight("CORS [GET] [several headers], server refuses", corsUrl, "GET", false, headers, safeHeaders);
+corsPreflight("CORS [PUT] [several headers], server allows", corsUrl, "PUT", true, headers, safeHeaders);
+corsPreflight("CORS [PUT] [several headers], server refuses", corsUrl, "PUT", false, headers, safeHeaders);
+
+corsPreflight("CORS [PUT] [only safe headers], server allows", corsUrl, "PUT", true, null, safeHeaders);
+
+promise_test(async t => {
+ const url = `${corsUrl}?allow_headers=*`;
+ await promise_rejects_js(t, TypeError, fetch(url, {
+ headers: {
+ authorization: 'foobar'
+ }
+ }));
+}, '"authorization" should not be covered by the wildcard symbol');
+
+promise_test(async t => {
+ const url = `${corsUrl}?allow_headers=authorization`;
+ await fetch(url, { headers: {
+ authorization: 'foobar'
+ }});
+}, '"authorization" should be covered by "authorization"'); \ No newline at end of file
diff --git a/testing/web-platform/tests/fetch/api/cors/cors-redirect-credentials.any.js b/testing/web-platform/tests/fetch/api/cors/cors-redirect-credentials.any.js
new file mode 100644
index 0000000000..2aff313406
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/cors-redirect-credentials.any.js
@@ -0,0 +1,52 @@
+// META: timeout=long
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsRedirectCredentials(desc, redirectUrl, redirectLocation, redirectStatus, locationCredentials) {
+ var url = redirectUrl
+ var urlParameters = "?redirect_status=" + redirectStatus;
+ urlParameters += "&location=" + redirectLocation.replace("://", "://" + locationCredentials + "@");
+
+ var requestInit = {"mode": "cors", "redirect": "follow"};
+
+ promise_test(t => {
+ const result = fetch(url + urlParameters, requestInit)
+ if(locationCredentials === "") {
+ return result;
+ } else {
+ return promise_rejects_js(t, TypeError, result);
+ }
+ }, desc);
+}
+
+var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py";
+var preflightPath = dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+
+var host_info = get_host_info();
+
+var localRedirect = host_info.HTTP_ORIGIN + redirPath;
+var remoteRedirect = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + redirPath;
+
+var localLocation = host_info.HTTP_ORIGIN + preflightPath;
+var remoteLocation = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + preflightPath;
+var remoteLocation2 = host_info.HTTP_REMOTE_ORIGIN + preflightPath;
+
+for (var code of [301, 302, 303, 307, 308]) {
+ corsRedirectCredentials("Redirect " + code + " from same origin to remote without user and password", localRedirect, remoteLocation, code, "");
+
+ corsRedirectCredentials("Redirect " + code + " from same origin to remote with user and password", localRedirect, remoteLocation, code, "user:password");
+ corsRedirectCredentials("Redirect " + code + " from same origin to remote with user", localRedirect, remoteLocation, code, "user:");
+ corsRedirectCredentials("Redirect " + code + " from same origin to remote with password", localRedirect, remoteLocation, code, ":password");
+
+ corsRedirectCredentials("Redirect " + code + " from remote to same origin with user and password", remoteRedirect, localLocation, code, "user:password");
+ corsRedirectCredentials("Redirect " + code + " from remote to same origin with user", remoteRedirect, localLocation, code, "user:");
+ corsRedirectCredentials("Redirect " + code + " from remote to same origin with password", remoteRedirect, localLocation, code, ":password");
+
+ corsRedirectCredentials("Redirect " + code + " from remote to same remote with user and password", remoteRedirect, remoteLocation, code, "user:password");
+ corsRedirectCredentials("Redirect " + code + " from remote to same remote with user", remoteRedirect, remoteLocation, code, "user:");
+ corsRedirectCredentials("Redirect " + code + " from remote to same remote with password", remoteRedirect, remoteLocation, code, ":password");
+
+ corsRedirectCredentials("Redirect " + code + " from remote to another remote with user and password", remoteRedirect, remoteLocation2, code, "user:password");
+ corsRedirectCredentials("Redirect " + code + " from remote to another remote with user", remoteRedirect, remoteLocation2, code, "user:");
+ corsRedirectCredentials("Redirect " + code + " from remote to another remote with password", remoteRedirect, remoteLocation2, code, ":password");
+}
diff --git a/testing/web-platform/tests/fetch/api/cors/cors-redirect-preflight.any.js b/testing/web-platform/tests/fetch/api/cors/cors-redirect-preflight.any.js
new file mode 100644
index 0000000000..50848170d0
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/cors-redirect-preflight.any.js
@@ -0,0 +1,46 @@
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsRedirect(desc, redirectUrl, redirectLocation, redirectStatus, expectSuccess) {
+ var urlBaseParameters = "&redirect_status=" + redirectStatus;
+ var urlParametersSuccess = urlBaseParameters + "&allow_headers=x-w3c&location=" + encodeURIComponent(redirectLocation + "?allow_headers=x-w3c");
+ var urlParametersFailure = urlBaseParameters + "&location=" + encodeURIComponent(redirectLocation);
+
+ var requestInit = {"mode": "cors", "redirect": "follow", "headers" : [["x-w3c", "test"]]};
+
+ promise_test(function(test) {
+ var uuid_token = token();
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ return fetch(redirectUrl + "?token=" + uuid_token + "&max_age=0" + urlParametersSuccess, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made");
+ });
+ });
+ }, desc + " (preflight after redirection success case)");
+ promise_test(function(test) {
+ var uuid_token = token();
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ return promise_rejects_js(test, TypeError, fetch(redirectUrl + "?token=" + uuid_token + "&max_age=0" + urlParametersFailure, requestInit));
+ });
+ }, desc + " (preflight after redirection failure case)");
+}
+
+var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py";
+var preflightPath = dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+
+var host_info = get_host_info();
+
+var localRedirect = host_info.HTTP_ORIGIN + redirPath;
+var remoteRedirect = host_info.HTTP_REMOTE_ORIGIN + redirPath;
+
+var localLocation = host_info.HTTP_ORIGIN + preflightPath;
+var remoteLocation = host_info.HTTP_REMOTE_ORIGIN + preflightPath;
+var remoteLocation2 = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + preflightPath;
+
+for (var code of [301, 302, 303, 307, 308]) {
+ corsRedirect("Redirect " + code + ": same origin to cors", localRedirect, remoteLocation, code);
+ corsRedirect("Redirect " + code + ": cors to same origin", remoteRedirect, localLocation, code);
+ corsRedirect("Redirect " + code + ": cors to another cors", remoteRedirect, remoteLocation2, code);
+}
diff --git a/testing/web-platform/tests/fetch/api/cors/cors-redirect.any.js b/testing/web-platform/tests/fetch/api/cors/cors-redirect.any.js
new file mode 100644
index 0000000000..cdf4097d56
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/cors-redirect.any.js
@@ -0,0 +1,42 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function corsRedirect(desc, redirectUrl, redirectLocation, redirectStatus, expectedOrigin) {
+ var uuid_token = token();
+ var url = redirectUrl;
+ var urlParameters = "?token=" + uuid_token + "&max_age=0";
+ urlParameters += "&redirect_status=" + redirectStatus;
+ urlParameters += "&location=" + encodeURIComponent(redirectLocation);
+
+ var requestInit = {"mode": "cors", "redirect": "follow"};
+
+ return promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) {
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "0", "No preflight request has been made");
+ assert_equals(resp.headers.get("x-origin"), expectedOrigin, "Origin is correctly set after redirect");
+ });
+ });
+ }, desc);
+}
+
+var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py";
+var preflightPath = dirname(location.pathname) + RESOURCES_DIR + "preflight.py";
+
+var host_info = get_host_info();
+
+var localRedirect = host_info.HTTP_ORIGIN + redirPath;
+var remoteRedirect = host_info.HTTP_REMOTE_ORIGIN + redirPath;
+
+var localLocation = host_info.HTTP_ORIGIN + preflightPath;
+var remoteLocation = host_info.HTTP_REMOTE_ORIGIN + preflightPath;
+var remoteLocation2 = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + preflightPath;
+
+for (var code of [301, 302, 303, 307, 308]) {
+ corsRedirect("Redirect " + code + ": cors to same cors", remoteRedirect, remoteLocation, code, location.origin);
+ corsRedirect("Redirect " + code + ": cors to another cors", remoteRedirect, remoteLocation2, code, "null");
+ corsRedirect("Redirect " + code + ": same origin to cors", localRedirect, remoteLocation, code, location.origin);
+ corsRedirect("Redirect " + code + ": cors to same origin", remoteRedirect, localLocation, code, "null");
+}
diff --git a/testing/web-platform/tests/fetch/api/cors/data-url-iframe.html b/testing/web-platform/tests/fetch/api/cors/data-url-iframe.html
new file mode 100644
index 0000000000..217baa3c46
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/data-url-iframe.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body></body>
+<script>
+
+const createDataUrlIframe = (url, cors) => {
+ const iframe = document.createElement("iframe");
+ const fetchURL = new URL(url, location.href) +
+ `${cors === 'null-origin'
+ ? '?pipe=header(Access-Control-Allow-Origin, null)' : ''}`;
+ const tag_name = 'script';
+ iframe.src =
+ `data:text/html, <${tag_name}>` +
+ `async function test() {` +
+ ` let allowed = true;` +
+ ` try {` +
+ ` await fetch('${fetchURL}');` +
+ ` } catch (e) {` +
+ ` allowed = false;` +
+ ` }` +
+ ` parent.postMessage({allowed}, '*');` +
+ `}` +
+ `test(); </${tag_name}>`;
+ return iframe;
+};
+
+const fetch_from_data_url_iframe_test =
+ (url, cors, expectation, description) => {
+ promise_test(async () => {
+ const iframe = createDataUrlIframe(url, cors);
+ document.body.appendChild(iframe);
+ const msgEvent = await new Promise(resolve => window.onmessage = resolve);
+ assert_equals(msgEvent.data.allowed ? 'allowed' : 'rejected', expectation);
+ }, description);
+};
+
+fetch_from_data_url_iframe_test(
+ '../resources/top.txt',
+ 'acao-omitted',
+ 'rejected',
+ 'fetching "top.txt" without ACAO should be rejected.'
+);
+fetch_from_data_url_iframe_test(
+ '../resources/top.txt',
+ 'null-origin',
+ 'allowed',
+ 'fetching "top.txt" with CORS allowing null origin should be allowed.'
+);
+fetch_from_data_url_iframe_test(
+ 'data:text/plain, top',
+ 'acao-omitted',
+ 'allowed',
+ 'fetching data url script should be allowed.'
+);
+
+</script>
diff --git a/testing/web-platform/tests/fetch/api/cors/data-url-shared-worker.html b/testing/web-platform/tests/fetch/api/cors/data-url-shared-worker.html
new file mode 100644
index 0000000000..d69748ab26
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/data-url-shared-worker.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+const fetch_from_data_url_worker_test =
+ (url, cors, expectation, description) => {
+ promise_test(async () => {
+ const fetchURL = new URL(url, location.href) +
+ `${cors === 'null-origin'
+ ? '?pipe=header(Access-Control-Allow-Origin, null)' : ''}`;
+ const scriptURL =
+ `data:text/javascript,` +
+ `async function test(port) {` +
+ ` let allowed = true;` +
+ ` try {` +
+ ` await fetch('${fetchURL}');` +
+ ` } catch (e) {` +
+ ` allowed = false;` +
+ ` }` +
+ ` port.postMessage({allowed});` +
+ `}` +
+ `onconnect = e => {` +
+ ` test(e.ports[0]);` +
+ `};`;
+ const worker = new SharedWorker(scriptURL);
+ const msgEvent =
+ await new Promise(resolve => worker.port.onmessage = resolve);
+ assert_equals(msgEvent.data.allowed ? 'allowed' : 'rejected', expectation);
+ }, description);
+};
+
+fetch_from_data_url_worker_test(
+ '../resources/top.txt',
+ 'acao-omitted',
+ 'rejected',
+ 'fetching "top.txt" without ACAO should be rejected.'
+);
+fetch_from_data_url_worker_test(
+ '../resources/top.txt',
+ 'null-origin',
+ 'allowed',
+ 'fetching "top.txt" with CORS allowing null origin should be allowed.'
+);
+fetch_from_data_url_worker_test(
+ 'data:text/plain, top',
+ 'acao-omitted',
+ 'allowed',
+ 'fetching data url script should be allowed.'
+);
+
+</script>
diff --git a/testing/web-platform/tests/fetch/api/cors/data-url-worker.html b/testing/web-platform/tests/fetch/api/cors/data-url-worker.html
new file mode 100644
index 0000000000..13113e6262
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/data-url-worker.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+const fetch_from_data_url_shared_worker_test =
+ (url, cors, expectation, description) => {
+ promise_test(async () => {
+ const fetchURL = new URL(url, location.href) +
+ `${cors === 'null-origin'
+ ? '?pipe=header(Access-Control-Allow-Origin, null)' : ''}`;
+ const scriptURL =
+ `data:text/javascript,` +
+ `async function test() {` +
+ ` let allowed = true;` +
+ ` try {` +
+ ` await fetch('${fetchURL}');` +
+ ` } catch (e) {` +
+ ` allowed = false;` +
+ ` }` +
+ ` postMessage({allowed});` +
+ `}` +
+ `test();`;
+ const worker = new Worker(scriptURL);
+ const msgEvent = await new Promise(resolve => worker.onmessage = resolve);
+ assert_equals(msgEvent.data.allowed ? 'allowed' : 'rejected', expectation);
+ }, description);
+};
+
+fetch_from_data_url_shared_worker_test(
+ '../resources/top.txt',
+ 'acao-omitted',
+ 'rejected',
+ 'fetching "top.txt" without ACAO should be rejected.'
+);
+fetch_from_data_url_shared_worker_test(
+ '../resources/top.txt',
+ 'null-origin',
+ 'allowed',
+ 'fetching "top.txt" with CORS allowing null origin should be allowed.'
+);
+fetch_from_data_url_shared_worker_test(
+ 'data:text/plain, top',
+ 'acao-omitted',
+ 'allowed',
+ 'fetching data url script should be allowed.'
+);
+
+</script>
diff --git a/testing/web-platform/tests/fetch/api/cors/resources/corspreflight.js b/testing/web-platform/tests/fetch/api/cors/resources/corspreflight.js
new file mode 100644
index 0000000000..18b8f6dfa2
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/resources/corspreflight.js
@@ -0,0 +1,58 @@
+function headerNames(headers) {
+ let names = [];
+ for (let header of headers) {
+ names.push(header[0].toLowerCase());
+ }
+ return names;
+}
+
+/*
+ Check preflight is done
+ Control if server allows method and headers and check accordingly
+ Check control access headers added by UA (for method and headers)
+*/
+function corsPreflight(desc, corsUrl, method, allowed, headers, safeHeaders) {
+ return promise_test(function(test) {
+ var uuid_token = token();
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(response) {
+ var url = corsUrl + (corsUrl.indexOf("?") === -1 ? "?" : "&");
+ var urlParameters = "token=" + uuid_token + "&max_age=0";
+ var requestInit = {"mode": "cors", "method": method};
+ var requestHeaders = [];
+ if (headers)
+ requestHeaders.push.apply(requestHeaders, headers);
+ if (safeHeaders)
+ requestHeaders.push.apply(requestHeaders, safeHeaders);
+ requestInit["headers"] = requestHeaders;
+
+ if (allowed) {
+ urlParameters += "&allow_methods=" + method + "&control_request_headers";
+ if (headers) {
+ //Make the server allow the headers
+ urlParameters += "&allow_headers=" + headerNames(headers).join("%20%2C");
+ }
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made");
+ if (headers) {
+ var actualHeaders = resp.headers.get("x-control-request-headers").toLowerCase().split(",");
+ for (var i in actualHeaders)
+ actualHeaders[i] = actualHeaders[i].trim();
+ for (var header of headers)
+ assert_in_array(header[0].toLowerCase(), actualHeaders, "Preflight asked permission for header: " + header);
+
+ let accessControlAllowHeaders = headerNames(headers).sort().join(",");
+ assert_equals(resp.headers.get("x-control-request-headers"), accessControlAllowHeaders, "Access-Control-Allow-Headers value");
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token);
+ } else {
+ assert_equals(resp.headers.get("x-control-request-headers"), null, "Access-Control-Request-Headers should be omitted")
+ }
+ });
+ } else {
+ return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)).then(function(){
+ return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token);
+ });
+ }
+ });
+ }, desc);
+}
diff --git a/testing/web-platform/tests/fetch/api/cors/resources/not-cors-safelisted.json b/testing/web-platform/tests/fetch/api/cors/resources/not-cors-safelisted.json
new file mode 100644
index 0000000000..945dc0f93b
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/resources/not-cors-safelisted.json
@@ -0,0 +1,13 @@
+[
+ ["accept", "\""],
+ ["accept", "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678"],
+ ["accept-language", "\u0001"],
+ ["accept-language", "@"],
+ ["authorization", "basics"],
+ ["content-language", "\u0001"],
+ ["content-language", "@"],
+ ["content-type", "text/html"],
+ ["content-type", "text/plain; long=0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901"],
+ ["range", "bytes 0-"],
+ ["test", "hi"]
+]
diff --git a/testing/web-platform/tests/fetch/api/cors/sandboxed-iframe.html b/testing/web-platform/tests/fetch/api/cors/sandboxed-iframe.html
new file mode 100644
index 0000000000..feb9f1f2e5
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/cors/sandboxed-iframe.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe sandbox="allow-scripts" src="../resources/sandboxed-iframe.html"></iframe>
+<script>
+promise_test(async (t) => {
+ const message = await new Promise((resolve) => {
+ window.addEventListener('message', e => resolve(e.data));
+ });
+ assert_equals(message, 'PASS');
+}, 'CORS with sandboxed iframe');
+</script>
+</html>
diff --git a/testing/web-platform/tests/fetch/api/crashtests/aborted-fetch-response.https.html b/testing/web-platform/tests/fetch/api/crashtests/aborted-fetch-response.https.html
new file mode 100644
index 0000000000..fa1ad1717f
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/crashtests/aborted-fetch-response.https.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<script type="module">
+ const abort = new AbortController();
+ const resp = await fetch("5401a7dfd80adbd578b3e91b86fdc6966a752de7.vtt", {
+ signal: abort.signal,
+ });
+ abort.abort();
+ await resp.body.closed;
+ const cache = await caches.open("cache_name_0");
+ await cache.put("bb4ea079adb4fe423f1d6cec18bc1caf78ac4cd6.ico", resp);
+</script>
diff --git a/testing/web-platform/tests/fetch/api/crashtests/body-window-destroy.html b/testing/web-platform/tests/fetch/api/crashtests/body-window-destroy.html
new file mode 100644
index 0000000000..646d3c5f8c
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/crashtests/body-window-destroy.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<iframe srcdoc='
+ <script>
+ let a = new Blob(["a", "𢕾"], {})
+ let b = new Response(a)
+ try { let _ = b.body } catch (e) { }
+ frameElement.remove()
+ b.json().catch(() => {})
+ </script>
+'></iframe>
diff --git a/testing/web-platform/tests/fetch/api/crashtests/request.html b/testing/web-platform/tests/fetch/api/crashtests/request.html
new file mode 100644
index 0000000000..2d21930c3b
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/crashtests/request.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/common/utils.js"></script>
+<script>
+ // Cycle collection test for a case where the Request object is alive and accessible globally.
+ var req = new Request(`/`);
+ fetch(req)
+</script>
diff --git a/testing/web-platform/tests/fetch/api/credentials/authentication-basic.any.js b/testing/web-platform/tests/fetch/api/credentials/authentication-basic.any.js
new file mode 100644
index 0000000000..31ccc38697
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/credentials/authentication-basic.any.js
@@ -0,0 +1,17 @@
+// META: global=window,worker
+
+function basicAuth(desc, user, pass, mode, status) {
+ promise_test(function(test) {
+ var headers = { "Authorization": "Basic " + btoa(user + ":" + pass)};
+ var requestInit = {"credentials": mode, "headers": headers};
+ return fetch("../resources/authentication.py?realm=test", requestInit).then(function(resp) {
+ assert_equals(resp.status, status, "HTTP status is " + status);
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ });
+ }, desc);
+}
+
+basicAuth("User-added Authorization header with include mode", "user", "password", "include", 200);
+basicAuth("User-added Authorization header with same-origin mode", "user", "password", "same-origin", 200);
+basicAuth("User-added Authorization header with omit mode", "user", "password", "omit", 200);
+basicAuth("User-added bogus Authorization header with omit mode", "notuser", "notpassword", "omit", 401);
diff --git a/testing/web-platform/tests/fetch/api/credentials/authentication-redirection.any.js b/testing/web-platform/tests/fetch/api/credentials/authentication-redirection.any.js
new file mode 100644
index 0000000000..16656b5435
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/credentials/authentication-redirection.any.js
@@ -0,0 +1,29 @@
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+
+const authorizationValue = "Basic " + btoa("user:pass");
+async function getAuthorizationHeaderValue(url)
+{
+ const headers = { "Authorization": authorizationValue};
+ const requestInit = {"headers": headers};
+ const response = await fetch(url, requestInit);
+ return response.text();
+}
+
+promise_test(async test => {
+ const result = await getAuthorizationHeaderValue("/fetch/api/resources/dump-authorization-header.py");
+ assert_equals(result, authorizationValue);
+}, "getAuthorizationHeaderValue - no redirection");
+
+promise_test(async test => {
+ result = await getAuthorizationHeaderValue("/fetch/api/resources/redirect.py?location=" + encodeURIComponent("/fetch/api/resources/dump-authorization-header.py"));
+ assert_equals(result, authorizationValue);
+
+ result = await getAuthorizationHeaderValue(get_host_info().HTTPS_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py?allow_headers=Authorization&location=" + encodeURIComponent(get_host_info().HTTPS_REMOTE_ORIGIN + "/fetch/api/resources/dump-authorization-header.py"));
+ assert_equals(result, authorizationValue);
+}, "getAuthorizationHeaderValue - same origin redirection");
+
+promise_test(async (test) => {
+ const result = await getAuthorizationHeaderValue(get_host_info().HTTPS_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py?allow_headers=Authorization&location=" + encodeURIComponent(get_host_info().HTTPS_ORIGIN + "/fetch/api/resources/dump-authorization-header.py"));
+ assert_equals(result, "none");
+}, "getAuthorizationHeaderValue - cross origin redirection");
diff --git a/testing/web-platform/tests/fetch/api/credentials/cookies.any.js b/testing/web-platform/tests/fetch/api/credentials/cookies.any.js
new file mode 100644
index 0000000000..de30e47765
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/credentials/cookies.any.js
@@ -0,0 +1,49 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+function cookies(desc, credentials1, credentials2 ,cookies) {
+ var url = RESOURCES_DIR + "top.txt"
+ var urlParameters = "";
+ var urlCleanParameters = "";
+ if (cookies) {
+ urlParameters +="?pipe=header(Set-Cookie,";
+ urlParameters += cookies.join(",True)|header(Set-Cookie,") + ",True)";
+ urlCleanParameters +="?pipe=header(Set-Cookie,";
+ urlCleanParameters += cookies.join("%3B%20max-age=0,True)|header(Set-Cookie,") + "%3B%20max-age=0,True)";
+ }
+
+ var requestInit = {"credentials": credentials1}
+ promise_test(function(test){
+ var requestInit = {"credentials": credentials1}
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ //check cookies sent
+ return fetch(RESOURCES_DIR + "inspect-headers.py?headers=cookie" , {"credentials": credentials2});
+ }).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ assert_false(resp.headers.has("Cookie") , "Cookie header is not exposed in response");
+ if (credentials1 != "omit" && credentials2 != "omit") {
+ assert_equals(resp.headers.get("x-request-cookie") , cookies.join("; "), "Request include cookie(s)");
+ }
+ else {
+ assert_false(resp.headers.has("x-request-cookie") , "Request does not have cookie(s)");
+ }
+ //clean cookies
+ return fetch(url + urlCleanParameters, {"credentials": "include"});
+ }).catch(function(e) {
+ return fetch(url + urlCleanParameters, {"credentials": "include"}).then(function() {
+ return Promise.reject(e);
+ });
+ });
+ }, desc);
+}
+
+cookies("Include mode: 1 cookie", "include", "include", ["a=1"]);
+cookies("Include mode: 2 cookies", "include", "include", ["b=2", "c=3"]);
+cookies("Omit mode: discard cookies", "omit", "omit", ["d=4"]);
+cookies("Omit mode: no cookie is stored", "omit", "include", ["e=5"]);
+cookies("Omit mode: no cookie is sent", "include", "omit", ["f=6"]);
+cookies("Same-origin mode: 1 cookie", "same-origin", "same-origin", ["a=1"]);
+cookies("Same-origin mode: 2 cookies", "same-origin", "same-origin", ["b=2", "c=3"]);
diff --git a/testing/web-platform/tests/fetch/api/headers/header-setcookie.any.js b/testing/web-platform/tests/fetch/api/headers/header-setcookie.any.js
new file mode 100644
index 0000000000..cafb780c2c
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/headers/header-setcookie.any.js
@@ -0,0 +1,266 @@
+// META: title=Headers set-cookie special cases
+// META: global=window,worker
+
+const headerList = [
+ ["set-cookie", "foo=bar"],
+ ["Set-Cookie", "fizz=buzz; domain=example.com"],
+];
+
+const setCookie2HeaderList = [
+ ["set-cookie2", "foo2=bar2"],
+ ["Set-Cookie2", "fizz2=buzz2; domain=example2.com"],
+];
+
+function assert_nested_array_equals(actual, expected) {
+ assert_equals(actual.length, expected.length, "Array length is not equal");
+ for (let i = 0; i < expected.length; i++) {
+ assert_array_equals(actual[i], expected[i]);
+ }
+}
+
+test(function () {
+ const headers = new Headers(headerList);
+ assert_equals(
+ headers.get("set-cookie"),
+ "foo=bar, fizz=buzz; domain=example.com",
+ );
+}, "Headers.prototype.get combines set-cookie headers in order");
+
+test(function () {
+ const headers = new Headers(headerList);
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["set-cookie", "foo=bar"],
+ ["set-cookie", "fizz=buzz; domain=example.com"],
+ ]);
+}, "Headers iterator does not combine set-cookie headers");
+
+test(function () {
+ const headers = new Headers(setCookie2HeaderList);
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"],
+ ]);
+}, "Headers iterator does not special case set-cookie2 headers");
+
+test(function () {
+ const headers = new Headers([...headerList, ...setCookie2HeaderList]);
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["set-cookie", "foo=bar"],
+ ["set-cookie", "fizz=buzz; domain=example.com"],
+ ["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"],
+ ]);
+}, "Headers iterator does not combine set-cookie & set-cookie2 headers");
+
+test(function () {
+ // Values are in non alphabetic order, and the iterator should yield in the
+ // headers in the exact order of the input.
+ const headers = new Headers([
+ ["set-cookie", "z=z"],
+ ["set-cookie", "a=a"],
+ ["set-cookie", "n=n"],
+ ]);
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["set-cookie", "z=z"],
+ ["set-cookie", "a=a"],
+ ["set-cookie", "n=n"],
+ ]);
+}, "Headers iterator preserves set-cookie ordering");
+
+test(
+ function () {
+ const headers = new Headers([
+ ["xylophone-header", "1"],
+ ["best-header", "2"],
+ ["set-cookie", "3"],
+ ["a-cool-header", "4"],
+ ["set-cookie", "5"],
+ ["a-cool-header", "6"],
+ ["best-header", "7"],
+ ]);
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["a-cool-header", "4, 6"],
+ ["best-header", "2, 7"],
+ ["set-cookie", "3"],
+ ["set-cookie", "5"],
+ ["xylophone-header", "1"],
+ ]);
+ },
+ "Headers iterator preserves per header ordering, but sorts keys alphabetically",
+);
+
+test(
+ function () {
+ const headers = new Headers([
+ ["xylophone-header", "7"],
+ ["best-header", "6"],
+ ["set-cookie", "5"],
+ ["a-cool-header", "4"],
+ ["set-cookie", "3"],
+ ["a-cool-header", "2"],
+ ["best-header", "1"],
+ ]);
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["a-cool-header", "4, 2"],
+ ["best-header", "6, 1"],
+ ["set-cookie", "5"],
+ ["set-cookie", "3"],
+ ["xylophone-header", "7"],
+ ]);
+ },
+ "Headers iterator preserves per header ordering, but sorts keys alphabetically (and ignores value ordering)",
+);
+
+test(function () {
+ const headers = new Headers([["fizz", "buzz"], ["X-Header", "test"]]);
+ const iterator = headers[Symbol.iterator]();
+ assert_array_equals(iterator.next().value, ["fizz", "buzz"]);
+ headers.append("Set-Cookie", "a=b");
+ assert_array_equals(iterator.next().value, ["set-cookie", "a=b"]);
+ headers.append("Accept", "text/html");
+ assert_array_equals(iterator.next().value, ["set-cookie", "a=b"]);
+ assert_array_equals(iterator.next().value, ["x-header", "test"]);
+ headers.append("set-cookie", "c=d");
+ assert_array_equals(iterator.next().value, ["x-header", "test"]);
+ assert_true(iterator.next().done);
+}, "Headers iterator is correctly updated with set-cookie changes");
+
+test(function () {
+ const headers = new Headers([
+ ["set-cookie", "a"],
+ ["set-cookie", "b"],
+ ["set-cookie", "c"]
+ ]);
+ const iterator = headers[Symbol.iterator]();
+ assert_array_equals(iterator.next().value, ["set-cookie", "a"]);
+ headers.delete("set-cookie");
+ headers.append("set-cookie", "d");
+ headers.append("set-cookie", "e");
+ headers.append("set-cookie", "f");
+ assert_array_equals(iterator.next().value, ["set-cookie", "e"]);
+ assert_array_equals(iterator.next().value, ["set-cookie", "f"]);
+ assert_true(iterator.next().done);
+}, "Headers iterator is correctly updated with set-cookie changes #2");
+
+test(function () {
+ const headers = new Headers(headerList);
+ assert_true(headers.has("sEt-cOoKiE"));
+}, "Headers.prototype.has works for set-cookie");
+
+test(function () {
+ const headers = new Headers(setCookie2HeaderList);
+ headers.append("set-Cookie", "foo=bar");
+ headers.append("sEt-cOoKiE", "fizz=buzz");
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["set-cookie", "foo=bar"],
+ ["set-cookie", "fizz=buzz"],
+ ["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"],
+ ]);
+}, "Headers.prototype.append works for set-cookie");
+
+test(function () {
+ const headers = new Headers(headerList);
+ headers.set("set-cookie", "foo2=bar2");
+ const list = [...headers];
+ assert_nested_array_equals(list, [
+ ["set-cookie", "foo2=bar2"],
+ ]);
+}, "Headers.prototype.set works for set-cookie");
+
+test(function () {
+ const headers = new Headers(headerList);
+ headers.delete("set-Cookie");
+ const list = [...headers];
+ assert_nested_array_equals(list, []);
+}, "Headers.prototype.delete works for set-cookie");
+
+test(function () {
+ const headers = new Headers();
+ assert_array_equals(headers.getSetCookie(), []);
+}, "Headers.prototype.getSetCookie with no headers present");
+
+test(function () {
+ const headers = new Headers([headerList[0]]);
+ assert_array_equals(headers.getSetCookie(), ["foo=bar"]);
+}, "Headers.prototype.getSetCookie with one header");
+
+test(function () {
+ const headers = new Headers({ "Set-Cookie": "foo=bar" });
+ assert_array_equals(headers.getSetCookie(), ["foo=bar"]);
+}, "Headers.prototype.getSetCookie with one header created from an object");
+
+test(function () {
+ const headers = new Headers(headerList);
+ assert_array_equals(headers.getSetCookie(), [
+ "foo=bar",
+ "fizz=buzz; domain=example.com",
+ ]);
+}, "Headers.prototype.getSetCookie with multiple headers");
+
+test(function () {
+ const headers = new Headers([["set-cookie", ""]]);
+ assert_array_equals(headers.getSetCookie(), [""]);
+}, "Headers.prototype.getSetCookie with an empty header");
+
+test(function () {
+ const headers = new Headers([["set-cookie", "x"], ["set-cookie", "x"]]);
+ assert_array_equals(headers.getSetCookie(), ["x", "x"]);
+}, "Headers.prototype.getSetCookie with two equal headers");
+
+test(function () {
+ const headers = new Headers([
+ ["set-cookie2", "x"],
+ ["set-cookie", "y"],
+ ["set-cookie2", "z"],
+ ]);
+ assert_array_equals(headers.getSetCookie(), ["y"]);
+}, "Headers.prototype.getSetCookie ignores set-cookie2 headers");
+
+test(function () {
+ // Values are in non alphabetic order, and the iterator should yield in the
+ // headers in the exact order of the input.
+ const headers = new Headers([
+ ["set-cookie", "z=z"],
+ ["set-cookie", "a=a"],
+ ["set-cookie", "n=n"],
+ ]);
+ assert_array_equals(headers.getSetCookie(), ["z=z", "a=a", "n=n"]);
+}, "Headers.prototype.getSetCookie preserves header ordering");
+
+test(function () {
+ const headers = new Headers({"Set-Cookie": " a=b\n"});
+ headers.append("set-cookie", "\n\rc=d ");
+ assert_nested_array_equals([...headers], [
+ ["set-cookie", "a=b"],
+ ["set-cookie", "c=d"]
+ ]);
+ headers.set("set-cookie", "\te=f ");
+ assert_nested_array_equals([...headers], [["set-cookie", "e=f"]]);
+}, "Adding Set-Cookie headers normalizes their value");
+
+test(function () {
+ assert_throws_js(TypeError, () => {
+ new Headers({"set-cookie": "\0"});
+ });
+
+ const headers = new Headers();
+ assert_throws_js(TypeError, () => {
+ headers.append("Set-Cookie", "a\nb");
+ });
+ assert_throws_js(TypeError, () => {
+ headers.set("Set-Cookie", "a\rb");
+ });
+}, "Adding invalid Set-Cookie headers throws");
+
+test(function () {
+ const response = new Response();
+ response.headers.append("Set-Cookie", "foo=bar");
+ assert_array_equals(response.headers.getSetCookie(), []);
+ response.headers.append("sEt-cOokIe", "bar=baz");
+ assert_array_equals(response.headers.getSetCookie(), []);
+}, "Set-Cookie is a forbidden response header");
diff --git a/testing/web-platform/tests/fetch/api/headers/header-values-normalize.any.js b/testing/web-platform/tests/fetch/api/headers/header-values-normalize.any.js
new file mode 100644
index 0000000000..5710554ada
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/headers/header-values-normalize.any.js
@@ -0,0 +1,72 @@
+// META: title=Header value normalizing test
+// META: global=window,worker
+// META: timeout=long
+
+"use strict";
+
+for(let i = 0; i < 0x21; i++) {
+ let fail = false,
+ strip = false
+
+ // REMOVE 0x0B/0x0C exception once https://github.com/web-platform-tests/wpt/issues/8372 is fixed
+ if(i === 0x0B || i === 0x0C)
+ continue
+
+ if(i === 0) {
+ fail = true
+ }
+
+ if(i === 0x09 || i === 0x0A || i === 0x0D || i === 0x20) {
+ strip = true
+ }
+
+ let url = "../resources/inspect-headers.py?headers=val1|val2|val3",
+ val = String.fromCharCode(i),
+ expectedVal = strip ? "" : val,
+ val1 = val,
+ expectedVal1 = expectedVal,
+ val2 = "x" + val,
+ expectedVal2 = "x" + expectedVal,
+ val3 = val + "x",
+ expectedVal3 = expectedVal + "x"
+
+ // XMLHttpRequest is not available in service workers
+ if (!self.GLOBAL.isWorker()) {
+ async_test((t) => {
+ let xhr = new XMLHttpRequest()
+ xhr.open("POST", url)
+ if(fail) {
+ assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("val1", val1))
+ assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("val2", val2))
+ assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("val3", val3))
+ t.done()
+ } else {
+ xhr.setRequestHeader("val1", val1)
+ xhr.setRequestHeader("val2", val2)
+ xhr.setRequestHeader("val3", val3)
+ xhr.onload = t.step_func_done(() => {
+ assert_equals(xhr.getResponseHeader("x-request-val1"), expectedVal1)
+ assert_equals(xhr.getResponseHeader("x-request-val2"), expectedVal2)
+ assert_equals(xhr.getResponseHeader("x-request-val3"), expectedVal3)
+ })
+ xhr.send()
+ }
+ }, "XMLHttpRequest with value " + encodeURI(val))
+ }
+
+ promise_test((t) => {
+ if(fail) {
+ return Promise.all([
+ promise_rejects_js(t, TypeError, fetch(url, { headers: {"val1": val1} })),
+ promise_rejects_js(t, TypeError, fetch(url, { headers: {"val2": val2} })),
+ promise_rejects_js(t, TypeError, fetch(url, { headers: {"val3": val3} }))
+ ])
+ } else {
+ return fetch(url, { headers: {"val1": val1, "val2": val2, "val3": val3} }).then((res) => {
+ assert_equals(res.headers.get("x-request-val1"), expectedVal1)
+ assert_equals(res.headers.get("x-request-val2"), expectedVal2)
+ assert_equals(res.headers.get("x-request-val3"), expectedVal3)
+ })
+ }
+ }, "fetch() with value " + encodeURI(val))
+}
diff --git a/testing/web-platform/tests/fetch/api/headers/header-values.any.js b/testing/web-platform/tests/fetch/api/headers/header-values.any.js
new file mode 100644
index 0000000000..bb7570c5a3
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/headers/header-values.any.js
@@ -0,0 +1,63 @@
+// META: title=Header value test
+// META: global=window,worker
+// META: timeout=long
+
+"use strict";
+
+// Invalid values
+[0, 0x0A, 0x0D].forEach(val => {
+ val = "x" + String.fromCharCode(val) + "x"
+
+ // XMLHttpRequest is not available in service workers
+ if (!self.GLOBAL.isWorker()) {
+ test(() => {
+ let xhr = new XMLHttpRequest()
+ xhr.open("POST", "/")
+ assert_throws_dom("SyntaxError", () => xhr.setRequestHeader("value-test", val))
+ }, "XMLHttpRequest with value " + encodeURI(val) + " needs to throw")
+ }
+
+ promise_test(t => promise_rejects_js(t, TypeError, fetch("/", { headers: {"value-test": val} })), "fetch() with value " + encodeURI(val) + " needs to throw")
+})
+
+// Valid values
+let headerValues =[]
+for(let i = 0; i < 0x100; i++) {
+ if(i === 0 || i === 0x0A || i === 0x0D) {
+ continue
+ }
+ headerValues.push("x" + String.fromCharCode(i) + "x")
+}
+var url = "../resources/inspect-headers.py?headers="
+headerValues.forEach((_, i) => {
+ url += "val" + i + "|"
+})
+
+// XMLHttpRequest is not available in service workers
+if (!self.GLOBAL.isWorker()) {
+ async_test((t) => {
+ let xhr = new XMLHttpRequest()
+ xhr.open("POST", url)
+ headerValues.forEach((val, i) => {
+ xhr.setRequestHeader("val" + i, val)
+ })
+ xhr.onload = t.step_func_done(() => {
+ headerValues.forEach((val, i) => {
+ assert_equals(xhr.getResponseHeader("x-request-val" + i), val)
+ })
+ })
+ xhr.send()
+ }, "XMLHttpRequest with all valid values")
+}
+
+promise_test((t) => {
+ const headers = new Headers
+ headerValues.forEach((val, i) => {
+ headers.append("val" + i, val)
+ })
+ return fetch(url, { headers }).then((res) => {
+ headerValues.forEach((val, i) => {
+ assert_equals(res.headers.get("x-request-val" + i), val)
+ })
+ })
+}, "fetch() with all valid values")
diff --git a/testing/web-platform/tests/fetch/api/headers/headers-basic.any.js b/testing/web-platform/tests/fetch/api/headers/headers-basic.any.js
new file mode 100644
index 0000000000..ead1047645
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/headers/headers-basic.any.js
@@ -0,0 +1,275 @@
+// META: title=Headers structure
+// META: global=window,worker
+
+"use strict";
+
+test(function() {
+ new Headers();
+}, "Create headers from no parameter");
+
+test(function() {
+ new Headers(undefined);
+}, "Create headers from undefined parameter");
+
+test(function() {
+ new Headers({});
+}, "Create headers from empty object");
+
+var parameters = [null, 1];
+parameters.forEach(function(parameter) {
+ test(function() {
+ assert_throws_js(TypeError, function() { new Headers(parameter) });
+ }, "Create headers with " + parameter + " should throw");
+});
+
+var headerDict = {"name1": "value1",
+ "name2": "value2",
+ "name3": "value3",
+ "name4": null,
+ "name5": undefined,
+ "name6": 1,
+ "Content-Type": "value4"
+};
+
+var headerSeq = [];
+for (var name in headerDict)
+ headerSeq.push([name, headerDict[name]]);
+
+test(function() {
+ var headers = new Headers(headerSeq);
+ for (name in headerDict) {
+ assert_equals(headers.get(name), String(headerDict[name]),
+ "name: " + name + " has value: " + headerDict[name]);
+ }
+ assert_equals(headers.get("length"), null, "init should be treated as a sequence, not as a dictionary");
+}, "Create headers with sequence");
+
+test(function() {
+ var headers = new Headers(headerDict);
+ for (name in headerDict) {
+ assert_equals(headers.get(name), String(headerDict[name]),
+ "name: " + name + " has value: " + headerDict[name]);
+ }
+}, "Create headers with record");
+
+test(function() {
+ var headers = new Headers(headerDict);
+ var headers2 = new Headers(headers);
+ for (name in headerDict) {
+ assert_equals(headers2.get(name), String(headerDict[name]),
+ "name: " + name + " has value: " + headerDict[name]);
+ }
+}, "Create headers with existing headers");
+
+test(function() {
+ var headers = new Headers()
+ headers[Symbol.iterator] = function *() {
+ yield ["test", "test"]
+ }
+ var headers2 = new Headers(headers)
+ assert_equals(headers2.get("test"), "test")
+}, "Create headers with existing headers with custom iterator");
+
+test(function() {
+ var headers = new Headers();
+ for (name in headerDict) {
+ headers.append(name, headerDict[name]);
+ assert_equals(headers.get(name), String(headerDict[name]),
+ "name: " + name + " has value: " + headerDict[name]);
+ }
+}, "Check append method");
+
+test(function() {
+ var headers = new Headers();
+ for (name in headerDict) {
+ headers.set(name, headerDict[name]);
+ assert_equals(headers.get(name), String(headerDict[name]),
+ "name: " + name + " has value: " + headerDict[name]);
+ }
+}, "Check set method");
+
+test(function() {
+ var headers = new Headers(headerDict);
+ for (name in headerDict)
+ assert_true(headers.has(name),"headers has name " + name);
+
+ assert_false(headers.has("nameNotInHeaders"),"headers do not have header: nameNotInHeaders");
+}, "Check has method");
+
+test(function() {
+ var headers = new Headers(headerDict);
+ for (name in headerDict) {
+ assert_true(headers.has(name),"headers have a header: " + name);
+ headers.delete(name)
+ assert_true(!headers.has(name),"headers do not have anymore a header: " + name);
+ }
+}, "Check delete method");
+
+test(function() {
+ var headers = new Headers(headerDict);
+ for (name in headerDict)
+ assert_equals(headers.get(name), String(headerDict[name]),
+ "name: " + name + " has value: " + headerDict[name]);
+
+ assert_equals(headers.get("nameNotInHeaders"), null, "header: nameNotInHeaders has no value");
+}, "Check get method");
+
+var headerEntriesDict = {"name1": "value1",
+ "Name2": "value2",
+ "name": "value3",
+ "content-Type": "value4",
+ "Content-Typ": "value5",
+ "Content-Types": "value6"
+};
+var sortedHeaderDict = {};
+var headerValues = [];
+var sortedHeaderKeys = Object.keys(headerEntriesDict).map(function(value) {
+ sortedHeaderDict[value.toLowerCase()] = headerEntriesDict[value];
+ headerValues.push(headerEntriesDict[value]);
+ return value.toLowerCase();
+}).sort();
+
+var iteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()));
+function checkIteratorProperties(iterator) {
+ var prototype = Object.getPrototypeOf(iterator);
+ assert_equals(Object.getPrototypeOf(prototype), iteratorPrototype);
+
+ var descriptor = Object.getOwnPropertyDescriptor(prototype, "next");
+ assert_true(descriptor.configurable, "configurable");
+ assert_true(descriptor.enumerable, "enumerable");
+ assert_true(descriptor.writable, "writable");
+}
+
+test(function() {
+ var headers = new Headers(headerEntriesDict);
+ var actual = headers.keys();
+ checkIteratorProperties(actual);
+
+ sortedHeaderKeys.forEach(function(key) {
+ const entry = actual.next();
+ assert_false(entry.done);
+ assert_equals(entry.value, key);
+ });
+ assert_true(actual.next().done);
+ assert_true(actual.next().done);
+
+ for (const key of headers.keys())
+ assert_true(sortedHeaderKeys.indexOf(key) != -1);
+}, "Check keys method");
+
+test(function() {
+ var headers = new Headers(headerEntriesDict);
+ var actual = headers.values();
+ checkIteratorProperties(actual);
+
+ sortedHeaderKeys.forEach(function(key) {
+ const entry = actual.next();
+ assert_false(entry.done);
+ assert_equals(entry.value, sortedHeaderDict[key]);
+ });
+ assert_true(actual.next().done);
+ assert_true(actual.next().done);
+
+ for (const value of headers.values())
+ assert_true(headerValues.indexOf(value) != -1);
+}, "Check values method");
+
+test(function() {
+ var headers = new Headers(headerEntriesDict);
+ var actual = headers.entries();
+ checkIteratorProperties(actual);
+
+ sortedHeaderKeys.forEach(function(key) {
+ const entry = actual.next();
+ assert_false(entry.done);
+ assert_equals(entry.value[0], key);
+ assert_equals(entry.value[1], sortedHeaderDict[key]);
+ });
+ assert_true(actual.next().done);
+ assert_true(actual.next().done);
+
+ for (const entry of headers.entries())
+ assert_equals(entry[1], sortedHeaderDict[entry[0]]);
+}, "Check entries method");
+
+test(function() {
+ var headers = new Headers(headerEntriesDict);
+ var actual = headers[Symbol.iterator]();
+
+ sortedHeaderKeys.forEach(function(key) {
+ const entry = actual.next();
+ assert_false(entry.done);
+ assert_equals(entry.value[0], key);
+ assert_equals(entry.value[1], sortedHeaderDict[key]);
+ });
+ assert_true(actual.next().done);
+ assert_true(actual.next().done);
+}, "Check Symbol.iterator method");
+
+test(function() {
+ var headers = new Headers(headerEntriesDict);
+ var reference = sortedHeaderKeys[Symbol.iterator]();
+ headers.forEach(function(value, key, container) {
+ assert_equals(headers, container);
+ const entry = reference.next();
+ assert_false(entry.done);
+ assert_equals(key, entry.value);
+ assert_equals(value, sortedHeaderDict[entry.value]);
+ });
+ assert_true(reference.next().done);
+}, "Check forEach method");
+
+test(() => {
+ const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0"});
+ const actualKeys = [];
+ const actualValues = [];
+ for (const [header, value] of headers) {
+ actualKeys.push(header);
+ actualValues.push(value);
+ headers.delete("foo");
+ }
+ assert_array_equals(actualKeys, ["bar", "baz"]);
+ assert_array_equals(actualValues, ["0", "1"]);
+}, "Iteration skips elements removed while iterating");
+
+test(() => {
+ const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0", "quux": "3"});
+ const actualKeys = [];
+ const actualValues = [];
+ for (const [header, value] of headers) {
+ actualKeys.push(header);
+ actualValues.push(value);
+ if (header === "baz")
+ headers.delete("bar");
+ }
+ assert_array_equals(actualKeys, ["bar", "baz", "quux"]);
+ assert_array_equals(actualValues, ["0", "1", "3"]);
+}, "Removing elements already iterated over causes an element to be skipped during iteration");
+
+test(() => {
+ const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0", "quux": "3"});
+ const actualKeys = [];
+ const actualValues = [];
+ for (const [header, value] of headers) {
+ actualKeys.push(header);
+ actualValues.push(value);
+ if (header === "baz")
+ headers.append("X-yZ", "4");
+ }
+ assert_array_equals(actualKeys, ["bar", "baz", "foo", "quux", "x-yz"]);
+ assert_array_equals(actualValues, ["0", "1", "2", "3", "4"]);
+}, "Appending a value pair during iteration causes it to be reached during iteration");
+
+test(() => {
+ const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0", "quux": "3"});
+ const actualKeys = [];
+ const actualValues = [];
+ for (const [header, value] of headers) {
+ actualKeys.push(header);
+ actualValues.push(value);
+ if (header === "baz")
+ headers.append("abc", "-1");
+ }
+ assert_array_equals(actualKeys, ["bar", "baz", "baz", "foo", "quux"]);
+ assert_array_equals(actualValues, ["0", "1", "1", "2", "3"]);
+}, "Prepending a value pair before the current element position causes it to be skipped during iteration and adds the current element a second time");
diff --git a/testing/web-platform/tests/fetch/api/headers/headers-casing.any.js b/testing/web-platform/tests/fetch/api/headers/headers-casing.any.js
new file mode 100644
index 0000000000..20b8a9d375
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/headers/headers-casing.any.js
@@ -0,0 +1,54 @@
+// META: title=Headers case management
+// META: global=window,worker
+
+"use strict";
+
+var headerDictCase = {"UPPERCASE": "value1",
+ "lowercase": "value2",
+ "mixedCase": "value3",
+ "Content-TYPE": "value4"
+ };
+
+function checkHeadersCase(originalName, headersToCheck, expectedDict) {
+ var lowCaseName = originalName.toLowerCase();
+ var upCaseName = originalName.toUpperCase();
+ var expectedValue = expectedDict[originalName];
+ assert_equals(headersToCheck.get(originalName), expectedValue,
+ "name: " + originalName + " has value: " + expectedValue);
+ assert_equals(headersToCheck.get(lowCaseName), expectedValue,
+ "name: " + lowCaseName + " has value: " + expectedValue);
+ assert_equals(headersToCheck.get(upCaseName), expectedValue,
+ "name: " + upCaseName + " has value: " + expectedValue);
+}
+
+test(function() {
+ var headers = new Headers(headerDictCase);
+ for (const name in headerDictCase)
+ checkHeadersCase(name, headers, headerDictCase)
+}, "Create headers, names use characters with different case");
+
+test(function() {
+ var headers = new Headers();
+ for (const name in headerDictCase) {
+ headers.append(name, headerDictCase[name]);
+ checkHeadersCase(name, headers, headerDictCase);
+ }
+}, "Check append method, names use characters with different case");
+
+test(function() {
+ var headers = new Headers();
+ for (const name in headerDictCase) {
+ headers.set(name, headerDictCase[name]);
+ checkHeadersCase(name, headers, headerDictCase);
+ }
+}, "Check set method, names use characters with different case");
+
+test(function() {
+ var headers = new Headers();
+ for (const name in headerDictCase)
+ headers.set(name, headerDictCase[name]);
+ for (const name in headerDictCase)
+ headers.delete(name.toLowerCase());
+ for (const name in headerDictCase)
+ assert_false(headers.has(name), "header " + name + " should have been deleted");
+}, "Check delete method, names use characters with different case");
diff --git a/testing/web-platform/tests/fetch/api/headers/headers-combine.any.js b/testing/web-platform/tests/fetch/api/headers/headers-combine.any.js
new file mode 100644
index 0000000000..4f3b6d11df
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/headers/headers-combine.any.js
@@ -0,0 +1,66 @@
+// META: title=Headers have combined (and sorted) values
+// META: global=window,worker
+
+"use strict";
+
+var headerSeqCombine = [["single", "singleValue"],
+ ["double", "doubleValue1"],
+ ["double", "doubleValue2"],
+ ["triple", "tripleValue1"],
+ ["triple", "tripleValue2"],
+ ["triple", "tripleValue3"]
+];
+var expectedDict = {"single": "singleValue",
+ "double": "doubleValue1, doubleValue2",
+ "triple": "tripleValue1, tripleValue2, tripleValue3"
+};
+
+test(function() {
+ var headers = new Headers(headerSeqCombine);
+ for (const name in expectedDict)
+ assert_equals(headers.get(name), expectedDict[name]);
+}, "Create headers using same name for different values");
+
+test(function() {
+ var headers = new Headers(headerSeqCombine);
+ for (const name in expectedDict) {
+ assert_true(headers.has(name), "name: " + name + " has value(s)");
+ headers.delete(name);
+ assert_false(headers.has(name), "name: " + name + " has no value(s) anymore");
+ }
+}, "Check delete and has methods when using same name for different values");
+
+test(function() {
+ var headers = new Headers(headerSeqCombine);
+ for (const name in expectedDict) {
+ headers.set(name,"newSingleValue");
+ assert_equals(headers.get(name), "newSingleValue", "name: " + name + " has value: newSingleValue");
+ }
+}, "Check set methods when called with already used name");
+
+test(function() {
+ var headers = new Headers(headerSeqCombine);
+ for (const name in expectedDict) {
+ var value = headers.get(name);
+ headers.append(name,"newSingleValue");
+ assert_equals(headers.get(name), (value + ", " + "newSingleValue"));
+ }
+}, "Check append methods when called with already used name");
+
+test(() => {
+ const headers = new Headers([["1", "a"],["1", "b"]]);
+ for(let header of headers) {
+ assert_array_equals(header, ["1", "a, b"]);
+ }
+}, "Iterate combined values");
+
+test(() => {
+ const headers = new Headers([["2", "a"], ["1", "b"], ["2", "b"]]),
+ expected = [["1", "b"], ["2", "a, b"]];
+ let i = 0;
+ for(let header of headers) {
+ assert_array_equals(header, expected[i]);
+ i++;
+ }
+ assert_equals(i, 2);
+}, "Iterate combined values in sorted order")
diff --git a/testing/web-platform/tests/fetch/api/headers/headers-errors.any.js b/testing/web-platform/tests/fetch/api/headers/headers-errors.any.js
new file mode 100644
index 0000000000..82dadd8234
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/headers/headers-errors.any.js
@@ -0,0 +1,96 @@
+// META: title=Headers errors
+// META: global=window,worker
+
+"use strict";
+
+test(function() {
+ assert_throws_js(TypeError, function() { new Headers([["name"]]); });
+}, "Create headers giving an array having one string as init argument");
+
+test(function() {
+ assert_throws_js(TypeError, function() { new Headers([["invalid", "invalidValue1", "invalidValue2"]]); });
+}, "Create headers giving an array having three strings as init argument");
+
+test(function() {
+ assert_throws_js(TypeError, function() { new Headers([["invalidĀ", "Value1"]]); });
+}, "Create headers giving bad header name as init argument");
+
+test(function() {
+ assert_throws_js(TypeError, function() { new Headers([["name", "invalidValueĀ"]]); });
+}, "Create headers giving bad header value as init argument");
+
+var badNames = ["invalidĀ", {}];
+var badValues = ["invalidĀ"];
+
+badNames.forEach(function(name) {
+ test(function() {
+ var headers = new Headers();
+ assert_throws_js(TypeError, function() { headers.get(name); });
+ }, "Check headers get with an invalid name " + name);
+});
+
+badNames.forEach(function(name) {
+ test(function() {
+ var headers = new Headers();
+ assert_throws_js(TypeError, function() { headers.delete(name); });
+ }, "Check headers delete with an invalid name " + name);
+});
+
+badNames.forEach(function(name) {
+ test(function() {
+ var headers = new Headers();
+ assert_throws_js(TypeError, function() { headers.has(name); });
+ }, "Check headers has with an invalid name " + name);
+});
+
+badNames.forEach(function(name) {
+ test(function() {
+ var headers = new Headers();
+ assert_throws_js(TypeError, function() { headers.set(name, "Value1"); });
+ }, "Check headers set with an invalid name " + name);
+});
+
+badValues.forEach(function(value) {
+ test(function() {
+ var headers = new Headers();
+ assert_throws_js(TypeError, function() { headers.set("name", value); });
+ }, "Check headers set with an invalid value " + value);
+});
+
+badNames.forEach(function(name) {
+ test(function() {
+ var headers = new Headers();
+ assert_throws_js(TypeError, function() { headers.append("invalidĀ", "Value1"); });
+ }, "Check headers append with an invalid name " + name);
+});
+
+badValues.forEach(function(value) {
+ test(function() {
+ var headers = new Headers();
+ assert_throws_js(TypeError, function() { headers.append("name", value); });
+ }, "Check headers append with an invalid value " + value);
+});
+
+test(function() {
+ var headers = new Headers([["name", "value"]]);
+ assert_throws_js(TypeError, function() { headers.forEach(); });
+ assert_throws_js(TypeError, function() { headers.forEach(undefined); });
+ assert_throws_js(TypeError, function() { headers.forEach(1); });
+}, "Headers forEach throws if argument is not callable");
+
+test(function() {
+ var headers = new Headers([["name1", "value1"], ["name2", "value2"], ["name3", "value3"]]);
+ var counter = 0;
+ try {
+ headers.forEach(function(value, name) {
+ counter++;
+ if (name == "name2")
+ throw "error";
+ });
+ } catch (e) {
+ assert_equals(counter, 2);
+ assert_equals(e, "error");
+ return;
+ }
+ assert_unreached();
+}, "Headers forEach loop should stop if callback is throwing exception");
diff --git a/testing/web-platform/tests/fetch/api/headers/headers-no-cors.any.js b/testing/web-platform/tests/fetch/api/headers/headers-no-cors.any.js
new file mode 100644
index 0000000000..60dbb9ef67
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/headers/headers-no-cors.any.js
@@ -0,0 +1,59 @@
+// META: global=window,worker
+
+"use strict";
+
+promise_test(() => fetch("../cors/resources/not-cors-safelisted.json").then(res => res.json().then(runTests)), "Loading data…");
+
+const longValue = "s".repeat(127);
+
+[
+ {
+ "headers": ["accept", "accept-language", "content-language"],
+ "values": [longValue, "", longValue]
+ },
+ {
+ "headers": ["accept", "accept-language", "content-language"],
+ "values": ["", longValue]
+ },
+ {
+ "headers": ["content-type"],
+ "values": ["text/plain;" + "s".repeat(116), "text/plain"]
+ }
+].forEach(testItem => {
+ testItem.headers.forEach(header => {
+ test(() => {
+ const noCorsHeaders = new Request("about:blank", { mode: "no-cors" }).headers;
+ testItem.values.forEach((value) => {
+ noCorsHeaders.append(header, value);
+ assert_equals(noCorsHeaders.get(header), testItem.values[0], '1');
+ });
+ noCorsHeaders.set(header, testItem.values.join(", "));
+ assert_equals(noCorsHeaders.get(header), testItem.values[0], '2');
+ noCorsHeaders.delete(header);
+ assert_false(noCorsHeaders.has(header));
+ }, "\"no-cors\" Headers object cannot have " + header + " set to " + testItem.values.join(", "));
+ });
+});
+
+function runTests(testArray) {
+ testArray = testArray.concat([
+ ["dpr", "2"],
+ ["rtt", "1.0"],
+ ["downlink", "-1.0"],
+ ["ect", "6g"],
+ ["save-data", "on"],
+ ["viewport-width", "100"],
+ ["width", "100"],
+ ["unknown", "doesitmatter"]
+ ]);
+ testArray.forEach(testItem => {
+ const [headerName, headerValue] = testItem;
+ test(() => {
+ const noCorsHeaders = new Request("about:blank", { mode: "no-cors" }).headers;
+ noCorsHeaders.append(headerName, headerValue);
+ assert_false(noCorsHeaders.has(headerName));
+ noCorsHeaders.set(headerName, headerValue);
+ assert_false(noCorsHeaders.has(headerName));
+ }, "\"no-cors\" Headers object cannot have " + headerName + "/" + headerValue + " as header");
+ });
+}
diff --git a/testing/web-platform/tests/fetch/api/headers/headers-normalize.any.js b/testing/web-platform/tests/fetch/api/headers/headers-normalize.any.js
new file mode 100644
index 0000000000..68cf5b85f3
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/headers/headers-normalize.any.js
@@ -0,0 +1,56 @@
+// META: title=Headers normalize values
+// META: global=window,worker
+
+"use strict";
+
+const expectations = {
+ "name1": [" space ", "space"],
+ "name2": ["\ttab\t", "tab"],
+ "name3": [" spaceAndTab\t", "spaceAndTab"],
+ "name4": ["\r\n newLine", "newLine"], //obs-fold cases
+ "name5": ["newLine\r\n ", "newLine"],
+ "name6": ["\r\n\tnewLine", "newLine"],
+ "name7": ["\t\f\tnewLine\n", "\f\tnewLine"],
+ "name8": ["newLine\xa0", "newLine\xa0"], // \xa0 == non breaking space
+};
+
+test(function () {
+ const headerDict = Object.fromEntries(
+ Object.entries(expectations).map(([name, [actual]]) => [name, actual]),
+ );
+ var headers = new Headers(headerDict);
+ for (const name in expectations) {
+ const expected = expectations[name][1];
+ assert_equals(
+ headers.get(name),
+ expected,
+ "name: " + name + " has normalized value: " + expected,
+ );
+ }
+}, "Create headers with not normalized values");
+
+test(function () {
+ var headers = new Headers();
+ for (const name in expectations) {
+ headers.append(name, expectations[name][0]);
+ const expected = expectations[name][1];
+ assert_equals(
+ headers.get(name),
+ expected,
+ "name: " + name + " has value: " + expected,
+ );
+ }
+}, "Check append method with not normalized values");
+
+test(function () {
+ var headers = new Headers();
+ for (const name in expectations) {
+ headers.set(name, expectations[name][0]);
+ const expected = expectations[name][1];
+ assert_equals(
+ headers.get(name),
+ expected,
+ "name: " + name + " has value: " + expected,
+ );
+ }
+}, "Check set method with not normalized values");
diff --git a/testing/web-platform/tests/fetch/api/headers/headers-record.any.js b/testing/web-platform/tests/fetch/api/headers/headers-record.any.js
new file mode 100644
index 0000000000..fa853914f4
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/headers/headers-record.any.js
@@ -0,0 +1,357 @@
+// META: global=window,worker
+
+"use strict";
+
+var log = [];
+function clearLog() {
+ log = [];
+}
+function addLogEntry(name, args) {
+ log.push([ name, ...args ]);
+}
+
+var loggingHandler = {
+};
+
+setup(function() {
+ for (let prop of Object.getOwnPropertyNames(Reflect)) {
+ loggingHandler[prop] = function(...args) {
+ addLogEntry(prop, args);
+ return Reflect[prop](...args);
+ }
+ }
+});
+
+test(function() {
+ var h = new Headers();
+ assert_equals([...h].length, 0);
+}, "Passing nothing to Headers constructor");
+
+test(function() {
+ var h = new Headers(undefined);
+ assert_equals([...h].length, 0);
+}, "Passing undefined to Headers constructor");
+
+test(function() {
+ assert_throws_js(TypeError, function() {
+ var h = new Headers(null);
+ });
+}, "Passing null to Headers constructor");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = { a: "b" };
+ var proxy = new Proxy(record, loggingHandler);
+ var h = new Headers(proxy);
+
+ assert_equals(log.length, 4);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[3], ["get", record, "a", proxy]);
+
+ // Check the results.
+ assert_equals([...h].length, 1);
+ assert_array_equals([...h.keys()], ["a"]);
+ assert_true(h.has("a"));
+ assert_equals(h.get("a"), "b");
+}, "Basic operation with one property");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var recordProto = { c: "d" };
+ var record = Object.create(recordProto, { a: { value: "b", enumerable: true } });
+ var proxy = new Proxy(record, loggingHandler);
+ var h = new Headers(proxy);
+
+ assert_equals(log.length, 4);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[3], ["get", record, "a", proxy]);
+
+ // Check the results.
+ assert_equals([...h].length, 1);
+ assert_array_equals([...h.keys()], ["a"]);
+ assert_true(h.has("a"));
+ assert_equals(h.get("a"), "b");
+}, "Basic operation with one property and a proto");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = { a: "b", c: "d" };
+ var proxy = new Proxy(record, loggingHandler);
+ var h = new Headers(proxy);
+
+ assert_equals(log.length, 6);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[3], ["get", record, "a", proxy]);
+ // Then the second [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[4], ["getOwnPropertyDescriptor", record, "c"]);
+ // Then the second [[Get]] from step 5.2.
+ assert_array_equals(log[5], ["get", record, "c", proxy]);
+
+ // Check the results.
+ assert_equals([...h].length, 2);
+ assert_array_equals([...h.keys()], ["a", "c"]);
+ assert_true(h.has("a"));
+ assert_equals(h.get("a"), "b");
+ assert_true(h.has("c"));
+ assert_equals(h.get("c"), "d");
+}, "Correct operation ordering with two properties");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = { a: "b", "\uFFFF": "d" };
+ var proxy = new Proxy(record, loggingHandler);
+ assert_throws_js(TypeError, function() {
+ var h = new Headers(proxy);
+ });
+
+ assert_equals(log.length, 5);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[3], ["get", record, "a", proxy]);
+ // Then the second [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[4], ["getOwnPropertyDescriptor", record, "\uFFFF"]);
+ // The second [[Get]] never happens, because we convert the invalid name to a
+ // ByteString first and throw.
+}, "Correct operation ordering with two properties one of which has an invalid name");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = { a: "\uFFFF", c: "d" }
+ var proxy = new Proxy(record, loggingHandler);
+ assert_throws_js(TypeError, function() {
+ var h = new Headers(proxy);
+ });
+
+ assert_equals(log.length, 4);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[3], ["get", record, "a", proxy]);
+ // Nothing else after this, because converting the result of that [[Get]] to a
+ // ByteString throws.
+}, "Correct operation ordering with two properties one of which has an invalid value");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = {};
+ Object.defineProperty(record, "a", { value: "b", enumerable: false });
+ Object.defineProperty(record, "c", { value: "d", enumerable: true });
+ Object.defineProperty(record, "e", { value: "f", enumerable: false });
+ var proxy = new Proxy(record, loggingHandler);
+ var h = new Headers(proxy);
+
+ assert_equals(log.length, 6);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // No [[Get]] because not enumerable
+ // Then the second [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[3], ["getOwnPropertyDescriptor", record, "c"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[4], ["get", record, "c", proxy]);
+ // Then the third [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[5], ["getOwnPropertyDescriptor", record, "e"]);
+ // No [[Get]] because not enumerable
+
+ // Check the results.
+ assert_equals([...h].length, 1);
+ assert_array_equals([...h.keys()], ["c"]);
+ assert_true(h.has("c"));
+ assert_equals(h.get("c"), "d");
+}, "Correct operation ordering with non-enumerable properties");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = {a: "b", c: "d", e: "f"};
+ var lyingHandler = {
+ getOwnPropertyDescriptor: function(target, name) {
+ if (name == "a" || name == "e") {
+ return undefined;
+ }
+ return Reflect.getOwnPropertyDescriptor(target, name);
+ }
+ };
+ var lyingProxy = new Proxy(record, lyingHandler);
+ var proxy = new Proxy(lyingProxy, loggingHandler);
+ var h = new Headers(proxy);
+
+ assert_equals(log.length, 6);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", lyingProxy, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", lyingProxy]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", lyingProxy, "a"]);
+ // No [[Get]] because no descriptor
+ // Then the second [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[3], ["getOwnPropertyDescriptor", lyingProxy, "c"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[4], ["get", lyingProxy, "c", proxy]);
+ // Then the third [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[5], ["getOwnPropertyDescriptor", lyingProxy, "e"]);
+ // No [[Get]] because no descriptor
+
+ // Check the results.
+ assert_equals([...h].length, 1);
+ assert_array_equals([...h.keys()], ["c"]);
+ assert_true(h.has("c"));
+ assert_equals(h.get("c"), "d");
+}, "Correct operation ordering with undefined descriptors");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = {a: "b", c: "d"};
+ var lyingHandler = {
+ ownKeys: function() {
+ return [ "a", "c", "a", "c" ];
+ },
+ };
+ var lyingProxy = new Proxy(record, lyingHandler);
+ var proxy = new Proxy(lyingProxy, loggingHandler);
+
+ // Returning duplicate keys from ownKeys() throws a TypeError.
+ assert_throws_js(TypeError,
+ function() { var h = new Headers(proxy); });
+
+ assert_equals(log.length, 2);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", lyingProxy, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", lyingProxy]);
+}, "Correct operation ordering with repeated keys");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = {
+ a: "b",
+ [Symbol.toStringTag]: {
+ // Make sure the ToString conversion of the value happens
+ // after the ToString conversion of the key.
+ toString: function () { addLogEntry("toString", [this]); return "nope"; }
+ },
+ c: "d" };
+ var proxy = new Proxy(record, loggingHandler);
+ assert_throws_js(TypeError,
+ function() { var h = new Headers(proxy); });
+
+ assert_equals(log.length, 7);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[3], ["get", record, "a", proxy]);
+ // Then the second [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[4], ["getOwnPropertyDescriptor", record, "c"]);
+ // Then the second [[Get]] from step 5.2.
+ assert_array_equals(log[5], ["get", record, "c", proxy]);
+ // Then the third [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[6], ["getOwnPropertyDescriptor", record,
+ Symbol.toStringTag]);
+ // Then we throw an exception converting the Symbol to a string, before we do
+ // the third [[Get]].
+}, "Basic operation with Symbol keys");
+
+test(function() {
+ this.add_cleanup(clearLog);
+ var record = {
+ a: {
+ toString: function() { addLogEntry("toString", [this]); return "b"; }
+ },
+ [Symbol.toStringTag]: {
+ toString: function () { addLogEntry("toString", [this]); return "nope"; }
+ },
+ c: {
+ toString: function() { addLogEntry("toString", [this]); return "d"; }
+ }
+ };
+ // Now make that Symbol-named property not enumerable.
+ Object.defineProperty(record, Symbol.toStringTag, { enumerable: false });
+ assert_array_equals(Reflect.ownKeys(record),
+ ["a", "c", Symbol.toStringTag]);
+
+ var proxy = new Proxy(record, loggingHandler);
+ var h = new Headers(proxy);
+
+ assert_equals(log.length, 9);
+ // The first thing is the [[Get]] of Symbol.iterator to figure out whether
+ // we're a sequence, during overload resolution.
+ assert_array_equals(log[0], ["get", record, Symbol.iterator, proxy]);
+ // Then we have the [[OwnPropertyKeys]] from
+ // https://webidl.spec.whatwg.org/#es-to-record step 4.
+ assert_array_equals(log[1], ["ownKeys", record]);
+ // Then the [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[2], ["getOwnPropertyDescriptor", record, "a"]);
+ // Then the [[Get]] from step 5.2.
+ assert_array_equals(log[3], ["get", record, "a", proxy]);
+ // Then the ToString on the value.
+ assert_array_equals(log[4], ["toString", record.a]);
+ // Then the second [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[5], ["getOwnPropertyDescriptor", record, "c"]);
+ // Then the second [[Get]] from step 5.2.
+ assert_array_equals(log[6], ["get", record, "c", proxy]);
+ // Then the ToString on the value.
+ assert_array_equals(log[7], ["toString", record.c]);
+ // Then the third [[GetOwnProperty]] from step 5.1.
+ assert_array_equals(log[8], ["getOwnPropertyDescriptor", record,
+ Symbol.toStringTag]);
+ // No [[Get]] because not enumerable.
+
+ // Check the results.
+ assert_equals([...h].length, 2);
+ assert_array_equals([...h.keys()], ["a", "c"]);
+ assert_true(h.has("a"));
+ assert_equals(h.get("a"), "b");
+ assert_true(h.has("c"));
+ assert_equals(h.get("c"), "d");
+}, "Operation with non-enumerable Symbol keys");
diff --git a/testing/web-platform/tests/fetch/api/headers/headers-structure.any.js b/testing/web-platform/tests/fetch/api/headers/headers-structure.any.js
new file mode 100644
index 0000000000..d826bcab2a
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/headers/headers-structure.any.js
@@ -0,0 +1,20 @@
+// META: title=Headers basic
+// META: global=window,worker
+
+"use strict";
+
+var headers = new Headers();
+var methods = ["append",
+ "delete",
+ "get",
+ "has",
+ "set",
+ //Headers is iterable
+ "entries",
+ "keys",
+ "values"
+ ];
+for (var idx in methods)
+ test(function() {
+ assert_true(methods[idx] in headers, "headers has " + methods[idx] + " method");
+ }, "Headers has " + methods[idx] + " method");
diff --git a/testing/web-platform/tests/fetch/api/idlharness.any.js b/testing/web-platform/tests/fetch/api/idlharness.any.js
new file mode 100644
index 0000000000..7b3c694e16
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/idlharness.any.js
@@ -0,0 +1,21 @@
+// META: global=window,worker
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+// META: timeout=long
+
+idl_test(
+ ['fetch'],
+ ['referrer-policy', 'html', 'dom'],
+ idl_array => {
+ idl_array.add_objects({
+ Headers: ["new Headers()"],
+ Request: ["new Request('about:blank')"],
+ Response: ["new Response()"],
+ });
+ if (self.GLOBAL.isWindow()) {
+ idl_array.add_objects({ Window: ['window'] });
+ } else if (self.GLOBAL.isWorker()) {
+ idl_array.add_objects({ WorkerGlobalScope: ['self'] });
+ }
+ }
+);
diff --git a/testing/web-platform/tests/fetch/api/policies/csp-blocked-worker.html b/testing/web-platform/tests/fetch/api/policies/csp-blocked-worker.html
new file mode 100644
index 0000000000..e8660dffa9
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/csp-blocked-worker.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch in worker: blocked by CSP</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ fetch_tests_from_worker(new Worker("csp-blocked.js"));
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/fetch/api/policies/csp-blocked.html b/testing/web-platform/tests/fetch/api/policies/csp-blocked.html
new file mode 100644
index 0000000000..99e90dfcd8
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/csp-blocked.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch: blocked by CSP</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script src="../resources/utils.js"></script>
+ <script src="csp-blocked.js"></script>
+ </body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/fetch/api/policies/csp-blocked.html.headers b/testing/web-platform/tests/fetch/api/policies/csp-blocked.html.headers
new file mode 100644
index 0000000000..c8c1e9ffbd
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/csp-blocked.html.headers
@@ -0,0 +1 @@
+Content-Security-Policy: connect-src 'none'; \ No newline at end of file
diff --git a/testing/web-platform/tests/fetch/api/policies/csp-blocked.js b/testing/web-platform/tests/fetch/api/policies/csp-blocked.js
new file mode 100644
index 0000000000..28653fff85
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/csp-blocked.js
@@ -0,0 +1,13 @@
+if (this.document === undefined) {
+ importScripts("/resources/testharness.js");
+ importScripts("../resources/utils.js");
+}
+
+//Content-Security-Policy: connect-src 'none'; cf .headers file
+cspViolationUrl = RESOURCES_DIR + "top.txt";
+
+promise_test(function(test) {
+ return promise_rejects_js(test, TypeError, fetch(cspViolationUrl));
+}, "Fetch is blocked by CSP, got a TypeError");
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/policies/csp-blocked.js.headers b/testing/web-platform/tests/fetch/api/policies/csp-blocked.js.headers
new file mode 100644
index 0000000000..c8c1e9ffbd
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/csp-blocked.js.headers
@@ -0,0 +1 @@
+Content-Security-Policy: connect-src 'none'; \ No newline at end of file
diff --git a/testing/web-platform/tests/fetch/api/policies/nested-policy.js b/testing/web-platform/tests/fetch/api/policies/nested-policy.js
new file mode 100644
index 0000000000..b0d17696c3
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/nested-policy.js
@@ -0,0 +1 @@
+// empty, but referrer-policy set on this file
diff --git a/testing/web-platform/tests/fetch/api/policies/nested-policy.js.headers b/testing/web-platform/tests/fetch/api/policies/nested-policy.js.headers
new file mode 100644
index 0000000000..7ffbf17d6b
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/nested-policy.js.headers
@@ -0,0 +1 @@
+Referrer-Policy: no-referrer
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer-service-worker.https.html b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer-service-worker.https.html
new file mode 100644
index 0000000000..af898aa29f
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer-service-worker.https.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch in service worker: referrer with no-referrer policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+ </head>
+ <body>
+ <script>
+ service_worker_test("referrer-no-referrer.js");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer-worker.html b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer-worker.html
new file mode 100644
index 0000000000..dbef9bb658
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer-worker.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch in worker: referrer with no-referrer policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ fetch_tests_from_worker(new Worker("referrer-no-referrer.js"));
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.html b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.html
new file mode 100644
index 0000000000..22a6f34c52
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch: referrer with no-referrer policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script src="../resources/utils.js"></script>
+ <script src="referrer-no-referrer.js"></script>
+ </body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.html.headers b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.html.headers
new file mode 100644
index 0000000000..7ffbf17d6b
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.html.headers
@@ -0,0 +1 @@
+Referrer-Policy: no-referrer
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.js b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.js
new file mode 100644
index 0000000000..60600bf081
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.js
@@ -0,0 +1,19 @@
+if (this.document === undefined) {
+ importScripts("/resources/testharness.js");
+ importScripts("../resources/utils.js");
+}
+
+var fetchedUrl = RESOURCES_DIR + "inspect-headers.py?headers=origin";
+
+promise_test(function(test) {
+ return fetch(fetchedUrl).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ var referrer = resp.headers.get("x-request-referer");
+ //Either no referrer header is sent or it is empty
+ if (referrer)
+ assert_equals(referrer, "", "request's referrer is empty");
+ });
+}, "Request's referrer is empty");
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.js.headers b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.js.headers
new file mode 100644
index 0000000000..7ffbf17d6b
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-no-referrer.js.headers
@@ -0,0 +1 @@
+Referrer-Policy: no-referrer
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin-service-worker.https.html b/testing/web-platform/tests/fetch/api/policies/referrer-origin-service-worker.https.html
new file mode 100644
index 0000000000..4018b83781
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin-service-worker.https.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch in service worker: referrer with no-referrer policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+ </head>
+ <body>
+ <script>
+ service_worker_test("referrer-origin.js?pipe=sub");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html
new file mode 100644
index 0000000000..d87192e227
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch in service worker: referrer with origin-when-cross-origin policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+ </head>
+ <body>
+ <script>
+ service_worker_test("referrer-origin-when-cross-origin.js?pipe=sub");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin-worker.html b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin-worker.html
new file mode 100644
index 0000000000..f95ae8cf08
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin-worker.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch in worker: referrer with origin-when-cross-origin policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ fetch_tests_from_worker(new Worker("referrer-origin-when-cross-origin.js?pipe=sub"));
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.html b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.html
new file mode 100644
index 0000000000..5cd79e4b53
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch: referrer with origin-when-cross-origin policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ </head>
+ <body>
+ <script src="../resources/utils.js"></script>
+ <script src="referrer-origin-when-cross-origin.js?pipe=sub"></script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.html.headers b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.html.headers
new file mode 100644
index 0000000000..ad768e6329
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.html.headers
@@ -0,0 +1 @@
+Referrer-Policy: origin-when-cross-origin
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.js b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.js
new file mode 100644
index 0000000000..0adadbc550
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.js
@@ -0,0 +1,21 @@
+if (this.document === undefined) {
+ importScripts("/resources/testharness.js");
+ importScripts("../resources/utils.js");
+ importScripts("/common/get-host-info.sub.js");
+
+ // A nested importScripts() with a referrer-policy should have no effect
+ // on overall worker policy.
+ importScripts("nested-policy.js");
+}
+
+var referrerOrigin = location.origin + '/';
+var fetchedUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer";
+
+promise_test(function(test) {
+ return fetch(fetchedUrl).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.headers.get("x-request-referer"), referrerOrigin, "request's referrer is " + referrerOrigin);
+ });
+}, "Request's referrer is origin");
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.js.headers b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.js.headers
new file mode 100644
index 0000000000..ad768e6329
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin-when-cross-origin.js.headers
@@ -0,0 +1 @@
+Referrer-Policy: origin-when-cross-origin
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin-worker.html b/testing/web-platform/tests/fetch/api/policies/referrer-origin-worker.html
new file mode 100644
index 0000000000..bb80dd54fb
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin-worker.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch in worker: referrer with origin policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ fetch_tests_from_worker(new Worker("referrer-origin.js?pipe=sub"));
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin.html b/testing/web-platform/tests/fetch/api/policies/referrer-origin.html
new file mode 100644
index 0000000000..b164afe01d
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch: referrer with origin policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script src="../resources/utils.js"></script>
+ <script src="referrer-origin.js?pipe=sub"></script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin.html.headers b/testing/web-platform/tests/fetch/api/policies/referrer-origin.html.headers
new file mode 100644
index 0000000000..5b29739bbd
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin.html.headers
@@ -0,0 +1 @@
+Referrer-Policy: origin
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin.js b/testing/web-platform/tests/fetch/api/policies/referrer-origin.js
new file mode 100644
index 0000000000..918f8f207c
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin.js
@@ -0,0 +1,30 @@
+if (this.document === undefined) {
+ importScripts("/resources/testharness.js");
+ importScripts("../resources/utils.js");
+
+ // A nested importScripts() with a referrer-policy should have no effect
+ // on overall worker policy.
+ importScripts("nested-policy.js");
+}
+
+var referrerOrigin = (new URL("/", location.href)).href;
+var fetchedUrl = RESOURCES_DIR + "inspect-headers.py?headers=referer";
+
+promise_test(function(test) {
+ return fetch(fetchedUrl).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ assert_equals(resp.headers.get("x-request-referer"), referrerOrigin, "request's referrer is " + referrerOrigin);
+ });
+}, "Request's referrer is origin");
+
+promise_test(function(test) {
+ var referrerUrl = "https://{{domains[www]}}:{{ports[https][0]}}/";
+ return fetch(fetchedUrl, { "referrer": referrerUrl }).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ assert_equals(resp.headers.get("x-request-referer"), referrerOrigin, "request's referrer is " + referrerOrigin);
+ });
+}, "Cross-origin referrer is overridden by client origin");
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-origin.js.headers b/testing/web-platform/tests/fetch/api/policies/referrer-origin.js.headers
new file mode 100644
index 0000000000..5b29739bbd
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-origin.js.headers
@@ -0,0 +1 @@
+Referrer-Policy: origin
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url-service-worker.https.html b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url-service-worker.https.html
new file mode 100644
index 0000000000..634877edae
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url-service-worker.https.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch in worker: referrer with unsafe-url policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+ </head>
+ <body>
+ <script>
+ service_worker_test("referrer-unsafe-url.js");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url-worker.html b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url-worker.html
new file mode 100644
index 0000000000..42045776b1
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url-worker.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch in worker: referrer with unsafe-url policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ fetch_tests_from_worker(new Worker("referrer-unsafe-url.js"));
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.html b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.html
new file mode 100644
index 0000000000..10dd79e3d3
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Fetch: referrer with unsafe-url policy</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#main-fetch">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script src="../resources/utils.js"></script>
+ <script src="referrer-unsafe-url.js"></script>
+ </body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.html.headers b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.html.headers
new file mode 100644
index 0000000000..8e23770bd6
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.html.headers
@@ -0,0 +1 @@
+Referrer-Policy: unsafe-url
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.js b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.js
new file mode 100644
index 0000000000..4d61172613
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.js
@@ -0,0 +1,21 @@
+if (this.document === undefined) {
+ importScripts("/resources/testharness.js");
+ importScripts("../resources/utils.js");
+
+ // A nested importScripts() with a referrer-policy should have no effect
+ // on overall worker policy.
+ importScripts("nested-policy.js");
+}
+
+var referrerUrl = location.href;
+var fetchedUrl = RESOURCES_DIR + "inspect-headers.py?headers=referer";
+
+promise_test(function(test) {
+ return fetch(fetchedUrl).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ assert_equals(resp.headers.get("x-request-referer"), referrerUrl, "request's referrer is " + referrerUrl);
+ });
+}, "Request's referrer is the full url of current document/worker");
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.js.headers b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.js.headers
new file mode 100644
index 0000000000..8e23770bd6
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/policies/referrer-unsafe-url.js.headers
@@ -0,0 +1 @@
+Referrer-Policy: unsafe-url
diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-back-to-original-origin.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-back-to-original-origin.any.js
new file mode 100644
index 0000000000..74d731f242
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/redirect/redirect-back-to-original-origin.any.js
@@ -0,0 +1,38 @@
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+
+const BASE = location.href;
+const IS_HTTPS = new URL(BASE).protocol === 'https:';
+const REMOTE_HOST = get_host_info()['REMOTE_HOST'];
+const REMOTE_PORT =
+ IS_HTTPS ? get_host_info()['HTTPS_PORT'] : get_host_info()['HTTP_PORT'];
+
+const REMOTE_ORIGIN =
+ new URL(`//${REMOTE_HOST}:${REMOTE_PORT}`, BASE).origin;
+const DESTINATION = new URL('../resources/cors-top.txt', BASE);
+
+function CreateURL(url, BASE, params) {
+ const u = new URL(url, BASE);
+ for (const {name, value} of params) {
+ u.searchParams.append(name, value);
+ }
+ return u;
+}
+
+const redirect =
+ CreateURL('/fetch/api/resources/redirect.py', REMOTE_ORIGIN,
+ [{name: 'redirect_status', value: 303},
+ {name: 'location', value: DESTINATION.href}]);
+
+promise_test(async (test) => {
+ const res = await fetch(redirect.href, {mode: 'no-cors'});
+ // This is discussed at https://github.com/whatwg/fetch/issues/737.
+ assert_equals(res.type, 'opaque');
+}, 'original => remote => original with mode: "no-cors"');
+
+promise_test(async (test) => {
+ const res = await fetch(redirect.href, {mode: 'cors'});
+ assert_equals(res.type, 'cors');
+}, 'original => remote => original with mode: "cors"');
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-count.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-count.any.js
new file mode 100644
index 0000000000..420f9c0dfc
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/redirect/redirect-count.any.js
@@ -0,0 +1,51 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+// META: script=/common/utils.js
+// META: timeout=long
+
+/**
+ * Fetches a target that returns response with HTTP status code `statusCode` to
+ * redirect `maxCount` times.
+ */
+function redirectCountTest(maxCount, {statusCode, shouldPass = true} = {}) {
+ const desc = `Redirect ${statusCode} ${maxCount} times`;
+
+ const fromUrl = `${RESOURCES_DIR}redirect.py`;
+ const toUrl = fromUrl;
+ const token1 = token();
+ const url = `${fromUrl}?token=${token1}` +
+ `&max_age=0` +
+ `&redirect_status=${statusCode}` +
+ `&max_count=${maxCount}` +
+ `&location=${encodeURIComponent(toUrl)}`;
+
+ const requestInit = {'redirect': 'follow'};
+
+ promise_test((test) => {
+ return fetch(`${RESOURCES_DIR}clean-stash.py?token=${token1}`)
+ .then((resp) => {
+ assert_equals(
+ resp.status, 200, 'Clean stash response\'s status is 200');
+
+ if (!shouldPass)
+ return promise_rejects_js(test, TypeError, fetch(url, requestInit));
+
+ return fetch(url, requestInit)
+ .then((resp) => {
+ assert_equals(resp.status, 200, 'Response\'s status is 200');
+ return resp.text();
+ })
+ .then((body) => {
+ assert_equals(
+ body, maxCount.toString(), `Redirected ${maxCount} times`);
+ });
+ });
+ }, desc);
+}
+
+for (const statusCode of [301, 302, 303, 307, 308]) {
+ redirectCountTest(20, {statusCode});
+ redirectCountTest(21, {statusCode, shouldPass: false});
+}
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-empty-location.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-empty-location.any.js
new file mode 100644
index 0000000000..487f4d42e9
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/redirect/redirect-empty-location.any.js
@@ -0,0 +1,21 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+// Tests receiving a redirect response with a Location header with an empty
+// value.
+
+const url = RESOURCES_DIR + 'redirect-empty-location.py';
+
+promise_test(t => {
+ return promise_rejects_js(t, TypeError, fetch(url, {redirect:'follow'}));
+}, 'redirect response with empty Location, follow mode');
+
+promise_test(t => {
+ return fetch(url, {redirect:'manual'})
+ .then(resp => {
+ assert_equals(resp.type, 'opaqueredirect');
+ assert_equals(resp.status, 0);
+ });
+}, 'redirect response with empty Location, manual mode');
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-keepalive.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-keepalive.any.js
new file mode 100644
index 0000000000..c9ac13f3db
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/redirect/redirect-keepalive.any.js
@@ -0,0 +1,35 @@
+// META: global=window
+// META: timeout=long
+// META: title=Fetch API: keepalive handling
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=../resources/keepalive-helper.js
+
+'use strict';
+
+const {
+ HTTP_NOTSAMESITE_ORIGIN,
+ HTTP_REMOTE_ORIGIN,
+ HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT
+} = get_host_info();
+
+
+keepaliveRedirectInUnloadTest('same-origin redirect');
+keepaliveRedirectInUnloadTest(
+ 'same-origin redirect + preflight', {withPreflight: true});
+keepaliveRedirectInUnloadTest('cross-origin redirect', {
+ origin1: HTTP_REMOTE_ORIGIN,
+ origin2: HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT
+});
+keepaliveRedirectInUnloadTest('cross-origin redirect + preflight', {
+ origin1: HTTP_REMOTE_ORIGIN,
+ origin2: HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT,
+ withPreflight: true
+});
+keepaliveRedirectInUnloadTest(
+ 'redirect to file URL',
+ {url2: 'file://tmp/bar.txt', expectFetchSucceed: false});
+keepaliveRedirectInUnloadTest('redirect to data URL', {
+ url2: 'data:text/plain;base64,cmVzcG9uc2UncyBib2R5',
+ expectFetchSucceed: false
+});
diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-keepalive.https.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-keepalive.https.any.js
new file mode 100644
index 0000000000..54e4bc31fa
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/redirect/redirect-keepalive.https.any.js
@@ -0,0 +1,18 @@
+// META: global=window
+// META: title=Fetch API: keepalive handling
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=../resources/keepalive-helper.js
+
+'use strict';
+
+const {
+ HTTP_NOTSAMESITE_ORIGIN,
+ HTTPS_NOTSAMESITE_ORIGIN,
+} = get_host_info();
+
+keepaliveRedirectTest(`mixed content redirect`, {
+ origin1: HTTPS_NOTSAMESITE_ORIGIN,
+ origin2: HTTP_NOTSAMESITE_ORIGIN,
+ expectFetchSucceed: false
+});
diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-location-escape.tentative.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-location-escape.tentative.any.js
new file mode 100644
index 0000000000..779ad70579
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/redirect/redirect-location-escape.tentative.any.js
@@ -0,0 +1,46 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+// See https://github.com/whatwg/fetch/issues/883 for the behavior covered by
+// this test. As of writing, the Fetch spec has not been updated to cover these.
+
+// redirectLocation tests that a Location header of |locationHeader| is resolved
+// to a URL which ends in |expectedUrlSuffix|. |locationHeader| is interpreted
+// as a byte sequence via isomorphic encode, as described in [INFRA]. This
+// allows the caller to specify byte sequences which are not valid UTF-8.
+// However, this means, e.g., U+2603 must be passed in as "\xe2\x98\x83", its
+// UTF-8 encoding, not "\u2603".
+//
+// [INFRA] https://infra.spec.whatwg.org/#isomorphic-encode
+function redirectLocation(
+ desc, redirectUrl, locationHeader, expectedUrlSuffix) {
+ promise_test(function(test) {
+ // Note we use escape() instead of encodeURIComponent(), so that characters
+ // are escaped as bytes in the isomorphic encoding.
+ var url = redirectUrl + '?simple=1&location=' + escape(locationHeader);
+
+ return fetch(url, {'redirect': 'follow'}).then(function(resp) {
+ assert_true(
+ resp.url.endsWith(expectedUrlSuffix),
+ resp.url + ' ends with ' + expectedUrlSuffix);
+ });
+ }, desc);
+}
+
+var redirUrl = RESOURCES_DIR + 'redirect.py';
+redirectLocation(
+ 'Redirect to escaped UTF-8', redirUrl, 'top.txt?%E2%98%83%e2%98%83',
+ 'top.txt?%E2%98%83%e2%98%83');
+redirectLocation(
+ 'Redirect to unescaped UTF-8', redirUrl, 'top.txt?\xe2\x98\x83',
+ 'top.txt?%E2%98%83');
+redirectLocation(
+ 'Redirect to escaped and unescaped UTF-8', redirUrl,
+ 'top.txt?\xe2\x98\x83%e2%98%83', 'top.txt?%E2%98%83%e2%98%83');
+redirectLocation(
+ 'Escaping produces double-percent', redirUrl, 'top.txt?%\xe2\x98\x83',
+ 'top.txt?%%E2%98%83');
+redirectLocation(
+ 'Redirect to invalid UTF-8', redirUrl, 'top.txt?\xff', 'top.txt?%FF');
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-location.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-location.any.js
new file mode 100644
index 0000000000..3d483bdcd4
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/redirect/redirect-location.any.js
@@ -0,0 +1,73 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+const VALID_URL = 'top.txt';
+const INVALID_URL = 'invalidurl:';
+const DATA_URL = 'data:text/plain;base64,cmVzcG9uc2UncyBib2R5';
+
+/**
+ * A test to fetch a URL that returns response redirecting to `toUrl` with
+ * `status` as its HTTP status code. `expectStatus` can be set to test the
+ * status code in fetch's Promise response.
+ */
+function redirectLocationTest(toUrlDesc, {
+ toUrl = undefined,
+ status,
+ expectStatus = undefined,
+ mode,
+ shouldPass = true
+} = {}) {
+ toUrlDesc = toUrl ? `with ${toUrlDesc}` : `without`;
+ const desc = `Redirect ${status} in "${mode}" mode ${toUrlDesc} location`;
+ const url = `${RESOURCES_DIR}redirect.py?redirect_status=${status}` +
+ (toUrl ? `&location=${encodeURIComponent(toUrl)}` : '');
+ const requestInit = {'redirect': mode};
+ if (!expectStatus)
+ expectStatus = status;
+
+ promise_test((test) => {
+ if (mode === 'error' || !shouldPass)
+ return promise_rejects_js(test, TypeError, fetch(url, requestInit));
+ if (mode === 'manual')
+ return fetch(url, requestInit).then((resp) => {
+ assert_equals(resp.status, 0, "Response's status is 0");
+ assert_equals(resp.type, "opaqueredirect", "Response's type is opaqueredirect");
+ assert_equals(resp.statusText, '', `Response's statusText is ""`);
+ assert_true(resp.headers.entries().next().done, "Headers should be empty");
+ });
+
+ if (mode === 'follow')
+ return fetch(url, requestInit).then((resp) => {
+ assert_equals(
+ resp.status, expectStatus, `Response's status is ${expectStatus}`);
+ });
+ assert_unreached(`${mode} is not a valid redirect mode`);
+ }, desc);
+}
+
+// FIXME: We may want to mix redirect-mode and cors-mode.
+for (const status of [301, 302, 303, 307, 308]) {
+ redirectLocationTest('without location', {status, mode: 'follow'});
+ redirectLocationTest('without location', {status, mode: 'manual'});
+ // FIXME: Add tests for "error" redirect-mode without location.
+
+ // When succeeded, `follow` mode should have followed all redirects.
+ redirectLocationTest(
+ 'valid', {toUrl: VALID_URL, status, expectStatus: 200, mode: 'follow'});
+ redirectLocationTest('valid', {toUrl: VALID_URL, status, mode: 'manual'});
+ redirectLocationTest('valid', {toUrl: VALID_URL, status, mode: 'error'});
+
+ redirectLocationTest(
+ 'invalid',
+ {toUrl: INVALID_URL, status, mode: 'follow', shouldPass: false});
+ redirectLocationTest('invalid', {toUrl: INVALID_URL, status, mode: 'manual'});
+ redirectLocationTest('invalid', {toUrl: INVALID_URL, status, mode: 'error'});
+
+ redirectLocationTest(
+ 'data', {toUrl: DATA_URL, status, mode: 'follow', shouldPass: false});
+ // FIXME: Should this pass?
+ redirectLocationTest('data', {toUrl: DATA_URL, status, mode: 'manual'});
+ redirectLocationTest('data', {toUrl: DATA_URL, status, mode: 'error'});
+}
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-method.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-method.any.js
new file mode 100644
index 0000000000..9fe086a9db
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/redirect/redirect-method.any.js
@@ -0,0 +1,112 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+// Creates a promise_test that fetches a URL that returns a redirect response.
+//
+// |opts| has additional options:
+// |opts.body|: the request body as a string or blob (default is empty body)
+// |opts.expectedBodyAsString|: the expected response body as a string. The
+// server is expected to echo the request body. The default is the empty string
+// if the request after redirection isn't POST; otherwise it's |opts.body|.
+// |opts.expectedRequestContentType|: the expected Content-Type of redirected
+// request.
+function redirectMethod(desc, redirectUrl, redirectLocation, redirectStatus, method, expectedMethod, opts) {
+ let url = redirectUrl;
+ let urlParameters = "?redirect_status=" + redirectStatus;
+ urlParameters += "&location=" + encodeURIComponent(redirectLocation);
+
+ let requestHeaders = {
+ "Content-Encoding": "Identity",
+ "Content-Language": "en-US",
+ "Content-Location": "foo",
+ };
+ let requestInit = {"method": method, "redirect": "follow", "headers" : requestHeaders};
+ opts = opts || {};
+ if (opts.body) {
+ requestInit.body = opts.body;
+ }
+
+ promise_test(function(test) {
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ let expectedRequestContentType = "NO";
+ if (opts.expectedRequestContentType) {
+ expectedRequestContentType = opts.expectedRequestContentType;
+ }
+
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_equals(resp.type, "basic", "Response's type basic");
+ assert_equals(
+ resp.headers.get("x-request-method"),
+ expectedMethod,
+ "Request method after redirection is " + expectedMethod);
+ let hasRequestBodyHeader = true;
+ if (opts.expectedStripRequestBodyHeader) {
+ hasRequestBodyHeader = !opts.expectedStripRequestBodyHeader;
+ }
+ assert_equals(
+ resp.headers.get("x-request-content-type"),
+ expectedRequestContentType,
+ "Request Content-Type after redirection is " + expectedRequestContentType);
+ [
+ "Content-Encoding",
+ "Content-Language",
+ "Content-Location"
+ ].forEach(header => {
+ let xHeader = "x-request-" + header.toLowerCase();
+ let expectedValue = hasRequestBodyHeader ? requestHeaders[header] : "NO";
+ assert_equals(
+ resp.headers.get(xHeader),
+ expectedValue,
+ "Request " + header + " after redirection is " + expectedValue);
+ });
+ assert_true(resp.redirected);
+ return resp.text().then(function(text) {
+ let expectedBody = "";
+ if (expectedMethod == "POST") {
+ expectedBody = opts.expectedBodyAsString || requestInit.body;
+ }
+ let expectedContentLength = expectedBody ? expectedBody.length.toString() : "NO";
+ assert_equals(text, expectedBody, "request body");
+ assert_equals(
+ resp.headers.get("x-request-content-length"),
+ expectedContentLength,
+ "Request Content-Length after redirection is " + expectedContentLength);
+ });
+ });
+ }, desc);
+}
+
+promise_test(function(test) {
+ assert_false(new Response().redirected);
+ return fetch(RESOURCES_DIR + "method.py").then(function(resp) {
+ assert_equals(resp.status, 200, "Response's status is 200");
+ assert_false(resp.redirected);
+ });
+}, "Response.redirected should be false on not-redirected responses");
+
+var redirUrl = RESOURCES_DIR + "redirect.py";
+var locationUrl = "method.py";
+
+const stringBody = "this is my body";
+const blobBody = new Blob(["it's me the blob!", " ", "and more blob!"]);
+const blobBodyAsString = "it's me the blob! and more blob!";
+
+redirectMethod("Redirect 301 with GET", redirUrl, locationUrl, 301, "GET", "GET");
+redirectMethod("Redirect 301 with POST", redirUrl, locationUrl, 301, "POST", "GET", { body: stringBody, expectedStripRequestBodyHeader: true });
+redirectMethod("Redirect 301 with HEAD", redirUrl, locationUrl, 301, "HEAD", "HEAD");
+
+redirectMethod("Redirect 302 with GET", redirUrl, locationUrl, 302, "GET", "GET");
+redirectMethod("Redirect 302 with POST", redirUrl, locationUrl, 302, "POST", "GET", { body: stringBody, expectedStripRequestBodyHeader: true });
+redirectMethod("Redirect 302 with HEAD", redirUrl, locationUrl, 302, "HEAD", "HEAD");
+
+redirectMethod("Redirect 303 with GET", redirUrl, locationUrl, 303, "GET", "GET");
+redirectMethod("Redirect 303 with POST", redirUrl, locationUrl, 303, "POST", "GET", { body: stringBody, expectedStripRequestBodyHeader: true });
+redirectMethod("Redirect 303 with HEAD", redirUrl, locationUrl, 303, "HEAD", "HEAD");
+redirectMethod("Redirect 303 with TESTING", redirUrl, locationUrl, 303, "TESTING", "GET", { expectedStripRequestBodyHeader: true });
+
+redirectMethod("Redirect 307 with GET", redirUrl, locationUrl, 307, "GET", "GET");
+redirectMethod("Redirect 307 with POST (string body)", redirUrl, locationUrl, 307, "POST", "POST", { body: stringBody , expectedRequestContentType: "text/plain;charset=UTF-8"});
+redirectMethod("Redirect 307 with POST (blob body)", redirUrl, locationUrl, 307, "POST", "POST", { body: blobBody, expectedBodyAsString: blobBodyAsString });
+redirectMethod("Redirect 307 with HEAD", redirUrl, locationUrl, 307, "HEAD", "HEAD");
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-mode.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-mode.any.js
new file mode 100644
index 0000000000..9f1ff98c65
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/redirect/redirect-mode.any.js
@@ -0,0 +1,59 @@
+// META: script=/common/get-host-info.sub.js
+
+var redirectLocation = "cors-top.txt";
+const { ORIGIN, REMOTE_ORIGIN } = get_host_info();
+
+function testRedirect(origin, redirectStatus, redirectMode, corsMode) {
+ var url = new URL("../resources/redirect.py", self.location);
+ if (origin === "cross-origin") {
+ url.host = get_host_info().REMOTE_HOST;
+ url.port = get_host_info().HTTP_PORT;
+ }
+
+ var urlParameters = "?redirect_status=" + redirectStatus;
+ urlParameters += "&location=" + encodeURIComponent(redirectLocation);
+
+ var requestInit = {redirect: redirectMode, mode: corsMode};
+
+ promise_test(function(test) {
+ if (redirectMode === "error" ||
+ (corsMode === "no-cors" && redirectMode !== "follow" && origin !== "same-origin"))
+ return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit));
+ if (redirectMode === "manual")
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 0, "Response's status is 0");
+ assert_equals(resp.type, "opaqueredirect", "Response's type is opaqueredirect");
+ assert_equals(resp.statusText, "", "Response's statusText is \"\"");
+ assert_equals(resp.url, url + urlParameters, "Response URL should be the original one");
+ });
+ if (redirectMode === "follow")
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ if (corsMode !== "no-cors" || origin === "same-origin") {
+ assert_true(new URL(resp.url).pathname.endsWith(redirectLocation), "Response's url should be the redirected one");
+ assert_equals(resp.status, 200, "Response's status is 200");
+ } else {
+ assert_equals(resp.type, "opaque", "Response is opaque");
+ }
+ });
+ assert_unreached(redirectMode + " is no a valid redirect mode");
+ }, origin + " redirect " + redirectStatus + " in " + redirectMode + " redirect and " + corsMode + " mode");
+}
+
+for (var origin of ["same-origin", "cross-origin"]) {
+ for (var statusCode of [301, 302, 303, 307, 308]) {
+ for (var redirect of ["error", "manual", "follow"]) {
+ for (var mode of ["cors", "no-cors"])
+ testRedirect(origin, statusCode, redirect, mode);
+ }
+ }
+}
+
+promise_test(async (t) => {
+ const destination = `${ORIGIN}/common/blank.html`;
+ // We use /common/redirect.py intentionally, as we want a CORS error.
+ const url =
+ `${REMOTE_ORIGIN}/common/redirect.py?location=${destination}`;
+ await promise_rejects_js(t, TypeError, fetch(url, { redirect: "manual" }));
+}, "manual redirect with a CORS error should be rejected");
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-origin.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-origin.any.js
new file mode 100644
index 0000000000..6001c509b1
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/redirect/redirect-origin.any.js
@@ -0,0 +1,68 @@
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+const {
+ HTTP_ORIGIN,
+ HTTP_REMOTE_ORIGIN,
+} = get_host_info();
+
+/**
+ * Fetches `fromUrl` with 'cors' and 'follow' modes that returns response to
+ * redirect to `toUrl`.
+ */
+function testOriginAfterRedirection(
+ desc, method, fromUrl, toUrl, statusCode, expectedOrigin) {
+ desc = `[${method}] Redirect ${statusCode} ${desc}`;
+ const token1 = token();
+ const url = `${fromUrl}?token=${token1}&max_age=0` +
+ `&redirect_status=${statusCode}` +
+ `&location=${encodeURIComponent(toUrl)}`;
+
+ const requestInit = {method, 'mode': 'cors', 'redirect': 'follow'};
+
+ promise_test(function(test) {
+ return fetch(`${RESOURCES_DIR}clean-stash.py?token=${token1}`)
+ .then((cleanResponse) => {
+ assert_equals(
+ cleanResponse.status, 200,
+ `Clean stash response's status is 200`);
+ return fetch(url, requestInit).then((redirectResponse) => {
+ assert_equals(
+ redirectResponse.status, 200,
+ `Inspect header response's status is 200`);
+ assert_equals(
+ redirectResponse.headers.get('x-request-origin'),
+ expectedOrigin, 'Check origin header');
+ });
+ });
+ }, desc);
+}
+
+const FROM_URL = `${RESOURCES_DIR}redirect.py`;
+const CORS_FROM_URL =
+ `${HTTP_REMOTE_ORIGIN}${dirname(location.pathname)}${FROM_URL}`;
+const TO_URL = `${HTTP_ORIGIN}${dirname(location.pathname)}${
+ RESOURCES_DIR}inspect-headers.py?headers=origin`;
+const CORS_TO_URL = `${HTTP_REMOTE_ORIGIN}${dirname(location.pathname)}${
+ RESOURCES_DIR}inspect-headers.py?cors&headers=origin`;
+
+for (const statusCode of [301, 302, 303, 307, 308]) {
+ for (const method of ['GET', 'POST']) {
+ testOriginAfterRedirection(
+ 'Same origin to same origin', method, FROM_URL, TO_URL, statusCode,
+ null);
+ testOriginAfterRedirection(
+ 'Same origin to other origin', method, FROM_URL, CORS_TO_URL,
+ statusCode, HTTP_ORIGIN);
+ testOriginAfterRedirection(
+ 'Other origin to other origin', method, CORS_FROM_URL, CORS_TO_URL,
+ statusCode, HTTP_ORIGIN);
+ // TODO(crbug.com/1432059): Fix broken tests.
+ testOriginAfterRedirection(
+ 'Other origin to same origin', method, CORS_FROM_URL, `${TO_URL}&cors`,
+ statusCode, 'null');
+ }
+}
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-referrer-override.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-referrer-override.any.js
new file mode 100644
index 0000000000..56e55d79e1
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/redirect/redirect-referrer-override.any.js
@@ -0,0 +1,104 @@
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function getExpectation(expectations, init, initScenario, redirectPolicy, redirectScenario) {
+ let policies = [
+ expectations[initPolicy][initScenario],
+ expectations[redirectPolicy][redirectScenario]
+ ];
+
+ if (policies.includes("omitted")) {
+ return null;
+ } else if (policies.includes("origin")) {
+ return referrerOrigin;
+ } else {
+ // "stripped-referrer"
+ return referrerUrl;
+ }
+}
+
+function testReferrerAfterRedirection(desc, redirectUrl, redirectLocation, referrerPolicy, redirectReferrerPolicy, expectedReferrer) {
+ var url = redirectUrl;
+ var urlParameters = "?location=" + encodeURIComponent(redirectLocation);
+ var description = desc + ", " + referrerPolicy + " init, " + redirectReferrerPolicy + " redirect header ";
+
+ if (redirectReferrerPolicy)
+ urlParameters += "&redirect_referrerpolicy=" + redirectReferrerPolicy;
+
+ var requestInit = {"redirect": "follow", "referrerPolicy": referrerPolicy};
+ promise_test(function(test) {
+ return fetch(url + urlParameters, requestInit).then(function(response) {
+ assert_equals(response.status, 200, "Inspect header response's status is 200");
+ assert_equals(response.headers.get("x-request-referer"), expectedReferrer ? expectedReferrer : null, "Check referrer header");
+ });
+ }, description);
+}
+
+var referrerOrigin = get_host_info().HTTP_ORIGIN + "/";
+var referrerUrl = location.href;
+
+var redirectUrl = RESOURCES_DIR + "redirect.py";
+var locationUrl = get_host_info().HTTP_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?headers=referer";
+var crossLocationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer";
+
+var expectations = {
+ "no-referrer": {
+ "same-origin": "omitted",
+ "cross-origin": "omitted"
+ },
+ "no-referrer-when-downgrade": {
+ "same-origin": "stripped-referrer",
+ "cross-origin": "stripped-referrer"
+ },
+ "origin": {
+ "same-origin": "origin",
+ "cross-origin": "origin"
+ },
+ "origin-when-cross-origin": {
+ "same-origin": "stripped-referrer",
+ "cross-origin": "origin",
+ },
+ "same-origin": {
+ "same-origin": "stripped-referrer",
+ "cross-origin": "omitted"
+ },
+ "strict-origin": {
+ "same-origin": "origin",
+ "cross-origin": "origin"
+ },
+ "strict-origin-when-cross-origin": {
+ "same-origin": "stripped-referrer",
+ "cross-origin": "origin"
+ },
+ "unsafe-url": {
+ "same-origin": "stripped-referrer",
+ "cross-origin": "stripped-referrer"
+ }
+};
+
+for (var initPolicy in expectations) {
+ for (var redirectPolicy in expectations) {
+
+ // Redirect to same-origin URL
+ testReferrerAfterRedirection(
+ "Same origin redirection",
+ redirectUrl,
+ locationUrl,
+ initPolicy,
+ redirectPolicy,
+ getExpectation(expectations, initPolicy, "same-origin", redirectPolicy, "same-origin"));
+
+ // Redirect to cross-origin URL
+ testReferrerAfterRedirection(
+ "Cross origin redirection",
+ redirectUrl,
+ crossLocationUrl,
+ initPolicy,
+ redirectPolicy,
+ getExpectation(expectations, initPolicy, "same-origin", redirectPolicy, "cross-origin"));
+ }
+}
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-referrer.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-referrer.any.js
new file mode 100644
index 0000000000..99fda42e69
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/redirect/redirect-referrer.any.js
@@ -0,0 +1,66 @@
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=../resources/utils.js
+// META: script=/common/get-host-info.sub.js
+
+function testReferrerAfterRedirection(desc, redirectUrl, redirectLocation, referrerPolicy, redirectReferrerPolicy, expectedReferrer) {
+ var url = redirectUrl;
+ var urlParameters = "?location=" + encodeURIComponent(redirectLocation);
+
+ if (redirectReferrerPolicy)
+ urlParameters += "&redirect_referrerpolicy=" + redirectReferrerPolicy;
+
+ var requestInit = {"redirect": "follow", "referrerPolicy": referrerPolicy};
+
+ promise_test(function(test) {
+ return fetch(url + urlParameters, requestInit).then(function(response) {
+ assert_equals(response.status, 200, "Inspect header response's status is 200");
+ assert_equals(response.headers.get("x-request-referer"), expectedReferrer ? expectedReferrer : null, "Check referrer header");
+ });
+ }, desc);
+}
+
+var referrerOrigin = get_host_info().HTTP_ORIGIN + "/";
+var referrerUrl = location.href;
+
+var redirectUrl = RESOURCES_DIR + "redirect.py";
+var locationUrl = get_host_info().HTTP_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?headers=referer";
+var crossLocationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer";
+
+testReferrerAfterRedirection("Same origin redirection, empty init, unsafe-url redirect header ", redirectUrl, locationUrl, "", "unsafe-url", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty init, no-referrer-when-downgrade redirect header ", redirectUrl, locationUrl, "", "no-referrer-when-downgrade", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty init, same-origin redirect header ", redirectUrl, locationUrl, "", "same-origin", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty init, origin redirect header ", redirectUrl, locationUrl, "", "origin", referrerOrigin);
+testReferrerAfterRedirection("Same origin redirection, empty init, origin-when-cross-origin redirect header ", redirectUrl, locationUrl, "", "origin-when-cross-origin", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty init, no-referrer redirect header ", redirectUrl, locationUrl, "", "no-referrer", null);
+testReferrerAfterRedirection("Same origin redirection, empty init, strict-origin redirect header ", redirectUrl, locationUrl, "", "strict-origin", referrerOrigin);
+testReferrerAfterRedirection("Same origin redirection, empty init, strict-origin-when-cross-origin redirect header ", redirectUrl, locationUrl, "", "strict-origin-when-cross-origin", referrerUrl);
+
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, unsafe-url init ", redirectUrl, locationUrl, "unsafe-url", "", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, no-referrer-when-downgrade init ", redirectUrl, locationUrl, "no-referrer-when-downgrade", "", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, same-origin init ", redirectUrl, locationUrl, "same-origin", "", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, origin init ", redirectUrl, locationUrl, "origin", "", referrerOrigin);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, origin-when-cross-origin init ", redirectUrl, locationUrl, "origin-when-cross-origin", "", referrerUrl);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, no-referrer init ", redirectUrl, locationUrl, "no-referrer", "", null);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, strict-origin init ", redirectUrl, locationUrl, "strict-origin", "", referrerOrigin);
+testReferrerAfterRedirection("Same origin redirection, empty redirect header, strict-origin-when-cross-origin init ", redirectUrl, locationUrl, "strict-origin-when-cross-origin", "", referrerUrl);
+
+testReferrerAfterRedirection("Cross origin redirection, empty init, unsafe-url redirect header ", redirectUrl, crossLocationUrl, "", "unsafe-url", referrerUrl);
+testReferrerAfterRedirection("Cross origin redirection, empty init, no-referrer-when-downgrade redirect header ", redirectUrl, crossLocationUrl, "", "no-referrer-when-downgrade", referrerUrl);
+testReferrerAfterRedirection("Cross origin redirection, empty init, same-origin redirect header ", redirectUrl, crossLocationUrl, "", "same-origin", null);
+testReferrerAfterRedirection("Cross origin redirection, empty init, origin redirect header ", redirectUrl, crossLocationUrl, "", "origin", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty init, origin-when-cross-origin redirect header ", redirectUrl, crossLocationUrl, "", "origin-when-cross-origin", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty init, no-referrer redirect header ", redirectUrl, crossLocationUrl, "", "no-referrer", null);
+testReferrerAfterRedirection("Cross origin redirection, empty init, strict-origin redirect header ", redirectUrl, crossLocationUrl, "", "strict-origin", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty init, strict-origin-when-cross-origin redirect header ", redirectUrl, crossLocationUrl, "", "strict-origin-when-cross-origin", referrerOrigin);
+
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, unsafe-url init ", redirectUrl, crossLocationUrl, "unsafe-url", "", referrerUrl);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, no-referrer-when-downgrade init ", redirectUrl, crossLocationUrl, "no-referrer-when-downgrade", "", referrerUrl);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, same-origin init ", redirectUrl, crossLocationUrl, "same-origin", "", null);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, origin init ", redirectUrl, crossLocationUrl, "origin", "", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, origin-when-cross-origin init ", redirectUrl, crossLocationUrl, "origin-when-cross-origin", "", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, no-referrer init ", redirectUrl, crossLocationUrl, "no-referrer", "", null);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, strict-origin init ", redirectUrl, crossLocationUrl, "strict-origin", "", referrerOrigin);
+testReferrerAfterRedirection("Cross origin redirection, empty redirect header, strict-origin-when-cross-origin init ", redirectUrl, crossLocationUrl, "strict-origin-when-cross-origin", "", referrerOrigin);
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-schemes.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-schemes.any.js
new file mode 100644
index 0000000000..31ec124fd6
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/redirect/redirect-schemes.any.js
@@ -0,0 +1,19 @@
+// META: title=Fetch: handling different schemes in redirects
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+
+// All non-HTTP(S) schemes cannot survive redirects
+var url = "../resources/redirect.py?location=";
+var tests = [
+ url + "mailto:a@a.com",
+ url + "data:,HI",
+ url + "facetime:a@a.org",
+ url + "about:blank",
+ url + "about:unicorn",
+ url + "blob:djfksfjs"
+];
+tests.forEach(function(url) {
+ promise_test(function(test) {
+ return promise_rejects_js(test, TypeError, fetch(url))
+ })
+})
diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-to-dataurl.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-to-dataurl.any.js
new file mode 100644
index 0000000000..9d0f147349
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/redirect/redirect-to-dataurl.any.js
@@ -0,0 +1,28 @@
+// META: global=window,worker
+// META: script=/common/get-host-info.sub.js
+
+var dataURL = "data:text/plain;base64,cmVzcG9uc2UncyBib2R5";
+var body = "response's body";
+var contentType = "text/plain";
+
+function redirectDataURL(desc, redirectUrl, mode) {
+ var url = redirectUrl + "?cors&location=" + encodeURIComponent(dataURL);
+
+ var requestInit = {"mode": mode};
+
+ promise_test(function(test) {
+ return promise_rejects_js(test, TypeError, fetch(url, requestInit));
+ }, desc);
+}
+
+var redirUrl = get_host_info().HTTP_ORIGIN + "/fetch/api/resources/redirect.py";
+var corsRedirUrl = get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py";
+
+redirectDataURL("Testing data URL loading after same-origin redirection (cors mode)", redirUrl, "cors");
+redirectDataURL("Testing data URL loading after same-origin redirection (no-cors mode)", redirUrl, "no-cors");
+redirectDataURL("Testing data URL loading after same-origin redirection (same-origin mode)", redirUrl, "same-origin");
+
+redirectDataURL("Testing data URL loading after cross-origin redirection (cors mode)", corsRedirUrl, "cors");
+redirectDataURL("Testing data URL loading after cross-origin redirection (no-cors mode)", corsRedirUrl, "no-cors");
+
+done();
diff --git a/testing/web-platform/tests/fetch/api/redirect/redirect-upload.h2.any.js b/testing/web-platform/tests/fetch/api/redirect/redirect-upload.h2.any.js
new file mode 100644
index 0000000000..521bd3adc2
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/redirect/redirect-upload.h2.any.js
@@ -0,0 +1,33 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+
+const redirectUrl = RESOURCES_DIR + "redirect.h2.py";
+const redirectLocation = "top.txt";
+
+async function fetchStreamRedirect(statusCode) {
+ const url = RESOURCES_DIR + "redirect.h2.py" +
+ `?redirect_status=${statusCode}&location=${redirectLocation}`;
+ const requestInit = {method: "POST"};
+ requestInit["body"] = new ReadableStream({start: controller => {
+ const encoder = new TextEncoder();
+ controller.enqueue(encoder.encode("Test"));
+ controller.close();
+ }});
+ requestInit.duplex = "half";
+ return fetch(url, requestInit);
+}
+
+promise_test(async () => {
+ const resp = await fetchStreamRedirect(303);
+ assert_equals(resp.status, 200);
+ assert_true(new URL(resp.url).pathname.endsWith(redirectLocation),
+ "Response's url should be the redirected one");
+}, "Fetch upload streaming should be accepted on 303");
+
+for (const statusCode of [301, 302, 307, 308]) {
+ promise_test(t => {
+ return promise_rejects_js(t, TypeError, fetchStreamRedirect(statusCode));
+ }, `Fetch upload streaming should fail on ${statusCode}`);
+}
diff --git a/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-frame.https.html b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-frame.https.html
new file mode 100644
index 0000000000..f3f9f7856d
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-frame.https.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<title>Fetch destination tests for resources with no load event</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+let frame;
+const kScope = 'resources/dummy.html?dest=frame';
+
+// Set up the service worker and the frame.
+promise_test(t => {
+ const kScript = 'resources/fetch-destination-worker-frame.js';
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ add_completion_callback(() => {
+ registration.unregister();
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ });
+ }, 'Initialize global state');
+
+var waitOnMessageFromSW = async t => {
+ await new Promise((resolve, reject) => {
+ navigator.serviceWorker.onmessage = t.step_func(event => {
+ if (event.data == "PASS") {
+ resolve();
+ } else {
+ reject();
+ }
+ });
+ }).catch(() => {;
+ assert_unreached("Wrong destination.");
+ });
+ t.add_cleanup(() => { frame.contentWindow.navigator.serviceWorker.onmessage = null; });
+}
+
+// Document destination
+///////////////////////
+promise_test(async t => {
+ var f = document.createElement('frame');
+ frame = f;
+ f.className = 'test-frame';
+ f.src = kScope;
+ document.body.appendChild(f);
+ await waitOnMessageFromSW(t);
+ add_completion_callback(() => { f.remove(); });
+}, 'frame fetches with a "frame" Request.destination');
+
+</script>
diff --git a/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-iframe.https.html b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-iframe.https.html
new file mode 100644
index 0000000000..1aa5a5613b
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-iframe.https.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<title>Fetch destination tests for resources with no load event</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+let frame;
+const kScope = 'resources/dummy.html?dest=iframe';
+
+// Set up the service worker and the frame.
+promise_test(t => {
+ const kScript = 'resources/fetch-destination-worker-iframe.js';
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ add_completion_callback(() => {
+ registration.unregister();
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ });
+ }, 'Initialize global state');
+
+var waitOnMessageFromSW = async t => {
+ await new Promise((resolve, reject) => {
+ navigator.serviceWorker.onmessage = t.step_func(event => {
+ if (event.data == "PASS") {
+ resolve();
+ } else {
+ reject();
+ }
+ });
+ }).catch(() => {;
+ assert_unreached("Wrong destination.");
+ });
+ t.add_cleanup(() => { frame.contentWindow.navigator.serviceWorker.onmessage = null; });
+}
+
+// Document destination
+///////////////////////
+promise_test(async t => {
+ var f = document.createElement('iframe');
+ frame = f;
+ f.className = 'test-iframe';
+ f.src = kScope;
+ document.body.appendChild(f);
+ await waitOnMessageFromSW(t);
+ add_completion_callback(() => { f.remove(); });
+}, 'iframe fetches with a "iframe" Request.destination');
+
+</script>
diff --git a/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-no-load-event.https.html b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-no-load-event.https.html
new file mode 100644
index 0000000000..1778bf2581
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-no-load-event.https.html
@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<title>Fetch destination tests for resources with no load event</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+let frame;
+
+// Set up the service worker and the frame.
+promise_test(t => {
+ const kScope = 'resources/';
+ const kFrame = 'resources/empty.https.html';
+ const kScript = 'resources/fetch-destination-worker-no-load-event.js';
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ add_completion_callback(() => {
+ registration.unregister();
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(kFrame);
+ })
+ .then(f => {
+ frame = f;
+ add_completion_callback(() => { f.remove(); });
+ });
+ }, 'Initialize global state');
+
+var waitOnMessageFromSW = async t => {
+ await new Promise((resolve, reject) => {
+ frame.contentWindow.navigator.serviceWorker.onmessage = t.step_func(event => {
+ if (event.data == "PASS") {
+ resolve();
+ } else {
+ reject();
+ }
+ });
+ }).catch(() => {;
+ assert_unreached("Wrong destination.");
+ });
+ t.add_cleanup(() => { frame.contentWindow.navigator.serviceWorker.onmessage = null; });
+}
+// Actual tests
+
+// Image destination
+////////////////////
+
+// CSS background image - image destination
+promise_test(async t => {
+ let node = frame.contentWindow.document.createElement("div");
+ node.style = "background-image: url(dummy.png?t=bg2&dest=image)";
+ frame.contentWindow.document.body.appendChild(node);
+
+ await waitOnMessageFromSW(t);
+}, 'Background image fetches with an "image" Request.destination');
+
+// Font destination
+///////////////////
+
+// Font loading API - font destination
+promise_test(async t => {
+ let font = new frame.contentWindow.FontFace("foo", "url(dummy.ttf?t=api&dest=font)");
+ font.load();
+
+ await waitOnMessageFromSW(t);
+}, 'Font loading API fetches with an "font" Request.destination');
+
+// CSS font - font destination
+promise_test(async t => {
+ let style = frame.contentWindow.document.createElement("style");
+ style.innerHTML = "@font-face { font-family: foo; src: url(dummy.ttf?t=css&dest=font); }";
+ style.innerHTML += "div {font-family: foo; }";
+ let div = frame.contentWindow.document.createElement("div");
+ div.innerHTML = "bar";
+ frame.contentWindow.document.body.appendChild(style);
+ frame.contentWindow.document.body.appendChild(div);
+
+ await waitOnMessageFromSW(t);
+}, 'CSS font fetches with an "font" Request.destination');
+
+// Empty string destination
+///////////////////////////
+
+// sendBeacon() - empty string destination
+promise_test(async t => {
+ frame.contentWindow.navigator.sendBeacon("dummy?t=beacon&dest=", "foobar");
+
+ await waitOnMessageFromSW(t);
+}, 'sendBeacon() fetches with an empty string Request.destination');
+
+// Cache.add() - empty string destination
+promise_test(async t => {
+ frame.contentWindow.caches.open("foo").then(cache => {
+ cache.add("dummy?t=cache&dest=");
+ });
+
+ await waitOnMessageFromSW(t);
+}, 'Cache.add() fetches with an empty string Request.destination');
+
+// script destination
+/////////////////////
+
+// importScripts() - script destination
+promise_test(async t => {
+ let worker = new frame.contentWindow.Worker("importer.js");
+
+ await waitOnMessageFromSW(t);
+}, 'importScripts() fetches with a "script" Request.destination');
+
+// style destination
+/////////////////////
+// @import - style destination
+promise_test(async t => {
+ let node = frame.contentWindow.document.createElement("style");
+ node.innerHTML = '@import url("dummy?t=import&dest=style")';
+ frame.contentWindow.document.body.appendChild(node);
+
+ await waitOnMessageFromSW(t);
+}, '@import fetches with a "style" Request.destination');
+
+</script>
diff --git a/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-prefetch.https.html b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-prefetch.https.html
new file mode 100644
index 0000000000..db99202df8
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-prefetch.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<title>Fetch destination test for prefetching</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/media.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+let frame;
+
+// Set up the service worker and the frame.
+promise_test(t => {
+ const kScope = 'resources/empty.https.html';
+ const kScript = 'resources/fetch-destination-worker.js';
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ add_completion_callback(() => {
+ registration.unregister();
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(kScope);
+ })
+ .then(f => {
+ frame = f;
+ add_completion_callback(() => { f.remove(); });
+ });
+ }, 'Initialize global state');
+
+// HTMLLinkElement with rel=prefetch - empty string destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "prefetch";
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?dest=";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=prefetch fetches with an empty string Request.destination');
+
+</script>
diff --git a/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-worker.https.html b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-worker.https.html
new file mode 100644
index 0000000000..5935c1ff31
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination-worker.https.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<title>Fetch destination tests for resources with no load event</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+let frame;
+
+// Set up the service worker and the frame.
+promise_test(t => {
+ const kScope = 'resources/dummy.html';
+ const kScript = 'resources/fetch-destination-worker-no-load-event.js';
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ add_completion_callback(() => {
+ registration.unregister();
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(kScope);
+ })
+ .then(f => {
+ frame = f;
+ add_completion_callback(() => { f.remove(); });
+ });
+ }, 'Initialize global state');
+
+var waitOnMessageFromSW = async t => {
+ await new Promise((resolve, reject) => {
+ frame.contentWindow.navigator.serviceWorker.onmessage = t.step_func(event => {
+ if (event.data == "PASS") {
+ resolve();
+ } else {
+ reject();
+ }
+ });
+ }).catch(() => {;
+ assert_unreached("Wrong destination.");
+ });
+ t.add_cleanup(() => { frame.contentWindow.navigator.serviceWorker.onmessage = null; });
+}
+
+// worker destination
+/////////////////////
+promise_test(async t => {
+ // We can use an html file as we don't really care about the dedicated worker successfully loading.
+ let worker = new frame.contentWindow.Worker("dummy.html?t=worker&dest=worker");
+ await waitOnMessageFromSW(t);
+}, 'DedicatedWorker fetches with a "worker" Request.destination');
+
+promise_test(async t => {
+ // We can use an html file as we don't really care about the shared worker successfully loading.
+ let worker = new frame.contentWindow.SharedWorker("dummy.html?t=sharedworker&dest=sharedworker");
+ await waitOnMessageFromSW(t);
+}, 'SharedWorker fetches with a "sharedworker" Request.destination');
+
+</script>
diff --git a/testing/web-platform/tests/fetch/api/request/destination/fetch-destination.https.html b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination.https.html
new file mode 100644
index 0000000000..1b6cf16914
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/fetch-destination.https.html
@@ -0,0 +1,485 @@
+<!DOCTYPE html>
+<title>Fetch destination tests</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/common/media.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script>
+let frame;
+
+// Set up the service worker and the frame.
+promise_test(t => {
+ const kScope = 'resources/empty.https.html';
+ const kScript = 'resources/fetch-destination-worker.js';
+ return service_worker_unregister_and_register(t, kScript, kScope)
+ .then(registration => {
+ add_completion_callback(() => {
+ registration.unregister();
+ });
+
+ return wait_for_state(t, registration.installing, 'activated');
+ })
+ .then(() => {
+ return with_iframe(kScope);
+ })
+ .then(f => {
+ frame = f;
+ add_completion_callback(() => { f.remove(); });
+ });
+ }, 'Initialize global state');
+
+// Actual tests
+
+// Image destination
+////////////////////
+
+// HTMLImageElement - image destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("img");
+ node.onload = resolve;
+ node.onerror = reject;
+ node.src = "dummy.png?dest=image";
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLImageElement fetches with an "image" Request.destination');
+
+// HTMLImageElement with srcset attribute - image destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("img");
+ node.onload = resolve;
+ node.onerror = reject;
+ node.srcset = "dummy.png?t=srcset&dest=image";
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLImageElement with srcset attribute fetches with an "image" Request.destination');
+
+// HTMLImageElement with srcset attribute - image destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let img = frame.contentWindow.document.createElement("img");
+ let picture = frame.contentWindow.document.createElement("picture");
+ let source = frame.contentWindow.document.createElement("source");
+ picture.appendChild(source);
+ picture.appendChild(img);
+ img.onload = resolve;
+ img.onerror = reject;
+ source.srcset = "dummy.png?t=picture&dest=image";
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLImageElement with a HTMLPictureElement parent attribute fetches with an "image" Request.destination');
+
+// SVGImageElement - image destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let svg = frame.contentWindow.document.createElementNS('http://www.w3.org/2000/svg','svg');
+ svg.setAttributeNS('http://www.w3.org/2000/svg','xlink','http://www.w3.org/1999/xlink');
+ let svgimg = frame.contentWindow.document.createElementNS('http://www.w3.org/2000/svg','image');
+ svgimg.onload = resolve;
+ svgimg.onerror = reject;
+ svgimg.setAttributeNS('http://www.w3.org/1999/xlink','href','dummy.png?t=svg&dest=image');
+ svg.appendChild(svgimg);
+ frame.contentWindow.document.documentElement.appendChild(svg);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'SVGImageElement fetches with an "image" Request.destination');
+
+// Empty string destination
+///////////////////////////
+
+// fetch() - empty string destination
+promise_test(async t => {
+ let response = await frame.contentWindow.fetch("dummy?dest=");
+ assert_true(response.ok);
+}, 'fetch() fetches with an empty string Request.destination');
+
+// XMLHttpRequest - empty string destination
+promise_test(async t => {
+ let xhr;
+ await new Promise((resolve, reject) => {
+ xhr = new frame.contentWindow.XMLHttpRequest();
+ xhr.onload = resolve;
+ xhr.onerror = reject;
+ xhr.open("GET", "dummy?t=xhr&dest=");
+ xhr.send();
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+ assert_equals(xhr.status, 200);
+}, 'XMLHttpRequest() fetches with an empty string Request.destination');
+
+// EventSource - empty string destination
+promise_test(async t => {
+ let xhr;
+ await new Promise((resolve, reject) => {
+ eventSource = new frame.contentWindow.EventSource("dummy.es?t=eventsource&dest=");
+ eventSource.onopen = resolve;
+ eventSource.onerror = reject;
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'EventSource() fetches with an empty string Request.destination');
+
+// HTMLAudioElement - audio destination
+///////////////////////////////////////
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let audioURL = getAudioURI("dummy_audio");
+ let node = frame.contentWindow.document.createElement("audio");
+ node.onloadeddata = resolve;
+ node.onerror = reject;
+ node.src = audioURL + "?dest=audio";
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLAudioElement fetches with an "audio" Request.destination');
+
+// HTMLVideoElement - video destination
+///////////////////////////////////////
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let videoURL = getVideoURI("dummy_video");
+ let node = frame.contentWindow.document.createElement("video");
+ node.onloadeddata = resolve;
+ node.onerror = reject;
+ node.src = videoURL + "?dest=video";
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLVideoElement fetches with a "video" Request.destination');
+
+// script destinations
+//////////////////////
+
+// HTMLScriptElement - script destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("script");
+ node.onload = resolve;
+ node.onerror = reject;
+ node.src = "dummy?dest=script";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLScriptElement fetches with a "script" Request.destination');
+
+// audioworklet destination
+//////////////////////
+promise_test(async t => {
+ let audioContext = new frame.contentWindow.AudioContext();
+ await audioContext.audioWorklet.addModule("dummy?dest=audioworklet");
+}, 'AudioWorklet module fetches with a "audioworklet" Request.destination');
+
+// Style destination
+////////////////////
+
+// HTMLLinkElement with rel=stylesheet - style destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "stylesheet";
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?dest=style";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=stylesheet fetches with a "style" Request.destination');
+
+// Import declaration with `type: "css"` - style destination
+promise_test(t => {
+ return new Promise((resolve, reject) => {
+ frame.contentWindow.onerror = reject;
+
+ let node = frame.contentWindow.document.createElement("script");
+ node.onload = resolve;
+ node.onerror = reject;
+ node.src = "import-declaration-type-css.js";
+ node.type = "module";
+ frame.contentWindow.document.body.appendChild(node);
+ }).then(() => {
+ frame.contentWindow.onerror = null;
+ });
+}, 'Import declaration with `type: "css"` fetches with a "style" Request.destination');
+
+// JSON destination
+///////////////////
+
+// Import declaration with `type: "json"` - json destination
+promise_test(t => {
+ return new Promise((resolve, reject) => {
+ frame.contentWindow.onerror = reject;
+ let node = frame.contentWindow.document.createElement("script");
+ node.onload = resolve;
+ node.onerror = reject;
+ node.src = "import-declaration-type-json.js";
+ node.type = "module";
+ frame.contentWindow.document.body.appendChild(node);
+ }).then(() => {
+ frame.contentWindow.onerror = null;
+ });
+}, 'Import declaration with `type: "json"` fetches with a "json" Request.destination');
+
+// Preload tests
+////////////////
+// HTMLLinkElement with rel=preload and as=fetch - empty string destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "fetch";
+ if (node.as != "fetch") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?t=2&dest=";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=fetch fetches with an empty string Request.destination');
+
+// HTMLLinkElement with rel=preload and as=style - style destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "style";
+ if (node.as != "style") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?t=2&dest=style";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=style fetches with a "style" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=json - json destination
+promise_test(t => {
+ return new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "json";
+ if (node.as != "json") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy.json?t=2&dest=json";
+ frame.contentWindow.document.body.appendChild(node);
+ });
+}, 'HTMLLinkElement with rel=preload and as=json fetches with a "json" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=script - script destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "script";
+ if (node.as != "script") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?t=2&dest=script";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=script fetches with a "script" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=font - font destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "font";
+ if (node.as != "font") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?t=2&dest=font";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=font fetches with a "font" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=image - image destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "image";
+ if (node.as != "image") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy.png?t=2&dest=image";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=image fetches with a "image" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=audio - audio destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let audioURL = getAudioURI("dummy_audio");
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "audio";
+ if (node.as != "audio") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = audioURL + "?dest=audio";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=audio fetches with a "audio" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=video - video destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let videoURL = getVideoURI("dummy_video");
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "video";
+ if (node.as != "video") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = videoURL + "?dest=video";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=video fetches with a "video" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=track - track destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "track";
+ if (node.as != "track") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?dest=track";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=track fetches with a "track" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=document - document destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "document";
+ if (node.as != "document") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?dest=document";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=document fetches with a "document" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=worker - worker destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "worker";
+ if (node.as != "worker") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?dest=worker";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=worker fetches with a "worker" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=sharedworker - sharedworker destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "sharedworker";
+ if (node.as != "sharedworker") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?dest=sharedworker";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=sharedworker fetches with a "sharedworker" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=xslt - xslt destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "xslt";
+ if (node.as != "xslt") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?dest=xslt";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=xslt fetches with a "xslt" Request.destination');
+
+// HTMLLinkElement with rel=preload and as=manifest - manifest destination
+promise_test(async t => {
+ await new Promise((resolve, reject) => {
+ let node = frame.contentWindow.document.createElement("link");
+ node.rel = "preload";
+ node.as = "manifest";
+ if (node.as != "manifest") {
+ resolve();
+ }
+ node.onload = resolve;
+ node.onerror = reject;
+ node.href = "dummy?dest=manifest";
+ frame.contentWindow.document.body.appendChild(node);
+ }).catch(() => {
+ assert_unreached("Fetch errored.");
+ });
+}, 'HTMLLinkElement with rel=preload and as=manifest fetches with a "manifest" Request.destination');
+
+</script>
diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy
diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.css b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.css
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.css
diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.es b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.es
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.es
diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.es.headers b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.es.headers
new file mode 100644
index 0000000000..9bb8badcad
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.es.headers
@@ -0,0 +1 @@
+Content-Type: text/event-stream
diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.html b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.html
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.html
diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.json b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.json
@@ -0,0 +1 @@
+{}
diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.png b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.png
new file mode 100644
index 0000000000..01c9666a8d
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.png
Binary files differ
diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.ttf b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.ttf
new file mode 100644
index 0000000000..9023592ef5
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy.ttf
Binary files differ
diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_audio.mp3 b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_audio.mp3
new file mode 100644
index 0000000000..0091330f1e
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_audio.mp3
Binary files differ
diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_audio.oga b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_audio.oga
new file mode 100644
index 0000000000..239ad2bd08
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_audio.oga
Binary files differ
diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_video.mp4 b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_video.mp4
new file mode 100644
index 0000000000..7022e75c15
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_video.mp4
Binary files differ
diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_video.ogv b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_video.ogv
new file mode 100644
index 0000000000..de99616ece
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_video.ogv
Binary files differ
diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_video.webm b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_video.webm
new file mode 100644
index 0000000000..c3d433a3e0
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/resources/dummy_video.webm
Binary files differ
diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/empty.https.html b/testing/web-platform/tests/fetch/api/request/destination/resources/empty.https.html
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/resources/empty.https.html
diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker-frame.js b/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker-frame.js
new file mode 100644
index 0000000000..b69de0b7df
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker-frame.js
@@ -0,0 +1,20 @@
+self.addEventListener('fetch', function(event) {
+ if (event.request.url.includes('dummy')) {
+ event.waitUntil(async function() {
+ let destination = new URL(event.request.url).searchParams.get("dest");
+ let clients = await self.clients.matchAll({"includeUncontrolled": true});
+ clients.forEach(function(client) {
+ if (client.url.includes("fetch-destination-frame")) {
+ if (event.request.destination == destination) {
+ client.postMessage("PASS");
+ } else {
+ client.postMessage("FAIL");
+ }
+ }
+ })
+ }());
+ }
+ event.respondWith(fetch(event.request));
+});
+
+
diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js b/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js
new file mode 100644
index 0000000000..76345839ea
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js
@@ -0,0 +1,20 @@
+self.addEventListener('fetch', function(event) {
+ if (event.request.url.includes('dummy')) {
+ event.waitUntil(async function() {
+ let destination = new URL(event.request.url).searchParams.get("dest");
+ let clients = await self.clients.matchAll({"includeUncontrolled": true});
+ clients.forEach(function(client) {
+ if (client.url.includes("fetch-destination-iframe")) {
+ if (event.request.destination == destination) {
+ client.postMessage("PASS");
+ } else {
+ client.postMessage("FAIL");
+ }
+ }
+ })
+ }());
+ }
+ event.respondWith(fetch(event.request));
+});
+
+
diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js b/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js
new file mode 100644
index 0000000000..a583b1272a
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js
@@ -0,0 +1,20 @@
+self.addEventListener('fetch', function(event) {
+ const url = event.request.url;
+ if (url.includes('dummy') && url.includes('?')) {
+ event.waitUntil(async function() {
+ let destination = new URL(url).searchParams.get("dest");
+ var result = "FAIL";
+ if (event.request.destination == destination ||
+ (event.request.destination == "empty" && destination == "")) {
+ result = "PASS";
+ }
+ let cl = await clients.matchAll({includeUncontrolled: true});
+ for (i = 0; i < cl.length; i++) {
+ cl[i].postMessage(result);
+ }
+ }())
+ }
+ event.respondWith(fetch(event.request));
+});
+
+
diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker.js b/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker.js
new file mode 100644
index 0000000000..904009c172
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/resources/fetch-destination-worker.js
@@ -0,0 +1,12 @@
+self.addEventListener('fetch', function(event) {
+ if (event.request.url.includes('dummy')) {
+ let destination = new URL(event.request.url).searchParams.get("dest");
+ if (event.request.destination == destination ||
+ (event.request.destination == "empty" && destination == "")) {
+ event.respondWith(fetch(event.request));
+ } else {
+ event.respondWith(Response.error());
+ }
+ }
+});
+
diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/import-declaration-type-css.js b/testing/web-platform/tests/fetch/api/request/destination/resources/import-declaration-type-css.js
new file mode 100644
index 0000000000..3c8cf1f44b
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/resources/import-declaration-type-css.js
@@ -0,0 +1 @@
+import "./dummy.css?dest=style" with { type: "css" };
diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/import-declaration-type-json.js b/testing/web-platform/tests/fetch/api/request/destination/resources/import-declaration-type-json.js
new file mode 100644
index 0000000000..b2d964dd82
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/resources/import-declaration-type-json.js
@@ -0,0 +1 @@
+import "./dummy.json?dest=json" with { type: "json" };
diff --git a/testing/web-platform/tests/fetch/api/request/destination/resources/importer.js b/testing/web-platform/tests/fetch/api/request/destination/resources/importer.js
new file mode 100644
index 0000000000..9568474d50
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/destination/resources/importer.js
@@ -0,0 +1 @@
+importScripts("dummy?t=importScripts&dest=script");
diff --git a/testing/web-platform/tests/fetch/api/request/forbidden-method.any.js b/testing/web-platform/tests/fetch/api/request/forbidden-method.any.js
new file mode 100644
index 0000000000..eb13f37f0b
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/forbidden-method.any.js
@@ -0,0 +1,13 @@
+// META: global=window,worker
+
+// https://fetch.spec.whatwg.org/#forbidden-method
+for (const method of [
+ 'CONNECT', 'TRACE', 'TRACK',
+ 'connect', 'trace', 'track'
+ ]) {
+ test(function() {
+ assert_throws_js(TypeError,
+ function() { new Request('./', {method: method}); }
+ );
+ }, 'Request() with a forbidden method ' + method + ' must throw.');
+}
diff --git a/testing/web-platform/tests/fetch/api/request/multi-globals/construct-in-detached-frame.window.js b/testing/web-platform/tests/fetch/api/request/multi-globals/construct-in-detached-frame.window.js
new file mode 100644
index 0000000000..b0d6ba5b80
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/multi-globals/construct-in-detached-frame.window.js
@@ -0,0 +1,11 @@
+// This is a regression test for Chromium issue https://crbug.com/1427266.
+test(() => {
+ const iframe = document.createElement('iframe');
+ document.body.append(iframe);
+ const otherRequest = iframe.contentWindow.Request;
+ iframe.remove();
+ const r1 = new otherRequest('resource', { method: 'POST', body: 'string' });
+ const r2 = new otherRequest(r1);
+ assert_true(r1.bodyUsed);
+ assert_false(r2.bodyUsed);
+}, 'creating a request from another request in a detached realm should work');
diff --git a/testing/web-platform/tests/fetch/api/request/multi-globals/current/current.html b/testing/web-platform/tests/fetch/api/request/multi-globals/current/current.html
new file mode 100644
index 0000000000..9bb6e0bbf3
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/multi-globals/current/current.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<title>Current page used as a test helper</title>
+<base href="success/">
diff --git a/testing/web-platform/tests/fetch/api/request/multi-globals/incumbent/incumbent.html b/testing/web-platform/tests/fetch/api/request/multi-globals/incumbent/incumbent.html
new file mode 100644
index 0000000000..a885b8a0a7
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/multi-globals/incumbent/incumbent.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<title>Incumbent page used as a test helper</title>
+
+<iframe src="../current/current.html" id="c"></iframe>
+
+<script>
+'use strict';
+
+window.createRequest = (...args) => {
+ const current = document.querySelector('#c').contentWindow;
+ return new current.Request(...args);
+};
+
+</script>
diff --git a/testing/web-platform/tests/fetch/api/request/multi-globals/url-parsing.html b/testing/web-platform/tests/fetch/api/request/multi-globals/url-parsing.html
new file mode 100644
index 0000000000..df60e72507
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/multi-globals/url-parsing.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<title>Request constructor URL parsing, with multiple globals in play</title>
+<link rel="help" href="https://fetch.spec.whatwg.org/#dom-request">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!-- This is the entry global -->
+
+<iframe src="incumbent/incumbent.html"></iframe>
+
+<script>
+'use strict';
+
+const loadPromise = new Promise(resolve => {
+ window.addEventListener("load", () => resolve());
+});
+
+promise_test(() => {
+ return loadPromise.then(() => {
+ const req = document.querySelector('iframe').contentWindow.createRequest("url");
+
+ assert_equals(req.url, new URL("current/success/url", location.href).href);
+ });
+}, "should parse the URL relative to the current settings object");
+
+</script>
diff --git a/testing/web-platform/tests/fetch/api/request/request-bad-port.any.js b/testing/web-platform/tests/fetch/api/request/request-bad-port.any.js
new file mode 100644
index 0000000000..b0684d4be0
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-bad-port.any.js
@@ -0,0 +1,92 @@
+// META: global=window,worker
+
+// list of bad ports according to
+// https://fetch.spec.whatwg.org/#port-blocking
+var BLOCKED_PORTS_LIST = [
+ 1, // tcpmux
+ 7, // echo
+ 9, // discard
+ 11, // systat
+ 13, // daytime
+ 15, // netstat
+ 17, // qotd
+ 19, // chargen
+ 20, // ftp-data
+ 21, // ftp
+ 22, // ssh
+ 23, // telnet
+ 25, // smtp
+ 37, // time
+ 42, // name
+ 43, // nicname
+ 53, // domain
+ 69, // tftp
+ 77, // priv-rjs
+ 79, // finger
+ 87, // ttylink
+ 95, // supdup
+ 101, // hostriame
+ 102, // iso-tsap
+ 103, // gppitnp
+ 104, // acr-nema
+ 109, // pop2
+ 110, // pop3
+ 111, // sunrpc
+ 113, // auth
+ 115, // sftp
+ 117, // uucp-path
+ 119, // nntp
+ 123, // ntp
+ 135, // loc-srv / epmap
+ 137, // netbios-ns
+ 139, // netbios-ssn
+ 143, // imap2
+ 161, // snmp
+ 179, // bgp
+ 389, // ldap
+ 427, // afp (alternate)
+ 465, // smtp (alternate)
+ 512, // print / exec
+ 513, // login
+ 514, // shell
+ 515, // printer
+ 526, // tempo
+ 530, // courier
+ 531, // chat
+ 532, // netnews
+ 540, // uucp
+ 548, // afp
+ 554, // rtsp
+ 556, // remotefs
+ 563, // nntp+ssl
+ 587, // smtp (outgoing)
+ 601, // syslog-conn
+ 636, // ldap+ssl
+ 989, // ftps-data
+ 990, // ftps
+ 993, // ldap+ssl
+ 995, // pop3+ssl
+ 1719, // h323gatestat
+ 1720, // h323hostcall
+ 1723, // pptp
+ 2049, // nfs
+ 3659, // apple-sasl
+ 4045, // lockd
+ 5060, // sip
+ 5061, // sips
+ 6000, // x11
+ 6566, // sane-port
+ 6665, // irc (alternate)
+ 6666, // irc (alternate)
+ 6667, // irc (default)
+ 6668, // irc (alternate)
+ 6669, // irc (alternate)
+ 6697, // irc+tls
+ 10080, // amanda
+];
+
+BLOCKED_PORTS_LIST.map(function(a){
+ promise_test(function(t){
+ return promise_rejects_js(t, TypeError, fetch("http://example.com:" + a))
+ }, 'Request on bad port ' + a + ' should throw TypeError.');
+});
diff --git a/testing/web-platform/tests/fetch/api/request/request-cache-default-conditional.any.js b/testing/web-platform/tests/fetch/api/request/request-cache-default-conditional.any.js
new file mode 100644
index 0000000000..c5b2001cc8
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-cache-default-conditional.any.js
@@ -0,0 +1,170 @@
+// META: global=window,worker
+// META: title=Request cache - default with conditional requests
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=request-cache.js
+
+var tests = [
+ {
+ name: 'RequestCache "default" mode with an If-Modified-Since header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Modified-Since": now.toGMTString()}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Modified-Since header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Modified-Since": now.toGMTString()}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Modified-Since header is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Modified-Since": now.toGMTString()}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Modified-Since header is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Modified-Since": now.toGMTString()}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-None-Match header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-None-Match": '"foo"'}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-None-Match header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-None-Match": '"foo"'}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-None-Match header is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-None-Match": '"foo"'}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-None-Match header is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-None-Match": '"foo"'}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Unmodified-Since header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Unmodified-Since": now.toGMTString()}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Unmodified-Since header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Unmodified-Since": now.toGMTString()}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Unmodified-Since header is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Unmodified-Since": now.toGMTString()}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Unmodified-Since header is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Unmodified-Since": now.toGMTString()}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Match header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Match": '"foo"'}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Match header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Match": '"foo"'}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Match header is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Match": '"foo"'}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Match header is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Match": '"foo"'}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Range header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Range": '"foo"'}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Range header (following a request without additional headers) is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{}, {"If-Range": '"foo"'}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Range header is treated similarly to "no-store"',
+ state: "stale",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Range": '"foo"'}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "default" mode with an If-Range header is treated similarly to "no-store"',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ request_headers: [{"If-Range": '"foo"'}, {}],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+];
+run_tests(tests);
diff --git a/testing/web-platform/tests/fetch/api/request/request-cache-default.any.js b/testing/web-platform/tests/fetch/api/request/request-cache-default.any.js
new file mode 100644
index 0000000000..dfa8369c9a
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-cache-default.any.js
@@ -0,0 +1,39 @@
+// META: global=window,worker
+// META: title=Request cache - default
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=request-cache.js
+
+var tests = [
+ {
+ name: 'RequestCache "default" mode checks the cache for previously cached content and goes to the network for stale responses',
+ state: "stale",
+ request_cache: ["default", "default"],
+ expected_validation_headers: [false, true],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'RequestCache "default" mode checks the cache for previously cached content and avoids going to the network if a fresh response exists',
+ state: "fresh",
+ request_cache: ["default", "default"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false],
+ },
+ {
+ name: 'Responses with the "Cache-Control: no-store" header are not stored in the cache',
+ state: "stale",
+ cache_control: "no-store",
+ request_cache: ["default", "default"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'Responses with the "Cache-Control: no-store" header are not stored in the cache',
+ state: "fresh",
+ cache_control: "no-store",
+ request_cache: ["default", "default"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, false],
+ },
+];
+run_tests(tests);
diff --git a/testing/web-platform/tests/fetch/api/request/request-cache-force-cache.any.js b/testing/web-platform/tests/fetch/api/request/request-cache-force-cache.any.js
new file mode 100644
index 0000000000..00dce096c7
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-cache-force-cache.any.js
@@ -0,0 +1,67 @@
+// META: global=window,worker
+// META: title=Request cache - force-cache
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=request-cache.js
+
+var tests = [
+ {
+ name: 'RequestCache "force-cache" mode checks the cache for previously cached content and avoid revalidation for stale responses',
+ state: "stale",
+ request_cache: ["default", "force-cache"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false],
+ },
+ {
+ name: 'RequestCache "force-cache" mode checks the cache for previously cached content and avoid revalidation for fresh responses',
+ state: "fresh",
+ request_cache: ["default", "force-cache"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false],
+ },
+ {
+ name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response is not found',
+ state: "stale",
+ request_cache: ["force-cache"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false],
+ },
+ {
+ name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response is not found',
+ state: "fresh",
+ request_cache: ["force-cache"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false],
+ },
+ {
+ name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response would vary',
+ state: "stale",
+ vary: "*",
+ request_cache: ["default", "force-cache"],
+ expected_validation_headers: [false, true],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response would vary',
+ state: "fresh",
+ vary: "*",
+ request_cache: ["default", "force-cache"],
+ expected_validation_headers: [false, true],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'RequestCache "force-cache" stores the response in the cache if it goes to the network',
+ state: "stale",
+ request_cache: ["force-cache", "default"],
+ expected_validation_headers: [false, true],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'RequestCache "force-cache" stores the response in the cache if it goes to the network',
+ state: "fresh",
+ request_cache: ["force-cache", "default"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false],
+ },
+];
+run_tests(tests);
diff --git a/testing/web-platform/tests/fetch/api/request/request-cache-no-cache.any.js b/testing/web-platform/tests/fetch/api/request/request-cache-no-cache.any.js
new file mode 100644
index 0000000000..41fc22baf2
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-cache-no-cache.any.js
@@ -0,0 +1,25 @@
+// META: global=window,worker
+// META: title=Request cache : no-cache
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=request-cache.js
+
+var tests = [
+ {
+ name: 'RequestCache "no-cache" mode revalidates stale responses found in the cache',
+ state: "stale",
+ request_cache: ["default", "no-cache"],
+ expected_validation_headers: [false, true],
+ expected_no_cache_headers: [false, false],
+ expected_max_age_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "no-cache" mode revalidates fresh responses found in the cache',
+ state: "fresh",
+ request_cache: ["default", "no-cache"],
+ expected_validation_headers: [false, true],
+ expected_no_cache_headers: [false, false],
+ expected_max_age_headers: [false, true],
+ },
+];
+run_tests(tests);
diff --git a/testing/web-platform/tests/fetch/api/request/request-cache-no-store.any.js b/testing/web-platform/tests/fetch/api/request/request-cache-no-store.any.js
new file mode 100644
index 0000000000..9a28718bf2
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-cache-no-store.any.js
@@ -0,0 +1,37 @@
+// META: global=window,worker
+// META: title=Request cache - no store
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=request-cache.js
+
+var tests = [
+ {
+ name: 'RequestCache "no-store" mode does not check the cache for previously cached content and goes to the network regardless',
+ state: "stale",
+ request_cache: ["default", "no-store"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "no-store" mode does not check the cache for previously cached content and goes to the network regardless',
+ state: "fresh",
+ request_cache: ["default", "no-store"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "no-store" mode does not store the response in the cache',
+ state: "stale",
+ request_cache: ["no-store", "default"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "no-store" mode does not store the response in the cache',
+ state: "fresh",
+ request_cache: ["no-store", "default"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [true, false],
+ },
+];
+run_tests(tests);
diff --git a/testing/web-platform/tests/fetch/api/request/request-cache-only-if-cached.any.js b/testing/web-platform/tests/fetch/api/request/request-cache-only-if-cached.any.js
new file mode 100644
index 0000000000..1305787c7c
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-cache-only-if-cached.any.js
@@ -0,0 +1,66 @@
+// META: global=window,dedicatedworker,sharedworker
+// META: title=Request cache - only-if-cached
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=request-cache.js
+
+// FIXME: avoid mixed content requests to enable service worker global
+var tests = [
+ {
+ name: 'RequestCache "only-if-cached" mode checks the cache for previously cached content and avoids revalidation for stale responses',
+ state: "stale",
+ request_cache: ["default", "only-if-cached"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false]
+ },
+ {
+ name: 'RequestCache "only-if-cached" mode checks the cache for previously cached content and avoids revalidation for fresh responses',
+ state: "fresh",
+ request_cache: ["default", "only-if-cached"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [false]
+ },
+ {
+ name: 'RequestCache "only-if-cached" mode checks the cache for previously cached content and does not go to the network if a cached response is not found',
+ state: "fresh",
+ request_cache: ["only-if-cached"],
+ response: ["error"],
+ expected_validation_headers: [],
+ expected_no_cache_headers: []
+ },
+ {
+ name: 'RequestCache "only-if-cached" (with "same-origin") uses cached same-origin redirects to same-origin content',
+ state: "fresh",
+ request_cache: ["default", "only-if-cached"],
+ redirect: "same-origin",
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'RequestCache "only-if-cached" (with "same-origin") uses cached same-origin redirects to same-origin content',
+ state: "stale",
+ request_cache: ["default", "only-if-cached"],
+ redirect: "same-origin",
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'RequestCache "only-if-cached" (with "same-origin") does not follow redirects across origins and rejects',
+ state: "fresh",
+ request_cache: ["default", "only-if-cached"],
+ redirect: "cross-origin",
+ response: [null, "error"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, false],
+ },
+ {
+ name: 'RequestCache "only-if-cached" (with "same-origin") does not follow redirects across origins and rejects',
+ state: "stale",
+ request_cache: ["default", "only-if-cached"],
+ redirect: "cross-origin",
+ response: [null, "error"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, false],
+ },
+];
+run_tests(tests);
diff --git a/testing/web-platform/tests/fetch/api/request/request-cache-reload.any.js b/testing/web-platform/tests/fetch/api/request/request-cache-reload.any.js
new file mode 100644
index 0000000000..c7bfffb398
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-cache-reload.any.js
@@ -0,0 +1,51 @@
+// META: global=window,worker
+// META: title=Request cache - reload
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=request-cache.js
+
+var tests = [
+ {
+ name: 'RequestCache "reload" mode does not check the cache for previously cached content and goes to the network regardless',
+ state: "stale",
+ request_cache: ["default", "reload"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "reload" mode does not check the cache for previously cached content and goes to the network regardless',
+ state: "fresh",
+ request_cache: ["default", "reload"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+ {
+ name: 'RequestCache "reload" mode does store the response in the cache',
+ state: "stale",
+ request_cache: ["reload", "default"],
+ expected_validation_headers: [false, true],
+ expected_no_cache_headers: [true, false],
+ },
+ {
+ name: 'RequestCache "reload" mode does store the response in the cache',
+ state: "fresh",
+ request_cache: ["reload", "default"],
+ expected_validation_headers: [false],
+ expected_no_cache_headers: [true],
+ },
+ {
+ name: 'RequestCache "reload" mode does store the response in the cache even if a previous response is already stored',
+ state: "stale",
+ request_cache: ["default", "reload", "default"],
+ expected_validation_headers: [false, false, true],
+ expected_no_cache_headers: [false, true, false],
+ },
+ {
+ name: 'RequestCache "reload" mode does store the response in the cache even if a previous response is already stored',
+ state: "fresh",
+ request_cache: ["default", "reload", "default"],
+ expected_validation_headers: [false, false],
+ expected_no_cache_headers: [false, true],
+ },
+];
+run_tests(tests);
diff --git a/testing/web-platform/tests/fetch/api/request/request-cache.js b/testing/web-platform/tests/fetch/api/request/request-cache.js
new file mode 100644
index 0000000000..f2fbecf496
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-cache.js
@@ -0,0 +1,223 @@
+/**
+ * Each test is run twice: once using etag/If-None-Match and once with
+ * date/If-Modified-Since. Each test run gets its own URL and randomized
+ * content and operates independently.
+ *
+ * The test steps are run with request_cache.length fetch requests issued
+ * and their immediate results sanity-checked. The cache.py server script
+ * stashes an entry containing any If-None-Match, If-Modified-Since, Pragma,
+ * and Cache-Control observed headers for each request it receives. When
+ * the test fetches have run, this state is retrieved from cache.py and the
+ * expected_* lists are checked, including their length.
+ *
+ * This means that if a request_* fetch is expected to hit the cache and not
+ * touch the network, then there will be no entry for it in the expect_*
+ * lists. AKA (request_cache.length - expected_validation_headers.length)
+ * should equal the number of cache hits that didn't touch the network.
+ *
+ * Test dictionary keys:
+ * - state: required string that determines whether the Expires response for
+ * the fetched document should be set in the future ("fresh") or past
+ * ("stale").
+ * - vary: optional string to be passed to the server for it to quote back
+ * in a Vary header on the response to us.
+ * - cache_control: optional string to be passed to the server for it to
+ * quote back in a Cache-Control header on the response to us.
+ * - redirect: optional string "same-origin" or "cross-origin". If
+ * provided, the server will issue an absolute redirect to the script on
+ * the same or a different origin, as appropriate. The redirected
+ * location is the script with the redirect parameter removed, so the
+ * content/state/etc. will be as if you hadn't specified a redirect.
+ * - request_cache: required array of cache modes to use (via `cache`).
+ * - request_headers: optional array of explicit fetch `headers` arguments.
+ * If provided, the server will log an empty dictionary for each request
+ * instead of the request headers it would normally log.
+ * - response: optional array of specialized response handling. Right now,
+ * "error" array entries indicate a network error response is expected
+ * which will reject with a TypeError.
+ * - expected_validation_headers: required boolean array indicating whether
+ * the server should have seen an If-None-Match/If-Modified-Since header
+ * in the request.
+ * - expected_no_cache_headers: required boolean array indicating whether
+ * the server should have seen Pragma/Cache-control:no-cache headers in
+ * the request.
+ * - expected_max_age_headers: optional boolean array indicating whether
+ * the server should have seen a Cache-Control:max-age=0 header in the
+ * request.
+ */
+
+var now = new Date();
+
+function base_path() {
+ return location.pathname.replace(/\/[^\/]*$/, '/');
+}
+function make_url(uuid, id, value, content, info) {
+ var dates = {
+ fresh: new Date(now.getFullYear() + 1, now.getMonth(), now.getDay()).toGMTString(),
+ stale: new Date(now.getFullYear() - 1, now.getMonth(), now.getDay()).toGMTString(),
+ };
+ var vary = "";
+ if ("vary" in info) {
+ vary = "&vary=" + info.vary;
+ }
+ var cache_control = "";
+ if ("cache_control" in info) {
+ cache_control = "&cache_control=" + info.cache_control;
+ }
+ var redirect = "";
+
+ var ignore_request_headers = "";
+ if ("request_headers" in info) {
+ // Ignore the request headers that we send since they may be synthesized by the test.
+ ignore_request_headers = "&ignore";
+ }
+ var url_sans_redirect = "resources/cache.py?token=" + uuid +
+ "&content=" + content +
+ "&" + id + "=" + value +
+ "&expires=" + dates[info.state] +
+ vary + cache_control + ignore_request_headers;
+ // If there's a redirect, the target is the script without any redirect at
+ // either the same domain or a different domain.
+ if ("redirect" in info) {
+ var host_info = get_host_info();
+ var origin;
+ switch (info.redirect) {
+ case "same-origin":
+ origin = host_info['HTTP_ORIGIN'];
+ break;
+ case "cross-origin":
+ origin = host_info['HTTP_REMOTE_ORIGIN'];
+ break;
+ }
+ var redirected_url = origin + base_path() + url_sans_redirect;
+ return url_sans_redirect + "&redirect=" + encodeURIComponent(redirected_url);
+ } else {
+ return url_sans_redirect;
+ }
+}
+function expected_status(type, identifier, init) {
+ if (type == "date" &&
+ init.headers &&
+ init.headers["If-Modified-Since"] == identifier) {
+ // The server will respond with a 304 in this case.
+ return [304, "Not Modified"];
+ }
+ return [200, "OK"];
+}
+function expected_response_text(type, identifier, init, content) {
+ if (type == "date" &&
+ init.headers &&
+ init.headers["If-Modified-Since"] == identifier) {
+ // The server will respond with a 304 in this case.
+ return "";
+ }
+ return content;
+}
+function server_state(uuid) {
+ return fetch("resources/cache.py?querystate&token=" + uuid)
+ .then(function(response) {
+ return response.text();
+ }).then(function(text) {
+ // null will be returned if the server never received any requests
+ // for the given uuid. Normalize that to an empty list consistent
+ // with our representation.
+ return JSON.parse(text) || [];
+ });
+}
+function make_test(type, info) {
+ return function(test) {
+ var uuid = token();
+ var identifier = (type == "tag" ? Math.random() : now.toGMTString());
+ var content = Math.random().toString();
+ var url = make_url(uuid, type, identifier, content, info);
+ var fetch_functions = [];
+ for (var i = 0; i < info.request_cache.length; ++i) {
+ fetch_functions.push(function(idx) {
+ var init = {cache: info.request_cache[idx]};
+ if ("request_headers" in info) {
+ init.headers = info.request_headers[idx];
+ }
+ if (init.cache === "only-if-cached") {
+ // only-if-cached requires we use same-origin mode.
+ init.mode = "same-origin";
+ }
+ return fetch(url, init)
+ .then(function(response) {
+ if ("response" in info && info.response[idx] === "error") {
+ assert_true(false, "fetch should have been an error");
+ return;
+ }
+ assert_array_equals([response.status, response.statusText],
+ expected_status(type, identifier, init));
+ return response.text();
+ }).then(function(text) {
+ assert_equals(text, expected_response_text(type, identifier, init, content));
+ }, function(reason) {
+ if ("response" in info && info.response[idx] === "error") {
+ assert_throws_js(TypeError, function() { throw reason; });
+ } else {
+ throw reason;
+ }
+ });
+ });
+ }
+ var i = 0;
+ function run_next_step() {
+ if (fetch_functions.length) {
+ return fetch_functions.shift()(i++)
+ .then(run_next_step);
+ } else {
+ return Promise.resolve();
+ }
+ }
+ return run_next_step()
+ .then(function() {
+ // Now, query the server state
+ return server_state(uuid);
+ }).then(function(state) {
+ var expectedState = [];
+ info.expected_validation_headers.forEach(function (validate) {
+ if (validate) {
+ if (type == "tag") {
+ expectedState.push({"If-None-Match": '"' + identifier + '"'});
+ } else {
+ expectedState.push({"If-Modified-Since": identifier});
+ }
+ } else {
+ expectedState.push({});
+ }
+ });
+ for (var i = 0; i < info.expected_no_cache_headers.length; ++i) {
+ if (info.expected_no_cache_headers[i]) {
+ expectedState[i]["Pragma"] = "no-cache";
+ expectedState[i]["Cache-Control"] = "no-cache";
+ }
+ }
+ if ("expected_max_age_headers" in info) {
+ for (var i = 0; i < info.expected_max_age_headers.length; ++i) {
+ if (info.expected_max_age_headers[i]) {
+ expectedState[i]["Cache-Control"] = "max-age=0";
+ }
+ }
+ }
+ assert_equals(state.length, expectedState.length);
+ for (var i = 0; i < state.length; ++i) {
+ for (var header in state[i]) {
+ assert_equals(state[i][header], expectedState[i][header]);
+ delete expectedState[i][header];
+ }
+ for (var header in expectedState[i]) {
+ assert_false(header in state[i]);
+ }
+ }
+ });
+ };
+}
+
+function run_tests(tests)
+{
+ tests.forEach(function(info) {
+ promise_test(make_test("tag", info), info.name + " with Etag and " + info.state + " response");
+ promise_test(make_test("date", info), info.name + " with Last-Modified and " + info.state + " response");
+ });
+}
diff --git a/testing/web-platform/tests/fetch/api/request/request-clone.sub.html b/testing/web-platform/tests/fetch/api/request/request-clone.sub.html
new file mode 100644
index 0000000000..c690bb3dc0
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-clone.sub.html
@@ -0,0 +1,63 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Request clone</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#request">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../resources/utils.js"></script>
+ </head>
+ <body>
+ <script>
+ var headers = new Headers({"name" : "value"});
+ var emptyHeaders = new Headers();
+
+ var initValuesDict = {"method" : "POST",
+ "referrer" : "http://{{host}}:{{ports[http][0]}}/",
+ "referrerPolicy" : "origin",
+ "mode" : "same-origin",
+ "credentials" : "include",
+ "cache" : "no-cache",
+ "redirect" : "error",
+ "integrity" : "Request's Integrity",
+ "headers" : headers,
+ "body" : "Request's body"
+ };
+
+ var expectedInitialized = {"method" : "POST",
+ "referrer" : "http://{{host}}:{{ports[http][0]}}/",
+ "referrerPolicy" : "origin",
+ "mode" : "same-origin",
+ "credentials" : "include",
+ "cache" : "no-cache",
+ "redirect" : "error",
+ "integrity" : "Request's Integrity",
+ "headers" : headers,
+ "body" : "Request's body"
+ };
+
+ test(function() {
+ var RequestInitialized = new Request("", initValuesDict);
+ var requestToCheck = RequestInitialized.clone();
+ checkRequest(requestToCheck, expectedInitialized);
+ }, "Check cloning a request");
+
+ test(function() {
+ var initialRequest = new Request("", {"headers" : new Headers({"a": "1", "b" : "2"})});
+ var request = initialRequest.clone();
+ assert_equals(request.headers.get("a"), "1", "cloned request should have header 'a'");
+ assert_equals(request.headers.get("b"), "2", "cloned request should have header 'b'");
+
+ initialRequest.headers.delete("a");
+ assert_equals(request.headers.get("a"), "1", "cloned request should still have header 'a'");
+
+ request.headers.delete("a");
+ assert_equals(initialRequest.headers.get("b"), "2", "initial request should have header 'b'");
+
+ }, "Check cloning a request copies the headers");
+
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/fetch/api/request/request-constructor-init-body-override.any.js b/testing/web-platform/tests/fetch/api/request/request-constructor-init-body-override.any.js
new file mode 100644
index 0000000000..27bb991871
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-constructor-init-body-override.any.js
@@ -0,0 +1,21 @@
+promise_test(async function () {
+ const req1 = new Request("https://example.com/", {
+ body: "req1",
+ method: "POST",
+ });
+
+ const text1 = await req1.text();
+ assert_equals(
+ text1,
+ "req1",
+ "The body of the first request should be 'req1'."
+ );
+
+ const req2 = new Request(req1, { body: "req2" });
+ const bodyText = await req2.text();
+ assert_equals(
+ bodyText,
+ "req2",
+ "The body of the second request should be overridden to 'req2'."
+ );
+}, "Check that the body of a new request can be overridden when created from an existing Request object");
diff --git a/testing/web-platform/tests/fetch/api/request/request-consume-empty.any.js b/testing/web-platform/tests/fetch/api/request/request-consume-empty.any.js
new file mode 100644
index 0000000000..034a86041a
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-consume-empty.any.js
@@ -0,0 +1,101 @@
+// META: global=window,worker
+// META: title=Request consume empty bodies
+
+function checkBodyText(test, request) {
+ return request.text().then(function(bodyAsText) {
+ assert_equals(bodyAsText, "", "Resolved value should be empty");
+ assert_false(request.bodyUsed);
+ });
+}
+
+function checkBodyBlob(test, request) {
+ return request.blob().then(function(bodyAsBlob) {
+ var promise = new Promise(function(resolve, reject) {
+ var reader = new FileReader();
+ reader.onload = function(evt) {
+ resolve(reader.result)
+ };
+ reader.onerror = function() {
+ reject("Blob's reader failed");
+ };
+ reader.readAsText(bodyAsBlob);
+ });
+ return promise.then(function(body) {
+ assert_equals(body, "", "Resolved value should be empty");
+ assert_false(request.bodyUsed);
+ });
+ });
+}
+
+function checkBodyArrayBuffer(test, request) {
+ return request.arrayBuffer().then(function(bodyAsArrayBuffer) {
+ assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty");
+ assert_false(request.bodyUsed);
+ });
+}
+
+function checkBodyJSON(test, request) {
+ return request.json().then(
+ function(bodyAsJSON) {
+ assert_unreached("JSON parsing should fail");
+ },
+ function() {
+ assert_false(request.bodyUsed);
+ });
+}
+
+function checkBodyFormData(test, request) {
+ return request.formData().then(function(bodyAsFormData) {
+ assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData");
+ assert_false(request.bodyUsed);
+ });
+}
+
+function checkBodyFormDataError(test, request) {
+ return promise_rejects_js(test, TypeError, request.formData()).then(function() {
+ assert_false(request.bodyUsed);
+ });
+}
+
+function checkRequestWithNoBody(bodyType, checkFunction, headers = []) {
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "headers": headers});
+ assert_false(request.bodyUsed);
+ return checkFunction(test, request);
+ }, "Consume request's body as " + bodyType);
+}
+
+checkRequestWithNoBody("text", checkBodyText);
+checkRequestWithNoBody("blob", checkBodyBlob);
+checkRequestWithNoBody("arrayBuffer", checkBodyArrayBuffer);
+checkRequestWithNoBody("json (error case)", checkBodyJSON);
+checkRequestWithNoBody("formData with correct multipart type (error case)", checkBodyFormDataError, [["Content-Type", 'multipart/form-data; boundary="boundary"']]);
+checkRequestWithNoBody("formData with correct urlencoded type", checkBodyFormData, [["Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"]]);
+checkRequestWithNoBody("formData without correct type (error case)", checkBodyFormDataError);
+
+function checkRequestWithEmptyBody(bodyType, body, asText) {
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "body": body});
+ assert_false(request.bodyUsed, "bodyUsed is false at init");
+ if (asText) {
+ return request.text().then(function(bodyAsString) {
+ assert_equals(bodyAsString.length, 0, "Resolved value should be empty");
+ assert_true(request.bodyUsed, "bodyUsed is true after being consumed");
+ });
+ }
+ return request.arrayBuffer().then(function(bodyAsArrayBuffer) {
+ assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty");
+ assert_true(request.bodyUsed, "bodyUsed is true after being consumed");
+ });
+ }, "Consume empty " + bodyType + " request body as " + (asText ? "text" : "arrayBuffer"));
+}
+
+// FIXME: Add BufferSource, FormData and URLSearchParams.
+checkRequestWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), false);
+checkRequestWithEmptyBody("text", "", false);
+checkRequestWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), true);
+checkRequestWithEmptyBody("text", "", true);
+checkRequestWithEmptyBody("URLSearchParams", new URLSearchParams(""), true);
+// FIXME: This test assumes that the empty string be returned but it is not clear whether that is right. See https://github.com/web-platform-tests/wpt/pull/3950.
+checkRequestWithEmptyBody("FormData", new FormData(), true);
+checkRequestWithEmptyBody("ArrayBuffer", new ArrayBuffer(), true);
diff --git a/testing/web-platform/tests/fetch/api/request/request-consume.any.js b/testing/web-platform/tests/fetch/api/request/request-consume.any.js
new file mode 100644
index 0000000000..aff5d65244
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-consume.any.js
@@ -0,0 +1,145 @@
+// META: global=window,worker
+// META: title=Request consume
+// META: script=../resources/utils.js
+
+function checkBodyText(request, expectedBody) {
+ return request.text().then(function(bodyAsText) {
+ assert_equals(bodyAsText, expectedBody, "Retrieve and verify request's body");
+ assert_true(request.bodyUsed, "body as text: bodyUsed turned true");
+ });
+}
+
+function checkBodyBlob(request, expectedBody, checkContentType) {
+ return request.blob().then(function(bodyAsBlob) {
+ if (checkContentType)
+ assert_equals(bodyAsBlob.type, "text/plain", "Blob body type should be computed from the request Content-Type");
+
+ var promise = new Promise(function (resolve, reject) {
+ var reader = new FileReader();
+ reader.onload = function(evt) {
+ resolve(reader.result)
+ };
+ reader.onerror = function() {
+ reject("Blob's reader failed");
+ };
+ reader.readAsText(bodyAsBlob);
+ });
+ return promise.then(function(body) {
+ assert_equals(body, expectedBody, "Retrieve and verify request's body");
+ assert_true(request.bodyUsed, "body as blob: bodyUsed turned true");
+ });
+ });
+}
+
+function checkBodyArrayBuffer(request, expectedBody) {
+ return request.arrayBuffer().then(function(bodyAsArrayBuffer) {
+ validateBufferFromString(bodyAsArrayBuffer, expectedBody, "Retrieve and verify request's body");
+ assert_true(request.bodyUsed, "body as arrayBuffer: bodyUsed turned true");
+ });
+}
+
+function checkBodyJSON(request, expectedBody) {
+ return request.json().then(function(bodyAsJSON) {
+ var strBody = JSON.stringify(bodyAsJSON)
+ assert_equals(strBody, expectedBody, "Retrieve and verify request's body");
+ assert_true(request.bodyUsed, "body as json: bodyUsed turned true");
+ });
+}
+
+function checkBodyFormData(request, expectedBody) {
+ return request.formData().then(function(bodyAsFormData) {
+ assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData");
+ assert_true(request.bodyUsed, "body as formData: bodyUsed turned true");
+ });
+}
+
+function checkRequestBody(body, expected, bodyType) {
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "body": body, "headers": [["Content-Type", "text/PLAIN"]] });
+ assert_false(request.bodyUsed, "bodyUsed is false at init");
+ return checkBodyText(request, expected);
+ }, "Consume " + bodyType + " request's body as text");
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "body": body });
+ assert_false(request.bodyUsed, "bodyUsed is false at init");
+ return checkBodyBlob(request, expected);
+ }, "Consume " + bodyType + " request's body as blob");
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "body": body });
+ assert_false(request.bodyUsed, "bodyUsed is false at init");
+ return checkBodyArrayBuffer(request, expected);
+ }, "Consume " + bodyType + " request's body as arrayBuffer");
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "body": body });
+ assert_false(request.bodyUsed, "bodyUsed is false at init");
+ return checkBodyJSON(request, expected);
+ }, "Consume " + bodyType + " request's body as JSON");
+}
+
+var textData = JSON.stringify("This is response's body");
+var blob = new Blob([textData], { "type" : "text/plain" });
+
+checkRequestBody(textData, textData, "String");
+
+var string = "\"123456\"";
+function getArrayBuffer() {
+ var arrayBuffer = new ArrayBuffer(8);
+ var int8Array = new Int8Array(arrayBuffer);
+ for (var cptr = 0; cptr < 8; cptr++)
+ int8Array[cptr] = string.charCodeAt(cptr);
+ return arrayBuffer;
+}
+
+function getArrayBufferWithZeros() {
+ var arrayBuffer = new ArrayBuffer(10);
+ var int8Array = new Int8Array(arrayBuffer);
+ for (var cptr = 0; cptr < 8; cptr++)
+ int8Array[cptr + 1] = string.charCodeAt(cptr);
+ return arrayBuffer;
+}
+
+checkRequestBody(getArrayBuffer(), string, "ArrayBuffer");
+checkRequestBody(new Uint8Array(getArrayBuffer()), string, "Uint8Array");
+checkRequestBody(new Int8Array(getArrayBufferWithZeros(), 1, 8), string, "Int8Array");
+checkRequestBody(new Float32Array(getArrayBuffer()), string, "Float32Array");
+checkRequestBody(new DataView(getArrayBufferWithZeros(), 1, 8), string, "DataView");
+
+promise_test(function(test) {
+ var formData = new FormData();
+ formData.append("name", "value")
+ var request = new Request("", {"method": "POST", "body": formData });
+ assert_false(request.bodyUsed, "bodyUsed is false at init");
+ return checkBodyFormData(request, formData);
+}, "Consume FormData request's body as FormData");
+
+function checkBlobResponseBody(blobBody, blobData, bodyType, checkFunction) {
+ promise_test(function(test) {
+ var response = new Response(blobBody);
+ assert_false(response.bodyUsed, "bodyUsed is false at init");
+ return checkFunction(response, blobData);
+ }, "Consume blob response's body as " + bodyType);
+}
+
+checkBlobResponseBody(blob, textData, "blob", checkBodyBlob);
+checkBlobResponseBody(blob, textData, "text", checkBodyText);
+checkBlobResponseBody(blob, textData, "json", checkBodyJSON);
+checkBlobResponseBody(blob, textData, "arrayBuffer", checkBodyArrayBuffer);
+checkBlobResponseBody(new Blob([""]), "", "blob (empty blob as input)", checkBodyBlob);
+
+var goodJSONValues = ["null", "1", "true", "\"string\""];
+goodJSONValues.forEach(function(value) {
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "body": value});
+ return request.json().then(function(v) {
+ assert_equals(v, JSON.parse(value));
+ });
+ }, "Consume JSON from text: '" + JSON.stringify(value) + "'");
+});
+
+var badJSONValues = ["undefined", "{", "a", "["];
+badJSONValues.forEach(function(value) {
+ promise_test(function(test) {
+ var request = new Request("", {"method": "POST", "body": value});
+ return promise_rejects_js(test, SyntaxError, request.json());
+ }, "Trying to consume bad JSON text as JSON: '" + value + "'");
+});
diff --git a/testing/web-platform/tests/fetch/api/request/request-disturbed.any.js b/testing/web-platform/tests/fetch/api/request/request-disturbed.any.js
new file mode 100644
index 0000000000..8a11de78ff
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-disturbed.any.js
@@ -0,0 +1,109 @@
+// META: global=window,worker
+// META: title=Request disturbed
+// META: script=../resources/utils.js
+
+var initValuesDict = {"method" : "POST",
+ "body" : "Request's body"
+};
+
+var noBodyConsumed = new Request("");
+var bodyConsumed = new Request("", initValuesDict);
+
+test(() => {
+ assert_equals(noBodyConsumed.body, null, "body's default value is null");
+ assert_false(noBodyConsumed.bodyUsed , "bodyUsed is false when request is not disturbed");
+ assert_not_equals(bodyConsumed.body, null, "non-null body");
+ assert_true(bodyConsumed.body instanceof ReadableStream, "non-null body type");
+ assert_false(noBodyConsumed.bodyUsed, "bodyUsed is false when request is not disturbed");
+}, "Request's body: initial state");
+
+noBodyConsumed.blob();
+bodyConsumed.blob();
+
+test(function() {
+ assert_false(noBodyConsumed.bodyUsed , "bodyUsed is false when request is not disturbed");
+ try {
+ noBodyConsumed.clone();
+ } catch (e) {
+ assert_unreached("Can use request not disturbed for creating or cloning request");
+ }
+}, "Request without body cannot be disturbed");
+
+test(function() {
+ assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed");
+ assert_throws_js(TypeError, function() { bodyConsumed.clone(); });
+}, "Check cloning a disturbed request");
+
+test(function() {
+ assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed");
+ assert_throws_js(TypeError, function() { new Request(bodyConsumed); });
+}, "Check creating a new request from a disturbed request");
+
+promise_test(function() {
+ assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed");
+ const originalBody = bodyConsumed.body;
+ const bodyReplaced = new Request(bodyConsumed, { body: "Replaced body" });
+ assert_not_equals(bodyReplaced.body, originalBody, "new request's body is new");
+ assert_false(bodyReplaced.bodyUsed, "bodyUsed is false when request is not disturbed");
+ return bodyReplaced.text().then(text => {
+ assert_equals(text, "Replaced body");
+ });
+}, "Check creating a new request with a new body from a disturbed request");
+
+promise_test(function() {
+ var bodyRequest = new Request("", initValuesDict);
+ const originalBody = bodyRequest.body;
+ assert_false(bodyRequest.bodyUsed , "bodyUsed is false when request is not disturbed");
+ var requestFromRequest = new Request(bodyRequest);
+ assert_true(bodyRequest.bodyUsed , "bodyUsed is true when request is disturbed");
+ assert_equals(bodyRequest.body, originalBody, "body should not change");
+ assert_not_equals(originalBody, undefined, "body should not be undefined");
+ assert_not_equals(originalBody, null, "body should not be null");
+ assert_not_equals(requestFromRequest.body, originalBody, "new request's body is new");
+ return requestFromRequest.text().then(text => {
+ assert_equals(text, "Request's body");
+ });
+}, "Input request used for creating new request became disturbed");
+
+promise_test(() => {
+ const bodyRequest = new Request("", initValuesDict);
+ const originalBody = bodyRequest.body;
+ assert_false(bodyRequest.bodyUsed , "bodyUsed is false when request is not disturbed");
+ const requestFromRequest = new Request(bodyRequest, { body : "init body" });
+ assert_true(bodyRequest.bodyUsed , "bodyUsed is true when request is disturbed");
+ assert_equals(bodyRequest.body, originalBody, "body should not change");
+ assert_not_equals(originalBody, undefined, "body should not be undefined");
+ assert_not_equals(originalBody, null, "body should not be null");
+ assert_not_equals(requestFromRequest.body, originalBody, "new request's body is new");
+
+ return requestFromRequest.text().then(text => {
+ assert_equals(text, "init body");
+ });
+}, "Input request used for creating new request became disturbed even if body is not used");
+
+promise_test(function(test) {
+ assert_true(bodyConsumed.bodyUsed , "bodyUsed is true when request is disturbed");
+ return promise_rejects_js(test, TypeError, bodyConsumed.blob());
+}, "Check consuming a disturbed request");
+
+test(function() {
+ var req = new Request(URL, {method: 'POST', body: 'hello'});
+ assert_false(req.bodyUsed,
+ 'Request should not be flagged as used if it has not been ' +
+ 'consumed.');
+ assert_throws_js(TypeError,
+ function() { new Request(req, {method: 'GET'}); },
+ 'A get request may not have body.');
+
+ assert_false(req.bodyUsed, 'After the GET case');
+
+ assert_throws_js(TypeError,
+ function() { new Request(req, {method: 'CONNECT'}); },
+ 'Request() with a forbidden method must throw.');
+
+ assert_false(req.bodyUsed, 'After the forbidden method case');
+
+ var req2 = new Request(req);
+ assert_true(req.bodyUsed,
+ 'Request should be flagged as used if it has been consumed.');
+}, 'Request construction failure should not set "bodyUsed"');
diff --git a/testing/web-platform/tests/fetch/api/request/request-error.any.js b/testing/web-platform/tests/fetch/api/request/request-error.any.js
new file mode 100644
index 0000000000..9ec8015198
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-error.any.js
@@ -0,0 +1,56 @@
+// META: global=window,worker
+// META: title=Request error
+// META: script=request-error.js
+
+// badRequestArgTests is from response-error.js
+for (const { args, testName } of badRequestArgTests) {
+ test(() => {
+ assert_throws_js(
+ TypeError,
+ () => new Request(...args),
+ "Expect TypeError exception"
+ );
+ }, testName);
+}
+
+test(function() {
+ assert_throws_js(
+ TypeError,
+ () => Request("about:blank"),
+ "Calling Request constructor without 'new' must throw"
+ );
+});
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders});
+ var request = new Request(initialRequest);
+ assert_equals(request.headers.get("Content-Type"), "potato");
+}, "Request should get its content-type from the init request");
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders});
+ var headers = new Headers([]);
+ var request = new Request(initialRequest, {"headers" : headers});
+ assert_false(request.headers.has("Content-Type"));
+}, "Request should not get its content-type from the init request if init headers are provided");
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type-Extra", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"});
+ var request = new Request(initialRequest);
+ assert_equals(request.headers.get("Content-Type"), "text/plain;charset=UTF-8");
+}, "Request should get its content-type from the body if none is provided");
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"});
+ var request = new Request(initialRequest);
+ assert_equals(request.headers.get("Content-Type"), "potato");
+}, "Request should get its content-type from init headers if one is provided");
+
+test(function() {
+ var options = {"cache": "only-if-cached", "mode": "same-origin"};
+ new Request("test", options);
+}, "Request with cache mode: only-if-cached and fetch mode: same-origin");
diff --git a/testing/web-platform/tests/fetch/api/request/request-error.js b/testing/web-platform/tests/fetch/api/request/request-error.js
new file mode 100644
index 0000000000..cf77313f5b
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-error.js
@@ -0,0 +1,57 @@
+const badRequestArgTests = [
+ {
+ args: ["", { "window": "http://test.url" }],
+ testName: "RequestInit's window is not null"
+ },
+ {
+ args: ["http://:not a valid URL"],
+ testName: "Input URL is not valid"
+ },
+ {
+ args: ["http://user:pass@test.url"],
+ testName: "Input URL has credentials"
+ },
+ {
+ args: ["", { "mode": "navigate" }],
+ testName: "RequestInit's mode is navigate"
+ },
+ {
+ args: ["", { "referrer": "http://:not a valid URL" }],
+ testName: "RequestInit's referrer is invalid"
+ },
+ {
+ args: ["", { "method": "IN VALID" }],
+ testName: "RequestInit's method is invalid"
+ },
+ {
+ args: ["", { "method": "TRACE" }],
+ testName: "RequestInit's method is forbidden"
+ },
+ {
+ args: ["", { "mode": "no-cors", "method": "PUT" }],
+ testName: "RequestInit's mode is no-cors and method is not simple"
+ },
+ {
+ args: ["", { "mode": "cors", "cache": "only-if-cached" }],
+ testName: "RequestInit's cache mode is only-if-cached and mode is not same-origin"
+ },
+ {
+ args: ["test", { "cache": "only-if-cached", "mode": "cors" }],
+ testName: "Request with cache mode: only-if-cached and fetch mode cors"
+ },
+ {
+ args: ["test", { "cache": "only-if-cached", "mode": "no-cors" }],
+ testName: "Request with cache mode: only-if-cached and fetch mode no-cors"
+ }
+];
+
+badRequestArgTests.push(
+ ...["referrerPolicy", "mode", "credentials", "cache", "redirect"].map(optionProp => {
+ const options = {};
+ options[optionProp] = "BAD";
+ return {
+ args: ["", options],
+ testName: `Bad ${optionProp} init parameter value`
+ };
+ })
+);
diff --git a/testing/web-platform/tests/fetch/api/request/request-headers.any.js b/testing/web-platform/tests/fetch/api/request/request-headers.any.js
new file mode 100644
index 0000000000..a766bcb5ff
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-headers.any.js
@@ -0,0 +1,177 @@
+// META: global=window,worker
+// META: title=Request Headers
+
+var validRequestHeaders = [
+ ["Content-Type", "OK"],
+ ["Potato", "OK"],
+ ["proxy", "OK"],
+ ["proxya", "OK"],
+ ["sec", "OK"],
+ ["secb", "OK"],
+ ["Set-Cookie2", "OK"],
+ ["User-Agent", "OK"],
+];
+var invalidRequestHeaders = [
+ ["Accept-Charset", "KO"],
+ ["accept-charset", "KO"],
+ ["ACCEPT-ENCODING", "KO"],
+ ["Accept-Encoding", "KO"],
+ ["Access-Control-Request-Headers", "KO"],
+ ["Access-Control-Request-Method", "KO"],
+ ["Connection", "KO"],
+ ["Content-Length", "KO"],
+ ["Cookie", "KO"],
+ ["Cookie2", "KO"],
+ ["Date", "KO"],
+ ["DNT", "KO"],
+ ["Expect", "KO"],
+ ["Host", "KO"],
+ ["Keep-Alive", "KO"],
+ ["Origin", "KO"],
+ ["Referer", "KO"],
+ ["Set-Cookie", "KO"],
+ ["TE", "KO"],
+ ["Trailer", "KO"],
+ ["Transfer-Encoding", "KO"],
+ ["Upgrade", "KO"],
+ ["Via", "KO"],
+ ["Proxy-", "KO"],
+ ["proxy-a", "KO"],
+ ["Sec-", "KO"],
+ ["sec-b", "KO"],
+];
+
+var validRequestNoCorsHeaders = [
+ ["Accept", "OK"],
+ ["Accept-Language", "OK"],
+ ["content-language", "OK"],
+ ["content-type", "application/x-www-form-urlencoded"],
+ ["content-type", "application/x-www-form-urlencoded;charset=UTF-8"],
+ ["content-type", "multipart/form-data"],
+ ["content-type", "multipart/form-data;charset=UTF-8"],
+ ["content-TYPE", "text/plain"],
+ ["CONTENT-type", "text/plain;charset=UTF-8"],
+];
+var invalidRequestNoCorsHeaders = [
+ ["Content-Type", "KO"],
+ ["Potato", "KO"],
+ ["proxy", "KO"],
+ ["proxya", "KO"],
+ ["sec", "KO"],
+ ["secb", "KO"],
+ ["Empty-Value", ""],
+];
+
+validRequestHeaders.forEach(function(header) {
+ test(function() {
+ var request = new Request("");
+ request.headers.set(header[0], header[1]);
+ assert_equals(request.headers.get(header[0]), header[1]);
+ }, "Adding valid request header \"" + header[0] + ": " + header[1] + "\"");
+});
+invalidRequestHeaders.forEach(function(header) {
+ test(function() {
+ var request = new Request("");
+ request.headers.set(header[0], header[1]);
+ assert_equals(request.headers.get(header[0]), null);
+ }, "Adding invalid request header \"" + header[0] + ": " + header[1] + "\"");
+});
+
+validRequestNoCorsHeaders.forEach(function(header) {
+ test(function() {
+ var requestNoCors = new Request("", {"mode": "no-cors"});
+ requestNoCors.headers.set(header[0], header[1]);
+ assert_equals(requestNoCors.headers.get(header[0]), header[1]);
+ }, "Adding valid no-cors request header \"" + header[0] + ": " + header[1] + "\"");
+});
+invalidRequestNoCorsHeaders.forEach(function(header) {
+ test(function() {
+ var requestNoCors = new Request("", {"mode": "no-cors"});
+ requestNoCors.headers.set(header[0], header[1]);
+ assert_equals(requestNoCors.headers.get(header[0]), null);
+ }, "Adding invalid no-cors request header \"" + header[0] + ": " + header[1] + "\"");
+});
+
+test(function() {
+ var headers = new Headers([["Cookie2", "potato"]]);
+ var request = new Request("", {"headers": headers});
+ assert_equals(request.headers.get("Cookie2"), null);
+}, "Check that request constructor is filtering headers provided as init parameter");
+
+test(function() {
+ var headers = new Headers([["Content-Type", "potato"]]);
+ var request = new Request("", {"headers": headers, "mode": "no-cors"});
+ assert_equals(request.headers.get("Content-Type"), null);
+}, "Check that no-cors request constructor is filtering headers provided as init parameter");
+
+test(function() {
+ var headers = new Headers([["Content-Type", "potato"]]);
+ var initialRequest = new Request("", {"headers": headers});
+ var request = new Request(initialRequest, {"mode": "no-cors"});
+ assert_equals(request.headers.get("Content-Type"), null);
+}, "Check that no-cors request constructor is filtering headers provided as part of request parameter");
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders});
+ var request = new Request(initialRequest);
+ assert_equals(request.headers.get("Content-Type"), "potato");
+}, "Request should get its content-type from the init request");
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders});
+ var headers = new Headers([]);
+ var request = new Request(initialRequest, {"headers" : headers});
+ assert_false(request.headers.has("Content-Type"));
+}, "Request should not get its content-type from the init request if init headers are provided");
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type-Extra", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"});
+ var request = new Request(initialRequest);
+ assert_equals(request.headers.get("Content-Type"), "text/plain;charset=UTF-8");
+}, "Request should get its content-type from the body if none is provided");
+
+test(function() {
+ var initialHeaders = new Headers([["Content-Type", "potato"]]);
+ var initialRequest = new Request("", {"headers" : initialHeaders, "body" : "this is my plate", "method" : "POST"});
+ var request = new Request(initialRequest);
+ assert_equals(request.headers.get("Content-Type"), "potato");
+}, "Request should get its content-type from init headers if one is provided");
+
+test(function() {
+ var array = [["hello", "worldAHH"]];
+ var object = {"hello": 'worldOOH'};
+ var headers = new Headers(array);
+
+ assert_equals(headers.get("hello"), "worldAHH");
+
+ var request1 = new Request("", {"headers": headers});
+ var request2 = new Request("", {"headers": array});
+ var request3 = new Request("", {"headers": object});
+
+ assert_equals(request1.headers.get("hello"), "worldAHH");
+ assert_equals(request2.headers.get("hello"), "worldAHH");
+ assert_equals(request3.headers.get("hello"), "worldOOH");
+}, "Testing request header creations with various objects");
+
+promise_test(function(test) {
+ var request = new Request("", {"headers" : [["Content-Type", ""]], "body" : "this is my plate", "method" : "POST"});
+ return request.blob().then(function(blob) {
+ assert_equals(blob.type, "", "Blob type should be the empty string");
+ });
+}, "Testing empty Request Content-Type header");
+
+test(function() {
+ const request1 = new Request("");
+ assert_equals(request1.headers, request1.headers);
+
+ const request2 = new Request("", {"headers": {"X-Foo": "bar"}});
+ assert_equals(request2.headers, request2.headers);
+ const headers = request2.headers;
+ request2.headers.set("X-Foo", "quux");
+ assert_equals(headers, request2.headers);
+ headers.set("X-Other-Header", "baz");
+ assert_equals(headers, request2.headers);
+}, "Test that Request.headers has the [SameObject] extended attribute");
diff --git a/testing/web-platform/tests/fetch/api/request/request-init-001.sub.html b/testing/web-platform/tests/fetch/api/request/request-init-001.sub.html
new file mode 100644
index 0000000000..cc495a6652
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-init-001.sub.html
@@ -0,0 +1,112 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Request init: simple cases</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#request">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script>
+ // https://fetch.spec.whatwg.org/#concept-method-normalize
+ var methods = {
+ "givenValues" : [
+ "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS",
+ "get", "head", "post", "put", "delete", "options",
+ "Get", "hEad", "poSt", "Put", "deleTe", "optionS",
+ "PATCH", "patch", "patCh"
+ ],
+ "expectedValues" : [
+ "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS",
+ "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS",
+ "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS",
+ "PATCH", "patch", "patCh"
+ ]
+ };
+ var referrers = {"givenValues" : ["/relative/ressource",
+ "http://{{host}}:{{ports[http][0]}}/relative/ressource?query=true#fragment",
+ "http://{{host}}:{{ports[http][0]}}/",
+ "http://test.url",
+ "about:client",
+ ""
+ ],
+ "expectedValues" : ["http://{{host}}:{{ports[http][0]}}/relative/ressource",
+ "http://{{host}}:{{ports[http][0]}}/relative/ressource?query=true#fragment",
+ "http://{{host}}:{{ports[http][0]}}/",
+ "about:client",
+ "about:client",
+ ""
+ ]
+ };
+ var referrerPolicies = {"givenValues" : [ "",
+ "no-referrer",
+ "no-referrer-when-downgrade",
+ "origin",
+ "origin-when-cross-origin",
+ "unsafe-url",
+ "same-origin",
+ "strict-origin",
+ "strict-origin-when-cross-origin"
+ ],
+ "expectedValues" : ["",
+ "no-referrer",
+ "no-referrer-when-downgrade",
+ "origin",
+ "origin-when-cross-origin",
+ "unsafe-url",
+ "same-origin",
+ "strict-origin",
+ "strict-origin-when-cross-origin"
+ ]
+ };
+ var modes = {"givenValues" : ["same-origin", "no-cors", "cors"],
+ "expectedValues" : ["same-origin", "no-cors", "cors"]
+ };
+ var credentials = {"givenValues" : ["omit", "same-origin", "include"],
+ "expectedValues" : ["omit", "same-origin", "include"]
+ };
+ var caches = {"givenValues" : [ "default", "no-store", "reload", "no-cache", "force-cache"],
+ "expectedValues" : [ "default", "no-store", "reload", "no-cache", "force-cache"]
+ };
+ var redirects = {"givenValues" : ["follow", "error", "manual"],
+ "expectedValues" : ["follow", "error", "manual"]
+ };
+ var integrities = {"givenValues" : ["", "AZERTYUIOP1234567890" ],
+ "expectedValues" : ["", "AZERTYUIOP1234567890"]
+ };
+
+ //there is no getter for window, init's window might be null
+ var windows = {"givenValues" : [ null ],
+ "expectedValues" : [undefined]
+ };
+
+ var initValuesDict = { "method" : methods,
+ "referrer" : referrers,
+ "referrerPolicy" : referrerPolicies,
+ "mode" : modes,
+ "credentials" : credentials,
+ "cache" : caches,
+ "redirect" : redirects,
+ "integrity" : integrities,
+ "window" : windows
+ };
+
+ for (var attributeName in initValuesDict) {
+ var valuesToTest = initValuesDict[attributeName];
+ for (var valueIdx in valuesToTest["givenValues"]) {
+ var givenValue = valuesToTest["givenValues"][valueIdx];
+ var expectedValue = valuesToTest["expectedValues"][valueIdx];
+ test(function() {
+ var requestInit = {};
+ requestInit[attributeName] = givenValue
+ var request = new Request("", requestInit);
+ assert_equals(request[attributeName], expectedValue,
+ "Expect request's " + attributeName + " is " + expectedValue + " when initialized with " + givenValue);
+ }, "Check " + attributeName + " init value of " + givenValue + " and associated getter");
+ }
+ }
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/fetch/api/request/request-init-002.any.js b/testing/web-platform/tests/fetch/api/request/request-init-002.any.js
new file mode 100644
index 0000000000..abb6689f1e
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-init-002.any.js
@@ -0,0 +1,60 @@
+// META: global=window,worker
+// META: title=Request init: headers and body
+
+test(function() {
+ var headerDict = {"name1": "value1",
+ "name2": "value2",
+ "name3": "value3"
+ };
+ var headers = new Headers(headerDict);
+ var request = new Request("", { "headers" : headers })
+ for (var name in headerDict) {
+ assert_equals(request.headers.get(name), headerDict[name],
+ "request's headers has " + name + " : " + headerDict[name]);
+ }
+}, "Initialize Request with headers values");
+
+function makeRequestInit(body, method) {
+ return {"method": method, "body": body};
+}
+
+function checkRequestInit(body, bodyType, expectedTextBody) {
+ promise_test(function(test) {
+ var request = new Request("", makeRequestInit(body, "POST"));
+ if (body) {
+ assert_throws_js(TypeError, function() { new Request("", makeRequestInit(body, "GET")); });
+ assert_throws_js(TypeError, function() { new Request("", makeRequestInit(body, "HEAD")); });
+ } else {
+ new Request("", makeRequestInit(body, "GET")); // should not throw
+ }
+ var reqHeaders = request.headers;
+ var mime = reqHeaders.get("Content-Type");
+ assert_true(!body || (mime && mime.search(bodyType) > -1), "Content-Type header should be \"" + bodyType + "\", not \"" + mime + "\"");
+ return request.text().then(function(bodyAsText) {
+ //not equals: cannot guess formData exact value
+ assert_true( bodyAsText.search(expectedTextBody) > -1, "Retrieve and verify request body");
+ });
+ }, `Initialize Request's body with "${body}", ${bodyType}`);
+}
+
+var blob = new Blob(["This is a blob"], {type: "application/octet-binary"});
+var formaData = new FormData();
+formaData.append("name", "value");
+var usvString = "This is a USVString"
+
+checkRequestInit(undefined, undefined, "");
+checkRequestInit(null, null, "");
+checkRequestInit(blob, "application/octet-binary", "This is a blob");
+checkRequestInit(formaData, "multipart/form-data", "name=\"name\"\r\n\r\nvalue");
+checkRequestInit(usvString, "text/plain;charset=UTF-8", "This is a USVString");
+checkRequestInit({toString: () => "hi!"}, "text/plain;charset=UTF-8", "hi!");
+
+// Ensure test does not time out in case of missing URLSearchParams support.
+if (self.URLSearchParams) {
+ var urlSearchParams = new URLSearchParams("name=value");
+ checkRequestInit(urlSearchParams, "application/x-www-form-urlencoded;charset=UTF-8", "name=value");
+} else {
+ promise_test(function(test) {
+ return Promise.reject("URLSearchParams not supported");
+ }, "Initialize Request's body with application/x-www-form-urlencoded;charset=UTF-8");
+}
diff --git a/testing/web-platform/tests/fetch/api/request/request-init-003.sub.html b/testing/web-platform/tests/fetch/api/request/request-init-003.sub.html
new file mode 100644
index 0000000000..79c91cdfe8
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-init-003.sub.html
@@ -0,0 +1,84 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Request: init with request or url</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#request">
+ <meta name="help" href="https://url.spec.whatwg.org/#concept-url-serializer">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ </head>
+ <body>
+ <script src="../resources/utils.js"></script>
+ <script>
+ var headers = new Headers( {"name":"value"} );
+ var emptyHeaders = new Headers();
+
+ var initValuesDict = {"method" : "POST",
+ "referrer" : "http://{{host}}:{{ports[http][0]}}/",
+ "referrerPolicy" : "origin",
+ "mode" : "same-origin",
+ "credentials" : "include",
+ "cache" : "no-cache",
+ "redirect" : "error",
+ "integrity" : "Request's Integrity",
+ "headers" : headers,
+ "body" : "Request's body"
+ };
+
+ var expectedInitialized = {"method" : "POST",
+ "referrer" : "http://{{host}}:{{ports[http][0]}}/",
+ "referrerPolicy" : "origin",
+ "mode" : "same-origin",
+ "credentials" : "include",
+ "cache" : "no-cache",
+ "redirect" : "error",
+ "integrity" : "Request's Integrity",
+ "headers" : headers,
+ "body" : "Request's body"
+ };
+
+ var expectedDefault = {"method" : "GET",
+ "url" : location.href,
+ "referrer" : "about:client",
+ "referrerPolicy" : "",
+ "mode" : "cors",
+ "credentials" : "same-origin",
+ "cache" : "default",
+ "redirect" : "follow",
+ "integrity" : "",
+ "headers" : emptyHeaders
+ };
+
+ var requestDefault = new Request("");
+ var requestInitialized = new Request("", initValuesDict);
+
+ test(function() {
+ var requestToCheck = new Request(requestInitialized);
+ checkRequest(requestToCheck, expectedInitialized);
+ }, "Check request values when initialized from Request");
+
+ test(function() {
+ var requestToCheck = new Request(requestDefault, initValuesDict);
+ checkRequest(requestToCheck, expectedInitialized);
+ }, "Check request values when initialized from Request and init values");
+
+ test(function() {
+ var url = "http://url.test:1234/path/subpath?query=true";
+ url += "#fragment";
+ expectedDefault["url"] = url;
+ var requestToCheck = new Request(url);
+ checkRequest(requestToCheck, expectedDefault);
+ }, "Check request values when initialized from url string");
+
+ test(function() {
+ var url = "http://url.test:1234/path/subpath?query=true";
+ url += "#fragment";
+ expectedInitialized["url"] = url;
+ var requestToCheck = new Request(url , initValuesDict);
+ checkRequest(requestToCheck, expectedInitialized);
+ }, "Check request values when initialized from url and init values");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/fetch/api/request/request-init-contenttype.any.js b/testing/web-platform/tests/fetch/api/request/request-init-contenttype.any.js
new file mode 100644
index 0000000000..18a6969d4f
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-init-contenttype.any.js
@@ -0,0 +1,141 @@
+function requestFromBody(body) {
+ return new Request(
+ "https://example.com",
+ {
+ method: "POST",
+ body,
+ duplex: "half",
+ },
+ );
+}
+
+test(() => {
+ const request = requestFromBody(undefined);
+ assert_equals(request.headers.get("Content-Type"), null);
+}, "Default Content-Type for Request with empty body");
+
+test(() => {
+ const blob = new Blob([]);
+ const request = requestFromBody(blob);
+ assert_equals(request.headers.get("Content-Type"), null);
+}, "Default Content-Type for Request with Blob body (no type set)");
+
+test(() => {
+ const blob = new Blob([], { type: "" });
+ const request = requestFromBody(blob);
+ assert_equals(request.headers.get("Content-Type"), null);
+}, "Default Content-Type for Request with Blob body (empty type)");
+
+test(() => {
+ const blob = new Blob([], { type: "a/b; c=d" });
+ const request = requestFromBody(blob);
+ assert_equals(request.headers.get("Content-Type"), "a/b; c=d");
+}, "Default Content-Type for Request with Blob body (set type)");
+
+test(() => {
+ const buffer = new Uint8Array();
+ const request = requestFromBody(buffer);
+ assert_equals(request.headers.get("Content-Type"), null);
+}, "Default Content-Type for Request with buffer source body");
+
+promise_test(async () => {
+ const formData = new FormData();
+ formData.append("a", "b");
+ const request = requestFromBody(formData);
+ const boundary = (await request.text()).split("\r\n")[0].slice(2);
+ assert_equals(
+ request.headers.get("Content-Type"),
+ `multipart/form-data; boundary=${boundary}`,
+ );
+}, "Default Content-Type for Request with FormData body");
+
+test(() => {
+ const usp = new URLSearchParams();
+ const request = requestFromBody(usp);
+ assert_equals(
+ request.headers.get("Content-Type"),
+ "application/x-www-form-urlencoded;charset=UTF-8",
+ );
+}, "Default Content-Type for Request with URLSearchParams body");
+
+test(() => {
+ const request = requestFromBody("");
+ assert_equals(
+ request.headers.get("Content-Type"),
+ "text/plain;charset=UTF-8",
+ );
+}, "Default Content-Type for Request with string body");
+
+test(() => {
+ const stream = new ReadableStream();
+ const request = requestFromBody(stream);
+ assert_equals(request.headers.get("Content-Type"), null);
+}, "Default Content-Type for Request with ReadableStream body");
+
+// -----------------------------------------------------------------------------
+
+const OVERRIDE_MIME = "test/only; mime=type";
+
+function requestFromBodyWithOverrideMime(body) {
+ return new Request(
+ "https://example.com",
+ {
+ method: "POST",
+ body,
+ headers: { "Content-Type": OVERRIDE_MIME },
+ duplex: "half",
+ },
+ );
+}
+
+test(() => {
+ const request = requestFromBodyWithOverrideMime(undefined);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with empty body");
+
+test(() => {
+ const blob = new Blob([]);
+ const request = requestFromBodyWithOverrideMime(blob);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with Blob body (no type set)");
+
+test(() => {
+ const blob = new Blob([], { type: "" });
+ const request = requestFromBodyWithOverrideMime(blob);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with Blob body (empty type)");
+
+test(() => {
+ const blob = new Blob([], { type: "a/b; c=d" });
+ const request = requestFromBodyWithOverrideMime(blob);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with Blob body (set type)");
+
+test(() => {
+ const buffer = new Uint8Array();
+ const request = requestFromBodyWithOverrideMime(buffer);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with buffer source body");
+
+test(() => {
+ const formData = new FormData();
+ const request = requestFromBodyWithOverrideMime(formData);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with FormData body");
+
+test(() => {
+ const usp = new URLSearchParams();
+ const request = requestFromBodyWithOverrideMime(usp);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with URLSearchParams body");
+
+test(() => {
+ const request = requestFromBodyWithOverrideMime("");
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with string body");
+
+test(() => {
+ const stream = new ReadableStream();
+ const request = requestFromBodyWithOverrideMime(stream);
+ assert_equals(request.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Request with ReadableStream body");
diff --git a/testing/web-platform/tests/fetch/api/request/request-init-priority.any.js b/testing/web-platform/tests/fetch/api/request/request-init-priority.any.js
new file mode 100644
index 0000000000..eb5073c857
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-init-priority.any.js
@@ -0,0 +1,26 @@
+var priorities = ["high",
+ "low",
+ "auto"
+ ];
+
+for (idx in priorities) {
+ test(() => {
+ new Request("", {priority: priorities[idx]});
+ }, "new Request() with a '" + priorities[idx] + "' priority does not throw an error");
+}
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new Request("", {priority: 'invalid'});
+ }, "a new Request() must throw a TypeError if RequestInit's priority is an invalid value");
+}, "new Request() throws a TypeError if any of RequestInit's members' values are invalid");
+
+for (idx in priorities) {
+ promise_test(function(t) {
+ return fetch('hello.txt', { priority: priorities[idx] });
+ }, "fetch() with a '" + priorities[idx] + "' priority completes successfully");
+}
+
+promise_test(function(t) {
+ return promise_rejects_js(t, TypeError, fetch('hello.txt', { priority: 'invalid' }));
+}, "fetch() with an invalid priority returns a rejected promise with a TypeError");
diff --git a/testing/web-platform/tests/fetch/api/request/request-init-stream.any.js b/testing/web-platform/tests/fetch/api/request/request-init-stream.any.js
new file mode 100644
index 0000000000..f0ae441a00
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-init-stream.any.js
@@ -0,0 +1,147 @@
+// META: global=window,worker
+
+"use strict";
+
+const duplex = "half";
+const method = "POST";
+
+test(() => {
+ const body = new ReadableStream();
+ const request = new Request("...", { method, body, duplex });
+ assert_equals(request.body, body);
+}, "Constructing a Request with a stream holds the original object.");
+
+test((t) => {
+ const body = new ReadableStream();
+ body.getReader();
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "Constructing a Request with a stream on which getReader() is called");
+
+test((t) => {
+ const body = new ReadableStream();
+ body.getReader().read();
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "Constructing a Request with a stream on which read() is called");
+
+promise_test(async (t) => {
+ const body = new ReadableStream({ pull: c => c.enqueue(new Uint8Array()) });
+ const reader = body.getReader();
+ await reader.read();
+ reader.releaseLock();
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "Constructing a Request with a stream on which read() and releaseLock() are called");
+
+test((t) => {
+ const request = new Request("...", { method: "POST", body: "..." });
+ request.body.getReader();
+ assert_throws_js(TypeError, () => new Request(request));
+ // This doesn't throw.
+ new Request(request, { body: "..." });
+}, "Constructing a Request with a Request on which body.getReader() is called");
+
+test((t) => {
+ const request = new Request("...", { method: "POST", body: "..." });
+ request.body.getReader().read();
+ assert_throws_js(TypeError, () => new Request(request));
+ // This doesn't throw.
+ new Request(request, { body: "..." });
+}, "Constructing a Request with a Request on which body.getReader().read() is called");
+
+promise_test(async (t) => {
+ const request = new Request("...", { method: "POST", body: "..." });
+ const reader = request.body.getReader();
+ await reader.read();
+ reader.releaseLock();
+ assert_throws_js(TypeError, () => new Request(request));
+ // This doesn't throw.
+ new Request(request, { body: "..." });
+}, "Constructing a Request with a Request on which read() and releaseLock() are called");
+
+test((t) => {
+ new Request("...", { method, body: null });
+}, "It is OK to omit .duplex when the body is null.");
+
+test((t) => {
+ new Request("...", { method, body: "..." });
+}, "It is OK to omit .duplex when the body is a string.");
+
+test((t) => {
+ new Request("...", { method, body: new Uint8Array(3) });
+}, "It is OK to omit .duplex when the body is a Uint8Array.");
+
+test((t) => {
+ new Request("...", { method, body: new Blob([]) });
+}, "It is OK to omit .duplex when the body is a Blob.");
+
+test((t) => {
+ const body = new ReadableStream();
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body }));
+}, "It is error to omit .duplex when the body is a ReadableStream.");
+
+test((t) => {
+ new Request("...", { method, body: null, duplex: "half" });
+}, "It is OK to set .duplex = 'half' when the body is null.");
+
+test((t) => {
+ new Request("...", { method, body: "...", duplex: "half" });
+}, "It is OK to set .duplex = 'half' when the body is a string.");
+
+test((t) => {
+ new Request("...", { method, body: new Uint8Array(3), duplex: "half" });
+}, "It is OK to set .duplex = 'half' when the body is a Uint8Array.");
+
+test((t) => {
+ new Request("...", { method, body: new Blob([]), duplex: "half" });
+}, "It is OK to set .duplex = 'half' when the body is a Blob.");
+
+test((t) => {
+ const body = new ReadableStream();
+ new Request("...", { method, body, duplex: "half" });
+}, "It is OK to set .duplex = 'half' when the body is a ReadableStream.");
+
+test((t) => {
+ const body = null;
+ const duplex = "full";
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "It is error to set .duplex = 'full' when the body is null.");
+
+test((t) => {
+ const body = "...";
+ const duplex = "full";
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "It is error to set .duplex = 'full' when the body is a string.");
+
+test((t) => {
+ const body = new Uint8Array(3);
+ const duplex = "full";
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "It is error to set .duplex = 'full' when the body is a Uint8Array.");
+
+test((t) => {
+ const body = new Blob([]);
+ const duplex = "full";
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "It is error to set .duplex = 'full' when the body is a Blob.");
+
+test((t) => {
+ const body = new ReadableStream();
+ const duplex = "full";
+ assert_throws_js(TypeError,
+ () => new Request("...", { method, body, duplex }));
+}, "It is error to set .duplex = 'full' when the body is a ReadableStream.");
+
+test((t) => {
+ const body = new ReadableStream();
+ const duplex = "half";
+ const req1 = new Request("...", { method, body, duplex });
+ const req2 = new Request(req1);
+}, "It is OK to omit duplex when init.body is not given and input.body is given.");
+
diff --git a/testing/web-platform/tests/fetch/api/request/request-keepalive-quota.html b/testing/web-platform/tests/fetch/api/request/request-keepalive-quota.html
new file mode 100644
index 0000000000..548ab38d7e
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-keepalive-quota.html
@@ -0,0 +1,97 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Request Keepalive Quota Tests</title>
+ <meta name="timeout" content="long">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#request">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#body-mixin">
+ <meta name="author" title="Microsoft Edge" href="https://www.microsoft.com">
+ <meta name="variant" content="?include=fast">
+ <meta name="variant" content="?include=slow-1">
+ <meta name="variant" content="?include=slow-2">
+ <meta name="variant" content="?include=slow-3">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/common/subset-tests-by-key.js"></script>
+ </head>
+ <body>
+ <script>
+ 'use strict';
+
+ // We want to ensure that our keepalive requests hang slightly before completing so we can validate
+ // the effects of a rolling quota. To do this we will utilize trickle.py with a 1s delay. This should
+ // prevent any of the Fetch's from finishing in this window.
+ const trickleURL = '../resources/trickle.py?count=1&ms=';
+ const noDelay = 0;
+ const standardDelay = 1000;
+ function wait(ms) {
+ return new Promise(resolve => step_timeout(resolve, ms));
+ }
+
+ // We should expect 64KiB of rolling quota for any type of keep-alive request sent.
+ const expectedQuota = 65536;
+
+ function fetchKeepAliveRequest(delay, bodySize) {
+ // Create a body of the specified size that's filled with *'s
+ const body = '*'.repeat(bodySize);
+ return fetch(trickleURL + delay, {keepalive: true, body, method: 'POST'}).then(res => {
+ return res.text();
+ }).then(() => {
+ return wait(1);
+ });
+ }
+
+ // Test 1 Byte
+ subsetTestByKey("fast", promise_test, function(test) {
+ return fetchKeepAliveRequest(noDelay, 1 /* bodySize */);
+ }, 'A Keep-Alive fetch() with a small body should succeed.');
+
+ // Test Quota full limit
+ subsetTestByKey("fast", promise_test, function(test) {
+ return fetchKeepAliveRequest(noDelay, expectedQuota /* bodySize */);
+ }, 'A Keep-Alive fetch() with a body at the Quota Limit should succeed.');
+
+ // Test Quota + 1 Byte
+ subsetTestByKey("fast", promise_test, function(test) {
+ return promise_rejects_js(test, TypeError, fetchKeepAliveRequest(noDelay, expectedQuota + 1));
+ }, 'A Keep-Alive fetch() with a body over the Quota Limit should reject.');
+
+ // Test the Quota becomes available upon promise completion.
+ subsetTestByKey("slow-1", promise_test, function (test) {
+ // Fill our Quota then try to send a second fetch.
+ return fetchKeepAliveRequest(standardDelay, expectedQuota).then(() => {
+ // Now validate that we can send another Keep-Alive fetch for the full size of the quota.
+ return fetchKeepAliveRequest(noDelay, expectedQuota);
+ });
+ }, 'A Keep-Alive fetch() should return its allocated Quota upon promise resolution.');
+
+ // Ensure only the correct amount of Quota becomes available when a fetch completes.
+ subsetTestByKey("slow-2", promise_test, function(test) {
+ // Create a fetch that uses all but 1 Byte of the Quota and runs for 2x as long as the other requests.
+ const first = fetchKeepAliveRequest(standardDelay * 2, expectedQuota - 1);
+
+ // Now create a single Byte request that will complete quicker.
+ const second = fetchKeepAliveRequest(standardDelay, 1 /* bodySize */).then(() => {
+ // We shouldn't be able to create a 2 Byte request right now as only 1 Byte should have freed up.
+ return promise_rejects_js(test, TypeError, fetchKeepAliveRequest(noDelay, 2 /* bodySize */));
+ }).then(() => {
+ // Now validate that we can send another Keep-Alive fetch for just 1 Byte.
+ return fetchKeepAliveRequest(noDelay, 1 /* bodySize */);
+ });
+
+ return Promise.all([first, second]);
+ }, 'A Keep-Alive fetch() should return only its allocated Quota upon promise resolution.');
+
+ // Test rejecting a fetch() after the quota is used up.
+ subsetTestByKey("slow-3", promise_test, function (test) {
+ // Fill our Quota then try to send a second fetch.
+ const p = fetchKeepAliveRequest(standardDelay, expectedQuota);
+
+ const q = promise_rejects_js(test, TypeError, fetchKeepAliveRequest(noDelay, 1 /* bodySize */));
+ return Promise.all([p, q]);
+ }, 'A Keep-Alive fetch() should not be allowed if the Quota is used up.');
+
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/fetch/api/request/request-keepalive.any.js b/testing/web-platform/tests/fetch/api/request/request-keepalive.any.js
new file mode 100644
index 0000000000..cb4506db46
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-keepalive.any.js
@@ -0,0 +1,17 @@
+// META: global=window,worker
+// META: title=Request keepalive
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+
+test(() => {
+ assert_false(new Request('/').keepalive, 'default');
+ assert_true(new Request('/', {keepalive: true}).keepalive, 'true');
+ assert_false(new Request('/', {keepalive: false}).keepalive, 'false');
+ assert_true(new Request('/', {keepalive: 1}).keepalive, 'truish');
+ assert_false(new Request('/', {keepalive: 0}).keepalive, 'falsy');
+}, 'keepalive flag');
+
+test(() => {
+ const init = {method: 'POST', keepalive: true, body: new ReadableStream()};
+ assert_throws_js(TypeError, () => {new Request('/', init)});
+}, 'keepalive flag with stream body');
diff --git a/testing/web-platform/tests/fetch/api/request/request-reset-attributes.https.html b/testing/web-platform/tests/fetch/api/request/request-reset-attributes.https.html
new file mode 100644
index 0000000000..7be3608d73
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-reset-attributes.https.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<body>
+<script>
+const worker = 'resources/request-reset-attributes-worker.js';
+
+function wait(ms) {
+ return new Promise(resolve => step_timeout(resolve, ms));
+}
+
+promise_test(async (t) => {
+ const scope = 'resources/hello.txt?name=isReloadNavigation';
+ let frame;
+ let reg;
+
+ try {
+ reg = await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, reg.installing, 'activated');
+ frame = await with_iframe(scope);
+ assert_equals(frame.contentDocument.body.textContent,
+ 'old: false, new: false');
+ await new Promise((resolve) => {
+ frame.onload = resolve;
+ frame.contentWindow.location.reload();
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'old: true, new: false');
+ } finally {
+ if (frame) {
+ frame.remove();
+ }
+ if (reg) {
+ await reg.unregister();
+ }
+ }
+ }, 'Request.isReloadNavigation is reset with non-empty RequestInit');
+
+promise_test(async (t) => {
+ const scope = 'resources/hello.html?name=isHistoryNavigation';
+ let frame;
+ let reg;
+
+ try {
+ reg = await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, reg.installing, 'activated');
+ frame = await with_iframe(scope);
+ assert_equals(frame.contentDocument.body.textContent,
+ 'old: false, new: false');
+ // Use step_timeout(0) to ensure the history entry is created for Blink
+ // and WebKit. See https://bugs.webkit.org/show_bug.cgi?id=42861.
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.onload = resolve;
+ frame.src = 'resources/hello.html?ignore';
+ });
+ await wait(0);
+ await new Promise((resolve) => {
+ frame.onload = resolve;
+ frame.contentWindow.history.go(-1);
+ });
+ assert_equals(frame.contentDocument.body.textContent,
+ 'old: true, new: false');
+ } finally {
+ if (frame) {
+ frame.remove();
+ }
+ if (reg) {
+ await reg.unregister();
+ }
+ }
+}, 'Request.isHistoryNavigation is reset with non-empty RequestInit');
+
+promise_test(async (t) => {
+ const scope = 'resources/hello.txt?name=mode';
+ let frame;
+ let reg;
+
+ try {
+ reg = await service_worker_unregister_and_register(t, worker, scope);
+ await wait_for_state(t, reg.installing, 'activated');
+ frame = await with_iframe(scope);
+ assert_equals(frame.contentDocument.body.textContent,
+ 'old: navigate, new: same-origin');
+ } finally {
+ if (frame) {
+ frame.remove();
+ }
+ if (reg) {
+ await reg.unregister();
+ }
+ }
+ }, 'Request.mode is reset with non-empty RequestInit when it\'s "navigate"');
+</script>
diff --git a/testing/web-platform/tests/fetch/api/request/request-structure.any.js b/testing/web-platform/tests/fetch/api/request/request-structure.any.js
new file mode 100644
index 0000000000..5e78553855
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/request-structure.any.js
@@ -0,0 +1,143 @@
+// META: global=window,worker
+// META: title=Request structure
+
+var request = new Request("");
+var methods = ["clone",
+ //Request implements Body
+ "arrayBuffer",
+ "blob",
+ "formData",
+ "json",
+ "text"
+ ];
+var attributes = ["method",
+ "url",
+ "headers",
+ "destination",
+ "referrer",
+ "referrerPolicy",
+ "mode",
+ "credentials",
+ "cache",
+ "redirect",
+ "integrity",
+ "isReloadNavigation",
+ "isHistoryNavigation",
+ "duplex",
+ //Request implements Body
+ "bodyUsed"
+ ];
+var internalAttributes = ["priority",
+ "internalpriority",
+ "blocking"
+ ];
+
+function isReadOnly(request, attributeToCheck) {
+ var defaultValue = undefined;
+ var newValue = undefined;
+ switch (attributeToCheck) {
+ case "method":
+ defaultValue = "GET";
+ newValue = "POST";
+ break;
+
+ case "url":
+ //default value is base url
+ //i.e http://example.com/fetch/api/request-structure.html
+ newValue = "http://url.test";
+ break;
+
+ case "headers":
+ request.headers = new Headers ({"name":"value"});
+ assert_false(request.headers.has("name"), "Headers attribute is read only");
+ return;
+
+ case "destination":
+ defaultValue = "";
+ newValue = "worker";
+ break;
+
+ case "referrer":
+ defaultValue = "about:client";
+ newValue = "http://url.test";
+ break;
+
+ case "referrerPolicy":
+ defaultValue = "";
+ newValue = "unsafe-url";
+ break;
+
+ case "mode":
+ defaultValue = "cors";
+ newValue = "navigate";
+ break;
+
+ case "credentials":
+ defaultValue = "same-origin";
+ newValue = "cors";
+ break;
+
+ case "cache":
+ defaultValue = "default";
+ newValue = "reload";
+ break;
+
+ case "redirect":
+ defaultValue = "follow";
+ newValue = "manual";
+ break;
+
+ case "integrity":
+ newValue = "CannotWriteIntegrity";
+ break;
+
+ case "bodyUsed":
+ defaultValue = false;
+ newValue = true;
+ break;
+
+ case "isReloadNavigation":
+ defaultValue = false;
+ newValue = true;
+ break;
+
+ case "isHistoryNavigation":
+ defaultValue = false;
+ newValue = true;
+ break;
+
+ case "duplex":
+ defaultValue = "half";
+ newValue = "full";
+ break;
+
+ default:
+ return;
+ }
+
+ request[attributeToCheck] = newValue;
+ if (defaultValue === undefined)
+ assert_not_equals(request[attributeToCheck], newValue, "Attribute " + attributeToCheck + " is read only");
+ else
+ assert_equals(request[attributeToCheck], defaultValue,
+ "Attribute " + attributeToCheck + " is read only. Default value is " + defaultValue);
+}
+
+for (var idx in methods) {
+ test(function() {
+ assert_true(methods[idx] in request, "request has " + methods[idx] + " method");
+ }, "Request has " + methods[idx] + " method");
+}
+
+for (var idx in attributes) {
+ test(function() {
+ assert_true(attributes[idx] in request, "request has " + attributes[idx] + " attribute");
+ isReadOnly(request, attributes[idx]);
+ }, "Check " + attributes[idx] + " attribute");
+}
+
+for (var idx in internalAttributes) {
+ test(function() {
+ assert_false(internalAttributes[idx] in request, "request does not expose " + internalAttributes[idx] + " attribute");
+ }, "Request does not expose " + internalAttributes[idx] + " attribute");
+} \ No newline at end of file
diff --git a/testing/web-platform/tests/fetch/api/request/resources/cache.py b/testing/web-platform/tests/fetch/api/request/resources/cache.py
new file mode 100644
index 0000000000..ca0bd644b4
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/resources/cache.py
@@ -0,0 +1,67 @@
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ token = request.GET.first(b"token", None)
+ if b"querystate" in request.GET:
+ from json import JSONEncoder
+ response.headers.set(b"Content-Type", b"text/plain")
+ return JSONEncoder().encode(request.server.stash.take(token))
+ content = request.GET.first(b"content", None)
+ tag = request.GET.first(b"tag", None)
+ date = request.GET.first(b"date", None)
+ expires = request.GET.first(b"expires", None)
+ vary = request.GET.first(b"vary", None)
+ cc = request.GET.first(b"cache_control", None)
+ redirect = request.GET.first(b"redirect", None)
+ inm = request.headers.get(b"If-None-Match", None)
+ ims = request.headers.get(b"If-Modified-Since", None)
+ pragma = request.headers.get(b"Pragma", None)
+ cache_control = request.headers.get(b"Cache-Control", None)
+ ignore = b"ignore" in request.GET
+
+ if tag:
+ tag = b'"%s"' % tag
+
+ server_state = request.server.stash.take(token)
+ if not server_state:
+ server_state = []
+ state = dict()
+ if not ignore:
+ if inm:
+ state[u"If-None-Match"] = isomorphic_decode(inm)
+ if ims:
+ state[u"If-Modified-Since"] = isomorphic_decode(ims)
+ if pragma:
+ state[u"Pragma"] = isomorphic_decode(pragma)
+ if cache_control:
+ state[u"Cache-Control"] = isomorphic_decode(cache_control)
+ server_state.append(state)
+ request.server.stash.put(token, server_state)
+
+ if tag:
+ response.headers.set(b"ETag", b'%s' % tag)
+ elif date:
+ response.headers.set(b"Last-Modified", date)
+ if expires:
+ response.headers.set(b"Expires", expires)
+ if vary:
+ response.headers.set(b"Vary", vary)
+ if cc:
+ response.headers.set(b"Cache-Control", cc)
+
+ # The only-if-cached redirect tests wants CORS to be okay, the other tests
+ # are all same-origin anyways and don't care.
+ response.headers.set(b"Access-Control-Allow-Origin", b"*")
+
+ if redirect:
+ response.headers.set(b"Location", redirect)
+ response.status = (302, b"Redirect")
+ return b""
+ elif ((inm is not None and inm == tag) or
+ (ims is not None and ims == date)):
+ response.status = (304, b"Not Modified")
+ return b""
+ else:
+ response.status = (200, b"OK")
+ response.headers.set(b"Content-Type", b"text/plain")
+ return content
diff --git a/testing/web-platform/tests/fetch/api/request/resources/hello.txt b/testing/web-platform/tests/fetch/api/request/resources/hello.txt
new file mode 100644
index 0000000000..ce01362503
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/resources/hello.txt
@@ -0,0 +1 @@
+hello
diff --git a/testing/web-platform/tests/fetch/api/request/resources/request-reset-attributes-worker.js b/testing/web-platform/tests/fetch/api/request/resources/request-reset-attributes-worker.js
new file mode 100644
index 0000000000..4b264ca2fe
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/resources/request-reset-attributes-worker.js
@@ -0,0 +1,19 @@
+self.addEventListener('fetch', (event) => {
+ const params = new URL(event.request.url).searchParams;
+ if (params.has('ignore')) {
+ return;
+ }
+ if (!params.has('name')) {
+ event.respondWith(Promise.reject(TypeError('No name is provided.')));
+ return;
+ }
+
+ const name = params.get('name');
+ const old_attribute = event.request[name];
+ // If any of |init|'s member is present...
+ const init = {cache: 'no-store'}
+ const new_attribute = (new Request(event.request, init))[name];
+
+ event.respondWith(
+ new Response(`old: ${old_attribute}, new: ${new_attribute}`));
+ });
diff --git a/testing/web-platform/tests/fetch/api/request/url-encoding.html b/testing/web-platform/tests/fetch/api/request/url-encoding.html
new file mode 100644
index 0000000000..31c1ed3920
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/request/url-encoding.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<meta charset=windows-1252>
+<title>Fetch: URL encoding</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+const expectedURL = new URL("?%C3%9F", location.href).href;
+const expectedURL2 = new URL("?%EF%BF%BD", location.href).href;
+test(() => {
+ let r = new Request("?\u00DF");
+ assert_equals(r.url, expectedURL);
+
+ r = new Request("?\uD83D");
+ assert_equals(r.url, expectedURL2);
+}, "URL encoding and Request");
+
+promise_test(() => {
+ return fetch("?\u00DF").then(res => {
+ assert_equals(res.url, expectedURL);
+ return fetch("?\uD83D").then(res2 => {
+ assert_equals(res2.url, expectedURL2);
+ });
+ });
+}, "URL encoding and fetch()");
+</script>
diff --git a/testing/web-platform/tests/fetch/api/resources/authentication.py b/testing/web-platform/tests/fetch/api/resources/authentication.py
new file mode 100644
index 0000000000..8b6b00b087
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/authentication.py
@@ -0,0 +1,14 @@
+def main(request, response):
+ user = request.auth.username
+ password = request.auth.password
+
+ if user == b"user" and password == b"password":
+ return b"Authentication done"
+
+ realm = b"test"
+ if b"realm" in request.GET:
+ realm = request.GET.first(b"realm")
+
+ return ((401, b"Unauthorized"),
+ [(b"WWW-Authenticate", b'Basic realm="' + realm + b'"')],
+ b"Please login with credentials 'user' and 'password'")
diff --git a/testing/web-platform/tests/fetch/api/resources/bad-chunk-encoding.py b/testing/web-platform/tests/fetch/api/resources/bad-chunk-encoding.py
new file mode 100644
index 0000000000..94a77adead
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/bad-chunk-encoding.py
@@ -0,0 +1,13 @@
+import time
+
+def main(request, response):
+ delay = float(request.GET.first(b"ms", 1000)) / 1E3
+ count = int(request.GET.first(b"count", 50))
+ time.sleep(delay)
+ response.headers.set(b"Transfer-Encoding", b"chunked")
+ response.write_status_headers()
+ time.sleep(delay)
+ for i in range(count):
+ response.writer.write_content(b"a\r\nTEST_CHUNK\r\n")
+ time.sleep(delay)
+ response.writer.write_content(b"garbage")
diff --git a/testing/web-platform/tests/fetch/api/resources/basic.html b/testing/web-platform/tests/fetch/api/resources/basic.html
new file mode 100644
index 0000000000..e23afd4bf6
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/basic.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<!--
+ Duplicating /common/blank.html to make service worker scoping simpler in
+ ../abort/serviceworker-intercepted.https.html
+-->
diff --git a/testing/web-platform/tests/fetch/api/resources/cache.py b/testing/web-platform/tests/fetch/api/resources/cache.py
new file mode 100644
index 0000000000..4de751e30b
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/cache.py
@@ -0,0 +1,18 @@
+ETAG = b'"123abc"'
+CONTENT_TYPE = b"text/plain"
+CONTENT = b"lorem ipsum dolor sit amet"
+
+
+def main(request, response):
+ # let caching kick in if possible (conditional GET)
+ etag = request.headers.get(b"If-None-Match", None)
+ if etag == ETAG:
+ response.headers.set(b"X-HTTP-STATUS", 304)
+ response.status = (304, b"Not Modified")
+ return b""
+
+ # cache miss, so respond with the actual content
+ response.status = (200, b"OK")
+ response.headers.set(b"ETag", ETAG)
+ response.headers.set(b"Content-Type", CONTENT_TYPE)
+ return CONTENT
diff --git a/testing/web-platform/tests/fetch/api/resources/clean-stash.py b/testing/web-platform/tests/fetch/api/resources/clean-stash.py
new file mode 100644
index 0000000000..ee8c69ac44
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/clean-stash.py
@@ -0,0 +1,6 @@
+def main(request, response):
+ token = request.GET.first(b"token")
+ if request.server.stash.take(token) is not None:
+ return b"1"
+ else:
+ return b"0"
diff --git a/testing/web-platform/tests/fetch/api/resources/cors-top.txt b/testing/web-platform/tests/fetch/api/resources/cors-top.txt
new file mode 100644
index 0000000000..83a3157d14
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/cors-top.txt
@@ -0,0 +1 @@
+top \ No newline at end of file
diff --git a/testing/web-platform/tests/fetch/api/resources/cors-top.txt.headers b/testing/web-platform/tests/fetch/api/resources/cors-top.txt.headers
new file mode 100644
index 0000000000..cb762eff80
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/cors-top.txt.headers
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: *
diff --git a/testing/web-platform/tests/fetch/api/resources/data.json b/testing/web-platform/tests/fetch/api/resources/data.json
new file mode 100644
index 0000000000..76519fa8cc
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/data.json
@@ -0,0 +1 @@
+{"key": "value"}
diff --git a/testing/web-platform/tests/fetch/api/resources/dump-authorization-header.py b/testing/web-platform/tests/fetch/api/resources/dump-authorization-header.py
new file mode 100644
index 0000000000..a651aeb4e8
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/dump-authorization-header.py
@@ -0,0 +1,14 @@
+def main(request, response):
+ headers = [(b"Content-Type", "text/html"),
+ (b"Cache-Control", b"no-cache")]
+
+ if b"Origin" in request.headers:
+ headers.append((b"Access-Control-Allow-Origin", request.headers.get(b"Origin", b"")))
+ headers.append((b"Access-Control-Allow-Credentials", b"true"))
+ else:
+ headers.append((b"Access-Control-Allow-Origin", b"*"))
+ headers.append((b"Access-Control-Allow-Headers", b'Authorization'))
+
+ if b"authorization" in request.headers:
+ return 200, headers, request.headers.get(b"Authorization")
+ return 200, headers, "none"
diff --git a/testing/web-platform/tests/fetch/api/resources/echo-content.h2.py b/testing/web-platform/tests/fetch/api/resources/echo-content.h2.py
new file mode 100644
index 0000000000..0be3ece4a5
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/echo-content.h2.py
@@ -0,0 +1,7 @@
+def handle_headers(frame, request, response):
+ response.status = 200
+ response.headers.update([('Content-Type', 'text/plain')])
+ response.write_status_headers()
+
+def handle_data(frame, request, response):
+ response.writer.write_data(frame.data)
diff --git a/testing/web-platform/tests/fetch/api/resources/echo-content.py b/testing/web-platform/tests/fetch/api/resources/echo-content.py
new file mode 100644
index 0000000000..5e137e15d7
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/echo-content.py
@@ -0,0 +1,12 @@
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+
+ headers = [(b"X-Request-Method", isomorphic_encode(request.method)),
+ (b"X-Request-Content-Length", request.headers.get(b"Content-Length", b"NO")),
+ (b"X-Request-Content-Type", request.headers.get(b"Content-Type", b"NO")),
+ # Avoid any kind of content sniffing on the response.
+ (b"Content-Type", b"text/plain")]
+ content = request.body
+
+ return headers, content
diff --git a/testing/web-platform/tests/fetch/api/resources/empty.txt b/testing/web-platform/tests/fetch/api/resources/empty.txt
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/empty.txt
diff --git a/testing/web-platform/tests/fetch/api/resources/infinite-slow-response.py b/testing/web-platform/tests/fetch/api/resources/infinite-slow-response.py
new file mode 100644
index 0000000000..a26cd8064c
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/infinite-slow-response.py
@@ -0,0 +1,35 @@
+import time
+
+
+def url_dir(request):
+ return u'/'.join(request.url_parts.path.split(u'/')[:-1]) + u'/'
+
+
+def stash_write(request, key, value):
+ """Write to the stash, overwriting any previous value"""
+ request.server.stash.take(key, url_dir(request))
+ request.server.stash.put(key, value, url_dir(request))
+
+
+def main(request, response):
+ stateKey = request.GET.first(b"stateKey", b"")
+ abortKey = request.GET.first(b"abortKey", b"")
+
+ if stateKey:
+ stash_write(request, stateKey, 'open')
+
+ response.headers.set(b"Content-type", b"text/plain")
+ response.write_status_headers()
+
+ # Writing an initial 2k so browsers realise it's there. *shrug*
+ response.writer.write(b"." * 2048)
+
+ while True:
+ if not response.writer.write(b"."):
+ break
+ if abortKey and request.server.stash.take(abortKey, url_dir(request)):
+ break
+ time.sleep(0.01)
+
+ if stateKey:
+ stash_write(request, stateKey, 'closed')
diff --git a/testing/web-platform/tests/fetch/api/resources/inspect-headers.py b/testing/web-platform/tests/fetch/api/resources/inspect-headers.py
new file mode 100644
index 0000000000..9ed566e607
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/inspect-headers.py
@@ -0,0 +1,24 @@
+def main(request, response):
+ headers = []
+ if b"headers" in request.GET:
+ checked_headers = request.GET.first(b"headers").split(b"|")
+ for header in checked_headers:
+ if header in request.headers:
+ headers.append((b"x-request-" + header, request.headers.get(header, b"")))
+
+ if b"cors" in request.GET:
+ if b"Origin" in request.headers:
+ headers.append((b"Access-Control-Allow-Origin", request.headers.get(b"Origin", b"")))
+ else:
+ headers.append((b"Access-Control-Allow-Origin", b"*"))
+ headers.append((b"Access-Control-Allow-Credentials", b"true"))
+ headers.append((b"Access-Control-Allow-Methods", b"GET, POST, HEAD"))
+ exposed_headers = [b"x-request-" + header for header in checked_headers]
+ headers.append((b"Access-Control-Expose-Headers", b", ".join(exposed_headers)))
+ if b"allow_headers" in request.GET:
+ headers.append((b"Access-Control-Allow-Headers", request.GET[b'allow_headers']))
+ else:
+ headers.append((b"Access-Control-Allow-Headers", b", ".join(request.headers)))
+
+ headers.append((b"content-type", b"text/plain"))
+ return headers, b""
diff --git a/testing/web-platform/tests/fetch/api/resources/keepalive-helper.js b/testing/web-platform/tests/fetch/api/resources/keepalive-helper.js
new file mode 100644
index 0000000000..f6f511631e
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/keepalive-helper.js
@@ -0,0 +1,176 @@
+// Utility functions to help testing keepalive requests.
+
+// Returns a URL to an iframe that loads a keepalive URL on iframe loaded.
+//
+// The keepalive URL points to a target that stores `token`. The token will then
+// be posted back on iframe loaded to the parent document.
+// `method` defaults to GET.
+// `frameOrigin` to specify the origin of the iframe to load. If not set,
+// default to a different site origin.
+// `requestOrigin` to specify the origin of the fetch request target.
+// `sendOn` to specify the name of the event when the keepalive request should
+// be sent instead of the default 'load'.
+// `mode` to specify the fetch request's CORS mode.
+// `disallowCrossOrigin` to ask the iframe to set up a server that disallows
+// cross origin requests.
+function getKeepAliveIframeUrl(token, method, {
+ frameOrigin = 'DEFAULT',
+ requestOrigin = '',
+ sendOn = 'load',
+ mode = 'cors',
+ disallowCrossOrigin = false
+} = {}) {
+ const https = location.protocol.startsWith('https');
+ frameOrigin = frameOrigin === 'DEFAULT' ?
+ get_host_info()[https ? 'HTTPS_NOTSAMESITE_ORIGIN' : 'HTTP_NOTSAMESITE_ORIGIN'] :
+ frameOrigin;
+ return `${frameOrigin}/fetch/api/resources/keepalive-iframe.html?` +
+ `token=${token}&` +
+ `method=${method}&` +
+ `sendOn=${sendOn}&` +
+ `mode=${mode}&` + (disallowCrossOrigin ? `disallowCrossOrigin=1&` : ``) +
+ `origin=${requestOrigin}`;
+}
+
+// Returns a different-site URL to an iframe that loads a keepalive URL.
+//
+// By default, the keepalive URL points to a target that redirects to another
+// same-origin destination storing `token`. The token will then be posted back
+// to parent document.
+//
+// The URL redirects can be customized from `origin1` to `origin2` if provided.
+// Sets `withPreflight` to true to get URL enabling preflight.
+function getKeepAliveAndRedirectIframeUrl(
+ token, origin1, origin2, withPreflight) {
+ const https = location.protocol.startsWith('https');
+ const frameOrigin =
+ get_host_info()[https ? 'HTTPS_NOTSAMESITE_ORIGIN' : 'HTTP_NOTSAMESITE_ORIGIN'];
+ return `${frameOrigin}/fetch/api/resources/keepalive-redirect-iframe.html?` +
+ `token=${token}&` +
+ `origin1=${origin1}&` +
+ `origin2=${origin2}&` + (withPreflight ? `with-headers` : ``);
+}
+
+async function iframeLoaded(iframe) {
+ return new Promise((resolve) => iframe.addEventListener('load', resolve));
+}
+
+// Obtains the token from the message posted by iframe after loading
+// `getKeepAliveAndRedirectIframeUrl()`.
+async function getTokenFromMessage() {
+ return new Promise((resolve) => {
+ window.addEventListener('message', (event) => {
+ resolve(event.data);
+ }, {once: true});
+ });
+}
+
+// Tells if `token` has been stored in the server.
+async function queryToken(token) {
+ const response = await fetch(`../resources/stash-take.py?key=${token}`);
+ const json = await response.json();
+ return json;
+}
+
+// A helper to assert the existence of `token` that should have been stored in
+// the server by fetching ../resources/stash-put.py.
+//
+// This function simply wait for a custom amount of time before trying to
+// retrieve `token` from the server.
+// `expectTokenExist` tells if `token` should be present or not.
+//
+// NOTE:
+// In order to parallelize the work, we are going to have an async_test
+// for the rest of the work. Note that we want the serialized behavior
+// for the steps so far, so we don't want to make the entire test case
+// an async_test.
+function assertStashedTokenAsync(
+ testName, token, {expectTokenExist = true} = {}) {
+ async_test(test => {
+ new Promise(resolve => test.step_timeout(resolve, 3000 /*ms*/))
+ .then(test.step_func(() => {
+ return queryToken(token);
+ }))
+ .then(test.step_func(result => {
+ if (expectTokenExist) {
+ assert_equals(result, 'on', `token should be on (stashed).`);
+ test.done();
+ } else {
+ assert_not_equals(
+ result, 'on', `token should not be on (stashed).`);
+ return Promise.reject(`Failed to retrieve token from server`);
+ }
+ }))
+ .catch(test.step_func(e => {
+ if (expectTokenExist) {
+ test.unreached_func(e);
+ } else {
+ test.done();
+ }
+ }));
+ }, testName);
+}
+
+/**
+ * In an iframe, and in `load` event handler, test to fetch a keepalive URL that
+ * involves in redirect to another URL.
+ *
+ * `unloadIframe` to unload the iframe before verifying stashed token to
+ * simulate the situation that unloads after fetching. Note that this test is
+ * different from `keepaliveRedirectInUnloadTest()` in that the the latter
+ * performs fetch() call directly in `unload` event handler, while this test
+ * does it in `load`.
+ */
+function keepaliveRedirectTest(desc, {
+ origin1 = '',
+ origin2 = '',
+ withPreflight = false,
+ unloadIframe = false,
+ expectFetchSucceed = true,
+} = {}) {
+ desc = `[keepalive][iframe][load] ${desc}` +
+ (unloadIframe ? ' [unload at end]' : '');
+ promise_test(async (test) => {
+ const tokenToStash = token();
+ const iframe = document.createElement('iframe');
+ iframe.src = getKeepAliveAndRedirectIframeUrl(
+ tokenToStash, origin1, origin2, withPreflight);
+ document.body.appendChild(iframe);
+ await iframeLoaded(iframe);
+ assert_equals(await getTokenFromMessage(), tokenToStash);
+ if (unloadIframe) {
+ iframe.remove();
+ }
+
+ assertStashedTokenAsync(
+ desc, tokenToStash, {expectTokenExist: expectFetchSucceed});
+ }, `${desc}; setting up`);
+}
+
+/**
+ * Opens a different site window, and in `unload` event handler, test to fetch
+ * a keepalive URL that involves in redirect to another URL.
+ */
+function keepaliveRedirectInUnloadTest(desc, {
+ origin1 = '',
+ origin2 = '',
+ url2 = '',
+ withPreflight = false,
+ expectFetchSucceed = true
+} = {}) {
+ desc = `[keepalive][new window][unload] ${desc}`;
+
+ promise_test(async (test) => {
+ const targetUrl =
+ `${HTTP_NOTSAMESITE_ORIGIN}/fetch/api/resources/keepalive-redirect-window.html?` +
+ `origin1=${origin1}&` +
+ `origin2=${origin2}&` +
+ `url2=${url2}&` + (withPreflight ? `with-headers` : ``);
+ const w = window.open(targetUrl);
+ const token = await getTokenFromMessage();
+ w.close();
+
+ assertStashedTokenAsync(
+ desc, token, {expectTokenExist: expectFetchSucceed});
+ }, `${desc}; setting up`);
+}
diff --git a/testing/web-platform/tests/fetch/api/resources/keepalive-iframe.html b/testing/web-platform/tests/fetch/api/resources/keepalive-iframe.html
new file mode 100644
index 0000000000..f9dae5a34e
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/keepalive-iframe.html
@@ -0,0 +1,22 @@
+<!doctype html>
+<html>
+<meta charset="utf-8">
+<script>
+const SEARCH_PARAMS = new URL(location.href).searchParams;
+const ORIGIN = SEARCH_PARAMS.get('origin') || '';
+const FRAME_ORIGIN = new URL(location.href).origin;
+const TOKEN = SEARCH_PARAMS.get('token') || '';
+const METHOD = SEARCH_PARAMS.get('method') || 'GET';
+const SEND_ON_EVENT = SEARCH_PARAMS.get('sendOn') || 'load';
+const MODE = SEARCH_PARAMS.get('mode') || 'cors';
+const DISALLOW_CROSS_ORIGIN = SEARCH_PARAMS.get('disallowCrossOrigin') || '';
+// CORS requests are allowed by this URL by default.
+const url = `${ORIGIN}/fetch/api/resources/stash-put.py?key=${TOKEN}&value=on&mode=${MODE}`
+ + `&frame_origin=${FRAME_ORIGIN}` + (DISALLOW_CROSS_ORIGIN ? `&disallow_cross_origin=1` : '');
+
+addEventListener(SEND_ON_EVENT, () => {
+ let p = fetch(url, {keepalive: true, method: METHOD, mode: MODE});
+ window.parent.postMessage(TOKEN, '*');
+});
+</script>
+</html>
diff --git a/testing/web-platform/tests/fetch/api/resources/keepalive-redirect-iframe.html b/testing/web-platform/tests/fetch/api/resources/keepalive-redirect-iframe.html
new file mode 100644
index 0000000000..fdee00f312
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/keepalive-redirect-iframe.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+<meta charset="utf-8">
+<script>
+const SEARCH_PARAMS = new URL(location.href).searchParams;
+const ORIGIN1 = SEARCH_PARAMS.get('origin1') || '';
+const ORIGIN2 = SEARCH_PARAMS.get('origin2') || '';
+const WITH_HEADERS = !!SEARCH_PARAMS.has('with-headers');
+const TOKEN = SEARCH_PARAMS.get('token') || '';
+
+const url =
+ `${ORIGIN1}/fetch/api/resources/redirect.py?` +
+ `delay=500&` +
+ `allow_headers=foo&` +
+ `location=${ORIGIN2}/fetch/api/resources/stash-put.py?key=${TOKEN}%26value=on`;
+
+addEventListener('load', () => {
+ const headers = WITH_HEADERS ? {'foo': 'bar'} : undefined;
+ let p = fetch(url, {keepalive: true, headers});
+ window.parent.postMessage(TOKEN, '*');
+});
+</script>
+</html>
diff --git a/testing/web-platform/tests/fetch/api/resources/keepalive-redirect-window.html b/testing/web-platform/tests/fetch/api/resources/keepalive-redirect-window.html
new file mode 100644
index 0000000000..c18650796c
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/keepalive-redirect-window.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<html>
+<meta charset="utf-8">
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script>
+const TOKEN = token();
+const {
+ HTTP_NOTSAMESITE_ORIGIN,
+ HTTP_REMOTE_ORIGIN,
+ HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT
+} = get_host_info();
+
+const SEARCH_PARAMS = new URL(location.href).searchParams;
+const WITH_HEADERS = !!SEARCH_PARAMS.has('with-headers');
+const ORIGIN1 = SEARCH_PARAMS.get('origin1') || '';
+const ORIGIN2 = SEARCH_PARAMS.get('origin2') || '';
+const URL2 = SEARCH_PARAMS.get('url2') || '';
+
+const REDIRECT_DESTINATION = URL2 ? URL2 :
+ `${ORIGIN2}/fetch/api/resources/stash-put.py` +
+ `?key=${TOKEN}&value=on`;
+const FROM_URL =
+ `${ORIGIN1}/fetch/api/resources/redirect.py?` +
+ `delay=500&` +
+ `allow_headers=foo&` +
+ `location=${encodeURIComponent(REDIRECT_DESTINATION)}`;
+
+addEventListener('load', () => {
+ const headers = WITH_HEADERS ? {'foo': 'bar'} : undefined;
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ iframe.contentWindow.addEventListener('unload', () => {
+ iframe.contentWindow.fetch(FROM_URL, {keepalive: true, headers});
+ });
+
+ window.opener.postMessage(TOKEN, '*');
+ // Do NOT remove `iframe` here. We want to check the case where the nested
+ // frame is implicitly closed by window closure.
+});
+</script>
+</html>
diff --git a/testing/web-platform/tests/fetch/api/resources/method.py b/testing/web-platform/tests/fetch/api/resources/method.py
new file mode 100644
index 0000000000..c1a111b4cd
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/method.py
@@ -0,0 +1,18 @@
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ headers = []
+ if b"cors" in request.GET:
+ headers.append((b"Access-Control-Allow-Origin", b"*"))
+ headers.append((b"Access-Control-Allow-Credentials", b"true"))
+ headers.append((b"Access-Control-Allow-Methods", b"GET, POST, PUT, FOO"))
+ headers.append((b"Access-Control-Allow-Headers", b"x-test, x-foo"))
+ headers.append((b"Access-Control-Expose-Headers", b"x-request-method"))
+
+ headers.append((b"x-request-method", isomorphic_encode(request.method)))
+ headers.append((b"x-request-content-type", request.headers.get(b"Content-Type", b"NO")))
+ headers.append((b"x-request-content-length", request.headers.get(b"Content-Length", b"NO")))
+ headers.append((b"x-request-content-encoding", request.headers.get(b"Content-Encoding", b"NO")))
+ headers.append((b"x-request-content-language", request.headers.get(b"Content-Language", b"NO")))
+ headers.append((b"x-request-content-location", request.headers.get(b"Content-Location", b"NO")))
+ return headers, request.body
diff --git a/testing/web-platform/tests/fetch/api/resources/preflight.py b/testing/web-platform/tests/fetch/api/resources/preflight.py
new file mode 100644
index 0000000000..f983ef9522
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/preflight.py
@@ -0,0 +1,78 @@
+def main(request, response):
+ headers = [(b"Content-Type", b"text/plain")]
+ stashed_data = {b'control_request_headers': b"", b'preflight': b"0", b'preflight_referrer': b""}
+
+ token = None
+ if b"token" in request.GET:
+ token = request.GET.first(b"token")
+
+ if b"origin" in request.GET:
+ for origin in request.GET[b'origin'].split(b", "):
+ headers.append((b"Access-Control-Allow-Origin", origin))
+ else:
+ headers.append((b"Access-Control-Allow-Origin", b"*"))
+
+ if b"clear-stash" in request.GET:
+ if request.server.stash.take(token) is not None:
+ return headers, b"1"
+ else:
+ return headers, b"0"
+
+ if b"credentials" in request.GET:
+ headers.append((b"Access-Control-Allow-Credentials", b"true"))
+
+ if request.method == u"OPTIONS":
+ if not b"Access-Control-Request-Method" in request.headers:
+ response.set_error(400, u"No Access-Control-Request-Method header")
+ return b"ERROR: No access-control-request-method in preflight!"
+
+ if request.headers.get(b"Accept", b"") != b"*/*":
+ response.set_error(400, u"Request does not have 'Accept: */*' header")
+ return b"ERROR: Invalid access in preflight!"
+
+ if b"control_request_headers" in request.GET:
+ stashed_data[b'control_request_headers'] = request.headers.get(b"Access-Control-Request-Headers", None)
+
+ if b"max_age" in request.GET:
+ headers.append((b"Access-Control-Max-Age", request.GET[b'max_age']))
+
+ if b"allow_headers" in request.GET:
+ headers.append((b"Access-Control-Allow-Headers", request.GET[b'allow_headers']))
+
+ if b"allow_methods" in request.GET:
+ headers.append((b"Access-Control-Allow-Methods", request.GET[b'allow_methods']))
+
+ preflight_status = 200
+ if b"preflight_status" in request.GET:
+ preflight_status = int(request.GET.first(b"preflight_status"))
+
+ stashed_data[b'preflight'] = b"1"
+ stashed_data[b'preflight_referrer'] = request.headers.get(b"Referer", b"")
+ stashed_data[b'preflight_user_agent'] = request.headers.get(b"User-Agent", b"")
+ if token:
+ request.server.stash.put(token, stashed_data)
+
+ return preflight_status, headers, b""
+
+
+ if token:
+ data = request.server.stash.take(token)
+ if data:
+ stashed_data = data
+
+ if b"checkUserAgentHeaderInPreflight" in request.GET and request.headers.get(b"User-Agent") != stashed_data[b'preflight_user_agent']:
+ return 400, headers, b"ERROR: No user-agent header in preflight"
+
+ #use x-* headers for returning value to bodyless responses
+ headers.append((b"Access-Control-Expose-Headers", b"x-did-preflight, x-control-request-headers, x-referrer, x-preflight-referrer, x-origin"))
+ headers.append((b"x-did-preflight", stashed_data[b'preflight']))
+ if stashed_data[b'control_request_headers'] != None:
+ headers.append((b"x-control-request-headers", stashed_data[b'control_request_headers']))
+ headers.append((b"x-preflight-referrer", stashed_data[b'preflight_referrer']))
+ headers.append((b"x-referrer", request.headers.get(b"Referer", b"")))
+ headers.append((b"x-origin", request.headers.get(b"Origin", b"")))
+
+ if token:
+ request.server.stash.put(token, stashed_data)
+
+ return headers, b""
diff --git a/testing/web-platform/tests/fetch/api/resources/redirect-empty-location.py b/testing/web-platform/tests/fetch/api/resources/redirect-empty-location.py
new file mode 100644
index 0000000000..1a5f7feb2a
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/redirect-empty-location.py
@@ -0,0 +1,3 @@
+def main(request, response):
+ headers = [(b"Location", b"")]
+ return 302, headers, b""
diff --git a/testing/web-platform/tests/fetch/api/resources/redirect.h2.py b/testing/web-platform/tests/fetch/api/resources/redirect.h2.py
new file mode 100644
index 0000000000..6937014587
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/redirect.h2.py
@@ -0,0 +1,14 @@
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+def handle_headers(frame, request, response):
+ status = 302
+ if b'redirect_status' in request.GET:
+ status = int(request.GET[b'redirect_status'])
+ response.status = status
+
+ if b'location' in request.GET:
+ url = isomorphic_decode(request.GET[b'location'])
+ response.headers[b'Location'] = isomorphic_encode(url)
+
+ response.headers.update([('Content-Type', 'text/plain')])
+ response.write_status_headers()
diff --git a/testing/web-platform/tests/fetch/api/resources/redirect.py b/testing/web-platform/tests/fetch/api/resources/redirect.py
new file mode 100644
index 0000000000..d52ab5f3ee
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/redirect.py
@@ -0,0 +1,73 @@
+import time
+
+from urllib.parse import urlencode, urlparse
+
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+def main(request, response):
+ stashed_data = {b'count': 0, b'preflight': b"0"}
+ status = 302
+ headers = [(b"Content-Type", b"text/plain"),
+ (b"Cache-Control", b"no-cache"),
+ (b"Pragma", b"no-cache")]
+ if b"Origin" in request.headers:
+ headers.append((b"Access-Control-Allow-Origin", request.headers.get(b"Origin", b"")))
+ headers.append((b"Access-Control-Allow-Credentials", b"true"))
+ else:
+ headers.append((b"Access-Control-Allow-Origin", b"*"))
+
+ token = None
+ if b"token" in request.GET:
+ token = request.GET.first(b"token")
+ data = request.server.stash.take(token)
+ if data:
+ stashed_data = data
+
+ if request.method == u"OPTIONS":
+ if b"allow_headers" in request.GET:
+ headers.append((b"Access-Control-Allow-Headers", request.GET[b'allow_headers']))
+ stashed_data[b'preflight'] = b"1"
+ #Preflight is not redirected: return 200
+ if not b"redirect_preflight" in request.GET:
+ if token:
+ request.server.stash.put(request.GET.first(b"token"), stashed_data)
+ return 200, headers, u""
+
+ if b"redirect_status" in request.GET:
+ status = int(request.GET[b'redirect_status'])
+ elif b"redirect_status" in request.POST:
+ status = int(request.POST[b'redirect_status'])
+
+ stashed_data[b'count'] += 1
+
+ if b"location" in request.GET:
+ url = isomorphic_decode(request.GET[b'location'])
+ if b"simple" not in request.GET:
+ scheme = urlparse(url).scheme
+ if scheme == u"" or scheme == u"http" or scheme == u"https":
+ url += u"&" if u'?' in url else u"?"
+ #keep url parameters in location
+ url_parameters = {}
+ for item in request.GET.items():
+ url_parameters[isomorphic_decode(item[0])] = isomorphic_decode(item[1][0])
+ url += urlencode(url_parameters)
+ #make sure location changes during redirection loop
+ url += u"&count=" + str(stashed_data[b'count'])
+ headers.append((b"Location", isomorphic_encode(url)))
+
+ if b"redirect_referrerpolicy" in request.GET:
+ headers.append((b"Referrer-Policy", request.GET[b'redirect_referrerpolicy']))
+
+ if b"delay" in request.GET:
+ time.sleep(float(request.GET.first(b"delay", 0)) / 1E3)
+
+ if token:
+ request.server.stash.put(request.GET.first(b"token"), stashed_data)
+ if b"max_count" in request.GET:
+ max_count = int(request.GET[b'max_count'])
+ #stop redirecting and return count
+ if stashed_data[b'count'] > max_count:
+ # -1 because the last is not a redirection
+ return str(stashed_data[b'count'] - 1)
+
+ return status, headers, u""
diff --git a/testing/web-platform/tests/fetch/api/resources/sandboxed-iframe.html b/testing/web-platform/tests/fetch/api/resources/sandboxed-iframe.html
new file mode 100644
index 0000000000..6e5d506547
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/sandboxed-iframe.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<html>
+<script>
+async function no_cors_should_be_rejected() {
+ let thrown = false;
+ try {
+ const resp = await fetch('top.txt');
+ } catch (e) {
+ thrown = true;
+ }
+ if (!thrown) {
+ throw Error('fetching "top.txt" should be rejected.');
+ }
+}
+
+async function null_origin_should_be_accepted() {
+ const url = 'top.txt?pipe=header(access-control-allow-origin,null)|' +
+ 'header(cache-control,no-store)';
+ const resp = await fetch(url);
+}
+
+async function test() {
+ try {
+ await no_cors_should_be_rejected();
+ await null_origin_should_be_accepted();
+ parent.postMessage('PASS', '*');
+ } catch (e) {
+ parent.postMessage(e.message, '*');
+ }
+}
+
+test();
+</script>
+</html>
diff --git a/testing/web-platform/tests/fetch/api/resources/script-with-header.py b/testing/web-platform/tests/fetch/api/resources/script-with-header.py
new file mode 100644
index 0000000000..9a9c70ef5c
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/script-with-header.py
@@ -0,0 +1,7 @@
+def main(request, response):
+ headers = [(b"Content-type", request.GET.first(b"mime"))]
+ if b"content" in request.GET and request.GET.first(b"content") == b"empty":
+ content = b''
+ else:
+ content = b"console.log('Script loaded')"
+ return 200, headers, content
diff --git a/testing/web-platform/tests/fetch/api/resources/stash-put.py b/testing/web-platform/tests/fetch/api/resources/stash-put.py
new file mode 100644
index 0000000000..91c198abb7
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/stash-put.py
@@ -0,0 +1,41 @@
+from wptserve.utils import isomorphic_decode
+
+def should_be_treated_as_same_origin_request(request):
+ """Tells whether request should be treated as same-origin request."""
+ # In both of the following cases, allow to proceed with handling to simulate
+ # 'no-cors' mode: response is sent, but browser will make it opaque.
+ if request.GET.first(b'mode') == b'no-cors':
+ return True
+
+ # We can't rely on the Origin header field of a fetch request, as it is only
+ # present for 'cors' mode or methods other than 'GET'/'HEAD' (i.e. present for
+ # 'POST'). See https://fetch.spec.whatwg.org/#http-origin
+ assert 'frame_origin ' in request.GET
+ frame_origin = request.GET.first(b'frame_origin').decode('utf-8')
+ host_origin = request.url_parts.scheme + '://' + request.url_parts.netloc
+ return frame_origin == host_origin
+
+def main(request, response):
+ if request.method == u'OPTIONS':
+ # CORS preflight
+ response.headers.set(b'Access-Control-Allow-Origin', b'*')
+ response.headers.set(b'Access-Control-Allow-Methods', b'*')
+ response.headers.set(b'Access-Control-Allow-Headers', b'*')
+ return 'done'
+
+ if b'disallow_cross_origin' not in request.GET:
+ response.headers.set(b'Access-Control-Allow-Origin', b'*')
+ elif not should_be_treated_as_same_origin_request(request):
+ # As simple requests will not trigger preflight, we have to manually block
+ # cors requests before making any changes to storage.
+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests
+ # https://fetch.spec.whatwg.org/#cors-preflight-fetch
+ return 'not stashing for cors request'
+
+ url_dir = u'/'.join(request.url_parts.path.split(u'/')[:-1]) + u'/'
+ key = request.GET.first(b'key')
+ value = request.GET.first(b'value')
+ # value here must be a text string. It will be json.dump()'ed in stash-take.py.
+ request.server.stash.put(key, isomorphic_decode(value), url_dir)
+
+ return 'done'
diff --git a/testing/web-platform/tests/fetch/api/resources/stash-take.py b/testing/web-platform/tests/fetch/api/resources/stash-take.py
new file mode 100644
index 0000000000..e6db80dd86
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/stash-take.py
@@ -0,0 +1,9 @@
+from wptserve.handlers import json_handler
+
+
+@json_handler
+def main(request, response):
+ dir = u'/'.join(request.url_parts.path.split(u'/')[:-1]) + u'/'
+ key = request.GET.first(b"key")
+ response.headers.set(b'Access-Control-Allow-Origin', b'*')
+ return request.server.stash.take(key, dir)
diff --git a/testing/web-platform/tests/fetch/api/resources/status.py b/testing/web-platform/tests/fetch/api/resources/status.py
new file mode 100644
index 0000000000..05a59d5a63
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/status.py
@@ -0,0 +1,11 @@
+from wptserve.utils import isomorphic_encode
+
+def main(request, response):
+ code = int(request.GET.first(b"code", 200))
+ text = request.GET.first(b"text", b"OMG")
+ content = request.GET.first(b"content", b"")
+ type = request.GET.first(b"type", b"")
+ status = (code, text)
+ headers = [(b"Content-Type", type),
+ (b"X-Request-Method", isomorphic_encode(request.method))]
+ return status, headers, content
diff --git a/testing/web-platform/tests/fetch/api/resources/sw-intercept-abort.js b/testing/web-platform/tests/fetch/api/resources/sw-intercept-abort.js
new file mode 100644
index 0000000000..19d4b189d8
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/sw-intercept-abort.js
@@ -0,0 +1,19 @@
+async function messageClient(clientId, message) {
+ const client = await clients.get(clientId);
+ client.postMessage(message);
+}
+
+addEventListener('fetch', event => {
+ let resolve;
+ const promise = new Promise(r => resolve = r);
+
+ function onAborted() {
+ messageClient(event.clientId, event.request.signal.reason);
+ resolve();
+ }
+
+ messageClient(event.clientId, 'fetch event has arrived');
+
+ event.respondWith(promise.then(() => new Response('hello')));
+ event.request.signal.addEventListener('abort', onAborted);
+});
diff --git a/testing/web-platform/tests/fetch/api/resources/sw-intercept.js b/testing/web-platform/tests/fetch/api/resources/sw-intercept.js
new file mode 100644
index 0000000000..b8166b62a5
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/sw-intercept.js
@@ -0,0 +1,10 @@
+async function broadcast(msg) {
+ for (const client of await clients.matchAll()) {
+ client.postMessage(msg);
+ }
+}
+
+addEventListener('fetch', event => {
+ event.waitUntil(broadcast(event.request.url));
+ event.respondWith(fetch(event.request));
+});
diff --git a/testing/web-platform/tests/fetch/api/resources/top.txt b/testing/web-platform/tests/fetch/api/resources/top.txt
new file mode 100644
index 0000000000..83a3157d14
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/top.txt
@@ -0,0 +1 @@
+top \ No newline at end of file
diff --git a/testing/web-platform/tests/fetch/api/resources/trickle.py b/testing/web-platform/tests/fetch/api/resources/trickle.py
new file mode 100644
index 0000000000..99833f1b38
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/trickle.py
@@ -0,0 +1,15 @@
+import time
+
+def main(request, response):
+ delay = float(request.GET.first(b"ms", 500)) / 1E3
+ count = int(request.GET.first(b"count", 50))
+ # Read request body
+ request.body
+ time.sleep(delay)
+ if not b"notype" in request.GET:
+ response.headers.set(b"Content-type", b"text/plain")
+ response.write_status_headers()
+ time.sleep(delay)
+ for i in range(count):
+ response.writer.write_content(b"TEST_TRICKLE\n")
+ time.sleep(delay)
diff --git a/testing/web-platform/tests/fetch/api/resources/utils.js b/testing/web-platform/tests/fetch/api/resources/utils.js
new file mode 100644
index 0000000000..3721d9bf9c
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/resources/utils.js
@@ -0,0 +1,120 @@
+var RESOURCES_DIR = "../resources/";
+
+function dirname(path) {
+ return path.replace(/\/[^\/]*$/, '/')
+}
+
+function checkRequest(request, ExpectedValuesDict) {
+ for (var attribute in ExpectedValuesDict) {
+ switch(attribute) {
+ case "headers":
+ for (var key in ExpectedValuesDict["headers"].keys()) {
+ assert_equals(request["headers"].get(key), ExpectedValuesDict["headers"].get(key),
+ "Check headers attribute has " + key + ":" + ExpectedValuesDict["headers"].get(key));
+ }
+ break;
+
+ case "body":
+ //for checking body's content, a dedicated asyncronous/promise test should be used
+ assert_true(request["headers"].has("Content-Type") , "Check request has body using Content-Type header")
+ break;
+
+ case "method":
+ case "referrer":
+ case "referrerPolicy":
+ case "credentials":
+ case "cache":
+ case "redirect":
+ case "integrity":
+ case "url":
+ case "destination":
+ assert_equals(request[attribute], ExpectedValuesDict[attribute], "Check " + attribute + " attribute")
+ break;
+
+ default:
+ break;
+ }
+ }
+}
+
+function stringToArray(str) {
+ var array = new Uint8Array(str.length);
+ for (var i=0, strLen = str.length; i < strLen; i++)
+ array[i] = str.charCodeAt(i);
+ return array;
+}
+
+function encode_utf8(str)
+{
+ if (self.TextEncoder)
+ return (new TextEncoder).encode(str);
+ return stringToArray(unescape(encodeURIComponent(str)));
+}
+
+function validateBufferFromString(buffer, expectedValue, message)
+{
+ return assert_array_equals(new Uint8Array(buffer !== undefined ? buffer : []), stringToArray(expectedValue), message);
+}
+
+function validateStreamFromString(reader, expectedValue, retrievedArrayBuffer) {
+ // Passing Uint8Array for byte streams; non-byte streams will simply ignore it
+ return reader.read(new Uint8Array(64)).then(function(data) {
+ if (!data.done) {
+ assert_true(data.value instanceof Uint8Array, "Fetch ReadableStream chunks should be Uint8Array");
+ var newBuffer;
+ if (retrievedArrayBuffer) {
+ newBuffer = new Uint8Array(data.value.length + retrievedArrayBuffer.length);
+ newBuffer.set(retrievedArrayBuffer, 0);
+ newBuffer.set(data.value, retrievedArrayBuffer.length);
+ } else {
+ newBuffer = data.value;
+ }
+ return validateStreamFromString(reader, expectedValue, newBuffer);
+ }
+ validateBufferFromString(retrievedArrayBuffer, expectedValue, "Retrieve and verify stream");
+ });
+}
+
+function validateStreamFromPartialString(reader, expectedValue, retrievedArrayBuffer) {
+ // Passing Uint8Array for byte streams; non-byte streams will simply ignore it
+ return reader.read(new Uint8Array(64)).then(function(data) {
+ if (!data.done) {
+ assert_true(data.value instanceof Uint8Array, "Fetch ReadableStream chunks should be Uint8Array");
+ var newBuffer;
+ if (retrievedArrayBuffer) {
+ newBuffer = new Uint8Array(data.value.length + retrievedArrayBuffer.length);
+ newBuffer.set(retrievedArrayBuffer, 0);
+ newBuffer.set(data.value, retrievedArrayBuffer.length);
+ } else {
+ newBuffer = data.value;
+ }
+ return validateStreamFromPartialString(reader, expectedValue, newBuffer);
+ }
+
+ var string = new TextDecoder("utf-8").decode(retrievedArrayBuffer);
+ return assert_true(string.search(expectedValue) != -1, "Retrieve and verify stream");
+ });
+}
+
+// From streams tests
+function delay(milliseconds)
+{
+ return new Promise(function(resolve) {
+ step_timeout(resolve, milliseconds);
+ });
+}
+
+function requestForbiddenHeaders(desc, forbiddenHeaders) {
+ var url = RESOURCES_DIR + "inspect-headers.py";
+ var requestInit = {"headers": forbiddenHeaders}
+ var urlParameters = "?headers=" + Object.keys(forbiddenHeaders).join("|");
+
+ promise_test(function(test){
+ return fetch(url + urlParameters, requestInit).then(function(resp) {
+ assert_equals(resp.status, 200, "HTTP status is 200");
+ assert_equals(resp.type , "basic", "Response's type is basic");
+ for (var header in forbiddenHeaders)
+ assert_not_equals(resp.headers.get("x-request-" + header), forbiddenHeaders[header], header + " does not have the value we defined");
+ });
+ }, desc);
+}
diff --git a/testing/web-platform/tests/fetch/api/response/json.any.js b/testing/web-platform/tests/fetch/api/response/json.any.js
new file mode 100644
index 0000000000..15f050e632
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/json.any.js
@@ -0,0 +1,14 @@
+// See also /xhr/json.any.js
+
+promise_test(async t => {
+ const response = await fetch(`data:,\uFEFF{ "b": 1, "a": 2, "b": 3 }`);
+ const json = await response.json();
+ assert_array_equals(Object.keys(json), ["b", "a"]);
+ assert_equals(json.a, 2);
+ assert_equals(json.b, 3);
+}, "Ensure the correct JSON parser is used");
+
+promise_test(async t => {
+ const response = await fetch("/xhr/resources/utf16-bom.json");
+ return promise_rejects_js(t, SyntaxError, response.json());
+}, "Ensure UTF-16 results in an error");
diff --git a/testing/web-platform/tests/fetch/api/response/many-empty-chunks-crash.html b/testing/web-platform/tests/fetch/api/response/many-empty-chunks-crash.html
new file mode 100644
index 0000000000..fe5e7d4c07
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/many-empty-chunks-crash.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script>
+ new Response(new ReadableStream({
+ start(c) {
+
+ for (const i of new Array(40000).fill()) {
+ c.enqueue(new Uint8Array(0));
+ }
+ c.close();
+
+ }
+ })).text();
+</script>
diff --git a/testing/web-platform/tests/fetch/api/response/multi-globals/current/current.html b/testing/web-platform/tests/fetch/api/response/multi-globals/current/current.html
new file mode 100644
index 0000000000..9bb6e0bbf3
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/multi-globals/current/current.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<title>Current page used as a test helper</title>
+<base href="success/">
diff --git a/testing/web-platform/tests/fetch/api/response/multi-globals/incumbent/incumbent.html b/testing/web-platform/tests/fetch/api/response/multi-globals/incumbent/incumbent.html
new file mode 100644
index 0000000000..f63372e64c
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/multi-globals/incumbent/incumbent.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<title>Incumbent page used as a test helper</title>
+
+<iframe src="../current/current.html" id="c"></iframe>
+<iframe src="../relevant/relevant.html" id="r"></iframe>
+
+<script>
+'use strict';
+
+window.createRedirectResponse = (...args) => {
+ const current = document.querySelector('#c').contentWindow;
+ const relevant = document.querySelector('#r').contentWindow;
+ return current.Response.redirect.call(relevant.Response, ...args);
+};
+
+</script>
diff --git a/testing/web-platform/tests/fetch/api/response/multi-globals/relevant/relevant.html b/testing/web-platform/tests/fetch/api/response/multi-globals/relevant/relevant.html
new file mode 100644
index 0000000000..44f42eda49
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/multi-globals/relevant/relevant.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<title>Relevant page used as a test helper</title>
diff --git a/testing/web-platform/tests/fetch/api/response/multi-globals/url-parsing.html b/testing/web-platform/tests/fetch/api/response/multi-globals/url-parsing.html
new file mode 100644
index 0000000000..5f2f42a1ce
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/multi-globals/url-parsing.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<title>Response.redirect URL parsing, with multiple globals in play</title>
+<link rel="help" href="https://fetch.spec.whatwg.org/#dom-response-redirect">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!-- This is the entry global -->
+
+<iframe src="incumbent/incumbent.html"></iframe>
+
+<script>
+'use strict';
+
+const loadPromise = new Promise(resolve => {
+ window.addEventListener("load", () => resolve());
+});
+
+promise_test(() => {
+ return loadPromise.then(() => {
+ const res = document.querySelector('iframe').contentWindow.createRedirectResponse("url");
+
+ assert_equals(res.headers.get("Location"), new URL("current/success/url", location.href).href);
+ });
+}, "should parse the redirect Location URL relative to the current settings object");
+
+</script>
diff --git a/testing/web-platform/tests/fetch/api/response/response-body-read-task-handling.html b/testing/web-platform/tests/fetch/api/response/response-body-read-task-handling.html
new file mode 100644
index 0000000000..64b0755666
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-body-read-task-handling.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title></title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+ <script>
+function performMicrotaskCheckpoint() {
+ document.createNodeIterator(document, -1, {
+ acceptNode() {
+ return NodeFilter.FILTER_ACCEPT;
+ }
+ }).nextNode();
+}
+
+promise_test(function() {
+ return fetch("../resources/data.json").then(function(response) {
+ // Add a getter for "then" that will incidentally be invoked
+ // during promise resolution.
+ Object.prototype.__defineGetter__('then', () => {
+ // Clean up behind ourselves.
+ delete Object.prototype.then;
+
+ // This promise should (like all promises) be resolved
+ // asynchronously.
+ var executed = false;
+ Promise.resolve().then(_ => { executed = true; });
+
+ // This shouldn't run microtasks! They should only run
+ // after the fetch is resolved.
+ performMicrotaskCheckpoint();
+
+ // The fulfill handler above shouldn't have run yet. If it has run,
+ // throw to reject this promise and fail the test.
+ assert_false(executed, "shouldn't have run microtasks yet");
+
+ // Otherwise act as if there's no "then" property so the promise
+ // fulfills and the test passes.
+ return undefined;
+ });
+
+ // Create a read request, incidentally resolving a promise with an
+ // object value, thereby invoking the getter installed above.
+ return response.body.getReader().read();
+ });
+}, "reading from a body stream should occur in a microtask scope");
+
+promise_test(function() {
+ return fetch("../resources/data.json").then(function(response) {
+ // Add a getter for "then" that will incidentally be invoked
+ // during promise resolution.
+ Object.prototype.__defineGetter__('then', () => {
+ // Clean up behind ourselves.
+ delete Object.prototype.then;
+
+ // This promise should (like all promises) be resolved
+ // asynchronously.
+ var executed = false;
+ Promise.resolve().then(_ => { executed = true; });
+
+ // This shouldn't run microtasks! They should only run
+ // after the fetch is resolved.
+ performMicrotaskCheckpoint();
+
+ // The fulfill handler above shouldn't have run yet. If it has run,
+ // throw to reject this promise and fail the test.
+ assert_false(executed, "shouldn't have run microtasks yet");
+
+ // Otherwise act as if there's no "then" property so the promise
+ // fulfills and the test passes.
+ return undefined;
+ });
+
+ // Create a read request, incidentally resolving a promise with an
+ // object value, thereby invoking the getter installed above.
+ return response.body.pipeTo(new WritableStream({
+ write(chunk) {}
+ }))
+ });
+}, "piping from a body stream to a JS-written WritableStream should occur in a microtask scope");
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/fetch/api/response/response-cancel-stream.any.js b/testing/web-platform/tests/fetch/api/response/response-cancel-stream.any.js
new file mode 100644
index 0000000000..91140d1afd
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-cancel-stream.any.js
@@ -0,0 +1,64 @@
+// META: global=window,worker
+// META: title=Response consume blob and http bodies
+// META: script=../resources/utils.js
+
+promise_test(function(test) {
+ return new Response(new Blob([], { "type" : "text/plain" })).body.cancel();
+}, "Cancelling a starting blob Response stream");
+
+promise_test(function(test) {
+ var response = new Response(new Blob(["This is data"], { "type" : "text/plain" }));
+ var reader = response.body.getReader();
+ reader.read();
+ return reader.cancel();
+}, "Cancelling a loading blob Response stream");
+
+promise_test(function(test) {
+ var response = new Response(new Blob(["T"], { "type" : "text/plain" }));
+ var reader = response.body.getReader();
+
+ var closedPromise = reader.closed.then(function() {
+ return reader.cancel();
+ });
+ reader.read().then(function readMore({done, value}) {
+ if (!done) return reader.read().then(readMore);
+ });
+ return closedPromise;
+}, "Cancelling a closed blob Response stream");
+
+promise_test(function(test) {
+ return fetch(RESOURCES_DIR + "trickle.py?ms=30&count=100").then(function(response) {
+ return response.body.cancel();
+ });
+}, "Cancelling a starting Response stream");
+
+promise_test(function() {
+ return fetch(RESOURCES_DIR + "trickle.py?ms=30&count=100").then(function(response) {
+ var reader = response.body.getReader();
+ return reader.read().then(function() {
+ return reader.cancel();
+ });
+ });
+}, "Cancelling a loading Response stream");
+
+promise_test(function() {
+ async function readAll(reader) {
+ while (true) {
+ const {value, done} = await reader.read();
+ if (done)
+ return;
+ }
+ }
+
+ return fetch(RESOURCES_DIR + "top.txt").then(function(response) {
+ var reader = response.body.getReader();
+ return readAll(reader).then(() => reader.cancel());
+ });
+}, "Cancelling a closed Response stream");
+
+promise_test(async () => {
+ const response = await fetch(RESOURCES_DIR + "top.txt");
+ const { body } = response;
+ await body.cancel();
+ assert_equals(body, response.body, ".body should not change after cancellation");
+}, "Accessing .body after canceling it");
diff --git a/testing/web-platform/tests/fetch/api/response/response-clone-iframe.window.js b/testing/web-platform/tests/fetch/api/response/response-clone-iframe.window.js
new file mode 100644
index 0000000000..da54616c37
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-clone-iframe.window.js
@@ -0,0 +1,32 @@
+// Verify that calling Response clone() in a detached iframe doesn't crash.
+// Regression test for https://crbug.com/1082688.
+
+'use strict';
+
+promise_test(async () => {
+ // Wait for the document body to be available.
+ await new Promise(resolve => {
+ onload = resolve;
+ });
+
+ window.iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ iframe.srcdoc = `<!doctype html>
+<script>
+const response = new Response('body');
+window.parent.postMessage('okay', '*');
+window.parent.iframe.remove();
+response.clone();
+</script>
+`;
+
+ await new Promise(resolve => {
+ onmessage = evt => {
+ if (evt.data === 'okay') {
+ resolve();
+ }
+ };
+ });
+
+ // If it got here without crashing, the test passed.
+}, 'clone within removed iframe should not crash');
diff --git a/testing/web-platform/tests/fetch/api/response/response-clone.any.js b/testing/web-platform/tests/fetch/api/response/response-clone.any.js
new file mode 100644
index 0000000000..f5cda75149
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-clone.any.js
@@ -0,0 +1,140 @@
+// META: global=window,worker
+// META: title=Response clone
+// META: script=../resources/utils.js
+
+var defaultValues = { "type" : "default",
+ "url" : "",
+ "ok" : true,
+ "status" : 200,
+ "statusText" : ""
+};
+
+var response = new Response();
+var clonedResponse = response.clone();
+test(function() {
+ for (var attributeName in defaultValues) {
+ var expectedValue = defaultValues[attributeName];
+ assert_equals(clonedResponse[attributeName], expectedValue,
+ "Expect default response." + attributeName + " is " + expectedValue);
+ }
+}, "Check Response's clone with default values, without body");
+
+var body = "This is response body";
+var headersInit = { "name" : "value" };
+var responseInit = { "status" : 200,
+ "statusText" : "GOOD",
+ "headers" : headersInit
+};
+var response = new Response(body, responseInit);
+var clonedResponse = response.clone();
+test(function() {
+ assert_equals(clonedResponse.status, responseInit["status"],
+ "Expect response.status is " + responseInit["status"]);
+ assert_equals(clonedResponse.statusText, responseInit["statusText"],
+ "Expect response.statusText is " + responseInit["statusText"]);
+ assert_equals(clonedResponse.headers.get("name"), "value",
+ "Expect response.headers has name:value header");
+}, "Check Response's clone has the expected attribute values");
+
+promise_test(function(test) {
+ return validateStreamFromString(response.body.getReader(), body);
+}, "Check orginal response's body after cloning");
+
+promise_test(function(test) {
+ return validateStreamFromString(clonedResponse.body.getReader(), body);
+}, "Check cloned response's body");
+
+promise_test(function(test) {
+ var disturbedResponse = new Response("data");
+ return disturbedResponse.text().then(function() {
+ assert_true(disturbedResponse.bodyUsed, "response is disturbed");
+ assert_throws_js(TypeError, function() { disturbedResponse.clone(); },
+ "Expect TypeError exception");
+ });
+}, "Cannot clone a disturbed response");
+
+promise_test(function(t) {
+ var clone;
+ var result;
+ var response;
+ return fetch('../resources/trickle.py?count=2&delay=100').then(function(res) {
+ clone = res.clone();
+ response = res;
+ return clone.text();
+ }).then(function(r) {
+ assert_equals(r.length, 26);
+ result = r;
+ return response.text();
+ }).then(function(r) {
+ assert_equals(r, result, "cloned responses should provide the same data");
+ });
+ }, 'Cloned responses should provide the same data');
+
+promise_test(function(t) {
+ var clone;
+ return fetch('../resources/trickle.py?count=2&delay=100').then(function(res) {
+ clone = res.clone();
+ res.body.cancel();
+ assert_true(res.bodyUsed);
+ assert_false(clone.bodyUsed);
+ return clone.arrayBuffer();
+ }).then(function(r) {
+ assert_equals(r.byteLength, 26);
+ assert_true(clone.bodyUsed);
+ });
+}, 'Cancelling stream should not affect cloned one');
+
+function testReadableStreamClone(initialBuffer, bufferType)
+{
+ promise_test(function(test) {
+ var response = new Response(new ReadableStream({start : function(controller) {
+ controller.enqueue(initialBuffer);
+ controller.close();
+ }}));
+
+ var clone = response.clone();
+ var stream1 = response.body;
+ var stream2 = clone.body;
+
+ var buffer;
+ return stream1.getReader().read().then(function(data) {
+ assert_false(data.done);
+ assert_equals(data.value, initialBuffer, "Buffer of being-cloned response stream is the same as the original buffer");
+ return stream2.getReader().read();
+ }).then(function(data) {
+ assert_false(data.done);
+ if (initialBuffer instanceof ArrayBuffer) {
+ assert_true(data.value instanceof ArrayBuffer, "Cloned buffer is ArrayBufer");
+ assert_equals(initialBuffer.byteLength, data.value.byteLength, "Length equal");
+ assert_array_equals(new Uint8Array(data.value), new Uint8Array(initialBuffer), "Cloned buffer chunks have the same content");
+ } else if (initialBuffer instanceof DataView) {
+ assert_true(data.value instanceof DataView, "Cloned buffer is DataView");
+ assert_equals(initialBuffer.byteLength, data.value.byteLength, "Lengths equal");
+ assert_equals(initialBuffer.byteOffset, data.value.byteOffset, "Offsets equal");
+ for (let i = 0; i < initialBuffer.byteLength; ++i) {
+ assert_equals(
+ data.value.getUint8(i), initialBuffer.getUint8(i), "Mismatch at byte ${i}");
+ }
+ } else {
+ assert_array_equals(data.value, initialBuffer, "Cloned buffer chunks have the same content");
+ }
+ assert_equals(Object.getPrototypeOf(data.value), Object.getPrototypeOf(initialBuffer), "Cloned buffers have the same type");
+ assert_not_equals(data.value, initialBuffer, "Buffer of cloned response stream is a clone of the original buffer");
+ });
+ }, "Check response clone use structureClone for teed ReadableStreams (" + bufferType + "chunk)");
+}
+
+var arrayBuffer = new ArrayBuffer(16);
+testReadableStreamClone(new Int8Array(arrayBuffer, 1), "Int8Array");
+testReadableStreamClone(new Int16Array(arrayBuffer, 2, 2), "Int16Array");
+testReadableStreamClone(new Int32Array(arrayBuffer), "Int32Array");
+testReadableStreamClone(arrayBuffer, "ArrayBuffer");
+testReadableStreamClone(new Uint8Array(arrayBuffer), "Uint8Array");
+testReadableStreamClone(new Uint8ClampedArray(arrayBuffer), "Uint8ClampedArray");
+testReadableStreamClone(new Uint16Array(arrayBuffer, 2), "Uint16Array");
+testReadableStreamClone(new Uint32Array(arrayBuffer), "Uint32Array");
+testReadableStreamClone(typeof BigInt64Array === "function" ? new BigInt64Array(arrayBuffer) : undefined, "BigInt64Array");
+testReadableStreamClone(typeof BigUint64Array === "function" ? new BigUint64Array(arrayBuffer) : undefined, "BigUint64Array");
+testReadableStreamClone(new Float32Array(arrayBuffer), "Float32Array");
+testReadableStreamClone(new Float64Array(arrayBuffer), "Float64Array");
+testReadableStreamClone(new DataView(arrayBuffer, 2, 8), "DataView");
diff --git a/testing/web-platform/tests/fetch/api/response/response-consume-empty.any.js b/testing/web-platform/tests/fetch/api/response/response-consume-empty.any.js
new file mode 100644
index 0000000000..0fa85ecbcb
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-consume-empty.any.js
@@ -0,0 +1,99 @@
+// META: global=window,worker
+// META: title=Response consume empty bodies
+
+function checkBodyText(test, response) {
+ return response.text().then(function(bodyAsText) {
+ assert_equals(bodyAsText, "", "Resolved value should be empty");
+ assert_false(response.bodyUsed);
+ });
+}
+
+function checkBodyBlob(test, response) {
+ return response.blob().then(function(bodyAsBlob) {
+ var promise = new Promise(function(resolve, reject) {
+ var reader = new FileReader();
+ reader.onload = function(evt) {
+ resolve(reader.result)
+ };
+ reader.onerror = function() {
+ reject("Blob's reader failed");
+ };
+ reader.readAsText(bodyAsBlob);
+ });
+ return promise.then(function(body) {
+ assert_equals(body, "", "Resolved value should be empty");
+ assert_false(response.bodyUsed);
+ });
+ });
+}
+
+function checkBodyArrayBuffer(test, response) {
+ return response.arrayBuffer().then(function(bodyAsArrayBuffer) {
+ assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty");
+ assert_false(response.bodyUsed);
+ });
+}
+
+function checkBodyJSON(test, response) {
+ return response.json().then(
+ function(bodyAsJSON) {
+ assert_unreached("JSON parsing should fail");
+ },
+ function() {
+ assert_false(response.bodyUsed);
+ });
+}
+
+function checkBodyFormData(test, response) {
+ return response.formData().then(function(bodyAsFormData) {
+ assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData");
+ assert_false(response.bodyUsed);
+ });
+}
+
+function checkBodyFormDataError(test, response) {
+ return promise_rejects_js(test, TypeError, response.formData()).then(function() {
+ assert_false(response.bodyUsed);
+ });
+}
+
+function checkResponseWithNoBody(bodyType, checkFunction, headers = []) {
+ promise_test(function(test) {
+ var response = new Response(undefined, { "headers": headers });
+ assert_false(response.bodyUsed);
+ return checkFunction(test, response);
+ }, "Consume response's body as " + bodyType);
+}
+
+checkResponseWithNoBody("text", checkBodyText);
+checkResponseWithNoBody("blob", checkBodyBlob);
+checkResponseWithNoBody("arrayBuffer", checkBodyArrayBuffer);
+checkResponseWithNoBody("json (error case)", checkBodyJSON);
+checkResponseWithNoBody("formData with correct multipart type (error case)", checkBodyFormDataError, [["Content-Type", 'multipart/form-data; boundary="boundary"']]);
+checkResponseWithNoBody("formData with correct urlencoded type", checkBodyFormData, [["Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"]]);
+checkResponseWithNoBody("formData without correct type (error case)", checkBodyFormDataError);
+
+function checkResponseWithEmptyBody(bodyType, body, asText) {
+ promise_test(function(test) {
+ var response = new Response(body);
+ assert_false(response.bodyUsed, "bodyUsed is false at init");
+ if (asText) {
+ return response.text().then(function(bodyAsString) {
+ assert_equals(bodyAsString.length, 0, "Resolved value should be empty");
+ assert_true(response.bodyUsed, "bodyUsed is true after being consumed");
+ });
+ }
+ return response.arrayBuffer().then(function(bodyAsArrayBuffer) {
+ assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty");
+ assert_true(response.bodyUsed, "bodyUsed is true after being consumed");
+ });
+ }, "Consume empty " + bodyType + " response body as " + (asText ? "text" : "arrayBuffer"));
+}
+
+checkResponseWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), false);
+checkResponseWithEmptyBody("text", "", false);
+checkResponseWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), true);
+checkResponseWithEmptyBody("text", "", true);
+checkResponseWithEmptyBody("URLSearchParams", new URLSearchParams(""), true);
+checkResponseWithEmptyBody("FormData", new FormData(), true);
+checkResponseWithEmptyBody("ArrayBuffer", new ArrayBuffer(), true);
diff --git a/testing/web-platform/tests/fetch/api/response/response-consume-stream.any.js b/testing/web-platform/tests/fetch/api/response/response-consume-stream.any.js
new file mode 100644
index 0000000000..f89d7341ac
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-consume-stream.any.js
@@ -0,0 +1,80 @@
+// META: global=window,worker
+// META: title=Response consume
+// META: script=../resources/utils.js
+
+promise_test(function(test) {
+ var body = "";
+ var response = new Response("");
+ return validateStreamFromString(response.body.getReader(), "");
+}, "Read empty text response's body as readableStream");
+
+promise_test(function(test) {
+ var response = new Response(new Blob([], { "type" : "text/plain" }));
+ return validateStreamFromString(response.body.getReader(), "");
+}, "Read empty blob response's body as readableStream");
+
+var formData = new FormData();
+formData.append("name", "value");
+var textData = JSON.stringify("This is response's body");
+var blob = new Blob([textData], { "type" : "text/plain" });
+var urlSearchParamsData = "name=value";
+var urlSearchParams = new URLSearchParams(urlSearchParamsData);
+
+for (const mode of [undefined, "byob"]) {
+ promise_test(function(test) {
+ var response = new Response(blob);
+ return validateStreamFromString(response.body.getReader({ mode }), textData);
+ }, `Read blob response's body as readableStream with mode=${mode}`);
+
+ promise_test(function(test) {
+ var response = new Response(textData);
+ return validateStreamFromString(response.body.getReader({ mode }), textData);
+ }, `Read text response's body as readableStream with mode=${mode}`);
+
+ promise_test(function(test) {
+ var response = new Response(urlSearchParams);
+ return validateStreamFromString(response.body.getReader({ mode }), urlSearchParamsData);
+ }, `Read URLSearchParams response's body as readableStream with mode=${mode}`);
+
+ promise_test(function(test) {
+ var arrayBuffer = new ArrayBuffer(textData.length);
+ var int8Array = new Int8Array(arrayBuffer);
+ for (var cptr = 0; cptr < textData.length; cptr++)
+ int8Array[cptr] = textData.charCodeAt(cptr);
+
+ return validateStreamFromString(new Response(arrayBuffer).body.getReader({ mode }), textData);
+ }, `Read array buffer response's body as readableStream with mode=${mode}`);
+
+ promise_test(function(test) {
+ var response = new Response(formData);
+ return validateStreamFromPartialString(response.body.getReader({ mode }),
+ "Content-Disposition: form-data; name=\"name\"\r\n\r\nvalue");
+ }, `Read form data response's body as readableStream with mode=${mode}`);
+}
+
+test(function() {
+ assert_equals(Response.error().body, null);
+}, "Getting an error Response stream");
+
+test(function() {
+ assert_equals(Response.redirect("/").body, null);
+}, "Getting a redirect Response stream");
+
+promise_test(async function(test) {
+ var buffer = new ArrayBuffer(textData.length);
+
+ var body = new Response(textData).body;
+ const reader = body.getReader( {mode: 'byob'} );
+
+ let offset = 3;
+ while (offset < textData.length) {
+ const {done, value} = await reader.read(new Uint8Array(buffer, offset));
+ if (done) {
+ break;
+ }
+ buffer = value.buffer;
+ offset += value.byteLength;
+ }
+
+ validateBufferFromString(buffer, `\0\0\0\"This is response's bo`, 'Buffer should be validated');
+}, `Reading with offset from Response stream`);
diff --git a/testing/web-platform/tests/fetch/api/response/response-consume.html b/testing/web-platform/tests/fetch/api/response/response-consume.html
new file mode 100644
index 0000000000..89fc49fd3c
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-consume.html
@@ -0,0 +1,317 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Response consume</title>
+ <meta name="help" href="https://fetch.spec.whatwg.org/#response">
+ <meta name="help" href="https://fetch.spec.whatwg.org/#body-mixin">
+ <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../resources/utils.js"></script>
+ </head>
+ <body>
+ <script>
+ function blobToFormDataResponse(name, blob) {
+ var formData = new FormData();
+ formData.append(name, blob);
+ return new Response(formData);
+ }
+
+ function readBlobAsArrayBuffer(blob) {
+ return new Promise(function(resolve, reject) {
+ var reader = new FileReader();
+ reader.onload = function(evt) {
+ resolve(reader.result);
+ };
+ reader.onerror = function(evt) {
+ reject("Blob's reader failed");
+ };
+ reader.readAsArrayBuffer(blob);
+ });
+ }
+
+ function blobToTypeViaFetch(blob) {
+ var url = URL.createObjectURL(blob);
+ return fetch(url).then(function(response) {
+ return response.headers.get('Content-Type');
+ });
+ }
+
+ function responsePromise(body, responseInit) {
+ return new Promise(function(resolve, reject) {
+ resolve(new Response(body, responseInit));
+ });
+ }
+
+ function responseStringToMultipartFormTextData(response, name, value) {
+ assert_true(response.headers.has("Content-Type"), "Response contains Content-Type header");
+ var boundaryMatches = response.headers.get("Content-Type").match(/;\s*boundary=("?)([^";\s]*)\1/);
+ assert_true(!!boundaryMatches, "Response contains boundary parameter");
+ return stringToMultipartFormTextData(boundaryMatches[2], name, value);
+ }
+
+ function streamResponsePromise(streamData, responseInit) {
+ return new Promise(function(resolve, reject) {
+ var stream = new ReadableStream({
+ start: function(controller) {
+ controller.enqueue(stringToArray(streamData));
+ controller.close();
+ }
+ });
+ resolve(new Response(stream, responseInit));
+ });
+ }
+
+ function stringToMultipartFormTextData(multipartBoundary, name, value) {
+ return ('--' + multipartBoundary + '\r\n' +
+ 'Content-Disposition: form-data;name="' + name + '"\r\n' +
+ '\r\n' +
+ value + '\r\n' +
+ '--' + multipartBoundary + '--\r\n');
+ }
+
+ function checkBodyText(test, response, expectedBody) {
+ return response.text().then( function(bodyAsText) {
+ assert_equals(bodyAsText, expectedBody, "Retrieve and verify response's body");
+ assert_true(response.bodyUsed, "body as text: bodyUsed turned true");
+ });
+ }
+
+ function checkBodyBlob(test, response, expectedBody, expectedType) {
+ return response.blob().then(function(bodyAsBlob) {
+ assert_equals(bodyAsBlob.type, expectedType || "text/plain", "Blob body type should be computed from the response Content-Type");
+
+ var promise = blobToTypeViaFetch(bodyAsBlob).then(function(type) {
+ assert_equals(type, expectedType || "text/plain", 'Type via blob URL');
+ return new Promise( function (resolve, reject) {
+ var reader = new FileReader();
+ reader.onload = function(evt) {
+ resolve(reader.result)
+ };
+ reader.onerror = function () {
+ reject("Blob's reader failed");
+ };
+ reader.readAsText(bodyAsBlob);
+ });
+ });
+ return promise.then(function(body) {
+ assert_equals(body, expectedBody, "Retrieve and verify response's body");
+ assert_true(response.bodyUsed, "body as blob: bodyUsed turned true");
+ });
+ });
+ }
+
+ function checkBodyArrayBuffer(test, response, expectedBody) {
+ return response.arrayBuffer().then( function(bodyAsArrayBuffer) {
+ validateBufferFromString(bodyAsArrayBuffer, expectedBody, "Retrieve and verify response's body");
+ assert_true(response.bodyUsed, "body as arrayBuffer: bodyUsed turned true");
+ });
+ }
+
+ function checkBodyJSON(test, response, expectedBody) {
+ return response.json().then(function(bodyAsJSON) {
+ var strBody = JSON.stringify(bodyAsJSON)
+ assert_equals(strBody, expectedBody, "Retrieve and verify response's body");
+ assert_true(response.bodyUsed, "body as json: bodyUsed turned true");
+ });
+ }
+
+ function checkBodyFormDataMultipart(test, response, expectedBody) {
+ return response.formData().then(function(bodyAsFormData) {
+ assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData");
+ var entryName = "name";
+ var strBody = responseStringToMultipartFormTextData(response, entryName, bodyAsFormData.get(entryName));
+ assert_equals(strBody, expectedBody, "Retrieve and verify response's body");
+ assert_true(response.bodyUsed, "body as formData: bodyUsed turned true");
+ });
+ }
+
+ function checkBodyFormDataUrlencoded(test, response, expectedBody) {
+ return response.formData().then(function(bodyAsFormData) {
+ assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData");
+ var entryName = "name";
+ var strBody = entryName + "=" + bodyAsFormData.get(entryName);
+ assert_equals(strBody, expectedBody, "Retrieve and verify response's body");
+ assert_true(response.bodyUsed, "body as formData: bodyUsed turned true");
+ });
+ }
+
+ function checkBodyFormDataError(test, response, expectedBody) {
+ return promise_rejects_js(test, TypeError, response.formData()).then(function() {
+ assert_true(response.bodyUsed, "body as formData: bodyUsed turned true");
+ });
+ }
+
+ function checkResponseBody(responsePromise, expectedBody, checkFunction, bodyTypes) {
+ promise_test(function(test) {
+ return responsePromise.then(function(response) {
+ assert_false(response.bodyUsed, "bodyUsed is false at init");
+ return checkFunction(test, response, expectedBody);
+ });
+ }, "Consume response's body: " + bodyTypes);
+ }
+
+ var textData = JSON.stringify("This is response's body");
+ var textResponseInit = { "headers": [["Content-Type", "text/PLAIN"]] };
+ var blob = new Blob([textData], { "type": "application/octet-stream" });
+ var multipartBoundary = "boundary-" + Math.random();
+ var formData = new FormData();
+ var formTextResponseInit = { "headers": [["Content-Type", 'multipart/FORM-data; boundary="' + multipartBoundary + '"']] };
+ var formTextData = stringToMultipartFormTextData(multipartBoundary, "name", textData);
+ var formBlob = new Blob([formTextData]);
+ var urlSearchParamsData = "name=value";
+ var urlSearchParams = new URLSearchParams(urlSearchParamsData);
+ var urlSearchParamsType = "application/x-www-form-urlencoded;charset=UTF-8";
+ var urlSearchParamsResponseInit = { "headers": [["Content-Type", urlSearchParamsType]] };
+ var urlSearchParamsBlob = new Blob([urlSearchParamsData], { "type": urlSearchParamsType });
+ formData.append("name", textData);
+
+ // https://fetch.spec.whatwg.org/#concept-body-package-data
+ // "UTF-8 decoded without BOM" is used for formData(), either in
+ // "multipart/form-data" and "application/x-www-form-urlencoded" cases,
+ // so BOMs in the values should be kept.
+ // (The "application/x-www-form-urlencoded" cases are tested in
+ // url/urlencoded-parser.any.js)
+ var textDataWithBom = "\uFEFFquick\uFEFFfox\uFEFF";
+ var formTextDataWithBom = stringToMultipartFormTextData(multipartBoundary, "name", textDataWithBom);
+ var formTextDataWithBomExpectedForMultipartFormData = stringToMultipartFormTextData(multipartBoundary, "name", textDataWithBom);
+
+ checkResponseBody(responsePromise(textData, textResponseInit), textData, checkBodyText, "from text to text");
+ checkResponseBody(responsePromise(textData, textResponseInit), textData, checkBodyBlob, "from text to blob");
+ checkResponseBody(responsePromise(textData, textResponseInit), textData, checkBodyArrayBuffer, "from text to arrayBuffer");
+ checkResponseBody(responsePromise(textData, textResponseInit), textData, checkBodyJSON, "from text to json");
+ checkResponseBody(responsePromise(formTextData, formTextResponseInit), formTextData, checkBodyFormDataMultipart, "from text with correct multipart type to formData");
+ checkResponseBody(responsePromise(formTextDataWithBom, formTextResponseInit), formTextDataWithBomExpectedForMultipartFormData, checkBodyFormDataMultipart, "from text with correct multipart type to formData with BOM");
+ checkResponseBody(responsePromise(formTextData, textResponseInit), undefined, checkBodyFormDataError, "from text without correct multipart type to formData (error case)");
+ checkResponseBody(responsePromise(urlSearchParamsData, urlSearchParamsResponseInit), urlSearchParamsData, checkBodyFormDataUrlencoded, "from text with correct urlencoded type to formData");
+ checkResponseBody(responsePromise(urlSearchParamsData, textResponseInit), undefined, checkBodyFormDataError, "from text without correct urlencoded type to formData (error case)");
+
+ checkResponseBody(responsePromise(blob, textResponseInit), textData, checkBodyBlob, "from blob to blob");
+ checkResponseBody(responsePromise(blob), textData, checkBodyText, "from blob to text");
+ checkResponseBody(responsePromise(blob), textData, checkBodyArrayBuffer, "from blob to arrayBuffer");
+ checkResponseBody(responsePromise(blob), textData, checkBodyJSON, "from blob to json");
+ checkResponseBody(responsePromise(formBlob, formTextResponseInit), formTextData, checkBodyFormDataMultipart, "from blob with correct multipart type to formData");
+ checkResponseBody(responsePromise(formBlob, textResponseInit), undefined, checkBodyFormDataError, "from blob without correct multipart type to formData (error case)");
+ checkResponseBody(responsePromise(urlSearchParamsBlob, urlSearchParamsResponseInit), urlSearchParamsData, checkBodyFormDataUrlencoded, "from blob with correct urlencoded type to formData");
+ checkResponseBody(responsePromise(urlSearchParamsBlob, textResponseInit), undefined, checkBodyFormDataError, "from blob without correct urlencoded type to formData (error case)");
+
+ function checkFormDataResponseBody(responsePromise, expectedName, expectedValue, checkFunction, bodyTypes) {
+ promise_test(function(test) {
+ return responsePromise.then(function(response) {
+ assert_false(response.bodyUsed, "bodyUsed is false at init");
+ var expectedBody = responseStringToMultipartFormTextData(response, expectedName, expectedValue);
+ return Promise.resolve().then(function() {
+ if (checkFunction === checkBodyFormDataMultipart)
+ return expectedBody;
+ // Modify expectedBody to use the same spacing for
+ // Content-Disposition parameters as Response and FormData does.
+ var response2 = new Response(formData);
+ return response2.text().then(function(formDataAsText) {
+ var reName = /[ \t]*;[ \t]*name=/;
+ var nameMatches = formDataAsText.match(reName);
+ return expectedBody.replace(reName, nameMatches[0]);
+ });
+ }).then(function(expectedBody) {
+ return checkFunction(test, response, expectedBody);
+ });
+ });
+ }, "Consume response's body: " + bodyTypes);
+ }
+
+ checkFormDataResponseBody(responsePromise(formData), "name", textData, checkBodyFormDataMultipart, "from FormData to formData");
+ checkResponseBody(responsePromise(formData, textResponseInit), undefined, checkBodyFormDataError, "from FormData without correct type to formData (error case)");
+ checkFormDataResponseBody(responsePromise(formData), "name", textData, function(test, response, expectedBody) { return checkBodyBlob(test, response, expectedBody, response.headers.get('Content-Type').toLowerCase()); }, "from FormData to blob");
+ checkFormDataResponseBody(responsePromise(formData), "name", textData, checkBodyText, "from FormData to text");
+ checkFormDataResponseBody(responsePromise(formData), "name", textData, checkBodyArrayBuffer, "from FormData to arrayBuffer");
+
+ checkResponseBody(responsePromise(urlSearchParams), urlSearchParamsData, checkBodyFormDataUrlencoded, "from URLSearchParams to formData");
+ checkResponseBody(responsePromise(urlSearchParams, textResponseInit), urlSearchParamsData, checkBodyFormDataError, "from URLSearchParams without correct type to formData (error case)");
+ checkResponseBody(responsePromise(urlSearchParams), urlSearchParamsData, function(test, response, expectedBody) { return checkBodyBlob(test, response, expectedBody, "application/x-www-form-urlencoded;charset=utf-8"); }, "from URLSearchParams to blob");
+ checkResponseBody(responsePromise(urlSearchParams), urlSearchParamsData, checkBodyText, "from URLSearchParams to text");
+ checkResponseBody(responsePromise(urlSearchParams), urlSearchParamsData, checkBodyArrayBuffer, "from URLSearchParams to arrayBuffer");
+
+ checkResponseBody(streamResponsePromise(textData, textResponseInit), textData, checkBodyBlob, "from stream to blob");
+ checkResponseBody(streamResponsePromise(textData), textData, checkBodyText, "from stream to text");
+ checkResponseBody(streamResponsePromise(textData), textData, checkBodyArrayBuffer, "from stream to arrayBuffer");
+ checkResponseBody(streamResponsePromise(textData), textData, checkBodyJSON, "from stream to json");
+ checkResponseBody(streamResponsePromise(formTextData, formTextResponseInit), formTextData, checkBodyFormDataMultipart, "from stream with correct multipart type to formData");
+ checkResponseBody(streamResponsePromise(formTextData), formTextData, checkBodyFormDataError, "from stream without correct multipart type to formData (error case)");
+ checkResponseBody(streamResponsePromise(urlSearchParamsData, urlSearchParamsResponseInit), urlSearchParamsData, checkBodyFormDataUrlencoded, "from stream with correct urlencoded type to formData");
+ checkResponseBody(streamResponsePromise(urlSearchParamsData), urlSearchParamsData, checkBodyFormDataError, "from stream without correct urlencoded type to formData (error case)");
+
+ checkResponseBody(fetch("../resources/top.txt"), "top", checkBodyBlob, "from fetch to blob");
+ checkResponseBody(fetch("../resources/top.txt"), "top", checkBodyText, "from fetch to text");
+ checkResponseBody(fetch("../resources/top.txt"), "top", checkBodyArrayBuffer, "from fetch to arrayBuffer");
+ checkResponseBody(fetch("../resources/top.txt"), "top", checkBodyFormDataError, "from fetch without correct type to formData (error case)");
+
+ promise_test(function(test) {
+ var response = new Response(new Blob([
+ "--boundary\r\n",
+ "Content-Disposition: form-data; name=string\r\n",
+ "\r\nvalue", new Uint8Array([0xC2, 0xA0]), "1\r\n",
+ "--boundary\r\n",
+ "Content-Disposition: form-data; name=string-with-default-charset\r\n",
+ "Content-Type: text/plain; charset=utf-8\r\n",
+ "\r\nvalue", new Uint8Array([0xC2, 0xA0]), "2\r\n",
+ "--boundary\r\n",
+ "Content-Disposition: form-data; name=string-with-non-default-charset\r\n",
+ "Content-Type: text/plain; charset=iso-8859-1\r\n",
+ "\r\nvalue", new Uint8Array([0xC2, 0xA0]), "3\r\n",
+ "--boundary\r\n",
+ "Content-Disposition: form-data; name=string-with-non-default-type\r\n",
+ "Content-Type: application/octet-stream\r\n",
+ "\r\nvalue", new Uint8Array([0xC2, 0xA0]), "4\r\n",
+ "--boundary\r\n",
+ "Content-Disposition: form-data; name=file; filename=file1\r\n",
+ "Content-Type: application/octet-stream; x-param=x-value\r\n",
+ "\r\n", new Uint8Array([5, 0x0, 0xFF]), "\r\n",
+ "--boundary\r\n",
+ "Content-Disposition: form-data; name=\"file-without-type\"; filename=\"file2\"\r\n",
+ "\r\n", new Uint8Array([6, 0x0, 0x7F, 0xFF]), "\r\n",
+ "--boundary--\r\n"
+ ]), { "headers": [["Content-Type", 'multipart/form-data; boundary="boundary"']] });
+ return response.formData().then(function(bodyAsFormData) {
+ // Non-file parts must always be decoded using utf-8 encoding.
+ assert_equals(bodyAsFormData.get("string"), "value\u00A01", "Retrieve and verify response's 1st entry value");
+ assert_equals(bodyAsFormData.get("string-with-default-charset"), "value\u00A02", "Retrieve and verify response's 2nd entry value");
+ assert_equals(bodyAsFormData.get("string-with-non-default-charset"), "value\u00A03", "Retrieve and verify response's 3rd entry value");
+ assert_equals(bodyAsFormData.get("string-with-non-default-type"), "value\u00A04", "Retrieve and verify response's 4th entry value");
+ // The name of a File must be taken from the filename parameter in
+ // the Content-Disposition header field.
+ assert_equals(bodyAsFormData.get("file").name, "file1", "Retrieve and verify response's 5th entry name property");
+ assert_equals(bodyAsFormData.get("file-without-type").name, "file2", "Retrieve and verify response's 6th entry name property");
+ // The type of a File must be taken from the Content-Type header field
+ // which defaults to "text/plain".
+ assert_equals(bodyAsFormData.get("file").type, "application/octet-stream; x-param=x-value", "Retrieve and verify response's 5th entry type property");
+ assert_equals(bodyAsFormData.get("file-without-type").type, "text/plain", "Retrieve and verify response's 6th entry type property");
+
+ return Promise.resolve().then(function() {
+ return blobToFormDataResponse("file", bodyAsFormData.get("file")).text().then(function(bodyAsText) {
+ // Verify that filename, name and type are preserved.
+ assert_regexp_match(bodyAsText, /\r\nContent-Disposition: *form-data;([^\r\n]*;)* *filename=("?)file1\2[;\r]/i, "Retrieve and verify response's 5th entry filename parameter");
+ assert_regexp_match(bodyAsText, /\r\nContent-Disposition: *form-data;([^\r\n]*;)* *name=("?)file\2[;\r]/i, "Retrieve and verify response's 5th entry name parameter");
+ assert_regexp_match(bodyAsText, /\r\nContent-Type: *application\/octet-stream; x-param=x-value\r\n/i, "Retrieve and verify response's 5th entry type field");
+ // Verify that the content is preserved.
+ return readBlobAsArrayBuffer(bodyAsFormData.get("file")).then(function(arrayBuffer) {
+ assert_array_equals(new Uint8Array(arrayBuffer), new Uint8Array([5, 0x0, 0xFF]), "Retrieve and verify response's 5th entry content");
+ });
+ });
+ }).then(function() {
+ return blobToFormDataResponse("file-without-type", bodyAsFormData.get("file-without-type")).text().then(function(bodyAsText) {
+ // Verify that filename, name and type are preserved.
+ assert_regexp_match(bodyAsText, /\r\nContent-Disposition: *form-data;([^\r\n]*;)* *filename=("?)file2\2[;\r]/i, "Retrieve and verify response's 6th entry filename parameter");
+ assert_regexp_match(bodyAsText, /\r\nContent-Disposition: *form-data;([^\r\n]*;)* *name=("?)file-without-type\2[;\r]/i, "Retrieve and verify response's 6th entry name parameter");
+ assert_regexp_match(bodyAsText, /\r\nContent-Type: *text\/plain\r\n/i, "Retrieve and verify response's 6th entry type field");
+ // Verify that the content is preserved.
+ return readBlobAsArrayBuffer(bodyAsFormData.get("file-without-type")).then(function(arrayBuffer) {
+ assert_array_equals(new Uint8Array(arrayBuffer), new Uint8Array([6, 0x0, 0x7F, 0xFF]), "Retrieve and verify response's 6th entry content");
+ });
+ });
+ });
+ });
+ }, "Consume response's body: from multipart form data blob to formData");
+
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/fetch/api/response/response-error-from-stream.any.js b/testing/web-platform/tests/fetch/api/response/response-error-from-stream.any.js
new file mode 100644
index 0000000000..118eb7d5cb
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-error-from-stream.any.js
@@ -0,0 +1,59 @@
+// META: global=window,worker
+// META: title=Response Receives Propagated Error from ReadableStream
+
+function newStreamWithStartError() {
+ var err = new Error("Start error");
+ return [new ReadableStream({
+ start(controller) {
+ controller.error(err);
+ }
+ }),
+ err]
+}
+
+function newStreamWithPullError() {
+ var err = new Error("Pull error");
+ return [new ReadableStream({
+ pull(controller) {
+ controller.error(err);
+ }
+ }),
+ err]
+}
+
+function runRequestPromiseTest([stream, err], responseReaderMethod, testDescription) {
+ promise_test(test => {
+ return promise_rejects_exactly(
+ test,
+ err,
+ new Response(stream)[responseReaderMethod](),
+ 'CustomTestError should propagate'
+ )
+ }, testDescription)
+}
+
+
+promise_test(test => {
+ var [stream, err] = newStreamWithStartError();
+ return promise_rejects_exactly(test, err, stream.getReader().read(), 'CustomTestError should propagate')
+}, "ReadableStreamDefaultReader Promise receives ReadableStream start() Error")
+
+promise_test(test => {
+ var [stream, err] = newStreamWithPullError();
+ return promise_rejects_exactly(test, err, stream.getReader().read(), 'CustomTestError should propagate')
+}, "ReadableStreamDefaultReader Promise receives ReadableStream pull() Error")
+
+
+// test start() errors for all Body reader methods
+runRequestPromiseTest(newStreamWithStartError(), 'arrayBuffer', 'ReadableStream start() Error propagates to Response.arrayBuffer() Promise');
+runRequestPromiseTest(newStreamWithStartError(), 'blob', 'ReadableStream start() Error propagates to Response.blob() Promise');
+runRequestPromiseTest(newStreamWithStartError(), 'formData', 'ReadableStream start() Error propagates to Response.formData() Promise');
+runRequestPromiseTest(newStreamWithStartError(), 'json', 'ReadableStream start() Error propagates to Response.json() Promise');
+runRequestPromiseTest(newStreamWithStartError(), 'text', 'ReadableStream start() Error propagates to Response.text() Promise');
+
+// test pull() errors for all Body reader methods
+runRequestPromiseTest(newStreamWithPullError(), 'arrayBuffer', 'ReadableStream pull() Error propagates to Response.arrayBuffer() Promise');
+runRequestPromiseTest(newStreamWithPullError(), 'blob', 'ReadableStream pull() Error propagates to Response.blob() Promise');
+runRequestPromiseTest(newStreamWithPullError(), 'formData', 'ReadableStream pull() Error propagates to Response.formData() Promise');
+runRequestPromiseTest(newStreamWithPullError(), 'json', 'ReadableStream pull() Error propagates to Response.json() Promise');
+runRequestPromiseTest(newStreamWithPullError(), 'text', 'ReadableStream pull() Error propagates to Response.text() Promise');
diff --git a/testing/web-platform/tests/fetch/api/response/response-error.any.js b/testing/web-platform/tests/fetch/api/response/response-error.any.js
new file mode 100644
index 0000000000..a76bc43802
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-error.any.js
@@ -0,0 +1,27 @@
+// META: global=window,worker
+// META: title=Response error
+
+var invalidStatus = [0, 100, 199, 600, 1000];
+invalidStatus.forEach(function(status) {
+ test(function() {
+ assert_throws_js(RangeError, function() { new Response("", { "status" : status }); },
+ "Expect RangeError exception when status is " + status);
+ },"Throws RangeError when responseInit's status is " + status);
+});
+
+var invalidStatusText = ["\n", "Ā"];
+invalidStatusText.forEach(function(statusText) {
+ test(function() {
+ assert_throws_js(TypeError, function() { new Response("", { "statusText" : statusText }); },
+ "Expect TypeError exception " + statusText);
+ },"Throws TypeError when responseInit's statusText is " + statusText);
+});
+
+var nullBodyStatus = [204, 205, 304];
+nullBodyStatus.forEach(function(status) {
+ test(function() {
+ assert_throws_js(TypeError,
+ function() { new Response("body", {"status" : status }); },
+ "Expect TypeError exception ");
+ },"Throws TypeError when building a response with body and a body status of " + status);
+});
diff --git a/testing/web-platform/tests/fetch/api/response/response-from-stream.any.js b/testing/web-platform/tests/fetch/api/response/response-from-stream.any.js
new file mode 100644
index 0000000000..ea5192bfb1
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-from-stream.any.js
@@ -0,0 +1,23 @@
+// META: global=window,worker
+
+"use strict";
+
+test(() => {
+ const stream = new ReadableStream();
+ stream.getReader();
+ assert_throws_js(TypeError, () => new Response(stream));
+}, "Constructing a Response with a stream on which getReader() is called");
+
+test(() => {
+ const stream = new ReadableStream();
+ stream.getReader().read();
+ assert_throws_js(TypeError, () => new Response(stream));
+}, "Constructing a Response with a stream on which read() is called");
+
+promise_test(async () => {
+ const stream = new ReadableStream({ pull: c => c.enqueue(new Uint8Array()) }),
+ reader = stream.getReader();
+ await reader.read();
+ reader.releaseLock();
+ assert_throws_js(TypeError, () => new Response(stream));
+}, "Constructing a Response with a stream on which read() and releaseLock() are called");
diff --git a/testing/web-platform/tests/fetch/api/response/response-headers-guard.any.js b/testing/web-platform/tests/fetch/api/response/response-headers-guard.any.js
new file mode 100644
index 0000000000..4a67d067a7
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-headers-guard.any.js
@@ -0,0 +1,8 @@
+// META: global=window,worker
+// META: title=Response: error static method
+
+promise_test (async () => {
+ const response = await fetch("../resources/data.json");
+ assert_throws_js(TypeError, () => { response.headers.append("name", "value"); });
+ assert_not_equals(response.headers.get("name"), "value", "response headers should be immutable");
+}, "Ensure response headers are immutable");
diff --git a/testing/web-platform/tests/fetch/api/response/response-init-001.any.js b/testing/web-platform/tests/fetch/api/response/response-init-001.any.js
new file mode 100644
index 0000000000..559e49ad11
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-init-001.any.js
@@ -0,0 +1,64 @@
+// META: global=window,worker
+// META: title=Response init: simple cases
+
+var defaultValues = { "type" : "default",
+ "url" : "",
+ "ok" : true,
+ "status" : 200,
+ "statusText" : "",
+ "body" : null
+};
+
+var statusCodes = { "givenValues" : [200, 300, 400, 500, 599],
+ "expectedValues" : [200, 300, 400, 500, 599]
+};
+var statusTexts = { "givenValues" : ["", "OK", "with space", String.fromCharCode(0x80)],
+ "expectedValues" : ["", "OK", "with space", String.fromCharCode(0x80)]
+};
+var initValuesDict = { "status" : statusCodes,
+ "statusText" : statusTexts
+};
+
+function isOkStatus(status) {
+ return 200 <= status && 299 >= status;
+}
+
+var response = new Response();
+for (var attributeName in defaultValues) {
+ test(function() {
+ var expectedValue = defaultValues[attributeName];
+ assert_equals(response[attributeName], expectedValue,
+ "Expect default response." + attributeName + " is " + expectedValue);
+ }, "Check default value for " + attributeName + " attribute");
+}
+
+for (var attributeName in initValuesDict) {
+ test(function() {
+ var valuesToTest = initValuesDict[attributeName];
+ for (var valueIdx in valuesToTest["givenValues"]) {
+ var givenValue = valuesToTest["givenValues"][valueIdx];
+ var expectedValue = valuesToTest["expectedValues"][valueIdx];
+ var responseInit = {};
+ responseInit[attributeName] = givenValue;
+ var response = new Response("", responseInit);
+ assert_equals(response[attributeName], expectedValue,
+ "Expect response." + attributeName + " is " + expectedValue +
+ " when initialized with " + givenValue);
+ assert_equals(response.ok, isOkStatus(response.status),
+ "Expect response.ok is " + isOkStatus(response.status));
+ }
+ }, "Check " + attributeName + " init values and associated getter");
+}
+
+test(function() {
+ const response1 = new Response("");
+ assert_equals(response1.headers, response1.headers);
+
+ const response2 = new Response("", {"headers": {"X-Foo": "bar"}});
+ assert_equals(response2.headers, response2.headers);
+ const headers = response2.headers;
+ response2.headers.set("X-Foo", "quux");
+ assert_equals(headers, response2.headers);
+ headers.set("X-Other-Header", "baz");
+ assert_equals(headers, response2.headers);
+}, "Test that Response.headers has the [SameObject] extended attribute");
diff --git a/testing/web-platform/tests/fetch/api/response/response-init-002.any.js b/testing/web-platform/tests/fetch/api/response/response-init-002.any.js
new file mode 100644
index 0000000000..6c0a46e480
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-init-002.any.js
@@ -0,0 +1,61 @@
+// META: global=window,worker
+// META: title=Response init: body and headers
+// META: script=../resources/utils.js
+
+test(function() {
+ var headerDict = {"name1": "value1",
+ "name2": "value2",
+ "name3": "value3"
+ };
+ var headers = new Headers(headerDict);
+ var response = new Response("", { "headers" : headers })
+ for (var name in headerDict) {
+ assert_equals(response.headers.get(name), headerDict[name],
+ "response's headers has " + name + " : " + headerDict[name]);
+ }
+}, "Initialize Response with headers values");
+
+function checkResponseInit(body, bodyType, expectedTextBody) {
+ promise_test(function(test) {
+ var response = new Response(body);
+ var resHeaders = response.headers;
+ var mime = resHeaders.get("Content-Type");
+ assert_true(mime && mime.search(bodyType) > -1, "Content-Type header should be \"" + bodyType + "\" ");
+ return response.text().then(function(bodyAsText) {
+ //not equals: cannot guess formData exact value
+ assert_true(bodyAsText.search(expectedTextBody) > -1, "Retrieve and verify response body");
+ });
+ }, "Initialize Response's body with " + bodyType);
+}
+
+var blob = new Blob(["This is a blob"], {type: "application/octet-binary"});
+var formaData = new FormData();
+formaData.append("name", "value");
+var urlSearchParams = "URLSearchParams are not supported";
+//avoid test timeout if not implemented
+if (self.URLSearchParams)
+ urlSearchParams = new URLSearchParams("name=value");
+var usvString = "This is a USVString"
+
+checkResponseInit(blob, "application/octet-binary", "This is a blob");
+checkResponseInit(formaData, "multipart/form-data", "name=\"name\"\r\n\r\nvalue");
+checkResponseInit(urlSearchParams, "application/x-www-form-urlencoded;charset=UTF-8", "name=value");
+checkResponseInit(usvString, "text/plain;charset=UTF-8", "This is a USVString");
+
+promise_test(function(test) {
+ var body = "This is response body";
+ var response = new Response(body);
+ return validateStreamFromString(response.body.getReader(), body);
+}, "Read Response's body as readableStream");
+
+promise_test(function(test) {
+ var response = new Response("This is my fork", {"headers" : [["Content-Type", ""]]});
+ return response.blob().then(function(blob) {
+ assert_equals(blob.type, "", "Blob type should be the empty string");
+ });
+}, "Testing empty Response Content-Type header");
+
+test(function() {
+ var response = new Response(null, {status: 204});
+ assert_equals(response.body, null);
+}, "Testing null Response body");
diff --git a/testing/web-platform/tests/fetch/api/response/response-init-contenttype.any.js b/testing/web-platform/tests/fetch/api/response/response-init-contenttype.any.js
new file mode 100644
index 0000000000..3a7744c287
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-init-contenttype.any.js
@@ -0,0 +1,125 @@
+test(() => {
+ const response = new Response();
+ assert_equals(response.headers.get("Content-Type"), null);
+}, "Default Content-Type for Response with empty body");
+
+test(() => {
+ const blob = new Blob([]);
+ const response = new Response(blob);
+ assert_equals(response.headers.get("Content-Type"), null);
+}, "Default Content-Type for Response with Blob body (no type set)");
+
+test(() => {
+ const blob = new Blob([], { type: "" });
+ const response = new Response(blob);
+ assert_equals(response.headers.get("Content-Type"), null);
+}, "Default Content-Type for Response with Blob body (empty type)");
+
+test(() => {
+ const blob = new Blob([], { type: "a/b; c=d" });
+ const response = new Response(blob);
+ assert_equals(response.headers.get("Content-Type"), "a/b; c=d");
+}, "Default Content-Type for Response with Blob body (set type)");
+
+test(() => {
+ const buffer = new Uint8Array();
+ const response = new Response(buffer);
+ assert_equals(response.headers.get("Content-Type"), null);
+}, "Default Content-Type for Response with buffer source body");
+
+promise_test(async () => {
+ const formData = new FormData();
+ formData.append("a", "b");
+ const response = new Response(formData);
+ const boundary = (await response.text()).split("\r\n")[0].slice(2);
+ assert_equals(
+ response.headers.get("Content-Type"),
+ `multipart/form-data; boundary=${boundary}`,
+ );
+}, "Default Content-Type for Response with FormData body");
+
+test(() => {
+ const usp = new URLSearchParams();
+ const response = new Response(usp);
+ assert_equals(
+ response.headers.get("Content-Type"),
+ "application/x-www-form-urlencoded;charset=UTF-8",
+ );
+}, "Default Content-Type for Response with URLSearchParams body");
+
+test(() => {
+ const response = new Response("");
+ assert_equals(
+ response.headers.get("Content-Type"),
+ "text/plain;charset=UTF-8",
+ );
+}, "Default Content-Type for Response with string body");
+
+test(() => {
+ const stream = new ReadableStream();
+ const response = new Response(stream);
+ assert_equals(response.headers.get("Content-Type"), null);
+}, "Default Content-Type for Response with ReadableStream body");
+
+// -----------------------------------------------------------------------------
+
+const OVERRIDE_MIME = "test/only; mime=type";
+
+function responseWithOverrideMime(body) {
+ return new Response(
+ body,
+ { headers: { "Content-Type": OVERRIDE_MIME } },
+ );
+}
+
+test(() => {
+ const response = responseWithOverrideMime(undefined);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with empty body");
+
+test(() => {
+ const blob = new Blob([]);
+ const response = responseWithOverrideMime(blob);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with Blob body (no type set)");
+
+test(() => {
+ const blob = new Blob([], { type: "" });
+ const response = responseWithOverrideMime(blob);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with Blob body (empty type)");
+
+test(() => {
+ const blob = new Blob([], { type: "a/b; c=d" });
+ const response = responseWithOverrideMime(blob);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with Blob body (set type)");
+
+test(() => {
+ const buffer = new Uint8Array();
+ const response = responseWithOverrideMime(buffer);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with buffer source body");
+
+test(() => {
+ const formData = new FormData();
+ const response = responseWithOverrideMime(formData);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with FormData body");
+
+test(() => {
+ const usp = new URLSearchParams();
+ const response = responseWithOverrideMime(usp);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with URLSearchParams body");
+
+test(() => {
+ const response = responseWithOverrideMime("");
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with string body");
+
+test(() => {
+ const stream = new ReadableStream();
+ const response = responseWithOverrideMime(stream);
+ assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME);
+}, "Can override Content-Type for Response with ReadableStream body");
diff --git a/testing/web-platform/tests/fetch/api/response/response-static-error.any.js b/testing/web-platform/tests/fetch/api/response/response-static-error.any.js
new file mode 100644
index 0000000000..4097eab37b
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-static-error.any.js
@@ -0,0 +1,22 @@
+// META: global=window,worker
+// META: title=Response: error static method
+
+test(function() {
+ var responseError = Response.error();
+ assert_equals(responseError.type, "error", "Network error response's type is error");
+ assert_equals(responseError.status, 0, "Network error response's status is 0");
+ assert_equals(responseError.statusText, "", "Network error response's statusText is empty");
+ assert_equals(responseError.body, null, "Network error response's body is null");
+
+ assert_true(responseError.headers.entries().next().done, "Headers should be empty");
+}, "Check response returned by static method error()");
+
+test(function() {
+ const headers = Response.error().headers;
+
+ // Avoid false positives if expected API is not available
+ assert_true(!!headers);
+ assert_equals(typeof headers.append, 'function');
+
+ assert_throws_js(TypeError, function () { headers.append('name', 'value'); });
+}, "the 'guard' of the Headers instance should be immutable");
diff --git a/testing/web-platform/tests/fetch/api/response/response-static-json.any.js b/testing/web-platform/tests/fetch/api/response/response-static-json.any.js
new file mode 100644
index 0000000000..5ec79e69aa
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-static-json.any.js
@@ -0,0 +1,96 @@
+// META: global=window,worker
+// META: title=Response: json static method
+
+const APPLICATION_JSON = "application/json";
+const FOO_BAR = "foo/bar";
+
+const INIT_TESTS = [
+ [undefined, 200, "", APPLICATION_JSON, {}],
+ [{ status: 400 }, 400, "", APPLICATION_JSON, {}],
+ [{ statusText: "foo" }, 200, "foo", APPLICATION_JSON, {}],
+ [{ headers: {} }, 200, "", APPLICATION_JSON, {}],
+ [{ headers: { "content-type": FOO_BAR } }, 200, "", FOO_BAR, {}],
+ [{ headers: { "x-foo": "bar" } }, 200, "", APPLICATION_JSON, { "x-foo": "bar" }],
+];
+
+for (const [init, expectedStatus, expectedStatusText, expectedContentType, expectedHeaders] of INIT_TESTS) {
+ promise_test(async function () {
+ const response = Response.json("hello world", init);
+ assert_equals(response.type, "default", "Response's type is default");
+ assert_equals(response.status, expectedStatus, "Response's status is " + expectedStatus);
+ assert_equals(response.statusText, expectedStatusText, "Response's statusText is " + JSON.stringify(expectedStatusText));
+ assert_equals(response.headers.get("content-type"), expectedContentType, "Response's content-type is " + expectedContentType);
+ for (const key in expectedHeaders) {
+ assert_equals(response.headers.get(key), expectedHeaders[key], "Response's header " + key + " is " + JSON.stringify(expectedHeaders[key]));
+ }
+
+ const data = await response.json();
+ assert_equals(data, "hello world", "Response's body is 'hello world'");
+ }, `Check response returned by static json() with init ${JSON.stringify(init)}`);
+}
+
+const nullBodyStatus = [204, 205, 304];
+for (const status of nullBodyStatus) {
+ test(function () {
+ assert_throws_js(
+ TypeError,
+ function () {
+ Response.json("hello world", { status: status });
+ },
+ );
+ }, `Throws TypeError when calling static json() with a status of ${status}`);
+}
+
+promise_test(async function () {
+ const response = Response.json({ foo: "bar" });
+ const data = await response.json();
+ assert_equals(typeof data, "object", "Response's json body is an object");
+ assert_equals(data.foo, "bar", "Response's json body is { foo: 'bar' }");
+}, "Check static json() encodes JSON objects correctly");
+
+test(function () {
+ assert_throws_js(
+ TypeError,
+ function () {
+ Response.json(Symbol("foo"));
+ },
+ );
+}, "Check static json() throws when data is not encodable");
+
+test(function () {
+ const a = { b: 1 };
+ a.a = a;
+ assert_throws_js(
+ TypeError,
+ function () {
+ Response.json(a);
+ },
+ );
+}, "Check static json() throws when data is circular");
+
+promise_test(async function () {
+ class CustomError extends Error {
+ name = "CustomError";
+ }
+ assert_throws_js(
+ CustomError,
+ function () {
+ Response.json({ get foo() { throw new CustomError("bar") }});
+ }
+ )
+}, "Check static json() propagates JSON serializer errors");
+
+const encodingChecks = [
+ ["𝌆", [34, 240, 157, 140, 134, 34]],
+ ["\uDF06\uD834", [34, 92, 117, 100, 102, 48, 54, 92, 117, 100, 56, 51, 52, 34]],
+ ["\uDEAD", [34, 92, 117, 100, 101, 97, 100, 34]],
+];
+
+for (const [input, expected] of encodingChecks) {
+ promise_test(async function () {
+ const response = Response.json(input);
+ const buffer = await response.arrayBuffer();
+ const data = new Uint8Array(buffer);
+ assert_array_equals(data, expected);
+ }, `Check response returned by static json() with input ${input}`);
+}
diff --git a/testing/web-platform/tests/fetch/api/response/response-static-redirect.any.js b/testing/web-platform/tests/fetch/api/response/response-static-redirect.any.js
new file mode 100644
index 0000000000..b16c56d830
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-static-redirect.any.js
@@ -0,0 +1,40 @@
+// META: global=window,worker
+// META: title=Response: redirect static method
+
+var url = "http://test.url:1234/";
+test(function() {
+ const redirectResponse = Response.redirect(url);
+ assert_equals(redirectResponse.type, "default");
+ assert_false(redirectResponse.redirected);
+ assert_false(redirectResponse.ok);
+ assert_equals(redirectResponse.status, 302, "Default redirect status is 302");
+ assert_equals(redirectResponse.headers.get("Location"), url,
+ "redirected response has Location header with the correct url");
+ assert_equals(redirectResponse.statusText, "");
+}, "Check default redirect response");
+
+[301, 302, 303, 307, 308].forEach(function(status) {
+ test(function() {
+ const redirectResponse = Response.redirect(url, status);
+ assert_equals(redirectResponse.type, "default");
+ assert_false(redirectResponse.redirected);
+ assert_false(redirectResponse.ok);
+ assert_equals(redirectResponse.status, status, "Redirect status is " + status);
+ assert_equals(redirectResponse.headers.get("Location"), url);
+ assert_equals(redirectResponse.statusText, "");
+ }, "Check response returned by static method redirect(), status = " + status);
+});
+
+test(function() {
+ var invalidUrl = "http://:This is not an url";
+ assert_throws_js(TypeError, function() { Response.redirect(invalidUrl); },
+ "Expect TypeError exception");
+}, "Check error returned when giving invalid url to redirect()");
+
+var invalidRedirectStatus = [200, 309, 400, 500];
+invalidRedirectStatus.forEach(function(invalidStatus) {
+ test(function() {
+ assert_throws_js(RangeError, function() { Response.redirect(url, invalidStatus); },
+ "Expect RangeError exception");
+ }, "Check error returned when giving invalid status to redirect(), status = " + invalidStatus);
+});
diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-bad-chunk.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-bad-chunk.any.js
new file mode 100644
index 0000000000..d3d92e1677
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-stream-bad-chunk.any.js
@@ -0,0 +1,24 @@
+// META: global=window,worker
+// META: title=Response causes TypeError from bad chunk type
+
+function runChunkTest(responseReaderMethod, testDescription) {
+ promise_test(test => {
+ let stream = new ReadableStream({
+ start(controller) {
+ controller.enqueue("not Uint8Array");
+ controller.close();
+ }
+ });
+
+ return promise_rejects_js(test, TypeError,
+ new Response(stream)[responseReaderMethod](),
+ 'TypeError should propagate'
+ )
+ }, testDescription)
+}
+
+runChunkTest('arrayBuffer', 'ReadableStream with non-Uint8Array chunk passed to Response.arrayBuffer() causes TypeError');
+runChunkTest('blob', 'ReadableStream with non-Uint8Array chunk passed to Response.blob() causes TypeError');
+runChunkTest('formData', 'ReadableStream with non-Uint8Array chunk passed to Response.formData() causes TypeError');
+runChunkTest('json', 'ReadableStream with non-Uint8Array chunk passed to Response.json() causes TypeError');
+runChunkTest('text', 'ReadableStream with non-Uint8Array chunk passed to Response.text() causes TypeError');
diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-1.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-1.any.js
new file mode 100644
index 0000000000..64f65f16f2
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-1.any.js
@@ -0,0 +1,44 @@
+// META: global=window,worker
+// META: title=Consuming Response body after getting a ReadableStream
+// META: script=./response-stream-disturbed-util.js
+
+async function createResponseWithReadableStream(bodySource, callback) {
+ const response = await responseFromBodySource(bodySource);
+ const reader = response.body.getReader();
+ reader.releaseLock();
+ return callback(response);
+}
+
+for (const bodySource of ["fetch", "stream", "string"]) {
+ promise_test(function() {
+ return createResponseWithReadableStream(bodySource, function(response) {
+ return response.blob().then(function(blob) {
+ assert_true(blob instanceof Blob);
+ });
+ });
+ }, `Getting blob after getting the Response body - not disturbed, not locked (body source: ${bodySource})`);
+
+ promise_test(function() {
+ return createResponseWithReadableStream(bodySource, function(response) {
+ return response.text().then(function(text) {
+ assert_true(text.length > 0);
+ });
+ });
+ }, `Getting text after getting the Response body - not disturbed, not locked (body source: ${bodySource})`);
+
+ promise_test(function() {
+ return createResponseWithReadableStream(bodySource, function(response) {
+ return response.json().then(function(json) {
+ assert_equals(typeof json, "object");
+ });
+ });
+ }, `Getting json after getting the Response body - not disturbed, not locked (body source: ${bodySource})`);
+
+ promise_test(function() {
+ return createResponseWithReadableStream(bodySource, function(response) {
+ return response.arrayBuffer().then(function(arrayBuffer) {
+ assert_true(arrayBuffer.byteLength > 0);
+ });
+ });
+ }, `Getting arrayBuffer after getting the Response body - not disturbed, not locked (body source: ${bodySource})`);
+}
diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-2.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-2.any.js
new file mode 100644
index 0000000000..c46a180a18
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-2.any.js
@@ -0,0 +1,35 @@
+// META: global=window,worker
+// META: title=Consuming Response body after getting a ReadableStream
+// META: script=./response-stream-disturbed-util.js
+
+async function createResponseWithLockedReadableStream(bodySource, callback) {
+ const response = await responseFromBodySource(bodySource);
+ response.body.getReader();
+ return callback(response);
+}
+
+for (const bodySource of ["fetch", "stream", "string"]) {
+ promise_test(function(test) {
+ return createResponseWithLockedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.blob());
+ });
+ }, `Getting blob after getting a locked Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithLockedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.text());
+ });
+ }, `Getting text after getting a locked Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithLockedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.json());
+ });
+ }, `Getting json after getting a locked Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithLockedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.arrayBuffer());
+ });
+ }, `Getting arrayBuffer after getting a locked Response body (body source: ${bodySource})`);
+}
diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-3.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-3.any.js
new file mode 100644
index 0000000000..35fb086469
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-3.any.js
@@ -0,0 +1,36 @@
+// META: global=window,worker
+// META: title=Consuming Response body after getting a ReadableStream
+// META: script=./response-stream-disturbed-util.js
+
+async function createResponseWithDisturbedReadableStream(bodySource, callback) {
+ const response = await responseFromBodySource(bodySource);
+ const reader = response.body.getReader();
+ reader.read();
+ return callback(response);
+}
+
+for (const bodySource of ["fetch", "stream", "string"]) {
+ promise_test(function(test) {
+ return createResponseWithDisturbedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.blob());
+ });
+ }, `Getting blob after reading the Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithDisturbedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.text());
+ });
+ }, `Getting text after reading the Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithDisturbedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.json());
+ });
+ }, `Getting json after reading the Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithDisturbedReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.arrayBuffer());
+ });
+ }, `Getting arrayBuffer after reading the Response body (body source: ${bodySource})`);
+}
diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-4.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-4.any.js
new file mode 100644
index 0000000000..490672febd
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-4.any.js
@@ -0,0 +1,35 @@
+// META: global=window,worker
+// META: title=Consuming Response body after getting a ReadableStream
+// META: script=./response-stream-disturbed-util.js
+
+async function createResponseWithCancelledReadableStream(bodySource, callback) {
+ const response = await responseFromBodySource(bodySource);
+ response.body.cancel();
+ return callback(response);
+}
+
+for (const bodySource of ["fetch", "stream", "string"]) {
+ promise_test(function(test) {
+ return createResponseWithCancelledReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.blob());
+ });
+ }, `Getting blob after cancelling the Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithCancelledReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.text());
+ });
+ }, `Getting text after cancelling the Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithCancelledReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.json());
+ });
+ }, `Getting json after cancelling the Response body (body source: ${bodySource})`);
+
+ promise_test(function(test) {
+ return createResponseWithCancelledReadableStream(bodySource, function(response) {
+ return promise_rejects_js(test, TypeError, response.arrayBuffer());
+ });
+ }, `Getting arrayBuffer after cancelling the Response body (body source: ${bodySource})`);
+}
diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-5.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-5.any.js
new file mode 100644
index 0000000000..348fc39383
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-5.any.js
@@ -0,0 +1,19 @@
+// META: global=window,worker
+// META: title=Consuming Response body after getting a ReadableStream
+// META: script=./response-stream-disturbed-util.js
+
+for (const bodySource of ["fetch", "stream", "string"]) {
+ for (const consumeAs of ["blob", "text", "json", "arrayBuffer"]) {
+ promise_test(
+ async () => {
+ const response = await responseFromBodySource(bodySource);
+ response[consumeAs]();
+ assert_not_equals(response.body, null);
+ assert_throws_js(TypeError, function () {
+ response.body.getReader();
+ });
+ },
+ `Getting a body reader after consuming as ${consumeAs} (body source: ${bodySource})`,
+ );
+ }
+}
diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-6.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-6.any.js
new file mode 100644
index 0000000000..61d8544f07
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-6.any.js
@@ -0,0 +1,76 @@
+// META: global=window,worker
+// META: title=ReadableStream disturbed tests, via Response's bodyUsed property
+
+"use strict";
+
+test(() => {
+ const stream = new ReadableStream();
+ const response = new Response(stream);
+ assert_false(response.bodyUsed, "On construction");
+
+ const reader = stream.getReader();
+ assert_false(response.bodyUsed, "After getting a reader");
+
+ reader.read();
+ assert_true(response.bodyUsed, "After calling stream.read()");
+}, "A non-closed stream on which read() has been called");
+
+test(() => {
+ const stream = new ReadableStream();
+ const response = new Response(stream);
+ assert_false(response.bodyUsed, "On construction");
+
+ const reader = stream.getReader();
+ assert_false(response.bodyUsed, "After getting a reader");
+
+ reader.cancel();
+ assert_true(response.bodyUsed, "After calling stream.cancel()");
+}, "A non-closed stream on which cancel() has been called");
+
+test(() => {
+ const stream = new ReadableStream({
+ start(c) {
+ c.close();
+ }
+ });
+ const response = new Response(stream);
+ assert_false(response.bodyUsed, "On construction");
+
+ const reader = stream.getReader();
+ assert_false(response.bodyUsed, "After getting a reader");
+
+ reader.read();
+ assert_true(response.bodyUsed, "After calling stream.read()");
+}, "A closed stream on which read() has been called");
+
+test(() => {
+ const stream = new ReadableStream({
+ start(c) {
+ c.error(new Error("some error"));
+ }
+ });
+ const response = new Response(stream);
+ assert_false(response.bodyUsed, "On construction");
+
+ const reader = stream.getReader();
+ assert_false(response.bodyUsed, "After getting a reader");
+
+ reader.read().then(() => { }, () => { });
+ assert_true(response.bodyUsed, "After calling stream.read()");
+}, "An errored stream on which read() has been called");
+
+test(() => {
+ const stream = new ReadableStream({
+ start(c) {
+ c.error(new Error("some error"));
+ }
+ });
+ const response = new Response(stream);
+ assert_false(response.bodyUsed, "On construction");
+
+ const reader = stream.getReader();
+ assert_false(response.bodyUsed, "After getting a reader");
+
+ reader.cancel().then(() => { }, () => { });
+ assert_true(response.bodyUsed, "After calling stream.cancel()");
+}, "An errored stream on which cancel() has been called");
diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-by-pipe.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-by-pipe.any.js
new file mode 100644
index 0000000000..5341b75271
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-by-pipe.any.js
@@ -0,0 +1,17 @@
+// META: global=window,worker
+
+test(() => {
+ const r = new Response(new ReadableStream());
+ // highWaterMark: 0 means that nothing will actually be read from the body.
+ r.body.pipeTo(new WritableStream({}, {highWaterMark: 0}));
+ assert_true(r.bodyUsed, 'bodyUsed should be true');
+}, 'using pipeTo on Response body should disturb it synchronously');
+
+test(() => {
+ const r = new Response(new ReadableStream());
+ r.body.pipeThrough({
+ writable: new WritableStream({}, {highWaterMark: 0}),
+ readable: new ReadableStream()
+ });
+ assert_true(r.bodyUsed, 'bodyUsed should be true');
+}, 'using pipeThrough on Response body should disturb it synchronously');
diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-util.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-util.js
new file mode 100644
index 0000000000..50bb586aa0
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-util.js
@@ -0,0 +1,17 @@
+const BODY = '{"key": "value"}';
+
+function responseFromBodySource(bodySource) {
+ if (bodySource === "fetch") {
+ return fetch("../resources/data.json");
+ } else if (bodySource === "stream") {
+ const stream = new ReadableStream({
+ start(controller) {
+ controller.enqueue(new TextEncoder().encode(BODY));
+ controller.close();
+ },
+ });
+ return new Response(stream);
+ } else {
+ return new Response(BODY);
+ }
+}
diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-with-broken-then.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-with-broken-then.any.js
new file mode 100644
index 0000000000..8fef66c8a2
--- /dev/null
+++ b/testing/web-platform/tests/fetch/api/response/response-stream-with-broken-then.any.js
@@ -0,0 +1,117 @@
+// META: global=window,worker
+// META: script=../resources/utils.js
+
+promise_test(async () => {
+ // t.add_cleanup doesn't work when Object.prototype.then is overwritten, so
+ // these tests use add_completion_callback for cleanup instead.
+ add_completion_callback(() => delete Object.prototype.then);
+ const hello = new TextEncoder().encode('hello');
+ const bye = new TextEncoder().encode('bye');
+ const rs = new ReadableStream({
+ start(controller) {
+ controller.enqueue(hello);
+ controller.close();
+ }
+ });
+ const resp = new Response(rs);
+ Object.prototype.then = (onFulfilled) => {
+ delete Object.prototype.then;
+ onFulfilled({done: false, value: bye});
+ };
+ const text = await resp.text();
+ delete Object.prototype.then;
+ assert_equals(text, 'hello', 'The value should be "hello".');
+}, 'Attempt to inject {done: false, value: bye} via Object.prototype.then.');
+
+promise_test(async (t) => {
+ add_completion_callback(() => delete Object.prototype.then);
+ const hello = new TextEncoder().encode('hello');
+ const rs = new ReadableStream({
+ start(controller) {
+ controller.enqueue(hello);
+ controller.close();
+ }
+ });
+ const resp = new Response(rs);
+ Object.prototype.then = (onFulfilled) => {
+ delete Object.prototype.then;
+ onFulfilled({done: false, value: undefined});
+ };
+ const text = await resp.text();
+ delete Object.prototype.then;
+ assert_equals(text, 'hello', 'The value should be "hello".');
+}, 'Attempt to inject value: undefined via Object.prototype.then.');
+
+promise_test(async (t) => {
+ add_completion_callback(() => delete Object.prototype.then);
+ const hello = new TextEncoder().encode('hello');
+ const rs = new ReadableStream({
+ start(controller) {
+ controller.enqueue(hello);
+ controller.close();
+ }
+ });
+ const resp = new Response(rs);
+ Object.prototype.then = (onFulfilled) => {
+ delete Object.prototype.then;
+ onFulfilled(undefined);
+ };
+ const text = await resp.text();
+ delete Object.prototype.then;
+ assert_equals(text, 'hello', 'The value should be "hello".');
+}, 'Attempt to inject undefined via Object.prototype.then.');
+
+promise_test(async (t) => {
+ add_completion_callback(() => delete Object.prototype.then);
+ const hello = new TextEncoder().encode('hello');
+ const rs = new ReadableStream({
+ start(controller) {
+ controller.enqueue(hello);
+ controller.close();
+ }
+ });
+ const resp = new Response(rs);
+ Object.prototype.then = (onFulfilled) => {
+ delete Object.prototype.then;
+ onFulfilled(8.2);
+ };
+ const text = await resp.text();
+ delete Object.prototype.then;
+ assert_equals(text, 'hello', 'The value should be "hello".');
+}, 'Attempt to inject 8.2 via Object.prototype.then.');
+
+promise_test(async () => {
+ add_completion_callback(() => delete Object.prototype.then);
+ const hello = new TextEncoder().encode('hello');
+ const bye = new TextEncoder().encode('bye');
+ const resp = new Response(hello);
+ Object.prototype.then = (onFulfilled) => {
+ delete Object.prototype.then;
+ onFulfilled({done: false, value: bye});
+ };
+ const text = await resp.text();
+ delete Object.prototype.then;
+ assert_equals(text, 'hello', 'The value should be "hello".');
+}, 'intercepting arraybuffer to text conversion via Object.prototype.then ' +
+ 'should not be possible');
+
+promise_test(async () => {
+ add_completion_callback(() => delete Object.prototype.then);
+ const u8a123 = new Uint8Array([1, 2, 3]);
+ const u8a456 = new Uint8Array([4, 5, 6]);
+ const resp = new Response(u8a123);
+ const writtenBytes = [];
+ const ws = new WritableStream({
+ write(chunk) {
+ writtenBytes.push(...Array.from(chunk));
+ }
+ });
+ Object.prototype.then = (onFulfilled) => {
+ delete Object.prototype.then;
+ onFulfilled({done: false, value: u8a456});
+ };
+ await resp.body.pipeTo(ws);
+ delete Object.prototype.then;
+ assert_array_equals(writtenBytes, u8a123, 'The value should be [1, 2, 3]');
+}, 'intercepting arraybuffer to body readable stream conversion via ' +
+ 'Object.prototype.then should not be possible');