diff --git a/portals/publisher/src/main/webapp/site/public/locales/en.json b/portals/publisher/src/main/webapp/site/public/locales/en.json index 94264b40d43..c926c3c81ef 100644 --- a/portals/publisher/src/main/webapp/site/public/locales/en.json +++ b/portals/publisher/src/main/webapp/site/public/locales/en.json @@ -80,6 +80,7 @@ "Apis.Create.AsyncAPI.Steps.ProvideAsyncAPI.solace.api.label": "Identified as Solace Event Portal API", "Apis.Create.AsyncAPI.Steps.ProvideAsyncAPI.solace.label": "Solace Event API", "Apis.Create.AsyncAPI.Steps.ProvideAsyncAPI.url.label": "AsyncAPI URL", + "Apis.Create.AsyncAPI.Steps.ProvideAsyncAPI.url.validation.error": "Failed to validate the AsyncAPI URL. Please try again.", "Apis.Create.Components.DefaultAPIForm.api.actual.context.helper": "API will be exposed in {actualContext} context at the gateway", "Apis.Create.Components.DefaultAPIForm.api.channel": "Channel", "Apis.Create.Components.DefaultAPIForm.api.context": "Context", @@ -91,6 +92,7 @@ "Apis.Create.Components.DefaultAPIForm.display.name.helper.text.api": "Display name for the API", "Apis.Create.Components.DefaultAPIForm.display.name.helper.text.mcp": "Display name for the MCP Server", "Apis.Create.Components.DefaultAPIForm.display.name.helper.text.product": "Display name for the API Product", + "Apis.Create.Components.DefaultAPIForm.endpoint.validation.error": "Error while validating endpoint URL", "Apis.Create.Components.DefaultAPIForm.mandatory.fields": "Mandatory fields", "Apis.Create.Components.DefaultAPIForm.name": "Name", "Apis.Create.Components.DefaultAPIForm.select.gateway.type": "Gateway type", @@ -170,6 +172,7 @@ "Apis.Create.OpenAPI.Steps.ProvideOpenAPI.Input.file.dropzone": "Drag & Drop Open API File/Archive here {break} or {break} Browse files", "Apis.Create.OpenAPI.Steps.ProvideOpenAPI.Input.file.upload": "Browse File to Upload", "Apis.Create.OpenAPI.Steps.ProvideOpenAPI.Input.type": "Input Type", + "Apis.Create.OpenAPI.Steps.ProvideOpenAPI.url.validation.error": "Failed to validate the OpenAPI URL. Please try again.", "Apis.Create.OpenAPI.Steps.ValidationResults.linter.results": "Linter Results", "Apis.Create.OpenAPI.Steps.ValidationResults.linting": "Generating Linter Results", "Apis.Create.OpenAPI.Steps.ValidationResults.validating": "Validating API definition", @@ -177,6 +180,7 @@ "Apis.Create.OpenAPI.create.api.form.file.label": "OpenAPI File/Archive", "Apis.Create.OpenAPI.create.api.form.url.label": "OpenAPI URL", "Apis.Create.OpenAPI.create.api.openapi.content.validation.failed": "OpenAPI content validation failed!", + "Apis.Create.OpenAPI.create.api.openapi.validation.error": "Error while validating OpenAPI definition", "Apis.Create.OpenAPI.create.api.url.helper.text": "Click away to validate the URL", "Apis.Create.OpenAPI.create.api.url.label": "OpenAPI URL", "Apis.Create.OpenAPI.create.api.url.placeholder": "Enter OpenAPI URL", @@ -210,6 +214,7 @@ "Apis.Create.WSDL.url.label": "WSDL URL", "Apis.Create.WSDL.url.placeholder": "Enter WSDL URL", "Apis.Create.WSDL.validation.error.response": "Error occurred during validation", + "Apis.Create.WSDL.validation.error.title": "Error while validating the WSDL definition", "Apis.Create.asyncAPI.Components.SelectPolicies.business.plans": "Protocol", "Apis.Create.streaming.Components.SelectPolicies.business.plans": "Protocol", "Apis.Create.streaming.Components.create.and.publish.btn": "Create & Publish", @@ -1008,6 +1013,7 @@ "Apis.Details.Endpoints.GenericEndpoint.check.endpoint": "Check endpoint status", "Apis.Details.Endpoints.GenericEndpoint.config.endpoint": "Endpoint configurations", "Apis.Details.Endpoints.GenericEndpoint.config.service.placeholder": "Select a service from the service list", + "Apis.Details.Endpoints.GenericEndpoint.endpoint.validation.error": "Error while validating endpoint URL", "Apis.Details.Endpoints.GenericEndpoint.no.ep.error": "Endpoint URL should not be empty", "Apis.Details.Endpoints.GenericEndpoint.security.endpoint": "Endpoint security", "Apis.Details.Endpoints.GenericEndpoint.service.url.input": "Service URL", @@ -2261,6 +2267,7 @@ "MCPServers.Create.MCPServerCreateProxy.url.required": "MCP Server URL is required", "MCPServers.Create.MCPServerCreateProxy.validate": "Next", "MCPServers.Create.MCPServerCreateProxy.validation.error": "Error while validating MCP Server", + "MCPServers.Create.MCPServerCreateProxy.validation.error.detail": "Failed to validate MCP Server URL.", "MCPServers.Create.MCPServerCreateProxy.validation.failed": "MCP Server validation failed. Please check the URL and security credentials (if any) and try again.", "MCPServers.Create.MCPServerCreateProxy.value.label": "Value", "MCPServers.Create.MCPServerCreateProxy.wizard.one": "Provide MCP Server URL", diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/AsyncAPI/Steps/ProvideAsyncAPI.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/AsyncAPI/Steps/ProvideAsyncAPI.jsx index 13a1ff51315..ad1599d3acd 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/AsyncAPI/Steps/ProvideAsyncAPI.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/AsyncAPI/Steps/ProvideAsyncAPI.jsx @@ -226,7 +226,14 @@ export default function ProvideAsyncAPI(props) { onValidate(isValidURL); setIsValidating(false); }).catch((error) => { - setValidity({ url: { message: error.message } }); + setValidity({ ...isValid, url: { message: error.response?.body?.description + || error.response?.body?.message + || error.message + || intl.formatMessage({ + id: 'Apis.Create.AsyncAPI.Steps.ProvideAsyncAPI.url.validation.error', + defaultMessage: 'Failed to validate the AsyncAPI URL. Please try again.', + }), + } }); onValidate(false); setIsValidating(false); console.error(error); diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/Components/DefaultAPIForm.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/Components/DefaultAPIForm.jsx index 535463e5195..744ee6e043e 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/Components/DefaultAPIForm.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/Components/DefaultAPIForm.jsx @@ -24,7 +24,7 @@ import { InputAdornment, IconButton, Icon, Select, MenuItem, InputLabel, Box } f import CircularProgress from '@mui/material/CircularProgress'; import Chip from '@mui/material/Chip'; import Typography from '@mui/material/Typography'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; import APIValidation from 'AppData/APIValidation'; import FormControl from '@mui/material/FormControl'; import FormHelperText from '@mui/material/FormHelperText'; @@ -142,6 +142,7 @@ export default function DefaultAPIForm(props) { const [isUpdating, setUpdating] = useState(false); const [isErrorCode, setIsErrorCode] = useState(false); const iff = (condition, then, otherwise) => (condition ? then : otherwise); + const intl = useIntl(); /** * Generate gradient background based on gateway name @@ -408,7 +409,20 @@ export default function DefaultAPIForm(props) { setIsEndpointValid(false); } } - }).finally(() => { + }) + .catch((error) => { + const message = error.response?.body?.description + || error.response?.body?.message + || error.message + || intl.formatMessage({ + id: 'Apis.Create.Components.DefaultAPIForm.endpoint.validation.error', + defaultMessage: 'Error while validating endpoint URL', + }); + setStatusCode(message); + setIsErrorCode(true); + setIsEndpointValid(false); + }) + .finally(() => { setUpdating(false); }); } diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/OpenAPI/Steps/ValidationResults.tsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/Components/ValidationResults.tsx similarity index 98% rename from portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/OpenAPI/Steps/ValidationResults.tsx rename to portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/Components/ValidationResults.tsx index fc51968259a..804aadbb9f6 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/OpenAPI/Steps/ValidationResults.tsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/Components/ValidationResults.tsx @@ -23,8 +23,8 @@ import { ExpandMore } from '@mui/icons-material'; import { FormattedMessage } from "react-intl"; import LinterUI from "AppComponents/Apis/Details/APIDefinition/LinterUI/LinterUI"; import APILintingSummary from "AppComponents/Apis/Details/APIDefinition/Linting/APILintingSummary"; -import { - spectralSeverityMap as severityMap } from "../../../Details/APIDefinition/Linting/Linting" +import { + spectralSeverityMap as severityMap } from "AppComponents/Apis/Details/APIDefinition/Linting/Linting" type APILintingProps = { inputValue: any, diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/Components/validationErrorUtils.test.ts b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/Components/validationErrorUtils.test.ts new file mode 100644 index 00000000000..280c2dfe8b7 --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/Components/validationErrorUtils.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import getValidationErrorsFromError from './validationErrorUtils'; + +const TITLE = 'Error while validating OpenAPI definition'; + +describe('getValidationErrorsFromError', () => { + it('surfaces the backend description from a rejected validation (e.g. an access-control block, HTTP 400 "not trusted")', () => { + const notTrusted = 'The provided URL is not trusted. Please contact the system administrator.'; + const error = { + response: { body: { code: 400, message: 'Bad Request', description: notTrusted } }, + }; + + expect(getValidationErrorsFromError(error, TITLE)).toEqual([ + { message: TITLE, description: notTrusted }, + ]); + }); + + it('returns [] without a backend description (network/other errors keep the generic fallback)', () => { + const noDescriptionCases = [ + new Error('Network Error'), + { response: { body: {} } }, + { response: {} }, + undefined, + ]; + noDescriptionCases.forEach((error) => { + expect(getValidationErrorsFromError(error, TITLE)).toEqual([]); + }); + }); +}); diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/Components/validationErrorUtils.ts b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/Components/validationErrorUtils.ts new file mode 100644 index 00000000000..153d80b084b --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/Components/validationErrorUtils.ts @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Build "Validation Errors" card entries from a rejected OpenAPI validation request. + * + * When validate-openapi returns 200 with `isValid:false`, its `errors` array is shown in the + * card directly. But when it *rejects* with an HTTP error (e.g. a network access-control block — + * 400 UNTRUSTED_URL, "The provided URL is not trusted."), the reason lives in + * `error.response.body.description` and was previously dropped to the console, leaving the + * card empty and the user with only a generic "validation failed" message. + * + * This surfaces that backend description as a card entry so a rejected validation is shown as + * prominently as the `isValid:false` path. Errors without a backend description (network + * failures, etc.) return `[]`, so callers keep their existing generic-message fallback. + * + * @param {any} error caught error from a validateOpenAPIByFile / validateOpenAPIByUrl call + * @param {string} title localized bold title shown above the backend description + * @returns {Array<{message: string, description: string}>} card entries (empty when there is + * no backend-provided description) + */ +export default function getValidationErrorsFromError( + error: any, + title: string, +): Array<{ message: string; description: string }> { + const description = error?.response?.body?.description; + if (!description) { + return []; + } + return [{ message: title, description }]; +} diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/GraphQL/Steps/ProvideGraphQL.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/GraphQL/Steps/ProvideGraphQL.jsx index 459c6ce18cc..03f9fc430b9 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/GraphQL/Steps/ProvideGraphQL.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/GraphQL/Steps/ProvideGraphQL.jsx @@ -156,11 +156,14 @@ export default function ProvideGraphQL(props) { inputsDispatcher({ action: 'graphQLInfo', value: graphQLInfo }); setValidity({ isValidURL, file: null }); } else { - let errorMessage; - if (inputType === ProvideGraphQL.INPUT_TYPES.ENDPOINT) { - errorMessage = 'Error occurred while generating GraphQL schema from endpoint'; - } else if (inputType === ProvideGraphQL.INPUT_TYPES.URL) { - errorMessage = 'Error occurred while retrieving GraphQL schema from url'; + let errorMessage = response.body?.description + || response.body?.message || response.body?.errorMessage; + if (!errorMessage) { + if (inputType === ProvideGraphQL.INPUT_TYPES.ENDPOINT) { + errorMessage = 'Error occurred while generating GraphQL schema from endpoint'; + } else if (inputType === ProvideGraphQL.INPUT_TYPES.URL) { + errorMessage = 'Error occurred while retrieving GraphQL schema from url'; + } } setValidity({ isValidURL, @@ -172,7 +175,10 @@ export default function ProvideGraphQL(props) { }; const handleError = (error) => { - setValidity({ url: { message: error.message } }); + setValidity({ url: { message: error.response?.body?.description + || error.response?.body?.message + || error.message, + }}); onValidate(false); setIsValidating(false); console.error(error); diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/OpenAPI/Steps/ProvideOpenAPI.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/OpenAPI/Steps/ProvideOpenAPI.jsx index c8075399fc5..cd935320fcf 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/OpenAPI/Steps/ProvideOpenAPI.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/OpenAPI/Steps/ProvideOpenAPI.jsx @@ -48,9 +48,10 @@ import API from 'AppData/api'; import MCPServer from 'AppData/MCPServer'; import DropZoneLocal, { humanFileSize } from 'AppComponents/Shared/DropZoneLocal'; import Utils from 'AppData/Utils'; -import { +import getValidationErrorsFromError from 'AppComponents/Apis/Create/Components/validationErrorUtils'; +import ValidationResults from 'AppComponents/Apis/Create/Components/ValidationResults'; +import { getLinterResultsFromContent } from "../../../Details/APIDefinition/Linting/Linting"; -import ValidationResults from './ValidationResults'; const PREFIX = 'ProvideOpenAPI'; @@ -90,6 +91,11 @@ export default function ProvideOpenAPI(props) { const intl = useIntl(); + const openApiValidationErrorTitle = intl.formatMessage({ + id: 'Apis.Create.OpenAPI.create.api.openapi.validation.error', + defaultMessage: 'Error while validating OpenAPI definition', + }); + function lint(content) { // Validate and linting setIsLinting(true); @@ -124,6 +130,21 @@ export default function ProvideOpenAPI(props) { const validateURLDebounced = useCallback( debounce((newURL) => { // Example: https://codesandbox.io/s/debounce-example-l7fq3?file=/src/App.js + const handleValidationError = (error) => { + const errorMessage = error.response?.body?.description + || error.response?.body?.message + || error.message + || intl.formatMessage({ + id: 'Apis.Create.OpenAPI.Steps.ProvideOpenAPI.url.validation.error', + defaultMessage: 'Failed to validate the OpenAPI URL. Please try again.', + }); + setValidity({ ...isValid, url: { message: errorMessage } }); + setValidationErrors(getValidationErrorsFromError(error, openApiValidationErrorTitle)); + onValidate(false); + setIsValidating(false); + console.error(error); + }; + if (isMCPServer) { MCPServer.validateOpenAPIByUrl(newURL, { returnContent: true }).then((response) => { const { @@ -162,12 +183,7 @@ export default function ProvideOpenAPI(props) { } onValidate(isValidURL); setIsValidating(false); - }).catch((error) => { - setValidity({ url: { message: error.message } }); - onValidate(false); - setIsValidating(false); - console.error(error); - }); + }).catch(handleValidationError); } else { API.validateOpenAPIByUrl(newURL, { returnContent: true }).then((response) => { const { @@ -197,13 +213,7 @@ export default function ProvideOpenAPI(props) { } onValidate(isValidURL); setIsValidating(false); - }).catch((error) => { - setValidity({ url: { message: error.message } }); - onValidate(false); - setIsValidating(false); - console.error(error); - - }); + }).catch(handleValidationError); } }, 750), [], @@ -231,6 +241,7 @@ export default function ProvideOpenAPI(props) { validFile = file; inputsDispatcher({ action: 'preSetAPI', value: info }); setValidity({ ...isValid, file: null }); + setValidationErrors([]); } else { setValidity({ ...isValid, file: { @@ -252,6 +263,7 @@ export default function ProvideOpenAPI(props) { }) } }); + setValidationErrors(getValidationErrorsFromError(error, openApiValidationErrorTitle)); console.error(error); }) .finally(() => { @@ -270,6 +282,7 @@ export default function ProvideOpenAPI(props) { validFile = file; inputsDispatcher({ action: 'preSetAPI', value: info }); setValidity({ ...isValid, file: null }); + setValidationErrors([]); } else { setValidity({ ...isValid, file: { @@ -291,6 +304,7 @@ export default function ProvideOpenAPI(props) { }) } }); + setValidationErrors(getValidationErrorsFromError(error, openApiValidationErrorTitle)); console.error(error); }) .finally(() => { diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/WSDL/Steps/ProvideWSDL.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/WSDL/Steps/ProvideWSDL.jsx index dbc62aa6fa1..66a81705b72 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/WSDL/Steps/ProvideWSDL.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Create/WSDL/Steps/ProvideWSDL.jsx @@ -15,7 +15,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; import Radio from '@mui/material/Radio'; @@ -43,8 +43,9 @@ import CheckIcon from '@mui/icons-material/Check'; import APIValidation from 'AppData/APIValidation'; import Wsdl from 'AppData/Wsdl'; -import Banner from 'AppComponents/Shared/Banner'; import DropZoneLocal, { humanFileSize } from 'AppComponents/Shared/DropZoneLocal'; +import getValidationErrorsFromError from 'AppComponents/Apis/Create/Components/validationErrorUtils'; +import ValidationResults from 'AppComponents/Apis/Create/Components/ValidationResults'; const PREFIX = 'ProvideWSDL'; @@ -77,9 +78,31 @@ export default function ProvideWSDL(props) { const [isError, setValidity] = useState(); // If valid value is `null` else an error object will be there const [isValidating, setIsValidating] = useState(false); + const [validationErrors, setValidationErrors] = useState([]); const isCreateMode = apiInputs.mode === 'create'; const intl = useIntl(); + + const wsdlValidationErrorTitle = intl.formatMessage({ + id: 'Apis.Create.WSDL.validation.error.title', + defaultMessage: 'Error while validating the WSDL definition', + }); + + /** + * Clear the validation state and the selected file/URL, bringing back the drop zone. + */ + function reset() { + setValidationErrors([]); + setValidity(); + inputsDispatcher({ action: 'inputValue', value: null }); + inputsDispatcher({ action: 'isFormValid', value: false }); + onValidate(false); + } + + // Clear validation state when the input type changes so a stale error does not linger. + useEffect(() => { + reset(); + }, [apiInputs.inputType]); /** * Handles WSDL validation response and returns the state. * @@ -91,30 +114,34 @@ export default function ProvideWSDL(props) { const isWSDLValid = response.body.isValid; let success = false; if (isWSDLValid) { + setValidationErrors([]); if (type === 'file') { setValidity({ ...isError, file: null }); } else { setValidity({ ...isError, url: null }); } success = true; - } else if (type === 'file') { - setValidity({ - ...isError, file: { - message: intl.formatMessage({ - id: 'Apis.Create.WSDL.content.validation.file.failed', - defaultMessage: 'WSDL content validation failed!', - }), - } - }); } else { - setValidity({ - ...isError, url: { - message: intl.formatMessage({ - id: 'Apis.Create.WSDL.content.validation.url.failed', - defaultMessage: 'Invalid WSDL URL!', - }), - } - }); + setValidationErrors(response.body.errors || []); + if (type === 'file') { + setValidity({ + ...isError, file: { + message: intl.formatMessage({ + id: 'Apis.Create.WSDL.content.validation.file.failed', + defaultMessage: 'WSDL content validation failed!', + }), + } + }); + } else { + setValidity({ + ...isError, url: { + message: intl.formatMessage({ + id: 'Apis.Create.WSDL.content.validation.url.failed', + defaultMessage: 'Invalid WSDL URL!', + }), + } + }); + } } onValidate(isWSDLValid); setIsValidating(false); @@ -132,14 +159,16 @@ export default function ProvideWSDL(props) { id: 'Apis.Create.WSDL.validation.error.response', defaultMessage: 'Error occurred during validation', }); - if (error.response && error.response.body.description) { + if (error.response?.body?.description) { message = error.response.body.description; } + setValidationErrors(getValidationErrorsFromError(error, wsdlValidationErrorTitle)); if (type === 'file') { setValidity({ ...isError, file: { message } }); } else { setValidity({ ...isError, url: { message } }); } + onValidate(false); setIsValidating(false); } @@ -172,11 +201,12 @@ export default function ProvideWSDL(props) { if (state === null) { setIsValidating(true); Wsdl.validateFileOrArchive(file).then((response) => { - if (handleWSDLValidationResponse(response, 'file')) { - inputsDispatcher({ action: 'inputValue', value: file }); - } + handleWSDLValidationResponse(response, 'file'); }).catch((error) => { handleWSDLValidationErrorResponse(error, 'file'); + }).finally(() => { + // Set the file as input value even when invalid so the uploaded-file row replaces the drop zone. + inputsDispatcher({ action: 'inputValue', value: file }); }); } else { setValidity({ ...isError, file: state }); @@ -214,10 +244,7 @@ export default function ProvideWSDL(props) { { - inputsDispatcher({ action: 'inputValue', value: null }); - inputsDispatcher({ action: 'isFormValid', value: false }); - }} + onClick={reset} size='large'> @@ -299,10 +326,10 @@ export default function ProvideWSDL(props) { return ( <> - + {isCreateMode && ( - + <> @@ -320,9 +347,9 @@ export default function ProvideWSDL(props) { onChange={ (event) => { inputsDispatcher({ action: 'type', value: event.target.value }); - inputsDispatcher({ action: 'isFormValid', value: false }); - inputsDispatcher({ action: 'inputValue', value: null }); inputsDispatcher({ action: 'inputType', value: 'url' }); + // Clear validation state so a stale error does not linger on type change. + reset(); } } > @@ -350,7 +377,7 @@ export default function ProvideWSDL(props) { )} - + <> @@ -390,20 +417,7 @@ export default function ProvideWSDL(props) { - {isError && isError.file - && ( - - setValidity({ file: null })} - disableActions - dense - paperProps={{ elevation: 1 }} - type='error' - message={isError.file.message} - /> - - )} - + {isFileInput ? renderFileUpload() : ( + {}} + /> ); diff --git a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/GenericEndpoint.jsx b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/GenericEndpoint.jsx index 367620acf54..bf5d845c1eb 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/GenericEndpoint.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/Apis/Details/Endpoints/GenericEndpoint.jsx @@ -24,7 +24,7 @@ import { InputAdornment, TextField, } from '@mui/material'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; import Tooltip from '@mui/material/Tooltip'; import CircularProgress from '@mui/material/CircularProgress'; import Chip from '@mui/material/Chip'; @@ -126,6 +126,7 @@ function GenericEndpoint(props) { const [isUpdating, setUpdating] = useState(false); const [isErrorCode, setIsErrorCode] = useState(false); const iff = (condition, then, otherwise) => (condition ? then : otherwise); + const intl = useIntl(); useEffect(() => { setServiceUrl(endpointURL); @@ -149,6 +150,18 @@ function GenericEndpoint(props) { } else { setIsEndpointValid(false); } + }).catch((error) => { + const message = error.response?.body?.description + || error.response?.body?.message + || error.message + || intl.formatMessage({ + id: 'Apis.Details.Endpoints.GenericEndpoint.endpoint.validation.error', + defaultMessage: 'Error while validating endpoint URL', + }); + setStatusCode(message); + setIsErrorCode(true); + setIsEndpointValid(false); + console.error(error); }).finally(() => { setUpdating(false); }); diff --git a/portals/publisher/src/main/webapp/source/src/app/components/MCPServers/Create/MCPServerCreateProxy.jsx b/portals/publisher/src/main/webapp/source/src/app/components/MCPServers/Create/MCPServerCreateProxy.jsx index 13ef4377d19..5cb1f752879 100644 --- a/portals/publisher/src/main/webapp/source/src/app/components/MCPServers/Create/MCPServerCreateProxy.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/components/MCPServers/Create/MCPServerCreateProxy.jsx @@ -231,7 +231,16 @@ const MCPServerCreateProxy = (props) => { }) .catch((error) => { if (error.response && error.response.body) { - setValidationError('Failed to validate MCP Server URL'); + const errorMessage = + error.response?.body?.description + || error.response?.body?.message + || error.message + || intl.formatMessage({ + id: 'MCPServers.Create.MCPServerCreateProxy.validation.error.detail', + defaultMessage: 'Failed to validate MCP Server URL.', + }); + + setValidationError(errorMessage); } else { const errorMsg = intl.formatMessage({ id: 'MCPServers.Create.MCPServerCreateProxy.validation.error',