Skip to content

Commit 2c10bb8

Browse files
Surface rejected OpenAPI validation errors in Create-API wizard
When validate-openapi rejects a definition by URL/file (HTTP 400 with a reason in the response body), the wizard's .catch handlers dropped the backend reason to console.error and showed only a generic "validation failed" message - the Validation Errors card stayed empty. A 400 was thus surfaced less prominently than a 200 isValid:false, which renders the backend errors array in the card. Add getValidationErrorsFromError() to thread the backend response.body.description into setValidationErrors() across the URL and file (API + MCP) rejection paths, so a rejected validation shows as prominently as the isValid:false path. Also clear stale validation errors on file-validation success. Errors without a backend description return [] and keep the existing generic-message fallback. Adds a unit test for the helper.
1 parent 8f28efd commit 2c10bb8

4 files changed

Lines changed: 115 additions & 1 deletion

File tree

portals/publisher/src/main/webapp/site/public/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@
180180
"Apis.Create.OpenAPI.create.api.form.file.label": "OpenAPI File/Archive",
181181
"Apis.Create.OpenAPI.create.api.form.url.label": "OpenAPI URL",
182182
"Apis.Create.OpenAPI.create.api.openapi.content.validation.failed": "OpenAPI content validation failed!",
183+
"Apis.Create.OpenAPI.create.api.openapi.validation.error": "Error while validating OpenAPI definition",
183184
"Apis.Create.OpenAPI.create.api.url.helper.text": "Click away to validate the URL",
184185
"Apis.Create.OpenAPI.create.api.url.label": "OpenAPI URL",
185186
"Apis.Create.OpenAPI.create.api.url.placeholder": "Enter OpenAPI URL",

portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/OpenAPI/Steps/ProvideOpenAPI.jsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ import API from 'AppData/api';
4848
import MCPServer from 'AppData/MCPServer';
4949
import DropZoneLocal, { humanFileSize } from 'AppComponents/Shared/DropZoneLocal';
5050
import Utils from 'AppData/Utils';
51-
import {
51+
import {
5252
getLinterResultsFromContent } from "../../../Details/APIDefinition/Linting/Linting";
53+
import getValidationErrorsFromError from './validationErrorUtils';
5354
import ValidationResults from './ValidationResults';
5455

5556
const PREFIX = 'ProvideOpenAPI';
@@ -133,6 +134,12 @@ export default function ProvideOpenAPI(props) {
133134
defaultMessage: 'Failed to validate the OpenAPI URL. Please try again.',
134135
});
135136
setValidity({ url: { message: errorMessage } });
137+
// Surface a rejected validation (e.g. an access-control block, HTTP 400 for a disallowed host)
138+
// in the Validation Errors card, matching the isValid:false path.
139+
setValidationErrors(getValidationErrorsFromError(error, intl.formatMessage({
140+
id: 'Apis.Create.OpenAPI.create.api.openapi.validation.error',
141+
defaultMessage: 'Error while validating OpenAPI definition',
142+
})));
136143
onValidate(false);
137144
setIsValidating(false);
138145
console.error(error);
@@ -234,6 +241,7 @@ export default function ProvideOpenAPI(props) {
234241
validFile = file;
235242
inputsDispatcher({ action: 'preSetAPI', value: info });
236243
setValidity({ ...isValid, file: null });
244+
setValidationErrors([]);
237245
} else {
238246
setValidity({
239247
...isValid, file: {
@@ -255,6 +263,12 @@ export default function ProvideOpenAPI(props) {
255263
})
256264
}
257265
});
266+
// Surface a rejected validation (e.g. an access-control block, HTTP 400 for a disallowed host)
267+
// in the Validation Errors card, matching the isValid:false path above.
268+
setValidationErrors(getValidationErrorsFromError(error, intl.formatMessage({
269+
id: 'Apis.Create.OpenAPI.create.api.openapi.validation.error',
270+
defaultMessage: 'Error while validating OpenAPI definition',
271+
})));
258272
console.error(error);
259273
})
260274
.finally(() => {
@@ -273,6 +287,7 @@ export default function ProvideOpenAPI(props) {
273287
validFile = file;
274288
inputsDispatcher({ action: 'preSetAPI', value: info });
275289
setValidity({ ...isValid, file: null });
290+
setValidationErrors([]);
276291
} else {
277292
setValidity({
278293
...isValid, file: {
@@ -294,6 +309,12 @@ export default function ProvideOpenAPI(props) {
294309
})
295310
}
296311
});
312+
// Surface a rejected validation (e.g. an access-control block, HTTP 400 for a disallowed host)
313+
// in the Validation Errors card, matching the isValid:false path above.
314+
setValidationErrors(getValidationErrorsFromError(error, intl.formatMessage({
315+
id: 'Apis.Create.OpenAPI.create.api.openapi.validation.error',
316+
defaultMessage: 'Error while validating OpenAPI definition',
317+
})));
297318
console.error(error);
298319
})
299320
.finally(() => {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved.
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
import getValidationErrorsFromError from './validationErrorUtils';
20+
21+
const TITLE = 'Error while validating OpenAPI definition';
22+
23+
describe('getValidationErrorsFromError', () => {
24+
it('surfaces the backend description from a rejected validation (e.g. an access-control block, HTTP 400 "not trusted")', () => {
25+
const notTrusted = 'The provided URL is not trusted. Please contact the system administrator.';
26+
const error = {
27+
response: { body: { code: 400, message: 'Bad Request', description: notTrusted } },
28+
};
29+
30+
expect(getValidationErrorsFromError(error, TITLE)).toEqual([
31+
{ message: TITLE, description: notTrusted },
32+
]);
33+
});
34+
35+
it('returns [] without a backend description (network/other errors keep the generic fallback)', () => {
36+
const noDescriptionCases = [
37+
new Error('Network Error'),
38+
{ response: { body: {} } },
39+
{ response: {} },
40+
undefined,
41+
];
42+
noDescriptionCases.forEach((error) => {
43+
expect(getValidationErrorsFromError(error, TITLE)).toEqual([]);
44+
});
45+
});
46+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved.
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
/**
20+
* Build "Validation Errors" card entries from a rejected OpenAPI validation request.
21+
*
22+
* When validate-openapi returns 200 with `isValid:false`, its `errors` array is shown in the
23+
* card directly. But when it *rejects* with an HTTP error (e.g. a network access-control block —
24+
* 400 UNTRUSTED_URL, "The provided URL is not trusted."), the reason lives in
25+
* `error.response.body.description` and was previously dropped to the console, leaving the
26+
* card empty and the user with only a generic "validation failed" message.
27+
*
28+
* This surfaces that backend description as a card entry so a rejected validation is shown as
29+
* prominently as the `isValid:false` path. Errors without a backend description (network
30+
* failures, etc.) return `[]`, so callers keep their existing generic-message fallback.
31+
*
32+
* @param {any} error caught error from a validateOpenAPIByFile / validateOpenAPIByUrl call
33+
* @param {string} title localized bold title shown above the backend description
34+
* @returns {Array<{message: string, description: string}>} card entries (empty when there is
35+
* no backend-provided description)
36+
*/
37+
export default function getValidationErrorsFromError(
38+
error: any,
39+
title: string,
40+
): Array<{ message: string; description: string }> {
41+
const description = error?.response?.body?.description;
42+
if (!description) {
43+
return [];
44+
}
45+
return [{ message: title, description }];
46+
}

0 commit comments

Comments
 (0)