From 2e542590784a71bbc7350d14523809bb70bc5c46 Mon Sep 17 00:00:00 2001 From: Thushani Jayasekera Date: Fri, 10 Jul 2026 00:06:32 +0530 Subject: [PATCH 1/5] Enhance error handling and introduce new error types This commit adds new error types for WebSub and WebBroker APIs, as well as HMAC secret management, to improve error categorization and handling. It also implements a new `Is` method for the `Error` type to allow comparison based on catalog codes rather than pointer identity. Additionally, the `FromError` and `LogAndWrite` functions are introduced to streamline error logging and response generation. Various API handlers have been updated to utilize a centralized error handling approach, enhancing maintainability and clarity across the codebase. --- platform-api/internal/apperror/apperror.go | 13 + .../internal/apperror/apperror_test.go | 26 ++ platform-api/internal/apperror/catalog.go | 20 ++ platform-api/internal/apperror/codes.go | 22 ++ platform-api/internal/apperror/respond.go | 95 +++++++ platform-api/internal/constants/error.go | 229 ---------------- platform-api/internal/handler/api.go | 127 +-------- .../internal/handler/api_deployment.go | 103 +------- platform-api/internal/handler/application.go | 81 +----- .../handler/artifact_guard_response.go | 47 ---- platform-api/internal/handler/gateway.go | 47 +--- .../internal/handler/gateway_internal.go | 101 ++++--- platform-api/internal/handler/llm.go | 247 ++---------------- platform-api/internal/handler/llm_apikey.go | 18 +- .../internal/handler/llm_deployment.go | 189 +------------- .../internal/handler/llm_proxy_apikey.go | 18 +- platform-api/internal/handler/mcp.go | 52 +--- .../internal/handler/mcp_deployment.go | 94 +------ platform-api/internal/handler/project.go | 10 +- platform-api/internal/handler/secret.go | 29 +- .../handler/secret_integration_test.go | 5 +- .../internal/handler/service_error.go | 47 ++++ .../internal/handler/subscription_handler.go | 39 +-- .../handler/subscription_plan_handler.go | 10 +- .../internal/middleware/error_mapper.go | 56 +--- .../internal/repository/custom_policy.go | 6 +- .../internal/repository/deployment.go | 10 +- platform-api/internal/repository/llm.go | 3 +- platform-api/internal/repository/secret.go | 12 +- .../internal/repository/secret_test.go | 12 +- .../repository/subscription_repository.go | 14 +- platform-api/internal/service/api.go | 66 ++--- platform-api/internal/service/api_test.go | 46 ++-- platform-api/internal/service/apikey.go | 12 +- platform-api/internal/service/application.go | 72 ++--- .../internal/service/application_test.go | 13 +- .../internal/service/artifact_guard.go | 11 +- .../internal/service/artifact_import.go | 17 +- .../service/artifact_import_lifecycle_test.go | 8 +- .../service/artifact_import_llm_provider.go | 6 +- .../service/artifact_import_llm_proxy.go | 5 +- .../internal/service/artifact_import_test.go | 16 +- .../artifact_runtime_immutable_test.go | 16 +- .../internal/service/custom_policy_test.go | 156 +++++------ platform-api/internal/service/deployment.go | 53 ++-- .../internal/service/deployment_test.go | 36 +-- .../service/deployment_undeploy_guard_test.go | 10 +- platform-api/internal/service/gateway.go | 44 ++-- .../internal/service/gateway_internal.go | 52 ++-- platform-api/internal/service/llm.go | 228 ++++++++-------- platform-api/internal/service/llm_apikey.go | 16 +- .../internal/service/llm_deployment.go | 119 +++++---- .../service/llm_provider_template_test.go | 58 ++-- .../internal/service/llm_proxy_apikey.go | 12 +- .../service/llm_secret_validation_test.go | 21 +- platform-api/internal/service/llm_test.go | 55 ++-- platform-api/internal/service/mcp.go | 53 ++-- .../internal/service/mcp_deployment.go | 71 ++--- platform-api/internal/service/mcp_test.go | 7 +- .../internal/service/policy_validation.go | 11 +- .../service/policy_validation_test.go | 13 +- .../internal/service/secret_service.go | 11 +- .../internal/service/secret_service_test.go | 37 +-- .../service/subscription_plan_service.go | 18 +- .../internal/service/subscription_service.go | 63 +++-- platform-api/internal/utils/handle.go | 22 +- platform-api/internal/utils/mcp.go | 5 +- .../internal/webhook/handlers_application.go | 11 +- .../internal/webhook/handlers_subscription.go | 13 +- platform-api/internal/webhook/receiver.go | 28 +- .../handler/artifact_guard_response.go | 50 ---- .../handler/catalog_error_response.go | 54 ++++ .../handler/catalog_error_response_test.go | 130 +++++++++ .../eventgateway/handler/webbroker_api.go | 29 +- .../handler/webbroker_api_deployment.go | 41 +-- .../eventgateway/handler/webbroker_apikey.go | 30 +-- .../eventgateway/handler/websub_api.go | 29 +- .../handler/websub_api_deployment.go | 41 +-- .../handler/websub_api_hmac_secret.go | 30 +-- .../eventgateway/handler/websub_apikey.go | 30 +-- platform-api/plugins/eventgateway/plugin.go | 5 +- .../plugins/eventgateway/service/helpers.go | 9 +- .../eventgateway/service/webbroker_api.go | 35 +-- .../service/webbroker_api_deployment.go | 55 ++-- .../eventgateway/service/websub_api.go | 35 +-- .../service/websub_api_deployment.go | 55 ++-- .../service/websub_api_hmac_secret.go | 26 +- 87 files changed, 1554 insertions(+), 2423 deletions(-) create mode 100644 platform-api/internal/apperror/respond.go delete mode 100644 platform-api/internal/constants/error.go delete mode 100644 platform-api/internal/handler/artifact_guard_response.go create mode 100644 platform-api/internal/handler/service_error.go delete mode 100644 platform-api/plugins/eventgateway/handler/artifact_guard_response.go create mode 100644 platform-api/plugins/eventgateway/handler/catalog_error_response.go create mode 100644 platform-api/plugins/eventgateway/handler/catalog_error_response_test.go diff --git a/platform-api/internal/apperror/apperror.go b/platform-api/internal/apperror/apperror.go index 67cb25318..afba7005f 100644 --- a/platform-api/internal/apperror/apperror.go +++ b/platform-api/internal/apperror/apperror.go @@ -60,6 +60,19 @@ func (e *Error) Error() string { // Unwrap exposes the wrapped cause to errors.Is/errors.As chains. func (e *Error) Unwrap() error { return e.Cause } +// Is reports whether target is an *Error carrying the same catalog code, so +// errors.Is(err, SomeDef.New()) matches on the code rather than on pointer +// identity. Without it, every Def.New() allocates a fresh value and no two +// instances of the same failure would ever compare equal — the trap that the +// package-level sentinel errors this catalog replaced used to paper over. +// +// Prefer Def.Is(err) at call sites; this method exists so catalog errors also +// behave correctly when passed through the standard errors.Is protocol. +func (e *Error) Is(target error) bool { + var t *Error + return errors.As(target, &t) && t.Code == e.Code +} + // NewValidation converts a request-validation failure into an Error, // mirroring NewValidationErrorResponse: validator.ValidationErrors // become field-level errors under VALIDATION_FAILED; anything else diff --git a/platform-api/internal/apperror/apperror_test.go b/platform-api/internal/apperror/apperror_test.go index 5b94f5420..f4679a804 100644 --- a/platform-api/internal/apperror/apperror_test.go +++ b/platform-api/internal/apperror/apperror_test.go @@ -137,3 +137,29 @@ func TestWriteHTTP(t *testing.T) { t.Errorf("expected trackingId echoed in body, got %+v", body) } } + +// TestErrorIs_MatchesOnCodeNotIdentity guards the behaviour that lets +// errors.Is treat two independently constructed instances of the same catalog +// entry as equal. Def.New allocates a fresh *Error each call, so pointer +// identity would never match. +func TestErrorIs_MatchesOnCodeNotIdentity(t *testing.T) { + a := NotFound.New() + b := NotFound.New() + if a == b { + t.Fatal("Def.New must allocate a distinct *Error each call") + } + if !errors.Is(a, b) { + t.Error("errors.Is should match two errors carrying the same catalog code") + } + if errors.Is(a, Conflict.New()) { + t.Error("errors.Is must not match errors carrying different catalog codes") + } + // The code must also be found through a wrapping chain. + wrapped := fmt.Errorf("context: %w", a) + if !errors.Is(wrapped, NotFound.New()) { + t.Error("errors.Is should see through fmt.Errorf %w wrapping") + } + if !NotFound.Is(wrapped) { + t.Error("Def.Is should see through fmt.Errorf %w wrapping") + } +} diff --git a/platform-api/internal/apperror/catalog.go b/platform-api/internal/apperror/catalog.go index b9cbeffea..05a8de39b 100644 --- a/platform-api/internal/apperror/catalog.go +++ b/platform-api/internal/apperror/catalog.go @@ -186,3 +186,23 @@ var ( ApplicationAPIKeyNotFound = def(CodeApplicationAPIKeyNotFound, http.StatusNotFound, "The specified API key could not be found.") ApplicationAPIKeyForbidden = def(CodeApplicationAPIKeyForbidden, http.StatusForbidden, "You do not have permission to access this API key.") ) + +// WebSub / WebBroker API entries. +var ( + WebSubAPINotFound = def(CodeWebSubAPINotFound, http.StatusNotFound, "The specified WebSub API could not be found.") + WebSubAPIExists = def(CodeWebSubAPIExists, http.StatusConflict, "A WebSub API with this ID already exists.") + WebSubAPILimitReached = def(CodeWebSubAPILimitReached, http.StatusConflict, "WebSub API limit reached for the organization.") + WebBrokerAPINotFound = def(CodeWebBrokerAPINotFound, http.StatusNotFound, "The specified WebBroker API could not be found.") + WebBrokerAPIExists = def(CodeWebBrokerAPIExists, http.StatusConflict, "A WebBroker API with this ID already exists.") + WebBrokerAPILimitReached = def(CodeWebBrokerAPILimitReached, http.StatusConflict, "WebBroker API limit reached for the organization.") +) + +// HMAC secret entries. The 32-character minimum is a fixed, publicly +// documented rule, so stating it in the client message reveals nothing the +// API contract does not already. +var ( + HmacSecretNotFound = def(CodeHmacSecretNotFound, http.StatusNotFound, "The specified HMAC secret could not be found.") + HmacSecretExists = def(CodeHmacSecretExists, http.StatusConflict, "An HMAC secret with this name already exists.") + HmacSecretInvalidValue = def(CodeHmacSecretInvalidValue, http.StatusBadRequest, "The secret value must be at least 32 characters.") + HmacSecretNotConfigured = def(CodeHmacSecretNotConfigured, http.StatusServiceUnavailable, "HMAC secret management is not configured on this server.") +) diff --git a/platform-api/internal/apperror/codes.go b/platform-api/internal/apperror/codes.go index 28a7f018f..572e5fef3 100644 --- a/platform-api/internal/apperror/codes.go +++ b/platform-api/internal/apperror/codes.go @@ -176,3 +176,25 @@ const ( CodeApplicationAPIKeyNotFound = "APPLICATION_API_KEY_NOT_FOUND" CodeApplicationAPIKeyForbidden = "APPLICATION_API_KEY_FORBIDDEN" ) + +// WebSub and WebBroker API domain codes, raised by the event-gateway plugin +// and by the two gateway-internal artifact lookups. +const ( + CodeWebSubAPINotFound = "WEBSUB_API_NOT_FOUND" + CodeWebSubAPIExists = "WEBSUB_API_EXISTS" + CodeWebSubAPILimitReached = "WEBSUB_API_LIMIT_REACHED" + CodeWebBrokerAPINotFound = "WEBBROKER_API_NOT_FOUND" + CodeWebBrokerAPIExists = "WEBBROKER_API_EXISTS" + CodeWebBrokerAPILimitReached = "WEBBROKER_API_LIMIT_REACHED" +) + +// HMAC secret domain codes (WebSub subscriber callback signing secrets). +// HMAC_SECRET_NOT_CONFIGURED is a 503 rather than a 500: the encryption key is +// a deployment-time setting, so the condition is transient from the client's +// point of view and resolvable without a code change. +const ( + CodeHmacSecretNotFound = "HMAC_SECRET_NOT_FOUND" + CodeHmacSecretExists = "HMAC_SECRET_EXISTS" + CodeHmacSecretInvalidValue = "HMAC_SECRET_INVALID_VALUE" + CodeHmacSecretNotConfigured = "HMAC_SECRET_NOT_CONFIGURED" +) diff --git a/platform-api/internal/apperror/respond.go b/platform-api/internal/apperror/respond.go new file mode 100644 index 000000000..3647cd7a0 --- /dev/null +++ b/platform-api/internal/apperror/respond.go @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * Licensed 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. + * + */ + +package apperror + +import ( + "errors" + "log/slog" + "net/http" + + "github.com/google/uuid" +) + +// FromError coerces err into an *Error. A non-catalog error becomes a generic +// Internal wrapping it, so internal details never reach the client. +// +// A handler fallback may have wrapped a more specific typed error produced +// deeper in the stack (service layer) in a generic Internal — the loop prefers +// the specific one so service-origin errors keep their code and status. +func FromError(err error) *Error { + var appErr *Error + if !errors.As(err, &appErr) { + return Internal.Wrap(err) + } + for appErr.Code == CodeCommonInternalError && appErr.Cause != nil { + var inner *Error + if !errors.As(appErr.Cause, &inner) { + break + } + appErr = inner + } + return appErr +} + +// LogAndWrite is the single implementation of the log-then-respond severity +// split, shared by middleware.MapErrors and the event-gateway plugin's +// respondCatalogError so the two paths cannot drift. +// +// A 4xx is a client outcome, not a system fault — it logs at WARN without the +// stack, and no tracking ID is exposed. A 5xx is a system fault — it logs at +// ERROR with the origin stack captured at the Def.New/Def.Wrap call site, and +// the tracking ID is echoed in the response body so the client can quote it +// back for correlation. +// +// extra supplies additional slog key/value pairs (e.g. "path", "method") and +// is appended after the status field so field order stays stable across +// callers. +func LogAndWrite(w http.ResponseWriter, slogger *slog.Logger, e *Error, extra ...any) { + trackID := uuid.NewString() + + logFields := []any{ + "trackingId", trackID, + "code", e.Code, + "status", e.HTTPStatus, + } + logFields = append(logFields, extra...) + if e.LogMessage != "" { + logFields = append(logFields, "detail", e.LogMessage) + } + if e.Cause != nil { + logFields = append(logFields, "cause", e.Cause.Error()) + } + // origin is the file:line where the error was actually constructed + // (Def.New/Def.Wrap call site) — not to be confused with slog's own + // "source" attribute, which always points at this log call. + if origin := e.Origin(); origin != "" { + logFields = append(logFields, "origin", origin) + } + + if e.HTTPStatus >= http.StatusInternalServerError { + if len(e.Stack) > 0 { + logFields = append(logFields, "stack", e.StackString()) + } + slogger.Error("request failed", logFields...) + WriteHTTP(w, e, trackID) + return + } + + slogger.Warn("request failed", logFields...) + WriteHTTP(w, e, "") +} diff --git a/platform-api/internal/constants/error.go b/platform-api/internal/constants/error.go deleted file mode 100644 index ce8ba1117..000000000 --- a/platform-api/internal/constants/error.go +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. - * - * Licensed 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. - * - */ - -package constants - -import "errors" - -var ( - ErrHandleExists = errors.New("handle already exists") - ErrHandleDoesNotExist = errors.New("handle does not exist") - ErrHandleEmpty = errors.New("handle cannot be empty") - ErrHandleTooShort = errors.New("handle must be at least 3 characters") - ErrHandleTooLong = errors.New("handle must be at most 63 characters") - ErrInvalidHandle = errors.New("handle must be lowercase alphanumeric with hyphens only (no consecutive hyphens, cannot start or end with hyphen)") - ErrHandleGenerationFailed = errors.New("failed to generate unique handle after maximum retries") - ErrHandleSourceEmpty = errors.New("source string cannot be empty for handle generation") - ErrOrganizationExists = errors.New("organization already exists with the given UUID") - ErrOrganizationNotFound = errors.New("organization not found") - ErrMultipleOrganizations = errors.New("multiple organizations found") - ErrInvalidInput = errors.New("invalid input parameters") - ErrHandleImmutable = errors.New("id is immutable and cannot be changed") -) - -var ( - ErrProjectExists = errors.New("project already exists in organization") - ErrProjectNotFound = errors.New("project not found") - ErrInvalidProjectName = errors.New("invalid project name") - ErrOrganizationMustHAveAtLeastOneProject = errors.New("organization must have at least one project") - ErrProjectHasAssociatedAPIs = errors.New("project has associated APIs") - ErrProjectHasAssociatedMCPProxies = errors.New("project has associated MCP proxies") - ErrProjectHasAssociatedApplications = errors.New("project has associated applications") - ErrorInvalidProjectUUID = errors.New("invalid project UUID") -) - -var ( - ErrApplicationExists = errors.New("application already exists in project") - ErrApplicationNotFound = errors.New("application not found") - ErrInvalidApplicationName = errors.New("invalid application name") - ErrInvalidApplicationType = errors.New("invalid application type") - ErrUnsupportedApplicationType = errors.New("unsupported application type") - ErrInvalidApplicationID = errors.New("invalid application ID") -) - -var ( - ErrAPINotFound = errors.New("api not found") - ErrAPIAlreadyExists = errors.New("api already exists in project") - ErrAPINameVersionAlreadyExists = errors.New("api with same name and version already exists") - ErrInvalidAPIContext = errors.New("invalid api context format") - ErrInvalidAPIVersion = errors.New("invalid api version format") - ErrInvalidAPIName = errors.New("invalid api name format") - ErrInvalidLifecycleState = errors.New("invalid lifecycle state") - ErrInvalidAPIType = errors.New("invalid api type") - ErrInvalidTransport = errors.New("invalid transport protocol") - ErrInvalidDeployment = errors.New("invalid api deployment") - ErrGatewayNotAssociated = errors.New("api is not associated with gateway") - ErrAPIContextVersionConflict = errors.New("api with same context and version already deployed in gateway") - ErrUpstreamRequired = errors.New("upstream configuration is required") -) - -var ( - ErrGatewayNotFound = errors.New("gateway not found") - ErrGatewayAlreadyAssociated = errors.New("gateway already associated with API") - ErrGatewayHasAssociatedAPIs = errors.New("cannot delete gateway: it has associated APIs. Please remove all API associations before deleting the gateway") - ErrGatewayHasDeployments = errors.New("cannot delete gateway: it has active API deployments. Please undeploy all APIs before deleting the gateway") - ErrGatewayVersionMismatch = errors.New("gateway version mismatch") - ErrGatewayFunctionalityTypeMismatch = errors.New("gateway functionality type mismatch") -) - -var ( - ErrCustomPolicyNotFound = errors.New("custom policy not found") - ErrCustomPolicyInUse = errors.New("custom policy is in use by one or more APIs") - ErrCustomPolicyVersionMismatch = errors.New("custom policy version does not match") -) - -var ( - ErrInvalidPolicyVersion = errors.New("policy version must be major-only, e.g. v1") -) - -var ( - ErrDeploymentNotFound = errors.New("deployment not found") - ErrDeploymentNotActive = errors.New("no active deployment found for this API on the gateway") - ErrDeploymentIsDeployed = errors.New("cannot delete an active deployment - undeploy it first") - ErrDeploymentAlreadyActive = errors.New("deployment is already active") - ErrBaseDeploymentNotFound = errors.New("base deployment not found") - ErrInvalidDeploymentStatus = errors.New("invalid deployment status") - ErrDeploymentNameRequired = errors.New("deployment name is required") - ErrDeploymentBaseRequired = errors.New("base is required") - ErrDeploymentGatewayIDRequired = errors.New("gatewayId is required") - ErrAPINoBackendServices = errors.New("API must have at least one backend service attached before deployment") - ErrDeploymentAlreadyDeployed = errors.New("cannot restore to the currently deployed deployment") - ErrInvalidDeploymentRestoreState = errors.New("deployment cannot be restored: only ARCHIVED or UNDEPLOYED deployments are eligible") - ErrGatewayIDMismatch = errors.New("gateway ID mismatch: deployment is bound to a different gateway") -) - -var ( - ErrArtifactNotFound = errors.New("artifact not found") - ErrArtifactExists = errors.New("artifact already exists") - ErrArtifactInvalidKind = errors.New("invalid artifact kind") - // ErrArtifactReadOnly is returned when a mutating control-plane operation is - // attempted on a data-plane-originated (origin=DP) artifact. Such artifacts are - // read-only in the control plane; only documentation/OpenAPI updates are allowed. - ErrArtifactReadOnly = errors.New("artifact is read-only: it originated from a data-plane gateway") - // ErrArtifactDeployed is returned when a DP-originated artifact is deleted from the - // control plane while still deployed on one or more gateways. It can only be deleted - // once it is undeployed on all gateways it was deployed to. - ErrArtifactDeployed = errors.New("artifact is still deployed on a gateway and cannot be deleted") - // ErrArtifactRuntimeImmutable is returned when an update to a data-plane-originated - // (origin=DP) artifact would change its gateway runtime artifact. The gateway owns - // the runtime artifact, so a change to it cannot be applied from the control plane; - // edits that leave the runtime artifact unchanged (description, lifecycle status, - // display metadata, docs) are allowed. It is a distinct error from ErrArtifactReadOnly - // (which blocks all edits) and is mapped to 403 separately by the guard-response handler. - ErrArtifactRuntimeImmutable = errors.New( - "the update changes the gateway runtime configuration, which is owned by the data-plane gateway and cannot be modified from the control plane") -) - -var ( - // API Project Import errors - ErrAPIProjectNotFound = errors.New("api project not found") - ErrMalformedAPIProject = errors.New("malformed api project") - ErrInvalidAPIProject = errors.New("invalid api project") - ErrConfigFileNotFound = errors.New("API Project config file not found") - ErrOpenAPIFileNotFound = errors.New("OpenAPI definition file not found") - ErrWSO2ArtifactNotFound = errors.New("WSO2 API artifact not found") -) - -var ( - ErrLLMProviderTemplateExists = errors.New("llm provider template already exists") - ErrLLMProviderTemplateNotFound = errors.New("llm provider template not found") - ErrLLMProviderTemplateVersionExists = errors.New("llm provider template version already exists") - ErrLLMProviderTemplateInUse = errors.New("llm provider template is in use by one or more providers") - ErrLLMProviderTemplateReadOnly = errors.New("built-in llm provider template is read-only") - ErrLLMProviderTemplateNotToggleable = errors.New("only built-in templates can be enabled or disabled") - ErrLLMProviderTemplateManagedByReserved = errors.New("'wso2' is reserved and cannot be used as managedBy on custom templates") - ErrLLMProviderExists = errors.New("llm provider already exists") - ErrLLMProviderNotFound = errors.New("llm provider not found") - ErrLLMProviderLimitReached = errors.New("llm provider limit reached for organization") - ErrLLMProxyExists = errors.New("llm proxy already exists") - ErrLLMProxyNotFound = errors.New("llm proxy not found") - ErrLLMProxyLimitReached = errors.New("llm proxy limit reached for organization") -) - -var ( - ErrMCPProxyExists = errors.New("mcp proxy already exists") - ErrMCPProxyNotFound = errors.New("mcp proxy not found") - ErrMCPProxyLimitReached = errors.New("mcp proxy limit reached for organization") -) - -var ( - ErrWebSubAPIExists = errors.New("websub api already exists") - ErrWebSubAPINotFound = errors.New("websub api not found") - ErrWebSubAPILimitReached = errors.New("websub api limit reached for organization") - ErrProjectHasAssociatedWebSubAPIs = errors.New("project has associated WebSub APIs") -) - -var ( - ErrHmacSecretNotFound = errors.New("hmac secret not found") - ErrHmacSecretAlreadyExists = errors.New("hmac secret with this name already exists") - ErrHmacSecretEncryptionKeyMissing = errors.New("hmac secret encryption key is not configured") - ErrHmacSecretInvalidValue = errors.New("secret value must be at least 32 characters") -) - -var ( - ErrWebBrokerAPIExists = errors.New("webbroker api already exists") - ErrWebBrokerAPINotFound = errors.New("webbroker api not found") - ErrWebBrokerAPILimitReached = errors.New("webbroker api limit reached for organization") - ErrProjectHasAssociatedWebBrokerAPIs = errors.New("project has associated WebBroker APIs") - ErrDevPortalNotFound = errors.New("dev portal not found") -) - -var ( - // API Key errors - ErrAPIKeyNotFound = errors.New("api key not found") - ErrAPIKeyAlreadyExists = errors.New("api key already exists") - ErrInvalidAPIKey = errors.New("invalid api key") - ErrAPIKeyForbidden = errors.New("forbidden: only the key creator can perform this action") - ErrGatewayUnavailable = errors.New("gateway unavailable") - ErrAPIKeyEventDelivery = errors.New("failed to deliver api key event to gateway") - ErrAPIKeyHashingFailed = errors.New("failed to hash api key") -) - -var ( - ErrInvalidURL = errors.New("invalid URL") - ErrURLUnreachable = errors.New("URL is unreachable") - ErrMCPServerUnauthorized = errors.New("MCP server returned 401 Unauthorized") -) - -var ( - // Subscription errors (application-level subscriptions for REST APIs) - ErrSubscriptionNotFound = errors.New("subscription not found") - ErrSubscriptionAlreadyExists = errors.New("application is already subscribed to this API") - ErrSubscriptionSubscriberMismatch = errors.New("subscriber does not match this subscription") -) - -var ( - // Subscription plan errors - ErrSubscriptionPlanNotFound = errors.New("subscription plan not found") - ErrSubscriptionPlanNotFoundOrInactive = errors.New("subscription plan not found or not active") - ErrSubscriptionPlanAlreadyExists = errors.New("subscription plan with this name already exists for the organization") - ErrInvalidThrottleLimitUnit = errors.New("invalid throttle limit unit: must be one of SECOND, MINUTE, HOUR, DAY, MONTH, YEAR") -) - -var ( - // Gateway Internal API errors - ErrMissingAPIKey = errors.New("API key is required") - ErrInvalidAPIToken = errors.New("invalid API token") -) - -var ( - ErrSecretAlreadyExists = errors.New("secret already exists for this organization and handle") - ErrSecretNotFound = errors.New("secret not found") - ErrSecretInUse = errors.New("secret is referenced by one or more resources") - ErrSecretRefMissing = errors.New("one or more referenced secrets do not exist") - ErrInvalidSecretType = errors.New("invalid secret type: must be GENERIC or CERTIFICATE") -) diff --git a/platform-api/internal/handler/api.go b/platform-api/internal/handler/api.go index e2f8c5658..d79c55314 100644 --- a/platform-api/internal/handler/api.go +++ b/platform-api/internal/handler/api.go @@ -19,7 +19,6 @@ package handler import ( "encoding/json" - "errors" "fmt" "log/slog" "net/http" @@ -85,56 +84,7 @@ func (h *APIHandler) CreateAPI(w http.ResponseWriter, r *http.Request) error { } apiResponse, err := h.apiService.CreateAPI(&req, orgId, createdBy) if err != nil { - if errors.Is(err, constants.ErrHandleExists) { - return apperror.RESTAPIExists.Wrap(err, "An API with this handle already exists."). - WithLogMessage(fmt.Sprintf("API handle already exists in org %s", orgId)) - } - if errors.Is(err, constants.ErrAPINameVersionAlreadyExists) { - return apperror.RESTAPIExists.Wrap(err, "An API with the same name and version already exists in the organization."). - WithLogMessage(fmt.Sprintf("API with same name and version already exists in org %s", orgId)) - } - if errors.Is(err, constants.ErrAPIAlreadyExists) { - return apperror.RESTAPIExists.Wrap(err, "An API already exists in the project."). - WithLogMessage(fmt.Sprintf("API already exists in the project in org %s", orgId)) - } - if errors.Is(err, constants.ErrProjectNotFound) { - return apperror.ProjectNotFound.Wrap(err). - WithLogMessage(fmt.Sprintf("project not found in org %s", orgId)) - } - if errors.Is(err, constants.ErrInvalidAPIName) { - return apperror.ValidationFailed.Wrap(err, "Invalid API name format"). - WithLogMessage(fmt.Sprintf("invalid API name format in org %s", orgId)) - } - if errors.Is(err, constants.ErrInvalidAPIContext) { - return apperror.ValidationFailed.Wrap(err, "Invalid API context format"). - WithLogMessage(fmt.Sprintf("invalid API context format in org %s", orgId)) - } - if errors.Is(err, constants.ErrInvalidAPIVersion) { - return apperror.ValidationFailed.Wrap(err, "Invalid API version format"). - WithLogMessage(fmt.Sprintf("invalid API version format in org %s", orgId)) - } - if errors.Is(err, constants.ErrInvalidLifecycleState) { - return apperror.ValidationFailed.Wrap(err, "Invalid lifecycle status"). - WithLogMessage(fmt.Sprintf("invalid lifecycle status in org %s", orgId)) - } - if errors.Is(err, constants.ErrInvalidAPIType) { - return apperror.ValidationFailed.Wrap(err, "Invalid API type"). - WithLogMessage(fmt.Sprintf("invalid API type in org %s", orgId)) - } - if errors.Is(err, constants.ErrInvalidTransport) { - return apperror.ValidationFailed.Wrap(err, "Invalid transport protocol"). - WithLogMessage(fmt.Sprintf("invalid transport protocol in org %s", orgId)) - } - if errors.Is(err, constants.ErrInvalidPolicyVersion) { - return apperror.ValidationFailed.Wrap(err, "Invalid policy version format"). - WithLogMessage(fmt.Sprintf("invalid policy version format in org %s", orgId)) - } - if errors.Is(err, constants.ErrSubscriptionPlanNotFoundOrInactive) { - return apperror.ValidationFailed.Wrap(err, "Subscription plan not found or not active"). - WithLogMessage(fmt.Sprintf("subscription plan not found or not active in org %s", orgId)) - } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to create API in org %s", orgId)) + return serviceError(err, fmt.Sprintf("failed to create API in org %s", orgId)) } setLocation(w, "rest-apis", strOrEmpty(apiResponse.Id)) @@ -157,12 +107,7 @@ func (h *APIHandler) GetAPI(w http.ResponseWriter, r *http.Request) error { apiResponse, err := h.apiService.GetAPIByHandle(apiId, orgId) if err != nil { - if errors.Is(err, constants.ErrAPINotFound) { - return apperror.RESTAPINotFound.Wrap(err). - WithLogMessage(fmt.Sprintf("API %s not found in org %s", apiId, orgId)) - } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to get API %s in org %s", apiId, orgId)) + return serviceError(err, fmt.Sprintf("failed to get API %s in org %s", apiId, orgId)) } httputil.WriteJSON(w, http.StatusOK, apiResponse) @@ -187,12 +132,7 @@ func (h *APIHandler) ListAPIs(w http.ResponseWriter, r *http.Request) error { apis, total, err := h.apiService.GetAPIsByOrganization(orgId, projectId, opts) if err != nil { - if errors.Is(err, constants.ErrProjectNotFound) { - return apperror.ProjectNotFound.Wrap(err). - WithLogMessage(fmt.Sprintf("project %s not found in org %s", projectId, orgId)) - } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to get APIs for project %s in org %s", projectId, orgId)) + return serviceError(err, fmt.Sprintf("failed to get APIs for project %s in org %s", projectId, orgId)) } response := api.RESTAPIListResponse{ @@ -238,38 +178,7 @@ func (h *APIHandler) UpdateAPI(w http.ResponseWriter, r *http.Request) error { } apiResponse, err := h.apiService.UpdateAPIByHandle(apiId, &req, orgId, updatedBy) if err != nil { - if guardErr := mapArtifactGuardError(err); guardErr != nil { - return guardErr - } - if errors.Is(err, constants.ErrAPINotFound) { - return apperror.RESTAPINotFound.Wrap(err). - WithLogMessage(fmt.Sprintf("API %s not found in org %s", apiId, orgId)) - } - if errors.Is(err, constants.ErrHandleImmutable) { - return apperror.ValidationFailed.Wrap(err, "The id is immutable and cannot be changed") - } - if errors.Is(err, constants.ErrInvalidLifecycleState) { - return apperror.ValidationFailed.Wrap(err, "Invalid lifecycle status"). - WithLogMessage(fmt.Sprintf("invalid lifecycle status for API %s in org %s", apiId, orgId)) - } - if errors.Is(err, constants.ErrInvalidAPIType) { - return apperror.ValidationFailed.Wrap(err, "Invalid API type"). - WithLogMessage(fmt.Sprintf("invalid API type for API %s in org %s", apiId, orgId)) - } - if errors.Is(err, constants.ErrInvalidTransport) { - return apperror.ValidationFailed.Wrap(err, "Invalid transport protocol"). - WithLogMessage(fmt.Sprintf("invalid transport protocol for API %s in org %s", apiId, orgId)) - } - if errors.Is(err, constants.ErrInvalidPolicyVersion) { - return apperror.ValidationFailed.Wrap(err, "Invalid policy version format"). - WithLogMessage(fmt.Sprintf("invalid policy version format for API %s in org %s", apiId, orgId)) - } - if errors.Is(err, constants.ErrSubscriptionPlanNotFoundOrInactive) { - return apperror.ValidationFailed.Wrap(err, "Subscription plan not found or not active"). - WithLogMessage(fmt.Sprintf("subscription plan not found or not active for API %s in org %s", apiId, orgId)) - } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to update API %s in org %s", apiId, orgId)) + return serviceError(err, fmt.Sprintf("failed to update API %s in org %s", apiId, orgId)) } httputil.WriteJSON(w, http.StatusOK, apiResponse) @@ -294,15 +203,7 @@ func (h *APIHandler) DeleteAPI(w http.ResponseWriter, r *http.Request) error { return err } if err := h.apiService.DeleteAPIByHandle(apiId, orgId, deletedBy); err != nil { - if guardErr := mapArtifactGuardError(err); guardErr != nil { - return guardErr - } - if errors.Is(err, constants.ErrAPINotFound) { - return apperror.RESTAPINotFound.Wrap(err). - WithLogMessage(fmt.Sprintf("API %s not found in org %s", apiId, orgId)) - } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to delete API %s in org %s", apiId, orgId)) + return serviceError(err, fmt.Sprintf("failed to delete API %s in org %s", apiId, orgId)) } httputil.WriteJSON(w, http.StatusNoContent, nil) @@ -339,16 +240,7 @@ func (h *APIHandler) AddGatewaysToAPI(w http.ResponseWriter, r *http.Request) er gatewaysResponse, err := h.apiService.AddGatewaysToAPIByHandle(apiId, gatewayIds, orgId) if err != nil { - if errors.Is(err, constants.ErrAPINotFound) { - return apperror.RESTAPINotFound.Wrap(err). - WithLogMessage(fmt.Sprintf("API %s not found in org %s", apiId, orgId)) - } - if errors.Is(err, constants.ErrGatewayNotFound) { - return apperror.GatewayNotFound.Wrap(err). - WithLogMessage(fmt.Sprintf("one or more gateways not found for API %s in org %s", apiId, orgId)) - } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to associate gateways with API %s in org %s", apiId, orgId)) + return serviceError(err, fmt.Sprintf("failed to associate gateways with API %s in org %s", apiId, orgId)) } httputil.WriteJSON(w, http.StatusOK, gatewaysResponse) @@ -372,12 +264,7 @@ func (h *APIHandler) GetAPIGateways(w http.ResponseWriter, r *http.Request) erro gatewaysResponse, err := h.apiService.GetAPIGatewaysByHandle(apiId, orgId, limit, offset) if err != nil { - if errors.Is(err, constants.ErrAPINotFound) { - return apperror.RESTAPINotFound.Wrap(err). - WithLogMessage(fmt.Sprintf("API %s not found in org %s", apiId, orgId)) - } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to get gateways for API %s in org %s", apiId, orgId)) + return serviceError(err, fmt.Sprintf("failed to get gateways for API %s in org %s", apiId, orgId)) } httputil.WriteJSON(w, http.StatusOK, gatewaysResponse) diff --git a/platform-api/internal/handler/api_deployment.go b/platform-api/internal/handler/api_deployment.go index f006546eb..a37a1c169 100644 --- a/platform-api/internal/handler/api_deployment.go +++ b/platform-api/internal/handler/api_deployment.go @@ -19,7 +19,6 @@ package handler import ( "encoding/json" - "errors" "fmt" "log/slog" "net/http" @@ -84,32 +83,7 @@ func (h *DeploymentHandler) DeployAPI(w http.ResponseWriter, r *http.Request) er } deployment, err := h.deploymentService.DeployAPIByHandle(apiId, &req, orgId, createdBy) if err != nil { - if guardErr := mapArtifactGuardError(err); guardErr != nil { - return guardErr - } - if errors.Is(err, constants.ErrAPINotFound) { - return apperror.RESTAPINotFound.Wrap(err) - } - if errors.Is(err, constants.ErrGatewayNotFound) { - return apperror.GatewayNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrBaseDeploymentNotFound) { - return apperror.DeploymentBaseNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrDeploymentNameRequired) { - return apperror.RESTAPIDeploymentValidationFailed.Wrap(err, "Deployment name is required") - } - if errors.Is(err, constants.ErrDeploymentBaseRequired) { - return apperror.RESTAPIDeploymentValidationFailed.Wrap(err, "Base is required (use 'current' or a deploymentId)") - } - if errors.Is(err, constants.ErrDeploymentGatewayIDRequired) { - return apperror.RESTAPIDeploymentValidationFailed.Wrap(err, "Gateway ID is required") - } - if errors.Is(err, constants.ErrAPINoBackendServices) { - return apperror.RESTAPIDeploymentValidationFailed.Wrap(err, "API must have at least one backend service attached before deployment") - } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to deploy API %s", apiId)) + return serviceError(err, fmt.Sprintf("failed to deploy API %s", apiId)) } setLocation(w, "rest-apis", apiId, "deployments", deployment.DeploymentId.String()) @@ -148,26 +122,7 @@ func (h *DeploymentHandler) UndeployDeployment(w http.ResponseWriter, r *http.Re deployment, err := h.deploymentService.UndeployDeploymentByHandle(apiId, deploymentId, gatewayId, orgId, actor) if err != nil { // DP-originated artifacts are read-only: undeployment cannot be initiated from the CP. - if guardErr := mapArtifactGuardError(err); guardErr != nil { - return guardErr - } - if errors.Is(err, constants.ErrAPINotFound) { - return apperror.RESTAPINotFound.Wrap(err) - } - if errors.Is(err, constants.ErrDeploymentNotFound) { - return apperror.DeploymentNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrGatewayNotFound) { - return apperror.GatewayNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrDeploymentNotActive) { - return apperror.DeploymentNotActive.Wrap(err, "API") - } - if errors.Is(err, constants.ErrGatewayIDMismatch) { - return apperror.DeploymentGatewayMismatch.Wrap(err) - } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to undeploy API %s deployment %s from gateway %s", apiId, deploymentId, gatewayId)) + return serviceError(err, fmt.Sprintf("failed to undeploy API %s deployment %s from gateway %s", apiId, deploymentId, gatewayId)) } httputil.WriteJSON(w, http.StatusOK, deployment) @@ -205,26 +160,7 @@ func (h *DeploymentHandler) RestoreDeployment(w http.ResponseWriter, r *http.Req deployment, err := h.deploymentService.RestoreDeploymentByHandle(apiId, deploymentId, gatewayId, orgId, actor) if err != nil { // DP-originated artifacts are read-only: restore cannot be initiated from the CP. - if guardErr := mapArtifactGuardError(err); guardErr != nil { - return guardErr - } - if errors.Is(err, constants.ErrAPINotFound) { - return apperror.RESTAPINotFound.Wrap(err) - } - if errors.Is(err, constants.ErrDeploymentNotFound) { - return apperror.DeploymentNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrGatewayNotFound) { - return apperror.GatewayNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrDeploymentAlreadyDeployed) { - return apperror.DeploymentRestoreConflict.Wrap(err) - } - if errors.Is(err, constants.ErrGatewayIDMismatch) { - return apperror.DeploymentGatewayMismatch.Wrap(err) - } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to restore API %s deployment %s on gateway %s", apiId, deploymentId, gatewayId)) + return serviceError(err, fmt.Sprintf("failed to restore API %s deployment %s on gateway %s", apiId, deploymentId, gatewayId)) } httputil.WriteJSON(w, http.StatusOK, deployment) @@ -255,20 +191,7 @@ func (h *DeploymentHandler) DeleteDeployment(w http.ResponseWriter, r *http.Requ return err } if err := h.deploymentService.DeleteDeploymentByHandle(apiId, deploymentId, orgId, actor); err != nil { - if errors.Is(err, constants.ErrAPINotFound) { - return apperror.RESTAPINotFound.Wrap(err) - } - if errors.Is(err, constants.ErrDeploymentNotFound) { - return apperror.DeploymentNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrDeploymentIsDeployed) { - return apperror.DeploymentActive.Wrap(err) - } - if guardErr := mapArtifactGuardError(err); guardErr != nil { - return guardErr - } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to delete API %s deployment %s", apiId, deploymentId)) + return serviceError(err, fmt.Sprintf("failed to delete API %s deployment %s", apiId, deploymentId)) } w.WriteHeader(http.StatusNoContent) @@ -296,14 +219,7 @@ func (h *DeploymentHandler) GetDeployment(w http.ResponseWriter, r *http.Request deployment, err := h.deploymentService.GetDeploymentByHandle(apiId, deploymentId, orgId) if err != nil { - if errors.Is(err, constants.ErrAPINotFound) { - return apperror.RESTAPINotFound.Wrap(err) - } - if errors.Is(err, constants.ErrDeploymentNotFound) { - return apperror.DeploymentNotFound.Wrap(err) - } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to get API %s deployment %s", apiId, deploymentId)) + return serviceError(err, fmt.Sprintf("failed to get API %s deployment %s", apiId, deploymentId)) } httputil.WriteJSON(w, http.StatusOK, deployment) @@ -346,14 +262,7 @@ func (h *DeploymentHandler) GetDeployments(w http.ResponseWriter, r *http.Reques deployments, err := h.deploymentService.GetDeploymentsByHandle(apiId, gatewayId, status, orgId) if err != nil { - if errors.Is(err, constants.ErrAPINotFound) { - return apperror.RESTAPINotFound.Wrap(err) - } - if errors.Is(err, constants.ErrInvalidDeploymentStatus) { - return apperror.DeploymentInvalidStatus.Wrap(err) - } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to get deployments for API %s", apiId)) + return serviceError(err, fmt.Sprintf("failed to get deployments for API %s", apiId)) } paginateDeploymentList(deployments, limit, offset) diff --git a/platform-api/internal/handler/application.go b/platform-api/internal/handler/application.go index f7ead9886..36fb665fe 100644 --- a/platform-api/internal/handler/application.go +++ b/platform-api/internal/handler/application.go @@ -19,7 +19,6 @@ package handler import ( "encoding/json" - "errors" "fmt" "log/slog" "net/http" @@ -72,8 +71,7 @@ func (h *ApplicationHandler) CreateApplication(w http.ResponseWriter, r *http.Re } app, err := h.applicationService.CreateApplication(&req, orgID, createdBy) if err != nil { - return h.mapApplicationError(err). - WithLogMessage(fmt.Sprintf("failed to create application in project %s for org %s by user %s", req.ProjectId, orgID, createdBy)) + return serviceError(err, fmt.Sprintf("failed to create application in project %s for org %s by user %s", req.ProjectId, orgID, createdBy)) } setLocation(w, "applications", app.Id) @@ -95,8 +93,7 @@ func (h *ApplicationHandler) GetApplication(w http.ResponseWriter, r *http.Reque app, err := h.applicationService.GetApplicationByID(appID, orgID) if err != nil { - return h.mapApplicationError(err). - WithLogMessage(fmt.Sprintf("failed to get application %s in org %s", appID, orgID)) + return serviceError(err, fmt.Sprintf("failed to get application %s in org %s", appID, orgID)) } httputil.WriteJSON(w, http.StatusOK, app) @@ -119,8 +116,7 @@ func (h *ApplicationHandler) ListApplications(w http.ResponseWriter, r *http.Req apps, err := h.applicationService.GetApplicationsByOrganization(orgID, projectID, opts) if err != nil { - return h.mapApplicationError(err). - WithLogMessage(fmt.Sprintf("failed to list applications for project %s in org %s", projectID, orgID)) + return serviceError(err, fmt.Sprintf("failed to list applications for project %s in org %s", projectID, orgID)) } httputil.WriteJSON(w, http.StatusOK, apps) @@ -150,8 +146,7 @@ func (h *ApplicationHandler) UpdateApplication(w http.ResponseWriter, r *http.Re app, err := h.applicationService.UpdateApplication(appID, &req, orgID, userID) if err != nil { - return h.mapApplicationError(err). - WithLogMessage(fmt.Sprintf("failed to update application %s in org %s by user %s", appID, orgID, userID)) + return serviceError(err, fmt.Sprintf("failed to update application %s in org %s by user %s", appID, orgID, userID)) } httputil.WriteJSON(w, http.StatusOK, app) @@ -175,8 +170,7 @@ func (h *ApplicationHandler) DeleteApplication(w http.ResponseWriter, r *http.Re } if err := h.applicationService.DeleteApplication(appID, orgID, userID); err != nil { - return h.mapApplicationError(err). - WithLogMessage(fmt.Sprintf("failed to delete application %s in org %s by user %s", appID, orgID, userID)) + return serviceError(err, fmt.Sprintf("failed to delete application %s in org %s by user %s", appID, orgID, userID)) } httputil.WriteJSON(w, http.StatusNoContent, nil) @@ -199,8 +193,7 @@ func (h *ApplicationHandler) ListApplicationAssociations(w http.ResponseWriter, associations, err := h.applicationService.ListApplicationAssociations(appID, orgID, limit, offset) if err != nil { - return h.mapApplicationError(err). - WithLogMessage(fmt.Sprintf("failed to list associations for application %s in org %s", appID, orgID)) + return serviceError(err, fmt.Sprintf("failed to list associations for application %s in org %s", appID, orgID)) } httputil.WriteJSON(w, http.StatusOK, associations) @@ -229,8 +222,7 @@ func (h *ApplicationHandler) AddApplicationAssociations(w http.ResponseWriter, r associations, err := h.applicationService.AddApplicationAssociations(appID, &req, orgID) if err != nil { - return h.mapApplicationError(err). - WithLogMessage(fmt.Sprintf("failed to add associations for application %s in org %s", appID, orgID)) + return serviceError(err, fmt.Sprintf("failed to add associations for application %s in org %s", appID, orgID)) } httputil.WriteJSON(w, http.StatusOK, associations) @@ -254,8 +246,7 @@ func (h *ApplicationHandler) RemoveApplicationAssociation(w http.ResponseWriter, } if err := h.applicationService.RemoveApplicationAssociation(appID, associationID, orgID); err != nil { - return h.mapApplicationError(err). - WithLogMessage(fmt.Sprintf("failed to remove association %s for application %s in org %s", associationID, appID, orgID)) + return serviceError(err, fmt.Sprintf("failed to remove association %s for application %s in org %s", associationID, appID, orgID)) } httputil.WriteJSON(w, http.StatusNoContent, nil) @@ -278,8 +269,7 @@ func (h *ApplicationHandler) ListApplicationAPIKeys(w http.ResponseWriter, r *ht keys, err := h.applicationService.ListMappedAPIKeys(appID, orgID, limit, offset) if err != nil { - return h.mapApplicationError(err). - WithLogMessage(fmt.Sprintf("failed to list mapped API keys for application %s in org %s", appID, orgID)) + return serviceError(err, fmt.Sprintf("failed to list mapped API keys for application %s in org %s", appID, orgID)) } httputil.WriteJSON(w, http.StatusOK, keys) @@ -306,8 +296,7 @@ func (h *ApplicationHandler) ListApplicationAssociationAPIKeys(w http.ResponseWr keys, err := h.applicationService.ListMappedAPIKeysForAssociation(appID, associationID, orgID, limit, offset) if err != nil { - return h.mapApplicationError(err). - WithLogMessage(fmt.Sprintf("failed to list mapped API keys for association %s of application %s in org %s", associationID, appID, orgID)) + return serviceError(err, fmt.Sprintf("failed to list mapped API keys for association %s of application %s in org %s", associationID, appID, orgID)) } httputil.WriteJSON(w, http.StatusOK, keys) @@ -340,8 +329,7 @@ func (h *ApplicationHandler) AddApplicationAPIKeys(w http.ResponseWriter, r *htt keys, err := h.applicationService.AddMappedAPIKeys(appID, &req, orgID, userID) if err != nil { - return h.mapApplicationError(err). - WithLogMessage(fmt.Sprintf("failed to add mapped API keys for application %s in org %s by user %s", appID, orgID, userID)) + return serviceError(err, fmt.Sprintf("failed to add mapped API keys for application %s in org %s by user %s", appID, orgID, userID)) } httputil.WriteJSON(w, http.StatusOK, keys) @@ -373,8 +361,7 @@ func (h *ApplicationHandler) RemoveApplicationAPIKey(w http.ResponseWriter, r *h } if err := h.applicationService.RemoveMappedAPIKey(appID, keyID, entityID, orgID, userID); err != nil { - return h.mapApplicationError(err). - WithLogMessage(fmt.Sprintf("failed to remove mapped API key %s for application %s in org %s by user %s", keyID, appID, orgID, userID)) + return serviceError(err, fmt.Sprintf("failed to remove mapped API key %s for application %s in org %s by user %s", keyID, appID, orgID, userID)) } httputil.WriteJSON(w, http.StatusNoContent, nil) @@ -397,47 +384,3 @@ func (h *ApplicationHandler) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("GET "+base+"/{applicationId}/associations/{associationId}/api-keys", middleware.MapErrors(h.slogger, h.ListApplicationAssociationAPIKeys)) mux.HandleFunc("DELETE "+base+"/{applicationId}/associations/{associationId}", middleware.MapErrors(h.slogger, h.RemoveApplicationAssociation)) } - -// mapApplicationError maps service errors to *apperror.Error values for the -// centralized error mapper, preserving the exact status/code/message each -// error produced before the migration. -func (h *ApplicationHandler) mapApplicationError(err error) *apperror.Error { - switch { - case errors.Is(err, constants.ErrApplicationNotFound): - return apperror.ApplicationNotFound.Wrap(err) - case errors.Is(err, constants.ErrProjectNotFound): - return apperror.ProjectNotFound.Wrap(err) - case errors.Is(err, constants.ErrOrganizationNotFound): - return apperror.OrganizationNotFound.Wrap(err) - case errors.Is(err, constants.ErrApplicationExists): - return apperror.ApplicationExists.Wrap(err) - case errors.Is(err, constants.ErrHandleExists): - return apperror.ApplicationExists.Wrap(err) - case errors.Is(err, constants.ErrAPIKeyNotFound): - return apperror.ApplicationAPIKeyNotFound.Wrap(err) - case errors.Is(err, constants.ErrAPIKeyForbidden): - return apperror.ApplicationAPIKeyForbidden.Wrap(err) - case errors.Is(err, constants.ErrInvalidApplicationName): - return apperror.ValidationFailed.Wrap(err, "displayName is required") - case errors.Is(err, constants.ErrInvalidApplicationType): - return apperror.ValidationFailed.Wrap(err, "Application type is required") - case errors.Is(err, constants.ErrUnsupportedApplicationType): - return apperror.ValidationFailed.Wrap(err, "Invalid application type. Only 'genai' is supported") - case errors.Is(err, constants.ErrInvalidHandle): - return apperror.ValidationFailed.Wrap(err, "Invalid application handle format") - case errors.Is(err, constants.ErrInvalidApplicationID): - return apperror.ValidationFailed.Wrap(err, "Invalid application id") - case errors.Is(err, constants.ErrInvalidAPIKey): - return apperror.ValidationFailed.Wrap(err, "Invalid API key id") - case errors.Is(err, constants.ErrArtifactNotFound): - return apperror.ArtifactNotFound.Wrap(err) - case errors.Is(err, constants.ErrArtifactInvalidKind): - return apperror.ValidationFailed.Wrap(err, "Invalid association kind. Only LlmProvider and LlmProxy are supported") - case errors.Is(err, constants.ErrInvalidInput): - return apperror.ValidationFailed.Wrap(err, "Invalid application association input") - case errors.Is(err, constants.ErrHandleImmutable): - return apperror.ValidationFailed.Wrap(err, "The id is immutable and must match the application being updated") - default: - return apperror.Internal.Wrap(err) - } -} diff --git a/platform-api/internal/handler/artifact_guard_response.go b/platform-api/internal/handler/artifact_guard_response.go deleted file mode 100644 index cc5e86384..000000000 --- a/platform-api/internal/handler/artifact_guard_response.go +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. - * - * Licensed 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. - * - */ - -package handler - -import ( - "errors" - - "github.com/wso2/api-platform/platform-api/internal/apperror" - "github.com/wso2/api-platform/platform-api/internal/constants" -) - -// mapArtifactGuardError maps read-only / deletion-guard errors raised when a -// mutating operation targets a data-plane-originated (origin=DP) artifact to -// the corresponding *apperror.Error, for the MapErrors middleware to log and -// serialize. It returns nil when err is not a guard error, so callers can -// `if guardErr := mapArtifactGuardError(err); guardErr != nil { return guardErr }`. -// -// - ErrArtifactReadOnly -> 403 Forbidden (update/deploy of a DP artifact) -// - ErrArtifactRuntimeImmutable -> 403 Forbidden (edit that would change a DP artifact's runtime config) -// - ErrArtifactDeployed -> 409 Conflict (delete of a still-deployed DP artifact) -func mapArtifactGuardError(err error) error { - switch { - case errors.Is(err, constants.ErrArtifactReadOnly): - return apperror.ArtifactReadOnly.Wrap(err, "Artifact is read-only: it originated from a data-plane gateway") - case errors.Is(err, constants.ErrArtifactRuntimeImmutable): - return apperror.ArtifactRuntimeImmutable.Wrap(err, "Runtime configuration of this artifact cannot be changed") - case errors.Is(err, constants.ErrArtifactDeployed): - return apperror.ArtifactDeployed.Wrap(err, "Artifact is still deployed on a gateway and cannot be deleted") - default: - return nil - } -} diff --git a/platform-api/internal/handler/gateway.go b/platform-api/internal/handler/gateway.go index f2f007be1..1059c3bf4 100644 --- a/platform-api/internal/handler/gateway.go +++ b/platform-api/internal/handler/gateway.go @@ -119,7 +119,7 @@ func (h *GatewayHandler) CreateGateway(w http.ResponseWriter, r *http.Request) e if errors.As(err, &appErr) { return err } - return apperror.Internal.Wrap(err).WithLogMessage("failed to register gateway") + return serviceError(err, "failed to register gateway") } // Return 201 Created with response @@ -139,7 +139,7 @@ func (h *GatewayHandler) ListGateways(w http.ResponseWriter, r *http.Request) er gateways, err := h.gatewayService.ListGateways(&organizationID, opts) if err != nil { - return apperror.Internal.Wrap(err).WithLogMessage("failed to list gateways") + return serviceError(err, "failed to list gateways") } // Return 200 OK with constitution-compliant envelope structure @@ -169,7 +169,7 @@ func (h *GatewayHandler) GetGateway(w http.ResponseWriter, r *http.Request) erro if strings.Contains(err.Error(), "invalid UUID") { return apperror.ValidationFailed.Wrap(err, "Invalid gateway ID format") } - return apperror.Internal.Wrap(err).WithLogMessage("failed to retrieve gateway") + return serviceError(err, "failed to retrieve gateway") } // Return 200 OK with gateway details @@ -196,7 +196,7 @@ func (h *GatewayHandler) GetGatewayStatus(w http.ResponseWriter, r *http.Request if errors.As(err, &appErr) { return err } - return apperror.Internal.Wrap(err).WithLogMessage("failed to get gateway status") + return serviceError(err, "failed to get gateway status") } httputil.WriteJSON(w, http.StatusOK, status) @@ -234,10 +234,7 @@ func (h *GatewayHandler) UpdateGateway(w http.ResponseWriter, r *http.Request) e } response, err := h.gatewayService.UpdateGateway(gatewayId, orgId, updatedBy, &req) if err != nil { - if errors.Is(err, constants.ErrGatewayNotFound) { - return apperror.GatewayNotFound.Wrap(err) - } - return apperror.Internal.Wrap(err).WithLogMessage("failed to update gateway") + return serviceError(err, "failed to update gateway") } httputil.WriteJSON(w, http.StatusOK, response) @@ -263,19 +260,13 @@ func (h *GatewayHandler) DeleteGateway(w http.ResponseWriter, r *http.Request) e } if err := h.gatewayService.DeleteGateway(gatewayId, orgId, deletedBy); err != nil { // Check for specific error types - if errors.Is(err, constants.ErrGatewayNotFound) { - return apperror.GatewayNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrGatewayHasAssociatedAPIs) { - return apperror.GatewayHasActiveDeployments.Wrap(err) - } if strings.Contains(err.Error(), "invalid UUID") { return apperror.ValidationFailed.Wrap(err, "Invalid gateway ID format") } // Internal server error - return apperror.Internal.Wrap(err).WithLogMessage("failed to delete gateway") + return serviceError(err, "failed to delete gateway") } // Return 204 No Content on successful deletion @@ -303,7 +294,7 @@ func (h *GatewayHandler) ListTokens(w http.ResponseWriter, r *http.Request) erro if errors.As(err, &appErr) { return err } - return apperror.Internal.Wrap(err).WithLogMessage("failed to list tokens") + return serviceError(err, "failed to list tokens") } httputil.WriteJSON(w, http.StatusOK, tokens) @@ -333,7 +324,7 @@ func (h *GatewayHandler) RotateToken(w http.ResponseWriter, r *http.Request) err if errors.As(err, &appErr) { return err } - return apperror.Internal.Wrap(err).WithLogMessage("failed to rotate token") + return serviceError(err, "failed to rotate token") } // Return 201 Created with response @@ -372,7 +363,7 @@ func (h *GatewayHandler) RevokeToken(w http.ResponseWriter, r *http.Request) err if errors.As(err, &appErr) { return err } - return apperror.Internal.Wrap(err).WithLogMessage("failed to revoke token") + return serviceError(err, "failed to revoke token") } httputil.WriteJSON(w, http.StatusOK, map[string]any{"message": "Token revoked successfully"}) @@ -397,10 +388,7 @@ func (h *GatewayHandler) GetGatewayManifest(w http.ResponseWriter, r *http.Reque if strings.Contains(err.Error(), "invalid UUID") { return apperror.ValidationFailed.Wrap(err, "Invalid gateway ID format") } - if errors.Is(err, constants.ErrGatewayNotFound) { - return apperror.GatewayNotFound.Wrap(err) - } - return apperror.Internal.Wrap(err).WithLogMessage("failed to retrieve gateway manifest") + return serviceError(err, "failed to retrieve gateway manifest") } httputil.WriteJSON(w, http.StatusOK, manifestSyncResponse{ @@ -431,7 +419,7 @@ func (h *GatewayHandler) SyncCustomPolicy(w http.ResponseWriter, r *http.Request if errors.As(err, &appErr) { return err } - return apperror.Internal.Wrap(err).WithLogMessage("failed to sync custom policy") + return serviceError(err, "failed to sync custom policy") } httputil.WriteJSON(w, http.StatusOK, policy) @@ -457,7 +445,7 @@ func (h *GatewayHandler) GetCustomPolicy(w http.ResponseWriter, r *http.Request) if errors.As(err, &appErr) { return err } - return apperror.Internal.Wrap(err).WithLogMessage("failed to get custom policy") + return serviceError(err, "failed to get custom policy") } httputil.WriteJSON(w, http.StatusOK, policy) @@ -483,14 +471,7 @@ func (h *GatewayHandler) DeleteCustomPolicy(w http.ResponseWriter, r *http.Reque return err } // Repository-origin sentinels (delete-if-unused path) are still untyped. - if errors.Is(err, constants.ErrCustomPolicyNotFound) { - return apperror.CustomPolicyNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrCustomPolicyInUse) { - return apperror.PolicyInUse.Wrap(err) - } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to delete custom policy, orgID=%s, policyUUID=%s", orgId, policyUUID)) + return serviceError(err, fmt.Sprintf("failed to delete custom policy, orgID=%s, policyUUID=%s", orgId, policyUUID)) } w.WriteHeader(http.StatusNoContent) @@ -508,7 +489,7 @@ func (h *GatewayHandler) ListCustomPolicies(w http.ResponseWriter, r *http.Reque policies, total, err := h.gatewayService.ListCustomPolicies(orgId, limit, offset) if err != nil { - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to list custom policies, orgID=%s", orgId)) + return serviceError(err, fmt.Sprintf("failed to list custom policies, orgID=%s", orgId)) } httputil.WriteJSON(w, http.StatusOK, map[string]any{ diff --git a/platform-api/internal/handler/gateway_internal.go b/platform-api/internal/handler/gateway_internal.go index ae67b01c3..602e4430f 100644 --- a/platform-api/internal/handler/gateway_internal.go +++ b/platform-api/internal/handler/gateway_internal.go @@ -21,17 +21,18 @@ import ( "encoding/json" "errors" "fmt" - "github.com/wso2/api-platform/platform-api/internal/apperror" - "github.com/wso2/api-platform/platform-api/internal/constants" - "github.com/wso2/api-platform/platform-api/internal/dto" - "github.com/wso2/api-platform/platform-api/internal/model" - "github.com/wso2/api-platform/platform-api/internal/utils" "log/slog" "net/http" "strconv" "strings" "time" + "github.com/wso2/api-platform/platform-api/internal/apperror" + "github.com/wso2/api-platform/platform-api/internal/constants" + "github.com/wso2/api-platform/platform-api/internal/dto" + "github.com/wso2/api-platform/platform-api/internal/model" + "github.com/wso2/api-platform/platform-api/internal/utils" + "github.com/wso2/api-platform/platform-api/internal/service" "github.com/wso2/go-httpkit/httputil" @@ -77,7 +78,7 @@ func (h *GatewayInternalAPIHandler) SetHmacSecretService(svc hmacSecretDecrypter // authenticateGateway validates the API key and returns the authenticated gateway. func (h *GatewayInternalAPIHandler) authenticateGateway(apiKey string) (*model.Gateway, error) { if apiKey == "" { - return nil, constants.ErrMissingAPIKey + return nil, apperror.Unauthorized.New().WithLogMessage("api-key header absent") } return h.gatewayService.VerifyToken(apiKey) } @@ -92,19 +93,21 @@ func (h *GatewayInternalAPIHandler) authenticateRequest(w http.ResponseWriter, r gateway, err := h.authenticateGateway(apiKey) if err != nil { - if errors.Is(err, constants.ErrMissingAPIKey) { - h.slogger.Warn("Unauthorized access attempt - Missing API key", "clientIP", clientIP) - httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", - "API key is required. Provide 'api-key' header.")) - } else if errors.Is(err, constants.ErrInvalidAPIToken) { - h.slogger.Warn("Authentication failed - Invalid API key", "clientIP", clientIP) - httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", - "Invalid or expired API key")) - } else { - h.slogger.Error("Authentication failed", "clientIP", clientIP, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", - "Error while validating API key")) - } + // Unified authentication failure (error-handling.md): a missing key, an + // unknown key, and an expired key must be indistinguishable to the caller, + // or the endpoint becomes a token-probing oracle. The specific reason is + // carried by the error's LogMessage and recorded here only. + var appErr *apperror.Error + if errors.As(err, &appErr) && appErr.HTTPStatus == http.StatusUnauthorized { + h.slogger.Warn("Gateway authentication failed", + "clientIP", clientIP, "detail", appErr.LogMessage) + httputil.WriteJSON(w, http.StatusUnauthorized, + apperror.NewErrorResponseWithCode(apperror.CodeCommonUnauthorized, "Invalid or expired credentials.")) + return "", "", false + } + h.slogger.Error("Gateway authentication errored", "clientIP", clientIP, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, + apperror.NewErrorResponseWithCode(apperror.CodeCommonInternalError, "An unexpected error occurred.")) return "", "", false } return gateway.OrganizationID, gateway.ID, true @@ -126,12 +129,12 @@ func (h *GatewayInternalAPIHandler) GetAPI(w http.ResponseWriter, r *http.Reques api, err := h.gatewayInternalService.GetActiveDeploymentByGateway(apiID, orgID, gatewayID) if err != nil { - if errors.Is(err, constants.ErrDeploymentNotActive) { + if apperror.DeploymentNotActive.Is(err) { httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "No active deployment found for this API on this gateway")) return } - if errors.Is(err, constants.ErrAPINotFound) { + if apperror.RESTAPINotFound.Is(err) { httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "API not found")) return @@ -214,12 +217,12 @@ func (h *GatewayInternalAPIHandler) GetLLMProvider(w http.ResponseWriter, r *htt provider, err := h.gatewayInternalService.GetActiveLLMProviderDeploymentByGateway(providerID, orgID, gatewayID) if err != nil { - if errors.Is(err, constants.ErrDeploymentNotActive) { + if apperror.DeploymentNotActive.Is(err) { httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "No active deployment found for this LLM provider on this gateway")) return } - if errors.Is(err, constants.ErrLLMProviderNotFound) { + if apperror.LLMProviderNotFound.Is(err) { httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "LLM provider not found")) return @@ -264,12 +267,12 @@ func (h *GatewayInternalAPIHandler) GetLLMProxy(w http.ResponseWriter, r *http.R proxy, err := h.gatewayInternalService.GetActiveLLMProxyDeploymentByGateway(proxyID, orgID, gatewayID) if err != nil { - if errors.Is(err, constants.ErrDeploymentNotActive) { + if apperror.DeploymentNotActive.Is(err) { httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "No active deployment found for this LLM proxy on this gateway")) return } - if errors.Is(err, constants.ErrLLMProxyNotFound) { + if apperror.LLMProxyNotFound.Is(err) { httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "LLM proxy not found")) return @@ -321,7 +324,7 @@ func (h *GatewayInternalAPIHandler) GetGatewayDeployments(w http.ResponseWriter, deployments, err := h.gatewayInternalService.GetDeploymentsByGateway(orgID, gatewayID, since) if err != nil { - if errors.Is(err, constants.ErrGatewayNotFound) { + if apperror.GatewayNotFound.Is(err) { httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "Gateway not found")) return @@ -367,7 +370,7 @@ func (h *GatewayInternalAPIHandler) BatchFetchDeployments(w http.ResponseWriter, // Fetch deployment content contentMap, err := h.gatewayInternalService.GetDeploymentContentBatch(orgID, gatewayID, req.DeploymentIDs) if err != nil { - if errors.Is(err, constants.ErrGatewayNotFound) { + if apperror.GatewayNotFound.Is(err) { httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "Gateway not found")) return @@ -420,7 +423,7 @@ func (h *GatewayInternalAPIHandler) GetSubscriptions(w http.ResponseWriter, r *h } if err := h.gatewayInternalService.IsAPIDeployedOnGateway(apiID, gatewayID, orgID); err != nil { - if errors.Is(err, constants.ErrAPINotFound) { + if apperror.RESTAPINotFound.Is(err) { h.slogger.Error("API not found when listing subscriptions", "apiId", apiID, "organizationId", orgID, @@ -430,7 +433,7 @@ func (h *GatewayInternalAPIHandler) GetSubscriptions(w http.ResponseWriter, r *h "API not found")) return } - if errors.Is(err, constants.ErrDeploymentNotActive) { + if apperror.DeploymentNotActive.Is(err) { h.slogger.Error("Subscription list denied - API has no active deployment status on gateway", "apiId", apiID, "organizationId", orgID, @@ -451,7 +454,7 @@ func (h *GatewayInternalAPIHandler) GetSubscriptions(w http.ResponseWriter, r *h subs, err := h.gatewayInternalService.ListSubscriptionsForAPI(apiID, orgID) if err != nil { - if errors.Is(err, constants.ErrAPINotFound) { + if apperror.RESTAPINotFound.Is(err) { h.slogger.Error("API not found when listing subscriptions", "apiId", apiID, "organizationId", orgID, @@ -512,13 +515,13 @@ func (h *GatewayInternalAPIHandler) GetMCPProxy(w http.ResponseWriter, r *http.R if i := strings.LastIndex(clientIP, ":"); i != -1 { clientIP = clientIP[:i] } - if errors.Is(err, constants.ErrDeploymentNotActive) { + if apperror.DeploymentNotActive.Is(err) { h.slogger.Error("No active deployment found for MCP proxy", "clientIP", clientIP, "proxyID", proxyID, "orgID", orgID, "gatewayID", gatewayID, "error", err) httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "No active deployment found for this MCP proxy on this gateway")) return } - if errors.Is(err, constants.ErrMCPProxyNotFound) { + if apperror.MCPProxyNotFound.Is(err) { h.slogger.Error("MCP proxy not found", "clientIP", clientIP, "proxyID", proxyID, "orgID", orgID, "gatewayID", gatewayID, "error", err) httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "MCP proxy not found")) @@ -569,13 +572,13 @@ func (h *GatewayInternalAPIHandler) GetWebSubAPI(w http.ResponseWriter, r *http. if i := strings.LastIndex(clientIP, ":"); i != -1 { clientIP = clientIP[:i] } - if errors.Is(err, constants.ErrDeploymentNotActive) { + if apperror.DeploymentNotActive.Is(err) { h.slogger.Error("No active deployment found for WebSub API", "clientIP", clientIP, "apiID", apiID, "orgID", orgID, "gatewayID", gatewayID, "error", err) httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "No active deployment found for this WebSub API on this gateway")) return } - if errors.Is(err, constants.ErrWebSubAPINotFound) { + if apperror.WebSubAPINotFound.Is(err) { h.slogger.Error("WebSub API not found", "clientIP", clientIP, "apiID", apiID, "orgID", orgID, "gatewayID", gatewayID, "error", err) httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "WebSub API not found")) @@ -626,13 +629,13 @@ func (h *GatewayInternalAPIHandler) GetWebBrokerAPI(w http.ResponseWriter, r *ht if i := strings.LastIndex(clientIP, ":"); i != -1 { clientIP = clientIP[:i] } - if errors.Is(err, constants.ErrDeploymentNotActive) { + if apperror.DeploymentNotActive.Is(err) { h.slogger.Error("No active deployment found for WebBroker API", "clientIP", clientIP, "apiID", apiID, "orgID", orgID, "gatewayID", gatewayID, "error", err) httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "No active deployment found for this WebBroker API on this gateway")) return } - if errors.Is(err, constants.ErrWebBrokerAPINotFound) { + if apperror.WebBrokerAPINotFound.Is(err) { h.slogger.Error("WebBroker API not found", "clientIP", clientIP, "apiID", apiID, "orgID", orgID, "gatewayID", gatewayID, "error", err) httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "WebBroker API not found")) @@ -682,23 +685,19 @@ func (h *GatewayInternalAPIHandler) ReceiveGatewayManifest(w http.ResponseWriter } if err := h.gatewayService.ReceiveGatewayManifest(orgID, gatewayID, body.Version, body.FunctionalityType, body.Policies); err != nil { - if errors.Is(err, constants.ErrGatewayVersionMismatch) { - h.slogger.Warn("Gateway manifest rejected: version mismatch", "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusConflict, apperror.NewErrorResponse(409, "Conflict", err.Error())) - return - } - if errors.Is(err, constants.ErrGatewayFunctionalityTypeMismatch) { - h.slogger.Warn("Gateway manifest rejected: functionality type mismatch", "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusConflict, apperror.NewErrorResponse(409, "Conflict", err.Error())) - return - } - if errors.Is(err, constants.ErrGatewayNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", err.Error())) + // A catalog error already carries the status, code, and a client-sterile + // message (version/type mismatches attach the registered vs reported values + // as structured details). Previously this echoed err.Error() into the body. + var appErr *apperror.Error + if errors.As(err, &appErr) { + h.slogger.Warn("Gateway manifest rejected", + "gatewayID", gatewayID, "code", appErr.Code, "detail", appErr.LogMessage) + apperror.WriteHTTP(w, appErr, "") return } h.slogger.Error("Failed to store gateway manifest", "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", - "Failed to store gateway manifest")) + httputil.WriteJSON(w, http.StatusInternalServerError, + apperror.NewErrorResponseWithCode(apperror.CodeCommonInternalError, "An unexpected error occurred.")) return } @@ -962,7 +961,7 @@ func (h *GatewayInternalAPIHandler) GetGatewaySecretValue(w http.ResponseWriter, plaintext, err := h.secretService.Decrypt(orgID, handle) if err != nil { - if errors.Is(err, constants.ErrSecretNotFound) { + if apperror.SecretNotFound.Is(err) { httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "Secret not found")) return } diff --git a/platform-api/internal/handler/llm.go b/platform-api/internal/handler/llm.go index 071c78f05..94209de4c 100644 --- a/platform-api/internal/handler/llm.go +++ b/platform-api/internal/handler/llm.go @@ -126,17 +126,7 @@ func (h *LLMHandler) CreateLLMProviderTemplate(w http.ResponseWriter, r *http.Re created, err := h.templateService.Create(orgID, createdBy, &req) if err != nil { - switch { - case errors.Is(err, constants.ErrLLMProviderTemplateExists): - return apperror.LLMProviderTemplateExists.Wrap(err) - case errors.Is(err, constants.ErrLLMProviderTemplateManagedByReserved): - return apperror.LLMProviderTemplateManagedByReserved.Wrap(err) - case errors.Is(err, constants.ErrInvalidInput): - return apperror.ValidationFailed.Wrap(err, "Invalid input") - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to create LLM provider template in org %s", orgID)) - } + return serviceError(err, fmt.Sprintf("failed to create LLM provider template in org %s", orgID)) } setLocation(w, "llm-provider-templates", strOrEmpty(created.Id)) @@ -180,19 +170,7 @@ func (h *LLMHandler) CopyLLMProviderTemplateVersion(w http.ResponseWriter, r *ht created, err := h.templateService.CopyVersion(orgID, fromTemplateID, toTemplateID, toVersion, createdBy, overrides) if err != nil { - switch { - case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - return apperror.LLMProviderTemplateVersionNotFound.Wrap(err) - case errors.Is(err, constants.ErrLLMProviderTemplateVersionExists): - return apperror.LLMProviderTemplateVersionExists.Wrap(err) - case errors.Is(err, constants.ErrLLMProviderTemplateManagedByReserved): - return apperror.LLMProviderTemplateManagedByReserved.Wrap(err) - case errors.Is(err, constants.ErrInvalidInput): - return apperror.ValidationFailed.Wrap(err, "Invalid input. toVersion must match the v. pattern starting from v1.0 (e.g. v1.0), and toTemplateId must match the family") - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to copy LLM provider template version from %s to version %s in org %s", fromTemplateID, toVersion, orgID)) - } + return serviceError(err, fmt.Sprintf("failed to copy LLM provider template version from %s to version %s in org %s", fromTemplateID, toVersion, orgID)) } setLocation(w, "llm-provider-templates", strOrEmpty(created.Id)) @@ -219,30 +197,14 @@ func (h *LLMHandler) ListLLMProviderTemplates(w http.ResponseWriter, r *http.Req if version != "" { resp, err := h.templateService.GetVersion(orgID, groupID, version) if err != nil { - switch { - case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - return apperror.LLMProviderTemplateVersionNotFound.Wrap(err) - case errors.Is(err, constants.ErrInvalidInput): - return apperror.ValidationFailed.Wrap(err, "Invalid version. Version must match the v. pattern (e.g. v1.0)") - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to get LLM provider template version %s of group %s in org %s", version, groupID, orgID)) - } + return serviceError(err, fmt.Sprintf("failed to get LLM provider template version %s of group %s in org %s", version, groupID, orgID)) } httputil.WriteJSON(w, http.StatusOK, resp) return nil } resp, err := h.templateService.ListVersions(orgID, groupID, limit, offset) if err != nil { - switch { - case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - return apperror.LLMProviderTemplateNotFound.Wrap(err) - case errors.Is(err, constants.ErrInvalidInput): - return apperror.ValidationFailed.Wrap(err, "Invalid groupId") - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to list LLM provider template versions of group %s in org %s", groupID, orgID)) - } + return serviceError(err, fmt.Sprintf("failed to list LLM provider template versions of group %s in org %s", groupID, orgID)) } httputil.WriteJSON(w, http.StatusOK, resp) return nil @@ -252,8 +214,7 @@ func (h *LLMHandler) ListLLMProviderTemplates(w http.ResponseWriter, r *http.Req resp, err := h.templateService.List(orgID, limit, offset, latestOnly) if err != nil { - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to list LLM provider templates in org %s", orgID)) + return serviceError(err, fmt.Sprintf("failed to list LLM provider templates in org %s", orgID)) } httputil.WriteJSON(w, http.StatusOK, resp) return nil @@ -269,15 +230,7 @@ func (h *LLMHandler) GetLLMProviderTemplate(w http.ResponseWriter, r *http.Reque resp, err := h.templateService.Get(orgID, id) if err != nil { - switch { - case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - return apperror.LLMProviderTemplateNotFound.Wrap(err) - case errors.Is(err, constants.ErrInvalidInput): - return apperror.ValidationFailed.Wrap(err, "Invalid template id") - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to get LLM provider template %s in org %s", id, orgID)) - } + return serviceError(err, fmt.Sprintf("failed to get LLM provider template %s in org %s", id, orgID)) } httputil.WriteJSON(w, http.StatusOK, resp) return nil @@ -300,19 +253,7 @@ func (h *LLMHandler) SetLLMProviderTemplateVersionEnabled(w http.ResponseWriter, resp, err := h.templateService.SetEnabledByHandle(orgID, id, *body.Enabled) if err != nil { - switch { - case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - return apperror.LLMProviderTemplateVersionNotFound.Wrap(err) - case errors.Is(err, constants.ErrInvalidInput): - return apperror.ValidationFailed.Wrap(err, "Invalid template id") - case errors.Is(err, constants.ErrLLMProviderTemplateInUse): - return apperror.LLMProviderTemplateInUse.Wrap(err) - case errors.Is(err, constants.ErrLLMProviderTemplateNotToggleable): - return apperror.LLMProviderTemplateNotToggleable.Wrap(err) - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to set LLM provider template version enabled for template %s in org %s", id, orgID)) - } + return serviceError(err, fmt.Sprintf("failed to set LLM provider template version enabled for template %s in org %s", id, orgID)) } httputil.WriteJSON(w, http.StatusOK, resp) return nil @@ -341,24 +282,7 @@ func (h *LLMHandler) UpdateLLMProviderTemplate(w http.ResponseWriter, r *http.Re } resp, err := h.templateService.Update(orgID, id, updatedBy, &req) if err != nil { - if guardErr := mapArtifactGuardError(err); guardErr != nil { - return guardErr - } - switch { - case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - return apperror.LLMProviderTemplateNotFound.Wrap(err) - case errors.Is(err, constants.ErrLLMProviderTemplateReadOnly): - return apperror.LLMProviderTemplateReadOnly.Wrap(err) - case errors.Is(err, constants.ErrLLMProviderTemplateManagedByReserved): - return apperror.LLMProviderTemplateManagedByReserved.Wrap(err) - case errors.Is(err, constants.ErrHandleImmutable): - return apperror.ValidationFailed.Wrap(err, "The id is immutable and cannot be changed") - case errors.Is(err, constants.ErrInvalidInput): - return apperror.ValidationFailed.Wrap(err, "Invalid input") - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to update LLM provider template %s in org %s", id, orgID)) - } + return serviceError(err, fmt.Sprintf("failed to update LLM provider template %s in org %s", id, orgID)) } httputil.WriteJSON(w, http.StatusOK, resp) return nil @@ -374,19 +298,7 @@ func (h *LLMHandler) DeleteLLMProviderTemplateVersion(w http.ResponseWriter, r * id := r.PathValue("llmProviderTemplateId") if err := h.templateService.DeleteByHandle(orgID, id); err != nil { - switch { - case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - return apperror.LLMProviderTemplateVersionNotFound.Wrap(err) - case errors.Is(err, constants.ErrLLMProviderTemplateInUse): - return apperror.LLMProviderTemplateInUse.Wrap(err) - case errors.Is(err, constants.ErrLLMProviderTemplateReadOnly): - return apperror.LLMProviderTemplateReadOnly.Wrap(err) - case errors.Is(err, constants.ErrInvalidInput): - return apperror.ValidationFailed.Wrap(err, "Invalid template id") - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to delete LLM provider template version %s in org %s", id, orgID)) - } + return serviceError(err, fmt.Sprintf("failed to delete LLM provider template version %s in org %s", id, orgID)) } w.WriteHeader(http.StatusNoContent) @@ -413,23 +325,7 @@ func (h *LLMHandler) CreateLLMProvider(w http.ResponseWriter, r *http.Request) e created, err := h.providerService.Create(orgID, createdBy, &req) if err != nil { - switch { - case errors.Is(err, constants.ErrLLMProviderLimitReached): - return apperror.LLMProviderLimitReached.Wrap(err) - case errors.Is(err, constants.ErrLLMProviderExists): - return apperror.LLMProviderExists.Wrap(err) - case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - return apperror.LLMProviderTemplateRefNotFound.Wrap(err) - case errors.Is(err, constants.ErrSecretRefMissing): - return apperror.ValidationFailed.Wrap(err, "One or more referenced secrets do not exist") - case errors.Is(err, constants.ErrInvalidPolicyVersion): - return apperror.ValidationFailed.Wrap(err, "Invalid policy version format") - case errors.Is(err, constants.ErrInvalidInput): - return apperror.ValidationFailed.Wrap(err, "Invalid input") - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to create LLM provider in org %s", orgID)) - } + return serviceError(err, fmt.Sprintf("failed to create LLM provider in org %s", orgID)) } setLocation(w, "llm-providers", strOrEmpty(created.Id)) httputil.WriteJSON(w, http.StatusCreated, created) @@ -447,8 +343,7 @@ func (h *LLMHandler) ListLLMProviders(w http.ResponseWriter, r *http.Request) er resp, err := h.providerService.List(orgID, limit, offset) if err != nil { - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to list LLM providers in org %s", orgID)) + return serviceError(err, fmt.Sprintf("failed to list LLM providers in org %s", orgID)) } httputil.WriteJSON(w, http.StatusOK, resp) return nil @@ -464,15 +359,7 @@ func (h *LLMHandler) GetLLMProvider(w http.ResponseWriter, r *http.Request) erro resp, err := h.providerService.Get(orgID, id) if err != nil { - switch { - case errors.Is(err, constants.ErrLLMProviderNotFound): - return apperror.LLMProviderNotFound.Wrap(err) - case errors.Is(err, constants.ErrInvalidInput): - return apperror.ValidationFailed.Wrap(err, "Invalid provider id") - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to get LLM provider %s in org %s", id, orgID)) - } + return serviceError(err, fmt.Sprintf("failed to get LLM provider %s in org %s", id, orgID)) } httputil.WriteJSON(w, http.StatusOK, resp) return nil @@ -501,26 +388,7 @@ func (h *LLMHandler) UpdateLLMProvider(w http.ResponseWriter, r *http.Request) e } resp, err := h.providerService.Update(orgID, id, updatedBy, &req) if err != nil { - if guardErr := mapArtifactGuardError(err); guardErr != nil { - return guardErr - } - switch { - case errors.Is(err, constants.ErrLLMProviderNotFound): - return apperror.LLMProviderNotFound.Wrap(err) - case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - return apperror.LLMProviderTemplateRefNotFound.Wrap(err) - case errors.Is(err, constants.ErrSecretRefMissing): - return apperror.ValidationFailed.Wrap(err, "One or more referenced secrets do not exist") - case errors.Is(err, constants.ErrHandleImmutable): - return apperror.ValidationFailed.Wrap(err, "The id is immutable and cannot be changed") - case errors.Is(err, constants.ErrInvalidPolicyVersion): - return apperror.ValidationFailed.Wrap(err, "Invalid policy version format") - case errors.Is(err, constants.ErrInvalidInput): - return apperror.ValidationFailed.Wrap(err, "Invalid input") - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to update LLM provider %s in org %s", id, orgID)) - } + return serviceError(err, fmt.Sprintf("failed to update LLM provider %s in org %s", id, orgID)) } httputil.WriteJSON(w, http.StatusOK, resp) return nil @@ -539,18 +407,7 @@ func (h *LLMHandler) DeleteLLMProvider(w http.ResponseWriter, r *http.Request) e } if err := h.providerService.Delete(orgID, id, deletedBy); err != nil { - if guardErr := mapArtifactGuardError(err); guardErr != nil { - return guardErr - } - switch { - case errors.Is(err, constants.ErrLLMProviderNotFound): - return apperror.LLMProviderNotFound.Wrap(err) - case errors.Is(err, constants.ErrInvalidInput): - return apperror.ValidationFailed.Wrap(err, "Invalid provider id") - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to delete LLM provider %s in org %s", id, orgID)) - } + return serviceError(err, fmt.Sprintf("failed to delete LLM provider %s in org %s", id, orgID)) } w.WriteHeader(http.StatusNoContent) return nil @@ -579,23 +436,7 @@ func (h *LLMHandler) CreateLLMProxy(w http.ResponseWriter, r *http.Request) erro created, err := h.proxyService.Create(orgID, createdBy, &req) if err != nil { - switch { - case errors.Is(err, constants.ErrLLMProxyLimitReached): - return apperror.LLMProxyLimitReached.Wrap(err) - case errors.Is(err, constants.ErrLLMProxyExists): - return apperror.LLMProxyExists.Wrap(err) - case errors.Is(err, constants.ErrLLMProviderNotFound): - return apperror.LLMProviderRefNotFound.Wrap(err) - case errors.Is(err, constants.ErrProjectNotFound): - return apperror.ProjectNotFound.Wrap(err) - case errors.Is(err, constants.ErrInvalidPolicyVersion): - return apperror.ValidationFailed.Wrap(err, "Invalid policy version format") - case errors.Is(err, constants.ErrInvalidInput): - return apperror.ValidationFailed.Wrap(err, "Invalid input") - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to create LLM proxy in org %s", orgID)) - } + return serviceError(err, fmt.Sprintf("failed to create LLM proxy in org %s", orgID)) } setLocation(w, "llm-proxies", strOrEmpty(created.Id)) httputil.WriteJSON(w, http.StatusCreated, created) @@ -617,11 +458,7 @@ func (h *LLMHandler) ListLLMProxies(w http.ResponseWriter, r *http.Request) erro resp, err := h.proxyService.List(orgID, &projectID, limit, offset) if err != nil { - if errors.Is(err, constants.ErrProjectNotFound) { - return apperror.ProjectNotFound.Wrap(err) - } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to list LLM proxies in org %s", orgID)) + return serviceError(err, fmt.Sprintf("failed to list LLM proxies in org %s", orgID)) } httputil.WriteJSON(w, http.StatusOK, resp) return nil @@ -639,15 +476,7 @@ func (h *LLMHandler) ListLLMProxiesByProvider(w http.ResponseWriter, r *http.Req resp, err := h.proxyService.ListByProvider(orgID, providerID, limit, offset) if err != nil { - switch { - case errors.Is(err, constants.ErrLLMProviderNotFound): - return apperror.LLMProviderNotFound.Wrap(err) - case errors.Is(err, constants.ErrInvalidInput): - return apperror.ValidationFailed.Wrap(err, "Invalid provider id") - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to list LLM proxies by provider %s in org %s", providerID, orgID)) - } + return serviceError(err, fmt.Sprintf("failed to list LLM proxies by provider %s in org %s", providerID, orgID)) } httputil.WriteJSON(w, http.StatusOK, resp) return nil @@ -663,15 +492,7 @@ func (h *LLMHandler) GetLLMProxy(w http.ResponseWriter, r *http.Request) error { resp, err := h.proxyService.Get(orgID, id) if err != nil { - switch { - case errors.Is(err, constants.ErrLLMProxyNotFound): - return apperror.LLMProxyNotFound.Wrap(err) - case errors.Is(err, constants.ErrInvalidInput): - return apperror.ValidationFailed.Wrap(err, "Invalid proxy id") - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to get LLM proxy %s in org %s", id, orgID)) - } + return serviceError(err, fmt.Sprintf("failed to get LLM proxy %s in org %s", id, orgID)) } httputil.WriteJSON(w, http.StatusOK, resp) return nil @@ -700,24 +521,7 @@ func (h *LLMHandler) UpdateLLMProxy(w http.ResponseWriter, r *http.Request) erro } resp, err := h.proxyService.Update(orgID, id, updatedBy, &req) if err != nil { - if guardErr := mapArtifactGuardError(err); guardErr != nil { - return guardErr - } - switch { - case errors.Is(err, constants.ErrLLMProxyNotFound): - return apperror.LLMProxyNotFound.Wrap(err) - case errors.Is(err, constants.ErrLLMProviderNotFound): - return apperror.LLMProviderRefNotFound.Wrap(err) - case errors.Is(err, constants.ErrHandleImmutable): - return apperror.ValidationFailed.Wrap(err, "The id is immutable and cannot be changed") - case errors.Is(err, constants.ErrInvalidPolicyVersion): - return apperror.ValidationFailed.Wrap(err, "Invalid policy version format") - case errors.Is(err, constants.ErrInvalidInput): - return apperror.ValidationFailed.Wrap(err, "Invalid input") - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to update LLM proxy %s in org %s", id, orgID)) - } + return serviceError(err, fmt.Sprintf("failed to update LLM proxy %s in org %s", id, orgID)) } httputil.WriteJSON(w, http.StatusOK, resp) return nil @@ -736,18 +540,7 @@ func (h *LLMHandler) DeleteLLMProxy(w http.ResponseWriter, r *http.Request) erro } if err := h.proxyService.Delete(orgID, id, deletedBy); err != nil { - if guardErr := mapArtifactGuardError(err); guardErr != nil { - return guardErr - } - switch { - case errors.Is(err, constants.ErrLLMProxyNotFound): - return apperror.LLMProxyNotFound.Wrap(err) - case errors.Is(err, constants.ErrInvalidInput): - return apperror.ValidationFailed.Wrap(err, "Invalid proxy id") - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to delete LLM proxy %s in org %s", id, orgID)) - } + return serviceError(err, fmt.Sprintf("failed to delete LLM proxy %s in org %s", id, orgID)) } w.WriteHeader(http.StatusNoContent) return nil diff --git a/platform-api/internal/handler/llm_apikey.go b/platform-api/internal/handler/llm_apikey.go index a9dcfb1bc..8d600fe85 100644 --- a/platform-api/internal/handler/llm_apikey.go +++ b/platform-api/internal/handler/llm_apikey.go @@ -76,8 +76,7 @@ func (h *LLMProviderAPIKeyHandler) ListAPIKeys(w http.ResponseWriter, r *http.Re if errors.As(err, &appErr) { return err } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to list LLM provider API keys for provider %s in org %s", providerID, orgID)) + return serviceError(err, fmt.Sprintf("failed to list LLM provider API keys for provider %s in org %s", providerID, orgID)) } httputil.WriteJSON(w, http.StatusOK, response) @@ -108,17 +107,7 @@ func (h *LLMProviderAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.R } if err := h.apiKeyService.DeleteLLMProviderAPIKey(r.Context(), providerID, orgID, callerUserID, keyName); err != nil { - if errors.Is(err, constants.ErrAPINotFound) { - return apperror.ArtifactNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrAPIKeyNotFound) { - return apperror.LLMProviderAPIKeyNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrAPIKeyForbidden) { - return apperror.LLMProviderAPIKeyForbidden.Wrap(err) - } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to delete LLM provider API key %s for provider %s in org %s", keyName, providerID, orgID)) + return serviceError(err, fmt.Sprintf("failed to delete LLM provider API key %s for provider %s in org %s", keyName, providerID, orgID)) } h.slogger.Info("Successfully deleted LLM provider API key", "providerId", providerID, "keyName", keyName, "organizationId", orgID) @@ -163,8 +152,7 @@ func (h *LLMProviderAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.R return err } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to create LLM provider API key for provider %s in org %s", providerID, orgID)) + return serviceError(err, fmt.Sprintf("failed to create LLM provider API key for provider %s in org %s", providerID, orgID)) } h.slogger.Info("Successfully created LLM provider API key", "providerId", providerID, "organizationId", orgID, "keyId", response.Id) diff --git a/platform-api/internal/handler/llm_deployment.go b/platform-api/internal/handler/llm_deployment.go index acd143962..8c8d60792 100644 --- a/platform-api/internal/handler/llm_deployment.go +++ b/platform-api/internal/handler/llm_deployment.go @@ -19,7 +19,6 @@ package handler import ( "encoding/json" - "errors" "fmt" "log/slog" "net/http" @@ -87,30 +86,7 @@ func (h *LLMProviderDeploymentHandler) DeployLLMProvider(w http.ResponseWriter, deployment, err := h.deploymentService.DeployLLMProvider(providerId, &req, orgId) if err != nil { - if guardErr := mapArtifactGuardError(err); guardErr != nil { - return guardErr - } - switch { - case errors.Is(err, constants.ErrLLMProviderNotFound): - return apperror.LLMProviderNotFound.Wrap(err) - case errors.Is(err, constants.ErrGatewayNotFound): - return apperror.GatewayNotFound.Wrap(err) - case errors.Is(err, constants.ErrBaseDeploymentNotFound): - return apperror.DeploymentBaseNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentNameRequired): - return apperror.LLMProviderDeploymentValidationFailed.Wrap(err, "Deployment name is required") - case errors.Is(err, constants.ErrDeploymentBaseRequired): - return apperror.LLMProviderDeploymentValidationFailed.Wrap(err, "Base is required (use 'current' or a deploymentId)") - case errors.Is(err, constants.ErrDeploymentGatewayIDRequired): - return apperror.LLMProviderDeploymentValidationFailed.Wrap(err, "Gateway ID is required") - case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - return apperror.LLMProviderDeploymentValidationFailed.Wrap(err, "Referenced template not found") - case errors.Is(err, constants.ErrInvalidInput): - return apperror.LLMProviderDeploymentValidationFailed.Wrap(err, "Invalid input") - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to deploy LLM provider %s", providerId)) - } + return serviceError(err, fmt.Sprintf("failed to deploy LLM provider %s", providerId)) } setLocation(w, "llm-providers", providerId, "deployments", deployment.DeploymentId.String()) @@ -136,24 +112,7 @@ func (h *LLMProviderDeploymentHandler) UndeployLLMProviderDeployment(w http.Resp deployment, err := h.deploymentService.UndeployLLMProviderDeployment(providerId, deploymentId, gatewayId, orgId) if err != nil { // DP-originated artifacts are read-only: undeployment cannot be initiated from the CP. - if guardErr := mapArtifactGuardError(err); guardErr != nil { - return guardErr - } - switch { - case errors.Is(err, constants.ErrLLMProviderNotFound): - return apperror.LLMProviderNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentNotFound): - return apperror.DeploymentNotFound.Wrap(err) - case errors.Is(err, constants.ErrGatewayNotFound): - return apperror.GatewayNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentNotActive): - return apperror.DeploymentNotActive.Wrap(err, "LLM provider") - case errors.Is(err, constants.ErrGatewayIDMismatch): - return apperror.DeploymentGatewayMismatch.Wrap(err) - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to undeploy LLM provider %s deployment %s on gateway %q", providerId, deploymentId, gatewayId)) - } + return serviceError(err, fmt.Sprintf("failed to undeploy LLM provider %s deployment %s on gateway %q", providerId, deploymentId, gatewayId)) } httputil.WriteJSON(w, http.StatusOK, deployment) @@ -178,24 +137,7 @@ func (h *LLMProviderDeploymentHandler) RestoreLLMProviderDeployment(w http.Respo deployment, err := h.deploymentService.RestoreLLMProviderDeployment(providerId, deploymentId, gatewayId, orgId) if err != nil { // DP-originated artifacts are read-only: restore cannot be initiated from the CP. - if guardErr := mapArtifactGuardError(err); guardErr != nil { - return guardErr - } - switch { - case errors.Is(err, constants.ErrLLMProviderNotFound): - return apperror.LLMProviderNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentNotFound): - return apperror.DeploymentNotFound.Wrap(err) - case errors.Is(err, constants.ErrGatewayNotFound): - return apperror.GatewayNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentAlreadyDeployed): - return apperror.DeploymentRestoreConflict.Wrap(err) - case errors.Is(err, constants.ErrGatewayIDMismatch): - return apperror.DeploymentGatewayMismatch.Wrap(err) - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to restore LLM provider %s deployment %s on gateway %q", providerId, deploymentId, gatewayId)) - } + return serviceError(err, fmt.Sprintf("failed to restore LLM provider %s deployment %s on gateway %q", providerId, deploymentId, gatewayId)) } httputil.WriteJSON(w, http.StatusOK, deployment) @@ -222,17 +164,7 @@ func (h *LLMProviderDeploymentHandler) DeleteLLMProviderDeployment(w http.Respon err := h.deploymentService.DeleteLLMProviderDeployment(providerId, deploymentId, orgId) if err != nil { - switch { - case errors.Is(err, constants.ErrLLMProviderNotFound): - return apperror.LLMProviderNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentNotFound): - return apperror.DeploymentNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentIsDeployed): - return apperror.DeploymentActive.Wrap(err) - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to delete LLM provider %s deployment %s", providerId, deploymentId)) - } + return serviceError(err, fmt.Sprintf("failed to delete LLM provider %s deployment %s", providerId, deploymentId)) } w.WriteHeader(http.StatusNoContent) @@ -259,15 +191,7 @@ func (h *LLMProviderDeploymentHandler) GetLLMProviderDeployment(w http.ResponseW deployment, err := h.deploymentService.GetLLMProviderDeployment(providerId, deploymentId, orgId) if err != nil { - switch { - case errors.Is(err, constants.ErrLLMProviderNotFound): - return apperror.LLMProviderNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentNotFound): - return apperror.DeploymentNotFound.Wrap(err) - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to get LLM provider %s deployment %s", providerId, deploymentId)) - } + return serviceError(err, fmt.Sprintf("failed to get LLM provider %s deployment %s", providerId, deploymentId)) } httputil.WriteJSON(w, http.StatusOK, deployment) @@ -300,15 +224,7 @@ func (h *LLMProviderDeploymentHandler) GetLLMProviderDeployments(w http.Response deployments, err := h.deploymentService.GetLLMProviderDeployments(providerId, orgId, gatewayId, status) if err != nil { - switch { - case errors.Is(err, constants.ErrLLMProviderNotFound): - return apperror.LLMProviderNotFound.Wrap(err) - case errors.Is(err, constants.ErrInvalidDeploymentStatus): - return apperror.DeploymentInvalidStatus.Wrap(err) - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to get LLM provider %s deployments", providerId)) - } + return serviceError(err, fmt.Sprintf("failed to get LLM provider %s deployments", providerId)) } paginateDeploymentList(deployments, limit, offset) @@ -358,28 +274,7 @@ func (h *LLMProxyDeploymentHandler) DeployLLMProxy(w http.ResponseWriter, r *htt deployment, err := h.deploymentService.DeployLLMProxy(proxyId, &req, orgId) if err != nil { - if guardErr := mapArtifactGuardError(err); guardErr != nil { - return guardErr - } - switch { - case errors.Is(err, constants.ErrLLMProxyNotFound): - return apperror.LLMProxyNotFound.Wrap(err) - case errors.Is(err, constants.ErrGatewayNotFound): - return apperror.GatewayNotFound.Wrap(err) - case errors.Is(err, constants.ErrBaseDeploymentNotFound): - return apperror.DeploymentBaseNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentNameRequired): - return apperror.LLMProxyDeploymentValidationFailed.Wrap(err, "Deployment name is required") - case errors.Is(err, constants.ErrDeploymentBaseRequired): - return apperror.LLMProxyDeploymentValidationFailed.Wrap(err, "Base is required (use 'current' or a deploymentId)") - case errors.Is(err, constants.ErrDeploymentGatewayIDRequired): - return apperror.LLMProxyDeploymentValidationFailed.Wrap(err, "Gateway ID is required") - case errors.Is(err, constants.ErrInvalidInput): - return apperror.LLMProxyDeploymentValidationFailed.Wrap(err, "Invalid input") - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to deploy LLM proxy %s", proxyId)) - } + return serviceError(err, fmt.Sprintf("failed to deploy LLM proxy %s", proxyId)) } setLocation(w, "llm-proxies", proxyId, "deployments", deployment.DeploymentId.String()) @@ -405,24 +300,7 @@ func (h *LLMProxyDeploymentHandler) UndeployLLMProxyDeployment(w http.ResponseWr deployment, err := h.deploymentService.UndeployLLMProxyDeployment(proxyId, deploymentId, gatewayId, orgId) if err != nil { // DP-originated artifacts are read-only: undeployment cannot be initiated from the CP. - if guardErr := mapArtifactGuardError(err); guardErr != nil { - return guardErr - } - switch { - case errors.Is(err, constants.ErrLLMProxyNotFound): - return apperror.LLMProxyNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentNotFound): - return apperror.DeploymentNotFound.Wrap(err) - case errors.Is(err, constants.ErrGatewayNotFound): - return apperror.GatewayNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentNotActive): - return apperror.DeploymentNotActive.Wrap(err, "LLM proxy") - case errors.Is(err, constants.ErrGatewayIDMismatch): - return apperror.DeploymentGatewayMismatch.Wrap(err) - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to undeploy LLM proxy %s deployment %s on gateway %q", proxyId, deploymentId, gatewayId)) - } + return serviceError(err, fmt.Sprintf("failed to undeploy LLM proxy %s deployment %s on gateway %q", proxyId, deploymentId, gatewayId)) } httputil.WriteJSON(w, http.StatusOK, deployment) @@ -447,24 +325,7 @@ func (h *LLMProxyDeploymentHandler) RestoreLLMProxyDeployment(w http.ResponseWri deployment, err := h.deploymentService.RestoreLLMProxyDeployment(proxyId, deploymentId, gatewayId, orgId) if err != nil { // DP-originated artifacts are read-only: restore cannot be initiated from the CP. - if guardErr := mapArtifactGuardError(err); guardErr != nil { - return guardErr - } - switch { - case errors.Is(err, constants.ErrLLMProxyNotFound): - return apperror.LLMProxyNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentNotFound): - return apperror.DeploymentNotFound.Wrap(err) - case errors.Is(err, constants.ErrGatewayNotFound): - return apperror.GatewayNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentAlreadyDeployed): - return apperror.DeploymentRestoreConflict.Wrap(err) - case errors.Is(err, constants.ErrGatewayIDMismatch): - return apperror.DeploymentGatewayMismatch.Wrap(err) - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to restore LLM proxy %s deployment %s on gateway %q", proxyId, deploymentId, gatewayId)) - } + return serviceError(err, fmt.Sprintf("failed to restore LLM proxy %s deployment %s on gateway %q", proxyId, deploymentId, gatewayId)) } httputil.WriteJSON(w, http.StatusOK, deployment) @@ -491,17 +352,7 @@ func (h *LLMProxyDeploymentHandler) DeleteLLMProxyDeployment(w http.ResponseWrit err := h.deploymentService.DeleteLLMProxyDeployment(proxyId, deploymentId, orgId) if err != nil { - switch { - case errors.Is(err, constants.ErrLLMProxyNotFound): - return apperror.LLMProxyNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentNotFound): - return apperror.DeploymentNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentIsDeployed): - return apperror.DeploymentActive.Wrap(err) - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to delete LLM proxy %s deployment %s", proxyId, deploymentId)) - } + return serviceError(err, fmt.Sprintf("failed to delete LLM proxy %s deployment %s", proxyId, deploymentId)) } w.WriteHeader(http.StatusNoContent) @@ -528,15 +379,7 @@ func (h *LLMProxyDeploymentHandler) GetLLMProxyDeployment(w http.ResponseWriter, deployment, err := h.deploymentService.GetLLMProxyDeployment(proxyId, deploymentId, orgId) if err != nil { - switch { - case errors.Is(err, constants.ErrLLMProxyNotFound): - return apperror.LLMProxyNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentNotFound): - return apperror.DeploymentNotFound.Wrap(err) - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to get LLM proxy %s deployment %s", proxyId, deploymentId)) - } + return serviceError(err, fmt.Sprintf("failed to get LLM proxy %s deployment %s", proxyId, deploymentId)) } httputil.WriteJSON(w, http.StatusOK, deployment) @@ -569,15 +412,7 @@ func (h *LLMProxyDeploymentHandler) GetLLMProxyDeployments(w http.ResponseWriter deployments, err := h.deploymentService.GetLLMProxyDeployments(proxyId, orgId, gatewayId, status) if err != nil { - switch { - case errors.Is(err, constants.ErrLLMProxyNotFound): - return apperror.LLMProxyNotFound.Wrap(err) - case errors.Is(err, constants.ErrInvalidDeploymentStatus): - return apperror.DeploymentInvalidStatus.Wrap(err) - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to get LLM proxy %s deployments", proxyId)) - } + return serviceError(err, fmt.Sprintf("failed to get LLM proxy %s deployments", proxyId)) } paginateDeploymentList(deployments, limit, offset) diff --git a/platform-api/internal/handler/llm_proxy_apikey.go b/platform-api/internal/handler/llm_proxy_apikey.go index f132d59e7..713371db6 100644 --- a/platform-api/internal/handler/llm_proxy_apikey.go +++ b/platform-api/internal/handler/llm_proxy_apikey.go @@ -76,8 +76,7 @@ func (h *LLMProxyAPIKeyHandler) ListAPIKeys(w http.ResponseWriter, r *http.Reque if errors.As(err, &appErr) { return err } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to list LLM proxy API keys for proxy %s in org %s", proxyID, orgID)) + return serviceError(err, fmt.Sprintf("failed to list LLM proxy API keys for proxy %s in org %s", proxyID, orgID)) } httputil.WriteJSON(w, http.StatusOK, response) @@ -108,17 +107,7 @@ func (h *LLMProxyAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.Requ } if err := h.apiKeyService.DeleteLLMProxyAPIKey(r.Context(), proxyID, orgID, callerUserID, keyName); err != nil { - if errors.Is(err, constants.ErrAPINotFound) { - return apperror.ArtifactNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrAPIKeyNotFound) { - return apperror.LLMProxyAPIKeyNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrAPIKeyForbidden) { - return apperror.LLMProxyAPIKeyForbidden.Wrap(err) - } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to delete LLM proxy API key %s for proxy %s in org %s", keyName, proxyID, orgID)) + return serviceError(err, fmt.Sprintf("failed to delete LLM proxy API key %s for proxy %s in org %s", keyName, proxyID, orgID)) } h.slogger.Info("Successfully deleted LLM proxy API key", "proxyId", proxyID, "keyName", keyName, "organizationId", orgID) @@ -163,8 +152,7 @@ func (h *LLMProxyAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Requ return err } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to create LLM proxy API key for proxy %s in org %s", proxyID, orgID)) + return serviceError(err, fmt.Sprintf("failed to create LLM proxy API key for proxy %s in org %s", proxyID, orgID)) } h.slogger.Info("Successfully created LLM proxy API key", "proxyId", proxyID, "organizationId", orgID, "keyId", response.Id) diff --git a/platform-api/internal/handler/mcp.go b/platform-api/internal/handler/mcp.go index ecb867114..b5edf8c3f 100644 --- a/platform-api/internal/handler/mcp.go +++ b/platform-api/internal/handler/mcp.go @@ -19,7 +19,6 @@ package handler import ( "encoding/json" - "errors" "fmt" "log/slog" "net/http" @@ -202,58 +201,15 @@ func (h *MCPProxyHandler) FetchMCPProxyServerInfo(w http.ResponseWriter, r *http resp, err := h.service.FetchServerInfo(orgID, &req) if err != nil { - reqURL := "" - if req.Url != nil { - reqURL = *req.Url - } - switch { - case errors.Is(err, constants.ErrInvalidURL): - return apperror.ValidationFailed.Wrap(err, "Invalid URL provided"). - WithLogMessage(fmt.Sprintf("invalid URL provided for MCP server info fetch: %s", reqURL)) - case errors.Is(err, constants.ErrURLUnreachable): - return apperror.ValidationFailed.Wrap(err, "URL is unreachable"). - WithLogMessage(fmt.Sprintf("MCP server URL is unreachable: %s", reqURL)) - case errors.Is(err, constants.ErrMCPServerUnauthorized): - return apperror.ValidationFailed.Wrap(err, "MCP server returned 401 Unauthorized. Check the provided credentials."). - WithLogMessage(fmt.Sprintf("MCP server returned 401 Unauthorized: %s", reqURL)) - default: - return h.mapServiceError(err) - } + return h.mapServiceError(err) } httputil.WriteJSON(w, http.StatusOK, resp) return nil } -// mapServiceError maps service errors to *apperror.Error values for the -// centralized error mapper, preserving the exact status/code/message each -// error produced before the migration (including the read-only / -// deletion-guard mapping previously handled by respondArtifactGuardError). +// mapServiceError hands a service-layer error to the centralized error mapper. +// See serviceError in service_error.go. func (h *MCPProxyHandler) mapServiceError(err error) error { - switch { - case errors.Is(err, constants.ErrArtifactReadOnly): - return apperror.ArtifactReadOnly.Wrap(err, "Artifact is read-only: it originated from a data-plane gateway") - case errors.Is(err, constants.ErrArtifactRuntimeImmutable): - return apperror.ArtifactRuntimeImmutable.Wrap(err, "Runtime configuration of this artifact cannot be changed") - case errors.Is(err, constants.ErrArtifactDeployed): - return apperror.ArtifactDeployed.Wrap(err, "Artifact is still deployed on a gateway and cannot be deleted") - case errors.Is(err, constants.ErrHandleImmutable): - return apperror.ValidationFailed.Wrap(err, "The id is immutable and cannot be changed") - case errors.Is(err, constants.ErrInvalidInput): - return apperror.ValidationFailed.Wrap(err, "Invalid input parameters") - case errors.Is(err, constants.ErrInvalidPolicyVersion): - return apperror.ValidationFailed.Wrap(err, "Invalid policy version format") - case errors.Is(err, constants.ErrMCPProxyNotFound): - return apperror.MCPProxyNotFound.Wrap(err) - case errors.Is(err, constants.ErrMCPProxyExists): - return apperror.MCPProxyExists.Wrap(err) - case errors.Is(err, constants.ErrProjectNotFound): - return apperror.ProjectRefNotFound.Wrap(err) - case errors.Is(err, constants.ErrMCPProxyLimitReached): - return apperror.MCPProxyLimitReached.Wrap(err) - case errors.Is(err, constants.ErrSecretRefMissing): - return apperror.ValidationFailed.Wrap(err, "One or more referenced secrets do not exist") - default: - return apperror.Internal.Wrap(err) - } + return serviceError(err, "MCP proxy operation failed") } diff --git a/platform-api/internal/handler/mcp_deployment.go b/platform-api/internal/handler/mcp_deployment.go index 4371b4df1..cd6c61877 100644 --- a/platform-api/internal/handler/mcp_deployment.go +++ b/platform-api/internal/handler/mcp_deployment.go @@ -21,7 +21,6 @@ package handler import ( "encoding/json" - "errors" "fmt" "log/slog" "net/http" @@ -97,28 +96,7 @@ func (h *MCPProxyDeploymentHandler) DeployMCPProxy(w http.ResponseWriter, r *htt } deployment, err := h.deploymentService.DeployMCPProxyByHandle(proxyId, &req, orgId, createdBy) if err != nil { - if guardErr := mapArtifactGuardError(err); guardErr != nil { - return guardErr - } - switch { - case errors.Is(err, constants.ErrMCPProxyNotFound): - return apperror.MCPProxyNotFound.Wrap(err) - case errors.Is(err, constants.ErrGatewayNotFound): - return apperror.GatewayNotFound.Wrap(err) - case errors.Is(err, constants.ErrBaseDeploymentNotFound): - return apperror.DeploymentBaseNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentNameRequired): - return apperror.MCPProxyDeploymentValidationFailed.Wrap(err, "Deployment name is required") - case errors.Is(err, constants.ErrDeploymentBaseRequired): - return apperror.MCPProxyDeploymentValidationFailed.Wrap(err, "Base is required") - case errors.Is(err, constants.ErrDeploymentGatewayIDRequired): - return apperror.MCPProxyDeploymentValidationFailed.Wrap(err, "Gateway ID is required") - case errors.Is(err, constants.ErrInvalidInput): - return apperror.MCPProxyDeploymentValidationFailed.Wrap(err, "Invalid input") - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to deploy MCP proxy %s", proxyId)) - } + return serviceError(err, fmt.Sprintf("failed to deploy MCP proxy %s", proxyId)) } setLocation(w, "mcp-proxies", proxyId, "deployments", deployment.DeploymentId.String()) @@ -151,24 +129,7 @@ func (h *MCPProxyDeploymentHandler) UndeployMCPProxyDeployment(w http.ResponseWr deployment, err := h.deploymentService.UndeployDeploymentByHandle(proxyId, deploymentId, gatewayId, orgId) if err != nil { // DP-originated artifacts are read-only: undeployment cannot be initiated from the CP. - if guardErr := mapArtifactGuardError(err); guardErr != nil { - return guardErr - } - switch { - case errors.Is(err, constants.ErrMCPProxyNotFound): - return apperror.MCPProxyNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentNotFound): - return apperror.DeploymentNotFound.Wrap(err) - case errors.Is(err, constants.ErrGatewayNotFound): - return apperror.GatewayNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentNotActive): - return apperror.DeploymentNotActive.Wrap(err, "MCP proxy") - case errors.Is(err, constants.ErrGatewayIDMismatch): - return apperror.DeploymentGatewayMismatch.Wrap(err) - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to undeploy MCP proxy %s deployment %s on gateway %s", proxyId, deploymentId, gatewayId)) - } + return serviceError(err, fmt.Sprintf("failed to undeploy MCP proxy %s deployment %s on gateway %s", proxyId, deploymentId, gatewayId)) } httputil.WriteJSON(w, http.StatusOK, deployment) @@ -200,24 +161,7 @@ func (h *MCPProxyDeploymentHandler) RestoreMCPProxyDeployment(w http.ResponseWri deployment, err := h.deploymentService.RestoreMCPDeploymentByHandle(proxyId, deploymentId, gatewayId, orgId) if err != nil { // DP-originated artifacts are read-only: restore cannot be initiated from the CP. - if guardErr := mapArtifactGuardError(err); guardErr != nil { - return guardErr - } - switch { - case errors.Is(err, constants.ErrMCPProxyNotFound): - return apperror.MCPProxyNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentNotFound): - return apperror.DeploymentNotFound.Wrap(err) - case errors.Is(err, constants.ErrGatewayNotFound): - return apperror.GatewayNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentAlreadyDeployed): - return apperror.DeploymentRestoreConflict.Wrap(err) - case errors.Is(err, constants.ErrGatewayIDMismatch): - return apperror.DeploymentGatewayMismatch.Wrap(err) - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to restore MCP proxy %s deployment %s on gateway %s", proxyId, deploymentId, gatewayId)) - } + return serviceError(err, fmt.Sprintf("failed to restore MCP proxy %s deployment %s on gateway %s", proxyId, deploymentId, gatewayId)) } httputil.WriteJSON(w, http.StatusOK, deployment) @@ -244,17 +188,7 @@ func (h *MCPProxyDeploymentHandler) DeleteMCPProxyDeployment(w http.ResponseWrit err := h.deploymentService.DeleteDeploymentByHandle(proxyId, deploymentId, orgId) if err != nil { - switch { - case errors.Is(err, constants.ErrMCPProxyNotFound): - return apperror.MCPProxyNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentNotFound): - return apperror.DeploymentNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentIsDeployed): - return apperror.DeploymentActive.Wrap(err) - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to delete MCP proxy %s deployment %s", proxyId, deploymentId)) - } + return serviceError(err, fmt.Sprintf("failed to delete MCP proxy %s deployment %s", proxyId, deploymentId)) } w.WriteHeader(http.StatusNoContent) @@ -281,15 +215,7 @@ func (h *MCPProxyDeploymentHandler) GetMCPProxyDeployment(w http.ResponseWriter, deployment, err := h.deploymentService.GetDeploymentByHandle(proxyId, deploymentId, orgId) if err != nil { - switch { - case errors.Is(err, constants.ErrMCPProxyNotFound): - return apperror.MCPProxyNotFound.Wrap(err) - case errors.Is(err, constants.ErrDeploymentNotFound): - return apperror.DeploymentNotFound.Wrap(err) - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to get MCP proxy %s deployment %s", proxyId, deploymentId)) - } + return serviceError(err, fmt.Sprintf("failed to get MCP proxy %s deployment %s", proxyId, deploymentId)) } httputil.WriteJSON(w, http.StatusOK, deployment) @@ -333,15 +259,7 @@ func (h *MCPProxyDeploymentHandler) GetMCPProxyDeployments(w http.ResponseWriter deployments, err := h.deploymentService.GetDeploymentsByHandle(proxyId, gatewayVal, statusVal, orgId) if err != nil { - switch { - case errors.Is(err, constants.ErrMCPProxyNotFound): - return apperror.MCPProxyNotFound.Wrap(err) - case errors.Is(err, constants.ErrInvalidDeploymentStatus): - return apperror.DeploymentInvalidStatus.Wrap(err) - default: - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to get MCP proxy %s deployments", proxyId)) - } + return serviceError(err, fmt.Sprintf("failed to get MCP proxy %s deployments", proxyId)) } paginateDeploymentList(deployments, limit, offset) diff --git a/platform-api/internal/handler/project.go b/platform-api/internal/handler/project.go index 561d74e5e..a92306d73 100644 --- a/platform-api/internal/handler/project.go +++ b/platform-api/internal/handler/project.go @@ -176,15 +176,7 @@ func (h *ProjectHandler) UpdateProject(w http.ResponseWriter, r *http.Request) e } project, err := h.projectService.UpdateProject(projectId, &req, orgID, actor) if err != nil { - var appErr *apperror.Error - if errors.As(err, &appErr) { - return err - } - if errors.Is(err, constants.ErrHandleImmutable) { - return apperror.ValidationFailed.Wrap(err, "Project id is immutable and cannot be changed") - } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to update project %s in org %s", projectId, orgID)) + return serviceError(err, fmt.Sprintf("failed to update project %s in org %s", projectId, orgID)) } httputil.WriteJSON(w, http.StatusOK, project) diff --git a/platform-api/internal/handler/secret.go b/platform-api/internal/handler/secret.go index 4698ff838..a7b2574e1 100644 --- a/platform-api/internal/handler/secret.go +++ b/platform-api/internal/handler/secret.go @@ -24,7 +24,6 @@ import ( "time" "github.com/wso2/api-platform/platform-api/internal/apperror" - "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/dto" "github.com/wso2/api-platform/platform-api/internal/middleware" "github.com/wso2/api-platform/platform-api/internal/service" @@ -80,15 +79,7 @@ func (h *SecretHandler) CreateSecret(w http.ResponseWriter, r *http.Request) err resp, err := h.secretService.Create(orgID, userID, &req) if err != nil { - var appErr *apperror.Error - if errors.As(err, &appErr) { - return err - } - // Repository-origin duplicate (unique constraint) still surfaces as a sentinel. - if errors.Is(err, constants.ErrSecretAlreadyExists) { - return apperror.SecretExists.Wrap(err) - } - return apperror.Internal.Wrap(err).WithLogMessage("failed to create secret") + return serviceError(err, "failed to create secret") } setLocation(w, "secrets", resp.Handle) @@ -137,10 +128,7 @@ func (h *SecretHandler) GetSecret(w http.ResponseWriter, r *http.Request) error summary, err := h.secretService.Get(orgID, handle) if err != nil { - if errors.Is(err, constants.ErrSecretNotFound) { - return apperror.SecretNotFound.Wrap(err) - } - return apperror.Internal.Wrap(err).WithLogMessage("failed to get secret") + return serviceError(err, "failed to get secret") } httputil.WriteJSON(w, http.StatusOK, summary) @@ -178,10 +166,7 @@ func (h *SecretHandler) UpdateSecret(w http.ResponseWriter, r *http.Request) err resp, err := h.secretService.Update(orgID, handle, userID, &req) if err != nil { - if errors.Is(err, constants.ErrSecretNotFound) { - return apperror.SecretNotFound.Wrap(err) - } - return apperror.Internal.Wrap(err).WithLogMessage("failed to update secret") + return serviceError(err, "failed to update secret") } httputil.WriteJSON(w, http.StatusOK, resp) @@ -207,10 +192,8 @@ func (h *SecretHandler) DeleteSecret(w http.ResponseWriter, r *http.Request) err err = h.secretService.Delete(orgID, handle, userID) if err != nil { - if errors.Is(err, constants.ErrSecretNotFound) { - return apperror.SecretNotFound.Wrap(err) - } - + // SecretInUseError is a typed service error rather than a catalog error: it + // carries the blocking references, which become the response's details field. var inUseErr *service.SecretInUseError if errors.As(err, &inUseErr) { refs := make([]dto.SecretReferenceDTO, 0, len(inUseErr.References)) @@ -220,7 +203,7 @@ func (h *SecretHandler) DeleteSecret(w http.ResponseWriter, r *http.Request) err return apperror.SecretInUse.New().WithDetails(dto.SecretInUseDetails{References: refs}) } - return apperror.Internal.Wrap(err).WithLogMessage("failed to delete secret") + return serviceError(err, "failed to delete secret") } w.WriteHeader(http.StatusNoContent) diff --git a/platform-api/internal/handler/secret_integration_test.go b/platform-api/internal/handler/secret_integration_test.go index 78e3fcb9c..26d07a7cb 100644 --- a/platform-api/internal/handler/secret_integration_test.go +++ b/platform-api/internal/handler/secret_integration_test.go @@ -21,7 +21,6 @@ import ( "bytes" "database/sql" "encoding/json" - "errors" "fmt" "log/slog" "mime/multipart" @@ -31,7 +30,7 @@ import ( "path/filepath" "testing" - "github.com/wso2/api-platform/platform-api/internal/constants" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/database" "github.com/wso2/api-platform/platform-api/internal/dto" "github.com/wso2/api-platform/platform-api/internal/middleware" @@ -923,7 +922,7 @@ func TestSecretService_ValidateSecretRefs_DeprecatedHandleRejected(t *testing.T) if err == nil { t.Fatal("expected validation error for DEPRECATED secret ref, got nil") } - if !errors.Is(err, constants.ErrSecretRefMissing) { + if !apperror.ValidationFailed.Is(err) { t.Errorf("expected ErrSecretRefMissing, got %v", err) } } diff --git a/platform-api/internal/handler/service_error.go b/platform-api/internal/handler/service_error.go new file mode 100644 index 000000000..fc8df6881 --- /dev/null +++ b/platform-api/internal/handler/service_error.go @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * Licensed 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. + * + */ + +package handler + +import ( + "errors" + + "github.com/wso2/api-platform/platform-api/internal/apperror" +) + +// serviceError adapts an error returned by the service layer for the error +// mapper (middleware.MapErrors). +// +// Services construct their failures from the catalog (apperror.Def.New / +// Def.Wrap), so a catalog error already carries the code, HTTP status, and +// client-facing message the mapper needs — it passes straight through, and the +// handler neither restates the message nor re-derives the status. Handlers used +// to translate service-layer sentinel errors here with errors.Is ladders, which +// meant a condition the ladder forgot silently degraded to a 500 (or, worse, +// leaked the sentinel's internal text as the client message). +// +// Anything else is an error the service did not classify — a driver error, an +// IO failure, a bug. Per the "zero internal details" rule in error-handling.md +// it collapses to a generic 500 with logMsg kept as internal-only context; the +// original error travels as the wrapped cause for the log line only. +func serviceError(err error, logMsg string) error { + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err + } + return apperror.Internal.Wrap(err).WithLogMessage(logMsg) +} diff --git a/platform-api/internal/handler/subscription_handler.go b/platform-api/internal/handler/subscription_handler.go index 2cdd8c2b5..c5a15e992 100644 --- a/platform-api/internal/handler/subscription_handler.go +++ b/platform-api/internal/handler/subscription_handler.go @@ -105,13 +105,11 @@ func (h *SubscriptionHandler) CreateSubscription(w http.ResponseWriter, r *http. if errors.As(err, &appErr) { return err } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to create subscription for api %s in org %s", req.APIID, orgId)) + return serviceError(err, fmt.Sprintf("failed to create subscription for api %s in org %s", req.APIID, orgId)) } resp, err := h.toSubscriptionResponse(sub, orgId) if err != nil { - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to resolve subscription identity for api %s in org %s", req.APIID, orgId)) + return serviceError(err, fmt.Sprintf("failed to resolve subscription identity for api %s in org %s", req.APIID, orgId)) } setLocation(w, "subscriptions", sub.UUID) httputil.WriteJSON(w, http.StatusCreated, resp) @@ -156,8 +154,7 @@ func (h *SubscriptionHandler) ListSubscriptions(w http.ResponseWriter, r *http.R if errors.As(err, &appErr) { return err } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to list subscriptions for api %s in org %s", apiId, orgId)) + return serviceError(err, fmt.Sprintf("failed to list subscriptions for api %s in org %s", apiId, orgId)) } // Bulk fetch API handles and plan names to avoid N+1 queries apiUUIDSet := make(map[string]struct{}) @@ -180,13 +177,11 @@ func (h *SubscriptionHandler) ListSubscriptions(w http.ResponseWriter, r *http.R } artifactMetaMap, err := h.subscriptionService.GetArtifactMetadataMap(apiUUIDs, orgId) if err != nil { - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to bulk fetch artifact metadata for list in org %s", orgId)) + return serviceError(err, fmt.Sprintf("failed to bulk fetch artifact metadata for list in org %s", orgId)) } planNameMap, err := h.subscriptionPlanService.GetPlanNameMap(planIDs, orgId) if err != nil { - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to bulk fetch plan names for list in org %s", orgId)) + return serviceError(err, fmt.Sprintf("failed to bulk fetch plan names for list in org %s", orgId)) } // Bulk-resolve createdBy UUIDs to their raw identity to avoid N+1 lookups. createdByUUIDs := make([]string, 0, len(list)) @@ -195,8 +190,7 @@ func (h *SubscriptionHandler) ListSubscriptions(w http.ResponseWriter, r *http.R } createdByMap, err := h.identity.SubsForUUIDs(createdByUUIDs) if err != nil { - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to resolve subscription creator identities in org %s", orgId)) + return serviceError(err, fmt.Sprintf("failed to resolve subscription creator identities in org %s", orgId)) } items := make([]map[string]any, 0, len(list)) for _, sub := range list { @@ -231,13 +225,11 @@ func (h *SubscriptionHandler) GetSubscription(w http.ResponseWriter, r *http.Req if errors.As(err, &appErr) { return err } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to get subscription %s in org %s", subscriptionId, orgId)) + return serviceError(err, fmt.Sprintf("failed to get subscription %s in org %s", subscriptionId, orgId)) } resp, err := h.toSubscriptionResponse(sub, orgId) if err != nil { - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to resolve subscription identity for subscription %s in org %s", subscriptionId, orgId)) + return serviceError(err, fmt.Sprintf("failed to resolve subscription identity for subscription %s in org %s", subscriptionId, orgId)) } httputil.WriteJSON(w, http.StatusOK, resp) return nil @@ -281,13 +273,11 @@ func (h *SubscriptionHandler) UpdateSubscription(w http.ResponseWriter, r *http. if errors.As(err, &appErr) { return err } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to update subscription %s in org %s", subscriptionId, orgId)) + return serviceError(err, fmt.Sprintf("failed to update subscription %s in org %s", subscriptionId, orgId)) } resp, err := h.toSubscriptionResponse(sub, orgId) if err != nil { - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to resolve subscription identity for subscription %s in org %s", subscriptionId, orgId)) + return serviceError(err, fmt.Sprintf("failed to resolve subscription identity for subscription %s in org %s", subscriptionId, orgId)) } httputil.WriteJSON(w, http.StatusOK, resp) return nil @@ -313,14 +303,7 @@ func (h *SubscriptionHandler) DeleteSubscription(w http.ResponseWriter, r *http. return err } if err := h.subscriptionService.DeleteSubscription(subscriptionId, orgId, subscriberID, actor); err != nil { - if errors.Is(err, constants.ErrSubscriptionNotFound) { - return apperror.SubscriptionNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrSubscriptionSubscriberMismatch) { - return apperror.SubscriptionForbidden.Wrap(err) - } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to delete subscription %s in org %s", subscriptionId, orgId)) + return serviceError(err, fmt.Sprintf("failed to delete subscription %s in org %s", subscriptionId, orgId)) } w.WriteHeader(http.StatusNoContent) return nil diff --git a/platform-api/internal/handler/subscription_plan_handler.go b/platform-api/internal/handler/subscription_plan_handler.go index 93aac5e64..a8f62e9e4 100644 --- a/platform-api/internal/handler/subscription_plan_handler.go +++ b/platform-api/internal/handler/subscription_plan_handler.go @@ -367,15 +367,7 @@ func (h *SubscriptionPlanHandler) UpdateSubscriptionPlan(w http.ResponseWriter, } updated, err := h.planService.UpdatePlan(planId, orgId, actor, update) if err != nil { - var appErr *apperror.Error - if errors.As(err, &appErr) { - return err - } - if errors.Is(err, constants.ErrHandleImmutable) { - return apperror.ValidationFailed.Wrap(err, "The plan id is immutable and cannot be changed") - } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to update subscription plan %s in org %s", planId, orgId)) + return serviceError(err, fmt.Sprintf("failed to update subscription plan %s in org %s", planId, orgId)) } resp, err := h.toSubscriptionPlanResponse(updated, true) if err != nil { diff --git a/platform-api/internal/middleware/error_mapper.go b/platform-api/internal/middleware/error_mapper.go index 6985936c4..dc73533d9 100644 --- a/platform-api/internal/middleware/error_mapper.go +++ b/platform-api/internal/middleware/error_mapper.go @@ -18,7 +18,6 @@ package middleware import ( - "errors" "log/slog" "net/http" "runtime/debug" @@ -66,56 +65,11 @@ func MapErrors(slogger *slog.Logger, next ErrorHandlerFunc) http.HandlerFunc { // client-facing response. Errors that are not *apperror.Error fall back to a // generic 500 per the "zero internal details" rule in error-handling.md. // -// Severity split: a 4xx is a client outcome, not a system fault — it logs at -// WARN without the stack. A 5xx is a system fault — it logs at ERROR with the -// origin stack, and the tracking ID is echoed in the response body so the -// client can quote it back for correlation. +// The log-then-respond severity split lives in apperror.LogAndWrite so this +// mapper and the event-gateway plugin's respondCatalogError share one +// implementation. func writeMappedError(w http.ResponseWriter, r *http.Request, slogger *slog.Logger, err error) { - trackID := uuid.NewString() - var appErr *apperror.Error - if !errors.As(err, &appErr) { - appErr = apperror.Internal.Wrap(err) - } - // A handler fallback may have wrapped a more specific typed error produced - // deeper in the stack (service layer) in a generic Internal — prefer the - // specific one so service-origin errors keep their code and status. - for appErr.Code == apperror.CodeCommonInternalError && appErr.Cause != nil { - var inner *apperror.Error - if !errors.As(appErr.Cause, &inner) { - break - } - appErr = inner - } - - logFields := []any{ - "trackingId", trackID, - "code", appErr.Code, - "status", appErr.HTTPStatus, + apperror.LogAndWrite(w, slogger, apperror.FromError(err), "path", r.URL.Path, - "method", r.Method, - } - if appErr.LogMessage != "" { - logFields = append(logFields, "detail", appErr.LogMessage) - } - if appErr.Cause != nil { - logFields = append(logFields, "cause", appErr.Cause.Error()) - } - // origin is the file:line where the error was actually constructed - // (Def.New/Def.Wrap call site) — not to be confused with slog's own - // "source" attribute, which always points at this mapper's log call. - if origin := appErr.Origin(); origin != "" { - logFields = append(logFields, "origin", origin) - } - - if appErr.HTTPStatus >= http.StatusInternalServerError { - if len(appErr.Stack) > 0 { - logFields = append(logFields, "stack", appErr.StackString()) - } - slogger.Error("request failed", logFields...) - apperror.WriteHTTP(w, appErr, trackID) - return - } - - slogger.Warn("request failed", logFields...) - apperror.WriteHTTP(w, appErr, "") + "method", r.Method) } diff --git a/platform-api/internal/repository/custom_policy.go b/platform-api/internal/repository/custom_policy.go index 5f34bab86..303e6c73d 100644 --- a/platform-api/internal/repository/custom_policy.go +++ b/platform-api/internal/repository/custom_policy.go @@ -22,7 +22,7 @@ import ( "errors" "time" - "github.com/wso2/api-platform/platform-api/internal/constants" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/database" "github.com/wso2/api-platform/platform-api/internal/model" ) @@ -262,7 +262,7 @@ func (r *CustomPolicyRepo) DeleteCustomPolicyIfUnused(orgUUID, policyUUID string return err } if count == 0 { - return constants.ErrCustomPolicyNotFound + return apperror.CustomPolicyNotFound.New() } - return constants.ErrCustomPolicyInUse + return apperror.PolicyInUse.New() } diff --git a/platform-api/internal/repository/deployment.go b/platform-api/internal/repository/deployment.go index 2bdce8df0..cf5d788a1 100644 --- a/platform-api/internal/repository/deployment.go +++ b/platform-api/internal/repository/deployment.go @@ -25,7 +25,7 @@ import ( "strings" "time" - "github.com/wso2/api-platform/platform-api/internal/constants" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/database" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/utils" @@ -247,7 +247,7 @@ func (r *DeploymentRepo) GetWithContent(deploymentID, artifactUUID, orgUUID stri if err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } return nil, err } @@ -273,7 +273,7 @@ func (r *DeploymentRepo) Delete(deploymentID, artifactUUID, orgUUID string) erro } if rowsAffected == 0 { - return constants.ErrDeploymentNotFound + return apperror.DeploymentNotFound.New() } return nil @@ -592,7 +592,7 @@ func (r *DeploymentRepo) GetWithState(deploymentID, artifactUUID, orgUUID string if err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } return nil, err } @@ -814,7 +814,7 @@ func (r *DeploymentRepo) GetControlPlaneDeploymentsByGateway(gatewayID, orgUUID FROM deployment_status s INNER JOIN artifacts a ON s.artifact_uuid = a.uuid INNER JOIN ( - `+r.reg.UnionAllSelect("uuid", "handle", "origin")+` + ` + r.reg.UnionAllSelect("uuid", "handle", "origin") + ` ) src ON src.uuid = s.artifact_uuid WHERE s.gateway_uuid = ? AND s.organization_uuid = ? AND src.origin <> 'gateway_api'` diff --git a/platform-api/internal/repository/llm.go b/platform-api/internal/repository/llm.go index 92475efe6..2e720e0b7 100644 --- a/platform-api/internal/repository/llm.go +++ b/platform-api/internal/repository/llm.go @@ -24,6 +24,7 @@ import ( "strings" "time" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/database" "github.com/wso2/api-platform/platform-api/internal/model" @@ -177,7 +178,7 @@ func (r *LLMProviderTemplateRepo) createNewVersionOnce(t *model.LLMProviderTempl return err } if sameVersion > 0 { - return constants.ErrLLMProviderTemplateVersionExists + return apperror.LLMProviderTemplateVersionExists.New() } // Demote the current latest within this family (same group_id). diff --git a/platform-api/internal/repository/secret.go b/platform-api/internal/repository/secret.go index 668a522cc..003d876f9 100644 --- a/platform-api/internal/repository/secret.go +++ b/platform-api/internal/repository/secret.go @@ -27,7 +27,7 @@ import ( "github.com/jackc/pgx/v5/pgconn" sqlite3 "github.com/mattn/go-sqlite3" - "github.com/wso2/api-platform/platform-api/internal/constants" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/database" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/utils" @@ -83,7 +83,7 @@ func (r *SecretRepo) Create(s *model.Secret) error { ) if err != nil { if isSecretUniqueViolation(err) { - return constants.ErrSecretAlreadyExists + return apperror.SecretExists.Wrap(err) } return fmt.Errorf("failed to create secret: %w", err) } @@ -124,7 +124,7 @@ func (r *SecretRepo) GetByHandle(orgID, handle string) (*model.Secret, error) { s, err := scanSecret(r.db.QueryRow(query, orgID, handle)) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, constants.ErrSecretNotFound + return nil, apperror.SecretNotFound.Wrap(err) } return nil, fmt.Errorf("failed to get secret: %w", err) } @@ -225,7 +225,7 @@ func (r *SecretRepo) Update(s *model.Secret) error { return err } if rows == 0 { - return constants.ErrSecretNotFound + return apperror.SecretNotFound.New() } return nil } @@ -249,7 +249,7 @@ func (r *SecretRepo) FindRefsAndSoftDelete(orgID, handle, updatedBy string) ([]m var lockedID string if err := tx.QueryRow(lockQuery, orgID, handle).Scan(&lockedID); err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, constants.ErrSecretNotFound + return nil, apperror.SecretNotFound.Wrap(err) } return nil, fmt.Errorf("failed to lock secret row: %w", err) } @@ -303,7 +303,7 @@ func (r *SecretRepo) FindRefsAndSoftDelete(orgID, handle, updatedBy string) ([]m return nil, err } if affected == 0 { - return nil, constants.ErrSecretNotFound + return nil, apperror.SecretNotFound.New() } if err := tx.Commit(); err != nil { diff --git a/platform-api/internal/repository/secret_test.go b/platform-api/internal/repository/secret_test.go index 48aa4ca84..d0cf07171 100644 --- a/platform-api/internal/repository/secret_test.go +++ b/platform-api/internal/repository/secret_test.go @@ -21,13 +21,15 @@ import ( "testing" "time" - "github.com/wso2/api-platform/platform-api/internal/constants" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/model" ) // ---- helpers ---------------------------------------------------------------- -func createTestSecret(t *testing.T, db interface{ Exec(string, ...interface{}) (interface{ RowsAffected() (int64, error) }, error) }, orgID, handle string) { +func createTestSecret(t *testing.T, db interface { + Exec(string, ...interface{}) (interface{ RowsAffected() (int64, error) }, error) +}, orgID, handle string) { t.Helper() } @@ -106,7 +108,7 @@ func TestSecretRepo_GetByHandle_NotFound(t *testing.T) { repo := NewSecretRepo(db) _, err := repo.GetByHandle(orgID, "nonexistent") - if err != constants.ErrSecretNotFound { + if !apperror.SecretNotFound.Is(err) { t.Errorf("expected ErrSecretNotFound, got %v", err) } } @@ -231,7 +233,7 @@ func TestSecretRepo_List_Pagination(t *testing.T) { for i := 0; i < 5; i++ { s := &model.Secret{ OrganizationID: orgID, - Handle: string(rune('a' + i)) + "-secret", + Handle: string(rune('a'+i)) + "-secret", Ciphertext: []byte("ct"), Hash: "h", CreatedBy: "u", @@ -852,7 +854,7 @@ func TestSecretRepo_Create_UniqueConstraint_409(t *testing.T) { if err == nil { t.Fatal("expected error on duplicate handle, got nil") } - if err != constants.ErrSecretAlreadyExists { + if !apperror.SecretExists.Is(err) { t.Errorf("expected ErrSecretAlreadyExists, got: %v", err) } } diff --git a/platform-api/internal/repository/subscription_repository.go b/platform-api/internal/repository/subscription_repository.go index 110504534..b67d121f6 100644 --- a/platform-api/internal/repository/subscription_repository.go +++ b/platform-api/internal/repository/subscription_repository.go @@ -28,7 +28,7 @@ import ( "time" "github.com/wso2/api-platform/platform-api/config" - "github.com/wso2/api-platform/platform-api/internal/constants" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/database" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/utils" @@ -125,7 +125,7 @@ func (r *SubscriptionRepo) Create(sub *model.Subscription) error { ) if err != nil { if isSubscriptionUniqueViolation(err) { - return constants.ErrSubscriptionAlreadyExists + return apperror.SubscriptionExists.New() } return fmt.Errorf("failed to insert subscription: %w", err) } @@ -175,7 +175,7 @@ func (r *SubscriptionRepo) GetByID(subscriptionID, orgUUID string) (*model.Subsc ) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, constants.ErrSubscriptionNotFound + return nil, apperror.SubscriptionNotFound.New() } return nil, err } @@ -324,7 +324,7 @@ func (r *SubscriptionRepo) Update(sub *model.Subscription) error { } n, _ := result.RowsAffected() if n == 0 { - return constants.ErrSubscriptionNotFound + return apperror.SubscriptionNotFound.New() } return nil } @@ -355,13 +355,13 @@ func (r *SubscriptionRepo) UpdateToken(subscriptionID, orgUUID, newToken string) result, err := r.db.Exec(r.db.Rebind(query), encryptedToken, hashedToken, time.Now(), subscriptionID, orgUUID) if err != nil { if isSubscriptionUniqueViolation(err) { - return constants.ErrSubscriptionAlreadyExists + return apperror.SubscriptionExists.New() } return fmt.Errorf("failed to update subscription token: %w", err) } n, _ := result.RowsAffected() if n == 0 { - return constants.ErrSubscriptionNotFound + return apperror.SubscriptionNotFound.New() } return nil } @@ -375,7 +375,7 @@ func (r *SubscriptionRepo) Delete(subscriptionID, orgUUID string) error { } n, _ := result.RowsAffected() if n == 0 { - return constants.ErrSubscriptionNotFound + return apperror.SubscriptionNotFound.New() } return nil } diff --git a/platform-api/internal/service/api.go b/platform-api/internal/service/api.go index 6a1b43ca1..de8451871 100644 --- a/platform-api/internal/service/api.go +++ b/platform-api/internal/service/api.go @@ -101,7 +101,7 @@ func (s *APIService) CreateAPI(req *api.CreateRESTAPIRequest, orgUUID, createdBy return nil, err } if project == nil { - return nil, constants.ErrProjectNotFound + return nil, apperror.ProjectNotFound.New() } // Handle the API handle (user-facing identifier) @@ -193,10 +193,10 @@ func (s *APIService) GetAPIByUUID(apiUUID, orgUUID string) (*api.RESTAPI, error) return nil, fmt.Errorf("failed to get api: %w", err) } if apiModel == nil { - return nil, constants.ErrAPINotFound + return nil, apperror.RESTAPINotFound.New() } if apiModel.OrganizationID != orgUUID { - return nil, constants.ErrAPINotFound + return nil, apperror.RESTAPINotFound.New() } return s.modelToRESTAPI(apiModel) @@ -236,7 +236,7 @@ func (s *APIService) getAPIUUIDByHandle(handle, orgUUID string) (string, error) return "", err } if metadata == nil { - return "", constants.ErrAPINotFound + return "", apperror.RESTAPINotFound.New() } return metadata.ID, nil @@ -253,7 +253,7 @@ func (s *APIService) GetAPIsByOrganization(orgUUID string, projectHandle string, return nil, 0, err } if project == nil { - return nil, 0, constants.ErrProjectNotFound + return nil, 0, apperror.ProjectNotFound.New() } projectUUID = project.ID } @@ -295,10 +295,10 @@ func (s *APIService) UpdateAPI(apiUUID string, req *api.RESTAPI, orgUUID, update return nil, err } if existingAPIModel == nil { - return nil, constants.ErrAPINotFound + return nil, apperror.RESTAPINotFound.New() } if existingAPIModel.OrganizationID != orgUUID { - return nil, constants.ErrAPINotFound + return nil, apperror.RESTAPINotFound.New() } // Apply updates using shared helper @@ -351,7 +351,11 @@ func (s *APIService) ensureRESTRuntimeArtifactUnchanged(existing, updated *model } existingArtifact, err := s.apiUtil.BuildAPIDeploymentYAML(existing) if err != nil { - return constants.ErrArtifactRuntimeImmutable + // Fail closed: without the existing runtime artifact we cannot prove the update + // leaves it unchanged, so treat it as a change the control plane may not make. + return apperror.ArtifactRuntimeImmutable.Wrap(err, + "The update changes the gateway runtime configuration, which is owned by the data-plane "+ + "gateway and cannot be modified from the control plane.") } updatedArtifact, err := s.apiUtil.BuildAPIDeploymentYAML(updated) if err != nil { @@ -372,10 +376,10 @@ func (s *APIService) DeleteAPI(apiUUID, orgUUID, deletedBy string) error { return err } if api == nil { - return constants.ErrAPINotFound + return apperror.RESTAPINotFound.New() } if api.OrganizationID != orgUUID { - return constants.ErrAPINotFound + return apperror.RESTAPINotFound.New() } // DP-originated artifacts may only be deleted once undeployed on all gateways. @@ -471,7 +475,7 @@ func (s *APIService) GetAPIGatewaysByHandle(handle, orgId string, limit, offset return nil, err } if apiModel == nil || apiModel.OrganizationID != orgId { - return nil, constants.ErrAPINotFound + return nil, apperror.RESTAPINotFound.New() } gatewayDetails, err := s.apiRepo.GetAPIGatewaysWithDetails(apiUUID, orgId) @@ -507,10 +511,10 @@ func (s *APIService) AddGatewaysToAPI(apiUUID string, gatewayIds []string, orgUU return nil, err } if apiModel == nil { - return nil, constants.ErrAPINotFound + return nil, apperror.RESTAPINotFound.New() } if apiModel.OrganizationID != orgUUID { - return nil, constants.ErrAPINotFound + return nil, apperror.RESTAPINotFound.New() } var validGateways []*model.Gateway for _, gatewayId := range gatewayIds { @@ -519,7 +523,7 @@ func (s *APIService) AddGatewaysToAPI(apiUUID string, gatewayIds []string, orgUU return nil, err } if gateway == nil { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } validGateways = append(validGateways, gateway) } @@ -560,10 +564,10 @@ func (s *APIService) GetAPIGateways(apiUUID, orgUUID string) (*api.RESTAPIGatewa return nil, err } if apiModel == nil { - return nil, constants.ErrAPINotFound + return nil, apperror.RESTAPINotFound.New() } if apiModel.OrganizationID != orgUUID { - return nil, constants.ErrAPINotFound + return nil, apperror.RESTAPINotFound.New() } gatewayDetails, err := s.apiRepo.GetAPIGatewaysWithDetails(apiUUID, orgUUID) if err != nil { @@ -599,17 +603,17 @@ func (s *APIService) validateCreateAPIRequest(req *api.CreateRESTAPIRequest, org return err } if handleExists { - return constants.ErrHandleExists + return apperror.RESTAPIExists.New("An API with this id already exists.") } } if req.DisplayName == "" { - return constants.ErrInvalidAPIName + return apperror.ValidationFailed.New("The displayName field is required.") } if !s.isValidContext(req.Context) { - return constants.ErrInvalidAPIContext + return apperror.ValidationFailed.New("Invalid API context format.") } if !s.isValidVersion(req.Version) { - return constants.ErrInvalidAPIVersion + return apperror.ValidationFailed.New("Invalid API version format.") } if strings.TrimSpace(req.ProjectId) == "" { return apperror.ValidationFailed.New("project id is required") @@ -620,17 +624,17 @@ func (s *APIService) validateCreateAPIRequest(req *api.CreateRESTAPIRequest, org return err } if nameVersionExists { - return constants.ErrAPINameVersionAlreadyExists + return apperror.RESTAPIExists.New("An API with the same name and version already exists in the organization.") } // Validate lifecycle status if provided if req.LifeCycleStatus != nil && !constants.ValidLifecycleStates[string(*req.LifeCycleStatus)] { - return constants.ErrInvalidLifecycleState + return apperror.ValidationFailed.New("Invalid lifecycle status.") } // Validate API type if provided if req.Kind != nil && !strings.EqualFold(*req.Kind, constants.RestApi) { - return constants.ErrInvalidAPIType + return apperror.ValidationFailed.New("Invalid API type.") } // Type-specific validations @@ -656,7 +660,7 @@ func (s *APIService) validateCreateAPIRequest(req *api.CreateRESTAPIRequest, org if req.Transport != nil && len(*req.Transport) > 0 { for _, transport := range *req.Transport { if !constants.ValidTransports[strings.ToLower(transport)] { - return constants.ErrInvalidTransport + return apperror.ValidationFailed.New(fmt.Sprintf("Invalid transport protocol %q.", transport)) } } } @@ -689,12 +693,14 @@ func (s *APIService) validateSubscriptionPlans(planHandles *[]string, orgUUID st plan, err := s.subscriptionPlanRepo.GetByHandleAndOrg(handle, orgUUID) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return fmt.Errorf("%w: plan %q", constants.ErrSubscriptionPlanNotFoundOrInactive, handle) + return apperror.ValidationFailed.Wrap(err, fmt.Sprintf( + "Subscription plan %q was not found or is not active.", handle)) } return err } if plan == nil || plan.Status != model.SubscriptionPlanStatusActive { - return fmt.Errorf("%w: plan %q", constants.ErrSubscriptionPlanNotFoundOrInactive, handle) + return apperror.ValidationFailed.New(fmt.Sprintf( + "Subscription plan %q was not found or is not active.", handle)) } } return nil @@ -750,25 +756,25 @@ func (s *APIService) validateUpdateAPIRequest(existingAPIModel *model.API, req * return err } if nameVersionExists { - return constants.ErrAPINameVersionAlreadyExists + return apperror.RESTAPIExists.New("An API with the same name and version already exists in the organization.") } } // Validate lifecycle status if provided if req.LifeCycleStatus != nil && !constants.ValidLifecycleStates[string(*req.LifeCycleStatus)] { - return constants.ErrInvalidLifecycleState + return apperror.ValidationFailed.New("Invalid lifecycle status.") } // Validate API type if provided if req.Kind != nil && !strings.EqualFold(*req.Kind, constants.RestApi) { - return constants.ErrInvalidAPIType + return apperror.ValidationFailed.New("Invalid API type.") } // Validate transport protocols if provided if req.Transport != nil { for _, transport := range *req.Transport { if !constants.ValidTransports[strings.ToLower(transport)] { - return constants.ErrInvalidTransport + return apperror.ValidationFailed.New(fmt.Sprintf("Invalid transport protocol %q.", transport)) } } } diff --git a/platform-api/internal/service/api_test.go b/platform-api/internal/service/api_test.go index 31957d72f..855177e7e 100644 --- a/platform-api/internal/service/api_test.go +++ b/platform-api/internal/service/api_test.go @@ -22,7 +22,7 @@ import ( "testing" "github.com/wso2/api-platform/platform-api/api" - "github.com/wso2/api-platform/platform-api/internal/constants" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/repository" "github.com/wso2/api-platform/platform-api/internal/utils" @@ -62,7 +62,7 @@ func TestValidateUpdateAPIRequest(t *testing.T) { mockNameVersionExists bool mockNameVersionError error wantErr bool - expectedErr error + expectedErr apperror.Def errContains string expectedExcludeHandle string verifyExcludeHandleCalled bool @@ -97,7 +97,7 @@ func TestValidateUpdateAPIRequest(t *testing.T) { req: &api.RESTAPI{DisplayName: "Conflicting Name"}, mockNameVersionExists: true, wantErr: true, - expectedErr: constants.ErrAPINameVersionAlreadyExists, + expectedErr: apperror.RESTAPIExists, }, { name: "invalid lifecycle state", @@ -107,7 +107,7 @@ func TestValidateUpdateAPIRequest(t *testing.T) { }, req: &api.RESTAPI{LifeCycleStatus: statusPtr("INVALID_STATE")}, wantErr: true, - expectedErr: constants.ErrInvalidLifecycleState, + expectedErr: apperror.ValidationFailed, }, { name: "invalid api type", @@ -117,7 +117,7 @@ func TestValidateUpdateAPIRequest(t *testing.T) { }, req: &api.RESTAPI{Kind: ptr("INVALID_TYPE")}, wantErr: true, - expectedErr: constants.ErrInvalidAPIType, + expectedErr: apperror.ValidationFailed, }, { name: "invalid transport", @@ -127,7 +127,7 @@ func TestValidateUpdateAPIRequest(t *testing.T) { }, req: &api.RESTAPI{Transport: slicePtr([]string{"invalid"})}, wantErr: true, - expectedErr: constants.ErrInvalidTransport, + expectedErr: apperror.ValidationFailed, }, { name: "valid lifecycle state", @@ -172,7 +172,7 @@ func TestValidateUpdateAPIRequest(t *testing.T) { }, }, wantErr: true, - errContains: "policy version must be major-only", + errContains: "must be major-only", }, { name: "invalid channel policy version", @@ -190,7 +190,7 @@ func TestValidateUpdateAPIRequest(t *testing.T) { }, }, wantErr: true, - errContains: "policy version must be major-only", + errContains: "must be major-only", }, } @@ -216,8 +216,8 @@ func TestValidateUpdateAPIRequest(t *testing.T) { } if tt.wantErr && err != nil { - if tt.expectedErr != nil && err != tt.expectedErr { - t.Errorf("validateUpdateAPIRequest() error = %v, expectedErr %v", err, tt.expectedErr) + if tt.expectedErr.Code != "" && !tt.expectedErr.Is(err) { + t.Errorf("validateUpdateAPIRequest() error = %v, expected code %s", err, tt.expectedErr.Code) } if tt.errContains != "" && !contains(err.Error(), tt.errContains) { t.Errorf("validateUpdateAPIRequest() error = %v, should contain %v", err, tt.errContains) @@ -245,7 +245,7 @@ func TestValidateCreateAPIRequest(t *testing.T) { mockNameVersionExists bool mockNameVersionError error wantErr bool - expectedErr error + expectedErr apperror.Def errContains string verifyExcludeHandleEmpty bool expectedExcludeHandle string @@ -276,7 +276,7 @@ func TestValidateCreateAPIRequest(t *testing.T) { }, mockHandleExists: true, wantErr: true, - expectedErr: constants.ErrHandleExists, + expectedErr: apperror.RESTAPIExists, }, { name: "name version already exists", @@ -289,7 +289,7 @@ func TestValidateCreateAPIRequest(t *testing.T) { }, mockNameVersionExists: true, wantErr: true, - expectedErr: constants.ErrAPINameVersionAlreadyExists, + expectedErr: apperror.RESTAPIExists, verifyExcludeHandleEmpty: true, expectedExcludeHandle: "", }, @@ -303,7 +303,7 @@ func TestValidateCreateAPIRequest(t *testing.T) { Upstream: api.Upstream{}, }, wantErr: true, - expectedErr: constants.ErrInvalidAPIName, + expectedErr: apperror.ValidationFailed, }, { name: "missing project id", @@ -327,7 +327,7 @@ func TestValidateCreateAPIRequest(t *testing.T) { Upstream: api.Upstream{}, }, wantErr: true, - expectedErr: constants.ErrInvalidAPIContext, + expectedErr: apperror.ValidationFailed, }, { name: "invalid version", @@ -339,7 +339,7 @@ func TestValidateCreateAPIRequest(t *testing.T) { Upstream: api.Upstream{}, }, wantErr: true, - expectedErr: constants.ErrInvalidAPIVersion, + expectedErr: apperror.ValidationFailed, }, { name: "invalid lifecycle state", @@ -352,7 +352,7 @@ func TestValidateCreateAPIRequest(t *testing.T) { Upstream: api.Upstream{}, }, wantErr: true, - expectedErr: constants.ErrInvalidLifecycleState, + expectedErr: apperror.ValidationFailed, }, { name: "invalid api type", @@ -365,7 +365,7 @@ func TestValidateCreateAPIRequest(t *testing.T) { Upstream: api.Upstream{}, }, wantErr: true, - expectedErr: constants.ErrInvalidAPIType, + expectedErr: apperror.ValidationFailed, }, { name: "invalid transport", @@ -378,7 +378,7 @@ func TestValidateCreateAPIRequest(t *testing.T) { Upstream: api.Upstream{}, }, wantErr: true, - expectedErr: constants.ErrInvalidTransport, + expectedErr: apperror.ValidationFailed, }, { name: "valid lifecycle state", @@ -442,7 +442,7 @@ func TestValidateCreateAPIRequest(t *testing.T) { }, }, wantErr: true, - errContains: "policy version must be major-only", + errContains: "must be major-only", }, { name: "invalid channel policy version", @@ -461,7 +461,7 @@ func TestValidateCreateAPIRequest(t *testing.T) { }, }, wantErr: true, - errContains: "policy version must be major-only", + errContains: "must be major-only", }, { name: "unspecified operation policy version is allowed (gateway resolves to latest)", @@ -508,8 +508,8 @@ func TestValidateCreateAPIRequest(t *testing.T) { } if tt.wantErr && err != nil { - if tt.expectedErr != nil && err != tt.expectedErr { - t.Errorf("validateCreateAPIRequest() error = %v, expectedErr %v", err, tt.expectedErr) + if tt.expectedErr.Code != "" && !tt.expectedErr.Is(err) { + t.Errorf("validateCreateAPIRequest() error = %v, expected code %s", err, tt.expectedErr.Code) } if tt.errContains != "" && !contains(err.Error(), tt.errContains) { t.Errorf("validateCreateAPIRequest() error = %v, should contain %v", err, tt.errContains) diff --git a/platform-api/internal/service/apikey.go b/platform-api/internal/service/apikey.go index a2a562baa..ac0d4cb34 100644 --- a/platform-api/internal/service/apikey.go +++ b/platform-api/internal/service/apikey.go @@ -288,7 +288,7 @@ func (s *APIKeyService) CreateAPIKey(ctx context.Context, apiHandle, kind, orgId } if apiMetadata == nil { s.slogger.Warn("API not found by handle", "apiHandle", apiHandle, "orgId", orgId) - return apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) + return apperror.ArtifactNotFound.New() } apiId := apiMetadata.ID @@ -298,7 +298,7 @@ func (s *APIKeyService) CreateAPIKey(ctx context.Context, apiHandle, kind, orgId return fmt.Errorf("failed to get API deployments for API handle: %s: %w", apiHandle, err) } if len(gateways) == 0 { - return apperror.GatewayConnectionUnavailable.Wrap(constants.ErrGatewayUnavailable) + return apperror.GatewayConnectionUnavailable.New() } // Resolve key name (required for DB uniqueness; derive from request or generate) @@ -414,7 +414,7 @@ func (s *APIKeyService) UpdateAPIKey(ctx context.Context, apiHandle, kind, orgId } if apiMetadata == nil { s.slogger.Warn("API not found by handle for API key update", "apiHandle", apiHandle) - return apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) + return apperror.ArtifactNotFound.New() } apiId := apiMetadata.ID @@ -426,7 +426,7 @@ func (s *APIKeyService) UpdateAPIKey(ctx context.Context, apiHandle, kind, orgId } if len(gateways) == 0 { s.slogger.Warn("No gateway deployments found for API", "apiHandle", apiHandle) - return apperror.GatewayConnectionUnavailable.Wrap(constants.ErrGatewayUnavailable) + return apperror.GatewayConnectionUnavailable.New() } // Hash the API key with all configured algorithms before storage and broadcast @@ -525,7 +525,7 @@ func (s *APIKeyService) RevokeAPIKey(ctx context.Context, apiHandle, kind, orgId } if apiMetadata == nil { s.slogger.Warn("API not found by handle for API key revocation", "apiHandle", apiHandle) - return apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) + return apperror.ArtifactNotFound.New() } apiId := apiMetadata.ID @@ -535,7 +535,7 @@ func (s *APIKeyService) RevokeAPIKey(ctx context.Context, apiHandle, kind, orgId return fmt.Errorf("failed to get API deployments: %w", err) } if len(gateways) == 0 { - return apperror.GatewayConnectionUnavailable.Wrap(constants.ErrGatewayUnavailable) + return apperror.GatewayConnectionUnavailable.New() } // Fetch UUID before revoke for consistent audit record (CREATE uses UUID, not name) diff --git a/platform-api/internal/service/application.go b/platform-api/internal/service/application.go index 77ed8c806..fad2c83a3 100644 --- a/platform-api/internal/service/application.go +++ b/platform-api/internal/service/application.go @@ -24,6 +24,7 @@ import ( "time" "github.com/wso2/api-platform/platform-api/api" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/repository" @@ -91,7 +92,7 @@ func NewApplicationService( func (s *ApplicationService) CreateApplication(req *api.CreateApplicationRequest, orgID, createdBy string) (*api.Application, error) { if strings.TrimSpace(req.DisplayName) == "" { - return nil, constants.ErrInvalidApplicationName + return nil, apperror.ValidationFailed.New("The displayName field is required.") } appType, err := normalizeApplicationType(string(req.Type)) if err != nil { @@ -103,7 +104,7 @@ func (s *ApplicationService) CreateApplication(req *api.CreateApplicationRequest return nil, err } if org == nil { - return nil, constants.ErrOrganizationNotFound + return nil, apperror.OrganizationNotFound.New() } // project_uuid is optional. When a project handle is supplied it must resolve, and name @@ -116,7 +117,7 @@ func (s *ApplicationService) CreateApplication(req *api.CreateApplicationRequest return nil, err } if project == nil { - return nil, constants.ErrProjectNotFound + return nil, apperror.ProjectNotFound.New() } projectID = project.ID @@ -125,7 +126,7 @@ func (s *ApplicationService) CreateApplication(req *api.CreateApplicationRequest return nil, err } if existingByName != nil { - return nil, constants.ErrApplicationExists + return nil, apperror.ApplicationExists.New() } } @@ -144,7 +145,7 @@ func (s *ApplicationService) CreateApplication(req *api.CreateApplicationRequest return nil, err } if exists { - return nil, constants.ErrHandleExists + return nil, apperror.ApplicationExists.New() } } @@ -176,7 +177,7 @@ func (s *ApplicationService) GetApplicationByID(appIDOrHandle, orgID string) (*a return nil, err } if app == nil { - return nil, constants.ErrApplicationNotFound + return nil, apperror.ApplicationNotFound.New() } return s.modelToApplicationResponse(app) @@ -188,7 +189,7 @@ func (s *ApplicationService) GetApplicationsByOrganization(orgID, projectHandle return nil, err } if org == nil { - return nil, constants.ErrOrganizationNotFound + return nil, apperror.OrganizationNotFound.New() } if opts.Limit <= 0 { @@ -201,7 +202,7 @@ func (s *ApplicationService) GetApplicationsByOrganization(orgID, projectHandle opts.Offset = 0 } if strings.TrimSpace(projectHandle) == "" { - return nil, constants.ErrProjectNotFound + return nil, apperror.ProjectNotFound.New() } project, err := s.projectRepo.GetProjectByHandleAndOrgID(projectHandle, orgID) @@ -209,7 +210,7 @@ func (s *ApplicationService) GetApplicationsByOrganization(orgID, projectHandle return nil, err } if project == nil { - return nil, constants.ErrProjectNotFound + return nil, apperror.ProjectNotFound.New() } totalCount, err := s.appRepo.CountApplicationsByProjectID(project.ID, orgID, opts.Search) @@ -253,7 +254,7 @@ func (s *ApplicationService) UpdateApplication(appIDOrHandle string, req *api.Ap return nil, err } if app == nil { - return nil, constants.ErrApplicationNotFound + return nil, apperror.ApplicationNotFound.New() } // The id (handle) is immutable: body id must be present and match the application being updated. @@ -263,7 +264,7 @@ func (s *ApplicationService) UpdateApplication(appIDOrHandle string, req *api.Ap name := strings.TrimSpace(req.DisplayName) if name == "" { - return nil, constants.ErrInvalidApplicationName + return nil, apperror.ValidationFailed.New("The displayName field is required.") } if name != app.Name { existing, err := s.appRepo.GetApplicationByNameInProject(name, app.ProjectUUID, orgID) @@ -271,7 +272,7 @@ func (s *ApplicationService) UpdateApplication(appIDOrHandle string, req *api.Ap return nil, err } if existing != nil && existing.UUID != app.UUID { - return nil, constants.ErrApplicationExists + return nil, apperror.ApplicationExists.New() } app.Name = name } @@ -313,7 +314,7 @@ func (s *ApplicationService) DeleteApplication(appIDOrHandle, orgID, actor strin return err } if app == nil { - return constants.ErrApplicationNotFound + return apperror.ApplicationNotFound.New() } // Capture the mapped keys before deletion so we can tell the gateways to drop them. Deleting the @@ -374,7 +375,7 @@ func (s *ApplicationService) ListMappedAPIKeysForAssociation(appIDOrHandle, asso return nil, err } if target == nil { - return nil, constants.ErrArtifactNotFound + return nil, apperror.ArtifactNotFound.New() } if err := s.validateAssociationTargetForApplication(target, app, orgID); err != nil { @@ -471,13 +472,13 @@ func (s *ApplicationService) SetAPIKeyApplication(keyName, artifactRef, kind, ap return err } if key == nil { - return constants.ErrAPIKeyNotFound + return apperror.ApplicationAPIKeyNotFound.New() } // Enforce kind scoping: the resolved key must belong to an artifact of the requested kind. A // handle can collide across kinds, so a key found under a same-named handle of another kind is // treated as not found for this event. if kind != "" && key.ArtifactType != kind { - return constants.ErrAPIKeyNotFound + return apperror.ApplicationAPIKeyNotFound.New() } // Capture the applications the key currently belongs to before removing the mapping, so a @@ -595,7 +596,7 @@ func (s *ApplicationService) RemoveApplicationAssociation(appIDOrHandle, associa return err } if target == nil { - return constants.ErrArtifactNotFound + return apperror.ArtifactNotFound.New() } if err := s.validateAssociationTargetForApplication(target, app, orgID); err != nil { @@ -621,7 +622,7 @@ func (s *ApplicationService) getApplication(appIDOrHandle, orgID string) (*model return nil, err } if app == nil { - return nil, constants.ErrApplicationNotFound + return nil, apperror.ApplicationNotFound.New() } return app, nil } @@ -671,7 +672,7 @@ func (s *ApplicationService) resolveAssociationTargets(selectors []ApplicationAs for _, selector := range selectors { targetID := strings.TrimSpace(selector.Id) if targetID == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("Each association selector must specify a non-empty id.") } kind, err := normalizeApplicationAssociationKind(selector.Kind) @@ -684,7 +685,7 @@ func (s *ApplicationService) resolveAssociationTargets(selectors []ApplicationAs return nil, err } if target == nil { - return nil, constants.ErrArtifactNotFound + return nil, apperror.ArtifactNotFound.New() } if err := s.validateAssociationTargetForApplication(target, app, orgID); err != nil { @@ -704,7 +705,7 @@ func (s *ApplicationService) resolveAssociationTargets(selectors []ApplicationAs func normalizeApplicationAssociationKind(kind string) (string, error) { trimmed := strings.TrimSpace(kind) if trimmed == "" { - return "", constants.ErrArtifactInvalidKind + return "", apperror.ValidationFailed.New("Invalid association kind. Only LlmProvider and LlmProxy are supported.") } switch { @@ -713,21 +714,21 @@ func normalizeApplicationAssociationKind(kind string) (string, error) { case strings.EqualFold(trimmed, constants.LLMProxy): return constants.LLMProxy, nil default: - return "", constants.ErrArtifactInvalidKind + return "", apperror.ValidationFailed.New("Invalid association kind. Only LlmProvider and LlmProxy are supported.") } } func (s *ApplicationService) validateAssociationTargetForApplication(target *model.Artifact, app *model.Application, orgID string) error { if target == nil { - return constants.ErrArtifactNotFound + return apperror.ArtifactNotFound.New() } if target.OrganizationUUID != orgID { - return constants.ErrArtifactNotFound + return apperror.ArtifactNotFound.New() } if target.Type != constants.LLMProvider && target.Type != constants.LLMProxy { - return constants.ErrArtifactInvalidKind + return apperror.ValidationFailed.New("Invalid association kind. Only LlmProvider and LlmProxy are supported.") } if target.Type == constants.LLMProxy { @@ -736,10 +737,11 @@ func (s *ApplicationService) validateAssociationTargetForApplication(target *mod return err } if strings.TrimSpace(proxyProjectUUID) == "" { - return constants.ErrArtifactNotFound + return apperror.ArtifactNotFound.New() } if proxyProjectUUID != app.ProjectUUID { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New( + "The LLM proxy belongs to a different project than the application.") } } @@ -751,7 +753,7 @@ func (s *ApplicationService) resolveAPIKey(selector api.APIKeyMappingSelector, o entityID := strings.TrimSpace(selector.AssociatedEntity.Id) if keyID == "" || entityID == "" { - return nil, constants.ErrInvalidAPIKey + return nil, apperror.ValidationFailed.New("The API key id and associated entity id are required.") } key, err := s.appRepo.GetAPIKeyByNameAndArtifactHandle(keyID, entityID, orgID) @@ -759,7 +761,7 @@ func (s *ApplicationService) resolveAPIKey(selector api.APIKeyMappingSelector, o return nil, err } if key == nil { - return nil, constants.ErrAPIKeyNotFound + return nil, apperror.ApplicationAPIKeyNotFound.New() } return key, nil @@ -767,7 +769,7 @@ func (s *ApplicationService) resolveAPIKey(selector api.APIKeyMappingSelector, o func (s *ApplicationService) validateAPIKeyBindingPermission(key *model.ApplicationAPIKey, userID string) error { if key == nil { - return constants.ErrAPIKeyNotFound + return apperror.ApplicationAPIKeyNotFound.New() } creator := strings.TrimSpace(key.CreatedBy) @@ -778,7 +780,7 @@ func (s *ApplicationService) validateAPIKeyBindingPermission(key *model.Applicat } if creator != requester { - return constants.ErrAPIKeyForbidden + return apperror.ApplicationAPIKeyForbidden.New() } return nil @@ -808,7 +810,7 @@ func (s *ApplicationService) buildMappedAPIKeyListForAssociationPaginated(applic } } if !associated { - return nil, constants.ErrArtifactNotFound + return nil, apperror.ArtifactNotFound.New() } for _, key := range keys { @@ -899,7 +901,7 @@ func (s *ApplicationService) modelToApplicationResponse(app *model.Application) return nil, err } if project == nil { - return nil, constants.ErrProjectNotFound + return nil, apperror.ProjectNotFound.New() } projectHandle = project.Handle } @@ -975,7 +977,7 @@ func valueOrEmptyApplication(value *string) string { func normalizeApplicationType(appType string) (string, error) { trimmed := strings.TrimSpace(appType) if trimmed == "" { - return "", constants.ErrInvalidApplicationType + return "", apperror.ValidationFailed.New("The application type is required.") } if strings.EqualFold(trimmed, "genai") { return "genai", nil @@ -983,7 +985,7 @@ func normalizeApplicationType(appType string) (string, error) { if strings.EqualFold(trimmed, "web") { return "web", nil } - return "", constants.ErrUnsupportedApplicationType + return "", apperror.ValidationFailed.New("Invalid application type. Only 'genai' and 'web' are supported.") } func (s *ApplicationService) broadcastApplicationMappingUpdate(app *model.Application, userID string, keys []*model.ApplicationAPIKey) error { diff --git a/platform-api/internal/service/application_test.go b/platform-api/internal/service/application_test.go index f657f96d8..77a0b5dd3 100644 --- a/platform-api/internal/service/application_test.go +++ b/platform-api/internal/service/application_test.go @@ -23,6 +23,7 @@ import ( "time" "github.com/wso2/api-platform/platform-api/api" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/repository" @@ -493,7 +494,7 @@ func TestListMappedAPIKeysForAssociation_ErrorsWhenAssociationMissing(t *testing svc := &ApplicationService{appRepo: appRepo, identity: newTestIdentityService()} _, err := svc.ListMappedAPIKeysForAssociation("my-app", "provider-1", "org-1", 20, 0) - if !errors.Is(err, constants.ErrArtifactNotFound) { + if !apperror.ArtifactNotFound.Is(err) { t.Fatalf("expected ErrArtifactNotFound, got %v", err) } } @@ -568,7 +569,7 @@ func TestAddMappedAPIKeys_RejectsWhenRequesterIsNotCreator(t *testing.T) { Id: "orders-api", }, }}}, "org-1", "different-user") - if !errors.Is(err, constants.ErrAPIKeyForbidden) { + if !apperror.ApplicationAPIKeyForbidden.Is(err) { t.Fatalf("expected ErrAPIKeyForbidden, got %v", err) } if appRepo.addMappedCalled { @@ -726,7 +727,7 @@ func TestAddApplicationAssociations_RejectsCrossProjectProxy(t *testing.T) { Id: "proxy-1", Kind: constants.LLMProxy, }}}, "org-1") - if !errors.Is(err, constants.ErrInvalidInput) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("expected ErrInvalidInput, got %v", err) } } @@ -993,7 +994,7 @@ func TestCreateApplication_RejectsUnsupportedType(t *testing.T) { DisplayName: "Bad Type App", Type: api.ApplicationType("mobile"), }, orgID, "") - if !errors.Is(err, constants.ErrUnsupportedApplicationType) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("expected ErrUnsupportedApplicationType, got %v", err) } } @@ -1009,7 +1010,7 @@ func TestUpdateApplication_RejectsHandleChange(t *testing.T) { resp, err := svc.UpdateApplication("my-app", &api.Application{ Id: "renamed-app", }, orgID, "user-1") - if !errors.Is(err, constants.ErrHandleImmutable) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("expected ErrHandleImmutable, got %v", err) } if resp != nil { @@ -1038,7 +1039,7 @@ func TestCreateApplication_ValidatesProvidedProjectID(t *testing.T) { ProjectId: "some-project-handle", Type: api.ApplicationType("genai"), }, orgID, "") - if !errors.Is(err, constants.ErrProjectNotFound) { + if !apperror.ProjectNotFound.Is(err) { t.Fatalf("expected ErrProjectNotFound, got %v", err) } } diff --git a/platform-api/internal/service/artifact_guard.go b/platform-api/internal/service/artifact_guard.go index 9e00beed1..77f15394a 100644 --- a/platform-api/internal/service/artifact_guard.go +++ b/platform-api/internal/service/artifact_guard.go @@ -21,6 +21,7 @@ import ( "bytes" "fmt" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/repository" @@ -54,7 +55,8 @@ func ensureArtifactMutableByUUID(repo repository.ArtifactRepository, artifactUUI // do not call this guard. func ensureOriginMutable(origin string) error { if origin == constants.OriginDP { - return constants.ErrArtifactReadOnly + return apperror.ArtifactReadOnly.New( + "This artifact is read-only because it originated from a data-plane gateway.") } return nil } @@ -108,7 +110,9 @@ func ensureRuntimeArtifactUnchanged(origin string, existing, proposed []byte) er if bytes.Equal(existing, proposed) { return nil } - return constants.ErrArtifactRuntimeImmutable + return apperror.ArtifactRuntimeImmutable.New( + "The update changes the gateway runtime configuration, which is owned by the data-plane " + + "gateway and cannot be modified from the control plane.") } // ensureOriginDeletable enforces the deletion rule for DP-originated artifacts: they @@ -126,7 +130,8 @@ func ensureOriginDeletable(deploymentRepo repository.DeploymentRepository, origi return err } if active { - return constants.ErrArtifactDeployed + return apperror.ArtifactDeployed.New( + "This artifact is still deployed on a gateway and cannot be deleted.") } return nil } diff --git a/platform-api/internal/service/artifact_import.go b/platform-api/internal/service/artifact_import.go index 46f5945f1..11df4b7d9 100644 --- a/platform-api/internal/service/artifact_import.go +++ b/platform-api/internal/service/artifact_import.go @@ -24,6 +24,7 @@ import ( "time" "github.com/wso2/api-platform/platform-api/config" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/dto" "github.com/wso2/api-platform/platform-api/internal/model" @@ -186,7 +187,7 @@ func (s *ArtifactImportService) ImportArtifacts(orgID, gatewayID string, reqs [] // the importer, and persists deployment/status for deployable kinds. func (s *ArtifactImportService) Import(orgID, gatewayID string, req dto.ImportGatewayArtifactRequest) (*dto.ImportGatewayArtifactResponse, error) { if orgID == "" || gatewayID == "" || req.DPID == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The organization, gateway and artifact id are required.") } // Verify the gateway exists and belongs to the org before importing. if _, err := s.getGatewayForOrg(orgID, gatewayID); err != nil { @@ -204,7 +205,7 @@ func (s *ArtifactImportService) getGatewayForOrg(orgID, gatewayID string) (*mode return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil || gateway.OrganizationID != orgID { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } return gateway, nil } @@ -213,13 +214,13 @@ func (s *ArtifactImportService) getGatewayForOrg(orgID, gatewayID string) (*mode // and validated for the org (so a batch validates the gateway once instead of per artifact). func (s *ArtifactImportService) importValidated(orgID, gatewayID string, req dto.ImportGatewayArtifactRequest) (*dto.ImportGatewayArtifactResponse, error) { if req.DPID == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The artifact id is required.") } kind := req.Configuration.Kind importer, ok := s.importers[kind] if !ok { s.slogger.Warn("Unsupported artifact kind for gateway import", "kind", kind, "artifactId", req.DPID) - return nil, fmt.Errorf("%w: %s", constants.ErrArtifactInvalidKind, kind) + return nil, apperror.ValidationFailed.New(fmt.Sprintf("Unsupported artifact kind %q.", kind)) } // An "undeployed" push means the artifact was deleted from the gateway. The control @@ -251,7 +252,7 @@ func (s *ArtifactImportService) importValidated(orgID, gatewayID string, req dto // project-scoped kinds; a push without one is a contract violation. s.slogger.Error("Project is required for gateway-imported artifact but was not provided", "kind", kind, "artifactId", req.DPID) - return nil, fmt.Errorf("%w: project is required for kind %s", constants.ErrInvalidInput, kind) + return nil, apperror.ValidationFailed.New(fmt.Sprintf("A project is required for artifact kind %q.", kind)) } project, err := s.projectRepo.GetProjectByNameAndOrgID(ictx.ProjectName, orgID) if err != nil { @@ -263,7 +264,8 @@ func (s *ArtifactImportService) importValidated(orgID, gatewayID string, req dto // the artifact is not created in the control plane. s.slogger.Error("Project is not available in the organization; gateway artifact not imported", "kind", kind, "artifactId", req.DPID, "project", ictx.ProjectName, "orgId", orgID) - return nil, fmt.Errorf("%w: project %q does not exist in org %q", constants.ErrProjectNotFound, ictx.ProjectName, orgID) + return nil, apperror.ProjectNotFound.New().WithLogMessage( + fmt.Sprintf("project %q does not exist in org %q", ictx.ProjectName, orgID)) } ictx.ProjectID = project.ID } @@ -284,7 +286,8 @@ func (s *ArtifactImportService) importValidated(orgID, gatewayID string, req dto // Guard against handle reuse across kinds. GetByHandle reports the artifact kind in // the Type field (the artifacts.type column). if existing.Type != kind { - return nil, fmt.Errorf("%w: artifact %q already exists with kind %s", constants.ErrArtifactExists, handle, existing.Type) + return nil, apperror.ArtifactExists.New().WithLogMessage( + fmt.Sprintf("artifact %q already exists with kind %s", handle, existing.Type)) } ictx.ID = existing.UUID ictx.Existing = existing diff --git a/platform-api/internal/service/artifact_import_lifecycle_test.go b/platform-api/internal/service/artifact_import_lifecycle_test.go index 300092774..1a69057a9 100644 --- a/platform-api/internal/service/artifact_import_lifecycle_test.go +++ b/platform-api/internal/service/artifact_import_lifecycle_test.go @@ -24,6 +24,7 @@ import ( "github.com/wso2/api-platform/platform-api/api" "github.com/wso2/api-platform/platform-api/config" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/dto" "github.com/wso2/api-platform/platform-api/internal/model" @@ -481,7 +482,7 @@ func TestImport_LLMProvider_MissingTemplate(t *testing.T) { d := setupImportTest(t) _, err := d.svc.Import(importTestOrgID, importTestGatewayID, dpProviderReq("dp-prov-1", "openai", "OpenAI", "does-not-exist")) - if !errors.Is(err, constants.ErrInvalidInput) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("Import() error = %v, want ErrInvalidInput for a missing template reference", err) } } @@ -591,7 +592,7 @@ func TestImport_LLMProxy_MissingProvider(t *testing.T) { d := setupImportTest(t) _, err := d.svc.Import(importTestOrgID, importTestGatewayID, dpProxyReq("dp-proxy-1", "chat-proxy", "Chat", "no-such-provider")) - if !errors.Is(err, constants.ErrInvalidInput) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("Import() error = %v, want ErrInvalidInput for a missing provider reference", err) } } @@ -898,7 +899,7 @@ func TestCPSideGuard_DPOriginUpdate(t *testing.T) { if _, err := svc.Update(importTestOrgID, "blk-tmpl", "tester", &api.LLMProviderTemplate{ DisplayName: "T", PromptTokens: &api.ExtractionIdentifier{Location: api.ExtractionIdentifierLocation("payload"), Identifier: "$.usage.prompt_tokens"}, - }); !errors.Is(err, constants.ErrArtifactRuntimeImmutable) { + }); !apperror.ArtifactRuntimeImmutable.Is(err) { t.Errorf("Template Update(DP, extraction change) = %v, want ErrArtifactRuntimeImmutable", err) } @@ -986,4 +987,3 @@ func TestCPSideGuard_DPOriginUpdate(t *testing.T) { } }) } - diff --git a/platform-api/internal/service/artifact_import_llm_provider.go b/platform-api/internal/service/artifact_import_llm_provider.go index 4e3199831..ecb18da3a 100644 --- a/platform-api/internal/service/artifact_import_llm_provider.go +++ b/platform-api/internal/service/artifact_import_llm_provider.go @@ -19,6 +19,8 @@ package service import ( "fmt" + + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/dto" "github.com/wso2/api-platform/platform-api/internal/constants" @@ -113,14 +115,14 @@ func (i *llmProviderImporter) Import(ctx *ImportContext) (*ImportResult, error) // to its UUID. The template must already exist (FK requirement). func (i *llmProviderImporter) resolveTemplateUUID(templateHandle, orgID string) (string, error) { if templateHandle == "" { - return "", fmt.Errorf("%w: LLM provider import requires a template reference", constants.ErrInvalidInput) + return "", apperror.ValidationFailed.New("The LLM provider import requires a template reference.") } tmpl, err := i.templateRepo.GetByID(templateHandle, orgID) if err != nil { return "", fmt.Errorf("failed to resolve LLM provider template %q: %w", templateHandle, err) } if tmpl == nil { - return "", fmt.Errorf("%w: referenced LLM provider template %q does not exist", constants.ErrInvalidInput, templateHandle) + return "", apperror.ValidationFailed.New(fmt.Sprintf("The referenced LLM provider template %q does not exist.", templateHandle)) } return tmpl.UUID, nil } diff --git a/platform-api/internal/service/artifact_import_llm_proxy.go b/platform-api/internal/service/artifact_import_llm_proxy.go index e985983a7..436591711 100644 --- a/platform-api/internal/service/artifact_import_llm_proxy.go +++ b/platform-api/internal/service/artifact_import_llm_proxy.go @@ -20,6 +20,7 @@ package service import ( "fmt" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/dto" "github.com/wso2/api-platform/platform-api/internal/model" @@ -117,14 +118,14 @@ func (i *llmProxyImporter) Import(ctx *ImportContext) (*ImportResult, error) { // does not exist, instead of letting a missing reference surface as a raw FK error. func (i *llmProxyImporter) resolveProviderUUID(providerHandle, orgID string) (string, error) { if providerHandle == "" { - return "", fmt.Errorf("%w: LLM proxy import requires a provider reference", constants.ErrInvalidInput) + return "", apperror.ValidationFailed.New("The LLM proxy import requires a provider reference.") } art, err := i.artifactRepo.GetByHandle(providerHandle, orgID) if err != nil { return "", fmt.Errorf("failed to validate referenced LLM provider %q: %w", providerHandle, err) } if art == nil || art.Type != constants.LLMProvider { - return "", fmt.Errorf("%w: referenced LLM provider %q does not exist", constants.ErrInvalidInput, providerHandle) + return "", apperror.ValidationFailed.New(fmt.Sprintf("The referenced LLM provider %q does not exist.", providerHandle)) } return art.UUID, nil } diff --git a/platform-api/internal/service/artifact_import_test.go b/platform-api/internal/service/artifact_import_test.go index dc2916812..780b5f60f 100644 --- a/platform-api/internal/service/artifact_import_test.go +++ b/platform-api/internal/service/artifact_import_test.go @@ -19,7 +19,6 @@ package service import ( "database/sql" - "errors" "io" "log/slog" "os" @@ -28,6 +27,7 @@ import ( "time" "github.com/wso2/api-platform/platform-api/config" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/database" "github.com/wso2/api-platform/platform-api/internal/dto" @@ -211,7 +211,7 @@ func TestArtifactImport_UnsupportedKind(t *testing.T) { req.Configuration.Kind = "Nonexistent" _, err := d.svc.Import(importTestOrgID, importTestGatewayID, req) - if !errors.Is(err, constants.ErrArtifactInvalidKind) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("Import() error = %v, want ErrArtifactInvalidKind", err) } } @@ -222,7 +222,7 @@ func TestArtifactImport_MissingProjectForProjectScopedKind(t *testing.T) { req.Configuration.Metadata.Annotations = nil // REST requires a project _, err := d.svc.Import(importTestOrgID, importTestGatewayID, req) - if !errors.Is(err, constants.ErrInvalidInput) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("Import() error = %v, want ErrInvalidInput", err) } } @@ -235,7 +235,7 @@ func TestArtifactImport_NonexistentProject(t *testing.T) { // Project provided but not present in the org -> the import fails with ErrProjectNotFound // and no artifact is created. - if _, err := d.svc.Import(importTestOrgID, importTestGatewayID, req); !errors.Is(err, constants.ErrProjectNotFound) { + if _, err := d.svc.Import(importTestOrgID, importTestGatewayID, req); !apperror.ProjectNotFound.Is(err) { t.Fatalf("Import() error = %v, want ErrProjectNotFound", err) } art, err := d.artifactRepo.GetByHandle("x", importTestOrgID) @@ -446,7 +446,7 @@ func TestArtifactImport_Enforcement_ReadOnlyAndDeletion(t *testing.T) { } // Update of a DP artifact is blocked. - if err := ensureOriginMutable(api.Origin); !errors.Is(err, constants.ErrArtifactReadOnly) { + if err := ensureOriginMutable(api.Origin); !apperror.ArtifactReadOnly.Is(err) { t.Errorf("ensureOriginMutable = %v, want ErrArtifactReadOnly", err) } @@ -458,7 +458,7 @@ func TestArtifactImport_Enforcement_ReadOnlyAndDeletion(t *testing.T) { if !active { t.Fatal("HasActiveDeployment = false, want true for a deployed artifact") } - if err := ensureOriginDeletable(d.deployment, api.Origin, cpID, importTestOrgID); !errors.Is(err, constants.ErrArtifactDeployed) { + if err := ensureOriginDeletable(d.deployment, api.Origin, cpID, importTestOrgID); !apperror.ArtifactDeployed.Is(err) { t.Errorf("ensureOriginDeletable (deployed) = %v, want ErrArtifactDeployed", err) } @@ -528,7 +528,7 @@ func TestArtifactImport_ProxyMissingProvider(t *testing.T) { } _, err := d.svc.Import(importTestOrgID, importTestGatewayID, req) - if !errors.Is(err, constants.ErrInvalidInput) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("Import() error = %v, want ErrInvalidInput for a missing provider reference", err) } } @@ -627,7 +627,7 @@ func TestArtifactImport_RedeployNoMetadataUpdateOnStalePush(t *testing.T) { func TestArtifactImport_GatewayNotFound(t *testing.T) { d := setupImportTest(t) _, err := d.svc.Import(importTestOrgID, "nonexistent-gateway", restImportRequest("a0000000-0000-0000-0000-000000000000", "x", "X")) - if !errors.Is(err, constants.ErrGatewayNotFound) { + if !apperror.GatewayNotFound.Is(err) { t.Fatalf("Import() error = %v, want ErrGatewayNotFound", err) } } diff --git a/platform-api/internal/service/artifact_runtime_immutable_test.go b/platform-api/internal/service/artifact_runtime_immutable_test.go index 2a51d9561..b6ba610ec 100644 --- a/platform-api/internal/service/artifact_runtime_immutable_test.go +++ b/platform-api/internal/service/artifact_runtime_immutable_test.go @@ -18,9 +18,9 @@ package service import ( - "errors" "testing" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/utils" @@ -42,11 +42,11 @@ func TestEnsureRuntimeArtifactUnchanged(t *testing.T) { t.Errorf("DP origin with identical artifacts: got %v, want nil", err) } err := ensureRuntimeArtifactUnchanged(constants.OriginDP, a, b) - if !errors.Is(err, constants.ErrArtifactRuntimeImmutable) { + if !apperror.ArtifactRuntimeImmutable.Is(err) { t.Errorf("DP origin with differing artifacts: got %v, want ErrArtifactRuntimeImmutable", err) } // It is a distinct sentinel, NOT wrapping the blanket read-only error. - if errors.Is(err, constants.ErrArtifactReadOnly) { + if apperror.ArtifactReadOnly.Is(err) { t.Errorf("ErrArtifactRuntimeImmutable must be distinct from ErrArtifactReadOnly") } } @@ -94,7 +94,7 @@ func TestRESTRuntimeArtifactGuard(t *testing.T) { existing := baseRESTAPI(constants.OriginDP) updated := baseRESTAPI(constants.OriginDP) updated.Configuration.Upstream.Main.URL = "https://new-backend.example.com" - if err := svc.ensureRESTRuntimeArtifactUnchanged(existing, updated); !errors.Is(err, constants.ErrArtifactRuntimeImmutable) { + if err := svc.ensureRESTRuntimeArtifactUnchanged(existing, updated); !apperror.ArtifactRuntimeImmutable.Is(err) { t.Errorf("upstream edit: got %v, want read-only", err) } }) @@ -104,7 +104,7 @@ func TestRESTRuntimeArtifactGuard(t *testing.T) { updated := baseRESTAPI(constants.OriginDP) updated.Configuration.Operations = append(updated.Configuration.Operations, model.Operation{Name: "post", Request: &model.OperationRequest{Method: "POST", Path: "/items"}}) - if err := svc.ensureRESTRuntimeArtifactUnchanged(existing, updated); !errors.Is(err, constants.ErrArtifactRuntimeImmutable) { + if err := svc.ensureRESTRuntimeArtifactUnchanged(existing, updated); !apperror.ArtifactRuntimeImmutable.Is(err) { t.Errorf("operation edit: got %v, want read-only", err) } }) @@ -172,7 +172,7 @@ func TestMCPProxyRuntimeArtifactGuard(t *testing.T) { existingFP := mcpFingerprint(t, existing) existing.Configuration.Context = rtStrPtr("/my-mcp-v2") updatedFP := mcpFingerprint(t, existing) - if err := ensureRuntimeArtifactUnchanged(constants.OriginDP, existingFP, updatedFP); !errors.Is(err, constants.ErrArtifactRuntimeImmutable) { + if err := ensureRuntimeArtifactUnchanged(constants.OriginDP, existingFP, updatedFP); !apperror.ArtifactRuntimeImmutable.Is(err) { t.Errorf("context edit: got %v, want read-only", err) } }) @@ -207,7 +207,7 @@ func TestTemplateRuntimeArtifactGuard(t *testing.T) { existing := baseTemplate(constants.OriginDP) updated := baseTemplate(constants.OriginDP) updated.PromptTokens = &model.ExtractionIdentifier{Location: "header", Identifier: "x-prompt-tokens"} - if err := ensureTemplateRuntimeArtifactUnchanged(existing, updated); !errors.Is(err, constants.ErrArtifactRuntimeImmutable) { + if err := ensureTemplateRuntimeArtifactUnchanged(existing, updated); !apperror.ArtifactRuntimeImmutable.Is(err) { t.Errorf("extraction identifier edit: got %v, want read-only", err) } }) @@ -223,7 +223,7 @@ func TestTemplateRuntimeArtifactGuard(t *testing.T) { }, }}, } - if err := ensureTemplateRuntimeArtifactUnchanged(existing, updated); !errors.Is(err, constants.ErrArtifactRuntimeImmutable) { + if err := ensureTemplateRuntimeArtifactUnchanged(existing, updated); !apperror.ArtifactRuntimeImmutable.Is(err) { t.Errorf("resource mapping edit: got %v, want read-only", err) } }) diff --git a/platform-api/internal/service/custom_policy_test.go b/platform-api/internal/service/custom_policy_test.go index 6baa73706..ac3ceeff7 100644 --- a/platform-api/internal/service/custom_policy_test.go +++ b/platform-api/internal/service/custom_policy_test.go @@ -22,27 +22,29 @@ import ( "errors" "io" "log/slog" - "github.com/wso2/api-platform/platform-api/internal/constants" - "github.com/wso2/api-platform/platform-api/internal/model" - "github.com/wso2/api-platform/platform-api/internal/repository" "strings" "testing" "time" + + "github.com/wso2/api-platform/platform-api/internal/apperror" + "github.com/wso2/api-platform/platform-api/internal/constants" + "github.com/wso2/api-platform/platform-api/internal/model" + "github.com/wso2/api-platform/platform-api/internal/repository" ) // mockGatewayRepoForPolicy mocks only the GatewayRepository methods used by custom-policy operations. type mockGatewayRepoForPolicy struct { repository.GatewayRepository - gateway *model.Gateway - gatewayErr error - manifest []byte - manifestErr error + gateway *model.Gateway + gatewayErr error + manifest []byte + manifestErr error // call tracking - getByUUIDCalled bool - getManifestCalled bool - lastGatewayID string + getByUUIDCalled bool + getManifestCalled bool + lastGatewayID string } func (m *mockGatewayRepoForPolicy) GetByUUID(gatewayID string) (*model.Gateway, error) { @@ -72,10 +74,10 @@ type mockCustomPolicyRepo struct { deleteIfUnusedErr error countUsages int countUsagesErr error - insertCalled bool - updateCalled bool - updateOldVersion string - deleteIfUnusedCalled bool + insertCalled bool + updateCalled bool + updateOldVersion string + deleteIfUnusedCalled bool } func (m *mockCustomPolicyRepo) InsertCustomPolicy(policy *model.CustomPolicy) error { @@ -166,31 +168,31 @@ func newTestGatewayService(gwRepo repository.GatewayRepository, cpRepo repositor func TestSyncCustomPolicy(t *testing.T) { const ( - orgID = "org-uuid-0001" - gwID = "gw-uuid-0001" - otherOrg = "org-uuid-OTHER" + orgID = "org-uuid-0001" + gwID = "gw-uuid-0001" + otherOrg = "org-uuid-OTHER" policyUUID = "pol-uuid-0001" ) tests := []struct { - name string - gateway *model.Gateway - gatewayErr error - manifest []byte - manifestErr error + name string + gateway *model.Gateway + gatewayErr error + manifest []byte + manifestErr error existingPolicies []*model.CustomPolicy existingPoliciesErr error insertErr error updateErr error - persistedPolicy *model.CustomPolicy - policyName string - version string - wantErr bool - errContains string - wantInsert bool - wantUpdate bool + persistedPolicy *model.CustomPolicy + policyName string + version string + wantErr bool + errContains string + wantInsert bool + wantUpdate bool }{ - // gateway validation + // gateway validation { name: "gateway not found - repo error", gatewayErr: errors.New("db error"), @@ -208,11 +210,11 @@ func TestSyncCustomPolicy(t *testing.T) { errContains: "GATEWAY_NOT_FOUND", }, { - name: "gateway belongs to different org", - gateway: &model.Gateway{ID: gwID, OrganizationID: otherOrg}, - policyName: "rate-limit", - version: "1.0.0", - wantErr: true, + name: "gateway belongs to different org", + gateway: &model.Gateway{ID: gwID, OrganizationID: otherOrg}, + policyName: "rate-limit", + version: "1.0.0", + wantErr: true, errContains: "GATEWAY_NOT_FOUND", }, @@ -271,8 +273,8 @@ func TestSyncCustomPolicy(t *testing.T) { // version conflict rules { - name: "exact same version already exists", - gateway: &model.Gateway{ID: gwID, OrganizationID: orgID}, + name: "exact same version already exists", + gateway: &model.Gateway{ID: gwID, OrganizationID: orgID}, manifest: sampleManifest("rate-limit", "1.2.0"), existingPolicies: []*model.CustomPolicy{ makeCustomPolicy(policyUUID, orgID, "rate-limit", "1.2.0"), @@ -283,8 +285,8 @@ func TestSyncCustomPolicy(t *testing.T) { errContains: "POLICY_VERSION_CONFLICT", }, { - name: "patch version update is not allowed", - gateway: &model.Gateway{ID: gwID, OrganizationID: orgID}, + name: "patch version update is not allowed", + gateway: &model.Gateway{ID: gwID, OrganizationID: orgID}, manifest: sampleManifest("rate-limit", "1.2.1"), existingPolicies: []*model.CustomPolicy{ makeCustomPolicy(policyUUID, orgID, "rate-limit", "1.2.0"), @@ -295,8 +297,8 @@ func TestSyncCustomPolicy(t *testing.T) { errContains: "POLICY_VERSION_CONFLICT", }, { - name: "downgrade is not allowed", - gateway: &model.Gateway{ID: gwID, OrganizationID: orgID}, + name: "downgrade is not allowed", + gateway: &model.Gateway{ID: gwID, OrganizationID: orgID}, manifest: sampleManifest("rate-limit", "1.1.0"), existingPolicies: []*model.CustomPolicy{ makeCustomPolicy(policyUUID, orgID, "rate-limit", "1.3.0"), @@ -321,8 +323,8 @@ func TestSyncCustomPolicy(t *testing.T) { wantUpdate: false, }, { - name: "minor version bump - existing record updated", - gateway: &model.Gateway{ID: gwID, OrganizationID: orgID}, + name: "minor version bump - existing record updated", + gateway: &model.Gateway{ID: gwID, OrganizationID: orgID}, manifest: sampleManifest("rate-limit", "1.3.0"), existingPolicies: []*model.CustomPolicy{ makeCustomPolicy(policyUUID, orgID, "rate-limit", "1.2.0"), @@ -335,8 +337,8 @@ func TestSyncCustomPolicy(t *testing.T) { wantUpdate: true, }, { - name: "new major version - separate record inserted", - gateway: &model.Gateway{ID: gwID, OrganizationID: orgID}, + name: "new major version - separate record inserted", + gateway: &model.Gateway{ID: gwID, OrganizationID: orgID}, manifest: sampleManifest("rate-limit", "2.0.0"), existingPolicies: []*model.CustomPolicy{ makeCustomPolicy(policyUUID, orgID, "rate-limit", "1.5.0"), @@ -349,9 +351,9 @@ func TestSyncCustomPolicy(t *testing.T) { wantUpdate: false, }, { - name: "policy name normalised to lowercase before lookup", - gateway: &model.Gateway{ID: gwID, OrganizationID: orgID}, - manifest: sampleManifest("rate-limit", "1.0.0"), + name: "policy name normalised to lowercase before lookup", + gateway: &model.Gateway{ID: gwID, OrganizationID: orgID}, + manifest: sampleManifest("rate-limit", "1.0.0"), existingPolicies: []*model.CustomPolicy{}, persistedPolicy: makeCustomPolicy(policyUUID, orgID, "rate-limit", "1.0.0"), policyName: "Rate-Limit", @@ -360,8 +362,8 @@ func TestSyncCustomPolicy(t *testing.T) { wantInsert: true, }, { - name: "minor version update preserves the existing UUID", - gateway: &model.Gateway{ID: gwID, OrganizationID: orgID}, + name: "minor version update preserves the existing UUID", + gateway: &model.Gateway{ID: gwID, OrganizationID: orgID}, manifest: sampleManifest("auth-policy", "1.2.0"), existingPolicies: []*model.CustomPolicy{ makeCustomPolicy("stable-uuid", orgID, "auth-policy", "1.1.0"), @@ -383,10 +385,10 @@ func TestSyncCustomPolicy(t *testing.T) { manifestErr: tt.manifestErr, } cpRepo := &mockCustomPolicyRepo{ - getPoliciesByName: tt.existingPolicies, - getPoliciesByNameErr: tt.existingPoliciesErr, - insertErr: tt.insertErr, - updateErr: tt.updateErr, + getPoliciesByName: tt.existingPolicies, + getPoliciesByNameErr: tt.existingPoliciesErr, + insertErr: tt.insertErr, + updateErr: tt.updateErr, getPolicyByNameVersion: tt.persistedPolicy, } @@ -470,37 +472,37 @@ func TestGetCustomPolicyByUUID(t *testing.T) { expectedErr error }{ { - name: "policy found with matching version", - repoPolicy: makeCustomPolicy(policyUUID, orgID, "rate-limit", "1.0.0"), - version: "1.0.0", - wantErr: false, + name: "policy found with matching version", + repoPolicy: makeCustomPolicy(policyUUID, orgID, "rate-limit", "1.0.0"), + version: "1.0.0", + wantErr: false, }, { name: "policy not found - nil from repo", repoPolicy: nil, version: "1.0.0", wantErr: true, - expectedErr: constants.ErrCustomPolicyNotFound, + expectedErr: apperror.CustomPolicyNotFound.New(), }, { - name: "repo returns error", - repoErr: errors.New("db failure"), - version: "1.0.0", - wantErr: true, + name: "repo returns error", + repoErr: errors.New("db failure"), + version: "1.0.0", + wantErr: true, }, { name: "version mismatch", repoPolicy: makeCustomPolicy(policyUUID, orgID, "rate-limit", "1.0.0"), version: "1.1.0", wantErr: true, - expectedErr: constants.ErrCustomPolicyVersionMismatch, + expectedErr: apperror.CustomPolicyVersionNotFnd.New(), }, { name: "major version mismatch", repoPolicy: makeCustomPolicy(policyUUID, orgID, "rate-limit", "1.0.0"), version: "2.0.0", wantErr: true, - expectedErr: constants.ErrCustomPolicyVersionMismatch, + expectedErr: apperror.CustomPolicyVersionNotFnd.New(), }, } @@ -561,28 +563,28 @@ func TestDeleteCustomPolicyByUUID(t *testing.T) { repoPolicy: nil, version: "1.0.0", wantErr: true, - expectedErr: constants.ErrCustomPolicyNotFound, + expectedErr: apperror.CustomPolicyNotFound.New(), }, { - name: "repo returns error on lookup", - repoErr: errors.New("db failure"), - version: "1.0.0", - wantErr: true, + name: "repo returns error on lookup", + repoErr: errors.New("db failure"), + version: "1.0.0", + wantErr: true, }, { name: "version mismatch", repoPolicy: makeCustomPolicy(policyUUID, orgID, "rate-limit", "1.0.0"), version: "2.0.0", wantErr: true, - expectedErr: constants.ErrCustomPolicyVersionMismatch, + expectedErr: apperror.CustomPolicyVersionNotFnd.New(), }, { name: "policy in use by APIs", repoPolicy: makeCustomPolicy(policyUUID, orgID, "rate-limit", "1.0.0"), version: "1.0.0", - deleteIfUnusedErr: constants.ErrCustomPolicyInUse, + deleteIfUnusedErr: apperror.PolicyInUse.New(), wantErr: true, - expectedErr: constants.ErrCustomPolicyInUse, + expectedErr: apperror.PolicyInUse.New(), wantDeleteCalled: true, }, } @@ -621,11 +623,11 @@ func TestListCustomPolicies(t *testing.T) { const orgID = "org-uuid-0001" tests := []struct { - name string + name string repoPolicies []*model.CustomPolicy - repoErr error - wantCount int - wantErr bool + repoErr error + wantCount int + wantErr bool }{ { name: "returns all policies for org", diff --git a/platform-api/internal/service/deployment.go b/platform-api/internal/service/deployment.go index e8240f063..e6f1af766 100644 --- a/platform-api/internal/service/deployment.go +++ b/platform-api/internal/service/deployment.go @@ -18,7 +18,6 @@ package service import ( - "errors" "fmt" "log/slog" "net/url" @@ -86,14 +85,14 @@ func NewDeploymentService( func (s *DeploymentService) DeployAPI(apiUUID string, req *api.DeployRequest, orgUUID, createdBy string) (*api.DeploymentResponse, error) { // Validate request if req == nil { - return nil, constants.ErrInvalidInput + return nil, apperror.RESTAPIDeploymentValidationFailed.New("A request body is required.") } if req.Base == "" { - return nil, constants.ErrDeploymentBaseRequired + return nil, apperror.RESTAPIDeploymentValidationFailed.New("Base is required (use 'current' or a deploymentId).") } gatewayHandle := strings.TrimSpace(req.GatewayId) if gatewayHandle == "" { - return nil, constants.ErrDeploymentGatewayIDRequired + return nil, apperror.RESTAPIDeploymentValidationFailed.New("Gateway ID is required.") } metadata := utils.MapValueOrEmpty(req.Metadata) @@ -103,7 +102,7 @@ func (s *DeploymentService) DeployAPI(apiUUID string, req *api.DeployRequest, or return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } gatewayID := gateway.ID @@ -113,7 +112,7 @@ func (s *DeploymentService) DeployAPI(apiUUID string, req *api.DeployRequest, or return nil, err } if apiModel == nil { - return nil, constants.ErrAPINotFound + return nil, apperror.RESTAPINotFound.New() } // DP-originated artifacts are read-only in the control plane and cannot be @@ -124,7 +123,7 @@ func (s *DeploymentService) DeployAPI(apiUUID string, req *api.DeployRequest, or // Validate deployment name is provided if req.Name == "" { - return nil, constants.ErrDeploymentNameRequired + return nil, apperror.RESTAPIDeploymentValidationFailed.New("Deployment name is required.") } var baseDeploymentID *string @@ -137,8 +136,8 @@ func (s *DeploymentService) DeployAPI(apiUUID string, req *api.DeployRequest, or var err error baseDeployment, err = s.deploymentRepo.GetWithContent(req.Base, apiUUID, orgUUID) if err != nil { - if errors.Is(err, constants.ErrDeploymentNotFound) { - return nil, constants.ErrBaseDeploymentNotFound + if apperror.DeploymentNotFound.Is(err) { + return nil, apperror.DeploymentBaseNotFound.Wrap(err) } return nil, fmt.Errorf("failed to get base deployment: %w", err) } @@ -372,12 +371,12 @@ func (s *DeploymentService) RestoreDeployment(apiUUID, deploymentID, gatewayID, return nil, err } if targetDeployment == nil { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } // Validate that the provided gatewayID matches the deployment's bound gateway if targetDeployment.GatewayID != gatewayID { - return nil, constants.ErrGatewayIDMismatch + return nil, apperror.DeploymentGatewayMismatch.New() } // Verify target deployment is NOT currently DEPLOYED @@ -386,7 +385,7 @@ func (s *DeploymentService) RestoreDeployment(apiUUID, deploymentID, gatewayID, return nil, fmt.Errorf("failed to get deployment status: %w", err) } if currentDeploymentID == deploymentID && status == model.DeploymentStatusDeployed { - return nil, constants.ErrDeploymentAlreadyDeployed + return nil, apperror.DeploymentRestoreConflict.New() } // Validate gateway exists and belongs to organization @@ -395,7 +394,7 @@ func (s *DeploymentService) RestoreDeployment(apiUUID, deploymentID, gatewayID, return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil || gateway.OrganizationID != orgUUID { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } // Set initial status based on config; transitional (DEPLOYING) only when enabled @@ -459,17 +458,17 @@ func (s *DeploymentService) UndeployDeployment(apiUUID, deploymentID, gatewayID, return nil, err } if deployment == nil { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } // Validate that the provided gatewayID matches the deployment's bound gateway if deployment.GatewayID != gatewayID { - return nil, constants.ErrGatewayIDMismatch + return nil, apperror.DeploymentGatewayMismatch.New() } // Verify deployment is currently DEPLOYED (status already populated by GetDeploymentWithState) if deployment.Status == nil || *deployment.Status != model.DeploymentStatusDeployed { - return nil, constants.ErrDeploymentNotActive + return nil, apperror.DeploymentNotActive.New("API") } // Validate gateway exists and belongs to organization @@ -478,7 +477,7 @@ func (s *DeploymentService) UndeployDeployment(apiUUID, deploymentID, gatewayID, return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } // Set initial status based on config; transitional (UNDEPLOYING) only when enabled @@ -535,12 +534,12 @@ func (s *DeploymentService) DeleteDeployment(apiUUID, deploymentID, orgUUID, act return err } if deployment == nil { - return constants.ErrDeploymentNotFound + return apperror.DeploymentNotFound.New() } // Verify deployment is NOT currently DEPLOYED (status already populated by GetDeploymentWithState) if deployment.Status != nil && *deployment.Status == model.DeploymentStatusDeployed { - return constants.ErrDeploymentIsDeployed + return apperror.DeploymentActive.New() } // Delete the deployment artifact @@ -748,7 +747,7 @@ func (s *DeploymentService) GetDeployments(apiUUID, orgUUID string, gatewayID *s return nil, err } if apiModel == nil { - return nil, constants.ErrAPINotFound + return nil, apperror.RESTAPINotFound.New() } // Validate status parameter @@ -762,7 +761,7 @@ func (s *DeploymentService) GetDeployments(apiUUID, orgUUID string, gatewayID *s string(model.DeploymentStatusArchived): true, } if !validStatuses[*status] { - return nil, constants.ErrInvalidDeploymentStatus + return nil, apperror.DeploymentInvalidStatus.New() } } @@ -809,7 +808,7 @@ func (s *DeploymentService) GetDeployment(apiUUID, deploymentID, orgUUID string) return nil, err } if apiModel == nil { - return nil, constants.ErrAPINotFound + return nil, apperror.RESTAPINotFound.New() } // Get deployment with state derived via LEFT JOIN @@ -818,7 +817,7 @@ func (s *DeploymentService) GetDeployment(apiUUID, deploymentID, orgUUID string) return nil, err } if deployment == nil { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } return toAPIDeploymentResponse( @@ -843,7 +842,7 @@ func (s *DeploymentService) GetDeploymentContent(apiUUID, deploymentID, orgUUID return nil, err } if deployment == nil { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } return deployment.Content, nil @@ -901,7 +900,7 @@ func (s *DeploymentService) RestoreDeploymentByHandle(apiHandle, deploymentID, g return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } return s.RestoreDeployment(apiUUID, deploymentID, gateway.ID, orgUUID, actor) @@ -918,7 +917,7 @@ func (s *DeploymentService) getUUIDByHandle(handle, orgUUID string) (string, err return "", err } if artifact == nil { - return "", constants.ErrArtifactNotFound + return "", apperror.ArtifactNotFound.New() } return artifact.UUID, nil @@ -983,7 +982,7 @@ func (s *DeploymentService) UndeployDeploymentByHandle(apiHandle, deploymentID, return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } return s.UndeployDeployment(apiUUID, deploymentID, gateway.ID, orgUUID, actor) diff --git a/platform-api/internal/service/deployment_test.go b/platform-api/internal/service/deployment_test.go index 6cd55f0a1..d105d98d5 100644 --- a/platform-api/internal/service/deployment_test.go +++ b/platform-api/internal/service/deployment_test.go @@ -25,7 +25,7 @@ import ( "time" "github.com/wso2/api-platform/platform-api/config" - "github.com/wso2/api-platform/platform-api/internal/constants" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/dto" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/repository" @@ -434,7 +434,7 @@ func TestRestoreDeployment(t *testing.T) { mockDeployment: nil, mockDeploymentError: nil, wantErr: true, - expectedErr: constants.ErrDeploymentNotFound, + expectedErr: apperror.DeploymentNotFound.New(), }, { name: "deployment fetch error", @@ -458,7 +458,7 @@ func TestRestoreDeployment(t *testing.T) { mockCurrentDeployment: testDeploymentID, // Same as target mockCurrentStatus: model.DeploymentStatusDeployed, wantErr: true, - expectedErr: constants.ErrDeploymentAlreadyDeployed, + expectedErr: apperror.DeploymentRestoreConflict.New(), }, { name: "deployment status fetch error", @@ -490,7 +490,7 @@ func TestRestoreDeployment(t *testing.T) { mockCurrentStatus: model.DeploymentStatusUndeployed, mockGateway: nil, wantErr: true, - expectedErr: constants.ErrGatewayNotFound, + expectedErr: apperror.GatewayNotFound.New(), }, { name: "gateway organization mismatch", @@ -511,7 +511,7 @@ func TestRestoreDeployment(t *testing.T) { Endpoints: []string{"https://api.example.com"}, }, wantErr: true, - expectedErr: constants.ErrGatewayNotFound, + expectedErr: apperror.GatewayNotFound.New(), }, { name: "set current deployment fails", @@ -669,7 +669,7 @@ func TestUndeployDeployment(t *testing.T) { mockDeployment: nil, mockDeploymentError: nil, wantErr: true, - expectedErr: constants.ErrDeploymentNotFound, + expectedErr: apperror.DeploymentNotFound.New(), }, { name: "deployment not active (UNDEPLOYED)", @@ -683,7 +683,7 @@ func TestUndeployDeployment(t *testing.T) { Status: &undeployedStatus, }, wantErr: true, - expectedErr: constants.ErrDeploymentNotActive, + expectedErr: apperror.DeploymentNotActive.New("API"), }, { name: "deployment not active (nil status - ARCHIVED)", @@ -697,7 +697,7 @@ func TestUndeployDeployment(t *testing.T) { Status: nil, // ARCHIVED }, wantErr: true, - expectedErr: constants.ErrDeploymentNotActive, + expectedErr: apperror.DeploymentNotActive.New("API"), }, { name: "gateway not found", @@ -712,7 +712,7 @@ func TestUndeployDeployment(t *testing.T) { }, mockGateway: nil, wantErr: true, - expectedErr: constants.ErrGatewayNotFound, + expectedErr: apperror.GatewayNotFound.New(), }, { name: "set current deployment fails", @@ -746,7 +746,7 @@ func TestUndeployDeployment(t *testing.T) { Status: &deployedStatus, }, wantErr: true, - expectedErr: constants.ErrGatewayIDMismatch, + expectedErr: apperror.DeploymentGatewayMismatch.New(), }, } @@ -872,7 +872,7 @@ func TestDeleteDeployment(t *testing.T) { mockDeployment: nil, mockDeploymentError: nil, wantErr: true, - expectedErr: constants.ErrDeploymentNotFound, + expectedErr: apperror.DeploymentNotFound.New(), }, { name: "cannot delete DEPLOYED deployment", @@ -885,7 +885,7 @@ func TestDeleteDeployment(t *testing.T) { Status: &deployedStatus, }, wantErr: true, - expectedErr: constants.ErrDeploymentIsDeployed, + expectedErr: apperror.DeploymentActive.New(), }, { name: "delete operation fails", @@ -1029,7 +1029,7 @@ func TestGetDeployments(t *testing.T) { mockAPI: nil, mockAPIError: nil, wantErr: true, - expectedErr: constants.ErrAPINotFound, + expectedErr: apperror.RESTAPINotFound.New(), }, { name: "invalid status parameter", @@ -1040,7 +1040,7 @@ func TestGetDeployments(t *testing.T) { OrganizationID: testOrgUUID, }, wantErr: true, - expectedErr: constants.ErrInvalidDeploymentStatus, + expectedErr: apperror.DeploymentInvalidStatus.New(), }, } @@ -1126,7 +1126,7 @@ func TestGetDeployment(t *testing.T) { mockAPI: nil, mockAPIError: nil, wantErr: true, - expectedErr: constants.ErrAPINotFound, + expectedErr: apperror.RESTAPINotFound.New(), }, { name: "deployment not found", @@ -1136,7 +1136,7 @@ func TestGetDeployment(t *testing.T) { }, mockDeployment: nil, wantErr: true, - expectedErr: constants.ErrDeploymentNotFound, + expectedErr: apperror.DeploymentNotFound.New(), }, } @@ -1638,7 +1638,7 @@ func TestRollbackDeployment_CurrentlyDeployedSameID(t *testing.T) { t.Fatal("Expected error when restoring to currently DEPLOYED deployment") } - if !errors.Is(err, constants.ErrDeploymentAlreadyDeployed) { + if !apperror.DeploymentRestoreConflict.Is(err) { t.Errorf("Expected ErrDeploymentAlreadyDeployed, got %v", err) } @@ -1676,7 +1676,7 @@ func TestDeleteDeployment_CannotDeleteDeployed(t *testing.T) { t.Fatal("Expected error when deleting DEPLOYED deployment") } - if !errors.Is(err, constants.ErrDeploymentIsDeployed) { + if !apperror.DeploymentActive.Is(err) { t.Errorf("Expected ErrDeploymentIsDeployed, got %v", err) } diff --git a/platform-api/internal/service/deployment_undeploy_guard_test.go b/platform-api/internal/service/deployment_undeploy_guard_test.go index 7ef69088d..ecdc647db 100644 --- a/platform-api/internal/service/deployment_undeploy_guard_test.go +++ b/platform-api/internal/service/deployment_undeploy_guard_test.go @@ -19,9 +19,9 @@ package service import ( "database/sql" - "errors" "testing" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/model" ) @@ -61,8 +61,8 @@ func TestUndeployDeployment_BlockedForDPOrigin(t *testing.T) { } _, err := svc.UndeployDeployment("artifact-1", "deployment-1", "gateway-1", "org-1", "tester") - if !errors.Is(err, constants.ErrArtifactReadOnly) { - t.Fatalf("UndeployDeployment(DP origin) error = %v, want ErrArtifactReadOnly", err) + if !apperror.ArtifactReadOnly.Is(err) { + t.Fatalf("UndeployDeployment(DP origin) error = %v, want ARTIFACT_READ_ONLY", err) } } @@ -76,7 +76,7 @@ func TestRestoreDeployment_BlockedForDPOrigin(t *testing.T) { } _, err := svc.RestoreDeployment("artifact-1", "deployment-1", "gateway-1", "org-1", "tester") - if !errors.Is(err, constants.ErrArtifactReadOnly) { - t.Fatalf("RestoreDeployment(DP origin) error = %v, want ErrArtifactReadOnly", err) + if !apperror.ArtifactReadOnly.Is(err) { + t.Fatalf("RestoreDeployment(DP origin) error = %v, want ARTIFACT_READ_ONLY", err) } } diff --git a/platform-api/internal/service/gateway.go b/platform-api/internal/service/gateway.go index 64db2fa49..0df1c8585 100644 --- a/platform-api/internal/service/gateway.go +++ b/platform-api/internal/service/gateway.go @@ -24,17 +24,18 @@ import ( "encoding/hex" "encoding/json" "fmt" + "log/slog" + "regexp" + "strconv" + "strings" + "time" + "github.com/wso2/api-platform/platform-api/api" "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/repository" "github.com/wso2/api-platform/platform-api/internal/utils" - "log/slog" - "regexp" - "strconv" - "strings" - "time" "github.com/google/uuid" ) @@ -109,10 +110,10 @@ func (s *GatewayService) GetStoredManifest(gatewayID, orgID string) (*Manifest, return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } if gateway.OrganizationID != orgID { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } raw, err := s.gatewayRepo.GetGatewayManifest(gatewayID) @@ -192,7 +193,7 @@ func (s *GatewayService) ReceiveGatewayManifest(orgID, gatewayID, gatewayVersion return fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil || gateway.OrganizationID != orgID { - return constants.ErrGatewayNotFound + return apperror.GatewayNotFound.New() } reported := strings.TrimSpace(gatewayVersion) @@ -206,7 +207,9 @@ func (s *GatewayService) ReceiveGatewayManifest(orgID, gatewayID, gatewayVersion } if reportedMinor != registeredMinor { if s.enableVersionVerification { - return fmt.Errorf("%w: registered=%s, reported=%s", constants.ErrGatewayVersionMismatch, registeredMinor, reportedMinor) + return apperror.Conflict.New(). + WithDetails(map[string]string{"registered": registeredMinor, "reported": reportedMinor}). + WithLogMessage(fmt.Sprintf("gateway version mismatch: registered=%s reported=%s", registeredMinor, reportedMinor)) } s.slogger.Warn("Gateway version mismatch ignored (verification disabled)", slog.String("org_id", orgID), @@ -222,7 +225,9 @@ func (s *GatewayService) ReceiveGatewayManifest(orgID, gatewayID, gatewayVersion } if !functionalityTypeCompatible(gateway.FunctionalityType, reportedType) { if s.enableFunctionalityTypeVerification { - return fmt.Errorf("%w: registered=%s, reported=%s", constants.ErrGatewayFunctionalityTypeMismatch, gateway.FunctionalityType, reportedType) + return apperror.Conflict.New(). + WithDetails(map[string]string{"registered": gateway.FunctionalityType, "reported": reportedType}). + WithLogMessage(fmt.Sprintf("gateway functionality type mismatch: registered=%s reported=%s", gateway.FunctionalityType, reportedType)) } s.slogger.Warn("Gateway functionality type mismatch ignored (verification disabled)", slog.String("org_id", orgID), @@ -503,10 +508,10 @@ func (s *GatewayService) GetCustomPolicyByUUID(orgID, policyUUID, version string return nil, fmt.Errorf("failed to retrieve custom policy (org_id=%s, policy_uuid=%s): %w", orgID, policyUUID, err) } if policy == nil { - return nil, apperror.CustomPolicyNotFound.Wrap(constants.ErrCustomPolicyNotFound) + return nil, apperror.CustomPolicyNotFound.New() } if policy.Version != version { - return nil, apperror.CustomPolicyVersionNotFnd.Wrap(constants.ErrCustomPolicyVersionMismatch) + return nil, apperror.CustomPolicyVersionNotFnd.New() } return policy, nil } @@ -518,10 +523,10 @@ func (s *GatewayService) DeleteCustomPolicyByUUID(orgID, policyUUID, version str return fmt.Errorf("failed to retrieve custom policy (org_id=%s, policy_uuid=%s): %w", orgID, policyUUID, err) } if policy == nil { - return apperror.CustomPolicyNotFound.Wrap(constants.ErrCustomPolicyNotFound) + return apperror.CustomPolicyNotFound.New() } if policy.Version != version { - return apperror.CustomPolicyVersionNotFnd.Wrap(constants.ErrCustomPolicyVersionMismatch) + return apperror.CustomPolicyVersionNotFnd.New() } if err := s.customPolicyRepo.DeleteCustomPolicyIfUnused(orgID, policyUUID); err != nil { @@ -701,7 +706,7 @@ func (s *GatewayService) UpdateGateway(gatewayId, orgId, updatedBy string, req * return nil, err } if gateway == nil { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } gateway.Name = req.DisplayName @@ -740,7 +745,7 @@ func (s *GatewayService) DeleteGateway(gatewayID, orgID, deletedBy string) error return err } if gateway == nil { - return constants.ErrGatewayNotFound + return apperror.GatewayNotFound.New() } // Delete gateway by UUID (FK CASCADE will automatically remove tokens and deployments; association_mappings cleanup is handled by the repository) @@ -759,7 +764,7 @@ func (s *GatewayService) DeleteGateway(gatewayID, orgID, deletedBy string) error // VerifyToken verifies a plain-text token and returns the associated gateway func (s *GatewayService) VerifyToken(plainToken string) (*model.Gateway, error) { if plainToken == "" { - return nil, constants.ErrMissingAPIKey + return nil, apperror.Unauthorized.New().WithLogMessage("gateway token missing from request") } // Hash the token and look it up directly in the database @@ -769,7 +774,7 @@ func (s *GatewayService) VerifyToken(plainToken string) (*model.Gateway, error) return nil, fmt.Errorf("failed to query token: %w", err) } if token == nil { - return nil, constants.ErrInvalidAPIToken + return nil, apperror.Unauthorized.New().WithLogMessage("no active gateway token matches the presented token hash") } // Fetch the associated gateway @@ -778,7 +783,8 @@ func (s *GatewayService) VerifyToken(plainToken string) (*model.Gateway, error) return nil, fmt.Errorf("failed to query gateway: %w", err) } if gateway == nil { - return nil, constants.ErrInvalidAPIToken + return nil, apperror.Unauthorized.New(). + WithLogMessage(fmt.Sprintf("gateway %s referenced by an active token no longer exists", token.GatewayID)) } return gateway, nil diff --git a/platform-api/internal/service/gateway_internal.go b/platform-api/internal/service/gateway_internal.go index 67c0d5406..c7d55444b 100644 --- a/platform-api/internal/service/gateway_internal.go +++ b/platform-api/internal/service/gateway_internal.go @@ -19,16 +19,16 @@ package service import ( "encoding/json" - "errors" "fmt" "log/slog" + "time" + "github.com/wso2/api-platform/platform-api/config" - "github.com/wso2/api-platform/platform-api/internal/constants" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/dto" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/repository" "github.com/wso2/api-platform/platform-api/internal/utils" - "time" ) // GatewayInternalAPIService handles internal gateway API operations @@ -129,10 +129,10 @@ func (s *GatewayInternalAPIService) GetAPIByUUID(apiId, orgId string) (map[strin return nil, fmt.Errorf("failed to get api: %w", err) } if apiModel == nil { - return nil, constants.ErrAPINotFound + return nil, apperror.RESTAPINotFound.New() } if apiModel.OrganizationID != orgId { - return nil, constants.ErrAPINotFound + return nil, apperror.RESTAPINotFound.New() } apiYaml, err := s.apiUtil.GenerateAPIDeploymentYAML(apiModel) @@ -153,7 +153,7 @@ func (s *GatewayInternalAPIService) GetActiveDeploymentByGateway(apiID, orgID, g return nil, fmt.Errorf("failed to get deployment: %w", err) } if deployment == nil { - return nil, constants.ErrDeploymentNotActive + return nil, apperror.DeploymentNotActive.New("API") } // Deployment content is already stored as YAML, so return it directly @@ -173,7 +173,7 @@ func (s *GatewayInternalAPIService) GetActiveLLMProviderDeploymentByGateway(prov return nil, fmt.Errorf("failed to get deployment: %w", err) } if deployment == nil { - return nil, constants.ErrDeploymentNotActive + return nil, apperror.DeploymentNotActive.New("LLM provider") } providerYaml := string(deployment.Content) @@ -191,7 +191,7 @@ func (s *GatewayInternalAPIService) GetActiveLLMProxyDeploymentByGateway(proxyID return nil, fmt.Errorf("failed to get deployment: %w", err) } if deployment == nil { - return nil, constants.ErrDeploymentNotActive + return nil, apperror.DeploymentNotActive.New("LLM proxy") } proxyYaml := string(deployment.Content) @@ -202,18 +202,18 @@ func (s *GatewayInternalAPIService) GetActiveLLMProxyDeploymentByGateway(proxyID } // IsAPIDeployedOnGateway returns nil if the API has an active deployment_status row on the gateway -// (DEPLOYED or UNDEPLOYED), ErrAPINotFound if the API does not exist, or ErrDeploymentNotActive +// (DEPLOYED or UNDEPLOYED), REST_API_NOT_FOUND if the API does not exist, or DEPLOYMENT_NOT_ACTIVE // if no active deployment_status exists for the API+gateway. func (s *GatewayInternalAPIService) IsAPIDeployedOnGateway(apiID, gatewayID, orgID string) error { api, err := s.apiRepo.GetAPIByUUID(apiID, orgID) if err != nil { - if errors.Is(err, constants.ErrAPINotFound) { + if apperror.RESTAPINotFound.Is(err) { return err } return fmt.Errorf("failed to get api: %w", err) } if api == nil || api.OrganizationID != orgID { - return constants.ErrAPINotFound + return apperror.RESTAPINotFound.New() } deploymentID, status, _, err := s.deploymentRepo.GetStatus(apiID, orgID, gatewayID) @@ -221,10 +221,10 @@ func (s *GatewayInternalAPIService) IsAPIDeployedOnGateway(apiID, gatewayID, org return fmt.Errorf("failed to get deployment status: %w", err) } if deploymentID == "" { - return constants.ErrDeploymentNotActive + return apperror.DeploymentNotActive.New("API") } if status != model.DeploymentStatusDeployed && status != model.DeploymentStatusUndeployed { - return constants.ErrDeploymentNotActive + return apperror.DeploymentNotActive.New("API") } return nil } @@ -232,7 +232,7 @@ func (s *GatewayInternalAPIService) IsAPIDeployedOnGateway(apiID, gatewayID, org // ListSubscriptionsForAPI lists subscriptions for a given API within an organization. func (s *GatewayInternalAPIService) ListSubscriptionsForAPI(apiID, orgID string) ([]dto.GatewaySubscriptionInfo, error) { if apiID == "" || orgID == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The API id and organization id are required.") } // apiID here is the API UUID (rest_apis.uuid) used as deployment id for REST APIs. // First, ensure the API exists and belongs to the organization so callers can map @@ -241,13 +241,13 @@ func (s *GatewayInternalAPIService) ListSubscriptionsForAPI(apiID, orgID string) if err != nil { // Preserve explicit not-found signaling from the repository so callers // can translate it to a 404, while still wrapping unexpected failures. - if errors.Is(err, constants.ErrAPINotFound) { + if apperror.RESTAPINotFound.Is(err) { return nil, err } return nil, fmt.Errorf("failed to get api for listing subscriptions: %w", err) } if apiModel == nil || apiModel.OrganizationID != orgID { - return nil, constants.ErrAPINotFound + return nil, apperror.RESTAPINotFound.New() } // Internal sync: fetch all subscriptions for the API via pagination so reconciliation @@ -285,7 +285,7 @@ func (s *GatewayInternalAPIService) ListSubscriptionsForAPI(apiID, orgID string) // ListSubscriptionPlansForOrg lists all subscription plans for an organization. func (s *GatewayInternalAPIService) ListSubscriptionPlansForOrg(orgID string) ([]dto.GatewaySubscriptionPlanInfo, error) { if orgID == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The organization id is required.") } // Internal sync: fetch all plans for the organization via pagination so reconciliation // never performs destructive deletes due to a cutoff. @@ -328,7 +328,7 @@ func (s *GatewayInternalAPIService) GetActiveMCPProxyDeploymentByGateway(proxyID return nil, fmt.Errorf("failed to get MCP proxy: %w", err) } if proxy == nil { - return nil, constants.ErrMCPProxyNotFound + return nil, apperror.MCPProxyNotFound.New() } deployment, err := s.deploymentRepo.GetCurrentByGateway(proxy.UUID, gatewayID, orgID) @@ -336,7 +336,7 @@ func (s *GatewayInternalAPIService) GetActiveMCPProxyDeploymentByGateway(proxyID return nil, fmt.Errorf("failed to get deployment: %w", err) } if deployment == nil { - return nil, constants.ErrDeploymentNotActive + return nil, apperror.DeploymentNotActive.New("MCP proxy") } proxyYaml := string(deployment.Content) @@ -349,14 +349,15 @@ func (s *GatewayInternalAPIService) GetActiveMCPProxyDeploymentByGateway(proxyID // GetActiveWebSubAPIDeploymentByGateway retrieves the currently deployed WebSub API artifact for a specific gateway func (s *GatewayInternalAPIService) GetActiveWebSubAPIDeploymentByGateway(apiID, orgID, gatewayID string) (map[string]string, error) { if s.websubAPIRepo == nil { - return nil, constants.ErrWebSubAPINotFound + return nil, apperror.WebSubAPINotFound.New(). + WithLogMessage("WebSub API repository unavailable: the event-gateway plugin is not enabled") } websubAPI, err := s.websubAPIRepo.GetByUUID(apiID, orgID) if err != nil { return nil, fmt.Errorf("failed to get WebSub API: %w", err) } if websubAPI == nil { - return nil, constants.ErrWebSubAPINotFound + return nil, apperror.WebSubAPINotFound.New() } deployment, err := s.deploymentRepo.GetCurrentByGateway(websubAPI.UUID, gatewayID, orgID) @@ -364,7 +365,7 @@ func (s *GatewayInternalAPIService) GetActiveWebSubAPIDeploymentByGateway(apiID, return nil, fmt.Errorf("failed to get deployment: %w", err) } if deployment == nil { - return nil, constants.ErrDeploymentNotActive + return nil, apperror.DeploymentNotActive.New("WebSub API") } apiYaml := string(deployment.Content) @@ -377,14 +378,15 @@ func (s *GatewayInternalAPIService) GetActiveWebSubAPIDeploymentByGateway(apiID, // GetActiveWebBrokerAPIDeploymentByGateway retrieves the currently deployed WebBroker API artifact for a specific gateway func (s *GatewayInternalAPIService) GetActiveWebBrokerAPIDeploymentByGateway(apiID, orgID, gatewayID string) (map[string]string, error) { if s.webbrokerAPIRepo == nil { - return nil, constants.ErrWebBrokerAPINotFound + return nil, apperror.WebBrokerAPINotFound.New(). + WithLogMessage("WebBroker API repository unavailable: the event-gateway plugin is not enabled") } webbrokerAPI, err := s.webbrokerAPIRepo.GetByUUID(apiID, orgID) if err != nil { return nil, fmt.Errorf("failed to get WebBroker API: %w", err) } if webbrokerAPI == nil { - return nil, constants.ErrWebBrokerAPINotFound + return nil, apperror.WebBrokerAPINotFound.New() } deployment, err := s.deploymentRepo.GetCurrentByGateway(webbrokerAPI.UUID, gatewayID, orgID) @@ -392,7 +394,7 @@ func (s *GatewayInternalAPIService) GetActiveWebBrokerAPIDeploymentByGateway(api return nil, fmt.Errorf("failed to get deployment: %w", err) } if deployment == nil { - return nil, constants.ErrDeploymentNotActive + return nil, apperror.DeploymentNotActive.New("WebBroker API") } apiYaml := string(deployment.Content) diff --git a/platform-api/internal/service/llm.go b/platform-api/internal/service/llm.go index 691dab392..2b1306ef1 100644 --- a/platform-api/internal/service/llm.go +++ b/platform-api/internal/service/llm.go @@ -200,33 +200,33 @@ func (s *LLMProxyService) toProxyAPI(m *model.LLMProxy) (*api.LLMProxy, error) { func (s *LLMProviderTemplateService) Create(orgUUID, createdBy string, req *api.LLMProviderTemplate) (*api.LLMProviderTemplate, error) { if req == nil { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("A request body is required.") } if req.DisplayName == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The displayName field is required.") } if req.Metadata == nil { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The metadata field is required.") } if err := utils.ValidateURL(strings.TrimSpace(utils.ValueOrEmpty(req.Metadata.EndpointUrl))); err != nil { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The metadata endpointUrl must be a valid URL.") } baseHandle, err := utils.GenerateHandle(req.DisplayName, nil) if err != nil || baseHandle == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The displayName must contain at least one alphanumeric character.") } version := "v1.0" if v := req.Version; v != "" { normalized, ok := normalizeTemplateVersion(v) if !ok || normalized != version { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The version must match the v. pattern (e.g. v1.0).") } } handle := makeTemplateHandle(baseHandle, version) if req.ManagedBy != nil && strings.TrimSpace(*req.ManagedBy) == constants.PolicyManagedByWSO2 { - return nil, apperror.LLMProviderTemplateManagedByReserved.Wrap(constants.ErrLLMProviderTemplateManagedByReserved) + return nil, apperror.LLMProviderTemplateManagedByReserved.New() } exists, err := s.repo.Exists(handle, orgUUID) @@ -234,7 +234,7 @@ func (s *LLMProviderTemplateService) Create(orgUUID, createdBy string, req *api. return nil, fmt.Errorf("failed to check template exists: %w", err) } if exists { - return nil, apperror.LLMProviderTemplateExists.Wrap(constants.ErrLLMProviderTemplateExists) + return nil, apperror.LLMProviderTemplateExists.New() } m := &model.LLMProviderTemplate{ @@ -264,7 +264,7 @@ func (s *LLMProviderTemplateService) Create(orgUUID, createdBy string, req *api. if err := s.repo.Create(m); err != nil { if isSQLiteUniqueConstraint(err) { - return nil, apperror.LLMProviderTemplateExists.Wrap(constants.ErrLLMProviderTemplateExists) + return nil, apperror.LLMProviderTemplateExists.New() } return nil, fmt.Errorf("failed to create template: %w", err) } @@ -310,27 +310,27 @@ func (s *LLMProviderTemplateService) List(orgUUID string, limit, offset int, lat func (s *LLMProviderTemplateService) Get(orgUUID, handle string) (*api.LLMProviderTemplate, error) { if handle == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The template id is required.") } m, err := s.repo.GetByID(handle, orgUUID) if err != nil { return nil, fmt.Errorf("failed to get template: %w", err) } if m == nil { - return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) + return nil, apperror.LLMProviderTemplateNotFound.New() } return s.toTemplateAPI(m) } func (s *LLMProviderTemplateService) Update(orgUUID, handle, updatedBy string, req *api.LLMProviderTemplate) (*api.LLMProviderTemplate, error) { if handle == "" || req == nil { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The template id and a request body are required.") } if req.DisplayName == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The displayName field is required.") } if req.ManagedBy != nil && strings.TrimSpace(*req.ManagedBy) == constants.PolicyManagedByWSO2 { - return nil, apperror.LLMProviderTemplateManagedByReserved.Wrap(constants.ErrLLMProviderTemplateManagedByReserved) + return nil, apperror.LLMProviderTemplateManagedByReserved.New() } existing, err := s.repo.GetByID(handle, orgUUID) @@ -338,14 +338,14 @@ func (s *LLMProviderTemplateService) Update(orgUUID, handle, updatedBy string, r return nil, fmt.Errorf("failed to resolve template: %w", err) } if existing == nil { - return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) + return nil, apperror.LLMProviderTemplateNotFound.New() } if existing.ManagedBy == "wso2" { - return nil, apperror.LLMProviderTemplateReadOnly.Wrap(constants.ErrLLMProviderTemplateReadOnly) + return nil, apperror.LLMProviderTemplateReadOnly.New() } if req.Version != "" && req.Version != existing.Version { - return nil, fmt.Errorf("%w: template version cannot be changed via update; use the versions endpoint", constants.ErrInvalidInput) + return nil, apperror.ValidationFailed.New("The template version cannot be changed via update; use the versions endpoint.") } managedBy := existing.ManagedBy @@ -393,7 +393,7 @@ func (s *LLMProviderTemplateService) Update(orgUUID, handle, updatedBy string, r if err := s.repo.Update(m); err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) + return nil, apperror.LLMProviderTemplateNotFound.New() } return nil, fmt.Errorf("failed to update template: %w", err) } @@ -413,7 +413,7 @@ func (s *LLMProviderTemplateService) Update(orgUUID, handle, updatedBy string, r return nil, fmt.Errorf("failed to fetch updated template: %w", err) } if updated == nil { - return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) + return nil, apperror.LLMProviderTemplateNotFound.New() } _ = s.auditRepo.Record("UPDATE", updated.UUID, "llm_provider_template", orgUUID, updatedBy) @@ -482,14 +482,14 @@ func templateVersionCreatable(v string) bool { func (s *LLMProviderTemplateService) CreateVersion(orgUUID, groupID, createdBy string, req *api.CreateLLMProviderTemplateVersionRequest) (*api.LLMProviderTemplate, error) { if groupID == "" || req == nil { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The template group id and a request body are required.") } if utils.ValueOrEmpty(req.DisplayName) == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The displayName field is required.") } version, ok := normalizeTemplateVersion(req.Version) if !ok || !templateVersionCreatable(version) { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The version must match the v. pattern starting from v1.0 (e.g. v1.0).") } managedBy := defaultTemplateManagedBy(req.ManagedBy) @@ -502,7 +502,7 @@ func (s *LLMProviderTemplateService) CreateVersion(orgUUID, groupID, createdBy s return nil, fmt.Errorf("failed to check template family: %w", err) } if count == 0 { - return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) + return nil, apperror.LLMProviderTemplateNotFound.New() } baseHandle := groupID @@ -534,9 +534,9 @@ func (s *LLMProviderTemplateService) CreateVersion(orgUUID, groupID, createdBy s if err := s.repo.CreateNewVersion(m); err != nil { switch { case errors.Is(err, sql.ErrNoRows): - return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) - case errors.Is(err, constants.ErrLLMProviderTemplateVersionExists): - return nil, apperror.LLMProviderTemplateVersionExists.Wrap(constants.ErrLLMProviderTemplateVersionExists) + return nil, apperror.LLMProviderTemplateNotFound.New() + case errors.Is(err, apperror.LLMProviderTemplateVersionExists.New()): + return nil, apperror.LLMProviderTemplateVersionExists.New() default: return nil, fmt.Errorf("failed to create new template version: %w", err) } @@ -548,11 +548,11 @@ func (s *LLMProviderTemplateService) CreateVersion(orgUUID, groupID, createdBy s func (s *LLMProviderTemplateService) CopyVersion(orgUUID, fromTemplateID, toTemplateID, toVersion, createdBy string, req *api.CreateLLMProviderTemplateVersionRequest) (*api.LLMProviderTemplate, error) { fromTemplateID = strings.TrimSpace(fromTemplateID) if fromTemplateID == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The source template id is required.") } version, ok := normalizeTemplateVersion(toVersion) if !ok || !templateVersionCreatable(version) { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The target version must match the v. pattern starting from v1.0 (e.g. v1.0).") } source, err := s.repo.GetByID(fromTemplateID, orgUUID) @@ -560,12 +560,12 @@ func (s *LLMProviderTemplateService) CopyVersion(orgUUID, fromTemplateID, toTemp return nil, fmt.Errorf("failed to resolve source template version: %w", err) } if source == nil { - return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) + return nil, apperror.LLMProviderTemplateNotFound.New() } groupID := source.GroupID if h := strings.TrimSpace(toTemplateID); h != "" && h != makeTemplateHandle(groupID, version) { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The toTemplateId must match the template family and target version.") } seed := mapTemplateModelToAPI(source) @@ -625,14 +625,14 @@ func (s *LLMProviderTemplateService) CopyVersion(orgUUID, fromTemplateID, toTemp func (s *LLMProviderTemplateService) ListVersions(orgUUID, groupID string, limit, offset int) (*api.LLMProviderTemplateListResponse, error) { if groupID == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The template group id is required.") } total, err := s.repo.CountVersions(groupID, orgUUID) if err != nil { return nil, fmt.Errorf("failed to count template versions: %w", err) } if total == 0 { - return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) + return nil, apperror.LLMProviderTemplateNotFound.New() } items, err := s.repo.ListVersions(groupID, orgUUID, limit, offset) if err != nil { @@ -660,11 +660,11 @@ func (s *LLMProviderTemplateService) ListVersions(orgUUID, groupID string, limit func (s *LLMProviderTemplateService) GetVersion(orgUUID, groupID, version string) (*api.LLMProviderTemplate, error) { v := strings.TrimSpace(version) if groupID == "" || v == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The template group id and version are required.") } normalized, ok := normalizeTemplateVersion(v) if !ok { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The version must match the v. pattern (e.g. v1.0).") } v = normalized m, err := s.repo.GetByVersion(groupID, orgUUID, v) @@ -672,7 +672,7 @@ func (s *LLMProviderTemplateService) GetVersion(orgUUID, groupID, version string return nil, fmt.Errorf("failed to get template version: %w", err) } if m == nil { - return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) + return nil, apperror.LLMProviderTemplateNotFound.New() } return s.toTemplateAPI(m) } @@ -680,11 +680,11 @@ func (s *LLMProviderTemplateService) GetVersion(orgUUID, groupID, version string func (s *LLMProviderTemplateService) SetVersionEnabled(orgUUID, groupID, version string, enabled bool) (*api.LLMProviderTemplate, error) { v := strings.TrimSpace(version) if groupID == "" || v == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The template group id and version are required.") } normalized, ok := normalizeTemplateVersion(v) if !ok { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The version must match the v. pattern (e.g. v1.0).") } v = normalized @@ -693,12 +693,12 @@ func (s *LLMProviderTemplateService) SetVersionEnabled(orgUUID, groupID, version return nil, fmt.Errorf("failed to resolve template version: %w", err) } if target == nil { - return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) + return nil, apperror.LLMProviderTemplateNotFound.New() } // Enable/disable is reserved for built-in ('wso2') templates only. Custom // templates are managed via update/delete and cannot be toggled. if target.ManagedBy != constants.PolicyManagedByWSO2 { - return nil, apperror.LLMProviderTemplateNotToggleable.Wrap(constants.ErrLLMProviderTemplateNotToggleable) + return nil, apperror.LLMProviderTemplateNotToggleable.New() } if err := ensureOriginMutable(target.Origin); err != nil { return nil, err @@ -709,12 +709,12 @@ func (s *LLMProviderTemplateService) SetVersionEnabled(orgUUID, groupID, version return nil, fmt.Errorf("failed to check template version usage: %w", err) } if inUse > 0 { - return nil, apperror.LLMProviderTemplateInUse.Wrap(constants.ErrLLMProviderTemplateInUse) + return nil, apperror.LLMProviderTemplateInUse.New() } } if err := s.repo.SetEnabled(groupID, orgUUID, v, enabled); err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) + return nil, apperror.LLMProviderTemplateNotFound.New() } return nil, fmt.Errorf("failed to set template version enabled: %w", err) } @@ -723,7 +723,7 @@ func (s *LLMProviderTemplateService) SetVersionEnabled(orgUUID, groupID, version return nil, fmt.Errorf("failed to reload template version: %w", err) } if m == nil { - return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) + return nil, apperror.LLMProviderTemplateNotFound.New() } return s.toTemplateAPI(m) } @@ -731,11 +731,11 @@ func (s *LLMProviderTemplateService) SetVersionEnabled(orgUUID, groupID, version func (s *LLMProviderTemplateService) DeleteVersion(orgUUID, groupID, version string) error { v := strings.TrimSpace(version) if groupID == "" || v == "" { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("The template group id and version are required.") } normalized, ok := normalizeTemplateVersion(v) if !ok { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("The version must match the v. pattern (e.g. v1.0).") } v = normalized target, err := s.repo.GetByVersion(groupID, orgUUID, v) @@ -743,10 +743,10 @@ func (s *LLMProviderTemplateService) DeleteVersion(orgUUID, groupID, version str return fmt.Errorf("failed to resolve template version: %w", err) } if target == nil { - return apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) + return apperror.LLMProviderTemplateNotFound.New() } if target.ManagedBy == "wso2" { - return apperror.LLMProviderTemplateReadOnly.Wrap(constants.ErrLLMProviderTemplateReadOnly) + return apperror.LLMProviderTemplateReadOnly.New() } // Block deletion while any provider built from this specific version still depends on it. inUse, err := s.repo.CountProvidersUsingTemplate(groupID, orgUUID, v) @@ -754,11 +754,11 @@ func (s *LLMProviderTemplateService) DeleteVersion(orgUUID, groupID, version str return fmt.Errorf("failed to check template version usage: %w", err) } if inUse > 0 { - return apperror.LLMProviderTemplateInUse.Wrap(constants.ErrLLMProviderTemplateInUse) + return apperror.LLMProviderTemplateInUse.New() } if err := s.repo.DeleteVersion(groupID, orgUUID, v); err != nil { if errors.Is(err, sql.ErrNoRows) { - return apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) + return apperror.LLMProviderTemplateNotFound.New() } return fmt.Errorf("failed to delete template version: %w", err) } @@ -769,38 +769,38 @@ func (s *LLMProviderTemplateService) DeleteVersion(orgUUID, groupID, version str // The handle is resolved to its (groupId, version) and the existing version-level rules apply (built-ins are read-only). func (s *LLMProviderTemplateService) SetEnabledByHandle(orgUUID, handle string, enabled bool) (*api.LLMProviderTemplate, error) { if strings.TrimSpace(handle) == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The template id is required.") } target, err := s.repo.GetByID(handle, orgUUID) if err != nil { return nil, fmt.Errorf("failed to resolve template: %w", err) } if target == nil { - return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) + return nil, apperror.LLMProviderTemplateNotFound.New() } return s.SetVersionEnabled(orgUUID, target.GroupID, target.Version, enabled) } func (s *LLMProviderTemplateService) DeleteByHandle(orgUUID, handle string) error { if strings.TrimSpace(handle) == "" { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("The template id is required.") } target, err := s.repo.GetByID(handle, orgUUID) if err != nil { return fmt.Errorf("failed to resolve template: %w", err) } if target == nil { - return apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) + return apperror.LLMProviderTemplateNotFound.New() } return s.DeleteVersion(orgUUID, target.GroupID, target.Version) } func (s *LLMProviderService) Create(orgUUID, createdBy string, req *api.LLMProvider) (*api.LLMProvider, error) { if req == nil { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("A request body is required.") } if req.DisplayName == "" || req.Version == "" || req.Template == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The displayName, version and template fields are required.") } if err := validatePolicyVersions(req.GlobalPolicies); err != nil { return nil, err @@ -820,7 +820,7 @@ func (s *LLMProviderService) Create(orgUUID, createdBy string, req *api.LLMProvi return nil, fmt.Errorf("failed to validate organization: %w", err) } if org == nil { - return nil, constants.ErrOrganizationNotFound + return nil, apperror.OrganizationNotFound.New() } } @@ -847,7 +847,7 @@ func (s *LLMProviderService) Create(orgUUID, createdBy string, req *api.LLMProvi } } if tpl == nil { - return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) + return nil, apperror.LLMProviderTemplateRefNotFound.New() } // Determine handle: use provided id or auto-generate from displayName @@ -859,7 +859,7 @@ func (s *LLMProviderService) Create(orgUUID, createdBy string, req *api.LLMProvi return nil, fmt.Errorf("failed to check provider exists: %w", err) } if exists { - return nil, constants.ErrLLMProviderExists + return nil, apperror.LLMProviderExists.New() } } else { var err error @@ -888,11 +888,11 @@ func (s *LLMProviderService) Create(orgUUID, createdBy string, req *api.LLMProvi if err != nil { return nil, fmt.Errorf("failed to count providers: %w", err) } - if err := validateLLMResourceLimit(providerCount, s.cfg.ArtifactLimits.MaxLLMProvidersPerOrg, constants.ErrLLMProviderLimitReached); err != nil { + if err := validateLLMResourceLimit(providerCount, s.cfg.ArtifactLimits.MaxLLMProvidersPerOrg, apperror.LLMProviderLimitReached.New()); err != nil { return nil, err } if !tpl.Enabled { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The referenced LLM provider template version is disabled.") } openapiSpec := utils.ValueOrEmpty(req.Openapi) @@ -936,7 +936,7 @@ func (s *LLMProviderService) Create(orgUUID, createdBy string, req *api.LLMProvi if err := s.repo.Create(m); err != nil { if isSQLiteUniqueConstraint(err) { - return nil, constants.ErrLLMProviderExists + return nil, apperror.LLMProviderExists.New() } return nil, fmt.Errorf("failed to create provider: %w", err) } @@ -946,7 +946,7 @@ func (s *LLMProviderService) Create(orgUUID, createdBy string, req *api.LLMProvi return nil, fmt.Errorf("failed to fetch created provider: %w", err) } if created == nil { - return nil, constants.ErrLLMProviderNotFound + return nil, apperror.LLMProviderNotFound.New() } _ = s.auditRepo.Record("CREATE", created.UUID, "llm_provider", orgUUID, createdBy) @@ -1010,14 +1010,14 @@ func (s *LLMProviderService) List(orgUUID string, limit, offset int) (*api.LLMPr func (s *LLMProviderService) Get(orgUUID, handle string) (*api.LLMProvider, error) { if handle == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The LLM provider id is required.") } m, err := s.repo.GetByID(handle, orgUUID) if err != nil { return nil, fmt.Errorf("failed to get provider: %w", err) } if m == nil { - return nil, constants.ErrLLMProviderNotFound + return nil, apperror.LLMProviderNotFound.New() } // Look up template handle from UUID tplHandle := "" @@ -1035,7 +1035,7 @@ func (s *LLMProviderService) Get(orgUUID, handle string) (*api.LLMProvider, erro func (s *LLMProviderService) Update(orgUUID, handle, updatedBy string, req *api.LLMProvider) (*api.LLMProvider, error) { if handle == "" || req == nil { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The LLM provider id and a request body are required.") } // Fetch existing provider to preserve sensitive fields on update existing, err := s.repo.GetByID(handle, orgUUID) @@ -1043,10 +1043,10 @@ func (s *LLMProviderService) Update(orgUUID, handle, updatedBy string, req *api. return nil, fmt.Errorf("failed to fetch existing provider: %w", err) } if existing == nil { - return nil, constants.ErrLLMProviderNotFound + return nil, apperror.LLMProviderNotFound.New() } if req.DisplayName == "" || req.Version == "" || req.Template == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The displayName, version and template fields are required.") } if err := validatePolicyVersions(req.GlobalPolicies); err != nil { return nil, err @@ -1073,7 +1073,7 @@ func (s *LLMProviderService) Update(orgUUID, handle, updatedBy string, req *api. return nil, fmt.Errorf("failed to validate template: %w", err) } if tpl == nil { - return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) + return nil, apperror.LLMProviderTemplateRefNotFound.New() } // Validate {{ secret "..." }} placeholders in the upstream config @@ -1145,7 +1145,7 @@ func (s *LLMProviderService) Update(orgUUID, handle, updatedBy string, req *api. if err := s.repo.Update(m); err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, constants.ErrLLMProviderNotFound + return nil, apperror.LLMProviderNotFound.New() } return nil, fmt.Errorf("failed to update provider: %w", err) } @@ -1155,7 +1155,7 @@ func (s *LLMProviderService) Update(orgUUID, handle, updatedBy string, req *api. return nil, fmt.Errorf("failed to fetch updated provider: %w", err) } if updated == nil { - return nil, constants.ErrLLMProviderNotFound + return nil, apperror.LLMProviderNotFound.New() } _ = s.auditRepo.Record("UPDATE", updated.UUID, "llm_provider", orgUUID, updatedBy) @@ -1165,7 +1165,7 @@ func (s *LLMProviderService) Update(orgUUID, handle, updatedBy string, req *api. func (s *LLMProviderService) Delete(orgUUID, handle, deletedBy string) error { if handle == "" { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("The LLM provider id is required.") } // Get the provider UUID before deletion (needed for deployment lookup) @@ -1174,7 +1174,7 @@ func (s *LLMProviderService) Delete(orgUUID, handle, deletedBy string) error { return fmt.Errorf("failed to get LLM provider: %w", err) } if provider == nil { - return constants.ErrLLMProviderNotFound + return apperror.LLMProviderNotFound.New() } // DP-originated artifacts may only be deleted once undeployed on all gateways. @@ -1198,7 +1198,7 @@ func (s *LLMProviderService) Delete(orgUUID, handle, deletedBy string) error { if err := s.repo.Delete(handle, orgUUID); err != nil { if errors.Is(err, sql.ErrNoRows) { - return constants.ErrLLMProviderNotFound + return apperror.LLMProviderNotFound.New() } return fmt.Errorf("failed to delete provider: %w", err) } @@ -1225,10 +1225,10 @@ func (s *LLMProviderService) Delete(orgUUID, handle, deletedBy string) error { func (s *LLMProxyService) Create(orgUUID, createdBy string, req *api.LLMProxy) (*api.LLMProxy, error) { if req == nil { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("A request body is required.") } if req.DisplayName == "" || req.Version == "" || req.Provider.Id == "" || req.ProjectId == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The displayName, version, provider id and projectId fields are required.") } if err := validatePolicyVersions(req.GlobalPolicies); err != nil { return nil, err @@ -1248,7 +1248,7 @@ func (s *LLMProxyService) Create(orgUUID, createdBy string, req *api.LLMProxy) ( return nil, fmt.Errorf("failed to validate project: %w", err) } if project == nil || project.OrganizationID != orgUUID { - return nil, constants.ErrProjectNotFound + return nil, apperror.ProjectNotFound.New() } projectUUID = project.ID } @@ -1259,7 +1259,7 @@ func (s *LLMProxyService) Create(orgUUID, createdBy string, req *api.LLMProxy) ( return nil, fmt.Errorf("failed to validate provider: %w", err) } if prov == nil { - return nil, constants.ErrLLMProviderNotFound + return nil, apperror.LLMProviderNotFound.New() } // Determine handle: use provided id or auto-generate from displayName @@ -1271,7 +1271,7 @@ func (s *LLMProxyService) Create(orgUUID, createdBy string, req *api.LLMProxy) ( return nil, fmt.Errorf("failed to check proxy exists: %w", err) } if exists { - return nil, constants.ErrLLMProxyExists + return nil, apperror.LLMProxyExists.New() } } else { var err error @@ -1289,7 +1289,7 @@ func (s *LLMProxyService) Create(orgUUID, createdBy string, req *api.LLMProxy) ( if err != nil { return nil, fmt.Errorf("failed to count proxies: %w", err) } - if err := validateLLMResourceLimit(proxyCount, s.cfg.ArtifactLimits.MaxLLMProxiesPerOrg, constants.ErrLLMProxyLimitReached); err != nil { + if err := validateLLMResourceLimit(proxyCount, s.cfg.ArtifactLimits.MaxLLMProxiesPerOrg, apperror.LLMProxyLimitReached.New()); err != nil { return nil, err } @@ -1328,7 +1328,7 @@ func (s *LLMProxyService) Create(orgUUID, createdBy string, req *api.LLMProxy) ( if err := s.repo.Create(m); err != nil { if isSQLiteUniqueConstraint(err) { - return nil, constants.ErrLLMProxyExists + return nil, apperror.LLMProxyExists.New() } return nil, fmt.Errorf("failed to create proxy: %w", err) } @@ -1339,7 +1339,7 @@ func (s *LLMProxyService) Create(orgUUID, createdBy string, req *api.LLMProxy) ( return nil, fmt.Errorf("failed to fetch created proxy: %w", err) } if created == nil { - return nil, constants.ErrLLMProxyNotFound + return nil, apperror.LLMProxyNotFound.New() } return s.toProxyAPI(created) } @@ -1348,14 +1348,14 @@ func (s *LLMProxyService) List(orgUUID string, projectHandle *string, limit, off var resolvedProjectUUID *string if projectHandle != nil && *projectHandle != "" { if s.projectRepo == nil { - return nil, constants.ErrProjectNotFound + return nil, apperror.ProjectNotFound.New() } project, err := s.projectRepo.GetProjectByHandleAndOrgID(*projectHandle, orgUUID) if err != nil { return nil, fmt.Errorf("failed to validate project: %w", err) } if project == nil || project.OrganizationID != orgUUID { - return nil, constants.ErrProjectNotFound + return nil, apperror.ProjectNotFound.New() } resolvedProjectUUID = &project.ID } @@ -1423,7 +1423,7 @@ func (s *LLMProxyService) List(orgUUID string, projectHandle *string, limit, off func (s *LLMProxyService) ListByProvider(orgUUID, providerID string, limit, offset int) (*api.LLMProxyListResponse, error) { if providerID == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The LLM provider id is required.") } if s.providerRepo == nil { return nil, fmt.Errorf("could not initialize llmprovider repository") @@ -1433,7 +1433,7 @@ func (s *LLMProxyService) ListByProvider(orgUUID, providerID string, limit, offs return nil, fmt.Errorf("failed to validate provider: %w", err) } if prov == nil { - return nil, constants.ErrLLMProviderNotFound + return nil, apperror.LLMProviderNotFound.New() } items, err := s.repo.ListByProvider(orgUUID, prov.UUID, limit, offset) @@ -1488,24 +1488,24 @@ func (s *LLMProxyService) ListByProvider(orgUUID, providerID string, limit, offs func (s *LLMProxyService) Get(orgUUID, handle string) (*api.LLMProxy, error) { if handle == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The LLM proxy id is required.") } m, err := s.repo.GetByID(handle, orgUUID) if err != nil { return nil, fmt.Errorf("failed to get proxy: %w", err) } if m == nil { - return nil, constants.ErrLLMProxyNotFound + return nil, apperror.LLMProxyNotFound.New() } return s.toProxyAPI(m) } func (s *LLMProxyService) Update(orgUUID, handle, updatedBy string, req *api.LLMProxy) (*api.LLMProxy, error) { if handle == "" || req == nil { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The LLM proxy id and a request body are required.") } if req.DisplayName == "" || req.Version == "" || req.Provider.Id == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The displayName, version and provider id fields are required.") } if err := validatePolicyVersions(req.GlobalPolicies); err != nil { return nil, err @@ -1522,7 +1522,7 @@ func (s *LLMProxyService) Update(orgUUID, handle, updatedBy string, req *api.LLM return nil, fmt.Errorf("failed to get existing proxy: %w", err) } if existing == nil { - return nil, constants.ErrLLMProxyNotFound + return nil, apperror.LLMProxyNotFound.New() } // Validate provider exists @@ -1531,7 +1531,7 @@ func (s *LLMProxyService) Update(orgUUID, handle, updatedBy string, req *api.LLM return nil, fmt.Errorf("failed to validate provider: %w", err) } if prov == nil { - return nil, constants.ErrLLMProviderNotFound + return nil, apperror.LLMProviderNotFound.New() } contextValue := utils.DefaultStringPtr(req.Context, "/") @@ -1587,7 +1587,7 @@ func (s *LLMProxyService) Update(orgUUID, handle, updatedBy string, req *api.LLM if err := s.repo.Update(m); err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, constants.ErrLLMProxyNotFound + return nil, apperror.LLMProxyNotFound.New() } return nil, fmt.Errorf("failed to update proxy: %w", err) } @@ -1597,7 +1597,7 @@ func (s *LLMProxyService) Update(orgUUID, handle, updatedBy string, req *api.LLM return nil, fmt.Errorf("failed to fetch updated proxy: %w", err) } if updated == nil { - return nil, constants.ErrLLMProxyNotFound + return nil, apperror.LLMProxyNotFound.New() } _ = s.auditRepo.Record("UPDATE", existing.UUID, "llm_proxy", orgUUID, updatedBy) return s.toProxyAPI(updated) @@ -1605,7 +1605,7 @@ func (s *LLMProxyService) Update(orgUUID, handle, updatedBy string, req *api.LLM func (s *LLMProxyService) Delete(orgUUID, handle, deletedBy string) error { if handle == "" { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("The LLM proxy id is required.") } // Get the proxy UUID before deletion (needed for deployment lookup) @@ -1614,7 +1614,7 @@ func (s *LLMProxyService) Delete(orgUUID, handle, deletedBy string) error { return fmt.Errorf("failed to get LLM proxy: %w", err) } if proxy == nil { - return constants.ErrLLMProxyNotFound + return apperror.LLMProxyNotFound.New() } // DP-originated artifacts may only be deleted once undeployed on all gateways. @@ -1638,7 +1638,7 @@ func (s *LLMProxyService) Delete(orgUUID, handle, deletedBy string) error { if err := s.repo.Delete(handle, orgUUID); err != nil { if errors.Is(err, sql.ErrNoRows) { - return constants.ErrLLMProxyNotFound + return apperror.LLMProxyNotFound.New() } return fmt.Errorf("failed to delete proxy: %w", err) } @@ -1675,7 +1675,7 @@ func validateUpstream(u api.Upstream) error { mainUrl := utils.ValueOrEmpty(u.Main.Url) mainRef := utils.ValueOrEmpty(u.Main.Ref) if strings.TrimSpace(mainUrl) == "" && strings.TrimSpace(mainRef) == "" { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("The upstream main must specify either a url or a ref.") } return nil } @@ -2382,7 +2382,7 @@ func mapTemplateResourceMappingAPI(in *api.LLMProviderTemplateResourceMapping) ( } resource, isValid := utils.NormalizeAndValidateLLMResourcePath(in.Resource) if !isValid { - return nil, fmt.Errorf("%w: resource mapping resource must be a valid path pattern", constants.ErrInvalidInput) + return nil, apperror.ValidationFailed.New("The resource mapping resource must be a valid path pattern.") } return &model.LLMProviderTemplateResourceMapping{ Resource: resource, @@ -2609,17 +2609,17 @@ func validateModelProviders(template string, providers *[]api.LLMModelProvider) "azureaifoundry": true, } if !aggregatorTemplates[template] && len(*providers) > 1 { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("Only aggregator templates support more than one provider.") } seenProviders := make(map[string]struct{}, len(*providers)) for _, p := range *providers { providerID := strings.TrimSpace(utils.ValueOrEmpty(p.Id)) if providerID == "" { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("Each provider must specify a non-empty id.") } if _, ok := seenProviders[providerID]; ok { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("Duplicate provider id in the request.") } seenProviders[providerID] = struct{}{} @@ -2631,10 +2631,10 @@ func validateModelProviders(template string, providers *[]api.LLMModelProvider) for _, m := range models { modelID := strings.TrimSpace(utils.ValueOrEmpty(m.Id)) if modelID == "" { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("Each model must specify a non-empty id.") } if _, ok := seenModels[modelID]; ok { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("Duplicate model id in the request.") } seenModels[modelID] = struct{}{} } @@ -2793,7 +2793,7 @@ func validateRateLimitingScope(scope *api.RateLimitingScopeConfig) error { return nil } if (scope.Global == nil && scope.ResourceWise == nil) || (scope.Global != nil && scope.ResourceWise != nil) { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("Exactly one of global or resourceWise rate limiting must be set.") } if scope.Global != nil { return validateRateLimitingLimit(scope.Global) @@ -2803,13 +2803,13 @@ func validateRateLimitingScope(scope *api.RateLimitingScopeConfig) error { func validateResourceWiseRateLimiting(cfg *api.ResourceWiseRateLimitingConfig) error { if cfg == nil { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("A rate limiting configuration is required.") } if err := validateRateLimitingLimit(&cfg.Default); err != nil { return err } if len(cfg.Resources) == 0 { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("At least one resource is required for resource-wise rate limiting.") } for _, r := range cfg.Resources { if err := validateRateLimitingLimit(&r.Limit); err != nil { @@ -2825,7 +2825,7 @@ func boolPtrTrue(b *bool) bool { func validateRateLimitingLimit(cfg *api.RateLimitingLimitConfig) error { if cfg == nil { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("A rate limiting limit configuration is required.") } requestEnabled := cfg.Request != nil && boolPtrTrue(cfg.Request.Enabled) tokenEnabled := cfg.Token != nil && boolPtrTrue(cfg.Token.Enabled) @@ -2837,26 +2837,26 @@ func validateRateLimitingLimit(cfg *api.RateLimitingLimitConfig) error { if requestEnabled { if cfg.Request.Count == nil || *cfg.Request.Count <= 0 || cfg.Request.Reset == nil || cfg.Request.Reset.Duration <= 0 { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("The request rate limit requires a positive count and reset duration.") } if !isValidResetUnit(string(cfg.Request.Reset.Unit)) { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("The request rate limit reset unit is invalid.") } } if tokenEnabled { if cfg.Token.Count == nil || *cfg.Token.Count <= 0 || cfg.Token.Reset == nil || cfg.Token.Reset.Duration <= 0 { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("The token rate limit requires a positive count and reset duration.") } if !isValidResetUnit(string(cfg.Token.Reset.Unit)) { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("The token rate limit reset unit is invalid.") } } if costEnabled { if cfg.Cost.Amount == nil || *cfg.Cost.Amount < 0 || cfg.Cost.Reset == nil || cfg.Cost.Reset.Duration <= 0 { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("The cost rate limit requires a non-negative amount and a positive reset duration.") } if !isValidResetUnit(string(cfg.Cost.Reset.Unit)) { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("The cost rate limit reset unit is invalid.") } } return nil @@ -3037,13 +3037,13 @@ func resolveAssociatedGateways(gatewayRepo repository.GatewayRepository, orgUUID return nil, fmt.Errorf("failed to validate associated gateway %q: %w", ag.Id, err) } if gw == nil { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } // Associations are a set (enforced by the artifact_gateway_mappings primary key). // Reject duplicate gateways up-front rather than letting the repo insert fail. if _, dup := seen[gw.ID]; dup { - return nil, fmt.Errorf("%w: duplicate associated gateway %q", constants.ErrInvalidInput, ag.Id) + return nil, apperror.ValidationFailed.New(fmt.Sprintf("Duplicate associated gateway %q.", ag.Id)) } seen[gw.ID] = struct{}{} diff --git a/platform-api/internal/service/llm_apikey.go b/platform-api/internal/service/llm_apikey.go index 7732cbf1d..fbcf52445 100644 --- a/platform-api/internal/service/llm_apikey.go +++ b/platform-api/internal/service/llm_apikey.go @@ -74,7 +74,7 @@ func (s *LLMProviderAPIKeyService) ListLLMProviderAPIKeys( return nil, fmt.Errorf("failed to get LLM provider: %w", err) } if provider == nil { - return nil, apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) + return nil, apperror.ArtifactNotFound.New() } keys, err := s.apiKeyRepo.ListByArtifact(provider.UUID) @@ -140,7 +140,7 @@ func (s *LLMProviderAPIKeyService) DeleteLLMProviderAPIKey( return fmt.Errorf("failed to get LLM provider: %w", err) } if provider == nil { - return apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) + return apperror.ArtifactNotFound.New() } existingKey, err := s.apiKeyRepo.GetByArtifactAndName(provider.UUID, keyName) @@ -149,11 +149,11 @@ func (s *LLMProviderAPIKeyService) DeleteLLMProviderAPIKey( return fmt.Errorf("failed to look up API key: %w", err) } if existingKey == nil { - return apperror.LLMProviderAPIKeyNotFound.Wrap(constants.ErrAPIKeyNotFound) + return apperror.LLMProviderAPIKeyNotFound.New() } if userID != "" && existingKey.CreatedBy != userID { - return apperror.LLMProviderAPIKeyForbidden.Wrap(constants.ErrAPIKeyForbidden) + return apperror.LLMProviderAPIKeyForbidden.New() } if err := s.apiKeyRepo.Delete(provider.UUID, keyName); err != nil { @@ -205,7 +205,7 @@ func (s *LLMProviderAPIKeyService) CreateLLMProviderAPIKey( } if provider == nil { s.slogger.Warn("LLM provider not found", "providerId", providerID, "organizationId", orgID) - return nil, apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) + return nil, apperror.ArtifactNotFound.New() } apiKey, err := utils.GenerateAPIKey() @@ -219,8 +219,8 @@ func (s *LLMProviderAPIKeyService) CreateLLMProviderAPIKey( name = *req.Id } else { if req.DisplayName == "" { - s.slogger.Error("Failed to generate API key name", "providerId", providerID, "error", constants.ErrHandleSourceEmpty) - return nil, fmt.Errorf("failed to generate API key name: both name and displayName are empty: %w", constants.ErrHandleSourceEmpty) + return nil, apperror.ValidationFailed.New("Either id or displayName is required."). + WithLogMessage(fmt.Sprintf("cannot generate API key name for provider %s: both id and displayName are empty", providerID)) } name, err = utils.GenerateHandle(req.DisplayName, nil) if err != nil { @@ -248,7 +248,7 @@ func (s *LLMProviderAPIKeyService) CreateLLMProviderAPIKey( if len(gateways) == 0 { s.slogger.Warn("No gateways found for organization", "organizationId", orgID) - return nil, apperror.GatewayConnectionUnavailable.Wrap(constants.ErrGatewayUnavailable) + return nil, apperror.GatewayConnectionUnavailable.New() } apiKeyHashesJSON, err := buildAPIKeyHashesJSON(apiKey, []string{defaultHashingAlgorithm}) diff --git a/platform-api/internal/service/llm_deployment.go b/platform-api/internal/service/llm_deployment.go index b64f2cc96..39e06174a 100644 --- a/platform-api/internal/service/llm_deployment.go +++ b/platform-api/internal/service/llm_deployment.go @@ -19,7 +19,6 @@ package service import ( "encoding/json" - "errors" "fmt" "log/slog" "strconv" @@ -122,14 +121,14 @@ func NewLLMProxyDeploymentService( func (s *LLMProviderDeploymentService) DeployLLMProvider(providerID string, req *api.DeployRequest, orgUUID string) (*api.DeploymentResponse, error) { // Validate request if req == nil { - return nil, constants.ErrInvalidInput + return nil, apperror.LLMProviderDeploymentValidationFailed.New("A request body is required.") } if req.Base == "" { - return nil, constants.ErrDeploymentBaseRequired + return nil, apperror.LLMProviderDeploymentValidationFailed.New("Base is required (use 'current' or a deploymentId).") } gatewayHandle := strings.TrimSpace(req.GatewayId) if gatewayHandle == "" { - return nil, constants.ErrDeploymentGatewayIDRequired + return nil, apperror.LLMProviderDeploymentValidationFailed.New("Gateway ID is required.") } metadata := utils.MapValueOrEmpty(req.Metadata) @@ -139,7 +138,7 @@ func (s *LLMProviderDeploymentService) DeployLLMProvider(providerID string, req return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } gatewayID := gateway.ID @@ -149,7 +148,7 @@ func (s *LLMProviderDeploymentService) DeployLLMProvider(providerID string, req return nil, err } if provider == nil { - return nil, constants.ErrLLMProviderNotFound + return nil, apperror.LLMProviderNotFound.New() } // DP-originated artifacts are read-only in the control plane and cannot be @@ -160,7 +159,7 @@ func (s *LLMProviderDeploymentService) DeployLLMProvider(providerID string, req // Validate deployment name is provided if req.Name == "" { - return nil, constants.ErrDeploymentNameRequired + return nil, apperror.LLMProviderDeploymentValidationFailed.New("Deployment name is required.") } // Ensure a gateway association exists for the target gateway before deploying, and @@ -216,8 +215,8 @@ func (s *LLMProviderDeploymentService) DeployLLMProvider(providerID string, req // Use existing deployment as base baseDeployment, err := s.deploymentRepo.GetWithContent(req.Base, provider.UUID, orgUUID) if err != nil { - if errors.Is(err, constants.ErrDeploymentNotFound) { - return nil, constants.ErrBaseDeploymentNotFound + if apperror.DeploymentNotFound.Is(err) { + return nil, apperror.DeploymentBaseNotFound.Wrap(err) } return nil, fmt.Errorf("failed to get base deployment: %w", err) } @@ -300,7 +299,7 @@ func (s *LLMProviderDeploymentService) RestoreLLMProviderDeployment(providerID, return nil, err } if provider == nil { - return nil, constants.ErrLLMProviderNotFound + return nil, apperror.LLMProviderNotFound.New() } // DP-originated artifacts are read-only in the control plane; restore cannot be CP-initiated. if err := ensureOriginMutable(provider.Origin); err != nil { @@ -312,7 +311,7 @@ func (s *LLMProviderDeploymentService) RestoreLLMProviderDeployment(providerID, return nil, err } if targetDeployment == nil { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } // gatewayID is a gateway handle (matching deploy); resolve it to the internal // gateway UUID stored on the deployment before comparing. @@ -321,10 +320,10 @@ func (s *LLMProviderDeploymentService) RestoreLLMProviderDeployment(providerID, return nil, fmt.Errorf("failed to get gateway: %w", err) } if resolvedGateway == nil { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } if targetDeployment.GatewayID != resolvedGateway.ID { - return nil, constants.ErrGatewayIDMismatch + return nil, apperror.DeploymentGatewayMismatch.New() } currentDeploymentID, status, _, err := s.deploymentRepo.GetStatus(provider.UUID, orgUUID, targetDeployment.GatewayID) @@ -332,7 +331,7 @@ func (s *LLMProviderDeploymentService) RestoreLLMProviderDeployment(providerID, return nil, fmt.Errorf("failed to get deployment status: %w", err) } if currentDeploymentID == deploymentID && status == model.DeploymentStatusDeployed { - return nil, constants.ErrDeploymentAlreadyDeployed + return nil, apperror.DeploymentRestoreConflict.New() } gateway, err := s.gatewayRepo.GetByUUID(targetDeployment.GatewayID) @@ -340,7 +339,7 @@ func (s *LLMProviderDeploymentService) RestoreLLMProviderDeployment(providerID, return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil || gateway.OrganizationID != orgUUID { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } // Set initial status based on config; transitional (DEPLOYING) only when enabled @@ -392,7 +391,7 @@ func (s *LLMProviderDeploymentService) UndeployLLMProviderDeployment(providerID, return nil, err } if provider == nil { - return nil, constants.ErrLLMProviderNotFound + return nil, apperror.LLMProviderNotFound.New() } // DP-originated artifacts are read-only in the control plane: their deploy/undeploy // lifecycle is owned by the data-plane gateway, so undeployment cannot be initiated @@ -406,7 +405,7 @@ func (s *LLMProviderDeploymentService) UndeployLLMProviderDeployment(providerID, return nil, err } if deployment == nil { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } // gatewayID is a gateway handle (matching deploy); resolve it to the internal // gateway UUID stored on the deployment before comparing. @@ -415,13 +414,13 @@ func (s *LLMProviderDeploymentService) UndeployLLMProviderDeployment(providerID, return nil, fmt.Errorf("failed to get gateway: %w", err) } if resolvedGateway == nil { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } if deployment.GatewayID != resolvedGateway.ID { - return nil, constants.ErrGatewayIDMismatch + return nil, apperror.DeploymentGatewayMismatch.New() } if deployment.Status == nil || *deployment.Status != model.DeploymentStatusDeployed { - return nil, constants.ErrDeploymentNotActive + return nil, apperror.DeploymentNotActive.New("LLM provider") } gateway, err := s.gatewayRepo.GetByUUID(deployment.GatewayID) @@ -429,7 +428,7 @@ func (s *LLMProviderDeploymentService) UndeployLLMProviderDeployment(providerID, return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil || gateway.OrganizationID != orgUUID { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } // Set initial status based on config; transitional (UNDEPLOYING) only when enabled @@ -481,7 +480,7 @@ func (s *LLMProviderDeploymentService) DeleteLLMProviderDeployment(providerID, d return err } if provider == nil { - return constants.ErrLLMProviderNotFound + return apperror.LLMProviderNotFound.New() } deployment, err := s.deploymentRepo.GetWithState(deploymentID, provider.UUID, orgUUID) @@ -489,10 +488,10 @@ func (s *LLMProviderDeploymentService) DeleteLLMProviderDeployment(providerID, d return err } if deployment == nil { - return constants.ErrDeploymentNotFound + return apperror.DeploymentNotFound.New() } if deployment.Status != nil && *deployment.Status == model.DeploymentStatusDeployed { - return constants.ErrDeploymentIsDeployed + return apperror.DeploymentActive.New() } if err := s.deploymentRepo.Delete(deploymentID, provider.UUID, orgUUID); err != nil { @@ -509,7 +508,7 @@ func (s *LLMProviderDeploymentService) GetLLMProviderDeployments(providerID, org return nil, err } if provider == nil { - return nil, constants.ErrLLMProviderNotFound + return nil, apperror.LLMProviderNotFound.New() } if status != nil { @@ -522,7 +521,7 @@ func (s *LLMProviderDeploymentService) GetLLMProviderDeployments(providerID, org string(model.DeploymentStatusArchived): true, } if !validStatuses[*status] { - return nil, constants.ErrInvalidDeploymentStatus + return nil, apperror.DeploymentInvalidStatus.New() } } @@ -578,7 +577,7 @@ func (s *LLMProviderDeploymentService) GetLLMProviderDeployment(providerID, depl return nil, err } if provider == nil { - return nil, constants.ErrLLMProviderNotFound + return nil, apperror.LLMProviderNotFound.New() } deployment, err := s.deploymentRepo.GetWithState(deploymentID, provider.UUID, orgUUID) @@ -586,7 +585,7 @@ func (s *LLMProviderDeploymentService) GetLLMProviderDeployment(providerID, depl return nil, err } if deployment == nil { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } return toAPIDeploymentResponse( @@ -605,14 +604,14 @@ func (s *LLMProviderDeploymentService) GetLLMProviderDeployment(providerID, depl func (s *LLMProviderDeploymentService) getTemplateHandle(templateUUID, orgUUID string) (string, error) { if templateUUID == "" { - return "", apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) + return "", apperror.LLMProviderTemplateNotFound.Wrap(apperror.LLMProviderDeploymentValidationFailed.New("The referenced LLM provider template could not be found.")) } tpl, err := s.templateRepo.GetByUUID(templateUUID, orgUUID) if err != nil { return "", fmt.Errorf("failed to resolve template: %w", err) } if tpl == nil { - return "", apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) + return "", apperror.LLMProviderTemplateNotFound.Wrap(apperror.LLMProviderDeploymentValidationFailed.New("The referenced LLM provider template could not be found.")) } return tpl.ID, nil } @@ -625,11 +624,11 @@ func generateLLMProviderDeploymentYAML(provider *model.LLMProvider, templateHand return dto.LLMProviderDeploymentYAML{}, apperror.Internal.New().WithLogMessage("generateLLMProviderDeploymentYAML: template handle is empty") } if provider.Configuration.Upstream == nil || provider.Configuration.Upstream.Main == nil { - return dto.LLMProviderDeploymentYAML{}, constants.ErrInvalidInput + return dto.LLMProviderDeploymentYAML{}, apperror.LLMProviderDeploymentValidationFailed.New("The LLM provider must define an upstream main configuration.") } main := provider.Configuration.Upstream.Main if main.URL == "" && main.Ref == "" { - return dto.LLMProviderDeploymentYAML{}, constants.ErrInvalidInput + return dto.LLMProviderDeploymentYAML{}, apperror.LLMProviderDeploymentValidationFailed.New("The LLM provider upstream main must specify either a url or a ref.") } contextValue := "/" @@ -1294,14 +1293,14 @@ func unmarshalDeploymentMetadata(s string) (map[string]any, error) { func (s *LLMProxyDeploymentService) DeployLLMProxy(proxyID string, req *api.DeployRequest, orgUUID string) (*api.DeploymentResponse, error) { // Validate request if req == nil { - return nil, constants.ErrInvalidInput + return nil, apperror.LLMProxyDeploymentValidationFailed.New("A request body is required.") } if req.Base == "" { - return nil, constants.ErrDeploymentBaseRequired + return nil, apperror.LLMProxyDeploymentValidationFailed.New("Base is required (use 'current' or a deploymentId).") } gatewayHandle := strings.TrimSpace(req.GatewayId) if gatewayHandle == "" { - return nil, constants.ErrDeploymentGatewayIDRequired + return nil, apperror.LLMProxyDeploymentValidationFailed.New("Gateway ID is required.") } metadata := utils.MapValueOrEmpty(req.Metadata) @@ -1311,7 +1310,7 @@ func (s *LLMProxyDeploymentService) DeployLLMProxy(proxyID string, req *api.Depl return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } gatewayID := gateway.ID @@ -1321,7 +1320,7 @@ func (s *LLMProxyDeploymentService) DeployLLMProxy(proxyID string, req *api.Depl return nil, err } if proxy == nil { - return nil, constants.ErrLLMProxyNotFound + return nil, apperror.LLMProxyNotFound.New() } // DP-originated artifacts are read-only in the control plane and cannot be @@ -1332,7 +1331,7 @@ func (s *LLMProxyDeploymentService) DeployLLMProxy(proxyID string, req *api.Depl // Validate deployment name is provided if req.Name == "" { - return nil, constants.ErrDeploymentNameRequired + return nil, apperror.LLMProxyDeploymentValidationFailed.New("Deployment name is required.") } // Ensure a gateway association exists for the target gateway before deploying, and @@ -1380,8 +1379,8 @@ func (s *LLMProxyDeploymentService) DeployLLMProxy(proxyID string, req *api.Depl // Use existing deployment as base baseDeployment, err := s.deploymentRepo.GetWithContent(req.Base, proxy.UUID, orgUUID) if err != nil { - if errors.Is(err, constants.ErrDeploymentNotFound) { - return nil, constants.ErrBaseDeploymentNotFound + if apperror.DeploymentNotFound.Is(err) { + return nil, apperror.DeploymentBaseNotFound.Wrap(err) } return nil, fmt.Errorf("failed to get base deployment: %w", err) } @@ -1464,7 +1463,7 @@ func (s *LLMProxyDeploymentService) RestoreLLMProxyDeployment(proxyID, deploymen return nil, err } if proxy == nil { - return nil, constants.ErrLLMProxyNotFound + return nil, apperror.LLMProxyNotFound.New() } // DP-originated artifacts are read-only in the control plane; restore cannot be CP-initiated. if err := ensureOriginMutable(proxy.Origin); err != nil { @@ -1476,7 +1475,7 @@ func (s *LLMProxyDeploymentService) RestoreLLMProxyDeployment(proxyID, deploymen return nil, err } if targetDeployment == nil { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } // gatewayID is a gateway handle (matching deploy); resolve it to the internal // gateway UUID stored on the deployment before comparing. @@ -1485,10 +1484,10 @@ func (s *LLMProxyDeploymentService) RestoreLLMProxyDeployment(proxyID, deploymen return nil, fmt.Errorf("failed to get gateway: %w", err) } if resolvedGateway == nil { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } if targetDeployment.GatewayID != resolvedGateway.ID { - return nil, constants.ErrGatewayIDMismatch + return nil, apperror.DeploymentGatewayMismatch.New() } currentDeploymentID, status, _, err := s.deploymentRepo.GetStatus(proxy.UUID, orgUUID, targetDeployment.GatewayID) @@ -1496,7 +1495,7 @@ func (s *LLMProxyDeploymentService) RestoreLLMProxyDeployment(proxyID, deploymen return nil, fmt.Errorf("failed to get deployment status: %w", err) } if currentDeploymentID == deploymentID && status == model.DeploymentStatusDeployed { - return nil, constants.ErrDeploymentAlreadyDeployed + return nil, apperror.DeploymentRestoreConflict.New() } gateway, err := s.gatewayRepo.GetByUUID(targetDeployment.GatewayID) @@ -1504,7 +1503,7 @@ func (s *LLMProxyDeploymentService) RestoreLLMProxyDeployment(proxyID, deploymen return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil || gateway.OrganizationID != orgUUID { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } // Set initial status based on config; transitional (DEPLOYING) only when enabled @@ -1556,7 +1555,7 @@ func (s *LLMProxyDeploymentService) UndeployLLMProxyDeployment(proxyID, deployme return nil, err } if proxy == nil { - return nil, constants.ErrLLMProxyNotFound + return nil, apperror.LLMProxyNotFound.New() } // DP-originated artifacts are read-only in the control plane: their deploy/undeploy // lifecycle is owned by the data-plane gateway, so undeployment cannot be initiated @@ -1570,7 +1569,7 @@ func (s *LLMProxyDeploymentService) UndeployLLMProxyDeployment(proxyID, deployme return nil, err } if deployment == nil { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } // gatewayID is a gateway handle (matching deploy); resolve it to the internal // gateway UUID stored on the deployment before comparing. @@ -1579,13 +1578,13 @@ func (s *LLMProxyDeploymentService) UndeployLLMProxyDeployment(proxyID, deployme return nil, fmt.Errorf("failed to get gateway: %w", err) } if resolvedGateway == nil { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } if deployment.GatewayID != resolvedGateway.ID { - return nil, constants.ErrGatewayIDMismatch + return nil, apperror.DeploymentGatewayMismatch.New() } if deployment.Status == nil || *deployment.Status != model.DeploymentStatusDeployed { - return nil, constants.ErrDeploymentNotActive + return nil, apperror.DeploymentNotActive.New("LLM proxy") } gateway, err := s.gatewayRepo.GetByUUID(deployment.GatewayID) @@ -1593,7 +1592,7 @@ func (s *LLMProxyDeploymentService) UndeployLLMProxyDeployment(proxyID, deployme return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil || gateway.OrganizationID != orgUUID { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } // Set initial status based on config; transitional (UNDEPLOYING) only when enabled @@ -1645,7 +1644,7 @@ func (s *LLMProxyDeploymentService) DeleteLLMProxyDeployment(proxyID, deployment return err } if proxy == nil { - return constants.ErrLLMProxyNotFound + return apperror.LLMProxyNotFound.New() } deployment, err := s.deploymentRepo.GetWithState(deploymentID, proxy.UUID, orgUUID) @@ -1653,10 +1652,10 @@ func (s *LLMProxyDeploymentService) DeleteLLMProxyDeployment(proxyID, deployment return err } if deployment == nil { - return constants.ErrDeploymentNotFound + return apperror.DeploymentNotFound.New() } if deployment.Status != nil && *deployment.Status == model.DeploymentStatusDeployed { - return constants.ErrDeploymentIsDeployed + return apperror.DeploymentActive.New() } if err := s.deploymentRepo.Delete(deploymentID, proxy.UUID, orgUUID); err != nil { @@ -1673,7 +1672,7 @@ func (s *LLMProxyDeploymentService) GetLLMProxyDeployments(proxyID, orgUUID stri return nil, err } if proxy == nil { - return nil, constants.ErrLLMProxyNotFound + return nil, apperror.LLMProxyNotFound.New() } if status != nil { @@ -1686,7 +1685,7 @@ func (s *LLMProxyDeploymentService) GetLLMProxyDeployments(proxyID, orgUUID stri string(model.DeploymentStatusArchived): true, } if !validStatuses[*status] { - return nil, constants.ErrInvalidDeploymentStatus + return nil, apperror.DeploymentInvalidStatus.New() } } @@ -1742,7 +1741,7 @@ func (s *LLMProxyDeploymentService) GetLLMProxyDeployment(proxyID, deploymentID, return nil, err } if proxy == nil { - return nil, constants.ErrLLMProxyNotFound + return nil, apperror.LLMProxyNotFound.New() } deployment, err := s.deploymentRepo.GetWithState(deploymentID, proxy.UUID, orgUUID) @@ -1750,7 +1749,7 @@ func (s *LLMProxyDeploymentService) GetLLMProxyDeployment(proxyID, deploymentID, return nil, err } if deployment == nil { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } return toAPIDeploymentResponse( @@ -1772,7 +1771,7 @@ func generateLLMProxyDeploymentYAML(proxy *model.LLMProxy) (dto.LLMProxyDeployme return dto.LLMProxyDeploymentYAML{}, apperror.Internal.New().WithLogMessage("generateLLMProxyDeploymentYAML: proxy is nil") } if proxy.Configuration.Provider == "" { - return dto.LLMProxyDeploymentYAML{}, constants.ErrInvalidInput + return dto.LLMProxyDeploymentYAML{}, apperror.LLMProxyDeploymentValidationFailed.New("The LLM proxy must reference a provider.") } contextValue := "/" diff --git a/platform-api/internal/service/llm_provider_template_test.go b/platform-api/internal/service/llm_provider_template_test.go index b26ea6366..0085bd90b 100644 --- a/platform-api/internal/service/llm_provider_template_test.go +++ b/platform-api/internal/service/llm_provider_template_test.go @@ -19,11 +19,11 @@ package service import ( "database/sql" - "errors" "strings" "testing" "github.com/wso2/api-platform/platform-api/api" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/repository" @@ -171,8 +171,8 @@ func (m *mockLLMProviderTemplateCRUDRepo) DeleteVersion(templateID, orgUUID, ver func validTemplateRequest(name string) *api.LLMProviderTemplate { endpoint := "https://api.example.com" return &api.LLMProviderTemplate{ - DisplayName: name, - Version: "v1.0", + DisplayName: name, + Version: "v1.0", Metadata: &api.LLMProviderTemplateMetadata{ EndpointUrl: &endpoint, }, @@ -206,7 +206,7 @@ func TestLLMProviderTemplateServiceCreate_RejectsMissingEndpoint(t *testing.T) { // No metadata at all. req := &api.LLMProviderTemplate{DisplayName: "No Endpoint", Version: "v1.0"} - if _, err := svc.Create("org-1", "alice", req); !errors.Is(err, constants.ErrInvalidInput) { + if _, err := svc.Create("org-1", "alice", req); !apperror.ValidationFailed.Is(err) { t.Fatalf("expected ErrInvalidInput when endpoint is missing, got: %v", err) } @@ -214,10 +214,10 @@ func TestLLMProviderTemplateServiceCreate_RejectsMissingEndpoint(t *testing.T) { blank := " " req2 := &api.LLMProviderTemplate{ DisplayName: "Blank Endpoint", - Version: "v1.0", - Metadata: &api.LLMProviderTemplateMetadata{EndpointUrl: &blank}, + Version: "v1.0", + Metadata: &api.LLMProviderTemplateMetadata{EndpointUrl: &blank}, } - if _, err := svc.Create("org-1", "alice", req2); !errors.Is(err, constants.ErrInvalidInput) { + if _, err := svc.Create("org-1", "alice", req2); !apperror.ValidationFailed.Is(err) { t.Fatalf("expected ErrInvalidInput when endpoint is blank, got: %v", err) } if repo.createCalled { @@ -231,7 +231,7 @@ func TestLLMProviderTemplateServiceCreate_RejectsEmptyName(t *testing.T) { req := validTemplateRequest("") _, err := svc.Create("org-1", "alice", req) - if !errors.Is(err, constants.ErrInvalidInput) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("expected ErrInvalidInput, got: %v", err) } if repo.createCalled { @@ -246,7 +246,7 @@ func TestLLMProviderTemplateServiceCreate_RejectsInvalidVersion(t *testing.T) { req := validTemplateRequest("My Provider") req.Version = "not-a-version" _, err := svc.Create("org-1", "alice", req) - if !errors.Is(err, constants.ErrInvalidInput) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("expected ErrInvalidInput, got: %v", err) } if repo.createCalled { @@ -262,7 +262,7 @@ func TestLLMProviderTemplateServiceCreate_RejectsReservedManagedBy(t *testing.T) req := validTemplateRequest("My Provider") req.ManagedBy = &wso2 _, err := svc.Create("org-1", "alice", req) - if !errors.Is(err, constants.ErrLLMProviderTemplateManagedByReserved) { + if !apperror.LLMProviderTemplateManagedByReserved.Is(err) { t.Fatalf("expected ErrLLMProviderTemplateManagedByReserved, got: %v", err) } if repo.createCalled { @@ -275,7 +275,7 @@ func TestLLMProviderTemplateServiceCreate_ReturnsConflictForDuplicateHandle(t *t svc := NewLLMProviderTemplateService(repo, &noopAuditRepo{}, newTestIdentityService()) _, err := svc.Create("org-1", "alice", validTemplateRequest("My Provider")) - if !errors.Is(err, constants.ErrLLMProviderTemplateExists) { + if !apperror.LLMProviderTemplateExists.Is(err) { t.Fatalf("expected ErrLLMProviderTemplateExists, got: %v", err) } if repo.createCalled { @@ -293,7 +293,7 @@ func TestLLMProviderTemplateServiceUpdate_RejectsReservedManagedBy(t *testing.T) req := validTemplateRequest("My Provider") req.ManagedBy = &wso2 _, err := svc.Update("org-1", "my-provider", "alice", req) - if !errors.Is(err, constants.ErrLLMProviderTemplateManagedByReserved) { + if !apperror.LLMProviderTemplateManagedByReserved.Is(err) { t.Fatalf("expected ErrLLMProviderTemplateManagedByReserved, got: %v", err) } if repo.updated != nil { @@ -309,7 +309,7 @@ func TestLLMProviderTemplateServiceUpdate_RejectsReadOnlyBuiltin(t *testing.T) { svc := NewLLMProviderTemplateService(repo, &noopAuditRepo{}, newTestIdentityService()) _, err := svc.Update("org-1", "openai", "alice", validTemplateRequest("OpenAI")) - if !errors.Is(err, constants.ErrLLMProviderTemplateReadOnly) { + if !apperror.LLMProviderTemplateReadOnly.Is(err) { t.Fatalf("expected ErrLLMProviderTemplateReadOnly, got: %v", err) } if repo.updated != nil { @@ -322,7 +322,7 @@ func TestLLMProviderTemplateServiceUpdate_ReturnsNotFoundWhenTemplateMissing(t * svc := NewLLMProviderTemplateService(repo, &noopAuditRepo{}, newTestIdentityService()) _, err := svc.Update("org-1", "does-not-exist", "alice", validTemplateRequest("Name")) - if !errors.Is(err, constants.ErrLLMProviderTemplateNotFound) { + if !apperror.LLMProviderTemplateNotFound.Is(err) { t.Fatalf("expected ErrLLMProviderTemplateNotFound, got: %v", err) } } @@ -427,7 +427,7 @@ func TestLLMProviderTemplateServiceCreateVersion_NotFoundWhenFamilyMissing(t *te req := &api.CreateLLMProviderTemplateVersionRequest{DisplayName: stringPtr("Mistral"), Version: "v2.0"} _, err := svc.CreateVersion("org-1", "does-not-exist", "test-user", req) - if !errors.Is(err, constants.ErrLLMProviderTemplateNotFound) { + if !apperror.LLMProviderTemplateNotFound.Is(err) { t.Fatalf("expected ErrLLMProviderTemplateNotFound, got: %v", err) } } @@ -435,13 +435,13 @@ func TestLLMProviderTemplateServiceCreateVersion_NotFoundWhenFamilyMissing(t *te func TestLLMProviderTemplateServiceCreateVersion_ConflictWhenVersionExists(t *testing.T) { repo := &mockLLMProviderTemplateCRUDRepo{ countVersionsResult: 1, - createNewVersionErr: constants.ErrLLMProviderTemplateVersionExists, + createNewVersionErr: apperror.LLMProviderTemplateVersionExists.New(), } svc := NewLLMProviderTemplateService(repo, &noopAuditRepo{}, newTestIdentityService()) req := &api.CreateLLMProviderTemplateVersionRequest{DisplayName: stringPtr("Mistral"), Version: "v1.0"} _, err := svc.CreateVersion("org-1", "mistralai", "test-user", req) - if !errors.Is(err, constants.ErrLLMProviderTemplateVersionExists) { + if !apperror.LLMProviderTemplateVersionExists.Is(err) { t.Fatalf("expected ErrLLMProviderTemplateVersionExists, got: %v", err) } } @@ -452,7 +452,7 @@ func TestLLMProviderTemplateServiceCreateVersion_RejectsInvalidVersionFormat(t * req := &api.CreateLLMProviderTemplateVersionRequest{DisplayName: stringPtr("Mistral"), Version: "2.0"} _, err := svc.CreateVersion("org-1", "mistralai", "test-user", req) - if !errors.Is(err, constants.ErrInvalidInput) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("expected ErrInvalidInput, got: %v", err) } if repo.createdVersion != nil { @@ -467,7 +467,7 @@ func TestLLMProviderTemplateServiceCreateVersion_RejectsV0(t *testing.T) { // Versions start at v1.0; v0.x is not creatable. req := &api.CreateLLMProviderTemplateVersionRequest{DisplayName: stringPtr("Mistral"), Version: "v0.0"} _, err := svc.CreateVersion("org-1", "mistralai", "test-user", req) - if !errors.Is(err, constants.ErrInvalidInput) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("expected ErrInvalidInput for v0.0, got: %v", err) } if repo.createdVersion != nil { @@ -525,7 +525,7 @@ func TestLLMProviderTemplateServiceCopyVersion_NotFoundWhenSourceMissing(t *test svc := NewLLMProviderTemplateService(repo, &noopAuditRepo{}, newTestIdentityService()) _, err := svc.CopyVersion("org-1", "nope-v1-0", "nope-v2-0", "v2.0", "test-user", nil) - if !errors.Is(err, constants.ErrLLMProviderTemplateNotFound) { + if !apperror.LLMProviderTemplateNotFound.Is(err) { t.Fatalf("expected ErrLLMProviderTemplateNotFound, got: %v", err) } } @@ -538,7 +538,7 @@ func TestLLMProviderTemplateServiceCopyVersion_RejectsMismatchedToTemplateID(t * svc := NewLLMProviderTemplateService(repo, &noopAuditRepo{}, newTestIdentityService()) _, err := svc.CopyVersion("org-1", "mistralai-v1-0", "other-family-v2-0", "v2.0", "test-user", nil) - if !errors.Is(err, constants.ErrInvalidInput) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("expected ErrInvalidInput for mismatched toTemplateId, got: %v", err) } if repo.createdVersion != nil { @@ -553,7 +553,7 @@ func TestLLMProviderTemplateServiceListVersions_NotFoundWhenNoVersions(t *testin svc := NewLLMProviderTemplateService(repo, &noopAuditRepo{}, newTestIdentityService()) _, err := svc.ListVersions("org-1", "does-not-exist", 10, 0) - if !errors.Is(err, constants.ErrLLMProviderTemplateNotFound) { + if !apperror.LLMProviderTemplateNotFound.Is(err) { t.Fatalf("expected ErrLLMProviderTemplateNotFound, got: %v", err) } } @@ -586,7 +586,7 @@ func TestLLMProviderTemplateServiceGetVersion_NotFound(t *testing.T) { svc := NewLLMProviderTemplateService(repo, &noopAuditRepo{}, newTestIdentityService()) _, err := svc.GetVersion("org-1", "mistralai", "v9.0") - if !errors.Is(err, constants.ErrLLMProviderTemplateNotFound) { + if !apperror.LLMProviderTemplateNotFound.Is(err) { t.Fatalf("expected ErrLLMProviderTemplateNotFound, got: %v", err) } } @@ -640,7 +640,7 @@ func TestLLMProviderTemplateServiceSetVersionEnabled_NotFound(t *testing.T) { svc := NewLLMProviderTemplateService(repo, &noopAuditRepo{}, newTestIdentityService()) _, err := svc.SetVersionEnabled("org-1", "does-not-exist", "v1.0", true) - if !errors.Is(err, constants.ErrLLMProviderTemplateNotFound) { + if !apperror.LLMProviderTemplateNotFound.Is(err) { t.Fatalf("expected ErrLLMProviderTemplateNotFound, got: %v", err) } } @@ -655,7 +655,7 @@ func TestLLMProviderTemplateServiceSetVersionEnabled_DisableBlocksWhenInUse(t *t svc := NewLLMProviderTemplateService(repo, &noopAuditRepo{}, newTestIdentityService()) _, err := svc.SetVersionEnabled("org-1", "openai", "v1.0", false) - if !errors.Is(err, constants.ErrLLMProviderTemplateInUse) { + if !apperror.LLMProviderTemplateInUse.Is(err) { t.Fatalf("expected ErrLLMProviderTemplateInUse, got: %v", err) } if repo.countProvidersUsingTemplateVersion != "v1.0" { @@ -698,7 +698,7 @@ func TestLLMProviderTemplateServiceSetVersionEnabled_RejectsCustomTemplate(t *te // Enable/disable is reserved for built-in ('wso2') templates; a custom // ('customer') template must be rejected and never touch SetEnabled. _, err := svc.SetVersionEnabled("org-1", "openai", "v2.0", false) - if !errors.Is(err, constants.ErrLLMProviderTemplateNotToggleable) { + if !apperror.LLMProviderTemplateNotToggleable.Is(err) { t.Fatalf("expected ErrLLMProviderTemplateNotToggleable, got: %v", err) } if repo.setEnabledCalled || repo.countProvidersUsingTemplateCalled { @@ -717,7 +717,7 @@ func TestLLMProviderTemplateServiceDeleteVersion_NotFoundWhenVersionMissing(t *t svc := NewLLMProviderTemplateService(repo, &noopAuditRepo{}, newTestIdentityService()) err := svc.DeleteVersion("org-1", "mistralai", "v9.0") - if !errors.Is(err, constants.ErrLLMProviderTemplateNotFound) { + if !apperror.LLMProviderTemplateNotFound.Is(err) { t.Fatalf("expected ErrLLMProviderTemplateNotFound, got: %v", err) } } @@ -731,7 +731,7 @@ func TestLLMProviderTemplateServiceDeleteVersion_BlocksReadOnlyBuiltin(t *testin svc := NewLLMProviderTemplateService(repo, &noopAuditRepo{}, newTestIdentityService()) err := svc.DeleteVersion("org-1", "openai", "v1.0") - if !errors.Is(err, constants.ErrLLMProviderTemplateReadOnly) { + if !apperror.LLMProviderTemplateReadOnly.Is(err) { t.Fatalf("expected ErrLLMProviderTemplateReadOnly, got: %v", err) } if repo.deleteVersionCalled { @@ -749,7 +749,7 @@ func TestLLMProviderTemplateServiceDeleteVersion_BlocksWhenInUse(t *testing.T) { svc := NewLLMProviderTemplateService(repo, &noopAuditRepo{}, newTestIdentityService()) err := svc.DeleteVersion("org-1", "mistralai-v2-0", "v2.0") - if !errors.Is(err, constants.ErrLLMProviderTemplateInUse) { + if !apperror.LLMProviderTemplateInUse.Is(err) { t.Fatalf("expected ErrLLMProviderTemplateInUse, got: %v", err) } if repo.countProvidersUsingTemplateVersion != "v2.0" { diff --git a/platform-api/internal/service/llm_proxy_apikey.go b/platform-api/internal/service/llm_proxy_apikey.go index 17e9d6663..44a3462fa 100644 --- a/platform-api/internal/service/llm_proxy_apikey.go +++ b/platform-api/internal/service/llm_proxy_apikey.go @@ -74,7 +74,7 @@ func (s *LLMProxyAPIKeyService) ListLLMProxyAPIKeys( return nil, fmt.Errorf("failed to get LLM proxy: %w", err) } if proxy == nil { - return nil, apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) + return nil, apperror.ArtifactNotFound.New() } keys, err := s.apiKeyRepo.ListByArtifact(proxy.UUID) @@ -112,7 +112,7 @@ func (s *LLMProxyAPIKeyService) DeleteLLMProxyAPIKey( return fmt.Errorf("failed to get LLM proxy: %w", err) } if proxy == nil { - return apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) + return apperror.ArtifactNotFound.New() } existingKey, err := s.apiKeyRepo.GetByArtifactAndName(proxy.UUID, keyName) @@ -121,12 +121,12 @@ func (s *LLMProxyAPIKeyService) DeleteLLMProxyAPIKey( return fmt.Errorf("failed to look up API key: %w", err) } if existingKey == nil { - return apperror.LLMProxyAPIKeyNotFound.Wrap(constants.ErrAPIKeyNotFound) + return apperror.LLMProxyAPIKeyNotFound.New() } // Non-admin callers (userID != "") must be the key creator. if userID != "" && existingKey.CreatedBy != userID { - return apperror.LLMProxyAPIKeyForbidden.Wrap(constants.ErrAPIKeyForbidden) + return apperror.LLMProxyAPIKeyForbidden.New() } if err := s.apiKeyRepo.Delete(proxy.UUID, keyName); err != nil { @@ -178,7 +178,7 @@ func (s *LLMProxyAPIKeyService) CreateLLMProxyAPIKey( } if proxy == nil { s.slogger.Warn("LLM proxy not found", "proxyId", proxyID, "organizationId", orgID) - return nil, apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) + return nil, apperror.ArtifactNotFound.New() } apiKey, err := utils.GenerateAPIKey() @@ -211,7 +211,7 @@ func (s *LLMProxyAPIKeyService) CreateLLMProxyAPIKey( if len(gateways) == 0 { s.slogger.Warn("No gateways found for organization", "organizationId", orgID) - return nil, apperror.GatewayConnectionUnavailable.Wrap(constants.ErrGatewayUnavailable) + return nil, apperror.GatewayConnectionUnavailable.New() } apiKeyHashesJSON, err := buildAPIKeyHashesJSON(apiKey, []string{defaultHashingAlgorithm}) diff --git a/platform-api/internal/service/llm_secret_validation_test.go b/platform-api/internal/service/llm_secret_validation_test.go index 15210ff41..72c529ebe 100644 --- a/platform-api/internal/service/llm_secret_validation_test.go +++ b/platform-api/internal/service/llm_secret_validation_test.go @@ -30,12 +30,11 @@ package service import ( "database/sql" - "errors" "os" "path/filepath" "testing" - "github.com/wso2/api-platform/platform-api/internal/constants" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/database" "github.com/wso2/api-platform/platform-api/internal/dto" "github.com/wso2/api-platform/platform-api/internal/repository" @@ -113,8 +112,8 @@ func TestLLMProviderService_Create_InvalidPlaceholder_Rejected(t *testing.T) { if err == nil { t.Fatal("expected error for non-existent secret placeholder, got nil") } - if !errors.Is(err, constants.ErrSecretRefMissing) { - t.Errorf("expected ErrSecretRefMissing, got: %v", err) + if !apperror.ValidationFailed.Is(err) { + t.Errorf("expected VALIDATION_FAILED, got: %v", err) } } @@ -146,8 +145,8 @@ func TestLLMProviderService_Update_InvalidPlaceholder_Rejected(t *testing.T) { if err == nil { t.Fatal("expected error for non-existent placeholder on update config, got nil") } - if !errors.Is(err, constants.ErrSecretRefMissing) { - t.Errorf("expected ErrSecretRefMissing, got: %v", err) + if !apperror.ValidationFailed.Is(err) { + t.Errorf("expected VALIDATION_FAILED, got: %v", err) } } @@ -162,10 +161,10 @@ func TestMCPProxyService_Create_MissingSecretRef_Rejected(t *testing.T) { config := `{"main":{"url":"https://mcp.example.com","auth":{"value":"{{ secret \"nonexistent-mcp-secret\" }}"}}}` err := svc.ValidateSecretRefs(orgID, config) if err == nil { - t.Fatal("expected ErrSecretRefMissing for MCP proxy create with missing secret ref, got nil") + t.Fatal("expected a validation error for MCP proxy create with missing secret ref, got nil") } - if !errors.Is(err, constants.ErrSecretRefMissing) { - t.Errorf("expected ErrSecretRefMissing, got: %v", err) + if !apperror.ValidationFailed.Is(err) { + t.Errorf("expected VALIDATION_FAILED, got: %v", err) } } @@ -181,7 +180,7 @@ func TestMCPProxyService_Update_MissingSecretRef_Rejected(t *testing.T) { if err == nil { t.Fatal("expected ErrSecretRefMissing for MCP proxy update with missing secret ref, got nil") } - if !errors.Is(err, constants.ErrSecretRefMissing) { - t.Errorf("expected ErrSecretRefMissing, got: %v", err) + if !apperror.ValidationFailed.Is(err) { + t.Errorf("expected VALIDATION_FAILED, got: %v", err) } } diff --git a/platform-api/internal/service/llm_test.go b/platform-api/internal/service/llm_test.go index f2c3f99ce..3342b57fd 100644 --- a/platform-api/internal/service/llm_test.go +++ b/platform-api/internal/service/llm_test.go @@ -1,14 +1,13 @@ package service import ( - "errors" "log/slog" "testing" "time" "github.com/wso2/api-platform/platform-api/api" "github.com/wso2/api-platform/platform-api/config" - "github.com/wso2/api-platform/platform-api/internal/constants" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/dto" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/repository" @@ -42,7 +41,7 @@ func TestMapTemplateResourceMappingAPI_RejectsEmptyResource(t *testing.T) { if mapped != nil { t.Fatal("expected mapped resource to be nil when validation fails") } - if !errors.Is(err, constants.ErrInvalidInput) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("expected ErrInvalidInput, got: %v", err) } } @@ -60,7 +59,7 @@ func TestMapTemplateResourceMappingsAPI_StopsOnInvalidResource(t *testing.T) { if mapped != nil { t.Fatal("expected mapped resources to be nil when validation fails") } - if !errors.Is(err, constants.ErrInvalidInput) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("expected ErrInvalidInput, got: %v", err) } } @@ -272,29 +271,29 @@ func TestMapProxyModelToAPI_DoesNotExposeProviderAuthValue(t *testing.T) { func TestValidateLLMResourceLimit(t *testing.T) { t.Run("below limit should pass", func(t *testing.T) { - err := validateLLMResourceLimit(4, 5, constants.ErrLLMProviderLimitReached) + err := validateLLMResourceLimit(4, 5, apperror.LLMProviderLimitReached.New()) if err != nil { t.Fatalf("expected no error below limit, got: %v", err) } }) t.Run("at limit should fail", func(t *testing.T) { - err := validateLLMResourceLimit(5, 5, constants.ErrLLMProviderLimitReached) - if err != constants.ErrLLMProviderLimitReached { + err := validateLLMResourceLimit(5, 5, apperror.LLMProviderLimitReached.New()) + if !apperror.LLMProviderLimitReached.Is(err) { t.Fatalf("expected ErrLLMProviderLimitReached, got: %v", err) } }) t.Run("above limit should fail", func(t *testing.T) { - err := validateLLMResourceLimit(6, 5, constants.ErrLLMProxyLimitReached) - if err != constants.ErrLLMProxyLimitReached { + err := validateLLMResourceLimit(6, 5, apperror.LLMProxyLimitReached.New()) + if !apperror.LLMProxyLimitReached.Is(err) { t.Fatalf("expected ErrLLMProxyLimitReached, got: %v", err) } }) t.Run("unlimited (limit <= 0) should always pass", func(t *testing.T) { for _, limit := range []int{0, -1} { - if err := validateLLMResourceLimit(1_000_000, limit, constants.ErrLLMProviderLimitReached); err != nil { + if err := validateLLMResourceLimit(1_000_000, limit, apperror.LLMProviderLimitReached.New()); err != nil { t.Fatalf("expected no error for unlimited (limit=%d), got: %v", limit, err) } } @@ -1116,7 +1115,7 @@ func TestLLMProviderServiceCreateRejectsMultipleModelProvidersForNativeTemplate( } _, err := service.Create("org-1", "alice", request) - if err != constants.ErrInvalidInput { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("expected ErrInvalidInput, got: %v", err) } if providerRepo.createCalled { @@ -1246,7 +1245,7 @@ func TestLLMProviderServiceCreateRejectsInvalidGlobalPolicyVersion(t *testing.T) request.GlobalPolicies = &[]api.Policy{{Name: "api-key-auth", Version: "v1.0.0"}} _, err := service.Create("org-1", "alice", request) - if !errors.Is(err, constants.ErrInvalidPolicyVersion) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("expected ErrInvalidPolicyVersion, got: %v", err) } } @@ -1258,7 +1257,7 @@ func TestLLMProviderServiceCreateRejectsInvalidOperationPolicyVersion(t *testing request.OperationPolicies = &[]api.OperationPolicy{{Name: "token-ratelimit", Version: "1"}} _, err := service.Create("org-1", "alice", request) - if !errors.Is(err, constants.ErrInvalidPolicyVersion) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("expected ErrInvalidPolicyVersion, got: %v", err) } } @@ -1270,7 +1269,7 @@ func TestLLMProviderServiceCreateRejectsInvalidLegacyPolicyVersion(t *testing.T) request.Policies = &[]api.LLMPolicy{{Name: "basic-ratelimit", Version: "V1"}} _, err := service.Create("org-1", "alice", request) - if !errors.Is(err, constants.ErrInvalidPolicyVersion) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("expected ErrInvalidPolicyVersion, got: %v", err) } } @@ -1287,7 +1286,7 @@ func TestLLMProviderServiceUpdateRejectsInvalidPolicyVersion(t *testing.T) { request.GlobalPolicies = &[]api.Policy{{Name: "api-key-auth", Version: "v1.0.0"}} _, err := service.Update("org-1", "provider-1", "alice", request) - if !errors.Is(err, constants.ErrInvalidPolicyVersion) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("expected ErrInvalidPolicyVersion, got: %v", err) } } @@ -1302,7 +1301,7 @@ func TestLLMProviderServiceCreateReturnsConflictForDuplicateHandle(t *testing.T) service := NewLLMProviderService(providerRepo, templateRepo, nil, nil, nil, nil, nil, slog.Default(), &noopAuditRepo{}, &config.Server{}, newTestIdentityService()) _, err := service.Create("org-1", "alice", validProviderRequest("openai")) - if err != constants.ErrLLMProviderExists { + if !apperror.LLMProviderExists.Is(err) { t.Fatalf("expected ErrLLMProviderExists, got: %v", err) } } @@ -1374,7 +1373,7 @@ func TestLLMProxyServiceCreateFailsWhenProviderNotFound(t *testing.T) { service := NewLLMProxyService(proxyRepo, providerRepo, projectRepo, nil, nil, nil, slog.Default(), &noopAuditRepo{}, &config.Server{}, newTestIdentityService()) _, err := service.Create("org-1", "alice", validProxyRequest("provider-1", "project-1")) - if err != constants.ErrLLMProviderNotFound { + if !apperror.LLMProviderNotFound.Is(err) { t.Fatalf("expected ErrLLMProviderNotFound, got: %v", err) } } @@ -1392,7 +1391,7 @@ func TestLLMProxyServiceCreateRejectsInvalidPolicyVersion(t *testing.T) { request.GlobalPolicies = &[]api.Policy{{Name: "api-key-auth", Version: "v1.0.0"}} _, err := service.Create("org-1", "alice", request) - if !errors.Is(err, constants.ErrInvalidPolicyVersion) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("expected ErrInvalidPolicyVersion, got: %v", err) } } @@ -1410,7 +1409,7 @@ func TestLLMProxyServiceUpdateRejectsInvalidPolicyVersion(t *testing.T) { request.OperationPolicies = &[]api.OperationPolicy{{Name: "token-ratelimit", Version: "1"}} _, err := service.Update("org-1", "proxy-1", "alice", request) - if !errors.Is(err, constants.ErrInvalidPolicyVersion) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("expected ErrInvalidPolicyVersion, got: %v", err) } } @@ -1425,7 +1424,7 @@ func TestLLMProxyServiceCreateReturnsConflictForDuplicateHandle(t *testing.T) { service := NewLLMProxyService(proxyRepo, providerRepo, nil, nil, nil, nil, slog.Default(), &noopAuditRepo{}, &config.Server{}, newTestIdentityService()) _, err := service.Create("org-1", "alice", validProxyRequest("provider-1", "project-1")) - if err != constants.ErrLLMProxyExists { + if !apperror.LLMProxyExists.Is(err) { t.Fatalf("expected ErrLLMProxyExists, got: %v", err) } } @@ -1522,10 +1521,10 @@ func TestLLMProxyServiceUpdatePreservesProviderAuthValue(t *testing.T) { func validProviderRequest(template string) *api.LLMProvider { return &api.LLMProvider{ - Id: strPointer("provider-1"), - DisplayName: "Test Provider", - Version: "v1.0", - Template: template, + Id: strPointer("provider-1"), + DisplayName: "Test Provider", + Version: "v1.0", + Template: template, Upstream: api.Upstream{ Main: api.UpstreamDefinition{Url: stringPtr("https://example.com/openai/v1")}, }, @@ -1535,10 +1534,10 @@ func validProviderRequest(template string) *api.LLMProvider { func validProxyRequest(providerID, projectID string) *api.LLMProxy { return &api.LLMProxy{ - Id: strPointer("proxy-1"), - DisplayName: "Test Proxy", - Version: "v1.0", - ProjectId: projectID, + Id: strPointer("proxy-1"), + DisplayName: "Test Proxy", + Version: "v1.0", + ProjectId: projectID, Provider: api.LLMProxyProvider{ Id: providerID, }, diff --git a/platform-api/internal/service/mcp.go b/platform-api/internal/service/mcp.go index fd21df2a2..5d154ca11 100644 --- a/platform-api/internal/service/mcp.go +++ b/platform-api/internal/service/mcp.go @@ -30,6 +30,7 @@ import ( "github.com/wso2/api-platform/platform-api/api" "github.com/wso2/api-platform/platform-api/config" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/repository" @@ -111,17 +112,17 @@ func (s *MCPProxyService) WithSecretService(ss *SecretService) { // Create creates a new MCP proxy func (s *MCPProxyService) Create(orgUUID, createdBy string, req *api.MCPProxy) (*api.MCPProxy, error) { if req == nil { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("A request body is required.") } if req.DisplayName == "" || req.Version == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The displayName and version fields are required.") } if err := validatePolicyVersions(req.Policies); err != nil { return nil, err } if req.Upstream.Main.Url == nil || *req.Upstream.Main.Url == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The upstream main url field is required.") } // req.ProjectId is the project handle; resolve it to the project UUID so the @@ -138,7 +139,7 @@ func (s *MCPProxyService) Create(orgUUID, createdBy string, req *api.MCPProxy) ( return nil, fmt.Errorf("failed to validate project: %w", err) } if project == nil || project.OrganizationID != orgUUID { - return nil, constants.ErrProjectNotFound + return nil, apperror.ProjectRefNotFound.New() } projectUUID = &project.ID } @@ -152,7 +153,7 @@ func (s *MCPProxyService) Create(orgUUID, createdBy string, req *api.MCPProxy) ( return nil, fmt.Errorf("failed to check MCP proxy exists: %w", err) } if exists { - return nil, constants.ErrMCPProxyExists + return nil, apperror.MCPProxyExists.New() } } else { var err error @@ -172,7 +173,7 @@ func (s *MCPProxyService) Create(orgUUID, createdBy string, req *api.MCPProxy) ( return nil, fmt.Errorf("failed to count existing MCP proxies: %w", err) } if config.LimitReached(proxyCount, s.cfg.ArtifactLimits.MaxMCPProxiesPerOrg) { - return nil, constants.ErrMCPProxyLimitReached + return nil, apperror.MCPProxyLimitReached.New() } // Validate {{ secret "..." }} placeholders in the upstream config @@ -213,13 +214,13 @@ func (s *MCPProxyService) Create(orgUUID, createdBy string, req *api.MCPProxy) ( Policies: mapMCPPoliciesAPIToModel(req.Policies), Capabilities: mapMcpCapabilitiesAPIToModel(req.Capabilities), }, - Origin: constants.OriginCP, + Origin: constants.OriginCP, AssociatedGateways: associatedGateways, } if err := s.repo.Create(m); err != nil { if isSQLiteUniqueConstraint(err) { - return nil, constants.ErrMCPProxyExists + return nil, apperror.MCPProxyExists.Wrap(err) } return nil, fmt.Errorf("failed to create MCP proxy: %w", err) } @@ -277,7 +278,7 @@ func (s *MCPProxyService) ListByProject(orgUUID, projectHandle string, limit, of return nil, fmt.Errorf("failed to validate project: %w", err) } if project == nil || project.OrganizationID != orgUUID { - return nil, constants.ErrProjectNotFound + return nil, apperror.ProjectRefNotFound.New() } projectUUID := project.ID @@ -318,7 +319,7 @@ func (s *MCPProxyService) ListByProject(orgUUID, projectHandle string, limit, of // Get retrieves an MCP proxy by its handle func (s *MCPProxyService) Get(orgUUID, handle string) (*api.MCPProxy, error) { if handle == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The MCP proxy id is required.") } m, err := s.repo.GetByHandle(handle, orgUUID) @@ -326,7 +327,7 @@ func (s *MCPProxyService) Get(orgUUID, handle string) (*api.MCPProxy, error) { return nil, fmt.Errorf("failed to get MCP proxy: %w", err) } if m == nil { - return nil, constants.ErrMCPProxyNotFound + return nil, apperror.MCPProxyNotFound.New() } return s.toMCPProxyAPI(m) @@ -335,17 +336,17 @@ func (s *MCPProxyService) Get(orgUUID, handle string) (*api.MCPProxy, error) { // Update updates an existing MCP proxy func (s *MCPProxyService) Update(orgUUID, handle, updatedBy string, req *api.MCPProxy) (*api.MCPProxy, error) { if handle == "" || req == nil { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The MCP proxy id and a request body are required.") } if req.DisplayName == "" || req.Version == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The displayName and version fields are required.") } if err := validatePolicyVersions(req.Policies); err != nil { return nil, err } if req.Upstream.Main.Url == nil || *req.Upstream.Main.Url == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The upstream main url field is required.") } // Get existing proxy @@ -354,7 +355,7 @@ func (s *MCPProxyService) Update(orgUUID, handle, updatedBy string, req *api.MCP return nil, fmt.Errorf("failed to get MCP proxy: %w", err) } if existing == nil { - return nil, constants.ErrMCPProxyNotFound + return nil, apperror.MCPProxyNotFound.New() } // Validate {{ secret "..." }} placeholders in the upstream config @@ -422,7 +423,7 @@ func (s *MCPProxyService) Update(orgUUID, handle, updatedBy string, req *api.MCP if err := s.repo.Update(existing); err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, constants.ErrMCPProxyNotFound + return nil, apperror.MCPProxyNotFound.Wrap(err) } return nil, fmt.Errorf("failed to update MCP proxy: %w", err) } @@ -434,7 +435,7 @@ func (s *MCPProxyService) Update(orgUUID, handle, updatedBy string, req *api.MCP // Delete deletes an MCP proxy by its handle func (s *MCPProxyService) Delete(orgUUID, handle, deletedBy string) error { if handle == "" { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("The MCP proxy id is required.") } // Get the MCP proxy UUID before deletion (needed for deployment lookup) @@ -443,7 +444,7 @@ func (s *MCPProxyService) Delete(orgUUID, handle, deletedBy string) error { return fmt.Errorf("failed to get MCP proxy: %w", err) } if mcpProxy == nil { - return constants.ErrMCPProxyNotFound + return apperror.MCPProxyNotFound.New() } // DP-originated artifacts may only be deleted once undeployed on all gateways. @@ -467,7 +468,7 @@ func (s *MCPProxyService) Delete(orgUUID, handle, deletedBy string) error { if err := s.repo.Delete(handle, orgUUID); err != nil { if errors.Is(err, sql.ErrNoRows) { - return constants.ErrMCPProxyNotFound + return apperror.MCPProxyNotFound.Wrap(err) } return fmt.Errorf("failed to delete MCP proxy: %w", err) } @@ -496,7 +497,7 @@ func (s *MCPProxyService) Delete(orgUUID, handle, deletedBy string) error { // When proxyId is not provided, url is required and auth is optional. func (s *MCPProxyService) FetchServerInfo(orgUUID string, req *api.MCPServerInfoFetchRequest) (*api.MCPServerInfoFetchResponse, error) { if req == nil { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("A request body is required.") } var url string @@ -513,7 +514,7 @@ func (s *MCPProxyService) FetchServerInfo(orgUUID string, req *api.MCPServerInfo return nil, fmt.Errorf("failed to get MCP proxy: %w", err) } if proxy == nil { - return nil, constants.ErrMCPProxyNotFound + return nil, apperror.MCPProxyNotFound.New() } // Use stored URL from proxy configuration @@ -523,7 +524,7 @@ func (s *MCPProxyService) FetchServerInfo(orgUUID string, req *api.MCPServerInfo normalizedURL, err := ensureMCPEndpointURL(url) if err != nil { - return nil, fmt.Errorf("%w: %v", constants.ErrInvalidURL, err) + return nil, apperror.ValidationFailed.Wrap(err, "The configured MCP server URL is invalid.") } url = normalizedURL @@ -535,7 +536,7 @@ func (s *MCPProxyService) FetchServerInfo(orgUUID string, req *api.MCPServerInfo } else { // No proxyId - initial creation flow, url is required if req.Url == nil || *req.Url == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The url field is required when proxyId is not provided.") } url = *req.Url @@ -547,11 +548,13 @@ func (s *MCPProxyService) FetchServerInfo(orgUUID string, req *api.MCPServerInfo } if err := utils.ValidateURL(url); err != nil { - return nil, fmt.Errorf("%w: %v", constants.ErrInvalidURL, err) + return nil, apperror.ValidationFailed.Wrap(err, "The provided URL is invalid."). + WithLogMessage(fmt.Sprintf("invalid MCP server URL: %s", url)) } if err := utils.CheckURLReachability(url, 10*time.Second); err != nil { - return nil, fmt.Errorf("%w: %v", constants.ErrURLUnreachable, err) + return nil, apperror.ValidationFailed.Wrap(err, "The provided URL is unreachable."). + WithLogMessage(fmt.Sprintf("MCP server URL is unreachable: %s", url)) } return utils.FetchMCPServerInfo(url, headerName, headerValue) diff --git a/platform-api/internal/service/mcp_deployment.go b/platform-api/internal/service/mcp_deployment.go index a955cd8c6..4c40dd9e5 100644 --- a/platform-api/internal/service/mcp_deployment.go +++ b/platform-api/internal/service/mcp_deployment.go @@ -20,8 +20,11 @@ package service import ( - "errors" "fmt" + "log/slog" + "strings" + "time" + "github.com/wso2/api-platform/platform-api/api" "github.com/wso2/api-platform/platform-api/config" "github.com/wso2/api-platform/platform-api/internal/apperror" @@ -29,9 +32,6 @@ import ( "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/repository" "github.com/wso2/api-platform/platform-api/internal/utils" - "log/slog" - "strings" - "time" "gopkg.in/yaml.v3" ) @@ -91,7 +91,7 @@ func (s *MCPDeploymentService) RestoreMCPDeploymentByHandle(proxyHandle, deploym return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } gatewayUUID := gateway.ID @@ -114,7 +114,7 @@ func (s *MCPDeploymentService) UndeployDeploymentByHandle(proxyHandle, deploymen return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } gatewayUUID := gateway.ID @@ -178,14 +178,14 @@ func (s *MCPDeploymentService) GetDeploymentsByHandle(proxyHandle, gatewayID, st // deployMCPProxy deploys an MCP proxy to a gateway func (s *MCPDeploymentService) deployMCPProxy(proxyUUID string, req *api.DeployRequest, orgId, createdBy string) (*api.DeploymentResponse, error) { if req == nil { - return nil, constants.ErrInvalidInput + return nil, apperror.MCPProxyDeploymentValidationFailed.New("A request body is required.") } if req.Base == "" { - return nil, constants.ErrDeploymentBaseRequired + return nil, apperror.MCPProxyDeploymentValidationFailed.New("Base is required.") } gatewayHandle := strings.TrimSpace(req.GatewayId) if gatewayHandle == "" { - return nil, constants.ErrDeploymentGatewayIDRequired + return nil, apperror.MCPProxyDeploymentValidationFailed.New("Gateway ID is required.") } metadata := utils.MapValueOrEmpty(req.Metadata) @@ -195,7 +195,7 @@ func (s *MCPDeploymentService) deployMCPProxy(proxyUUID string, req *api.DeployR return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } gatewayID := gateway.ID @@ -204,7 +204,7 @@ func (s *MCPDeploymentService) deployMCPProxy(proxyUUID string, req *api.DeployR return nil, err } if mcpProxy == nil { - return nil, constants.ErrMCPProxyNotFound + return nil, apperror.MCPProxyNotFound.New() } // DP-originated artifacts are read-only in the control plane and cannot be @@ -273,8 +273,8 @@ func (s *MCPDeploymentService) deployMCPProxy(proxyUUID string, req *api.DeployR // Use existing deployment as base baseDeployment, err := s.deploymentRepo.GetWithContent(req.Base, proxyUUID, orgId) if err != nil { - if errors.Is(err, constants.ErrDeploymentNotFound) { - return nil, constants.ErrBaseDeploymentNotFound + if apperror.DeploymentNotFound.Is(err) { + return nil, apperror.DeploymentBaseNotFound.Wrap(err) } return nil, fmt.Errorf("failed to get base deployment: %w", err) } @@ -371,13 +371,15 @@ func extractEndpointURLOverride(metadata map[string]interface{}) (*string, error } eu, ok := v.(string) if !ok { - return nil, fmt.Errorf("%w: invalid endpoint URL in metadata: expected string, got %T", constants.ErrInvalidInput, v) + return nil, apperror.MCPProxyDeploymentValidationFailed.New( + fmt.Sprintf("The endpointUrl in metadata must be a string, got %T.", v)) } if eu == "" { return nil, nil } if err := validateEndpointURL(eu); err != nil { - return nil, fmt.Errorf("%w: invalid endpoint URL in metadata: %v", constants.ErrInvalidInput, err) + return nil, apperror.MCPProxyDeploymentValidationFailed.Wrap(err, + "The endpointUrl in metadata is not a valid URL.") } return &eu, nil } @@ -396,7 +398,7 @@ func (s *MCPDeploymentService) undeployMCPProxyDeployment(proxyUUID string, depl return nil, err } if mcpProxy == nil { - return nil, constants.ErrMCPProxyNotFound + return nil, apperror.MCPProxyNotFound.New() } // Resolve deployment using provided deploymentId or gatewayId @@ -408,7 +410,7 @@ func (s *MCPDeploymentService) undeployMCPProxyDeployment(proxyUUID string, depl return nil, err } if deployment == nil { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } } else if gatewayId != nil { // Find current deployment for this gateway @@ -417,20 +419,21 @@ func (s *MCPDeploymentService) undeployMCPProxyDeployment(proxyUUID string, depl return nil, err } if deployment == nil { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } } else { - return nil, constants.ErrInvalidInput + return nil, apperror.MCPProxyDeploymentValidationFailed.New( + "Either a deploymentId or a gatewayId is required.") } // Validate that the provided gatewayId matches the deployment's bound gateway if gatewayId != nil && deployment.GatewayID != *gatewayId { - return nil, constants.ErrGatewayIDMismatch + return nil, apperror.DeploymentGatewayMismatch.New() } // Verify deployment is currently DEPLOYED (status already populated by GetWithState) if deployment.Status == nil || *deployment.Status != model.DeploymentStatusDeployed { - return nil, constants.ErrDeploymentNotActive + return nil, apperror.DeploymentNotActive.New("MCP proxy") } // Validate gateway exists and belongs to organization @@ -439,7 +442,7 @@ func (s *MCPDeploymentService) undeployMCPProxyDeployment(proxyUUID string, depl return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } // Set initial status based on config; transitional (UNDEPLOYING) only when enabled @@ -498,12 +501,12 @@ func (s *MCPDeploymentService) restoreMCPProxyDeployment(proxyUUID string, deplo return nil, err } if targetDeployment == nil { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } // Validate that the provided gatewayID matches the deployment's bound gateway if targetDeployment.GatewayID != *gatewayId { - return nil, constants.ErrGatewayIDMismatch + return nil, apperror.DeploymentGatewayMismatch.New() } // Verify target deployment is NOT currently DEPLOYED @@ -512,7 +515,7 @@ func (s *MCPDeploymentService) restoreMCPProxyDeployment(proxyUUID string, deplo return nil, fmt.Errorf("failed to get deployment status: %w", err) } if currentDeploymentID == *deploymentId && status == model.DeploymentStatusDeployed { - return nil, constants.ErrDeploymentAlreadyDeployed + return nil, apperror.DeploymentRestoreConflict.New() } // Validate gateway exists and belongs to organization @@ -521,7 +524,7 @@ func (s *MCPDeploymentService) restoreMCPProxyDeployment(proxyUUID string, deplo return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil || gateway.OrganizationID != orgId { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } // Set initial status based on config; transitional (DEPLOYING) only when enabled @@ -574,7 +577,7 @@ func (s *MCPDeploymentService) getMCPProxyDeployment(proxyUUID string, deploymen return nil, err } if mcpProxy == nil { - return nil, constants.ErrMCPProxyNotFound + return nil, apperror.MCPProxyNotFound.New() } // Get deployment with state derived via LEFT JOIN @@ -583,7 +586,7 @@ func (s *MCPDeploymentService) getMCPProxyDeployment(proxyUUID string, deploymen return nil, err } if deployment == nil { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } return toAPIDeploymentResponse( @@ -608,7 +611,7 @@ func (s *MCPDeploymentService) getMCPProxyDeployments(proxyUUID string, orgId st return nil, err } if mcpProxy == nil { - return nil, constants.ErrMCPProxyNotFound + return nil, apperror.MCPProxyNotFound.New() } // Validate status parameter @@ -622,7 +625,7 @@ func (s *MCPDeploymentService) getMCPProxyDeployments(proxyUUID string, orgId st string(model.DeploymentStatusFailed): true, } if !validStatuses[*status] { - return nil, constants.ErrInvalidDeploymentStatus + return nil, apperror.DeploymentInvalidStatus.New() } } @@ -670,7 +673,7 @@ func (s *MCPDeploymentService) deleteMCPProxyDeployment(proxyUUID string, deploy return err } if mcpProxy == nil { - return constants.ErrMCPProxyNotFound + return apperror.MCPProxyNotFound.New() } // Verify deployment exists and belongs to the MCP proxy @@ -679,12 +682,12 @@ func (s *MCPDeploymentService) deleteMCPProxyDeployment(proxyUUID string, deploy return err } if deployment == nil { - return constants.ErrDeploymentNotFound + return apperror.DeploymentNotFound.New() } // Verify deployment is NOT currently DEPLOYED (status already populated by GetWithState) if deployment.Status != nil && *deployment.Status == model.DeploymentStatusDeployed { - return constants.ErrDeploymentIsDeployed + return apperror.DeploymentActive.New() } // Delete the deployment artifact @@ -706,7 +709,7 @@ func (s *MCPDeploymentService) getMCPProxyUUIDByHandle(handle, orgUUID string) ( return "", err } if artifact == nil { - return "", constants.ErrArtifactNotFound + return "", apperror.ArtifactNotFound.New() } return artifact.UUID, nil diff --git a/platform-api/internal/service/mcp_test.go b/platform-api/internal/service/mcp_test.go index deb036475..6c3b7f904 100644 --- a/platform-api/internal/service/mcp_test.go +++ b/platform-api/internal/service/mcp_test.go @@ -18,13 +18,12 @@ package service import ( - "errors" "log/slog" "testing" "github.com/wso2/api-platform/platform-api/api" "github.com/wso2/api-platform/platform-api/config" - "github.com/wso2/api-platform/platform-api/internal/constants" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/repository" ) @@ -58,7 +57,7 @@ func TestMCPProxyServiceCreateRejectsInvalidPolicyVersion(t *testing.T) { request.Policies = &[]api.Policy{{Name: "api-key-auth", Version: "v1.0.0"}} _, err := service.Create("org-1", "alice", request) - if !errors.Is(err, constants.ErrInvalidPolicyVersion) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("expected ErrInvalidPolicyVersion, got: %v", err) } } @@ -71,7 +70,7 @@ func TestMCPProxyServiceUpdateRejectsInvalidPolicyVersion(t *testing.T) { request.Policies = &[]api.Policy{{Name: "api-key-auth", Version: "1"}} _, err := service.Update("org-1", "mcp-proxy-1", "alice", request) - if !errors.Is(err, constants.ErrInvalidPolicyVersion) { + if !apperror.ValidationFailed.Is(err) { t.Fatalf("expected ErrInvalidPolicyVersion, got: %v", err) } } diff --git a/platform-api/internal/service/policy_validation.go b/platform-api/internal/service/policy_validation.go index 270969ec7..59fc82cc0 100644 --- a/platform-api/internal/service/policy_validation.go +++ b/platform-api/internal/service/policy_validation.go @@ -23,7 +23,7 @@ import ( "strings" "github.com/wso2/api-platform/platform-api/api" - "github.com/wso2/api-platform/platform-api/internal/constants" + "github.com/wso2/api-platform/platform-api/internal/apperror" ) // policyVersionPattern mirrors the `pattern: '^v\d+$'` constraint on Policy.version, @@ -50,7 +50,8 @@ func validatePolicyVersions(policies *[]api.Policy) error { } for _, p := range *policies { if !isValidPolicyVersion(p.Version) { - return fmt.Errorf("%w: policy %q version %q", constants.ErrInvalidPolicyVersion, p.Name, p.Version) + return apperror.ValidationFailed.New(fmt.Sprintf( + "Policy %q has an invalid version %q. Policy versions must be major-only, e.g. v1.", p.Name, p.Version)) } } return nil @@ -63,7 +64,8 @@ func validateLLMPolicyVersions(policies *[]api.LLMPolicy) error { } for _, p := range *policies { if !isValidPolicyVersion(p.Version) { - return fmt.Errorf("%w: policy %q version %q", constants.ErrInvalidPolicyVersion, p.Name, p.Version) + return apperror.ValidationFailed.New(fmt.Sprintf( + "Policy %q has an invalid version %q. Policy versions must be major-only, e.g. v1.", p.Name, p.Version)) } } return nil @@ -76,7 +78,8 @@ func validateOperationPolicyVersions(policies *[]api.OperationPolicy) error { } for _, p := range *policies { if !isValidPolicyVersion(p.Version) { - return fmt.Errorf("%w: policy %q version %q", constants.ErrInvalidPolicyVersion, p.Name, p.Version) + return apperror.ValidationFailed.New(fmt.Sprintf( + "Policy %q has an invalid version %q. Policy versions must be major-only, e.g. v1.", p.Name, p.Version)) } } return nil diff --git a/platform-api/internal/service/policy_validation_test.go b/platform-api/internal/service/policy_validation_test.go index 84a4f19e1..59594d124 100644 --- a/platform-api/internal/service/policy_validation_test.go +++ b/platform-api/internal/service/policy_validation_test.go @@ -18,11 +18,10 @@ package service import ( - "errors" "testing" "github.com/wso2/api-platform/platform-api/api" - "github.com/wso2/api-platform/platform-api/internal/constants" + "github.com/wso2/api-platform/platform-api/internal/apperror" ) func TestIsValidPolicyVersion(t *testing.T) { @@ -65,7 +64,7 @@ func TestValidatePolicyVersions(t *testing.T) { } invalid := []api.Policy{{Name: "SET_HEADER", Version: "v1.0.0"}} err := validatePolicyVersions(&invalid) - if !errors.Is(err, constants.ErrInvalidPolicyVersion) { + if !apperror.ValidationFailed.Is(err) { t.Errorf("expected ErrInvalidPolicyVersion, got %v", err) } } @@ -76,7 +75,7 @@ func TestValidateLLMPolicyVersions(t *testing.T) { } invalid := []api.LLMPolicy{{Name: "API_KEY_AUTH", Version: "1.0"}} err := validateLLMPolicyVersions(&invalid) - if !errors.Is(err, constants.ErrInvalidPolicyVersion) { + if !apperror.ValidationFailed.Is(err) { t.Errorf("expected ErrInvalidPolicyVersion, got %v", err) } } @@ -87,7 +86,7 @@ func TestValidateOperationPolicyVersions(t *testing.T) { } invalid := []api.OperationPolicy{{Name: "RATE_LIMIT", Version: "v1.2"}} err := validateOperationPolicyVersions(&invalid) - if !errors.Is(err, constants.ErrInvalidPolicyVersion) { + if !apperror.ValidationFailed.Is(err) { t.Errorf("expected ErrInvalidPolicyVersion, got %v", err) } } @@ -105,7 +104,7 @@ func TestValidateOperationAndChannelPolicyVersions(t *testing.T) { t.Run("bad operation policy version", func(t *testing.T) { operations := []api.Operation{{Request: api.OperationRequest{Policies: &badPolicies}}} err := validateOperationAndChannelPolicyVersions(&operations, nil) - if !errors.Is(err, constants.ErrInvalidPolicyVersion) { + if !apperror.ValidationFailed.Is(err) { t.Errorf("expected ErrInvalidPolicyVersion, got %v", err) } }) @@ -113,7 +112,7 @@ func TestValidateOperationAndChannelPolicyVersions(t *testing.T) { t.Run("bad channel policy version", func(t *testing.T) { channels := []api.Channel{{Request: api.ChannelRequest{Policies: &badPolicies}}} err := validateOperationAndChannelPolicyVersions(nil, &channels) - if !errors.Is(err, constants.ErrInvalidPolicyVersion) { + if !apperror.ValidationFailed.Is(err) { t.Errorf("expected ErrInvalidPolicyVersion, got %v", err) } }) diff --git a/platform-api/internal/service/secret_service.go b/platform-api/internal/service/secret_service.go index 28877d457..5ed1fb6d3 100644 --- a/platform-api/internal/service/secret_service.go +++ b/platform-api/internal/service/secret_service.go @@ -23,6 +23,7 @@ import ( "crypto/sha256" "errors" "fmt" + "strings" "time" "github.com/wso2/api-platform/platform-api/internal/apperror" @@ -38,7 +39,7 @@ type SecretInUseError struct { } func (e *SecretInUseError) Error() string { - return constants.ErrSecretInUse.Error() + return "secret is referenced by one or more resources" } type SecretService struct { @@ -91,7 +92,7 @@ func (s *SecretService) Create(orgID, createdBy string, req *dto.CreateSecretReq if secretType == "" { secretType = model.SecretTypeGeneric } else if secretType != model.SecretTypeGeneric && secretType != model.SecretTypeCertificate { - return nil, apperror.ValidationFailed.Wrap(constants.ErrInvalidSecretType, "Invalid secret type: must be GENERIC or CERTIFICATE") + return nil, apperror.ValidationFailed.New("The secret type must be one of GENERIC or CERTIFICATE.") } exists, err := s.repo.Exists(orgID, req.Handle) @@ -99,7 +100,7 @@ func (s *SecretService) Create(orgID, createdBy string, req *dto.CreateSecretReq return nil, fmt.Errorf("failed to check secret existence: %w", err) } if exists { - return nil, apperror.SecretExists.Wrap(constants.ErrSecretAlreadyExists) + return nil, apperror.SecretExists.New() } ciphertext, err := s.vault.Encrypt(context.Background(), req.Value) @@ -239,8 +240,8 @@ func (s *SecretService) ValidateSecretRefs(orgID, configText string) error { } if len(missing) > 0 { - return apperror.ValidationFailed.Wrap(fmt.Errorf("%w: %v", constants.ErrSecretRefMissing, missing), - "One or more referenced secrets do not exist") + return apperror.ValidationFailed.New(fmt.Sprintf( + "The following referenced secrets do not exist: %s.", strings.Join(missing, ", "))) } return nil } diff --git a/platform-api/internal/service/secret_service_test.go b/platform-api/internal/service/secret_service_test.go index 4989cbdf7..d02b75141 100644 --- a/platform-api/internal/service/secret_service_test.go +++ b/platform-api/internal/service/secret_service_test.go @@ -23,7 +23,7 @@ import ( "testing" "time" - "github.com/wso2/api-platform/platform-api/internal/constants" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/dto" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/repository" @@ -64,14 +64,14 @@ type mockSecretRepo struct { secrets map[string]*model.Secret - createFn func(*model.Secret) error - existsFn func(orgID, handle string) (bool, error) - getByHandleFn func(orgID, handle string) (*model.Secret, error) - updateFn func(*model.Secret) error + createFn func(*model.Secret) error + existsFn func(orgID, handle string) (bool, error) + getByHandleFn func(orgID, handle string) (*model.Secret, error) + updateFn func(*model.Secret) error findRefsAndSoftDeleteFn func(orgID, handle, by string) ([]model.SecretReference, error) findRefsFn func(orgID, handle string) ([]model.SecretReference, error) - listFn func(orgID string, limit, offset int, after *time.Time) ([]*model.Secret, error) - countFn func(orgID string) (int, error) + listFn func(orgID string, limit, offset int, after *time.Time) ([]*model.Secret, error) + countFn func(orgID string) (int, error) } func newMockRepo() *mockSecretRepo { @@ -83,7 +83,7 @@ func (m *mockSecretRepo) Create(s *model.Secret) error { return m.createFn(s) } if _, exists := m.secrets[s.Handle]; exists { - return constants.ErrSecretAlreadyExists + return apperror.SecretExists.New() } if s.UUID == "" { s.UUID = "uuid-" + s.Handle @@ -106,7 +106,7 @@ func (m *mockSecretRepo) GetByHandle(orgID, handle string) (*model.Secret, error } s, ok := m.secrets[handle] if !ok { - return nil, constants.ErrSecretNotFound + return nil, apperror.SecretNotFound.New() } return s, nil } @@ -116,7 +116,7 @@ func (m *mockSecretRepo) Update(s *model.Secret) error { return m.updateFn(s) } if _, ok := m.secrets[s.Handle]; !ok { - return constants.ErrSecretNotFound + return apperror.SecretNotFound.New() } m.secrets[s.Handle] = s return nil @@ -134,7 +134,7 @@ func (m *mockSecretRepo) FindRefsAndSoftDelete(orgID, handle, by string) ([]mode } s, ok := m.secrets[handle] if !ok { - return nil, constants.ErrSecretNotFound + return nil, apperror.SecretNotFound.New() } s.Status = model.SecretStatusDeprecated return nil, nil @@ -221,7 +221,7 @@ func TestSecretService_Create_DuplicateHandle_ReturnsError(t *testing.T) { Handle: "dup", Value: "val", }) - if !errors.Is(err, constants.ErrSecretAlreadyExists) { + if !apperror.SecretExists.Is(err) { t.Errorf("expected ErrSecretAlreadyExists, got %v", err) } } @@ -271,10 +271,11 @@ func TestSecretService_Create_InvalidType_ReturnsError(t *testing.T) { Value: "val", Type: "API_KEY", }) - if !errors.Is(err, constants.ErrInvalidSecretType) { + if !apperror.ValidationFailed.Is(err) { t.Errorf("expected ErrInvalidSecretType, got %v", err) } } + // ---- List tests ------------------------------------------------------------- func TestSecretService_List_ReturnsPagination(t *testing.T) { @@ -323,7 +324,7 @@ func TestSecretService_Get_NotFound(t *testing.T) { svc := NewSecretService(repo, &mockVault{}, newTestIdentityService()) _, err := svc.Get("org1", "missing") - if !errors.Is(err, constants.ErrSecretNotFound) { + if !apperror.SecretNotFound.Is(err) { t.Errorf("expected ErrSecretNotFound, got %v", err) } } @@ -353,7 +354,7 @@ func TestSecretService_Update_NotFound(t *testing.T) { svc := NewSecretService(repo, &mockVault{}, newTestIdentityService()) _, err := svc.Update("org1", "ghost", "alice", &dto.UpdateSecretRequest{Value: "v"}) - if !errors.Is(err, constants.ErrSecretNotFound) { + if !apperror.SecretNotFound.Is(err) { t.Errorf("expected ErrSecretNotFound, got %v", err) } } @@ -436,7 +437,7 @@ func TestSecretService_Delete_NotFound(t *testing.T) { svc := NewSecretService(repo, &mockVault{}, newTestIdentityService()) err := svc.Delete("org1", "ghost", "alice") - if !errors.Is(err, constants.ErrSecretNotFound) { + if !apperror.SecretNotFound.Is(err) { t.Errorf("expected ErrSecretNotFound, got %v", err) } } @@ -473,7 +474,7 @@ func TestSecretService_ValidateSecretRefs_MissingHandle(t *testing.T) { if err == nil { t.Fatal("expected error for missing secret handle") } - if !errors.Is(err, constants.ErrSecretRefMissing) { + if !apperror.ValidationFailed.Is(err) { t.Errorf("expected ErrSecretRefMissing, got %v", err) } } @@ -500,7 +501,7 @@ func TestSecretService_ValidateSecretRefs_JSONEscapedForm_Missing(t *testing.T) if err == nil { t.Fatal("expected error for missing JSON-escaped secret handle") } - if !errors.Is(err, constants.ErrSecretRefMissing) { + if !apperror.ValidationFailed.Is(err) { t.Errorf("expected ErrSecretRefMissing, got %v", err) } } diff --git a/platform-api/internal/service/subscription_plan_service.go b/platform-api/internal/service/subscription_plan_service.go index a2540f073..ef3a815ac 100644 --- a/platform-api/internal/service/subscription_plan_service.go +++ b/platform-api/internal/service/subscription_plan_service.go @@ -93,7 +93,7 @@ func (s *SubscriptionPlanService) CreatePlan(orgUUID, actor string, plan *model. return nil, err } if exists { - return nil, apperror.SubscriptionPlanExists.Wrap(constants.ErrSubscriptionPlanAlreadyExists) + return nil, apperror.SubscriptionPlanExists.New() } plan.OrganizationUUID = orgUUID @@ -103,7 +103,7 @@ func (s *SubscriptionPlanService) CreatePlan(orgUUID, actor string, plan *model. plan.Status = model.SubscriptionPlanStatusActive } if plan.ThrottleLimitUnit != "" && !constants.ValidThrottleLimitUnits[plan.ThrottleLimitUnit] { - return nil, apperror.ValidationFailed.Wrap(constants.ErrInvalidThrottleLimitUnit, "Invalid throttle limit unit") + return nil, apperror.ValidationFailed.New("Invalid throttle limit unit") } if err := s.planRepo.Create(plan); err != nil { @@ -133,12 +133,12 @@ func (s *SubscriptionPlanService) GetPlan(handle, orgUUID string) (*model.Subscr plan, err := s.planRepo.GetByHandleAndOrg(handle, orgUUID) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, apperror.SubscriptionPlanNotFound.Wrap(constants.ErrSubscriptionPlanNotFound) + return nil, apperror.SubscriptionPlanNotFound.New() } return nil, err } if plan == nil { - return nil, apperror.SubscriptionPlanNotFound.Wrap(constants.ErrSubscriptionPlanNotFound) + return nil, apperror.SubscriptionPlanNotFound.New() } return plan, nil } @@ -163,12 +163,12 @@ func (s *SubscriptionPlanService) UpdatePlan(handle, orgUUID, actor string, upda existing, err := s.planRepo.GetByHandleAndOrg(handle, orgUUID) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, apperror.SubscriptionPlanNotFound.Wrap(constants.ErrSubscriptionPlanNotFound) + return nil, apperror.SubscriptionPlanNotFound.New() } return nil, err } if existing == nil { - return nil, apperror.SubscriptionPlanNotFound.Wrap(constants.ErrSubscriptionPlanNotFound) + return nil, apperror.SubscriptionPlanNotFound.New() } if update.Name != nil { @@ -189,7 +189,7 @@ func (s *SubscriptionPlanService) UpdatePlan(handle, orgUUID, actor string, upda } if update.ThrottleLimitUnit != nil { if !constants.ValidThrottleLimitUnits[*update.ThrottleLimitUnit] { - return nil, apperror.ValidationFailed.Wrap(constants.ErrInvalidThrottleLimitUnit, "Invalid throttle limit unit") + return nil, apperror.ValidationFailed.New("Invalid throttle limit unit") } existing.ThrottleLimitUnit = *update.ThrottleLimitUnit } @@ -233,12 +233,12 @@ func (s *SubscriptionPlanService) DeletePlan(handle, orgUUID, actor string) erro existing, err := s.planRepo.GetByHandleAndOrg(handle, orgUUID) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return apperror.SubscriptionPlanNotFound.Wrap(constants.ErrSubscriptionPlanNotFound) + return apperror.SubscriptionPlanNotFound.New() } return err } if existing == nil { - return apperror.SubscriptionPlanNotFound.Wrap(constants.ErrSubscriptionPlanNotFound) + return apperror.SubscriptionPlanNotFound.New() } if err := s.planRepo.Delete(existing.UUID, orgUUID); err != nil { diff --git a/platform-api/internal/service/subscription_service.go b/platform-api/internal/service/subscription_service.go index d8923822a..ceec9462c 100644 --- a/platform-api/internal/service/subscription_service.go +++ b/platform-api/internal/service/subscription_service.go @@ -24,7 +24,6 @@ import ( "log/slog" "github.com/wso2/api-platform/platform-api/internal/apperror" - "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/repository" ) @@ -85,14 +84,14 @@ func (s *SubscriptionService) ResolveOrgHandle(orgUUID string) string { // where the caller specifies the kind so the handle is resolved against exactly one table. func (s *SubscriptionService) resolveArtifactUUIDByKind(apiId, kind, orgUUID string) (string, error) { if apiId == "" || kind == "" { - return "", apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) + return "", apperror.ArtifactNotFound.New() } metadata, err := s.artifactRepo.GetAPIMetadataByHandleAndKind(apiId, kind, orgUUID) if err != nil { return "", fmt.Errorf("failed to resolve artifact by handle and kind: %w", err) } if metadata == nil { - return "", apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) + return "", apperror.ArtifactNotFound.New() } return metadata.ID, nil } @@ -100,11 +99,11 @@ func (s *SubscriptionService) resolveArtifactUUIDByKind(apiId, kind, orgUUID str // resolveAPIUUID resolves apiId (handle or UUID) to rest_apis.uuid for the organization func (s *SubscriptionService) resolveAPIUUID(apiId, orgUUID string) (string, error) { if apiId == "" { - return "", apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) + return "", apperror.ArtifactNotFound.New() } apiModel, err := s.apiRepo.GetAPIByUUID(apiId, orgUUID) if err != nil { - if !errors.Is(err, constants.ErrAPINotFound) { + if !apperror.RESTAPINotFound.Is(err) { return "", fmt.Errorf("failed to resolve API by UUID: %w", err) } } else if apiModel != nil { @@ -113,13 +112,13 @@ func (s *SubscriptionService) resolveAPIUUID(apiId, orgUUID string) (string, err metadata, err := s.artifactRepo.GetAPIMetadataByHandle(apiId, orgUUID) if err != nil { - if errors.Is(err, constants.ErrAPINotFound) { - return "", apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) + if apperror.RESTAPINotFound.Is(err) { + return "", apperror.ArtifactNotFound.New() } return "", fmt.Errorf("failed to resolve API by handle: %w", err) } if metadata == nil { - return "", apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) + return "", apperror.ArtifactNotFound.New() } return metadata.ID, nil } @@ -180,7 +179,7 @@ func (s *SubscriptionService) CreateSubscription(apiId, kind, orgUUID string, su return nil, err } if exists { - return nil, apperror.SubscriptionExists.Wrap(constants.ErrSubscriptionAlreadyExists) + return nil, apperror.SubscriptionExists.New() } // subscriptionPlanId carries the Developer Portal subscription plan handle. Resolve it to the @@ -189,12 +188,12 @@ func (s *SubscriptionService) CreateSubscription(apiId, kind, orgUUID string, su plan, err := s.planRepo.GetByHandleAndOrg(*subscriptionPlanId, orgUUID) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, apperror.SubscriptionPlanNotFound.Wrap(constants.ErrSubscriptionPlanNotFound) + return nil, apperror.SubscriptionPlanNotFound.New() } return nil, err } if plan == nil { - return nil, apperror.SubscriptionPlanNotFound.Wrap(constants.ErrSubscriptionPlanNotFound) + return nil, apperror.SubscriptionPlanNotFound.New() } subscriptionPlanId = &plan.UUID } @@ -257,13 +256,13 @@ func (s *SubscriptionService) CreateSubscription(apiId, kind, orgUUID string, su func (s *SubscriptionService) GetSubscription(subscriptionId, orgUUID string) (*model.Subscription, error) { sub, err := s.subscriptionRepo.GetByID(subscriptionId, orgUUID) if err != nil { - if errors.Is(err, constants.ErrSubscriptionNotFound) { - return nil, apperror.SubscriptionNotFound.Wrap(constants.ErrSubscriptionNotFound) + if apperror.SubscriptionNotFound.Is(err) { + return nil, apperror.SubscriptionNotFound.New() } return nil, err } if sub == nil { - return nil, apperror.SubscriptionNotFound.Wrap(constants.ErrSubscriptionNotFound) + return nil, apperror.SubscriptionNotFound.New() } return sub, nil } @@ -316,16 +315,16 @@ func (s *SubscriptionService) FindByArtifactKindAndSubscriber(orgUUID, apiHandle func (s *SubscriptionService) UpdateSubscription(subscriptionId, orgUUID, subscriberID, status, actor string) (*model.Subscription, error) { sub, err := s.subscriptionRepo.GetByID(subscriptionId, orgUUID) if err != nil { - if errors.Is(err, constants.ErrSubscriptionNotFound) { - return nil, apperror.SubscriptionNotFound.Wrap(constants.ErrSubscriptionNotFound) + if apperror.SubscriptionNotFound.Is(err) { + return nil, apperror.SubscriptionNotFound.New() } return nil, err } if sub == nil { - return nil, apperror.SubscriptionNotFound.Wrap(constants.ErrSubscriptionNotFound) + return nil, apperror.SubscriptionNotFound.New() } if sub.SubscriberID != subscriberID { - return nil, apperror.SubscriptionForbidden.Wrap(constants.ErrSubscriptionSubscriberMismatch) + return nil, apperror.SubscriptionForbidden.New() } if status != "" { st := model.SubscriptionStatus(status) @@ -382,16 +381,16 @@ func (s *SubscriptionService) UpdateSubscription(subscriptionId, orgUUID, subscr func (s *SubscriptionService) ChangePlan(subscriptionId, orgUUID, subscriberID, planHandle string) (*model.Subscription, error) { sub, err := s.subscriptionRepo.GetByID(subscriptionId, orgUUID) if err != nil { - if errors.Is(err, constants.ErrSubscriptionNotFound) { - return nil, apperror.SubscriptionNotFound.Wrap(constants.ErrSubscriptionNotFound) + if apperror.SubscriptionNotFound.Is(err) { + return nil, apperror.SubscriptionNotFound.New() } return nil, err } if sub == nil { - return nil, apperror.SubscriptionNotFound.Wrap(constants.ErrSubscriptionNotFound) + return nil, apperror.SubscriptionNotFound.New() } if sub.SubscriberID != subscriberID { - return nil, apperror.SubscriptionForbidden.Wrap(constants.ErrSubscriptionSubscriberMismatch) + return nil, apperror.SubscriptionForbidden.New() } // planHandle carries the Developer Portal subscription plan handle. Resolve it to the plan's @@ -399,12 +398,12 @@ func (s *SubscriptionService) ChangePlan(subscriptionId, orgUUID, subscriberID, planRecord, err := s.planRepo.GetByHandleAndOrg(planHandle, orgUUID) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, apperror.SubscriptionPlanNotFound.Wrap(constants.ErrSubscriptionPlanNotFound) + return nil, apperror.SubscriptionPlanNotFound.New() } return nil, err } if planRecord == nil { - return nil, apperror.SubscriptionPlanNotFound.Wrap(constants.ErrSubscriptionPlanNotFound) + return nil, apperror.SubscriptionPlanNotFound.New() } plan := planRecord.UUID sub.SubscriptionPlanID = &plan @@ -451,16 +450,16 @@ func (s *SubscriptionService) RegenerateToken(subscriptionId, orgUUID, subscribe } sub, err := s.subscriptionRepo.GetByID(subscriptionId, orgUUID) if err != nil { - if errors.Is(err, constants.ErrSubscriptionNotFound) { - return nil, apperror.SubscriptionNotFound.Wrap(constants.ErrSubscriptionNotFound) + if apperror.SubscriptionNotFound.Is(err) { + return nil, apperror.SubscriptionNotFound.New() } return nil, err } if sub == nil { - return nil, apperror.SubscriptionNotFound.Wrap(constants.ErrSubscriptionNotFound) + return nil, apperror.SubscriptionNotFound.New() } if sub.SubscriberID != subscriberID { - return nil, apperror.SubscriptionForbidden.Wrap(constants.ErrSubscriptionSubscriberMismatch) + return nil, apperror.SubscriptionForbidden.New() } if err := s.subscriptionRepo.UpdateToken(subscriptionId, orgUUID, newToken); err != nil { @@ -503,16 +502,16 @@ func (s *SubscriptionService) RegenerateToken(subscriptionId, orgUUID, subscribe func (s *SubscriptionService) DeleteSubscription(subscriptionId, orgUUID, subscriberID, actor string) error { sub, err := s.subscriptionRepo.GetByID(subscriptionId, orgUUID) if err != nil { - if errors.Is(err, constants.ErrSubscriptionNotFound) { - return apperror.SubscriptionNotFound.Wrap(constants.ErrSubscriptionNotFound) + if apperror.SubscriptionNotFound.Is(err) { + return apperror.SubscriptionNotFound.New() } return err } if sub == nil { - return apperror.SubscriptionNotFound.Wrap(constants.ErrSubscriptionNotFound) + return apperror.SubscriptionNotFound.New() } if sub.SubscriberID != subscriberID { - return apperror.SubscriptionForbidden.Wrap(constants.ErrSubscriptionSubscriberMismatch) + return apperror.SubscriptionForbidden.New() } if err := s.subscriptionRepo.Delete(subscriptionId, orgUUID); err != nil { diff --git a/platform-api/internal/utils/handle.go b/platform-api/internal/utils/handle.go index f1ad01a31..c3af27a36 100644 --- a/platform-api/internal/utils/handle.go +++ b/platform-api/internal/utils/handle.go @@ -18,10 +18,11 @@ package utils import ( + "fmt" "regexp" "strings" - "github.com/wso2/api-platform/platform-api/internal/constants" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/google/uuid" ) @@ -56,7 +57,7 @@ func ValidateHandleImmutable(pathHandle string, bodyHandle *string) error { // request body to explicitly include a handle/id value matching the path handle. func ValidateHandleImmutableRequired(pathHandle, bodyHandle string) error { if bodyHandle == "" || bodyHandle != pathHandle { - return constants.ErrHandleImmutable + return apperror.ValidationFailed.New("The id is immutable and cannot be changed.") } return nil } @@ -71,16 +72,19 @@ func ValidateHandleImmutableRequired(pathHandle, bodyHandle string) error { // - Length between 3 and 63 characters func ValidateHandle(handle string) error { if handle == "" { - return constants.ErrHandleEmpty + return apperror.ValidationFailed.New("The id cannot be empty.") } if len(handle) < handleMinLength { - return constants.ErrHandleTooShort + return apperror.ValidationFailed.New( + fmt.Sprintf("The id must be at least %d characters.", handleMinLength)) } if len(handle) > handleMaxLength { - return constants.ErrHandleTooLong + return apperror.ValidationFailed.New( + fmt.Sprintf("The id must be at most %d characters.", handleMaxLength)) } if !validHandleRegex.MatchString(handle) { - return constants.ErrInvalidHandle + return apperror.ValidationFailed.New("The id must be lowercase alphanumeric with hyphens only " + + "(no consecutive hyphens, cannot start or end with a hyphen).") } return nil } @@ -98,7 +102,8 @@ func ValidateHandle(handle string) error { // - Error if source is empty or all retries exhausted func GenerateHandle(source string, existsCheck func(string) bool) (string, error) { if strings.TrimSpace(source) == "" { - return "", constants.ErrHandleSourceEmpty + return "", apperror.Internal.New(). + WithLogMessage("source string cannot be empty for handle generation") } // Generate base handle from source @@ -135,7 +140,8 @@ func GenerateHandle(source string, existsCheck func(string) bool) (string, error } } - return "", constants.ErrHandleGenerationFailed + return "", apperror.Internal.New(). + WithLogMessage(fmt.Sprintf("failed to generate a unique handle for %q after %d retries", source, maxRetries)) } // sanitizeToHandle converts a string to a valid handle format diff --git a/platform-api/internal/utils/mcp.go b/platform-api/internal/utils/mcp.go index cbfdc087e..b75632a09 100644 --- a/platform-api/internal/utils/mcp.go +++ b/platform-api/internal/utils/mcp.go @@ -30,6 +30,7 @@ import ( "time" "github.com/wso2/api-platform/platform-api/api" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/model" @@ -290,7 +291,9 @@ func initializeMCPServer(url string, headerName string, headerValue string) (str // Check HTTP status code if resp.StatusCode == http.StatusUnauthorized { - return "", nil, constants.ErrMCPServerUnauthorized + return "", nil, apperror.ValidationFailed.New( + "The MCP server returned 401 Unauthorized. Check the provided credentials."). + WithLogMessage("MCP server returned 401 Unauthorized to the initialize request") } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return "", nil, fmt.Errorf("initialize request failed with status %d: %s", resp.StatusCode, string(body)) diff --git a/platform-api/internal/webhook/handlers_application.go b/platform-api/internal/webhook/handlers_application.go index 5915ccc8e..cded229db 100644 --- a/platform-api/internal/webhook/handlers_application.go +++ b/platform-api/internal/webhook/handlers_application.go @@ -19,12 +19,11 @@ package webhook import ( "context" - "errors" "fmt" "strings" "github.com/wso2/api-platform/platform-api/api" - "github.com/wso2/api-platform/platform-api/internal/constants" + "github.com/wso2/api-platform/platform-api/internal/apperror" ) // Application event types. apikey.application_updated is intentionally handled with the other @@ -63,7 +62,7 @@ func (r *Receiver) handleApplicationCreated(ctx context.Context, env *Envelope) _, err := r.apps.CreateApplicationFromWebhook(d.Handle, d.DisplayName, d.Description, d.Type, env.OrgID) if err != nil { // Domain-level idempotency: a duplicate delivery whose application already exists is success. - if errors.Is(err, constants.ErrApplicationExists) || errors.Is(err, constants.ErrHandleExists) { + if apperror.ApplicationExists.Is(err) { return nil } return err @@ -97,12 +96,12 @@ func (r *Receiver) handleApplicationUpdated(ctx context.Context, env *Envelope) if err == nil { return nil } - if !errors.Is(err, constants.ErrApplicationNotFound) || name == "" { + if !apperror.ApplicationNotFound.Is(err) || name == "" { return err } // Upsert: the create event was likely missed. if _, cerr := r.apps.CreateApplicationFromWebhook(handle, name, d.Description, d.Type, env.OrgID); cerr != nil && - !errors.Is(cerr, constants.ErrApplicationExists) && !errors.Is(cerr, constants.ErrHandleExists) { + !apperror.ApplicationExists.Is(cerr) { return cerr } return nil @@ -119,7 +118,7 @@ func (r *Receiver) handleApplicationDeleted(ctx context.Context, env *Envelope) } if err := r.apps.DeleteApplication(d.Handle, env.OrgID, ""); err != nil { // Domain-level idempotency: already gone is success. - if errors.Is(err, constants.ErrApplicationNotFound) { + if apperror.ApplicationNotFound.Is(err) { return nil } return err diff --git a/platform-api/internal/webhook/handlers_subscription.go b/platform-api/internal/webhook/handlers_subscription.go index e63a2ecc1..967fb905c 100644 --- a/platform-api/internal/webhook/handlers_subscription.go +++ b/platform-api/internal/webhook/handlers_subscription.go @@ -19,11 +19,10 @@ package webhook import ( "context" - "errors" "fmt" "strings" - "github.com/wso2/api-platform/platform-api/internal/constants" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/model" ) @@ -114,7 +113,7 @@ func (r *Receiver) handleSubscriptionCreated(ctx context.Context, env *Envelope) _, err := r.subs.CreateSubscription(d.API.RefID, d.API.kind(), env.OrgID, d.SubscriberID, d.applicationIDPtr(), &d.SubscriptionPlan.RefID, token, d.Status, "") if err != nil { // Domain-level idempotency: a duplicate delivery whose subscription already exists is success. - if errors.Is(err, constants.ErrSubscriptionAlreadyExists) { + if apperror.SubscriptionExists.Is(err) { return nil } return err @@ -141,7 +140,7 @@ func (r *Receiver) handleSubscriptionUpdated(ctx context.Context, env *Envelope) return err } if sub == nil { - return constants.ErrSubscriptionNotFound + return apperror.SubscriptionNotFound.New() } _, err = r.subs.UpdateSubscription(sub.UUID, env.OrgID, d.SubscriberID, d.Status, "") @@ -165,7 +164,7 @@ func (r *Receiver) handleSubscriptionPlanChanged(ctx context.Context, env *Envel return err } if sub == nil { - return constants.ErrSubscriptionNotFound + return apperror.SubscriptionNotFound.New() } _, err = r.subs.ChangePlan(sub.UUID, env.OrgID, d.SubscriberID, d.SubscriptionPlan.RefID) @@ -197,7 +196,7 @@ func (r *Receiver) handleSubscriptionTokenRegenerated(ctx context.Context, env * return err } if sub == nil { - return constants.ErrSubscriptionNotFound + return apperror.SubscriptionNotFound.New() } _, err = r.subs.RegenerateToken(sub.UUID, env.OrgID, d.SubscriberID, token) @@ -224,7 +223,7 @@ func (r *Receiver) handleSubscriptionDeleted(ctx context.Context, env *Envelope) } if err := r.subs.DeleteSubscription(sub.UUID, env.OrgID, d.SubscriberID, ""); err != nil { - if errors.Is(err, constants.ErrSubscriptionNotFound) { + if apperror.SubscriptionNotFound.Is(err) { return nil } return err diff --git a/platform-api/internal/webhook/receiver.go b/platform-api/internal/webhook/receiver.go index c9b9bc8a9..4e75f6527 100644 --- a/platform-api/internal/webhook/receiver.go +++ b/platform-api/internal/webhook/receiver.go @@ -210,34 +210,26 @@ func (r *Receiver) resolveOrgUUID(env *Envelope) error { return err } if org == nil { - return constants.ErrOrganizationNotFound + return apperror.OrganizationNotFound.New() } env.OrgID = org.ID return nil } -// mapWebhookError maps domain errors returned by the reused services to the matching apperror -// catalog entry, preserving the same HTTP status classification the old statusForError gave them. +// mapWebhookError maps errors raised while processing a webhook event to an +// apperror catalog entry. The services this receiver reuses now build their +// failures from the catalog, so a domain error already carries the right code +// and status and passes through untouched; only the receiver's own envelope +// sentinels and unclassified failures need mapping here. func mapWebhookError(err error) *apperror.Error { - // Services that have migrated to the apperror catalog return typed errors - // directly — pass them through untouched. var appErr *apperror.Error if errors.As(err, &appErr) { return appErr } - switch { - case errors.Is(err, ErrInvalidEnvelope), errors.Is(err, ErrUnsupportedEvent), errors.Is(err, ErrDecryptionFailed): + // Envelope-level failures are the receiver's own sentinels, not catalog errors. + if errors.Is(err, ErrInvalidEnvelope) || errors.Is(err, ErrUnsupportedEvent) || errors.Is(err, ErrDecryptionFailed) { return apperror.ValidationFailed.Wrap(err, "Failed to process event") - case errors.Is(err, constants.ErrOrganizationNotFound): - return apperror.OrganizationNotFound.Wrap(err) - case errors.Is(err, constants.ErrSubscriptionNotFound): - return apperror.SubscriptionNotFound.Wrap(err) - case errors.Is(err, constants.ErrSubscriptionPlanNotFound), errors.Is(err, constants.ErrSubscriptionPlanNotFoundOrInactive): - return apperror.SubscriptionPlanNotFound.Wrap(err) - case errors.Is(err, constants.ErrAPINotFound), errors.Is(err, constants.ErrAPIKeyNotFound): - return apperror.ArtifactNotFound.Wrap(err) - default: - // Storage/EventHub failures and unexpected errors are retryable. - return apperror.Internal.Wrap(err).WithLogMessage("failed to process webhook event") } + // Storage/EventHub failures and unexpected errors are retryable. + return apperror.Internal.Wrap(err).WithLogMessage("failed to process webhook event") } diff --git a/platform-api/plugins/eventgateway/handler/artifact_guard_response.go b/platform-api/plugins/eventgateway/handler/artifact_guard_response.go deleted file mode 100644 index 6babbda30..000000000 --- a/platform-api/plugins/eventgateway/handler/artifact_guard_response.go +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). - * - * 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. - * - */ - -package handler - -import ( - "errors" - "net/http" - - "github.com/wso2/api-platform/platform-api/internal/apperror" - "github.com/wso2/api-platform/platform-api/internal/constants" - - "github.com/wso2/go-httpkit/httputil" -) - -// respondArtifactGuardError writes the appropriate HTTP response for read-only / -// deletion-guard errors raised when a mutating operation targets a -// data-plane-originated (origin=DP) artifact. It returns true when it handled the -// error (and wrote a response), so callers can simply `return`. -// -// - ErrArtifactReadOnly -> 403 Forbidden (update/deploy of a DP artifact) -// - ErrArtifactDeployed -> 409 Conflict (delete of a still-deployed DP artifact) -func respondArtifactGuardError(w http.ResponseWriter, err error) bool { - switch { - case errors.Is(err, constants.ErrArtifactReadOnly): - httputil.WriteJSON(w, http.StatusForbidden, apperror.NewErrorResponse(403, "Forbidden", err.Error())) - return true - case errors.Is(err, constants.ErrArtifactDeployed): - httputil.WriteJSON(w, http.StatusConflict, apperror.NewErrorResponse(409, "Conflict", err.Error())) - return true - default: - return false - } -} diff --git a/platform-api/plugins/eventgateway/handler/catalog_error_response.go b/platform-api/plugins/eventgateway/handler/catalog_error_response.go new file mode 100644 index 000000000..7b6f3ce17 --- /dev/null +++ b/platform-api/plugins/eventgateway/handler/catalog_error_response.go @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). + * + * 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. + * + */ + +package handler + +import ( + "errors" + "log/slog" + "net/http" + + "github.com/wso2/api-platform/platform-api/internal/apperror" +) + +// respondCatalogError logs err and writes it as the standard ErrorResponse when +// it is an *apperror.Error, reporting whether it did so the caller can simply +// return. +// +// The services these handlers call build their failures from the error catalog, +// so the error already carries the HTTP status, the stable machine-readable +// code, and a client-sterile message. Responding through apperror.LogAndWrite — +// the same helper the main error mapper uses — keeps both the wire format and +// the log severity from drifting between the plugin and the rest of the API. +// That matters most for a 5xx: this plugin does not run behind the error +// mapper, so writing the response directly (as an earlier version did) served a +// 500 with no log line and no stack trace at all. +// +// Every client-facing condition these services raise — including the WebSub, +// WebBroker, and HMAC-secret ones — now comes from the catalog, so a false +// return means the error is an unmapped internal failure and the caller should +// log it and answer 500. +func respondCatalogError(w http.ResponseWriter, slogger *slog.Logger, err error) bool { + var appErr *apperror.Error + if !errors.As(err, &appErr) { + return false + } + apperror.LogAndWrite(w, slogger, apperror.FromError(err)) + return true +} diff --git a/platform-api/plugins/eventgateway/handler/catalog_error_response_test.go b/platform-api/plugins/eventgateway/handler/catalog_error_response_test.go new file mode 100644 index 000000000..9b84ec40a --- /dev/null +++ b/platform-api/plugins/eventgateway/handler/catalog_error_response_test.go @@ -0,0 +1,130 @@ +package handler + +import ( + "bytes" + "encoding/json" + "io" + "log/slog" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/wso2/api-platform/platform-api/internal/apperror" + "github.com/wso2/api-platform/platform-api/internal/utils" +) + +func testLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) } + +// The plugin handlers used to translate internal sentinel errors with errors.Is. +// After the sentinels moved to the catalog, a stale guard would still compile but +// silently fall through to a 500. These lock in the catalog pass-through. +func TestRespondCatalogError_PassesThroughServiceLayerError(t *testing.T) { + // utils.ValidateHandleImmutable is an internal helper the plugin services call. + handle := "different-handle" + err := utils.ValidateHandleImmutable("real-handle", &handle) + if err == nil { + t.Fatal("expected an immutability error") + } + + rec := httptest.NewRecorder() + if !respondCatalogError(rec, testLogger(), err) { + t.Fatal("respondCatalogError should have handled a catalog error") + } + if rec.Code != http.StatusBadRequest { + t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest) + } + + var body apperror.ErrorResponse + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("response is not a valid ErrorResponse: %v", err) + } + if body.Code != apperror.CodeCommonValidationFailed { + t.Errorf("code = %q, want %q", body.Code, apperror.CodeCommonValidationFailed) + } + // The body must carry the sterile catalog message, never err.Error() (which + // would prefix the code and append the wrapped cause). + if got, want := body.Message, "The id is immutable and cannot be changed."; got != want { + t.Errorf("message = %q, want %q", got, want) + } +} + +func TestRespondCatalogError_IgnoresNonCatalogError(t *testing.T) { + rec := httptest.NewRecorder() + if respondCatalogError(rec, testLogger(), errPlain{}) { + t.Error("respondCatalogError must not handle a non-catalog error") + } + if rec.Code != http.StatusOK || rec.Body.Len() != 0 { + t.Error("respondCatalogError must not write anything for a non-catalog error") + } +} + +// A 5xx must never leave the process unlogged. The plugin does not run behind +// middleware.MapErrors, so respondCatalogError is the only thing standing +// between a server fault and a silent 500 — an earlier version wrote the +// response with no log line and no stack at all. +func TestRespondCatalogError_ServerErrorIsLoggedWithStack(t *testing.T) { + var buf bytes.Buffer + slogger := slog.New(slog.NewTextHandler(&buf, nil)) + + rec := httptest.NewRecorder() + err := apperror.Internal.Wrap(errPlain{}).WithLogMessage("db pool exhausted") + if !respondCatalogError(rec, slogger, err) { + t.Fatal("respondCatalogError should have handled a catalog error") + } + if rec.Code != http.StatusInternalServerError { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusInternalServerError) + } + + out := buf.String() + if !strings.Contains(out, "level=ERROR") { + t.Errorf("a 5xx must log at ERROR; got: %s", out) + } + if !strings.Contains(out, "stack=") { + t.Errorf("a 5xx must log the origin stack; got: %s", out) + } + // The wrapped cause and the internal detail belong in the log, never the body. + if !strings.Contains(out, "db pool exhausted") { + t.Errorf("log must carry the internal detail; got: %s", out) + } + if !strings.Contains(out, "some driver failure") { + t.Errorf("log must carry the wrapped cause; got: %s", out) + } + + var body apperror.ErrorResponse + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("response is not a valid ErrorResponse: %v", err) + } + if body.TrackingID == "" { + t.Error("a 5xx response must echo a tracking ID for log correlation") + } + if strings.Contains(body.Message, "driver") || strings.Contains(body.Message, "pool") { + t.Errorf("internal detail leaked into the client message: %q", body.Message) + } +} + +// A 4xx is a client outcome, not a system fault: WARN, no stack, no tracking ID. +func TestRespondCatalogError_ClientErrorIsWarnWithoutStack(t *testing.T) { + var buf bytes.Buffer + slogger := slog.New(slog.NewTextHandler(&buf, nil)) + + rec := httptest.NewRecorder() + if !respondCatalogError(rec, slogger, apperror.WebSubAPINotFound.New()) { + t.Fatal("respondCatalogError should have handled a catalog error") + } + if rec.Code != http.StatusNotFound { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound) + } + + out := buf.String() + if !strings.Contains(out, "level=WARN") { + t.Errorf("a 4xx must log at WARN; got: %s", out) + } + if strings.Contains(out, "stack=") { + t.Errorf("a 4xx must not log a stack; got: %s", out) + } +} + +type errPlain struct{} + +func (errPlain) Error() string { return "some driver failure" } diff --git a/platform-api/plugins/eventgateway/handler/webbroker_api.go b/platform-api/plugins/eventgateway/handler/webbroker_api.go index ecba1bde2..bb53b5099 100644 --- a/platform-api/plugins/eventgateway/handler/webbroker_api.go +++ b/platform-api/plugins/eventgateway/handler/webbroker_api.go @@ -21,7 +21,6 @@ package handler import ( "encoding/json" - "errors" "log/slog" "net/http" "strconv" @@ -214,26 +213,12 @@ func (h *WebBrokerAPIHandler) DeleteWebBrokerAPI(w http.ResponseWriter, r *http. // handleServiceError maps service errors to HTTP responses func (h *WebBrokerAPIHandler) handleServiceError(w http.ResponseWriter, err error) { - if respondArtifactGuardError(w, err) { - return - } - switch { - case errors.Is(err, constants.ErrHandleImmutable): - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", err.Error())) - case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", err.Error())) - case errors.Is(err, constants.ErrWebBrokerAPINotFound): - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "WebBroker API not found")) - case errors.Is(err, constants.ErrWebBrokerAPIExists): - httputil.WriteJSON(w, http.StatusConflict, apperror.NewErrorResponse(409, "Conflict", "WebBroker API with this ID already exists")) - case errors.Is(err, constants.ErrWebBrokerAPILimitReached): - httputil.WriteJSON(w, http.StatusConflict, apperror.NewErrorResponse(409, "Conflict", "WebBroker API limit reached for the organization")) - case errors.Is(err, constants.ErrProjectNotFound): - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Project not found")) - case errors.Is(err, constants.ErrDevPortalNotFound): - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "DevPortal not found")) - default: - h.slogger.Error("WebBroker API service error", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) + // The service builds every client-facing failure from the error catalog, so + // the error already carries its status, code, and sterile client message. + // Anything reaching the fallback is an unmapped internal failure. + if respondCatalogError(w, h.slogger, err) { + return } + h.slogger.Error("WebBroker API service error", "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) } diff --git a/platform-api/plugins/eventgateway/handler/webbroker_api_deployment.go b/platform-api/plugins/eventgateway/handler/webbroker_api_deployment.go index c8249b040..c86874008 100644 --- a/platform-api/plugins/eventgateway/handler/webbroker_api_deployment.go +++ b/platform-api/plugins/eventgateway/handler/webbroker_api_deployment.go @@ -21,7 +21,6 @@ package handler import ( "encoding/json" - "errors" "log/slog" "net/http" "strings" @@ -101,9 +100,6 @@ func (h *WebBrokerAPIDeploymentHandler) DeployWebBrokerAPI(w http.ResponseWriter } deployment, err := h.webbrokerAPIDeploymentService.DeployWebBrokerAPIByHandle(apiId, &req, orgId, createdBy) if err != nil { - if respondArtifactGuardError(w, err) { - return - } h.handleDeploymentError(w, err, apiId) return } @@ -133,9 +129,6 @@ func (h *WebBrokerAPIDeploymentHandler) UndeployDeployment(w http.ResponseWriter deployment, err := h.webbrokerAPIDeploymentService.UndeployWebBrokerAPIDeploymentByHandle(apiId, deploymentId, gatewayId, orgId) if err != nil { - if respondArtifactGuardError(w, err) { - return - } h.handleDeploymentError(w, err, apiId) return } @@ -165,9 +158,6 @@ func (h *WebBrokerAPIDeploymentHandler) RestoreDeployment(w http.ResponseWriter, deployment, err := h.webbrokerAPIDeploymentService.RestoreWebBrokerAPIDeploymentByHandle(apiId, deploymentId, gatewayId, orgId) if err != nil { - if respondArtifactGuardError(w, err) { - return - } h.handleDeploymentError(w, err, apiId) return } @@ -256,29 +246,12 @@ func (h *WebBrokerAPIDeploymentHandler) DeleteDeployment(w http.ResponseWriter, } func (h *WebBrokerAPIDeploymentHandler) handleDeploymentError(w http.ResponseWriter, err error, apiId string) { - switch { - case errors.Is(err, constants.ErrWebBrokerAPINotFound): - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "WebBroker API not found")) - case errors.Is(err, constants.ErrGatewayNotFound): - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "Gateway not found")) - case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "Deployment not found")) - case errors.Is(err, constants.ErrBaseDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "Base deployment not found")) - case errors.Is(err, constants.ErrDeploymentNotActive): - httputil.WriteJSON(w, http.StatusConflict, apperror.NewErrorResponse(409, "Conflict", "No active deployment found for this API on the gateway")) - case errors.Is(err, constants.ErrDeploymentIsDeployed): - httputil.WriteJSON(w, http.StatusConflict, apperror.NewErrorResponse(409, "Conflict", "Cannot delete an active deployment - undeploy it first")) - case errors.Is(err, constants.ErrDeploymentAlreadyDeployed): - httputil.WriteJSON(w, http.StatusConflict, apperror.NewErrorResponse(409, "Conflict", "Cannot restore currently deployed deployment")) - case errors.Is(err, constants.ErrInvalidDeploymentRestoreState): - httputil.WriteJSON(w, http.StatusConflict, apperror.NewErrorResponse(409, "Conflict", "Deployment cannot be restored: only ARCHIVED or UNDEPLOYED deployments are eligible")) - case errors.Is(err, constants.ErrGatewayIDMismatch): - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Deployment is bound to a different gateway")) - case errors.Is(err, constants.ErrAPINoBackendServices): - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API must have at least one backend service configured")) - default: - h.slogger.Error("WebBroker API deployment error", "apiId", apiId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) + // Deployment conditions (gateway/deployment not found, restore conflicts, + // gateway mismatch, validation failures) are catalog errors raised by the + // service and already carry status, code, and a sterile client message. + if respondCatalogError(w, h.slogger, err) { + return } + h.slogger.Error("WebBroker API deployment error", "apiId", apiId, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) } diff --git a/platform-api/plugins/eventgateway/handler/webbroker_apikey.go b/platform-api/plugins/eventgateway/handler/webbroker_apikey.go index 67082b412..113527853 100644 --- a/platform-api/plugins/eventgateway/handler/webbroker_apikey.go +++ b/platform-api/plugins/eventgateway/handler/webbroker_apikey.go @@ -21,7 +21,6 @@ package handler import ( "encoding/json" - "errors" "fmt" "log/slog" "net/http" @@ -112,11 +111,11 @@ func (h *WebBrokerAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Req } if err := h.apiKeyService.CreateAPIKey(r.Context(), apiHandle, constants.WebBrokerApi, orgID, userId, &req); err != nil { - if errors.Is(err, constants.ErrAPINotFound) { + if apperror.ArtifactNotFound.Is(err) { httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "WebBroker API not found")) return } - if errors.Is(err, constants.ErrGatewayUnavailable) { + if apperror.GatewayConnectionUnavailable.Is(err) { httputil.WriteJSON(w, http.StatusServiceUnavailable, apperror.NewErrorResponse(503, "Service Unavailable", "No gateway connections available")) return } @@ -184,11 +183,11 @@ func (h *WebBrokerAPIKeyHandler) UpdateAPIKey(w http.ResponseWriter, r *http.Req } if err := h.apiKeyService.UpdateAPIKey(r.Context(), apiHandle, constants.WebBrokerApi, orgID, keyName, userId, &req); err != nil { - if errors.Is(err, constants.ErrAPINotFound) { + if apperror.ArtifactNotFound.Is(err) { httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "WebBroker API not found")) return } - if errors.Is(err, constants.ErrGatewayUnavailable) { + if apperror.GatewayConnectionUnavailable.Is(err) { httputil.WriteJSON(w, http.StatusServiceUnavailable, apperror.NewErrorResponse(503, "Service Unavailable", "No gateway connections available")) return } @@ -231,15 +230,15 @@ func (h *WebBrokerAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.Req } if err := h.apiKeyService.RevokeAPIKey(r.Context(), apiHandle, constants.WebBrokerApi, orgID, keyName, userId); err != nil { - if errors.Is(err, constants.ErrAPINotFound) { + if apperror.ArtifactNotFound.Is(err) { httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "WebBroker API not found")) return } - if errors.Is(err, constants.ErrAPIKeyNotFound) { + if apperror.ApplicationAPIKeyNotFound.Is(err) { httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "API key not found")) return } - if errors.Is(err, constants.ErrGatewayUnavailable) { + if apperror.GatewayConnectionUnavailable.Is(err) { httputil.WriteJSON(w, http.StatusServiceUnavailable, apperror.NewErrorResponse(503, "Service Unavailable", "No gateway connections available")) return } @@ -253,13 +252,12 @@ func (h *WebBrokerAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.Req // handleServiceError maps service errors to HTTP responses func (h *WebBrokerAPIKeyHandler) handleServiceError(w http.ResponseWriter, err error) { - switch { - case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", err.Error())) - case errors.Is(err, constants.ErrWebBrokerAPINotFound): - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "WebBroker API not found")) - default: - h.slogger.Error("WebBroker API key service error", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) + // Catalog errors — validation failures, an unknown referenced project, and the + // data-plane-origin guard — already carry their status, code, and a sterile + // client message. Only this plugin's own sentinel conditions are mapped below. + if respondCatalogError(w, h.slogger, err) { + return } + h.slogger.Error("WebBroker API key service error", "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) } diff --git a/platform-api/plugins/eventgateway/handler/websub_api.go b/platform-api/plugins/eventgateway/handler/websub_api.go index b3a6fdba9..28efa77f2 100644 --- a/platform-api/plugins/eventgateway/handler/websub_api.go +++ b/platform-api/plugins/eventgateway/handler/websub_api.go @@ -21,7 +21,6 @@ package handler import ( "encoding/json" - "errors" "log/slog" "net/http" "strconv" @@ -205,26 +204,12 @@ func (h *WebSubAPIHandler) DeleteWebSubAPI(w http.ResponseWriter, r *http.Reques // handleServiceError maps service errors to HTTP responses func (h *WebSubAPIHandler) handleServiceError(w http.ResponseWriter, err error) { - if respondArtifactGuardError(w, err) { - return - } - switch { - case errors.Is(err, constants.ErrHandleImmutable): - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", err.Error())) - case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", err.Error())) - case errors.Is(err, constants.ErrWebSubAPINotFound): - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "WebSub API not found")) - case errors.Is(err, constants.ErrWebSubAPIExists): - httputil.WriteJSON(w, http.StatusConflict, apperror.NewErrorResponse(409, "Conflict", "WebSub API with this ID already exists")) - case errors.Is(err, constants.ErrWebSubAPILimitReached): - httputil.WriteJSON(w, http.StatusConflict, apperror.NewErrorResponse(409, "Conflict", "WebSub API limit reached for the organization")) - case errors.Is(err, constants.ErrProjectNotFound): - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Project not found")) - case errors.Is(err, constants.ErrDevPortalNotFound): - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "DevPortal not found")) - default: - h.slogger.Error("WebSub API service error", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) + // The service builds every client-facing failure from the error catalog, so + // the error already carries its status, code, and sterile client message. + // Anything reaching the fallback is an unmapped internal failure. + if respondCatalogError(w, h.slogger, err) { + return } + h.slogger.Error("WebSub API service error", "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) } diff --git a/platform-api/plugins/eventgateway/handler/websub_api_deployment.go b/platform-api/plugins/eventgateway/handler/websub_api_deployment.go index fec8db9d8..d6e57a466 100644 --- a/platform-api/plugins/eventgateway/handler/websub_api_deployment.go +++ b/platform-api/plugins/eventgateway/handler/websub_api_deployment.go @@ -21,7 +21,6 @@ package handler import ( "encoding/json" - "errors" "log/slog" "net/http" "strings" @@ -101,9 +100,6 @@ func (h *WebSubAPIDeploymentHandler) DeployWebSubAPI(w http.ResponseWriter, r *h } deployment, err := h.websubAPIDeploymentService.DeployWebSubAPIByHandle(apiId, &req, orgId, createdBy) if err != nil { - if respondArtifactGuardError(w, err) { - return - } h.handleDeploymentError(w, err, apiId) return } @@ -133,9 +129,6 @@ func (h *WebSubAPIDeploymentHandler) UndeployDeployment(w http.ResponseWriter, r deployment, err := h.websubAPIDeploymentService.UndeployWebSubAPIDeploymentByHandle(apiId, deploymentId, gatewayId, orgId) if err != nil { - if respondArtifactGuardError(w, err) { - return - } h.handleDeploymentError(w, err, apiId) return } @@ -165,9 +158,6 @@ func (h *WebSubAPIDeploymentHandler) RestoreDeployment(w http.ResponseWriter, r deployment, err := h.websubAPIDeploymentService.RestoreWebSubAPIDeploymentByHandle(apiId, deploymentId, gatewayId, orgId) if err != nil { - if respondArtifactGuardError(w, err) { - return - } h.handleDeploymentError(w, err, apiId) return } @@ -256,29 +246,12 @@ func (h *WebSubAPIDeploymentHandler) DeleteDeployment(w http.ResponseWriter, r * } func (h *WebSubAPIDeploymentHandler) handleDeploymentError(w http.ResponseWriter, err error, apiId string) { - switch { - case errors.Is(err, constants.ErrWebSubAPINotFound): - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "WebSub API not found")) - case errors.Is(err, constants.ErrGatewayNotFound): - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "Gateway not found")) - case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "Deployment not found")) - case errors.Is(err, constants.ErrBaseDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "Base deployment not found")) - case errors.Is(err, constants.ErrDeploymentNotActive): - httputil.WriteJSON(w, http.StatusConflict, apperror.NewErrorResponse(409, "Conflict", "No active deployment found for this API on the gateway")) - case errors.Is(err, constants.ErrDeploymentIsDeployed): - httputil.WriteJSON(w, http.StatusConflict, apperror.NewErrorResponse(409, "Conflict", "Cannot delete an active deployment - undeploy it first")) - case errors.Is(err, constants.ErrDeploymentAlreadyDeployed): - httputil.WriteJSON(w, http.StatusConflict, apperror.NewErrorResponse(409, "Conflict", "Cannot restore currently deployed deployment")) - case errors.Is(err, constants.ErrInvalidDeploymentRestoreState): - httputil.WriteJSON(w, http.StatusConflict, apperror.NewErrorResponse(409, "Conflict", "Deployment cannot be restored: only ARCHIVED or UNDEPLOYED deployments are eligible")) - case errors.Is(err, constants.ErrGatewayIDMismatch): - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Deployment is bound to a different gateway")) - case errors.Is(err, constants.ErrAPINoBackendServices): - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API must have at least one backend service configured")) - default: - h.slogger.Error("WebSub API deployment error", "apiId", apiId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) + // Deployment conditions (gateway/deployment not found, restore conflicts, + // gateway mismatch, validation failures) are catalog errors raised by the + // service and already carry status, code, and a sterile client message. + if respondCatalogError(w, h.slogger, err) { + return } + h.slogger.Error("WebSub API deployment error", "apiId", apiId, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) } diff --git a/platform-api/plugins/eventgateway/handler/websub_api_hmac_secret.go b/platform-api/plugins/eventgateway/handler/websub_api_hmac_secret.go index 360e4b70b..8b42710ca 100644 --- a/platform-api/plugins/eventgateway/handler/websub_api_hmac_secret.go +++ b/platform-api/plugins/eventgateway/handler/websub_api_hmac_secret.go @@ -28,7 +28,6 @@ import ( "github.com/wso2/api-platform/platform-api/api" "github.com/wso2/api-platform/platform-api/internal/apperror" - "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/middleware" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/service" @@ -64,8 +63,11 @@ func (h *WebSubAPIHmacSecretHandler) RegisterRoutes(mux *http.ServeMux) { func (h *WebSubAPIHmacSecretHandler) featureUnavailable(w http.ResponseWriter) bool { if h.secretService == nil { - httputil.WriteJSON(w, http.StatusServiceUnavailable, apperror.NewErrorResponse(503, "Service Unavailable", - "HMAC secret management is not configured on this server")) + // The service is nil only when its constructor rejected a missing + // encryption key, which plugin startup already logged. Respond through + // LogAndWrite anyway: this is a 503, and a 5xx must never leave the + // process without a log line naming the request that hit it. + apperror.LogAndWrite(w, h.slogger, apperror.HmacSecretNotConfigured.New()) return true } return false @@ -255,22 +257,14 @@ func (h *WebSubAPIHmacSecretHandler) RegenerateHmacSecret(w http.ResponseWriter, } func (h *WebSubAPIHmacSecretHandler) handleServiceError(w http.ResponseWriter, err error) { - switch { - case errors.Is(err, constants.ErrWebSubAPINotFound): - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "WebSub API not found")) - case errors.Is(err, constants.ErrHmacSecretNotFound): - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "HMAC secret not found")) - case errors.Is(err, constants.ErrHmacSecretAlreadyExists): - httputil.WriteJSON(w, http.StatusConflict, apperror.NewErrorResponse(409, "Conflict", "An HMAC secret with this name already exists")) - case errors.Is(err, constants.ErrHmacSecretInvalidValue): - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Secret value must be at least 32 characters")) - case errors.Is(err, constants.ErrHmacSecretEncryptionKeyMissing): - h.slogger.Error("HMAC secret encryption key is not configured") - httputil.WriteJSON(w, http.StatusServiceUnavailable, apperror.NewErrorResponse(503, "Service Unavailable", "HMAC secret management is not configured on this server")) - default: - h.slogger.Error("HMAC secret service error", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) + // The service builds every client-facing failure from the error catalog, so + // the error already carries its status, code, and sterile client message. + // Anything reaching the fallback is an unmapped internal failure. + if respondCatalogError(w, h.slogger, err) { + return } + h.slogger.Error("HMAC secret service error", "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) } func secretToInfo(s *model.WebSubAPIHmacSecret) *api.WebSubAPIHmacSecretInfo { diff --git a/platform-api/plugins/eventgateway/handler/websub_apikey.go b/platform-api/plugins/eventgateway/handler/websub_apikey.go index 2c5ad5c7c..4287c5945 100644 --- a/platform-api/plugins/eventgateway/handler/websub_apikey.go +++ b/platform-api/plugins/eventgateway/handler/websub_apikey.go @@ -21,7 +21,6 @@ package handler import ( "encoding/json" - "errors" "fmt" "log/slog" "net/http" @@ -112,11 +111,11 @@ func (h *WebSubAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Reques } if err := h.apiKeyService.CreateAPIKey(r.Context(), apiHandle, constants.WebSubApi, orgID, userId, &req); err != nil { - if errors.Is(err, constants.ErrAPINotFound) { + if apperror.ArtifactNotFound.Is(err) { httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "WebSub API not found")) return } - if errors.Is(err, constants.ErrGatewayUnavailable) { + if apperror.GatewayConnectionUnavailable.Is(err) { httputil.WriteJSON(w, http.StatusServiceUnavailable, apperror.NewErrorResponse(503, "Service Unavailable", "No gateway connections available")) return } @@ -184,11 +183,11 @@ func (h *WebSubAPIKeyHandler) UpdateAPIKey(w http.ResponseWriter, r *http.Reques } if err := h.apiKeyService.UpdateAPIKey(r.Context(), apiHandle, constants.WebSubApi, orgID, keyName, userId, &req); err != nil { - if errors.Is(err, constants.ErrAPINotFound) { + if apperror.ArtifactNotFound.Is(err) { httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "WebSub API not found")) return } - if errors.Is(err, constants.ErrGatewayUnavailable) { + if apperror.GatewayConnectionUnavailable.Is(err) { httputil.WriteJSON(w, http.StatusServiceUnavailable, apperror.NewErrorResponse(503, "Service Unavailable", "No gateway connections available")) return } @@ -232,15 +231,15 @@ func (h *WebSubAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.Reques } if err := h.apiKeyService.RevokeAPIKey(r.Context(), apiHandle, constants.WebSubApi, orgID, keyName, userId); err != nil { - if errors.Is(err, constants.ErrAPINotFound) { + if apperror.ArtifactNotFound.Is(err) { httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "WebSub API not found")) return } - if errors.Is(err, constants.ErrAPIKeyNotFound) { + if apperror.ApplicationAPIKeyNotFound.Is(err) { httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "API key not found")) return } - if errors.Is(err, constants.ErrGatewayUnavailable) { + if apperror.GatewayConnectionUnavailable.Is(err) { httputil.WriteJSON(w, http.StatusServiceUnavailable, apperror.NewErrorResponse(503, "Service Unavailable", "No gateway connections available")) return } @@ -254,13 +253,12 @@ func (h *WebSubAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.Reques // handleServiceError maps service errors to HTTP responses func (h *WebSubAPIKeyHandler) handleServiceError(w http.ResponseWriter, err error) { - switch { - case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", err.Error())) - case errors.Is(err, constants.ErrWebSubAPINotFound): - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "WebSub API not found")) - default: - h.slogger.Error("WebSub API key service error", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) + // Catalog errors — validation failures, an unknown referenced project, and the + // data-plane-origin guard — already carry their status, code, and a sterile + // client message. Only this plugin's own sentinel conditions are mapped below. + if respondCatalogError(w, h.slogger, err) { + return } + h.slogger.Error("WebSub API key service error", "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) } diff --git a/platform-api/plugins/eventgateway/plugin.go b/platform-api/plugins/eventgateway/plugin.go index 95e057e27..1c500ea43 100644 --- a/platform-api/plugins/eventgateway/plugin.go +++ b/platform-api/plugins/eventgateway/plugin.go @@ -33,6 +33,7 @@ import ( "github.com/wso2/api-platform/platform-api/api" "github.com/wso2/api-platform/platform-api/config" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/database" "github.com/wso2/api-platform/platform-api/internal/plugin" @@ -256,14 +257,14 @@ func (p *EventGatewayPlugin) CheckProjectDeletion(orgID, projectID string) error return err } if websubCount > 0 { - return constants.ErrProjectHasAssociatedWebSubAPIs + return apperror.ValidationFailed.New("Project has associated WebSub APIs") } webbrokerCount, err := p.webbrokerAPIRepo.CountByProject(orgID, projectID) if err != nil { return err } if webbrokerCount > 0 { - return constants.ErrProjectHasAssociatedWebBrokerAPIs + return apperror.ValidationFailed.New("Project has associated WebBroker APIs") } return nil } diff --git a/platform-api/plugins/eventgateway/service/helpers.go b/platform-api/plugins/eventgateway/service/helpers.go index 4c2ff27d3..332b26f16 100644 --- a/platform-api/plugins/eventgateway/service/helpers.go +++ b/platform-api/plugins/eventgateway/service/helpers.go @@ -25,6 +25,7 @@ import ( "time" "github.com/wso2/api-platform/platform-api/api" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/repository" @@ -39,11 +40,13 @@ func isSQLiteUniqueConstraint(err error) bool { return strings.Contains(err.Error(), "UNIQUE constraint failed") } -// ensureOriginMutable returns ErrArtifactReadOnly when the artifact originated from -// a data-plane gateway. DP artifacts are read-only in the control plane. +// ensureOriginMutable returns an ARTIFACT_READ_ONLY error when the artifact +// originated from a data-plane gateway. DP artifacts are read-only in the +// control plane. func ensureOriginMutable(origin string) error { if origin == constants.OriginDP { - return constants.ErrArtifactReadOnly + return apperror.ArtifactReadOnly.New( + "This artifact is read-only because it originated from a data-plane gateway.") } return nil } diff --git a/platform-api/plugins/eventgateway/service/webbroker_api.go b/platform-api/plugins/eventgateway/service/webbroker_api.go index 788280843..76f65cdad 100644 --- a/platform-api/plugins/eventgateway/service/webbroker_api.go +++ b/platform-api/plugins/eventgateway/service/webbroker_api.go @@ -27,6 +27,7 @@ import ( "github.com/wso2/api-platform/platform-api/api" "github.com/wso2/api-platform/platform-api/config" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/repository" @@ -104,13 +105,13 @@ func (s *WebBrokerAPIService) webBrokerAPIListItemResolved(m *model.WebBrokerAPI // Create creates a new WebBroker API func (s *WebBrokerAPIService) Create(orgUUID, createdBy string, req *api.WebBrokerAPI) (*api.WebBrokerAPI, error) { if req == nil { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("A request body is required.") } if utils.ValueOrEmpty(req.Id) == "" || req.DisplayName == "" || req.Version == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The id, displayName and version fields are required.") } if req.ProjectId == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The projectId field is required.") } handle := utils.ValueOrEmpty(req.Id) @@ -122,10 +123,10 @@ func (s *WebBrokerAPIService) Create(orgUUID, createdBy string, req *api.WebBrok return nil, fmt.Errorf("failed to validate project: %w", err) } if project == nil { - return nil, constants.ErrProjectNotFound + return nil, apperror.ProjectRefNotFound.New() } if project.OrganizationID != orgUUID { - return nil, constants.ErrProjectNotFound + return nil, apperror.ProjectRefNotFound.New() } } @@ -135,7 +136,7 @@ func (s *WebBrokerAPIService) Create(orgUUID, createdBy string, req *api.WebBrok return nil, fmt.Errorf("failed to check WebBroker API exists: %w", err) } if exists { - return nil, constants.ErrWebBrokerAPIExists + return nil, apperror.WebBrokerAPIExists.New() } // Enforce the per-organization WebBroker API limit (unlimited when not configured). @@ -144,7 +145,7 @@ func (s *WebBrokerAPIService) Create(orgUUID, createdBy string, req *api.WebBrok return nil, fmt.Errorf("failed to count existing WebBroker APIs: %w", err) } if config.LimitReached(count, s.cfg.ArtifactLimits.MaxWebBrokerAPIsPerOrg) { - return nil, constants.ErrWebBrokerAPILimitReached + return nil, apperror.WebBrokerAPILimitReached.New() } transport := []string{"http", "https"} @@ -190,7 +191,7 @@ func (s *WebBrokerAPIService) Create(orgUUID, createdBy string, req *api.WebBrok if err := s.repo.Create(m); err != nil { if isSQLiteUniqueConstraint(err) { - return nil, constants.ErrWebBrokerAPIExists + return nil, apperror.WebBrokerAPIExists.Wrap(err) } return nil, fmt.Errorf("failed to create WebBroker API: %w", err) } @@ -204,7 +205,7 @@ func (s *WebBrokerAPIService) Create(orgUUID, createdBy string, req *api.WebBrok // Get retrieves a WebBroker API by its handle func (s *WebBrokerAPIService) Get(orgUUID, handle string) (*api.WebBrokerAPI, error) { if handle == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The WebBroker API id is required.") } m, err := s.repo.GetByHandle(handle, orgUUID) @@ -212,7 +213,7 @@ func (s *WebBrokerAPIService) Get(orgUUID, handle string) (*api.WebBrokerAPI, er return nil, fmt.Errorf("failed to get WebBroker API: %w", err) } if m == nil { - return nil, constants.ErrWebBrokerAPINotFound + return nil, apperror.WebBrokerAPINotFound.New() } return s.toWebBrokerAPI(m) @@ -261,10 +262,10 @@ func (s *WebBrokerAPIService) List(orgUUID, projectUUID string, limit, offset in // Update updates an existing WebBroker API func (s *WebBrokerAPIService) Update(orgUUID, handle, updatedBy string, req *api.WebBrokerAPI) (*api.WebBrokerAPI, error) { if handle == "" || req == nil { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The WebBroker API id and a request body are required.") } if req.DisplayName == "" || req.Version == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The displayName and version fields are required.") } // Get existing existing, err := s.repo.GetByHandle(handle, orgUUID) @@ -272,7 +273,7 @@ func (s *WebBrokerAPIService) Update(orgUUID, handle, updatedBy string, req *api return nil, fmt.Errorf("failed to get WebBroker API: %w", err) } if existing == nil { - return nil, constants.ErrWebBrokerAPINotFound + return nil, apperror.WebBrokerAPINotFound.New() } // DP-originated artifacts are read-only in the control plane. if err := ensureOriginMutable(existing.Origin); err != nil { @@ -316,7 +317,7 @@ func (s *WebBrokerAPIService) Update(orgUUID, handle, updatedBy string, req *api if err := s.repo.Update(existing); err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, constants.ErrWebBrokerAPINotFound + return nil, apperror.WebBrokerAPINotFound.Wrap(err) } return nil, fmt.Errorf("failed to update WebBroker API: %w", err) } @@ -330,7 +331,7 @@ func (s *WebBrokerAPIService) Update(orgUUID, handle, updatedBy string, req *api // Delete deletes a WebBroker API by its handle func (s *WebBrokerAPIService) Delete(orgUUID, handle, deletedBy string) error { if handle == "" { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("The WebBroker API id is required.") } // Get the WebBroker API UUID before deletion (needed for gateway deletion event) @@ -339,7 +340,7 @@ func (s *WebBrokerAPIService) Delete(orgUUID, handle, deletedBy string) error { return fmt.Errorf("failed to get WebBroker API: %w", err) } if webbrokerAPI == nil { - return constants.ErrWebBrokerAPINotFound + return apperror.WebBrokerAPINotFound.New() } // DP-originated artifacts are read-only in the control plane and cannot be deleted from the CP. if err := ensureOriginMutable(webbrokerAPI.Origin); err != nil { @@ -359,7 +360,7 @@ func (s *WebBrokerAPIService) Delete(orgUUID, handle, deletedBy string) error { if err := s.repo.Delete(handle, orgUUID); err != nil { if errors.Is(err, sql.ErrNoRows) { - return constants.ErrWebBrokerAPINotFound + return apperror.WebBrokerAPINotFound.Wrap(err) } return fmt.Errorf("failed to delete WebBroker API: %w", err) } diff --git a/platform-api/plugins/eventgateway/service/webbroker_api_deployment.go b/platform-api/plugins/eventgateway/service/webbroker_api_deployment.go index 93661be63..9f18c499a 100644 --- a/platform-api/plugins/eventgateway/service/webbroker_api_deployment.go +++ b/platform-api/plugins/eventgateway/service/webbroker_api_deployment.go @@ -28,6 +28,7 @@ import ( "github.com/wso2/api-platform/platform-api/api" "github.com/wso2/api-platform/platform-api/config" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/repository" @@ -152,18 +153,18 @@ func (s *WebBrokerAPIDeploymentService) GetWebBrokerAPIDeploymentsByHandle(apiHa // deployWebBrokerAPI deploys a WebBroker API to a gateway func (s *WebBrokerAPIDeploymentService) deployWebBrokerAPI(apiUUID string, req *api.DeployRequest, orgID, createdBy string) (*api.DeploymentResponse, error) { if req == nil { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("A request body is required.") } // DP-originated artifacts are read-only in the control plane; deployment cannot be CP-initiated. if err := ensureArtifactMutableByUUID(s.artifactRepo, apiUUID, orgID); err != nil { return nil, err } if req.Base == "" { - return nil, constants.ErrDeploymentBaseRequired + return nil, apperror.ValidationFailed.New("Base is required (use 'current' or a deploymentId).") } gatewayHandle := strings.TrimSpace(req.GatewayId) if gatewayHandle == "" { - return nil, constants.ErrDeploymentGatewayIDRequired + return nil, apperror.ValidationFailed.New("Gateway ID is required.") } metadata := utils.MapValueOrEmpty(req.Metadata) @@ -173,7 +174,7 @@ func (s *WebBrokerAPIDeploymentService) deployWebBrokerAPI(apiUUID string, req * return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } gatewayID := gateway.ID @@ -182,7 +183,7 @@ func (s *WebBrokerAPIDeploymentService) deployWebBrokerAPI(apiUUID string, req * return nil, err } if webbrokerAPI == nil { - return nil, constants.ErrWebBrokerAPINotFound + return nil, apperror.WebBrokerAPINotFound.New() } // Generate deployment ID @@ -203,8 +204,8 @@ func (s *WebBrokerAPIDeploymentService) deployWebBrokerAPI(apiUUID string, req * } else { baseDeployment, err := s.deploymentRepo.GetWithContent(req.Base, apiUUID, orgID) if err != nil { - if errors.Is(err, constants.ErrDeploymentNotFound) { - return nil, constants.ErrBaseDeploymentNotFound + if apperror.DeploymentNotFound.Is(err) { + return nil, apperror.DeploymentBaseNotFound.New() } return nil, fmt.Errorf("failed to get base deployment: %w", err) } @@ -298,7 +299,7 @@ func (s *WebBrokerAPIDeploymentService) undeployWebBrokerAPIDeployment(apiUUID s return nil, err } if webbrokerAPI == nil { - return nil, constants.ErrWebBrokerAPINotFound + return nil, apperror.WebBrokerAPINotFound.New() } var deployment *model.Deployment @@ -308,7 +309,7 @@ func (s *WebBrokerAPIDeploymentService) undeployWebBrokerAPIDeployment(apiUUID s return nil, err } if deployment == nil { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } } else if gatewayId != nil { deployment, err = s.deploymentRepo.GetCurrentByGateway(apiUUID, *gatewayId, orgID) @@ -316,18 +317,18 @@ func (s *WebBrokerAPIDeploymentService) undeployWebBrokerAPIDeployment(apiUUID s return nil, err } if deployment == nil { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } } else { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("Either a deploymentId or a gatewayId is required.") } if gatewayId != nil && deployment.GatewayID != *gatewayId { - return nil, constants.ErrGatewayIDMismatch + return nil, apperror.DeploymentGatewayMismatch.New() } if deployment.Status == nil || *deployment.Status != model.DeploymentStatusDeployed { - return nil, constants.ErrDeploymentNotActive + return nil, apperror.DeploymentNotActive.New("WebBroker API") } gateway, err := s.gatewayRepo.GetByUUID(deployment.GatewayID) @@ -335,7 +336,7 @@ func (s *WebBrokerAPIDeploymentService) undeployWebBrokerAPIDeployment(apiUUID s return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } initialStatus := model.DeploymentStatusUndeployed @@ -391,16 +392,16 @@ func (s *WebBrokerAPIDeploymentService) restoreWebBrokerAPIDeployment(apiUUID st return nil, err } if targetDeployment == nil { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } // Only allow restoring ARCHIVED (nil status) or UNDEPLOYED deployments if targetDeployment.Status != nil && *targetDeployment.Status != model.DeploymentStatusUndeployed { - return nil, constants.ErrInvalidDeploymentRestoreState + return nil, apperror.DeploymentRestoreConflict.New() } if targetDeployment.GatewayID != *gatewayId { - return nil, constants.ErrGatewayIDMismatch + return nil, apperror.DeploymentGatewayMismatch.New() } currentDeploymentID, status, _, err := s.deploymentRepo.GetStatus(apiUUID, orgID, targetDeployment.GatewayID) @@ -408,7 +409,7 @@ func (s *WebBrokerAPIDeploymentService) restoreWebBrokerAPIDeployment(apiUUID st return nil, fmt.Errorf("failed to get deployment status: %w", err) } if currentDeploymentID == *deploymentId && status == model.DeploymentStatusDeployed { - return nil, constants.ErrDeploymentAlreadyDeployed + return nil, apperror.DeploymentRestoreConflict.New() } gateway, err := s.gatewayRepo.GetByUUID(targetDeployment.GatewayID) @@ -416,7 +417,7 @@ func (s *WebBrokerAPIDeploymentService) restoreWebBrokerAPIDeployment(apiUUID st return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil || gateway.OrganizationID != orgID { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } initialStatus := model.DeploymentStatusDeployed @@ -466,7 +467,7 @@ func (s *WebBrokerAPIDeploymentService) getWebBrokerAPIDeployment(apiUUID, deplo return nil, err } if webbrokerAPI == nil { - return nil, constants.ErrWebBrokerAPINotFound + return nil, apperror.WebBrokerAPINotFound.New() } deployment, err := s.deploymentRepo.GetWithState(deploymentID, apiUUID, orgID) @@ -474,7 +475,7 @@ func (s *WebBrokerAPIDeploymentService) getWebBrokerAPIDeployment(apiUUID, deplo return nil, err } if deployment == nil { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } return toAPIDeploymentResponse( @@ -498,7 +499,7 @@ func (s *WebBrokerAPIDeploymentService) getWebBrokerAPIDeployments(apiUUID, orgI return nil, err } if webbrokerAPI == nil { - return nil, constants.ErrWebBrokerAPINotFound + return nil, apperror.WebBrokerAPINotFound.New() } if status != nil { @@ -511,7 +512,7 @@ func (s *WebBrokerAPIDeploymentService) getWebBrokerAPIDeployments(apiUUID, orgI string(model.DeploymentStatusFailed): true, } if !validStatuses[*status] { - return nil, constants.ErrInvalidDeploymentStatus + return nil, apperror.DeploymentInvalidStatus.New() } } @@ -557,7 +558,7 @@ func (s *WebBrokerAPIDeploymentService) deleteWebBrokerAPIDeployment(apiUUID, de return err } if webbrokerAPI == nil { - return constants.ErrWebBrokerAPINotFound + return apperror.WebBrokerAPINotFound.New() } deployment, err := s.deploymentRepo.GetWithState(deploymentID, apiUUID, orgID) @@ -565,11 +566,11 @@ func (s *WebBrokerAPIDeploymentService) deleteWebBrokerAPIDeployment(apiUUID, de return err } if deployment == nil { - return constants.ErrDeploymentNotFound + return apperror.DeploymentNotFound.New() } if deployment.Status != nil && *deployment.Status == model.DeploymentStatusDeployed { - return constants.ErrDeploymentIsDeployed + return apperror.DeploymentActive.New() } if err := s.deploymentRepo.Delete(deploymentID, apiUUID, orgID); err != nil { @@ -617,7 +618,7 @@ func (s *WebBrokerAPIDeploymentService) getWebBrokerAPIUUIDByHandle(handle, orgU return "", err } if artifact == nil { - return "", constants.ErrArtifactNotFound + return "", apperror.ArtifactNotFound.New() } return artifact.UUID, nil diff --git a/platform-api/plugins/eventgateway/service/websub_api.go b/platform-api/plugins/eventgateway/service/websub_api.go index 89d0cf7cc..36791a272 100644 --- a/platform-api/plugins/eventgateway/service/websub_api.go +++ b/platform-api/plugins/eventgateway/service/websub_api.go @@ -27,6 +27,7 @@ import ( "github.com/wso2/api-platform/platform-api/api" "github.com/wso2/api-platform/platform-api/config" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/repository" @@ -104,13 +105,13 @@ func (s *WebSubAPIService) webSubAPIListItemResolved(m *model.WebSubAPI) (*api.W // Create creates a new WebSub API func (s *WebSubAPIService) Create(orgUUID, createdBy string, req *api.WebSubAPI) (*api.WebSubAPI, error) { if req == nil { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("A request body is required.") } if utils.ValueOrEmpty(req.Id) == "" || req.DisplayName == "" || req.Version == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The id, displayName and version fields are required.") } if req.ProjectId == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The projectId field is required.") } handle := utils.ValueOrEmpty(req.Id) @@ -122,10 +123,10 @@ func (s *WebSubAPIService) Create(orgUUID, createdBy string, req *api.WebSubAPI) return nil, fmt.Errorf("failed to validate project: %w", err) } if project == nil { - return nil, constants.ErrProjectNotFound + return nil, apperror.ProjectRefNotFound.New() } if project.OrganizationID != orgUUID { - return nil, constants.ErrProjectNotFound + return nil, apperror.ProjectRefNotFound.New() } } @@ -135,7 +136,7 @@ func (s *WebSubAPIService) Create(orgUUID, createdBy string, req *api.WebSubAPI) return nil, fmt.Errorf("failed to check WebSub API exists: %w", err) } if exists { - return nil, constants.ErrWebSubAPIExists + return nil, apperror.WebSubAPIExists.New() } // Enforce the per-organization WebSub API limit (unlimited when not configured). @@ -144,7 +145,7 @@ func (s *WebSubAPIService) Create(orgUUID, createdBy string, req *api.WebSubAPI) return nil, fmt.Errorf("failed to count existing WebSub APIs: %w", err) } if config.LimitReached(count, s.cfg.ArtifactLimits.MaxWebSubAPIsPerOrg) { - return nil, constants.ErrWebSubAPILimitReached + return nil, apperror.WebSubAPILimitReached.New() } transport := []string{"http", "https"} @@ -189,7 +190,7 @@ func (s *WebSubAPIService) Create(orgUUID, createdBy string, req *api.WebSubAPI) if err := s.repo.Create(m); err != nil { if isSQLiteUniqueConstraint(err) { - return nil, constants.ErrWebSubAPIExists + return nil, apperror.WebSubAPIExists.Wrap(err) } return nil, fmt.Errorf("failed to create WebSub API: %w", err) } @@ -203,7 +204,7 @@ func (s *WebSubAPIService) Create(orgUUID, createdBy string, req *api.WebSubAPI) // Get retrieves a WebSub API by its handle func (s *WebSubAPIService) Get(orgUUID, handle string) (*api.WebSubAPI, error) { if handle == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The WebSub API id is required.") } m, err := s.repo.GetByHandle(handle, orgUUID) @@ -211,7 +212,7 @@ func (s *WebSubAPIService) Get(orgUUID, handle string) (*api.WebSubAPI, error) { return nil, fmt.Errorf("failed to get WebSub API: %w", err) } if m == nil { - return nil, constants.ErrWebSubAPINotFound + return nil, apperror.WebSubAPINotFound.New() } return s.toWebSubAPI(m) @@ -260,10 +261,10 @@ func (s *WebSubAPIService) List(orgUUID, projectUUID string, limit, offset int) // Update updates an existing WebSub API func (s *WebSubAPIService) Update(orgUUID, handle, updatedBy string, req *api.WebSubAPI) (*api.WebSubAPI, error) { if handle == "" || req == nil { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The WebSub API id and a request body are required.") } if req.DisplayName == "" || req.Version == "" { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("The displayName and version fields are required.") } // Get existing existing, err := s.repo.GetByHandle(handle, orgUUID) @@ -271,7 +272,7 @@ func (s *WebSubAPIService) Update(orgUUID, handle, updatedBy string, req *api.We return nil, fmt.Errorf("failed to get WebSub API: %w", err) } if existing == nil { - return nil, constants.ErrWebSubAPINotFound + return nil, apperror.WebSubAPINotFound.New() } // DP-originated artifacts are read-only in the control plane. if err := ensureOriginMutable(existing.Origin); err != nil { @@ -314,7 +315,7 @@ func (s *WebSubAPIService) Update(orgUUID, handle, updatedBy string, req *api.We if err := s.repo.Update(existing); err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, constants.ErrWebSubAPINotFound + return nil, apperror.WebSubAPINotFound.Wrap(err) } return nil, fmt.Errorf("failed to update WebSub API: %w", err) } @@ -328,7 +329,7 @@ func (s *WebSubAPIService) Update(orgUUID, handle, updatedBy string, req *api.We // Delete deletes a WebSub API by its handle func (s *WebSubAPIService) Delete(orgUUID, handle, deletedBy string) error { if handle == "" { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New("The WebSub API id is required.") } // Get the WebSub API UUID before deletion (needed for gateway deletion event) @@ -337,7 +338,7 @@ func (s *WebSubAPIService) Delete(orgUUID, handle, deletedBy string) error { return fmt.Errorf("failed to get WebSub API: %w", err) } if websubAPI == nil { - return constants.ErrWebSubAPINotFound + return apperror.WebSubAPINotFound.New() } // DP-originated artifacts are read-only in the control plane and cannot be deleted from the CP. if err := ensureOriginMutable(websubAPI.Origin); err != nil { @@ -357,7 +358,7 @@ func (s *WebSubAPIService) Delete(orgUUID, handle, deletedBy string) error { if err := s.repo.Delete(handle, orgUUID); err != nil { if errors.Is(err, sql.ErrNoRows) { - return constants.ErrWebSubAPINotFound + return apperror.WebSubAPINotFound.Wrap(err) } return fmt.Errorf("failed to delete WebSub API: %w", err) } diff --git a/platform-api/plugins/eventgateway/service/websub_api_deployment.go b/platform-api/plugins/eventgateway/service/websub_api_deployment.go index e81e4a198..96945ac2e 100644 --- a/platform-api/plugins/eventgateway/service/websub_api_deployment.go +++ b/platform-api/plugins/eventgateway/service/websub_api_deployment.go @@ -28,6 +28,7 @@ import ( "github.com/wso2/api-platform/platform-api/api" "github.com/wso2/api-platform/platform-api/config" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/repository" @@ -152,18 +153,18 @@ func (s *WebSubAPIDeploymentService) GetWebSubAPIDeploymentsByHandle(apiHandle, // deployWebSubAPI deploys a WebSub API to a gateway func (s *WebSubAPIDeploymentService) deployWebSubAPI(apiUUID string, req *api.DeployRequest, orgID, createdBy string) (*api.DeploymentResponse, error) { if req == nil { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("A request body is required.") } // DP-originated artifacts are read-only in the control plane; deployment cannot be CP-initiated. if err := ensureArtifactMutableByUUID(s.artifactRepo, apiUUID, orgID); err != nil { return nil, err } if req.Base == "" { - return nil, constants.ErrDeploymentBaseRequired + return nil, apperror.ValidationFailed.New("Base is required (use 'current' or a deploymentId).") } gatewayHandle := strings.TrimSpace(req.GatewayId) if gatewayHandle == "" { - return nil, constants.ErrDeploymentGatewayIDRequired + return nil, apperror.ValidationFailed.New("Gateway ID is required.") } metadata := utils.MapValueOrEmpty(req.Metadata) @@ -173,7 +174,7 @@ func (s *WebSubAPIDeploymentService) deployWebSubAPI(apiUUID string, req *api.De return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } gatewayID := gateway.ID @@ -182,7 +183,7 @@ func (s *WebSubAPIDeploymentService) deployWebSubAPI(apiUUID string, req *api.De return nil, err } if websubAPI == nil { - return nil, constants.ErrWebSubAPINotFound + return nil, apperror.WebSubAPINotFound.New() } // Generate deployment ID @@ -203,8 +204,8 @@ func (s *WebSubAPIDeploymentService) deployWebSubAPI(apiUUID string, req *api.De } else { baseDeployment, err := s.deploymentRepo.GetWithContent(req.Base, apiUUID, orgID) if err != nil { - if errors.Is(err, constants.ErrDeploymentNotFound) { - return nil, constants.ErrBaseDeploymentNotFound + if apperror.DeploymentNotFound.Is(err) { + return nil, apperror.DeploymentBaseNotFound.New() } return nil, fmt.Errorf("failed to get base deployment: %w", err) } @@ -298,7 +299,7 @@ func (s *WebSubAPIDeploymentService) undeployWebSubAPIDeployment(apiUUID string, return nil, err } if websubAPI == nil { - return nil, constants.ErrWebSubAPINotFound + return nil, apperror.WebSubAPINotFound.New() } var deployment *model.Deployment @@ -308,7 +309,7 @@ func (s *WebSubAPIDeploymentService) undeployWebSubAPIDeployment(apiUUID string, return nil, err } if deployment == nil { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } } else if gatewayId != nil { deployment, err = s.deploymentRepo.GetCurrentByGateway(apiUUID, *gatewayId, orgID) @@ -316,18 +317,18 @@ func (s *WebSubAPIDeploymentService) undeployWebSubAPIDeployment(apiUUID string, return nil, err } if deployment == nil { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } } else { - return nil, constants.ErrInvalidInput + return nil, apperror.ValidationFailed.New("Either a deploymentId or a gatewayId is required.") } if gatewayId != nil && deployment.GatewayID != *gatewayId { - return nil, constants.ErrGatewayIDMismatch + return nil, apperror.DeploymentGatewayMismatch.New() } if deployment.Status == nil || *deployment.Status != model.DeploymentStatusDeployed { - return nil, constants.ErrDeploymentNotActive + return nil, apperror.DeploymentNotActive.New("WebSub API") } gateway, err := s.gatewayRepo.GetByUUID(deployment.GatewayID) @@ -335,7 +336,7 @@ func (s *WebSubAPIDeploymentService) undeployWebSubAPIDeployment(apiUUID string, return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } initialStatus := model.DeploymentStatusUndeployed @@ -390,16 +391,16 @@ func (s *WebSubAPIDeploymentService) restoreWebSubAPIDeployment(apiUUID string, return nil, err } if targetDeployment == nil { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } // Only allow restoring ARCHIVED (nil status) or UNDEPLOYED deployments if targetDeployment.Status != nil && *targetDeployment.Status != model.DeploymentStatusUndeployed { - return nil, constants.ErrInvalidDeploymentRestoreState + return nil, apperror.DeploymentRestoreConflict.New() } if targetDeployment.GatewayID != *gatewayId { - return nil, constants.ErrGatewayIDMismatch + return nil, apperror.DeploymentGatewayMismatch.New() } currentDeploymentID, status, _, err := s.deploymentRepo.GetStatus(apiUUID, orgID, targetDeployment.GatewayID) @@ -407,7 +408,7 @@ func (s *WebSubAPIDeploymentService) restoreWebSubAPIDeployment(apiUUID string, return nil, fmt.Errorf("failed to get deployment status: %w", err) } if currentDeploymentID == *deploymentId && status == model.DeploymentStatusDeployed { - return nil, constants.ErrDeploymentAlreadyDeployed + return nil, apperror.DeploymentRestoreConflict.New() } gateway, err := s.gatewayRepo.GetByUUID(targetDeployment.GatewayID) @@ -415,7 +416,7 @@ func (s *WebSubAPIDeploymentService) restoreWebSubAPIDeployment(apiUUID string, return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil || gateway.OrganizationID != orgID { - return nil, constants.ErrGatewayNotFound + return nil, apperror.GatewayNotFound.New() } initialStatus := model.DeploymentStatusDeployed @@ -464,7 +465,7 @@ func (s *WebSubAPIDeploymentService) getWebSubAPIDeployment(apiUUID, deploymentI return nil, err } if websubAPI == nil { - return nil, constants.ErrWebSubAPINotFound + return nil, apperror.WebSubAPINotFound.New() } deployment, err := s.deploymentRepo.GetWithState(deploymentID, apiUUID, orgID) @@ -472,7 +473,7 @@ func (s *WebSubAPIDeploymentService) getWebSubAPIDeployment(apiUUID, deploymentI return nil, err } if deployment == nil { - return nil, constants.ErrDeploymentNotFound + return nil, apperror.DeploymentNotFound.New() } return toAPIDeploymentResponse( @@ -496,7 +497,7 @@ func (s *WebSubAPIDeploymentService) getWebSubAPIDeployments(apiUUID, orgID stri return nil, err } if websubAPI == nil { - return nil, constants.ErrWebSubAPINotFound + return nil, apperror.WebSubAPINotFound.New() } if status != nil { @@ -509,7 +510,7 @@ func (s *WebSubAPIDeploymentService) getWebSubAPIDeployments(apiUUID, orgID stri string(model.DeploymentStatusFailed): true, } if !validStatuses[*status] { - return nil, constants.ErrInvalidDeploymentStatus + return nil, apperror.DeploymentInvalidStatus.New() } } @@ -555,7 +556,7 @@ func (s *WebSubAPIDeploymentService) deleteWebSubAPIDeployment(apiUUID, deployme return err } if websubAPI == nil { - return constants.ErrWebSubAPINotFound + return apperror.WebSubAPINotFound.New() } deployment, err := s.deploymentRepo.GetWithState(deploymentID, apiUUID, orgID) @@ -563,11 +564,11 @@ func (s *WebSubAPIDeploymentService) deleteWebSubAPIDeployment(apiUUID, deployme return err } if deployment == nil { - return constants.ErrDeploymentNotFound + return apperror.DeploymentNotFound.New() } if deployment.Status != nil && *deployment.Status == model.DeploymentStatusDeployed { - return constants.ErrDeploymentIsDeployed + return apperror.DeploymentActive.New() } if err := s.deploymentRepo.Delete(deploymentID, apiUUID, orgID); err != nil { @@ -615,7 +616,7 @@ func (s *WebSubAPIDeploymentService) getWebSubAPIUUIDByHandle(handle, orgUUID st return "", err } if artifact == nil { - return "", constants.ErrArtifactNotFound + return "", apperror.ArtifactNotFound.New() } return artifact.UUID, nil diff --git a/platform-api/plugins/eventgateway/service/websub_api_hmac_secret.go b/platform-api/plugins/eventgateway/service/websub_api_hmac_secret.go index e96f94567..7fc93fb85 100644 --- a/platform-api/plugins/eventgateway/service/websub_api_hmac_secret.go +++ b/platform-api/plugins/eventgateway/service/websub_api_hmac_secret.go @@ -28,7 +28,7 @@ import ( "log/slog" "strings" - "github.com/wso2/api-platform/platform-api/internal/constants" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/repository" coreservice "github.com/wso2/api-platform/platform-api/internal/service" @@ -62,7 +62,7 @@ func NewWebSubAPIHmacSecretService( slogger *slog.Logger, ) (*WebSubAPIHmacSecretService, error) { if encryptionKeyStr == "" { - return nil, fmt.Errorf("%w", constants.ErrHmacSecretEncryptionKeyMissing) + return nil, apperror.HmacSecretNotConfigured.New() } key, err := utils.DeriveEncryptionKey(encryptionKeyStr) if err != nil { @@ -87,7 +87,7 @@ func (s *WebSubAPIHmacSecretService) Generate(orgUUID, apiHandle, name, external return nil, "", fmt.Errorf("failed to look up WebSub API: %w", err) } if api == nil { - return nil, "", constants.ErrWebSubAPINotFound + return nil, "", apperror.WebSubAPINotFound.New() } handle := slugifyHmacSecret(name) @@ -104,7 +104,7 @@ func (s *WebSubAPIHmacSecretService) Generate(orgUUID, apiHandle, name, external var plaintext string if externalSecret != "" { if len(externalSecret) < 32 { - return nil, "", constants.ErrHmacSecretInvalidValue + return nil, "", apperror.HmacSecretInvalidValue.New() } plaintext = externalSecret } else { @@ -137,7 +137,7 @@ func (s *WebSubAPIHmacSecretService) Generate(orgUUID, apiHandle, name, external if err := s.repo.Create(secret); err != nil { if isUniqueConstraintError(err) { - return nil, "", constants.ErrHmacSecretAlreadyExists + return nil, "", apperror.HmacSecretExists.New() } return nil, "", fmt.Errorf("failed to persist HMAC secret: %w", err) } @@ -153,7 +153,7 @@ func (s *WebSubAPIHmacSecretService) List(orgUUID, apiHandle string) ([]*model.W return nil, fmt.Errorf("failed to look up WebSub API: %w", err) } if api == nil { - return nil, constants.ErrWebSubAPINotFound + return nil, apperror.WebSubAPINotFound.New() } return s.repo.ListByArtifact(api.UUID) } @@ -167,7 +167,7 @@ func (s *WebSubAPIHmacSecretService) Regenerate(orgUUID, apiHandle, secretName, return nil, "", fmt.Errorf("failed to look up WebSub API: %w", err) } if api == nil { - return nil, "", constants.ErrWebSubAPINotFound + return nil, "", apperror.WebSubAPINotFound.New() } existing, err := s.repo.GetByArtifactAndName(api.UUID, secretName) @@ -175,13 +175,13 @@ func (s *WebSubAPIHmacSecretService) Regenerate(orgUUID, apiHandle, secretName, return nil, "", fmt.Errorf("failed to look up HMAC secret: %w", err) } if existing == nil { - return nil, "", constants.ErrHmacSecretNotFound + return nil, "", apperror.HmacSecretNotFound.New() } var plaintext string if externalSecret != "" { if len(externalSecret) < 32 { - return nil, "", constants.ErrHmacSecretInvalidValue + return nil, "", apperror.HmacSecretInvalidValue.New() } plaintext = externalSecret } else { @@ -200,7 +200,7 @@ func (s *WebSubAPIHmacSecretService) Regenerate(orgUUID, apiHandle, secretName, existing.UpdatedBy = userID if err := s.repo.Update(existing); err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, "", constants.ErrHmacSecretNotFound + return nil, "", apperror.HmacSecretNotFound.Wrap(err) } return nil, "", fmt.Errorf("failed to update HMAC secret: %w", err) } @@ -216,7 +216,7 @@ func (s *WebSubAPIHmacSecretService) Delete(orgUUID, apiHandle, secretName strin return fmt.Errorf("failed to look up WebSub API: %w", err) } if api == nil { - return constants.ErrWebSubAPINotFound + return apperror.WebSubAPINotFound.New() } existing, err := s.repo.GetByArtifactAndName(api.UUID, secretName) @@ -224,12 +224,12 @@ func (s *WebSubAPIHmacSecretService) Delete(orgUUID, apiHandle, secretName strin return fmt.Errorf("failed to look up HMAC secret: %w", err) } if existing == nil { - return constants.ErrHmacSecretNotFound + return apperror.HmacSecretNotFound.New() } if err := s.repo.Delete(api.UUID, secretName); err != nil { if errors.Is(err, sql.ErrNoRows) { - return constants.ErrHmacSecretNotFound + return apperror.HmacSecretNotFound.Wrap(err) } return fmt.Errorf("failed to delete HMAC secret: %w", err) } From 891f8a73a6d0a8f4265a67d882857cadf14d93ac Mon Sep 17 00:00:00 2001 From: Thushani Jayasekera Date: Fri, 10 Jul 2026 10:12:30 +0530 Subject: [PATCH 2/5] Refactor error handling in service and handler layers This commit enhances error handling across multiple services and handlers by replacing generic error messages with specific validation error types. Changes include updating error responses in the LLM provider API key deletion, project handle validation, and subscription plan creation and updates. Additionally, the MCP server initialization now returns a more descriptive unauthorized error. These improvements aim to provide clearer feedback for API consumers and streamline error management throughout the codebase. --- platform-api/internal/handler/llm_apikey.go | 4 ++++ platform-api/internal/handler/secret_integration_test.go | 2 +- platform-api/internal/service/application.go | 2 +- platform-api/internal/service/llm.go | 2 +- .../internal/service/subscription_plan_service.go | 8 ++++---- platform-api/internal/utils/mcp.go | 3 +-- 6 files changed, 12 insertions(+), 9 deletions(-) diff --git a/platform-api/internal/handler/llm_apikey.go b/platform-api/internal/handler/llm_apikey.go index 8d600fe85..c47e27dfe 100644 --- a/platform-api/internal/handler/llm_apikey.go +++ b/platform-api/internal/handler/llm_apikey.go @@ -107,6 +107,10 @@ func (h *LLMProviderAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.R } if err := h.apiKeyService.DeleteLLMProviderAPIKey(r.Context(), providerID, orgID, callerUserID, keyName); err != nil { + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err + } return serviceError(err, fmt.Sprintf("failed to delete LLM provider API key %s for provider %s in org %s", keyName, providerID, orgID)) } diff --git a/platform-api/internal/handler/secret_integration_test.go b/platform-api/internal/handler/secret_integration_test.go index 26d07a7cb..a2e839eff 100644 --- a/platform-api/internal/handler/secret_integration_test.go +++ b/platform-api/internal/handler/secret_integration_test.go @@ -923,7 +923,7 @@ func TestSecretService_ValidateSecretRefs_DeprecatedHandleRejected(t *testing.T) t.Fatal("expected validation error for DEPRECATED secret ref, got nil") } if !apperror.ValidationFailed.Is(err) { - t.Errorf("expected ErrSecretRefMissing, got %v", err) + t.Errorf("expected ValidationFailed, got %v", err) } } diff --git a/platform-api/internal/service/application.go b/platform-api/internal/service/application.go index fad2c83a3..95c0b470c 100644 --- a/platform-api/internal/service/application.go +++ b/platform-api/internal/service/application.go @@ -202,7 +202,7 @@ func (s *ApplicationService) GetApplicationsByOrganization(orgID, projectHandle opts.Offset = 0 } if strings.TrimSpace(projectHandle) == "" { - return nil, apperror.ProjectNotFound.New() + return nil, apperror.ValidationFailed.New("The project handle is required.") } project, err := s.projectRepo.GetProjectByHandleAndOrgID(projectHandle, orgID) diff --git a/platform-api/internal/service/llm.go b/platform-api/internal/service/llm.go index 2b1306ef1..fbb013768 100644 --- a/platform-api/internal/service/llm.go +++ b/platform-api/internal/service/llm.go @@ -1348,7 +1348,7 @@ func (s *LLMProxyService) List(orgUUID string, projectHandle *string, limit, off var resolvedProjectUUID *string if projectHandle != nil && *projectHandle != "" { if s.projectRepo == nil { - return nil, apperror.ProjectNotFound.New() + return nil, fmt.Errorf("cannot resolve project handle: project repository unavailable") } project, err := s.projectRepo.GetProjectByHandleAndOrgID(*projectHandle, orgUUID) if err != nil { diff --git a/platform-api/internal/service/subscription_plan_service.go b/platform-api/internal/service/subscription_plan_service.go index ef3a815ac..29ffeddf6 100644 --- a/platform-api/internal/service/subscription_plan_service.go +++ b/platform-api/internal/service/subscription_plan_service.go @@ -82,10 +82,10 @@ func (s *SubscriptionPlanService) ResolveOrgHandle(orgUUID string) string { // CreatePlan creates a new subscription plan func (s *SubscriptionPlanService) CreatePlan(orgUUID, actor string, plan *model.SubscriptionPlan) (*model.SubscriptionPlan, error) { if plan.Handle == "" { - return nil, fmt.Errorf("handle is required") + return nil, apperror.ValidationFailed.New("handle is required") } if plan.Name == "" { - return nil, fmt.Errorf("name is required") + return nil, apperror.ValidationFailed.New("name is required") } exists, err := s.planRepo.ExistsByHandleAndOrg(plan.Handle, orgUUID) @@ -173,7 +173,7 @@ func (s *SubscriptionPlanService) UpdatePlan(handle, orgUUID, actor string, upda if update.Name != nil { if *update.Name == "" { - return nil, fmt.Errorf("name is required") + return nil, apperror.ValidationFailed.New("name is required") } existing.Name = *update.Name } @@ -201,7 +201,7 @@ func (s *SubscriptionPlanService) UpdatePlan(handle, orgUUID, actor string, upda case model.SubscriptionPlanStatusActive, model.SubscriptionPlanStatusInactive: existing.Status = *update.Status default: - return nil, fmt.Errorf("invalid status: %s", *update.Status) + return nil, apperror.ValidationFailed.New(fmt.Sprintf("invalid status: %s", *update.Status)) } } diff --git a/platform-api/internal/utils/mcp.go b/platform-api/internal/utils/mcp.go index b75632a09..ad2da1f96 100644 --- a/platform-api/internal/utils/mcp.go +++ b/platform-api/internal/utils/mcp.go @@ -291,8 +291,7 @@ func initializeMCPServer(url string, headerName string, headerValue string) (str // Check HTTP status code if resp.StatusCode == http.StatusUnauthorized { - return "", nil, apperror.ValidationFailed.New( - "The MCP server returned 401 Unauthorized. Check the provided credentials."). + return "", nil, apperror.Unauthorized.New(). WithLogMessage("MCP server returned 401 Unauthorized to the initialize request") } if resp.StatusCode < 200 || resp.StatusCode >= 300 { From 5052e9a8338f7181d6a7d24f426e4e815ed391cd Mon Sep 17 00:00:00 2001 From: Thushani Jayasekera Date: Fri, 10 Jul 2026 10:57:57 +0530 Subject: [PATCH 3/5] Add InternalErrorResponse DTO and refactor error handling in Gateway Internal API This commit introduces a new InternalErrorResponse struct to standardize error responses for the Gateway Internal API. The error handling in the GatewayInternalAPIHandler has been updated to utilize this new structure, ensuring consistent error messages across various endpoints. This change enhances clarity for API consumers and improves maintainability by separating internal error representations from public API responses. --- .../internal/dto/gateway_internal_error.go | 47 ++++ .../internal/handler/gateway_internal.go | 204 ++++++++++-------- 2 files changed, 161 insertions(+), 90 deletions(-) create mode 100644 platform-api/internal/dto/gateway_internal_error.go diff --git a/platform-api/internal/dto/gateway_internal_error.go b/platform-api/internal/dto/gateway_internal_error.go new file mode 100644 index 000000000..8517c8c95 --- /dev/null +++ b/platform-api/internal/dto/gateway_internal_error.go @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * Licensed 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. + * + */ + +package dto + +// InternalErrorResponse is the error body of the Gateway Internal API +// (/api/internal/v1/*), as declared by the ErrorResponse schema in +// resources/gateway-internal-api.yaml: an integer HTTP status in Code, a +// short reason phrase in Message, and the specific detail in Description. +// +// It is deliberately NOT apperror.ErrorResponse. The public API +// (resources/openapi.yaml) returns {status, code, message} where code is a +// stable machine-readable catalog string; the gateway data plane consumes +// this older {code, message, description} contract instead. Keeping the two +// shapes in separate types is what stops a change to one from silently +// rewriting the wire format of the other — which is exactly what happened +// when both surfaces shared a single ErrorResponse struct. +type InternalErrorResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Description string `json:"description,omitempty"` +} + +// NewInternalErrorResponse builds a Gateway Internal API error body. code is +// the HTTP status, message the reason phrase ("Not Found"), and the optional +// description the specific detail ("API not found"). +func NewInternalErrorResponse(code int, message string, description ...string) InternalErrorResponse { + resp := InternalErrorResponse{Code: code, Message: message} + if len(description) > 0 { + resp.Description = description[0] + } + return resp +} diff --git a/platform-api/internal/handler/gateway_internal.go b/platform-api/internal/handler/gateway_internal.go index 602e4430f..e020ff90a 100644 --- a/platform-api/internal/handler/gateway_internal.go +++ b/platform-api/internal/handler/gateway_internal.go @@ -23,6 +23,7 @@ import ( "fmt" "log/slog" "net/http" + "sort" "strconv" "strings" "time" @@ -75,39 +76,62 @@ func (h *GatewayInternalAPIHandler) SetHmacSecretService(svc hmacSecretDecrypter h.hmacSecretService = svc } -// authenticateGateway validates the API key and returns the authenticated gateway. -func (h *GatewayInternalAPIHandler) authenticateGateway(apiKey string) (*model.Gateway, error) { - if apiKey == "" { - return nil, apperror.Unauthorized.New().WithLogMessage("api-key header absent") - } - return h.gatewayService.VerifyToken(apiKey) +// internalErrorFromApp renders a catalog error in the Gateway Internal API's +// error shape (see dto.InternalErrorResponse). That shape has no structured +// field for Details, so a map detail — the registered vs reported values on a +// manifest mismatch — is flattened into the description, sorted by key so the +// rendering is stable. LogMessage is internal-only and is never surfaced here. +func internalErrorFromApp(e *apperror.Error) dto.InternalErrorResponse { + description := e.Message + if details, ok := e.Details.(map[string]string); ok && len(details) > 0 { + keys := make([]string, 0, len(details)) + for k := range details { + keys = append(keys, k) + } + sort.Strings(keys) + parts := make([]string, 0, len(keys)) + for _, k := range keys { + parts = append(parts, fmt.Sprintf("%s=%s", k, details[k])) + } + description = fmt.Sprintf("%s (%s)", description, strings.Join(parts, ", ")) + } + return dto.NewInternalErrorResponse(e.HTTPStatus, http.StatusText(e.HTTPStatus), description) } // authenticateRequest extracts the API key from headers and authenticates the gateway. +// +// An absent header and a rejected key are reported distinctly, matching the +// Gateway Internal API contract. This is not a credential-probing oracle: the +// "header is required" branch fires only when the caller sent no key at all — +// something the caller already knows — while every outcome of VerifyToken +// (unknown key, expired key, or a backend failure) collapses to the same +// "Invalid or expired API key" response. A backend failure is still logged at +// ERROR so an outage is not silently indistinguishable from a bad key. func (h *GatewayInternalAPIHandler) authenticateRequest(w http.ResponseWriter, r *http.Request) (orgID, gatewayID string, ok bool) { clientIP := r.RemoteAddr if i := strings.LastIndex(clientIP, ":"); i != -1 { clientIP = clientIP[:i] } + apiKey := r.Header.Get("api-key") + if apiKey == "" { + h.slogger.Warn("Gateway authentication failed", "clientIP", clientIP, "detail", "api-key header absent") + httputil.WriteJSON(w, http.StatusUnauthorized, dto.NewInternalErrorResponse(401, "Unauthorized", + "API key is required. Provide 'api-key' header.")) + return "", "", false + } - gateway, err := h.authenticateGateway(apiKey) + gateway, err := h.gatewayService.VerifyToken(apiKey) if err != nil { - // Unified authentication failure (error-handling.md): a missing key, an - // unknown key, and an expired key must be indistinguishable to the caller, - // or the endpoint becomes a token-probing oracle. The specific reason is - // carried by the error's LogMessage and recorded here only. var appErr *apperror.Error if errors.As(err, &appErr) && appErr.HTTPStatus == http.StatusUnauthorized { h.slogger.Warn("Gateway authentication failed", "clientIP", clientIP, "detail", appErr.LogMessage) - httputil.WriteJSON(w, http.StatusUnauthorized, - apperror.NewErrorResponseWithCode(apperror.CodeCommonUnauthorized, "Invalid or expired credentials.")) - return "", "", false + } else { + h.slogger.Error("Gateway authentication errored", "clientIP", clientIP, "error", err) } - h.slogger.Error("Gateway authentication errored", "clientIP", clientIP, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, - apperror.NewErrorResponseWithCode(apperror.CodeCommonInternalError, "An unexpected error occurred.")) + httputil.WriteJSON(w, http.StatusUnauthorized, dto.NewInternalErrorResponse(401, "Unauthorized", + "Invalid or expired API key")) return "", "", false } return gateway.OrganizationID, gateway.ID, true @@ -122,7 +146,7 @@ func (h *GatewayInternalAPIHandler) GetAPI(w http.ResponseWriter, r *http.Reques apiID := r.PathValue("apiId") if apiID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, dto.NewInternalErrorResponse(400, "Bad Request", "API ID is required")) return } @@ -130,16 +154,16 @@ func (h *GatewayInternalAPIHandler) GetAPI(w http.ResponseWriter, r *http.Reques api, err := h.gatewayInternalService.GetActiveDeploymentByGateway(apiID, orgID, gatewayID) if err != nil { if apperror.DeploymentNotActive.Is(err) { - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(404, "Not Found", "No active deployment found for this API on this gateway")) return } if apperror.RESTAPINotFound.Is(err) { - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(404, "Not Found", "API not found")) return } - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to get API")) return } @@ -148,7 +172,7 @@ func (h *GatewayInternalAPIHandler) GetAPI(w http.ResponseWriter, r *http.Reques zipData, err := utils.CreateAPIYamlZip(api) if err != nil { h.slogger.Error("Failed to create ZIP file", "apiID", apiID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to create API package")) return } @@ -184,7 +208,7 @@ func (h *GatewayInternalAPIHandler) ImportGatewayArtifacts(w http.ResponseWriter clientIP = clientIP[:i] } h.slogger.Warn("Invalid import-gateway-artifacts request", "clientIP", clientIP, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, dto.NewInternalErrorResponse(400, "Bad Request", err.Error())) return } // 'total' is advisory: log a mismatch but proceed with what the zip actually contained. @@ -210,7 +234,7 @@ func (h *GatewayInternalAPIHandler) GetLLMProvider(w http.ResponseWriter, r *htt providerID := r.PathValue("providerId") if providerID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, dto.NewInternalErrorResponse(400, "Bad Request", "Provider ID is required")) return } @@ -218,16 +242,16 @@ func (h *GatewayInternalAPIHandler) GetLLMProvider(w http.ResponseWriter, r *htt provider, err := h.gatewayInternalService.GetActiveLLMProviderDeploymentByGateway(providerID, orgID, gatewayID) if err != nil { if apperror.DeploymentNotActive.Is(err) { - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(404, "Not Found", "No active deployment found for this LLM provider on this gateway")) return } if apperror.LLMProviderNotFound.Is(err) { - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(404, "Not Found", "LLM provider not found")) return } - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to get LLM provider")) return } @@ -236,7 +260,7 @@ func (h *GatewayInternalAPIHandler) GetLLMProvider(w http.ResponseWriter, r *htt zipData, err := utils.CreateLLMProviderYamlZip(provider) if err != nil { h.slogger.Error("Failed to create ZIP file", "providerID", providerID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to create LLM provider package")) return } @@ -260,7 +284,7 @@ func (h *GatewayInternalAPIHandler) GetLLMProxy(w http.ResponseWriter, r *http.R proxyID := r.PathValue("proxyId") if proxyID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, dto.NewInternalErrorResponse(400, "Bad Request", "Proxy ID is required")) return } @@ -268,16 +292,16 @@ func (h *GatewayInternalAPIHandler) GetLLMProxy(w http.ResponseWriter, r *http.R proxy, err := h.gatewayInternalService.GetActiveLLMProxyDeploymentByGateway(proxyID, orgID, gatewayID) if err != nil { if apperror.DeploymentNotActive.Is(err) { - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(404, "Not Found", "No active deployment found for this LLM proxy on this gateway")) return } if apperror.LLMProxyNotFound.Is(err) { - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(404, "Not Found", "LLM proxy not found")) return } - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to get LLM proxy")) return } @@ -286,7 +310,7 @@ func (h *GatewayInternalAPIHandler) GetLLMProxy(w http.ResponseWriter, r *http.R zipData, err := utils.CreateLLMProxyYamlZip(proxy) if err != nil { h.slogger.Error("Failed to create ZIP file", "proxyID", proxyID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to create LLM proxy package")) return } @@ -315,7 +339,7 @@ func (h *GatewayInternalAPIHandler) GetGatewayDeployments(w http.ResponseWriter, if sinceStr != "" { parsedTime, err := time.Parse(time.RFC3339, sinceStr) if err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, dto.NewInternalErrorResponse(400, "Bad Request", "Invalid 'since' parameter. Expected ISO 8601 format (e.g., 2026-03-04T10:00:00Z)")) return } @@ -325,12 +349,12 @@ func (h *GatewayInternalAPIHandler) GetGatewayDeployments(w http.ResponseWriter, deployments, err := h.gatewayInternalService.GetDeploymentsByGateway(orgID, gatewayID, since) if err != nil { if apperror.GatewayNotFound.Is(err) { - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(404, "Not Found", "Gateway not found")) return } h.slogger.Error("Failed to get gateway deployments", "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to get deployments")) return } @@ -348,7 +372,7 @@ func (h *GatewayInternalAPIHandler) BatchFetchDeployments(w http.ResponseWriter, // Enforce Accept header - only application/x-tar+gzip is supported if accept := r.Header.Get("Accept"); accept != "application/x-tar+gzip" { - httputil.WriteJSON(w, http.StatusNotAcceptable, apperror.NewErrorResponse(406, "Not Acceptable", + httputil.WriteJSON(w, http.StatusNotAcceptable, dto.NewInternalErrorResponse(406, "Not Acceptable", "This endpoint only supports Accept: application/x-tar+gzip")) return } @@ -356,13 +380,13 @@ func (h *GatewayInternalAPIHandler) BatchFetchDeployments(w http.ResponseWriter, // Parse request body var req dto.DeploymentsBatchFetchRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, dto.NewInternalErrorResponse(400, "Bad Request", "Invalid request body: "+err.Error())) return } if len(req.DeploymentIDs) == 0 { - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, dto.NewInternalErrorResponse(400, "Bad Request", "At least one deployment ID is required")) return } @@ -371,12 +395,12 @@ func (h *GatewayInternalAPIHandler) BatchFetchDeployments(w http.ResponseWriter, contentMap, err := h.gatewayInternalService.GetDeploymentContentBatch(orgID, gatewayID, req.DeploymentIDs) if err != nil { if apperror.GatewayNotFound.Is(err) { - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(404, "Not Found", "Gateway not found")) return } h.slogger.Error("Failed to get deployment content batch", "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to get deployment content")) return } @@ -385,7 +409,7 @@ func (h *GatewayInternalAPIHandler) BatchFetchDeployments(w http.ResponseWriter, tarGzData, err := utils.CreateBatchDeploymentTarGz(contentMap) if err != nil { h.slogger.Error("Failed to create batch TAR.GZ archive", "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to create deployment package")) return } @@ -417,7 +441,7 @@ func (h *GatewayInternalAPIHandler) GetSubscriptions(w http.ResponseWriter, r *h "clientIP", clientIP, "organizationId", orgID, "apiId", apiID) - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, dto.NewInternalErrorResponse(400, "Bad Request", "API ID is required")) return } @@ -429,7 +453,7 @@ func (h *GatewayInternalAPIHandler) GetSubscriptions(w http.ResponseWriter, r *h "organizationId", orgID, "gatewayId", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(404, "Not Found", "API not found")) return } @@ -438,7 +462,7 @@ func (h *GatewayInternalAPIHandler) GetSubscriptions(w http.ResponseWriter, r *h "apiId", apiID, "organizationId", orgID, "gatewayId", gatewayID) - httputil.WriteJSON(w, http.StatusForbidden, apperror.NewErrorResponse(403, "Forbidden", + httputil.WriteJSON(w, http.StatusForbidden, dto.NewInternalErrorResponse(403, "Forbidden", "API is not associated with this gateway")) return } @@ -447,7 +471,7 @@ func (h *GatewayInternalAPIHandler) GetSubscriptions(w http.ResponseWriter, r *h "organizationId", orgID, "gatewayId", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to verify API deployment")) return } @@ -459,7 +483,7 @@ func (h *GatewayInternalAPIHandler) GetSubscriptions(w http.ResponseWriter, r *h "apiId", apiID, "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(404, "Not Found", "API not found")) return } @@ -467,7 +491,7 @@ func (h *GatewayInternalAPIHandler) GetSubscriptions(w http.ResponseWriter, r *h "apiId", apiID, "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to get subscriptions")) return } @@ -487,7 +511,7 @@ func (h *GatewayInternalAPIHandler) GetSubscriptionPlans(w http.ResponseWriter, h.slogger.Error("Failed to list subscription plans", "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to get subscription plans")) return } @@ -504,7 +528,7 @@ func (h *GatewayInternalAPIHandler) GetMCPProxy(w http.ResponseWriter, r *http.R } proxyID := r.PathValue("proxyId") if proxyID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, dto.NewInternalErrorResponse(400, "Bad Request", "Proxy ID is required")) return } @@ -517,18 +541,18 @@ func (h *GatewayInternalAPIHandler) GetMCPProxy(w http.ResponseWriter, r *http.R } if apperror.DeploymentNotActive.Is(err) { h.slogger.Error("No active deployment found for MCP proxy", "clientIP", clientIP, "proxyID", proxyID, "orgID", orgID, "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(404, "Not Found", "No active deployment found for this MCP proxy on this gateway")) return } if apperror.MCPProxyNotFound.Is(err) { h.slogger.Error("MCP proxy not found", "clientIP", clientIP, "proxyID", proxyID, "orgID", orgID, "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(404, "Not Found", "MCP proxy not found")) return } h.slogger.Error("Failed to get MCP proxy", "clientIP", clientIP, "proxyID", proxyID, "orgID", orgID, "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to get MCP proxy")) return } @@ -537,7 +561,7 @@ func (h *GatewayInternalAPIHandler) GetMCPProxy(w http.ResponseWriter, r *http.R zipData, err := utils.CreateMCPProxyYamlZip(proxy) if err != nil { h.slogger.Error("Failed to create ZIP file", "proxyID", proxyID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to create MCP proxy package")) return } @@ -561,7 +585,7 @@ func (h *GatewayInternalAPIHandler) GetWebSubAPI(w http.ResponseWriter, r *http. apiID := r.PathValue("apiId") if apiID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, dto.NewInternalErrorResponse(400, "Bad Request", "API ID is required")) return } @@ -574,18 +598,18 @@ func (h *GatewayInternalAPIHandler) GetWebSubAPI(w http.ResponseWriter, r *http. } if apperror.DeploymentNotActive.Is(err) { h.slogger.Error("No active deployment found for WebSub API", "clientIP", clientIP, "apiID", apiID, "orgID", orgID, "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(404, "Not Found", "No active deployment found for this WebSub API on this gateway")) return } if apperror.WebSubAPINotFound.Is(err) { h.slogger.Error("WebSub API not found", "clientIP", clientIP, "apiID", apiID, "orgID", orgID, "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(404, "Not Found", "WebSub API not found")) return } h.slogger.Error("Failed to get WebSub API", "clientIP", clientIP, "apiID", apiID, "orgID", orgID, "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to get WebSub API")) return } @@ -594,7 +618,7 @@ func (h *GatewayInternalAPIHandler) GetWebSubAPI(w http.ResponseWriter, r *http. zipData, err := utils.CreateWebSubAPIYamlZip(api) if err != nil { h.slogger.Error("Failed to create ZIP file", "apiID", apiID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to create WebSub API package")) return } @@ -618,7 +642,7 @@ func (h *GatewayInternalAPIHandler) GetWebBrokerAPI(w http.ResponseWriter, r *ht apiID := r.PathValue("apiId") if apiID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, dto.NewInternalErrorResponse(400, "Bad Request", "API ID is required")) return } @@ -631,18 +655,18 @@ func (h *GatewayInternalAPIHandler) GetWebBrokerAPI(w http.ResponseWriter, r *ht } if apperror.DeploymentNotActive.Is(err) { h.slogger.Error("No active deployment found for WebBroker API", "clientIP", clientIP, "apiID", apiID, "orgID", orgID, "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(404, "Not Found", "No active deployment found for this WebBroker API on this gateway")) return } if apperror.WebBrokerAPINotFound.Is(err) { h.slogger.Error("WebBroker API not found", "clientIP", clientIP, "apiID", apiID, "orgID", orgID, "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(404, "Not Found", "WebBroker API not found")) return } h.slogger.Error("Failed to get WebBroker API", "clientIP", clientIP, "apiID", apiID, "orgID", orgID, "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to get WebBroker API")) return } @@ -651,7 +675,7 @@ func (h *GatewayInternalAPIHandler) GetWebBrokerAPI(w http.ResponseWriter, r *ht zipData, err := utils.CreateWebBrokerAPIYamlZip(api) if err != nil { h.slogger.Error("Failed to create ZIP file", "apiID", apiID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to create WebBroker API package")) return } @@ -680,24 +704,24 @@ func (h *GatewayInternalAPIHandler) ReceiveGatewayManifest(w http.ResponseWriter Policies []service.GatewayPolicyInput `json:"policies"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, dto.NewInternalErrorResponse(400, "Bad Request", err.Error())) return } if err := h.gatewayService.ReceiveGatewayManifest(orgID, gatewayID, body.Version, body.FunctionalityType, body.Policies); err != nil { - // A catalog error already carries the status, code, and a client-sterile - // message (version/type mismatches attach the registered vs reported values - // as structured details). Previously this echoed err.Error() into the body. + // A catalog error already carries the status and a client-sterile message + // (version/type mismatches attach the registered vs reported values as + // structured details). Previously this echoed err.Error() into the body. var appErr *apperror.Error if errors.As(err, &appErr) { h.slogger.Warn("Gateway manifest rejected", "gatewayID", gatewayID, "code", appErr.Code, "detail", appErr.LogMessage) - apperror.WriteHTTP(w, appErr, "") + httputil.WriteJSON(w, appErr.HTTPStatus, internalErrorFromApp(appErr)) return } h.slogger.Error("Failed to store gateway manifest", "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, - apperror.NewErrorResponseWithCode(apperror.CodeCommonInternalError, "An unexpected error occurred.")) + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, + "Internal Server Error", "Failed to store gateway manifest")) return } @@ -714,7 +738,7 @@ func (h *GatewayInternalAPIHandler) GetRestAPIAPIKeys(w http.ResponseWriter, r * keys, err := h.gatewayInternalService.GetAPIKeysByKind(gatewayID, orgID, constants.RestApi, issuer) if err != nil { h.slogger.Error("Failed to get API keys for REST APIs", "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to get API keys")) return } httputil.WriteJSON(w, http.StatusOK, keys) @@ -730,7 +754,7 @@ func (h *GatewayInternalAPIHandler) GetLLMProviderAPIKeys(w http.ResponseWriter, keys, err := h.gatewayInternalService.GetAPIKeysByKind(gatewayID, orgID, constants.LLMProvider, issuer) if err != nil { h.slogger.Error("Failed to get API keys for LLM providers", "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to get API keys")) return } httputil.WriteJSON(w, http.StatusOK, keys) @@ -746,7 +770,7 @@ func (h *GatewayInternalAPIHandler) GetLLMProxyAPIKeys(w http.ResponseWriter, r keys, err := h.gatewayInternalService.GetAPIKeysByKind(gatewayID, orgID, constants.LLMProxy, issuer) if err != nil { h.slogger.Error("Failed to get API keys for LLM proxies", "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to get API keys")) return } httputil.WriteJSON(w, http.StatusOK, keys) @@ -762,7 +786,7 @@ func (h *GatewayInternalAPIHandler) GetWebSubAPIAPIKeys(w http.ResponseWriter, r keys, err := h.gatewayInternalService.GetAPIKeysByKind(gatewayID, orgID, constants.WebSubApi, issuer) if err != nil { h.slogger.Error("Failed to get API keys for WebSub APIs", "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to get API keys")) return } httputil.WriteJSON(w, http.StatusOK, keys) @@ -778,7 +802,7 @@ func (h *GatewayInternalAPIHandler) GetWebBrokerAPIAPIKeys(w http.ResponseWriter keys, err := h.gatewayInternalService.GetAPIKeysByKind(gatewayID, orgID, constants.WebBrokerApi, issuer) if err != nil { h.slogger.Error("Failed to get API keys for WebBroker APIs", "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to get API keys")) return } httputil.WriteJSON(w, http.StatusOK, keys) @@ -796,7 +820,7 @@ func (h *GatewayInternalAPIHandler) CheckArtifactsExist(w http.ResponseWriter, r var req dto.ArtifactsExistRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, dto.NewInternalErrorResponse(400, "Bad Request", "Invalid request body: artifactIds is required and must be a non-empty array")) return } @@ -804,7 +828,7 @@ func (h *GatewayInternalAPIHandler) CheckArtifactsExist(w http.ResponseWriter, r existingIDs, err := h.gatewayInternalService.CheckArtifactsExist(orgID, req.ArtifactIDs) if err != nil { h.slogger.Error("Failed to check artifact existence", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to check artifact existence")) return } @@ -840,20 +864,20 @@ func (h *GatewayInternalAPIHandler) GetWebSubAPIHmacSecrets(w http.ResponseWrite apiID := r.PathValue("apiId") if apiID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, dto.NewInternalErrorResponse(400, "Bad Request", "API ID is required")) return } if h.hmacSecretService == nil { h.slogger.Warn("HMAC secret service not configured", "apiID", apiID) - httputil.WriteJSON(w, http.StatusServiceUnavailable, apperror.NewErrorResponse(503, "Service Unavailable", "HMAC secret management is not configured on this server")) + httputil.WriteJSON(w, http.StatusServiceUnavailable, dto.NewInternalErrorResponse(503, "Service Unavailable", "HMAC secret management is not configured on this server")) return } secrets, err := h.hmacSecretService.ListByArtifactUUID(apiID) if err != nil { h.slogger.Error("Failed to list HMAC secrets for WebSub API", "apiID", apiID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to get HMAC secrets")) + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to get HMAC secrets")) return } @@ -862,7 +886,7 @@ func (h *GatewayInternalAPIHandler) GetWebSubAPIHmacSecrets(w http.ResponseWrite plaintext, err := h.hmacSecretService.DecryptSecret(s) if err != nil { h.slogger.Error("Failed to decrypt HMAC secret", "apiID", apiID, "secretName", s.Handle, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to decrypt HMAC secret")) + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to decrypt HMAC secret")) return } items = append(items, dto.GatewayHmacSecretInfo{Name: s.Handle, Secret: plaintext}) @@ -886,7 +910,7 @@ func (h *GatewayInternalAPIHandler) GetGatewaySecrets(w http.ResponseWriter, r * if s := r.URL.Query().Get("updatedAfter"); s != "" { t, err := time.Parse(time.RFC3339, s) if err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, dto.NewInternalErrorResponse(400, "Bad Request", "Invalid 'updatedAfter' parameter. Expected RFC3339 format.")) return } @@ -898,7 +922,7 @@ func (h *GatewayInternalAPIHandler) GetGatewaySecrets(w http.ResponseWriter, r * secrets, err := h.gatewayInternalService.GetSecretsByGateway(orgID, gatewayID, updatedAfter) if err != nil { h.slogger.Error("Failed to list gateway secrets", "orgID", orgID, "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to retrieve secrets")) return } @@ -920,7 +944,7 @@ func (h *GatewayInternalAPIHandler) GetGatewaySecrets(w http.ResponseWriter, r * plaintext, err := h.secretService.DecryptCiphertext(s.Ciphertext) if err != nil { h.slogger.Error("Failed to decrypt secret for bulk fetch", "orgID", orgID, "handle", s.Handle, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to decrypt secret")) return } @@ -943,7 +967,7 @@ func (h *GatewayInternalAPIHandler) GetGatewaySecretValue(w http.ResponseWriter, handle := r.PathValue("handle") if handle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Secret handle is required")) + httputil.WriteJSON(w, http.StatusBadRequest, dto.NewInternalErrorResponse(400, "Bad Request", "Secret handle is required")) return } @@ -951,22 +975,22 @@ func (h *GatewayInternalAPIHandler) GetGatewaySecretValue(w http.ResponseWriter, deployed, err := h.gatewayInternalService.IsSecretDeployedOnGateway(orgID, gatewayID, handle) if err != nil { h.slogger.Error("Failed to check secret deployment scope", "orgID", orgID, "gatewayID", gatewayID, "handle", handle, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to verify secret access")) + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to verify secret access")) return } if !deployed { - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "Secret not found")) + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(404, "Not Found", "Secret not found")) return } plaintext, err := h.secretService.Decrypt(orgID, handle) if err != nil { if apperror.SecretNotFound.Is(err) { - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "Secret not found")) + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(404, "Not Found", "Secret not found")) return } h.slogger.Error("Failed to decrypt secret for gateway", "orgID", orgID, "handle", handle, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, "Internal Server Error", "Failed to decrypt secret")) return } From 9c8bf7849f6351bc1d9f1345a0a7f97d2a18d5a8 Mon Sep 17 00:00:00 2001 From: Thushani Jayasekera Date: Fri, 10 Jul 2026 11:24:03 +0530 Subject: [PATCH 4/5] Refactor error responses in portal API to standardize error handling This commit updates the portal API's error response structure by replacing the generic ErrorResponse schema with a more detailed Error schema. The new schema includes specific fields for status, code, message, and trackingId, providing clearer and more consistent error messages for API consumers. Additionally, the descriptions for various error responses have been enhanced to improve clarity regarding authentication failures and server errors. --- platform-api/resources/portal-api.yaml | 60 ++++++++++++++++++++------ 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/platform-api/resources/portal-api.yaml b/platform-api/resources/portal-api.yaml index a5ee98b35..99977462a 100644 --- a/platform-api/resources/portal-api.yaml +++ b/platform-api/resources/portal-api.yaml @@ -61,25 +61,34 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ErrorResponse' + $ref: '#/components/schemas/Error' example: - error: username and password are required + status: error + code: VALIDATION_FAILED + message: username and password are required '401': - description: Invalid credentials. + description: | + Authentication failed. The response is identical for an unknown username, a wrong + password, and a malformed credential, so it cannot be used to enumerate users. content: application/json: schema: - $ref: '#/components/schemas/ErrorResponse' + $ref: '#/components/schemas/Error' example: - error: invalid credentials + status: error + code: UNAUTHORIZED + message: Invalid or expired credentials. '500': description: The server failed to issue a token. content: application/json: schema: - $ref: '#/components/schemas/ErrorResponse' + $ref: '#/components/schemas/Error' example: - error: failed to issue token + status: error + code: INTERNAL_ERROR + message: An unexpected error occurred. + trackingId: 4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11 components: schemas: @@ -115,15 +124,40 @@ components: description: Token expiry as a Unix timestamp (seconds since epoch). example: 1735689600 - ErrorResponse: - type: object + Error: + title: Standard error response + description: | + The error shape shared with the main Platform API (see the `Error` schema in + openapi.yaml). Portal errors are produced by the same central error mapper, so the + two surfaces cannot drift. required: - - error + - status + - code + - message + type: object properties: - error: + status: + type: string + enum: [error] + description: Always the literal "error". + example: error + code: + type: string + pattern: '^[A-Z][A-Z0-9_]*$' + description: "Stable, machine-readable error code from the error catalog. Clients should branch on this, not on the HTTP status." + example: UNAUTHORIZED + message: + type: string + description: Human-readable description of the error. + example: Invalid or expired credentials. + trackingId: type: string - description: Human-readable error message. - example: invalid credentials + format: uuid + description: >- + Correlation ID for server-side failures. Present only on 5xx + responses; quote it when reporting the error so operators can + find the corresponding server log entry. + example: 4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11 tags: - name: Portal Authentication From 9799f069b648876cbabb28181504e0de4347863f Mon Sep 17 00:00:00 2001 From: Thushani Jayasekera Date: Sun, 12 Jul 2026 09:42:12 +0530 Subject: [PATCH 5/5] Refactor error handling in LLMProxyService to use apperror for provider validation failures Updated tests to reflect changes in error handling for missing additional providers and input validation. Replaced constants with appropriate apperror types for better clarity and consistency in error responses. --- platform-api/internal/service/llm.go | 8 ++++++-- platform-api/internal/service/llm_test.go | 16 ++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/platform-api/internal/service/llm.go b/platform-api/internal/service/llm.go index 23b37dcb6..7c934e38e 100644 --- a/platform-api/internal/service/llm.go +++ b/platform-api/internal/service/llm.go @@ -1266,14 +1266,18 @@ func (s *LLMProxyService) validateAdditionalProviders(orgUUID, primaryProviderID return fmt.Errorf("failed to validate additional provider %q: %w", ap.Id, err) } if prov == nil { - return constants.ErrLLMProviderNotFound + // The provider is referenced from the request body, not targeted by + // the URL, so this is a 400 REF_NOT_FOUND rather than a 404. + return apperror.LLMProviderRefNotFound.New(). + WithLogMessage(fmt.Sprintf("additional provider %q not found in org %s", ap.Id, orgUUID)) } name := ap.Id if ap.As != nil && *ap.As != "" { name = *ap.As } if seen[name] { - return constants.ErrInvalidInput + return apperror.ValidationFailed.New( + fmt.Sprintf("The upstream name %q is used by more than one provider in this proxy.", name)) } seen[name] = true } diff --git a/platform-api/internal/service/llm_test.go b/platform-api/internal/service/llm_test.go index c76cc3fc0..0d802f0fc 100644 --- a/platform-api/internal/service/llm_test.go +++ b/platform-api/internal/service/llm_test.go @@ -1792,8 +1792,8 @@ func TestLLMProxyServiceCreateFailsWhenAdditionalProviderNotFound(t *testing.T) req := validProxyRequest("provider-1", "project-1") req.AdditionalProviders = &[]api.LLMProxyAdditionalProvider{{Id: "missing-provider"}} - if _, err := service.Create("org-1", "alice", req); err != constants.ErrLLMProviderNotFound { - t.Fatalf("expected ErrLLMProviderNotFound, got: %v", err) + if _, err := service.Create("org-1", "alice", req); !apperror.LLMProviderRefNotFound.Is(err) { + t.Fatalf("expected LLMProviderRefNotFound, got: %v", err) } } @@ -1813,8 +1813,8 @@ func TestLLMProxyServiceCreateFailsWhenAdditionalProviderNameCollides(t *testing {Id: "provider-2", As: stringPtr("provider-1")}, } - if _, err := service.Create("org-1", "alice", req); err != constants.ErrInvalidInput { - t.Fatalf("expected ErrInvalidInput, got: %v", err) + if _, err := service.Create("org-1", "alice", req); !apperror.ValidationFailed.Is(err) { + t.Fatalf("expected ValidationFailed, got: %v", err) } } @@ -1847,8 +1847,8 @@ func TestLLMProxyServiceUpdateFailsWhenAdditionalProviderNotFound(t *testing.T) req := validProxyRequest("provider-1", "project-1") req.AdditionalProviders = &[]api.LLMProxyAdditionalProvider{{Id: "missing-provider"}} - if _, err := service.Update("org-1", "proxy-1", "test-user", req); err != constants.ErrLLMProviderNotFound { - t.Fatalf("expected ErrLLMProviderNotFound, got: %v", err) + if _, err := service.Update("org-1", "proxy-1", "test-user", req); !apperror.LLMProviderRefNotFound.Is(err) { + t.Fatalf("expected LLMProviderRefNotFound, got: %v", err) } if proxyRepo.updated != nil { t.Fatalf("expected update to be rejected before persisting, but proxy was updated") @@ -1885,8 +1885,8 @@ func TestLLMProxyServiceUpdateFailsWhenAdditionalProviderNameCollides(t *testing {Id: "provider-3", As: stringPtr("shared")}, } - if _, err := service.Update("org-1", "proxy-1", "test-user", req); err != constants.ErrInvalidInput { - t.Fatalf("expected ErrInvalidInput, got: %v", err) + if _, err := service.Update("org-1", "proxy-1", "test-user", req); !apperror.ValidationFailed.Is(err) { + t.Fatalf("expected ValidationFailed, got: %v", err) } if proxyRepo.updated != nil { t.Fatalf("expected update to be rejected before persisting, but proxy was updated")