mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-28 09:08:54 -04:00
fix(ui): show the server's reason when an add-on request is refused
A 403 or 400 answer showed a generic message and hid the reason in the response body, so a refusal by the browser guard could not be told from a missing permission. Show error.message from the body as the server sent it, and keep the generic text when the body has none. Assisted-by: Claude Code:claude-sonnet-5-5 node Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
5 files changed
+33
-8
No files matched your search
@@ -267,7 +267,7 @@ function AppCard({ app, t, language, actions, onStartStop }) {
|
||||
}
|
||||
|
||||
function ActionResult({ outcome, t }) {
|
||||
const text = describeOutcome(outcome.outcome, t)
|
||||
const text = describeOutcome(outcome.outcome, t, outcome.error)
|
||||
if (!text) return null
|
||||
return <p className="ad-card__note" data-tone="error" role="alert">{text}</p>
|
||||
}
|
||||
@@ -86,7 +86,7 @@ export default function BrowseView({ api, adapter }) {
|
||||
</div>
|
||||
{pending && <UnknownNotice addonId={entry.id} actions={actions} />}
|
||||
{!pending && st && !working && st.outcome !== 'completed' && (
|
||||
<p className="ad-entry__note" data-tone="error" role="alert">{describeOutcome(st.outcome, t)}</p>
|
||||
<p className="ad-entry__note" data-tone="error" role="alert">{describeOutcome(st.outcome, t, st.error)}</p>
|
||||
)}
|
||||
{st && st.outcome === 'completed' && <p className="ad-entry__note" data-tone="ok" role="status">{t('browse.installedNotEnabled')}</p>}
|
||||
</div>
|
||||
|
||||
@@ -194,7 +194,7 @@ function latest(actions, addon, verbs) {
|
||||
function outcomeText(result, t) {
|
||||
const type = result && result.error && result.error.type
|
||||
if (type && UPDATE_TYPES.has(type)) return t(`updates.errors.${type}`)
|
||||
return describeOutcome(result.outcome, t)
|
||||
return describeOutcome(result.outcome, t, result.error)
|
||||
}
|
||||
|
||||
function UpdateFact({ view, t }) {
|
||||
|
||||
+14
-5
@@ -1,11 +1,20 @@
|
||||
// The reason the server gave for a refusal, as sent. AddonsError falls back to
|
||||
// the kind when the body had no message, and that is not a reason.
|
||||
function serverReason(error) {
|
||||
const m = error && typeof error.message === 'string' ? error.message.trim() : ''
|
||||
return m && m !== error.kind ? m : ''
|
||||
}
|
||||
|
||||
// One message per kind of answer, the same on every page. The server's words
|
||||
// are never shown as success and never hidden behind a generic "failed".
|
||||
// are never shown as success and never hidden behind a generic "failed". A
|
||||
// refusal (403 or 400) shows the server's own reason when the body has one,
|
||||
// untranslated, and the generic text only when it has none.
|
||||
export function describeError(error, t) {
|
||||
const kind = error && error.kind
|
||||
switch (kind) {
|
||||
case 'auth': return t('errors.auth')
|
||||
case 'denied': return t('errors.denied')
|
||||
case 'invalid': return t('errors.invalid')
|
||||
case 'denied': return serverReason(error) || t('errors.denied')
|
||||
case 'invalid': return serverReason(error) || t('errors.invalid')
|
||||
case 'unknown_outcome': return t('errors.unknownOutcome')
|
||||
case 'network': return t('errors.network')
|
||||
default: return t('errors.unavailable')
|
||||
@@ -14,13 +23,13 @@ export function describeError(error, t) {
|
||||
|
||||
// The words for the outcome of an action the user started. `completed` has
|
||||
// none: the list on the page shows the new state.
|
||||
export function describeOutcome(outcome, t) {
|
||||
export function describeOutcome(outcome, t, error) {
|
||||
switch (outcome) {
|
||||
case 'completed': return ''
|
||||
case 'failed': return t('actions.failed')
|
||||
case 'unknown': return t('actions.unknown')
|
||||
case 'not_applied': return t('actions.notApplied')
|
||||
case 'blocked': return t('actions.blocked')
|
||||
default: return describeError({ kind: outcome }, t)
|
||||
default: return describeError({ kind: outcome, message: error && error.message }, t)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { createAddonsApi, createAddonsCatalogAdapter, AddonsError } from '../../src/utils/addonsApi.js'
|
||||
import { runLifecycle, settleOperation } from '../../src/components/addons/lifecycleFlow.js'
|
||||
import { createActionBook, effectOutcome } from '../../src/components/addons/actionBook.js'
|
||||
import { describeError, describeOutcome } from '../../src/components/addons/errors.js'
|
||||
|
||||
const json = (status, body) => ({ ok: status >= 200 && status < 300, status, json: async () => body })
|
||||
|
||||
@@ -262,3 +263,18 @@ test('a completed status that left the add-on unchanged is not reported as done'
|
||||
assert.equal(effectOutcome('failed', false), 'failed')
|
||||
assert.equal(effectOutcome('unknown', false), 'unknown')
|
||||
})
|
||||
|
||||
test('a refusal shows the server reason and falls back to the generic text', () => {
|
||||
const t = key => `T:${key}`
|
||||
const withBody = new AddonsError(classifyResponse(403, { error: { type: 'addon_error', message: 'Same-origin browser request required' } }))
|
||||
assert.equal(describeError(withBody, t), 'Same-origin browser request required')
|
||||
assert.equal(describeOutcome('denied', t, withBody), 'Same-origin browser request required')
|
||||
const noBody = new AddonsError(classifyResponse(403, null))
|
||||
assert.equal(describeError(noBody, t), 'T:errors.denied')
|
||||
assert.equal(describeOutcome('denied', t, noBody), 'T:errors.denied')
|
||||
assert.equal(describeOutcome('denied', t), 'T:errors.denied')
|
||||
const invalid = new AddonsError(classifyResponse(400, { error: { message: 'Bad id' } }))
|
||||
assert.equal(describeError(invalid, t), 'Bad id')
|
||||
// Other kinds keep their own words.
|
||||
assert.equal(describeError(new AddonsError(classifyResponse(401, { error: { message: 'x' } })), t), 'T:errors.auth')
|
||||
})
|
||||
Reference in new issue
Block a user