From 54bf24bd23cbd0d78f9fa8efc7a2da38594c4580 Mon Sep 17 00:00:00 2001 From: jinbagi <4094424+jinbagi@users.noreply.github.com> Date: Fri, 3 Jul 2026 20:47:53 +0900 Subject: [PATCH] feat: keep api console request errors recoverable --- e2e/tests/api-console.spec.ts | 42 +++++++++++++++++ src/routes/raw_api/index.module.css | 4 ++ src/routes/raw_api/index.tsx | 72 ++++++++++++++++++++++++++++- 3 files changed, 116 insertions(+), 2 deletions(-) diff --git a/e2e/tests/api-console.spec.ts b/e2e/tests/api-console.spec.ts index 4bfbf8f0..7a8ed9d6 100644 --- a/e2e/tests/api-console.spec.ts +++ b/e2e/tests/api-console.spec.ts @@ -257,6 +257,48 @@ test('loads existing resources as editable request bodies', async ({ page }) => await expect(requestEditor).toContainText('"create_time": 1710000000'); }); +test('keeps blocked request body errors visible with recovery actions', async ({ + page, +}) => { + const routeId = 'invalid-json-console-route'; + let blockedRequests = 0; + page.on('request', (request) => { + if ( + request.method() === 'PUT' && + request.url().includes(`/apisix/admin/routes/${routeId}`) + ) { + blockedRequests += 1; + } + }); + + await page.getByRole('combobox', { name: /Path suffix/ }).fill(routeId); + + const requestEditor = page.locator('.monaco-editor').first(); + await uiFillMonacoEditor(page, requestEditor, '{'); + + await page.getByRole('button', { name: /Send PUT/ }).click(); + await page + .getByRole('dialog', { name: `PUT /routes/${routeId}` }) + .getByRole('button', { name: 'Execute' }) + .click(); + + const requestBodyError = page.getByRole('alert').filter({ + hasText: 'Resolve APISIX schema issues before sending.', + }); + await expect(requestBodyError).toBeVisible(); + await expect( + requestBodyError.getByRole('button', { name: 'Format JSON' }) + ).toBeVisible(); + await expect( + requestBodyError.getByRole('button', { name: 'Reset to template' }) + ).toBeVisible(); + expect(blockedRequests).toBe(0); + + await requestBodyError.getByRole('button', { name: 'Reset to template' }).click(); + await expect(requestBodyError).toBeHidden(); + await expect(requestEditor).toContainText('"uri": "/"'); +}); + test('executes confirmed PUT requests with JSON body and history', async ({ page, }) => { diff --git a/src/routes/raw_api/index.module.css b/src/routes/raw_api/index.module.css index d43617ed..715e5770 100644 --- a/src/routes/raw_api/index.module.css +++ b/src/routes/raw_api/index.module.css @@ -249,6 +249,10 @@ margin-top: 10px; } +.requestBodyError { + margin-top: 10px; +} + .editor { flex: 1; min-height: 0; diff --git a/src/routes/raw_api/index.tsx b/src/routes/raw_api/index.tsx index ea91892a..207b8a22 100644 --- a/src/routes/raw_api/index.tsx +++ b/src/routes/raw_api/index.tsx @@ -75,7 +75,10 @@ import { stripPatchReadonlyFields, } from '@/utils/apisixEditable'; import { createRequiredJsonTemplate } from '@/utils/jsonRequiredTemplate'; -import { getJsonSchemaFeedback } from '@/utils/jsonSchemaFeedback'; +import { + formatJsonSchemaPath, + getJsonSchemaFeedback, +} from '@/utils/jsonSchemaFeedback'; import { getResourceConditionalRequirements } from '@/utils/resourceJsonSchema'; import classes from './index.module.css'; @@ -158,6 +161,10 @@ type LoadedBodyNotice = { rawBody: string; removedKeys: string[]; }; +type RequestBodyError = { + message: string; + details: string[]; +}; const REQUEST_HISTORY_KEY = 'api-console:session-history'; const REQUEST_PRESETS_KEY = 'api-console:session-presets'; @@ -429,6 +436,8 @@ function RawApiPage() { const [presetName, setPresetName] = useState(''); const [loadedBodyNotice, setLoadedBodyNotice] = useState(null); + const [requestBodyError, setRequestBodyError] = + useState(null); const [requestHistory, setRequestHistory] = useState( readRequestHistory ); @@ -464,6 +473,7 @@ function RawApiPage() { const value = res.data?.value ?? res.data; const editableBody = getEditableLoadedBody(value); setBody(editableBody.body); + setRequestBodyError(null); setLoadedBodyNotice( editableBody.removedKeys.length > 0 ? { @@ -515,6 +525,7 @@ function RawApiPage() { setQueryString(requestSnapshot.queryString); setBody(requestSnapshot.body); setLoadedBodyNotice(null); + setRequestBodyError(null); setResponse(null); setResponseError(null); setResponseView('Body'); @@ -563,18 +574,31 @@ function RawApiPage() { try { parsedBody = JSON.parse(activeBody); } catch (e) { - message.error('Invalid JSON: ' + String(e)); + setRequestBodyError({ + message: 'Fix request JSON before sending.', + details: [e instanceof Error ? e.message : String(e)], + }); + message.error('Fix request JSON before sending.'); return; } if (activeRequestBodySchema) { const feedback = getJsonSchemaFeedback(activeRequestBodySchema, activeBody); if (feedback.syntaxError || feedback.issues.length > 0) { + setRequestBodyError({ + message: 'Resolve APISIX schema issues before sending.', + details: feedback.syntaxError + ? [feedback.syntaxError] + : feedback.issues.map( + (issue) => `${formatJsonSchemaPath(issue)}: ${issue.message}` + ), + }); message.error('Resolve the APISIX schema issues before executing this request.'); return; } } } setLoading(true); + setRequestBodyError(null); setLastRequest(requestSnapshot); setResponse(null); setResponseError(null); @@ -674,12 +698,24 @@ function RawApiPage() { const formatRequestBody = useCallback(() => { try { setBody(JSON.stringify(JSON.parse(body), null, 2)); + setRequestBodyError(null); message.success('Request JSON formatted'); } catch (error) { + setRequestBodyError({ + message: 'Fix request JSON before formatting.', + details: [error instanceof Error ? error.message : String(error)], + }); message.error(`Invalid JSON: ${String(error)}`); } }, [body]); + const resetRequestBodyTemplate = useCallback(() => { + setBody(stringifyRequiredRequestTemplate(resource, method, normalizedPathSuffix)); + setLoadedBodyNotice(null); + setRequestBodyError(null); + message.success('Request JSON reset to template'); + }, [method, normalizedPathSuffix, resource]); + const restoreHistoryEntry = useCallback((entry: RequestHistoryEntry) => { restoreRequest(entry); setHistoryOpen(false); @@ -819,6 +855,7 @@ function RawApiPage() { onChange={(value) => { setMethod(value); setLoadedBodyNotice(null); + setRequestBodyError(null); setBody( stringifyRequiredRequestTemplate( resource, @@ -844,6 +881,7 @@ function RawApiPage() { onChange={(v) => { setResource(v); setLoadedBodyNotice(null); + setRequestBodyError(null); setBody(stringifyRequiredRequestTemplate(v, method, '')); setPathSuffix(''); setQueryString(''); @@ -866,6 +904,7 @@ function RawApiPage() { onChange={(value) => { setPathSuffix(value); setLoadedBodyNotice(null); + setRequestBodyError(null); }} options={existingResources.map((r) => ({ value: r.path, @@ -997,6 +1036,34 @@ function RawApiPage() { className={classes.loadedBodyAlert} /> )} + {requestBodyError && ( + + {requestBodyError.details.slice(0, 5).map((detail) => ( +
  • {detail}
  • + ))} + {requestBodyError.details.length > 5 && ( +
  • {requestBodyError.details.length - 5} more issue(s)
  • + )} + + } + action={ + + + + + } + className={classes.requestBodyError} + /> + )}
    { setBody(nextValue ?? ''); setLoadedBodyNotice(null); + setRequestBodyError(null); }} variant="flush" />