feat(extension): expose request mutation methods on task.meta.req (#1538)

This commit is contained in:
Levi authored and GitHub committed 2026-09-23 09:33:28 +08:00
1 parent 6e60889ce8
commit f0dab90e09
13 files changed
+523 -41

No files matched your search

-18
View File
@@ -25,24 +25,6 @@ type Request struct {
SkipVerifyCert bool `json:"skipVerifyCert"`
}
// SetLabels replaces all labels on the request.
func (r *Request) SetLabels(labels map[string]string) {
r.Labels = labels
}
// PutLabel sets a label on the request.
func (r *Request) PutLabel(key, value string) {
if r.Labels == nil {
r.Labels = make(map[string]string)
}
r.Labels[key] = value
}
// DelLabel deletes a label from the request.
func (r *Request) DelLabel(key string) {
delete(r.Labels, key)
}
func (r *Request) Validate() error {
if r.URL == "" {
return fmt.Errorf("invalid request url")
+26 -6
View File
@@ -45,6 +45,32 @@ type generationTestManager struct {
func TestExtensionTaskMethodsMatchEventCapabilities(t *testing.T) {
taskType := reflect.TypeOf((*Task)(nil))
for _, typ := range []reflect.Type{taskType, reflect.TypeOf((*ExtensionTask)(nil)), reflect.TypeOf((*OnErrorExtensionTask)(nil))} {
for _, name := range []string{"SetUrl", "SetExtra", "PatchExtra", "PatchHeaders", "SetMethod", "SetBody", "SetHeaders", "PutHeader", "DelHeader", "SetTrackers"} {
if _, ok := typ.MethodByName(name); ok {
t.Fatalf("%v unexpectedly exposes %s", typ, name)
}
}
}
requestType := reflect.TypeOf((*ExtensionTaskRequest)(nil))
for _, name := range []string{"SetUrl", "SetLabels", "PutLabel", "DelLabel", "SetMethod", "SetBody", "SetHeaders", "PutHeader", "DelHeader", "SetTrackers"} {
if _, ok := requestType.MethodByName(name); !ok {
t.Fatalf("request wrapper does not expose %s", name)
}
}
for _, typ := range []reflect.Type{reflect.TypeOf(ExtensionTask{}), reflect.TypeOf(OnErrorExtensionTask{})} {
if _, ok := typ.FieldByName("Req"); ok {
t.Fatalf("%v exposes the removed task.req entry", typ)
}
field, ok := typ.FieldByName("Meta")
if !ok || field.Type != reflect.TypeOf((*ExtensionTaskMeta)(nil)) {
t.Fatalf("%v has no metadata wrapper", typ)
}
}
field, ok := reflect.TypeOf(ExtensionTaskMeta{}).FieldByName("Req")
if !ok || field.Type != requestType {
t.Fatal("meta.req has no request wrapper")
}
if _, ok := taskType.MethodByName("Continue"); ok {
t.Fatal("regular task unexpectedly exposes Continue")
}
@@ -53,17 +79,11 @@ func TestExtensionTaskMethodsMatchEventCapabilities(t *testing.T) {
}
extensionTaskType := reflect.TypeOf((*ExtensionTask)(nil))
if _, ok := extensionTaskType.MethodByName("SetUrl"); !ok {
t.Fatal("extension task does not expose SetUrl")
}
if _, ok := extensionTaskType.MethodByName("Continue"); ok {
t.Fatal("non-error extension task unexpectedly exposes Continue")
}
errorTaskType := reflect.TypeOf((*OnErrorExtensionTask)(nil))
if _, ok := errorTaskType.MethodByName("SetUrl"); !ok {
t.Fatal("onError task does not expose SetUrl")
}
if _, ok := errorTaskType.MethodByName("Continue"); !ok {
t.Fatal("onError task does not expose Continue")
}
+17 -6
View File
@@ -11,6 +11,7 @@ import (
"strings"
"time"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/logger"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/download/engine"
@@ -733,6 +734,14 @@ type OnDoneContext struct {
// only some fields can be modified, such as request info.
type ExtensionTask struct {
*Task
Meta *ExtensionTaskMeta `json:"meta"`
}
// ExtensionTaskMeta preserves metadata fields while exposing request operations
// at the existing meta.req path.
type ExtensionTaskMeta struct {
*fetcher.FetcherMeta
Req *ExtensionTaskRequest `json:"req"`
}
// OnErrorExtensionTask adds error-recovery controls to ExtensionTask.
@@ -750,7 +759,14 @@ func cloneExtensionTask(task *Task) *Task {
}
func newExtensionTask(task *Task) *ExtensionTask {
return &ExtensionTask{Task: cloneExtensionTask(task)}
view := cloneExtensionTask(task)
return &ExtensionTask{
Task: view,
Meta: &ExtensionTaskMeta{
FetcherMeta: view.Meta,
Req: &ExtensionTaskRequest{Request: view.Meta.Req, task: view},
},
}
}
func newOnStartExtensionTask(task *Task) *ExtensionTask {
@@ -768,11 +784,6 @@ func newOnDoneExtensionTask(task *Task) *Task {
return cloneExtensionTask(task)
}
// SetUrl replaces the task request URL.
func (t *ExtensionTask) SetUrl(url string) {
t.Meta.Req.URL = url
}
func (t *OnErrorExtensionTask) Continue() error {
return t.download.Continue(&TaskFilter{
IDs: []string{t.ID},
@@ -0,0 +1,188 @@
package download
import (
"fmt"
"reflect"
"testing"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/base"
btProtocol "github.com/GopeedLab/gopeed/pkg/protocol/bt"
httpProtocol "github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/dop251/goja"
)
func TestExtensionProtocolMethods(t *testing.T) {
for _, extra := range []any{nil, map[string]any{"header": map[string]any{"Keep": "yes"}}, &httpProtocol.ReqExtra{Header: map[string]string{"Keep": "yes"}}} {
vm := goja.New()
vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
req := &base.Request{Extra: extra}
task := &Task{Protocol: "http", Meta: &fetcher.FetcherMeta{Req: req, Opts: &base.Options{}}}
if err := vm.Set("task", newOnStartExtensionTask(task)); err != nil {
t.Fatal(err)
}
_, err := vm.RunString(`task.meta.req.setMethod('POST'); task.meta.req.setBody('payload'); task.meta.req.putHeader('Authorization','Bearer token'); task.meta.req.putHeader('Remove','remove'); task.meta.req.delHeader('REMOVE');`)
if err != nil {
t.Fatal(err)
}
if err := base.ParseReqExtra[httpProtocol.ReqExtra](req); err != nil {
t.Fatal(err)
}
got := req.Extra.(*httpProtocol.ReqExtra)
if got.Method != "POST" || got.Body != "payload" || got.Header["Authorization"] != "Bearer token" {
t.Fatalf("mutation not applied: %#v", got)
}
if _, ok := got.Header["Remove"]; ok {
t.Fatal("header deletion failed")
}
if extra != nil && got.Header["Keep"] != "yes" {
t.Fatal("existing header lost")
}
}
vm := goja.New()
vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
req := &base.Request{Extra: &btProtocol.ReqExtra{Trackers: []string{"old"}}}
task := &Task{Protocol: "bt", Meta: &fetcher.FetcherMeta{Req: req, Opts: &base.Options{}}}
if err := vm.Set("task", newOnStartExtensionTask(task)); err != nil {
t.Fatal(err)
}
if _, err := vm.RunString(`task.meta.req.setTrackers(['new','another']);`); err != nil {
t.Fatal(err)
}
got := req.Extra.(*btProtocol.ReqExtra).Trackers
if len(got) != 2 || got[0] != "new" || got[1] != "another" {
t.Fatalf("trackers = %#v", got)
}
}
func TestExtensionProtocolMethodsRejectInvalidMutation(t *testing.T) {
vm := goja.New()
vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
original := &httpProtocol.ReqExtra{Method: "POST", Header: map[string]string{"authorization": "old", "Keep": "yes"}}
req := &base.Request{Extra: original}
task := newOnStartExtensionTask(&Task{Protocol: "http", Meta: &fetcher.FetcherMeta{Req: req, Opts: &base.Options{}}})
if err := vm.Set("task", task); err != nil {
t.Fatal(err)
}
for _, script := range []string{`task.meta.req.setHeaders({Keep:null})`, `task.meta.req.setHeaders({Keep:123})`, `task.meta.req.setHeaders({a:'1',A:'2'})`} {
if _, err := vm.RunString(script); err == nil {
t.Fatalf("accepted invalid mutation: %s", script)
}
if req.Extra != original || original.Header["Keep"] != "yes" {
t.Fatal("invalid operation changed original extra")
}
}
if _, err := vm.RunString(`task.meta.req.putHeader('Authorization','new');`); err != nil {
t.Fatal(err)
}
got := req.Extra.(*httpProtocol.ReqExtra)
if len(got.Header) != 2 || got.Header["Authorization"] != "new" || original.Header["authorization"] != "old" {
t.Fatalf("header update = %#v", got.Header)
}
if _, err := vm.RunString(`task.meta.req.setHeaders({Only:'one'});`); err != nil {
t.Fatal(err)
}
got = req.Extra.(*httpProtocol.ReqExtra)
if got.Method != "POST" || len(got.Header) != 1 {
t.Fatalf("setHeaders = %#v", got)
}
req.Extra = map[string]any{"header": 123}
if _, err := vm.RunString(`task.meta.req.setBody('new')`); err == nil {
t.Fatal("invalid protocol extra accepted")
}
if req.Extra.(map[string]any)["header"] != 123 {
t.Fatal("conversion failure mutated extra")
}
}
func TestExtensionProtocolMethodsOtherProtocolsAreNoOp(t *testing.T) {
for _, protocol := range []string{"http", "bt", "hls"} {
for _, onError := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/onError=%v", protocol, onError), func(t *testing.T) {
vm := goja.New()
vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
// An invalid extra proves that unrelated methods skip conversion entirely.
original := map[string]any{"unchanged": true, "header": 123}
req := &base.Request{Extra: original}
task := &Task{Protocol: protocol, Meta: &fetcher.FetcherMeta{Req: req, Opts: &base.Options{}}}
var wrapper any = newOnStartExtensionTask(task)
if onError {
wrapper = newOnErrorExtensionTask(nil, task)
}
if err := vm.Set("task", wrapper); err != nil {
t.Fatal(err)
}
if protocol != "http" {
if _, err := vm.RunString(`task.meta.req.setMethod('POST');task.meta.req.setBody('body');task.meta.req.setHeaders({a:null});task.meta.req.putHeader('A','value');task.meta.req.delHeader('A');`); err != nil {
t.Fatal(err)
}
}
if protocol != "bt" {
if _, err := vm.RunString(`task.meta.req.setTrackers(['udp://example.com']);`); err != nil {
t.Fatal(err)
}
}
if !reflect.DeepEqual(req.Extra, original) {
t.Fatalf("extra changed: %#v", req.Extra)
}
original["same-object"] = true
if req.Extra.(map[string]any)["same-object"] != true {
t.Fatal("no-op replaced extra")
}
})
}
}
}
func TestExtensionOnErrorRequestWritesToOriginalTask(t *testing.T) {
vm := goja.New()
vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
req := &base.Request{URL: "https://example.com", Extra: &httpProtocol.ReqExtra{Header: map[string]string{"Keep": "yes"}}}
task := &Task{Protocol: "http", Meta: &fetcher.FetcherMeta{Req: req, Opts: &base.Options{}}}
if err := vm.Set("task", newOnErrorExtensionTask(nil, task)); err != nil {
t.Fatal(err)
}
if _, err := vm.RunString(`
if (typeof task.continue !== 'function') throw new Error('missing continue');
if (typeof task.putHeader !== 'undefined') throw new Error('task still exposes protocol methods');
if (typeof task.req !== 'undefined') throw new Error('legacy task.req');
const request = task.meta.req;
if (request.url !== 'https://example.com' || request.extra.header.Keep !== 'yes') throw new Error('request data missing');
if (!task.meta.opts) throw new Error('metadata fields missing');
request.setHeaders({Keep:'new'});
request.putHeader('Authorization','Bearer new');
request.setMethod('POST');
if (typeof task.setUrl !== 'undefined' || typeof task.extra !== 'undefined') throw new Error('legacy task API');
request.setUrl('https://example.com/new');
request.setLabels({keep:'yes',remove:'yes'});
request.putLabel('new','value');
request.delLabel('remove');
if (request.url !== 'https://example.com/new' || request.labels.new !== 'value' || request.extra.method !== 'POST' || request.extra.header.Authorization !== 'Bearer new') throw new Error('request data did not refresh');
`); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(req.Labels, map[string]string{"keep": "yes", "new": "value"}) {
t.Fatalf("labels = %#v", req.Labels)
}
got := req.Extra.(*httpProtocol.ReqExtra)
if got.Method != "POST" || got.Header["Keep"] != "new" || got.Header["Authorization"] != "Bearer new" || req.URL != "https://example.com/new" {
t.Fatalf("original request not updated: %#v, %#v", req, got)
}
}
func TestExtensionBTRequestInitializesNilExtra(t *testing.T) {
vm := goja.New()
vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
req := &base.Request{}
task := &Task{Protocol: "bt", Meta: &fetcher.FetcherMeta{Req: req, Opts: &base.Options{}}}
if err := vm.Set("task", newOnStartExtensionTask(task)); err != nil {
t.Fatal(err)
}
if _, err := vm.RunString(`task.meta.req.setTrackers(['udp://example.com']);`); err != nil {
t.Fatal(err)
}
got, ok := req.Extra.(*btProtocol.ReqExtra)
if !ok || !reflect.DeepEqual(got.Trackers, []string{"udp://example.com"}) {
t.Fatalf("extra = %#v", req.Extra)
}
}
+104
View File
@@ -0,0 +1,104 @@
package download
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/GopeedLab/gopeed/pkg/base"
httpProtocol "github.com/GopeedLab/gopeed/pkg/protocol/http"
)
func TestExtensionExtraMutationHTTP(t *testing.T) {
for _, resolved := range []bool{false, true} {
for _, invalid := range []bool{false, true} {
t.Run(fmt.Sprintf("resolved=%v/invalid=%v", resolved, invalid), func(t *testing.T) {
var updatedRequests atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-New") == "new" {
updatedRequests.Add(1)
if r.Header.Get("X-Old") != "" {
t.Error("extra mutation retained the old header")
}
}
http.ServeContent(w, r, "test.txt", time.Time{}, strings.NewReader("extra mutation payload"))
}))
defer server.Close()
d := NewDownloader(&DownloaderConfig{Storage: NewMemStorage(), StorageDir: t.TempDir()})
if err := d.Setup(); err != nil {
t.Fatal(err)
}
defer d.Clear()
extDir := t.TempDir()
manifest := `{"name":"http-methods","title":"HTTP methods test","version":"0.0.1","scripts":[{"event":"onStart","match":{"labels":["test"]},"entry":"index.js"}]}`
mutation := `await ctx.task.meta.req.setMethod('GET');
await ctx.task.meta.req.setBody('');
await ctx.task.meta.req.delHeader('x-old');
await ctx.task.meta.req.putHeader('X-New', 'new');`
if invalid {
mutation = `try { await ctx.task.meta.req.setHeaders({'X-New':123}); }
catch (err) { await ctx.task.meta.req.putLabel('rejected','true'); } ` + mutation
}
script := `gopeed.events.onStart(async ctx => { ` + mutation + ` });`
for name, content := range map[string]string{"manifest.json": manifest, "index.js": script} {
if err := os.WriteFile(filepath.Join(extDir, name), []byte(content), 0600); err != nil {
t.Fatal(err)
}
}
if _, err := d.InstallExtensionByFolder(extDir, false); err != nil {
t.Fatal(err)
}
done := make(chan error, 1)
d.Listener(func(event *Event) {
if event.Key == EventKeyFinally {
done <- event.Err
}
})
req := &base.Request{URL: server.URL + "/test.txt", Labels: map[string]string{"test": "true"}, Extra: &httpProtocol.ReqExtra{Header: map[string]string{"X-Old": "old"}}}
opts := &base.Options{Path: t.TempDir()}
var id string
var err error
if resolved {
rr, resolveErr := d.Resolve(req, opts)
if resolveErr != nil {
t.Fatal(resolveErr)
}
id, err = d.Create(rr.ID)
} else {
id, err = d.CreateDirect(req, opts)
}
if err != nil {
t.Fatal(err)
}
select {
case err = <-done:
case <-time.After(10 * time.Second):
t.Fatal("download timed out")
}
if invalid && d.GetTask(id).Meta.Req.Labels["rejected"] != "true" {
t.Fatal("invalid header was not rejected")
}
if err != nil {
t.Fatal(err)
}
got, ok := d.GetTask(id).Meta.Req.Extra.(*httpProtocol.ReqExtra)
if !ok || got.Header["X-New"] != "new" || len(got.Header) != 1 {
t.Fatalf("unexpected protocol extra: %#v", d.GetTask(id).Meta.Req.Extra)
}
if !resolved && updatedRequests.Load() == 0 {
t.Fatal("download did not use the extension headers")
}
data, err := os.ReadFile(filepath.Join(opts.Path, "test.txt"))
if err != nil || string(data) != "extra mutation payload" {
t.Fatalf("download content = %q, error = %v", data, err)
}
})
}
}
}
+140
View File
@@ -0,0 +1,140 @@
package download
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/GopeedLab/gopeed/pkg/base"
btProtocol "github.com/GopeedLab/gopeed/pkg/protocol/bt"
httpProtocol "github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/dop251/goja"
)
// ExtensionTaskRequest exposes request operations without exposing the backing task.
// It always reads the current request so a retained handle sees subsequent updates.
type ExtensionTaskRequest struct {
*base.Request
task *Task
}
// SetUrl replaces the task request URL.
func (r *ExtensionTaskRequest) SetUrl(url string) { r.task.Meta.Req.URL = url }
// SetLabels replaces all labels on the request.
func (r *ExtensionTaskRequest) SetLabels(labels map[string]string) {
r.Labels = labels
}
// PutLabel sets a label on the request.
func (r *ExtensionTaskRequest) PutLabel(key, value string) {
if r.Labels == nil {
r.Labels = make(map[string]string)
}
r.Labels[key] = value
}
// DelLabel deletes a label from the request.
func (r *ExtensionTaskRequest) DelLabel(key string) {
delete(r.Labels, key)
}
// mutateTaskExtra converts into an independent protocol value before mutation.
// Other protocols are a no-op. Failed conversion or mutation must not change the live request.
func mutateTaskExtra[E any](task *Task, protocol string, mutate func(*E) error) error {
if task.Protocol != protocol {
return nil
}
if task.Meta == nil || task.Meta.Req == nil {
return fmt.Errorf("task request is missing")
}
var extra E
if err := util.MapToStruct(task.Meta.Req.Extra, &extra); err != nil {
return err
}
if err := mutate(&extra); err != nil {
return err
}
task.Meta.Req.Extra = &extra
return nil
}
func (t *ExtensionTaskRequest) SetMethod(method string) error {
return mutateTaskExtra(t.task, "http", func(extra *httpProtocol.ReqExtra) error { extra.Method = method; return nil })
}
func (t *ExtensionTaskRequest) SetBody(body string) error {
return mutateTaskExtra(t.task, "http", func(extra *httpProtocol.ReqExtra) error { extra.Body = body; return nil })
}
// decodeHeaders omits undefined properties and rejects non-string values.
func decodeHeaders(value *goja.Object) (map[string]string, error) {
if value == nil {
return nil, fmt.Errorf("headers must be an object")
}
data, err := value.MarshalJSON()
if err != nil {
return nil, err
}
var input map[string]*string
if err := json.Unmarshal(data, &input); err != nil {
return nil, err
}
if input == nil {
return nil, fmt.Errorf("headers must be an object")
}
headers := make(map[string]string, len(input))
seen := make(map[string]bool, len(input))
for name, value := range input {
if value == nil {
return nil, fmt.Errorf("header %q must be a string", name)
}
lower := strings.ToLower(name)
if seen[lower] {
return nil, fmt.Errorf("duplicate header name %q", name)
}
seen[lower] = true
headers[http.CanonicalHeaderKey(name)] = *value
}
return headers, nil
}
func (t *ExtensionTaskRequest) SetHeaders(headers *goja.Object) error {
return mutateTaskExtra(t.task, "http", func(extra *httpProtocol.ReqExtra) error {
values, err := decodeHeaders(headers)
if err != nil {
return err
}
extra.Header = values
return nil
})
}
func (t *ExtensionTaskRequest) PutHeader(name, value string) error {
return mutateTaskExtra(t.task, "http", func(extra *httpProtocol.ReqExtra) error {
if extra.Header == nil {
extra.Header = make(map[string]string)
}
deleteHeader(extra.Header, name)
extra.Header[http.CanonicalHeaderKey(name)] = value
return nil
})
}
func (t *ExtensionTaskRequest) DelHeader(name string) error {
return mutateTaskExtra(t.task, "http", func(extra *httpProtocol.ReqExtra) error { deleteHeader(extra.Header, name); return nil })
}
func deleteHeader(headers map[string]string, name string) {
for key := range headers {
if strings.EqualFold(key, name) {
delete(headers, key)
}
}
}
func (t *ExtensionTaskRequest) SetTrackers(trackers []string) error {
return mutateTaskExtra(t.task, "bt", func(extra *btProtocol.ReqExtra) error { extra.Trackers = append([]string{}, trackers...); return nil })
}
+36 -3
View File
@@ -14,10 +14,12 @@ import (
"testing"
"time"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/logger"
"github.com/GopeedLab/gopeed/pkg/base"
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
enginewebview "github.com/GopeedLab/gopeed/pkg/download/engine/webview"
httpProtocol "github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/dop251/goja"
)
@@ -38,13 +40,13 @@ func TestDownloader_InstallExtensionByFolder(t *testing.T) {
t.Fatal("resolve error")
}
if req.Labels["replaced"] != "true" || req.Labels["modified"] != "true" {
t.Fatalf("request label methods did not update labels: %#v", req.Labels)
t.Fatalf("declarative labels did not update: %#v", req.Labels)
}
if _, ok := req.Labels["original"]; ok {
t.Fatalf("setLabels did not replace existing labels: %#v", req.Labels)
t.Fatalf("label assignment did not replace existing labels: %#v", req.Labels)
}
if _, ok := req.Labels["removed"]; ok {
t.Fatalf("delLabel did not remove label: %#v", req.Labels)
t.Fatalf("declarative label deletion failed: %#v", req.Labels)
}
})
}
@@ -1188,6 +1190,37 @@ func TestDownloader_UpgradeExtension(t *testing.T) {
})
}
func TestDownloader_Extension_OnStartExtraMutation(t *testing.T) {
d := NewDownloader(&DownloaderConfig{Storage: NewMemStorage(), StorageDir: t.TempDir()})
if err := d.Setup(); err != nil {
t.Fatal(err)
}
defer d.Clear()
if _, err := d.InstallExtensionByFolder("./testdata/extensions/on_start", false); err != nil {
t.Fatal(err)
}
for _, extra := range []any{nil, &httpProtocol.ReqExtra{
Method: "POST", Body: "old body", Header: map[string]string{"Old": "value"},
}} {
req := &base.Request{URL: "https://github.com/test", Extra: extra}
task := &Task{Protocol: "http", Meta: &fetcher.FetcherMeta{Req: req, Opts: &base.Options{}}}
d.triggerOnStart(task)
if err := base.ParseReqExtra[httpProtocol.ReqExtra](req); err != nil {
t.Fatal(err)
}
got, ok := req.Extra.(*httpProtocol.ReqExtra)
if !ok || got.Header["X-Gopeed-Test"] != "on-start" {
t.Fatalf("setHeaders did not update the original request: %#v", req.Extra)
}
if len(got.Header) != 1 {
t.Fatalf("headers not replaced: %#v", got)
}
if original, ok := extra.(*httpProtocol.ReqExtra); ok && (got.Method != original.Method || got.Body != original.Body) {
t.Fatalf("setHeaders changed unrelated fields: %#v", got)
}
}
}
func TestDownloader_Extension_OnStart(t *testing.T) {
downloadAndCheck := func(req *base.Request) {
setupDownloader(func(downloader *Downloader) {
+6 -3
View File
@@ -1,7 +1,10 @@
gopeed.events.onResolve(async function (ctx) {
await ctx.req.setLabels({ replaced: "true", removed: "true" });
await ctx.req.putLabel("modified", "true");
await ctx.req.delLabel("removed");
for (const method of ['setUrl', 'setLabels', 'putLabel', 'delLabel', 'setMethod', 'putHeader', 'setTrackers']) {
if (typeof ctx.req[method] !== 'undefined') throw new Error(`onResolve exposes ${method}`);
}
ctx.req.labels = { replaced: "true", removed: "true" };
ctx.req.labels.modified = "true";
delete ctx.req.labels.removed;
ctx.res = {
name: "test",
files: Array(2).fill(true).map((_, i) => ({
@@ -132,7 +132,7 @@ gopeed.events.onError(async function (ctx) {
return;
}
await ctx.task.setUrl(await createPayloadURL(source, false));
await ctx.task.meta.req.setUrl(await createPayloadURL(source, false));
await req.putLabel('started', 'true');
await req.putLabel('rebuilt', 'true');
await ctx.task.continue();
+1 -1
View File
@@ -41,6 +41,6 @@ gopeed.events.onError(async function (ctx) {
}
await req.putLabel("recovered", "true");
await ctx.task.setUrl(await gopeed.runtime.blob.createObjectURL(new Blob(["ok\n"], { type: "text/plain" })));
await ctx.task.meta.req.setUrl(await gopeed.runtime.blob.createObjectURL(new Blob(["ok\n"], { type: "text/plain" })));
await ctx.task.continue();
});
+1 -1
View File
@@ -46,7 +46,7 @@ gopeed.events.onError(async function (ctx) {
}
try {
await ctx.task.setUrl(await createRangeBlobUrl(req.rawUrl));
await ctx.task.meta.req.setUrl(await createRangeBlobUrl(req.rawUrl));
await req.putLabel('started', 'true');
await req.putLabel('rebuilt', 'true');
await ctx.task.continue();
+1 -1
View File
@@ -1,6 +1,6 @@
gopeed.events.onError(async function (ctx) {
gopeed.logger.info("url", ctx.task.meta.req.url);
gopeed.logger.info("error", ctx.error);
await ctx.task.setUrl("https://github.com");
await ctx.task.meta.req.setUrl("https://github.com");
await ctx.task.continue();
});
+2 -1
View File
@@ -1,5 +1,6 @@
gopeed.events.onStart(async function (ctx) {
gopeed.logger.info("url", ctx.task.meta.req.url);
await ctx.task.setUrl("https://github.com");
await ctx.task.meta.req.setUrl("https://github.com");
await ctx.task.meta.req.setHeaders({ "X-Gopeed-Test": "on-start" });
await ctx.task.meta.req.putLabel('modified', 'true');
});