[Console] Fix parsing requests with errors (#215568)

Fixes https://github.com/elastic/kibana/issues/211031

## Summary

This PR fixes the selection of requests in Console when a request
contains an error. It also adds an error toast when the user tries to
send a request containing an error, as the response from Elasticsearch
is usually too long and not very helpful.




https://github.com/user-attachments/assets/4de10953-9ee5-489b-94fb-fd8a772bd598
This commit is contained in:
Elena Stoeva 2025-04-01 18:28:24 +01:00 committed by GitHub
parent c8fc5e74d9
commit 4557b73959
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 128 additions and 27 deletions

View file

@ -8,7 +8,7 @@
*/
import { ConsoleWorkerProxyService } from './console_worker_proxy';
import { ParsedRequest } from './types';
import { ErrorAnnotation, ParsedRequest } from './types';
import { monaco } from '../../monaco_imports';
/*
@ -29,4 +29,11 @@ export class ConsoleParsedRequestsProvider {
const parserResult = await this.workerProxyService.getParserResult(this.model.uri);
return parserResult?.requests ?? [];
}
public async getErrors(): Promise<ErrorAnnotation[]> {
if (!this.model) {
return [];
}
const parserResult = await this.workerProxyService.getParserResult(this.model.uri);
return parserResult?.errors ?? [];
}
}

View file

@ -56,6 +56,8 @@ export const createParser = () => {
},
reset = function (newAt) {
ch = text.charAt(newAt);
updateRequestEnd();
addRequestEnd();
at = newAt + 1;
},
next = function (c) {
@ -413,10 +415,16 @@ export const createParser = () => {
} catch (e) {
addError(e.message);
// snap
const substring = text.substr(at);
const nextMatch = substring.search(/^POST|HEAD|GET|PUT|DELETE|PATCH/m);
if (nextMatch < 1) return;
reset(at + nextMatch);
const remainingText = text.substr(at);
const nextMethodIndex = remainingText.search(/^\s*(POST|HEAD|GET|PUT|DELETE|PATCH)\b/mi);
const nextCommentLine = remainingText.search(/^\s*(#|\/\*|\/\/).*$/m);
if (nextMethodIndex === -1 && nextCommentLine === -1) {
// If there are no comments or other requests after the error, there is no point in parsing more so we stop here
return;
}
// Reset parser at the next request or the next comment, whichever comes first
at += Math.min(...[nextMethodIndex, nextCommentLine].filter(i => i !== -1));
reset(at);
}
}
};

View file

@ -53,6 +53,48 @@ describe('console parser', () => {
expect(endOffset).toBe(52);
});
it('parses requests with an error', () => {
const input =
'GET _search\n' +
'{ERROR\n' +
' "query": {\n' +
' "match_all": {}\n' +
' }\n' +
'}\n\n' +
'POST _test_index';
const { requests, errors } = parser(input) as ConsoleParserResult;
expect(requests.length).toBe(2);
expect(requests[0].startOffset).toBe(0);
expect(requests[0].endOffset).toBe(57);
expect(requests[1].startOffset).toBe(59);
expect(requests[1].endOffset).toBe(75);
expect(errors.length).toBe(1);
expect(errors[0].offset).toBe(14);
expect(errors[0].text).toBe('Bad string');
});
it('parses requests with an error and a comment before the next request', () => {
const input =
'GET _search\n' +
'{ERROR\n' +
' "query": {\n' +
' "match_all": {}\n' +
' }\n' +
'}\n\n' +
'# This is a comment\n' +
'POST _test_index\n' +
'// Another comment\n';
const { requests, errors } = parser(input) as ConsoleParserResult;
expect(requests.length).toBe(2);
expect(requests[0].startOffset).toBe(0);
expect(requests[0].endOffset).toBe(57);
expect(requests[1].startOffset).toBe(79); // The next request should start after the comment
expect(requests[1].endOffset).toBe(95);
expect(errors.length).toBe(1);
expect(errors[0].offset).toBe(14);
expect(errors[0].text).toBe('Bad string');
});
describe('case insensitive methods', () => {
const expectedRequests = [
{

View file

@ -13,6 +13,7 @@ import { ConsoleParsedRequestsProvider, getParsedRequestsProvider, monaco } from
import { i18n } from '@kbn/i18n';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { XJson } from '@kbn/es-ui-shared-plugin/public';
import { ErrorAnnotation } from '@kbn/monaco/src/languages/console/types';
import { isQuotaExceededError } from '../../../services/history';
import { DEFAULT_VARIABLES, KIBANA_API_PREFIX } from '../../../../common/constants';
import { getStorage, StorageKeys } from '../../../services';
@ -233,6 +234,30 @@ export class MonacoEditorActionsProvider {
return selectedRequests;
}
private async getErrorsBetweenLines(
startLineNumber: number,
endLineNumber: number
): Promise<ErrorAnnotation[]> {
const model = this.editor.getModel();
if (!model) {
return [];
}
const parsedErrors = await this.parsedRequestsProvider.getErrors();
const selectedErrors: ErrorAnnotation[] = [];
for (const parsedError of parsedErrors) {
const errorLine = model.getPositionAt(parsedError.offset).lineNumber;
if (errorLine > endLineNumber) {
// error is past the selection, no need to check further errors
break;
}
if (errorLine >= startLineNumber) {
// error is selected
selectedErrors.push(parsedError);
}
}
return selectedErrors;
}
public async getRequests() {
const model = this.editor.getModel();
if (!model) {
@ -276,6 +301,25 @@ export class MonacoEditorActionsProvider {
try {
const allRequests = await this.getRequests();
const selectedRequests = await this.getSelectedParsedRequests();
if (selectedRequests.length) {
const selectedErrors = await this.getErrorsBetweenLines(
selectedRequests.at(0)!.startLineNumber,
selectedRequests.at(-1)!.endLineNumber
);
if (selectedErrors.length) {
toasts.addDanger(
i18n.translate('console.notification.monaco.error.errorInSelection', {
defaultMessage:
'The selected {requestCount, plural, one {request contains} other {requests contain}} {errorCount, plural, one {an error} other {errors}}. Please resolve {errorCount, plural, one {it} other {them}} and try again.',
values: {
requestCount: selectedRequests.length,
errorCount: selectedErrors.length,
},
})
);
return;
}
}
const requests = allRequests
// if any request doesnt have a method then we gonna treat it as a non-valid

View file

@ -75,20 +75,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await PageObjects.console.getEditorText()).to.be.empty();
});
it('should return statusCode 400 to unsupported HTTP verbs', async () => {
const expectedResponseContains = '"statusCode": 400';
await PageObjects.console.clearEditorText();
await PageObjects.console.enterText('OPTIONS /');
await PageObjects.console.clickPlay();
await retry.try(async () => {
const actualResponse = await PageObjects.console.getOutputText();
log.debug(actualResponse);
expect(actualResponse).to.contain(expectedResponseContains);
expect(await PageObjects.console.hasSuccessBadge()).to.be(false);
});
});
describe('tabs navigation', () => {
let currentUrl: string;

View file

@ -17,6 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const browser = getService('browser');
const PageObjects = getPageObjects(['common', 'console', 'header']);
const toasts = getService('toasts');
describe('misc console behavior', function testMiscConsoleBehavior() {
before(async () => {
@ -204,17 +205,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
});
it(`should include an invalid json when sending a request`, async () => {
it(`should display an error toast when sending a request with invalid body`, async () => {
await PageObjects.console.clearEditorText();
await PageObjects.console.enterText(invalidRequestText);
await PageObjects.console.selectCurrentRequest();
await PageObjects.console.pressCtrlEnter();
await PageObjects.console.clickPlay();
await retry.try(async () => {
const actualResponse = await PageObjects.console.getOutputText();
expect(actualResponse).to.contain('parsing_exception');
expect(await PageObjects.console.hasSuccessBadge()).to.be(false);
});
expect(await toasts.getCount()).to.be(1);
const resultToast = await toasts.getElementByIndex(1);
const toastText = await resultToast.getVisibleText();
expect(toastText).to.be(
'The selected request contains an error. Please resolve it and try again.'
);
await toasts.dismissAll();
});
it('should display an error toast to unsupported HTTP verbs', async () => {
await PageObjects.console.clearEditorText();
await PageObjects.console.enterText('OPTIONS /');
await PageObjects.console.clickPlay();
expect(await toasts.getCount()).to.be(1);
const resultToast = await toasts.getElementByIndex(1);
const toastText = await resultToast.getVisibleText();
expect(toastText).to.be(
'The selected request contains errors. Please resolve them and try again.'
);
});
});