From b0c19bfac130713abb3594a613e4bbc2ce6e0f30 Mon Sep 17 00:00:00 2001 From: Maurice Date: Tue, 22 Sep 2026 11:55:43 +0200 Subject: [PATCH] fix(settings): the client registration form says what the server refused Registering an OAuth client by hand and getting only "Failed to register OAuth client" left nothing to act on: the server names the rule it broke (a scope, the ten-client cap, a redirect URI it will not take) and the toast swallowed it in a bare catch. Both shells show the server's message now and keep the generic text for a failure that carries none. --- .../Settings/IntegrationsTab.test.tsx | 21 ++++++++++++++++--- .../components/Settings/IntegrationsTab.tsx | 8 +++++-- .../mobile/screens/settings/MSettingsMcp.tsx | 8 +++++-- .../mobile/settings/MSettingsMcp.test.tsx | 10 +++++---- 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/client/src/components/Settings/IntegrationsTab.test.tsx b/client/src/components/Settings/IntegrationsTab.test.tsx index e04a95619..e3ce2e6e9 100644 --- a/client/src/components/Settings/IntegrationsTab.test.tsx +++ b/client/src/components/Settings/IntegrationsTab.test.tsx @@ -644,11 +644,11 @@ describe('IntegrationsTab', () => { expect(createBtn).toBeDisabled(); }); - it('FE-COMP-INTEGRATIONS-032: error toast shown when create OAuth client fails', async () => { + it('FE-COMP-INTEGRATIONS-032: a refused client registration toasts the reason the server gave', async () => { const user = userEvent.setup(); server.use( http.post('/api/oauth/clients', () => - HttpResponse.json({ error: 'server error' }, { status: 500 }) + HttpResponse.json({ error: 'Redirect URI must use HTTPS, loopback HTTP, or a private custom scheme: http://192.168.1.5/cb' }, { status: 400 }) ) ); enableMcp(); @@ -657,9 +657,24 @@ describe('IntegrationsTab', () => { await user.click(screen.getByRole('button', { name: /New Client/i })); await screen.findByText('Register OAuth Client'); await user.type(screen.getByPlaceholderText(/Claude Web, My MCP App/i), 'Fail Client'); + await user.type(screen.getByPlaceholderText(/https:\/\/your-app/i), 'http://192.168.1.5/cb'); + await user.click(screen.getByRole('button', { name: /Register Client/i })); + expect(await screen.findByText(/Redirect URI must use HTTPS/)).toBeInTheDocument(); + expect(screen.queryByText('Failed to register OAuth client')).not.toBeInTheDocument(); + }); + + it('FE-COMP-INTEGRATIONS-032b: a failure the server says nothing about keeps the generic text', async () => { + const user = userEvent.setup(); + server.use(http.post('/api/oauth/clients', () => HttpResponse.error())); + enableMcp(); + render(<>); + await screen.findByText('MCP Configuration'); + await user.click(screen.getByRole('button', { name: /New Client/i })); + await screen.findByText('Register OAuth Client'); + await user.type(screen.getByPlaceholderText(/Claude Web, My MCP App/i), 'Fail Client'); await user.type(screen.getByPlaceholderText(/https:\/\/your-app/i), 'http://localhost'); await user.click(screen.getByRole('button', { name: /Register Client/i })); - expect(await screen.findByText(/Failed to register/i)).toBeInTheDocument(); + expect(await screen.findByText('Failed to register OAuth client')).toBeInTheDocument(); }); }); diff --git a/client/src/components/Settings/IntegrationsTab.tsx b/client/src/components/Settings/IntegrationsTab.tsx index 23085f799..4310717a9 100644 --- a/client/src/components/Settings/IntegrationsTab.tsx +++ b/client/src/components/Settings/IntegrationsTab.tsx @@ -13,6 +13,7 @@ import ApiKeysSection from './ApiKeysSection' import { PRESET_SCOPES_DEFAULT, PRESET_SCOPES_READONLY } from '../../api/oauthScopes' import ScopeGroupPicker from '../OAuth/ScopeGroupPicker' import { useAuthStore } from '../../store/authStore' +import { getApiErrorMessage } from '../../utils/apiError' interface OAuthPreset { id: string @@ -266,8 +267,11 @@ function useIntegrations() { setOauthNewUris('') setOauthNewScopes([]) setOauthIsMachine(false) - } catch { - toast.error(t('settings.oauth.toast.createError')) + } catch (err) { + // The server names the rule that refused the client (a scope, the + // ten-client cap, a redirect URI it will not take); swallowing it left + // "Failed to register OAuth client" and nothing to act on. + toast.error(getApiErrorMessage(err, t('settings.oauth.toast.createError'))) } finally { setOauthCreating(false) } diff --git a/client/src/mobile/screens/settings/MSettingsMcp.tsx b/client/src/mobile/screens/settings/MSettingsMcp.tsx index b4304cf6e..873837007 100644 --- a/client/src/mobile/screens/settings/MSettingsMcp.tsx +++ b/client/src/mobile/screens/settings/MSettingsMcp.tsx @@ -11,6 +11,7 @@ import MChip from '../../components/MChip' import { MSetCard, MSetEyebrow, MSetInput, MSetTextarea, MSetButton, MSetHint, MSetRow } from './MSettingsUi' import MConfirmSheet from './MConfirmSheet' import MSegmented from '../../components/MSegmented' +import { getApiErrorMessage } from '../../../utils/apiError' interface OAuthPreset { id: string @@ -188,8 +189,11 @@ export default function MSettingsMcp() { setNewUris('') setNewScopes([]) setIsMachine(false) - } catch { - toast.error(t('settings.oauth.toast.createError')) + } catch (err) { + // The server names the rule that refused the client (a scope, the + // ten-client cap, a redirect URI it will not take); swallowing it left + // "Failed to register OAuth client" and nothing to act on. + toast.error(getApiErrorMessage(err, t('settings.oauth.toast.createError'))) } finally { setCreating(false) } diff --git a/client/tests/unit/mobile/settings/MSettingsMcp.test.tsx b/client/tests/unit/mobile/settings/MSettingsMcp.test.tsx index 7839c7599..fbacac3c3 100644 --- a/client/tests/unit/mobile/settings/MSettingsMcp.test.tsx +++ b/client/tests/unit/mobile/settings/MSettingsMcp.test.tsx @@ -265,17 +265,19 @@ describe('MSettingsMcp', () => { expect(writeText).toHaveBeenNthCalledWith(2, 'sec-copy'); }); - it('FE-MOB-SETMCP-011: a failed registration surfaces an error toast and keeps the form open', async () => { + it('FE-MOB-SETMCP-011: a refused registration surfaces the reason the server gave and keeps the form open', async () => { const user = userEvent.setup(); - server.use(http.post('/api/oauth/clients', () => HttpResponse.json({ error: 'boom' }, { status: 500 }))); + server.use(http.post('/api/oauth/clients', () => + HttpResponse.json({ error: 'Redirect URI must use HTTPS, loopback HTTP, or a private custom scheme: http://192.168.1.5/cb' }, { status: 400 }))); renderScreen(); await user.click(screen.getByRole('button', { name: 'New Client' })); await user.type(await screen.findByPlaceholderText(/Claude Web, My MCP App/), 'Broken'); - fireEvent.change(screen.getByPlaceholderText(/your-app/), { target: { value: 'http://localhost' } }); + fireEvent.change(screen.getByPlaceholderText(/your-app/), { target: { value: 'http://192.168.1.5/cb' } }); await user.click(screen.getByRole('button', { name: 'Register Client' })); - await screen.findByText('Failed to register OAuth client'); + await screen.findByText(/Redirect URI must use HTTPS/); + expect(screen.queryByText('Failed to register OAuth client')).toBeNull(); expect(screen.getByText('Register OAuth Client')).toBeInTheDocument(); });