feat: HTTP add TLS fingerprint fallback (#1444)

This commit is contained in:
Levi authored and GitHub committed 2026-08-20 20:21:02 +08:00
1 parent 99924296ef
commit cfdf87c3b8
68 files changed
+7243 -1604

No files matched your search

+33 -8
View File
@@ -8,8 +8,8 @@ import (
"github.com/GopeedLab/gopeed/pkg/base"
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
fetchapi "github.com/GopeedLab/gopeed/pkg/download/engine/inject/fetch"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/stream"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/vm"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/xhr"
@@ -24,6 +24,10 @@ var polyfillScript string
type Engine struct {
loop *eventloop.EventLoop
closeOnce sync.Once
cleanupMu sync.Mutex
cleanups []func()
Runtime *goja.Runtime
}
@@ -133,7 +137,25 @@ func (e *Engine) runOnLoop(fn func(runtime *goja.Runtime) (goja.Value, error)) (
}
func (e *Engine) Close() {
e.loop.Terminate()
e.closeOnce.Do(func() {
e.cleanupMu.Lock()
cleanups := e.cleanups
e.cleanups = nil
e.cleanupMu.Unlock()
for _, cleanup := range cleanups {
cleanup()
}
e.loop.Terminate()
})
}
func (e *Engine) addCleanup(cleanup func()) {
if cleanup == nil {
return
}
e.cleanupMu.Lock()
e.cleanups = append(e.cleanups, cleanup)
e.cleanupMu.Unlock()
}
type Config struct {
@@ -163,12 +185,6 @@ func NewEngine(cfg *Config) *Engine {
if err := file.Enable(runtime); err != nil {
return
}
if err := formdata.Enable(runtime); err != nil {
return
}
if err := xhr.Enable(runtime, cfg.ProxyConfig.ToHandler()); err != nil {
return
}
if _, err := runtime.RunString(polyfillScript); err != nil {
return
}
@@ -187,6 +203,15 @@ func NewEngine(cfg *Config) *Engine {
if err := stream.Enable(runtime, loop, cfg.StreamConfig); err != nil {
return
}
if err := fetchapi.Enable(runtime, loop, &fetchapi.Config{
ProxyHandler: cfg.ProxyConfig.ToHandler(),
RegisterCleanup: engine.addCleanup,
}); err != nil {
return
}
if err := xhr.Enable(runtime); err != nil {
return
}
return
})
<-done
@@ -0,0 +1,168 @@
package engine_test
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/GopeedLab/gopeed/pkg/download/engine"
)
func TestFetchDoesNotDependOnXMLHttpRequest(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/target", func(w http.ResponseWriter, request *http.Request) {
w.Header().Set("X-Method", request.Method)
_, _ = w.Write([]byte("ok"))
})
mux.HandleFunc("/redirect", func(w http.ResponseWriter, request *http.Request) {
http.Redirect(w, request, "/target", http.StatusFound)
})
server := httptest.NewServer(mux)
t.Cleanup(server.Close)
runtime := engine.NewEngine(nil)
t.Cleanup(runtime.Close)
value, err := runtime.RunString(fmt.Sprintf(`
(async () => {
globalThis.XMLHttpRequest = function () {
throw new Error("fetch used XMLHttpRequest");
};
const get = await fetch(%q);
const head = await fetch(%q, { method: "HEAD" });
const manual = await fetch(%q, { redirect: "manual" });
let redirectError = false;
try {
await fetch(%q, { redirect: "error" });
} catch (_) {
redirectError = true;
}
return JSON.stringify({
get: await get.text(),
headStatus: head.status,
headMethod: head.headers.get("x-method"),
manualStatus: manual.status,
manualLocation: manual.headers.get("location"),
redirectError,
});
})()
`, server.URL+"/target", server.URL+"/target", server.URL+"/redirect", server.URL+"/redirect"))
if err != nil {
t.Fatal(err)
}
const want = `{"get":"ok","headStatus":200,"headMethod":"HEAD","manualStatus":302,"manualLocation":"/target","redirectError":true}`
if value != want {
t.Fatalf("unexpected fetch result: got %#v want %s", value, want)
}
}
func TestXMLHttpRequestUsesFetch(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("X-Test", "value")
_, _ = w.Write([]byte("payload"))
}))
t.Cleanup(server.Close)
runtime := engine.NewEngine(nil)
t.Cleanup(runtime.Close)
value, err := runtime.RunString(fmt.Sprintf(`
(async () => {
const originalFetch = globalThis.fetch;
let fetchCalls = 0;
globalThis.fetch = function (...args) {
fetchCalls++;
return originalFetch(...args);
};
const result = await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const states = [];
xhr.onreadystatechange = () => states.push(xhr.readyState);
xhr.onerror = () => reject(new Error("XHR failed"));
xhr.onload = () => resolve({
status: xhr.status,
text: xhr.responseText,
header: xhr.getResponseHeader("x-test"),
states,
});
xhr.open("GET", %q);
xhr.send();
});
result.fetchCalls = fetchCalls;
return JSON.stringify(result);
})()
`, server.URL))
if err != nil {
t.Fatal(err)
}
const want = `{"status":200,"text":"payload","header":"value","states":[1,2,3,4],"fetchCalls":1}`
if value != want {
t.Fatalf("unexpected XMLHttpRequest result: got %#v want %s", value, want)
}
}
func TestFetchResponseAndRequestCloneHaveIndependentBodies(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodPost {
_, _ = response.Write([]byte("posted"))
return
}
_, _ = response.Write([]byte("payload"))
}))
t.Cleanup(server.Close)
runtime := engine.NewEngine(nil)
t.Cleanup(runtime.Close)
value, err := runtime.RunString(fmt.Sprintf(`
(async () => {
const response = await fetch(%q);
const responseClone = response.clone();
const responseBodies = await Promise.all([response.text(), responseClone.text()]);
const request = new Request(%q, { method: "POST", body: "request-body" });
const requestClone = request.clone();
const requestBodies = await Promise.all([request.text(), requestClone.text()]);
return JSON.stringify({ responseBodies, requestBodies });
})()
`, server.URL, server.URL))
if err != nil {
t.Fatal(err)
}
const want = `{"responseBodies":["payload","payload"],"requestBodies":["request-body","request-body"]}`
if value != want {
t.Fatalf("unexpected clone result: got %#v want %s", value, want)
}
}
func TestFetchFormDataUsesWebEntryListSemantics(t *testing.T) {
runtime := engine.NewEngine(nil)
t.Cleanup(runtime.Close)
value, err := runtime.RunString(`
(() => {
const data = new FormData();
data.append("a", "one");
data.append("a", 2);
data.append("b", "three");
const beforeSet = Array.from(data.entries());
data.set("a", "four");
const afterSet = Array.from(data);
const all = data.getAll("a");
data.delete("b");
return JSON.stringify({ beforeSet, afterSet, all, hasB: data.has("b") });
})()
`)
if err != nil {
t.Fatal(err)
}
const want = `{"beforeSet":[["a","one"],["a","2"],["b","three"]],"afterSet":[["a","four"],["b","three"]],"all":["four"],"hasB":false}`
if value != want {
t.Fatalf("unexpected FormData result: got %#v want %s", value, want)
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,68 +0,0 @@
package formdata
import "github.com/dop251/goja"
type FormData struct {
data map[string]any
}
func (fd *FormData) Append(name string, value any) {
fd.data[name] = value
}
func (fd *FormData) Delete(name string) {
delete(fd.data, name)
}
func (fd *FormData) Entries() []any {
var entries []any
for k, v := range fd.data {
entries = append(entries, []any{k, v})
}
return entries
}
func (fd *FormData) Get(name string) any {
return fd.data[name]
}
func (fd *FormData) GetAll(name string) []any {
return []any{fd.data[name]}
}
func (fd *FormData) Has(name string) bool {
_, ok := fd.data[name]
return ok
}
func (fd *FormData) Keys() []string {
var keys []string
for k := range fd.data {
keys = append(keys, k)
}
return keys
}
func (fd *FormData) Set(name string, value any) {
fd.data[name] = value
}
func (fd *FormData) Values() []any {
var values []any
for _, v := range fd.data {
values = append(values, v)
}
return values
}
func Enable(runtime *goja.Runtime) error {
file := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
instance := &FormData{
data: make(map[string]any),
}
instanceValue := runtime.ToValue(instance).(*goja.Object)
instanceValue.SetPrototype(call.This.Prototype())
return instanceValue
})
return runtime.Set("FormData", file)
}
File diff suppressed because it is too large. Load diff
@@ -258,7 +258,6 @@ func waitForJSValue(t *testing.T, engine *engine.Engine, expression, want string
func newStreamTestEngine(t *testing.T, created chan<- createdObjectURL) (*engine.Engine, func()) {
t.Helper()
var next atomic.Int64
var cleanup func()
e := engine.NewEngine(&engine.Config{
StreamConfig: &stream.Config{
CreateObjectURL: func(opts *stream.ObjectURLOptions, open stream.ObjectURLOpener) (string, error) {
@@ -266,13 +265,7 @@ func newStreamTestEngine(t *testing.T, created chan<- createdObjectURL) (*engine
return fmt.Sprintf("blob:test-%d", next.Add(1)), nil
},
RevokeObjectURL: func(string) error { return nil },
RegisterCleanup: func(fn func()) {
cleanup = fn
},
},
})
if cleanup == nil {
t.Fatal("stream cleanup was not registered")
}
return e, cleanup
return e, e.Close
}
@@ -1,191 +0,0 @@
package stream
import (
"context"
"io"
"strings"
"sync/atomic"
"testing"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
"github.com/dop251/goja"
)
func TestExportFetchRequestSnapshotsArrayBuffer(t *testing.T) {
runtime := goja.New()
buffer := runtime.NewArrayBuffer([]byte("before"))
request := runtime.NewObject()
setFetchRequestDefaults(t, request)
if err := request.Set("body", buffer); err != nil {
t.Fatal(err)
}
meta, err := exportFetchRequest(runtime, request)
if err != nil {
t.Fatal(err)
}
buffer.Bytes()[0] = 'X'
body, ok := meta.Body.([]byte)
if !ok {
t.Fatalf("expected byte snapshot, got %T", meta.Body)
}
if got := string(body); got != "before" {
t.Fatalf("ArrayBuffer snapshot changed after export: %q", got)
}
}
func TestExportFetchRequestSnapshotsFormData(t *testing.T) {
runtime := goja.New()
runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
if err := formdata.Enable(runtime); err != nil {
t.Fatal(err)
}
value, err := runtime.RunString(`
globalThis.form = new FormData();
form.append("field", "before");
form;
`)
if err != nil {
t.Fatal(err)
}
request := runtime.NewObject()
setFetchRequestDefaults(t, request)
if err := request.Set("body", value); err != nil {
t.Fatal(err)
}
meta, err := exportFetchRequest(runtime, request)
if err != nil {
t.Fatal(err)
}
if _, err := runtime.RunString(`form.set("field", "after")`); err != nil {
t.Fatal(err)
}
body, ok := meta.Body.(*fetchFormDataSnapshot)
if !ok {
t.Fatalf("expected FormData snapshot, got %T", meta.Body)
}
if len(body.entries) != 1 {
t.Fatalf("expected one FormData entry, got %d", len(body.entries))
}
if got := body.entries[0]; got.name != "field" || got.value != "before" {
t.Fatalf("unexpected FormData snapshot: %#v", got)
}
}
func setFetchRequestDefaults(t *testing.T, request *goja.Object) {
t.Helper()
for key, value := range map[string]any{
"url": "https://example.test",
"method": "POST",
"redirect": "follow",
"credentials": "same-origin",
} {
if err := request.Set(key, value); err != nil {
t.Fatal(err)
}
}
}
func TestSnapshotFetchBodyCopiesFileMetadata(t *testing.T) {
source := &file.File{
Reader: strings.NewReader("payload"),
Name: "before.txt",
Size: 7,
}
value, err := snapshotFetchBody(source)
if err != nil {
t.Fatal(err)
}
snapshot, ok := value.(*file.File)
if !ok {
t.Fatalf("expected file snapshot, got %T", value)
}
source.Name = "after.txt"
source.Size = 99
if snapshot.Name != "before.txt" || snapshot.Size != 7 {
t.Fatalf("file metadata was not snapshotted: %#v", snapshot)
}
}
func TestFetchRegistryCloseAll(t *testing.T) {
registry := newFetchRegistry()
streamCtx, cancel := context.WithCancel(context.Background())
body := &trackingReadCloser{}
registry.streams["active"] = &fetchStream{
body: body,
cancel: cancel,
ctx: streamCtx,
ch: make(chan fetchChunk),
}
registry.CloseAll()
registry.CloseAll()
select {
case <-streamCtx.Done():
default:
t.Fatal("active fetch context was not canceled")
}
if got := body.closeCalls.Load(); got != 1 {
t.Fatalf("expected body to close once, got %d", got)
}
registry.mu.Lock()
closed := registry.closed
remaining := len(registry.streams)
registry.mu.Unlock()
if !closed || remaining != 0 {
t.Fatalf("registry not fully closed: closed=%v remaining=%d", closed, remaining)
}
if _, err := registry.Open("", nil, &fetchRequest{URL: "https://example.test"}); err == nil {
t.Fatal("expected a closed registry to reject new fetches")
}
}
func TestFetchRegistryReadPreservesChunkOrder(t *testing.T) {
registry := newFetchRegistry()
ctx, cancel := context.WithCancel(context.Background())
stream := &fetchStream{
body: io.NopCloser(strings.NewReader("")),
cancel: cancel,
ctx: ctx,
ch: make(chan fetchChunk, 2),
}
stream.ch <- fetchChunk{data: []byte("abc")}
stream.ch <- fetchChunk{data: []byte("def")}
registry.streams["ordered"] = stream
defer registry.CloseAll()
var got strings.Builder
for range 4 {
chunk, done, err := registry.Read("ordered", 2)
if err != nil {
t.Fatal(err)
}
if done {
t.Fatal("stream ended before buffered chunks were read")
}
got.Write(chunk)
}
if got.String() != "abcdef" {
t.Fatalf("fetch chunks were reordered: %q", got.String())
}
}
type trackingReadCloser struct {
closeCalls atomic.Int32
}
func (r *trackingReadCloser) Read([]byte) (int, error) {
return 0, io.EOF
}
func (r *trackingReadCloser) Close() error {
r.closeCalls.Add(1)
return nil
}
+90 -244
View File
@@ -1,10 +1,6 @@
(function () {
const createBlobObjectURL = globalThis.__gopeed_create_blob_object_url;
const revokeObjectURL = globalThis.__gopeed_revoke_object_url;
const fetchOpen = globalThis.__gopeed_fetch_open;
const fetchRead = globalThis.__gopeed_fetch_read;
const fetchClose = globalThis.__gopeed_fetch_close;
const fetchAbort = globalThis.__gopeed_fetch_abort;
if (typeof globalThis.ReadableStream === "undefined") {
class ReadableStreamDefaultController {
@@ -30,7 +26,10 @@
this._stream = stream;
}
read() {
read(view) {
if (view !== undefined) {
return this._stream._readInto(view);
}
return this._stream._read();
}
@@ -59,6 +58,7 @@
this._errored = null;
this._reader = null;
this._pulling = false;
this._disturbed = false;
this.locked = false;
this._controller = new ReadableStreamDefaultController(this);
if (typeof source.start === "function") {
@@ -96,6 +96,7 @@
if (this._errored) {
return Promise.reject(this._errored);
}
this._disturbed = true;
this._markBodyUsed();
if (this._queue.length > 0) {
return Promise.resolve({ done: false, value: this._queue.shift() });
@@ -116,6 +117,26 @@
});
}
async _readInto(view) {
if (!ArrayBuffer.isView(view) || view.byteLength === 0) {
throw new TypeError("BYOB reads require a non-empty ArrayBufferView");
}
const result = await this._read();
if (result.done) {
return { done: true, value: new Uint8Array(view.buffer, view.byteOffset, 0) };
}
if (!(result.value instanceof Uint8Array)) {
throw new TypeError("Byte streams require Uint8Array chunks");
}
const length = Math.min(view.byteLength, result.value.byteLength);
const target = new Uint8Array(view.buffer, view.byteOffset, length);
target.set(result.value.subarray(0, length));
if (length < result.value.byteLength) {
this._queue.unshift(result.value.subarray(length));
}
return { done: false, value: target };
}
_maybePull() {
if (this._closed || this._errored || this._pulling) {
return;
@@ -155,6 +176,7 @@
}
cancel(reason) {
this._disturbed = true;
this._markBodyUsed();
if (this._source && typeof this._source.cancel === "function") {
return Promise.resolve(this._source.cancel(reason)).then(() => {
@@ -185,6 +207,69 @@
writer.releaseLock();
}
}
tee() {
const reader = this.getReader();
let reading = false;
let currentRead = null;
let finished = false;
const waiting = [[], []];
const queues = [[], []];
const cloneChunk = (chunk) => {
if (chunk instanceof ArrayBuffer) return chunk.slice(0);
if (ArrayBuffer.isView(chunk)) {
const buffer = chunk.buffer.slice(0);
if (chunk instanceof DataView) return new DataView(buffer, chunk.byteOffset, chunk.byteLength);
return new chunk.constructor(buffer, chunk.byteOffset, chunk.length);
}
return chunk;
};
const flush = (index) => {
while (waiting[index].length && queues[index].length) {
waiting[index].shift().enqueue(queues[index].shift());
}
if (finished && queues[index].length === 0) {
while (waiting[index].length) waiting[index].shift().close();
}
};
const pump = () => {
if (reading) return currentRead;
if (finished) return Promise.resolve();
reading = true;
currentRead = (async () => {
try {
const result = await reader.read();
if (result.done) {
finished = true;
} else {
queues[0].push(result.value);
queues[1].push(cloneChunk(result.value));
}
} catch (error) {
finished = true;
for (const branches of waiting) {
while (branches.length) branches.shift().error(error);
}
throw error;
} finally {
reading = false;
currentRead = null;
flush(0);
flush(1);
}
})();
return currentRead;
};
const branch = (index) => new ReadableStream({
pull(controller) {
waiting[index].push(controller);
flush(index);
return pump();
},
});
return [branch(0), branch(1)];
}
}
globalThis.ReadableStream = ReadableStream;
@@ -351,167 +436,6 @@
};
}
function toUint8Array(chunk) {
if (chunk == null) {
return new Uint8Array(0);
}
if (chunk instanceof Uint8Array) {
return chunk;
}
if (typeof chunk === "string") {
return new TextEncoder().encode(chunk);
}
if (chunk instanceof ArrayBuffer) {
return new Uint8Array(chunk);
}
if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView && ArrayBuffer.isView(chunk)) {
return new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
}
if (typeof Blob !== "undefined" && chunk instanceof Blob) {
if (chunk._buffer instanceof Uint8Array) {
return chunk._buffer;
}
}
return new Uint8Array(0);
}
function createBodyReadableStream(owner) {
return new ReadableStream({
start(controller) {
Promise.resolve().then(async () => {
owner.bodyUsed = true;
if (owner._noBody) {
controller.close();
return;
}
if (owner._bodyArrayBuffer) {
controller.enqueue(toUint8Array(owner._bodyArrayBuffer));
controller.close();
return;
}
if (owner._bodyBlob) {
const data = await owner._bodyBlob.arrayBuffer();
controller.enqueue(new Uint8Array(data));
controller.close();
return;
}
if (owner._bodyText != null) {
controller.enqueue(toUint8Array(owner._bodyText));
controller.close();
return;
}
if (owner._bodyInit != null) {
controller.enqueue(toUint8Array(owner._bodyInit));
}
controller.close();
}).catch((err) => {
controller.error(err);
});
}
});
}
async function readAllFromStream(stream, asText) {
if (!stream) {
return asText ? "" : new Uint8Array(0);
}
const reader = stream.getReader();
const chunks = [];
let total = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
const chunk = toUint8Array(value);
chunks.push(chunk);
total += chunk.byteLength;
}
} finally {
reader.releaseLock();
}
const merged = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
merged.set(chunk, offset);
offset += chunk.byteLength;
}
if (asText) {
return new TextDecoder().decode(merged);
}
return merged;
}
function attachResponseStreaming(response, stream) {
response.__gopeedBodyStream = stream;
response.__gopeedBodyConsumed = false;
const ensureUnused = () => {
if (response.__gopeedBodyConsumed) {
throw new TypeError("Already read");
}
};
const markBodyUsed = () => {
ensureUnused();
response.__gopeedBodyConsumed = true;
response.bodyUsed = true;
};
if (stream) {
stream.__gopeedMarkBodyUsed = () => {
if (!response.__gopeedBodyConsumed) {
response.__gopeedBodyConsumed = true;
response.bodyUsed = true;
}
};
}
response.text = async function () {
ensureUnused();
markBodyUsed();
return readAllFromStream(stream, true);
};
response.arrayBuffer = async function () {
ensureUnused();
markBodyUsed();
const bytes = await readAllFromStream(stream, false);
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
};
response.blob = async function () {
ensureUnused();
markBodyUsed();
const contentType = this.headers && this.headers.get ? (this.headers.get("content-type") || "") : "";
// Response.blob() is a materializing body consumer. Keeping the live
// response stream behind an empty one-shot Blob makes retry, range and a
// second object-URL reader silently produce an empty file.
const bytes = await readAllFromStream(stream, false);
return new Blob([bytes], { type: contentType });
};
response.json = async function () {
const text = await this.text();
return JSON.parse(text);
};
return response;
}
if (typeof globalThis.Response === "function") {
const responseProto = globalThis.Response.prototype;
const bodyDescriptor = Object.getOwnPropertyDescriptor(responseProto, "body");
if (!bodyDescriptor || typeof bodyDescriptor.get !== "function") {
Object.defineProperty(responseProto, "body", {
configurable: true,
enumerable: true,
get() {
if (this.__gopeedBodyStream) {
return this.__gopeedBodyStream;
}
if (!this.__gopeedBodyStream) {
this.__gopeedBodyStream = createBodyReadableStream(this);
}
return this.__gopeedBodyStream;
}
});
}
}
const blobReaders = new Map();
function getValueTypeName(value) {
@@ -883,82 +807,4 @@
}
};
const originalFetch = typeof globalThis.fetch === "function"
? globalThis.fetch.bind(globalThis)
: null;
if (typeof fetchOpen === "function") {
globalThis.fetch = async function (input, init) {
const request = new Request(input, init);
if (originalFetch && (request.method === "HEAD" || request.redirect === "manual" || request.redirect === "error")) {
return originalFetch(input, init);
}
let body = null;
if (request._bodyFormData) {
body = request._bodyFormData;
} else if (request._bodyArrayBuffer) {
body = request._bodyArrayBuffer;
} else if (request._bodyBlob) {
body = await request._bodyBlob.arrayBuffer();
} else if (request._bodyInit != null && typeof request._bodyInit === "object") {
body = request._bodyInit;
} else if (request._bodyText != null) {
body = request._bodyText;
} else if (request._bodyInit != null) {
body = request._bodyInit;
}
const headers = [];
request.headers.forEach((value, key) => {
headers.push([key, value]);
});
let meta;
try {
meta = await fetchOpen({
url: request.url,
method: request.method,
headers,
body,
redirect: request.redirect,
credentials: request.credentials
});
} catch (error) {
throw error instanceof Error ? error : new TypeError(String(error));
}
const stream = new ReadableStream({
async pull(controller) {
let chunk;
try {
chunk = await fetchRead(meta.id, 64 * 1024);
} catch (error) {
fetchClose(meta.id);
controller.error(error instanceof Error ? error : new TypeError(String(error)));
return;
}
if (chunk == null) {
fetchClose(meta.id);
controller.close();
return;
}
const bytes = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
if (bytes.byteLength === 0) {
fetchClose(meta.id);
controller.close();
return;
}
controller.enqueue(bytes);
},
cancel(reason) {
fetchAbort(meta.id, reason == null ? "" : String(reason));
}
});
const response = new Response(null, {
status: meta.status,
statusText: meta.statusText,
headers: meta.headers,
url: meta.url
});
return attachResponseStreaming(response, stream);
};
globalThis.fetch.__gopeedOriginalFetch = originalFetch;
}
})();
+6 -473
View File
@@ -1,482 +1,15 @@
package xhr
import (
"bytes"
"errors"
"io"
"mime/multipart"
"net"
"net/http"
"net/url"
"strings"
"time"
_ "embed"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
"github.com/GopeedLab/gopeed/pkg/download/engine/util"
"github.com/dop251/goja"
"github.com/imroc/req/v3"
)
const (
eventLoad = "load"
eventReadystatechange = "readystatechange"
eventProgress = "progress"
eventAbort = "abort"
eventError = "error"
eventTimeout = "timeout"
)
//go:embed xhr.js
var script string
const (
redirectError = "error"
redirectFollow = "follow"
redirectManual = "manual"
)
type ProgressEvent struct {
Type string `json:"type"`
LengthComputable bool `json:"lengthComputable"`
Loaded int64 `json:"loaded"`
Total int64 `json:"total"`
}
type EventProp struct {
eventListeners map[string]func(event *ProgressEvent)
Onload func(event *ProgressEvent) `json:"onload"`
Onprogress func(event *ProgressEvent) `json:"onprogress"`
Onabort func(event *ProgressEvent) `json:"onabort"`
Onerror func(event *ProgressEvent) `json:"onerror"`
Ontimeout func(event *ProgressEvent) `json:"ontimeout"`
}
func (ep *EventProp) AddEventListener(event string, cb func(event *ProgressEvent)) {
ep.eventListeners[event] = cb
}
func (ep *EventProp) RemoveEventListener(event string) {
delete(ep.eventListeners, event)
}
func (ep *EventProp) callOnload() {
event := &ProgressEvent{
Type: eventLoad,
LengthComputable: false,
}
if ep.Onload != nil {
ep.Onload(event)
}
ep.callEventListener(event)
}
func (ep *EventProp) callOnprogress(loaded, total int64) {
event := &ProgressEvent{
Type: eventProgress,
LengthComputable: true,
Loaded: loaded,
Total: total,
}
if ep.Onprogress != nil {
ep.Onprogress(event)
}
ep.callEventListener(event)
}
func (ep *EventProp) callOnabort() {
event := &ProgressEvent{
Type: eventAbort,
LengthComputable: false,
}
if ep.Onabort != nil {
ep.Onabort(event)
}
ep.callEventListener(event)
}
func (ep *EventProp) callOnerror() {
event := &ProgressEvent{
Type: eventError,
LengthComputable: false,
}
if ep.Onerror != nil {
ep.Onerror(event)
}
ep.callEventListener(event)
}
func (ep *EventProp) callOntimeout() {
event := &ProgressEvent{
Type: eventTimeout,
LengthComputable: false,
}
if ep.Ontimeout != nil {
ep.Ontimeout(event)
}
ep.callEventListener(event)
}
func (ep *EventProp) callEventListener(event *ProgressEvent) {
if cb, ok := ep.eventListeners[event.Type]; ok {
cb(event)
}
}
type XMLHttpRequestUpload struct {
*EventProp
}
type XMLHttpRequest struct {
method string
url string
requestHeaders http.Header
responseHeaders http.Header
aborted bool
client *req.Client
fingerprint string
WithCredentials bool `json:"withCredentials"`
Upload *XMLHttpRequestUpload `json:"upload"`
Timeout int `json:"timeout"`
ReadyState int `json:"readyState"`
Status int `json:"status"`
StatusText string `json:"statusText"`
Response string `json:"response"`
ResponseText string `json:"responseText"`
// https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/responseURL
// https://xhr.spec.whatwg.org/#the-responseurl-attribute
ResponseUrl string `json:"responseURL"`
// extend fetch redirect
// https://developer.mozilla.org/en-US/docs/Web/API/RequestInit#redirect
// https://fetch.spec.whatwg.org/#concept-request-redirect-mode
Redirect string `json:"redirect"`
*EventProp
Onreadystatechange func(event *ProgressEvent) `json:"onreadystatechange"`
}
func (xhr *XMLHttpRequest) Open(method, url string) {
xhr.method = method
xhr.url = url
xhr.requestHeaders = make(http.Header)
xhr.responseHeaders = make(http.Header)
xhr.doReadystatechange(1)
}
func (xhr *XMLHttpRequest) SetRequestHeader(key, value string) {
xhr.requestHeaders.Add(key, value)
}
func (xhr *XMLHttpRequest) Send(data goja.Value) {
setFingerprint(xhr.client, xhr.fingerprint)
d := xhr.parseData(data)
var (
contentType string
contentLength int64
isStringBody bool
)
// Create request using req library
reqBuilder := xhr.client.R()
// Set headers first
if xhr.requestHeaders != nil {
for key, values := range xhr.requestHeaders {
if len(values) > 0 {
// Merge multiple values with comma separator (HTTP standard)
mergedValue := strings.Join(values, ", ")
reqBuilder.SetHeader(key, mergedValue)
}
}
}
// Handle request body
if d != nil && xhr.method != "GET" && xhr.method != "HEAD" {
switch v := d.(type) {
case string:
reqBuilder.SetBody(v)
contentType = "text/plain;charset=UTF-8"
contentLength = int64(len(v))
isStringBody = true
case *file.File:
reqBuilder.SetBody(v.Reader)
contentType = "application/octet-stream"
contentLength = v.Size
case *formdata.FormData:
pr, pw := io.Pipe()
mw := NewMultipart(pw)
for _, e := range v.Entries() {
arr := e.([]any)
k := arr[0].(string)
v := arr[1]
switch v := v.(type) {
case string:
mw.WriteField(k, v)
case *file.File:
mw.WriteFile(k, v)
}
}
go func() {
defer pw.Close()
defer mw.Close()
mw.Send()
}()
reqBuilder.SetBody(pr)
contentType = mw.FormDataContentType()
contentLength = mw.Size()
}
}
// Only string body can specify Content-Type header by user
if contentType != "" && (!isStringBody || xhr.requestHeaders.Get("Content-Type") == "") {
reqBuilder.SetHeader("Content-Type", contentType)
}
// Set timeout
if xhr.Timeout > 0 {
xhr.client.SetTimeout(time.Duration(xhr.Timeout) * time.Millisecond)
}
// Configure redirect behavior
xhr.client.SetRedirectPolicy(func(req *http.Request, via []*http.Request) error {
if xhr.Redirect == redirectManual {
return http.ErrUseLastResponse
}
if xhr.Redirect == redirectError {
return errors.New("redirect failed")
}
if len(via) > 20 {
return errors.New("too many redirects")
}
return nil
})
// Execute request
resp, err := reqBuilder.Send(xhr.method, xhr.url)
if err != nil {
// handle timeout error
var ne net.Error
if errors.As(err, &ne) && ne.Timeout() {
if xhr.Timeout > 0 {
xhr.Upload.callOntimeout()
xhr.callOntimeout()
}
return
}
xhr.Upload.callOnerror()
xhr.callOnerror()
return
}
xhr.Upload.callOnprogress(contentLength, contentLength)
if !xhr.aborted {
xhr.Upload.callOnload()
}
// Set response URL (final URL after redirects)
if resp.Response != nil && resp.Response.Request != nil && resp.Response.Request.URL != nil {
responseUrl := resp.Response.Request.URL
responseUrl.Fragment = ""
xhr.ResponseUrl = responseUrl.String()
} else {
xhr.ResponseUrl = xhr.url
}
// Set response headers
xhr.responseHeaders = resp.Header
xhr.Status = resp.StatusCode
xhr.StatusText = resp.Status
xhr.doReadystatechange(2)
bodyBytes := resp.Bytes()
xhr.doReadystatechange(3)
xhr.Response = string(bodyBytes)
xhr.ResponseText = xhr.Response
xhr.doReadystatechange(4)
respBodyLen := int64(len(bodyBytes))
xhr.callOnprogress(respBodyLen, respBodyLen)
if !xhr.aborted {
xhr.callOnload()
}
}
func (xhr *XMLHttpRequest) Abort() {
xhr.doReadystatechange(0)
xhr.aborted = true
xhr.Upload.callOnabort()
xhr.callOnabort()
}
func (xhr *XMLHttpRequest) GetResponseHeader(key string) string {
if xhr.responseHeaders == nil {
return ""
}
return strings.Join(xhr.responseHeaders.Values(key), ", ")
}
func (xhr *XMLHttpRequest) GetAllResponseHeaders() string {
var buf bytes.Buffer
for k, v := range xhr.responseHeaders {
buf.WriteString(k)
buf.WriteString(": ")
buf.WriteString(strings.Join(v, ", "))
buf.WriteString("\r\n")
}
return buf.String()
}
func (xhr *XMLHttpRequest) callOnreadystatechange() {
event := &ProgressEvent{
Type: eventReadystatechange,
LengthComputable: false,
}
if xhr.Onreadystatechange != nil {
xhr.Onreadystatechange(event)
}
xhr.callEventListener(event)
}
func (xhr *XMLHttpRequest) doReadystatechange(state int) {
if xhr.aborted {
return
}
xhr.ReadyState = state
xhr.callOnreadystatechange()
}
// parse js data to go struct
func (xhr *XMLHttpRequest) parseData(data goja.Value) any {
// check if data is null or undefined
if data == nil || goja.IsNull(data) || goja.IsUndefined(data) || goja.IsNaN(data) {
return nil
}
// check if data is File
f, ok := data.Export().(*file.File)
if ok {
return f
}
// check if data is FormData
fd, ok := data.Export().(*formdata.FormData)
if ok {
return fd
}
// otherwise, return data as string
return data.String()
}
func Enable(runtime *goja.Runtime, proxyHandler func(r *http.Request) (*url.URL, error)) error {
progressEvent := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
if len(call.Arguments) < 1 {
util.ThrowTypeError(runtime, "Failed to construct 'ProgressEvent': 1 argument required, but only 0 present.")
}
instance := &ProgressEvent{
Type: call.Argument(0).String(),
}
instanceValue := runtime.ToValue(instance).(*goja.Object)
instanceValue.SetPrototype(call.This.Prototype())
return instanceValue
})
xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
// Create req client with proxy support
client := req.C()
if proxyHandler != nil {
client.SetProxy(proxyHandler)
}
instance := &XMLHttpRequest{
client: client,
fingerprint: util.SafeGet[string](runtime, FingerprintMagicKey),
Upload: &XMLHttpRequestUpload{
EventProp: &EventProp{
eventListeners: make(map[string]func(event *ProgressEvent)),
},
},
EventProp: &EventProp{
eventListeners: make(map[string]func(event *ProgressEvent)),
},
}
instanceValue := runtime.ToValue(instance).(*goja.Object)
instanceValue.SetPrototype(call.This.Prototype())
return instanceValue
})
runtime.Set("__gopeed_setFingerprint", func(fingerprint string) {
runtime.Set(FingerprintMagicKey, fingerprint)
})
if err := runtime.Set("ProgressEvent", progressEvent); err != nil {
return err
}
if err := runtime.Set("XMLHttpRequest", xhr); err != nil {
return err
}
return nil
}
// Wrap multipart.Writer and stat content length
type multipartWrapper struct {
statBuffer *bytes.Buffer
statWriter *multipart.Writer
writer *multipart.Writer
fields map[string]any
}
func NewMultipart(w io.Writer) *multipartWrapper {
var buf bytes.Buffer
return &multipartWrapper{
statBuffer: &buf,
statWriter: multipart.NewWriter(&buf),
writer: multipart.NewWriter(w),
fields: make(map[string]any),
}
}
func (w *multipartWrapper) WriteField(fieldname string, value string) error {
w.fields[fieldname] = value
return w.statWriter.WriteField(fieldname, value)
}
func (w *multipartWrapper) WriteFile(fieldname string, file *file.File) error {
w.fields[fieldname] = file
_, err := w.statWriter.CreateFormFile(fieldname, file.Name)
if err != nil {
return err
}
return nil
}
func (w *multipartWrapper) Size() int64 {
w.statWriter.Close()
size := int64(w.statBuffer.Len())
for _, v := range w.fields {
switch v := v.(type) {
case *file.File:
size += v.Size
}
}
return size
}
func (w *multipartWrapper) Send() error {
for k, v := range w.fields {
switch v := v.(type) {
case string:
if err := w.writer.WriteField(k, v); err != nil {
return err
}
case *file.File:
fw, err := w.writer.CreateFormFile(k, v.Name)
if err != nil {
return err
}
if _, err = io.Copy(fw, v); err != nil {
return err
}
}
}
return nil
}
func (w *multipartWrapper) FormDataContentType() string {
return w.writer.FormDataContentType()
}
func (w *multipartWrapper) Close() error {
return w.writer.Close()
func Enable(runtime *goja.Runtime) error {
_, err := runtime.RunString(script)
return err
}
@@ -1,24 +0,0 @@
package xhr
import "github.com/imroc/req/v3"
type Fingerprint string
const (
FingerprintMagicKey = "__gopeed_xhr_fingerprint"
fingerprintChrome = "chrome"
fingerprintFirefox = "firefox"
fingerprintSafari = "safari"
)
func setFingerprint(client *req.Client, fingerprint string) {
switch fingerprint {
case fingerprintChrome:
client.ImpersonateChrome()
case fingerprintFirefox:
client.ImpersonateFirefox()
case fingerprintSafari:
client.ImpersonateSafari()
}
}
+302
View File
@@ -0,0 +1,302 @@
(function () {
if (typeof globalThis.ProgressEvent === "undefined") {
globalThis.ProgressEvent = class ProgressEvent extends Event {
constructor(type, init) {
super(type);
init = init || {};
this.lengthComputable = Boolean(init.lengthComputable);
this.loaded = Number(init.loaded) || 0;
this.total = Number(init.total) || 0;
}
};
}
function createProgressEvent(type, loaded, total) {
const lengthComputable = Number.isFinite(total) && total >= 0;
return new ProgressEvent(type, {
lengthComputable,
loaded: loaded || 0,
total: lengthComputable ? total : 0,
});
}
function canonicalHeaderName(name) {
return String(name).toLowerCase().split("-").map(function (part) {
return part ? part.charAt(0).toUpperCase() + part.slice(1) : part;
}).join("-");
}
function bodySize(body) {
if (body == null) return 0;
if (typeof body === "string") return new TextEncoder().encode(body).byteLength;
if (body instanceof ArrayBuffer) return body.byteLength;
if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView && ArrayBuffer.isView(body)) {
return body.byteLength;
}
if (typeof Blob !== "undefined" && body instanceof Blob) return Number(body.size) || 0;
if (body && Number.isFinite(Number(body.size))) return Number(body.size);
return 0;
}
class XMLHttpRequestUpload extends EventTarget {
constructor() {
super();
this.onloadstart = null;
this.onprogress = null;
this.onabort = null;
this.onerror = null;
this.onload = null;
this.ontimeout = null;
this.onloadend = null;
}
_emit(type, loaded, total) {
const event = createProgressEvent(type, loaded, total);
const handler = this["on" + type];
if (typeof handler === "function") handler.call(this, event);
this.dispatchEvent(event);
}
}
class XMLHttpRequest extends EventTarget {
constructor() {
super();
this.readyState = XMLHttpRequest.UNSENT;
this.response = "";
this.responseText = "";
this.responseType = "";
this.responseURL = "";
this.responseXML = null;
this.status = 0;
this.statusText = "";
this.timeout = 0;
this.withCredentials = false;
this.upload = new XMLHttpRequestUpload();
this.redirect = "follow";
this.onreadystatechange = null;
this.onloadstart = null;
this.onprogress = null;
this.onabort = null;
this.onerror = null;
this.onload = null;
this.ontimeout = null;
this.onloadend = null;
this._method = "GET";
this._url = "";
this._requestHeaders = new Headers();
this._responseHeaders = null;
this._controller = null;
this._sendStarted = false;
this._aborted = false;
this._timedOut = false;
this._timeoutID = null;
}
open(method, url, async) {
if (arguments.length < 2) {
throw new TypeError("XMLHttpRequest.open requires at least 2 arguments");
}
if (async === false) {
throw new DOMException("Synchronous XMLHttpRequest is not supported", "NotSupportedError");
}
this._method = String(method).toUpperCase();
this._url = String(url);
this._requestHeaders = new Headers();
this._responseHeaders = null;
this._sendStarted = false;
this._aborted = false;
this._timedOut = false;
this.response = "";
this.responseText = "";
this.responseURL = "";
this.status = 0;
this.statusText = "";
this._setReadyState(XMLHttpRequest.OPENED);
}
setRequestHeader(name, value) {
if (this.readyState !== XMLHttpRequest.OPENED || this._sendStarted) {
throw new DOMException("XMLHttpRequest is not opened", "InvalidStateError");
}
this._requestHeaders.append(name, value);
}
getResponseHeader(name) {
if (!this._responseHeaders || this.readyState < XMLHttpRequest.HEADERS_RECEIVED) return null;
return this._responseHeaders.get(name);
}
getAllResponseHeaders() {
if (!this._responseHeaders || this.readyState < XMLHttpRequest.HEADERS_RECEIVED) return "";
let result = "";
this._responseHeaders.forEach(function (value, name) {
result += canonicalHeaderName(name) + ": " + value + "\r\n";
});
return result;
}
overrideMimeType() {
// Kept as a compatibility no-op until MIME decoding is configurable.
}
send(body) {
if (this.readyState !== XMLHttpRequest.OPENED || this._sendStarted) {
throw new DOMException("XMLHttpRequest is not opened", "InvalidStateError");
}
this._sendStarted = true;
this._aborted = false;
this._timedOut = false;
this._controller = new AbortController();
const uploadTotal = bodySize(body);
this._emit("loadstart", 0, -1);
this.upload._emit("loadstart", 0, uploadTotal);
if (this.timeout > 0) {
this._timeoutID = setTimeout(() => {
this._timedOut = true;
this._controller.abort();
}, this.timeout);
}
const options = {
method: this._method,
headers: this._requestHeaders,
redirect: this.redirect || "follow",
credentials: this.withCredentials ? "include" : "same-origin",
signal: this._controller.signal,
};
if (this._method !== "GET" && this._method !== "HEAD" && body != null) {
options.body = body;
}
Promise.resolve().then(() => globalThis.fetch(this._url, options)).then(async (response) => {
if (this._aborted || this._timedOut) return;
this.upload._emit("progress", uploadTotal, uploadTotal);
this.upload._emit("load", uploadTotal, uploadTotal);
this.upload._emit("loadend", uploadTotal, uploadTotal);
this.status = response.status;
this.statusText = response.statusText;
this.responseURL = response.url || this._url;
this._responseHeaders = response.headers;
this._setReadyState(XMLHttpRequest.HEADERS_RECEIVED);
const contentLengthValue = response.headers.get("content-length");
const contentLength = contentLengthValue == null ? -1 : Number(contentLengthValue);
const chunks = [];
let loaded = 0;
if (response.body) {
const reader = response.body.getReader();
try {
while (true) {
const item = await reader.read();
if (item.done) break;
const chunk = item.value instanceof Uint8Array ? item.value : new Uint8Array(item.value);
chunks.push(chunk);
loaded += chunk.byteLength;
this._setReadyState(XMLHttpRequest.LOADING);
this._emit("progress", loaded, contentLength);
}
} finally {
if (typeof reader.releaseLock === "function") reader.releaseLock();
}
}
const bytes = new Uint8Array(loaded);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
const text = new TextDecoder().decode(bytes);
switch (this.responseType) {
case "arraybuffer":
this.response = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
break;
case "blob":
this.response = new Blob([bytes], { type: response.headers.get("content-type") || "" });
break;
case "json":
try {
this.response = text === "" ? null : JSON.parse(text);
} catch (_) {
this.response = null;
}
break;
case "":
case "text":
default:
this.response = text;
this.responseText = text;
break;
}
this._clearTimeout();
this._setReadyState(XMLHttpRequest.DONE);
this._emit("load", loaded, contentLength);
this._emit("loadend", loaded, contentLength);
}).catch((error) => {
this._clearTimeout();
if (this._timedOut) {
this.status = 0;
this._setReadyState(XMLHttpRequest.DONE);
this.upload._emit("timeout", 0, uploadTotal);
this._emit("timeout", 0, -1);
this._emit("loadend", 0, -1);
return;
}
if (this._aborted || (error && error.name === "AbortError")) {
this.status = 0;
this._setReadyState(XMLHttpRequest.DONE);
this.upload._emit("abort", 0, uploadTotal);
this._emit("abort", 0, -1);
this._emit("loadend", 0, -1);
return;
}
this.status = 0;
this._setReadyState(XMLHttpRequest.DONE);
this.upload._emit("error", 0, uploadTotal);
this._emit("error", 0, -1);
this._emit("loadend", 0, -1);
});
}
abort() {
if (!this._sendStarted || this.readyState === XMLHttpRequest.DONE) return;
this._aborted = true;
this._clearTimeout();
if (this._controller) this._controller.abort();
}
_clearTimeout() {
if (this._timeoutID != null) {
clearTimeout(this._timeoutID);
this._timeoutID = null;
}
}
_setReadyState(state) {
this.readyState = state;
this._emit("readystatechange", 0, -1);
}
_emit(type, loaded, total) {
const event = createProgressEvent(type, loaded, total);
const handler = this["on" + type];
if (typeof handler === "function") handler.call(this, event);
this.dispatchEvent(event);
}
}
XMLHttpRequest.UNSENT = 0;
XMLHttpRequest.OPENED = 1;
XMLHttpRequest.HEADERS_RECEIVED = 2;
XMLHttpRequest.LOADING = 3;
XMLHttpRequest.DONE = 4;
XMLHttpRequest.prototype.UNSENT = 0;
XMLHttpRequest.prototype.OPENED = 1;
XMLHttpRequest.prototype.HEADERS_RECEIVED = 2;
XMLHttpRequest.prototype.LOADING = 3;
XMLHttpRequest.prototype.DONE = 4;
globalThis.XMLHttpRequest = XMLHttpRequest;
})();
+1 -1
View File
@@ -1 +1 @@
(()=>{var e={847(e,t,r){var n,o;n=function(){"use strict";function e(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=Array(t);r<t;r++)n[r]=e[r];return n}function t(e,t,r){return t=l(t),function(e,t){if(t&&("object"==typeof t||"function"==typeof t))return t;if(void 0!==t)throw new TypeError("Derived constructors may only return object or undefined");return function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}(e)}(e,c()?Reflect.construct(t,r||[],l(e).constructor):t.apply(e,r))}function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){for(var r=0;r<t.length;r++){var n=t[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,f(n.key),n)}}function i(e,t,r){return t&&o(e.prototype,t),r&&o(e,r),Object.defineProperty(e,"prototype",{writable:!1}),e}function a(t,r){var n="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!n){if(Array.isArray(t)||(n=function(t,r){if(t){if("string"==typeof t)return e(t,r);var n={}.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?e(t,r):void 0}}(t))||r&&t&&"number"==typeof t.length){n&&(t=n);var o=0,i=function(){};return{s:i,n:function(){return o>=t.length?{done:!0}:{done:!1,value:t[o++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,s=!0,l=!1;return{s:function(){n=n.call(t)},n:function(){var e=n.next();return s=e.done,e},e:function(e){l=!0,a=e},f:function(){try{s||null==n.return||n.return()}finally{if(l)throw a}}}}function s(){return s="undefined"!=typeof Reflect&&Reflect.get?Reflect.get.bind():function(e,t,r){var n=function(e,t){for(;!{}.hasOwnProperty.call(e,t)&&null!==(e=l(e)););return e}(e,t);if(n){var o=Object.getOwnPropertyDescriptor(n,t);return o.get?o.get.call(arguments.length<3?e:r):o.value}},s.apply(null,arguments)}function l(e){return l=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(e){return e.__proto__||Object.getPrototypeOf(e)},l(e)}function c(){try{var e=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){}))}catch(e){}return(c=function(){return!!e})()}function u(e,t){return u=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},u(e,t)}function f(e){var t=function(e,t){if("object"!=typeof e||!e)return e;var r=e[Symbol.toPrimitive];if(void 0!==r){var n=r.call(e,t||"default");if("object"!=typeof n)return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==typeof t?t:t+""}!function(e){e.AbortSignal,e.AbortController}("undefined"!=typeof self?self:r.g);var p=function(){return i(function e(){n(this,e),Object.defineProperty(this,"listeners",{value:{},writable:!0,configurable:!0})},[{key:"addEventListener",value:function(e,t,r){e in this.listeners||(this.listeners[e]=[]),this.listeners[e].push({callback:t,options:r})}},{key:"removeEventListener",value:function(e,t){if(e in this.listeners)for(var r=this.listeners[e],n=0,o=r.length;n<o;n++)if(r[n].callback===t)return void r.splice(n,1)}},{key:"dispatchEvent",value:function(e){var t=this;if(e.type in this.listeners){for(var r=this.listeners[e.type].slice(),n=function(){var n=r[o];try{n.callback.call(t,e)}catch(e){Promise.resolve().then(function(){throw e})}n.options&&n.options.once&&t.removeEventListener(e.type,n.callback)},o=0,i=r.length;o<i;o++)n();return!e.defaultPrevented}}}])}(),d=function(e){function r(){var e;return n(this,r),(e=t(this,r)).listeners||p.call(e),Object.defineProperty(e,"aborted",{value:!1,writable:!0,configurable:!0}),Object.defineProperty(e,"onabort",{value:null,writable:!0,configurable:!0}),Object.defineProperty(e,"reason",{value:void 0,writable:!0,configurable:!0}),e}return function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),Object.defineProperty(e,"prototype",{writable:!1}),t&&u(e,t)}(r,e),i(r,[{key:"toString",value:function(){return"[object AbortSignal]"}},{key:"dispatchEvent",value:function(e){var t,n,o,i,a;"abort"===e.type&&(this.aborted=!0,"function"==typeof this.onabort&&this.onabort.call(this,e)),(t=r,n="dispatchEvent",o=this,a=s(l(1&(i=3)?t.prototype:t),n,o),2&i&&"function"==typeof a?function(e){return a.apply(o,e)}:a)([e])}},{key:"throwIfAborted",value:function(){var e=this.aborted,t=this.reason;if(e)throw void 0===t?"Aborted":t}}],[{key:"timeout",value:function(e){var t=new h;return setTimeout(function(){return t.abort(new DOMException("This signal is timeout in ".concat(e,"ms"),"TimeoutError"))},e),t.signal}},{keLine truncated
(()=>{var e={847(e,t,n){var r,o;r=function(){"use strict";function e(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n<t;n++)r[n]=e[n];return r}function t(e,t,n){return t=s(t),function(e,t){if(t&&("object"==typeof t||"function"==typeof t))return t;if(void 0!==t)throw new TypeError("Derived constructors may only return object or undefined");return function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}(e)}(e,c()?Reflect.construct(t,n||[],s(e).constructor):t.apply(e,n))}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,f(r.key),r)}}function a(e,t,n){return t&&o(e.prototype,t),n&&o(e,n),Object.defineProperty(e,"prototype",{writable:!1}),e}function i(t,n){var r="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!r){if(Array.isArray(t)||(r=function(t,n){if(t){if("string"==typeof t)return e(t,n);var r={}.toString.call(t).slice(8,-1);return"Object"===r&&t.constructor&&(r=t.constructor.name),"Map"===r||"Set"===r?Array.from(t):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?e(t,n):void 0}}(t))||n&&t&&"number"==typeof t.length){r&&(t=r);var o=0,a=function(){};return{s:a,n:function(){return o>=t.length?{done:!0}:{done:!1,value:t[o++]}},e:function(e){throw e},f:a}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,l=!0,s=!1;return{s:function(){r=r.call(t)},n:function(){var e=r.next();return l=e.done,e},e:function(e){s=!0,i=e},f:function(){try{l||null==r.return||r.return()}finally{if(s)throw i}}}}function l(){return l="undefined"!=typeof Reflect&&Reflect.get?Reflect.get.bind():function(e,t,n){var r=function(e,t){for(;!{}.hasOwnProperty.call(e,t)&&null!==(e=s(e)););return e}(e,t);if(r){var o=Object.getOwnPropertyDescriptor(r,t);return o.get?o.get.call(arguments.length<3?e:n):o.value}},l.apply(null,arguments)}function s(e){return s=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(e){return e.__proto__||Object.getPrototypeOf(e)},s(e)}function c(){try{var e=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){}))}catch(e){}return(c=function(){return!!e})()}function u(e,t){return u=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},u(e,t)}function f(e){var t=function(e,t){if("object"!=typeof e||!e)return e;var n=e[Symbol.toPrimitive];if(void 0!==n){var r=n.call(e,t||"default");if("object"!=typeof r)return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==typeof t?t:t+""}!function(e){e.AbortSignal,e.AbortController}("undefined"!=typeof self?self:n.g);var p=function(){return a(function e(){r(this,e),Object.defineProperty(this,"listeners",{value:{},writable:!0,configurable:!0})},[{key:"addEventListener",value:function(e,t,n){e in this.listeners||(this.listeners[e]=[]),this.listeners[e].push({callback:t,options:n})}},{key:"removeEventListener",value:function(e,t){if(e in this.listeners)for(var n=this.listeners[e],r=0,o=n.length;r<o;r++)if(n[r].callback===t)return void n.splice(r,1)}},{key:"dispatchEvent",value:function(e){var t=this;if(e.type in this.listeners){for(var n=this.listeners[e.type].slice(),r=function(){var r=n[o];try{r.callback.call(t,e)}catch(e){Promise.resolve().then(function(){throw e})}r.options&&r.options.once&&t.removeEventListener(e.type,r.callback)},o=0,a=n.length;o<a;o++)r();return!e.defaultPrevented}}}])}(),b=function(e){function n(){var e;return r(this,n),(e=t(this,n)).listeners||p.call(e),Object.defineProperty(e,"aborted",{value:!1,writable:!0,configurable:!0}),Object.defineProperty(e,"onabort",{value:null,writable:!0,configurable:!0}),Object.defineProperty(e,"reason",{value:void 0,writable:!0,configurable:!0}),e}return function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),Object.defineProperty(e,"prototype",{writable:!1}),t&&u(e,t)}(n,e),a(n,[{key:"toString",value:function(){return"[object AbortSignal]"}},{key:"dispatchEvent",value:function(e){var t,r,o,a,i;"abort"===e.type&&(this.aborted=!0,"function"==typeof this.onabort&&this.onabort.call(this,e)),(t=n,r="dispatchEvent",o=this,i=l(s(1&(a=3)?t.prototype:t),r,o),2&a&&"function"==typeof i?function(e){return i.apply(o,e)}:i)([e])}},{key:"throwIfAborted",value:function(){var e=this.aborted,t=this.reason;if(e)throw void 0===t?"Aborted":t}}],[{key:"timeout",value:function(e){var t=new d;return setTimeout(function(){return t.abort(new DOMException("This signal is timeout in ".concat(e,"ms"),"TimeoutError"))},e),t.signal}},{keLine truncated
+1 -8
View File
@@ -13,8 +13,7 @@
"abortcontroller-polyfill": "^1.7.8",
"blob-polyfill": "^7.0.20220408",
"event-target-shim": "^6.0.2",
"fastestsmallesttextencoderdecoder": "^1.0.22",
"whatwg-fetch": "^3.6.20"
"fastestsmallesttextencoderdecoder": "^1.0.22"
},
"devDependencies": {
"patch-package": "^8.0.0",
@@ -2106,12 +2105,6 @@
"node": ">=10.13.0"
}
},
"node_modules/whatwg-fetch": {
"version": "3.6.20",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
"license": "MIT"
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+1 -2
View File
@@ -21,7 +21,6 @@
"abortcontroller-polyfill": "^1.7.8",
"blob-polyfill": "^7.0.20220408",
"event-target-shim": "^6.0.2",
"fastestsmallesttextencoderdecoder": "^1.0.22",
"whatwg-fetch": "^3.6.20"
"fastestsmallesttextencoderdecoder": "^1.0.22"
}
}
@@ -1,46 +0,0 @@
diff --git a/node_modules/whatwg-fetch/dist/fetch.umd.js b/node_modules/whatwg-fetch/dist/fetch.umd.js
index 7a0d852..604691e 100644
--- a/node_modules/whatwg-fetch/dist/fetch.umd.js
+++ b/node_modules/whatwg-fetch/dist/fetch.umd.js
@@ -394,6 +394,7 @@
}
}());
this.referrer = null;
+ this.redirect = options.redirect || 'follow'
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
throw new TypeError('Body not allowed for GET or HEAD requests')
@@ -606,6 +607,10 @@
}
}
+ if ('redirect' in xhr) {
+ xhr.redirect = request.redirect
+ }
+
if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) {
var names = [];
Object.getOwnPropertyNames(init.headers).forEach(function(name) {
diff --git a/node_modules/whatwg-fetch/fetch.js b/node_modules/whatwg-fetch/fetch.js
index f39a983..d1fc903 100644
--- a/node_modules/whatwg-fetch/fetch.js
+++ b/node_modules/whatwg-fetch/fetch.js
@@ -388,6 +388,7 @@ export function Request(input, options) {
}
}());
this.referrer = null
+ this.redirect = options.redirect || 'follow'
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
throw new TypeError('Body not allowed for GET or HEAD requests')
@@ -600,6 +601,10 @@ export function fetch(input, init) {
}
}
+ if ('redirect' in xhr) {
+ xhr.redirect = request.redirect
+ }
+
if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) {
var names = [];
Object.getOwnPropertyNames(init.headers).forEach(function(name) {
@@ -1 +0,0 @@
import 'whatwg-fetch'
@@ -4,4 +4,3 @@ import "./blob/index.js"
import "./crypto/index.js"
// polyfill TextEncoder
import 'fastestsmallesttextencoderdecoder';
import "./fetch/index.js"
+11
View File
@@ -0,0 +1,11 @@
# The 3-Clause BSD License
Copyright © web-platform-tests contributors
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+28
View File
@@ -0,0 +1,28 @@
# Web Platform Tests provenance
The files under `fetch/` are copied from the official
[web-platform-tests/wpt](https://github.com/web-platform-tests/wpt) repository.
- Upstream commit: `ce5d9e7e28b27528213bceea40d9e78462487105`
- License: BSD-3-Clause; see `LICENSE.md`
- Canonical Fetch tests: `fetch/`
Gopeed executes applicable worker-style Fetch API tests at the extension
JavaScript boundary. The checked-in runner currently executes unchanged WPT
tests for Headers, no-CORS header guards, Request construction and body
consumption, ReadableStream bodies, keepalive, Request errors, Response
construction and body consumption, immutable network response headers, and
the Response static constructors.
Passing this subset is an extension-worker Fetch API compatibility claim, not
a claim that Gopeed is a browser. Browser-only suites that require a document,
CORS origin enforcement or preflight, CSP, Mixed Content, Service Workers,
browser HTTP cache, navigation, browser authentication UI, or WPT's
multi-origin server infrastructure are intentionally excluded. Gopeed also
keeps one extension-specific behavior: `redirect: "manual"` exposes the HTTP
redirect response so download extensions can inspect `Location`, instead of
returning a browser `opaqueredirect` response.
`wpt_harness.js` is a small Goja adapter maintained by Gopeed; it is not copied
from WPT. Test logic below `fetch/` is unchanged; the checked-in copy may only
normalize a final newline.
@@ -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"]
]
@@ -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");
@@ -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");
@@ -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");
@@ -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")
@@ -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");
@@ -0,0 +1,51 @@
// META: global=window,worker
let forbiddenMethods = [
"TRACE",
"TRACK",
"CONNECT",
"trace",
"track",
"connect",
"\rtrace",
"\ttrack",
"\nconnect",
"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) {
test(() => {
let r = new Request("https://site.example/");
r.headers.append(overrideHeader, forbiddenMethod);
assert_false(r.headers.has(overrideHeader));
}, `header ${overrideHeader} is forbidden to use value ${forbiddenMethod}`);
}
}
let permittedValues = [
"GETTRACE",
"GET",
"\",TRACE\",",
];
for (permittedValue of permittedValues) {
for (overrideHeader of overrideHeaders) {
test(() => {
let r = new Request("https://site.example/");
r.headers.append(overrideHeader, permittedValue);
assert_equals(permittedValue, r.headers.get(overrideHeader));
}, `header ${overrideHeader} is allowed to use value ${permittedValue}`);
}
}
@@ -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");
});
}
@@ -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");
@@ -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");
@@ -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");
@@ -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.');
}
@@ -0,0 +1,22 @@
// META: global=window,worker
promise_test(async () => {
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode("hello"));
controller.close();
}
});
const r1 = new Request("https://example.com/", { method: "POST", body: stream, duplex: "half" });
const r2 = r1.clone();
assert_false(r2.bodyUsed, "clone should not be marked as used");
assert_not_equals(r2.body, null, "clone should have a body");
// Constructing a new Request from the clone should preserve the body.
const r3 = new Request(r2);
assert_not_equals(r3.body, null, "Request constructed from clone should have a body");
const text = await r3.text();
assert_equals(text, "hello", "body content should be preserved through clone() and new Request()");
}, "new Request(clone) preserves a ReadableStream body that came from clone()");
@@ -0,0 +1,37 @@
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");
promise_test(async function () {
const req1 = new Request("https://example.com/", {
body: "req1",
method: "POST",
});
const req2 = new Request("https://example.com/", req1);
const bodyText = await req2.text();
assert_equals(
bodyText,
"req1",
"The body of the second request should be the same as the first."
);
}, "Check that the body of a new request can be duplicated from an existing Request object");
@@ -0,0 +1,89 @@
// 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);
});
}
async function checkBodyBlob(test, request) {
const bodyAsBlob = await request.blob();
const body = await bodyAsBlob.text();
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);
@@ -0,0 +1,148 @@
// 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");
});
}
async function checkBodyBlob(request, expectedBody, checkContentType) {
const bodyAsBlob = await request.blob();
if (checkContentType)
assert_equals(bodyAsBlob.type, "text/plain", "Blob body type should be computed from the request Content-Type");
const body = await bodyAsBlob.text();
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 checkBodyBytes(request, expectedBody) {
return request.bytes().then(function(bodyAsUint8Array) {
assert_true(bodyAsUint8Array instanceof Uint8Array);
validateBufferFromString(bodyAsUint8Array.buffer, expectedBody, "Retrieve and verify request's body");
assert_true(request.bodyUsed, "body as bytes: 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 checkBodyBytes(request, expected);
}, "Consume " + bodyType + " request's body as bytes");
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(blob, textData, "bytes", checkBodyBytes);
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 + "'");
});
@@ -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"');
@@ -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");
@@ -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:[email protected]"],
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`
};
})
);
@@ -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");
@@ -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");
}
@@ -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");
@@ -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");
@@ -0,0 +1,146 @@
// 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.");
@@ -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');
@@ -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");
}
@@ -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);
}
@@ -0,0 +1,88 @@
// 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);
});
}
async function checkBodyBlob(test, response) {
const bodyAsBlob = await response.blob();
const body = await bodyAsBlob.text();
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);
@@ -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`);
@@ -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);
});
Loaded 50 of 68 files, more files were not shown because too many files have changed in this diff. Show more