onedrive: poll async copy status with the configured HTTP client

waitForJob polled the copy monitor URL with http.Get, which uses
http.DefaultClient and no context. The poll ignored the configured
transport (--ca-cert, --no-check-certificate, --timeout, --user-agent,
--dump and so on) and could not be cancelled, so a monitor that stopped
answering blocked a server-side copy indefinitely.

Build the request with the context and send it through the unAuth
client, which uses the configured transport without adding the
Authorization header the monitor URL does not need.
This commit is contained in:
Soujas K authored and Nick Craig-Wood committed 2026-10-31 15:17:16 +00:00
1 parent b44c24cfca
commit 0a0661ade0
2 files changed
+55 -1

No files matched your search

+7 -1
View File
@@ -1726,7 +1726,13 @@ func (f *Fs) waitForJob(ctx context.Context, location string, o *Object) error {
var err error
var body []byte
err = f.pacer.Call(func() (bool, error) {
resp, err = http.Get(location)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, location, nil)
if err != nil {
return false, err
}
// The monitor URL needs no authentication, but it should still
// go through the configured transport and honour ctx
resp, err = f.unAuth.Do(req)
if err != nil {
return fserrors.ShouldRetry(err), err
}
@@ -15,6 +15,7 @@ import (
_ "github.com/rclone/rclone/backend/local"
"github.com/rclone/rclone/backend/onedrive/api"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/fshttp"
"github.com/rclone/rclone/fs/operations"
"github.com/rclone/rclone/fstest"
"github.com/rclone/rclone/fstest/fstests"
@@ -770,3 +771,50 @@ func TestListDirectorySize(t *testing.T) {
assert.Equal(t, int64(2646204760), dir.Size())
assert.Equal(t, int64(3), dir.Items())
}
// TestWaitForJobUsesConfiguredClient checks that the async job monitor is
// polled with the configured HTTP client and request context rather than
// http.DefaultClient, so proxy, TLS and timeout options and cancellation apply.
func TestWaitForJobUsesConfiguredClient(t *testing.T) {
ctx, ci := fs.AddConfig(context.Background())
ci.UserAgent = "rclone-waitforjob-test"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "rclone-waitforjob-test", r.UserAgent())
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(api.AsyncOperationStatus{Status: "deleteFailed"})
}))
t.Cleanup(server.Close)
f, _ := newChangeNotifyTestFs(t, http.NotFoundHandler())
f.unAuth = rest.NewClient(fshttp.NewClient(ctx))
err := f.waitForJob(ctx, server.URL+"/monitor", &Object{remote: "file.txt"})
require.Error(t, err)
assert.Contains(t, err.Error(), "deleteFailed")
}
// TestWaitForJobCancel checks that cancelling the context stops a poll of
// the async job monitor which is not answering.
func TestWaitForJobCancel(t *testing.T) {
release := make(chan struct{})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case <-r.Context().Done():
case <-release:
}
}))
t.Cleanup(server.Close)
t.Cleanup(func() { close(release) })
f, _ := newChangeNotifyTestFs(t, http.NotFoundHandler())
f.unAuth = rest.NewClient(fshttp.NewClient(context.Background()))
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
done := make(chan error, 1)
go func() { done <- f.waitForJob(ctx, server.URL+"/monitor", &Object{remote: "file.txt"}) }()
select {
case err := <-done:
assert.ErrorIs(t, err, context.DeadlineExceeded)
case <-time.After(5 * time.Second):
t.Fatal("waitForJob did not return after the context was cancelled")
}
}