Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions apps/files_sharing/src/components/NewFileRequestDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -332,10 +332,17 @@ export default defineComponent({
// Move to the last page
this.currentStep = STEP.LAST
} catch (error) {
const errorMessage = (error as AxiosError<OCSResponse>)?.response?.data?.ocs?.meta?.message
showError(errorMessage
? t('files_sharing', 'Error creating the share: {errorMessage}', { errorMessage })
: t('files_sharing', 'Error creating the share'))
const axiosError = error as AxiosError<OCSResponse>
let errorMessage: string
if (axiosError?.response?.status === 429) {
errorMessage = t('files_sharing', 'Share creation is temporarily rate limited. Please wait a few minutes before creating more shares.')
} else {
const message = axiosError?.response?.data?.ocs?.meta?.message
errorMessage = message
? t('files_sharing', 'Error creating the share: {errorMessage}', { errorMessage: message })
: t('files_sharing', 'Error creating the share')
}
showError(errorMessage)
logger.error('Error while creating share', { error, errorMessage })
throw error
} finally {
Expand Down
7 changes: 7 additions & 0 deletions apps/files_sharing/src/components/SharingEntryLink.vue
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,13 @@ export default {
}
showSuccess(t('files_sharing', 'Link share created'))
} catch (data) {
if (data?.cause?.response?.status === 429 || data?.response?.status === 429) {
const rateLimitMessage = t('files_sharing', 'Share creation is temporarily rate limited. Please wait a few minutes before creating more shares.')
showError(rateLimitMessage)
logger.error('Rate limit reached while creating the share', { error: data })
return
}

const message = data?.response?.data?.ocs?.meta?.message
if (!message) {
showError(t('files_sharing', 'Error while creating the share'))
Expand Down
16 changes: 11 additions & 5 deletions apps/files_sharing/src/mixins/ShareRequests.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import axios, { isAxiosError } from '@nextcloud/axios'
import { showError } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
import { t } from '@nextcloud/l10n'
import { generateOcsUrl } from '@nextcloud/router'
import Share from '../models/Share.ts'
import logger from '../services/logger.ts'
Expand Down Expand Up @@ -101,11 +102,16 @@ export default {
* @return {string|undefined} the error message if it could be extracted from the response, otherwise undefined
*/
function getErrorMessage(error) {
if (isAxiosError(error) && error.response.data?.ocs) {
/** @type {import('@nextcloud/typings/ocs').OCSResponse} */
const response = error.response.data
if (response.ocs.meta?.message) {
return response.ocs.meta.message
if (isAxiosError(error)) {
if (error.response?.status === 429) {
return t('files_sharing', 'Share creation is temporarily rate limited. Please wait a few minutes before creating more shares.')
}
if (error.response?.data?.ocs) {
/** @type {import('@nextcloud/typings/ocs').OCSResponse} */
const response = error.response.data
if (response.ocs.meta?.message) {
return response.ocs.meta.message
}
}
}
}
120 changes: 120 additions & 0 deletions apps/files_sharing/src/mixins/ShareRequests.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { AxiosError } from 'axios'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const { showError } = vi.hoisted(() => ({
showError: vi.fn(),
}))

vi.mock('@nextcloud/dialogs', () => ({
showError,
}))

vi.mock(import('@nextcloud/event-bus'), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
emit: vi.fn(),
}
})

vi.mock('@nextcloud/router', () => ({
generateOcsUrl: vi.fn().mockReturnValue('/ocs/v2.php/apps/files_sharing/api/v1/shares'),
}))

vi.mock('../models/Share.ts', () => ({
default: vi.fn().mockImplementation(function(data) {
Object.assign(this, data)
this.id = data?.id ?? 1
}),
}))

vi.mock('../services/logger.ts', () => ({
default: { error: vi.fn(), debug: vi.fn(), info: vi.fn() },
}))

import axios from '@nextcloud/axios'
import ShareRequests from './ShareRequests.js'

describe('ShareRequests mixin', () => {
beforeEach(() => {
vi.clearAllMocks()
})

describe('createShare', () => {
it('creates a share successfully', async () => {
const shareData = { id: 123, path: '/test.txt' }
vi.spyOn(axios, 'post').mockResolvedValueOnce({
data: { ocs: { data: shareData } },
} as never)

const result = await ShareRequests.methods.createShare({
path: '/test.txt',
shareType: 3,
})

expect(result).toMatchObject(shareData)
expect(showError).not.toHaveBeenCalled()
})

it('shows a dedicated rate limit message on HTTP 429 response', async () => {
const axiosError = new AxiosError('Too Many Requests')
axiosError.response = {
status: 429,
statusText: 'Too Many Requests',
headers: {},
config: {} as never,
data: {},
}
vi.spyOn(axios, 'post').mockRejectedValueOnce(axiosError)

await expect(ShareRequests.methods.createShare({
path: '/test.txt',
shareType: 3,
})).rejects.toThrow('Share creation is temporarily rate limited. Please wait a few minutes before creating more shares.')

expect(showError).toHaveBeenCalledWith('Share creation is temporarily rate limited. Please wait a few minutes before creating more shares.')
})

it('shows the backend error message if provided on non-429 failure', async () => {
const axiosError = new AxiosError('Bad Request')
axiosError.response = {
status: 400,
statusText: 'Bad Request',
headers: {},
config: {} as never,
data: {
ocs: {
meta: {
message: 'Custom backend validation failed',
},
},
},
}
vi.spyOn(axios, 'post').mockRejectedValueOnce(axiosError)

await expect(ShareRequests.methods.createShare({
path: '/test.txt',
shareType: 3,
})).rejects.toThrow('Custom backend validation failed')

expect(showError).toHaveBeenCalledWith('Custom backend validation failed')
})

it('falls back to default error message when no specific message is available', async () => {
const genericError = new Error('Network failure')
vi.spyOn(axios, 'post').mockRejectedValueOnce(genericError)

await expect(ShareRequests.methods.createShare({
path: '/test.txt',
shareType: 3,
})).rejects.toThrow('Error creating the share')

expect(showError).toHaveBeenCalledWith('Error creating the share')
})
})
})