Skip to content
Merged
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
42 changes: 42 additions & 0 deletions e2e/tests/api-console.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
});
Comment on lines +285 to +287

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Expect the malformed JSON alert text

This test fills the editor with {, but doExecute catches that in the JSON.parse block and renders the request-body alert with Fix request JSON before sending., returning before the APISIX schema feedback path can set Resolve APISIX schema issues before sending.. As written, the new E2E test will time out on this locator whenever it runs against the changed code; assert the JSON syntax error message here or use syntactically valid JSON that violates the schema.

Useful? React with 👍 / 👎.

Comment on lines +277 to +287
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,
}) => {
Expand Down
4 changes: 4 additions & 0 deletions src/routes/raw_api/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,10 @@
margin-top: 10px;
}

.requestBodyError {
margin-top: 10px;
}

.editor {
flex: 1;
min-height: 0;
Expand Down
72 changes: 70 additions & 2 deletions src/routes/raw_api/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -429,6 +436,8 @@ function RawApiPage() {
const [presetName, setPresetName] = useState('');
const [loadedBodyNotice, setLoadedBodyNotice] =
useState<LoadedBodyNotice | null>(null);
const [requestBodyError, setRequestBodyError] =
useState<RequestBodyError | null>(null);
const [requestHistory, setRequestHistory] = useState<RequestHistoryEntry[]>(
readRequestHistory
);
Expand Down Expand Up @@ -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
? {
Expand Down Expand Up @@ -515,6 +525,7 @@ function RawApiPage() {
setQueryString(requestSnapshot.queryString);
setBody(requestSnapshot.body);
setLoadedBodyNotice(null);
setRequestBodyError(null);
setResponse(null);
setResponseError(null);
setResponseView('Body');
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -819,6 +855,7 @@ function RawApiPage() {
onChange={(value) => {
setMethod(value);
setLoadedBodyNotice(null);
setRequestBodyError(null);
setBody(
stringifyRequiredRequestTemplate(
resource,
Expand All @@ -844,6 +881,7 @@ function RawApiPage() {
onChange={(v) => {
setResource(v);
setLoadedBodyNotice(null);
setRequestBodyError(null);
setBody(stringifyRequiredRequestTemplate(v, method, ''));
setPathSuffix('');
setQueryString('');
Expand All @@ -866,6 +904,7 @@ function RawApiPage() {
onChange={(value) => {
setPathSuffix(value);
setLoadedBodyNotice(null);
setRequestBodyError(null);
}}
options={existingResources.map((r) => ({
value: r.path,
Expand Down Expand Up @@ -997,6 +1036,34 @@ function RawApiPage() {
className={classes.loadedBodyAlert}
/>
)}
{requestBodyError && (
<Alert
type="error"
showIcon
message={requestBodyError.message}
description={
<ul style={{ margin: 0, paddingLeft: 18 }}>
{requestBodyError.details.slice(0, 5).map((detail) => (
<li key={detail}>{detail}</li>
))}
Comment on lines +1046 to +1048
{requestBodyError.details.length > 5 && (
<li>{requestBodyError.details.length - 5} more issue(s)</li>
)}
</ul>
}
action={
<Space wrap>
<Button size="small" onClick={formatRequestBody}>
Format JSON
</Button>
<Button size="small" onClick={resetRequestBodyTemplate}>
Reset to template
</Button>
</Space>
}
className={classes.requestBodyError}
/>
)}
</div>
<div className={classes.editor}>
<JsonCodeEditor
Expand All @@ -1005,6 +1072,7 @@ function RawApiPage() {
onChange={(nextValue) => {
setBody(nextValue ?? '');
setLoadedBodyNotice(null);
setRequestBodyError(null);
}}
variant="flush"
/>
Expand Down