From 33a1d04deab9615f01f59e2ce7af6667a9e1d573 Mon Sep 17 00:00:00 2001 From: lh01217311 Date: Mon, 27 Jul 2026 17:38:12 +0800 Subject: [PATCH 1/5] feat: add retryUpload method for upload - Add retryUpload method to AjaxUploader - Add retryUpload method to Upload component - Add unit test for retryUpload - Update README docs Co-Authored-By: Claude --- README.md | 8 ++++---- README.zh-CN.md | 8 ++++---- src/AjaxUploader.tsx | 8 ++++++++ src/Upload.tsx | 4 ++++ tests/uploader.spec.tsx | 23 +++++++++++++++++++++++ 5 files changed, 43 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 440e9659..a40f79a7 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,6 @@

English | 简体中文

- ## Highlights - Supports Ajax uploads with progress, headers, credentials, and custom request overrides. @@ -98,9 +97,10 @@ Then open `http://localhost:8000`. ### Methods -| Name | Type | Description | -| ------- | ------------------------- | ----------------------- | -| `abort` | `(file: RcFile) => void` | Abort an active upload. | +| Name | Type | Description | +| ------------- | ------------------------ | ------------------------------------ | +| `abort` | `(file: RcFile) => void` | Abort an active upload. | +| `retryUpload` | `(file: RcFile) => void` | Retry an upload for a specific file. | ## Development diff --git a/README.zh-CN.md b/README.zh-CN.md index cc0e8dc8..04e23474 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -15,7 +15,6 @@

English | 简体中文

- ## 特性 - 支持带进度、请求头、凭证和自定义请求覆盖的 Ajax 上传。 @@ -98,9 +97,10 @@ npm start ### 方法 -| 名称 | 类型 | 说明 | -| ------- | ------------------------- | ----------------------- | -| `abort` | `(file: RcFile) => void` | 中止进行中的上传。 | +| 名称 | 类型 | 说明 | +| ------------- | ------------------------ | -------------------- | +| `abort` | `(file: RcFile) => void` | 中止进行中的上传。 | +| `retryUpload` | `(file: RcFile) => void` | 重试特定文件的上传。 | ## 本地开发 diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx index 0244c3be..d90532a8 100644 --- a/src/AjaxUploader.tsx +++ b/src/AjaxUploader.tsx @@ -301,6 +301,14 @@ class AjaxUploader extends Component { this.reqs[uid] = request(requestOption, { defaultRequest }); } + retryUpload = (originFile: RcFile) => { + this.processFile(originFile, [originFile]).then(fileInfo => { + if (fileInfo.parsedFile !== null) { + this.post(fileInfo); + } + }); + }; + reset() { this.setState({ uid: getUid(), diff --git a/src/Upload.tsx b/src/Upload.tsx index 23541e31..6fe755ec 100644 --- a/src/Upload.tsx +++ b/src/Upload.tsx @@ -30,6 +30,10 @@ class Upload extends Component { this.uploader.abort(file); } + retryUpload(file: RcFile) { + this.uploader.retryUpload(file); + } + saveUploader = (node: AjaxUpload) => { this.uploader = node; }; diff --git a/tests/uploader.spec.tsx b/tests/uploader.spec.tsx index ef835041..172ee3fe 100644 --- a/tests/uploader.spec.tsx +++ b/tests/uploader.spec.tsx @@ -253,6 +253,29 @@ describe('uploader', () => { }, 100); }); + it('retryUpload should make new request', done => { + const uploadRef = React.createRef(); + render(); + + const file = { + name: 'retry.png', + toString() { + return this.name; + }, + }; + const files = [file]; + (files as any).item = (i: number) => files[i]; + + const initialRequestCount = requests.length; + + uploadRef.current.retryUpload(file as any); + + setTimeout(() => { + expect(requests.length).toBe(initialRequestCount + 1); + done(); + }, 100); + }); + it('drag to upload', done => { const input = uploader.container.querySelector('input')!; From 5f6c5c1e94cb0b33355e5cf188533a13f02008ac Mon Sep 17 00:00:00 2001 From: lh01217311 Date: Mon, 24 Aug 2026 14:50:57 +0800 Subject: [PATCH 2/5] fix: handle processFile rejection and improve test fixture --- src/AjaxUploader.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx index d90532a8..349a75d0 100644 --- a/src/AjaxUploader.tsx +++ b/src/AjaxUploader.tsx @@ -302,11 +302,13 @@ class AjaxUploader extends Component { } retryUpload = (originFile: RcFile) => { - this.processFile(originFile, [originFile]).then(fileInfo => { - if (fileInfo.parsedFile !== null) { - this.post(fileInfo); - } - }); + this.processFile(originFile, [originFile]) + .then(fileInfo => { + if (fileInfo.parsedFile) { + this.post(fileInfo); + } + }) + .catch(() => {}); }; reset() { From 6082a3e723ed98a83d88fa1dd3f559df59fac2eb Mon Sep 17 00:00:00 2001 From: lh01217311 Date: Mon, 24 Aug 2026 15:16:09 +0800 Subject: [PATCH 3/5] test: cover retryUpload when action rejects Co-Authored-By: Claude --- tests/uploader.spec.tsx | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/uploader.spec.tsx b/tests/uploader.spec.tsx index 172ee3fe..e73ec873 100644 --- a/tests/uploader.spec.tsx +++ b/tests/uploader.spec.tsx @@ -276,6 +276,34 @@ describe('uploader', () => { }, 100); }); + it('retryUpload should not make request when action rejects', done => { + const uploadRef = React.createRef(); + render( + { + throw new Error('action error'); + }} + />, + ); + + const file = { + name: 'reject.png', + toString() { + return this.name; + }, + }; + + const initialRequestCount = requests.length; + + uploadRef.current.retryUpload(file as any); + + setTimeout(() => { + expect(requests.length).toBe(initialRequestCount); + done(); + }, 100); + }); + it('drag to upload', done => { const input = uploader.container.querySelector('input')!; From 9b6ff0840dddbd4ac1a0f6e1ff55494a64f2bf45 Mon Sep 17 00:00:00 2001 From: lh01217311 Date: Fri, 28 Aug 2026 12:06:07 +0800 Subject: [PATCH 4/5] fix: block overlapping retry for the same file uid A retry that overlaps an in-flight attempt for the same uid starts a second request that shares the reqs[uid] key. The older completion then unconditionally deletes the newer handle, leaving the latest request unabortable on abort/unmount. Guard retryUpload against an in-flight same-uid request and add a regression test that repeated retry still yields a single abortable request. Co-Authored-By: Claude --- src/AjaxUploader.tsx | 4 ++++ tests/uploader.spec.tsx | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx index 349a75d0..4212aadf 100644 --- a/src/AjaxUploader.tsx +++ b/src/AjaxUploader.tsx @@ -302,8 +302,12 @@ class AjaxUploader extends Component { } retryUpload = (originFile: RcFile) => { + const { uid } = originFile; this.processFile(originFile, [originFile]) .then(fileInfo => { + if (this.reqs[uid]) { + return; + } if (fileInfo.parsedFile) { this.post(fileInfo); } diff --git a/tests/uploader.spec.tsx b/tests/uploader.spec.tsx index e73ec873..913f0626 100644 --- a/tests/uploader.spec.tsx +++ b/tests/uploader.spec.tsx @@ -304,6 +304,34 @@ describe('uploader', () => { }, 100); }); + it('retryUpload should not start overlapping request for the same file', done => { + const uploadRef = React.createRef(); + render(); + + const file = { + name: 'overlap.png', + toString() { + return this.name; + }, + }; + (file as any).uid = 'fixed-overlap-uid'; + + const initialRequestCount = requests.length; + + uploadRef.current.retryUpload(file as any); + uploadRef.current.retryUpload(file as any); + + setTimeout(() => { + expect(requests.length).toBe(initialRequestCount + 1); + + expect(requests[requests.length - 1].aborted).toBeFalsy(); + + uploadRef.current.abort(file); + expect(requests[requests.length - 1].aborted).toBe(true); + done(); + }, 100); + }); + it('drag to upload', done => { const input = uploader.container.querySelector('input')!; From 5ca86cef679c470bd4c4955f220a0b1827b58020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 10 Sep 2026 18:18:54 +0800 Subject: [PATCH 5/5] refactor: rename retryUpload to retry --- README.md | 8 ++++---- README.zh-CN.md | 8 ++++---- src/AjaxUploader.tsx | 2 +- src/Upload.tsx | 4 ++-- tests/uploader.spec.tsx | 14 +++++++------- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index a40f79a7..fd0e832d 100644 --- a/README.md +++ b/README.md @@ -97,10 +97,10 @@ Then open `http://localhost:8000`. ### Methods -| Name | Type | Description | -| ------------- | ------------------------ | ------------------------------------ | -| `abort` | `(file: RcFile) => void` | Abort an active upload. | -| `retryUpload` | `(file: RcFile) => void` | Retry an upload for a specific file. | +| Name | Type | Description | +| ------- | ------------------------ | ------------------------------------ | +| `abort` | `(file: RcFile) => void` | Abort an active upload. | +| `retry` | `(file: RcFile) => void` | Retry an upload for a specific file. | ## Development diff --git a/README.zh-CN.md b/README.zh-CN.md index 04e23474..8b35710f 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -97,10 +97,10 @@ npm start ### 方法 -| 名称 | 类型 | 说明 | -| ------------- | ------------------------ | -------------------- | -| `abort` | `(file: RcFile) => void` | 中止进行中的上传。 | -| `retryUpload` | `(file: RcFile) => void` | 重试特定文件的上传。 | +| 名称 | 类型 | 说明 | +| ------- | ------------------------ | -------------------- | +| `abort` | `(file: RcFile) => void` | 中止进行中的上传。 | +| `retry` | `(file: RcFile) => void` | 重试特定文件的上传。 | ## 本地开发 diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx index 4212aadf..2d4de936 100644 --- a/src/AjaxUploader.tsx +++ b/src/AjaxUploader.tsx @@ -301,7 +301,7 @@ class AjaxUploader extends Component { this.reqs[uid] = request(requestOption, { defaultRequest }); } - retryUpload = (originFile: RcFile) => { + retry = (originFile: RcFile) => { const { uid } = originFile; this.processFile(originFile, [originFile]) .then(fileInfo => { diff --git a/src/Upload.tsx b/src/Upload.tsx index 6fe755ec..46f4dac8 100644 --- a/src/Upload.tsx +++ b/src/Upload.tsx @@ -30,8 +30,8 @@ class Upload extends Component { this.uploader.abort(file); } - retryUpload(file: RcFile) { - this.uploader.retryUpload(file); + retry(file: RcFile) { + this.uploader.retry(file); } saveUploader = (node: AjaxUpload) => { diff --git a/tests/uploader.spec.tsx b/tests/uploader.spec.tsx index 913f0626..41f42fe4 100644 --- a/tests/uploader.spec.tsx +++ b/tests/uploader.spec.tsx @@ -253,7 +253,7 @@ describe('uploader', () => { }, 100); }); - it('retryUpload should make new request', done => { + it('retry should make new request', done => { const uploadRef = React.createRef(); render(); @@ -268,7 +268,7 @@ describe('uploader', () => { const initialRequestCount = requests.length; - uploadRef.current.retryUpload(file as any); + uploadRef.current.retry(file as any); setTimeout(() => { expect(requests.length).toBe(initialRequestCount + 1); @@ -276,7 +276,7 @@ describe('uploader', () => { }, 100); }); - it('retryUpload should not make request when action rejects', done => { + it('retry should not make request when action rejects', done => { const uploadRef = React.createRef(); render( { const initialRequestCount = requests.length; - uploadRef.current.retryUpload(file as any); + uploadRef.current.retry(file as any); setTimeout(() => { expect(requests.length).toBe(initialRequestCount); @@ -304,7 +304,7 @@ describe('uploader', () => { }, 100); }); - it('retryUpload should not start overlapping request for the same file', done => { + it('retry should not start overlapping request for the same file', done => { const uploadRef = React.createRef(); render(); @@ -318,8 +318,8 @@ describe('uploader', () => { const initialRequestCount = requests.length; - uploadRef.current.retryUpload(file as any); - uploadRef.current.retryUpload(file as any); + uploadRef.current.retry(file as any); + uploadRef.current.retry(file as any); setTimeout(() => { expect(requests.length).toBe(initialRequestCount + 1);