Skip to content

Commit de626fe

Browse files
authored
Merge pull request #1657 from nextcloud-libraries/fix/retry
fix(upload): do not retry when insufficient storage but when locked
2 parents 520326e + b085061 commit de626fe

3 files changed

Lines changed: 109 additions & 1 deletion

File tree

__tests__/utils/upload.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ describe('Upload data', () => {
124124
},
125125
'axios-retry': {
126126
retries: 5,
127+
retryCondition: expect.any(Function),
127128
retryDelay: expect.any(Function),
128129
onRetry: expect.any(Function),
129130
},
@@ -154,6 +155,7 @@ describe('Upload data', () => {
154155
},
155156
'axios-retry': {
156157
retries: 5,
158+
retryCondition: expect.any(Function),
157159
retryDelay: expect.any(Function),
158160
onRetry: expect.any(Function),
159161
},
@@ -183,6 +185,7 @@ describe('Upload data', () => {
183185
},
184186
'axios-retry': {
185187
retries: 5,
188+
retryCondition: expect.any(Function),
186189
retryDelay: expect.any(Function),
187190
onRetry: expect.any(Function),
188191
},
@@ -211,6 +214,7 @@ describe('Upload data', () => {
211214
},
212215
'axios-retry': {
213216
retries: 5,
217+
retryCondition: expect.any(Function),
214218
retryDelay: expect.any(Function),
215219
onRetry: onUploadRetry,
216220
},
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { Folder, Permission } from '@nextcloud/files'
7+
import { getUploader, UploadPicker } from '../../../lib/index.ts'
8+
import { generateRemoteUrl } from '@nextcloud/router'
9+
10+
let state: string | undefined
11+
12+
before(() => {
13+
cy.window().then((win) => {
14+
state = win.document.body.innerHTML
15+
})
16+
})
17+
18+
const resetDocument = () => {
19+
if (state) {
20+
cy.window().then((win) => {
21+
win.document.body.innerHTML = state!
22+
})
23+
}
24+
}
25+
26+
describe('UploadPicker: retry requests', () => {
27+
28+
beforeEach(() => {
29+
// Make sure we reset the destination
30+
// so other tests do not interfere
31+
const propsData = {
32+
destination: new Folder({
33+
id: 56,
34+
owner: 'user',
35+
source: generateRemoteUrl('dav/files/user'),
36+
permissions: Permission.ALL,
37+
root: '/files/user',
38+
}),
39+
}
40+
41+
getUploader(false, true).pause()
42+
43+
// Mount picker
44+
cy.mount(UploadPicker, {
45+
propsData,
46+
}).as('uploadPicker')
47+
48+
// Check and init aliases
49+
cy.get('[data-cy-upload-picker] [data-cy-upload-picker-input]').as('input').should('exist')
50+
cy.get('[data-cy-upload-picker] .upload-picker__progress').as('progress').should('exist')
51+
})
52+
53+
afterEach(() => resetDocument())
54+
55+
const testCases: [string, Parameters<Cypress.Chainable['intercept']>[2], number][] = [
56+
['retries on network error', { forceNetworkError: true }, 2],
57+
['retries on timeout', { statusCode: 504 }, 2],
58+
['retries when locked', { statusCode: 423 }, 2],
59+
['does not retry when insufficient storage', { statusCode: 507 }, 1],
60+
]
61+
for (const [testCase, response, requests] of testCases) {
62+
it(testCase, () => {
63+
let request = 0
64+
cy.intercept('PUT', '/remote.php/dav/files/user/file.txt', (rq) => {
65+
if (request++ === 0) {
66+
rq.reply(response)
67+
} else {
68+
rq.reply({ statusCode: 201 })
69+
}
70+
}).as('uploadRequest')
71+
72+
// Start upload
73+
cy.get('@input')
74+
.attachFile({
75+
fileContent: new Blob([new ArrayBuffer(256 * 1024)]),
76+
fileName: 'file.txt',
77+
mimeType: 'text/plain',
78+
})
79+
.then(() => getUploader().start())
80+
81+
cy.wait('@uploadRequest')
82+
83+
// wait finished
84+
cy.get('[data-cy-upload-picker] .upload-picker__progress')
85+
.as('progress')
86+
.should('not.be.visible')
87+
88+
cy.get('@uploadRequest.all')
89+
.should('have.length', requests)
90+
})
91+
}
92+
})

lib/utils/upload.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { AxiosProgressEvent, AxiosResponse, AxiosError } from 'axios'
66
import { generateRemoteUrl } from '@nextcloud/router'
77
import { getCurrentUser } from '@nextcloud/auth'
88
import axios from '@nextcloud/axios'
9-
import axiosRetry, { exponentialDelay } from 'axios-retry'
9+
import axiosRetry, { exponentialDelay, isNetworkOrIdempotentRequestError } from 'axios-retry'
1010

1111
import logger from './logger'
1212

@@ -78,6 +78,18 @@ export async function uploadData(
7878
'axios-retry': {
7979
retries: options.retries,
8080
retryDelay: (retryCount: number, error: AxiosError) => exponentialDelay(retryCount, error, 1000),
81+
retryCondition(error: AxiosError): boolean {
82+
// Do not retry on insufficient storage - this is permanent
83+
if (error.status === 507) {
84+
return false
85+
}
86+
// Do a retry on locked error as this is often just some preview generation
87+
if (error.status === 423) {
88+
return true
89+
}
90+
// Otherwise fallback to default behavior
91+
return isNetworkOrIdempotentRequestError(error)
92+
},
8193
onRetry: options.onUploadRetry,
8294
},
8395
})

0 commit comments

Comments
 (0)