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/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/api.go b/platform-api/internal/handler/api.go index 692b438e2..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,59 +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)) - } - if errors.Is(err, constants.ErrSecretRefMissing) { - return apperror.ValidationFailed.Wrap(err, "One or more referenced secrets do not exist") - } - 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)) @@ -160,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) @@ -190,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{ @@ -241,41 +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)) - } - if errors.Is(err, constants.ErrSecretRefMissing) { - return apperror.ValidationFailed.Wrap(err, "One or more referenced secrets do not exist") - } - 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) @@ -300,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) @@ -345,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) @@ -378,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..e020ff90a 100644 --- a/platform-api/internal/handler/gateway_internal.go +++ b/platform-api/internal/handler/gateway_internal.go @@ -21,17 +21,19 @@ 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" + "sort" "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" @@ -74,37 +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, constants.ErrMissingAPIKey - } - 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 { - 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")) + var appErr *apperror.Error + if errors.As(err, &appErr) && appErr.HTTPStatus == http.StatusUnauthorized { + h.slogger.Warn("Gateway authentication failed", + "clientIP", clientIP, "detail", appErr.LogMessage) } 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")) + h.slogger.Error("Gateway authentication errored", "clientIP", clientIP, "error", err) } + httputil.WriteJSON(w, http.StatusUnauthorized, dto.NewInternalErrorResponse(401, "Unauthorized", + "Invalid or expired API key")) return "", "", false } return gateway.OrganizationID, gateway.ID, true @@ -119,24 +146,24 @@ 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 } api, err := h.gatewayInternalService.GetActiveDeploymentByGateway(apiID, orgID, gatewayID) if err != nil { - if errors.Is(err, constants.ErrDeploymentNotActive) { - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + if apperror.DeploymentNotActive.Is(err) { + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(404, "Not Found", "No active deployment found for this API on this gateway")) return } - if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + if apperror.RESTAPINotFound.Is(err) { + 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 } @@ -145,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 } @@ -181,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. @@ -207,24 +234,24 @@ 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 } provider, err := h.gatewayInternalService.GetActiveLLMProviderDeploymentByGateway(providerID, orgID, gatewayID) if err != nil { - if errors.Is(err, constants.ErrDeploymentNotActive) { - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + if apperror.DeploymentNotActive.Is(err) { + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(404, "Not Found", "No active deployment found for this LLM provider on this gateway")) return } - if errors.Is(err, constants.ErrLLMProviderNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + if apperror.LLMProviderNotFound.Is(err) { + 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 } @@ -233,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 } @@ -257,24 +284,24 @@ 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 } proxy, err := h.gatewayInternalService.GetActiveLLMProxyDeploymentByGateway(proxyID, orgID, gatewayID) if err != nil { - if errors.Is(err, constants.ErrDeploymentNotActive) { - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + if apperror.DeploymentNotActive.Is(err) { + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(404, "Not Found", "No active deployment found for this LLM proxy on this gateway")) return } - if errors.Is(err, constants.ErrLLMProxyNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + if apperror.LLMProxyNotFound.Is(err) { + 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 } @@ -283,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 } @@ -312,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 } @@ -321,13 +348,13 @@ func (h *GatewayInternalAPIHandler) GetGatewayDeployments(w http.ResponseWriter, deployments, err := h.gatewayInternalService.GetDeploymentsByGateway(orgID, gatewayID, since) if err != nil { - if errors.Is(err, constants.ErrGatewayNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + if apperror.GatewayNotFound.Is(err) { + 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 } @@ -345,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 } @@ -353,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 } @@ -367,13 +394,13 @@ 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) { - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", + if apperror.GatewayNotFound.Is(err) { + 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 } @@ -382,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 } @@ -414,28 +441,28 @@ 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 } 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, "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 } - 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, "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 } @@ -444,19 +471,19 @@ 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 } 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, "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 } @@ -464,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 } @@ -484,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 } @@ -501,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 } @@ -512,20 +539,20 @@ 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", + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(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", + 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 } @@ -534,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 } @@ -558,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 } @@ -569,20 +596,20 @@ 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", + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(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", + 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 } @@ -591,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 } @@ -615,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 } @@ -626,20 +653,20 @@ 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", + httputil.WriteJSON(w, http.StatusNotFound, dto.NewInternalErrorResponse(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", + 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 } @@ -648,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 } @@ -677,28 +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 { - 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 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) + 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.NewErrorResponse(500, "Internal Server Error", - "Failed to store gateway manifest")) + httputil.WriteJSON(w, http.StatusInternalServerError, dto.NewInternalErrorResponse(500, + "Internal Server Error", "Failed to store gateway manifest")) return } @@ -715,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) @@ -731,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) @@ -747,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) @@ -763,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) @@ -779,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) @@ -797,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 } @@ -805,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 } @@ -841,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 } @@ -863,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}) @@ -887,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 } @@ -899,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 } @@ -921,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 } @@ -944,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 } @@ -952,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 errors.Is(err, constants.ErrSecretNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "Secret not found")) + if apperror.SecretNotFound.Is(err) { + 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 } diff --git a/platform-api/internal/handler/llm.go b/platform-api/internal/handler/llm.go index 8d8e2c7a3..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,25 +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.ErrSecretRefMissing): - return apperror.ValidationFailed.Wrap(err, "One or more referenced secrets do not exist") - 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) @@ -619,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 @@ -641,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 @@ -665,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 @@ -702,26 +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.ErrSecretRefMissing): - return apperror.ValidationFailed.Wrap(err, "One or more referenced secrets do not exist") - 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 @@ -740,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..c47e27dfe 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,11 @@ 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) + var appErr *apperror.Error + if errors.As(err, &appErr) { + return 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 +156,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..a2e839eff 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,8 +922,8 @@ 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) { - t.Errorf("expected ErrSecretRefMissing, got %v", err) + if !apperror.ValidationFailed.Is(err) { + t.Errorf("expected ValidationFailed, 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 3362d9cb6..8626eeef2 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 2746c3498..547955277 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" @@ -118,7 +118,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) } @@ -168,7 +168,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 } @@ -308,7 +308,7 @@ func (r *SubscriptionRepo) Update(sub *model.Subscription) error { } n, _ := result.RowsAffected() if n == 0 { - return constants.ErrSubscriptionNotFound + return apperror.SubscriptionNotFound.New() } return nil } @@ -339,13 +339,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 } @@ -359,7 +359,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 637474d3f..aee338c6f 100644 --- a/platform-api/internal/service/api.go +++ b/platform-api/internal/service/api.go @@ -123,7 +123,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) @@ -215,10 +215,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) @@ -258,7 +258,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 @@ -275,7 +275,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 } @@ -317,10 +317,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() } // Validate {{ secret "..." }} placeholders anywhere in the request — see @@ -405,7 +405,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 { @@ -426,10 +430,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. @@ -525,7 +529,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) @@ -561,10 +565,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 { @@ -573,7 +577,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) } @@ -614,10 +618,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 { @@ -653,17 +657,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") @@ -674,17 +678,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 @@ -710,7 +714,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)) } } } @@ -743,12 +747,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 @@ -845,25 +851,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_secret_integration_test.go b/platform-api/internal/service/api_secret_integration_test.go index 54f75dbf2..7d337cd07 100644 --- a/platform-api/internal/service/api_secret_integration_test.go +++ b/platform-api/internal/service/api_secret_integration_test.go @@ -33,7 +33,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/database" "github.com/wso2/api-platform/platform-api/internal/dto" "github.com/wso2/api-platform/platform-api/internal/model" @@ -251,8 +251,8 @@ func TestAPIServiceSecretLifecycle_Integration(t *testing.T) { } if _, err := apiSvc.UpdateAPIByHandle(*apiUUID, badUpstreamReq, apiSecretITOrgUUID, "alice"); err == nil { t.Fatal("expected update with a nonexistent upstream secret ref to be rejected, got nil error") - } else if !errors.Is(err, constants.ErrSecretRefMissing) { - t.Errorf("expected ErrSecretRefMissing, got: %v", err) + } else if !apperror.ValidationFailed.Is(err) { + t.Errorf("expected a validation error for missing secret ref, got: %v", err) } // --- Validation: the same check covers policy params, not just upstream --- @@ -268,7 +268,7 @@ func TestAPIServiceSecretLifecycle_Integration(t *testing.T) { } if _, err := apiSvc.UpdateAPIByHandle(*apiUUID, badPolicyReq, apiSecretITOrgUUID, "alice"); err == nil { t.Fatal("expected update with a nonexistent policy secret ref to be rejected, got nil error") - } else if !errors.Is(err, constants.ErrSecretRefMissing) { - t.Errorf("expected ErrSecretRefMissing, got: %v", err) + } else if !apperror.ValidationFailed.Is(err) { + t.Errorf("expected a validation error for missing secret ref, got: %v", err) } } diff --git a/platform-api/internal/service/api_test.go b/platform-api/internal/service/api_test.go index 920204ca7..e74b70e33 100644 --- a/platform-api/internal/service/api_test.go +++ b/platform-api/internal/service/api_test.go @@ -18,12 +18,11 @@ package service import ( - "errors" "reflect" "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" @@ -83,7 +82,7 @@ func TestValidateUpdateAPIRequest(t *testing.T) { mockNameVersionExists bool mockNameVersionError error wantErr bool - expectedErr error + expectedErr apperror.Def errContains string expectedExcludeHandle string verifyExcludeHandleCalled bool @@ -118,7 +117,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", @@ -128,7 +127,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", @@ -138,7 +137,7 @@ func TestValidateUpdateAPIRequest(t *testing.T) { }, req: &api.RESTAPI{Kind: ptr("INVALID_TYPE")}, wantErr: true, - expectedErr: constants.ErrInvalidAPIType, + expectedErr: apperror.ValidationFailed, }, { name: "invalid transport", @@ -148,7 +147,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", @@ -193,7 +192,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", @@ -211,7 +210,7 @@ func TestValidateUpdateAPIRequest(t *testing.T) { }, }, wantErr: true, - errContains: "policy version must be major-only", + errContains: "must be major-only", }, } @@ -237,8 +236,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) @@ -266,7 +265,7 @@ func TestValidateCreateAPIRequest(t *testing.T) { mockNameVersionExists bool mockNameVersionError error wantErr bool - expectedErr error + expectedErr apperror.Def errContains string verifyExcludeHandleEmpty bool expectedExcludeHandle string @@ -297,7 +296,7 @@ func TestValidateCreateAPIRequest(t *testing.T) { }, mockHandleExists: true, wantErr: true, - expectedErr: constants.ErrHandleExists, + expectedErr: apperror.RESTAPIExists, }, { name: "name version already exists", @@ -310,7 +309,7 @@ func TestValidateCreateAPIRequest(t *testing.T) { }, mockNameVersionExists: true, wantErr: true, - expectedErr: constants.ErrAPINameVersionAlreadyExists, + expectedErr: apperror.RESTAPIExists, verifyExcludeHandleEmpty: true, expectedExcludeHandle: "", }, @@ -324,7 +323,7 @@ func TestValidateCreateAPIRequest(t *testing.T) { Upstream: api.Upstream{}, }, wantErr: true, - expectedErr: constants.ErrInvalidAPIName, + expectedErr: apperror.ValidationFailed, }, { name: "missing project id", @@ -348,7 +347,7 @@ func TestValidateCreateAPIRequest(t *testing.T) { Upstream: api.Upstream{}, }, wantErr: true, - expectedErr: constants.ErrInvalidAPIContext, + expectedErr: apperror.ValidationFailed, }, { name: "invalid version", @@ -360,7 +359,7 @@ func TestValidateCreateAPIRequest(t *testing.T) { Upstream: api.Upstream{}, }, wantErr: true, - expectedErr: constants.ErrInvalidAPIVersion, + expectedErr: apperror.ValidationFailed, }, { name: "invalid lifecycle state", @@ -373,7 +372,7 @@ func TestValidateCreateAPIRequest(t *testing.T) { Upstream: api.Upstream{}, }, wantErr: true, - expectedErr: constants.ErrInvalidLifecycleState, + expectedErr: apperror.ValidationFailed, }, { name: "invalid api type", @@ -386,7 +385,7 @@ func TestValidateCreateAPIRequest(t *testing.T) { Upstream: api.Upstream{}, }, wantErr: true, - expectedErr: constants.ErrInvalidAPIType, + expectedErr: apperror.ValidationFailed, }, { name: "invalid transport", @@ -399,7 +398,7 @@ func TestValidateCreateAPIRequest(t *testing.T) { Upstream: api.Upstream{}, }, wantErr: true, - expectedErr: constants.ErrInvalidTransport, + expectedErr: apperror.ValidationFailed, }, { name: "valid lifecycle state", @@ -463,7 +462,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", @@ -482,7 +481,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)", @@ -529,8 +528,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) @@ -808,8 +807,8 @@ func TestAPIServiceCreate_MissingSecretRef_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 a validation error for missing secret ref, got: %v", err) } if apiRepo.created != nil { t.Error("expected API creation to be aborted, but repo.CreateAPI was called") @@ -848,8 +847,8 @@ func TestAPIServiceUpdate_MissingSecretRef_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 a validation error for missing secret ref, got: %v", err) } if apiRepo.updated != nil { t.Error("expected API update to be aborted, but repo.UpdateAPI was called") 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..95c0b470c 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.ValidationFailed.New("The project handle is required.") } 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 031170da7..b2f90d88a 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 } @@ -217,13 +218,13 @@ const importConflictMaxRetries = 3 // 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 @@ -255,7 +256,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 { @@ -267,7 +268,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 } @@ -326,7 +328,8 @@ func (s *ArtifactImportService) resolveAndImport( // 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 5b16a4390..fac9f4094 100644 --- a/platform-api/internal/service/artifact_import_test.go +++ b/platform-api/internal/service/artifact_import_test.go @@ -28,6 +28,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 +212,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 +223,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 +236,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) @@ -633,7 +634,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) } @@ -645,7 +646,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) } @@ -715,7 +716,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) } } @@ -814,7 +815,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 80119a8d3..7c934e38e 100644 --- a/platform-api/internal/service/llm.go +++ b/platform-api/internal/service/llm.go @@ -208,33 +208,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) @@ -242,7 +242,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{ @@ -272,7 +272,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) } @@ -318,27 +318,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) @@ -346,14 +346,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 @@ -401,7 +401,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) } @@ -421,7 +421,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) @@ -490,14 +490,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) @@ -510,7 +510,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 @@ -542,9 +542,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) } @@ -556,11 +556,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) @@ -568,12 +568,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) @@ -633,14 +633,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 { @@ -668,11 +668,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) @@ -680,7 +680,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) } @@ -688,11 +688,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 @@ -701,12 +701,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 @@ -717,12 +717,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) } @@ -731,7 +731,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) } @@ -739,11 +739,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) @@ -751,10 +751,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) @@ -762,11 +762,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) } @@ -777,38 +777,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 @@ -828,7 +828,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() } } @@ -855,7 +855,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 @@ -867,7 +867,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 @@ -899,11 +899,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) @@ -947,7 +947,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) } @@ -957,7 +957,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) @@ -1021,14 +1021,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 := "" @@ -1046,7 +1046,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) @@ -1054,10 +1054,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 @@ -1084,7 +1084,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 anywhere in the request — see @@ -1157,7 +1157,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) } @@ -1180,7 +1180,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) @@ -1190,7 +1190,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) @@ -1199,7 +1199,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. @@ -1223,7 +1223,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) } @@ -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 } @@ -1282,10 +1286,10 @@ func (s *LLMProxyService) validateAdditionalProviders(orgUUID, primaryProviderID 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 @@ -1305,7 +1309,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 } @@ -1316,7 +1320,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() } // Validate additional providers exist and have unique upstream names @@ -1333,7 +1337,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 @@ -1365,7 +1369,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 } @@ -1405,7 +1409,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) } @@ -1416,7 +1420,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) } @@ -1425,14 +1429,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, fmt.Errorf("cannot resolve project handle: project repository unavailable") } 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 } @@ -1500,7 +1504,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") @@ -1510,7 +1514,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) @@ -1565,24 +1569,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 @@ -1599,7 +1603,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 @@ -1608,7 +1612,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() } // Validate {{ secret "..." }} placeholders anywhere in the request. Checked @@ -1686,7 +1690,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) } @@ -1709,7 +1713,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) @@ -1717,7 +1721,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) @@ -1726,7 +1730,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. @@ -1750,7 +1754,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) } @@ -1787,7 +1791,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 } @@ -2544,7 +2548,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, @@ -2771,17 +2775,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{}{} @@ -2793,10 +2797,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{}{} } @@ -2955,7 +2959,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) @@ -2965,13 +2969,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 { @@ -2987,7 +2991,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) @@ -2999,26 +3003,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 @@ -3202,13 +3206,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 dcff53ac9..25275c15b 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 38314685f..b127b5ee0 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" @@ -164,8 +164,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, }, @@ -199,7 +199,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) } @@ -207,10 +207,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 { @@ -224,7 +224,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 { @@ -239,7 +239,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 { @@ -255,7 +255,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 { @@ -268,7 +268,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 { @@ -286,7 +286,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 { @@ -302,7 +302,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 { @@ -315,7 +315,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) } } @@ -419,7 +419,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) } } @@ -427,13 +427,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) } } @@ -444,7 +444,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 { @@ -459,7 +459,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 { @@ -517,7 +517,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) } } @@ -530,7 +530,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 { @@ -545,7 +545,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) } } @@ -578,7 +578,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) } } @@ -632,7 +632,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) } } @@ -647,7 +647,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" { @@ -690,7 +690,7 @@ func TestLLMProviderTemplateServiceSetVersionEnabled_RejectsCustomTemplate(t *te // Enable/disable is reserved for built-in ('wso2') templates; a custom // ('organization') 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 { @@ -709,7 +709,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) } } @@ -723,7 +723,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 { @@ -741,7 +741,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 0d2349af9..0d802f0fc 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) } } @@ -1544,8 +1543,8 @@ func TestLLMProviderServiceCreate_PolicySecretRef_Rejected(t *testing.T) { if err == nil { t.Fatal("expected error for non-existent secret placeholder in a policy param, got nil") } - if !errors.Is(err, constants.ErrSecretRefMissing) { - t.Errorf("expected ErrSecretRefMissing, got: %v", err) + if !apperror.ValidationFailed.Is(err) { + t.Errorf("expected a validation error for missing secret ref, got: %v", err) } if providerRepo.created != nil { t.Error("expected provider creation to be aborted, but repo.Create was called") @@ -1574,8 +1573,8 @@ func TestLLMProxyServiceCreate_MissingSecretRef_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 a validation error for missing secret ref, got: %v", err) } if proxyRepo.created != nil { t.Error("expected proxy creation to be aborted, but repo.Create was called") @@ -1617,8 +1616,8 @@ func TestLLMProxyServiceUpdate_MissingSecretRef_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 a validation error for missing secret ref, got: %v", err) } if proxyRepo.updated != nil { t.Error("expected proxy update to be aborted, but repo.Update was called") @@ -1793,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) } } @@ -1814,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) } } @@ -1848,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") @@ -1886,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") diff --git a/platform-api/internal/service/mcp.go b/platform-api/internal/service/mcp.go index 224f1aa36..d09620150 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 anywhere in the request — the @@ -216,13 +217,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) } @@ -280,7 +281,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 @@ -321,7 +322,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) @@ -329,7 +330,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) @@ -338,17 +339,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 @@ -357,7 +358,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 anywhere in the request — the @@ -428,7 +429,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) } @@ -453,7 +454,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) @@ -462,7 +463,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. @@ -486,7 +487,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) } @@ -515,7 +516,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 @@ -532,7 +533,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 @@ -542,7 +543,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 @@ -554,7 +555,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 @@ -566,11 +567,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 4469d532e..89691e367 100644 --- a/platform-api/internal/service/secret_service.go +++ b/platform-api/internal/service/secret_service.go @@ -24,6 +24,7 @@ import ( "errors" "fmt" "log/slog" + "strings" "time" "github.com/wso2/api-platform/platform-api/internal/apperror" @@ -39,7 +40,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 { @@ -92,7 +93,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) @@ -100,7 +101,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) @@ -295,8 +296,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 a62720abe..152a63793 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..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) @@ -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,17 +163,17 @@ 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 { if *update.Name == "" { - return nil, fmt.Errorf("name is required") + return nil, apperror.ValidationFailed.New("name is required") } existing.Name = *update.Name } @@ -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 } @@ -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)) } } @@ -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..ad2da1f96 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,8 @@ 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.Unauthorized.New(). + 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 e1f798651..b24c8f32f 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) } 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