From 37770ed0f037c97ced2e9b57fd4242eae01e2592 Mon Sep 17 00:00:00 2001 From: Thushani Jayasekera Date: Tue, 7 Jul 2026 15:43:23 +0530 Subject: [PATCH 01/12] Refactor error handling in API handlers to use standardized error response codes. --- platform-api/internal/dto/secret.go | 15 +- platform-api/internal/handler/api.go | 203 +++++----- .../internal/handler/api_deployment.go | 212 +++++----- platform-api/internal/handler/api_key.go | 92 ++--- platform-api/internal/handler/apikey_user.go | 8 +- platform-api/internal/handler/application.go | 151 ++++--- .../handler/artifact_guard_response.go | 9 +- platform-api/internal/handler/auth_login.go | 11 +- platform-api/internal/handler/gateway.go | 262 ++++++------ .../internal/handler/identity_helper.go | 2 +- platform-api/internal/handler/llm.go | 318 ++++++++++----- platform-api/internal/handler/llm_apikey.go | 72 ++-- .../internal/handler/llm_deployment.go | 380 +++++++++--------- .../handler/llm_provider_integration_test.go | 289 +++++++++++++ .../internal/handler/llm_proxy_apikey.go | 72 ++-- platform-api/internal/handler/mcp.go | 63 ++- .../internal/handler/mcp_deployment.go | 204 +++++----- platform-api/internal/handler/organization.go | 42 +- platform-api/internal/handler/project.go | 108 ++--- platform-api/internal/handler/secret.go | 70 ++-- .../handler/secret_integration_test.go | 11 +- .../internal/handler/subscription_handler.go | 116 ++++-- .../handler/subscription_plan_handler.go | 116 ++++-- platform-api/internal/middleware/auth.go | 10 +- platform-api/internal/utils/codes.go | 161 ++++++++ platform-api/internal/utils/error.go | 76 ++-- 26 files changed, 1935 insertions(+), 1138 deletions(-) create mode 100644 platform-api/internal/handler/llm_provider_integration_test.go create mode 100644 platform-api/internal/utils/codes.go diff --git a/platform-api/internal/dto/secret.go b/platform-api/internal/dto/secret.go index 9b6b2e5607..c35edde45f 100644 --- a/platform-api/internal/dto/secret.go +++ b/platform-api/internal/dto/secret.go @@ -88,15 +88,16 @@ type SecretSyncListResponse struct { Count int `json:"count"` } -// SecretDeleteConflictResponse is returned with 409 when a secret has active references. -type SecretDeleteConflictResponse struct { - Error string `json:"error"` - References []SecretReferenceDTO `json:"references"` -} - -// SecretReferenceDTO identifies a resource that references a secret. +// SecretReferenceDTO identifies a resource that references a secret. Carried +// in the standard error response's `details.references` when a delete is +// blocked by SECRET_IN_USE (see utils.ErrorResponse.Details). type SecretReferenceDTO struct { Type string `json:"type"` Handle string `json:"handle"` Name string `json:"name"` } + +// SecretInUseDetails is the `details` payload for a SECRET_IN_USE error. +type SecretInUseDetails struct { + References []SecretReferenceDTO `json:"references"` +} diff --git a/platform-api/internal/handler/api.go b/platform-api/internal/handler/api.go index 3394932df1..15a2f9a5c8 100644 --- a/platform-api/internal/handler/api.go +++ b/platform-api/internal/handler/api.go @@ -50,8 +50,8 @@ func NewAPIHandler(apiService *service.APIService, identity *service.IdentitySer func (h *APIHandler) CreateAPI(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -63,29 +63,29 @@ func (h *APIHandler) CreateAPI(w http.ResponseWriter, r *http.Request) { // Validate required fields if req.DisplayName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API name is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API name is required")) return } if req.Context == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API context is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API context is required")) return } if req.Version == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API version is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API version is required")) return } if strings.TrimSpace(req.ProjectId) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Project ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Project ID is required")) return } if isEmptyUpstreamDefinition(req.Upstream.Main) && (req.Upstream.Sandbox == nil || isEmptyUpstreamDefinition(*req.Upstream.Sandbox)) { h.slogger.Error("Validation failed: No upstream endpoints provided", "organizationId", orgId) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "At least one upstream endpoint (main or sandbox) is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "At least one upstream endpoint (main or sandbox) is required")) return } @@ -97,73 +97,73 @@ func (h *APIHandler) CreateAPI(w http.ResponseWriter, r *http.Request) { if err != nil { if errors.Is(err, constants.ErrHandleExists) { h.slogger.Error("API handle already exists", "organizationId", orgId) - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", - "API handle already exists")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeRESTAPIExists, "An API with this handle already exists.")) return } if errors.Is(err, constants.ErrAPINameVersionAlreadyExists) { h.slogger.Error("API with same name and version already exists", "organizationId", orgId) - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", - "API with same name and version already exists in the organization")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeRESTAPIExists, "An API with the same name and version already exists in the organization.")) return } if errors.Is(err, constants.ErrAPIAlreadyExists) { h.slogger.Error("API already exists in the project", "organizationId", orgId) - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", - "API already exists in the project")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeRESTAPIExists, "An API already exists in the project.")) return } if errors.Is(err, constants.ErrProjectNotFound) { h.slogger.Error("Project not found", "organizationId", orgId) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Project not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeProjectNotFound, "The specified project could not be found.")) return } if errors.Is(err, constants.ErrInvalidAPIName) { h.slogger.Error("Invalid API name format", "organizationId", orgId) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid API name format")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid API name format")) return } if errors.Is(err, constants.ErrInvalidAPIContext) { h.slogger.Error("Invalid API context format", "organizationId", orgId) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid API context format")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid API context format")) return } if errors.Is(err, constants.ErrInvalidAPIVersion) { h.slogger.Error("Invalid API version format", "organizationId", orgId) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid API version format")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid API version format")) return } if errors.Is(err, constants.ErrInvalidLifecycleState) { h.slogger.Error("Invalid lifecycle status", "organizationId", orgId) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid lifecycle status")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid lifecycle status")) return } if errors.Is(err, constants.ErrInvalidAPIType) { h.slogger.Error("Invalid API type", "organizationId", orgId) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid API type")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid API type")) return } if errors.Is(err, constants.ErrInvalidTransport) { h.slogger.Error("Invalid transport protocol", "organizationId", orgId) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid transport protocol")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid transport protocol")) return } if errors.Is(err, constants.ErrSubscriptionPlanNotFoundOrInactive) { h.slogger.Error("Subscription plan not found or not active", "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, err.Error())) return } h.slogger.Error("Failed to create API", "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to create API")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to create API")) return } @@ -174,15 +174,15 @@ func (h *APIHandler) CreateAPI(w http.ResponseWriter, r *http.Request) { func (h *APIHandler) GetAPI(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } apiId := r.PathValue("restApiId") if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API ID is required")) return } @@ -190,13 +190,13 @@ func (h *APIHandler) GetAPI(w http.ResponseWriter, r *http.Request) { if err != nil { if errors.Is(err, constants.ErrAPINotFound) { h.slogger.Error("API not found", "apiId", apiId, "organizationId", orgId) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "API not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) return } h.slogger.Error("Failed to get API", "apiId", apiId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to get API")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to get API")) return } @@ -208,14 +208,15 @@ func (h *APIHandler) ListAPIs(w http.ResponseWriter, r *http.Request) { // Get organization from JWT token orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } projectId := strings.TrimSpace(r.URL.Query().Get("projectId")) if projectId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "projectId query parameter is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "projectId query parameter is required")) return } @@ -223,13 +224,13 @@ func (h *APIHandler) ListAPIs(w http.ResponseWriter, r *http.Request) { if err != nil { if errors.Is(err, constants.ErrProjectNotFound) { h.slogger.Error("Project not found", "organizationId", orgId, "projectId", projectId) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Project not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeProjectNotFound, "The specified project could not be found.")) return } h.slogger.Error("Failed to get APIs", "organizationId", orgId, "projectId", projectId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to get APIs")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to get APIs")) return } @@ -250,29 +251,28 @@ func (h *APIHandler) ListAPIs(w http.ResponseWriter, r *http.Request) { func (h *APIHandler) UpdateAPI(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } apiId := r.PathValue("restApiId") if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API ID is required")) return } var req api.RESTAPI if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - err.Error())) + utils.NewValidationErrorResponse(w, err) return } // Validate upstream configuration if provided if isEmptyUpstreamDefinition(req.Upstream.Main) && (req.Upstream.Sandbox == nil || isEmptyUpstreamDefinition(*req.Upstream.Sandbox)) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "At least one upstream endpoint (main or sandbox) is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "At least one upstream endpoint (main or sandbox) is required")) return } @@ -287,41 +287,42 @@ func (h *APIHandler) UpdateAPI(w http.ResponseWriter, r *http.Request) { } if errors.Is(err, constants.ErrAPINotFound) { h.slogger.Error("API not found", "apiId", apiId, "organizationId", orgId) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "API not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) return } if errors.Is(err, constants.ErrHandleImmutable) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, err.Error())) return } if errors.Is(err, constants.ErrInvalidLifecycleState) { h.slogger.Error("Invalid lifecycle status", "apiId", apiId, "organizationId", orgId) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid lifecycle status")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid lifecycle status")) return } if errors.Is(err, constants.ErrInvalidAPIType) { h.slogger.Error("Invalid API type", "apiId", apiId, "organizationId", orgId) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid API type")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid API type")) return } if errors.Is(err, constants.ErrInvalidTransport) { h.slogger.Error("Invalid transport protocol", "apiId", apiId, "organizationId", orgId) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid transport protocol")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid transport protocol")) return } if errors.Is(err, constants.ErrSubscriptionPlanNotFoundOrInactive) { h.slogger.Error("Subscription plan not found or not active", "apiId", apiId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, err.Error())) return } h.slogger.Error("Failed to update API", "apiId", apiId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to update API")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to update API")) return } @@ -332,15 +333,15 @@ func (h *APIHandler) UpdateAPI(w http.ResponseWriter, r *http.Request) { func (h *APIHandler) DeleteAPI(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } apiId := r.PathValue("restApiId") if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API ID is required")) return } @@ -355,13 +356,13 @@ func (h *APIHandler) DeleteAPI(w http.ResponseWriter, r *http.Request) { } if errors.Is(err, constants.ErrAPINotFound) { h.slogger.Error("API not found", "apiId", apiId, "organizationId", orgId) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "API not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) return } h.slogger.Error("Failed to delete API", "apiId", apiId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to delete API")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to delete API")) return } @@ -372,27 +373,27 @@ func (h *APIHandler) DeleteAPI(w http.ResponseWriter, r *http.Request) { func (h *APIHandler) AddGatewaysToAPI(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } apiId := r.PathValue("restApiId") if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API ID is required")) return } var req []api.AddGatewayToRESTAPIRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + utils.NewValidationErrorResponse(w, err) return } if len(req) == 0 { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "At least one gateway ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "At least one gateway ID is required")) return } @@ -406,19 +407,19 @@ func (h *APIHandler) AddGatewaysToAPI(w http.ResponseWriter, r *http.Request) { if err != nil { if errors.Is(err, constants.ErrAPINotFound) { h.slogger.Error("API not found", "apiId", apiId, "organizationId", orgId) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "API not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) return } if errors.Is(err, constants.ErrGatewayNotFound) { h.slogger.Error("One or more gateways not found", "apiId", apiId, "organizationId", orgId) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "One or more gateways not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayNotFound, "One or more of the specified gateways could not be found.")) return } h.slogger.Error("Failed to associate gateways with API", "apiId", apiId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to associate gateways with API")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to associate gateways with API")) return } @@ -429,15 +430,15 @@ func (h *APIHandler) AddGatewaysToAPI(w http.ResponseWriter, r *http.Request) { func (h *APIHandler) GetAPIGateways(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } apiId := r.PathValue("restApiId") if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API ID is required")) return } @@ -445,13 +446,13 @@ func (h *APIHandler) GetAPIGateways(w http.ResponseWriter, r *http.Request) { if err != nil { if errors.Is(err, constants.ErrAPINotFound) { h.slogger.Error("API not found", "apiId", apiId, "organizationId", orgId) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "API not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) return } h.slogger.Error("Failed to get API gateways", "apiId", apiId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to get API gateways")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to get API gateways")) return } diff --git a/platform-api/internal/handler/api_deployment.go b/platform-api/internal/handler/api_deployment.go index 6dd2516c7c..1c89e9b841 100644 --- a/platform-api/internal/handler/api_deployment.go +++ b/platform-api/internal/handler/api_deployment.go @@ -52,38 +52,39 @@ func NewDeploymentHandler(deploymentService *service.DeploymentService, identity func (h *DeploymentHandler) DeployAPI(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } apiId := r.PathValue("restApiId") if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API ID is required")) return } var req api.DeployRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, err.Error())) return } // Validate required fields if req.Name == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "name is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeRESTAPIDeploymentValidationFailed, "name is required")) return } if req.Base == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "base is required (use 'current' or a deploymentId)")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeRESTAPIDeploymentValidationFailed, "base is required (use 'current' or a deploymentId)")) return } if strings.TrimSpace(req.GatewayId) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "gatewayId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeRESTAPIDeploymentValidationFailed, "gatewayId is required")) return } @@ -97,43 +98,43 @@ func (h *DeploymentHandler) DeployAPI(w http.ResponseWriter, r *http.Request) { return } if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "API not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) return } if errors.Is(err, constants.ErrGatewayNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Gateway not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayNotFound, "The specified gateway could not be found.")) return } if errors.Is(err, constants.ErrBaseDeploymentNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Base deployment not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeDeploymentBaseNotFound, "The specified base deployment could not be found.")) return } if errors.Is(err, constants.ErrDeploymentNameRequired) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Deployment name is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeRESTAPIDeploymentValidationFailed, "Deployment name is required")) return } if errors.Is(err, constants.ErrDeploymentBaseRequired) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Base is required (use 'current' or a deploymentId)")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeRESTAPIDeploymentValidationFailed, "Base is required (use 'current' or a deploymentId)")) return } if errors.Is(err, constants.ErrDeploymentGatewayIDRequired) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Gateway ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeRESTAPIDeploymentValidationFailed, "Gateway ID is required")) return } if errors.Is(err, constants.ErrAPINoBackendServices) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API must have at least one backend service attached before deployment")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeRESTAPIDeploymentValidationFailed, "API must have at least one backend service attached before deployment")) return } h.slogger.Error("Failed to deploy API", "apiId", apiId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to deploy API")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to deploy API")) return } @@ -144,8 +145,8 @@ func (h *DeploymentHandler) DeployAPI(w http.ResponseWriter, r *http.Request) { func (h *DeploymentHandler) UndeployDeployment(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -153,24 +154,24 @@ func (h *DeploymentHandler) UndeployDeployment(w http.ResponseWriter, r *http.Re deploymentId := r.PathValue("deploymentId") gatewayId := r.URL.Query().Get("gatewayId") if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "deploymentId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "deploymentId is required")) return } if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "gatewayId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "gatewayId is required")) return } if deploymentId == "00000000-0000-0000-0000-000000000000" || gatewayId == "00000000-0000-0000-0000-000000000000" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "deploymentId/gatewayId cannot be zero-value UUID")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "deploymentId/gatewayId cannot be zero-value UUID")) return } if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API ID is required")) return } actor, ok := resolveActor(w, r, h.identity, h.slogger, "undeploy API") @@ -184,32 +185,33 @@ func (h *DeploymentHandler) UndeployDeployment(w http.ResponseWriter, r *http.Re return } if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "API not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) return } if errors.Is(err, constants.ErrDeploymentNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Deployment not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) return } if errors.Is(err, constants.ErrGatewayNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Gateway not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayNotFound, "The specified gateway could not be found.")) return } if errors.Is(err, constants.ErrDeploymentNotActive) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", - "No active deployment found for this API on the gateway")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeDeploymentNotActive, "No active deployment found for this API on the gateway.")) return } if errors.Is(err, constants.ErrGatewayIDMismatch) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Deployment is bound to a different gateway")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeDeploymentGatewayMismatch, "Deployment is bound to a different gateway.")) return } h.slogger.Error("Failed to undeploy", "apiId", apiId, "deploymentId", deploymentId, "gatewayId", gatewayId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to undeploy deployment")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to undeploy deployment")) return } @@ -220,8 +222,8 @@ func (h *DeploymentHandler) UndeployDeployment(w http.ResponseWriter, r *http.Re func (h *DeploymentHandler) RestoreDeployment(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -229,24 +231,24 @@ func (h *DeploymentHandler) RestoreDeployment(w http.ResponseWriter, r *http.Req deploymentId := r.PathValue("deploymentId") gatewayId := r.URL.Query().Get("gatewayId") if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "deploymentId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "deploymentId is required")) return } if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "gatewayId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "gatewayId is required")) return } if deploymentId == "00000000-0000-0000-0000-000000000000" || gatewayId == "00000000-0000-0000-0000-000000000000" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "deploymentId/gatewayId cannot be zero-value UUID")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "deploymentId/gatewayId cannot be zero-value UUID")) return } if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API ID is required")) return } actor, ok := resolveActor(w, r, h.identity, h.slogger, "restore API deployment") @@ -260,32 +262,33 @@ func (h *DeploymentHandler) RestoreDeployment(w http.ResponseWriter, r *http.Req return } if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "API not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) return } if errors.Is(err, constants.ErrDeploymentNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Deployment not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) return } if errors.Is(err, constants.ErrGatewayNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Gateway not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayNotFound, "The specified gateway could not be found.")) return } if errors.Is(err, constants.ErrDeploymentAlreadyDeployed) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", - "Cannot restore currently deployed deployment")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeDeploymentRestoreConflict, "Cannot restore the currently deployed deployment, or the deployment is invalid.")) return } if errors.Is(err, constants.ErrGatewayIDMismatch) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Deployment is bound to a different gateway")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeDeploymentGatewayMismatch, "Deployment is bound to a different gateway.")) return } h.slogger.Error("Failed to restore deployment", "apiId", apiId, "deploymentId", deploymentId, "gatewayId", gatewayId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to restore deployment")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to restore deployment")) return } @@ -297,8 +300,8 @@ func (h *DeploymentHandler) RestoreDeployment(w http.ResponseWriter, r *http.Req func (h *DeploymentHandler) DeleteDeployment(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -306,13 +309,13 @@ func (h *DeploymentHandler) DeleteDeployment(w http.ResponseWriter, r *http.Requ deploymentId := r.PathValue("deploymentId") if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API ID is required")) return } if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Deployment ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Deployment ID is required")) return } @@ -323,25 +326,26 @@ func (h *DeploymentHandler) DeleteDeployment(w http.ResponseWriter, r *http.Requ err := h.deploymentService.DeleteDeploymentByHandle(apiId, deploymentId, orgId, actor) if err != nil { if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "API not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) return } if errors.Is(err, constants.ErrDeploymentNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Deployment not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) return } if errors.Is(err, constants.ErrDeploymentIsDeployed) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", - "Cannot delete an active deployment - undeploy it first")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeDeploymentActive, "Cannot delete an active deployment - undeploy it first.")) return } if respondArtifactGuardError(w, err) { return } h.slogger.Error("Failed to delete deployment", "apiId", apiId, "deploymentId", deploymentId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to delete deployment")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to delete deployment")) return } @@ -353,8 +357,8 @@ func (h *DeploymentHandler) DeleteDeployment(w http.ResponseWriter, r *http.Requ func (h *DeploymentHandler) GetDeployment(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -362,31 +366,31 @@ func (h *DeploymentHandler) GetDeployment(w http.ResponseWriter, r *http.Request deploymentId := r.PathValue("deploymentId") if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API ID is required")) return } if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Deployment ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Deployment ID is required")) return } deployment, err := h.deploymentService.GetDeploymentByHandle(apiId, deploymentId, orgId) if err != nil { if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "API not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) return } if errors.Is(err, constants.ErrDeploymentNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Deployment not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) return } h.slogger.Error("Failed to get deployment", "apiId", apiId, "deploymentId", deploymentId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to retrieve deployment")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to retrieve deployment")) return } @@ -398,15 +402,15 @@ func (h *DeploymentHandler) GetDeployment(w http.ResponseWriter, r *http.Request func (h *DeploymentHandler) GetDeployments(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } apiId := r.PathValue("restApiId") if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API ID is required")) return } @@ -431,18 +435,18 @@ 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) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "API not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) return } if errors.Is(err, constants.ErrInvalidDeploymentStatus) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid deployment status")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeDeploymentInvalidStatus, "The specified deployment status filter is invalid.")) return } h.slogger.Error("Failed to get deployments", "apiId", apiId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to retrieve deployments")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to retrieve deployments")) return } diff --git a/platform-api/internal/handler/api_key.go b/platform-api/internal/handler/api_key.go index 6547181b73..526826c3bb 100644 --- a/platform-api/internal/handler/api_key.go +++ b/platform-api/internal/handler/api_key.go @@ -55,8 +55,8 @@ func (h *APIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Request) { // Extract organization from JWT token orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -68,8 +68,8 @@ func (h *APIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Request) { // Extract API handle from path parameter (parameter named apiId for backward compatibility, but contains handle) apiHandle := r.PathValue("restApiId") if apiHandle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API handle is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API handle is required")) return } @@ -77,14 +77,14 @@ func (h *APIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Request) { var req api.CreateAPIKeyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.slogger.Error("Invalid API key creation request", "userId", userId, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid request body")) return } if req.ApiKey == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API key value is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API key value is required")) return } @@ -95,8 +95,8 @@ func (h *APIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Request) { } else { generatedName, err := utils.GenerateHandle(req.DisplayName, nil) if err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Failed to generate API key name")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Failed to generate API key name")) return } name = generatedName @@ -108,13 +108,13 @@ func (h *APIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Request) { if err != nil { // Handle specific error cases if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "API not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) return } if errors.Is(err, constants.ErrGatewayUnavailable) { - httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponse(503, "Service Unavailable", - "No gateway connections available for API")) + httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponseWithCode( + utils.CodeGatewayConnectionUnavailable, "No gateway connections are currently available.")) return } @@ -123,8 +123,8 @@ func (h *APIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Request) { keyName = *req.Id } h.slogger.Error("Failed to create API key", "userId", userId, "apiHandle", apiHandle, "orgId", orgId, "keyName", keyName, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to create API key")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to create API key")) return } @@ -148,8 +148,8 @@ func (h *APIKeyHandler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) { // Extract organization from JWT token orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -161,15 +161,15 @@ func (h *APIKeyHandler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) { // Extract API ID and key name from path parameters apiHandle := r.PathValue("restApiId") if apiHandle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API handle is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API handle is required")) return } keyName := r.PathValue("apiKeyId") if keyName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API key name is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API key name is required")) return } @@ -177,23 +177,23 @@ func (h *APIKeyHandler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) { var req api.UpdateAPIKeyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.slogger.Warn("Invalid API key update request", "userId", userId, "orgId", orgId, "apiHandle", apiHandle, "keyName", keyName, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid request body: "+err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid request body: "+err.Error())) return } // Validate new API key value if req.ApiKey == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API key value is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API key value is required")) return } // Validate that the name in the request body (if provided) matches the URL path parameter if req.Name != nil && *req.Name != "" && *req.Name != keyName { h.slogger.Warn("API key name mismatch", "userId", userId, "orgId", orgId, "apiHandle", apiHandle, "urlKeyName", keyName, "bodyKeyName", *req.Name) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - fmt.Sprintf("API key name mismatch: name in request body '%s' must match the key name in URL '%s'", *req.Name, keyName))) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, fmt.Sprintf("API key name mismatch: name in request body '%s' must match the key name in URL '%s'", *req.Name, keyName))) return } @@ -202,19 +202,19 @@ func (h *APIKeyHandler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) { if err != nil { // Handle specific error cases if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "API not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) return } if errors.Is(err, constants.ErrGatewayUnavailable) { - httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponse(503, "Service Unavailable", - "No gateway connections available for API")) + httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponseWithCode( + utils.CodeGatewayConnectionUnavailable, "No gateway connections are currently available.")) return } h.slogger.Error("Failed to update API key", "userId", userId, "apiHandle", apiHandle, "orgId", orgId, "keyName", keyName, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to update API key")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to update API key")) return } @@ -234,23 +234,23 @@ func (h *APIKeyHandler) RevokeAPIKey(w http.ResponseWriter, r *http.Request) { // Extract organization from JWT token orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } // Extract API ID and key name from path parameters apiHandle := r.PathValue("restApiId") if apiHandle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API handle is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API handle is required")) return } keyName := r.PathValue("apiKeyId") if keyName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API key name is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API key name is required")) return } @@ -264,19 +264,19 @@ func (h *APIKeyHandler) RevokeAPIKey(w http.ResponseWriter, r *http.Request) { if err != nil { // Handle specific error cases if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "API not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) return } if errors.Is(err, constants.ErrGatewayUnavailable) { - httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponse(503, "Service Unavailable", - "No gateway connections available for API")) + httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponseWithCode( + utils.CodeGatewayConnectionUnavailable, "No gateway connections are currently available.")) return } h.slogger.Error("Failed to revoke API key", "userId", userId, "apiHandle", apiHandle, "orgId", orgId, "keyName", keyName, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to revoke API key in one or more gateways")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to revoke API key in one or more gateways")) return } diff --git a/platform-api/internal/handler/apikey_user.go b/platform-api/internal/handler/apikey_user.go index ff17cfda23..e1744169fb 100644 --- a/platform-api/internal/handler/apikey_user.go +++ b/platform-api/internal/handler/apikey_user.go @@ -50,8 +50,8 @@ func NewAPIKeyUserHandler(apiKeyUserService *service.APIKeyUserService, identity func (h *APIKeyUserHandler) ListUserAPIKeys(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -68,8 +68,8 @@ func (h *APIKeyUserHandler) ListUserAPIKeys(w http.ResponseWriter, r *http.Reque response, err := h.apiKeyUserService.ListAPIKeysByUser(r.Context(), orgID, callerUserID, types) if err != nil { h.slogger.Error("Failed to list API keys for user", "orgId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to list API keys")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to list API keys")) return } diff --git a/platform-api/internal/handler/application.go b/platform-api/internal/handler/application.go index f99778ca97..c94782ee94 100644 --- a/platform-api/internal/handler/application.go +++ b/platform-api/internal/handler/application.go @@ -47,7 +47,8 @@ func NewApplicationHandler(applicationService *service.ApplicationService, ident func (h *ApplicationHandler) CreateApplication(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -58,15 +59,18 @@ func (h *ApplicationHandler) CreateApplication(w http.ResponseWriter, r *http.Re } if strings.TrimSpace(req.DisplayName) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "displayName is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "displayName is required")) return } if strings.TrimSpace(req.ProjectId) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Project ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Project ID is required")) return } if strings.TrimSpace(string(req.Type)) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Application type is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Application type is required")) return } @@ -86,13 +90,15 @@ func (h *ApplicationHandler) CreateApplication(w http.ResponseWriter, r *http.Re func (h *ApplicationHandler) GetApplication(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } appID := r.PathValue("applicationId") if strings.TrimSpace(appID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Application ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Application ID is required")) return } @@ -108,13 +114,15 @@ func (h *ApplicationHandler) GetApplication(w http.ResponseWriter, r *http.Reque func (h *ApplicationHandler) ListApplications(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } projectID := strings.TrimSpace(r.URL.Query().Get("projectId")) if projectID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Project ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Project ID is required")) return } @@ -156,13 +164,15 @@ func (h *ApplicationHandler) ListApplications(w http.ResponseWriter, r *http.Req func (h *ApplicationHandler) UpdateApplication(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } appID := r.PathValue("applicationId") if strings.TrimSpace(appID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Application ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Application ID is required")) return } userID, ok := resolveActor(w, r, h.identity, h.slogger, "update application") @@ -188,13 +198,15 @@ func (h *ApplicationHandler) UpdateApplication(w http.ResponseWriter, r *http.Re func (h *ApplicationHandler) DeleteApplication(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } appID := r.PathValue("applicationId") if strings.TrimSpace(appID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Application ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Application ID is required")) return } userID, ok := resolveActor(w, r, h.identity, h.slogger, "delete application") @@ -213,13 +225,15 @@ func (h *ApplicationHandler) DeleteApplication(w http.ResponseWriter, r *http.Re func (h *ApplicationHandler) ListApplicationAssociations(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } appID := r.PathValue("applicationId") if strings.TrimSpace(appID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Application ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Application ID is required")) return } @@ -254,13 +268,15 @@ func (h *ApplicationHandler) ListApplicationAssociations(w http.ResponseWriter, func (h *ApplicationHandler) AddApplicationAssociations(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } appID := r.PathValue("applicationId") if strings.TrimSpace(appID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Application ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Application ID is required")) return } @@ -270,7 +286,8 @@ func (h *ApplicationHandler) AddApplicationAssociations(w http.ResponseWriter, r return } if len(req.Associations) == 0 { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "At least one association is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "At least one association is required")) return } @@ -286,18 +303,21 @@ func (h *ApplicationHandler) AddApplicationAssociations(w http.ResponseWriter, r func (h *ApplicationHandler) RemoveApplicationAssociation(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } appID := r.PathValue("applicationId") associationID := r.PathValue("associationId") if strings.TrimSpace(appID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Application ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Application ID is required")) return } if strings.TrimSpace(associationID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Association ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Association ID is required")) return } @@ -312,13 +332,15 @@ func (h *ApplicationHandler) RemoveApplicationAssociation(w http.ResponseWriter, func (h *ApplicationHandler) ListApplicationAPIKeys(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } appID := r.PathValue("applicationId") if strings.TrimSpace(appID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Application ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Application ID is required")) return } @@ -353,18 +375,21 @@ func (h *ApplicationHandler) ListApplicationAPIKeys(w http.ResponseWriter, r *ht func (h *ApplicationHandler) ListApplicationAssociationAPIKeys(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } appID := r.PathValue("applicationId") associationID := r.PathValue("associationId") if strings.TrimSpace(appID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Application ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Application ID is required")) return } if strings.TrimSpace(associationID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Association ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Association ID is required")) return } @@ -399,7 +424,8 @@ func (h *ApplicationHandler) ListApplicationAssociationAPIKeys(w http.ResponseWr func (h *ApplicationHandler) AddApplicationAPIKeys(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -409,7 +435,8 @@ func (h *ApplicationHandler) AddApplicationAPIKeys(w http.ResponseWriter, r *htt return } if strings.TrimSpace(appID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Application ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Application ID is required")) return } @@ -419,7 +446,8 @@ func (h *ApplicationHandler) AddApplicationAPIKeys(w http.ResponseWriter, r *htt return } if len(req.ApiKeys) == 0 { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "At least one API key mapping is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "At least one API key mapping is required")) return } @@ -435,7 +463,8 @@ func (h *ApplicationHandler) AddApplicationAPIKeys(w http.ResponseWriter, r *htt func (h *ApplicationHandler) RemoveApplicationAPIKey(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -447,15 +476,18 @@ func (h *ApplicationHandler) RemoveApplicationAPIKey(w http.ResponseWriter, r *h return } if strings.TrimSpace(appID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Application ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Application ID is required")) return } if strings.TrimSpace(keyID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API key id is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API key id is required")) return } if entityID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Entity ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Entity ID is required")) return } @@ -496,41 +528,58 @@ func (h *ApplicationHandler) writeApplicationError(w http.ResponseWriter, r *htt switch { case errors.Is(err, constants.ErrApplicationNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Application not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeApplicationNotFound, "The specified application could not be found.")) case errors.Is(err, constants.ErrProjectNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Project not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeProjectNotFound, "The specified project could not be found.")) case errors.Is(err, constants.ErrOrganizationNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Organization not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeOrganizationNotFound, "The specified organization could not be found.")) case errors.Is(err, constants.ErrApplicationExists): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", "Application already exists in project")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeApplicationExists, "An application already exists in this project.")) case errors.Is(err, constants.ErrHandleExists): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", "Application handle already exists in organization")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeApplicationExists, "An application with this handle already exists in the organization.")) case errors.Is(err, constants.ErrAPIKeyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "API key not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeCommonNotFound, "API key not found")) case errors.Is(err, constants.ErrAPIKeyForbidden): - httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponse(403, "Forbidden", "Only the key creator can perform this action")) + httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( + utils.CodeCommonForbidden, "Only the key creator can perform this action")) case errors.Is(err, constants.ErrInvalidApplicationName): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "displayName is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "displayName is required")) case errors.Is(err, constants.ErrInvalidApplicationType): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Application type is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Application type is required")) case errors.Is(err, constants.ErrUnsupportedApplicationType): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid application type. Only 'genai' is supported")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid application type. Only 'genai' is supported")) case errors.Is(err, constants.ErrInvalidHandle): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid application handle format")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid application handle format")) case errors.Is(err, constants.ErrInvalidApplicationID): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid application id")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid application id")) case errors.Is(err, constants.ErrInvalidAPIKey): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid API key id")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid API key id")) case errors.Is(err, constants.ErrArtifactNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Association target not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeCommonNotFound, "Association target not found")) case errors.Is(err, constants.ErrArtifactInvalidKind): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid association kind. Only LlmProvider and LlmProxy are supported")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid association kind. Only LlmProvider and LlmProxy are supported")) case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid application association input")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid application association input")) case errors.Is(err, constants.ErrHandleImmutable): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "The id is immutable and must match the application being updated")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "The id is immutable and must match the application being updated")) default: - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", fallback)) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, fallback)) } } diff --git a/platform-api/internal/handler/artifact_guard_response.go b/platform-api/internal/handler/artifact_guard_response.go index 024c328d43..0dc0c03dd6 100644 --- a/platform-api/internal/handler/artifact_guard_response.go +++ b/platform-api/internal/handler/artifact_guard_response.go @@ -38,13 +38,16 @@ import ( func respondArtifactGuardError(w http.ResponseWriter, err error) bool { switch { case errors.Is(err, constants.ErrArtifactReadOnly): - httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponse(403, "Forbidden", err.Error())) + httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( + utils.CodeCommonForbidden, err.Error())) return true case errors.Is(err, constants.ErrArtifactRuntimeImmutable): - httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponse(403, "Forbidden", err.Error())) + httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( + utils.CodeCommonForbidden, err.Error())) return true case errors.Is(err, constants.ErrArtifactDeployed): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", err.Error())) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeCommonConflict, err.Error())) return true default: return false diff --git a/platform-api/internal/handler/auth_login.go b/platform-api/internal/handler/auth_login.go index 40b8360a80..d0fdfc2266 100644 --- a/platform-api/internal/handler/auth_login.go +++ b/platform-api/internal/handler/auth_login.go @@ -22,6 +22,7 @@ import ( "time" "github.com/wso2/api-platform/platform-api/config" + "github.com/wso2/api-platform/platform-api/internal/utils" "github.com/golang-jwt/jwt/v5" "github.com/wso2/go-httpkit/httputil" @@ -54,13 +55,13 @@ func (h *AuthLoginHandler) RegisterPublicRoutes(mux *http.ServeMux) { func (h *AuthLoginHandler) Login(w http.ResponseWriter, r *http.Request) { var req loginRequest if err := r.ParseForm(); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, map[string]any{"error": "username and password are required"}) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode(utils.CodeCommonValidationFailed, "username and password are required")) return } req.Username = r.PostForm.Get("username") req.Password = r.PostForm.Get("password") if req.Username == "" || req.Password == "" { - httputil.WriteJSON(w, http.StatusBadRequest, map[string]any{"error": "username and password are required"}) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode(utils.CodeCommonValidationFailed, "username and password are required")) return } @@ -77,12 +78,12 @@ func (h *AuthLoginHandler) Login(w http.ResponseWriter, r *http.Request) { // timing-based username enumeration. if matched == nil { _ = bcrypt.CompareHashAndPassword([]byte("$2a$10$notarealhashjustpadding000000000000000000000000000"), []byte(req.Password)) - httputil.WriteJSON(w, http.StatusUnauthorized, map[string]any{"error": "invalid credentials"}) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode(utils.CodeCommonUnauthorized, "Invalid or expired credentials.")) return } if err := bcrypt.CompareHashAndPassword([]byte(matched.PasswordHash), []byte(req.Password)); err != nil { - httputil.WriteJSON(w, http.StatusUnauthorized, map[string]any{"error": "invalid credentials"}) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode(utils.CodeCommonUnauthorized, "Invalid or expired credentials.")) return } @@ -102,7 +103,7 @@ func (h *AuthLoginHandler) Login(w http.ResponseWriter, r *http.Request) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) signed, err := token.SignedString([]byte(h.cfg.Auth.JWT.SecretKey)) if err != nil { - httputil.WriteJSON(w, http.StatusInternalServerError, map[string]any{"error": "failed to issue token"}) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode(utils.CodeCommonInternalError, "Failed to issue token.")) return } diff --git a/platform-api/internal/handler/gateway.go b/platform-api/internal/handler/gateway.go index 611fe55cd4..2b7a569a77 100644 --- a/platform-api/internal/handler/gateway.go +++ b/platform-api/internal/handler/gateway.go @@ -66,14 +66,14 @@ type manifestSyncResponse struct { func (h *GatewayHandler) CreateGateway(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } var req api.CreateGatewayRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + utils.NewValidationErrorResponse(w, err) return } @@ -102,8 +102,8 @@ func (h *GatewayHandler) CreateGateway(w http.ResponseWriter, r *http.Request) { version = strings.TrimSpace(*req.Version) } if version != "" && !gatewayVersionPattern.MatchString(version) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "version must be in 'major.minor' format (e.g. '1.0') or CalVer 'YYYY.MM.DD' format (e.g. '2026.05.13')")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "version must be in 'major.minor' format (e.g. '1.0') or CalVer 'YYYY.MM.DD' format (e.g. '2026.05.13')")) return } @@ -119,27 +119,30 @@ func (h *GatewayHandler) CreateGateway(w http.ResponseWriter, r *http.Request) { // Check for specific error types if strings.Contains(errMsg, "organization not found") { h.slogger.Error("Organization not found during gateway creation", "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", errMsg)) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeOrganizationNotFound, "The specified organization could not be found.")) return } if strings.Contains(errMsg, "already exists") { h.slogger.Error("Gateway already exists", "error", err) - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", errMsg)) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeGatewayNameConflict, "A gateway with this name already exists within the organization.")) return } if strings.Contains(errMsg, "required") || strings.Contains(errMsg, "invalid") || strings.Contains(errMsg, "must") || strings.Contains(errMsg, "cannot") { h.slogger.Error("Invalid gateway creation request", "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", errMsg)) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, errMsg)) return } // Internal server error h.slogger.Error("Failed to register gateway", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to register gateway")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to register gateway")) return } @@ -151,16 +154,16 @@ func (h *GatewayHandler) CreateGateway(w http.ResponseWriter, r *http.Request) { func (h *GatewayHandler) ListGateways(w http.ResponseWriter, r *http.Request) { organizationID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } gateways, err := h.gatewayService.ListGateways(&organizationID) if err != nil { h.slogger.Error("Failed to list gateways", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to list gateways")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to list gateways")) return } @@ -172,16 +175,16 @@ func (h *GatewayHandler) ListGateways(w http.ResponseWriter, r *http.Request) { func (h *GatewayHandler) GetGateway(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } // Extract UUID path parameter gatewayId := r.PathValue("gatewayId") if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Gateway ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Gateway ID is required")) return } @@ -192,20 +195,22 @@ func (h *GatewayHandler) GetGateway(w http.ResponseWriter, r *http.Request) { // Check for specific error types if strings.Contains(errMsg, "not found") { h.slogger.Error("Gateway not found", "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", errMsg)) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayNotFound, "The specified gateway could not be found.")) return } if strings.Contains(errMsg, "invalid UUID") { h.slogger.Error("Invalid gateway UUID", "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", errMsg)) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid gateway ID format")) return } // Internal server error h.slogger.Error("Failed to retrieve gateway", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to retrieve gateway")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to retrieve gateway")) return } @@ -217,8 +222,8 @@ func (h *GatewayHandler) GetGateway(w http.ResponseWriter, r *http.Request) { func (h *GatewayHandler) GetGatewayStatus(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -231,12 +236,12 @@ func (h *GatewayHandler) GetGatewayStatus(w http.ResponseWriter, r *http.Request status, err := h.gatewayService.GetGatewayStatus(orgId, gatewayIdPtr) if err != nil { if strings.Contains(err.Error(), "gateway not found") { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Gateway not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayNotFound, "The specified gateway could not be found.")) return } - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to get gateway status")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to get gateway status")) return } @@ -247,33 +252,33 @@ func (h *GatewayHandler) GetGatewayStatus(w http.ResponseWriter, r *http.Request func (h *GatewayHandler) UpdateGateway(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } gatewayId := r.PathValue("gatewayId") if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Gateway ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Gateway ID is required")) return } var req api.GatewayResponse if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + utils.NewValidationErrorResponse(w, err) return } if req.Id != nil && *req.Id != gatewayId { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Gateway id is immutable and cannot be changed")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Gateway id is immutable and cannot be changed")) return } if req.DisplayName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "displayName is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "displayName is required")) return } @@ -285,13 +290,13 @@ func (h *GatewayHandler) UpdateGateway(w http.ResponseWriter, r *http.Request) { if err != nil { if errors.Is(err, constants.ErrGatewayNotFound) { h.slogger.Error("Gateway not found during update", "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Gateway not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayNotFound, "The specified gateway could not be found.")) return } h.slogger.Error("Failed to update gateway", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to update gateway")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to update gateway")) return } @@ -302,16 +307,16 @@ func (h *GatewayHandler) UpdateGateway(w http.ResponseWriter, r *http.Request) { func (h *GatewayHandler) DeleteGateway(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } // Extract UUID path parameter gatewayId := r.PathValue("gatewayId") if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Gateway ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Gateway ID is required")) return } @@ -324,28 +329,28 @@ func (h *GatewayHandler) DeleteGateway(w http.ResponseWriter, r *http.Request) { // Check for specific error types if errors.Is(err, constants.ErrGatewayNotFound) { h.slogger.Error("Gateway not found during deletion", "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "The specified resource does not exist")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayNotFound, "The specified gateway could not be found.")) return } if errors.Is(err, constants.ErrGatewayHasAssociatedAPIs) { h.slogger.Error("Gateway has associated APIs during deletion", "error", err) - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", - "The gateway has associated APIs. Please remove all API associations before deleting the gateway")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeGatewayHasActiveDeployments, "The gateway has associated APIs. Please remove all API associations before deleting the gateway.")) return } if strings.Contains(err.Error(), "invalid UUID") { h.slogger.Error("Invalid UUID during gateway deletion", "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid gateway ID format")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid gateway ID format")) return } // Internal server error h.slogger.Error("Failed to delete gateway", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "The server encountered an internal error. Please contact administrator.")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "The server encountered an internal error. Please contact administrator.")) return } @@ -357,15 +362,15 @@ func (h *GatewayHandler) DeleteGateway(w http.ResponseWriter, r *http.Request) { func (h *GatewayHandler) ListTokens(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } gatewayId := r.PathValue("gatewayId") if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Gateway ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Gateway ID is required")) return } @@ -375,13 +380,14 @@ func (h *GatewayHandler) ListTokens(w http.ResponseWriter, r *http.Request) { if strings.Contains(errMsg, "gateway not found") { h.slogger.Error("Gateway not found during token listing", "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", errMsg)) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayNotFound, "The specified gateway could not be found.")) return } h.slogger.Error("Failed to list tokens", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to list tokens")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to list tokens")) return } @@ -392,16 +398,16 @@ func (h *GatewayHandler) ListTokens(w http.ResponseWriter, r *http.Request) { func (h *GatewayHandler) RotateToken(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } // Extract ID path parameter gatewayId := r.PathValue("gatewayId") if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Gateway ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Gateway ID is required")) return } @@ -416,20 +422,22 @@ func (h *GatewayHandler) RotateToken(w http.ResponseWriter, r *http.Request) { // Check for specific error types if strings.Contains(errMsg, "gateway not found") { h.slogger.Error("Gateway not found during token rotation", "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", errMsg)) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayNotFound, "The specified gateway could not be found.")) return } if strings.Contains(errMsg, "maximum") || strings.Contains(errMsg, "Revoke") { h.slogger.Error("Token rotation request validation failed", "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", errMsg)) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeGatewayTokenLimitReached, "The maximum number of active tokens has been reached.")) return } // Internal server error h.slogger.Error("Failed to rotate token", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to rotate token")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to rotate token")) return } @@ -441,22 +449,22 @@ func (h *GatewayHandler) RotateToken(w http.ResponseWriter, r *http.Request) { func (h *GatewayHandler) RevokeToken(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } gatewayId := r.PathValue("gatewayId") if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Gateway ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Gateway ID is required")) return } tokenId := r.PathValue("tokenId") if tokenId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Token ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Token ID is required")) return } @@ -470,13 +478,19 @@ func (h *GatewayHandler) RevokeToken(w http.ResponseWriter, r *http.Request) { if strings.Contains(errMsg, "not found") { h.slogger.Error("Resource not found during token revocation", "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", errMsg)) + if strings.Contains(errMsg, "gateway") { + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayNotFound, "The specified gateway could not be found.")) + return + } + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayTokenNotFound, "The specified gateway token could not be found.")) return } h.slogger.Error("Failed to revoke token", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to revoke token")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to revoke token")) return } @@ -488,32 +502,32 @@ func (h *GatewayHandler) RevokeToken(w http.ResponseWriter, r *http.Request) { func (h *GatewayHandler) GetGatewayManifest(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } gatewayId := r.PathValue("gatewayId") if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Gateway ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Gateway ID is required")) return } dataFromDb, err := h.gatewayService.GetStoredManifest(gatewayId, orgId) if err != nil { if strings.Contains(err.Error(), "invalid UUID") { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid gateway ID format")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid gateway ID format")) return } - if strings.Contains(err.Error(), "gateway not found") { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Gateway not found")) + if errors.Is(err, constants.ErrGatewayNotFound) { + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayNotFound, "The specified gateway could not be found.")) return } - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to retrieve gateway manifest")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to retrieve gateway manifest")) return } @@ -527,8 +541,8 @@ func (h *GatewayHandler) GetGatewayManifest(w http.ResponseWriter, r *http.Reque func (h *GatewayHandler) SyncCustomPolicy(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -537,8 +551,8 @@ func (h *GatewayHandler) SyncCustomPolicy(w http.ResponseWriter, r *http.Request version := r.URL.Query().Get("policyVersion") if gatewayId == "" || policyName == "" || version == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "gatewayId, policyName and policyVersion are required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "gatewayId, policyName and policyVersion are required")) return } @@ -546,24 +560,28 @@ func (h *GatewayHandler) SyncCustomPolicy(w http.ResponseWriter, r *http.Request if err != nil { msg := err.Error() if strings.Contains(msg, "gateway not found") { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", msg)) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayNotFound, "The specified gateway could not be found.")) return } if strings.Contains(msg, "not found in gateway manifest") { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", msg)) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeCommonNotFound, "The specified policy version could not be found in the gateway manifest.")) return } if strings.Contains(msg, "not a custom policy") || strings.Contains(msg, "manifest is not available") { - httputil.WriteJSON(w, http.StatusUnprocessableEntity, utils.NewErrorResponse(422, "Unprocessable Entity", msg)) + httputil.WriteJSON(w, http.StatusUnprocessableEntity, utils.NewErrorResponseWithCode( + utils.CodePolicyInvalidState, "The policy is not a custom policy, or its manifest is unavailable.")) return } if strings.Contains(msg, "already exists") || strings.Contains(msg, "patch version updates are not allowed") || strings.Contains(msg, "cannot downgrade") { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", msg)) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodePolicyVersionConflict, "The policy already exists, or a patch-version update or downgrade is not allowed.")) return } h.slogger.Error("Failed to sync custom policy", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to sync custom policy")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to sync custom policy")) return } @@ -574,32 +592,34 @@ func (h *GatewayHandler) SyncCustomPolicy(w http.ResponseWriter, r *http.Request func (h *GatewayHandler) GetCustomPolicy(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } policyUUID := r.PathValue("gatewayCustomPolicyId") version := r.PathValue("version") if policyUUID == "" || version == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "customPolicyUuid and version are required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "customPolicyUuid and version are required")) return } policy, err := h.gatewayService.GetCustomPolicyByUUID(orgId, policyUUID, version) if err != nil { if errors.Is(err, constants.ErrCustomPolicyNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Custom policy not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeCommonNotFound, "Custom policy not found.")) return } if errors.Is(err, constants.ErrCustomPolicyVersionMismatch) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Custom policy not found with the specified version")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeCommonNotFound, "Custom policy not found with the specified version.")) return } h.slogger.Error("Failed to get custom policy", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to get custom policy")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to get custom policy")) return } @@ -610,37 +630,39 @@ func (h *GatewayHandler) GetCustomPolicy(w http.ResponseWriter, r *http.Request) func (h *GatewayHandler) DeleteCustomPolicy(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } policyUUID := r.PathValue("gatewayCustomPolicyId") version := r.PathValue("version") if policyUUID == "" || version == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "customPolicyUuid and version are required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "customPolicyUuid and version are required")) return } err := h.gatewayService.DeleteCustomPolicyByUUID(orgId, policyUUID, version) if err != nil { if errors.Is(err, constants.ErrCustomPolicyNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Custom policy not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeCommonNotFound, "Custom policy not found.")) return } if errors.Is(err, constants.ErrCustomPolicyVersionMismatch) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Custom policy not found with the specified version")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeCommonNotFound, "Custom policy not found with the specified version.")) return } if errors.Is(err, constants.ErrCustomPolicyInUse) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", - "Custom policy is in use by one or more APIs and cannot be deleted")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodePolicyInUse, "The custom policy is in use by one or more APIs and cannot be removed.")) return } h.slogger.Error("Failed to delete custom policy", "org_id", orgId, "policy_uuid", policyUUID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to delete custom policy")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to delete custom policy")) return } @@ -651,16 +673,16 @@ func (h *GatewayHandler) DeleteCustomPolicy(w http.ResponseWriter, r *http.Reque func (h *GatewayHandler) ListCustomPolicies(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } policies, err := h.gatewayService.ListCustomPolicies(orgId) if err != nil { h.slogger.Error("Failed to list custom policies", "org_id", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to list custom policies")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to list custom policies")) return } diff --git a/platform-api/internal/handler/identity_helper.go b/platform-api/internal/handler/identity_helper.go index 0eb6b69701..b74636e961 100644 --- a/platform-api/internal/handler/identity_helper.go +++ b/platform-api/internal/handler/identity_helper.go @@ -37,7 +37,7 @@ func resolveActor(w http.ResponseWriter, r *http.Request, identity *service.Iden actor, err := identity.InternalUserID(r) if err != nil { slogger.Error("Failed to resolve user identity", "action", action, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode(utils.CodeCommonInternalError, "Failed to resolve user identity")) return "", false } diff --git a/platform-api/internal/handler/llm.go b/platform-api/internal/handler/llm.go index f6c9ef8d99..1f7eacc1bf 100644 --- a/platform-api/internal/handler/llm.go +++ b/platform-api/internal/handler/llm.go @@ -109,13 +109,15 @@ func parseTemplateQuery(raw string) (q templateQuery, found bool) { func (h *LLMHandler) CreateLLMProviderTemplate(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } var req api.LLMProviderTemplate if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid request body")) return } @@ -128,17 +130,21 @@ func (h *LLMHandler) CreateLLMProviderTemplate(w http.ResponseWriter, r *http.Re if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderTemplateExists): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", "LLM provider template already exists")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderTemplateExists, "An LLM provider template with this ID already exists.")) return case errors.Is(err, constants.ErrLLMProviderTemplateManagedByReserved): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "'wso2' is reserved and cannot be used as managedBy on custom templates")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderTemplateManagedByReserved, "'wso2' is reserved and cannot be used as managedBy on custom templates.")) return case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid input")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid input")) return default: h.slogger.Error("Failed to create LLM provider template", "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to create LLM provider template")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to create LLM provider template")) return } } @@ -154,7 +160,8 @@ func (h *LLMHandler) CreateLLMProviderTemplate(w http.ResponseWriter, r *http.Re func (h *LLMHandler) CopyLLMProviderTemplateVersion(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -167,7 +174,8 @@ func (h *LLMHandler) CopyLLMProviderTemplateVersion(w http.ResponseWriter, r *ht toTemplateID := strings.TrimSpace(r.URL.Query().Get("toTemplateId")) toVersion := strings.TrimSpace(r.URL.Query().Get("toVersion")) if fromTemplateID == "" || toVersion == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "fromTemplateId and toVersion are required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "fromTemplateId and toVersion are required")) return } @@ -176,7 +184,8 @@ func (h *LLMHandler) CopyLLMProviderTemplateVersion(w http.ResponseWriter, r *ht if r.Body != nil && r.ContentLength != 0 { var vreq api.CreateLLMProviderTemplateVersionRequest if err := json.NewDecoder(r.Body).Decode(&vreq); err != nil && !errors.Is(err, io.EOF) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid request body")) return } overrides = &vreq @@ -186,20 +195,25 @@ func (h *LLMHandler) CopyLLMProviderTemplateVersion(w http.ResponseWriter, r *ht if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Source LLM provider template version not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderTemplateNotFound, "The source LLM provider template version could not be found.")) return case errors.Is(err, constants.ErrLLMProviderTemplateVersionExists): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", "The version already exists")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderTemplateExists, "This template version already exists.")) return case errors.Is(err, constants.ErrLLMProviderTemplateManagedByReserved): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "'wso2' is reserved and cannot be used as managedBy on custom templates")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderTemplateManagedByReserved, "'wso2' is reserved and cannot be used as managedBy on custom templates.")) return case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid input. toVersion must match the v. pattern starting from v1.0 (e.g. v1.0), and toTemplateId must match the family")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid input. toVersion must match the v. pattern starting from v1.0 (e.g. v1.0), and toTemplateId must match the family")) return default: h.slogger.Error("Failed to copy LLM provider template version", "organizationId", orgID, "fromTemplateId", fromTemplateID, "toVersion", toVersion, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to copy LLM provider template version")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to copy LLM provider template version")) return } } @@ -210,7 +224,8 @@ func (h *LLMHandler) CopyLLMProviderTemplateVersion(w http.ResponseWriter, r *ht func (h *LLMHandler) ListLLMProviderTemplates(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -240,7 +255,8 @@ func (h *LLMHandler) ListLLMProviderTemplates(w http.ResponseWriter, r *http.Req if familyScoped { groupID := q.GroupID if groupID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid groupId")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid groupId")) return } version := q.Version @@ -249,14 +265,17 @@ func (h *LLMHandler) ListLLMProviderTemplates(w http.ResponseWriter, r *http.Req if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "LLM provider template version not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderTemplateNotFound, "The specified LLM provider template version could not be found.")) return case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid version. Version must match the v. pattern (e.g. v1.0)")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid version. Version must match the v. pattern (e.g. v1.0)")) return default: h.slogger.Error("Failed to get LLM provider template version", "organizationId", orgID, "groupId", groupID, "version", version, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get LLM provider template version")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to get LLM provider template version")) return } } @@ -267,14 +286,17 @@ func (h *LLMHandler) ListLLMProviderTemplates(w http.ResponseWriter, r *http.Req if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "LLM provider template not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderTemplateNotFound, "The specified LLM provider template could not be found.")) return case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid groupId")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid groupId")) return default: h.slogger.Error("Failed to list LLM provider template versions", "organizationId", orgID, "groupId", groupID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to list LLM provider template versions")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to list LLM provider template versions")) return } } @@ -287,7 +309,8 @@ func (h *LLMHandler) ListLLMProviderTemplates(w http.ResponseWriter, r *http.Req resp, err := h.templateService.List(orgID, limit, offset, latestOnly) if err != nil { h.slogger.Error("Failed to list LLM provider templates", "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to list LLM provider templates")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to list LLM provider templates")) return } httputil.WriteJSON(w, http.StatusOK, resp) @@ -296,7 +319,8 @@ func (h *LLMHandler) ListLLMProviderTemplates(w http.ResponseWriter, r *http.Req func (h *LLMHandler) GetLLMProviderTemplate(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } id := r.PathValue("llmProviderTemplateId") @@ -305,14 +329,17 @@ func (h *LLMHandler) GetLLMProviderTemplate(w http.ResponseWriter, r *http.Reque if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "LLM provider template not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderTemplateNotFound, "The specified LLM provider template could not be found.")) return case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid template id")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid template id")) return default: h.slogger.Error("Failed to get LLM provider template", "organizationId", orgID, "templateId", id, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get LLM provider template")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to get LLM provider template")) return } } @@ -322,7 +349,8 @@ func (h *LLMHandler) GetLLMProviderTemplate(w http.ResponseWriter, r *http.Reque func (h *LLMHandler) SetLLMProviderTemplateVersionEnabled(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } id := r.PathValue("llmProviderTemplateId") @@ -331,7 +359,8 @@ func (h *LLMHandler) SetLLMProviderTemplateVersionEnabled(w http.ResponseWriter, Enabled *bool `json:"enabled"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Enabled == nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Request body must include a boolean 'enabled' field")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Request body must include a boolean 'enabled' field")) return } @@ -339,20 +368,25 @@ func (h *LLMHandler) SetLLMProviderTemplateVersionEnabled(w http.ResponseWriter, if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "LLM provider template version not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderTemplateNotFound, "The specified LLM provider template version could not be found.")) return case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid template id")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid template id")) return case errors.Is(err, constants.ErrLLMProviderTemplateInUse): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", "Cannot disable template version while providers are using it")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderTemplateInUse, "Cannot disable this template version while providers are using it.")) return case errors.Is(err, constants.ErrLLMProviderTemplateNotToggleable): - httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponse(403, "Forbidden", "Only built-in templates can be enabled or disabled")) + httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderTemplateNotToggleable, "Only built-in templates can be enabled or disabled.")) return default: h.slogger.Error("Failed to set LLM provider template version enabled", "organizationId", orgID, "templateId", id, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to update template version")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to update template version")) return } } @@ -362,14 +396,16 @@ func (h *LLMHandler) SetLLMProviderTemplateVersionEnabled(w http.ResponseWriter, func (h *LLMHandler) UpdateLLMProviderTemplate(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } id := r.PathValue("llmProviderTemplateId") var req api.LLMProviderTemplate if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid request body")) return } @@ -384,23 +420,29 @@ func (h *LLMHandler) UpdateLLMProviderTemplate(w http.ResponseWriter, r *http.Re } switch { case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "LLM provider template not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderTemplateNotFound, "The specified LLM provider template could not be found.")) return case errors.Is(err, constants.ErrLLMProviderTemplateReadOnly): - httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponse(403, "Forbidden", "Built-in templates are read-only and cannot be edited")) + httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderTemplateReadOnly, "Built-in templates are read-only and cannot be edited.")) return case errors.Is(err, constants.ErrLLMProviderTemplateManagedByReserved): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "'wso2' is reserved and cannot be used as managedBy on custom templates")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderTemplateManagedByReserved, "'wso2' is reserved and cannot be used as managedBy on custom templates.")) return case errors.Is(err, constants.ErrHandleImmutable): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, err.Error())) return case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid input")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid input")) return default: h.slogger.Error("Failed to update LLM provider template", "organizationId", orgID, "templateId", id, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to update LLM provider template")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to update LLM provider template")) return } } @@ -411,7 +453,8 @@ func (h *LLMHandler) UpdateLLMProviderTemplate(w http.ResponseWriter, r *http.Re func (h *LLMHandler) DeleteLLMProviderTemplateVersion(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } id := r.PathValue("llmProviderTemplateId") @@ -419,20 +462,25 @@ func (h *LLMHandler) DeleteLLMProviderTemplateVersion(w http.ResponseWriter, r * if err := h.templateService.DeleteByHandle(orgID, id); err != nil { switch { case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "LLM provider template version not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderTemplateNotFound, "The specified LLM provider template version could not be found.")) return case errors.Is(err, constants.ErrLLMProviderTemplateInUse): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", "Template version cannot be deleted while providers are using it")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderTemplateInUse, "This template version cannot be deleted while providers are using it.")) return case errors.Is(err, constants.ErrLLMProviderTemplateReadOnly): - httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponse(403, "Forbidden", "Built-in template versions are read-only and cannot be deleted")) + httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderTemplateReadOnly, "Built-in template versions are read-only and cannot be deleted.")) return case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid template id")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid template id")) return default: h.slogger.Error("Failed to delete LLM provider template version", "organizationId", orgID, "templateId", id, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to delete template version")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to delete template version")) return } } @@ -445,13 +493,15 @@ func (h *LLMHandler) DeleteLLMProviderTemplateVersion(w http.ResponseWriter, r * func (h *LLMHandler) CreateLLMProvider(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } var req api.LLMProvider if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid request body")) return } createdBy, ok := resolveActor(w, r, h.identity, h.slogger, "create LLM provider") @@ -463,23 +513,29 @@ func (h *LLMHandler) CreateLLMProvider(w http.ResponseWriter, r *http.Request) { if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderLimitReached): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", "LLM provider limit reached for organization")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderLimitReached, "LLM provider limit reached for the organization.")) return case errors.Is(err, constants.ErrLLMProviderExists): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", "LLM provider already exists")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderExists, "An LLM provider with this ID already exists.")) return case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Referenced template not found")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderTemplateNotFound, "The referenced LLM provider template could not be found.")) return case errors.Is(err, constants.ErrSecretRefMissing): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, err.Error())) return case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid input")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid input")) return default: h.slogger.Error("Failed to create LLM provider", "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to create LLM provider")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to create LLM provider")) return } } @@ -489,7 +545,8 @@ func (h *LLMHandler) CreateLLMProvider(w http.ResponseWriter, r *http.Request) { func (h *LLMHandler) ListLLMProviders(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -518,7 +575,8 @@ func (h *LLMHandler) ListLLMProviders(w http.ResponseWriter, r *http.Request) { resp, err := h.providerService.List(orgID, limit, offset) if err != nil { h.slogger.Error("Failed to list LLM providers", "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to list LLM providers")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to list LLM providers")) return } httputil.WriteJSON(w, http.StatusOK, resp) @@ -527,7 +585,8 @@ func (h *LLMHandler) ListLLMProviders(w http.ResponseWriter, r *http.Request) { func (h *LLMHandler) GetLLMProvider(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } id := r.PathValue("llmProviderId") @@ -536,14 +595,17 @@ func (h *LLMHandler) GetLLMProvider(w http.ResponseWriter, r *http.Request) { if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "LLM provider not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) return case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid provider id")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid provider id")) return default: h.slogger.Error("Failed to get LLM provider", "organizationId", orgID, "providerId", id, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get LLM provider")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to get LLM provider")) return } } @@ -553,14 +615,16 @@ func (h *LLMHandler) GetLLMProvider(w http.ResponseWriter, r *http.Request) { func (h *LLMHandler) UpdateLLMProvider(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } id := r.PathValue("llmProviderId") var req api.LLMProvider if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid request body")) return } @@ -575,23 +639,29 @@ func (h *LLMHandler) UpdateLLMProvider(w http.ResponseWriter, r *http.Request) { } switch { case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "LLM provider not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) return case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Referenced template not found")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderTemplateNotFound, "The referenced LLM provider template could not be found.")) return case errors.Is(err, constants.ErrSecretRefMissing): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, err.Error())) return case errors.Is(err, constants.ErrHandleImmutable): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, err.Error())) return case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid input")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid input")) return default: h.slogger.Error("Failed to update LLM provider", "organizationId", orgID, "providerId", id, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to update LLM provider")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to update LLM provider")) return } } @@ -601,7 +671,8 @@ func (h *LLMHandler) UpdateLLMProvider(w http.ResponseWriter, r *http.Request) { func (h *LLMHandler) DeleteLLMProvider(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } id := r.PathValue("llmProviderId") @@ -616,14 +687,17 @@ func (h *LLMHandler) DeleteLLMProvider(w http.ResponseWriter, r *http.Request) { } switch { case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "LLM provider not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) return case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid provider id")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid provider id")) return default: h.slogger.Error("Failed to delete LLM provider", "organizationId", orgID, "providerId", id, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to delete LLM provider")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to delete LLM provider")) return } } @@ -635,17 +709,20 @@ func (h *LLMHandler) DeleteLLMProvider(w http.ResponseWriter, r *http.Request) { func (h *LLMHandler) CreateLLMProxy(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } var req api.LLMProxy if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid request body")) return } if req.ProjectId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Project ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Project ID is required")) return } createdBy, ok := resolveActor(w, r, h.identity, h.slogger, "create LLM proxy") @@ -657,23 +734,29 @@ func (h *LLMHandler) CreateLLMProxy(w http.ResponseWriter, r *http.Request) { if err != nil { switch { case errors.Is(err, constants.ErrLLMProxyLimitReached): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", "LLM proxy limit reached for organization")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeLLMProxyLimitReached, "LLM proxy limit reached for the organization.")) return case errors.Is(err, constants.ErrLLMProxyExists): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", "LLM proxy already exists")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeLLMProxyExists, "An LLM proxy with this ID already exists.")) return case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Referenced provider not found")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderNotFound, "The referenced LLM provider could not be found.")) return case errors.Is(err, constants.ErrProjectNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Project not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeProjectNotFound, "The specified project could not be found.")) return case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid input")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid input")) return default: h.slogger.Error("Failed to create LLM proxy", "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to create LLM proxy")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to create LLM proxy")) return } } @@ -683,12 +766,14 @@ func (h *LLMHandler) CreateLLMProxy(w http.ResponseWriter, r *http.Request) { func (h *LLMHandler) ListLLMProxies(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } projectID := strings.TrimSpace(r.URL.Query().Get("projectId")) if projectID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "projectId query parameter is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "projectId query parameter is required")) return } @@ -717,11 +802,13 @@ func (h *LLMHandler) ListLLMProxies(w http.ResponseWriter, r *http.Request) { resp, err := h.proxyService.List(orgID, &projectID, limit, offset) if err != nil { if errors.Is(err, constants.ErrProjectNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Project not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeProjectNotFound, "The specified project could not be found.")) return } h.slogger.Error("Failed to list LLM proxies", "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to list LLM proxies")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to list LLM proxies")) return } httputil.WriteJSON(w, http.StatusOK, resp) @@ -730,7 +817,8 @@ func (h *LLMHandler) ListLLMProxies(w http.ResponseWriter, r *http.Request) { func (h *LLMHandler) ListLLMProxiesByProvider(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } providerID := r.PathValue("llmProviderId") @@ -761,14 +849,17 @@ func (h *LLMHandler) ListLLMProxiesByProvider(w http.ResponseWriter, r *http.Req if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "LLM provider not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) return case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid provider id")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid provider id")) return default: h.slogger.Error("Failed to list LLM proxies by provider", "organizationId", orgID, "providerId", providerID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to list LLM proxies")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to list LLM proxies")) return } } @@ -778,7 +869,8 @@ func (h *LLMHandler) ListLLMProxiesByProvider(w http.ResponseWriter, r *http.Req func (h *LLMHandler) GetLLMProxy(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } id := r.PathValue("llmProxyId") @@ -787,14 +879,17 @@ func (h *LLMHandler) GetLLMProxy(w http.ResponseWriter, r *http.Request) { if err != nil { switch { case errors.Is(err, constants.ErrLLMProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "LLM proxy not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) return case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid proxy id")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid proxy id")) return default: h.slogger.Error("Failed to get LLM proxy", "organizationId", orgID, "proxyId", id, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get LLM proxy")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to get LLM proxy")) return } } @@ -804,14 +899,16 @@ func (h *LLMHandler) GetLLMProxy(w http.ResponseWriter, r *http.Request) { func (h *LLMHandler) UpdateLLMProxy(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } id := r.PathValue("llmProxyId") var req api.LLMProxy if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid request body")) return } @@ -826,20 +923,25 @@ func (h *LLMHandler) UpdateLLMProxy(w http.ResponseWriter, r *http.Request) { } switch { case errors.Is(err, constants.ErrLLMProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "LLM proxy not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) return case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Referenced provider not found")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderNotFound, "The referenced LLM provider could not be found.")) return case errors.Is(err, constants.ErrHandleImmutable): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, err.Error())) return case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid input")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid input")) return default: h.slogger.Error("Failed to update LLM proxy", "organizationId", orgID, "proxyId", id, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to update LLM proxy")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to update LLM proxy")) return } } @@ -849,7 +951,8 @@ func (h *LLMHandler) UpdateLLMProxy(w http.ResponseWriter, r *http.Request) { func (h *LLMHandler) DeleteLLMProxy(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } id := r.PathValue("llmProxyId") @@ -864,14 +967,17 @@ func (h *LLMHandler) DeleteLLMProxy(w http.ResponseWriter, r *http.Request) { } switch { case errors.Is(err, constants.ErrLLMProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "LLM proxy not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) return case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid proxy id")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid proxy id")) return default: h.slogger.Error("Failed to delete LLM proxy", "organizationId", orgID, "proxyId", id, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to delete LLM proxy")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to delete LLM proxy")) return } } diff --git a/platform-api/internal/handler/llm_apikey.go b/platform-api/internal/handler/llm_apikey.go index bb9973556d..a390e3911b 100644 --- a/platform-api/internal/handler/llm_apikey.go +++ b/platform-api/internal/handler/llm_apikey.go @@ -53,15 +53,15 @@ func NewLLMProviderAPIKeyHandler(apiKeyService *service.LLMProviderAPIKeyService func (h *LLMProviderAPIKeyHandler) ListAPIKeys(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } providerID := r.PathValue("llmProviderId") if providerID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "LLM provider ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "LLM provider ID is required")) return } @@ -73,13 +73,13 @@ func (h *LLMProviderAPIKeyHandler) ListAPIKeys(w http.ResponseWriter, r *http.Re response, err := h.apiKeyService.ListLLMProviderAPIKeys(r.Context(), providerID, orgID, callerUserID) if err != nil { if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "LLM provider not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) return } h.slogger.Error("Failed to list LLM provider API keys", "providerId", providerID, "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to list API keys")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to list API keys")) return } @@ -90,22 +90,22 @@ func (h *LLMProviderAPIKeyHandler) ListAPIKeys(w http.ResponseWriter, r *http.Re func (h *LLMProviderAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } providerID := r.PathValue("llmProviderId") if providerID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "LLM provider ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "LLM provider ID is required")) return } keyName := r.PathValue("apiKeyId") if keyName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API key name is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API key name is required")) return } @@ -117,23 +117,23 @@ func (h *LLMProviderAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.R err := h.apiKeyService.DeleteLLMProviderAPIKey(r.Context(), providerID, orgID, callerUserID, keyName) if err != nil { if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "LLM provider not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) return } if errors.Is(err, constants.ErrAPIKeyNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "API key not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderAPIKeyNotFound, "The specified API key could not be found.")) return } if errors.Is(err, constants.ErrAPIKeyForbidden) { - httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponse(403, "Forbidden", - "Only the key creator can delete this API key")) + httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( + utils.CodeCommonForbidden, "Only the key creator can delete this API key")) return } h.slogger.Error("Failed to delete LLM provider API key", "providerId", providerID, "keyName", keyName, "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to delete API key")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to delete API key")) return } @@ -145,31 +145,31 @@ func (h *LLMProviderAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.R func (h *LLMProviderAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } providerID := r.PathValue("llmProviderId") if providerID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "LLM provider ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "LLM provider ID is required")) return } var req api.CreateLLMProviderAPIKeyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.slogger.Error("Invalid LLM provider API key creation request", "providerId", providerID, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid request body")) return } // Validate that displayName is provided (name is optional; auto-generated from displayName if absent) req.DisplayName = strings.TrimSpace(req.DisplayName) if req.DisplayName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "'displayName' is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "'displayName' is required")) return } @@ -181,19 +181,19 @@ func (h *LLMProviderAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.R response, err := h.apiKeyService.CreateLLMProviderAPIKey(r.Context(), providerID, orgID, userID, &req) if err != nil { if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "LLM provider not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) return } if errors.Is(err, constants.ErrGatewayUnavailable) { - httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponse(503, "Service Unavailable", - "No gateway connections available")) + httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponseWithCode( + utils.CodeGatewayConnectionUnavailable, "No gateway connections are currently available.")) return } h.slogger.Error("Failed to create LLM provider API key", "providerId", providerID, "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to create API key")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to create API key")) return } diff --git a/platform-api/internal/handler/llm_deployment.go b/platform-api/internal/handler/llm_deployment.go index 017e5764f0..cd83cf06bd 100644 --- a/platform-api/internal/handler/llm_deployment.go +++ b/platform-api/internal/handler/llm_deployment.go @@ -59,37 +59,38 @@ func NewLLMProxyDeploymentHandler(deploymentService *service.LLMProxyDeploymentS func (h *LLMProviderDeploymentHandler) DeployLLMProvider(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } providerId := r.PathValue("llmProviderId") if providerId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "LLM provider ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "LLM provider ID is required")) return } var req api.DeployRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, err.Error())) return } if req.Name == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "name is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderDeploymentValidationFailed, "name is required")) return } if req.Base == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "base is required (use 'current' or a deploymentId)")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderDeploymentValidationFailed, "base is required (use 'current' or a deploymentId)")) return } if strings.TrimSpace(req.GatewayId) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "gatewayId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderDeploymentValidationFailed, "gatewayId is required")) return } @@ -100,41 +101,41 @@ func (h *LLMProviderDeploymentHandler) DeployLLMProvider(w http.ResponseWriter, } switch { case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "LLM provider not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) return case errors.Is(err, constants.ErrGatewayNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Gateway not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayNotFound, "The specified gateway could not be found.")) return case errors.Is(err, constants.ErrBaseDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Base deployment not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeDeploymentBaseNotFound, "The specified base deployment could not be found.")) return case errors.Is(err, constants.ErrDeploymentNameRequired): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Deployment name is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderDeploymentValidationFailed, "Deployment name is required")) return case errors.Is(err, constants.ErrDeploymentBaseRequired): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Base is required (use 'current' or a deploymentId)")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderDeploymentValidationFailed, "Base is required (use 'current' or a deploymentId)")) return case errors.Is(err, constants.ErrDeploymentGatewayIDRequired): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Gateway ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderDeploymentValidationFailed, "Gateway ID is required")) return case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Referenced template not found")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderDeploymentValidationFailed, "Referenced template not found")) return case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid input")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderDeploymentValidationFailed, "Invalid input")) return default: h.slogger.Error("Failed to deploy LLM provider", "providerId", providerId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to deploy LLM provider")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to deploy LLM provider")) return } } @@ -146,8 +147,8 @@ func (h *LLMProviderDeploymentHandler) DeployLLMProvider(w http.ResponseWriter, func (h *LLMProviderDeploymentHandler) UndeployLLMProviderDeployment(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -156,8 +157,8 @@ func (h *LLMProviderDeploymentHandler) UndeployLLMProviderDeployment(w http.Resp gatewayId := r.URL.Query().Get("gatewayId") if providerId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "LLM provider ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "LLM provider ID is required")) return } deployment, err := h.deploymentService.UndeployLLMProviderDeployment(providerId, deploymentId, gatewayId, orgId) @@ -168,28 +169,29 @@ func (h *LLMProviderDeploymentHandler) UndeployLLMProviderDeployment(w http.Resp } switch { case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "LLM provider not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) return case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Deployment not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) return case errors.Is(err, constants.ErrGatewayNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Gateway not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayNotFound, "The specified gateway could not be found.")) return case errors.Is(err, constants.ErrDeploymentNotActive): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", - "No active deployment found for this LLM provider on the gateway")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeDeploymentNotActive, "No active deployment found for this LLM provider on the gateway.")) return case errors.Is(err, constants.ErrGatewayIDMismatch): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Deployment is bound to a different gateway")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeDeploymentGatewayMismatch, "Deployment is bound to a different gateway.")) return default: h.slogger.Error("Failed to undeploy LLM provider", "providerId", providerId, "deploymentId", deploymentId, "gatewayId", gatewayId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to undeploy deployment")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to undeploy deployment")) return } } @@ -201,8 +203,8 @@ func (h *LLMProviderDeploymentHandler) UndeployLLMProviderDeployment(w http.Resp func (h *LLMProviderDeploymentHandler) RestoreLLMProviderDeployment(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -211,8 +213,8 @@ func (h *LLMProviderDeploymentHandler) RestoreLLMProviderDeployment(w http.Respo gatewayId := r.URL.Query().Get("gatewayId") if providerId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "LLM provider ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "LLM provider ID is required")) return } deployment, err := h.deploymentService.RestoreLLMProviderDeployment(providerId, deploymentId, gatewayId, orgId) @@ -223,28 +225,29 @@ func (h *LLMProviderDeploymentHandler) RestoreLLMProviderDeployment(w http.Respo } switch { case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "LLM provider not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) return case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Deployment not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) return case errors.Is(err, constants.ErrGatewayNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Gateway not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayNotFound, "The specified gateway could not be found.")) return case errors.Is(err, constants.ErrDeploymentAlreadyDeployed): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", - "Cannot restore currently deployed deployment")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeDeploymentRestoreConflict, "Cannot restore the currently deployed deployment, or the deployment is invalid.")) return case errors.Is(err, constants.ErrGatewayIDMismatch): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Deployment is bound to a different gateway")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeDeploymentGatewayMismatch, "Deployment is bound to a different gateway.")) return default: h.slogger.Error("Failed to restore LLM provider deployment", "providerId", providerId, "deploymentId", deploymentId, "gatewayId", gatewayId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to restore deployment")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to restore deployment")) return } } @@ -256,8 +259,8 @@ func (h *LLMProviderDeploymentHandler) RestoreLLMProviderDeployment(w http.Respo func (h *LLMProviderDeploymentHandler) DeleteLLMProviderDeployment(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -265,13 +268,13 @@ func (h *LLMProviderDeploymentHandler) DeleteLLMProviderDeployment(w http.Respon deploymentId := r.PathValue("deploymentId") if providerId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "LLM provider ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "LLM provider ID is required")) return } if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Deployment ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Deployment ID is required")) return } @@ -279,20 +282,21 @@ func (h *LLMProviderDeploymentHandler) DeleteLLMProviderDeployment(w http.Respon if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "LLM provider not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) return case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Deployment not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) return case errors.Is(err, constants.ErrDeploymentIsDeployed): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", - "Cannot delete an active deployment - undeploy it first")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeDeploymentActive, "Cannot delete an active deployment - undeploy it first.")) return default: h.slogger.Error("Failed to delete LLM provider deployment", "providerId", providerId, "deploymentId", deploymentId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to delete deployment")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to delete deployment")) return } } @@ -304,8 +308,8 @@ func (h *LLMProviderDeploymentHandler) DeleteLLMProviderDeployment(w http.Respon func (h *LLMProviderDeploymentHandler) GetLLMProviderDeployment(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -313,13 +317,13 @@ func (h *LLMProviderDeploymentHandler) GetLLMProviderDeployment(w http.ResponseW deploymentId := r.PathValue("deploymentId") if providerId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "LLM provider ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "LLM provider ID is required")) return } if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Deployment ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Deployment ID is required")) return } @@ -327,17 +331,17 @@ func (h *LLMProviderDeploymentHandler) GetLLMProviderDeployment(w http.ResponseW if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "LLM provider not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) return case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Deployment not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) return default: h.slogger.Error("Failed to get LLM provider deployment", "providerId", providerId, "deploymentId", deploymentId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to retrieve deployment")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to retrieve deployment")) return } } @@ -349,15 +353,15 @@ func (h *LLMProviderDeploymentHandler) GetLLMProviderDeployment(w http.ResponseW func (h *LLMProviderDeploymentHandler) GetLLMProviderDeployments(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } providerId := r.PathValue("llmProviderId") if providerId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "LLM provider ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "LLM provider ID is required")) return } @@ -374,17 +378,17 @@ func (h *LLMProviderDeploymentHandler) GetLLMProviderDeployments(w http.Response if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "LLM provider not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) return case errors.Is(err, constants.ErrInvalidDeploymentStatus): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid deployment status")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeDeploymentInvalidStatus, "The specified deployment status filter is invalid.")) return default: h.slogger.Error("Failed to get LLM provider deployments", "providerId", providerId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to retrieve deployments")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to retrieve deployments")) return } } @@ -407,37 +411,38 @@ func (h *LLMProviderDeploymentHandler) RegisterRoutes(mux *http.ServeMux) { func (h *LLMProxyDeploymentHandler) DeployLLMProxy(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } proxyId := r.PathValue("llmProxyId") if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "LLM proxy ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "LLM proxy ID is required")) return } var req api.DeployRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, err.Error())) return } if req.Name == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "name is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeLLMProxyDeploymentValidationFailed, "name is required")) return } if req.Base == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "base is required (use 'current' or a deploymentId)")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeLLMProxyDeploymentValidationFailed, "base is required (use 'current' or a deploymentId)")) return } if strings.TrimSpace(req.GatewayId) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "gatewayId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeLLMProxyDeploymentValidationFailed, "gatewayId is required")) return } @@ -448,37 +453,37 @@ func (h *LLMProxyDeploymentHandler) DeployLLMProxy(w http.ResponseWriter, r *htt } switch { case errors.Is(err, constants.ErrLLMProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "LLM proxy not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) return case errors.Is(err, constants.ErrGatewayNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Gateway not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayNotFound, "The specified gateway could not be found.")) return case errors.Is(err, constants.ErrBaseDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Base deployment not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeDeploymentBaseNotFound, "The specified base deployment could not be found.")) return case errors.Is(err, constants.ErrDeploymentNameRequired): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Deployment name is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeLLMProxyDeploymentValidationFailed, "Deployment name is required")) return case errors.Is(err, constants.ErrDeploymentBaseRequired): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Base is required (use 'current' or a deploymentId)")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeLLMProxyDeploymentValidationFailed, "Base is required (use 'current' or a deploymentId)")) return case errors.Is(err, constants.ErrDeploymentGatewayIDRequired): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Gateway ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeLLMProxyDeploymentValidationFailed, "Gateway ID is required")) return case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid input")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeLLMProxyDeploymentValidationFailed, "Invalid input")) return default: h.slogger.Error("Failed to deploy LLM proxy", "proxyId", proxyId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to deploy LLM proxy")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to deploy LLM proxy")) return } } @@ -490,8 +495,8 @@ func (h *LLMProxyDeploymentHandler) DeployLLMProxy(w http.ResponseWriter, r *htt func (h *LLMProxyDeploymentHandler) UndeployLLMProxyDeployment(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -500,8 +505,8 @@ func (h *LLMProxyDeploymentHandler) UndeployLLMProxyDeployment(w http.ResponseWr gatewayId := r.URL.Query().Get("gatewayId") if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "LLM proxy ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "LLM proxy ID is required")) return } deployment, err := h.deploymentService.UndeployLLMProxyDeployment(proxyId, deploymentId, gatewayId, orgId) @@ -512,28 +517,29 @@ func (h *LLMProxyDeploymentHandler) UndeployLLMProxyDeployment(w http.ResponseWr } switch { case errors.Is(err, constants.ErrLLMProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "LLM proxy not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) return case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Deployment not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) return case errors.Is(err, constants.ErrGatewayNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Gateway not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayNotFound, "The specified gateway could not be found.")) return case errors.Is(err, constants.ErrDeploymentNotActive): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", - "No active deployment found for this LLM proxy on the gateway")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeDeploymentNotActive, "No active deployment found for this LLM proxy on the gateway.")) return case errors.Is(err, constants.ErrGatewayIDMismatch): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Deployment is bound to a different gateway")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeDeploymentGatewayMismatch, "Deployment is bound to a different gateway.")) return default: h.slogger.Error("Failed to undeploy LLM proxy", "proxyId", proxyId, "deploymentId", deploymentId, "gatewayId", gatewayId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to undeploy deployment")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to undeploy deployment")) return } } @@ -545,8 +551,8 @@ func (h *LLMProxyDeploymentHandler) UndeployLLMProxyDeployment(w http.ResponseWr func (h *LLMProxyDeploymentHandler) RestoreLLMProxyDeployment(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -555,8 +561,8 @@ func (h *LLMProxyDeploymentHandler) RestoreLLMProxyDeployment(w http.ResponseWri gatewayId := r.URL.Query().Get("gatewayId") if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "LLM proxy ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "LLM proxy ID is required")) return } deployment, err := h.deploymentService.RestoreLLMProxyDeployment(proxyId, deploymentId, gatewayId, orgId) @@ -567,28 +573,29 @@ func (h *LLMProxyDeploymentHandler) RestoreLLMProxyDeployment(w http.ResponseWri } switch { case errors.Is(err, constants.ErrLLMProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "LLM proxy not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) return case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Deployment not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) return case errors.Is(err, constants.ErrGatewayNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Gateway not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayNotFound, "The specified gateway could not be found.")) return case errors.Is(err, constants.ErrDeploymentAlreadyDeployed): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", - "Cannot restore currently deployed deployment")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeDeploymentRestoreConflict, "Cannot restore the currently deployed deployment, or the deployment is invalid.")) return case errors.Is(err, constants.ErrGatewayIDMismatch): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Deployment is bound to a different gateway")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeDeploymentGatewayMismatch, "Deployment is bound to a different gateway.")) return default: h.slogger.Error("Failed to restore LLM proxy deployment", "proxyId", proxyId, "deploymentId", deploymentId, "gatewayId", gatewayId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to restore deployment")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to restore deployment")) return } } @@ -600,8 +607,8 @@ func (h *LLMProxyDeploymentHandler) RestoreLLMProxyDeployment(w http.ResponseWri func (h *LLMProxyDeploymentHandler) DeleteLLMProxyDeployment(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -609,13 +616,13 @@ func (h *LLMProxyDeploymentHandler) DeleteLLMProxyDeployment(w http.ResponseWrit deploymentId := r.PathValue("deploymentId") if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "LLM proxy ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "LLM proxy ID is required")) return } if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Deployment ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Deployment ID is required")) return } @@ -623,20 +630,21 @@ func (h *LLMProxyDeploymentHandler) DeleteLLMProxyDeployment(w http.ResponseWrit if err != nil { switch { case errors.Is(err, constants.ErrLLMProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "LLM proxy not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) return case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Deployment not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) return case errors.Is(err, constants.ErrDeploymentIsDeployed): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", - "Cannot delete an active deployment - undeploy it first")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeDeploymentActive, "Cannot delete an active deployment - undeploy it first.")) return default: h.slogger.Error("Failed to delete LLM proxy deployment", "proxyId", proxyId, "deploymentId", deploymentId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to delete deployment")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to delete deployment")) return } } @@ -648,8 +656,8 @@ func (h *LLMProxyDeploymentHandler) DeleteLLMProxyDeployment(w http.ResponseWrit func (h *LLMProxyDeploymentHandler) GetLLMProxyDeployment(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -657,13 +665,13 @@ func (h *LLMProxyDeploymentHandler) GetLLMProxyDeployment(w http.ResponseWriter, deploymentId := r.PathValue("deploymentId") if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "LLM proxy ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "LLM proxy ID is required")) return } if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Deployment ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Deployment ID is required")) return } @@ -671,17 +679,17 @@ func (h *LLMProxyDeploymentHandler) GetLLMProxyDeployment(w http.ResponseWriter, if err != nil { switch { case errors.Is(err, constants.ErrLLMProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "LLM proxy not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) return case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Deployment not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) return default: h.slogger.Error("Failed to get LLM proxy deployment", "proxyId", proxyId, "deploymentId", deploymentId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to retrieve deployment")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to retrieve deployment")) return } } @@ -693,15 +701,15 @@ func (h *LLMProxyDeploymentHandler) GetLLMProxyDeployment(w http.ResponseWriter, func (h *LLMProxyDeploymentHandler) GetLLMProxyDeployments(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } proxyId := r.PathValue("llmProxyId") if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "LLM proxy ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "LLM proxy ID is required")) return } @@ -718,17 +726,17 @@ func (h *LLMProxyDeploymentHandler) GetLLMProxyDeployments(w http.ResponseWriter if err != nil { switch { case errors.Is(err, constants.ErrLLMProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "LLM proxy not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) return case errors.Is(err, constants.ErrInvalidDeploymentStatus): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid deployment status")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeDeploymentInvalidStatus, "The specified deployment status filter is invalid.")) return default: h.slogger.Error("Failed to get LLM proxy deployments", "proxyId", proxyId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to retrieve deployments")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to retrieve deployments")) return } } diff --git a/platform-api/internal/handler/llm_provider_integration_test.go b/platform-api/internal/handler/llm_provider_integration_test.go new file mode 100644 index 0000000000..7da88a6eb2 --- /dev/null +++ b/platform-api/internal/handler/llm_provider_integration_test.go @@ -0,0 +1,289 @@ +/* + * 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 ( + "bytes" + "database/sql" + "fmt" + "log/slog" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/wso2/api-platform/platform-api/config" + "github.com/wso2/api-platform/platform-api/internal/database" + "github.com/wso2/api-platform/platform-api/internal/middleware" + "github.com/wso2/api-platform/platform-api/internal/repository" + "github.com/wso2/api-platform/platform-api/internal/service" + "github.com/wso2/api-platform/platform-api/internal/utils" + "github.com/wso2/api-platform/platform-api/internal/vault" + + _ "github.com/mattn/go-sqlite3" +) + +const provBase = "/api/v0.9/llm-providers" +const provOrg = "org-prov-it-001" + +// setupLLMProviderEnv builds the real route -> handler -> service -> repository +// stack over an in-memory SQLite DB, seeded with the shipped built-in templates +// and a test organization. maxProviders <= 0 means unlimited (see +// config.ArtifactLimits), letting individual tests exercise the limit-reached path. +func setupLLMProviderEnv(t *testing.T, maxProviders int) (http.Handler, func()) { + t.Helper() + + dbPath := filepath.Join(t.TempDir(), "test.db") + sqlDB, err := sql.Open("sqlite3", dbPath) + if err != nil { + t.Fatalf("open sqlite: %v", err) + } + sqlDB.Exec("PRAGMA foreign_keys = ON") + db := &database.DB{DB: sqlDB} + + schema, err := os.ReadFile(filepath.Join("..", "database", "schema.sqlite.sql")) + if err != nil { + t.Fatalf("read schema: %v", err) + } + if _, err = db.Exec(string(schema)); err != nil { + t.Fatalf("apply schema: %v", err) + } + if _, err = db.Exec(`INSERT INTO organizations (uuid, handle, display_name, region, idp_organization_ref_uuid, created_at, updated_at) + VALUES ('` + provOrg + `', 'test-prov-org', 'Test Prov Org', 'default', 'idp-ref', datetime('now'), datetime('now'))`); err != nil { + t.Fatalf("insert org: %v", err) + } + + templateRepo := repository.NewLLMProviderTemplateRepo(db) + providerRepo := repository.NewLLMProviderRepo(db) + orgRepo := repository.NewOrganizationRepo(db) + + builtins, err := utils.LoadLLMProviderTemplatesFromDirectory( + filepath.Join("..", "..", "resources", "default-llm-provider-templates")) + if err != nil { + t.Fatalf("load built-ins: %v", err) + } + seeder := service.NewLLMTemplateSeeder(templateRepo, builtins) + if err := seeder.SeedForOrg(provOrg); err != nil { + t.Fatalf("seed built-ins: %v", err) + } + + identityService := service.NewIdentityService(repository.NewUserIdentityMappingRepo(db)) + cfg := &config.Server{ArtifactLimits: config.ArtifactLimits{MaxLLMProvidersPerOrg: maxProviders}} + + providerService := service.NewLLMProviderService( + providerRepo, templateRepo, orgRepo, seeder, + nil, nil, nil, // deploymentRepo, gatewayRepo, gatewayEventsService: unused by Create in these tests + slog.Default(), noopAudit{}, cfg, identityService, + ) + + secretRepo := repository.NewSecretRepo(db) + v, err := vault.NewInHouseVault([]byte("12345678901234567890123456789012")) + if err != nil { + t.Fatalf("create vault: %v", err) + } + secretService := service.NewSecretService(secretRepo, v, identityService) + providerService.SetSecretService(secretService) + + h := NewLLMHandler(nil, providerService, nil, identityService, slog.Default()) + mux := http.NewServeMux() + h.RegisterRoutes(mux) + + return middleware.NewTestContextMiddleware(mux), func() { _ = sqlDB.Close() } +} + +// doProviderJSON issues an HTTP request against r, scoped to provOrg (auth=true +// sets X-Test-Org/X-Test-User; auth=false omits them to exercise the 401 path). +// Self-contained rather than reusing llm_template_integration_test.go's doJSON, +// which hardcodes a different org constant. +func doProviderJSON(t *testing.T, r http.Handler, method, path, body string, auth bool) *httptest.ResponseRecorder { + t.Helper() + req, _ := http.NewRequest(method, path, bytes.NewBufferString(body)) + if body != "" { + req.Header.Set("Content-Type", "application/json") + } + if auth { + req.Header.Set("X-Test-Org", provOrg) + req.Header.Set("X-Test-User", "alice") + } + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + return w +} + +// validProviderBody returns a minimal, valid create-provider JSON body backed +// by the built-in "openai" template. id is left auto-generated unless idSuffix +// is non-empty, so successive calls in the same test don't collide on handle. +func validProviderBody(idSuffix string) string { + name := "Test Provider" + if idSuffix != "" { + name = "Test Provider " + idSuffix + } + return fmt.Sprintf(`{ + "displayName": %q, + "version": "v1.0", + "template": "openai", + "upstream": {"main": {"url": "https://api.openai.com/v1"}}, + "accessControl": {"mode": "allow_all"} + }`, name) +} + +// ---- Auth ------------------------------------------------------------- + +func TestLLMProviderHTTP_CreateRequiresOrg_401(t *testing.T) { + r, cleanup := setupLLMProviderEnv(t, 0) + defer cleanup() + + w := doProviderJSON(t, r, http.MethodPost, provBase, validProviderBody(""), false) + if w.Code != http.StatusUnauthorized { + t.Fatalf("expected 401 without org, got %d: %s", w.Code, w.Body.String()) + } + body := bodyMap(t, w) + if body["status"] != "error" { + t.Errorf("expected status=error, got %v", body["status"]) + } + if body["code"] != "COMMON_UNAUTHORIZED" { + t.Errorf("expected code=COMMON_UNAUTHORIZED, got %v", body["code"]) + } +} + +// ---- Create: validation errors ----------------------------------------- + +func TestLLMProviderHTTP_Create_InvalidBody_400(t *testing.T) { + r, cleanup := setupLLMProviderEnv(t, 0) + defer cleanup() + + w := doProviderJSON(t, r, http.MethodPost, provBase, `not-json`, true) + if w.Code != http.StatusBadRequest { + t.Fatalf("invalid JSON: expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestLLMProviderHTTP_Create_MissingRequiredFields_400(t *testing.T) { + r, cleanup := setupLLMProviderEnv(t, 0) + defer cleanup() + + // Missing displayName/version/template entirely. + w := doProviderJSON(t, r, http.MethodPost, provBase, `{"upstream":{"main":{"url":"https://x"}},"accessControl":{"mode":"allow_all"}}`, true) + if w.Code != http.StatusBadRequest { + t.Fatalf("missing required fields: expected 400, got %d: %s", w.Code, w.Body.String()) + } + body := bodyMap(t, w) + if body["status"] != "error" || body["code"] == nil || body["message"] == nil { + t.Errorf("expected standard error shape, got %v", body) + } +} + +func TestLLMProviderHTTP_Create_UnknownTemplate_400(t *testing.T) { + r, cleanup := setupLLMProviderEnv(t, 0) + defer cleanup() + + body := `{ + "displayName": "Bad Template Provider", + "version": "v1.0", + "template": "does-not-exist", + "upstream": {"main": {"url": "https://api.example.com"}}, + "accessControl": {"mode": "allow_all"} + }` + w := doProviderJSON(t, r, http.MethodPost, provBase, body, true) + if w.Code != http.StatusBadRequest { + t.Fatalf("unknown template: expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +// ---- Create: conflicts -------------------------------------------------- + +func TestLLMProviderHTTP_Create_DuplicateHandle_409(t *testing.T) { + r, cleanup := setupLLMProviderEnv(t, 0) + defer cleanup() + + body := `{ + "id": "dup-provider", + "displayName": "Dup Provider", + "version": "v1.0", + "template": "openai", + "upstream": {"main": {"url": "https://api.openai.com/v1"}}, + "accessControl": {"mode": "allow_all"} + }` + if w := doProviderJSON(t, r, http.MethodPost, provBase, body, true); w.Code != http.StatusCreated { + t.Fatalf("first create: expected 201, got %d: %s", w.Code, w.Body.String()) + } + + w := doProviderJSON(t, r, http.MethodPost, provBase, body, true) + if w.Code != http.StatusConflict { + t.Fatalf("duplicate handle: expected 409, got %d: %s", w.Code, w.Body.String()) + } + respBody := bodyMap(t, w) + if respBody["status"] != "error" { + t.Errorf("expected status=error, got %v", respBody["status"]) + } +} + +func TestLLMProviderHTTP_Create_LimitReached_409(t *testing.T) { + r, cleanup := setupLLMProviderEnv(t, 1) // only one provider allowed for this org + defer cleanup() + + if w := doProviderJSON(t, r, http.MethodPost, provBase, validProviderBody("A"), true); w.Code != http.StatusCreated { + t.Fatalf("first create: expected 201, got %d: %s", w.Code, w.Body.String()) + } + + w := doProviderJSON(t, r, http.MethodPost, provBase, validProviderBody("B"), true) + if w.Code != http.StatusConflict { + t.Fatalf("limit reached: expected 409, got %d: %s", w.Code, w.Body.String()) + } +} + +// ---- Create: secret placeholder validation ------------------------------ + +func TestLLMProviderHTTP_Create_MissingSecretRef_400(t *testing.T) { + r, cleanup := setupLLMProviderEnv(t, 0) + defer cleanup() + + body := `{ + "displayName": "Secret Ref Provider", + "version": "v1.0", + "template": "openai", + "upstream": { + "main": { + "url": "https://api.openai.com/v1", + "auth": {"type": "api-key", "header": "Authorization", "value": "{{ secret \"does-not-exist\" }}"} + } + }, + "accessControl": {"mode": "allow_all"} + }` + w := doProviderJSON(t, r, http.MethodPost, provBase, body, true) + if w.Code != http.StatusBadRequest { + t.Fatalf("missing secret ref: expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +// ---- Create: happy path (sanity check for the error tests above) ------- + +func TestLLMProviderHTTP_Create_Success(t *testing.T) { + r, cleanup := setupLLMProviderEnv(t, 0) + defer cleanup() + + w := doProviderJSON(t, r, http.MethodPost, provBase, validProviderBody(""), true) + if w.Code != http.StatusCreated { + t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String()) + } + body := bodyMap(t, w) + if body["displayName"] != "Test Provider" { + t.Errorf("expected displayName echoed back, got %v", body["displayName"]) + } +} diff --git a/platform-api/internal/handler/llm_proxy_apikey.go b/platform-api/internal/handler/llm_proxy_apikey.go index d9ed497d17..5c45015fd4 100644 --- a/platform-api/internal/handler/llm_proxy_apikey.go +++ b/platform-api/internal/handler/llm_proxy_apikey.go @@ -53,15 +53,15 @@ func NewLLMProxyAPIKeyHandler(apiKeyService *service.LLMProxyAPIKeyService, iden func (h *LLMProxyAPIKeyHandler) ListAPIKeys(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } proxyID := r.PathValue("llmProxyId") if proxyID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "LLM proxy ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "LLM proxy ID is required")) return } @@ -73,13 +73,13 @@ func (h *LLMProxyAPIKeyHandler) ListAPIKeys(w http.ResponseWriter, r *http.Reque response, err := h.apiKeyService.ListLLMProxyAPIKeys(r.Context(), proxyID, orgID, callerUserID) if err != nil { if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "LLM proxy not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) return } h.slogger.Error("Failed to list LLM proxy API keys", "proxyId", proxyID, "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to list API keys")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to list API keys")) return } @@ -90,22 +90,22 @@ func (h *LLMProxyAPIKeyHandler) ListAPIKeys(w http.ResponseWriter, r *http.Reque func (h *LLMProxyAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } proxyID := r.PathValue("llmProxyId") if proxyID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "LLM proxy ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "LLM proxy ID is required")) return } keyName := r.PathValue("apiKeyId") if keyName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API key name is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API key name is required")) return } @@ -117,23 +117,23 @@ func (h *LLMProxyAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.Requ err := h.apiKeyService.DeleteLLMProxyAPIKey(r.Context(), proxyID, orgID, callerUserID, keyName) if err != nil { if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "LLM proxy not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) return } if errors.Is(err, constants.ErrAPIKeyNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "API key not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProxyAPIKeyNotFound, "The specified API key could not be found.")) return } if errors.Is(err, constants.ErrAPIKeyForbidden) { - httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponse(403, "Forbidden", - "Only the key creator can delete this API key")) + httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( + utils.CodeCommonForbidden, "Only the key creator can delete this API key")) return } h.slogger.Error("Failed to delete LLM proxy API key", "proxyId", proxyID, "keyName", keyName, "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to delete API key")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to delete API key")) return } @@ -145,31 +145,31 @@ func (h *LLMProxyAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.Requ func (h *LLMProxyAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } proxyID := r.PathValue("llmProxyId") if proxyID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "LLM proxy ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "LLM proxy ID is required")) return } var req api.CreateLLMProxyAPIKeyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.slogger.Error("Invalid LLM proxy API key creation request", "proxyId", proxyID, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid request body")) return } // Validate that displayName is provided (name is optional; auto-generated from displayName if absent) req.DisplayName = strings.TrimSpace(req.DisplayName) if req.DisplayName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "'displayName' is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "'displayName' is required")) return } @@ -181,19 +181,19 @@ func (h *LLMProxyAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Requ response, err := h.apiKeyService.CreateLLMProxyAPIKey(r.Context(), proxyID, orgID, userID, &req) if err != nil { if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "LLM proxy not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) return } if errors.Is(err, constants.ErrGatewayUnavailable) { - httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponse(503, "Service Unavailable", - "No gateway connections available")) + httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponseWithCode( + utils.CodeGatewayConnectionUnavailable, "No gateway connections are currently available.")) return } h.slogger.Error("Failed to create LLM proxy API key", "proxyId", proxyID, "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to create API key")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to create API key")) return } diff --git a/platform-api/internal/handler/mcp.go b/platform-api/internal/handler/mcp.go index 12816829f8..1b8ac7820e 100644 --- a/platform-api/internal/handler/mcp.go +++ b/platform-api/internal/handler/mcp.go @@ -62,14 +62,16 @@ func (h *MCPProxyHandler) CreateMCPProxy(w http.ResponseWriter, r *http.Request) orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { h.slogger.Error("MCP request validation failed", "reason", "Organization claim not found in token") - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } var req api.MCPProxy if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.slogger.Error("MCP request validation failed", "org_id", orgID, "reason", "Invalid request body", "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid request body")) return } @@ -96,13 +98,15 @@ func (h *MCPProxyHandler) ListMCPProxies(w http.ResponseWriter, r *http.Request) orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { h.slogger.Error("MCP request validation failed", "reason", "Organization claim not found in token") - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } projectID := strings.TrimSpace(r.URL.Query().Get("projectId")) if projectID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "projectId query parameter is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "projectId query parameter is required")) return } @@ -144,7 +148,8 @@ func (h *MCPProxyHandler) GetMCPProxy(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { h.slogger.Error("MCP request validation failed", "reason", "Organization claim not found in token") - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } id := r.PathValue("mcpProxyId") @@ -163,7 +168,8 @@ func (h *MCPProxyHandler) UpdateMCPProxy(w http.ResponseWriter, r *http.Request) orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { h.slogger.Error("MCP request validation failed", "reason", "Organization claim not found in token") - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } id := r.PathValue("mcpProxyId") @@ -171,7 +177,8 @@ func (h *MCPProxyHandler) UpdateMCPProxy(w http.ResponseWriter, r *http.Request) var req api.MCPProxy if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.slogger.Error("MCP request validation failed", "org_id", orgID, "proxy_id", id, "reason", "Invalid request body", "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid request body")) return } @@ -193,7 +200,8 @@ func (h *MCPProxyHandler) DeleteMCPProxy(w http.ResponseWriter, r *http.Request) orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { h.slogger.Error("MCP request validation failed", "reason", "Organization claim not found in token") - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } id := r.PathValue("mcpProxyId") @@ -215,14 +223,16 @@ func (h *MCPProxyHandler) FetchMCPProxyServerInfo(w http.ResponseWriter, r *http orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { h.slogger.Error("MCP request validation failed", "reason", "Organization claim not found in token") - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } var req api.MCPServerInfoFetchRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.slogger.Error("MCP request validation failed", "org_id", orgID, "reason", "Invalid request body", "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid request body")) return } @@ -231,15 +241,18 @@ func (h *MCPProxyHandler) FetchMCPProxyServerInfo(w http.ResponseWriter, r *http switch { case errors.Is(err, constants.ErrInvalidURL): h.slogger.Error("Invalid URL provided for MCP server info fetch", "error", err, "inputUrl", req.Url) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, err.Error())) return case errors.Is(err, constants.ErrURLUnreachable): h.slogger.Error("MCP server URL is unreachable", "error", err, "inputUrl", req.Url) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", strings.Split(err.Error(), ":")[0])) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, strings.Split(err.Error(), ":")[0])) return case errors.Is(err, constants.ErrMCPServerUnauthorized): h.slogger.Error("MCP server returned 401 Unauthorized", "error", err, "inputUrl", req.Url) - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(400, "Bad Request", "MCP server returned 401 Unauthorized. Check the provided credentials.")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "MCP server returned 401 Unauthorized. Check the provided credentials.")) return default: h.handleServiceError(w, err) @@ -258,27 +271,35 @@ func (h *MCPProxyHandler) handleServiceError(w http.ResponseWriter, err error) { switch { case errors.Is(err, constants.ErrHandleImmutable): h.slogger.Error("MCP handle immutability violation", "reason", err.Error()) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, err.Error())) case errors.Is(err, constants.ErrInvalidInput): h.slogger.Error("MCP request validation failed", "reason", err.Error()) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, err.Error())) case errors.Is(err, constants.ErrMCPProxyNotFound): h.slogger.Error("MCP proxy not found", "reason", err.Error()) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "MCP proxy not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeMCPProxyNotFound, "The specified MCP proxy could not be found.")) case errors.Is(err, constants.ErrMCPProxyExists): h.slogger.Error("MCP proxy conflict", "reason", err.Error()) - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", "MCP proxy with this ID already exists")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeMCPProxyExists, "An MCP proxy with this ID already exists.")) case errors.Is(err, constants.ErrProjectNotFound): h.slogger.Error("MCP request validation failed", "reason", "Project not found") - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Project not found")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeProjectNotFound, "The specified project could not be found.")) case errors.Is(err, constants.ErrMCPProxyLimitReached): h.slogger.Error("MCP proxy limit reached", "reason", err.Error()) - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", "MCP proxy limit reached for the organization")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeMCPProxyLimitReached, "MCP proxy limit reached for the organization.")) case errors.Is(err, constants.ErrSecretRefMissing): h.slogger.Error("MCP proxy secret ref missing", "reason", err.Error()) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, err.Error())) default: h.slogger.Error("MCP proxy service error", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "An unexpected error occurred")) } } diff --git a/platform-api/internal/handler/mcp_deployment.go b/platform-api/internal/handler/mcp_deployment.go index 02a30e5ea1..402ad7bf09 100644 --- a/platform-api/internal/handler/mcp_deployment.go +++ b/platform-api/internal/handler/mcp_deployment.go @@ -65,37 +65,38 @@ func (h *MCPProxyDeploymentHandler) RegisterRoutes(mux *http.ServeMux) { func (h *MCPProxyDeploymentHandler) DeployMCPProxy(w http.ResponseWriter, r *http.Request) { orgId, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } proxyId := r.PathValue("mcpProxyId") if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "MCP proxy ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "MCP proxy ID is required")) return } var req api.DeployRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, err.Error())) return } if req.Name == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "name is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeMCPProxyDeploymentValidationFailed, "name is required")) return } if req.Base == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "base is required (use 'current' or a deploymentId)")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeMCPProxyDeploymentValidationFailed, "base is required (use 'current' or a deploymentId)")) return } if strings.TrimSpace(req.GatewayId) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "gatewayId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeMCPProxyDeploymentValidationFailed, "gatewayId is required")) return } @@ -110,37 +111,37 @@ func (h *MCPProxyDeploymentHandler) DeployMCPProxy(w http.ResponseWriter, r *htt } switch { case errors.Is(err, constants.ErrMCPProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "MCP proxy not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeMCPProxyNotFound, "The specified MCP proxy could not be found.")) return case errors.Is(err, constants.ErrGatewayNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Gateway not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayNotFound, "The specified gateway could not be found.")) return case errors.Is(err, constants.ErrBaseDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Base deployment not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeDeploymentBaseNotFound, "The specified base deployment could not be found.")) return case errors.Is(err, constants.ErrDeploymentNameRequired): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Deployment name is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeMCPProxyDeploymentValidationFailed, "Deployment name is required")) return case errors.Is(err, constants.ErrDeploymentBaseRequired): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Base is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeMCPProxyDeploymentValidationFailed, "Base is required")) return case errors.Is(err, constants.ErrDeploymentGatewayIDRequired): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Gateway ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeMCPProxyDeploymentValidationFailed, "Gateway ID is required")) return case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid input")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeMCPProxyDeploymentValidationFailed, "Invalid input")) return default: h.slogger.Error("Failed to deploy MCP proxy", "proxyId", proxyId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to deploy MCP proxy")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to deploy MCP proxy")) return } } @@ -152,8 +153,8 @@ func (h *MCPProxyDeploymentHandler) DeployMCPProxy(w http.ResponseWriter, r *htt func (h *MCPProxyDeploymentHandler) UndeployMCPProxyDeployment(w http.ResponseWriter, r *http.Request) { orgId, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -161,19 +162,19 @@ func (h *MCPProxyDeploymentHandler) UndeployMCPProxyDeployment(w http.ResponseWr deploymentId := r.PathValue("deploymentId") gatewayId := r.URL.Query().Get("gatewayId") if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "deploymentId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "deploymentId is required")) return } if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "gatewayId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "gatewayId is required")) return } if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "MCP proxy ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "MCP proxy ID is required")) return } @@ -185,28 +186,29 @@ func (h *MCPProxyDeploymentHandler) UndeployMCPProxyDeployment(w http.ResponseWr } switch { case errors.Is(err, constants.ErrMCPProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "MCP proxy not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeMCPProxyNotFound, "The specified MCP proxy could not be found.")) return case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Deployment not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) return case errors.Is(err, constants.ErrGatewayNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Gateway not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayNotFound, "The specified gateway could not be found.")) return case errors.Is(err, constants.ErrDeploymentNotActive): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", - "No active deployment found for this MCP proxy on the gateway")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeDeploymentNotActive, "No active deployment found for this MCP proxy on the gateway.")) return case errors.Is(err, constants.ErrGatewayIDMismatch): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Deployment is bound to a different gateway")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeDeploymentGatewayMismatch, "Deployment is bound to a different gateway.")) return default: h.slogger.Error("Failed to undeploy MCP proxy", "proxyId", proxyId, "deploymentId", deploymentId, "gatewayId", gatewayId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to undeploy deployment")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to undeploy deployment")) return } } @@ -218,8 +220,8 @@ func (h *MCPProxyDeploymentHandler) UndeployMCPProxyDeployment(w http.ResponseWr func (h *MCPProxyDeploymentHandler) RestoreMCPProxyDeployment(w http.ResponseWriter, r *http.Request) { orgId, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -228,18 +230,18 @@ func (h *MCPProxyDeploymentHandler) RestoreMCPProxyDeployment(w http.ResponseWri gatewayId := r.URL.Query().Get("gatewayId") if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "deploymentId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "deploymentId is required")) return } if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "gatewayId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "gatewayId is required")) return } if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "MCP proxy ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "MCP proxy ID is required")) return } @@ -251,28 +253,29 @@ func (h *MCPProxyDeploymentHandler) RestoreMCPProxyDeployment(w http.ResponseWri } switch { case errors.Is(err, constants.ErrMCPProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "MCP proxy not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeMCPProxyNotFound, "The specified MCP proxy could not be found.")) return case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Deployment not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) return case errors.Is(err, constants.ErrGatewayNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Gateway not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeGatewayNotFound, "The specified gateway could not be found.")) return case errors.Is(err, constants.ErrDeploymentAlreadyDeployed): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", - "Cannot restore currently deployed deployment")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeDeploymentRestoreConflict, "Cannot restore the currently deployed deployment, or the deployment is invalid.")) return case errors.Is(err, constants.ErrGatewayIDMismatch): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Deployment is bound to a different gateway")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeDeploymentGatewayMismatch, "Deployment is bound to a different gateway.")) return default: h.slogger.Error("Failed to restore MCP proxy deployment", "proxyId", proxyId, "deploymentId", deploymentId, "gatewayId", gatewayId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to restore deployment")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to restore deployment")) return } } @@ -284,8 +287,8 @@ func (h *MCPProxyDeploymentHandler) RestoreMCPProxyDeployment(w http.ResponseWri func (h *MCPProxyDeploymentHandler) DeleteMCPProxyDeployment(w http.ResponseWriter, r *http.Request) { orgId, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -293,13 +296,13 @@ func (h *MCPProxyDeploymentHandler) DeleteMCPProxyDeployment(w http.ResponseWrit deploymentId := r.PathValue("deploymentId") if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "MCP proxy ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "MCP proxy ID is required")) return } if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Deployment ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Deployment ID is required")) return } @@ -307,20 +310,21 @@ func (h *MCPProxyDeploymentHandler) DeleteMCPProxyDeployment(w http.ResponseWrit if err != nil { switch { case errors.Is(err, constants.ErrMCPProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "MCP proxy not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeMCPProxyNotFound, "The specified MCP proxy could not be found.")) return case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Deployment not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) return case errors.Is(err, constants.ErrDeploymentIsDeployed): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", - "Cannot delete an active deployment - undeploy it first")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeDeploymentActive, "Cannot delete an active deployment - undeploy it first.")) return default: h.slogger.Error("Failed to delete MCP proxy deployment", "proxyId", proxyId, "deploymentId", deploymentId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to delete deployment")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to delete deployment")) return } } @@ -332,8 +336,8 @@ func (h *MCPProxyDeploymentHandler) DeleteMCPProxyDeployment(w http.ResponseWrit func (h *MCPProxyDeploymentHandler) GetMCPProxyDeployment(w http.ResponseWriter, r *http.Request) { orgId, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -341,13 +345,13 @@ func (h *MCPProxyDeploymentHandler) GetMCPProxyDeployment(w http.ResponseWriter, deploymentId := r.PathValue("deploymentId") if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "MCP proxy ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "MCP proxy ID is required")) return } if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Deployment ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Deployment ID is required")) return } @@ -355,17 +359,17 @@ func (h *MCPProxyDeploymentHandler) GetMCPProxyDeployment(w http.ResponseWriter, if err != nil { switch { case errors.Is(err, constants.ErrMCPProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "MCP proxy not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeMCPProxyNotFound, "The specified MCP proxy could not be found.")) return case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Deployment not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) return default: h.slogger.Error("Failed to get MCP proxy deployment", "proxyId", proxyId, "deploymentId", deploymentId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to retrieve deployment")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to retrieve deployment")) return } } @@ -377,15 +381,15 @@ func (h *MCPProxyDeploymentHandler) GetMCPProxyDeployment(w http.ResponseWriter, func (h *MCPProxyDeploymentHandler) GetMCPProxyDeployments(w http.ResponseWriter, r *http.Request) { orgId, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } proxyId := r.PathValue("mcpProxyId") if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "MCP proxy ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "MCP proxy ID is required")) return } @@ -413,17 +417,17 @@ func (h *MCPProxyDeploymentHandler) GetMCPProxyDeployments(w http.ResponseWriter if err != nil { switch { case errors.Is(err, constants.ErrMCPProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "MCP proxy not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeMCPProxyNotFound, "The specified MCP proxy could not be found.")) return case errors.Is(err, constants.ErrInvalidDeploymentStatus): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid deployment status")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeDeploymentInvalidStatus, "The specified deployment status filter is invalid.")) return default: h.slogger.Error("Failed to get MCP proxy deployments", "proxyId", proxyId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to retrieve deployments")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to retrieve deployments")) return } } diff --git a/platform-api/internal/handler/organization.go b/platform-api/internal/handler/organization.go index 36352fbe20..6898e9db43 100644 --- a/platform-api/internal/handler/organization.go +++ b/platform-api/internal/handler/organization.go @@ -53,26 +53,28 @@ func (h *OrganizationHandler) RegisterOrganization(w http.ResponseWriter, r *htt var req api.Organization if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid request body")) return } // Validate required fields if req.DisplayName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "displayName is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "displayName is required")) return } if req.Region == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Region is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Region is required")) return } // UUID is always server-generated id, genErr := utils.GenerateUUID() if genErr != nil { - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to generate organization ID")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to generate organization ID")) return } @@ -94,18 +96,18 @@ func (h *OrganizationHandler) RegisterOrganization(w http.ResponseWriter, r *htt org, err := h.orgService.RegisterOrganization(id, handle, req.DisplayName, req.Region, idpOrgRefUUID, performedBy) if err != nil { if errors.Is(err, constants.ErrHandleExists) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", - "Organization already exists")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeOrganizationExists, "An organization with this handle already exists.")) return } if errors.Is(err, constants.ErrOrganizationExists) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", - "Organization with the given ID already exists")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeOrganizationExists, "An organization with the given ID already exists.")) return } h.slogger.Error("Failed to create organization", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to create organization")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to create organization")) return } @@ -125,8 +127,8 @@ func (h *OrganizationHandler) HeadOrganization(w http.ResponseWriter, r *http.Re // to do: enable this check after finalizing authentication method // if orgID != organizationIdFromContext { - // httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponse(403, "Forbidden", - // "Organization ID in token does not match the requested organization ID")) + // httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( + // utils.CodeCommonForbidden, "Organization ID in token does not match the requested organization ID")) // return // } @@ -150,13 +152,13 @@ func (h *OrganizationHandler) GetOrganizationByID(w http.ResponseWriter, r *http org, err := h.orgService.GetOrganizationByHandle(handle) if err != nil { if errors.Is(err, constants.ErrOrganizationNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Organization not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeOrganizationNotFound, "The specified organization could not be found.")) return } h.slogger.Error("Failed to get organization by handle", "handle", handle, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to get organization")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to get organization")) return } @@ -185,8 +187,8 @@ func (h *OrganizationHandler) ListOrganizations(w http.ResponseWriter, r *http.R orgs, total, err := h.orgService.ListOrganizations(limit, offset) if err != nil { h.slogger.Error("Failed to list organizations", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to list organizations")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to list organizations")) return } diff --git a/platform-api/internal/handler/project.go b/platform-api/internal/handler/project.go index 751f1d1f5c..3a3dd12de9 100644 --- a/platform-api/internal/handler/project.go +++ b/platform-api/internal/handler/project.go @@ -49,8 +49,8 @@ func NewProjectHandler(projectService *service.ProjectService, identity *service func (h *ProjectHandler) CreateProject(w http.ResponseWriter, r *http.Request) { organizationID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -61,8 +61,8 @@ func (h *ProjectHandler) CreateProject(w http.ResponseWriter, r *http.Request) { } if req.DisplayName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Project displayName is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Project displayName is required")) return } @@ -73,23 +73,23 @@ func (h *ProjectHandler) CreateProject(w http.ResponseWriter, r *http.Request) { project, err := h.projectService.CreateProject(&req, organizationID, actor) if err != nil { if errors.Is(err, constants.ErrProjectExists) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", - "Project already exists in organization")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeProjectExists, "A project with this name already exists in the organization.")) return } if errors.Is(err, constants.ErrOrganizationNotFound) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Organization not found")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeOrganizationNotFound, "The specified organization could not be found.")) return } if errors.Is(err, constants.ErrInvalidProjectName) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Project displayName is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Project displayName is required")) return } h.slogger.Error("Failed to create project", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to create project")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to create project")) return } @@ -100,28 +100,28 @@ func (h *ProjectHandler) CreateProject(w http.ResponseWriter, r *http.Request) { func (h *ProjectHandler) GetProject(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } projectId := r.PathValue("projectId") if projectId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Project ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Project ID is required")) return } project, err := h.projectService.GetProjectByHandle(projectId, orgID) if err != nil { if errors.Is(err, constants.ErrProjectNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Project not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeProjectNotFound, "The specified project could not be found.")) return } h.slogger.Error("Failed to get project", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to get project")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to get project")) return } @@ -132,21 +132,21 @@ func (h *ProjectHandler) GetProject(w http.ResponseWriter, r *http.Request) { func (h *ProjectHandler) ListProjects(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } projects, err := h.projectService.GetProjectsByOrganization(orgID) if err != nil { if errors.Is(err, constants.ErrOrganizationNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Organization not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeOrganizationNotFound, "The specified organization could not be found.")) return } h.slogger.Error("Failed to list projects", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to get projects")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to get projects")) return } @@ -166,15 +166,15 @@ func (h *ProjectHandler) ListProjects(w http.ResponseWriter, r *http.Request) { func (h *ProjectHandler) UpdateProject(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } projectId := r.PathValue("projectId") if projectId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Project ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Project ID is required")) return } @@ -191,23 +191,23 @@ func (h *ProjectHandler) UpdateProject(w http.ResponseWriter, r *http.Request) { project, err := h.projectService.UpdateProject(projectId, &req, orgID, actor) if err != nil { if errors.Is(err, constants.ErrProjectNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Project not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeProjectNotFound, "The specified project could not be found.")) return } if errors.Is(err, constants.ErrHandleImmutable) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Project id is immutable and cannot be changed")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Project id is immutable and cannot be changed")) return } if errors.Is(err, constants.ErrProjectExists) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", - "Project already exists in organization")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeProjectExists, "A project with this name already exists in the organization.")) return } h.slogger.Error("Failed to update project", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to update project")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to update project")) return } @@ -218,15 +218,15 @@ func (h *ProjectHandler) UpdateProject(w http.ResponseWriter, r *http.Request) { func (h *ProjectHandler) DeleteProject(w http.ResponseWriter, r *http.Request) { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } projectId := r.PathValue("projectId") if projectId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Project ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Project ID is required")) return } @@ -237,33 +237,33 @@ func (h *ProjectHandler) DeleteProject(w http.ResponseWriter, r *http.Request) { err := h.projectService.DeleteProject(projectId, orgID, actor) if err != nil { if errors.Is(err, constants.ErrProjectNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "Project not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeProjectNotFound, "The specified project could not be found.")) return } if errors.Is(err, constants.ErrOrganizationMustHAveAtLeastOneProject) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Organization must have at least one project")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Organization must have at least one project")) return } if errors.Is(err, constants.ErrProjectHasAssociatedAPIs) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Project has associated APIs")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Project has associated APIs")) return } if errors.Is(err, constants.ErrProjectHasAssociatedMCPProxies) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Project has associated MCP proxies")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Project has associated MCP proxies")) return } if errors.Is(err, constants.ErrProjectHasAssociatedApplications) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Project has associated applications")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Project has associated applications")) return } h.slogger.Error("Failed to delete project", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to delete project")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to delete project")) return } diff --git a/platform-api/internal/handler/secret.go b/platform-api/internal/handler/secret.go index c2a34aade8..0e60b29369 100644 --- a/platform-api/internal/handler/secret.go +++ b/platform-api/internal/handler/secret.go @@ -56,7 +56,8 @@ func (h *SecretHandler) RegisterRoutes(mux *http.ServeMux) { func (h *SecretHandler) CreateSecret(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -76,22 +77,26 @@ func (h *SecretHandler) CreateSecret(w http.ResponseWriter, r *http.Request) { Type: r.FormValue("type"), } if req.Handle == "" || req.DisplayName == "" || req.Value == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "id, displayName and value are required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "id, displayName and value are required")) return } resp, err := h.secretService.Create(orgID, userID, &req) if err != nil { if errors.Is(err, constants.ErrSecretAlreadyExists) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", "A secret with this name already exists in this scope")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeCommonConflict, "A secret with this name already exists in this scope")) return } if errors.Is(err, constants.ErrInvalidSecretType) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, err.Error())) return } h.slogger.Error("failed to create secret", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to create secret")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to create secret")) return } @@ -101,7 +106,8 @@ func (h *SecretHandler) CreateSecret(w http.ResponseWriter, r *http.Request) { func (h *SecretHandler) ListSecrets(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -125,7 +131,8 @@ func (h *SecretHandler) ListSecrets(w http.ResponseWriter, r *http.Request) { if ua := r.URL.Query().Get("updatedAfter"); ua != "" { t, err := time.Parse(time.RFC3339, ua) if err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "updatedAfter must be an RFC3339 timestamp")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "updatedAfter must be an RFC3339 timestamp")) return } updatedAfter = &t @@ -134,7 +141,8 @@ func (h *SecretHandler) ListSecrets(w http.ResponseWriter, r *http.Request) { resp, err := h.secretService.List(orgID, limit, offset, updatedAfter) if err != nil { h.slogger.Error("failed to list secrets", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to list secrets")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to list secrets")) return } @@ -144,24 +152,28 @@ func (h *SecretHandler) ListSecrets(w http.ResponseWriter, r *http.Request) { func (h *SecretHandler) GetSecret(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } handle := r.PathValue("secretId") if handle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Secret name is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Secret name is required")) return } summary, err := h.secretService.Get(orgID, handle) if err != nil { if errors.Is(err, constants.ErrSecretNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Secret not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeCommonNotFound, "Secret not found")) return } h.slogger.Error("failed to get secret", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get secret")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to get secret")) return } @@ -171,13 +183,15 @@ func (h *SecretHandler) GetSecret(w http.ResponseWriter, r *http.Request) { func (h *SecretHandler) UpdateSecret(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } handle := r.PathValue("secretId") if handle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Secret name is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Secret name is required")) return } @@ -195,18 +209,21 @@ func (h *SecretHandler) UpdateSecret(w http.ResponseWriter, r *http.Request) { Value: r.FormValue("value"), } if req.Value == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "value is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "value is required")) return } resp, err := h.secretService.Update(orgID, handle, userID, &req) if err != nil { if errors.Is(err, constants.ErrSecretNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Secret not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeCommonNotFound, "Secret not found")) return } h.slogger.Error("failed to update secret", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to update secret")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to update secret")) return } @@ -216,13 +233,15 @@ func (h *SecretHandler) UpdateSecret(w http.ResponseWriter, r *http.Request) { func (h *SecretHandler) DeleteSecret(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } handle := r.PathValue("secretId") if handle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Secret name is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Secret name is required")) return } @@ -234,7 +253,8 @@ func (h *SecretHandler) DeleteSecret(w http.ResponseWriter, r *http.Request) { err := h.secretService.Delete(orgID, handle, userID) if err != nil { if errors.Is(err, constants.ErrSecretNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Secret not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeCommonNotFound, "Secret not found")) return } @@ -244,15 +264,15 @@ func (h *SecretHandler) DeleteSecret(w http.ResponseWriter, r *http.Request) { for _, ref := range inUseErr.References { refs = append(refs, dto.SecretReferenceDTO{Type: ref.Type, Handle: ref.Handle, Name: ref.Name}) } - httputil.WriteJSON(w, http.StatusConflict, dto.SecretDeleteConflictResponse{ - Error: "secret is referenced by active resources", - References: refs, - }) + resp := utils.NewErrorResponseWithCode(utils.CodeSecretInUse, "The secret is referenced by one or more active resources.") + resp.Details = dto.SecretInUseDetails{References: refs} + httputil.WriteJSON(w, http.StatusConflict, resp) return } h.slogger.Error("failed to delete secret", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to delete secret")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to delete secret")) return } diff --git a/platform-api/internal/handler/secret_integration_test.go b/platform-api/internal/handler/secret_integration_test.go index 9df09b9dfe..50f5b15338 100644 --- a/platform-api/internal/handler/secret_integration_test.go +++ b/platform-api/internal/handler/secret_integration_test.go @@ -510,9 +510,16 @@ func TestSecretHandler_Delete_409_ReferencedByArtifact(t *testing.T) { } resp := parseBody(wDel) - refs, ok := resp["references"].([]interface{}) + if resp["code"] != "SECRET_IN_USE" { + t.Errorf("expected code SECRET_IN_USE, got: %v", resp) + } + details, ok := resp["details"].(map[string]interface{}) + if !ok { + t.Fatalf("expected details object, got: %v", resp) + } + refs, ok := details["references"].([]interface{}) if !ok || len(refs) == 0 { - t.Errorf("expected non-empty references array, got: %v", resp) + t.Errorf("expected non-empty details.references array, got: %v", resp) } } diff --git a/platform-api/internal/handler/subscription_handler.go b/platform-api/internal/handler/subscription_handler.go index 21714ca896..5fa3af684b 100644 --- a/platform-api/internal/handler/subscription_handler.go +++ b/platform-api/internal/handler/subscription_handler.go @@ -71,36 +71,42 @@ func (h *SubscriptionHandler) CreateSubscription(w http.ResponseWriter, r *http. orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { h.slogger.Error("Organization claim not found in token when creating subscription") - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } var req CreateSubscriptionRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.slogger.Error("Invalid create subscription request body", "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid request body")) return } if req.APIID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "API ID is required")) return } if req.SubscriberID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "subscriberId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "subscriberId is required")) return } if req.Kind == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "kind is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "kind is required")) return } if !constants.ValidArtifactKinds[req.Kind] { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid kind value")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid kind value")) return } switch req.Status { case "", "ACTIVE", "INACTIVE", "REVOKED": default: - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid status value")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid status value")) return } actor, ok := resolveActor(w, r, h.identity, h.slogger, "create subscription") @@ -110,21 +116,25 @@ func (h *SubscriptionHandler) CreateSubscription(w http.ResponseWriter, r *http. sub, err := h.subscriptionService.CreateSubscription(req.APIID, req.Kind, orgId, req.SubscriberID, req.ApplicationID, req.SubscriptionPlanID, "", req.Status, actor) if err != nil { if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "API not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) return } if errors.Is(err, constants.ErrSubscriptionAlreadyExists) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", "Subscription already exists for this API")) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeSubscriptionExists, "A subscription already exists for this API.")) return } h.slogger.Error("Failed to create subscription", "apiId", req.APIID, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to create subscription")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to create subscription")) return } resp, err := h.toSubscriptionResponse(sub, orgId) if err != nil { h.slogger.Error("Failed to resolve subscription identity", "apiId", req.APIID, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to create subscription")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to create subscription")) return } httputil.WriteJSON(w, http.StatusCreated, resp) @@ -139,8 +149,8 @@ func (h *SubscriptionHandler) ListSubscriptions(w http.ResponseWriter, r *http.R orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -158,7 +168,8 @@ func (h *SubscriptionHandler) ListSubscriptions(w http.ResponseWriter, r *http.R switch status { case "ACTIVE", "INACTIVE", "REVOKED": default: - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid status value")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid status value")) return } statusPtr = &status @@ -189,11 +200,13 @@ func (h *SubscriptionHandler) ListSubscriptions(w http.ResponseWriter, r *http.R list, total, err := h.subscriptionService.ListSubscriptionsByFilters(orgId, apiIDPtr, subscriberIDPtr, appIDPtr, statusPtr, limit, offset) if err != nil { if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "API not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) return } h.slogger.Error("Failed to list subscriptions", "apiId", apiId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to list subscriptions")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to list subscriptions")) return } // Bulk fetch API handles and plan names to avoid N+1 queries @@ -218,13 +231,15 @@ func (h *SubscriptionHandler) ListSubscriptions(w http.ResponseWriter, r *http.R artifactMetaMap, err := h.subscriptionService.GetArtifactMetadataMap(apiUUIDs, orgId) if err != nil { h.slogger.Error("Failed to bulk fetch artifact metadata for list", "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to list subscriptions")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to list subscriptions")) return } planNameMap, err := h.subscriptionPlanService.GetPlanNameMap(planIDs, orgId) if err != nil { h.slogger.Error("Failed to bulk fetch plan names for list", "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to list subscriptions")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to list subscriptions")) return } // Bulk-resolve createdBy UUIDs to their raw identity to avoid N+1 lookups. @@ -235,7 +250,8 @@ func (h *SubscriptionHandler) ListSubscriptions(w http.ResponseWriter, r *http.R createdByMap, err := h.identity.SubsForUUIDs(createdByUUIDs) if err != nil { h.slogger.Error("Failed to resolve subscription creator identities", "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to list subscriptions")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to list subscriptions")) return } items := make([]map[string]any, 0, len(list)) @@ -257,29 +273,33 @@ func (h *SubscriptionHandler) ListSubscriptions(w http.ResponseWriter, r *http.R func (h *SubscriptionHandler) GetSubscription(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } subscriptionId := r.PathValue("subscriptionId") if subscriptionId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Subscription ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Subscription ID is required")) return } sub, err := h.subscriptionService.GetSubscription(subscriptionId, orgId) if err != nil { if errors.Is(err, constants.ErrSubscriptionNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Subscription not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeSubscriptionNotFound, "The specified subscription could not be found.")) return } h.slogger.Error("Failed to get subscription", "subscriptionId", subscriptionId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get subscription")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to get subscription")) return } resp, err := h.toSubscriptionResponse(sub, orgId) if err != nil { h.slogger.Error("Failed to resolve subscription identity", "subscriptionId", subscriptionId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get subscription")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to get subscription")) return } httputil.WriteJSON(w, http.StatusOK, resp) @@ -289,18 +309,20 @@ func (h *SubscriptionHandler) GetSubscription(w http.ResponseWriter, r *http.Req func (h *SubscriptionHandler) UpdateSubscription(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } subscriptionId := r.PathValue("subscriptionId") if subscriptionId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Subscription ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Subscription ID is required")) return } var req api.Subscription if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid request body")) return } var status string @@ -310,7 +332,8 @@ func (h *SubscriptionHandler) UpdateSubscription(w http.ResponseWriter, r *http. switch status { case "", "ACTIVE", "INACTIVE", "REVOKED": default: - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid subscription status")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid subscription status")) return } subscriberID, ok := requireSubscriptionSubscriberQuery(w, r) @@ -324,21 +347,25 @@ func (h *SubscriptionHandler) UpdateSubscription(w http.ResponseWriter, r *http. sub, err := h.subscriptionService.UpdateSubscription(subscriptionId, orgId, subscriberID, status, actor) if err != nil { if errors.Is(err, constants.ErrSubscriptionNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Subscription not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeSubscriptionNotFound, "The specified subscription could not be found.")) return } if errors.Is(err, constants.ErrSubscriptionSubscriberMismatch) { - httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponse(403, "Forbidden", "subscriberId does not match this subscription")) + httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( + utils.CodeCommonForbidden, "subscriberId does not match this subscription")) return } h.slogger.Error("Failed to update subscription", "subscriptionId", subscriptionId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to update subscription")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to update subscription")) return } resp, err := h.toSubscriptionResponse(sub, orgId) if err != nil { h.slogger.Error("Failed to resolve subscription identity", "subscriptionId", subscriptionId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to update subscription")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to update subscription")) return } httputil.WriteJSON(w, http.StatusOK, resp) @@ -348,13 +375,14 @@ func (h *SubscriptionHandler) UpdateSubscription(w http.ResponseWriter, r *http. func (h *SubscriptionHandler) DeleteSubscription(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } subscriptionId := r.PathValue("subscriptionId") if subscriptionId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Subscription ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Subscription ID is required")) return } subscriberID, ok := requireSubscriptionSubscriberQuery(w, r) @@ -368,15 +396,18 @@ func (h *SubscriptionHandler) DeleteSubscription(w http.ResponseWriter, r *http. err := h.subscriptionService.DeleteSubscription(subscriptionId, orgId, subscriberID, actor) if err != nil { if errors.Is(err, constants.ErrSubscriptionNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Subscription not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeSubscriptionNotFound, "The specified subscription could not be found.")) return } if errors.Is(err, constants.ErrSubscriptionSubscriberMismatch) { - httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponse(403, "Forbidden", "subscriberId does not match this subscription")) + httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( + utils.CodeCommonForbidden, "subscriberId does not match this subscription")) return } h.slogger.Error("Failed to delete subscription", "subscriptionId", subscriptionId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to delete subscription")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to delete subscription")) return } w.WriteHeader(http.StatusNoContent) @@ -385,7 +416,8 @@ func (h *SubscriptionHandler) DeleteSubscription(w http.ResponseWriter, r *http. func requireSubscriptionSubscriberQuery(w http.ResponseWriter, r *http.Request) (string, bool) { q := strings.TrimSpace(r.URL.Query().Get("subscriberId")) if q == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "subscriberId query parameter is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "subscriberId query parameter is required")) return "", false } return q, true diff --git a/platform-api/internal/handler/subscription_plan_handler.go b/platform-api/internal/handler/subscription_plan_handler.go index 5f3f17cfe7..360760d217 100644 --- a/platform-api/internal/handler/subscription_plan_handler.go +++ b/platform-api/internal/handler/subscription_plan_handler.go @@ -144,14 +144,16 @@ type CreateSubscriptionPlanRequest struct { func (h *SubscriptionPlanHandler) CreateSubscriptionPlan(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } var req CreateSubscriptionPlanRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.slogger.Error("Invalid create subscription plan request body", "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid request body")) return } @@ -159,7 +161,8 @@ func (h *SubscriptionPlanHandler) CreateSubscriptionPlan(w http.ResponseWriter, switch req.Status { case "ACTIVE", "INACTIVE": default: - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid status value; must be ACTIVE or INACTIVE")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid status value; must be ACTIVE or INACTIVE")) return } } @@ -172,7 +175,8 @@ func (h *SubscriptionPlanHandler) CreateSubscriptionPlan(w http.ResponseWriter, } if limit := firstLimit(req.Limits); limit != nil { if errMsg := normalizeAndValidateLimit(limit); errMsg != "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", errMsg)) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, errMsg)) return } count := limit.LimitCount @@ -185,7 +189,8 @@ func (h *SubscriptionPlanHandler) CreateSubscriptionPlan(w http.ResponseWriter, if req.ExpiryTime != nil { t, err := time.Parse(time.RFC3339, *req.ExpiryTime) if err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid expiryTime format; use RFC3339")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid expiryTime format; use RFC3339")) return } plan.ExpiryTime = &t @@ -193,29 +198,34 @@ func (h *SubscriptionPlanHandler) CreateSubscriptionPlan(w http.ResponseWriter, rawActor, ok := middleware.GetActorIdentityFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "User ID claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "User ID claim not found in token")) return } actor, err := h.identity.ToInternalUUID(rawActor) if err != nil { h.slogger.Error("Failed to resolve user identity", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to resolve user identity")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to resolve user identity")) return } created, err := h.planService.CreatePlan(orgId, actor, plan) if err != nil { if errors.Is(err, constants.ErrSubscriptionPlanAlreadyExists) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", err.Error())) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeSubscriptionPlanExists, "A subscription plan with this name already exists for the organization.")) return } h.slogger.Error("Failed to create subscription plan", "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to create subscription plan")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to create subscription plan")) return } resp, err := h.toSubscriptionPlanResponse(created, true) if err != nil { h.slogger.Error("Failed to resolve subscription plan identity", "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to create subscription plan")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to create subscription plan")) return } httputil.WriteJSON(w, http.StatusCreated, resp) @@ -225,7 +235,8 @@ func (h *SubscriptionPlanHandler) CreateSubscriptionPlan(w http.ResponseWriter, func (h *SubscriptionPlanHandler) ListSubscriptionPlans(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } @@ -256,7 +267,8 @@ func (h *SubscriptionPlanHandler) ListSubscriptionPlans(w http.ResponseWriter, r list, err := h.planService.ListPlans(orgId, limit, offset) if err != nil { h.slogger.Error("Failed to list subscription plans", "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to list subscription plans")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to list subscription plans")) return } items := make([]map[string]any, 0, len(list)) @@ -264,7 +276,8 @@ func (h *SubscriptionPlanHandler) ListSubscriptionPlans(w http.ResponseWriter, r item, err := h.toSubscriptionPlanResponse(p, false) if err != nil { h.slogger.Error("Failed to resolve subscription plan identity", "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to list subscription plans")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to list subscription plans")) return } items = append(items, item) @@ -276,30 +289,35 @@ func (h *SubscriptionPlanHandler) ListSubscriptionPlans(w http.ResponseWriter, r func (h *SubscriptionPlanHandler) GetSubscriptionPlan(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } planId := r.PathValue("subscriptionPlanId") if planId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Plan ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Plan ID is required")) return } plan, err := h.planService.GetPlan(planId, orgId) if err != nil { if errors.Is(err, constants.ErrSubscriptionPlanNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Subscription plan not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeSubscriptionPlanNotFound, "The specified subscription plan could not be found.")) return } h.slogger.Error("Failed to get subscription plan", "planId", planId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get subscription plan")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to get subscription plan")) return } resp, err := h.toSubscriptionPlanResponse(plan, true) if err != nil { h.slogger.Error("Failed to resolve subscription plan identity", "planId", planId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get subscription plan")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to get subscription plan")) return } httputil.WriteJSON(w, http.StatusOK, resp) @@ -309,32 +327,36 @@ func (h *SubscriptionPlanHandler) GetSubscriptionPlan(w http.ResponseWriter, r * func (h *SubscriptionPlanHandler) UpdateSubscriptionPlan(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } planId := r.PathValue("subscriptionPlanId") if planId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Plan ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Plan ID is required")) return } var req api.SubscriptionPlan if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.slogger.Error("Invalid update subscription plan request body", "planId", planId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid request body")) return } if req.Id != nil && *req.Id != "" && *req.Id != planId { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "The plan id is immutable and cannot be changed")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "The plan id is immutable and cannot be changed")) return } displayName := req.DisplayName if displayName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "displayName is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "displayName is required")) return } update := &model.SubscriptionPlanUpdate{ @@ -344,7 +366,8 @@ func (h *SubscriptionPlanHandler) UpdateSubscriptionPlan(w http.ResponseWriter, if req.Limits != nil { if limit := firstLimit(apiLimitsToRequests(*req.Limits)); limit != nil { if errMsg := normalizeAndValidateLimit(limit); errMsg != "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", errMsg)) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, errMsg)) return } count := limit.LimitCount @@ -365,45 +388,52 @@ func (h *SubscriptionPlanHandler) UpdateSubscriptionPlan(w http.ResponseWriter, st := model.SubscriptionPlanStatus(*req.Status) update.Status = &st default: - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid status value; must be ACTIVE or INACTIVE")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Invalid status value; must be ACTIVE or INACTIVE")) return } } rawActor, ok := middleware.GetActorIdentityFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "User ID claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "User ID claim not found in token")) return } actor, err := h.identity.ToInternalUUID(rawActor) if err != nil { h.slogger.Error("Failed to resolve user identity", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to resolve user identity")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to resolve user identity")) return } updated, err := h.planService.UpdatePlan(planId, orgId, actor, update) if err != nil { if errors.Is(err, constants.ErrHandleImmutable) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "The plan id is immutable and cannot be changed")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "The plan id is immutable and cannot be changed")) return } if errors.Is(err, constants.ErrSubscriptionPlanNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Subscription plan not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeSubscriptionPlanNotFound, "The specified subscription plan could not be found.")) return } if errors.Is(err, constants.ErrSubscriptionPlanAlreadyExists) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", err.Error())) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( + utils.CodeSubscriptionPlanExists, "A subscription plan with this name already exists for the organization.")) return } h.slogger.Error("Failed to update subscription plan", "planId", planId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to update subscription plan")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to update subscription plan")) return } resp, err := h.toSubscriptionPlanResponse(updated, true) if err != nil { h.slogger.Error("Failed to resolve subscription plan identity", "planId", planId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to update subscription plan")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to update subscription plan")) return } httputil.WriteJSON(w, http.StatusOK, resp) @@ -413,35 +443,41 @@ func (h *SubscriptionPlanHandler) UpdateSubscriptionPlan(w http.ResponseWriter, func (h *SubscriptionPlanHandler) DeleteSubscriptionPlan(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "Organization claim not found in token")) return } planId := r.PathValue("subscriptionPlanId") if planId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Plan ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( + utils.CodeCommonValidationFailed, "Plan ID is required")) return } rawActor, ok := middleware.GetActorIdentityFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "User ID claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( + utils.CodeCommonUnauthorized, "User ID claim not found in token")) return } actor, err := h.identity.ToInternalUUID(rawActor) if err != nil { h.slogger.Error("Failed to resolve user identity", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to resolve user identity")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to resolve user identity")) return } err = h.planService.DeletePlan(planId, orgId, actor) if err != nil { if errors.Is(err, constants.ErrSubscriptionPlanNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Subscription plan not found")) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( + utils.CodeSubscriptionPlanNotFound, "The specified subscription plan could not be found.")) return } h.slogger.Error("Failed to delete subscription plan", "planId", planId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to delete subscription plan")) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( + utils.CodeCommonInternalError, "Failed to delete subscription plan")) return } w.WriteHeader(http.StatusNoContent) diff --git a/platform-api/internal/middleware/auth.go b/platform-api/internal/middleware/auth.go index b4d5f367e9..58c055fe80 100644 --- a/platform-api/internal/middleware/auth.go +++ b/platform-api/internal/middleware/auth.go @@ -21,10 +21,12 @@ import ( "context" "encoding/json" "fmt" + "log/slog" "net/http" "strings" "github.com/wso2/api-platform/common/authenticators" + "github.com/wso2/api-platform/platform-api/internal/utils" "github.com/golang-jwt/jwt/v5" ) @@ -83,11 +85,12 @@ type PlatformClaimNames struct { RoleScopeMap map[string][]string } -// writeJSONError is a helper to write a JSON error body without depending on httputil. +// writeJSONError is a helper to write a standard-shape JSON error body +// ({status, code, message}) without depending on httputil. func writeJSONError(w http.ResponseWriter, status int, msg string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) - _ = json.NewEncoder(w).Encode(map[string]string{"error": msg}) + _ = json.NewEncoder(w).Encode(utils.NewErrorResponse(status, http.StatusText(status), msg)) } // LocalJWTAuthMiddleware returns a middleware for locally-issued JWT validation. @@ -116,7 +119,8 @@ func LocalJWTAuthMiddleware(config AuthConfig) func(http.Handler) http.Handler { enriched, err := validateLocalJWT(r, tokenString, config) if err != nil { - writeJSONError(w, http.StatusUnauthorized, err.Error()) + slog.Warn("local JWT validation failed", "reason", err.Error()) + writeJSONError(w, http.StatusUnauthorized, "Invalid or expired credentials.") return } diff --git a/platform-api/internal/utils/codes.go b/platform-api/internal/utils/codes.go new file mode 100644 index 0000000000..d44d7c33d7 --- /dev/null +++ b/platform-api/internal/utils/codes.go @@ -0,0 +1,161 @@ +/* + * 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 utils + +import "net/http" + +// Common, domain-agnostic catalog codes. These mirror the COMMON_* codes +// documented as shared response examples in resources/openapi.yaml, and are +// the fallback used by NewErrorResponse when a handler hasn't been migrated +// to a more specific domain code via NewErrorResponseWithCode. +const ( + CodeCommonValidationFailed = "COMMON_VALIDATION_FAILED" + CodeCommonUnauthorized = "COMMON_UNAUTHORIZED" + CodeCommonForbidden = "COMMON_FORBIDDEN" + CodeCommonNotFound = "COMMON_NOT_FOUND" + CodeCommonConflict = "COMMON_CONFLICT" + CodeCommonNotAcceptable = "COMMON_NOT_ACCEPTABLE" + CodeCommonUnprocessableEntity = "COMMON_UNPROCESSABLE_ENTITY" + CodeCommonInternalError = "COMMON_INTERNAL_ERROR" + CodeCommonServiceUnavailable = "COMMON_SERVICE_UNAVAILABLE" +) + +// commonCodeByStatus maps an HTTP status code to its fallback catalog code. +// 400 maps to COMMON_VALIDATION_FAILED (not a separate COMMON_BAD_REQUEST) to +// match the single code documented for the shared BadRequest response in +// resources/openapi.yaml. +var commonCodeByStatus = map[int]string{ + http.StatusBadRequest: CodeCommonValidationFailed, + http.StatusUnauthorized: CodeCommonUnauthorized, + http.StatusForbidden: CodeCommonForbidden, + http.StatusNotFound: CodeCommonNotFound, + http.StatusConflict: CodeCommonConflict, + http.StatusNotAcceptable: CodeCommonNotAcceptable, + http.StatusUnprocessableEntity: CodeCommonUnprocessableEntity, + http.StatusInternalServerError: CodeCommonInternalError, + http.StatusServiceUnavailable: CodeCommonServiceUnavailable, +} + +// codeForStatus returns the fallback catalog code for an HTTP status, +// defaulting to COMMON_INTERNAL_ERROR for unmapped statuses. +func codeForStatus(httpStatus int) string { + if code, ok := commonCodeByStatus[httpStatus]; ok { + return code + } + return CodeCommonInternalError +} + +// LLM provider/proxy domain codes, matching the examples documented in +// resources/openapi.yaml for the corresponding operations. +const ( + CodeLLMProviderNotFound = "LLM_PROVIDER_NOT_FOUND" + CodeLLMProviderExists = "LLM_PROVIDER_EXISTS" + CodeLLMProviderLimitReached = "LLM_PROVIDER_LIMIT_REACHED" + CodeLLMProviderAPIKeyNotFound = "LLM_PROVIDER_API_KEY_NOT_FOUND" + CodeLLMProviderDeploymentValidationFailed = "LLM_PROVIDER_DEPLOYMENT_VALIDATION_FAILED" + CodeLLMProxyNotFound = "LLM_PROXY_NOT_FOUND" + CodeLLMProxyExists = "LLM_PROXY_EXISTS" + CodeLLMProxyLimitReached = "LLM_PROXY_LIMIT_REACHED" + CodeLLMProxyAPIKeyNotFound = "LLM_PROXY_API_KEY_NOT_FOUND" + CodeLLMProxyDeploymentValidationFailed = "LLM_PROXY_DEPLOYMENT_VALIDATION_FAILED" +) + +// LLM provider template domain codes. +const ( + CodeLLMProviderTemplateNotFound = "LLM_PROVIDER_TEMPLATE_NOT_FOUND" + CodeLLMProviderTemplateExists = "LLM_PROVIDER_TEMPLATE_EXISTS" + CodeLLMProviderTemplateManagedByReserved = "LLM_PROVIDER_TEMPLATE_MANAGED_BY_RESERVED" + CodeLLMProviderTemplateInUse = "LLM_PROVIDER_TEMPLATE_IN_USE" + CodeLLMProviderTemplateReadOnly = "LLM_PROVIDER_TEMPLATE_READ_ONLY" + CodeLLMProviderTemplateNotToggleable = "LLM_PROVIDER_TEMPLATE_NOT_TOGGLEABLE" +) + +// Gateway domain codes, matching the examples documented in resources/openapi.yaml. +const ( + CodeGatewayNotFound = "GATEWAY_NOT_FOUND" + CodeGatewayTokenNotFound = "GATEWAY_TOKEN_NOT_FOUND" + CodeGatewayConnectionUnavailable = "GATEWAY_CONNECTION_UNAVAILABLE" + CodeGatewayHasActiveDeployments = "GATEWAY_HAS_ACTIVE_DEPLOYMENTS" + CodeGatewayNameConflict = "GATEWAY_NAME_CONFLICT" + CodeGatewayTokenLimitReached = "GATEWAY_TOKEN_LIMIT_REACHED" +) + +// Deployment domain codes, shared across REST API / LLM provider / LLM proxy / +// MCP proxy deployment operations (identical conditions across all four). +const ( + CodeDeploymentBaseNotFound = "DEPLOYMENT_BASE_NOT_FOUND" + CodeDeploymentRestoreConflict = "DEPLOYMENT_RESTORE_CONFLICT" + CodeDeploymentNotFound = "DEPLOYMENT_NOT_FOUND" + CodeDeploymentNotActive = "DEPLOYMENT_NOT_ACTIVE" + CodeDeploymentGatewayMismatch = "DEPLOYMENT_GATEWAY_MISMATCH" + CodeDeploymentActive = "DEPLOYMENT_ACTIVE" + CodeDeploymentInvalidStatus = "DEPLOYMENT_INVALID_STATUS" +) + +// REST API domain codes. +const ( + CodeRESTAPINotFound = "REST_API_NOT_FOUND" + CodeRESTAPIExists = "REST_API_EXISTS" + CodeRESTAPIDeploymentValidationFailed = "REST_API_DEPLOYMENT_VALIDATION_FAILED" +) + +// MCP proxy domain codes. +const ( + CodeMCPProxyNotFound = "MCP_PROXY_NOT_FOUND" + CodeMCPProxyExists = "MCP_PROXY_EXISTS" + CodeMCPProxyLimitReached = "MCP_PROXY_LIMIT_REACHED" + CodeMCPProxyDeploymentValidationFailed = "MCP_PROXY_DEPLOYMENT_VALIDATION_FAILED" +) + +// Organization domain codes. +const ( + CodeOrganizationNotFound = "ORGANIZATION_NOT_FOUND" + CodeOrganizationExists = "ORGANIZATION_EXISTS" +) + +// Project domain codes. +const ( + CodeProjectNotFound = "PROJECT_NOT_FOUND" + CodeProjectExists = "PROJECT_EXISTS" +) + +// Application domain codes. +const ( + CodeApplicationNotFound = "APPLICATION_NOT_FOUND" + CodeApplicationExists = "APPLICATION_EXISTS" +) + +// Subscription domain codes. +const ( + CodeSubscriptionNotFound = "SUBSCRIPTION_NOT_FOUND" + CodeSubscriptionExists = "SUBSCRIPTION_EXISTS" + CodeSubscriptionPlanNotFound = "SUBSCRIPTION_PLAN_NOT_FOUND" + CodeSubscriptionPlanExists = "SUBSCRIPTION_PLAN_EXISTS" +) + +// Custom policy domain codes. +const ( + CodePolicyVersionConflict = "POLICY_VERSION_CONFLICT" + CodePolicyInvalidState = "POLICY_INVALID_STATE" + CodePolicyInUse = "POLICY_IN_USE" +) + +// Secret domain codes. +const ( + CodeSecretInUse = "SECRET_IN_USE" +) diff --git a/platform-api/internal/utils/error.go b/platform-api/internal/utils/error.go index 5beaf1611e..b716b818b6 100644 --- a/platform-api/internal/utils/error.go +++ b/platform-api/internal/utils/error.go @@ -27,23 +27,50 @@ import ( "github.com/go-playground/validator/v10" ) -// ErrorResponse represents the standard error response format +// ErrorResponse is the standard error response shape mandated by APR-004: +// { status, code, message, errors[], details }. status is always "error"; +// code is a stable, machine-readable string from the error catalog (see +// codes.go). Details carries optional structured metadata whose shape is +// specific to the code (e.g. the resources referencing a secret that +// blocked its deletion) — it is not a substitute for errors[], which is +// reserved for field-level validation failures. type ErrorResponse struct { - Code int `json:"code"` - Message string `json:"message"` - Description string `json:"description,omitempty"` + Status string `json:"status"` + Code string `json:"code"` + Message string `json:"message"` + Errors []FieldError `json:"errors,omitempty"` + Details any `json:"details,omitempty"` } -// NewErrorResponse creates a new error response -func NewErrorResponse(code int, message string, description ...string) ErrorResponse { - resp := ErrorResponse{ +// FieldError describes a single field-level validation failure. +type FieldError struct { + Field string `json:"field"` + Message string `json:"message"` +} + +// NewErrorResponse builds a standard ErrorResponse for the given HTTP status. +// title is retained for call-site compatibility but is no longer surfaced in +// the response body; the catalog code is derived from httpStatus (see +// codeForStatus in codes.go), and description[0], when given, becomes the +// human-readable message. Handlers that need a specific catalog code should +// use NewErrorResponseWithCode instead. +func NewErrorResponse(httpStatus int, title string, description ...string) ErrorResponse { + message := title + if len(description) > 0 && description[0] != "" { + message = description[0] + } + return NewErrorResponseWithCode(codeForStatus(httpStatus), message) +} + +// NewErrorResponseWithCode builds a standard ErrorResponse using an explicit +// catalog code (see codes.go), for handlers that know the precise failure +// reason rather than just the HTTP status. +func NewErrorResponseWithCode(code, message string) ErrorResponse { + return ErrorResponse{ + Status: "error", Code: code, Message: message, } - if len(description) > 0 { - resp.Description = description[0] - } - return resp } // NewValidationErrorResponse writes a 400 JSON error response for validation errors. @@ -51,28 +78,27 @@ func NewValidationErrorResponse(w http.ResponseWriter, err error) { w.Header().Set("Content-Type", "application/json") var ve validator.ValidationErrors if errors.As(err, &ve) { - errorsList := make([]map[string]string, 0, len(ve)) + fieldErrors := make([]FieldError, 0, len(ve)) for _, fe := range ve { - errorsList = append(errorsList, map[string]string{ - "field": fe.Field(), - "reason": fe.Tag(), - "message": fmt.Sprintf("The field %s is %s", fe.Field(), fe.Tag()), + fieldErrors = append(fieldErrors, FieldError{ + Field: fe.Field(), + Message: fmt.Sprintf("The field %s is %s", fe.Field(), fe.Tag()), }) } w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]any{ - "code": 400, - "title": "Bad Request", - "details": "Validation failed for the request parameters", - "errors": errorsList, + _ = json.NewEncoder(w).Encode(ErrorResponse{ + Status: "error", + Code: CodeCommonValidationFailed, + Message: "Validation failed for the request parameters.", + Errors: fieldErrors, }) return } log.Printf("[ERROR] Request validation fallback error: %v", err) w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]any{ - "code": 400, - "title": "Bad Request", - "details": "Invalid input", + _ = json.NewEncoder(w).Encode(ErrorResponse{ + Status: "error", + Code: CodeCommonValidationFailed, + Message: "Invalid input.", }) } From 60d4c81aff8433e93c149c2148da9f2e32ddc570 Mon Sep 17 00:00:00 2001 From: Thushani Jayasekera Date: Tue, 7 Jul 2026 15:59:24 +0530 Subject: [PATCH 02/12] Update error handling in API key and application handlers to use new standardized error codes --- platform-api/internal/handler/api_key.go | 6 +- platform-api/internal/handler/application.go | 6 +- .../handler/artifact_guard_response.go | 6 +- platform-api/internal/handler/gateway.go | 10 +- platform-api/internal/handler/llm_apikey.go | 8 +- .../internal/handler/llm_proxy_apikey.go | 8 +- platform-api/internal/handler/secret.go | 8 +- .../internal/handler/subscription_handler.go | 8 +- platform-api/internal/utils/codes.go | 34 +- platform-api/resources/openapi.yaml | 368 +++++++----------- 10 files changed, 203 insertions(+), 259 deletions(-) diff --git a/platform-api/internal/handler/api_key.go b/platform-api/internal/handler/api_key.go index 526826c3bb..329b4da715 100644 --- a/platform-api/internal/handler/api_key.go +++ b/platform-api/internal/handler/api_key.go @@ -109,7 +109,7 @@ func (h *APIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Request) { // Handle specific error cases if errors.Is(err, constants.ErrAPINotFound) { httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) + utils.CodeArtifactNotFound, "The specified artifact could not be found.")) return } if errors.Is(err, constants.ErrGatewayUnavailable) { @@ -203,7 +203,7 @@ func (h *APIKeyHandler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) { // Handle specific error cases if errors.Is(err, constants.ErrAPINotFound) { httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) + utils.CodeArtifactNotFound, "The specified artifact could not be found.")) return } if errors.Is(err, constants.ErrGatewayUnavailable) { @@ -265,7 +265,7 @@ func (h *APIKeyHandler) RevokeAPIKey(w http.ResponseWriter, r *http.Request) { // Handle specific error cases if errors.Is(err, constants.ErrAPINotFound) { httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) + utils.CodeArtifactNotFound, "The specified artifact could not be found.")) return } if errors.Is(err, constants.ErrGatewayUnavailable) { diff --git a/platform-api/internal/handler/application.go b/platform-api/internal/handler/application.go index c94782ee94..e76cfdc631 100644 --- a/platform-api/internal/handler/application.go +++ b/platform-api/internal/handler/application.go @@ -544,10 +544,10 @@ func (h *ApplicationHandler) writeApplicationError(w http.ResponseWriter, r *htt utils.CodeApplicationExists, "An application with this handle already exists in the organization.")) case errors.Is(err, constants.ErrAPIKeyNotFound): httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeCommonNotFound, "API key not found")) + utils.CodeApplicationAPIKeyNotFound, "API key not found")) case errors.Is(err, constants.ErrAPIKeyForbidden): httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( - utils.CodeCommonForbidden, "Only the key creator can perform this action")) + utils.CodeApplicationAPIKeyForbidden, "Only the key creator can perform this action")) case errors.Is(err, constants.ErrInvalidApplicationName): httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( utils.CodeCommonValidationFailed, "displayName is required")) @@ -568,7 +568,7 @@ func (h *ApplicationHandler) writeApplicationError(w http.ResponseWriter, r *htt utils.CodeCommonValidationFailed, "Invalid API key id")) case errors.Is(err, constants.ErrArtifactNotFound): httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeCommonNotFound, "Association target not found")) + utils.CodeArtifactNotFound, "The specified artifact could not be found.")) case errors.Is(err, constants.ErrArtifactInvalidKind): httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( utils.CodeCommonValidationFailed, "Invalid association kind. Only LlmProvider and LlmProxy are supported")) diff --git a/platform-api/internal/handler/artifact_guard_response.go b/platform-api/internal/handler/artifact_guard_response.go index 0dc0c03dd6..6d660269ef 100644 --- a/platform-api/internal/handler/artifact_guard_response.go +++ b/platform-api/internal/handler/artifact_guard_response.go @@ -39,15 +39,15 @@ func respondArtifactGuardError(w http.ResponseWriter, err error) bool { switch { case errors.Is(err, constants.ErrArtifactReadOnly): httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( - utils.CodeCommonForbidden, err.Error())) + utils.CodeArtifactReadOnly, err.Error())) return true case errors.Is(err, constants.ErrArtifactRuntimeImmutable): httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( - utils.CodeCommonForbidden, err.Error())) + utils.CodeArtifactRuntimeImmutable, err.Error())) return true case errors.Is(err, constants.ErrArtifactDeployed): httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeCommonConflict, err.Error())) + utils.CodeArtifactDeployed, err.Error())) return true default: return false diff --git a/platform-api/internal/handler/gateway.go b/platform-api/internal/handler/gateway.go index 2b7a569a77..f6e936f5d9 100644 --- a/platform-api/internal/handler/gateway.go +++ b/platform-api/internal/handler/gateway.go @@ -566,7 +566,7 @@ func (h *GatewayHandler) SyncCustomPolicy(w http.ResponseWriter, r *http.Request } if strings.Contains(msg, "not found in gateway manifest") { httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeCommonNotFound, "The specified policy version could not be found in the gateway manifest.")) + utils.CodeCustomPolicyVersionNotFound, "The specified policy version could not be found in the gateway manifest.")) return } if strings.Contains(msg, "not a custom policy") || strings.Contains(msg, "manifest is not available") { @@ -609,12 +609,12 @@ func (h *GatewayHandler) GetCustomPolicy(w http.ResponseWriter, r *http.Request) if err != nil { if errors.Is(err, constants.ErrCustomPolicyNotFound) { httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeCommonNotFound, "Custom policy not found.")) + utils.CodeCustomPolicyNotFound, "Custom policy not found.")) return } if errors.Is(err, constants.ErrCustomPolicyVersionMismatch) { httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeCommonNotFound, "Custom policy not found with the specified version.")) + utils.CodeCustomPolicyVersionNotFound, "Custom policy not found with the specified version.")) return } h.slogger.Error("Failed to get custom policy", "error", err) @@ -647,12 +647,12 @@ func (h *GatewayHandler) DeleteCustomPolicy(w http.ResponseWriter, r *http.Reque if err != nil { if errors.Is(err, constants.ErrCustomPolicyNotFound) { httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeCommonNotFound, "Custom policy not found.")) + utils.CodeCustomPolicyNotFound, "Custom policy not found.")) return } if errors.Is(err, constants.ErrCustomPolicyVersionMismatch) { httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeCommonNotFound, "Custom policy not found with the specified version.")) + utils.CodeCustomPolicyVersionNotFound, "Custom policy not found with the specified version.")) return } if errors.Is(err, constants.ErrCustomPolicyInUse) { diff --git a/platform-api/internal/handler/llm_apikey.go b/platform-api/internal/handler/llm_apikey.go index a390e3911b..e1b26dff6c 100644 --- a/platform-api/internal/handler/llm_apikey.go +++ b/platform-api/internal/handler/llm_apikey.go @@ -74,7 +74,7 @@ func (h *LLMProviderAPIKeyHandler) ListAPIKeys(w http.ResponseWriter, r *http.Re if err != nil { if errors.Is(err, constants.ErrAPINotFound) { httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) + utils.CodeArtifactNotFound, "The specified artifact could not be found.")) return } h.slogger.Error("Failed to list LLM provider API keys", "providerId", providerID, "organizationId", orgID, "error", err) @@ -118,7 +118,7 @@ func (h *LLMProviderAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.R if err != nil { if errors.Is(err, constants.ErrAPINotFound) { httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) + utils.CodeArtifactNotFound, "The specified artifact could not be found.")) return } if errors.Is(err, constants.ErrAPIKeyNotFound) { @@ -128,7 +128,7 @@ func (h *LLMProviderAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.R } if errors.Is(err, constants.ErrAPIKeyForbidden) { httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( - utils.CodeCommonForbidden, "Only the key creator can delete this API key")) + utils.CodeLLMProviderAPIKeyForbidden, "Only the key creator can delete this API key")) return } h.slogger.Error("Failed to delete LLM provider API key", "providerId", providerID, "keyName", keyName, "organizationId", orgID, "error", err) @@ -182,7 +182,7 @@ func (h *LLMProviderAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.R if err != nil { if errors.Is(err, constants.ErrAPINotFound) { httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) + utils.CodeArtifactNotFound, "The specified artifact could not be found.")) return } if errors.Is(err, constants.ErrGatewayUnavailable) { diff --git a/platform-api/internal/handler/llm_proxy_apikey.go b/platform-api/internal/handler/llm_proxy_apikey.go index 5c45015fd4..6d300677c3 100644 --- a/platform-api/internal/handler/llm_proxy_apikey.go +++ b/platform-api/internal/handler/llm_proxy_apikey.go @@ -74,7 +74,7 @@ func (h *LLMProxyAPIKeyHandler) ListAPIKeys(w http.ResponseWriter, r *http.Reque if err != nil { if errors.Is(err, constants.ErrAPINotFound) { httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) + utils.CodeArtifactNotFound, "The specified artifact could not be found.")) return } h.slogger.Error("Failed to list LLM proxy API keys", "proxyId", proxyID, "organizationId", orgID, "error", err) @@ -118,7 +118,7 @@ func (h *LLMProxyAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.Requ if err != nil { if errors.Is(err, constants.ErrAPINotFound) { httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) + utils.CodeArtifactNotFound, "The specified artifact could not be found.")) return } if errors.Is(err, constants.ErrAPIKeyNotFound) { @@ -128,7 +128,7 @@ func (h *LLMProxyAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.Requ } if errors.Is(err, constants.ErrAPIKeyForbidden) { httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( - utils.CodeCommonForbidden, "Only the key creator can delete this API key")) + utils.CodeLLMProxyAPIKeyForbidden, "Only the key creator can delete this API key")) return } h.slogger.Error("Failed to delete LLM proxy API key", "proxyId", proxyID, "keyName", keyName, "organizationId", orgID, "error", err) @@ -182,7 +182,7 @@ func (h *LLMProxyAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Requ if err != nil { if errors.Is(err, constants.ErrAPINotFound) { httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) + utils.CodeArtifactNotFound, "The specified artifact could not be found.")) return } if errors.Is(err, constants.ErrGatewayUnavailable) { diff --git a/platform-api/internal/handler/secret.go b/platform-api/internal/handler/secret.go index 0e60b29369..d1ea8f1279 100644 --- a/platform-api/internal/handler/secret.go +++ b/platform-api/internal/handler/secret.go @@ -86,7 +86,7 @@ func (h *SecretHandler) CreateSecret(w http.ResponseWriter, r *http.Request) { if err != nil { if errors.Is(err, constants.ErrSecretAlreadyExists) { httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeCommonConflict, "A secret with this name already exists in this scope")) + utils.CodeSecretExists, "A secret with this name already exists in this scope")) return } if errors.Is(err, constants.ErrInvalidSecretType) { @@ -168,7 +168,7 @@ func (h *SecretHandler) GetSecret(w http.ResponseWriter, r *http.Request) { if err != nil { if errors.Is(err, constants.ErrSecretNotFound) { httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeCommonNotFound, "Secret not found")) + utils.CodeSecretNotFound, "Secret not found")) return } h.slogger.Error("failed to get secret", "error", err) @@ -218,7 +218,7 @@ func (h *SecretHandler) UpdateSecret(w http.ResponseWriter, r *http.Request) { if err != nil { if errors.Is(err, constants.ErrSecretNotFound) { httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeCommonNotFound, "Secret not found")) + utils.CodeSecretNotFound, "Secret not found")) return } h.slogger.Error("failed to update secret", "error", err) @@ -254,7 +254,7 @@ func (h *SecretHandler) DeleteSecret(w http.ResponseWriter, r *http.Request) { if err != nil { if errors.Is(err, constants.ErrSecretNotFound) { httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeCommonNotFound, "Secret not found")) + utils.CodeSecretNotFound, "Secret not found")) return } diff --git a/platform-api/internal/handler/subscription_handler.go b/platform-api/internal/handler/subscription_handler.go index 5fa3af684b..19eb4f928d 100644 --- a/platform-api/internal/handler/subscription_handler.go +++ b/platform-api/internal/handler/subscription_handler.go @@ -117,7 +117,7 @@ func (h *SubscriptionHandler) CreateSubscription(w http.ResponseWriter, r *http. if err != nil { if errors.Is(err, constants.ErrAPINotFound) { httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) + utils.CodeArtifactNotFound, "The specified artifact could not be found.")) return } if errors.Is(err, constants.ErrSubscriptionAlreadyExists) { @@ -201,7 +201,7 @@ func (h *SubscriptionHandler) ListSubscriptions(w http.ResponseWriter, r *http.R if err != nil { if errors.Is(err, constants.ErrAPINotFound) { httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) + utils.CodeArtifactNotFound, "The specified artifact could not be found.")) return } h.slogger.Error("Failed to list subscriptions", "apiId", apiId, "organizationId", orgId, "error", err) @@ -353,7 +353,7 @@ func (h *SubscriptionHandler) UpdateSubscription(w http.ResponseWriter, r *http. } if errors.Is(err, constants.ErrSubscriptionSubscriberMismatch) { httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( - utils.CodeCommonForbidden, "subscriberId does not match this subscription")) + utils.CodeSubscriptionForbidden, "subscriberId does not match this subscription")) return } h.slogger.Error("Failed to update subscription", "subscriptionId", subscriptionId, "organizationId", orgId, "error", err) @@ -402,7 +402,7 @@ func (h *SubscriptionHandler) DeleteSubscription(w http.ResponseWriter, r *http. } if errors.Is(err, constants.ErrSubscriptionSubscriberMismatch) { httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( - utils.CodeCommonForbidden, "subscriberId does not match this subscription")) + utils.CodeSubscriptionForbidden, "subscriberId does not match this subscription")) return } h.slogger.Error("Failed to delete subscription", "subscriptionId", subscriptionId, "organizationId", orgId, "error", err) diff --git a/platform-api/internal/utils/codes.go b/platform-api/internal/utils/codes.go index d44d7c33d7..1fbe14e871 100644 --- a/platform-api/internal/utils/codes.go +++ b/platform-api/internal/utils/codes.go @@ -73,6 +73,8 @@ const ( CodeLLMProxyLimitReached = "LLM_PROXY_LIMIT_REACHED" CodeLLMProxyAPIKeyNotFound = "LLM_PROXY_API_KEY_NOT_FOUND" CodeLLMProxyDeploymentValidationFailed = "LLM_PROXY_DEPLOYMENT_VALIDATION_FAILED" + CodeLLMProviderAPIKeyForbidden = "LLM_PROVIDER_API_KEY_FORBIDDEN" + CodeLLMProxyAPIKeyForbidden = "LLM_PROXY_API_KEY_FORBIDDEN" ) // LLM provider template domain codes. @@ -144,6 +146,7 @@ const ( const ( CodeSubscriptionNotFound = "SUBSCRIPTION_NOT_FOUND" CodeSubscriptionExists = "SUBSCRIPTION_EXISTS" + CodeSubscriptionForbidden = "SUBSCRIPTION_FORBIDDEN" CodeSubscriptionPlanNotFound = "SUBSCRIPTION_PLAN_NOT_FOUND" CodeSubscriptionPlanExists = "SUBSCRIPTION_PLAN_EXISTS" ) @@ -157,5 +160,34 @@ const ( // Secret domain codes. const ( - CodeSecretInUse = "SECRET_IN_USE" + CodeSecretNotFound = "SECRET_NOT_FOUND" + CodeSecretExists = "SECRET_EXISTS" + CodeSecretInUse = "SECRET_IN_USE" +) + +// Artifact domain codes. Used by flows that operate on a generic artifact +// reference (REST API / LLM provider / LLM proxy / MCP proxy) — API keys, +// subscriptions, and application associations — where the caller shouldn't +// need to know which concrete artifact kind was targeted, and by the +// data-plane-origin guard that protects DP-originated artifacts from +// control-plane mutation. +const ( + CodeArtifactNotFound = "ARTIFACT_NOT_FOUND" + CodeArtifactExists = "ARTIFACT_EXISTS" + CodeArtifactReadOnly = "ARTIFACT_READ_ONLY" + CodeArtifactRuntimeImmutable = "ARTIFACT_RUNTIME_IMMUTABLE" + CodeArtifactDeployed = "ARTIFACT_DEPLOYED" +) + +// Custom policy domain codes. +const ( + CodeCustomPolicyNotFound = "CUSTOM_POLICY_NOT_FOUND" + CodeCustomPolicyVersionNotFound = "CUSTOM_POLICY_VERSION_NOT_FOUND" +) + +// Application API key domain codes (application-scoped API key mappings, +// distinct from the LLM provider/proxy API key codes above). +const ( + CodeApplicationAPIKeyNotFound = "APPLICATION_API_KEY_NOT_FOUND" + CodeApplicationAPIKeyForbidden = "APPLICATION_API_KEY_FORBIDDEN" ) diff --git a/platform-api/resources/openapi.yaml b/platform-api/resources/openapi.yaml index d21acaa391..01d4588cd5 100644 --- a/platform-api/resources/openapi.yaml +++ b/platform-api/resources/openapi.yaml @@ -69,7 +69,7 @@ paths: '400': $ref: '#/components/responses/BadRequest' '409': - $ref: '#/components/responses/Conflict' + $ref: '#/components/responses/Conflict' '500': $ref: '#/components/responses/InternalServerError' @@ -174,7 +174,7 @@ paths: '401': $ref: '#/components/responses/Unauthorized' '404': - description: Organization not found + $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' @@ -635,11 +635,7 @@ paths: '404': $ref: '#/components/responses/NotFound' '503': - description: Service Unavailable - No gateway connections available - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/GatewayConnectionUnavailable' '500': $ref: '#/components/responses/InternalServerError' @@ -691,11 +687,7 @@ paths: '404': $ref: '#/components/responses/NotFound' '503': - description: Service Unavailable - No gateway connections available - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/GatewayConnectionUnavailable' '500': $ref: '#/components/responses/InternalServerError' @@ -735,11 +727,7 @@ paths: '404': $ref: '#/components/responses/NotFound' '503': - description: Service Unavailable - No gateway connections available - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/GatewayConnectionUnavailable' '500': $ref: '#/components/responses/InternalServerError' @@ -777,21 +765,13 @@ paths: schema: $ref: '#/components/schemas/DeploymentResponse' '400': - description: Bad Request. Validation errors (missing name, base, gatewayId, or API has no backend services) - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': - description: Not Found. API, gateway, or base deployment not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' @@ -1002,11 +982,7 @@ paths: '404': $ref: '#/components/responses/NotFound' '409': - description: Conflict. Cannot restore currently deployed deployment or deployment is invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/Conflict' '500': $ref: '#/components/responses/InternalServerError' @@ -1636,21 +1612,13 @@ paths: schema: $ref: '#/components/schemas/DeploymentResponse' '400': - description: Bad Request. Validation errors (missing name, base, gatewayId, or invalid input) - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': - description: Not Found. LLM provider, gateway, or base deployment not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' @@ -1880,11 +1848,7 @@ paths: '404': $ref: '#/components/responses/NotFound' '409': - description: Conflict. Cannot restore currently deployed deployment or deployment is invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/Conflict' '500': $ref: '#/components/responses/InternalServerError' @@ -1981,17 +1945,9 @@ paths: '401': $ref: '#/components/responses/Unauthorized' '404': - description: LLM provider not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/NotFound' '503': - description: No gateway connections available - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/GatewayConnectionUnavailable' '500': $ref: '#/components/responses/InternalServerError' get: @@ -2024,11 +1980,7 @@ paths: '401': $ref: '#/components/responses/Unauthorized' '404': - description: LLM provider not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' @@ -2067,17 +2019,9 @@ paths: '401': $ref: '#/components/responses/Unauthorized' '404': - description: LLM provider or API key not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/NotFound' '503': - description: No gateway connections available - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/GatewayConnectionUnavailable' '500': $ref: '#/components/responses/InternalServerError' @@ -2112,6 +2056,8 @@ paths: $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '500': @@ -2299,21 +2245,13 @@ paths: schema: $ref: '#/components/schemas/DeploymentResponse' '400': - description: Bad Request. Validation errors (missing name, base, gatewayId, or invalid input) - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': - description: Not Found. LLM proxy, gateway, or base deployment not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' @@ -2543,11 +2481,7 @@ paths: '404': $ref: '#/components/responses/NotFound' '409': - description: Conflict. Cannot restore currently deployed deployment or deployment is invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/Conflict' '500': $ref: '#/components/responses/InternalServerError' @@ -2593,17 +2527,9 @@ paths: '401': $ref: '#/components/responses/Unauthorized' '404': - description: LLM proxy not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/NotFound' '503': - description: No gateway connections available - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/GatewayConnectionUnavailable' '500': $ref: '#/components/responses/InternalServerError' get: @@ -2636,11 +2562,7 @@ paths: '401': $ref: '#/components/responses/Unauthorized' '404': - description: LLM proxy not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' @@ -2679,17 +2601,9 @@ paths: '401': $ref: '#/components/responses/Unauthorized' '404': - description: LLM proxy or API key not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/NotFound' '503': - description: No gateway connections available - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/GatewayConnectionUnavailable' '500': $ref: '#/components/responses/InternalServerError' @@ -2911,19 +2825,11 @@ paths: schema: $ref: '#/components/schemas/DeploymentResponse' '400': - description: Bad Request. Validation errors (missing name, base, gatewayId, or MCP proxy has no backend services) - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '404': - description: Not Found. MCP proxy, gateway, or base deployment not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' @@ -3153,11 +3059,7 @@ paths: '404': $ref: '#/components/responses/NotFound' '409': - description: Conflict. Cannot restore currently deployed deployment or deployment is invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/Conflict' '500': $ref: '#/components/responses/InternalServerError' @@ -3231,17 +3133,9 @@ paths: '403': $ref: '#/components/responses/Forbidden' '404': - description: Organization not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/NotFound' '409': - description: Gateway name already exists within organization - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/Conflict' '500': $ref: '#/components/responses/InternalServerError' @@ -3368,22 +3262,7 @@ paths: '404': $ref: '#/components/responses/NotFound' '409': - description: Conflict. Gateway has active deployments or connections - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - examples: - activeDeployments: - value: - code: 409 - message: Conflict - description: Cannot delete gateway - 3 active API deployment(s) exist - activeConnections: - value: - code: 409 - message: Conflict - description: Cannot delete gateway - 2 active connection(s) exist + $ref: '#/components/responses/Conflict' '500': $ref: '#/components/responses/InternalServerError' @@ -3441,11 +3320,7 @@ paths: schema: $ref: '#/components/schemas/TokenRotationResponse' '400': - description: Bad request (e.g., maximum active tokens reached) - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': @@ -3490,11 +3365,7 @@ paths: '403': $ref: '#/components/responses/Forbidden' '404': - description: Gateway or token not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' @@ -3612,17 +3483,17 @@ paths: '404': $ref: '#/components/responses/NotFound' '409': - description: Policy already exists, patch version update, or downgrade is not allowed - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/Conflict' '422': description: Policy is not a custom policy or manifest is unavailable content: application/json: schema: $ref: '#/components/schemas/Error' + example: + status: error + code: POLICY_INVALID_STATE + message: "The policy is not a custom policy, or its manifest is unavailable." '500': $ref: '#/components/responses/InternalServerError' @@ -3703,11 +3574,7 @@ paths: '404': $ref: '#/components/responses/NotFound' '409': - description: Custom policy is in use by one or more APIs - content: - application/json: - schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/responses/Conflict' '500': $ref: '#/components/responses/InternalServerError' @@ -4430,6 +4297,8 @@ paths: $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' @@ -4807,11 +4676,20 @@ paths: '404': $ref: '#/components/responses/NotFound' '409': - description: Secret is referenced by active resources + description: Conflict. The secret is referenced by one or more active resources. content: application/json: schema: - $ref: '#/components/schemas/SecretDeleteConflict' + $ref: '#/components/schemas/Error' + example: + status: error + code: SECRET_IN_USE + message: The secret is referenced by one or more active resources. + details: + references: + - type: llm_provider + handle: wso2-openai-provider + name: WSO2 OpenAI Provider '503': $ref: '#/components/responses/ServiceUnavailable' '500': @@ -6661,26 +6539,56 @@ components: $ref: '#/components/schemas/Pagination' Error: - title: Error object returned + title: Standard error response + description: The single error shape returned by every failed request across the API. required: + - status - code - - title + - message type: object properties: + status: + type: string + enum: [error] + description: Always the literal "error". + example: error code: - type: integer - format: int64 - title: type: string - description: Error message - details: + pattern: '^[A-Z][A-Z0-9_]*$' + description: "Stable, machine-readable error code from the error catalog, in the form `_` (e.g. `REST_API_NOT_FOUND`). Clients and agents should branch on this, not on the HTTP status." + example: REST_API_NOT_FOUND + message: type: string - description: A detailed description about the error message + description: Human-readable description of the error. + example: The requested REST API could not be found. errors: type: array - description: List of specific errors (useful for validation errors) + description: Per-field validation failures. Present when the error is a validation failure. items: - type: object + $ref: '#/components/schemas/FieldError' + details: + type: object + description: >- + Optional structured metadata specific to this error condition + (e.g. the resources referencing a secret that blocked its + deletion). Shape varies by `code`; absent when not applicable. + additionalProperties: true + + FieldError: + title: Field-level validation error + required: + - field + - message + type: object + properties: + field: + type: string + description: Path of the offending field. + example: spec.context + message: + type: string + description: Why the field failed validation. + example: must start with / DeployRequest: type: object @@ -8474,27 +8382,6 @@ components: pagination: $ref: '#/components/schemas/Pagination' - SecretDeleteConflict: - type: object - properties: - error: - type: string - example: secret is referenced by active resources - references: - type: array - items: - type: object - properties: - type: - type: string - example: llm_provider - handle: - type: string - example: wso2-openai-provider - name: - type: string - example: WSO2 OpenAI Provider - responses: Unauthorized: description: Unauthorized. Authentication credentials are missing or invalid. @@ -8503,9 +8390,9 @@ components: schema: $ref: '#/components/schemas/Error' example: - code: 401 - message: Unauthorized - description: Authorization header is required or token is invalid + status: error + code: COMMON_UNAUTHORIZED + message: Authorization header is required, or the token is invalid or expired. BadRequest: description: Bad Request. Invalid request or validation error. @@ -8514,9 +8401,12 @@ components: schema: $ref: '#/components/schemas/Error' example: - code: 400 - message: Bad Request - description: Invalid request or validation error + status: error + code: COMMON_VALIDATION_FAILED + message: The request failed validation. + errors: + - field: spec.context + message: must start with / Forbidden: description: Forbidden. The authenticated user does not have permission to access this resource. content: @@ -8524,9 +8414,9 @@ components: schema: $ref: '#/components/schemas/Error' example: - code: 403 - title: Forbidden - details: You do not have permission to perform this action + status: error + code: COMMON_FORBIDDEN + message: You do not have permission to perform this action. NotFound: description: Not Found. The specified resource does not exist. content: @@ -8534,9 +8424,9 @@ components: schema: $ref: '#/components/schemas/Error' example: - code: 404 - message: Not Found - description: The specified resource does not exist + status: error + code: COMMON_NOT_FOUND + message: The specified resource does not exist. Conflict: description: Conflict. Specified resource already exists. content: @@ -8544,9 +8434,9 @@ components: schema: $ref: '#/components/schemas/Error' example: - code: 409 - message: Conflict - description: Specified resource already exists + status: error + code: COMMON_CONFLICT + message: The specified resource already exists. InternalServerError: description: Internal Server Error. content: @@ -8554,10 +8444,9 @@ components: schema: $ref: '#/components/schemas/Error' example: - code: 500 - message: Internal Server Error - description: The server encountered an internal error. Please contact - administrator. + status: error + code: COMMON_INTERNAL_ERROR + message: The server encountered an internal error. Please contact the administrator. ServiceUnavailable: description: Service Unavailable. The secrets management feature is not configured. content: @@ -8565,9 +8454,32 @@ components: schema: $ref: '#/components/schemas/Error' example: - code: 503 - message: Service Unavailable - description: Secrets management is not configured — set PLATFORM_SECRET_ENCRYPTION_KEY + status: error + code: COMMON_SERVICE_UNAVAILABLE + message: Secrets management is not configured. Set PLATFORM_SECRET_ENCRYPTION_KEY. + + GatewayConnectionUnavailable: + description: Service Unavailable. No gateway connections are currently available to service this request. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + status: error + code: GATEWAY_CONNECTION_UNAVAILABLE + message: No gateway connections are currently available. + + examples: + GatewayNotFoundExample: + value: + status: error + code: GATEWAY_NOT_FOUND + message: "The specified gateway could not be found." + DeploymentBaseNotFoundExample: + value: + status: error + code: DEPLOYMENT_BASE_NOT_FOUND + message: "The specified base deployment could not be found." parameters: projectId: name: projectId From 2d66374e536c0f9235878c9ca48b714b042c2fd2 Mon Sep 17 00:00:00 2001 From: Thushani Jayasekera Date: Tue, 7 Jul 2026 21:15:19 +0530 Subject: [PATCH 03/12] Add application error handling with standardized error codes and tests This commit introduces a new error handling mechanism in the application by implementing a structured error catalog. It includes the creation of error definitions, a new error type, and standardized error responses across various API handlers. Additionally, comprehensive unit tests have been added to ensure the correctness of the error handling logic. --- platform-api/internal/apperror/apperror.go | 149 ++++ .../internal/apperror/apperror_test.go | 141 ++++ platform-api/internal/apperror/catalog.go | 190 +++++ .../internal/apperror/catalog_test.go | 123 ++++ platform-api/internal/apperror/def.go | 69 ++ platform-api/internal/apperror/write.go | 45 ++ platform-api/internal/handler/api.go | 346 ++++------ .../internal/handler/api_deployment.go | 310 +++------ platform-api/internal/handler/api_key.go | 154 ++--- platform-api/internal/handler/apikey_user.go | 24 +- platform-api/internal/handler/application.go | 363 ++++------ .../handler/artifact_guard_response.go | 30 +- platform-api/internal/handler/auth_login.go | 29 +- platform-api/internal/handler/gateway.go | 422 ++++------- .../internal/handler/gateway_internal.go | 650 +++++++---------- .../internal/handler/identity_helper.go | 22 +- platform-api/internal/handler/llm.go | 653 +++++++----------- platform-api/internal/handler/llm_apikey.go | 122 ++-- .../internal/handler/llm_deployment.go | 513 +++++--------- .../internal/handler/llm_proxy_apikey.go | 122 ++-- platform-api/internal/handler/mcp.go | 199 +++--- .../internal/handler/mcp_deployment.go | 278 +++----- platform-api/internal/handler/organization.go | 91 +-- platform-api/internal/handler/project.go | 179 ++--- platform-api/internal/handler/secret.go | 146 ++-- .../internal/handler/subscription_handler.go | 243 +++---- .../handler/subscription_plan_handler.go | 223 +++--- platform-api/internal/handler/websocket.go | 47 +- platform-api/internal/middleware/auth.go | 47 +- .../internal/middleware/authorization.go | 3 +- .../internal/middleware/error_mapper.go | 105 +++ .../internal/middleware/error_mapper_test.go | 195 ++++++ platform-api/internal/utils/codes.go | 21 +- platform-api/internal/utils/error.go | 4 + platform-api/internal/webhook/receiver.go | 70 +- platform-api/resources/openapi.yaml | 11 +- 36 files changed, 3012 insertions(+), 3327 deletions(-) create mode 100644 platform-api/internal/apperror/apperror.go create mode 100644 platform-api/internal/apperror/apperror_test.go create mode 100644 platform-api/internal/apperror/catalog.go create mode 100644 platform-api/internal/apperror/catalog_test.go create mode 100644 platform-api/internal/apperror/def.go create mode 100644 platform-api/internal/apperror/write.go create mode 100644 platform-api/internal/middleware/error_mapper.go create mode 100644 platform-api/internal/middleware/error_mapper_test.go diff --git a/platform-api/internal/apperror/apperror.go b/platform-api/internal/apperror/apperror.go new file mode 100644 index 0000000000..05b419ee5f --- /dev/null +++ b/platform-api/internal/apperror/apperror.go @@ -0,0 +1,149 @@ +/* + * 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 defines the typed application error that handlers, +// services, and repositories return instead of writing HTTP error responses +// inline. A single mapper at the routing layer (middleware.MapErrors) catches +// the error, logs it, and serializes it onto the wire via WriteHTTP — see +// resources/ERROR_HANDLING_IMPLEMENTATION_PLAN.md. +package apperror + +import ( + "errors" + "fmt" + "net/http" + "runtime" + "strings" + + "github.com/wso2/api-platform/platform-api/internal/utils" + + "github.com/go-playground/validator/v10" +) + +// Error carries everything the error mapper needs to log a failure and write +// the client-facing utils.ErrorResponse: the catalog code and HTTP status, +// the sanitized client message, optional field-level validation errors and +// structured details, plus internal-only diagnostics (log message, wrapped +// cause, and the call stack captured at construction time). +type Error struct { + Code string // catalog code, e.g. utils.CodeCommonValidationFailed + HTTPStatus int // status to write + Message string // client-facing message + FieldErrors []utils.FieldError // optional, for validation failures + Details any // optional structured metadata + LogMessage string // internal-only detail; never sent to client + Cause error // wrapped original error, for logging/unwrapping + Stack []uintptr // captured at construction time, see New +} + +// Error satisfies the error interface. It intentionally includes only the +// code and client message — internal diagnostics stay in LogMessage/Cause. +func (e *Error) Error() string { + if e.Cause != nil { + return fmt.Sprintf("%s: %s: %v", e.Code, e.Message, e.Cause) + } + return fmt.Sprintf("%s: %s", e.Code, e.Message) +} + +// Unwrap exposes the wrapped cause to errors.Is/errors.As chains. +func (e *Error) Unwrap() error { return e.Cause } + +// NewValidation converts a request-validation failure into an Error, +// mirroring utils.NewValidationErrorResponse: validator.ValidationErrors +// become field-level errors under COMMON_VALIDATION_FAILED; anything else +// maps to the generic "Invalid input." message with the original error kept +// as the cause for internal logging. +func NewValidation(err error) *Error { + var ve validator.ValidationErrors + if errors.As(err, &ve) { + fieldErrors := make([]utils.FieldError, 0, len(ve)) + for _, fe := range ve { + fieldErrors = append(fieldErrors, utils.FieldError{ + Field: fe.Field(), + Message: fmt.Sprintf("The field %s is %s", fe.Field(), fe.Tag()), + }) + } + return newWithSkip(3, utils.CodeCommonValidationFailed, http.StatusBadRequest, + "Validation failed for the request parameters.").WithFieldErrors(fieldErrors) + } + return newWithSkip(3, utils.CodeCommonValidationFailed, http.StatusBadRequest, "Invalid input.").WithCause(err) +} + +// newWithSkip captures the stack skipping the given number of frames so the +// first frame is the exported constructor's caller. It is the only way an +// *Error comes into existence — exported construction goes through the +// catalog (Def.New / Def.Wrap, see catalog.go) or NewValidation, so the +// stack capture can never be skipped and code/status/message always come +// from a declared catalog entry. +// +// LogMessage defaults to the client message so the mapper always has +// something to log, even if the call site never chains WithLogMessage. +// WithLogMessage overrides this default with a more specific internal +// detail when the client message is too generic to be useful for diagnosis +// (e.g. Unauthorized's fixed message). +func newWithSkip(skip int, code string, status int, message string) *Error { + var pcs [32]uintptr + n := runtime.Callers(skip, pcs[:]) + return &Error{Code: code, HTTPStatus: status, Message: message, LogMessage: message, Stack: pcs[:n]} +} + +// WithLogMessage overrides the default internal-only detail message (see +// newWithSkip) that the mapper logs but never sends to the client. +func (e *Error) WithLogMessage(msg string) *Error { + e.LogMessage = msg + return e +} + +// WithFieldErrors attaches field-level validation errors surfaced to the +// client in the errors[] array. +func (e *Error) WithFieldErrors(fe []utils.FieldError) *Error { + e.FieldErrors = fe + return e +} + +// WithDetails attaches optional structured metadata surfaced to the client +// in the details field. +func (e *Error) WithDetails(details any) *Error { + e.Details = details + return e +} + +// WithCause wraps the original error for logging and errors.Is/As chains. +func (e *Error) WithCause(err error) *Error { + e.Cause = err + return e +} + +// StackString symbolizes the stack captured at construction time. Kept as +// raw []uintptr on the struct (cheap to capture) and only symbolized lazily +// when the mapper actually logs it, rather than paying runtime.CallersFrames +// cost on every construction. +func (e *Error) StackString() string { + if len(e.Stack) == 0 { + return "" + } + var sb strings.Builder + frames := runtime.CallersFrames(e.Stack) + for { + frame, more := frames.Next() + fmt.Fprintf(&sb, "%s\n\t%s:%d\n", frame.Function, frame.File, frame.Line) + if !more { + break + } + } + return sb.String() +} diff --git a/platform-api/internal/apperror/apperror_test.go b/platform-api/internal/apperror/apperror_test.go new file mode 100644 index 0000000000..bb08d18034 --- /dev/null +++ b/platform-api/internal/apperror/apperror_test.go @@ -0,0 +1,141 @@ +/* + * 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 ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/wso2/api-platform/platform-api/internal/utils" +) + +func TestDefNewCapturesStack(t *testing.T) { + e := Internal.New() + if len(e.Stack) == 0 { + t.Fatal("expected non-empty stack captured at construction time") + } + stack := e.StackString() + if !strings.Contains(stack, "TestDefNewCapturesStack") { + t.Errorf("expected stack trace to include the calling function, got:\n%s", stack) + } +} + +func TestDefNewFormatsMessage(t *testing.T) { + e := ValidationFailed.New("API name is required") + if e.Message != "API name is required" { + t.Errorf("unexpected message %q", e.Message) + } + if e.Code != utils.CodeCommonValidationFailed || e.HTTPStatus != http.StatusBadRequest { + t.Errorf("Def did not carry its declared code/status: %+v", e) + } + + // Fixed-message entries called with no args return the template verbatim. + if got := RESTAPINotFound.New().Message; got != "The specified REST API could not be found." { + t.Errorf("unexpected fixed message %q", got) + } + + // Templates with verbs interpolate args. + if got := DeploymentNotActive.New("API").Message; got != "No active deployment found for this API on the gateway." { + t.Errorf("unexpected formatted message %q", got) + } +} + +func TestDefWrapAttachesCause(t *testing.T) { + cause := errors.New("db connection refused") + e := Internal.Wrap(cause) + if !errors.Is(e, cause) { + t.Error("expected errors.Is to find the wrapped cause") + } + if len(e.Stack) == 0 { + t.Error("expected Wrap to capture the stack too") + } + + // A wrapped *Error must still be discoverable via errors.As — the + // propagation contract relies on this. + wrapped := fmt.Errorf("service layer context: %w", e) + var appErr *Error + if !errors.As(wrapped, &appErr) { + t.Fatal("expected errors.As to find *Error through a %w wrap") + } + if appErr.Code != utils.CodeCommonInternalError { + t.Errorf("unexpected code %q", appErr.Code) + } +} + +func TestDefIs(t *testing.T) { + err := fmt.Errorf("outer: %w", ProjectNotFound.New()) + if !ProjectNotFound.Is(err) { + t.Error("expected Def.Is to match through a %w wrap") + } + if RESTAPINotFound.Is(err) { + t.Error("Def.Is matched a different catalog entry") + } + if ProjectNotFound.Is(errors.New("plain")) { + t.Error("Def.Is matched a plain error") + } +} + +func TestNewValidationFallback(t *testing.T) { + e := NewValidation(errors.New("bad json")) + if e.HTTPStatus != http.StatusBadRequest { + t.Errorf("expected 400, got %d", e.HTTPStatus) + } + if e.Code != utils.CodeCommonValidationFailed { + t.Errorf("expected %s, got %s", utils.CodeCommonValidationFailed, e.Code) + } + if e.Message != "Invalid input." { + t.Errorf("unexpected message %q", e.Message) + } +} + +func TestWriteHTTP(t *testing.T) { + rec := httptest.NewRecorder() + WriteHTTP(rec, ProjectNotFound.Wrap(errors.New("sql: no rows in result set")). + WithLogMessage("internal detail that must not leak"), "") + + if rec.Code != http.StatusNotFound { + t.Fatalf("expected 404, got %d", rec.Code) + } + var body utils.ErrorResponse + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("invalid JSON body: %v", err) + } + if body.Status != "error" || body.Code != utils.CodeProjectNotFound { + t.Errorf("unexpected envelope: %+v", body) + } + if strings.Contains(rec.Body.String(), "sql:") || strings.Contains(rec.Body.String(), "internal detail") { + t.Error("internal diagnostics leaked into the client response") + } + if strings.Contains(rec.Body.String(), "trackingId") { + t.Error("empty trackingId must be omitted from the response") + } + + rec = httptest.NewRecorder() + WriteHTTP(rec, Internal.New(), "d2f1c0aa-0000-0000-0000-000000000000") + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("invalid JSON body: %v", err) + } + if body.TrackingID != "d2f1c0aa-0000-0000-0000-000000000000" { + t.Errorf("expected trackingId echoed in body, got %+v", body) + } +} diff --git a/platform-api/internal/apperror/catalog.go b/platform-api/internal/apperror/catalog.go new file mode 100644 index 0000000000..d1ab40898a --- /dev/null +++ b/platform-api/internal/apperror/catalog.go @@ -0,0 +1,190 @@ +/* + * 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 ( + "net/http" + + "github.com/wso2/api-platform/platform-api/internal/utils" +) + +// This file is the error catalog: every client-facing error condition is +// declared here exactly once, binding its code, HTTP status, and message +// template together (see resources/ERROR_CATALOG_PLAN.md). Codes reference +// the string constants in utils/codes.go, which stay the single source for +// the code values while handlers not yet on the error-mapper pattern still +// build responses from them directly. +// +// Message templates are client-sterile: no internal detail, no file paths, +// no raw error text. A template of "%s" marks a code whose human message is +// genuinely call-site-specific (validation-style codes); interpolated args +// must be user-supplied values (names, handles, IDs), never internal error +// strings. Fixed-message entries are called with no args. +// +// allDefs feeds the catalog integrity test (catalog_test.go): unique codes, +// valid statuses, and message/verb sanity are asserted there. + +var allDefs []Def + +func def(code string, status int, messageFmt string) Def { + d := Def{Code: code, HTTPStatus: status, MessageFmt: messageFmt} + allDefs = append(allDefs, d) + return d +} + +// Common, domain-agnostic entries. Unauthorized is intentionally the ONLY +// 401 entry, with a fixed message: every authentication failure (missing, +// expired, invalid, revoked) returns the identical payload per the unified +// auth-failure rule in error-handling.md; the specific reason travels in the +// wrapped cause / log message only. +var ( + Unauthorized = def(utils.CodeCommonUnauthorized, http.StatusUnauthorized, "Invalid or expired credentials.") + Forbidden = def(utils.CodeCommonForbidden, http.StatusForbidden, "You do not have permission to perform this action.") + ValidationFailed = def(utils.CodeCommonValidationFailed, http.StatusBadRequest, "%s") + NotFound = def(utils.CodeCommonNotFound, http.StatusNotFound, "The requested resource could not be found.") + Conflict = def(utils.CodeCommonConflict, http.StatusConflict, "The request conflicts with the current state of the resource.") + NotAcceptable = def(utils.CodeCommonNotAcceptable, http.StatusNotAcceptable, "The requested media type is not supported.") + UnprocessableEntity = def(utils.CodeCommonUnprocessableEntity, http.StatusUnprocessableEntity, "The request could not be processed.") + Internal = def(utils.CodeCommonInternalError, http.StatusInternalServerError, "An unexpected error occurred.") + ServiceUnavailable = def(utils.CodeCommonServiceUnavailable, http.StatusServiceUnavailable, "The service is temporarily unavailable.") + TooManyRequests = def(utils.CodeCommonTooManyRequests, http.StatusTooManyRequests, "%s") +) + +// REST API entries. +var ( + RESTAPINotFound = def(utils.CodeRESTAPINotFound, http.StatusNotFound, "The specified REST API could not be found.") + // RESTAPIExists covers three distinct conflicts (handle, name+version, + // project) — the call site supplies which one. + RESTAPIExists = def(utils.CodeRESTAPIExists, http.StatusConflict, "%s") + RESTAPIDeploymentValidationFailed = def(utils.CodeRESTAPIDeploymentValidationFailed, http.StatusBadRequest, "%s") +) + +// LLM provider / proxy entries. +var ( + LLMProviderNotFound = def(utils.CodeLLMProviderNotFound, http.StatusNotFound, "The specified LLM provider could not be found.") + LLMProviderRefNotFound = def(utils.CodeLLMProviderRefNotFound, http.StatusBadRequest, "The referenced LLM provider could not be found.") + LLMProviderExists = def(utils.CodeLLMProviderExists, http.StatusConflict, "An LLM provider with this ID already exists.") + LLMProviderLimitReached = def(utils.CodeLLMProviderLimitReached, http.StatusConflict, "LLM provider limit reached for the organization.") + LLMProviderAPIKeyNotFound = def(utils.CodeLLMProviderAPIKeyNotFound, http.StatusNotFound, "The specified API key could not be found.") + LLMProviderAPIKeyForbidden = def(utils.CodeLLMProviderAPIKeyForbidden, http.StatusForbidden, "You do not have permission to access this API key.") + LLMProviderDeploymentValidationFailed = def(utils.CodeLLMProviderDeploymentValidationFailed, http.StatusBadRequest, "%s") + + LLMProxyNotFound = def(utils.CodeLLMProxyNotFound, http.StatusNotFound, "The specified LLM proxy could not be found.") + LLMProxyExists = def(utils.CodeLLMProxyExists, http.StatusConflict, "An LLM proxy with this ID already exists.") + LLMProxyLimitReached = def(utils.CodeLLMProxyLimitReached, http.StatusConflict, "LLM proxy limit reached for the organization.") + LLMProxyAPIKeyNotFound = def(utils.CodeLLMProxyAPIKeyNotFound, http.StatusNotFound, "The specified API key could not be found.") + LLMProxyAPIKeyForbidden = def(utils.CodeLLMProxyAPIKeyForbidden, http.StatusForbidden, "You do not have permission to access this API key.") + LLMProxyDeploymentValidationFailed = def(utils.CodeLLMProxyDeploymentValidationFailed, http.StatusBadRequest, "%s") +) + +// LLM provider template entries. +var ( + LLMProviderTemplateNotFound = def(utils.CodeLLMProviderTemplateNotFound, http.StatusNotFound, "The specified LLM provider template could not be found.") + LLMProviderTemplateVersionNotFound = def(utils.CodeLLMProviderTemplateVersionNotFound, http.StatusNotFound, "The specified LLM provider template version could not be found.") + LLMProviderTemplateRefNotFound = def(utils.CodeLLMProviderTemplateRefNotFound, http.StatusBadRequest, "The referenced LLM provider template could not be found.") + LLMProviderTemplateExists = def(utils.CodeLLMProviderTemplateExists, http.StatusConflict, "An LLM provider template with this ID already exists.") + LLMProviderTemplateVersionExists = def(utils.CodeLLMProviderTemplateVersionExists, http.StatusConflict, "This template version already exists.") + LLMProviderTemplateManagedByReserved = def(utils.CodeLLMProviderTemplateManagedByReserved, http.StatusBadRequest, "'wso2' is reserved and cannot be used as managedBy on custom templates.") + LLMProviderTemplateInUse = def(utils.CodeLLMProviderTemplateInUse, http.StatusConflict, "This template version is in use by one or more providers.") + LLMProviderTemplateReadOnly = def(utils.CodeLLMProviderTemplateReadOnly, http.StatusForbidden, "Built-in templates are read-only and cannot be modified.") + LLMProviderTemplateNotToggleable = def(utils.CodeLLMProviderTemplateNotToggleable, http.StatusForbidden, "Only built-in templates can be enabled or disabled.") +) + +// Gateway entries. +var ( + GatewayNotFound = def(utils.CodeGatewayNotFound, http.StatusNotFound, "The specified gateway could not be found.") + GatewayTokenNotFound = def(utils.CodeGatewayTokenNotFound, http.StatusNotFound, "The specified gateway token could not be found.") + GatewayConnectionUnavailable = def(utils.CodeGatewayConnectionUnavailable, http.StatusServiceUnavailable, "No gateway connections are currently available.") + GatewayHasActiveDeployments = def(utils.CodeGatewayHasActiveDeployments, http.StatusConflict, "The gateway has active deployments and cannot be deleted.") + GatewayNameConflict = def(utils.CodeGatewayNameConflict, http.StatusConflict, "A gateway with this name already exists.") + GatewayTokenLimitReached = def(utils.CodeGatewayTokenLimitReached, http.StatusConflict, "Gateway token limit reached.") +) + +// Deployment entries, shared across REST API / LLM provider / LLM proxy / +// MCP proxy deployment operations. DeploymentNotActive's verb is the artifact +// kind, e.g. "API", "LLM provider". +var ( + DeploymentBaseNotFound = def(utils.CodeDeploymentBaseNotFound, http.StatusNotFound, "The specified base deployment could not be found.") + DeploymentRestoreConflict = def(utils.CodeDeploymentRestoreConflict, http.StatusConflict, "Cannot restore the currently deployed deployment, or the deployment is invalid.") + DeploymentNotFound = def(utils.CodeDeploymentNotFound, http.StatusNotFound, "The specified deployment could not be found.") + DeploymentNotActive = def(utils.CodeDeploymentNotActive, http.StatusConflict, "No active deployment found for this %s on the gateway.") + DeploymentGatewayMismatch = def(utils.CodeDeploymentGatewayMismatch, http.StatusBadRequest, "Deployment is bound to a different gateway.") + DeploymentActive = def(utils.CodeDeploymentActive, http.StatusConflict, "Cannot delete an active deployment - undeploy it first.") + DeploymentInvalidStatus = def(utils.CodeDeploymentInvalidStatus, http.StatusBadRequest, "The specified deployment status filter is invalid.") +) + +// MCP proxy entries. +var ( + MCPProxyNotFound = def(utils.CodeMCPProxyNotFound, http.StatusNotFound, "The specified MCP proxy could not be found.") + MCPProxyExists = def(utils.CodeMCPProxyExists, http.StatusConflict, "An MCP proxy with this ID already exists.") + MCPProxyLimitReached = def(utils.CodeMCPProxyLimitReached, http.StatusConflict, "MCP proxy limit reached for the organization.") + MCPProxyDeploymentValidationFailed = def(utils.CodeMCPProxyDeploymentValidationFailed, http.StatusBadRequest, "%s") +) + +// Organization / project / application entries. +var ( + OrganizationNotFound = def(utils.CodeOrganizationNotFound, http.StatusNotFound, "The specified organization could not be found.") + OrganizationExists = def(utils.CodeOrganizationExists, http.StatusConflict, "An organization with this name already exists.") + ProjectNotFound = def(utils.CodeProjectNotFound, http.StatusNotFound, "The specified project could not be found.") + ProjectRefNotFound = def(utils.CodeProjectRefNotFound, http.StatusBadRequest, "The referenced project could not be found.") + ProjectExists = def(utils.CodeProjectExists, http.StatusConflict, "A project with this name already exists in the organization.") + ApplicationNotFound = def(utils.CodeApplicationNotFound, http.StatusNotFound, "The specified application could not be found.") + ApplicationExists = def(utils.CodeApplicationExists, http.StatusConflict, "An application with this name already exists.") +) + +// Subscription entries. +var ( + SubscriptionNotFound = def(utils.CodeSubscriptionNotFound, http.StatusNotFound, "The specified subscription could not be found.") + SubscriptionExists = def(utils.CodeSubscriptionExists, http.StatusConflict, "A subscription for this application and API already exists.") + SubscriptionForbidden = def(utils.CodeSubscriptionForbidden, http.StatusForbidden, "You do not have permission to access this subscription.") + SubscriptionPlanNotFound = def(utils.CodeSubscriptionPlanNotFound, http.StatusNotFound, "The specified subscription plan could not be found.") + SubscriptionPlanExists = def(utils.CodeSubscriptionPlanExists, http.StatusConflict, "A subscription plan with this name already exists.") +) + +// Custom policy entries. +var ( + PolicyVersionConflict = def(utils.CodePolicyVersionConflict, http.StatusConflict, "The policy version conflicts with an existing version.") + PolicyInvalidState = def(utils.CodePolicyInvalidState, http.StatusConflict, "The policy is not in a valid state for this operation.") + PolicyInUse = def(utils.CodePolicyInUse, http.StatusConflict, "The policy is in use and cannot be deleted.") + CustomPolicyNotFound = def(utils.CodeCustomPolicyNotFound, http.StatusNotFound, "The specified policy could not be found.") + CustomPolicyVersionNotFnd = def(utils.CodeCustomPolicyVersionNotFound, http.StatusNotFound, "The specified policy version could not be found.") +) + +// Secret entries. +var ( + SecretNotFound = def(utils.CodeSecretNotFound, http.StatusNotFound, "The specified secret could not be found.") + SecretExists = def(utils.CodeSecretExists, http.StatusConflict, "A secret with this name already exists.") + SecretInUse = def(utils.CodeSecretInUse, http.StatusConflict, "The secret is in use and cannot be deleted.") +) + +// Artifact entries — generic artifact-reference flows and the +// data-plane-origin guard. The guard produces client-appropriate messages at +// the call site, so these are passthrough templates. +var ( + ArtifactNotFound = def(utils.CodeArtifactNotFound, http.StatusNotFound, "The specified artifact could not be found.") + ArtifactExists = def(utils.CodeArtifactExists, http.StatusConflict, "The artifact already exists.") + ArtifactReadOnly = def(utils.CodeArtifactReadOnly, http.StatusForbidden, "%s") + ArtifactRuntimeImmutable = def(utils.CodeArtifactRuntimeImmutable, http.StatusForbidden, "%s") + ArtifactDeployed = def(utils.CodeArtifactDeployed, http.StatusConflict, "%s") +) + +// Application API key entries. +var ( + ApplicationAPIKeyNotFound = def(utils.CodeApplicationAPIKeyNotFound, http.StatusNotFound, "The specified API key could not be found.") + ApplicationAPIKeyForbidden = def(utils.CodeApplicationAPIKeyForbidden, http.StatusForbidden, "You do not have permission to access this API key.") +) diff --git a/platform-api/internal/apperror/catalog_test.go b/platform-api/internal/apperror/catalog_test.go new file mode 100644 index 0000000000..05334ba3cb --- /dev/null +++ b/platform-api/internal/apperror/catalog_test.go @@ -0,0 +1,123 @@ +/* + * 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 ( + "net/http" + "regexp" + "strings" + "testing" +) + +// codeShape is the format every catalog code must follow: uppercase +// SCREAMING_SNAKE_CASE, stable and client-visible. +var codeShape = regexp.MustCompile(`^[A-Z][A-Z0-9_]*$`) + +// fmtVerb matches fmt verbs in message templates (ignoring literal %%). +var fmtVerb = regexp.MustCompile(`%[^%]`) + +// messageArity declares, for every catalog entry whose MessageFmt contains +// fmt verbs, how many arguments call sites must pass. Adding a verb to a +// template without updating this table fails the test — the arity is part of +// the entry's contract, mirroring the argument list of a Java ExceptionCodes +// enum constant. +var messageArity = map[string]int{ + CodeOf(ValidationFailed): 1, + CodeOf(RESTAPIExists): 1, + CodeOf(RESTAPIDeploymentValidationFailed): 1, + CodeOf(LLMProviderDeploymentValidationFailed): 1, + CodeOf(LLMProxyDeploymentValidationFailed): 1, + CodeOf(MCPProxyDeploymentValidationFailed): 1, + CodeOf(DeploymentNotActive): 1, + CodeOf(ArtifactReadOnly): 1, + CodeOf(ArtifactRuntimeImmutable): 1, + CodeOf(ArtifactDeployed): 1, + CodeOf(TooManyRequests): 1, +} + +// CodeOf makes the arity table read as a set of catalog references rather +// than duplicated string literals. +func CodeOf(d Def) string { return d.Code } + +// internalMarkers are substrings that must never appear in a client-facing +// message template (error-handling.md: zero internal details). +var internalMarkers = []string{"sql", "pq:", "goroutine", ".go:", "/internal/", "stack", "panic"} + +func TestCatalogIntegrity(t *testing.T) { + if len(allDefs) == 0 { + t.Fatal("catalog is empty — def() registration broken?") + } + + seen := make(map[string]Def, len(allDefs)) + for _, d := range allDefs { + if !codeShape.MatchString(d.Code) { + t.Errorf("%s: code is not SCREAMING_SNAKE_CASE", d.Code) + } + if prev, dup := seen[d.Code]; dup { + t.Errorf("%s: declared twice (statuses %d and %d) — codes must be unique", d.Code, prev.HTTPStatus, d.HTTPStatus) + } + seen[d.Code] = d + + if d.HTTPStatus < 400 || d.HTTPStatus > 599 { + t.Errorf("%s: status %d is not a 4xx/5xx error status", d.Code, d.HTTPStatus) + } + if http.StatusText(d.HTTPStatus) == "" { + t.Errorf("%s: status %d is not a registered HTTP status", d.Code, d.HTTPStatus) + } + + if strings.TrimSpace(d.MessageFmt) == "" { + t.Errorf("%s: empty message template", d.Code) + } + lower := strings.ToLower(d.MessageFmt) + for _, marker := range internalMarkers { + if strings.Contains(lower, marker) { + t.Errorf("%s: message template contains internal marker %q: %q", d.Code, marker, d.MessageFmt) + } + } + + verbs := len(fmtVerb.FindAllString(strings.ReplaceAll(d.MessageFmt, "%%", ""), -1)) + want, declared := messageArity[d.Code] + if verbs > 0 && !declared { + t.Errorf("%s: template has %d fmt verb(s) but no entry in messageArity", d.Code, verbs) + } + if declared && verbs != want { + t.Errorf("%s: messageArity declares %d arg(s) but template has %d verb(s)", d.Code, want, verbs) + } + } +} + +// TestUnauthorizedIsUnified pins the unified-auth-failure rule: exactly one +// 401 entry exists and its message is the standard generic one, so no auth +// path can leak why authentication failed. +func TestUnauthorizedIsUnified(t *testing.T) { + var unauthorized []Def + for _, d := range allDefs { + if d.HTTPStatus == http.StatusUnauthorized { + unauthorized = append(unauthorized, d) + } + } + if len(unauthorized) != 1 { + t.Fatalf("expected exactly one 401 catalog entry, found %d", len(unauthorized)) + } + if unauthorized[0].MessageFmt != "Invalid or expired credentials." { + t.Errorf("401 message must be the standard generic one, got %q", unauthorized[0].MessageFmt) + } + if strings.Contains(unauthorized[0].MessageFmt, "%") { + t.Error("401 message must not be parameterizable") + } +} diff --git a/platform-api/internal/apperror/def.go b/platform-api/internal/apperror/def.go new file mode 100644 index 0000000000..398bf28dea --- /dev/null +++ b/platform-api/internal/apperror/def.go @@ -0,0 +1,69 @@ +/* + * 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" + "fmt" +) + +// Def is a catalog entry: the code, HTTP status, and client-facing message +// template are bound together at declaration time (see catalog.go) so call +// sites cannot pair a code with the wrong status or a drifting message. It is +// the Go equivalent of a Java ExceptionCodes enum constant. Errors are +// created only through Def.New / Def.Wrap (plus NewValidation for +// validator.ValidationErrors), never by picking a code/status/message triple +// at the call site. +type Def struct { + Code string // stable, client-visible catalog code, e.g. "REST_API_EXISTS" + HTTPStatus int + MessageFmt string // client-facing message template; may contain fmt verbs +} + +// New instantiates the catalog entry for a business-rule failure with no +// underlying error. args fill the MessageFmt verbs, if any; they must be +// user-supplied values (names, handles, IDs), never internal error text. The +// call stack is captured automatically. +func (d Def) New(args ...any) *Error { + return newWithSkip(3, d.Code, d.HTTPStatus, d.message(args)) +} + +// Wrap instantiates the catalog entry for a failure caused by a lower-level +// error (DB, IO, downstream call). The cause is a required positional +// parameter — not a fluent afterthought — so it cannot be forgotten; it feeds +// the mapper's log line and errors.Is/As chains, and is never sent to the +// client. +func (d Def) Wrap(cause error, args ...any) *Error { + e := newWithSkip(3, d.Code, d.HTTPStatus, d.message(args)) + e.Cause = cause + return e +} + +// Is reports whether err is (or wraps) an *Error carrying this Def's code, +// letting callers branch on catalog entries without sentinel errors. +func (d Def) Is(err error) bool { + var e *Error + return errors.As(err, &e) && e.Code == d.Code +} + +func (d Def) message(args []any) string { + if len(args) == 0 { + return d.MessageFmt + } + return fmt.Sprintf(d.MessageFmt, args...) +} diff --git a/platform-api/internal/apperror/write.go b/platform-api/internal/apperror/write.go new file mode 100644 index 0000000000..51d8402ac9 --- /dev/null +++ b/platform-api/internal/apperror/write.go @@ -0,0 +1,45 @@ +/* + * 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 ( + "net/http" + + "github.com/wso2/api-platform/platform-api/internal/utils" + + "github.com/wso2/go-httpkit/httputil" +) + +// WriteHTTP is the one place in the codebase that serializes an *Error onto +// an http.ResponseWriter as the standard utils.ErrorResponse shape. Both the +// error mapper (middleware.MapErrors) and the pre-routing auth middleware +// call it, so the wire format cannot drift between the two paths. +// +// trackingID, when non-empty, is echoed in the response body so the client +// can quote it back for log correlation; the mapper passes it only for 5xx +// responses. Callers with no correlation ID to expose pass "". +func WriteHTTP(w http.ResponseWriter, e *Error, trackingID string) { + httputil.WriteJSON(w, e.HTTPStatus, utils.ErrorResponse{ + Status: "error", + Code: e.Code, + Message: e.Message, + Errors: e.FieldErrors, + Details: e.Details, + TrackingID: trackingID, + }) +} diff --git a/platform-api/internal/handler/api.go b/platform-api/internal/handler/api.go index 15a2f9a5c8..55a3c12400 100644 --- a/platform-api/internal/handler/api.go +++ b/platform-api/internal/handler/api.go @@ -20,14 +20,16 @@ package handler import ( "encoding/json" "errors" + "fmt" "log/slog" "net/http" + "strings" + "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/service" - "github.com/wso2/api-platform/platform-api/internal/utils" - "strings" "github.com/wso2/go-httpkit/httputil" ) @@ -47,191 +49,143 @@ func NewAPIHandler(apiService *service.APIService, identity *service.IdentitySer } // CreateAPI handles POST /api/v0.9/rest-apis and creates a new API -func (h *APIHandler) CreateAPI(w http.ResponseWriter, r *http.Request) { +func (h *APIHandler) CreateAPI(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } var req api.CreateRESTAPIRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - utils.NewValidationErrorResponse(w, err) - return + return apperror.NewValidation(err) } // Validate required fields if req.DisplayName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API name is required")) - return + return apperror.ValidationFailed.New("API name is required") } if req.Context == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API context is required")) - return + return apperror.ValidationFailed.New("API context is required") } if req.Version == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API version is required")) - return + return apperror.ValidationFailed.New("API version is required") } if strings.TrimSpace(req.ProjectId) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Project ID is required")) - return + return apperror.ValidationFailed.New("Project ID is required") } if isEmptyUpstreamDefinition(req.Upstream.Main) && (req.Upstream.Sandbox == nil || isEmptyUpstreamDefinition(*req.Upstream.Sandbox)) { - h.slogger.Error("Validation failed: No upstream endpoints provided", "organizationId", orgId) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "At least one upstream endpoint (main or sandbox) is required")) - return + return apperror.ValidationFailed.New("At least one upstream endpoint (main or sandbox) is required"). + WithLogMessage(fmt.Sprintf("no upstream endpoints provided for org %s", orgId)) } - createdBy, ok := resolveActor(w, r, h.identity, h.slogger, "create API") - if !ok { - return + createdBy, err := resolveActorErr(r, h.identity, "create API") + if err != nil { + return err } apiResponse, err := h.apiService.CreateAPI(&req, orgId, createdBy) if err != nil { if errors.Is(err, constants.ErrHandleExists) { - h.slogger.Error("API handle already exists", "organizationId", orgId) - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeRESTAPIExists, "An API with this handle already exists.")) - return + 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) { - h.slogger.Error("API with same name and version already exists", "organizationId", orgId) - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeRESTAPIExists, "An API with the same name and version already exists in the organization.")) - return + 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) { - h.slogger.Error("API already exists in the project", "organizationId", orgId) - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeRESTAPIExists, "An API already exists in the project.")) - return + 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) { - h.slogger.Error("Project not found", "organizationId", orgId) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeProjectNotFound, "The specified project could not be found.")) - return + return apperror.ProjectNotFound.Wrap(err). + WithLogMessage(fmt.Sprintf("project not found in org %s", orgId)) } if errors.Is(err, constants.ErrInvalidAPIName) { - h.slogger.Error("Invalid API name format", "organizationId", orgId) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid API name format")) - return + 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) { - h.slogger.Error("Invalid API context format", "organizationId", orgId) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid API context format")) - return + 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) { - h.slogger.Error("Invalid API version format", "organizationId", orgId) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid API version format")) - return + 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) { - h.slogger.Error("Invalid lifecycle status", "organizationId", orgId) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid lifecycle status")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid lifecycle status"). + WithLogMessage(fmt.Sprintf("invalid lifecycle status in org %s", orgId)) } if errors.Is(err, constants.ErrInvalidAPIType) { - h.slogger.Error("Invalid API type", "organizationId", orgId) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid API type")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid API type"). + WithLogMessage(fmt.Sprintf("invalid API type in org %s", orgId)) } if errors.Is(err, constants.ErrInvalidTransport) { - h.slogger.Error("Invalid transport protocol", "organizationId", orgId) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid transport protocol")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid transport protocol"). + WithLogMessage(fmt.Sprintf("invalid transport protocol in org %s", orgId)) } if errors.Is(err, constants.ErrSubscriptionPlanNotFoundOrInactive) { - h.slogger.Error("Subscription plan not found or not active", "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, err.Error())) - return + return apperror.ValidationFailed.Wrap(err, err.Error()). + WithLogMessage(fmt.Sprintf("subscription plan not found or not active in org %s", orgId)) } - h.slogger.Error("Failed to create API", "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to create API")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to create API in org %s", orgId)) } httputil.WriteJSON(w, http.StatusCreated, apiResponse) + return nil } // GetAPI handles GET /api/v0.9/rest-apis/:apiId and retrieves an API by its handle -func (h *APIHandler) GetAPI(w http.ResponseWriter, r *http.Request) { +func (h *APIHandler) GetAPI(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } apiId := r.PathValue("restApiId") if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API ID is required")) - return + return apperror.ValidationFailed.New("API ID is required") } apiResponse, err := h.apiService.GetAPIByHandle(apiId, orgId) if err != nil { if errors.Is(err, constants.ErrAPINotFound) { - h.slogger.Error("API not found", "apiId", apiId, "organizationId", orgId) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) - return + return apperror.RESTAPINotFound.Wrap(err). + WithLogMessage(fmt.Sprintf("API %s not found in org %s", apiId, orgId)) } - h.slogger.Error("Failed to get API", "apiId", apiId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to get API")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to get API %s in org %s", apiId, orgId)) } httputil.WriteJSON(w, http.StatusOK, apiResponse) + return nil } // ListAPIs handles GET /api/v0.9/rest-apis and lists APIs for an organization filtered by project -func (h *APIHandler) ListAPIs(w http.ResponseWriter, r *http.Request) { +func (h *APIHandler) ListAPIs(w http.ResponseWriter, r *http.Request) error { // Get organization from JWT token orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } projectId := strings.TrimSpace(r.URL.Query().Get("projectId")) if projectId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "projectId query parameter is required")) - return + return apperror.ValidationFailed.New("projectId query parameter is required") } apis, err := h.apiService.GetAPIsByOrganization(orgId, projectId) if err != nil { if errors.Is(err, constants.ErrProjectNotFound) { - h.slogger.Error("Project not found", "organizationId", orgId, "projectId", projectId) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeProjectNotFound, "The specified project could not be found.")) - return + return apperror.ProjectNotFound.Wrap(err). + WithLogMessage(fmt.Sprintf("project %s not found in org %s", projectId, orgId)) } - h.slogger.Error("Failed to get APIs", "organizationId", orgId, "projectId", projectId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to get APIs")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to get APIs for project %s in org %s", projectId, orgId)) } response := api.RESTAPIListResponse{ @@ -245,156 +199,125 @@ func (h *APIHandler) ListAPIs(w http.ResponseWriter, r *http.Request) { } httputil.WriteJSON(w, http.StatusOK, response) + return nil } // UpdateAPI updates an existing API identified by handle -func (h *APIHandler) UpdateAPI(w http.ResponseWriter, r *http.Request) { +func (h *APIHandler) UpdateAPI(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } apiId := r.PathValue("restApiId") if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API ID is required")) - return + return apperror.ValidationFailed.New("API ID is required") } var req api.RESTAPI if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - utils.NewValidationErrorResponse(w, err) - return + return apperror.NewValidation(err) } // Validate upstream configuration if provided if isEmptyUpstreamDefinition(req.Upstream.Main) && (req.Upstream.Sandbox == nil || isEmptyUpstreamDefinition(*req.Upstream.Sandbox)) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "At least one upstream endpoint (main or sandbox) is required")) - return + return apperror.ValidationFailed.New("At least one upstream endpoint (main or sandbox) is required") } - updatedBy, ok := resolveActor(w, r, h.identity, h.slogger, "update API") - if !ok { - return + updatedBy, err := resolveActorErr(r, h.identity, "update API") + if err != nil { + return err } apiResponse, err := h.apiService.UpdateAPIByHandle(apiId, &req, orgId, updatedBy) if err != nil { - if respondArtifactGuardError(w, err) { - return + if guardErr := mapArtifactGuardError(err); guardErr != nil { + return guardErr } if errors.Is(err, constants.ErrAPINotFound) { - h.slogger.Error("API not found", "apiId", apiId, "organizationId", orgId) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) - return + return apperror.RESTAPINotFound.Wrap(err). + WithLogMessage(fmt.Sprintf("API %s not found in org %s", apiId, orgId)) } if errors.Is(err, constants.ErrHandleImmutable) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, err.Error())) - return + return apperror.ValidationFailed.Wrap(err, "The id is immutable and cannot be changed") } if errors.Is(err, constants.ErrInvalidLifecycleState) { - h.slogger.Error("Invalid lifecycle status", "apiId", apiId, "organizationId", orgId) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid lifecycle status")) - return + 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) { - h.slogger.Error("Invalid API type", "apiId", apiId, "organizationId", orgId) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid API type")) - return + 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) { - h.slogger.Error("Invalid transport protocol", "apiId", apiId, "organizationId", orgId) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid transport protocol")) - return + 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.ErrSubscriptionPlanNotFoundOrInactive) { - h.slogger.Error("Subscription plan not found or not active", "apiId", apiId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, err.Error())) - return + return apperror.ValidationFailed.Wrap(err, err.Error()). + WithLogMessage(fmt.Sprintf("subscription plan not found or not active for API %s in org %s", apiId, orgId)) } - h.slogger.Error("Failed to update API", "apiId", apiId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to update API")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to update API %s in org %s", apiId, orgId)) } httputil.WriteJSON(w, http.StatusOK, apiResponse) + return nil } // DeleteAPI handles DELETE /api/v0.9/rest-apis/:apiId and deletes an API by its handle -func (h *APIHandler) DeleteAPI(w http.ResponseWriter, r *http.Request) { +func (h *APIHandler) DeleteAPI(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } apiId := r.PathValue("restApiId") if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API ID is required")) - return + return apperror.ValidationFailed.New("API ID is required") } - deletedBy, ok := resolveActor(w, r, h.identity, h.slogger, "delete API") - if !ok { - return - } - err := h.apiService.DeleteAPIByHandle(apiId, orgId, deletedBy) + deletedBy, err := resolveActorErr(r, h.identity, "delete API") if err != nil { - if respondArtifactGuardError(w, err) { - return + 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) { - h.slogger.Error("API not found", "apiId", apiId, "organizationId", orgId) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) - return + return apperror.RESTAPINotFound.Wrap(err). + WithLogMessage(fmt.Sprintf("API %s not found in org %s", apiId, orgId)) } - h.slogger.Error("Failed to delete API", "apiId", apiId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to delete API")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to delete API %s in org %s", apiId, orgId)) } httputil.WriteJSON(w, http.StatusNoContent, nil) + return nil } // AddGatewaysToAPI handles POST /api/v0.9/rest-apis/:apiId/gateways to associate gateways with an API -func (h *APIHandler) AddGatewaysToAPI(w http.ResponseWriter, r *http.Request) { +func (h *APIHandler) AddGatewaysToAPI(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } apiId := r.PathValue("restApiId") if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API ID is required")) - return + return apperror.ValidationFailed.New("API ID is required") } var req []api.AddGatewayToRESTAPIRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - utils.NewValidationErrorResponse(w, err) - return + return apperror.NewValidation(err) } if len(req) == 0 { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "At least one gateway ID is required")) - return + return apperror.ValidationFailed.New("At least one gateway ID is required") } // Extract gateway IDs from request @@ -406,70 +329,59 @@ func (h *APIHandler) AddGatewaysToAPI(w http.ResponseWriter, r *http.Request) { gatewaysResponse, err := h.apiService.AddGatewaysToAPIByHandle(apiId, gatewayIds, orgId) if err != nil { if errors.Is(err, constants.ErrAPINotFound) { - h.slogger.Error("API not found", "apiId", apiId, "organizationId", orgId) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) - return + return apperror.RESTAPINotFound.Wrap(err). + WithLogMessage(fmt.Sprintf("API %s not found in org %s", apiId, orgId)) } if errors.Is(err, constants.ErrGatewayNotFound) { - h.slogger.Error("One or more gateways not found", "apiId", apiId, "organizationId", orgId) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayNotFound, "One or more of the specified gateways could not be found.")) - return + return apperror.GatewayNotFound.Wrap(err). + WithLogMessage(fmt.Sprintf("one or more gateways not found for API %s in org %s", apiId, orgId)) } - h.slogger.Error("Failed to associate gateways with API", "apiId", apiId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to associate gateways with API")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to associate gateways with API %s in org %s", apiId, orgId)) } httputil.WriteJSON(w, http.StatusOK, gatewaysResponse) + return nil } // GetAPIGateways handles GET /api/v0.9/rest-apis/:apiId/gateways to get gateways associated with an API including deployment details -func (h *APIHandler) GetAPIGateways(w http.ResponseWriter, r *http.Request) { +func (h *APIHandler) GetAPIGateways(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } apiId := r.PathValue("restApiId") if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API ID is required")) - return + return apperror.ValidationFailed.New("API ID is required") } gatewaysResponse, err := h.apiService.GetAPIGatewaysByHandle(apiId, orgId) if err != nil { if errors.Is(err, constants.ErrAPINotFound) { - h.slogger.Error("API not found", "apiId", apiId, "organizationId", orgId) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) - return + return apperror.RESTAPINotFound.Wrap(err). + WithLogMessage(fmt.Sprintf("API %s not found in org %s", apiId, orgId)) } - h.slogger.Error("Failed to get API gateways", "apiId", apiId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to get API gateways")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to get gateways for API %s in org %s", apiId, orgId)) } httputil.WriteJSON(w, http.StatusOK, gatewaysResponse) + return nil } // RegisterRoutes registers all API routes func (h *APIHandler) RegisterRoutes(mux *http.ServeMux) { h.slogger.Debug("Registering REST API routes") base := constants.APIBasePath + "/rest-apis" - mux.HandleFunc("POST "+base, h.CreateAPI) - mux.HandleFunc("GET "+base, h.ListAPIs) - mux.HandleFunc("GET "+base+"/{restApiId}", h.GetAPI) - mux.HandleFunc("PUT "+base+"/{restApiId}", h.UpdateAPI) - mux.HandleFunc("DELETE "+base+"/{restApiId}", h.DeleteAPI) - mux.HandleFunc("GET "+base+"/{restApiId}/gateways", h.GetAPIGateways) - mux.HandleFunc("POST "+base+"/{restApiId}/gateways", h.AddGatewaysToAPI) + mux.HandleFunc("POST "+base, middleware.MapErrors(h.slogger, h.CreateAPI)) + mux.HandleFunc("GET "+base, middleware.MapErrors(h.slogger, h.ListAPIs)) + mux.HandleFunc("GET "+base+"/{restApiId}", middleware.MapErrors(h.slogger, h.GetAPI)) + mux.HandleFunc("PUT "+base+"/{restApiId}", middleware.MapErrors(h.slogger, h.UpdateAPI)) + mux.HandleFunc("DELETE "+base+"/{restApiId}", middleware.MapErrors(h.slogger, h.DeleteAPI)) + mux.HandleFunc("GET "+base+"/{restApiId}/gateways", middleware.MapErrors(h.slogger, h.GetAPIGateways)) + mux.HandleFunc("POST "+base+"/{restApiId}/gateways", middleware.MapErrors(h.slogger, h.AddGatewaysToAPI)) } func isEmptyUpstreamDefinition(definition api.UpstreamDefinition) bool { diff --git a/platform-api/internal/handler/api_deployment.go b/platform-api/internal/handler/api_deployment.go index 1c89e9b841..682c65a445 100644 --- a/platform-api/internal/handler/api_deployment.go +++ b/platform-api/internal/handler/api_deployment.go @@ -20,15 +20,16 @@ package handler import ( "encoding/json" "errors" + "fmt" "log/slog" "net/http" "strings" "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/service" - "github.com/wso2/api-platform/platform-api/internal/utils" "github.com/wso2/go-httpkit/httputil" ) @@ -49,369 +50,277 @@ func NewDeploymentHandler(deploymentService *service.DeploymentService, identity // DeployAPI handles POST /api/v0.9/rest-apis/:apiId/deployments // Creates a new immutable deployment artifact and deploys it to a gateway -func (h *DeploymentHandler) DeployAPI(w http.ResponseWriter, r *http.Request) { +func (h *DeploymentHandler) DeployAPI(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } apiId := r.PathValue("restApiId") if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API ID is required")) - return + return apperror.ValidationFailed.New("API ID is required") } var req api.DeployRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, err.Error())) - return + return apperror.NewValidation(err) } // Validate required fields if req.Name == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeRESTAPIDeploymentValidationFailed, "name is required")) - return + return apperror.RESTAPIDeploymentValidationFailed.New("name is required") } if req.Base == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeRESTAPIDeploymentValidationFailed, "base is required (use 'current' or a deploymentId)")) - return + return apperror.RESTAPIDeploymentValidationFailed.New("base is required (use 'current' or a deploymentId)") } if strings.TrimSpace(req.GatewayId) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeRESTAPIDeploymentValidationFailed, "gatewayId is required")) - return + return apperror.RESTAPIDeploymentValidationFailed.New("gatewayId is required") } - createdBy, ok := resolveActor(w, r, h.identity, h.slogger, "deploy API") - if !ok { - return + createdBy, err := resolveActorErr(r, h.identity, "deploy API") + if err != nil { + return err } deployment, err := h.deploymentService.DeployAPIByHandle(apiId, &req, orgId, createdBy) if err != nil { - if respondArtifactGuardError(w, err) { - return + if guardErr := mapArtifactGuardError(err); guardErr != nil { + return guardErr } if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) - return + return apperror.RESTAPINotFound.Wrap(err) } if errors.Is(err, constants.ErrGatewayNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayNotFound, "The specified gateway could not be found.")) - return + return apperror.GatewayNotFound.Wrap(err) } if errors.Is(err, constants.ErrBaseDeploymentNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeDeploymentBaseNotFound, "The specified base deployment could not be found.")) - return + return apperror.DeploymentBaseNotFound.Wrap(err) } if errors.Is(err, constants.ErrDeploymentNameRequired) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeRESTAPIDeploymentValidationFailed, "Deployment name is required")) - return + return apperror.RESTAPIDeploymentValidationFailed.Wrap(err, "Deployment name is required") } if errors.Is(err, constants.ErrDeploymentBaseRequired) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeRESTAPIDeploymentValidationFailed, "Base is required (use 'current' or a deploymentId)")) - return + return apperror.RESTAPIDeploymentValidationFailed.Wrap(err, "Base is required (use 'current' or a deploymentId)") } if errors.Is(err, constants.ErrDeploymentGatewayIDRequired) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeRESTAPIDeploymentValidationFailed, "Gateway ID is required")) - return + return apperror.RESTAPIDeploymentValidationFailed.Wrap(err, "Gateway ID is required") } if errors.Is(err, constants.ErrAPINoBackendServices) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeRESTAPIDeploymentValidationFailed, "API must have at least one backend service attached before deployment")) - return + return apperror.RESTAPIDeploymentValidationFailed.Wrap(err, "API must have at least one backend service attached before deployment") } - h.slogger.Error("Failed to deploy API", "apiId", apiId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to deploy API")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to deploy API %s", apiId)) } httputil.WriteJSON(w, http.StatusCreated, deployment) + return nil } // UndeployDeployment handles POST /api/v0.9/rest-apis/:apiId/deployments/:deploymentId/undeploy -func (h *DeploymentHandler) UndeployDeployment(w http.ResponseWriter, r *http.Request) { +func (h *DeploymentHandler) UndeployDeployment(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } apiId := r.PathValue("restApiId") deploymentId := r.PathValue("deploymentId") gatewayId := r.URL.Query().Get("gatewayId") if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "deploymentId is required")) - return + return apperror.ValidationFailed.New("deploymentId is required") } if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "gatewayId is required")) - return + return apperror.ValidationFailed.New("gatewayId is required") } if deploymentId == "00000000-0000-0000-0000-000000000000" || gatewayId == "00000000-0000-0000-0000-000000000000" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "deploymentId/gatewayId cannot be zero-value UUID")) - return + return apperror.ValidationFailed.New("deploymentId/gatewayId cannot be zero-value UUID") } if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API ID is required")) - return + return apperror.ValidationFailed.New("API ID is required") } - actor, ok := resolveActor(w, r, h.identity, h.slogger, "undeploy API") - if !ok { - return + actor, err := resolveActorErr(r, h.identity, "undeploy API") + if err != nil { + return err } 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 respondArtifactGuardError(w, err) { - return + if guardErr := mapArtifactGuardError(err); guardErr != nil { + return guardErr } if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) - return + return apperror.RESTAPINotFound.Wrap(err) } if errors.Is(err, constants.ErrDeploymentNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) - return + return apperror.DeploymentNotFound.Wrap(err) } if errors.Is(err, constants.ErrGatewayNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayNotFound, "The specified gateway could not be found.")) - return + return apperror.GatewayNotFound.Wrap(err) } if errors.Is(err, constants.ErrDeploymentNotActive) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeDeploymentNotActive, "No active deployment found for this API on the gateway.")) - return + return apperror.DeploymentNotActive.Wrap(err, "API") } if errors.Is(err, constants.ErrGatewayIDMismatch) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeDeploymentGatewayMismatch, "Deployment is bound to a different gateway.")) - return + return apperror.DeploymentGatewayMismatch.Wrap(err) } - h.slogger.Error("Failed to undeploy", "apiId", apiId, "deploymentId", deploymentId, "gatewayId", gatewayId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to undeploy deployment")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to undeploy API %s deployment %s from gateway %s", apiId, deploymentId, gatewayId)) } httputil.WriteJSON(w, http.StatusOK, deployment) + return nil } // RestoreDeployment handles POST /api/v0.9/rest-apis/:apiId/deployments/:deploymentId/restore -func (h *DeploymentHandler) RestoreDeployment(w http.ResponseWriter, r *http.Request) { +func (h *DeploymentHandler) RestoreDeployment(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } apiId := r.PathValue("restApiId") deploymentId := r.PathValue("deploymentId") gatewayId := r.URL.Query().Get("gatewayId") if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "deploymentId is required")) - return + return apperror.ValidationFailed.New("deploymentId is required") } if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "gatewayId is required")) - return + return apperror.ValidationFailed.New("gatewayId is required") } if deploymentId == "00000000-0000-0000-0000-000000000000" || gatewayId == "00000000-0000-0000-0000-000000000000" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "deploymentId/gatewayId cannot be zero-value UUID")) - return + return apperror.ValidationFailed.New("deploymentId/gatewayId cannot be zero-value UUID") } if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API ID is required")) - return + return apperror.ValidationFailed.New("API ID is required") } - actor, ok := resolveActor(w, r, h.identity, h.slogger, "restore API deployment") - if !ok { - return + actor, err := resolveActorErr(r, h.identity, "restore API deployment") + if err != nil { + return err } 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 respondArtifactGuardError(w, err) { - return + if guardErr := mapArtifactGuardError(err); guardErr != nil { + return guardErr } if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) - return + return apperror.RESTAPINotFound.Wrap(err) } if errors.Is(err, constants.ErrDeploymentNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) - return + return apperror.DeploymentNotFound.Wrap(err) } if errors.Is(err, constants.ErrGatewayNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayNotFound, "The specified gateway could not be found.")) - return + return apperror.GatewayNotFound.Wrap(err) } if errors.Is(err, constants.ErrDeploymentAlreadyDeployed) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeDeploymentRestoreConflict, "Cannot restore the currently deployed deployment, or the deployment is invalid.")) - return + return apperror.DeploymentRestoreConflict.Wrap(err) } if errors.Is(err, constants.ErrGatewayIDMismatch) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeDeploymentGatewayMismatch, "Deployment is bound to a different gateway.")) - return + return apperror.DeploymentGatewayMismatch.Wrap(err) } - h.slogger.Error("Failed to restore deployment", "apiId", apiId, "deploymentId", deploymentId, "gatewayId", gatewayId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to restore deployment")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to restore API %s deployment %s on gateway %s", apiId, deploymentId, gatewayId)) } httputil.WriteJSON(w, http.StatusOK, deployment) + return nil } // DeleteDeployment handles DELETE /api/v0.9/rest-apis/:apiId/deployments/:deploymentId // Permanently deletes an undeployed deployment artifact -func (h *DeploymentHandler) DeleteDeployment(w http.ResponseWriter, r *http.Request) { +func (h *DeploymentHandler) DeleteDeployment(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } apiId := r.PathValue("restApiId") deploymentId := r.PathValue("deploymentId") if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API ID is required")) - return + return apperror.ValidationFailed.New("API ID is required") } if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Deployment ID is required")) - return + return apperror.ValidationFailed.New("Deployment ID is required") } - actor, ok := resolveActor(w, r, h.identity, h.slogger, "delete API deployment") - if !ok { - return - } - err := h.deploymentService.DeleteDeploymentByHandle(apiId, deploymentId, orgId, actor) + actor, err := resolveActorErr(r, h.identity, "delete API deployment") if err != nil { + return err + } + if err := h.deploymentService.DeleteDeploymentByHandle(apiId, deploymentId, orgId, actor); err != nil { if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) - return + return apperror.RESTAPINotFound.Wrap(err) } if errors.Is(err, constants.ErrDeploymentNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) - return + return apperror.DeploymentNotFound.Wrap(err) } if errors.Is(err, constants.ErrDeploymentIsDeployed) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeDeploymentActive, "Cannot delete an active deployment - undeploy it first.")) - return + return apperror.DeploymentActive.Wrap(err) } - if respondArtifactGuardError(w, err) { - return + if guardErr := mapArtifactGuardError(err); guardErr != nil { + return guardErr } - h.slogger.Error("Failed to delete deployment", "apiId", apiId, "deploymentId", deploymentId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to delete deployment")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to delete API %s deployment %s", apiId, deploymentId)) } w.WriteHeader(http.StatusNoContent) + return nil } // GetDeployment handles GET /api/v0.9/rest-apis/:apiId/deployments/:deploymentId // Retrieves metadata for a specific deployment artifact -func (h *DeploymentHandler) GetDeployment(w http.ResponseWriter, r *http.Request) { +func (h *DeploymentHandler) GetDeployment(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } apiId := r.PathValue("restApiId") deploymentId := r.PathValue("deploymentId") if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API ID is required")) - return + return apperror.ValidationFailed.New("API ID is required") } if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Deployment ID is required")) - return + return apperror.ValidationFailed.New("Deployment ID is required") } deployment, err := h.deploymentService.GetDeploymentByHandle(apiId, deploymentId, orgId) if err != nil { if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) - return + return apperror.RESTAPINotFound.Wrap(err) } if errors.Is(err, constants.ErrDeploymentNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) - return + return apperror.DeploymentNotFound.Wrap(err) } - h.slogger.Error("Failed to get deployment", "apiId", apiId, "deploymentId", deploymentId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to retrieve deployment")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to get API %s deployment %s", apiId, deploymentId)) } httputil.WriteJSON(w, http.StatusOK, deployment) + return nil } // GetDeployments handles GET /api/v0.9/rest-apis/:apiId/deployments // Retrieves all deployment records for an API with optional filters -func (h *DeploymentHandler) GetDeployments(w http.ResponseWriter, r *http.Request) { +func (h *DeploymentHandler) GetDeployments(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } apiId := r.PathValue("restApiId") if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API ID is required")) - return + return apperror.ValidationFailed.New("API ID is required") } var params api.GetDeploymentsParams @@ -435,32 +344,27 @@ 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) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeRESTAPINotFound, "The specified REST API could not be found.")) - return + return apperror.RESTAPINotFound.Wrap(err) } if errors.Is(err, constants.ErrInvalidDeploymentStatus) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeDeploymentInvalidStatus, "The specified deployment status filter is invalid.")) - return + return apperror.DeploymentInvalidStatus.Wrap(err) } - h.slogger.Error("Failed to get deployments", "apiId", apiId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to retrieve deployments")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to get deployments for API %s", apiId)) } httputil.WriteJSON(w, http.StatusOK, deployments) + return nil } // RegisterRoutes registers all deployment-related routes func (h *DeploymentHandler) RegisterRoutes(mux *http.ServeMux) { h.slogger.Debug("Registering deployment routes") base := constants.APIBasePath + "/rest-apis/{restApiId}" - mux.HandleFunc("POST "+base+"/deployments", h.DeployAPI) - mux.HandleFunc("POST "+base+"/deployments/{deploymentId}/undeploy", h.UndeployDeployment) - mux.HandleFunc("POST "+base+"/deployments/{deploymentId}/restore", h.RestoreDeployment) - mux.HandleFunc("GET "+base+"/deployments", h.GetDeployments) - mux.HandleFunc("GET "+base+"/deployments/{deploymentId}", h.GetDeployment) - mux.HandleFunc("DELETE "+base+"/deployments/{deploymentId}", h.DeleteDeployment) + mux.HandleFunc("POST "+base+"/deployments", middleware.MapErrors(h.slogger, h.DeployAPI)) + mux.HandleFunc("POST "+base+"/deployments/{deploymentId}/undeploy", middleware.MapErrors(h.slogger, h.UndeployDeployment)) + mux.HandleFunc("POST "+base+"/deployments/{deploymentId}/restore", middleware.MapErrors(h.slogger, h.RestoreDeployment)) + mux.HandleFunc("GET "+base+"/deployments", middleware.MapErrors(h.slogger, h.GetDeployments)) + mux.HandleFunc("GET "+base+"/deployments/{deploymentId}", middleware.MapErrors(h.slogger, h.GetDeployment)) + mux.HandleFunc("DELETE "+base+"/deployments/{deploymentId}", middleware.MapErrors(h.slogger, h.DeleteDeployment)) } diff --git a/platform-api/internal/handler/api_key.go b/platform-api/internal/handler/api_key.go index 329b4da715..6ef03aac79 100644 --- a/platform-api/internal/handler/api_key.go +++ b/platform-api/internal/handler/api_key.go @@ -25,6 +25,7 @@ import ( "net/http" "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/service" @@ -51,41 +52,34 @@ func NewAPIKeyHandler(apiKeyService *service.APIKeyService, identity *service.Id // CreateAPIKey handles POST /rest-apis/{restApiId}/api-keys // This endpoint allows users to inject external API keys to all the gateways where the API is deployed -func (h *APIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Request) { +func (h *APIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Request) error { // Extract organization from JWT token orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } - userId, ok := resolveActor(w, r, h.identity, h.slogger, "create API key") - if !ok { - return + userId, err := resolveActorErr(r, h.identity, "create API key") + if err != nil { + return err } // Extract API handle from path parameter (parameter named apiId for backward compatibility, but contains handle) apiHandle := r.PathValue("restApiId") if apiHandle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API handle is required")) - return + return apperror.ValidationFailed.New("API handle is required") } // Parse and validate request body var req api.CreateAPIKeyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - h.slogger.Error("Invalid API key creation request", "userId", userId, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid request body")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body"). + WithLogMessage(fmt.Sprintf("invalid API key creation request for user %s", userId)) } if req.ApiKey == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API key value is required")) - return + return apperror.ValidationFailed.New("API key value is required") } // If user has provided an id, use it. Otherwise, generate one from the display name. @@ -95,37 +89,24 @@ func (h *APIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Request) { } else { generatedName, err := utils.GenerateHandle(req.DisplayName, nil) if err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Failed to generate API key name")) - return + return apperror.ValidationFailed.Wrap(err, "Failed to generate API key name") } name = generatedName req.Id = &name } // Create the API key and broadcast to gateways - err := h.apiKeyService.CreateAPIKey(r.Context(), apiHandle, constants.RestApi, orgId, userId, &req) - if err != nil { + if err := h.apiKeyService.CreateAPIKey(r.Context(), apiHandle, constants.RestApi, orgId, userId, &req); err != nil { // Handle specific error cases if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeArtifactNotFound, "The specified artifact could not be found.")) - return + return apperror.ArtifactNotFound.Wrap(err) } if errors.Is(err, constants.ErrGatewayUnavailable) { - httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponseWithCode( - utils.CodeGatewayConnectionUnavailable, "No gateway connections are currently available.")) - return + return apperror.GatewayConnectionUnavailable.Wrap(err) } - keyName := "" - if req.Id != nil { - keyName = *req.Id - } - h.slogger.Error("Failed to create API key", "userId", userId, "apiHandle", apiHandle, "orgId", orgId, "keyName", keyName, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to create API key")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to create API key %q for API %s in org %s by user %s", name, apiHandle, orgId, userId)) } keyName := "" @@ -140,82 +121,65 @@ func (h *APIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Request) { KeyId: req.Id, Message: "API key created and broadcasted to gateways successfully", }) + return nil } // UpdateAPIKey handles PUT /rest-apis/{restApiId}/api-keys/{apiKeyId} // This endpoint allows external platforms to update/regenerate external API keys on hybrid gateways -func (h *APIKeyHandler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) { +func (h *APIKeyHandler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) error { // Extract organization from JWT token orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } - userId, ok := resolveActor(w, r, h.identity, h.slogger, "update API key") - if !ok { - return + userId, err := resolveActorErr(r, h.identity, "update API key") + if err != nil { + return err } // Extract API ID and key name from path parameters apiHandle := r.PathValue("restApiId") if apiHandle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API handle is required")) - return + return apperror.ValidationFailed.New("API handle is required") } keyName := r.PathValue("apiKeyId") if keyName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API key name is required")) - return + return apperror.ValidationFailed.New("API key name is required") } // Parse and validate request body var req api.UpdateAPIKeyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - h.slogger.Warn("Invalid API key update request", "userId", userId, "orgId", orgId, "apiHandle", apiHandle, "keyName", keyName, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid request body: "+err.Error())) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body"). + WithLogMessage(fmt.Sprintf("invalid API key update request for key %s of API %s in org %s by user %s", keyName, apiHandle, orgId, userId)) } // Validate new API key value if req.ApiKey == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API key value is required")) - return + return apperror.ValidationFailed.New("API key value is required") } // Validate that the name in the request body (if provided) matches the URL path parameter if req.Name != nil && *req.Name != "" && *req.Name != keyName { - h.slogger.Warn("API key name mismatch", "userId", userId, "orgId", orgId, "apiHandle", apiHandle, "urlKeyName", keyName, "bodyKeyName", *req.Name) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, fmt.Sprintf("API key name mismatch: name in request body '%s' must match the key name in URL '%s'", *req.Name, keyName))) - return + return apperror.ValidationFailed.New(fmt.Sprintf("API key name mismatch: name in request body '%s' must match the key name in URL '%s'", *req.Name, keyName)). + WithLogMessage(fmt.Sprintf("API key name mismatch for API %s in org %s by user %s", apiHandle, orgId, userId)) } // Update the API key and broadcast to gateways - err := h.apiKeyService.UpdateAPIKey(r.Context(), apiHandle, constants.RestApi, orgId, keyName, userId, &req) - if err != nil { + if err := h.apiKeyService.UpdateAPIKey(r.Context(), apiHandle, constants.RestApi, orgId, keyName, userId, &req); err != nil { // Handle specific error cases if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeArtifactNotFound, "The specified artifact could not be found.")) - return + return apperror.ArtifactNotFound.Wrap(err) } if errors.Is(err, constants.ErrGatewayUnavailable) { - httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponseWithCode( - utils.CodeGatewayConnectionUnavailable, "No gateway connections are currently available.")) - return + return apperror.GatewayConnectionUnavailable.Wrap(err) } - h.slogger.Error("Failed to update API key", "userId", userId, "apiHandle", apiHandle, "orgId", orgId, "keyName", keyName, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to update API key")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to update API key %s for API %s in org %s by user %s", keyName, apiHandle, orgId, userId)) } h.slogger.Info("Successfully updated API key", "userId", userId, "apiHandle", apiHandle, "orgId", orgId, "keyName", keyName) @@ -226,71 +190,61 @@ func (h *APIKeyHandler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) { Message: "API key updated and broadcasted to gateways successfully", KeyId: &keyName, }) + return nil } // RevokeAPIKey handles DELETE /rest-apis/{restApiId}/api-keys/{apiKeyId} // This endpoint allows Cloud APIM to revoke external API keys on hybrid gateways -func (h *APIKeyHandler) RevokeAPIKey(w http.ResponseWriter, r *http.Request) { +func (h *APIKeyHandler) RevokeAPIKey(w http.ResponseWriter, r *http.Request) error { // Extract organization from JWT token orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } // Extract API ID and key name from path parameters apiHandle := r.PathValue("restApiId") if apiHandle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API handle is required")) - return + return apperror.ValidationFailed.New("API handle is required") } keyName := r.PathValue("apiKeyId") if keyName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API key name is required")) - return + return apperror.ValidationFailed.New("API key name is required") } - userId, ok := resolveActor(w, r, h.identity, h.slogger, "revoke API key") - if !ok { - return + userId, err := resolveActorErr(r, h.identity, "revoke API key") + if err != nil { + return err } // Revoke the API key and broadcast to gateways - err := h.apiKeyService.RevokeAPIKey(r.Context(), apiHandle, constants.RestApi, orgId, keyName, userId) - if err != nil { + if err := h.apiKeyService.RevokeAPIKey(r.Context(), apiHandle, constants.RestApi, orgId, keyName, userId); err != nil { // Handle specific error cases if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeArtifactNotFound, "The specified artifact could not be found.")) - return + return apperror.ArtifactNotFound.Wrap(err) } if errors.Is(err, constants.ErrGatewayUnavailable) { - httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponseWithCode( - utils.CodeGatewayConnectionUnavailable, "No gateway connections are currently available.")) - return + return apperror.GatewayConnectionUnavailable.Wrap(err) } - h.slogger.Error("Failed to revoke API key", "userId", userId, "apiHandle", apiHandle, "orgId", orgId, "keyName", keyName, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to revoke API key in one or more gateways")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to revoke API key %s for API %s in org %s by user %s", keyName, apiHandle, orgId, userId)) } h.slogger.Info("Successfully revoked API key", "userId", userId, "apiHandle", apiHandle, "orgId", orgId, "keyName", keyName) // Return success response (204 No Content) w.WriteHeader(http.StatusNoContent) + return nil } // RegisterRoutes registers API key routes with the router func (h *APIKeyHandler) RegisterRoutes(mux *http.ServeMux) { h.slogger.Debug("Registering API key routes") base := constants.APIBasePath + "/rest-apis/{restApiId}/api-keys" - mux.HandleFunc("POST "+base, h.CreateAPIKey) - mux.HandleFunc("PUT "+base+"/{apiKeyId}", h.UpdateAPIKey) - mux.HandleFunc("DELETE "+base+"/{apiKeyId}", h.RevokeAPIKey) + mux.HandleFunc("POST "+base, middleware.MapErrors(h.slogger, h.CreateAPIKey)) + mux.HandleFunc("PUT "+base+"/{apiKeyId}", middleware.MapErrors(h.slogger, h.UpdateAPIKey)) + mux.HandleFunc("DELETE "+base+"/{apiKeyId}", middleware.MapErrors(h.slogger, h.RevokeAPIKey)) } diff --git a/platform-api/internal/handler/apikey_user.go b/platform-api/internal/handler/apikey_user.go index e1744169fb..2f84529f35 100644 --- a/platform-api/internal/handler/apikey_user.go +++ b/platform-api/internal/handler/apikey_user.go @@ -22,10 +22,10 @@ import ( "net/http" "strings" + "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/service" - "github.com/wso2/api-platform/platform-api/internal/utils" "github.com/wso2/go-httpkit/httputil" ) @@ -47,17 +47,16 @@ func NewAPIKeyUserHandler(apiKeyUserService *service.APIKeyUserService, identity } // ListUserAPIKeys handles GET /api/v0.9/me/api-keys -func (h *APIKeyUserHandler) ListUserAPIKeys(w http.ResponseWriter, r *http.Request) { +func (h *APIKeyUserHandler) ListUserAPIKeys(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } - callerUserID, ok := resolveActor(w, r, h.identity, h.slogger, "list user API keys") - if !ok { - return + callerUserID, err := resolveActorErr(r, h.identity, "list user API keys") + if err != nil { + return err } var types []string @@ -67,16 +66,15 @@ func (h *APIKeyUserHandler) ListUserAPIKeys(w http.ResponseWriter, r *http.Reque response, err := h.apiKeyUserService.ListAPIKeysByUser(r.Context(), orgID, callerUserID, types) if err != nil { - h.slogger.Error("Failed to list API keys for user", "orgId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to list API keys")) - return + return apperror.Internal.Wrap(err). + WithLogMessage("failed to list API keys for user in org " + orgID) } httputil.WriteJSON(w, http.StatusOK, response) + return nil } // RegisterRoutes registers the user API key routes. func (h *APIKeyUserHandler) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("GET "+constants.APIBasePath+"/me/api-keys", h.ListUserAPIKeys) + mux.HandleFunc("GET "+constants.APIBasePath+"/me/api-keys", middleware.MapErrors(h.slogger, h.ListUserAPIKeys)) } diff --git a/platform-api/internal/handler/application.go b/platform-api/internal/handler/application.go index e76cfdc631..a710a6171e 100644 --- a/platform-api/internal/handler/application.go +++ b/platform-api/internal/handler/application.go @@ -20,16 +20,17 @@ package handler import ( "encoding/json" "errors" + "fmt" "log/slog" "net/http" "strconv" "strings" "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/service" - "github.com/wso2/api-platform/platform-api/internal/utils" "github.com/wso2/go-httpkit/httputil" ) @@ -44,86 +45,74 @@ func NewApplicationHandler(applicationService *service.ApplicationService, ident return &ApplicationHandler{applicationService: applicationService, identity: identity, slogger: slogger} } -func (h *ApplicationHandler) CreateApplication(w http.ResponseWriter, r *http.Request) { +func (h *ApplicationHandler) CreateApplication(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } var req api.CreateApplicationRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - utils.NewValidationErrorResponse(w, err) - return + return apperror.NewValidation(err) } if strings.TrimSpace(req.DisplayName) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "displayName is required")) - return + return apperror.ValidationFailed.New("displayName is required") } if strings.TrimSpace(req.ProjectId) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Project ID is required")) - return + return apperror.ValidationFailed.New("Project ID is required") } if strings.TrimSpace(string(req.Type)) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Application type is required")) - return + return apperror.ValidationFailed.New("Application type is required") } - createdBy, ok := resolveActor(w, r, h.identity, h.slogger, "create application") - if !ok { - return + createdBy, err := resolveActorErr(r, h.identity, "create application") + if err != nil { + return err } app, err := h.applicationService.CreateApplication(&req, orgID, createdBy) if err != nil { - h.writeApplicationError(w, r, err, "Failed to create application") - return + return h.mapApplicationError(err). + WithLogMessage(fmt.Sprintf("failed to create application in project %s for org %s by user %s", req.ProjectId, orgID, createdBy)) } httputil.WriteJSON(w, http.StatusCreated, app) + return nil } -func (h *ApplicationHandler) GetApplication(w http.ResponseWriter, r *http.Request) { +func (h *ApplicationHandler) GetApplication(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } appID := r.PathValue("applicationId") if strings.TrimSpace(appID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Application ID is required")) - return + return apperror.ValidationFailed.New("Application ID is required") } app, err := h.applicationService.GetApplicationByID(appID, orgID) if err != nil { - h.writeApplicationError(w, r, err, "Failed to get application") - return + return h.mapApplicationError(err). + WithLogMessage(fmt.Sprintf("failed to get application %s in org %s", appID, orgID)) } httputil.WriteJSON(w, http.StatusOK, app) + return nil } -func (h *ApplicationHandler) ListApplications(w http.ResponseWriter, r *http.Request) { +func (h *ApplicationHandler) ListApplications(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } projectID := strings.TrimSpace(r.URL.Query().Get("projectId")) if projectID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Project ID is required")) - return + return apperror.ValidationFailed.New("Project ID is required") } var limitStr string @@ -154,87 +143,80 @@ func (h *ApplicationHandler) ListApplications(w http.ResponseWriter, r *http.Req apps, err := h.applicationService.GetApplicationsByOrganization(orgID, projectID, limit, offset) if err != nil { - h.writeApplicationError(w, r, err, "Failed to list applications") - return + return h.mapApplicationError(err). + WithLogMessage(fmt.Sprintf("failed to list applications for project %s in org %s", projectID, orgID)) } httputil.WriteJSON(w, http.StatusOK, apps) + return nil } -func (h *ApplicationHandler) UpdateApplication(w http.ResponseWriter, r *http.Request) { +func (h *ApplicationHandler) UpdateApplication(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } appID := r.PathValue("applicationId") if strings.TrimSpace(appID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Application ID is required")) - return + return apperror.ValidationFailed.New("Application ID is required") } - userID, ok := resolveActor(w, r, h.identity, h.slogger, "update application") - if !ok { - return + userID, err := resolveActorErr(r, h.identity, "update application") + if err != nil { + return err } var req api.Application if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - utils.NewValidationErrorResponse(w, err) - return + return apperror.NewValidation(err) } app, err := h.applicationService.UpdateApplication(appID, &req, orgID, userID) if err != nil { - h.writeApplicationError(w, r, err, "Failed to update application") - return + return h.mapApplicationError(err). + WithLogMessage(fmt.Sprintf("failed to update application %s in org %s by user %s", appID, orgID, userID)) } httputil.WriteJSON(w, http.StatusOK, app) + return nil } -func (h *ApplicationHandler) DeleteApplication(w http.ResponseWriter, r *http.Request) { +func (h *ApplicationHandler) DeleteApplication(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } appID := r.PathValue("applicationId") if strings.TrimSpace(appID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Application ID is required")) - return + return apperror.ValidationFailed.New("Application ID is required") } - userID, ok := resolveActor(w, r, h.identity, h.slogger, "delete application") - if !ok { - return + userID, err := resolveActorErr(r, h.identity, "delete application") + if err != nil { + return err } if err := h.applicationService.DeleteApplication(appID, orgID, userID); err != nil { - h.writeApplicationError(w, r, err, "Failed to delete application") - return + return h.mapApplicationError(err). + WithLogMessage(fmt.Sprintf("failed to delete application %s in org %s by user %s", appID, orgID, userID)) } httputil.WriteJSON(w, http.StatusNoContent, nil) + return nil } -func (h *ApplicationHandler) ListApplicationAssociations(w http.ResponseWriter, r *http.Request) { +func (h *ApplicationHandler) ListApplicationAssociations(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } appID := r.PathValue("applicationId") if strings.TrimSpace(appID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Application ID is required")) - return + return apperror.ValidationFailed.New("Application ID is required") } limit := 20 @@ -258,90 +240,79 @@ func (h *ApplicationHandler) ListApplicationAssociations(w http.ResponseWriter, associations, err := h.applicationService.ListApplicationAssociations(appID, orgID, limit, offset) if err != nil { - h.writeApplicationError(w, r, err, "Failed to list application associations") - return + return h.mapApplicationError(err). + WithLogMessage(fmt.Sprintf("failed to list associations for application %s in org %s", appID, orgID)) } httputil.WriteJSON(w, http.StatusOK, associations) + return nil } -func (h *ApplicationHandler) AddApplicationAssociations(w http.ResponseWriter, r *http.Request) { +func (h *ApplicationHandler) AddApplicationAssociations(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } appID := r.PathValue("applicationId") if strings.TrimSpace(appID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Application ID is required")) - return + return apperror.ValidationFailed.New("Application ID is required") } var req service.AddApplicationAssociationsRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - utils.NewValidationErrorResponse(w, err) - return + return apperror.NewValidation(err) } if len(req.Associations) == 0 { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "At least one association is required")) - return + return apperror.ValidationFailed.New("At least one association is required") } associations, err := h.applicationService.AddApplicationAssociations(appID, &req, orgID) if err != nil { - h.writeApplicationError(w, r, err, "Failed to add application associations") - return + return h.mapApplicationError(err). + WithLogMessage(fmt.Sprintf("failed to add associations for application %s in org %s", appID, orgID)) } httputil.WriteJSON(w, http.StatusOK, associations) + return nil } -func (h *ApplicationHandler) RemoveApplicationAssociation(w http.ResponseWriter, r *http.Request) { +func (h *ApplicationHandler) RemoveApplicationAssociation(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } appID := r.PathValue("applicationId") associationID := r.PathValue("associationId") if strings.TrimSpace(appID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Application ID is required")) - return + return apperror.ValidationFailed.New("Application ID is required") } if strings.TrimSpace(associationID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Association ID is required")) - return + return apperror.ValidationFailed.New("Association ID is required") } if err := h.applicationService.RemoveApplicationAssociation(appID, associationID, orgID); err != nil { - h.writeApplicationError(w, r, err, "Failed to remove application association") - return + return h.mapApplicationError(err). + WithLogMessage(fmt.Sprintf("failed to remove association %s for application %s in org %s", associationID, appID, orgID)) } httputil.WriteJSON(w, http.StatusNoContent, nil) + return nil } -func (h *ApplicationHandler) ListApplicationAPIKeys(w http.ResponseWriter, r *http.Request) { +func (h *ApplicationHandler) ListApplicationAPIKeys(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } appID := r.PathValue("applicationId") if strings.TrimSpace(appID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Application ID is required")) - return + return apperror.ValidationFailed.New("Application ID is required") } limit := 20 @@ -365,32 +336,28 @@ func (h *ApplicationHandler) ListApplicationAPIKeys(w http.ResponseWriter, r *ht keys, err := h.applicationService.ListMappedAPIKeys(appID, orgID, limit, offset) if err != nil { - h.writeApplicationError(w, r, err, "Failed to list mapped API keys") - return + return h.mapApplicationError(err). + WithLogMessage(fmt.Sprintf("failed to list mapped API keys for application %s in org %s", appID, orgID)) } httputil.WriteJSON(w, http.StatusOK, keys) + return nil } -func (h *ApplicationHandler) ListApplicationAssociationAPIKeys(w http.ResponseWriter, r *http.Request) { +func (h *ApplicationHandler) ListApplicationAssociationAPIKeys(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } appID := r.PathValue("applicationId") associationID := r.PathValue("associationId") if strings.TrimSpace(appID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Application ID is required")) - return + return apperror.ValidationFailed.New("Application ID is required") } if strings.TrimSpace(associationID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Association ID is required")) - return + return apperror.ValidationFailed.New("Association ID is required") } limit := 20 @@ -414,172 +381,138 @@ func (h *ApplicationHandler) ListApplicationAssociationAPIKeys(w http.ResponseWr keys, err := h.applicationService.ListMappedAPIKeysForAssociation(appID, associationID, orgID, limit, offset) if err != nil { - h.writeApplicationError(w, r, err, "Failed to list mapped API keys for association") - return + 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)) } httputil.WriteJSON(w, http.StatusOK, keys) + return nil } -func (h *ApplicationHandler) AddApplicationAPIKeys(w http.ResponseWriter, r *http.Request) { +func (h *ApplicationHandler) AddApplicationAPIKeys(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } appID := r.PathValue("applicationId") - userID, ok := resolveActor(w, r, h.identity, h.slogger, "add application API keys") - if !ok { - return + userID, err := resolveActorErr(r, h.identity, "add application API keys") + if err != nil { + return err } if strings.TrimSpace(appID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Application ID is required")) - return + return apperror.ValidationFailed.New("Application ID is required") } var req api.AddApplicationAPIKeysRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - utils.NewValidationErrorResponse(w, err) - return + return apperror.NewValidation(err) } if len(req.ApiKeys) == 0 { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "At least one API key mapping is required")) - return + return apperror.ValidationFailed.New("At least one API key mapping is required") } keys, err := h.applicationService.AddMappedAPIKeys(appID, &req, orgID, userID) if err != nil { - h.writeApplicationError(w, r, err, "Failed to add mapped API keys") - return + 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)) } httputil.WriteJSON(w, http.StatusOK, keys) + return nil } -func (h *ApplicationHandler) RemoveApplicationAPIKey(w http.ResponseWriter, r *http.Request) { +func (h *ApplicationHandler) RemoveApplicationAPIKey(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } appID := r.PathValue("applicationId") keyID := r.PathValue("apiKeyId") entityID := strings.TrimSpace(r.URL.Query().Get("entityID")) - userID, ok := resolveActor(w, r, h.identity, h.slogger, "remove mapped application API key") - if !ok { - return + userID, err := resolveActorErr(r, h.identity, "remove mapped application API key") + if err != nil { + return err } if strings.TrimSpace(appID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Application ID is required")) - return + return apperror.ValidationFailed.New("Application ID is required") } if strings.TrimSpace(keyID) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API key id is required")) - return + return apperror.ValidationFailed.New("API key id is required") } if entityID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Entity ID is required")) - return + return apperror.ValidationFailed.New("Entity ID is required") } if err := h.applicationService.RemoveMappedAPIKey(appID, keyID, entityID, orgID, userID); err != nil { - h.writeApplicationError(w, r, err, "Failed to remove mapped API key") - return + 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)) } httputil.WriteJSON(w, http.StatusNoContent, nil) + return nil } func (h *ApplicationHandler) RegisterRoutes(mux *http.ServeMux) { base := constants.APIBasePath + "/applications" - mux.HandleFunc("GET "+base, h.ListApplications) - mux.HandleFunc("POST "+base, h.CreateApplication) - mux.HandleFunc("GET "+base+"/{applicationId}", h.GetApplication) - mux.HandleFunc("PUT "+base+"/{applicationId}", h.UpdateApplication) - mux.HandleFunc("DELETE "+base+"/{applicationId}", h.DeleteApplication) - - mux.HandleFunc("GET "+base+"/{applicationId}/api-keys", h.ListApplicationAPIKeys) - mux.HandleFunc("POST "+base+"/{applicationId}/api-keys", h.AddApplicationAPIKeys) - mux.HandleFunc("DELETE "+base+"/{applicationId}/api-keys/{apiKeyId}", h.RemoveApplicationAPIKey) - mux.HandleFunc("GET "+base+"/{applicationId}/associations", h.ListApplicationAssociations) - mux.HandleFunc("POST "+base+"/{applicationId}/associations", h.AddApplicationAssociations) - mux.HandleFunc("GET "+base+"/{applicationId}/associations/{associationId}/api-keys", h.ListApplicationAssociationAPIKeys) - mux.HandleFunc("DELETE "+base+"/{applicationId}/associations/{associationId}", h.RemoveApplicationAssociation) + mux.HandleFunc("GET "+base, middleware.MapErrors(h.slogger, h.ListApplications)) + mux.HandleFunc("POST "+base, middleware.MapErrors(h.slogger, h.CreateApplication)) + mux.HandleFunc("GET "+base+"/{applicationId}", middleware.MapErrors(h.slogger, h.GetApplication)) + mux.HandleFunc("PUT "+base+"/{applicationId}", middleware.MapErrors(h.slogger, h.UpdateApplication)) + mux.HandleFunc("DELETE "+base+"/{applicationId}", middleware.MapErrors(h.slogger, h.DeleteApplication)) + + mux.HandleFunc("GET "+base+"/{applicationId}/api-keys", middleware.MapErrors(h.slogger, h.ListApplicationAPIKeys)) + mux.HandleFunc("POST "+base+"/{applicationId}/api-keys", middleware.MapErrors(h.slogger, h.AddApplicationAPIKeys)) + mux.HandleFunc("DELETE "+base+"/{applicationId}/api-keys/{apiKeyId}", middleware.MapErrors(h.slogger, h.RemoveApplicationAPIKey)) + mux.HandleFunc("GET "+base+"/{applicationId}/associations", middleware.MapErrors(h.slogger, h.ListApplicationAssociations)) + mux.HandleFunc("POST "+base+"/{applicationId}/associations", middleware.MapErrors(h.slogger, h.AddApplicationAssociations)) + 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)) } -func (h *ApplicationHandler) writeApplicationError(w http.ResponseWriter, r *http.Request, err error, fallback string) { - if h.slogger != nil { - h.slogger.Error(fallback, - "error", err, - "path", r.URL.Path, - "method", r.Method, - "id", r.PathValue("applicationId"), - ) - } - +// 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): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeApplicationNotFound, "The specified application could not be found.")) + return apperror.ApplicationNotFound.Wrap(err) case errors.Is(err, constants.ErrProjectNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeProjectNotFound, "The specified project could not be found.")) + return apperror.ProjectNotFound.Wrap(err) case errors.Is(err, constants.ErrOrganizationNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeOrganizationNotFound, "The specified organization could not be found.")) + return apperror.OrganizationNotFound.Wrap(err) case errors.Is(err, constants.ErrApplicationExists): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeApplicationExists, "An application already exists in this project.")) + return apperror.ApplicationExists.Wrap(err) case errors.Is(err, constants.ErrHandleExists): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeApplicationExists, "An application with this handle already exists in the organization.")) + return apperror.ApplicationExists.Wrap(err) case errors.Is(err, constants.ErrAPIKeyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeApplicationAPIKeyNotFound, "API key not found")) + return apperror.ApplicationAPIKeyNotFound.Wrap(err) case errors.Is(err, constants.ErrAPIKeyForbidden): - httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( - utils.CodeApplicationAPIKeyForbidden, "Only the key creator can perform this action")) + return apperror.ApplicationAPIKeyForbidden.Wrap(err) case errors.Is(err, constants.ErrInvalidApplicationName): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "displayName is required")) + return apperror.ValidationFailed.Wrap(err, "displayName is required") case errors.Is(err, constants.ErrInvalidApplicationType): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Application type is required")) + return apperror.ValidationFailed.Wrap(err, "Application type is required") case errors.Is(err, constants.ErrUnsupportedApplicationType): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid application type. Only 'genai' is supported")) + return apperror.ValidationFailed.Wrap(err, "Invalid application type. Only 'genai' is supported") case errors.Is(err, constants.ErrInvalidHandle): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid application handle format")) + return apperror.ValidationFailed.Wrap(err, "Invalid application handle format") case errors.Is(err, constants.ErrInvalidApplicationID): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid application id")) + return apperror.ValidationFailed.Wrap(err, "Invalid application id") case errors.Is(err, constants.ErrInvalidAPIKey): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid API key id")) + return apperror.ValidationFailed.Wrap(err, "Invalid API key id") case errors.Is(err, constants.ErrArtifactNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeArtifactNotFound, "The specified artifact could not be found.")) + return apperror.ArtifactNotFound.Wrap(err) case errors.Is(err, constants.ErrArtifactInvalidKind): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid association kind. Only LlmProvider and LlmProxy are supported")) + return apperror.ValidationFailed.Wrap(err, "Invalid association kind. Only LlmProvider and LlmProxy are supported") case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid application association input")) + return apperror.ValidationFailed.Wrap(err, "Invalid application association input") case errors.Is(err, constants.ErrHandleImmutable): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "The id is immutable and must match the application being updated")) + return apperror.ValidationFailed.Wrap(err, "The id is immutable and must match the application being updated") default: - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, fallback)) + 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 index 6d660269ef..3bb31ac2b7 100644 --- a/platform-api/internal/handler/artifact_guard_response.go +++ b/platform-api/internal/handler/artifact_guard_response.go @@ -19,37 +19,29 @@ 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/api-platform/platform-api/internal/utils" - - "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`. +// 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 respondArtifactGuardError(w http.ResponseWriter, err error) bool { +func mapArtifactGuardError(err error) error { switch { case errors.Is(err, constants.ErrArtifactReadOnly): - httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( - utils.CodeArtifactReadOnly, err.Error())) - return true + return apperror.ArtifactReadOnly.Wrap(err, err.Error()) case errors.Is(err, constants.ErrArtifactRuntimeImmutable): - httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( - utils.CodeArtifactRuntimeImmutable, err.Error())) - return true + return apperror.ArtifactRuntimeImmutable.Wrap(err, err.Error()) case errors.Is(err, constants.ErrArtifactDeployed): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeArtifactDeployed, err.Error())) - return true + return apperror.ArtifactDeployed.Wrap(err, err.Error()) default: - return false + return nil } } diff --git a/platform-api/internal/handler/auth_login.go b/platform-api/internal/handler/auth_login.go index d0fdfc2266..aab14a85ee 100644 --- a/platform-api/internal/handler/auth_login.go +++ b/platform-api/internal/handler/auth_login.go @@ -18,11 +18,13 @@ package handler import ( + "log/slog" "net/http" "time" "github.com/wso2/api-platform/platform-api/config" - "github.com/wso2/api-platform/platform-api/internal/utils" + "github.com/wso2/api-platform/platform-api/internal/apperror" + "github.com/wso2/api-platform/platform-api/internal/middleware" "github.com/golang-jwt/jwt/v5" "github.com/wso2/go-httpkit/httputil" @@ -41,28 +43,27 @@ type loginResponse struct { // AuthLoginHandler issues JWT tokens for locally-configured users (file-based auth mode). type AuthLoginHandler struct { - cfg *config.Server + cfg *config.Server + slogger *slog.Logger } func NewAuthLoginHandler(cfg *config.Server) *AuthLoginHandler { - return &AuthLoginHandler{cfg: cfg} + return &AuthLoginHandler{cfg: cfg, slogger: slog.Default()} } func (h *AuthLoginHandler) RegisterPublicRoutes(mux *http.ServeMux) { - mux.HandleFunc("POST /api/portal/v0.9/auth/login", h.Login) + mux.HandleFunc("POST /api/portal/v0.9/auth/login", middleware.MapErrors(h.slogger, h.Login)) } -func (h *AuthLoginHandler) Login(w http.ResponseWriter, r *http.Request) { +func (h *AuthLoginHandler) Login(w http.ResponseWriter, r *http.Request) error { var req loginRequest if err := r.ParseForm(); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode(utils.CodeCommonValidationFailed, "username and password are required")) - return + return apperror.ValidationFailed.New("username and password are required") } req.Username = r.PostForm.Get("username") req.Password = r.PostForm.Get("password") if req.Username == "" || req.Password == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode(utils.CodeCommonValidationFailed, "username and password are required")) - return + return apperror.ValidationFailed.New("username and password are required") } fileBasedAuth := &h.cfg.Auth.FileBased @@ -78,13 +79,11 @@ func (h *AuthLoginHandler) Login(w http.ResponseWriter, r *http.Request) { // timing-based username enumeration. if matched == nil { _ = bcrypt.CompareHashAndPassword([]byte("$2a$10$notarealhashjustpadding000000000000000000000000000"), []byte(req.Password)) - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode(utils.CodeCommonUnauthorized, "Invalid or expired credentials.")) - return + return apperror.Unauthorized.New().WithLogMessage("login failed: user not found") } if err := bcrypt.CompareHashAndPassword([]byte(matched.PasswordHash), []byte(req.Password)); err != nil { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode(utils.CodeCommonUnauthorized, "Invalid or expired credentials.")) - return + return apperror.Unauthorized.New().WithLogMessage("login failed: password mismatch") } expiry := time.Now().Add(8 * time.Hour) @@ -103,12 +102,12 @@ func (h *AuthLoginHandler) Login(w http.ResponseWriter, r *http.Request) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) signed, err := token.SignedString([]byte(h.cfg.Auth.JWT.SecretKey)) if err != nil { - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode(utils.CodeCommonInternalError, "Failed to issue token.")) - return + return apperror.Internal.Wrap(err).WithLogMessage("failed to issue token") } httputil.WriteJSON(w, http.StatusOK, loginResponse{ Token: signed, ExpiresAt: expiry.Unix(), }) + return nil } diff --git a/platform-api/internal/handler/gateway.go b/platform-api/internal/handler/gateway.go index f6e936f5d9..9a10934704 100644 --- a/platform-api/internal/handler/gateway.go +++ b/platform-api/internal/handler/gateway.go @@ -20,16 +20,18 @@ package handler import ( "encoding/json" "errors" + "fmt" "log/slog" "net/http" - "github.com/wso2/api-platform/platform-api/api" - "github.com/wso2/api-platform/platform-api/internal/constants" "regexp" "strings" + "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/service" - "github.com/wso2/api-platform/platform-api/internal/utils" "github.com/wso2/go-httpkit/httputil" ) @@ -63,18 +65,15 @@ type manifestSyncResponse struct { } // CreateGateway handles POST /api/v0.9/gateways -func (h *GatewayHandler) CreateGateway(w http.ResponseWriter, r *http.Request) { +func (h *GatewayHandler) CreateGateway(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New().WithLogMessage("organization claim not found in token") } var req api.CreateGatewayRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - utils.NewValidationErrorResponse(w, err) - return + return apperror.NewValidation(err) } // Convert functionality type to string @@ -102,14 +101,13 @@ func (h *GatewayHandler) CreateGateway(w http.ResponseWriter, r *http.Request) { version = strings.TrimSpace(*req.Version) } if version != "" && !gatewayVersionPattern.MatchString(version) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "version must be in 'major.minor' format (e.g. '1.0') or CalVer 'YYYY.MM.DD' format (e.g. '2026.05.13')")) - return + return apperror.ValidationFailed.New( + "version must be in 'major.minor' format (e.g. '1.0') or CalVer 'YYYY.MM.DD' format (e.g. '2026.05.13')") } - createdBy, ok := resolveActor(w, r, h.identity, h.slogger, "register gateway") - if !ok { - return + createdBy, err := resolveActorErr(r, h.identity, "register gateway") + if err != nil { + return err } gateway, err := h.gatewayService.RegisterGateway(orgId, req.Id, req.DisplayName, description, req.Endpoints, isCritical, functionalityType, version, createdBy, properties) @@ -118,74 +116,55 @@ func (h *GatewayHandler) CreateGateway(w http.ResponseWriter, r *http.Request) { // Check for specific error types if strings.Contains(errMsg, "organization not found") { - h.slogger.Error("Organization not found during gateway creation", "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeOrganizationNotFound, "The specified organization could not be found.")) - return + return apperror.OrganizationNotFound.Wrap(err) } if strings.Contains(errMsg, "already exists") { - h.slogger.Error("Gateway already exists", "error", err) - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeGatewayNameConflict, "A gateway with this name already exists within the organization.")) - return + return apperror.GatewayNameConflict.Wrap(err) } if strings.Contains(errMsg, "required") || strings.Contains(errMsg, "invalid") || strings.Contains(errMsg, "must") || strings.Contains(errMsg, "cannot") { - h.slogger.Error("Invalid gateway creation request", "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, errMsg)) - return + return apperror.ValidationFailed.Wrap(err, errMsg) } // Internal server error - h.slogger.Error("Failed to register gateway", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to register gateway")) - return + return apperror.Internal.Wrap(err).WithLogMessage("failed to register gateway") } // Return 201 Created with response httputil.WriteJSON(w, http.StatusCreated, gateway) + return nil } // ListGateways handles GET /api/v0.9/gateways with constitution-compliant response -func (h *GatewayHandler) ListGateways(w http.ResponseWriter, r *http.Request) { +func (h *GatewayHandler) ListGateways(w http.ResponseWriter, r *http.Request) error { organizationID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New().WithLogMessage("organization claim not found in token") } gateways, err := h.gatewayService.ListGateways(&organizationID) if err != nil { - h.slogger.Error("Failed to list gateways", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to list gateways")) - return + return apperror.Internal.Wrap(err).WithLogMessage("failed to list gateways") } // Return 200 OK with constitution-compliant envelope structure httputil.WriteJSON(w, http.StatusOK, gateways) + return nil } // GetGateway handles GET /api/v0.9/gateways/:gatewayId -func (h *GatewayHandler) GetGateway(w http.ResponseWriter, r *http.Request) { +func (h *GatewayHandler) GetGateway(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New().WithLogMessage("organization claim not found in token") } // Extract UUID path parameter gatewayId := r.PathValue("gatewayId") if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Gateway ID is required")) - return + return apperror.ValidationFailed.New("Gateway ID is required") } gateway, err := h.gatewayService.GetGateway(gatewayId, orgId) @@ -194,37 +173,27 @@ func (h *GatewayHandler) GetGateway(w http.ResponseWriter, r *http.Request) { // Check for specific error types if strings.Contains(errMsg, "not found") { - h.slogger.Error("Gateway not found", "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayNotFound, "The specified gateway could not be found.")) - return + return apperror.GatewayNotFound.Wrap(err) } if strings.Contains(errMsg, "invalid UUID") { - h.slogger.Error("Invalid gateway UUID", "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid gateway ID format")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid gateway ID format") } // Internal server error - h.slogger.Error("Failed to retrieve gateway", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to retrieve gateway")) - return + return apperror.Internal.Wrap(err).WithLogMessage("failed to retrieve gateway") } // Return 200 OK with gateway details httputil.WriteJSON(w, http.StatusOK, gateway) + return nil } // GetGatewayStatus retrieves gateway status, optionally filtered by gatewayId query param. -func (h *GatewayHandler) GetGatewayStatus(w http.ResponseWriter, r *http.Request) { +func (h *GatewayHandler) GetGatewayStatus(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New().WithLogMessage("organization claim not found in token") } gatewayId := r.URL.Query().Get("gatewayId") @@ -236,142 +205,105 @@ func (h *GatewayHandler) GetGatewayStatus(w http.ResponseWriter, r *http.Request status, err := h.gatewayService.GetGatewayStatus(orgId, gatewayIdPtr) if err != nil { if strings.Contains(err.Error(), "gateway not found") { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayNotFound, "The specified gateway could not be found.")) - return + return apperror.GatewayNotFound.Wrap(err) } - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to get gateway status")) - return + return apperror.Internal.Wrap(err).WithLogMessage("failed to get gateway status") } httputil.WriteJSON(w, http.StatusOK, status) + return nil } // UpdateGateway handles PUT /api/v0.9/gateways/:gatewayId -func (h *GatewayHandler) UpdateGateway(w http.ResponseWriter, r *http.Request) { +func (h *GatewayHandler) UpdateGateway(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New().WithLogMessage("organization claim not found in token") } gatewayId := r.PathValue("gatewayId") if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Gateway ID is required")) - return + return apperror.ValidationFailed.New("Gateway ID is required") } var req api.GatewayResponse if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - utils.NewValidationErrorResponse(w, err) - return + return apperror.NewValidation(err) } if req.Id != nil && *req.Id != gatewayId { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Gateway id is immutable and cannot be changed")) - return + return apperror.ValidationFailed.New("Gateway id is immutable and cannot be changed") } if req.DisplayName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "displayName is required")) - return + return apperror.ValidationFailed.New("displayName is required") } - updatedBy, ok := resolveActor(w, r, h.identity, h.slogger, "update gateway") - if !ok { - return + updatedBy, err := resolveActorErr(r, h.identity, "update gateway") + if err != nil { + return err } response, err := h.gatewayService.UpdateGateway(gatewayId, orgId, updatedBy, &req) if err != nil { if errors.Is(err, constants.ErrGatewayNotFound) { - h.slogger.Error("Gateway not found during update", "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayNotFound, "The specified gateway could not be found.")) - return + return apperror.GatewayNotFound.Wrap(err) } - h.slogger.Error("Failed to update gateway", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to update gateway")) - return + return apperror.Internal.Wrap(err).WithLogMessage("failed to update gateway") } httputil.WriteJSON(w, http.StatusOK, response) + return nil } // DeleteGateway handles DELETE /api/v0.9/gateways/:gatewayId -func (h *GatewayHandler) DeleteGateway(w http.ResponseWriter, r *http.Request) { +func (h *GatewayHandler) DeleteGateway(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New().WithLogMessage("organization claim not found in token") } // Extract UUID path parameter gatewayId := r.PathValue("gatewayId") if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Gateway ID is required")) - return + return apperror.ValidationFailed.New("Gateway ID is required") } - deletedBy, ok := resolveActor(w, r, h.identity, h.slogger, "delete gateway") - if !ok { - return - } - err := h.gatewayService.DeleteGateway(gatewayId, orgId, deletedBy) + deletedBy, err := resolveActorErr(r, h.identity, "delete gateway") if err != nil { + return err + } + if err := h.gatewayService.DeleteGateway(gatewayId, orgId, deletedBy); err != nil { // Check for specific error types if errors.Is(err, constants.ErrGatewayNotFound) { - h.slogger.Error("Gateway not found during deletion", "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayNotFound, "The specified gateway could not be found.")) - return + return apperror.GatewayNotFound.Wrap(err) } if errors.Is(err, constants.ErrGatewayHasAssociatedAPIs) { - h.slogger.Error("Gateway has associated APIs during deletion", "error", err) - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeGatewayHasActiveDeployments, "The gateway has associated APIs. Please remove all API associations before deleting the gateway.")) - return + return apperror.GatewayHasActiveDeployments.Wrap(err) } if strings.Contains(err.Error(), "invalid UUID") { - h.slogger.Error("Invalid UUID during gateway deletion", "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid gateway ID format")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid gateway ID format") } // Internal server error - h.slogger.Error("Failed to delete gateway", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "The server encountered an internal error. Please contact administrator.")) - return + return apperror.Internal.Wrap(err).WithLogMessage("failed to delete gateway") } // Return 204 No Content on successful deletion w.WriteHeader(http.StatusNoContent) + return nil } // ListTokens handles GET /api/v0.9/gateways/:gatewayId/tokens -func (h *GatewayHandler) ListTokens(w http.ResponseWriter, r *http.Request) { +func (h *GatewayHandler) ListTokens(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New().WithLogMessage("organization claim not found in token") } gatewayId := r.PathValue("gatewayId") if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Gateway ID is required")) - return + return apperror.ValidationFailed.New("Gateway ID is required") } tokens, err := h.gatewayService.ListTokens(gatewayId, orgId) @@ -379,41 +311,32 @@ func (h *GatewayHandler) ListTokens(w http.ResponseWriter, r *http.Request) { errMsg := err.Error() if strings.Contains(errMsg, "gateway not found") { - h.slogger.Error("Gateway not found during token listing", "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayNotFound, "The specified gateway could not be found.")) - return + return apperror.GatewayNotFound.Wrap(err) } - h.slogger.Error("Failed to list tokens", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to list tokens")) - return + return apperror.Internal.Wrap(err).WithLogMessage("failed to list tokens") } httputil.WriteJSON(w, http.StatusOK, tokens) + return nil } // RotateToken handles POST /api/v0.9/gateways/:gatewayId/tokens -func (h *GatewayHandler) RotateToken(w http.ResponseWriter, r *http.Request) { +func (h *GatewayHandler) RotateToken(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New().WithLogMessage("organization claim not found in token") } // Extract ID path parameter gatewayId := r.PathValue("gatewayId") if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Gateway ID is required")) - return + return apperror.ValidationFailed.New("Gateway ID is required") } - createdBy, ok := resolveActor(w, r, h.identity, h.slogger, "rotate gateway token") - if !ok { - return + createdBy, err := resolveActorErr(r, h.identity, "rotate gateway token") + if err != nil { + return err } response, err := h.gatewayService.RotateToken(gatewayId, orgId, createdBy) if err != nil { @@ -421,129 +344,96 @@ func (h *GatewayHandler) RotateToken(w http.ResponseWriter, r *http.Request) { // Check for specific error types if strings.Contains(errMsg, "gateway not found") { - h.slogger.Error("Gateway not found during token rotation", "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayNotFound, "The specified gateway could not be found.")) - return + return apperror.GatewayNotFound.Wrap(err) } if strings.Contains(errMsg, "maximum") || strings.Contains(errMsg, "Revoke") { - h.slogger.Error("Token rotation request validation failed", "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeGatewayTokenLimitReached, "The maximum number of active tokens has been reached.")) - return + return apperror.GatewayTokenLimitReached.Wrap(err) } // Internal server error - h.slogger.Error("Failed to rotate token", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to rotate token")) - return + return apperror.Internal.Wrap(err).WithLogMessage("failed to rotate token") } // Return 201 Created with response httputil.WriteJSON(w, http.StatusCreated, response) + return nil } // RevokeToken handles DELETE /api/v0.9/gateways/:gatewayId/tokens/:tokenId -func (h *GatewayHandler) RevokeToken(w http.ResponseWriter, r *http.Request) { +func (h *GatewayHandler) RevokeToken(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New().WithLogMessage("organization claim not found in token") } gatewayId := r.PathValue("gatewayId") if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Gateway ID is required")) - return + return apperror.ValidationFailed.New("Gateway ID is required") } tokenId := r.PathValue("tokenId") if tokenId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Token ID is required")) - return + return apperror.ValidationFailed.New("Token ID is required") } - revokedBy, ok := resolveActor(w, r, h.identity, h.slogger, "revoke gateway token") - if !ok { - return - } - err := h.gatewayService.RevokeToken(gatewayId, tokenId, orgId, revokedBy) + revokedBy, err := resolveActorErr(r, h.identity, "revoke gateway token") if err != nil { + return err + } + if err := h.gatewayService.RevokeToken(gatewayId, tokenId, orgId, revokedBy); err != nil { errMsg := err.Error() if strings.Contains(errMsg, "not found") { - h.slogger.Error("Resource not found during token revocation", "error", err) if strings.Contains(errMsg, "gateway") { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayNotFound, "The specified gateway could not be found.")) - return + return apperror.GatewayNotFound.Wrap(err) } - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayTokenNotFound, "The specified gateway token could not be found.")) - return + return apperror.GatewayTokenNotFound.Wrap(err) } - h.slogger.Error("Failed to revoke token", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to revoke token")) - return + return apperror.Internal.Wrap(err).WithLogMessage("failed to revoke token") } httputil.WriteJSON(w, http.StatusOK, map[string]any{"message": "Token revoked successfully"}) + return nil } // GetGatewayManifest handles GET /api/v0.9/gateways/{gatewayId}/manifest // Called by APIM to retrieve the manifest pushed by the gateway controller on connect. -func (h *GatewayHandler) GetGatewayManifest(w http.ResponseWriter, r *http.Request) { +func (h *GatewayHandler) GetGatewayManifest(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New().WithLogMessage("organization claim not found in token") } gatewayId := r.PathValue("gatewayId") if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Gateway ID is required")) - return + return apperror.ValidationFailed.New("Gateway ID is required") } dataFromDb, err := h.gatewayService.GetStoredManifest(gatewayId, orgId) if err != nil { if strings.Contains(err.Error(), "invalid UUID") { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid gateway ID format")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid gateway ID format") } if errors.Is(err, constants.ErrGatewayNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayNotFound, "The specified gateway could not be found.")) - return + return apperror.GatewayNotFound.Wrap(err) } - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to retrieve gateway manifest")) - return + return apperror.Internal.Wrap(err).WithLogMessage("failed to retrieve gateway manifest") } httputil.WriteJSON(w, http.StatusOK, manifestSyncResponse{ Policies: dataFromDb.Policies, }) + return nil } // SyncCustomPolicy handles POST /api/v0.9/gateway-custom-policies/sync // It upserts a custom policy from the gateway's stored manifest into the gateway_custom_policies table. -func (h *GatewayHandler) SyncCustomPolicy(w http.ResponseWriter, r *http.Request) { +func (h *GatewayHandler) SyncCustomPolicy(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New().WithLogMessage("organization claim not found in token") } gatewayId := r.URL.Query().Get("gatewayId") @@ -551,159 +441,121 @@ func (h *GatewayHandler) SyncCustomPolicy(w http.ResponseWriter, r *http.Request version := r.URL.Query().Get("policyVersion") if gatewayId == "" || policyName == "" || version == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "gatewayId, policyName and policyVersion are required")) - return + return apperror.ValidationFailed.New("gatewayId, policyName and policyVersion are required") } policy, err := h.gatewayService.SyncCustomPolicy(gatewayId, orgId, policyName, version) if err != nil { msg := err.Error() if strings.Contains(msg, "gateway not found") { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayNotFound, "The specified gateway could not be found.")) - return + return apperror.GatewayNotFound.Wrap(err) } if strings.Contains(msg, "not found in gateway manifest") { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeCustomPolicyVersionNotFound, "The specified policy version could not be found in the gateway manifest.")) - return + return apperror.CustomPolicyVersionNotFnd.Wrap(err) } if strings.Contains(msg, "not a custom policy") || strings.Contains(msg, "manifest is not available") { - httputil.WriteJSON(w, http.StatusUnprocessableEntity, utils.NewErrorResponseWithCode( - utils.CodePolicyInvalidState, "The policy is not a custom policy, or its manifest is unavailable.")) - return + return apperror.PolicyInvalidState.Wrap(err) } if strings.Contains(msg, "already exists") || strings.Contains(msg, "patch version updates are not allowed") || strings.Contains(msg, "cannot downgrade") { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodePolicyVersionConflict, "The policy already exists, or a patch-version update or downgrade is not allowed.")) - return + return apperror.PolicyVersionConflict.Wrap(err) } - h.slogger.Error("Failed to sync custom policy", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to sync custom policy")) - return + return apperror.Internal.Wrap(err).WithLogMessage("failed to sync custom policy") } httputil.WriteJSON(w, http.StatusOK, policy) + return nil } // GetCustomPolicy handles GET /api/v0.9/gateway-custom-policies/:customPolicyUuid/versions/:version -func (h *GatewayHandler) GetCustomPolicy(w http.ResponseWriter, r *http.Request) { +func (h *GatewayHandler) GetCustomPolicy(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New().WithLogMessage("organization claim not found in token") } policyUUID := r.PathValue("gatewayCustomPolicyId") version := r.PathValue("version") if policyUUID == "" || version == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "customPolicyUuid and version are required")) - return + return apperror.ValidationFailed.New("customPolicyUuid and version are required") } policy, err := h.gatewayService.GetCustomPolicyByUUID(orgId, policyUUID, version) if err != nil { if errors.Is(err, constants.ErrCustomPolicyNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeCustomPolicyNotFound, "Custom policy not found.")) - return + return apperror.CustomPolicyNotFound.Wrap(err) } if errors.Is(err, constants.ErrCustomPolicyVersionMismatch) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeCustomPolicyVersionNotFound, "Custom policy not found with the specified version.")) - return + return apperror.CustomPolicyVersionNotFnd.Wrap(err) } - h.slogger.Error("Failed to get custom policy", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to get custom policy")) - return + return apperror.Internal.Wrap(err).WithLogMessage("failed to get custom policy") } httputil.WriteJSON(w, http.StatusOK, policy) + return nil } // DeleteCustomPolicy handles DELETE /api/v0.9/gateway-custom-policies/:customPolicyUuid/versions/:version -func (h *GatewayHandler) DeleteCustomPolicy(w http.ResponseWriter, r *http.Request) { +func (h *GatewayHandler) DeleteCustomPolicy(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New().WithLogMessage("organization claim not found in token") } policyUUID := r.PathValue("gatewayCustomPolicyId") version := r.PathValue("version") if policyUUID == "" || version == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "customPolicyUuid and version are required")) - return + return apperror.ValidationFailed.New("customPolicyUuid and version are required") } - err := h.gatewayService.DeleteCustomPolicyByUUID(orgId, policyUUID, version) - if err != nil { + if err := h.gatewayService.DeleteCustomPolicyByUUID(orgId, policyUUID, version); err != nil { if errors.Is(err, constants.ErrCustomPolicyNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeCustomPolicyNotFound, "Custom policy not found.")) - return + return apperror.CustomPolicyNotFound.Wrap(err) } if errors.Is(err, constants.ErrCustomPolicyVersionMismatch) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeCustomPolicyVersionNotFound, "Custom policy not found with the specified version.")) - return + return apperror.CustomPolicyVersionNotFnd.Wrap(err) } if errors.Is(err, constants.ErrCustomPolicyInUse) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodePolicyInUse, "The custom policy is in use by one or more APIs and cannot be removed.")) - return + return apperror.PolicyInUse.Wrap(err) } - h.slogger.Error("Failed to delete custom policy", "org_id", orgId, "policy_uuid", policyUUID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to delete custom policy")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to delete custom policy, orgID=%s, policyUUID=%s", orgId, policyUUID)) } w.WriteHeader(http.StatusNoContent) + return nil } // ListCustomPolicies handles GET /api/v0.9/gateway-custom-policies -func (h *GatewayHandler) ListCustomPolicies(w http.ResponseWriter, r *http.Request) { +func (h *GatewayHandler) ListCustomPolicies(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New().WithLogMessage("organization claim not found in token") } policies, err := h.gatewayService.ListCustomPolicies(orgId) if err != nil { - h.slogger.Error("Failed to list custom policies", "org_id", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to list custom policies")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to list custom policies, orgID=%s", orgId)) } httputil.WriteJSON(w, http.StatusOK, policies) + return nil } // RegisterRoutes registers gateway routes with the router func (h *GatewayHandler) RegisterRoutes(mux *http.ServeMux) { h.slogger.Debug("Registering gateway routes") - mux.HandleFunc("POST "+constants.APIBasePath+"/gateways", h.CreateGateway) - mux.HandleFunc("GET "+constants.APIBasePath+"/gateways", h.ListGateways) - mux.HandleFunc("GET "+constants.APIBasePath+"/gateways/{gatewayId}", h.GetGateway) - mux.HandleFunc("PUT "+constants.APIBasePath+"/gateways/{gatewayId}", h.UpdateGateway) - mux.HandleFunc("DELETE "+constants.APIBasePath+"/gateways/{gatewayId}", h.DeleteGateway) - mux.HandleFunc("GET "+constants.APIBasePath+"/gateways/{gatewayId}/tokens", h.ListTokens) - mux.HandleFunc("POST "+constants.APIBasePath+"/gateways/{gatewayId}/tokens", h.RotateToken) - mux.HandleFunc("DELETE "+constants.APIBasePath+"/gateways/{gatewayId}/tokens/{tokenId}", h.RevokeToken) - mux.HandleFunc("GET "+constants.APIBasePath+"/gateways/{gatewayId}/manifest", h.GetGatewayManifest) - - mux.HandleFunc("GET "+constants.APIBasePath+"/gateway-custom-policies", h.ListCustomPolicies) - mux.HandleFunc("POST "+constants.APIBasePath+"/gateway-custom-policies/sync", h.SyncCustomPolicy) - mux.HandleFunc("GET "+constants.APIBasePath+"/gateway-custom-policies/{gatewayCustomPolicyId}/versions/{version}", h.GetCustomPolicy) - mux.HandleFunc("DELETE "+constants.APIBasePath+"/gateway-custom-policies/{gatewayCustomPolicyId}/versions/{version}", h.DeleteCustomPolicy) + mux.HandleFunc("POST "+constants.APIBasePath+"/gateways", middleware.MapErrors(h.slogger, h.CreateGateway)) + mux.HandleFunc("GET "+constants.APIBasePath+"/gateways", middleware.MapErrors(h.slogger, h.ListGateways)) + mux.HandleFunc("GET "+constants.APIBasePath+"/gateways/{gatewayId}", middleware.MapErrors(h.slogger, h.GetGateway)) + mux.HandleFunc("PUT "+constants.APIBasePath+"/gateways/{gatewayId}", middleware.MapErrors(h.slogger, h.UpdateGateway)) + mux.HandleFunc("DELETE "+constants.APIBasePath+"/gateways/{gatewayId}", middleware.MapErrors(h.slogger, h.DeleteGateway)) + mux.HandleFunc("GET "+constants.APIBasePath+"/gateways/{gatewayId}/tokens", middleware.MapErrors(h.slogger, h.ListTokens)) + mux.HandleFunc("POST "+constants.APIBasePath+"/gateways/{gatewayId}/tokens", middleware.MapErrors(h.slogger, h.RotateToken)) + mux.HandleFunc("DELETE "+constants.APIBasePath+"/gateways/{gatewayId}/tokens/{tokenId}", middleware.MapErrors(h.slogger, h.RevokeToken)) + mux.HandleFunc("GET "+constants.APIBasePath+"/gateways/{gatewayId}/manifest", middleware.MapErrors(h.slogger, h.GetGatewayManifest)) + + mux.HandleFunc("GET "+constants.APIBasePath+"/gateway-custom-policies", middleware.MapErrors(h.slogger, h.ListCustomPolicies)) + mux.HandleFunc("POST "+constants.APIBasePath+"/gateway-custom-policies/sync", middleware.MapErrors(h.slogger, h.SyncCustomPolicy)) + mux.HandleFunc("GET "+constants.APIBasePath+"/gateway-custom-policies/{gatewayCustomPolicyId}/versions/{version}", middleware.MapErrors(h.slogger, h.GetCustomPolicy)) + mux.HandleFunc("DELETE "+constants.APIBasePath+"/gateway-custom-policies/{gatewayCustomPolicyId}/versions/{version}", middleware.MapErrors(h.slogger, h.DeleteCustomPolicy)) } diff --git a/platform-api/internal/handler/gateway_internal.go b/platform-api/internal/handler/gateway_internal.go index c0cbaa7a35..eb82e7bc8c 100644 --- a/platform-api/internal/handler/gateway_internal.go +++ b/platform-api/internal/handler/gateway_internal.go @@ -23,15 +23,17 @@ import ( "fmt" "log/slog" "net/http" - "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" "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/middleware" + "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/service" + "github.com/wso2/api-platform/platform-api/internal/utils" "github.com/wso2/go-httpkit/httputil" ) @@ -81,72 +83,60 @@ func (h *GatewayInternalAPIHandler) authenticateGateway(apiKey string) (*model.G return h.gatewayService.VerifyToken(apiKey) } -// authenticateRequest extracts the API key from headers and authenticates the gateway. -func (h *GatewayInternalAPIHandler) authenticateRequest(w http.ResponseWriter, r *http.Request) (orgID, gatewayID string, ok bool) { +// authenticateRequest extracts the API key from headers and authenticates the gateway. Per the +// unified auth-failure rule, a missing/invalid key returns the identical generic 401; the +// specific reason travels internally via WithLogMessage only. +func (h *GatewayInternalAPIHandler) authenticateRequest(r *http.Request) (orgID, gatewayID string, err error) { clientIP := r.RemoteAddr if i := strings.LastIndex(clientIP, ":"); i != -1 { clientIP = clientIP[:i] } apiKey := r.Header.Get("api-key") - gateway, err := h.authenticateGateway(apiKey) - if err != nil { - if errors.Is(err, constants.ErrMissingAPIKey) { - h.slogger.Warn("Unauthorized access attempt - Missing API key", "clientIP", clientIP) - httputil.WriteJSON(w, http.StatusUnauthorized, utils.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, utils.NewErrorResponse(401, "Unauthorized", - "Invalid or expired API key")) - } else { - h.slogger.Error("Authentication failed", "clientIP", clientIP, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Error while validating API key")) + gateway, authErr := h.authenticateGateway(apiKey) + if authErr != nil { + if errors.Is(authErr, constants.ErrMissingAPIKey) { + return "", "", apperror.Unauthorized.New(). + WithLogMessage(fmt.Sprintf("unauthorized access attempt - missing API key, clientIP=%s", clientIP)) + } + if errors.Is(authErr, constants.ErrInvalidAPIToken) { + return "", "", apperror.Unauthorized.Wrap(authErr). + WithLogMessage(fmt.Sprintf("authentication failed - invalid API key, clientIP=%s", clientIP)) } - return "", "", false + return "", "", apperror.Internal.Wrap(authErr). + WithLogMessage(fmt.Sprintf("authentication failed, clientIP=%s", clientIP)) } - return gateway.OrganizationID, gateway.ID, true + return gateway.OrganizationID, gateway.ID, nil } // GetAPI handles GET /api/internal/v1/apis/:apiId -func (h *GatewayInternalAPIHandler) GetAPI(w http.ResponseWriter, r *http.Request) { - orgID, gatewayID, ok := h.authenticateRequest(w, r) - if !ok { - return +func (h *GatewayInternalAPIHandler) GetAPI(w http.ResponseWriter, r *http.Request) error { + orgID, gatewayID, err := h.authenticateRequest(r) + if err != nil { + return err } apiID := r.PathValue("apiId") if apiID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API ID is required")) - return + return apperror.ValidationFailed.New("API ID is required") } api, err := h.gatewayInternalService.GetActiveDeploymentByGateway(apiID, orgID, gatewayID) if err != nil { if errors.Is(err, constants.ErrDeploymentNotActive) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "No active deployment found for this API on this gateway")) - return + return apperror.DeploymentNotFound.Wrap(err). + WithLogMessage(fmt.Sprintf("no active deployment found for API %s on gateway %s", apiID, gatewayID)) } if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "API not found")) - return + return apperror.RESTAPINotFound.Wrap(err) } - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to get API")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get API %s", apiID)) } // Create ZIP file from API YAML file 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, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to create API package")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to create ZIP file for API %s", apiID)) } // Set headers for ZIP file download @@ -157,6 +147,7 @@ func (h *GatewayInternalAPIHandler) GetAPI(w http.ResponseWriter, r *http.Reques // Return ZIP file w.WriteHeader(http.StatusOK) _, _ = w.Write(zipData) + return nil } // ImportGatewayArtifacts handles POST /api/internal/v1/artifacts/import-gateway-artifacts. @@ -167,10 +158,10 @@ func (h *GatewayInternalAPIHandler) GetAPI(w http.ResponseWriter, r *http.Reques // continue-on-error: a failure on one artifact is recorded against its dpid and does not // abort the rest. The response maps each artifact's dpid to its result, with total/success/ // failed counts. Only a malformed request (bad multipart/zip) returns a non-200. -func (h *GatewayInternalAPIHandler) ImportGatewayArtifacts(w http.ResponseWriter, r *http.Request) { - orgID, gatewayID, ok := h.authenticateRequest(w, r) - if !ok { - return +func (h *GatewayInternalAPIHandler) ImportGatewayArtifacts(w http.ResponseWriter, r *http.Request) error { + orgID, gatewayID, err := h.authenticateRequest(r) + if err != nil { + return err } reqs, err := utils.ParseGatewayArtifactsRequest(r) @@ -179,9 +170,8 @@ func (h *GatewayInternalAPIHandler) ImportGatewayArtifacts(w http.ResponseWriter if i := strings.LastIndex(clientIP, ":"); i != -1 { clientIP = clientIP[:i] } - h.slogger.Warn("Invalid import-gateway-artifacts request", "clientIP", clientIP, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) - return + return apperror.ValidationFailed.Wrap(err, err.Error()). + WithLogMessage(fmt.Sprintf("invalid import-gateway-artifacts request, clientIP=%s", clientIP)) } // 'total' is advisory: log a mismatch but proceed with what the zip actually contained. if totalStr := r.FormValue("total"); totalStr != "" { @@ -195,46 +185,37 @@ func (h *GatewayInternalAPIHandler) ImportGatewayArtifacts(w http.ResponseWriter h.slogger.Info("Imported gateway artifacts batch", "gatewayID", gatewayID, "total", resp.Total, "success", resp.Success, "failed", resp.Failed) httputil.WriteJSON(w, http.StatusOK, resp) + return nil } // GetLLMProvider handles GET /api/internal/v1/llm-providers/:providerId -func (h *GatewayInternalAPIHandler) GetLLMProvider(w http.ResponseWriter, r *http.Request) { - orgID, gatewayID, ok := h.authenticateRequest(w, r) - if !ok { - return +func (h *GatewayInternalAPIHandler) GetLLMProvider(w http.ResponseWriter, r *http.Request) error { + orgID, gatewayID, err := h.authenticateRequest(r) + if err != nil { + return err } providerID := r.PathValue("providerId") if providerID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Provider ID is required")) - return + return apperror.ValidationFailed.New("Provider ID is required") } provider, err := h.gatewayInternalService.GetActiveLLMProviderDeploymentByGateway(providerID, orgID, gatewayID) if err != nil { if errors.Is(err, constants.ErrDeploymentNotActive) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "No active deployment found for this LLM provider on this gateway")) - return + return apperror.DeploymentNotFound.Wrap(err). + WithLogMessage(fmt.Sprintf("no active deployment found for LLM provider %s on gateway %s", providerID, gatewayID)) } if errors.Is(err, constants.ErrLLMProviderNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "LLM provider not found")) - return + return apperror.LLMProviderNotFound.Wrap(err) } - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to get LLM provider")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get LLM provider %s", providerID)) } // Create ZIP file from LLM provider YAML file 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, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to create LLM provider package")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to create ZIP file for LLM provider %s", providerID)) } // Set headers for ZIP file download @@ -245,46 +226,37 @@ func (h *GatewayInternalAPIHandler) GetLLMProvider(w http.ResponseWriter, r *htt // Return ZIP file w.WriteHeader(http.StatusOK) _, _ = w.Write(zipData) + return nil } // GetLLMProxy handles GET /api/internal/v1/llm-proxies/:proxyId -func (h *GatewayInternalAPIHandler) GetLLMProxy(w http.ResponseWriter, r *http.Request) { - orgID, gatewayID, ok := h.authenticateRequest(w, r) - if !ok { - return +func (h *GatewayInternalAPIHandler) GetLLMProxy(w http.ResponseWriter, r *http.Request) error { + orgID, gatewayID, err := h.authenticateRequest(r) + if err != nil { + return err } proxyID := r.PathValue("proxyId") if proxyID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Proxy ID is required")) - return + return apperror.ValidationFailed.New("Proxy ID is required") } proxy, err := h.gatewayInternalService.GetActiveLLMProxyDeploymentByGateway(proxyID, orgID, gatewayID) if err != nil { if errors.Is(err, constants.ErrDeploymentNotActive) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "No active deployment found for this LLM proxy on this gateway")) - return + return apperror.DeploymentNotFound.Wrap(err). + WithLogMessage(fmt.Sprintf("no active deployment found for LLM proxy %s on gateway %s", proxyID, gatewayID)) } if errors.Is(err, constants.ErrLLMProxyNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "LLM proxy not found")) - return + return apperror.LLMProxyNotFound.Wrap(err) } - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to get LLM proxy")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get LLM proxy %s", proxyID)) } // Create ZIP file from LLM proxy YAML file 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, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to create LLM proxy package")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to create ZIP file for LLM proxy %s", proxyID)) } // Set headers for ZIP file download @@ -295,25 +267,25 @@ func (h *GatewayInternalAPIHandler) GetLLMProxy(w http.ResponseWriter, r *http.R // Return ZIP file w.WriteHeader(http.StatusOK) _, _ = w.Write(zipData) + return nil } // GetGatewayDeployments handles GET /api/internal/v1/deployments // Returns the list of deployments that should be active on a gateway for startup sync -func (h *GatewayInternalAPIHandler) GetGatewayDeployments(w http.ResponseWriter, r *http.Request) { - orgID, gatewayID, ok := h.authenticateRequest(w, r) - if !ok { - return +func (h *GatewayInternalAPIHandler) GetGatewayDeployments(w http.ResponseWriter, r *http.Request) error { + orgID, gatewayID, err := h.authenticateRequest(r) + if err != nil { + return err } // Parse optional "since" query parameter for incremental sync var since *time.Time sinceStr := r.URL.Query().Get("since") if sinceStr != "" { - parsedTime, err := time.Parse(time.RFC3339, sinceStr) - if err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid 'since' parameter. Expected ISO 8601 format (e.g., 2026-03-04T10:00:00Z)")) - return + parsedTime, parseErr := time.Parse(time.RFC3339, sinceStr) + if parseErr != nil { + return apperror.ValidationFailed.Wrap(parseErr, + "Invalid 'since' parameter. Expected ISO 8601 format (e.g., 2026-03-04T10:00:00Z)") } since = &parsedTime } @@ -321,69 +293,51 @@ 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, utils.NewErrorResponse(404, "Not Found", - "Gateway not found")) - return + return apperror.GatewayNotFound.Wrap(err) } - h.slogger.Error("Failed to get gateway deployments", "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to get deployments")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get gateway deployments for gateway %s", gatewayID)) } httputil.WriteJSON(w, http.StatusOK, deployments) + return nil } // BatchFetchDeployments handles POST /api/internal/v1/deployments/fetch-batch // Fetches multiple deployment artifacts in a single request for gateway startup sync -func (h *GatewayInternalAPIHandler) BatchFetchDeployments(w http.ResponseWriter, r *http.Request) { - orgID, gatewayID, ok := h.authenticateRequest(w, r) - if !ok { - return +func (h *GatewayInternalAPIHandler) BatchFetchDeployments(w http.ResponseWriter, r *http.Request) error { + orgID, gatewayID, err := h.authenticateRequest(r) + if err != nil { + return err } // 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, utils.NewErrorResponse(406, "Not Acceptable", - "This endpoint only supports Accept: application/x-tar+gzip")) - return + return apperror.NotAcceptable.New() } // Parse request body var req dto.DeploymentsBatchFetchRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid request body: "+err.Error())) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body: "+err.Error()) } if len(req.DeploymentIDs) == 0 { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "At least one deployment ID is required")) - return + return apperror.ValidationFailed.New("At least one deployment ID is required") } // 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, utils.NewErrorResponse(404, "Not Found", - "Gateway not found")) - return + return apperror.GatewayNotFound.Wrap(err) } - h.slogger.Error("Failed to get deployment content batch", "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to get deployment content")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get deployment content batch for gateway %s", gatewayID)) } // Create TAR.GZ archive from deployment content 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, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to create deployment package")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to create batch TAR.GZ archive for gateway %s", gatewayID)) } // Set headers for TAR.GZ download @@ -394,148 +348,92 @@ func (h *GatewayInternalAPIHandler) BatchFetchDeployments(w http.ResponseWriter, // Return TAR.GZ archive w.WriteHeader(http.StatusOK) _, _ = w.Write(tarGzData) + return nil } // GetSubscriptions handles GET /api/internal/v1/apis/:apiId/subscriptions -func (h *GatewayInternalAPIHandler) GetSubscriptions(w http.ResponseWriter, r *http.Request) { - orgID, gatewayID, ok := h.authenticateRequest(w, r) - if !ok { - return +func (h *GatewayInternalAPIHandler) GetSubscriptions(w http.ResponseWriter, r *http.Request) error { + orgID, gatewayID, err := h.authenticateRequest(r) + if err != nil { + return err } apiID := r.PathValue("apiId") if apiID == "" { - clientIP := r.RemoteAddr - if i := strings.LastIndex(clientIP, ":"); i != -1 { - clientIP = clientIP[:i] - } - h.slogger.Error("API ID is required for subscriptions request", - "clientIP", clientIP, - "organizationId", orgID, - "apiId", apiID) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API ID is required")) - return + return apperror.ValidationFailed.New("API ID is required"). + WithLogMessage(fmt.Sprintf("API ID is required for subscriptions request, orgID=%s", orgID)) } if err := h.gatewayInternalService.IsAPIDeployedOnGateway(apiID, gatewayID, orgID); err != nil { if errors.Is(err, constants.ErrAPINotFound) { - h.slogger.Error("API not found when listing subscriptions", - "apiId", apiID, - "organizationId", orgID, - "gatewayId", gatewayID, - "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "API not found")) - return + return apperror.RESTAPINotFound.Wrap(err). + WithLogMessage(fmt.Sprintf("API not found when listing subscriptions, apiId=%s, orgID=%s, gatewayID=%s", apiID, orgID, gatewayID)) } if errors.Is(err, constants.ErrDeploymentNotActive) { - 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, utils.NewErrorResponse(403, "Forbidden", - "API is not associated with this gateway")) - return + return apperror.Forbidden.New(). + WithLogMessage(fmt.Sprintf("subscription list denied - API has no active deployment status on gateway, apiId=%s, orgID=%s, gatewayID=%s", apiID, orgID, gatewayID)) } - h.slogger.Error("Failed to verify API deployment for subscriptions", - "apiId", apiID, - "organizationId", orgID, - "gatewayId", gatewayID, - "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to verify API deployment")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to verify API deployment for subscriptions, apiId=%s, orgID=%s, gatewayID=%s", apiID, orgID, gatewayID)) } subs, err := h.gatewayInternalService.ListSubscriptionsForAPI(apiID, orgID) if err != nil { if errors.Is(err, constants.ErrAPINotFound) { - h.slogger.Error("API not found when listing subscriptions", - "apiId", apiID, - "organizationId", orgID, - "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "API not found")) - return + return apperror.RESTAPINotFound.Wrap(err). + WithLogMessage(fmt.Sprintf("API not found when listing subscriptions, apiId=%s, orgID=%s", apiID, orgID)) } - h.slogger.Error("Failed to list subscriptions for API", - "apiId", apiID, - "organizationId", orgID, - "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to get subscriptions")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to list subscriptions for API, apiId=%s, orgID=%s", apiID, orgID)) } httputil.WriteJSON(w, http.StatusOK, subs) + return nil } // GetSubscriptionPlans handles GET /api/internal/v1/subscription-plans -func (h *GatewayInternalAPIHandler) GetSubscriptionPlans(w http.ResponseWriter, r *http.Request) { - orgID, _, ok := h.authenticateRequest(w, r) - if !ok { - return +func (h *GatewayInternalAPIHandler) GetSubscriptionPlans(w http.ResponseWriter, r *http.Request) error { + orgID, _, err := h.authenticateRequest(r) + if err != nil { + return err } plans, err := h.gatewayInternalService.ListSubscriptionPlansForOrg(orgID) if err != nil { - h.slogger.Error("Failed to list subscription plans", - "organizationId", orgID, - "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to get subscription plans")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to list subscription plans, orgID=%s", orgID)) } httputil.WriteJSON(w, http.StatusOK, plans) + return nil } // GetMCPProxy handles GET /api/internal/v1/mcp-proxies/:proxyId -func (h *GatewayInternalAPIHandler) GetMCPProxy(w http.ResponseWriter, r *http.Request) { - - orgID, gatewayID, ok := h.authenticateRequest(w, r) - if !ok { - return +func (h *GatewayInternalAPIHandler) GetMCPProxy(w http.ResponseWriter, r *http.Request) error { + orgID, gatewayID, err := h.authenticateRequest(r) + if err != nil { + return err } proxyID := r.PathValue("proxyId") if proxyID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Proxy ID is required")) - return + return apperror.ValidationFailed.New("Proxy ID is required") } proxy, err := h.gatewayInternalService.GetActiveMCPProxyDeploymentByGateway(proxyID, orgID, gatewayID) if err != nil { - clientIP := r.RemoteAddr - if i := strings.LastIndex(clientIP, ":"); i != -1 { - clientIP = clientIP[:i] - } if errors.Is(err, constants.ErrDeploymentNotActive) { - 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, utils.NewErrorResponse(404, "Not Found", - "No active deployment found for this MCP proxy on this gateway")) - return + return apperror.DeploymentNotFound.Wrap(err). + WithLogMessage(fmt.Sprintf("no active deployment found for MCP proxy %s on gateway %s", proxyID, gatewayID)) } if errors.Is(err, constants.ErrMCPProxyNotFound) { - h.slogger.Error("MCP proxy not found", "clientIP", clientIP, "proxyID", proxyID, "orgID", orgID, "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "MCP proxy not found")) - return + return apperror.MCPProxyNotFound.Wrap(err) } - h.slogger.Error("Failed to get MCP proxy", "clientIP", clientIP, "proxyID", proxyID, "orgID", orgID, "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to get MCP proxy")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get MCP proxy %s", proxyID)) } // Create ZIP file from MCP proxy YAML file 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, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to create MCP proxy package")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to create ZIP file for MCP proxy %s", proxyID)) } // Set headers for ZIP file download @@ -546,53 +444,37 @@ func (h *GatewayInternalAPIHandler) GetMCPProxy(w http.ResponseWriter, r *http.R // Return ZIP file w.WriteHeader(http.StatusOK) _, _ = w.Write(zipData) + return nil } // GetWebSubAPI handles GET /api/internal/v1/websub-apis/:apiId -func (h *GatewayInternalAPIHandler) GetWebSubAPI(w http.ResponseWriter, r *http.Request) { - orgID, gatewayID, ok := h.authenticateRequest(w, r) - if !ok { - return +func (h *GatewayInternalAPIHandler) GetWebSubAPI(w http.ResponseWriter, r *http.Request) error { + orgID, gatewayID, err := h.authenticateRequest(r) + if err != nil { + return err } apiID := r.PathValue("apiId") if apiID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API ID is required")) - return + return apperror.ValidationFailed.New("API ID is required") } api, err := h.gatewayInternalService.GetActiveWebSubAPIDeploymentByGateway(apiID, orgID, gatewayID) if err != nil { - clientIP := r.RemoteAddr - if i := strings.LastIndex(clientIP, ":"); i != -1 { - clientIP = clientIP[:i] - } if errors.Is(err, constants.ErrDeploymentNotActive) { - 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, utils.NewErrorResponse(404, "Not Found", - "No active deployment found for this WebSub API on this gateway")) - return + return apperror.DeploymentNotFound.Wrap(err). + WithLogMessage(fmt.Sprintf("no active deployment found for WebSub API %s on gateway %s", apiID, gatewayID)) } if errors.Is(err, constants.ErrWebSubAPINotFound) { - h.slogger.Error("WebSub API not found", "clientIP", clientIP, "apiID", apiID, "orgID", orgID, "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "WebSub API not found")) - return + return apperror.ArtifactNotFound.Wrap(err).WithLogMessage(fmt.Sprintf("WebSub API not found, apiID=%s", apiID)) } - h.slogger.Error("Failed to get WebSub API", "clientIP", clientIP, "apiID", apiID, "orgID", orgID, "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to get WebSub API")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get WebSub API %s", apiID)) } // Create ZIP file from WebSub API YAML file 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, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to create WebSub API package")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to create ZIP file for WebSub API %s", apiID)) } // Set headers for ZIP file download @@ -603,53 +485,37 @@ func (h *GatewayInternalAPIHandler) GetWebSubAPI(w http.ResponseWriter, r *http. // Return ZIP file w.WriteHeader(http.StatusOK) _, _ = w.Write(zipData) + return nil } // GetWebBrokerAPI handles GET /api/internal/v1/webbroker-apis/:apiId -func (h *GatewayInternalAPIHandler) GetWebBrokerAPI(w http.ResponseWriter, r *http.Request) { - orgID, gatewayID, ok := h.authenticateRequest(w, r) - if !ok { - return +func (h *GatewayInternalAPIHandler) GetWebBrokerAPI(w http.ResponseWriter, r *http.Request) error { + orgID, gatewayID, err := h.authenticateRequest(r) + if err != nil { + return err } apiID := r.PathValue("apiId") if apiID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "API ID is required")) - return + return apperror.ValidationFailed.New("API ID is required") } api, err := h.gatewayInternalService.GetActiveWebBrokerAPIDeploymentByGateway(apiID, orgID, gatewayID) if err != nil { - clientIP := r.RemoteAddr - if i := strings.LastIndex(clientIP, ":"); i != -1 { - clientIP = clientIP[:i] - } if errors.Is(err, constants.ErrDeploymentNotActive) { - 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, utils.NewErrorResponse(404, "Not Found", - "No active deployment found for this WebBroker API on this gateway")) - return + return apperror.DeploymentNotFound.Wrap(err). + WithLogMessage(fmt.Sprintf("no active deployment found for WebBroker API %s on gateway %s", apiID, gatewayID)) } if errors.Is(err, constants.ErrWebBrokerAPINotFound) { - h.slogger.Error("WebBroker API not found", "clientIP", clientIP, "apiID", apiID, "orgID", orgID, "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", - "WebBroker API not found")) - return + return apperror.ArtifactNotFound.Wrap(err).WithLogMessage(fmt.Sprintf("WebBroker API not found, apiID=%s", apiID)) } - h.slogger.Error("Failed to get WebBroker API", "clientIP", clientIP, "apiID", apiID, "orgID", orgID, "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to get WebBroker API")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get WebBroker API %s", apiID)) } // Create ZIP file from WebBroker API YAML file 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, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to create WebBroker API package")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to create ZIP file for WebBroker API %s", apiID)) } // Set headers for ZIP file download @@ -660,14 +526,15 @@ func (h *GatewayInternalAPIHandler) GetWebBrokerAPI(w http.ResponseWriter, r *ht // Return ZIP file w.WriteHeader(http.StatusOK) _, _ = w.Write(zipData) + return nil } // ReceiveGatewayManifest handles POST /api/internal/v1/gateways/:gatewayId/manifest // Called by the gateway controller to post back its installed custom policy manifest. -func (h *GatewayInternalAPIHandler) ReceiveGatewayManifest(w http.ResponseWriter, r *http.Request) { - orgID, gatewayID, ok := h.authenticateRequest(w, r) - if !ok { - return +func (h *GatewayInternalAPIHandler) ReceiveGatewayManifest(w http.ResponseWriter, r *http.Request) error { + orgID, gatewayID, err := h.authenticateRequest(r) + if err != nil { + return err } var body struct { @@ -676,137 +543,119 @@ 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, utils.NewErrorResponse(400, "Bad Request", err.Error())) - return + return apperror.ValidationFailed.Wrap(err, err.Error()) } 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, utils.NewErrorResponse(409, "Conflict", err.Error())) - return + return apperror.Conflict.Wrap(err).WithLogMessage(fmt.Sprintf("gateway manifest rejected: version mismatch, gatewayID=%s", gatewayID)) } if errors.Is(err, constants.ErrGatewayFunctionalityTypeMismatch) { - h.slogger.Warn("Gateway manifest rejected: functionality type mismatch", "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", err.Error())) - return + return apperror.Conflict.Wrap(err).WithLogMessage(fmt.Sprintf("gateway manifest rejected: functionality type mismatch, gatewayID=%s", gatewayID)) } if errors.Is(err, constants.ErrGatewayNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", err.Error())) - return + return apperror.GatewayNotFound.Wrap(err) } - h.slogger.Error("Failed to store gateway manifest", "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to store gateway manifest")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to store gateway manifest, gatewayID=%s", gatewayID)) } w.WriteHeader(http.StatusNoContent) + return nil } // GetRestAPIAPIKeys handles GET /api/internal/v1/apis/api-keys -func (h *GatewayInternalAPIHandler) GetRestAPIAPIKeys(w http.ResponseWriter, r *http.Request) { - orgID, gatewayID, ok := h.authenticateRequest(w, r) - if !ok { - return +func (h *GatewayInternalAPIHandler) GetRestAPIAPIKeys(w http.ResponseWriter, r *http.Request) error { + orgID, gatewayID, err := h.authenticateRequest(r) + if err != nil { + return err } issuer := r.URL.Query().Get("issuer") 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, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get API keys for REST APIs, gatewayID=%s", gatewayID)) } httputil.WriteJSON(w, http.StatusOK, keys) + return nil } // GetLLMProviderAPIKeys handles GET /api/internal/v1/llm-providers/api-keys -func (h *GatewayInternalAPIHandler) GetLLMProviderAPIKeys(w http.ResponseWriter, r *http.Request) { - orgID, gatewayID, ok := h.authenticateRequest(w, r) - if !ok { - return +func (h *GatewayInternalAPIHandler) GetLLMProviderAPIKeys(w http.ResponseWriter, r *http.Request) error { + orgID, gatewayID, err := h.authenticateRequest(r) + if err != nil { + return err } issuer := r.URL.Query().Get("issuer") 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, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get API keys for LLM providers, gatewayID=%s", gatewayID)) } httputil.WriteJSON(w, http.StatusOK, keys) + return nil } // GetLLMProxyAPIKeys handles GET /api/internal/v1/llm-proxies/api-keys -func (h *GatewayInternalAPIHandler) GetLLMProxyAPIKeys(w http.ResponseWriter, r *http.Request) { - orgID, gatewayID, ok := h.authenticateRequest(w, r) - if !ok { - return +func (h *GatewayInternalAPIHandler) GetLLMProxyAPIKeys(w http.ResponseWriter, r *http.Request) error { + orgID, gatewayID, err := h.authenticateRequest(r) + if err != nil { + return err } issuer := r.URL.Query().Get("issuer") 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, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get API keys for LLM proxies, gatewayID=%s", gatewayID)) } httputil.WriteJSON(w, http.StatusOK, keys) + return nil } // GetWebSubAPIAPIKeys handles GET /api/internal/v1/websub-apis/api-keys -func (h *GatewayInternalAPIHandler) GetWebSubAPIAPIKeys(w http.ResponseWriter, r *http.Request) { - orgID, gatewayID, ok := h.authenticateRequest(w, r) - if !ok { - return +func (h *GatewayInternalAPIHandler) GetWebSubAPIAPIKeys(w http.ResponseWriter, r *http.Request) error { + orgID, gatewayID, err := h.authenticateRequest(r) + if err != nil { + return err } issuer := r.URL.Query().Get("issuer") 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, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get API keys for WebSub APIs, gatewayID=%s", gatewayID)) } httputil.WriteJSON(w, http.StatusOK, keys) + return nil } // GetWebBrokerAPIAPIKeys handles GET /api/internal/v1/webbroker-apis/api-keys -func (h *GatewayInternalAPIHandler) GetWebBrokerAPIAPIKeys(w http.ResponseWriter, r *http.Request) { - orgID, gatewayID, ok := h.authenticateRequest(w, r) - if !ok { - return +func (h *GatewayInternalAPIHandler) GetWebBrokerAPIAPIKeys(w http.ResponseWriter, r *http.Request) error { + orgID, gatewayID, err := h.authenticateRequest(r) + if err != nil { + return err } issuer := r.URL.Query().Get("issuer") 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, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get API keys for WebBroker APIs, gatewayID=%s", gatewayID)) } httputil.WriteJSON(w, http.StatusOK, keys) + return nil } // CheckArtifactsExist handles POST /api/internal/v1/artifacts/exists // Returns the subset of provided artifact UUIDs that still exist on the platform. // Used by the gateway during sync to avoid deleting artifacts that still exist // but have no active deployment (e.g., after deployment deletion). -func (h *GatewayInternalAPIHandler) CheckArtifactsExist(w http.ResponseWriter, r *http.Request) { - orgID, _, ok := h.authenticateRequest(w, r) - if !ok { - return +func (h *GatewayInternalAPIHandler) CheckArtifactsExist(w http.ResponseWriter, r *http.Request) error { + orgID, _, err := h.authenticateRequest(r) + if err != nil { + return err } var req dto.ArtifactsExistRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid request body: artifactIds is required and must be a non-empty array")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body: artifactIds is required and must be a non-empty array") } 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, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to check artifact existence")) - return + return apperror.Internal.Wrap(err).WithLogMessage("failed to check artifact existence") } // Build a set of existing IDs for O(1) lookup @@ -828,47 +677,44 @@ func (h *GatewayInternalAPIHandler) CheckArtifactsExist(w http.ResponseWriter, r httputil.WriteJSON(w, http.StatusOK, dto.ArtifactsExistResponse{ Artifacts: artifacts, }) + return nil } // GetWebSubAPIHmacSecrets handles GET /api/internal/v1/websub-apis/:apiId/secrets // Returns decrypted plaintext HMAC secrets for the gateway-controller to load into its webhook secret store. -func (h *GatewayInternalAPIHandler) GetWebSubAPIHmacSecrets(w http.ResponseWriter, r *http.Request) { - _, _, ok := h.authenticateRequest(w, r) - if !ok { - return +func (h *GatewayInternalAPIHandler) GetWebSubAPIHmacSecrets(w http.ResponseWriter, r *http.Request) error { + _, _, err := h.authenticateRequest(r) + if err != nil { + return err } apiID := r.PathValue("apiId") if apiID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API ID is required")) - return + return apperror.ValidationFailed.New("API ID is required") } if h.hmacSecretService == nil { - h.slogger.Warn("HMAC secret service not configured", "apiID", apiID) - httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponse(503, "Service Unavailable", "HMAC secret management is not configured on this server")) - return + return apperror.ServiceUnavailable.New(). + WithLogMessage(fmt.Sprintf("HMAC secret service not configured, apiID=%s", apiID)) } 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, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get HMAC secrets")) - return + return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to list HMAC secrets for WebSub API %s", apiID)) } items := make([]dto.GatewayHmacSecretInfo, 0, len(secrets)) for _, s := range secrets { 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, utils.NewErrorResponse(500, "Internal Server Error", "Failed to decrypt HMAC secret")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to decrypt HMAC secret, apiID=%s, secretName=%s", apiID, s.Handle)) } items = append(items, dto.GatewayHmacSecretInfo{Name: s.Handle, Secret: plaintext}) } httputil.WriteJSON(w, http.StatusOK, dto.GatewayHmacSecretsResponse{ArtifactID: apiID, Secrets: items}) + return nil } // GetGatewaySecrets handles GET /api/internal/v1/secrets @@ -876,19 +722,17 @@ func (h *GatewayInternalAPIHandler) GetWebSubAPIHmacSecrets(w http.ResponseWrite // Supports ?updatedAfter= for incremental sync. // Supports ?includeValues=true for startup bulk fetch — decrypts all secrets server-side // and returns plaintext values in a single response, avoiding N per-secret round trips. -func (h *GatewayInternalAPIHandler) GetGatewaySecrets(w http.ResponseWriter, r *http.Request) { - orgID, gatewayID, ok := h.authenticateRequest(w, r) - if !ok { - return +func (h *GatewayInternalAPIHandler) GetGatewaySecrets(w http.ResponseWriter, r *http.Request) error { + orgID, gatewayID, err := h.authenticateRequest(r) + if err != nil { + return err } var updatedAfter *time.Time if s := r.URL.Query().Get("updatedAfter"); s != "" { - t, err := time.Parse(time.RFC3339, s) - if err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", - "Invalid 'updatedAfter' parameter. Expected RFC3339 format.")) - return + t, parseErr := time.Parse(time.RFC3339, s) + if parseErr != nil { + return apperror.ValidationFailed.Wrap(parseErr, "Invalid 'updatedAfter' parameter. Expected RFC3339 format.") } updatedAfter = &t } @@ -897,10 +741,8 @@ 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, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to retrieve secrets")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to list gateway secrets, orgID=%s, gatewayID=%s", orgID, gatewayID)) } items := make([]dto.SecretSyncItem, 0, len(secrets)) @@ -919,81 +761,75 @@ func (h *GatewayInternalAPIHandler) GetGatewaySecrets(w http.ResponseWriter, r * if includeValues && s.Status == model.SecretStatusActive { 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, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to decrypt secret")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to decrypt secret for bulk fetch, orgID=%s, handle=%s", orgID, s.Handle)) } item.Value = &plaintext } items = append(items, item) } httputil.WriteJSON(w, http.StatusOK, dto.SecretSyncListResponse{List: items, Count: len(items)}) + return nil } // GetGatewaySecretValue handles GET /api/internal/v1/secrets/{handle}/value // Returns the decrypted plaintext value of a secret. Called by the GW controller // only when the secret's hash has changed, minimising decryption calls. // Authenticated via gateway api-key — no JWT required. -func (h *GatewayInternalAPIHandler) GetGatewaySecretValue(w http.ResponseWriter, r *http.Request) { - orgID, gatewayID, ok := h.authenticateRequest(w, r) - if !ok { - return +func (h *GatewayInternalAPIHandler) GetGatewaySecretValue(w http.ResponseWriter, r *http.Request) error { + orgID, gatewayID, err := h.authenticateRequest(r) + if err != nil { + return err } handle := r.PathValue("handle") if handle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Secret handle is required")) - return + return apperror.ValidationFailed.New("Secret handle is required") } // Only serve secrets that are referenced by artifacts deployed on this gateway. 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, utils.NewErrorResponse(500, "Internal Server Error", "Failed to verify secret access")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to check secret deployment scope, orgID=%s, gatewayID=%s, handle=%s", orgID, gatewayID, handle)) } if !deployed { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Secret not found")) - return + return apperror.SecretNotFound.New() } plaintext, err := h.secretService.Decrypt(orgID, handle) if err != nil { if errors.Is(err, constants.ErrSecretNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Secret not found")) - return + return apperror.SecretNotFound.Wrap(err) } - h.slogger.Error("Failed to decrypt secret for gateway", "orgID", orgID, "handle", handle, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", - "Failed to decrypt secret")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to decrypt secret for gateway, orgID=%s, handle=%s", orgID, handle)) } httputil.WriteJSON(w, http.StatusOK, map[string]any{"value": plaintext}) + return nil } func (h *GatewayInternalAPIHandler) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("GET /api/internal/v1/apis/api-keys", h.GetRestAPIAPIKeys) - mux.HandleFunc("GET /api/internal/v1/apis/{apiId}", h.GetAPI) - mux.HandleFunc("GET /api/internal/v1/apis/{apiId}/subscriptions", h.GetSubscriptions) - mux.HandleFunc("GET /api/internal/v1/subscription-plans", h.GetSubscriptionPlans) - mux.HandleFunc("GET /api/internal/v1/secrets", h.GetGatewaySecrets) - mux.HandleFunc("GET /api/internal/v1/secrets/{handle}/value", h.GetGatewaySecretValue) - mux.HandleFunc("GET /api/internal/v1/llm-providers/api-keys", h.GetLLMProviderAPIKeys) - mux.HandleFunc("GET /api/internal/v1/llm-providers/{providerId}", h.GetLLMProvider) - mux.HandleFunc("GET /api/internal/v1/llm-proxies/api-keys", h.GetLLMProxyAPIKeys) - mux.HandleFunc("GET /api/internal/v1/llm-proxies/{proxyId}", h.GetLLMProxy) - mux.HandleFunc("GET /api/internal/v1/deployments", h.GetGatewayDeployments) - mux.HandleFunc("POST /api/internal/v1/deployments/fetch-batch", h.BatchFetchDeployments) - mux.HandleFunc("GET /api/internal/v1/mcp-proxies/{proxyId}", h.GetMCPProxy) - mux.HandleFunc("GET /api/internal/v1/websub-apis/api-keys", h.GetWebSubAPIAPIKeys) - mux.HandleFunc("GET /api/internal/v1/websub-apis/{apiId}", h.GetWebSubAPI) - mux.HandleFunc("GET /api/internal/v1/websub-apis/{apiId}/secrets", h.GetWebSubAPIHmacSecrets) - mux.HandleFunc("GET /api/internal/v1/webbroker-apis/api-keys", h.GetWebBrokerAPIAPIKeys) - mux.HandleFunc("GET /api/internal/v1/webbroker-apis/{apiId}", h.GetWebBrokerAPI) - mux.HandleFunc("POST /api/internal/v1/gateways/{gatewayId}/manifest", h.ReceiveGatewayManifest) - mux.HandleFunc("POST /api/internal/v1/artifacts/exists", h.CheckArtifactsExist) - mux.HandleFunc("POST /api/internal/v1/artifacts/import-gateway-artifacts", h.ImportGatewayArtifacts) + mux.HandleFunc("GET /api/internal/v1/apis/api-keys", middleware.MapErrors(h.slogger, h.GetRestAPIAPIKeys)) + mux.HandleFunc("GET /api/internal/v1/apis/{apiId}", middleware.MapErrors(h.slogger, h.GetAPI)) + mux.HandleFunc("GET /api/internal/v1/apis/{apiId}/subscriptions", middleware.MapErrors(h.slogger, h.GetSubscriptions)) + mux.HandleFunc("GET /api/internal/v1/subscription-plans", middleware.MapErrors(h.slogger, h.GetSubscriptionPlans)) + mux.HandleFunc("GET /api/internal/v1/secrets", middleware.MapErrors(h.slogger, h.GetGatewaySecrets)) + mux.HandleFunc("GET /api/internal/v1/secrets/{handle}/value", middleware.MapErrors(h.slogger, h.GetGatewaySecretValue)) + mux.HandleFunc("GET /api/internal/v1/llm-providers/api-keys", middleware.MapErrors(h.slogger, h.GetLLMProviderAPIKeys)) + mux.HandleFunc("GET /api/internal/v1/llm-providers/{providerId}", middleware.MapErrors(h.slogger, h.GetLLMProvider)) + mux.HandleFunc("GET /api/internal/v1/llm-proxies/api-keys", middleware.MapErrors(h.slogger, h.GetLLMProxyAPIKeys)) + mux.HandleFunc("GET /api/internal/v1/llm-proxies/{proxyId}", middleware.MapErrors(h.slogger, h.GetLLMProxy)) + mux.HandleFunc("GET /api/internal/v1/deployments", middleware.MapErrors(h.slogger, h.GetGatewayDeployments)) + mux.HandleFunc("POST /api/internal/v1/deployments/fetch-batch", middleware.MapErrors(h.slogger, h.BatchFetchDeployments)) + mux.HandleFunc("GET /api/internal/v1/mcp-proxies/{proxyId}", middleware.MapErrors(h.slogger, h.GetMCPProxy)) + mux.HandleFunc("GET /api/internal/v1/websub-apis/api-keys", middleware.MapErrors(h.slogger, h.GetWebSubAPIAPIKeys)) + mux.HandleFunc("GET /api/internal/v1/websub-apis/{apiId}", middleware.MapErrors(h.slogger, h.GetWebSubAPI)) + mux.HandleFunc("GET /api/internal/v1/websub-apis/{apiId}/secrets", middleware.MapErrors(h.slogger, h.GetWebSubAPIHmacSecrets)) + mux.HandleFunc("GET /api/internal/v1/webbroker-apis/api-keys", middleware.MapErrors(h.slogger, h.GetWebBrokerAPIAPIKeys)) + mux.HandleFunc("GET /api/internal/v1/webbroker-apis/{apiId}", middleware.MapErrors(h.slogger, h.GetWebBrokerAPI)) + mux.HandleFunc("POST /api/internal/v1/gateways/{gatewayId}/manifest", middleware.MapErrors(h.slogger, h.ReceiveGatewayManifest)) + mux.HandleFunc("POST /api/internal/v1/artifacts/exists", middleware.MapErrors(h.slogger, h.CheckArtifactsExist)) + mux.HandleFunc("POST /api/internal/v1/artifacts/import-gateway-artifacts", middleware.MapErrors(h.slogger, h.ImportGatewayArtifacts)) } diff --git a/platform-api/internal/handler/identity_helper.go b/platform-api/internal/handler/identity_helper.go index b74636e961..53219076c7 100644 --- a/platform-api/internal/handler/identity_helper.go +++ b/platform-api/internal/handler/identity_helper.go @@ -18,28 +18,24 @@ package handler import ( - "log/slog" "net/http" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/service" - "github.com/wso2/api-platform/platform-api/internal/utils" - - "github.com/wso2/go-httpkit/httputil" ) -// resolveActor resolves the internal platform UUID for the actor behind r via -// identity, for use in audit columns (created_by/updated_by/revoked_by/ +// resolveActorErr resolves the internal platform UUID for the actor behind r +// via identity, for use in audit columns (created_by/updated_by/revoked_by/ // performed_by). Identity is always resolved from the token claim (sub, // falling back to the configured claim / user_id) — a missing identity mints // an anonymous UUID rather than failing (see IdentityService.InternalUserID), -// so this only fails (500) on a genuine DB error. -func resolveActor(w http.ResponseWriter, r *http.Request, identity *service.IdentityService, slogger *slog.Logger, action string) (actor string, ok bool) { +// so this only fails (500) on a genuine DB error. Returns an *apperror.Error +// for the mapper to log and serialize. +func resolveActorErr(r *http.Request, identity *service.IdentityService, action string) (string, error) { actor, err := identity.InternalUserID(r) if err != nil { - slogger.Error("Failed to resolve user identity", "action", action, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode(utils.CodeCommonInternalError, - "Failed to resolve user identity")) - return "", false + return "", apperror.Internal.Wrap(err). + WithLogMessage("failed to resolve user identity for action: " + action) } - return actor, true + return actor, nil } diff --git a/platform-api/internal/handler/llm.go b/platform-api/internal/handler/llm.go index 1f7eacc1bf..7ca735dca3 100644 --- a/platform-api/internal/handler/llm.go +++ b/platform-api/internal/handler/llm.go @@ -20,6 +20,7 @@ package handler import ( "encoding/json" "errors" + "fmt" "io" "log/slog" "net/http" @@ -27,10 +28,10 @@ import ( "strings" "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/service" - "github.com/wso2/api-platform/platform-api/internal/utils" "github.com/wso2/go-httpkit/httputil" ) @@ -55,28 +56,28 @@ func NewLLMHandler( func (h *LLMHandler) RegisterRoutes(mux *http.ServeMux) { // LLM Provider Templates - mux.HandleFunc("POST "+constants.APIBasePath+"/llm-provider-templates", h.CreateLLMProviderTemplate) - mux.HandleFunc("POST "+constants.APIBasePath+"/llm-provider-templates/copy", h.CopyLLMProviderTemplateVersion) - mux.HandleFunc("GET "+constants.APIBasePath+"/llm-provider-templates", h.ListLLMProviderTemplates) - mux.HandleFunc("GET "+constants.APIBasePath+"/llm-provider-templates/{llmProviderTemplateId}", h.GetLLMProviderTemplate) - mux.HandleFunc("PUT "+constants.APIBasePath+"/llm-provider-templates/{llmProviderTemplateId}", h.UpdateLLMProviderTemplate) - mux.HandleFunc("PATCH "+constants.APIBasePath+"/llm-provider-templates/{llmProviderTemplateId}", h.SetLLMProviderTemplateVersionEnabled) - mux.HandleFunc("DELETE "+constants.APIBasePath+"/llm-provider-templates/{llmProviderTemplateId}", h.DeleteLLMProviderTemplateVersion) + mux.HandleFunc("POST "+constants.APIBasePath+"/llm-provider-templates", middleware.MapErrors(h.slogger, h.CreateLLMProviderTemplate)) + mux.HandleFunc("POST "+constants.APIBasePath+"/llm-provider-templates/copy", middleware.MapErrors(h.slogger, h.CopyLLMProviderTemplateVersion)) + mux.HandleFunc("GET "+constants.APIBasePath+"/llm-provider-templates", middleware.MapErrors(h.slogger, h.ListLLMProviderTemplates)) + mux.HandleFunc("GET "+constants.APIBasePath+"/llm-provider-templates/{llmProviderTemplateId}", middleware.MapErrors(h.slogger, h.GetLLMProviderTemplate)) + mux.HandleFunc("PUT "+constants.APIBasePath+"/llm-provider-templates/{llmProviderTemplateId}", middleware.MapErrors(h.slogger, h.UpdateLLMProviderTemplate)) + mux.HandleFunc("PATCH "+constants.APIBasePath+"/llm-provider-templates/{llmProviderTemplateId}", middleware.MapErrors(h.slogger, h.SetLLMProviderTemplateVersionEnabled)) + mux.HandleFunc("DELETE "+constants.APIBasePath+"/llm-provider-templates/{llmProviderTemplateId}", middleware.MapErrors(h.slogger, h.DeleteLLMProviderTemplateVersion)) // LLM Providers - mux.HandleFunc("POST "+constants.APIBasePath+"/llm-providers", h.CreateLLMProvider) - mux.HandleFunc("GET "+constants.APIBasePath+"/llm-providers", h.ListLLMProviders) - mux.HandleFunc("GET "+constants.APIBasePath+"/llm-providers/{llmProviderId}", h.GetLLMProvider) - mux.HandleFunc("GET "+constants.APIBasePath+"/llm-providers/{llmProviderId}/llm-proxies", h.ListLLMProxiesByProvider) - mux.HandleFunc("PUT "+constants.APIBasePath+"/llm-providers/{llmProviderId}", h.UpdateLLMProvider) - mux.HandleFunc("DELETE "+constants.APIBasePath+"/llm-providers/{llmProviderId}", h.DeleteLLMProvider) + mux.HandleFunc("POST "+constants.APIBasePath+"/llm-providers", middleware.MapErrors(h.slogger, h.CreateLLMProvider)) + mux.HandleFunc("GET "+constants.APIBasePath+"/llm-providers", middleware.MapErrors(h.slogger, h.ListLLMProviders)) + mux.HandleFunc("GET "+constants.APIBasePath+"/llm-providers/{llmProviderId}", middleware.MapErrors(h.slogger, h.GetLLMProvider)) + mux.HandleFunc("GET "+constants.APIBasePath+"/llm-providers/{llmProviderId}/llm-proxies", middleware.MapErrors(h.slogger, h.ListLLMProxiesByProvider)) + mux.HandleFunc("PUT "+constants.APIBasePath+"/llm-providers/{llmProviderId}", middleware.MapErrors(h.slogger, h.UpdateLLMProvider)) + mux.HandleFunc("DELETE "+constants.APIBasePath+"/llm-providers/{llmProviderId}", middleware.MapErrors(h.slogger, h.DeleteLLMProvider)) // LLM Proxies - mux.HandleFunc("POST "+constants.APIBasePath+"/llm-proxies", h.CreateLLMProxy) - mux.HandleFunc("GET "+constants.APIBasePath+"/llm-proxies", h.ListLLMProxies) - mux.HandleFunc("GET "+constants.APIBasePath+"/llm-proxies/{llmProxyId}", h.GetLLMProxy) - mux.HandleFunc("PUT "+constants.APIBasePath+"/llm-proxies/{llmProxyId}", h.UpdateLLMProxy) - mux.HandleFunc("DELETE "+constants.APIBasePath+"/llm-proxies/{llmProxyId}", h.DeleteLLMProxy) + mux.HandleFunc("POST "+constants.APIBasePath+"/llm-proxies", middleware.MapErrors(h.slogger, h.CreateLLMProxy)) + mux.HandleFunc("GET "+constants.APIBasePath+"/llm-proxies", middleware.MapErrors(h.slogger, h.ListLLMProxies)) + mux.HandleFunc("GET "+constants.APIBasePath+"/llm-proxies/{llmProxyId}", middleware.MapErrors(h.slogger, h.GetLLMProxy)) + mux.HandleFunc("PUT "+constants.APIBasePath+"/llm-proxies/{llmProxyId}", middleware.MapErrors(h.slogger, h.UpdateLLMProxy)) + mux.HandleFunc("DELETE "+constants.APIBasePath+"/llm-proxies/{llmProxyId}", middleware.MapErrors(h.slogger, h.DeleteLLMProxy)) } // templateQuery holds the fields parsed from the ?query= search DSL used by the @@ -106,50 +107,40 @@ func parseTemplateQuery(raw string) (q templateQuery, found bool) { return q, found } -func (h *LLMHandler) CreateLLMProviderTemplate(w http.ResponseWriter, r *http.Request) { +func (h *LLMHandler) CreateLLMProviderTemplate(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } var req api.LLMProviderTemplate if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid request body")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body") } - createdBy, ok := resolveActor(w, r, h.identity, h.slogger, "create LLM provider template") - if !ok { - return + createdBy, err := resolveActorErr(r, h.identity, "create LLM provider template") + if err != nil { + return err } created, err := h.templateService.Create(orgID, createdBy, &req) if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderTemplateExists): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderTemplateExists, "An LLM provider template with this ID already exists.")) - return + return apperror.LLMProviderTemplateExists.Wrap(err) case errors.Is(err, constants.ErrLLMProviderTemplateManagedByReserved): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderTemplateManagedByReserved, "'wso2' is reserved and cannot be used as managedBy on custom templates.")) - return + return apperror.LLMProviderTemplateManagedByReserved.Wrap(err) case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid input")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid input") default: - h.slogger.Error("Failed to create LLM provider template", "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to create LLM provider template")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to create LLM provider template in org %s", orgID)) } } httputil.WriteJSON(w, http.StatusCreated, created) + return nil } // CopyLLMProviderTemplateVersion creates a new version within a family by @@ -157,26 +148,23 @@ func (h *LLMHandler) CreateLLMProviderTemplate(w http.ResponseWriter, r *http.Re // handle and the new version are given as query params // (fromTemplateId, toTemplateId, toVersion); an optional body overrides fields // on top of the copied config. -func (h *LLMHandler) CopyLLMProviderTemplateVersion(w http.ResponseWriter, r *http.Request) { +func (h *LLMHandler) CopyLLMProviderTemplateVersion(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } - createdBy, ok := resolveActor(w, r, h.identity, h.slogger, "copy LLM provider template version") - if !ok { - return + createdBy, err := resolveActorErr(r, h.identity, "copy LLM provider template version") + if err != nil { + return err } fromTemplateID := strings.TrimSpace(r.URL.Query().Get("fromTemplateId")) toTemplateID := strings.TrimSpace(r.URL.Query().Get("toTemplateId")) toVersion := strings.TrimSpace(r.URL.Query().Get("toVersion")) if fromTemplateID == "" || toVersion == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "fromTemplateId and toVersion are required")) - return + return apperror.ValidationFailed.New("fromTemplateId and toVersion are required") } // The body is optional; only decode overrides when one is present. @@ -184,9 +172,7 @@ func (h *LLMHandler) CopyLLMProviderTemplateVersion(w http.ResponseWriter, r *ht if r.Body != nil && r.ContentLength != 0 { var vreq api.CreateLLMProviderTemplateVersionRequest if err := json.NewDecoder(r.Body).Decode(&vreq); err != nil && !errors.Is(err, io.EOF) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid request body")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body") } overrides = &vreq } @@ -195,38 +181,28 @@ func (h *LLMHandler) CopyLLMProviderTemplateVersion(w http.ResponseWriter, r *ht if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderTemplateNotFound, "The source LLM provider template version could not be found.")) - return + return apperror.LLMProviderTemplateVersionNotFound.Wrap(err) case errors.Is(err, constants.ErrLLMProviderTemplateVersionExists): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderTemplateExists, "This template version already exists.")) - return + return apperror.LLMProviderTemplateVersionExists.Wrap(err) case errors.Is(err, constants.ErrLLMProviderTemplateManagedByReserved): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderTemplateManagedByReserved, "'wso2' is reserved and cannot be used as managedBy on custom templates.")) - return + return apperror.LLMProviderTemplateManagedByReserved.Wrap(err) case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid input. toVersion must match the v. pattern starting from v1.0 (e.g. v1.0), and toTemplateId must match the family")) - return + 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: - h.slogger.Error("Failed to copy LLM provider template version", "organizationId", orgID, "fromTemplateId", fromTemplateID, "toVersion", toVersion, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to copy LLM provider template version")) - return + 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)) } } httputil.WriteJSON(w, http.StatusCreated, created) + return nil } -func (h *LLMHandler) ListLLMProviderTemplates(w http.ResponseWriter, r *http.Request) { +func (h *LLMHandler) ListLLMProviderTemplates(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } limitStr := r.URL.Query().Get("limit") @@ -255,9 +231,7 @@ func (h *LLMHandler) ListLLMProviderTemplates(w http.ResponseWriter, r *http.Req if familyScoped { groupID := q.GroupID if groupID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid groupId")) - return + return apperror.ValidationFailed.New("Invalid groupId") } version := q.Version if version != "" { @@ -265,63 +239,49 @@ func (h *LLMHandler) ListLLMProviderTemplates(w http.ResponseWriter, r *http.Req if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderTemplateNotFound, "The specified LLM provider template version could not be found.")) - return + return apperror.LLMProviderTemplateVersionNotFound.Wrap(err) case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid version. Version must match the v. pattern (e.g. v1.0)")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid version. Version must match the v. pattern (e.g. v1.0)") default: - h.slogger.Error("Failed to get LLM provider template version", "organizationId", orgID, "groupId", groupID, "version", version, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to get LLM provider template version")) - return + 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)) } } httputil.WriteJSON(w, http.StatusOK, resp) - return + return nil } resp, err := h.templateService.ListVersions(orgID, groupID, limit, offset) if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderTemplateNotFound, "The specified LLM provider template could not be found.")) - return + return apperror.LLMProviderTemplateNotFound.Wrap(err) case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid groupId")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid groupId") default: - h.slogger.Error("Failed to list LLM provider template versions", "organizationId", orgID, "groupId", groupID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to list LLM provider template versions")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to list LLM provider template versions of group %s in org %s", groupID, orgID)) } } httputil.WriteJSON(w, http.StatusOK, resp) - return + return nil } latestOnly := q.Latest resp, err := h.templateService.List(orgID, limit, offset, latestOnly) if err != nil { - h.slogger.Error("Failed to list LLM provider templates", "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to list LLM provider templates")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to list LLM provider templates in org %s", orgID)) } httputil.WriteJSON(w, http.StatusOK, resp) + return nil } -func (h *LLMHandler) GetLLMProviderTemplate(w http.ResponseWriter, r *http.Request) { +func (h *LLMHandler) GetLLMProviderTemplate(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } id := r.PathValue("llmProviderTemplateId") @@ -329,29 +289,23 @@ func (h *LLMHandler) GetLLMProviderTemplate(w http.ResponseWriter, r *http.Reque if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderTemplateNotFound, "The specified LLM provider template could not be found.")) - return + return apperror.LLMProviderTemplateNotFound.Wrap(err) case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid template id")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid template id") default: - h.slogger.Error("Failed to get LLM provider template", "organizationId", orgID, "templateId", id, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to get LLM provider template")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to get LLM provider template %s in org %s", id, orgID)) } } httputil.WriteJSON(w, http.StatusOK, resp) + return nil } -func (h *LLMHandler) SetLLMProviderTemplateVersionEnabled(w http.ResponseWriter, r *http.Request) { +func (h *LLMHandler) SetLLMProviderTemplateVersionEnabled(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } id := r.PathValue("llmProviderTemplateId") @@ -359,195 +313,145 @@ func (h *LLMHandler) SetLLMProviderTemplateVersionEnabled(w http.ResponseWriter, Enabled *bool `json:"enabled"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Enabled == nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Request body must include a boolean 'enabled' field")) - return + return apperror.ValidationFailed.New("Request body must include a boolean 'enabled' field") } resp, err := h.templateService.SetEnabledByHandle(orgID, id, *body.Enabled) if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderTemplateNotFound, "The specified LLM provider template version could not be found.")) - return + return apperror.LLMProviderTemplateVersionNotFound.Wrap(err) case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid template id")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid template id") case errors.Is(err, constants.ErrLLMProviderTemplateInUse): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderTemplateInUse, "Cannot disable this template version while providers are using it.")) - return + return apperror.LLMProviderTemplateInUse.Wrap(err) case errors.Is(err, constants.ErrLLMProviderTemplateNotToggleable): - httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderTemplateNotToggleable, "Only built-in templates can be enabled or disabled.")) - return + return apperror.LLMProviderTemplateNotToggleable.Wrap(err) default: - h.slogger.Error("Failed to set LLM provider template version enabled", "organizationId", orgID, "templateId", id, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to update template version")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(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 } -func (h *LLMHandler) UpdateLLMProviderTemplate(w http.ResponseWriter, r *http.Request) { +func (h *LLMHandler) UpdateLLMProviderTemplate(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } id := r.PathValue("llmProviderTemplateId") var req api.LLMProviderTemplate if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid request body")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body") } - updatedBy, ok := resolveActor(w, r, h.identity, h.slogger, "update LLM provider template") - if !ok { - return + updatedBy, err := resolveActorErr(r, h.identity, "update LLM provider template") + if err != nil { + return err } resp, err := h.templateService.Update(orgID, id, updatedBy, &req) if err != nil { - if respondArtifactGuardError(w, err) { - return + if guardErr := mapArtifactGuardError(err); guardErr != nil { + return guardErr } switch { case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderTemplateNotFound, "The specified LLM provider template could not be found.")) - return + return apperror.LLMProviderTemplateNotFound.Wrap(err) case errors.Is(err, constants.ErrLLMProviderTemplateReadOnly): - httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderTemplateReadOnly, "Built-in templates are read-only and cannot be edited.")) - return + return apperror.LLMProviderTemplateReadOnly.Wrap(err) case errors.Is(err, constants.ErrLLMProviderTemplateManagedByReserved): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderTemplateManagedByReserved, "'wso2' is reserved and cannot be used as managedBy on custom templates.")) - return + return apperror.LLMProviderTemplateManagedByReserved.Wrap(err) case errors.Is(err, constants.ErrHandleImmutable): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, err.Error())) - return + return apperror.ValidationFailed.Wrap(err, "The id is immutable and cannot be changed") case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid input")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid input") default: - h.slogger.Error("Failed to update LLM provider template", "organizationId", orgID, "templateId", id, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to update LLM provider template")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to update LLM provider template %s in org %s", id, orgID)) } } httputil.WriteJSON(w, http.StatusOK, resp) + return nil } // DeleteLLMProviderTemplateVersion removes a single version of a template. -func (h *LLMHandler) DeleteLLMProviderTemplateVersion(w http.ResponseWriter, r *http.Request) { +func (h *LLMHandler) DeleteLLMProviderTemplateVersion(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } id := r.PathValue("llmProviderTemplateId") if err := h.templateService.DeleteByHandle(orgID, id); err != nil { switch { case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderTemplateNotFound, "The specified LLM provider template version could not be found.")) - return + return apperror.LLMProviderTemplateVersionNotFound.Wrap(err) case errors.Is(err, constants.ErrLLMProviderTemplateInUse): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderTemplateInUse, "This template version cannot be deleted while providers are using it.")) - return + return apperror.LLMProviderTemplateInUse.Wrap(err) case errors.Is(err, constants.ErrLLMProviderTemplateReadOnly): - httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderTemplateReadOnly, "Built-in template versions are read-only and cannot be deleted.")) - return + return apperror.LLMProviderTemplateReadOnly.Wrap(err) case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid template id")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid template id") default: - h.slogger.Error("Failed to delete LLM provider template version", "organizationId", orgID, "templateId", id, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to delete template version")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to delete LLM provider template version %s in org %s", id, orgID)) } } w.WriteHeader(http.StatusNoContent) + return nil } // ---- Providers ---- -func (h *LLMHandler) CreateLLMProvider(w http.ResponseWriter, r *http.Request) { +func (h *LLMHandler) CreateLLMProvider(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } var req api.LLMProvider if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid request body")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body") } - createdBy, ok := resolveActor(w, r, h.identity, h.slogger, "create LLM provider") - if !ok { - return + createdBy, err := resolveActorErr(r, h.identity, "create LLM provider") + if err != nil { + return err } created, err := h.providerService.Create(orgID, createdBy, &req) if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderLimitReached): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderLimitReached, "LLM provider limit reached for the organization.")) - return + return apperror.LLMProviderLimitReached.Wrap(err) case errors.Is(err, constants.ErrLLMProviderExists): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderExists, "An LLM provider with this ID already exists.")) - return + return apperror.LLMProviderExists.Wrap(err) case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderTemplateNotFound, "The referenced LLM provider template could not be found.")) - return + return apperror.LLMProviderTemplateRefNotFound.Wrap(err) case errors.Is(err, constants.ErrSecretRefMissing): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, err.Error())) - return + return apperror.ValidationFailed.Wrap(err, err.Error()) case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid input")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid input") default: - h.slogger.Error("Failed to create LLM provider", "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to create LLM provider")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to create LLM provider in org %s", orgID)) } } httputil.WriteJSON(w, http.StatusCreated, created) + return nil } -func (h *LLMHandler) ListLLMProviders(w http.ResponseWriter, r *http.Request) { +func (h *LLMHandler) ListLLMProviders(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } limitStr := r.URL.Query().Get("limit") @@ -574,20 +478,18 @@ func (h *LLMHandler) ListLLMProviders(w http.ResponseWriter, r *http.Request) { resp, err := h.providerService.List(orgID, limit, offset) if err != nil { - h.slogger.Error("Failed to list LLM providers", "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to list LLM providers")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to list LLM providers in org %s", orgID)) } httputil.WriteJSON(w, http.StatusOK, resp) + return nil } -func (h *LLMHandler) GetLLMProvider(w http.ResponseWriter, r *http.Request) { +func (h *LLMHandler) GetLLMProvider(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } id := r.PathValue("llmProviderId") @@ -595,186 +497,142 @@ func (h *LLMHandler) GetLLMProvider(w http.ResponseWriter, r *http.Request) { if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) - return + return apperror.LLMProviderNotFound.Wrap(err) case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid provider id")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid provider id") default: - h.slogger.Error("Failed to get LLM provider", "organizationId", orgID, "providerId", id, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to get LLM provider")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to get LLM provider %s in org %s", id, orgID)) } } httputil.WriteJSON(w, http.StatusOK, resp) + return nil } -func (h *LLMHandler) UpdateLLMProvider(w http.ResponseWriter, r *http.Request) { +func (h *LLMHandler) UpdateLLMProvider(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } id := r.PathValue("llmProviderId") var req api.LLMProvider if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid request body")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body") } - updatedBy, ok := resolveActor(w, r, h.identity, h.slogger, "update LLM provider") - if !ok { - return + updatedBy, err := resolveActorErr(r, h.identity, "update LLM provider") + if err != nil { + return err } resp, err := h.providerService.Update(orgID, id, updatedBy, &req) if err != nil { - if respondArtifactGuardError(w, err) { - return + if guardErr := mapArtifactGuardError(err); guardErr != nil { + return guardErr } switch { case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) - return + return apperror.LLMProviderNotFound.Wrap(err) case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderTemplateNotFound, "The referenced LLM provider template could not be found.")) - return + return apperror.LLMProviderTemplateRefNotFound.Wrap(err) case errors.Is(err, constants.ErrSecretRefMissing): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, err.Error())) - return + return apperror.ValidationFailed.Wrap(err, err.Error()) case errors.Is(err, constants.ErrHandleImmutable): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, err.Error())) - return + return apperror.ValidationFailed.Wrap(err, "The id is immutable and cannot be changed") case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid input")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid input") default: - h.slogger.Error("Failed to update LLM provider", "organizationId", orgID, "providerId", id, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to update LLM provider")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to update LLM provider %s in org %s", id, orgID)) } } httputil.WriteJSON(w, http.StatusOK, resp) + return nil } -func (h *LLMHandler) DeleteLLMProvider(w http.ResponseWriter, r *http.Request) { +func (h *LLMHandler) DeleteLLMProvider(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } id := r.PathValue("llmProviderId") - deletedBy, ok := resolveActor(w, r, h.identity, h.slogger, "delete LLM provider") - if !ok { - return + deletedBy, err := resolveActorErr(r, h.identity, "delete LLM provider") + if err != nil { + return err } if err := h.providerService.Delete(orgID, id, deletedBy); err != nil { - if respondArtifactGuardError(w, err) { - return + if guardErr := mapArtifactGuardError(err); guardErr != nil { + return guardErr } switch { case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) - return + return apperror.LLMProviderNotFound.Wrap(err) case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid provider id")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid provider id") default: - h.slogger.Error("Failed to delete LLM provider", "organizationId", orgID, "providerId", id, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to delete LLM provider")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to delete LLM provider %s in org %s", id, orgID)) } } w.WriteHeader(http.StatusNoContent) + return nil } // ---- Proxies ---- -func (h *LLMHandler) CreateLLMProxy(w http.ResponseWriter, r *http.Request) { +func (h *LLMHandler) CreateLLMProxy(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } var req api.LLMProxy if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid request body")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body") } if req.ProjectId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Project ID is required")) - return + return apperror.ValidationFailed.New("Project ID is required") } - createdBy, ok := resolveActor(w, r, h.identity, h.slogger, "create LLM proxy") - if !ok { - return + createdBy, err := resolveActorErr(r, h.identity, "create LLM proxy") + if err != nil { + return err } created, err := h.proxyService.Create(orgID, createdBy, &req) if err != nil { switch { case errors.Is(err, constants.ErrLLMProxyLimitReached): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyLimitReached, "LLM proxy limit reached for the organization.")) - return + return apperror.LLMProxyLimitReached.Wrap(err) case errors.Is(err, constants.ErrLLMProxyExists): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyExists, "An LLM proxy with this ID already exists.")) - return + return apperror.LLMProxyExists.Wrap(err) case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderNotFound, "The referenced LLM provider could not be found.")) - return + return apperror.LLMProviderRefNotFound.Wrap(err) case errors.Is(err, constants.ErrProjectNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeProjectNotFound, "The specified project could not be found.")) - return + return apperror.ProjectNotFound.Wrap(err) case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid input")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid input") default: - h.slogger.Error("Failed to create LLM proxy", "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to create LLM proxy")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to create LLM proxy in org %s", orgID)) } } httputil.WriteJSON(w, http.StatusCreated, created) + return nil } -func (h *LLMHandler) ListLLMProxies(w http.ResponseWriter, r *http.Request) { +func (h *LLMHandler) ListLLMProxies(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } projectID := strings.TrimSpace(r.URL.Query().Get("projectId")) if projectID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "projectId query parameter is required")) - return + return apperror.ValidationFailed.New("projectId query parameter is required") } limitStr := r.URL.Query().Get("limit") @@ -802,24 +660,20 @@ func (h *LLMHandler) ListLLMProxies(w http.ResponseWriter, r *http.Request) { resp, err := h.proxyService.List(orgID, &projectID, limit, offset) if err != nil { if errors.Is(err, constants.ErrProjectNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeProjectNotFound, "The specified project could not be found.")) - return + return apperror.ProjectNotFound.Wrap(err) } - h.slogger.Error("Failed to list LLM proxies", "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to list LLM proxies")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to list LLM proxies in org %s", orgID)) } httputil.WriteJSON(w, http.StatusOK, resp) + return nil } -func (h *LLMHandler) ListLLMProxiesByProvider(w http.ResponseWriter, r *http.Request) { +func (h *LLMHandler) ListLLMProxiesByProvider(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } providerID := r.PathValue("llmProviderId") @@ -849,29 +703,23 @@ func (h *LLMHandler) ListLLMProxiesByProvider(w http.ResponseWriter, r *http.Req if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) - return + return apperror.LLMProviderNotFound.Wrap(err) case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid provider id")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid provider id") default: - h.slogger.Error("Failed to list LLM proxies by provider", "organizationId", orgID, "providerId", providerID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to list LLM proxies")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to list LLM proxies by provider %s in org %s", providerID, orgID)) } } httputil.WriteJSON(w, http.StatusOK, resp) + return nil } -func (h *LLMHandler) GetLLMProxy(w http.ResponseWriter, r *http.Request) { +func (h *LLMHandler) GetLLMProxy(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } id := r.PathValue("llmProxyId") @@ -879,107 +727,84 @@ func (h *LLMHandler) GetLLMProxy(w http.ResponseWriter, r *http.Request) { if err != nil { switch { case errors.Is(err, constants.ErrLLMProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) - return + return apperror.LLMProxyNotFound.Wrap(err) case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid proxy id")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid proxy id") default: - h.slogger.Error("Failed to get LLM proxy", "organizationId", orgID, "proxyId", id, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to get LLM proxy")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to get LLM proxy %s in org %s", id, orgID)) } } httputil.WriteJSON(w, http.StatusOK, resp) + return nil } -func (h *LLMHandler) UpdateLLMProxy(w http.ResponseWriter, r *http.Request) { +func (h *LLMHandler) UpdateLLMProxy(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } id := r.PathValue("llmProxyId") var req api.LLMProxy if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid request body")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body") } - updatedBy, ok := resolveActor(w, r, h.identity, h.slogger, "update LLM proxy") - if !ok { - return + updatedBy, err := resolveActorErr(r, h.identity, "update LLM proxy") + if err != nil { + return err } resp, err := h.proxyService.Update(orgID, id, updatedBy, &req) if err != nil { - if respondArtifactGuardError(w, err) { - return + if guardErr := mapArtifactGuardError(err); guardErr != nil { + return guardErr } switch { case errors.Is(err, constants.ErrLLMProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) - return + return apperror.LLMProxyNotFound.Wrap(err) case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderNotFound, "The referenced LLM provider could not be found.")) - return + return apperror.LLMProviderRefNotFound.Wrap(err) case errors.Is(err, constants.ErrHandleImmutable): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, err.Error())) - return + return apperror.ValidationFailed.Wrap(err, "The id is immutable and cannot be changed") case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid input")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid input") default: - h.slogger.Error("Failed to update LLM proxy", "organizationId", orgID, "proxyId", id, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to update LLM proxy")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to update LLM proxy %s in org %s", id, orgID)) } } httputil.WriteJSON(w, http.StatusOK, resp) + return nil } -func (h *LLMHandler) DeleteLLMProxy(w http.ResponseWriter, r *http.Request) { +func (h *LLMHandler) DeleteLLMProxy(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } id := r.PathValue("llmProxyId") - deletedBy, ok := resolveActor(w, r, h.identity, h.slogger, "delete LLM proxy") - if !ok { - return + deletedBy, err := resolveActorErr(r, h.identity, "delete LLM proxy") + if err != nil { + return err } if err := h.proxyService.Delete(orgID, id, deletedBy); err != nil { - if respondArtifactGuardError(w, err) { - return + if guardErr := mapArtifactGuardError(err); guardErr != nil { + return guardErr } switch { case errors.Is(err, constants.ErrLLMProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) - return + return apperror.LLMProxyNotFound.Wrap(err) case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid proxy id")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid proxy id") default: - h.slogger.Error("Failed to delete LLM proxy", "organizationId", orgID, "proxyId", id, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to delete LLM proxy")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(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 e1b26dff6c..72e418c4fc 100644 --- a/platform-api/internal/handler/llm_apikey.go +++ b/platform-api/internal/handler/llm_apikey.go @@ -20,15 +20,16 @@ package handler import ( "encoding/json" "errors" + "fmt" "log/slog" "net/http" "strings" "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/service" - "github.com/wso2/api-platform/platform-api/internal/utils" "github.com/wso2/go-httpkit/httputil" ) @@ -50,161 +51,130 @@ func NewLLMProviderAPIKeyHandler(apiKeyService *service.LLMProviderAPIKeyService } // ListAPIKeys handles GET /api/v0.9/llm-providers/{llmProviderId}/api-keys -func (h *LLMProviderAPIKeyHandler) ListAPIKeys(w http.ResponseWriter, r *http.Request) { +func (h *LLMProviderAPIKeyHandler) ListAPIKeys(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } providerID := r.PathValue("llmProviderId") if providerID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "LLM provider ID is required")) - return + return apperror.ValidationFailed.New("LLM provider ID is required") } - callerUserID, ok := resolveActor(w, r, h.identity, h.slogger, "list LLM provider API keys") - if !ok { - return + callerUserID, err := resolveActorErr(r, h.identity, "list LLM provider API keys") + if err != nil { + return err } response, err := h.apiKeyService.ListLLMProviderAPIKeys(r.Context(), providerID, orgID, callerUserID) if err != nil { if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeArtifactNotFound, "The specified artifact could not be found.")) - return + return apperror.ArtifactNotFound.Wrap(err) } - h.slogger.Error("Failed to list LLM provider API keys", "providerId", providerID, "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to list API keys")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to list LLM provider API keys for provider %s in org %s", providerID, orgID)) } httputil.WriteJSON(w, http.StatusOK, response) + return nil } // DeleteAPIKey handles DELETE /api/v0.9/llm-providers/{llmProviderId}/api-keys/{apiKeyId} -func (h *LLMProviderAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.Request) { +func (h *LLMProviderAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } providerID := r.PathValue("llmProviderId") if providerID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "LLM provider ID is required")) - return + return apperror.ValidationFailed.New("LLM provider ID is required") } keyName := r.PathValue("apiKeyId") if keyName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API key name is required")) - return + return apperror.ValidationFailed.New("API key name is required") } - callerUserID, ok := resolveActor(w, r, h.identity, h.slogger, "delete LLM provider API key") - if !ok { - return + callerUserID, err := resolveActorErr(r, h.identity, "delete LLM provider API key") + if err != nil { + return err } - err := h.apiKeyService.DeleteLLMProviderAPIKey(r.Context(), providerID, orgID, callerUserID, keyName) - if err != nil { + if err := h.apiKeyService.DeleteLLMProviderAPIKey(r.Context(), providerID, orgID, callerUserID, keyName); err != nil { if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeArtifactNotFound, "The specified artifact could not be found.")) - return + return apperror.ArtifactNotFound.Wrap(err) } if errors.Is(err, constants.ErrAPIKeyNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderAPIKeyNotFound, "The specified API key could not be found.")) - return + return apperror.LLMProviderAPIKeyNotFound.Wrap(err) } if errors.Is(err, constants.ErrAPIKeyForbidden) { - httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderAPIKeyForbidden, "Only the key creator can delete this API key")) - return + return apperror.LLMProviderAPIKeyForbidden.Wrap(err) } - h.slogger.Error("Failed to delete LLM provider API key", "providerId", providerID, "keyName", keyName, "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to delete API key")) - return + 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)) } h.slogger.Info("Successfully deleted LLM provider API key", "providerId", providerID, "keyName", keyName, "organizationId", orgID) w.WriteHeader(http.StatusNoContent) + return nil } // CreateAPIKey handles POST /api/v0.9/llm-providers/{llmProviderId}/api-keys -func (h *LLMProviderAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Request) { +func (h *LLMProviderAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } providerID := r.PathValue("llmProviderId") if providerID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "LLM provider ID is required")) - return + return apperror.ValidationFailed.New("LLM provider ID is required") } var req api.CreateLLMProviderAPIKeyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - h.slogger.Error("Invalid LLM provider API key creation request", "providerId", providerID, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid request body")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body"). + WithLogMessage(fmt.Sprintf("invalid LLM provider API key creation request for provider %s", providerID)) } // Validate that displayName is provided (name is optional; auto-generated from displayName if absent) req.DisplayName = strings.TrimSpace(req.DisplayName) if req.DisplayName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "'displayName' is required")) - return + return apperror.ValidationFailed.New("'displayName' is required") } - userID, ok := resolveActor(w, r, h.identity, h.slogger, "create LLM provider API key") - if !ok { - return + userID, err := resolveActorErr(r, h.identity, "create LLM provider API key") + if err != nil { + return err } response, err := h.apiKeyService.CreateLLMProviderAPIKey(r.Context(), providerID, orgID, userID, &req) if err != nil { if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeArtifactNotFound, "The specified artifact could not be found.")) - return + return apperror.ArtifactNotFound.Wrap(err) } if errors.Is(err, constants.ErrGatewayUnavailable) { - httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponseWithCode( - utils.CodeGatewayConnectionUnavailable, "No gateway connections are currently available.")) - return + return apperror.GatewayConnectionUnavailable.Wrap(err) } - h.slogger.Error("Failed to create LLM provider API key", "providerId", providerID, "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to create API key")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(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) httputil.WriteJSON(w, http.StatusCreated, response) + return nil } // RegisterRoutes registers LLM provider API key routes with the router func (h *LLMProviderAPIKeyHandler) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("POST "+constants.APIBasePath+"/llm-providers/{llmProviderId}/api-keys", h.CreateAPIKey) - mux.HandleFunc("GET "+constants.APIBasePath+"/llm-providers/{llmProviderId}/api-keys", h.ListAPIKeys) - mux.HandleFunc("DELETE "+constants.APIBasePath+"/llm-providers/{llmProviderId}/api-keys/{apiKeyId}", h.DeleteAPIKey) + mux.HandleFunc("POST "+constants.APIBasePath+"/llm-providers/{llmProviderId}/api-keys", middleware.MapErrors(h.slogger, h.CreateAPIKey)) + mux.HandleFunc("GET "+constants.APIBasePath+"/llm-providers/{llmProviderId}/api-keys", middleware.MapErrors(h.slogger, h.ListAPIKeys)) + mux.HandleFunc("DELETE "+constants.APIBasePath+"/llm-providers/{llmProviderId}/api-keys/{apiKeyId}", middleware.MapErrors(h.slogger, h.DeleteAPIKey)) } diff --git a/platform-api/internal/handler/llm_deployment.go b/platform-api/internal/handler/llm_deployment.go index cd83cf06bd..44af607d09 100644 --- a/platform-api/internal/handler/llm_deployment.go +++ b/platform-api/internal/handler/llm_deployment.go @@ -20,15 +20,16 @@ package handler import ( "encoding/json" "errors" + "fmt" "log/slog" "net/http" "strings" "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/service" - "github.com/wso2/api-platform/platform-api/internal/utils" "github.com/wso2/go-httpkit/httputil" ) @@ -56,100 +57,72 @@ func NewLLMProxyDeploymentHandler(deploymentService *service.LLMProxyDeploymentS } // DeployLLMProvider handles POST /api/v0.9/llm-providers/{llmProviderId}/deployments -func (h *LLMProviderDeploymentHandler) DeployLLMProvider(w http.ResponseWriter, r *http.Request) { +func (h *LLMProviderDeploymentHandler) DeployLLMProvider(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } providerId := r.PathValue("llmProviderId") if providerId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "LLM provider ID is required")) - return + return apperror.ValidationFailed.New("LLM provider ID is required") } var req api.DeployRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, err.Error())) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body"). + WithLogMessage(fmt.Sprintf("invalid LLM provider deployment request body for provider %s", providerId)) } if req.Name == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderDeploymentValidationFailed, "name is required")) - return + return apperror.LLMProviderDeploymentValidationFailed.New("name is required") } if req.Base == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderDeploymentValidationFailed, "base is required (use 'current' or a deploymentId)")) - return + return apperror.LLMProviderDeploymentValidationFailed.New("base is required (use 'current' or a deploymentId)") } if strings.TrimSpace(req.GatewayId) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderDeploymentValidationFailed, "gatewayId is required")) - return + return apperror.LLMProviderDeploymentValidationFailed.New("gatewayId is required") } deployment, err := h.deploymentService.DeployLLMProvider(providerId, &req, orgId) if err != nil { - if respondArtifactGuardError(w, err) { - return + if guardErr := mapArtifactGuardError(err); guardErr != nil { + return guardErr } switch { case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) - return + return apperror.LLMProviderNotFound.Wrap(err) case errors.Is(err, constants.ErrGatewayNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayNotFound, "The specified gateway could not be found.")) - return + return apperror.GatewayNotFound.Wrap(err) case errors.Is(err, constants.ErrBaseDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeDeploymentBaseNotFound, "The specified base deployment could not be found.")) - return + return apperror.DeploymentBaseNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentNameRequired): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderDeploymentValidationFailed, "Deployment name is required")) - return + return apperror.LLMProviderDeploymentValidationFailed.Wrap(err, "Deployment name is required") case errors.Is(err, constants.ErrDeploymentBaseRequired): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderDeploymentValidationFailed, "Base is required (use 'current' or a deploymentId)")) - return + return apperror.LLMProviderDeploymentValidationFailed.Wrap(err, "Base is required (use 'current' or a deploymentId)") case errors.Is(err, constants.ErrDeploymentGatewayIDRequired): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderDeploymentValidationFailed, "Gateway ID is required")) - return + return apperror.LLMProviderDeploymentValidationFailed.Wrap(err, "Gateway ID is required") case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderDeploymentValidationFailed, "Referenced template not found")) - return + return apperror.LLMProviderDeploymentValidationFailed.Wrap(err, "Referenced template not found") case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderDeploymentValidationFailed, "Invalid input")) - return + return apperror.LLMProviderDeploymentValidationFailed.Wrap(err, "Invalid input") default: - h.slogger.Error("Failed to deploy LLM provider", "providerId", providerId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to deploy LLM provider")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to deploy LLM provider %s", providerId)) } } httputil.WriteJSON(w, http.StatusCreated, deployment) + return nil } // UndeployLLMProviderDeployment handles POST /api/v0.9/llm-providers/{llmProviderId}/deployments/{deploymentId}/undeploy -func (h *LLMProviderDeploymentHandler) UndeployLLMProviderDeployment(w http.ResponseWriter, r *http.Request) { +func (h *LLMProviderDeploymentHandler) UndeployLLMProviderDeployment(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } providerId := r.PathValue("llmProviderId") @@ -157,55 +130,41 @@ func (h *LLMProviderDeploymentHandler) UndeployLLMProviderDeployment(w http.Resp gatewayId := r.URL.Query().Get("gatewayId") if providerId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "LLM provider ID is required")) - return + return apperror.ValidationFailed.New("LLM provider ID is required") } 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 respondArtifactGuardError(w, err) { - return + if guardErr := mapArtifactGuardError(err); guardErr != nil { + return guardErr } switch { case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) - return + return apperror.LLMProviderNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) - return + return apperror.DeploymentNotFound.Wrap(err) case errors.Is(err, constants.ErrGatewayNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayNotFound, "The specified gateway could not be found.")) - return + return apperror.GatewayNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentNotActive): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeDeploymentNotActive, "No active deployment found for this LLM provider on the gateway.")) - return + return apperror.DeploymentNotActive.Wrap(err, "LLM provider") case errors.Is(err, constants.ErrGatewayIDMismatch): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeDeploymentGatewayMismatch, "Deployment is bound to a different gateway.")) - return + return apperror.DeploymentGatewayMismatch.Wrap(err) default: - h.slogger.Error("Failed to undeploy LLM provider", "providerId", providerId, "deploymentId", deploymentId, "gatewayId", gatewayId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to undeploy deployment")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to undeploy LLM provider %s deployment %s on gateway %q", providerId, deploymentId, gatewayId)) } } httputil.WriteJSON(w, http.StatusOK, deployment) + return nil } // RestoreLLMProviderDeployment handles POST /api/v0.9/llm-providers/{llmProviderId}/deployments/{deploymentId}/restore -func (h *LLMProviderDeploymentHandler) RestoreLLMProviderDeployment(w http.ResponseWriter, r *http.Request) { +func (h *LLMProviderDeploymentHandler) RestoreLLMProviderDeployment(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } providerId := r.PathValue("llmProviderId") @@ -213,156 +172,118 @@ func (h *LLMProviderDeploymentHandler) RestoreLLMProviderDeployment(w http.Respo gatewayId := r.URL.Query().Get("gatewayId") if providerId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "LLM provider ID is required")) - return + return apperror.ValidationFailed.New("LLM provider ID is required") } 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 respondArtifactGuardError(w, err) { - return + if guardErr := mapArtifactGuardError(err); guardErr != nil { + return guardErr } switch { case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) - return + return apperror.LLMProviderNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) - return + return apperror.DeploymentNotFound.Wrap(err) case errors.Is(err, constants.ErrGatewayNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayNotFound, "The specified gateway could not be found.")) - return + return apperror.GatewayNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentAlreadyDeployed): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeDeploymentRestoreConflict, "Cannot restore the currently deployed deployment, or the deployment is invalid.")) - return + return apperror.DeploymentRestoreConflict.Wrap(err) case errors.Is(err, constants.ErrGatewayIDMismatch): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeDeploymentGatewayMismatch, "Deployment is bound to a different gateway.")) - return + return apperror.DeploymentGatewayMismatch.Wrap(err) default: - h.slogger.Error("Failed to restore LLM provider deployment", "providerId", providerId, "deploymentId", deploymentId, "gatewayId", gatewayId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to restore deployment")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to restore LLM provider %s deployment %s on gateway %q", providerId, deploymentId, gatewayId)) } } httputil.WriteJSON(w, http.StatusOK, deployment) + return nil } // DeleteLLMProviderDeployment handles DELETE /api/v0.9/llm-providers/{llmProviderId}/deployments/{deploymentId} -func (h *LLMProviderDeploymentHandler) DeleteLLMProviderDeployment(w http.ResponseWriter, r *http.Request) { +func (h *LLMProviderDeploymentHandler) DeleteLLMProviderDeployment(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } providerId := r.PathValue("llmProviderId") deploymentId := r.PathValue("deploymentId") if providerId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "LLM provider ID is required")) - return + return apperror.ValidationFailed.New("LLM provider ID is required") } if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Deployment ID is required")) - return + return apperror.ValidationFailed.New("Deployment ID is required") } err := h.deploymentService.DeleteLLMProviderDeployment(providerId, deploymentId, orgId) if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) - return + return apperror.LLMProviderNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) - return + return apperror.DeploymentNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentIsDeployed): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeDeploymentActive, "Cannot delete an active deployment - undeploy it first.")) - return + return apperror.DeploymentActive.Wrap(err) default: - h.slogger.Error("Failed to delete LLM provider deployment", "providerId", providerId, "deploymentId", deploymentId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to delete deployment")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to delete LLM provider %s deployment %s", providerId, deploymentId)) } } w.WriteHeader(http.StatusNoContent) + return nil } // GetLLMProviderDeployment handles GET /api/v0.9/llm-providers/{llmProviderId}/deployments/{deploymentId} -func (h *LLMProviderDeploymentHandler) GetLLMProviderDeployment(w http.ResponseWriter, r *http.Request) { +func (h *LLMProviderDeploymentHandler) GetLLMProviderDeployment(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } providerId := r.PathValue("llmProviderId") deploymentId := r.PathValue("deploymentId") if providerId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "LLM provider ID is required")) - return + return apperror.ValidationFailed.New("LLM provider ID is required") } if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Deployment ID is required")) - return + return apperror.ValidationFailed.New("Deployment ID is required") } deployment, err := h.deploymentService.GetLLMProviderDeployment(providerId, deploymentId, orgId) if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) - return + return apperror.LLMProviderNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) - return + return apperror.DeploymentNotFound.Wrap(err) default: - h.slogger.Error("Failed to get LLM provider deployment", "providerId", providerId, "deploymentId", deploymentId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to retrieve deployment")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to get LLM provider %s deployment %s", providerId, deploymentId)) } } httputil.WriteJSON(w, http.StatusOK, deployment) + return nil } // GetLLMProviderDeployments handles GET /api/v0.9/llm-providers/{llmProviderId}/deployments -func (h *LLMProviderDeploymentHandler) GetLLMProviderDeployments(w http.ResponseWriter, r *http.Request) { +func (h *LLMProviderDeploymentHandler) GetLLMProviderDeployments(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } providerId := r.PathValue("llmProviderId") if providerId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "LLM provider ID is required")) - return + return apperror.ValidationFailed.New("LLM provider ID is required") } q := r.URL.Query() @@ -378,126 +299,95 @@ func (h *LLMProviderDeploymentHandler) GetLLMProviderDeployments(w http.Response if err != nil { switch { case errors.Is(err, constants.ErrLLMProviderNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProviderNotFound, "The specified LLM provider could not be found.")) - return + return apperror.LLMProviderNotFound.Wrap(err) case errors.Is(err, constants.ErrInvalidDeploymentStatus): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeDeploymentInvalidStatus, "The specified deployment status filter is invalid.")) - return + return apperror.DeploymentInvalidStatus.Wrap(err) default: - h.slogger.Error("Failed to get LLM provider deployments", "providerId", providerId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to retrieve deployments")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to get LLM provider %s deployments", providerId)) } } httputil.WriteJSON(w, http.StatusOK, deployments) + return nil } // RegisterRoutes registers all LLM provider deployment-related routes func (h *LLMProviderDeploymentHandler) RegisterRoutes(mux *http.ServeMux) { base := constants.APIBasePath + "/llm-providers/{llmProviderId}" - mux.HandleFunc("POST "+base+"/deployments", h.DeployLLMProvider) - mux.HandleFunc("POST "+base+"/deployments/{deploymentId}/undeploy", h.UndeployLLMProviderDeployment) - mux.HandleFunc("POST "+base+"/deployments/{deploymentId}/restore", h.RestoreLLMProviderDeployment) - mux.HandleFunc("GET "+base+"/deployments", h.GetLLMProviderDeployments) - mux.HandleFunc("GET "+base+"/deployments/{deploymentId}", h.GetLLMProviderDeployment) - mux.HandleFunc("DELETE "+base+"/deployments/{deploymentId}", h.DeleteLLMProviderDeployment) + mux.HandleFunc("POST "+base+"/deployments", middleware.MapErrors(h.slogger, h.DeployLLMProvider)) + mux.HandleFunc("POST "+base+"/deployments/{deploymentId}/undeploy", middleware.MapErrors(h.slogger, h.UndeployLLMProviderDeployment)) + mux.HandleFunc("POST "+base+"/deployments/{deploymentId}/restore", middleware.MapErrors(h.slogger, h.RestoreLLMProviderDeployment)) + mux.HandleFunc("GET "+base+"/deployments", middleware.MapErrors(h.slogger, h.GetLLMProviderDeployments)) + mux.HandleFunc("GET "+base+"/deployments/{deploymentId}", middleware.MapErrors(h.slogger, h.GetLLMProviderDeployment)) + mux.HandleFunc("DELETE "+base+"/deployments/{deploymentId}", middleware.MapErrors(h.slogger, h.DeleteLLMProviderDeployment)) } // DeployLLMProxy handles POST /api/v0.9/llm-proxies/{llmProxyId}/deployments -func (h *LLMProxyDeploymentHandler) DeployLLMProxy(w http.ResponseWriter, r *http.Request) { +func (h *LLMProxyDeploymentHandler) DeployLLMProxy(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } proxyId := r.PathValue("llmProxyId") if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "LLM proxy ID is required")) - return + return apperror.ValidationFailed.New("LLM proxy ID is required") } var req api.DeployRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, err.Error())) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body"). + WithLogMessage(fmt.Sprintf("invalid LLM proxy deployment request body for proxy %s", proxyId)) } if req.Name == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyDeploymentValidationFailed, "name is required")) - return + return apperror.LLMProxyDeploymentValidationFailed.New("name is required") } if req.Base == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyDeploymentValidationFailed, "base is required (use 'current' or a deploymentId)")) - return + return apperror.LLMProxyDeploymentValidationFailed.New("base is required (use 'current' or a deploymentId)") } if strings.TrimSpace(req.GatewayId) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyDeploymentValidationFailed, "gatewayId is required")) - return + return apperror.LLMProxyDeploymentValidationFailed.New("gatewayId is required") } deployment, err := h.deploymentService.DeployLLMProxy(proxyId, &req, orgId) if err != nil { - if respondArtifactGuardError(w, err) { - return + if guardErr := mapArtifactGuardError(err); guardErr != nil { + return guardErr } switch { case errors.Is(err, constants.ErrLLMProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) - return + return apperror.LLMProxyNotFound.Wrap(err) case errors.Is(err, constants.ErrGatewayNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayNotFound, "The specified gateway could not be found.")) - return + return apperror.GatewayNotFound.Wrap(err) case errors.Is(err, constants.ErrBaseDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeDeploymentBaseNotFound, "The specified base deployment could not be found.")) - return + return apperror.DeploymentBaseNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentNameRequired): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyDeploymentValidationFailed, "Deployment name is required")) - return + return apperror.LLMProxyDeploymentValidationFailed.Wrap(err, "Deployment name is required") case errors.Is(err, constants.ErrDeploymentBaseRequired): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyDeploymentValidationFailed, "Base is required (use 'current' or a deploymentId)")) - return + return apperror.LLMProxyDeploymentValidationFailed.Wrap(err, "Base is required (use 'current' or a deploymentId)") case errors.Is(err, constants.ErrDeploymentGatewayIDRequired): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyDeploymentValidationFailed, "Gateway ID is required")) - return + return apperror.LLMProxyDeploymentValidationFailed.Wrap(err, "Gateway ID is required") case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyDeploymentValidationFailed, "Invalid input")) - return + return apperror.LLMProxyDeploymentValidationFailed.Wrap(err, "Invalid input") default: - h.slogger.Error("Failed to deploy LLM proxy", "proxyId", proxyId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to deploy LLM proxy")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to deploy LLM proxy %s", proxyId)) } } httputil.WriteJSON(w, http.StatusCreated, deployment) + return nil } // UndeployLLMProxyDeployment handles POST /api/v0.9/llm-proxies/{llmProxyId}/deployments/{deploymentId}/undeploy -func (h *LLMProxyDeploymentHandler) UndeployLLMProxyDeployment(w http.ResponseWriter, r *http.Request) { +func (h *LLMProxyDeploymentHandler) UndeployLLMProxyDeployment(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } proxyId := r.PathValue("llmProxyId") @@ -505,55 +395,41 @@ func (h *LLMProxyDeploymentHandler) UndeployLLMProxyDeployment(w http.ResponseWr gatewayId := r.URL.Query().Get("gatewayId") if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "LLM proxy ID is required")) - return + return apperror.ValidationFailed.New("LLM proxy ID is required") } 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 respondArtifactGuardError(w, err) { - return + if guardErr := mapArtifactGuardError(err); guardErr != nil { + return guardErr } switch { case errors.Is(err, constants.ErrLLMProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) - return + return apperror.LLMProxyNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) - return + return apperror.DeploymentNotFound.Wrap(err) case errors.Is(err, constants.ErrGatewayNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayNotFound, "The specified gateway could not be found.")) - return + return apperror.GatewayNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentNotActive): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeDeploymentNotActive, "No active deployment found for this LLM proxy on the gateway.")) - return + return apperror.DeploymentNotActive.Wrap(err, "LLM proxy") case errors.Is(err, constants.ErrGatewayIDMismatch): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeDeploymentGatewayMismatch, "Deployment is bound to a different gateway.")) - return + return apperror.DeploymentGatewayMismatch.Wrap(err) default: - h.slogger.Error("Failed to undeploy LLM proxy", "proxyId", proxyId, "deploymentId", deploymentId, "gatewayId", gatewayId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to undeploy deployment")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to undeploy LLM proxy %s deployment %s on gateway %q", proxyId, deploymentId, gatewayId)) } } httputil.WriteJSON(w, http.StatusOK, deployment) + return nil } // RestoreLLMProxyDeployment handles POST /api/v0.9/llm-proxies/{llmProxyId}/deployments/{deploymentId}/restore -func (h *LLMProxyDeploymentHandler) RestoreLLMProxyDeployment(w http.ResponseWriter, r *http.Request) { +func (h *LLMProxyDeploymentHandler) RestoreLLMProxyDeployment(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } proxyId := r.PathValue("llmProxyId") @@ -561,156 +437,118 @@ func (h *LLMProxyDeploymentHandler) RestoreLLMProxyDeployment(w http.ResponseWri gatewayId := r.URL.Query().Get("gatewayId") if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "LLM proxy ID is required")) - return + return apperror.ValidationFailed.New("LLM proxy ID is required") } 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 respondArtifactGuardError(w, err) { - return + if guardErr := mapArtifactGuardError(err); guardErr != nil { + return guardErr } switch { case errors.Is(err, constants.ErrLLMProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) - return + return apperror.LLMProxyNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) - return + return apperror.DeploymentNotFound.Wrap(err) case errors.Is(err, constants.ErrGatewayNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayNotFound, "The specified gateway could not be found.")) - return + return apperror.GatewayNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentAlreadyDeployed): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeDeploymentRestoreConflict, "Cannot restore the currently deployed deployment, or the deployment is invalid.")) - return + return apperror.DeploymentRestoreConflict.Wrap(err) case errors.Is(err, constants.ErrGatewayIDMismatch): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeDeploymentGatewayMismatch, "Deployment is bound to a different gateway.")) - return + return apperror.DeploymentGatewayMismatch.Wrap(err) default: - h.slogger.Error("Failed to restore LLM proxy deployment", "proxyId", proxyId, "deploymentId", deploymentId, "gatewayId", gatewayId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to restore deployment")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to restore LLM proxy %s deployment %s on gateway %q", proxyId, deploymentId, gatewayId)) } } httputil.WriteJSON(w, http.StatusOK, deployment) + return nil } // DeleteLLMProxyDeployment handles DELETE /api/v0.9/llm-proxies/{llmProxyId}/deployments/{deploymentId} -func (h *LLMProxyDeploymentHandler) DeleteLLMProxyDeployment(w http.ResponseWriter, r *http.Request) { +func (h *LLMProxyDeploymentHandler) DeleteLLMProxyDeployment(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } proxyId := r.PathValue("llmProxyId") deploymentId := r.PathValue("deploymentId") if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "LLM proxy ID is required")) - return + return apperror.ValidationFailed.New("LLM proxy ID is required") } if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Deployment ID is required")) - return + return apperror.ValidationFailed.New("Deployment ID is required") } err := h.deploymentService.DeleteLLMProxyDeployment(proxyId, deploymentId, orgId) if err != nil { switch { case errors.Is(err, constants.ErrLLMProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) - return + return apperror.LLMProxyNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) - return + return apperror.DeploymentNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentIsDeployed): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeDeploymentActive, "Cannot delete an active deployment - undeploy it first.")) - return + return apperror.DeploymentActive.Wrap(err) default: - h.slogger.Error("Failed to delete LLM proxy deployment", "proxyId", proxyId, "deploymentId", deploymentId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to delete deployment")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to delete LLM proxy %s deployment %s", proxyId, deploymentId)) } } w.WriteHeader(http.StatusNoContent) + return nil } // GetLLMProxyDeployment handles GET /api/v0.9/llm-proxies/{llmProxyId}/deployments/{deploymentId} -func (h *LLMProxyDeploymentHandler) GetLLMProxyDeployment(w http.ResponseWriter, r *http.Request) { +func (h *LLMProxyDeploymentHandler) GetLLMProxyDeployment(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } proxyId := r.PathValue("llmProxyId") deploymentId := r.PathValue("deploymentId") if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "LLM proxy ID is required")) - return + return apperror.ValidationFailed.New("LLM proxy ID is required") } if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Deployment ID is required")) - return + return apperror.ValidationFailed.New("Deployment ID is required") } deployment, err := h.deploymentService.GetLLMProxyDeployment(proxyId, deploymentId, orgId) if err != nil { switch { case errors.Is(err, constants.ErrLLMProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) - return + return apperror.LLMProxyNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) - return + return apperror.DeploymentNotFound.Wrap(err) default: - h.slogger.Error("Failed to get LLM proxy deployment", "proxyId", proxyId, "deploymentId", deploymentId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to retrieve deployment")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to get LLM proxy %s deployment %s", proxyId, deploymentId)) } } httputil.WriteJSON(w, http.StatusOK, deployment) + return nil } // GetLLMProxyDeployments handles GET /api/v0.9/llm-proxies/{llmProxyId}/deployments -func (h *LLMProxyDeploymentHandler) GetLLMProxyDeployments(w http.ResponseWriter, r *http.Request) { +func (h *LLMProxyDeploymentHandler) GetLLMProxyDeployments(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } proxyId := r.PathValue("llmProxyId") if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "LLM proxy ID is required")) - return + return apperror.ValidationFailed.New("LLM proxy ID is required") } q := r.URL.Query() @@ -726,31 +564,26 @@ func (h *LLMProxyDeploymentHandler) GetLLMProxyDeployments(w http.ResponseWriter if err != nil { switch { case errors.Is(err, constants.ErrLLMProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyNotFound, "The specified LLM proxy could not be found.")) - return + return apperror.LLMProxyNotFound.Wrap(err) case errors.Is(err, constants.ErrInvalidDeploymentStatus): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeDeploymentInvalidStatus, "The specified deployment status filter is invalid.")) - return + return apperror.DeploymentInvalidStatus.Wrap(err) default: - h.slogger.Error("Failed to get LLM proxy deployments", "proxyId", proxyId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to retrieve deployments")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to get LLM proxy %s deployments", proxyId)) } } httputil.WriteJSON(w, http.StatusOK, deployments) + return nil } // RegisterRoutes registers all LLM proxy deployment-related routes func (h *LLMProxyDeploymentHandler) RegisterRoutes(mux *http.ServeMux) { base := constants.APIBasePath + "/llm-proxies/{llmProxyId}" - mux.HandleFunc("POST "+base+"/deployments", h.DeployLLMProxy) - mux.HandleFunc("POST "+base+"/deployments/{deploymentId}/undeploy", h.UndeployLLMProxyDeployment) - mux.HandleFunc("POST "+base+"/deployments/{deploymentId}/restore", h.RestoreLLMProxyDeployment) - mux.HandleFunc("GET "+base+"/deployments", h.GetLLMProxyDeployments) - mux.HandleFunc("GET "+base+"/deployments/{deploymentId}", h.GetLLMProxyDeployment) - mux.HandleFunc("DELETE "+base+"/deployments/{deploymentId}", h.DeleteLLMProxyDeployment) + mux.HandleFunc("POST "+base+"/deployments", middleware.MapErrors(h.slogger, h.DeployLLMProxy)) + mux.HandleFunc("POST "+base+"/deployments/{deploymentId}/undeploy", middleware.MapErrors(h.slogger, h.UndeployLLMProxyDeployment)) + mux.HandleFunc("POST "+base+"/deployments/{deploymentId}/restore", middleware.MapErrors(h.slogger, h.RestoreLLMProxyDeployment)) + mux.HandleFunc("GET "+base+"/deployments", middleware.MapErrors(h.slogger, h.GetLLMProxyDeployments)) + mux.HandleFunc("GET "+base+"/deployments/{deploymentId}", middleware.MapErrors(h.slogger, h.GetLLMProxyDeployment)) + mux.HandleFunc("DELETE "+base+"/deployments/{deploymentId}", middleware.MapErrors(h.slogger, h.DeleteLLMProxyDeployment)) } diff --git a/platform-api/internal/handler/llm_proxy_apikey.go b/platform-api/internal/handler/llm_proxy_apikey.go index 6d300677c3..640808a064 100644 --- a/platform-api/internal/handler/llm_proxy_apikey.go +++ b/platform-api/internal/handler/llm_proxy_apikey.go @@ -20,15 +20,16 @@ package handler import ( "encoding/json" "errors" + "fmt" "log/slog" "net/http" "strings" "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/service" - "github.com/wso2/api-platform/platform-api/internal/utils" "github.com/wso2/go-httpkit/httputil" ) @@ -50,161 +51,130 @@ func NewLLMProxyAPIKeyHandler(apiKeyService *service.LLMProxyAPIKeyService, iden } // ListAPIKeys handles GET /api/v0.9/llm-proxies/{llmProxyId}/api-keys -func (h *LLMProxyAPIKeyHandler) ListAPIKeys(w http.ResponseWriter, r *http.Request) { +func (h *LLMProxyAPIKeyHandler) ListAPIKeys(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } proxyID := r.PathValue("llmProxyId") if proxyID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "LLM proxy ID is required")) - return + return apperror.ValidationFailed.New("LLM proxy ID is required") } - callerUserID, ok := resolveActor(w, r, h.identity, h.slogger, "list LLM proxy API keys") - if !ok { - return + callerUserID, err := resolveActorErr(r, h.identity, "list LLM proxy API keys") + if err != nil { + return err } response, err := h.apiKeyService.ListLLMProxyAPIKeys(r.Context(), proxyID, orgID, callerUserID) if err != nil { if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeArtifactNotFound, "The specified artifact could not be found.")) - return + return apperror.ArtifactNotFound.Wrap(err) } - h.slogger.Error("Failed to list LLM proxy API keys", "proxyId", proxyID, "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to list API keys")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to list LLM proxy API keys for proxy %s in org %s", proxyID, orgID)) } httputil.WriteJSON(w, http.StatusOK, response) + return nil } // DeleteAPIKey handles DELETE /api/v0.9/llm-proxies/{llmProxyId}/api-keys/{apiKeyId} -func (h *LLMProxyAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.Request) { +func (h *LLMProxyAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } proxyID := r.PathValue("llmProxyId") if proxyID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "LLM proxy ID is required")) - return + return apperror.ValidationFailed.New("LLM proxy ID is required") } keyName := r.PathValue("apiKeyId") if keyName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API key name is required")) - return + return apperror.ValidationFailed.New("API key name is required") } - callerUserID, ok := resolveActor(w, r, h.identity, h.slogger, "delete LLM proxy API key") - if !ok { - return + callerUserID, err := resolveActorErr(r, h.identity, "delete LLM proxy API key") + if err != nil { + return err } - err := h.apiKeyService.DeleteLLMProxyAPIKey(r.Context(), proxyID, orgID, callerUserID, keyName) - if err != nil { + if err := h.apiKeyService.DeleteLLMProxyAPIKey(r.Context(), proxyID, orgID, callerUserID, keyName); err != nil { if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeArtifactNotFound, "The specified artifact could not be found.")) - return + return apperror.ArtifactNotFound.Wrap(err) } if errors.Is(err, constants.ErrAPIKeyNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyAPIKeyNotFound, "The specified API key could not be found.")) - return + return apperror.LLMProxyAPIKeyNotFound.Wrap(err) } if errors.Is(err, constants.ErrAPIKeyForbidden) { - httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( - utils.CodeLLMProxyAPIKeyForbidden, "Only the key creator can delete this API key")) - return + return apperror.LLMProxyAPIKeyForbidden.Wrap(err) } - h.slogger.Error("Failed to delete LLM proxy API key", "proxyId", proxyID, "keyName", keyName, "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to delete API key")) - return + 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)) } h.slogger.Info("Successfully deleted LLM proxy API key", "proxyId", proxyID, "keyName", keyName, "organizationId", orgID) w.WriteHeader(http.StatusNoContent) + return nil } // CreateAPIKey handles POST /api/v0.9/llm-proxies/{llmProxyId}/api-keys -func (h *LLMProxyAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Request) { +func (h *LLMProxyAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } proxyID := r.PathValue("llmProxyId") if proxyID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "LLM proxy ID is required")) - return + return apperror.ValidationFailed.New("LLM proxy ID is required") } var req api.CreateLLMProxyAPIKeyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - h.slogger.Error("Invalid LLM proxy API key creation request", "proxyId", proxyID, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid request body")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body"). + WithLogMessage(fmt.Sprintf("invalid LLM proxy API key creation request for proxy %s", proxyID)) } // Validate that displayName is provided (name is optional; auto-generated from displayName if absent) req.DisplayName = strings.TrimSpace(req.DisplayName) if req.DisplayName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "'displayName' is required")) - return + return apperror.ValidationFailed.New("'displayName' is required") } - userID, ok := resolveActor(w, r, h.identity, h.slogger, "create LLM proxy API key") - if !ok { - return + userID, err := resolveActorErr(r, h.identity, "create LLM proxy API key") + if err != nil { + return err } response, err := h.apiKeyService.CreateLLMProxyAPIKey(r.Context(), proxyID, orgID, userID, &req) if err != nil { if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeArtifactNotFound, "The specified artifact could not be found.")) - return + return apperror.ArtifactNotFound.Wrap(err) } if errors.Is(err, constants.ErrGatewayUnavailable) { - httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponseWithCode( - utils.CodeGatewayConnectionUnavailable, "No gateway connections are currently available.")) - return + return apperror.GatewayConnectionUnavailable.Wrap(err) } - h.slogger.Error("Failed to create LLM proxy API key", "proxyId", proxyID, "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to create API key")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(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) httputil.WriteJSON(w, http.StatusCreated, response) + return nil } // RegisterRoutes registers LLM proxy API key routes with the router func (h *LLMProxyAPIKeyHandler) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("POST "+constants.APIBasePath+"/llm-proxies/{llmProxyId}/api-keys", h.CreateAPIKey) - mux.HandleFunc("GET "+constants.APIBasePath+"/llm-proxies/{llmProxyId}/api-keys", h.ListAPIKeys) - mux.HandleFunc("DELETE "+constants.APIBasePath+"/llm-proxies/{llmProxyId}/api-keys/{apiKeyId}", h.DeleteAPIKey) + mux.HandleFunc("POST "+constants.APIBasePath+"/llm-proxies/{llmProxyId}/api-keys", middleware.MapErrors(h.slogger, h.CreateAPIKey)) + mux.HandleFunc("GET "+constants.APIBasePath+"/llm-proxies/{llmProxyId}/api-keys", middleware.MapErrors(h.slogger, h.ListAPIKeys)) + mux.HandleFunc("DELETE "+constants.APIBasePath+"/llm-proxies/{llmProxyId}/api-keys/{apiKeyId}", middleware.MapErrors(h.slogger, h.DeleteAPIKey)) } diff --git a/platform-api/internal/handler/mcp.go b/platform-api/internal/handler/mcp.go index 1b8ac7820e..56b1f138ed 100644 --- a/platform-api/internal/handler/mcp.go +++ b/platform-api/internal/handler/mcp.go @@ -20,16 +20,17 @@ package handler import ( "encoding/json" "errors" + "fmt" "log/slog" "net/http" "strconv" "strings" "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/service" - "github.com/wso2/api-platform/platform-api/internal/utils" "github.com/wso2/go-httpkit/httputil" ) @@ -49,35 +50,31 @@ func NewMCPProxyHandler(service *service.MCPProxyService, identity *service.Iden } func (h *MCPProxyHandler) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("POST "+constants.APIBasePath+"/mcp-proxies", h.CreateMCPProxy) - mux.HandleFunc("GET "+constants.APIBasePath+"/mcp-proxies", h.ListMCPProxies) - mux.HandleFunc("POST "+constants.APIBasePath+"/mcp-proxies/fetch-server-info", h.FetchMCPProxyServerInfo) - mux.HandleFunc("GET "+constants.APIBasePath+"/mcp-proxies/{mcpProxyId}", h.GetMCPProxy) - mux.HandleFunc("PUT "+constants.APIBasePath+"/mcp-proxies/{mcpProxyId}", h.UpdateMCPProxy) - mux.HandleFunc("DELETE "+constants.APIBasePath+"/mcp-proxies/{mcpProxyId}", h.DeleteMCPProxy) + mux.HandleFunc("POST "+constants.APIBasePath+"/mcp-proxies", middleware.MapErrors(h.slogger, h.CreateMCPProxy)) + mux.HandleFunc("GET "+constants.APIBasePath+"/mcp-proxies", middleware.MapErrors(h.slogger, h.ListMCPProxies)) + mux.HandleFunc("POST "+constants.APIBasePath+"/mcp-proxies/fetch-server-info", middleware.MapErrors(h.slogger, h.FetchMCPProxyServerInfo)) + mux.HandleFunc("GET "+constants.APIBasePath+"/mcp-proxies/{mcpProxyId}", middleware.MapErrors(h.slogger, h.GetMCPProxy)) + mux.HandleFunc("PUT "+constants.APIBasePath+"/mcp-proxies/{mcpProxyId}", middleware.MapErrors(h.slogger, h.UpdateMCPProxy)) + mux.HandleFunc("DELETE "+constants.APIBasePath+"/mcp-proxies/{mcpProxyId}", middleware.MapErrors(h.slogger, h.DeleteMCPProxy)) } // CreateMCPProxy handles POST /api/v0.9/mcp-proxies -func (h *MCPProxyHandler) CreateMCPProxy(w http.ResponseWriter, r *http.Request) { +func (h *MCPProxyHandler) CreateMCPProxy(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - h.slogger.Error("MCP request validation failed", "reason", "Organization claim not found in token") - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } var req api.MCPProxy if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - h.slogger.Error("MCP request validation failed", "org_id", orgID, "reason", "Invalid request body", "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid request body")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body"). + WithLogMessage(fmt.Sprintf("invalid MCP proxy creation request body for org %s", orgID)) } - createdBy, ok := resolveActor(w, r, h.identity, h.slogger, "create MCP proxy") - if !ok { - return + createdBy, err := resolveActorErr(r, h.identity, "create MCP proxy") + if err != nil { + return err } if req.ProjectId == nil { @@ -86,28 +83,24 @@ func (h *MCPProxyHandler) CreateMCPProxy(w http.ResponseWriter, r *http.Request) resp, err := h.service.Create(orgID, createdBy, &req) if err != nil { - h.handleServiceError(w, err) - return + return h.mapServiceError(err) } httputil.WriteJSON(w, http.StatusCreated, resp) + return nil } // ListMCPProxies handles GET /api/v0.9/mcp-proxies -func (h *MCPProxyHandler) ListMCPProxies(w http.ResponseWriter, r *http.Request) { +func (h *MCPProxyHandler) ListMCPProxies(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - h.slogger.Error("MCP request validation failed", "reason", "Organization claim not found in token") - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } projectID := strings.TrimSpace(r.URL.Query().Get("projectId")) if projectID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "projectId query parameter is required")) - return + return apperror.ValidationFailed.New("projectId query parameter is required") } limitStr := r.URL.Query().Get("limit") @@ -135,171 +128,147 @@ func (h *MCPProxyHandler) ListMCPProxies(w http.ResponseWriter, r *http.Request) } resp, err := h.service.ListByProject(orgID, projectID, limit, offset) - if err != nil { - h.handleServiceError(w, err) - return + return h.mapServiceError(err) } + httputil.WriteJSON(w, http.StatusOK, resp) + return nil } // GetMCPProxy handles GET /api/v0.9/mcp-proxies/:id -func (h *MCPProxyHandler) GetMCPProxy(w http.ResponseWriter, r *http.Request) { +func (h *MCPProxyHandler) GetMCPProxy(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - h.slogger.Error("MCP request validation failed", "reason", "Organization claim not found in token") - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } id := r.PathValue("mcpProxyId") resp, err := h.service.Get(orgID, id) if err != nil { - h.handleServiceError(w, err) - return + return h.mapServiceError(err) } httputil.WriteJSON(w, http.StatusOK, resp) + return nil } // UpdateMCPProxy handles PUT /api/v0.9/mcp-proxies/:id -func (h *MCPProxyHandler) UpdateMCPProxy(w http.ResponseWriter, r *http.Request) { +func (h *MCPProxyHandler) UpdateMCPProxy(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - h.slogger.Error("MCP request validation failed", "reason", "Organization claim not found in token") - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } id := r.PathValue("mcpProxyId") var req api.MCPProxy if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - h.slogger.Error("MCP request validation failed", "org_id", orgID, "proxy_id", id, "reason", "Invalid request body", "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid request body")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body"). + WithLogMessage(fmt.Sprintf("invalid MCP proxy update request body for proxy %s in org %s", id, orgID)) } - updatedBy, ok := resolveActor(w, r, h.identity, h.slogger, "update MCP proxy") - if !ok { - return + updatedBy, err := resolveActorErr(r, h.identity, "update MCP proxy") + if err != nil { + return err } resp, err := h.service.Update(orgID, id, updatedBy, &req) if err != nil { - h.handleServiceError(w, err) - return + return h.mapServiceError(err) } httputil.WriteJSON(w, http.StatusOK, resp) + return nil } // DeleteMCPProxy handles DELETE /api/v0.9/mcp-proxies/:id -func (h *MCPProxyHandler) DeleteMCPProxy(w http.ResponseWriter, r *http.Request) { +func (h *MCPProxyHandler) DeleteMCPProxy(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - h.slogger.Error("MCP request validation failed", "reason", "Organization claim not found in token") - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } id := r.PathValue("mcpProxyId") - deletedBy, ok := resolveActor(w, r, h.identity, h.slogger, "delete MCP proxy") - if !ok { - return + deletedBy, err := resolveActorErr(r, h.identity, "delete MCP proxy") + if err != nil { + return err } if err := h.service.Delete(orgID, id, deletedBy); err != nil { - h.handleServiceError(w, err) - return + return h.mapServiceError(err) } w.WriteHeader(http.StatusNoContent) + return nil } // FetchMCPProxyServerInfo handles POST /api/v0.9/mcp-proxies/fetch-server-info -func (h *MCPProxyHandler) FetchMCPProxyServerInfo(w http.ResponseWriter, r *http.Request) { +func (h *MCPProxyHandler) FetchMCPProxyServerInfo(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - h.slogger.Error("MCP request validation failed", "reason", "Organization claim not found in token") - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } var req api.MCPServerInfoFetchRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - h.slogger.Error("MCP request validation failed", "org_id", orgID, "reason", "Invalid request body", "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid request body")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body"). + WithLogMessage(fmt.Sprintf("invalid MCP server info fetch request body for org %s", orgID)) } 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): - h.slogger.Error("Invalid URL provided for MCP server info fetch", "error", err, "inputUrl", req.Url) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, err.Error())) - return + return apperror.ValidationFailed.Wrap(err, err.Error()). + WithLogMessage(fmt.Sprintf("invalid URL provided for MCP server info fetch: %s", reqURL)) case errors.Is(err, constants.ErrURLUnreachable): - h.slogger.Error("MCP server URL is unreachable", "error", err, "inputUrl", req.Url) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, strings.Split(err.Error(), ":")[0])) - return + return apperror.ValidationFailed.Wrap(err, strings.Split(err.Error(), ":")[0]). + WithLogMessage(fmt.Sprintf("MCP server URL is unreachable: %s", reqURL)) case errors.Is(err, constants.ErrMCPServerUnauthorized): - h.slogger.Error("MCP server returned 401 Unauthorized", "error", err, "inputUrl", req.Url) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "MCP server returned 401 Unauthorized. Check the provided credentials.")) - return + 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: - h.handleServiceError(w, err) - return + return h.mapServiceError(err) } } httputil.WriteJSON(w, http.StatusOK, resp) + return nil } -// handleServiceError maps service errors to HTTP responses -func (h *MCPProxyHandler) handleServiceError(w http.ResponseWriter, err error) { - if respondArtifactGuardError(w, err) { - return - } +// 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). +func (h *MCPProxyHandler) mapServiceError(err error) error { switch { + case errors.Is(err, constants.ErrArtifactReadOnly): + return apperror.ArtifactReadOnly.Wrap(err, err.Error()) + case errors.Is(err, constants.ErrArtifactRuntimeImmutable): + return apperror.ArtifactRuntimeImmutable.Wrap(err, err.Error()) + case errors.Is(err, constants.ErrArtifactDeployed): + return apperror.ArtifactDeployed.Wrap(err, err.Error()) case errors.Is(err, constants.ErrHandleImmutable): - h.slogger.Error("MCP handle immutability violation", "reason", err.Error()) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, err.Error())) + return apperror.ValidationFailed.Wrap(err, "The id is immutable and cannot be changed") case errors.Is(err, constants.ErrInvalidInput): - h.slogger.Error("MCP request validation failed", "reason", err.Error()) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, err.Error())) + return apperror.ValidationFailed.Wrap(err, err.Error()) case errors.Is(err, constants.ErrMCPProxyNotFound): - h.slogger.Error("MCP proxy not found", "reason", err.Error()) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeMCPProxyNotFound, "The specified MCP proxy could not be found.")) + return apperror.MCPProxyNotFound.Wrap(err) case errors.Is(err, constants.ErrMCPProxyExists): - h.slogger.Error("MCP proxy conflict", "reason", err.Error()) - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeMCPProxyExists, "An MCP proxy with this ID already exists.")) + return apperror.MCPProxyExists.Wrap(err) case errors.Is(err, constants.ErrProjectNotFound): - h.slogger.Error("MCP request validation failed", "reason", "Project not found") - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeProjectNotFound, "The specified project could not be found.")) + return apperror.ProjectRefNotFound.Wrap(err) case errors.Is(err, constants.ErrMCPProxyLimitReached): - h.slogger.Error("MCP proxy limit reached", "reason", err.Error()) - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeMCPProxyLimitReached, "MCP proxy limit reached for the organization.")) + return apperror.MCPProxyLimitReached.Wrap(err) case errors.Is(err, constants.ErrSecretRefMissing): - h.slogger.Error("MCP proxy secret ref missing", "reason", err.Error()) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, err.Error())) + return apperror.ValidationFailed.Wrap(err, err.Error()) default: - h.slogger.Error("MCP proxy service error", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "An unexpected error occurred")) + return apperror.Internal.Wrap(err) } } diff --git a/platform-api/internal/handler/mcp_deployment.go b/platform-api/internal/handler/mcp_deployment.go index 402ad7bf09..119baec109 100644 --- a/platform-api/internal/handler/mcp_deployment.go +++ b/platform-api/internal/handler/mcp_deployment.go @@ -22,15 +22,16 @@ package handler import ( "encoding/json" "errors" + "fmt" "log/slog" "net/http" "strings" "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/service" - "github.com/wso2/api-platform/platform-api/internal/utils" "github.com/wso2/go-httpkit/httputil" ) @@ -53,176 +54,132 @@ func NewMCPProxyDeploymentHandler(deploymentService *service.MCPDeploymentServic // RegisterRoutes registers all MCP proxy deployment-related routes func (h *MCPProxyDeploymentHandler) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("POST "+constants.APIBasePath+"/mcp-proxies/{mcpProxyId}/deployments", h.DeployMCPProxy) - mux.HandleFunc("POST "+constants.APIBasePath+"/mcp-proxies/{mcpProxyId}/deployments/{deploymentId}/undeploy", h.UndeployMCPProxyDeployment) - mux.HandleFunc("POST "+constants.APIBasePath+"/mcp-proxies/{mcpProxyId}/deployments/{deploymentId}/restore", h.RestoreMCPProxyDeployment) - mux.HandleFunc("GET "+constants.APIBasePath+"/mcp-proxies/{mcpProxyId}/deployments", h.GetMCPProxyDeployments) - mux.HandleFunc("GET "+constants.APIBasePath+"/mcp-proxies/{mcpProxyId}/deployments/{deploymentId}", h.GetMCPProxyDeployment) - mux.HandleFunc("DELETE "+constants.APIBasePath+"/mcp-proxies/{mcpProxyId}/deployments/{deploymentId}", h.DeleteMCPProxyDeployment) + mux.HandleFunc("POST "+constants.APIBasePath+"/mcp-proxies/{mcpProxyId}/deployments", middleware.MapErrors(h.slogger, h.DeployMCPProxy)) + mux.HandleFunc("POST "+constants.APIBasePath+"/mcp-proxies/{mcpProxyId}/deployments/{deploymentId}/undeploy", middleware.MapErrors(h.slogger, h.UndeployMCPProxyDeployment)) + mux.HandleFunc("POST "+constants.APIBasePath+"/mcp-proxies/{mcpProxyId}/deployments/{deploymentId}/restore", middleware.MapErrors(h.slogger, h.RestoreMCPProxyDeployment)) + mux.HandleFunc("GET "+constants.APIBasePath+"/mcp-proxies/{mcpProxyId}/deployments", middleware.MapErrors(h.slogger, h.GetMCPProxyDeployments)) + mux.HandleFunc("GET "+constants.APIBasePath+"/mcp-proxies/{mcpProxyId}/deployments/{deploymentId}", middleware.MapErrors(h.slogger, h.GetMCPProxyDeployment)) + mux.HandleFunc("DELETE "+constants.APIBasePath+"/mcp-proxies/{mcpProxyId}/deployments/{deploymentId}", middleware.MapErrors(h.slogger, h.DeleteMCPProxyDeployment)) } // DeployMCPProxy handles POST /api/v0.9/mcp-proxies/:id/deployments -func (h *MCPProxyDeploymentHandler) DeployMCPProxy(w http.ResponseWriter, r *http.Request) { +func (h *MCPProxyDeploymentHandler) DeployMCPProxy(w http.ResponseWriter, r *http.Request) error { orgId, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } proxyId := r.PathValue("mcpProxyId") if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "MCP proxy ID is required")) - return + return apperror.ValidationFailed.New("MCP proxy ID is required") } var req api.DeployRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, err.Error())) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body"). + WithLogMessage(fmt.Sprintf("invalid MCP proxy deployment request body for proxy %s", proxyId)) } if req.Name == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeMCPProxyDeploymentValidationFailed, "name is required")) - return + return apperror.MCPProxyDeploymentValidationFailed.New("name is required") } if req.Base == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeMCPProxyDeploymentValidationFailed, "base is required (use 'current' or a deploymentId)")) - return + return apperror.MCPProxyDeploymentValidationFailed.New("base is required (use 'current' or a deploymentId)") } if strings.TrimSpace(req.GatewayId) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeMCPProxyDeploymentValidationFailed, "gatewayId is required")) - return + return apperror.MCPProxyDeploymentValidationFailed.New("gatewayId is required") } - createdBy, ok := resolveActor(w, r, h.identity, h.slogger, "deploy MCP proxy") - if !ok { - return + createdBy, err := resolveActorErr(r, h.identity, "deploy MCP proxy") + if err != nil { + return err } deployment, err := h.deploymentService.DeployMCPProxyByHandle(proxyId, &req, orgId, createdBy) if err != nil { - if respondArtifactGuardError(w, err) { - return + if guardErr := mapArtifactGuardError(err); guardErr != nil { + return guardErr } switch { case errors.Is(err, constants.ErrMCPProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeMCPProxyNotFound, "The specified MCP proxy could not be found.")) - return + return apperror.MCPProxyNotFound.Wrap(err) case errors.Is(err, constants.ErrGatewayNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayNotFound, "The specified gateway could not be found.")) - return + return apperror.GatewayNotFound.Wrap(err) case errors.Is(err, constants.ErrBaseDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeDeploymentBaseNotFound, "The specified base deployment could not be found.")) - return + return apperror.DeploymentBaseNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentNameRequired): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeMCPProxyDeploymentValidationFailed, "Deployment name is required")) - return + return apperror.MCPProxyDeploymentValidationFailed.Wrap(err, "Deployment name is required") case errors.Is(err, constants.ErrDeploymentBaseRequired): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeMCPProxyDeploymentValidationFailed, "Base is required")) - return + return apperror.MCPProxyDeploymentValidationFailed.Wrap(err, "Base is required") case errors.Is(err, constants.ErrDeploymentGatewayIDRequired): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeMCPProxyDeploymentValidationFailed, "Gateway ID is required")) - return + return apperror.MCPProxyDeploymentValidationFailed.Wrap(err, "Gateway ID is required") case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeMCPProxyDeploymentValidationFailed, "Invalid input")) - return + return apperror.MCPProxyDeploymentValidationFailed.Wrap(err, "Invalid input") default: - h.slogger.Error("Failed to deploy MCP proxy", "proxyId", proxyId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to deploy MCP proxy")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to deploy MCP proxy %s", proxyId)) } } httputil.WriteJSON(w, http.StatusCreated, deployment) + return nil } // UndeployMCPProxyDeployment handles POST /api/v0.9/mcp-proxies/:id/deployments/:deploymentId/undeploy -func (h *MCPProxyDeploymentHandler) UndeployMCPProxyDeployment(w http.ResponseWriter, r *http.Request) { +func (h *MCPProxyDeploymentHandler) UndeployMCPProxyDeployment(w http.ResponseWriter, r *http.Request) error { orgId, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } proxyId := r.PathValue("mcpProxyId") deploymentId := r.PathValue("deploymentId") gatewayId := r.URL.Query().Get("gatewayId") if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "deploymentId is required")) - return + return apperror.ValidationFailed.New("deploymentId is required") } if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "gatewayId is required")) - return + return apperror.ValidationFailed.New("gatewayId is required") } if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "MCP proxy ID is required")) - return + return apperror.ValidationFailed.New("MCP proxy ID is required") } 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 respondArtifactGuardError(w, err) { - return + if guardErr := mapArtifactGuardError(err); guardErr != nil { + return guardErr } switch { case errors.Is(err, constants.ErrMCPProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeMCPProxyNotFound, "The specified MCP proxy could not be found.")) - return + return apperror.MCPProxyNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) - return + return apperror.DeploymentNotFound.Wrap(err) case errors.Is(err, constants.ErrGatewayNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayNotFound, "The specified gateway could not be found.")) - return + return apperror.GatewayNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentNotActive): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeDeploymentNotActive, "No active deployment found for this MCP proxy on the gateway.")) - return + return apperror.DeploymentNotActive.Wrap(err, "MCP proxy") case errors.Is(err, constants.ErrGatewayIDMismatch): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeDeploymentGatewayMismatch, "Deployment is bound to a different gateway.")) - return + return apperror.DeploymentGatewayMismatch.Wrap(err) default: - h.slogger.Error("Failed to undeploy MCP proxy", "proxyId", proxyId, "deploymentId", deploymentId, "gatewayId", gatewayId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to undeploy deployment")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to undeploy MCP proxy %s deployment %s on gateway %s", proxyId, deploymentId, gatewayId)) } } httputil.WriteJSON(w, http.StatusOK, deployment) + return nil } // RestoreMCPProxyDeployment handles POST /api/v0.9/mcp-proxies/:id/deployments/:deploymentId/restore -func (h *MCPProxyDeploymentHandler) RestoreMCPProxyDeployment(w http.ResponseWriter, r *http.Request) { +func (h *MCPProxyDeploymentHandler) RestoreMCPProxyDeployment(w http.ResponseWriter, r *http.Request) error { orgId, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } proxyId := r.PathValue("mcpProxyId") @@ -230,167 +187,125 @@ func (h *MCPProxyDeploymentHandler) RestoreMCPProxyDeployment(w http.ResponseWri gatewayId := r.URL.Query().Get("gatewayId") if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "deploymentId is required")) - return + return apperror.ValidationFailed.New("deploymentId is required") } if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "gatewayId is required")) - return + return apperror.ValidationFailed.New("gatewayId is required") } if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "MCP proxy ID is required")) - return + return apperror.ValidationFailed.New("MCP proxy ID is required") } 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 respondArtifactGuardError(w, err) { - return + if guardErr := mapArtifactGuardError(err); guardErr != nil { + return guardErr } switch { case errors.Is(err, constants.ErrMCPProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeMCPProxyNotFound, "The specified MCP proxy could not be found.")) - return + return apperror.MCPProxyNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) - return + return apperror.DeploymentNotFound.Wrap(err) case errors.Is(err, constants.ErrGatewayNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeGatewayNotFound, "The specified gateway could not be found.")) - return + return apperror.GatewayNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentAlreadyDeployed): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeDeploymentRestoreConflict, "Cannot restore the currently deployed deployment, or the deployment is invalid.")) - return + return apperror.DeploymentRestoreConflict.Wrap(err) case errors.Is(err, constants.ErrGatewayIDMismatch): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeDeploymentGatewayMismatch, "Deployment is bound to a different gateway.")) - return + return apperror.DeploymentGatewayMismatch.Wrap(err) default: - h.slogger.Error("Failed to restore MCP proxy deployment", "proxyId", proxyId, "deploymentId", deploymentId, "gatewayId", gatewayId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to restore deployment")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to restore MCP proxy %s deployment %s on gateway %s", proxyId, deploymentId, gatewayId)) } } httputil.WriteJSON(w, http.StatusOK, deployment) + return nil } // DeleteMCPProxyDeployment handles DELETE /api/v0.9/mcp-proxies/:id/deployments/:deploymentId -func (h *MCPProxyDeploymentHandler) DeleteMCPProxyDeployment(w http.ResponseWriter, r *http.Request) { +func (h *MCPProxyDeploymentHandler) DeleteMCPProxyDeployment(w http.ResponseWriter, r *http.Request) error { orgId, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } proxyId := r.PathValue("mcpProxyId") deploymentId := r.PathValue("deploymentId") if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "MCP proxy ID is required")) - return + return apperror.ValidationFailed.New("MCP proxy ID is required") } if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Deployment ID is required")) - return + return apperror.ValidationFailed.New("Deployment ID is required") } err := h.deploymentService.DeleteDeploymentByHandle(proxyId, deploymentId, orgId) if err != nil { switch { case errors.Is(err, constants.ErrMCPProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeMCPProxyNotFound, "The specified MCP proxy could not be found.")) - return + return apperror.MCPProxyNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) - return + return apperror.DeploymentNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentIsDeployed): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeDeploymentActive, "Cannot delete an active deployment - undeploy it first.")) - return + return apperror.DeploymentActive.Wrap(err) default: - h.slogger.Error("Failed to delete MCP proxy deployment", "proxyId", proxyId, "deploymentId", deploymentId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to delete deployment")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to delete MCP proxy %s deployment %s", proxyId, deploymentId)) } } w.WriteHeader(http.StatusNoContent) + return nil } // GetMCPProxyDeployment handles GET /api/v0.9/mcp-proxies/:id/deployments/:deploymentId -func (h *MCPProxyDeploymentHandler) GetMCPProxyDeployment(w http.ResponseWriter, r *http.Request) { +func (h *MCPProxyDeploymentHandler) GetMCPProxyDeployment(w http.ResponseWriter, r *http.Request) error { orgId, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } proxyId := r.PathValue("mcpProxyId") deploymentId := r.PathValue("deploymentId") if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "MCP proxy ID is required")) - return + return apperror.ValidationFailed.New("MCP proxy ID is required") } if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Deployment ID is required")) - return + return apperror.ValidationFailed.New("Deployment ID is required") } deployment, err := h.deploymentService.GetDeploymentByHandle(proxyId, deploymentId, orgId) if err != nil { switch { case errors.Is(err, constants.ErrMCPProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeMCPProxyNotFound, "The specified MCP proxy could not be found.")) - return + return apperror.MCPProxyNotFound.Wrap(err) case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeDeploymentNotFound, "The specified deployment could not be found.")) - return + return apperror.DeploymentNotFound.Wrap(err) default: - h.slogger.Error("Failed to get MCP proxy deployment", "proxyId", proxyId, "deploymentId", deploymentId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to retrieve deployment")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to get MCP proxy %s deployment %s", proxyId, deploymentId)) } } httputil.WriteJSON(w, http.StatusOK, deployment) + return nil } // GetMCPProxyDeployments handles GET /api/v0.9/mcp-proxies/:id/deployments -func (h *MCPProxyDeploymentHandler) GetMCPProxyDeployments(w http.ResponseWriter, r *http.Request) { +func (h *MCPProxyDeploymentHandler) GetMCPProxyDeployments(w http.ResponseWriter, r *http.Request) error { orgId, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } proxyId := r.PathValue("mcpProxyId") if proxyId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "MCP proxy ID is required")) - return + return apperror.ValidationFailed.New("MCP proxy ID is required") } var params api.GetMCPProxyDeploymentsParams @@ -417,20 +332,15 @@ func (h *MCPProxyDeploymentHandler) GetMCPProxyDeployments(w http.ResponseWriter if err != nil { switch { case errors.Is(err, constants.ErrMCPProxyNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeMCPProxyNotFound, "The specified MCP proxy could not be found.")) - return + return apperror.MCPProxyNotFound.Wrap(err) case errors.Is(err, constants.ErrInvalidDeploymentStatus): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeDeploymentInvalidStatus, "The specified deployment status filter is invalid.")) - return + return apperror.DeploymentInvalidStatus.Wrap(err) default: - h.slogger.Error("Failed to get MCP proxy deployments", "proxyId", proxyId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to retrieve deployments")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to get MCP proxy %s deployments", proxyId)) } } httputil.WriteJSON(w, http.StatusOK, deployments) + return nil } diff --git a/platform-api/internal/handler/organization.go b/platform-api/internal/handler/organization.go index 6898e9db43..81cb7dad63 100644 --- a/platform-api/internal/handler/organization.go +++ b/platform-api/internal/handler/organization.go @@ -20,12 +20,14 @@ package handler import ( "encoding/json" "errors" + "fmt" "log/slog" "net/http" "strconv" "strings" "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/service" @@ -49,33 +51,26 @@ func NewOrganizationHandler(orgService *service.OrganizationService, identity *s } // RegisterOrganization handles POST /api/v0.9/organizations -func (h *OrganizationHandler) RegisterOrganization(w http.ResponseWriter, r *http.Request) { +func (h *OrganizationHandler) RegisterOrganization(w http.ResponseWriter, r *http.Request) error { var req api.Organization if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid request body")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body") } // Validate required fields if req.DisplayName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "displayName is required")) - return + return apperror.ValidationFailed.New("displayName is required") } if req.Region == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Region is required")) - return + return apperror.ValidationFailed.New("Region is required") } // UUID is always server-generated id, genErr := utils.GenerateUUID() if genErr != nil { - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to generate organization ID")) - return + return apperror.Internal.Wrap(genErr). + WithLogMessage("failed to generate organization ID") } // Extract handle from id field (optional — auto-generated from displayName if absent) @@ -84,9 +79,9 @@ func (h *OrganizationHandler) RegisterOrganization(w http.ResponseWriter, r *htt handle = *req.Id } - performedBy, ok := resolveActor(w, r, h.identity, h.slogger, "register organization") - if !ok { - return + performedBy, err := resolveActorErr(r, h.identity, "register organization") + if err != nil { + return err } // The IDP's organization UUID is derived server-side from the token's raw // organization claim (the IDP org id), never client-supplied. This uses the @@ -96,30 +91,25 @@ func (h *OrganizationHandler) RegisterOrganization(w http.ResponseWriter, r *htt org, err := h.orgService.RegisterOrganization(id, handle, req.DisplayName, req.Region, idpOrgRefUUID, performedBy) if err != nil { if errors.Is(err, constants.ErrHandleExists) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeOrganizationExists, "An organization with this handle already exists.")) - return + return apperror.OrganizationExists.Wrap(err) } if errors.Is(err, constants.ErrOrganizationExists) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeOrganizationExists, "An organization with the given ID already exists.")) - return + return apperror.OrganizationExists.Wrap(err) } - h.slogger.Error("Failed to create organization", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to create organization")) - return + return apperror.Internal.Wrap(err). + WithLogMessage("failed to create organization") } httputil.WriteJSON(w, http.StatusCreated, org) + return nil } // HeadOrganization handles HEAD /api/v0.9/organizations/{organizationId} -func (h *OrganizationHandler) HeadOrganization(w http.ResponseWriter, r *http.Request) { +func (h *OrganizationHandler) HeadOrganization(w http.ResponseWriter, r *http.Request) error { organizationIdFromContext, exists := middleware.GetOrganizationFromRequest(r) if !exists { - w.WriteHeader(http.StatusUnauthorized) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } handle := r.PathValue("organizationId") @@ -127,46 +117,42 @@ func (h *OrganizationHandler) HeadOrganization(w http.ResponseWriter, r *http.Re // to do: enable this check after finalizing authentication method // if orgID != organizationIdFromContext { - // httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( - // utils.CodeCommonForbidden, "Organization ID in token does not match the requested organization ID")) - // return + // return apperror.Forbidden.New(). + // WithLogMessage("Organization ID in token does not match the requested organization ID") // } _, err := h.orgService.GetOrganizationByHandle(handle) if err != nil { if errors.Is(err, constants.ErrOrganizationNotFound) { - w.WriteHeader(http.StatusNotFound) - return + return apperror.OrganizationNotFound.Wrap(err) } - w.WriteHeader(http.StatusInternalServerError) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to get organization by handle %s", handle)) } w.WriteHeader(http.StatusOK) + return nil } // GetOrganizationByID handles GET /api/v0.9/organizations/{organizationId} -func (h *OrganizationHandler) GetOrganizationByID(w http.ResponseWriter, r *http.Request) { +func (h *OrganizationHandler) GetOrganizationByID(w http.ResponseWriter, r *http.Request) error { handle := r.PathValue("organizationId") org, err := h.orgService.GetOrganizationByHandle(handle) if err != nil { if errors.Is(err, constants.ErrOrganizationNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeOrganizationNotFound, "The specified organization could not be found.")) - return + return apperror.OrganizationNotFound.Wrap(err) } - h.slogger.Error("Failed to get organization by handle", "handle", handle, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to get organization")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to get organization by handle %s", handle)) } httputil.WriteJSON(w, http.StatusOK, org) + return nil } // ListOrganizations handles GET /api/v0.9/organizations -func (h *OrganizationHandler) ListOrganizations(w http.ResponseWriter, r *http.Request) { +func (h *OrganizationHandler) ListOrganizations(w http.ResponseWriter, r *http.Request) error { limit := 20 if v := strings.TrimSpace(r.URL.Query().Get("limit")); v != "" { if parsed, err := strconv.Atoi(v); err == nil && parsed > 0 { @@ -186,10 +172,8 @@ func (h *OrganizationHandler) ListOrganizations(w http.ResponseWriter, r *http.R orgs, total, err := h.orgService.ListOrganizations(limit, offset) if err != nil { - h.slogger.Error("Failed to list organizations", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to list organizations")) - return + return apperror.Internal.Wrap(err). + WithLogMessage("failed to list organizations") } httputil.WriteJSON(w, http.StatusOK, api.OrganizationListResponse{ @@ -201,11 +185,12 @@ func (h *OrganizationHandler) ListOrganizations(w http.ResponseWriter, r *http.R Limit: limit, }, }) + return nil } func (h *OrganizationHandler) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("POST "+constants.APIBasePath+"/organizations", h.RegisterOrganization) - mux.HandleFunc("GET "+constants.APIBasePath+"/organizations", h.ListOrganizations) - mux.HandleFunc("HEAD "+constants.APIBasePath+"/organizations/{organizationId}", h.HeadOrganization) - mux.HandleFunc("GET "+constants.APIBasePath+"/organizations/{organizationId}", h.GetOrganizationByID) + mux.HandleFunc("POST "+constants.APIBasePath+"/organizations", middleware.MapErrors(h.slogger, h.RegisterOrganization)) + mux.HandleFunc("GET "+constants.APIBasePath+"/organizations", middleware.MapErrors(h.slogger, h.ListOrganizations)) + mux.HandleFunc("HEAD "+constants.APIBasePath+"/organizations/{organizationId}", middleware.MapErrors(h.slogger, h.HeadOrganization)) + mux.HandleFunc("GET "+constants.APIBasePath+"/organizations/{organizationId}", middleware.MapErrors(h.slogger, h.GetOrganizationByID)) } diff --git a/platform-api/internal/handler/project.go b/platform-api/internal/handler/project.go index 3a3dd12de9..613bc9006b 100644 --- a/platform-api/internal/handler/project.go +++ b/platform-api/internal/handler/project.go @@ -20,13 +20,15 @@ package handler import ( "encoding/json" "errors" + "fmt" "log/slog" "net/http" + "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/service" - "github.com/wso2/api-platform/platform-api/internal/utils" "github.com/wso2/go-httpkit/httputil" ) @@ -46,108 +48,86 @@ func NewProjectHandler(projectService *service.ProjectService, identity *service } // CreateProject handles POST /api/v0.9/projects -func (h *ProjectHandler) CreateProject(w http.ResponseWriter, r *http.Request) { +func (h *ProjectHandler) CreateProject(w http.ResponseWriter, r *http.Request) error { organizationID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } var req api.CreateProjectRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - utils.NewValidationErrorResponse(w, err) - return + return apperror.NewValidation(err) } if req.DisplayName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Project displayName is required")) - return + return apperror.ValidationFailed.New("Project displayName is required") } - actor, ok := resolveActor(w, r, h.identity, h.slogger, "create project") - if !ok { - return + actor, err := resolveActorErr(r, h.identity, "create project") + if err != nil { + return err } project, err := h.projectService.CreateProject(&req, organizationID, actor) if err != nil { if errors.Is(err, constants.ErrProjectExists) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeProjectExists, "A project with this name already exists in the organization.")) - return + return apperror.ProjectExists.Wrap(err) } if errors.Is(err, constants.ErrOrganizationNotFound) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeOrganizationNotFound, "The specified organization could not be found.")) - return + return apperror.OrganizationNotFound.Wrap(err) } if errors.Is(err, constants.ErrInvalidProjectName) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Project displayName is required")) - return + return apperror.ValidationFailed.Wrap(err, "Project displayName is required") } - h.slogger.Error("Failed to create project", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to create project")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to create project in org %s", organizationID)) } httputil.WriteJSON(w, http.StatusCreated, project) + return nil } // GetProject handles GET /api/v0.9/projects/:projectId -func (h *ProjectHandler) GetProject(w http.ResponseWriter, r *http.Request) { +func (h *ProjectHandler) GetProject(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } projectId := r.PathValue("projectId") if projectId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Project ID is required")) - return + return apperror.ValidationFailed.New("Project ID is required") } project, err := h.projectService.GetProjectByHandle(projectId, orgID) if err != nil { if errors.Is(err, constants.ErrProjectNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeProjectNotFound, "The specified project could not be found.")) - return + return apperror.ProjectNotFound.Wrap(err) } - h.slogger.Error("Failed to get project", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to get project")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to get project %s in org %s", projectId, orgID)) } httputil.WriteJSON(w, http.StatusOK, project) + return nil } // ListProjects handles GET /api/v0.9/projects -func (h *ProjectHandler) ListProjects(w http.ResponseWriter, r *http.Request) { +func (h *ProjectHandler) ListProjects(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } projects, err := h.projectService.GetProjectsByOrganization(orgID) if err != nil { if errors.Is(err, constants.ErrOrganizationNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeOrganizationNotFound, "The specified organization could not be found.")) - return + return apperror.OrganizationNotFound.Wrap(err) } - h.slogger.Error("Failed to list projects", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to get projects")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to list projects in org %s", orgID)) } // Return constitution-compliant list response @@ -160,120 +140,95 @@ func (h *ProjectHandler) ListProjects(w http.ResponseWriter, r *http.Request) { Limit: len(projects), }, }) + return nil } // UpdateProject handles PUT /api/v0.9/projects/:projectId -func (h *ProjectHandler) UpdateProject(w http.ResponseWriter, r *http.Request) { +func (h *ProjectHandler) UpdateProject(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } projectId := r.PathValue("projectId") if projectId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Project ID is required")) - return + return apperror.ValidationFailed.New("Project ID is required") } var req api.Project if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - utils.NewValidationErrorResponse(w, err) - return + return apperror.NewValidation(err) } - actor, ok := resolveActor(w, r, h.identity, h.slogger, "update project") - if !ok { - return + actor, err := resolveActorErr(r, h.identity, "update project") + if err != nil { + return err } project, err := h.projectService.UpdateProject(projectId, &req, orgID, actor) if err != nil { if errors.Is(err, constants.ErrProjectNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeProjectNotFound, "The specified project could not be found.")) - return + return apperror.ProjectNotFound.Wrap(err) } if errors.Is(err, constants.ErrHandleImmutable) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Project id is immutable and cannot be changed")) - return + return apperror.ValidationFailed.Wrap(err, "Project id is immutable and cannot be changed") } if errors.Is(err, constants.ErrProjectExists) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeProjectExists, "A project with this name already exists in the organization.")) - return + return apperror.ProjectExists.Wrap(err) } - h.slogger.Error("Failed to update project", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to update project")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to update project %s in org %s", projectId, orgID)) } httputil.WriteJSON(w, http.StatusOK, project) + return nil } // DeleteProject handles DELETE /api/v0.9/projects/:projectId -func (h *ProjectHandler) DeleteProject(w http.ResponseWriter, r *http.Request) { +func (h *ProjectHandler) DeleteProject(w http.ResponseWriter, r *http.Request) error { orgID, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } projectId := r.PathValue("projectId") if projectId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Project ID is required")) - return + return apperror.ValidationFailed.New("Project ID is required") } - actor, ok := resolveActor(w, r, h.identity, h.slogger, "delete project") - if !ok { - return - } - err := h.projectService.DeleteProject(projectId, orgID, actor) + actor, err := resolveActorErr(r, h.identity, "delete project") if err != nil { + return err + } + if err := h.projectService.DeleteProject(projectId, orgID, actor); err != nil { if errors.Is(err, constants.ErrProjectNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeProjectNotFound, "The specified project could not be found.")) - return + return apperror.ProjectNotFound.Wrap(err) } if errors.Is(err, constants.ErrOrganizationMustHAveAtLeastOneProject) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Organization must have at least one project")) - return + return apperror.ValidationFailed.Wrap(err, "Organization must have at least one project") } if errors.Is(err, constants.ErrProjectHasAssociatedAPIs) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Project has associated APIs")) - return + return apperror.ValidationFailed.Wrap(err, "Project has associated APIs") } if errors.Is(err, constants.ErrProjectHasAssociatedMCPProxies) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Project has associated MCP proxies")) - return + return apperror.ValidationFailed.Wrap(err, "Project has associated MCP proxies") } if errors.Is(err, constants.ErrProjectHasAssociatedApplications) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Project has associated applications")) - return + return apperror.ValidationFailed.Wrap(err, "Project has associated applications") } - h.slogger.Error("Failed to delete project", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to delete project")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to delete project %s in org %s", projectId, orgID)) } httputil.WriteJSON(w, http.StatusNoContent, nil) + return nil } func (h *ProjectHandler) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("GET "+constants.APIBasePath+"/projects", h.ListProjects) - mux.HandleFunc("POST "+constants.APIBasePath+"/projects", h.CreateProject) - mux.HandleFunc("GET "+constants.APIBasePath+"/projects/{projectId}", h.GetProject) - mux.HandleFunc("PUT "+constants.APIBasePath+"/projects/{projectId}", h.UpdateProject) - mux.HandleFunc("DELETE "+constants.APIBasePath+"/projects/{projectId}", h.DeleteProject) + mux.HandleFunc("GET "+constants.APIBasePath+"/projects", middleware.MapErrors(h.slogger, h.ListProjects)) + mux.HandleFunc("POST "+constants.APIBasePath+"/projects", middleware.MapErrors(h.slogger, h.CreateProject)) + mux.HandleFunc("GET "+constants.APIBasePath+"/projects/{projectId}", middleware.MapErrors(h.slogger, h.GetProject)) + mux.HandleFunc("PUT "+constants.APIBasePath+"/projects/{projectId}", middleware.MapErrors(h.slogger, h.UpdateProject)) + mux.HandleFunc("DELETE "+constants.APIBasePath+"/projects/{projectId}", middleware.MapErrors(h.slogger, h.DeleteProject)) } diff --git a/platform-api/internal/handler/secret.go b/platform-api/internal/handler/secret.go index d1ea8f1279..65f3faa40c 100644 --- a/platform-api/internal/handler/secret.go +++ b/platform-api/internal/handler/secret.go @@ -24,11 +24,11 @@ import ( "strconv" "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" - "github.com/wso2/api-platform/platform-api/internal/utils" "github.com/wso2/go-httpkit/httputil" ) @@ -45,25 +45,24 @@ func NewSecretHandler(secretService *service.SecretService, identity *service.Id func (h *SecretHandler) RegisterRoutes(mux *http.ServeMux) { for _, version := range []string{"/api/v0.9", "/api/v1"} { - mux.HandleFunc("POST "+version+"/secrets", h.CreateSecret) - mux.HandleFunc("GET "+version+"/secrets", h.ListSecrets) - mux.HandleFunc("GET "+version+"/secrets/{secretId}", h.GetSecret) - mux.HandleFunc("PUT "+version+"/secrets/{secretId}", h.UpdateSecret) - mux.HandleFunc("DELETE "+version+"/secrets/{secretId}", h.DeleteSecret) + mux.HandleFunc("POST "+version+"/secrets", middleware.MapErrors(h.slogger, h.CreateSecret)) + mux.HandleFunc("GET "+version+"/secrets", middleware.MapErrors(h.slogger, h.ListSecrets)) + mux.HandleFunc("GET "+version+"/secrets/{secretId}", middleware.MapErrors(h.slogger, h.GetSecret)) + mux.HandleFunc("PUT "+version+"/secrets/{secretId}", middleware.MapErrors(h.slogger, h.UpdateSecret)) + mux.HandleFunc("DELETE "+version+"/secrets/{secretId}", middleware.MapErrors(h.slogger, h.DeleteSecret)) } } -func (h *SecretHandler) CreateSecret(w http.ResponseWriter, r *http.Request) { +func (h *SecretHandler) CreateSecret(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } - userID, ok := resolveActor(w, r, h.identity, h.slogger, "create secret") - if !ok { - return + userID, err := resolveActorErr(r, h.identity, "create secret") + if err != nil { + return err } if err := r.ParseMultipartForm(32 << 20); err != nil { @@ -77,38 +76,29 @@ func (h *SecretHandler) CreateSecret(w http.ResponseWriter, r *http.Request) { Type: r.FormValue("type"), } if req.Handle == "" || req.DisplayName == "" || req.Value == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "id, displayName and value are required")) - return + return apperror.ValidationFailed.New("id, displayName and value are required") } resp, err := h.secretService.Create(orgID, userID, &req) if err != nil { if errors.Is(err, constants.ErrSecretAlreadyExists) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeSecretExists, "A secret with this name already exists in this scope")) - return + return apperror.SecretExists.Wrap(err) } if errors.Is(err, constants.ErrInvalidSecretType) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, err.Error())) - return + return apperror.ValidationFailed.Wrap(err, err.Error()) } - h.slogger.Error("failed to create secret", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to create secret")) - return + return apperror.Internal.Wrap(err).WithLogMessage("failed to create secret") } httputil.WriteJSON(w, http.StatusCreated, resp) + return nil } -func (h *SecretHandler) ListSecrets(w http.ResponseWriter, r *http.Request) { +func (h *SecretHandler) ListSecrets(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } limit := 25 @@ -131,73 +121,59 @@ func (h *SecretHandler) ListSecrets(w http.ResponseWriter, r *http.Request) { if ua := r.URL.Query().Get("updatedAfter"); ua != "" { t, err := time.Parse(time.RFC3339, ua) if err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "updatedAfter must be an RFC3339 timestamp")) - return + return apperror.ValidationFailed.Wrap(err, "updatedAfter must be an RFC3339 timestamp") } updatedAfter = &t } resp, err := h.secretService.List(orgID, limit, offset, updatedAfter) if err != nil { - h.slogger.Error("failed to list secrets", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to list secrets")) - return + return apperror.Internal.Wrap(err).WithLogMessage("failed to list secrets") } httputil.WriteJSON(w, http.StatusOK, resp) + return nil } -func (h *SecretHandler) GetSecret(w http.ResponseWriter, r *http.Request) { +func (h *SecretHandler) GetSecret(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } handle := r.PathValue("secretId") if handle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Secret name is required")) - return + return apperror.ValidationFailed.New("Secret name is required") } summary, err := h.secretService.Get(orgID, handle) if err != nil { if errors.Is(err, constants.ErrSecretNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeSecretNotFound, "Secret not found")) - return + return apperror.SecretNotFound.Wrap(err) } - h.slogger.Error("failed to get secret", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to get secret")) - return + return apperror.Internal.Wrap(err).WithLogMessage("failed to get secret") } httputil.WriteJSON(w, http.StatusOK, summary) + return nil } -func (h *SecretHandler) UpdateSecret(w http.ResponseWriter, r *http.Request) { +func (h *SecretHandler) UpdateSecret(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } handle := r.PathValue("secretId") if handle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Secret name is required")) - return + return apperror.ValidationFailed.New("Secret name is required") } - userID, ok := resolveActor(w, r, h.identity, h.slogger, "update secret") - if !ok { - return + userID, err := resolveActorErr(r, h.identity, "update secret") + if err != nil { + return err } if err := r.ParseMultipartForm(32 << 20); err != nil { @@ -209,53 +185,42 @@ func (h *SecretHandler) UpdateSecret(w http.ResponseWriter, r *http.Request) { Value: r.FormValue("value"), } if req.Value == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "value is required")) - return + return apperror.ValidationFailed.New("value is required") } resp, err := h.secretService.Update(orgID, handle, userID, &req) if err != nil { if errors.Is(err, constants.ErrSecretNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeSecretNotFound, "Secret not found")) - return + return apperror.SecretNotFound.Wrap(err) } - h.slogger.Error("failed to update secret", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to update secret")) - return + return apperror.Internal.Wrap(err).WithLogMessage("failed to update secret") } httputil.WriteJSON(w, http.StatusOK, resp) + return nil } -func (h *SecretHandler) DeleteSecret(w http.ResponseWriter, r *http.Request) { +func (h *SecretHandler) DeleteSecret(w http.ResponseWriter, r *http.Request) error { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } handle := r.PathValue("secretId") if handle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Secret name is required")) - return + return apperror.ValidationFailed.New("Secret name is required") } - userID, ok := resolveActor(w, r, h.identity, h.slogger, "delete secret") - if !ok { - return + userID, err := resolveActorErr(r, h.identity, "delete secret") + if err != nil { + return err } - err := h.secretService.Delete(orgID, handle, userID) + err = h.secretService.Delete(orgID, handle, userID) if err != nil { if errors.Is(err, constants.ErrSecretNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeSecretNotFound, "Secret not found")) - return + return apperror.SecretNotFound.Wrap(err) } var inUseErr *service.SecretInUseError @@ -264,17 +229,12 @@ func (h *SecretHandler) DeleteSecret(w http.ResponseWriter, r *http.Request) { for _, ref := range inUseErr.References { refs = append(refs, dto.SecretReferenceDTO{Type: ref.Type, Handle: ref.Handle, Name: ref.Name}) } - resp := utils.NewErrorResponseWithCode(utils.CodeSecretInUse, "The secret is referenced by one or more active resources.") - resp.Details = dto.SecretInUseDetails{References: refs} - httputil.WriteJSON(w, http.StatusConflict, resp) - return + return apperror.SecretInUse.New().WithDetails(dto.SecretInUseDetails{References: refs}) } - h.slogger.Error("failed to delete secret", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to delete secret")) - return + return apperror.Internal.Wrap(err).WithLogMessage("failed to delete secret") } w.WriteHeader(http.StatusNoContent) + return nil } diff --git a/platform-api/internal/handler/subscription_handler.go b/platform-api/internal/handler/subscription_handler.go index 19eb4f928d..b584a970a2 100644 --- a/platform-api/internal/handler/subscription_handler.go +++ b/platform-api/internal/handler/subscription_handler.go @@ -20,17 +20,18 @@ package handler import ( "encoding/json" "errors" + "fmt" "log/slog" "net/http" "strconv" "strings" api "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" - "github.com/wso2/api-platform/platform-api/internal/utils" "github.com/wso2/go-httpkit/httputil" ) @@ -67,81 +68,60 @@ type CreateSubscriptionRequest struct { } // CreateSubscription handles POST /api/v0.9/subscriptions -func (h *SubscriptionHandler) CreateSubscription(w http.ResponseWriter, r *http.Request) { +func (h *SubscriptionHandler) CreateSubscription(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - h.slogger.Error("Organization claim not found in token when creating subscription") - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token when creating subscription") } var req CreateSubscriptionRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - h.slogger.Error("Invalid create subscription request body", "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid request body")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body"). + WithLogMessage(fmt.Sprintf("invalid create subscription request body for org %s", orgId)) } if req.APIID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "API ID is required")) - return + return apperror.ValidationFailed.New("API ID is required") } if req.SubscriberID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "subscriberId is required")) - return + return apperror.ValidationFailed.New("subscriberId is required") } if req.Kind == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "kind is required")) - return + return apperror.ValidationFailed.New("kind is required") } if !constants.ValidArtifactKinds[req.Kind] { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid kind value")) - return + return apperror.ValidationFailed.New("Invalid kind value") } switch req.Status { case "", "ACTIVE", "INACTIVE", "REVOKED": default: - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid status value")) - return + return apperror.ValidationFailed.New("Invalid status value") } - actor, ok := resolveActor(w, r, h.identity, h.slogger, "create subscription") - if !ok { - return + actor, err := resolveActorErr(r, h.identity, "create subscription") + if err != nil { + return err } sub, err := h.subscriptionService.CreateSubscription(req.APIID, req.Kind, orgId, req.SubscriberID, req.ApplicationID, req.SubscriptionPlanID, "", req.Status, actor) if err != nil { if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeArtifactNotFound, "The specified artifact could not be found.")) - return + return apperror.ArtifactNotFound.Wrap(err) } if errors.Is(err, constants.ErrSubscriptionAlreadyExists) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeSubscriptionExists, "A subscription already exists for this API.")) - return + return apperror.SubscriptionExists.Wrap(err) } - h.slogger.Error("Failed to create subscription", "apiId", req.APIID, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to create subscription")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to create subscription for api %s in org %s", req.APIID, orgId)) } resp, err := h.toSubscriptionResponse(sub, orgId) if err != nil { - h.slogger.Error("Failed to resolve subscription identity", "apiId", req.APIID, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to create subscription")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to resolve subscription identity for api %s in org %s", req.APIID, orgId)) } httputil.WriteJSON(w, http.StatusCreated, resp) + return nil } // ListSubscriptions handles GET /api/v0.9/subscriptions -func (h *SubscriptionHandler) ListSubscriptions(w http.ResponseWriter, r *http.Request) { +func (h *SubscriptionHandler) ListSubscriptions(w http.ResponseWriter, r *http.Request) error { apiId := r.URL.Query().Get("apiId") subscriberID := r.URL.Query().Get("subscriberId") applicationID := r.URL.Query().Get("applicationId") @@ -149,9 +129,8 @@ func (h *SubscriptionHandler) ListSubscriptions(w http.ResponseWriter, r *http.R orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } var apiIDPtr, subscriberIDPtr, appIDPtr, statusPtr *string @@ -168,9 +147,7 @@ func (h *SubscriptionHandler) ListSubscriptions(w http.ResponseWriter, r *http.R switch status { case "ACTIVE", "INACTIVE", "REVOKED": default: - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid status value")) - return + return apperror.ValidationFailed.New("Invalid status value") } statusPtr = &status } @@ -200,14 +177,10 @@ func (h *SubscriptionHandler) ListSubscriptions(w http.ResponseWriter, r *http.R list, total, err := h.subscriptionService.ListSubscriptionsByFilters(orgId, apiIDPtr, subscriberIDPtr, appIDPtr, statusPtr, limit, offset) if err != nil { if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeArtifactNotFound, "The specified artifact could not be found.")) - return + return apperror.ArtifactNotFound.Wrap(err) } - h.slogger.Error("Failed to list subscriptions", "apiId", apiId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to list subscriptions")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(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{}) @@ -230,17 +203,13 @@ func (h *SubscriptionHandler) ListSubscriptions(w http.ResponseWriter, r *http.R } artifactMetaMap, err := h.subscriptionService.GetArtifactMetadataMap(apiUUIDs, orgId) if err != nil { - h.slogger.Error("Failed to bulk fetch artifact metadata for list", "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to list subscriptions")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to bulk fetch artifact metadata for list in org %s", orgId)) } planNameMap, err := h.subscriptionPlanService.GetPlanNameMap(planIDs, orgId) if err != nil { - h.slogger.Error("Failed to bulk fetch plan names for list", "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to list subscriptions")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(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)) @@ -249,10 +218,8 @@ func (h *SubscriptionHandler) ListSubscriptions(w http.ResponseWriter, r *http.R } createdByMap, err := h.identity.SubsForUUIDs(createdByUUIDs) if err != nil { - h.slogger.Error("Failed to resolve subscription creator identities", "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to list subscriptions")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to resolve subscription creator identities in org %s", orgId)) } items := make([]map[string]any, 0, len(list)) for _, sub := range list { @@ -267,63 +234,51 @@ func (h *SubscriptionHandler) ListSubscriptions(w http.ResponseWriter, r *http.R "limit": limit, }, }) + return nil } // GetSubscription handles GET /api/v0.9/subscriptions/:subscriptionId -func (h *SubscriptionHandler) GetSubscription(w http.ResponseWriter, r *http.Request) { +func (h *SubscriptionHandler) GetSubscription(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } subscriptionId := r.PathValue("subscriptionId") if subscriptionId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Subscription ID is required")) - return + return apperror.ValidationFailed.New("Subscription ID is required") } sub, err := h.subscriptionService.GetSubscription(subscriptionId, orgId) if err != nil { if errors.Is(err, constants.ErrSubscriptionNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeSubscriptionNotFound, "The specified subscription could not be found.")) - return + return apperror.SubscriptionNotFound.Wrap(err) } - h.slogger.Error("Failed to get subscription", "subscriptionId", subscriptionId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to get subscription")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to get subscription %s in org %s", subscriptionId, orgId)) } resp, err := h.toSubscriptionResponse(sub, orgId) if err != nil { - h.slogger.Error("Failed to resolve subscription identity", "subscriptionId", subscriptionId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to get subscription")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to resolve subscription identity for subscription %s in org %s", subscriptionId, orgId)) } httputil.WriteJSON(w, http.StatusOK, resp) + return nil } // UpdateSubscription handles PUT /api/v0.9/subscriptions/:subscriptionId -func (h *SubscriptionHandler) UpdateSubscription(w http.ResponseWriter, r *http.Request) { +func (h *SubscriptionHandler) UpdateSubscription(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } subscriptionId := r.PathValue("subscriptionId") if subscriptionId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Subscription ID is required")) - return + return apperror.ValidationFailed.New("Subscription ID is required") } var req api.Subscription if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid request body")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body") } var status string if req.Status != nil { @@ -332,103 +287,83 @@ func (h *SubscriptionHandler) UpdateSubscription(w http.ResponseWriter, r *http. switch status { case "", "ACTIVE", "INACTIVE", "REVOKED": default: - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid subscription status")) - return + return apperror.ValidationFailed.New("Invalid subscription status") } - subscriberID, ok := requireSubscriptionSubscriberQuery(w, r) - if !ok { - return + subscriberID, err := requireSubscriptionSubscriberQuery(r) + if err != nil { + return err } - actor, ok := resolveActor(w, r, h.identity, h.slogger, "update subscription") - if !ok { - return + actor, err := resolveActorErr(r, h.identity, "update subscription") + if err != nil { + return err } sub, err := h.subscriptionService.UpdateSubscription(subscriptionId, orgId, subscriberID, status, actor) if err != nil { if errors.Is(err, constants.ErrSubscriptionNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeSubscriptionNotFound, "The specified subscription could not be found.")) - return + return apperror.SubscriptionNotFound.Wrap(err) } if errors.Is(err, constants.ErrSubscriptionSubscriberMismatch) { - httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( - utils.CodeSubscriptionForbidden, "subscriberId does not match this subscription")) - return + return apperror.SubscriptionForbidden.Wrap(err) } - h.slogger.Error("Failed to update subscription", "subscriptionId", subscriptionId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to update subscription")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to update subscription %s in org %s", subscriptionId, orgId)) } resp, err := h.toSubscriptionResponse(sub, orgId) if err != nil { - h.slogger.Error("Failed to resolve subscription identity", "subscriptionId", subscriptionId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to update subscription")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to resolve subscription identity for subscription %s in org %s", subscriptionId, orgId)) } httputil.WriteJSON(w, http.StatusOK, resp) + return nil } // DeleteSubscription handles DELETE /api/v0.9/subscriptions/:subscriptionId -func (h *SubscriptionHandler) DeleteSubscription(w http.ResponseWriter, r *http.Request) { +func (h *SubscriptionHandler) DeleteSubscription(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } subscriptionId := r.PathValue("subscriptionId") if subscriptionId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Subscription ID is required")) - return + return apperror.ValidationFailed.New("Subscription ID is required") } - subscriberID, ok := requireSubscriptionSubscriberQuery(w, r) - if !ok { - return - } - actor, ok := resolveActor(w, r, h.identity, h.slogger, "delete subscription") - if !ok { - return + subscriberID, err := requireSubscriptionSubscriberQuery(r) + if err != nil { + return err } - err := h.subscriptionService.DeleteSubscription(subscriptionId, orgId, subscriberID, actor) + actor, err := resolveActorErr(r, h.identity, "delete subscription") if err != nil { + return err + } + if err := h.subscriptionService.DeleteSubscription(subscriptionId, orgId, subscriberID, actor); err != nil { if errors.Is(err, constants.ErrSubscriptionNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeSubscriptionNotFound, "The specified subscription could not be found.")) - return + return apperror.SubscriptionNotFound.Wrap(err) } if errors.Is(err, constants.ErrSubscriptionSubscriberMismatch) { - httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponseWithCode( - utils.CodeSubscriptionForbidden, "subscriberId does not match this subscription")) - return + return apperror.SubscriptionForbidden.Wrap(err) } - h.slogger.Error("Failed to delete subscription", "subscriptionId", subscriptionId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to delete subscription")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to delete subscription %s in org %s", subscriptionId, orgId)) } w.WriteHeader(http.StatusNoContent) + return nil } -func requireSubscriptionSubscriberQuery(w http.ResponseWriter, r *http.Request) (string, bool) { +func requireSubscriptionSubscriberQuery(r *http.Request) (string, error) { q := strings.TrimSpace(r.URL.Query().Get("subscriberId")) if q == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "subscriberId query parameter is required")) - return "", false + return "", apperror.ValidationFailed.New("subscriberId query parameter is required") } - return q, true + return q, nil } func (h *SubscriptionHandler) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("POST "+constants.APIBasePath+"/subscriptions", h.CreateSubscription) - mux.HandleFunc("GET "+constants.APIBasePath+"/subscriptions", h.ListSubscriptions) - mux.HandleFunc("GET "+constants.APIBasePath+"/subscriptions/{subscriptionId}", h.GetSubscription) - mux.HandleFunc("PUT "+constants.APIBasePath+"/subscriptions/{subscriptionId}", h.UpdateSubscription) - mux.HandleFunc("DELETE "+constants.APIBasePath+"/subscriptions/{subscriptionId}", h.DeleteSubscription) + mux.HandleFunc("POST "+constants.APIBasePath+"/subscriptions", middleware.MapErrors(h.slogger, h.CreateSubscription)) + mux.HandleFunc("GET "+constants.APIBasePath+"/subscriptions", middleware.MapErrors(h.slogger, h.ListSubscriptions)) + mux.HandleFunc("GET "+constants.APIBasePath+"/subscriptions/{subscriptionId}", middleware.MapErrors(h.slogger, h.GetSubscription)) + mux.HandleFunc("PUT "+constants.APIBasePath+"/subscriptions/{subscriptionId}", middleware.MapErrors(h.slogger, h.UpdateSubscription)) + mux.HandleFunc("DELETE "+constants.APIBasePath+"/subscriptions/{subscriptionId}", middleware.MapErrors(h.slogger, h.DeleteSubscription)) } func (h *SubscriptionHandler) toSubscriptionResponse(sub *model.Subscription, orgId string) (map[string]any, error) { diff --git a/platform-api/internal/handler/subscription_plan_handler.go b/platform-api/internal/handler/subscription_plan_handler.go index 360760d217..6681aaf1a0 100644 --- a/platform-api/internal/handler/subscription_plan_handler.go +++ b/platform-api/internal/handler/subscription_plan_handler.go @@ -20,17 +20,18 @@ package handler import ( "encoding/json" "errors" + "fmt" "log/slog" "net/http" "strconv" "time" api "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" - "github.com/wso2/api-platform/platform-api/internal/utils" "github.com/wso2/go-httpkit/httputil" ) @@ -141,29 +142,31 @@ type CreateSubscriptionPlanRequest struct { } // CreateSubscriptionPlan handles POST /api/v0.9/subscription-plans -func (h *SubscriptionPlanHandler) CreateSubscriptionPlan(w http.ResponseWriter, r *http.Request) { +func (h *SubscriptionPlanHandler) CreateSubscriptionPlan(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } var req CreateSubscriptionPlanRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - h.slogger.Error("Invalid create subscription plan request body", "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid request body")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body"). + WithLogMessage(fmt.Sprintf("invalid create subscription plan request body for org %s", orgId)) + } + + if req.Id == "" { + return apperror.ValidationFailed.New("id is required") + } + if req.DisplayName == "" { + return apperror.ValidationFailed.New("displayName is required") } if req.Status != "" { switch req.Status { case "ACTIVE", "INACTIVE": default: - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid status value; must be ACTIVE or INACTIVE")) - return + return apperror.ValidationFailed.New("Invalid status value; must be ACTIVE or INACTIVE") } } @@ -175,9 +178,7 @@ func (h *SubscriptionPlanHandler) CreateSubscriptionPlan(w http.ResponseWriter, } if limit := firstLimit(req.Limits); limit != nil { if errMsg := normalizeAndValidateLimit(limit); errMsg != "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, errMsg)) - return + return apperror.ValidationFailed.New(errMsg) } count := limit.LimitCount plan.ThrottleLimitCount = &count @@ -189,55 +190,44 @@ func (h *SubscriptionPlanHandler) CreateSubscriptionPlan(w http.ResponseWriter, if req.ExpiryTime != nil { t, err := time.Parse(time.RFC3339, *req.ExpiryTime) if err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid expiryTime format; use RFC3339")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid expiryTime format; use RFC3339") } plan.ExpiryTime = &t } rawActor, ok := middleware.GetActorIdentityFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "User ID claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("user id claim not found in token") } actor, err := h.identity.ToInternalUUID(rawActor) if err != nil { - h.slogger.Error("Failed to resolve user identity", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to resolve user identity")) - return + return apperror.Internal.Wrap(err). + WithLogMessage("failed to resolve user identity") } created, err := h.planService.CreatePlan(orgId, actor, plan) if err != nil { if errors.Is(err, constants.ErrSubscriptionPlanAlreadyExists) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeSubscriptionPlanExists, "A subscription plan with this name already exists for the organization.")) - return + return apperror.SubscriptionPlanExists.Wrap(err) } - h.slogger.Error("Failed to create subscription plan", "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to create subscription plan")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to create subscription plan for org %s", orgId)) } resp, err := h.toSubscriptionPlanResponse(created, true) if err != nil { - h.slogger.Error("Failed to resolve subscription plan identity", "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to create subscription plan")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to resolve subscription plan identity for org %s", orgId)) } httputil.WriteJSON(w, http.StatusCreated, resp) + return nil } // ListSubscriptionPlans handles GET /api/v0.9/subscription-plans -func (h *SubscriptionPlanHandler) ListSubscriptionPlans(w http.ResponseWriter, r *http.Request) { +func (h *SubscriptionPlanHandler) ListSubscriptionPlans(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } var limitStr string @@ -266,98 +256,78 @@ func (h *SubscriptionPlanHandler) ListSubscriptionPlans(w http.ResponseWriter, r list, err := h.planService.ListPlans(orgId, limit, offset) if err != nil { - h.slogger.Error("Failed to list subscription plans", "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to list subscription plans")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to list subscription plans for org %s", orgId)) } items := make([]map[string]any, 0, len(list)) for _, p := range list { item, err := h.toSubscriptionPlanResponse(p, false) if err != nil { - h.slogger.Error("Failed to resolve subscription plan identity", "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to list subscription plans")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to resolve subscription plan identity for org %s", orgId)) } items = append(items, item) } httputil.WriteJSON(w, http.StatusOK, map[string]any{"subscriptionPlans": items, "count": len(items)}) + return nil } // GetSubscriptionPlan handles GET /api/v0.9/subscription-plans/:planId -func (h *SubscriptionPlanHandler) GetSubscriptionPlan(w http.ResponseWriter, r *http.Request) { +func (h *SubscriptionPlanHandler) GetSubscriptionPlan(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } planId := r.PathValue("subscriptionPlanId") if planId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Plan ID is required")) - return + return apperror.ValidationFailed.New("Plan ID is required") } plan, err := h.planService.GetPlan(planId, orgId) if err != nil { if errors.Is(err, constants.ErrSubscriptionPlanNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeSubscriptionPlanNotFound, "The specified subscription plan could not be found.")) - return + return apperror.SubscriptionPlanNotFound.Wrap(err) } - h.slogger.Error("Failed to get subscription plan", "planId", planId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to get subscription plan")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to get subscription plan %s in org %s", planId, orgId)) } resp, err := h.toSubscriptionPlanResponse(plan, true) if err != nil { - h.slogger.Error("Failed to resolve subscription plan identity", "planId", planId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to get subscription plan")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to resolve subscription plan identity for plan %s in org %s", planId, orgId)) } httputil.WriteJSON(w, http.StatusOK, resp) + return nil } // UpdateSubscriptionPlan handles PUT /api/v0.9/subscription-plans/:planId -func (h *SubscriptionPlanHandler) UpdateSubscriptionPlan(w http.ResponseWriter, r *http.Request) { +func (h *SubscriptionPlanHandler) UpdateSubscriptionPlan(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } planId := r.PathValue("subscriptionPlanId") if planId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Plan ID is required")) - return + return apperror.ValidationFailed.New("Plan ID is required") } var req api.SubscriptionPlan if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - h.slogger.Error("Invalid update subscription plan request body", "planId", planId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid request body")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid request body"). + WithLogMessage(fmt.Sprintf("invalid update subscription plan request body for plan %s in org %s", planId, orgId)) } if req.Id != nil && *req.Id != "" && *req.Id != planId { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "The plan id is immutable and cannot be changed")) - return + return apperror.ValidationFailed.New("The plan id is immutable and cannot be changed") } displayName := req.DisplayName if displayName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "displayName is required")) - return + return apperror.ValidationFailed.New("displayName is required") } update := &model.SubscriptionPlanUpdate{ Name: &displayName, @@ -366,9 +336,7 @@ func (h *SubscriptionPlanHandler) UpdateSubscriptionPlan(w http.ResponseWriter, if req.Limits != nil { if limit := firstLimit(apiLimitsToRequests(*req.Limits)); limit != nil { if errMsg := normalizeAndValidateLimit(limit); errMsg != "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, errMsg)) - return + return apperror.ValidationFailed.New(errMsg) } count := limit.LimitCount update.ThrottleLimitCount = &count @@ -388,108 +356,85 @@ func (h *SubscriptionPlanHandler) UpdateSubscriptionPlan(w http.ResponseWriter, st := model.SubscriptionPlanStatus(*req.Status) update.Status = &st default: - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Invalid status value; must be ACTIVE or INACTIVE")) - return + return apperror.ValidationFailed.New("Invalid status value; must be ACTIVE or INACTIVE") } } rawActor, ok := middleware.GetActorIdentityFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "User ID claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("user id claim not found in token") } actor, err := h.identity.ToInternalUUID(rawActor) if err != nil { - h.slogger.Error("Failed to resolve user identity", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to resolve user identity")) - return + return apperror.Internal.Wrap(err). + WithLogMessage("failed to resolve user identity") } updated, err := h.planService.UpdatePlan(planId, orgId, actor, update) if err != nil { if errors.Is(err, constants.ErrHandleImmutable) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "The plan id is immutable and cannot be changed")) - return + return apperror.ValidationFailed.Wrap(err, "The plan id is immutable and cannot be changed") } if errors.Is(err, constants.ErrSubscriptionPlanNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeSubscriptionPlanNotFound, "The specified subscription plan could not be found.")) - return + return apperror.SubscriptionPlanNotFound.Wrap(err) } if errors.Is(err, constants.ErrSubscriptionPlanAlreadyExists) { - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponseWithCode( - utils.CodeSubscriptionPlanExists, "A subscription plan with this name already exists for the organization.")) - return + return apperror.SubscriptionPlanExists.Wrap(err) } - h.slogger.Error("Failed to update subscription plan", "planId", planId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to update subscription plan")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to update subscription plan %s in org %s", planId, orgId)) } resp, err := h.toSubscriptionPlanResponse(updated, true) if err != nil { - h.slogger.Error("Failed to resolve subscription plan identity", "planId", planId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to update subscription plan")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to resolve subscription plan identity for plan %s in org %s", planId, orgId)) } httputil.WriteJSON(w, http.StatusOK, resp) + return nil } // DeleteSubscriptionPlan handles DELETE /api/v0.9/subscription-plans/:planId -func (h *SubscriptionPlanHandler) DeleteSubscriptionPlan(w http.ResponseWriter, r *http.Request) { +func (h *SubscriptionPlanHandler) DeleteSubscriptionPlan(w http.ResponseWriter, r *http.Request) error { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "Organization claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("organization claim not found in token") } planId := r.PathValue("subscriptionPlanId") if planId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode( - utils.CodeCommonValidationFailed, "Plan ID is required")) - return + return apperror.ValidationFailed.New("Plan ID is required") } rawActor, ok := middleware.GetActorIdentityFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponseWithCode( - utils.CodeCommonUnauthorized, "User ID claim not found in token")) - return + return apperror.Unauthorized.New(). + WithLogMessage("user id claim not found in token") } actor, err := h.identity.ToInternalUUID(rawActor) if err != nil { - h.slogger.Error("Failed to resolve user identity", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to resolve user identity")) - return + return apperror.Internal.Wrap(err). + WithLogMessage("failed to resolve user identity") } err = h.planService.DeletePlan(planId, orgId, actor) if err != nil { if errors.Is(err, constants.ErrSubscriptionPlanNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponseWithCode( - utils.CodeSubscriptionPlanNotFound, "The specified subscription plan could not be found.")) - return + return apperror.SubscriptionPlanNotFound.Wrap(err) } - h.slogger.Error("Failed to delete subscription plan", "planId", planId, "organizationId", orgId, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponseWithCode( - utils.CodeCommonInternalError, "Failed to delete subscription plan")) - return + return apperror.Internal.Wrap(err). + WithLogMessage(fmt.Sprintf("failed to delete subscription plan %s in org %s", planId, orgId)) } w.WriteHeader(http.StatusNoContent) + return nil } // RegisterRoutes registers subscription plan routes func (h *SubscriptionPlanHandler) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("POST "+constants.APIBasePath+"/subscription-plans", h.CreateSubscriptionPlan) - mux.HandleFunc("GET "+constants.APIBasePath+"/subscription-plans", h.ListSubscriptionPlans) - mux.HandleFunc("GET "+constants.APIBasePath+"/subscription-plans/{subscriptionPlanId}", h.GetSubscriptionPlan) - mux.HandleFunc("PUT "+constants.APIBasePath+"/subscription-plans/{subscriptionPlanId}", h.UpdateSubscriptionPlan) - mux.HandleFunc("DELETE "+constants.APIBasePath+"/subscription-plans/{subscriptionPlanId}", h.DeleteSubscriptionPlan) + mux.HandleFunc("POST "+constants.APIBasePath+"/subscription-plans", middleware.MapErrors(h.slogger, h.CreateSubscriptionPlan)) + mux.HandleFunc("GET "+constants.APIBasePath+"/subscription-plans", middleware.MapErrors(h.slogger, h.ListSubscriptionPlans)) + mux.HandleFunc("GET "+constants.APIBasePath+"/subscription-plans/{subscriptionPlanId}", middleware.MapErrors(h.slogger, h.GetSubscriptionPlan)) + mux.HandleFunc("PUT "+constants.APIBasePath+"/subscription-plans/{subscriptionPlanId}", middleware.MapErrors(h.slogger, h.UpdateSubscriptionPlan)) + mux.HandleFunc("DELETE "+constants.APIBasePath+"/subscription-plans/{subscriptionPlanId}", middleware.MapErrors(h.slogger, h.DeleteSubscriptionPlan)) } // toSubscriptionPlanResponse builds the API response for a plan. diff --git a/platform-api/internal/handler/websocket.go b/platform-api/internal/handler/websocket.go index 60b21c0533..c41d42fc3e 100644 --- a/platform-api/internal/handler/websocket.go +++ b/platform-api/internal/handler/websocket.go @@ -25,14 +25,14 @@ import ( "sync" "time" + "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/middleware" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/service" - "github.com/wso2/api-platform/platform-api/internal/utils" ws "github.com/wso2/api-platform/platform-api/internal/websocket" "github.com/gorilla/websocket" - "github.com/wso2/go-httpkit/httputil" ) // WebSocketHandler handles WebSocket connection upgrades and lifecycle @@ -70,7 +70,7 @@ func NewWebSocketHandler(manager *ws.Manager, gatewayService *service.GatewaySer // Connect handles WebSocket upgrade requests at /api/internal/v1/ws/gateways/connect // This is the entry point for gateway connections. -func (h *WebSocketHandler) Connect(w http.ResponseWriter, r *http.Request) { +func (h *WebSocketHandler) Connect(w http.ResponseWriter, r *http.Request) error { // Extract client IP for rate limiting clientIP := r.RemoteAddr if i := strings.LastIndex(clientIP, ":"); i != -1 { @@ -79,51 +79,45 @@ func (h *WebSocketHandler) Connect(w http.ResponseWriter, r *http.Request) { // Check rate limit if !h.checkRateLimit(clientIP) { - h.slogger.Warn("Rate limit exceeded for IP", "ip", clientIP) h.manager.IncrementFailedConnections() - httputil.WriteJSON(w, http.StatusTooManyRequests, utils.NewErrorResponse(429, "Too Many Requests", - "Connection rate limit exceeded. Please try again later.")) - return + return apperror.TooManyRequests.New("Connection rate limit exceeded. Please try again later."). + WithLogMessage(fmt.Sprintf("rate limit exceeded for IP %s", clientIP)) } - // Extract and validate API key from header + // Extract and validate API key from header. Per the unified auth-failure rule, a missing key + // and an invalid key both surface the identical generic response; the specific reason is + // internal-only via WithLogMessage. apiKey := r.Header.Get("api-key") if apiKey == "" { - h.slogger.Warn("WebSocket connection attempt without API key", "ip", clientIP) h.manager.IncrementFailedConnections() - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "API key is required. Provide 'api-key' header.")) - return + return apperror.Unauthorized.New(). + WithLogMessage(fmt.Sprintf("WebSocket connection attempt without API key from IP %s", clientIP)) } // Authenticate gateway using API key gateway, err := h.gatewayService.VerifyToken(apiKey) if err != nil { - h.slogger.Warn("WebSocket authentication failed", "ip", clientIP, "error", err) h.manager.IncrementFailedConnections() - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", - "Invalid or expired API key")) - return + return apperror.Unauthorized.Wrap(err). + WithLogMessage(fmt.Sprintf("WebSocket authentication failed from IP %s", clientIP)) } // Check organization connection limit before upgrading to WebSocket if !h.manager.CanAcceptOrgConnection(gateway.OrganizationID) { stats := h.manager.GetOrgConnectionStats(gateway.OrganizationID) - h.slogger.Warn("Organization connection limit exceeded", "orgID", gateway.OrganizationID, - "count", stats.CurrentCount, "max", stats.MaxAllowed) h.manager.IncrementFailedConnections() - httputil.WriteJSON(w, http.StatusTooManyRequests, utils.NewErrorResponse(429, "Too Many Requests", - "Organization connection limit reached. Maximum allowed connections: "+ - fmt.Sprintf("%d", stats.MaxAllowed))) - return + return apperror.TooManyRequests.New(fmt.Sprintf( + "Organization connection limit reached. Maximum allowed connections: %d", stats.MaxAllowed)). + WithLogMessage(fmt.Sprintf("organization connection limit exceeded for org %s (count=%d, max=%d)", + gateway.OrganizationID, stats.CurrentCount, stats.MaxAllowed)) } // Upgrade HTTP connection to WebSocket conn, err := h.upgrader.Upgrade(w, r, nil) if err != nil { h.slogger.Error("WebSocket upgrade failed", "gatewayID", gateway.ID, "error", err) - // Upgrade error is already sent by upgrader - return + // Upgrade error response is already written to w by the upgrader itself. + return nil } // Create WebSocket transport @@ -160,7 +154,7 @@ func (h *WebSocketHandler) Connect(w http.ResponseWriter, r *http.Request) { } } conn.Close() - return + return nil } // Send connection acknowledgment @@ -201,6 +195,7 @@ func (h *WebSocketHandler) Connect(w http.ResponseWriter, r *http.Request) { h.slogger.Error("Failed to update gateway active status to false", "gatewayID", gateway.ID, "error", err) } } + return nil } // readLoop reads messages from the WebSocket connection and routes them to handlers. @@ -316,5 +311,5 @@ func (h *WebSocketHandler) checkRateLimit(clientIP string) bool { // RegisterRoutes registers WebSocket routes with the mux. func (h *WebSocketHandler) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("GET /api/internal/v1/ws/gateways/connect", h.Connect) + mux.HandleFunc("GET /api/internal/v1/ws/gateways/connect", middleware.MapErrors(h.slogger, h.Connect)) } diff --git a/platform-api/internal/middleware/auth.go b/platform-api/internal/middleware/auth.go index 58c055fe80..1c1cc5e3b8 100644 --- a/platform-api/internal/middleware/auth.go +++ b/platform-api/internal/middleware/auth.go @@ -19,16 +19,16 @@ package middleware import ( "context" - "encoding/json" "fmt" "log/slog" "net/http" "strings" "github.com/wso2/api-platform/common/authenticators" - "github.com/wso2/api-platform/platform-api/internal/utils" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" ) // contextKey is an unexported type for context keys in this package. @@ -85,12 +85,30 @@ type PlatformClaimNames struct { RoleScopeMap map[string][]string } -// writeJSONError is a helper to write a standard-shape JSON error body -// ({status, code, message}) without depending on httputil. -func writeJSONError(w http.ResponseWriter, status int, msg string) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - _ = json.NewEncoder(w).Encode(utils.NewErrorResponse(status, http.StatusText(status), msg)) +// writeAuthError writes the unified 401 response. The auth middleware runs +// ahead of routing so it can't return an error through the MapErrors chain; +// instead it logs the specific failure reason internally and serializes +// through the same apperror.WriteHTTP the mapper uses, so both the log shape +// and the wire format have a single owner. Every authentication failure — +// missing header, malformed header, invalid or expired token — produces the +// identical payload (apperror.Unauthorized) per the unified-auth rule in +// error-handling.md; reason is internal-only and must never contain a raw +// token. +func writeAuthError(w http.ResponseWriter, reason string) { + writeError(w, apperror.Unauthorized.New(), reason) +} + +// writeError logs a pre-routing failure (all 4xx — WARN, no stack, per the +// severity split in error_mapper.go) and serializes it through the shared +// apperror.WriteHTTP. reason is internal-only and must never contain a raw +// token. +func writeError(w http.ResponseWriter, appErr *apperror.Error, reason string) { + slog.Warn("request failed", + "trackingId", uuid.NewString(), + "code", appErr.Code, + "status", appErr.HTTPStatus, + "reason", reason) + apperror.WriteHTTP(w, appErr, "") } // LocalJWTAuthMiddleware returns a middleware for locally-issued JWT validation. @@ -107,20 +125,19 @@ func LocalJWTAuthMiddleware(config AuthConfig) func(http.Handler) http.Handler { authHeader := r.Header.Get("Authorization") if authHeader == "" { - writeJSONError(w, http.StatusUnauthorized, "Authorization header is required") + writeAuthError(w, "authorization header missing") return } tokenString := strings.TrimPrefix(authHeader, "Bearer ") if tokenString == authHeader { - writeJSONError(w, http.StatusUnauthorized, "Invalid authorization header format. Expected: Bearer ") + writeAuthError(w, "authorization header is not a Bearer token") return } enriched, err := validateLocalJWT(r, tokenString, config) if err != nil { - slog.Warn("local JWT validation failed", "reason", err.Error()) - writeJSONError(w, http.StatusUnauthorized, "Invalid or expired credentials.") + writeAuthError(w, "local JWT validation failed: "+err.Error()) return } @@ -538,18 +555,18 @@ func RequireOrganization(organizationParam string) func(http.Handler) http.Handl return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tokenOrg, exists := GetOrganizationFromRequest(r) if !exists { - writeJSONError(w, http.StatusForbidden, "No organization found in token") + writeError(w, apperror.Forbidden.New(), "no organization claim in token") return } requestedOrg := r.PathValue(organizationParam) if requestedOrg == "" { - writeJSONError(w, http.StatusBadRequest, "Organization parameter is required") + writeError(w, apperror.ValidationFailed.New("Organization parameter is required"), "missing organization path parameter") return } if tokenOrg != requestedOrg { - writeJSONError(w, http.StatusForbidden, "Access denied for the requested organization") + writeError(w, apperror.Forbidden.New(), "token organization does not match requested organization") return } diff --git a/platform-api/internal/middleware/authorization.go b/platform-api/internal/middleware/authorization.go index 28afc13c13..4e0e295e58 100644 --- a/platform-api/internal/middleware/authorization.go +++ b/platform-api/internal/middleware/authorization.go @@ -22,6 +22,7 @@ import ( "strings" "github.com/wso2/api-platform/common/authenticators" + "github.com/wso2/api-platform/platform-api/internal/apperror" ) const ( @@ -93,7 +94,7 @@ func ScopeEnforcer(registry *ScopeRegistry, cfg ScopeEnforcerConfig) func(http.H } } - writeJSONError(w, http.StatusForbidden, "insufficient permissions") + writeError(w, apperror.Forbidden.New(), "insufficient scopes for route") }) } } diff --git a/platform-api/internal/middleware/error_mapper.go b/platform-api/internal/middleware/error_mapper.go new file mode 100644 index 0000000000..5c547995ee --- /dev/null +++ b/platform-api/internal/middleware/error_mapper.go @@ -0,0 +1,105 @@ +/* + * 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 middleware + +import ( + "errors" + "log/slog" + "net/http" + "runtime/debug" + + "github.com/wso2/api-platform/platform-api/internal/apperror" + + "github.com/google/uuid" +) + +// ErrorHandlerFunc is the handler signature for routes that participate in +// centralized error mapping: instead of writing HTTP error responses inline, +// the handler returns an error (ideally an *apperror.Error) and MapErrors +// logs it and writes the standard utils.ErrorResponse. Success responses are +// still written directly by the handler — the mapper only owns the error path. +type ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request) error + +// MapErrors adapts an ErrorHandlerFunc to http.HandlerFunc for registration +// on the mux. It is the single catch point for handler errors: it recovers +// panics into a structured 500, maps *apperror.Error values onto the wire +// via apperror.WriteHTTP, and collapses any other error into a generic 500 +// COMMON_INTERNAL_ERROR so internal details never reach the client. +func MapErrors(slogger *slog.Logger, next ErrorHandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + defer func() { + if rec := recover(); rec != nil { + trackID := uuid.NewString() + slogger.Error("panic recovered", + "trackingId", trackID, + "panic", rec, + "path", r.URL.Path, + "method", r.Method, + "stack", string(debug.Stack())) + apperror.WriteHTTP(w, apperror.Internal.New(), trackID) + } + }() + + if err := next(w, r); err != nil { + writeMappedError(w, r, slogger, err) + } + } +} + +// writeMappedError logs the failure with its internal diagnostics (log +// message, cause, origin stack, tracking ID) and writes the sanitized +// 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. +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) + } + + logFields := []any{ + "trackingId", trackID, + "code", appErr.Code, + "status", appErr.HTTPStatus, + "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()) + } + + 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, "") +} diff --git a/platform-api/internal/middleware/error_mapper_test.go b/platform-api/internal/middleware/error_mapper_test.go new file mode 100644 index 0000000000..407d55886b --- /dev/null +++ b/platform-api/internal/middleware/error_mapper_test.go @@ -0,0 +1,195 @@ +/* + * 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 middleware + +import ( + "bytes" + "encoding/json" + "errors" + "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(buf *bytes.Buffer) *slog.Logger { + return slog.New(slog.NewJSONHandler(buf, nil)) +} + +func TestMapErrorsAppError(t *testing.T) { + var logBuf bytes.Buffer + h := MapErrors(testLogger(&logBuf), func(w http.ResponseWriter, r *http.Request) error { + return apperror.ProjectNotFound.New(). + WithLogMessage("project p1 missing in org o1") + }) + + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest("GET", "/api/v0.9/projects/p1", nil)) + + if rec.Code != http.StatusNotFound { + t.Fatalf("expected 404, got %d", rec.Code) + } + var body utils.ErrorResponse + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("invalid JSON body: %v", err) + } + if body.Status != "error" || body.Code != utils.CodeProjectNotFound || + body.Message != "The specified project could not be found." { + t.Errorf("unexpected body: %+v", body) + } + log := logBuf.String() + if !strings.Contains(log, "trackingId") { + t.Error("expected the mapper to log a trackingId") + } + if !strings.Contains(log, "project p1 missing in org o1") { + t.Error("expected the mapper to log the internal detail") + } + if strings.Contains(rec.Body.String(), "project p1 missing") { + t.Error("internal log message leaked into the client response") + } +} + +func TestMapErrorsSeveritySplit(t *testing.T) { + // 4xx: WARN, no stack, no trackingId in the body. + var warnBuf bytes.Buffer + h := MapErrors(testLogger(&warnBuf), func(w http.ResponseWriter, r *http.Request) error { + return apperror.ProjectNotFound.New() + }) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest("GET", "/x", nil)) + + log := warnBuf.String() + if !strings.Contains(log, `"level":"WARN"`) { + t.Errorf("expected 4xx to log at WARN, got: %s", log) + } + if strings.Contains(log, `"stack"`) { + t.Error("4xx must not log a stack trace") + } + var body utils.ErrorResponse + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("invalid JSON body: %v", err) + } + if body.TrackingID != "" { + t.Error("4xx response must not carry a trackingId") + } + + // 5xx: ERROR, stack logged, trackingId echoed in the body and matching the log. + var errBuf bytes.Buffer + h = MapErrors(testLogger(&errBuf), func(w http.ResponseWriter, r *http.Request) error { + return apperror.Internal.Wrap(errors.New("db down")) + }) + rec = httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest("GET", "/x", nil)) + + log = errBuf.String() + if !strings.Contains(log, `"level":"ERROR"`) { + t.Errorf("expected 5xx to log at ERROR, got: %s", log) + } + if !strings.Contains(log, `"stack"`) { + t.Error("5xx must log the origin stack trace") + } + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("invalid JSON body: %v", err) + } + if body.TrackingID == "" { + t.Error("5xx response must carry a trackingId for log correlation") + } + if !strings.Contains(log, body.TrackingID) { + t.Error("trackingId in the response must match the logged one") + } +} + +func TestMapErrorsPlainErrorFallsBackToGeneric500(t *testing.T) { + var logBuf bytes.Buffer + h := MapErrors(testLogger(&logBuf), func(w http.ResponseWriter, r *http.Request) error { + return errors.New("pq: connection reset by peer at 10.0.0.5:5432") + }) + + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest("GET", "/x", nil)) + + if rec.Code != http.StatusInternalServerError { + t.Fatalf("expected 500, got %d", rec.Code) + } + var body utils.ErrorResponse + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("invalid JSON body: %v", err) + } + if body.Code != utils.CodeCommonInternalError || body.Message != "An unexpected error occurred." { + t.Errorf("unexpected body: %+v", body) + } + if strings.Contains(rec.Body.String(), "10.0.0.5") { + t.Error("raw internal error leaked into the client response") + } + if !strings.Contains(logBuf.String(), "10.0.0.5") { + t.Error("expected the raw cause to be logged internally") + } +} + +func TestMapErrorsRecoversPanic(t *testing.T) { + var logBuf bytes.Buffer + h := MapErrors(testLogger(&logBuf), func(w http.ResponseWriter, r *http.Request) error { + panic("secret internal state: token=abc123") + }) + + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest("GET", "/x", nil)) + + if rec.Code != http.StatusInternalServerError { + t.Fatalf("expected 500, got %d", rec.Code) + } + var body utils.ErrorResponse + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("invalid JSON body: %v", err) + } + if body.Code != utils.CodeCommonInternalError { + t.Errorf("unexpected code %q", body.Code) + } + if strings.Contains(rec.Body.String(), "abc123") { + t.Error("panic value leaked into the client response") + } + log := logBuf.String() + if !strings.Contains(log, "panic recovered") || !strings.Contains(log, "trackingId") { + t.Error("expected a structured panic log with trackingId") + } +} + +func TestMapErrorsSuccessPathWritesNothingExtra(t *testing.T) { + var logBuf bytes.Buffer + h := MapErrors(testLogger(&logBuf), func(w http.ResponseWriter, r *http.Request) error { + w.WriteHeader(http.StatusNoContent) + return nil + }) + + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest("DELETE", "/x", nil)) + + if rec.Code != http.StatusNoContent { + t.Fatalf("expected 204, got %d", rec.Code) + } + if rec.Body.Len() != 0 { + t.Errorf("expected empty body, got %q", rec.Body.String()) + } + if strings.Contains(logBuf.String(), "request failed") { + t.Error("mapper logged a failure for a successful request") + } +} diff --git a/platform-api/internal/utils/codes.go b/platform-api/internal/utils/codes.go index 1fbe14e871..fd48ff6919 100644 --- a/platform-api/internal/utils/codes.go +++ b/platform-api/internal/utils/codes.go @@ -33,6 +33,7 @@ const ( CodeCommonUnprocessableEntity = "COMMON_UNPROCESSABLE_ENTITY" CodeCommonInternalError = "COMMON_INTERNAL_ERROR" CodeCommonServiceUnavailable = "COMMON_SERVICE_UNAVAILABLE" + CodeCommonTooManyRequests = "COMMON_TOO_MANY_REQUESTS" ) // commonCodeByStatus maps an HTTP status code to its fallback catalog code. @@ -49,6 +50,7 @@ var commonCodeByStatus = map[int]string{ http.StatusUnprocessableEntity: CodeCommonUnprocessableEntity, http.StatusInternalServerError: CodeCommonInternalError, http.StatusServiceUnavailable: CodeCommonServiceUnavailable, + http.StatusTooManyRequests: CodeCommonTooManyRequests, } // codeForStatus returns the fallback catalog code for an HTTP status, @@ -72,15 +74,23 @@ const ( CodeLLMProxyExists = "LLM_PROXY_EXISTS" CodeLLMProxyLimitReached = "LLM_PROXY_LIMIT_REACHED" CodeLLMProxyAPIKeyNotFound = "LLM_PROXY_API_KEY_NOT_FOUND" + CodeLLMProviderRefNotFound = "LLM_PROVIDER_REF_NOT_FOUND" CodeLLMProxyDeploymentValidationFailed = "LLM_PROXY_DEPLOYMENT_VALIDATION_FAILED" CodeLLMProviderAPIKeyForbidden = "LLM_PROVIDER_API_KEY_FORBIDDEN" CodeLLMProxyAPIKeyForbidden = "LLM_PROXY_API_KEY_FORBIDDEN" ) -// LLM provider template domain codes. +// LLM provider template domain codes. The *_VERSION_* and *_REF_* codes keep +// one HTTP status per code in the apperror catalog: NOT_FOUND codes are 404s +// for the resource targeted by the URL, REF_NOT_FOUND codes are 400s for a +// resource referenced in the request body, and VERSION codes distinguish a +// missing/duplicate version from a missing/duplicate template. const ( CodeLLMProviderTemplateNotFound = "LLM_PROVIDER_TEMPLATE_NOT_FOUND" + CodeLLMProviderTemplateVersionNotFound = "LLM_PROVIDER_TEMPLATE_VERSION_NOT_FOUND" + CodeLLMProviderTemplateRefNotFound = "LLM_PROVIDER_TEMPLATE_REF_NOT_FOUND" CodeLLMProviderTemplateExists = "LLM_PROVIDER_TEMPLATE_EXISTS" + CodeLLMProviderTemplateVersionExists = "LLM_PROVIDER_TEMPLATE_VERSION_EXISTS" CodeLLMProviderTemplateManagedByReserved = "LLM_PROVIDER_TEMPLATE_MANAGED_BY_RESERVED" CodeLLMProviderTemplateInUse = "LLM_PROVIDER_TEMPLATE_IN_USE" CodeLLMProviderTemplateReadOnly = "LLM_PROVIDER_TEMPLATE_READ_ONLY" @@ -130,10 +140,13 @@ const ( CodeOrganizationExists = "ORGANIZATION_EXISTS" ) -// Project domain codes. +// Project domain codes. PROJECT_REF_NOT_FOUND is the 400 counterpart of +// PROJECT_NOT_FOUND for a project referenced in a request body (e.g. the +// projectId in a create request) rather than targeted by the URL. const ( - CodeProjectNotFound = "PROJECT_NOT_FOUND" - CodeProjectExists = "PROJECT_EXISTS" + CodeProjectNotFound = "PROJECT_NOT_FOUND" + CodeProjectRefNotFound = "PROJECT_REF_NOT_FOUND" + CodeProjectExists = "PROJECT_EXISTS" ) // Application domain codes. diff --git a/platform-api/internal/utils/error.go b/platform-api/internal/utils/error.go index b716b818b6..ff95a0c566 100644 --- a/platform-api/internal/utils/error.go +++ b/platform-api/internal/utils/error.go @@ -40,6 +40,10 @@ type ErrorResponse struct { Message string `json:"message"` Errors []FieldError `json:"errors,omitempty"` Details any `json:"details,omitempty"` + // TrackingID correlates a 5xx response with its server-side log line (a + // bare UUID, no source markers). Set only for 5xx responses — 4xx + // responses already tell the client exactly what was wrong. + TrackingID string `json:"trackingId,omitempty"` } // FieldError describes a single field-level validation failure. diff --git a/platform-api/internal/webhook/receiver.go b/platform-api/internal/webhook/receiver.go index 057981c4f8..86eb76178f 100644 --- a/platform-api/internal/webhook/receiver.go +++ b/platform-api/internal/webhook/receiver.go @@ -20,6 +20,7 @@ package webhook import ( "context" "errors" + "fmt" "io" "log/slog" "net/http" @@ -29,9 +30,10 @@ 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/middleware" "github.com/wso2/api-platform/platform-api/internal/model" - "github.com/wso2/api-platform/platform-api/internal/utils" ) // RoutePath is the webhook endpoint under the resource API version prefix (constants.APIVersion, @@ -127,31 +129,29 @@ func NewReceiver( // RegisterRoutes registers the webhook endpoint on the mux. Only called when the webhook is enabled. func (r *Receiver) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("POST "+RoutePath, r.ReceiveEvent) + mux.HandleFunc("POST "+RoutePath, middleware.MapErrors(r.slogger, r.ReceiveEvent)) } // ReceiveEvent runs the full webhook flow: size-limited read -> signature verify -> envelope // decode/validate -> gateway_type filter -> idempotency -> dispatch -> mark processed. -func (r *Receiver) ReceiveEvent(w http.ResponseWriter, req *http.Request) { +func (r *Receiver) ReceiveEvent(w http.ResponseWriter, req *http.Request) error { if !r.cfg.Enabled { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(http.StatusNotFound, "Not Found", "Webhook endpoint is disabled")) - return + return apperror.NotFound.New().WithLogMessage("webhook endpoint is disabled") } // Read the raw body with a size cap. The raw bytes are needed verbatim for HMAC verification. limited := http.MaxBytesReader(w, req.Body, r.cfg.MaxBodySize) body, err := io.ReadAll(limited) if err != nil { - r.slogger.Warn("Webhook body read failed (possibly too large)", "limit", r.cfg.MaxBodySize, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(http.StatusBadRequest, "Bad Request", "Request body too large or unreadable")) - return + return apperror.ValidationFailed.Wrap(err, "Request body too large or unreadable"). + WithLogMessage(fmt.Sprintf("webhook body read failed (possibly too large), limit=%d", r.cfg.MaxBodySize)) } - // 1. Verify HMAC signature over the raw body. + // 1. Verify HMAC signature over the raw body. Per the unified auth-failure rule, this returns the + // same generic response as any other auth failure; the specific reason is internal-only. if err := r.verifier.Verify(req.Header.Get(r.cfg.SignatureHeader), body, time.Now()); err != nil { - r.slogger.Warn("Webhook signature verification failed", "clientIP", req.RemoteAddr, "error", err) - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(http.StatusUnauthorized, "Unauthorized", "Signature verification failed")) - return + return apperror.Unauthorized.Wrap(err). + WithLogMessage(fmt.Sprintf("webhook signature verification failed, clientIP=%s", req.RemoteAddr)) } // 2. Decode + validate the envelope. @@ -160,9 +160,7 @@ func (r *Receiver) ReceiveEvent(w http.ResponseWriter, req *http.Request) { err = env.Validate() } if err != nil { - r.slogger.Warn("Webhook envelope invalid", "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(http.StatusBadRequest, "Bad Request", "Invalid event envelope")) - return + return apperror.ValidationFailed.Wrap(err, "Invalid event envelope") } log := r.slogger.With("eventId", env.EventID, "eventType", env.EventType, "orgHandle", env.OrgID) @@ -172,10 +170,8 @@ func (r *Receiver) ReceiveEvent(w http.ResponseWriter, req *http.Request) { // the UUID. An unknown handle is a terminal 404 (not retryable) since the event references an // organization that does not exist in the control plane. if err := r.resolveOrgUUID(env); err != nil { - status := statusForError(err) - log.Warn("Webhook organization resolution failed", "status", status, "error", err) - httputil.WriteJSON(w, status, utils.NewErrorResponse(status, http.StatusText(status), "Unknown organization")) - return + log.Warn("Webhook organization resolution failed", "error", err) + return mapWebhookError(err) } log = log.With("orgId", env.OrgID) @@ -183,7 +179,7 @@ func (r *Receiver) ReceiveEvent(w http.ResponseWriter, req *http.Request) { if r.cfg.GatewayType != "" && env.GatewayType != "" && env.GatewayType != r.cfg.GatewayType { log.Info("Webhook event for a different gateway_type; accepting as no-op", "eventGatewayType", env.GatewayType) httputil.WriteJSON(w, http.StatusAccepted, map[string]string{"status": "ignored", "reason": "gateway_type mismatch"}) - return + return nil } // 5. Dispatch to the matching handler. Duplicate (at-least-once) deliveries are made safe by @@ -191,19 +187,17 @@ func (r *Receiver) ReceiveEvent(w http.ResponseWriter, req *http.Request) { handle, ok := r.handlers[env.EventType] if !ok { log.Warn("Unsupported webhook event type") - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(http.StatusBadRequest, "Bad Request", "Unsupported event type")) - return + return apperror.ValidationFailed.New("Unsupported event type") } if err := handle(req.Context(), env); err != nil { - status := statusForError(err) - log.Error("Webhook event handling failed", "status", status, "error", err) - httputil.WriteJSON(w, status, utils.NewErrorResponse(status, http.StatusText(status), "Failed to process event")) - return + log.Error("Webhook event handling failed", "error", err) + return mapWebhookError(err) } log.Info("Webhook event processed") httputil.WriteJSON(w, http.StatusOK, map[string]string{"status": "processed"}) + return nil } // resolveOrgUUID converts the organization handle carried in the envelope (org.ref_id, normalized @@ -222,20 +216,22 @@ func (r *Receiver) resolveOrgUUID(env *Envelope) error { return nil } -// statusForError maps domain errors returned by the reused services to HTTP status codes. -func statusForError(err error) int { +// 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. +func mapWebhookError(err error) *apperror.Error { switch { case errors.Is(err, ErrInvalidEnvelope), errors.Is(err, ErrUnsupportedEvent), errors.Is(err, ErrDecryptionFailed): - return http.StatusBadRequest - case errors.Is(err, constants.ErrAPINotFound), - errors.Is(err, constants.ErrOrganizationNotFound), - errors.Is(err, constants.ErrSubscriptionNotFound), - errors.Is(err, constants.ErrSubscriptionPlanNotFound), - errors.Is(err, constants.ErrSubscriptionPlanNotFoundOrInactive), - errors.Is(err, constants.ErrAPIKeyNotFound): - return http.StatusNotFound + 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 http.StatusInternalServerError + return apperror.Internal.Wrap(err).WithLogMessage("failed to process webhook event") } } diff --git a/platform-api/resources/openapi.yaml b/platform-api/resources/openapi.yaml index 01d4588cd5..4b2084e83f 100644 --- a/platform-api/resources/openapi.yaml +++ b/platform-api/resources/openapi.yaml @@ -6573,6 +6573,14 @@ components: (e.g. the resources referencing a secret that blocked its deletion). Shape varies by `code`; absent when not applicable. additionalProperties: true + trackingId: + type: string + 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 FieldError: title: Field-level validation error @@ -8446,7 +8454,8 @@ components: example: status: error code: COMMON_INTERNAL_ERROR - message: The server encountered an internal error. Please contact the administrator. + message: An unexpected error occurred. + trackingId: 4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11 ServiceUnavailable: description: Service Unavailable. The secrets management feature is not configured. content: From 8f134e0f0f8eed07a5235f656aef2b8f8cb2c4b6 Mon Sep 17 00:00:00 2001 From: Thushani Jayasekera Date: Tue, 7 Jul 2026 21:36:13 +0530 Subject: [PATCH 04/12] Refactor error messages in API handlers for clarity and consistency --- platform-api/internal/handler/api.go | 4 ++-- platform-api/internal/handler/llm.go | 4 ++-- platform-api/internal/handler/mcp.go | 14 +++++++------- platform-api/internal/handler/organization.go | 9 ++++----- platform-api/internal/handler/secret.go | 2 +- 5 files changed, 16 insertions(+), 17 deletions(-) diff --git a/platform-api/internal/handler/api.go b/platform-api/internal/handler/api.go index 55a3c12400..797e2b27df 100644 --- a/platform-api/internal/handler/api.go +++ b/platform-api/internal/handler/api.go @@ -126,7 +126,7 @@ func (h *APIHandler) CreateAPI(w http.ResponseWriter, r *http.Request) error { WithLogMessage(fmt.Sprintf("invalid transport protocol in org %s", orgId)) } if errors.Is(err, constants.ErrSubscriptionPlanNotFoundOrInactive) { - return apperror.ValidationFailed.Wrap(err, err.Error()). + return apperror.ValidationFailed.Wrap(err, "Subscription plan not found or not active"). WithLogMessage(fmt.Sprintf("subscription plan not found or not active in org %s", orgId)) } return apperror.Internal.Wrap(err). @@ -254,7 +254,7 @@ func (h *APIHandler) UpdateAPI(w http.ResponseWriter, r *http.Request) error { WithLogMessage(fmt.Sprintf("invalid transport protocol for API %s in org %s", apiId, orgId)) } if errors.Is(err, constants.ErrSubscriptionPlanNotFoundOrInactive) { - return apperror.ValidationFailed.Wrap(err, err.Error()). + return apperror.ValidationFailed.Wrap(err, "Subscription plan not found or not active"). WithLogMessage(fmt.Sprintf("subscription plan not found or not active for API %s in org %s", apiId, orgId)) } return apperror.Internal.Wrap(err). diff --git a/platform-api/internal/handler/llm.go b/platform-api/internal/handler/llm.go index 7ca735dca3..44eb4fa143 100644 --- a/platform-api/internal/handler/llm.go +++ b/platform-api/internal/handler/llm.go @@ -435,7 +435,7 @@ func (h *LLMHandler) CreateLLMProvider(w http.ResponseWriter, r *http.Request) e case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): return apperror.LLMProviderTemplateRefNotFound.Wrap(err) case errors.Is(err, constants.ErrSecretRefMissing): - return apperror.ValidationFailed.Wrap(err, err.Error()) + 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: @@ -537,7 +537,7 @@ func (h *LLMHandler) UpdateLLMProvider(w http.ResponseWriter, r *http.Request) e case errors.Is(err, constants.ErrLLMProviderTemplateNotFound): return apperror.LLMProviderTemplateRefNotFound.Wrap(err) case errors.Is(err, constants.ErrSecretRefMissing): - return apperror.ValidationFailed.Wrap(err, err.Error()) + 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.ErrInvalidInput): diff --git a/platform-api/internal/handler/mcp.go b/platform-api/internal/handler/mcp.go index 56b1f138ed..e509378f3c 100644 --- a/platform-api/internal/handler/mcp.go +++ b/platform-api/internal/handler/mcp.go @@ -225,10 +225,10 @@ func (h *MCPProxyHandler) FetchMCPProxyServerInfo(w http.ResponseWriter, r *http } switch { case errors.Is(err, constants.ErrInvalidURL): - return apperror.ValidationFailed.Wrap(err, err.Error()). + 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, strings.Split(err.Error(), ":")[0]). + 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."). @@ -249,15 +249,15 @@ func (h *MCPProxyHandler) FetchMCPProxyServerInfo(w http.ResponseWriter, r *http func (h *MCPProxyHandler) mapServiceError(err error) error { switch { case errors.Is(err, constants.ErrArtifactReadOnly): - return apperror.ArtifactReadOnly.Wrap(err, err.Error()) + 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, err.Error()) + return apperror.ArtifactRuntimeImmutable.Wrap(err, "Runtime configuration of this artifact cannot be changed") case errors.Is(err, constants.ErrArtifactDeployed): - return apperror.ArtifactDeployed.Wrap(err, err.Error()) + 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, err.Error()) + return apperror.ValidationFailed.Wrap(err, "Invalid input parameters") case errors.Is(err, constants.ErrMCPProxyNotFound): return apperror.MCPProxyNotFound.Wrap(err) case errors.Is(err, constants.ErrMCPProxyExists): @@ -267,7 +267,7 @@ func (h *MCPProxyHandler) mapServiceError(err error) error { case errors.Is(err, constants.ErrMCPProxyLimitReached): return apperror.MCPProxyLimitReached.Wrap(err) case errors.Is(err, constants.ErrSecretRefMissing): - return apperror.ValidationFailed.Wrap(err, err.Error()) + return apperror.ValidationFailed.Wrap(err, "One or more referenced secrets do not exist") default: return apperror.Internal.Wrap(err) } diff --git a/platform-api/internal/handler/organization.go b/platform-api/internal/handler/organization.go index 81cb7dad63..cc97593808 100644 --- a/platform-api/internal/handler/organization.go +++ b/platform-api/internal/handler/organization.go @@ -114,12 +114,11 @@ func (h *OrganizationHandler) HeadOrganization(w http.ResponseWriter, r *http.Re handle := r.PathValue("organizationId") h.slogger.Debug("Organization from token", "organizationId", organizationIdFromContext) - // to do: enable this check after finalizing authentication method - // if orgID != organizationIdFromContext { - // return apperror.Forbidden.New(). - // WithLogMessage("Organization ID in token does not match the requested organization ID") - // } + if handle != organizationIdFromContext { + return apperror.Forbidden.New(). + WithLogMessage("Organization ID in token does not match the requested organization ID") + } _, err := h.orgService.GetOrganizationByHandle(handle) if err != nil { diff --git a/platform-api/internal/handler/secret.go b/platform-api/internal/handler/secret.go index 65f3faa40c..0b4e6db3d9 100644 --- a/platform-api/internal/handler/secret.go +++ b/platform-api/internal/handler/secret.go @@ -85,7 +85,7 @@ func (h *SecretHandler) CreateSecret(w http.ResponseWriter, r *http.Request) err return apperror.SecretExists.Wrap(err) } if errors.Is(err, constants.ErrInvalidSecretType) { - return apperror.ValidationFailed.Wrap(err, err.Error()) + return apperror.ValidationFailed.Wrap(err, "Invalid secret type: must be GENERIC or CERTIFICATE") } return apperror.Internal.Wrap(err).WithLogMessage("failed to create secret") } From 33a7e4d8bc428270a233fc36442e167e11fe8a05 Mon Sep 17 00:00:00 2001 From: Thushani Jayasekera Date: Tue, 7 Jul 2026 21:45:00 +0530 Subject: [PATCH 05/12] Enhance error messages in artifact guard and gateway handlers for improved clarity and user feedback --- .../handler/artifact_guard_response.go | 6 +- .../internal/handler/gateway_internal.go | 650 +++++++++++------- 2 files changed, 410 insertions(+), 246 deletions(-) diff --git a/platform-api/internal/handler/artifact_guard_response.go b/platform-api/internal/handler/artifact_guard_response.go index 3bb31ac2b7..cc5e863846 100644 --- a/platform-api/internal/handler/artifact_guard_response.go +++ b/platform-api/internal/handler/artifact_guard_response.go @@ -36,11 +36,11 @@ import ( func mapArtifactGuardError(err error) error { switch { case errors.Is(err, constants.ErrArtifactReadOnly): - return apperror.ArtifactReadOnly.Wrap(err, err.Error()) + 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, err.Error()) + return apperror.ArtifactRuntimeImmutable.Wrap(err, "Runtime configuration of this artifact cannot be changed") case errors.Is(err, constants.ErrArtifactDeployed): - return apperror.ArtifactDeployed.Wrap(err, err.Error()) + 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_internal.go b/platform-api/internal/handler/gateway_internal.go index eb82e7bc8c..c0cbaa7a35 100644 --- a/platform-api/internal/handler/gateway_internal.go +++ b/platform-api/internal/handler/gateway_internal.go @@ -23,17 +23,15 @@ import ( "fmt" "log/slog" "net/http" + "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" "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/middleware" - "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/service" - "github.com/wso2/api-platform/platform-api/internal/utils" "github.com/wso2/go-httpkit/httputil" ) @@ -83,60 +81,72 @@ func (h *GatewayInternalAPIHandler) authenticateGateway(apiKey string) (*model.G return h.gatewayService.VerifyToken(apiKey) } -// authenticateRequest extracts the API key from headers and authenticates the gateway. Per the -// unified auth-failure rule, a missing/invalid key returns the identical generic 401; the -// specific reason travels internally via WithLogMessage only. -func (h *GatewayInternalAPIHandler) authenticateRequest(r *http.Request) (orgID, gatewayID string, err error) { +// authenticateRequest extracts the API key from headers and authenticates the gateway. +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") - gateway, authErr := h.authenticateGateway(apiKey) - if authErr != nil { - if errors.Is(authErr, constants.ErrMissingAPIKey) { - return "", "", apperror.Unauthorized.New(). - WithLogMessage(fmt.Sprintf("unauthorized access attempt - missing API key, clientIP=%s", clientIP)) - } - if errors.Is(authErr, constants.ErrInvalidAPIToken) { - return "", "", apperror.Unauthorized.Wrap(authErr). - WithLogMessage(fmt.Sprintf("authentication failed - invalid API key, clientIP=%s", clientIP)) + gateway, err := h.authenticateGateway(apiKey) + if err != nil { + if errors.Is(err, constants.ErrMissingAPIKey) { + h.slogger.Warn("Unauthorized access attempt - Missing API key", "clientIP", clientIP) + httputil.WriteJSON(w, http.StatusUnauthorized, utils.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, utils.NewErrorResponse(401, "Unauthorized", + "Invalid or expired API key")) + } else { + h.slogger.Error("Authentication failed", "clientIP", clientIP, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Error while validating API key")) } - return "", "", apperror.Internal.Wrap(authErr). - WithLogMessage(fmt.Sprintf("authentication failed, clientIP=%s", clientIP)) + return "", "", false } - return gateway.OrganizationID, gateway.ID, nil + return gateway.OrganizationID, gateway.ID, true } // GetAPI handles GET /api/internal/v1/apis/:apiId -func (h *GatewayInternalAPIHandler) GetAPI(w http.ResponseWriter, r *http.Request) error { - orgID, gatewayID, err := h.authenticateRequest(r) - if err != nil { - return err +func (h *GatewayInternalAPIHandler) GetAPI(w http.ResponseWriter, r *http.Request) { + orgID, gatewayID, ok := h.authenticateRequest(w, r) + if !ok { + return } apiID := r.PathValue("apiId") if apiID == "" { - return apperror.ValidationFailed.New("API ID is required") + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(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) { - return apperror.DeploymentNotFound.Wrap(err). - WithLogMessage(fmt.Sprintf("no active deployment found for API %s on gateway %s", apiID, gatewayID)) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "No active deployment found for this API on this gateway")) + return } if errors.Is(err, constants.ErrAPINotFound) { - return apperror.RESTAPINotFound.Wrap(err) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "API not found")) + return } - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get API %s", apiID)) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to get API")) + return } // Create ZIP file from API YAML file zipData, err := utils.CreateAPIYamlZip(api) if err != nil { - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to create ZIP file for API %s", apiID)) + h.slogger.Error("Failed to create ZIP file", "apiID", apiID, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to create API package")) + return } // Set headers for ZIP file download @@ -147,7 +157,6 @@ func (h *GatewayInternalAPIHandler) GetAPI(w http.ResponseWriter, r *http.Reques // Return ZIP file w.WriteHeader(http.StatusOK) _, _ = w.Write(zipData) - return nil } // ImportGatewayArtifacts handles POST /api/internal/v1/artifacts/import-gateway-artifacts. @@ -158,10 +167,10 @@ func (h *GatewayInternalAPIHandler) GetAPI(w http.ResponseWriter, r *http.Reques // continue-on-error: a failure on one artifact is recorded against its dpid and does not // abort the rest. The response maps each artifact's dpid to its result, with total/success/ // failed counts. Only a malformed request (bad multipart/zip) returns a non-200. -func (h *GatewayInternalAPIHandler) ImportGatewayArtifacts(w http.ResponseWriter, r *http.Request) error { - orgID, gatewayID, err := h.authenticateRequest(r) - if err != nil { - return err +func (h *GatewayInternalAPIHandler) ImportGatewayArtifacts(w http.ResponseWriter, r *http.Request) { + orgID, gatewayID, ok := h.authenticateRequest(w, r) + if !ok { + return } reqs, err := utils.ParseGatewayArtifactsRequest(r) @@ -170,8 +179,9 @@ func (h *GatewayInternalAPIHandler) ImportGatewayArtifacts(w http.ResponseWriter if i := strings.LastIndex(clientIP, ":"); i != -1 { clientIP = clientIP[:i] } - return apperror.ValidationFailed.Wrap(err, err.Error()). - WithLogMessage(fmt.Sprintf("invalid import-gateway-artifacts request, clientIP=%s", clientIP)) + h.slogger.Warn("Invalid import-gateway-artifacts request", "clientIP", clientIP, "error", err) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + return } // 'total' is advisory: log a mismatch but proceed with what the zip actually contained. if totalStr := r.FormValue("total"); totalStr != "" { @@ -185,37 +195,46 @@ func (h *GatewayInternalAPIHandler) ImportGatewayArtifacts(w http.ResponseWriter h.slogger.Info("Imported gateway artifacts batch", "gatewayID", gatewayID, "total", resp.Total, "success", resp.Success, "failed", resp.Failed) httputil.WriteJSON(w, http.StatusOK, resp) - return nil } // GetLLMProvider handles GET /api/internal/v1/llm-providers/:providerId -func (h *GatewayInternalAPIHandler) GetLLMProvider(w http.ResponseWriter, r *http.Request) error { - orgID, gatewayID, err := h.authenticateRequest(r) - if err != nil { - return err +func (h *GatewayInternalAPIHandler) GetLLMProvider(w http.ResponseWriter, r *http.Request) { + orgID, gatewayID, ok := h.authenticateRequest(w, r) + if !ok { + return } providerID := r.PathValue("providerId") if providerID == "" { - return apperror.ValidationFailed.New("Provider ID is required") + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(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) { - return apperror.DeploymentNotFound.Wrap(err). - WithLogMessage(fmt.Sprintf("no active deployment found for LLM provider %s on gateway %s", providerID, gatewayID)) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "No active deployment found for this LLM provider on this gateway")) + return } if errors.Is(err, constants.ErrLLMProviderNotFound) { - return apperror.LLMProviderNotFound.Wrap(err) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "LLM provider not found")) + return } - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get LLM provider %s", providerID)) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to get LLM provider")) + return } // Create ZIP file from LLM provider YAML file zipData, err := utils.CreateLLMProviderYamlZip(provider) if err != nil { - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to create ZIP file for LLM provider %s", providerID)) + h.slogger.Error("Failed to create ZIP file", "providerID", providerID, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to create LLM provider package")) + return } // Set headers for ZIP file download @@ -226,37 +245,46 @@ func (h *GatewayInternalAPIHandler) GetLLMProvider(w http.ResponseWriter, r *htt // Return ZIP file w.WriteHeader(http.StatusOK) _, _ = w.Write(zipData) - return nil } // GetLLMProxy handles GET /api/internal/v1/llm-proxies/:proxyId -func (h *GatewayInternalAPIHandler) GetLLMProxy(w http.ResponseWriter, r *http.Request) error { - orgID, gatewayID, err := h.authenticateRequest(r) - if err != nil { - return err +func (h *GatewayInternalAPIHandler) GetLLMProxy(w http.ResponseWriter, r *http.Request) { + orgID, gatewayID, ok := h.authenticateRequest(w, r) + if !ok { + return } proxyID := r.PathValue("proxyId") if proxyID == "" { - return apperror.ValidationFailed.New("Proxy ID is required") + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(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) { - return apperror.DeploymentNotFound.Wrap(err). - WithLogMessage(fmt.Sprintf("no active deployment found for LLM proxy %s on gateway %s", proxyID, gatewayID)) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "No active deployment found for this LLM proxy on this gateway")) + return } if errors.Is(err, constants.ErrLLMProxyNotFound) { - return apperror.LLMProxyNotFound.Wrap(err) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "LLM proxy not found")) + return } - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get LLM proxy %s", proxyID)) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to get LLM proxy")) + return } // Create ZIP file from LLM proxy YAML file zipData, err := utils.CreateLLMProxyYamlZip(proxy) if err != nil { - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to create ZIP file for LLM proxy %s", proxyID)) + h.slogger.Error("Failed to create ZIP file", "proxyID", proxyID, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to create LLM proxy package")) + return } // Set headers for ZIP file download @@ -267,25 +295,25 @@ func (h *GatewayInternalAPIHandler) GetLLMProxy(w http.ResponseWriter, r *http.R // Return ZIP file w.WriteHeader(http.StatusOK) _, _ = w.Write(zipData) - return nil } // GetGatewayDeployments handles GET /api/internal/v1/deployments // Returns the list of deployments that should be active on a gateway for startup sync -func (h *GatewayInternalAPIHandler) GetGatewayDeployments(w http.ResponseWriter, r *http.Request) error { - orgID, gatewayID, err := h.authenticateRequest(r) - if err != nil { - return err +func (h *GatewayInternalAPIHandler) GetGatewayDeployments(w http.ResponseWriter, r *http.Request) { + orgID, gatewayID, ok := h.authenticateRequest(w, r) + if !ok { + return } // Parse optional "since" query parameter for incremental sync var since *time.Time sinceStr := r.URL.Query().Get("since") if sinceStr != "" { - parsedTime, parseErr := time.Parse(time.RFC3339, sinceStr) - if parseErr != nil { - return apperror.ValidationFailed.Wrap(parseErr, - "Invalid 'since' parameter. Expected ISO 8601 format (e.g., 2026-03-04T10:00:00Z)") + parsedTime, err := time.Parse(time.RFC3339, sinceStr) + if err != nil { + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Invalid 'since' parameter. Expected ISO 8601 format (e.g., 2026-03-04T10:00:00Z)")) + return } since = &parsedTime } @@ -293,51 +321,69 @@ func (h *GatewayInternalAPIHandler) GetGatewayDeployments(w http.ResponseWriter, deployments, err := h.gatewayInternalService.GetDeploymentsByGateway(orgID, gatewayID, since) if err != nil { if errors.Is(err, constants.ErrGatewayNotFound) { - return apperror.GatewayNotFound.Wrap(err) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "Gateway not found")) + return } - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get gateway deployments for gateway %s", gatewayID)) + h.slogger.Error("Failed to get gateway deployments", "gatewayID", gatewayID, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to get deployments")) + return } httputil.WriteJSON(w, http.StatusOK, deployments) - return nil } // BatchFetchDeployments handles POST /api/internal/v1/deployments/fetch-batch // Fetches multiple deployment artifacts in a single request for gateway startup sync -func (h *GatewayInternalAPIHandler) BatchFetchDeployments(w http.ResponseWriter, r *http.Request) error { - orgID, gatewayID, err := h.authenticateRequest(r) - if err != nil { - return err +func (h *GatewayInternalAPIHandler) BatchFetchDeployments(w http.ResponseWriter, r *http.Request) { + orgID, gatewayID, ok := h.authenticateRequest(w, r) + if !ok { + return } // Enforce Accept header - only application/x-tar+gzip is supported if accept := r.Header.Get("Accept"); accept != "application/x-tar+gzip" { - return apperror.NotAcceptable.New() + httputil.WriteJSON(w, http.StatusNotAcceptable, utils.NewErrorResponse(406, "Not Acceptable", + "This endpoint only supports Accept: application/x-tar+gzip")) + return } // Parse request body var req dto.DeploymentsBatchFetchRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return apperror.ValidationFailed.Wrap(err, "Invalid request body: "+err.Error()) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Invalid request body: "+err.Error())) + return } if len(req.DeploymentIDs) == 0 { - return apperror.ValidationFailed.New("At least one deployment ID is required") + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "At least one deployment ID is required")) + return } // Fetch deployment content contentMap, err := h.gatewayInternalService.GetDeploymentContentBatch(orgID, gatewayID, req.DeploymentIDs) if err != nil { if errors.Is(err, constants.ErrGatewayNotFound) { - return apperror.GatewayNotFound.Wrap(err) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "Gateway not found")) + return } - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get deployment content batch for gateway %s", gatewayID)) + h.slogger.Error("Failed to get deployment content batch", "gatewayID", gatewayID, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to get deployment content")) + return } // Create TAR.GZ archive from deployment content tarGzData, err := utils.CreateBatchDeploymentTarGz(contentMap) if err != nil { - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to create batch TAR.GZ archive for gateway %s", gatewayID)) + h.slogger.Error("Failed to create batch TAR.GZ archive", "gatewayID", gatewayID, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to create deployment package")) + return } // Set headers for TAR.GZ download @@ -348,92 +394,148 @@ func (h *GatewayInternalAPIHandler) BatchFetchDeployments(w http.ResponseWriter, // Return TAR.GZ archive w.WriteHeader(http.StatusOK) _, _ = w.Write(tarGzData) - return nil } // GetSubscriptions handles GET /api/internal/v1/apis/:apiId/subscriptions -func (h *GatewayInternalAPIHandler) GetSubscriptions(w http.ResponseWriter, r *http.Request) error { - orgID, gatewayID, err := h.authenticateRequest(r) - if err != nil { - return err +func (h *GatewayInternalAPIHandler) GetSubscriptions(w http.ResponseWriter, r *http.Request) { + orgID, gatewayID, ok := h.authenticateRequest(w, r) + if !ok { + return } apiID := r.PathValue("apiId") if apiID == "" { - return apperror.ValidationFailed.New("API ID is required"). - WithLogMessage(fmt.Sprintf("API ID is required for subscriptions request, orgID=%s", orgID)) + clientIP := r.RemoteAddr + if i := strings.LastIndex(clientIP, ":"); i != -1 { + clientIP = clientIP[:i] + } + h.slogger.Error("API ID is required for subscriptions request", + "clientIP", clientIP, + "organizationId", orgID, + "apiId", apiID) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "API ID is required")) + return } if err := h.gatewayInternalService.IsAPIDeployedOnGateway(apiID, gatewayID, orgID); err != nil { if errors.Is(err, constants.ErrAPINotFound) { - return apperror.RESTAPINotFound.Wrap(err). - WithLogMessage(fmt.Sprintf("API not found when listing subscriptions, apiId=%s, orgID=%s, gatewayID=%s", apiID, orgID, gatewayID)) + h.slogger.Error("API not found when listing subscriptions", + "apiId", apiID, + "organizationId", orgID, + "gatewayId", gatewayID, + "error", err) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "API not found")) + return } if errors.Is(err, constants.ErrDeploymentNotActive) { - return apperror.Forbidden.New(). - WithLogMessage(fmt.Sprintf("subscription list denied - API has no active deployment status on gateway, apiId=%s, orgID=%s, gatewayID=%s", apiID, orgID, gatewayID)) + 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, utils.NewErrorResponse(403, "Forbidden", + "API is not associated with this gateway")) + return } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to verify API deployment for subscriptions, apiId=%s, orgID=%s, gatewayID=%s", apiID, orgID, gatewayID)) + h.slogger.Error("Failed to verify API deployment for subscriptions", + "apiId", apiID, + "organizationId", orgID, + "gatewayId", gatewayID, + "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(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) { - return apperror.RESTAPINotFound.Wrap(err). - WithLogMessage(fmt.Sprintf("API not found when listing subscriptions, apiId=%s, orgID=%s", apiID, orgID)) + h.slogger.Error("API not found when listing subscriptions", + "apiId", apiID, + "organizationId", orgID, + "error", err) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "API not found")) + return } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to list subscriptions for API, apiId=%s, orgID=%s", apiID, orgID)) + h.slogger.Error("Failed to list subscriptions for API", + "apiId", apiID, + "organizationId", orgID, + "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to get subscriptions")) + return } httputil.WriteJSON(w, http.StatusOK, subs) - return nil } // GetSubscriptionPlans handles GET /api/internal/v1/subscription-plans -func (h *GatewayInternalAPIHandler) GetSubscriptionPlans(w http.ResponseWriter, r *http.Request) error { - orgID, _, err := h.authenticateRequest(r) - if err != nil { - return err +func (h *GatewayInternalAPIHandler) GetSubscriptionPlans(w http.ResponseWriter, r *http.Request) { + orgID, _, ok := h.authenticateRequest(w, r) + if !ok { + return } plans, err := h.gatewayInternalService.ListSubscriptionPlansForOrg(orgID) if err != nil { - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to list subscription plans, orgID=%s", orgID)) + h.slogger.Error("Failed to list subscription plans", + "organizationId", orgID, + "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to get subscription plans")) + return } httputil.WriteJSON(w, http.StatusOK, plans) - return nil } // GetMCPProxy handles GET /api/internal/v1/mcp-proxies/:proxyId -func (h *GatewayInternalAPIHandler) GetMCPProxy(w http.ResponseWriter, r *http.Request) error { - orgID, gatewayID, err := h.authenticateRequest(r) - if err != nil { - return err +func (h *GatewayInternalAPIHandler) GetMCPProxy(w http.ResponseWriter, r *http.Request) { + + orgID, gatewayID, ok := h.authenticateRequest(w, r) + if !ok { + return } proxyID := r.PathValue("proxyId") if proxyID == "" { - return apperror.ValidationFailed.New("Proxy ID is required") + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Proxy ID is required")) + return } proxy, err := h.gatewayInternalService.GetActiveMCPProxyDeploymentByGateway(proxyID, orgID, gatewayID) if err != nil { + clientIP := r.RemoteAddr + if i := strings.LastIndex(clientIP, ":"); i != -1 { + clientIP = clientIP[:i] + } if errors.Is(err, constants.ErrDeploymentNotActive) { - return apperror.DeploymentNotFound.Wrap(err). - WithLogMessage(fmt.Sprintf("no active deployment found for MCP proxy %s on gateway %s", proxyID, gatewayID)) + 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, utils.NewErrorResponse(404, "Not Found", + "No active deployment found for this MCP proxy on this gateway")) + return } if errors.Is(err, constants.ErrMCPProxyNotFound) { - return apperror.MCPProxyNotFound.Wrap(err) + h.slogger.Error("MCP proxy not found", "clientIP", clientIP, "proxyID", proxyID, "orgID", orgID, "gatewayID", gatewayID, "error", err) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "MCP proxy not found")) + return } - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get MCP proxy %s", proxyID)) + h.slogger.Error("Failed to get MCP proxy", "clientIP", clientIP, "proxyID", proxyID, "orgID", orgID, "gatewayID", gatewayID, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to get MCP proxy")) + return } // Create ZIP file from MCP proxy YAML file zipData, err := utils.CreateMCPProxyYamlZip(proxy) if err != nil { - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to create ZIP file for MCP proxy %s", proxyID)) + h.slogger.Error("Failed to create ZIP file", "proxyID", proxyID, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to create MCP proxy package")) + return } // Set headers for ZIP file download @@ -444,37 +546,53 @@ func (h *GatewayInternalAPIHandler) GetMCPProxy(w http.ResponseWriter, r *http.R // Return ZIP file w.WriteHeader(http.StatusOK) _, _ = w.Write(zipData) - return nil } // GetWebSubAPI handles GET /api/internal/v1/websub-apis/:apiId -func (h *GatewayInternalAPIHandler) GetWebSubAPI(w http.ResponseWriter, r *http.Request) error { - orgID, gatewayID, err := h.authenticateRequest(r) - if err != nil { - return err +func (h *GatewayInternalAPIHandler) GetWebSubAPI(w http.ResponseWriter, r *http.Request) { + orgID, gatewayID, ok := h.authenticateRequest(w, r) + if !ok { + return } apiID := r.PathValue("apiId") if apiID == "" { - return apperror.ValidationFailed.New("API ID is required") + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "API ID is required")) + return } api, err := h.gatewayInternalService.GetActiveWebSubAPIDeploymentByGateway(apiID, orgID, gatewayID) if err != nil { + clientIP := r.RemoteAddr + if i := strings.LastIndex(clientIP, ":"); i != -1 { + clientIP = clientIP[:i] + } if errors.Is(err, constants.ErrDeploymentNotActive) { - return apperror.DeploymentNotFound.Wrap(err). - WithLogMessage(fmt.Sprintf("no active deployment found for WebSub API %s on gateway %s", apiID, gatewayID)) + 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, utils.NewErrorResponse(404, "Not Found", + "No active deployment found for this WebSub API on this gateway")) + return } if errors.Is(err, constants.ErrWebSubAPINotFound) { - return apperror.ArtifactNotFound.Wrap(err).WithLogMessage(fmt.Sprintf("WebSub API not found, apiID=%s", apiID)) + h.slogger.Error("WebSub API not found", "clientIP", clientIP, "apiID", apiID, "orgID", orgID, "gatewayID", gatewayID, "error", err) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "WebSub API not found")) + return } - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get WebSub API %s", apiID)) + h.slogger.Error("Failed to get WebSub API", "clientIP", clientIP, "apiID", apiID, "orgID", orgID, "gatewayID", gatewayID, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to get WebSub API")) + return } // Create ZIP file from WebSub API YAML file zipData, err := utils.CreateWebSubAPIYamlZip(api) if err != nil { - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to create ZIP file for WebSub API %s", apiID)) + h.slogger.Error("Failed to create ZIP file", "apiID", apiID, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to create WebSub API package")) + return } // Set headers for ZIP file download @@ -485,37 +603,53 @@ func (h *GatewayInternalAPIHandler) GetWebSubAPI(w http.ResponseWriter, r *http. // Return ZIP file w.WriteHeader(http.StatusOK) _, _ = w.Write(zipData) - return nil } // GetWebBrokerAPI handles GET /api/internal/v1/webbroker-apis/:apiId -func (h *GatewayInternalAPIHandler) GetWebBrokerAPI(w http.ResponseWriter, r *http.Request) error { - orgID, gatewayID, err := h.authenticateRequest(r) - if err != nil { - return err +func (h *GatewayInternalAPIHandler) GetWebBrokerAPI(w http.ResponseWriter, r *http.Request) { + orgID, gatewayID, ok := h.authenticateRequest(w, r) + if !ok { + return } apiID := r.PathValue("apiId") if apiID == "" { - return apperror.ValidationFailed.New("API ID is required") + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "API ID is required")) + return } api, err := h.gatewayInternalService.GetActiveWebBrokerAPIDeploymentByGateway(apiID, orgID, gatewayID) if err != nil { + clientIP := r.RemoteAddr + if i := strings.LastIndex(clientIP, ":"); i != -1 { + clientIP = clientIP[:i] + } if errors.Is(err, constants.ErrDeploymentNotActive) { - return apperror.DeploymentNotFound.Wrap(err). - WithLogMessage(fmt.Sprintf("no active deployment found for WebBroker API %s on gateway %s", apiID, gatewayID)) + 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, utils.NewErrorResponse(404, "Not Found", + "No active deployment found for this WebBroker API on this gateway")) + return } if errors.Is(err, constants.ErrWebBrokerAPINotFound) { - return apperror.ArtifactNotFound.Wrap(err).WithLogMessage(fmt.Sprintf("WebBroker API not found, apiID=%s", apiID)) + h.slogger.Error("WebBroker API not found", "clientIP", clientIP, "apiID", apiID, "orgID", orgID, "gatewayID", gatewayID, "error", err) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + "WebBroker API not found")) + return } - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get WebBroker API %s", apiID)) + h.slogger.Error("Failed to get WebBroker API", "clientIP", clientIP, "apiID", apiID, "orgID", orgID, "gatewayID", gatewayID, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to get WebBroker API")) + return } // Create ZIP file from WebBroker API YAML file zipData, err := utils.CreateWebBrokerAPIYamlZip(api) if err != nil { - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to create ZIP file for WebBroker API %s", apiID)) + h.slogger.Error("Failed to create ZIP file", "apiID", apiID, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to create WebBroker API package")) + return } // Set headers for ZIP file download @@ -526,15 +660,14 @@ func (h *GatewayInternalAPIHandler) GetWebBrokerAPI(w http.ResponseWriter, r *ht // Return ZIP file w.WriteHeader(http.StatusOK) _, _ = w.Write(zipData) - return nil } // ReceiveGatewayManifest handles POST /api/internal/v1/gateways/:gatewayId/manifest // Called by the gateway controller to post back its installed custom policy manifest. -func (h *GatewayInternalAPIHandler) ReceiveGatewayManifest(w http.ResponseWriter, r *http.Request) error { - orgID, gatewayID, err := h.authenticateRequest(r) - if err != nil { - return err +func (h *GatewayInternalAPIHandler) ReceiveGatewayManifest(w http.ResponseWriter, r *http.Request) { + orgID, gatewayID, ok := h.authenticateRequest(w, r) + if !ok { + return } var body struct { @@ -543,119 +676,137 @@ func (h *GatewayInternalAPIHandler) ReceiveGatewayManifest(w http.ResponseWriter Policies []service.GatewayPolicyInput `json:"policies"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - return apperror.ValidationFailed.Wrap(err, err.Error()) + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(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) { - return apperror.Conflict.Wrap(err).WithLogMessage(fmt.Sprintf("gateway manifest rejected: version mismatch, gatewayID=%s", gatewayID)) + h.slogger.Warn("Gateway manifest rejected: version mismatch", "gatewayID", gatewayID, "error", err) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", err.Error())) + return } if errors.Is(err, constants.ErrGatewayFunctionalityTypeMismatch) { - return apperror.Conflict.Wrap(err).WithLogMessage(fmt.Sprintf("gateway manifest rejected: functionality type mismatch, gatewayID=%s", gatewayID)) + h.slogger.Warn("Gateway manifest rejected: functionality type mismatch", "gatewayID", gatewayID, "error", err) + httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", err.Error())) + return } if errors.Is(err, constants.ErrGatewayNotFound) { - return apperror.GatewayNotFound.Wrap(err) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", err.Error())) + return } - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to store gateway manifest, gatewayID=%s", gatewayID)) + h.slogger.Error("Failed to store gateway manifest", "gatewayID", gatewayID, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to store gateway manifest")) + return } w.WriteHeader(http.StatusNoContent) - return nil } // GetRestAPIAPIKeys handles GET /api/internal/v1/apis/api-keys -func (h *GatewayInternalAPIHandler) GetRestAPIAPIKeys(w http.ResponseWriter, r *http.Request) error { - orgID, gatewayID, err := h.authenticateRequest(r) - if err != nil { - return err +func (h *GatewayInternalAPIHandler) GetRestAPIAPIKeys(w http.ResponseWriter, r *http.Request) { + orgID, gatewayID, ok := h.authenticateRequest(w, r) + if !ok { + return } issuer := r.URL.Query().Get("issuer") keys, err := h.gatewayInternalService.GetAPIKeysByKind(gatewayID, orgID, constants.RestApi, issuer) if err != nil { - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get API keys for REST APIs, gatewayID=%s", gatewayID)) + h.slogger.Error("Failed to get API keys for REST APIs", "gatewayID", gatewayID, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) + return } httputil.WriteJSON(w, http.StatusOK, keys) - return nil } // GetLLMProviderAPIKeys handles GET /api/internal/v1/llm-providers/api-keys -func (h *GatewayInternalAPIHandler) GetLLMProviderAPIKeys(w http.ResponseWriter, r *http.Request) error { - orgID, gatewayID, err := h.authenticateRequest(r) - if err != nil { - return err +func (h *GatewayInternalAPIHandler) GetLLMProviderAPIKeys(w http.ResponseWriter, r *http.Request) { + orgID, gatewayID, ok := h.authenticateRequest(w, r) + if !ok { + return } issuer := r.URL.Query().Get("issuer") keys, err := h.gatewayInternalService.GetAPIKeysByKind(gatewayID, orgID, constants.LLMProvider, issuer) if err != nil { - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get API keys for LLM providers, gatewayID=%s", gatewayID)) + h.slogger.Error("Failed to get API keys for LLM providers", "gatewayID", gatewayID, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) + return } httputil.WriteJSON(w, http.StatusOK, keys) - return nil } // GetLLMProxyAPIKeys handles GET /api/internal/v1/llm-proxies/api-keys -func (h *GatewayInternalAPIHandler) GetLLMProxyAPIKeys(w http.ResponseWriter, r *http.Request) error { - orgID, gatewayID, err := h.authenticateRequest(r) - if err != nil { - return err +func (h *GatewayInternalAPIHandler) GetLLMProxyAPIKeys(w http.ResponseWriter, r *http.Request) { + orgID, gatewayID, ok := h.authenticateRequest(w, r) + if !ok { + return } issuer := r.URL.Query().Get("issuer") keys, err := h.gatewayInternalService.GetAPIKeysByKind(gatewayID, orgID, constants.LLMProxy, issuer) if err != nil { - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get API keys for LLM proxies, gatewayID=%s", gatewayID)) + h.slogger.Error("Failed to get API keys for LLM proxies", "gatewayID", gatewayID, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) + return } httputil.WriteJSON(w, http.StatusOK, keys) - return nil } // GetWebSubAPIAPIKeys handles GET /api/internal/v1/websub-apis/api-keys -func (h *GatewayInternalAPIHandler) GetWebSubAPIAPIKeys(w http.ResponseWriter, r *http.Request) error { - orgID, gatewayID, err := h.authenticateRequest(r) - if err != nil { - return err +func (h *GatewayInternalAPIHandler) GetWebSubAPIAPIKeys(w http.ResponseWriter, r *http.Request) { + orgID, gatewayID, ok := h.authenticateRequest(w, r) + if !ok { + return } issuer := r.URL.Query().Get("issuer") keys, err := h.gatewayInternalService.GetAPIKeysByKind(gatewayID, orgID, constants.WebSubApi, issuer) if err != nil { - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get API keys for WebSub APIs, gatewayID=%s", gatewayID)) + h.slogger.Error("Failed to get API keys for WebSub APIs", "gatewayID", gatewayID, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) + return } httputil.WriteJSON(w, http.StatusOK, keys) - return nil } // GetWebBrokerAPIAPIKeys handles GET /api/internal/v1/webbroker-apis/api-keys -func (h *GatewayInternalAPIHandler) GetWebBrokerAPIAPIKeys(w http.ResponseWriter, r *http.Request) error { - orgID, gatewayID, err := h.authenticateRequest(r) - if err != nil { - return err +func (h *GatewayInternalAPIHandler) GetWebBrokerAPIAPIKeys(w http.ResponseWriter, r *http.Request) { + orgID, gatewayID, ok := h.authenticateRequest(w, r) + if !ok { + return } issuer := r.URL.Query().Get("issuer") keys, err := h.gatewayInternalService.GetAPIKeysByKind(gatewayID, orgID, constants.WebBrokerApi, issuer) if err != nil { - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to get API keys for WebBroker APIs, gatewayID=%s", gatewayID)) + h.slogger.Error("Failed to get API keys for WebBroker APIs", "gatewayID", gatewayID, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) + return } httputil.WriteJSON(w, http.StatusOK, keys) - return nil } // CheckArtifactsExist handles POST /api/internal/v1/artifacts/exists // Returns the subset of provided artifact UUIDs that still exist on the platform. // Used by the gateway during sync to avoid deleting artifacts that still exist // but have no active deployment (e.g., after deployment deletion). -func (h *GatewayInternalAPIHandler) CheckArtifactsExist(w http.ResponseWriter, r *http.Request) error { - orgID, _, err := h.authenticateRequest(r) - if err != nil { - return err +func (h *GatewayInternalAPIHandler) CheckArtifactsExist(w http.ResponseWriter, r *http.Request) { + orgID, _, ok := h.authenticateRequest(w, r) + if !ok { + return } var req dto.ArtifactsExistRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return apperror.ValidationFailed.Wrap(err, "Invalid request body: artifactIds is required and must be a non-empty array") + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Invalid request body: artifactIds is required and must be a non-empty array")) + return } existingIDs, err := h.gatewayInternalService.CheckArtifactsExist(orgID, req.ArtifactIDs) if err != nil { - return apperror.Internal.Wrap(err).WithLogMessage("failed to check artifact existence") + h.slogger.Error("Failed to check artifact existence", "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to check artifact existence")) + return } // Build a set of existing IDs for O(1) lookup @@ -677,44 +828,47 @@ func (h *GatewayInternalAPIHandler) CheckArtifactsExist(w http.ResponseWriter, r httputil.WriteJSON(w, http.StatusOK, dto.ArtifactsExistResponse{ Artifacts: artifacts, }) - return nil } // GetWebSubAPIHmacSecrets handles GET /api/internal/v1/websub-apis/:apiId/secrets // Returns decrypted plaintext HMAC secrets for the gateway-controller to load into its webhook secret store. -func (h *GatewayInternalAPIHandler) GetWebSubAPIHmacSecrets(w http.ResponseWriter, r *http.Request) error { - _, _, err := h.authenticateRequest(r) - if err != nil { - return err +func (h *GatewayInternalAPIHandler) GetWebSubAPIHmacSecrets(w http.ResponseWriter, r *http.Request) { + _, _, ok := h.authenticateRequest(w, r) + if !ok { + return } apiID := r.PathValue("apiId") if apiID == "" { - return apperror.ValidationFailed.New("API ID is required") + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API ID is required")) + return } if h.hmacSecretService == nil { - return apperror.ServiceUnavailable.New(). - WithLogMessage(fmt.Sprintf("HMAC secret service not configured, apiID=%s", apiID)) + h.slogger.Warn("HMAC secret service not configured", "apiID", apiID) + httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponse(503, "Service Unavailable", "HMAC secret management is not configured on this server")) + return } secrets, err := h.hmacSecretService.ListByArtifactUUID(apiID) if err != nil { - return apperror.Internal.Wrap(err).WithLogMessage(fmt.Sprintf("failed to list HMAC secrets for WebSub API %s", apiID)) + h.slogger.Error("Failed to list HMAC secrets for WebSub API", "apiID", apiID, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get HMAC secrets")) + return } items := make([]dto.GatewayHmacSecretInfo, 0, len(secrets)) for _, s := range secrets { plaintext, err := h.hmacSecretService.DecryptSecret(s) if err != nil { - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to decrypt HMAC secret, apiID=%s, secretName=%s", apiID, s.Handle)) + h.slogger.Error("Failed to decrypt HMAC secret", "apiID", apiID, "secretName", s.Handle, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to decrypt HMAC secret")) + return } items = append(items, dto.GatewayHmacSecretInfo{Name: s.Handle, Secret: plaintext}) } httputil.WriteJSON(w, http.StatusOK, dto.GatewayHmacSecretsResponse{ArtifactID: apiID, Secrets: items}) - return nil } // GetGatewaySecrets handles GET /api/internal/v1/secrets @@ -722,17 +876,19 @@ func (h *GatewayInternalAPIHandler) GetWebSubAPIHmacSecrets(w http.ResponseWrite // Supports ?updatedAfter= for incremental sync. // Supports ?includeValues=true for startup bulk fetch — decrypts all secrets server-side // and returns plaintext values in a single response, avoiding N per-secret round trips. -func (h *GatewayInternalAPIHandler) GetGatewaySecrets(w http.ResponseWriter, r *http.Request) error { - orgID, gatewayID, err := h.authenticateRequest(r) - if err != nil { - return err +func (h *GatewayInternalAPIHandler) GetGatewaySecrets(w http.ResponseWriter, r *http.Request) { + orgID, gatewayID, ok := h.authenticateRequest(w, r) + if !ok { + return } var updatedAfter *time.Time if s := r.URL.Query().Get("updatedAfter"); s != "" { - t, parseErr := time.Parse(time.RFC3339, s) - if parseErr != nil { - return apperror.ValidationFailed.Wrap(parseErr, "Invalid 'updatedAfter' parameter. Expected RFC3339 format.") + t, err := time.Parse(time.RFC3339, s) + if err != nil { + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + "Invalid 'updatedAfter' parameter. Expected RFC3339 format.")) + return } updatedAfter = &t } @@ -741,8 +897,10 @@ func (h *GatewayInternalAPIHandler) GetGatewaySecrets(w http.ResponseWriter, r * secrets, err := h.gatewayInternalService.GetSecretsByGateway(orgID, gatewayID, updatedAfter) if err != nil { - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to list gateway secrets, orgID=%s, gatewayID=%s", orgID, gatewayID)) + h.slogger.Error("Failed to list gateway secrets", "orgID", orgID, "gatewayID", gatewayID, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to retrieve secrets")) + return } items := make([]dto.SecretSyncItem, 0, len(secrets)) @@ -761,75 +919,81 @@ func (h *GatewayInternalAPIHandler) GetGatewaySecrets(w http.ResponseWriter, r * if includeValues && s.Status == model.SecretStatusActive { plaintext, err := h.secretService.DecryptCiphertext(s.Ciphertext) if err != nil { - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to decrypt secret for bulk fetch, orgID=%s, handle=%s", orgID, s.Handle)) + h.slogger.Error("Failed to decrypt secret for bulk fetch", "orgID", orgID, "handle", s.Handle, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to decrypt secret")) + return } item.Value = &plaintext } items = append(items, item) } httputil.WriteJSON(w, http.StatusOK, dto.SecretSyncListResponse{List: items, Count: len(items)}) - return nil } // GetGatewaySecretValue handles GET /api/internal/v1/secrets/{handle}/value // Returns the decrypted plaintext value of a secret. Called by the GW controller // only when the secret's hash has changed, minimising decryption calls. // Authenticated via gateway api-key — no JWT required. -func (h *GatewayInternalAPIHandler) GetGatewaySecretValue(w http.ResponseWriter, r *http.Request) error { - orgID, gatewayID, err := h.authenticateRequest(r) - if err != nil { - return err +func (h *GatewayInternalAPIHandler) GetGatewaySecretValue(w http.ResponseWriter, r *http.Request) { + orgID, gatewayID, ok := h.authenticateRequest(w, r) + if !ok { + return } handle := r.PathValue("handle") if handle == "" { - return apperror.ValidationFailed.New("Secret handle is required") + httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Secret handle is required")) + return } // Only serve secrets that are referenced by artifacts deployed on this gateway. deployed, err := h.gatewayInternalService.IsSecretDeployedOnGateway(orgID, gatewayID, handle) if err != nil { - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to check secret deployment scope, orgID=%s, gatewayID=%s, handle=%s", orgID, gatewayID, handle)) + h.slogger.Error("Failed to check secret deployment scope", "orgID", orgID, "gatewayID", gatewayID, "handle", handle, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to verify secret access")) + return } if !deployed { - return apperror.SecretNotFound.New() + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Secret not found")) + return } plaintext, err := h.secretService.Decrypt(orgID, handle) if err != nil { if errors.Is(err, constants.ErrSecretNotFound) { - return apperror.SecretNotFound.Wrap(err) + httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Secret not found")) + return } - return apperror.Internal.Wrap(err). - WithLogMessage(fmt.Sprintf("failed to decrypt secret for gateway, orgID=%s, handle=%s", orgID, handle)) + h.slogger.Error("Failed to decrypt secret for gateway", "orgID", orgID, "handle", handle, "error", err) + httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + "Failed to decrypt secret")) + return } httputil.WriteJSON(w, http.StatusOK, map[string]any{"value": plaintext}) - return nil } func (h *GatewayInternalAPIHandler) RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("GET /api/internal/v1/apis/api-keys", middleware.MapErrors(h.slogger, h.GetRestAPIAPIKeys)) - mux.HandleFunc("GET /api/internal/v1/apis/{apiId}", middleware.MapErrors(h.slogger, h.GetAPI)) - mux.HandleFunc("GET /api/internal/v1/apis/{apiId}/subscriptions", middleware.MapErrors(h.slogger, h.GetSubscriptions)) - mux.HandleFunc("GET /api/internal/v1/subscription-plans", middleware.MapErrors(h.slogger, h.GetSubscriptionPlans)) - mux.HandleFunc("GET /api/internal/v1/secrets", middleware.MapErrors(h.slogger, h.GetGatewaySecrets)) - mux.HandleFunc("GET /api/internal/v1/secrets/{handle}/value", middleware.MapErrors(h.slogger, h.GetGatewaySecretValue)) - mux.HandleFunc("GET /api/internal/v1/llm-providers/api-keys", middleware.MapErrors(h.slogger, h.GetLLMProviderAPIKeys)) - mux.HandleFunc("GET /api/internal/v1/llm-providers/{providerId}", middleware.MapErrors(h.slogger, h.GetLLMProvider)) - mux.HandleFunc("GET /api/internal/v1/llm-proxies/api-keys", middleware.MapErrors(h.slogger, h.GetLLMProxyAPIKeys)) - mux.HandleFunc("GET /api/internal/v1/llm-proxies/{proxyId}", middleware.MapErrors(h.slogger, h.GetLLMProxy)) - mux.HandleFunc("GET /api/internal/v1/deployments", middleware.MapErrors(h.slogger, h.GetGatewayDeployments)) - mux.HandleFunc("POST /api/internal/v1/deployments/fetch-batch", middleware.MapErrors(h.slogger, h.BatchFetchDeployments)) - mux.HandleFunc("GET /api/internal/v1/mcp-proxies/{proxyId}", middleware.MapErrors(h.slogger, h.GetMCPProxy)) - mux.HandleFunc("GET /api/internal/v1/websub-apis/api-keys", middleware.MapErrors(h.slogger, h.GetWebSubAPIAPIKeys)) - mux.HandleFunc("GET /api/internal/v1/websub-apis/{apiId}", middleware.MapErrors(h.slogger, h.GetWebSubAPI)) - mux.HandleFunc("GET /api/internal/v1/websub-apis/{apiId}/secrets", middleware.MapErrors(h.slogger, h.GetWebSubAPIHmacSecrets)) - mux.HandleFunc("GET /api/internal/v1/webbroker-apis/api-keys", middleware.MapErrors(h.slogger, h.GetWebBrokerAPIAPIKeys)) - mux.HandleFunc("GET /api/internal/v1/webbroker-apis/{apiId}", middleware.MapErrors(h.slogger, h.GetWebBrokerAPI)) - mux.HandleFunc("POST /api/internal/v1/gateways/{gatewayId}/manifest", middleware.MapErrors(h.slogger, h.ReceiveGatewayManifest)) - mux.HandleFunc("POST /api/internal/v1/artifacts/exists", middleware.MapErrors(h.slogger, h.CheckArtifactsExist)) - mux.HandleFunc("POST /api/internal/v1/artifacts/import-gateway-artifacts", middleware.MapErrors(h.slogger, h.ImportGatewayArtifacts)) + mux.HandleFunc("GET /api/internal/v1/apis/api-keys", h.GetRestAPIAPIKeys) + mux.HandleFunc("GET /api/internal/v1/apis/{apiId}", h.GetAPI) + mux.HandleFunc("GET /api/internal/v1/apis/{apiId}/subscriptions", h.GetSubscriptions) + mux.HandleFunc("GET /api/internal/v1/subscription-plans", h.GetSubscriptionPlans) + mux.HandleFunc("GET /api/internal/v1/secrets", h.GetGatewaySecrets) + mux.HandleFunc("GET /api/internal/v1/secrets/{handle}/value", h.GetGatewaySecretValue) + mux.HandleFunc("GET /api/internal/v1/llm-providers/api-keys", h.GetLLMProviderAPIKeys) + mux.HandleFunc("GET /api/internal/v1/llm-providers/{providerId}", h.GetLLMProvider) + mux.HandleFunc("GET /api/internal/v1/llm-proxies/api-keys", h.GetLLMProxyAPIKeys) + mux.HandleFunc("GET /api/internal/v1/llm-proxies/{proxyId}", h.GetLLMProxy) + mux.HandleFunc("GET /api/internal/v1/deployments", h.GetGatewayDeployments) + mux.HandleFunc("POST /api/internal/v1/deployments/fetch-batch", h.BatchFetchDeployments) + mux.HandleFunc("GET /api/internal/v1/mcp-proxies/{proxyId}", h.GetMCPProxy) + mux.HandleFunc("GET /api/internal/v1/websub-apis/api-keys", h.GetWebSubAPIAPIKeys) + mux.HandleFunc("GET /api/internal/v1/websub-apis/{apiId}", h.GetWebSubAPI) + mux.HandleFunc("GET /api/internal/v1/websub-apis/{apiId}/secrets", h.GetWebSubAPIHmacSecrets) + mux.HandleFunc("GET /api/internal/v1/webbroker-apis/api-keys", h.GetWebBrokerAPIAPIKeys) + mux.HandleFunc("GET /api/internal/v1/webbroker-apis/{apiId}", h.GetWebBrokerAPI) + mux.HandleFunc("POST /api/internal/v1/gateways/{gatewayId}/manifest", h.ReceiveGatewayManifest) + mux.HandleFunc("POST /api/internal/v1/artifacts/exists", h.CheckArtifactsExist) + mux.HandleFunc("POST /api/internal/v1/artifacts/import-gateway-artifacts", h.ImportGatewayArtifacts) } From e887a34697b8396803832a7c612bc1e18dc061a1 Mon Sep 17 00:00:00 2001 From: Thushani Jayasekera Date: Tue, 7 Jul 2026 22:28:51 +0530 Subject: [PATCH 06/12] Add setLocation utility to various handlers for consistent Location header management This commit introduces the setLocation function across multiple API handlers to standardize the setting of the Location response header for newly created resources. Additionally, it includes unit tests for the setLocation function and the strOrEmpty utility to ensure proper functionality and coverage. --- platform-api/internal/handler/api.go | 1 + .../internal/handler/api_deployment.go | 1 + platform-api/internal/handler/api_key.go | 1 + platform-api/internal/handler/application.go | 1 + platform-api/internal/handler/gateway.go | 6 ++ platform-api/internal/handler/llm.go | 4 ++ platform-api/internal/handler/llm_apikey.go | 1 + .../internal/handler/llm_deployment.go | 2 + .../internal/handler/llm_proxy_apikey.go | 1 + platform-api/internal/handler/location.go | 33 +++++++++ .../internal/handler/location_test.go | 56 +++++++++++++++ platform-api/internal/handler/mcp.go | 1 + .../internal/handler/mcp_deployment.go | 1 + platform-api/internal/handler/organization.go | 1 + platform-api/internal/handler/project.go | 1 + platform-api/internal/handler/secret.go | 1 + .../internal/handler/subscription_handler.go | 1 + .../handler/subscription_plan_handler.go | 1 + platform-api/resources/openapi.yaml | 69 +++++++++++++++++++ 19 files changed, 183 insertions(+) create mode 100644 platform-api/internal/handler/location.go create mode 100644 platform-api/internal/handler/location_test.go diff --git a/platform-api/internal/handler/api.go b/platform-api/internal/handler/api.go index 797e2b27df..231e174c88 100644 --- a/platform-api/internal/handler/api.go +++ b/platform-api/internal/handler/api.go @@ -133,6 +133,7 @@ func (h *APIHandler) CreateAPI(w http.ResponseWriter, r *http.Request) error { WithLogMessage(fmt.Sprintf("failed to create API in org %s", orgId)) } + setLocation(w, "rest-apis", strOrEmpty(apiResponse.Id)) httputil.WriteJSON(w, http.StatusCreated, apiResponse) return nil } diff --git a/platform-api/internal/handler/api_deployment.go b/platform-api/internal/handler/api_deployment.go index 682c65a445..056b448325 100644 --- a/platform-api/internal/handler/api_deployment.go +++ b/platform-api/internal/handler/api_deployment.go @@ -112,6 +112,7 @@ func (h *DeploymentHandler) DeployAPI(w http.ResponseWriter, r *http.Request) er WithLogMessage(fmt.Sprintf("failed to deploy API %s", apiId)) } + setLocation(w, "rest-apis", apiId, "deployments", deployment.DeploymentId.String()) httputil.WriteJSON(w, http.StatusCreated, deployment) return nil } diff --git a/platform-api/internal/handler/api_key.go b/platform-api/internal/handler/api_key.go index 3cd0b7fbe4..a014789df3 100644 --- a/platform-api/internal/handler/api_key.go +++ b/platform-api/internal/handler/api_key.go @@ -116,6 +116,7 @@ func (h *APIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Request) err h.slogger.Info("Successfully created API key", "userId", userId, "apiHandle", apiHandle, "orgId", orgId, "keyName", keyName) // Return success response + setLocation(w, "rest-apis", apiHandle, "api-keys", name) httputil.WriteJSON(w, http.StatusCreated, api.CreateAPIKeyResponse{ Status: api.CreateAPIKeyResponseStatusSuccess, KeyId: req.Id, diff --git a/platform-api/internal/handler/application.go b/platform-api/internal/handler/application.go index a710a6171e..23b5494efd 100644 --- a/platform-api/internal/handler/application.go +++ b/platform-api/internal/handler/application.go @@ -77,6 +77,7 @@ func (h *ApplicationHandler) CreateApplication(w http.ResponseWriter, r *http.Re WithLogMessage(fmt.Sprintf("failed to create application in project %s for org %s by user %s", req.ProjectId, orgID, createdBy)) } + setLocation(w, "applications", app.Id) httputil.WriteJSON(w, http.StatusCreated, app) return nil } diff --git a/platform-api/internal/handler/gateway.go b/platform-api/internal/handler/gateway.go index 76e8c7ed0c..bcf86ab3b2 100644 --- a/platform-api/internal/handler/gateway.go +++ b/platform-api/internal/handler/gateway.go @@ -134,6 +134,7 @@ func (h *GatewayHandler) CreateGateway(w http.ResponseWriter, r *http.Request) e } // Return 201 Created with response + setLocation(w, "gateways", strOrEmpty(gateway.Id)) httputil.WriteJSON(w, http.StatusCreated, gateway) return nil } @@ -357,6 +358,11 @@ func (h *GatewayHandler) RotateToken(w http.ResponseWriter, r *http.Request) err } // Return 201 Created with response + tokenId := "" + if response.Id != nil { + tokenId = response.Id.String() + } + setLocation(w, "gateways", gatewayId, "tokens", tokenId) httputil.WriteJSON(w, http.StatusCreated, response) return nil } diff --git a/platform-api/internal/handler/llm.go b/platform-api/internal/handler/llm.go index a7dd7ba19b..bf717e4b35 100644 --- a/platform-api/internal/handler/llm.go +++ b/platform-api/internal/handler/llm.go @@ -140,6 +140,7 @@ func (h *LLMHandler) CreateLLMProviderTemplate(w http.ResponseWriter, r *http.Re } } + setLocation(w, "llm-provider-templates", strOrEmpty(created.Id)) httputil.WriteJSON(w, http.StatusCreated, created) return nil } @@ -195,6 +196,7 @@ func (h *LLMHandler) CopyLLMProviderTemplateVersion(w http.ResponseWriter, r *ht } } + setLocation(w, "llm-provider-templates", strOrEmpty(created.Id)) httputil.WriteJSON(w, http.StatusCreated, created) return nil } @@ -448,6 +450,7 @@ func (h *LLMHandler) CreateLLMProvider(w http.ResponseWriter, r *http.Request) e WithLogMessage(fmt.Sprintf("failed to create LLM provider in org %s", orgID)) } } + setLocation(w, "llm-providers", strOrEmpty(created.Id)) httputil.WriteJSON(w, http.StatusCreated, created) return nil } @@ -629,6 +632,7 @@ func (h *LLMHandler) CreateLLMProxy(w http.ResponseWriter, r *http.Request) erro WithLogMessage(fmt.Sprintf("failed to create LLM proxy in org %s", orgID)) } } + setLocation(w, "llm-proxies", strOrEmpty(created.Id)) httputil.WriteJSON(w, http.StatusCreated, created) return nil } diff --git a/platform-api/internal/handler/llm_apikey.go b/platform-api/internal/handler/llm_apikey.go index 72e418c4fc..18b54f7c58 100644 --- a/platform-api/internal/handler/llm_apikey.go +++ b/platform-api/internal/handler/llm_apikey.go @@ -168,6 +168,7 @@ func (h *LLMProviderAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.R h.slogger.Info("Successfully created LLM provider API key", "providerId", providerID, "organizationId", orgID, "keyId", response.Id) + setLocation(w, "llm-providers", providerID, "api-keys", response.Id) httputil.WriteJSON(w, http.StatusCreated, response) return nil } diff --git a/platform-api/internal/handler/llm_deployment.go b/platform-api/internal/handler/llm_deployment.go index 44af607d09..c062684c9b 100644 --- a/platform-api/internal/handler/llm_deployment.go +++ b/platform-api/internal/handler/llm_deployment.go @@ -113,6 +113,7 @@ func (h *LLMProviderDeploymentHandler) DeployLLMProvider(w http.ResponseWriter, } } + setLocation(w, "llm-providers", providerId, "deployments", deployment.DeploymentId.String()) httputil.WriteJSON(w, http.StatusCreated, deployment) return nil } @@ -378,6 +379,7 @@ func (h *LLMProxyDeploymentHandler) DeployLLMProxy(w http.ResponseWriter, r *htt } } + setLocation(w, "llm-proxies", proxyId, "deployments", deployment.DeploymentId.String()) httputil.WriteJSON(w, http.StatusCreated, deployment) return nil } diff --git a/platform-api/internal/handler/llm_proxy_apikey.go b/platform-api/internal/handler/llm_proxy_apikey.go index 640808a064..8eda38dd2a 100644 --- a/platform-api/internal/handler/llm_proxy_apikey.go +++ b/platform-api/internal/handler/llm_proxy_apikey.go @@ -168,6 +168,7 @@ func (h *LLMProxyAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Requ h.slogger.Info("Successfully created LLM proxy API key", "proxyId", proxyID, "organizationId", orgID, "keyId", response.Id) + setLocation(w, "llm-proxies", proxyID, "api-keys", response.Id) httputil.WriteJSON(w, http.StatusCreated, response) return nil } diff --git a/platform-api/internal/handler/location.go b/platform-api/internal/handler/location.go new file mode 100644 index 0000000000..2af102a817 --- /dev/null +++ b/platform-api/internal/handler/location.go @@ -0,0 +1,33 @@ +package handler + +import ( + "net/http" + "net/url" + "strings" + + "github.com/wso2/api-platform/platform-api/internal/constants" +) + +// setLocation sets the Location response header to the path-absolute URL of a +// newly created resource under the API base path. It is a no-op when any +// segment is empty so a missing identifier never yields a malformed URL. +func setLocation(w http.ResponseWriter, segments ...string) { + var b strings.Builder + b.WriteString(constants.APIBasePath) + for _, s := range segments { + if s == "" { + return + } + b.WriteString("/") + b.WriteString(url.PathEscape(s)) + } + w.Header().Set("Location", b.String()) +} + +// strOrEmpty returns the dereferenced string, or "" for nil. +func strOrEmpty(s *string) string { + if s == nil { + return "" + } + return *s +} diff --git a/platform-api/internal/handler/location_test.go b/platform-api/internal/handler/location_test.go new file mode 100644 index 0000000000..75aee5875f --- /dev/null +++ b/platform-api/internal/handler/location_test.go @@ -0,0 +1,56 @@ +package handler + +import ( + "net/http/httptest" + "testing" + + "github.com/wso2/api-platform/platform-api/internal/constants" +) + +func TestSetLocation(t *testing.T) { + tests := []struct { + name string + segments []string + want string + }{ + { + name: "single resource", + segments: []string{"rest-apis", "my-api"}, + want: constants.APIBasePath + "/rest-apis/my-api", + }, + { + name: "nested resource", + segments: []string{"gateways", "gw-1", "tokens", "550e8400-e29b-41d4-a716-446655440000"}, + want: constants.APIBasePath + "/gateways/gw-1/tokens/550e8400-e29b-41d4-a716-446655440000", + }, + { + name: "segment needing escaping", + segments: []string{"projects", "a b/c"}, + want: constants.APIBasePath + "/projects/a%20b%2Fc", + }, + { + name: "empty segment suppresses header", + segments: []string{"projects", ""}, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := httptest.NewRecorder() + setLocation(w, tt.segments...) + if got := w.Header().Get("Location"); got != tt.want { + t.Errorf("Location = %q, want %q", got, tt.want) + } + }) + } +} + +func TestStrOrEmpty(t *testing.T) { + if got := strOrEmpty(nil); got != "" { + t.Errorf("strOrEmpty(nil) = %q, want empty", got) + } + s := "id-1" + if got := strOrEmpty(&s); got != "id-1" { + t.Errorf("strOrEmpty(&s) = %q, want %q", got, s) + } +} diff --git a/platform-api/internal/handler/mcp.go b/platform-api/internal/handler/mcp.go index dcd9aa6871..7edd16e74e 100644 --- a/platform-api/internal/handler/mcp.go +++ b/platform-api/internal/handler/mcp.go @@ -87,6 +87,7 @@ func (h *MCPProxyHandler) CreateMCPProxy(w http.ResponseWriter, r *http.Request) return h.mapServiceError(err) } + setLocation(w, "mcp-proxies", strOrEmpty(resp.Id)) httputil.WriteJSON(w, http.StatusCreated, resp) return nil } diff --git a/platform-api/internal/handler/mcp_deployment.go b/platform-api/internal/handler/mcp_deployment.go index 119baec109..b44e2d80d2 100644 --- a/platform-api/internal/handler/mcp_deployment.go +++ b/platform-api/internal/handler/mcp_deployment.go @@ -121,6 +121,7 @@ func (h *MCPProxyDeploymentHandler) DeployMCPProxy(w http.ResponseWriter, r *htt } } + setLocation(w, "mcp-proxies", proxyId, "deployments", deployment.DeploymentId.String()) httputil.WriteJSON(w, http.StatusCreated, deployment) return nil } diff --git a/platform-api/internal/handler/organization.go b/platform-api/internal/handler/organization.go index cc97593808..20214b4a1a 100644 --- a/platform-api/internal/handler/organization.go +++ b/platform-api/internal/handler/organization.go @@ -100,6 +100,7 @@ func (h *OrganizationHandler) RegisterOrganization(w http.ResponseWriter, r *htt WithLogMessage("failed to create organization") } + setLocation(w, "organizations", strOrEmpty(org.Id)) httputil.WriteJSON(w, http.StatusCreated, org) return nil } diff --git a/platform-api/internal/handler/project.go b/platform-api/internal/handler/project.go index 04f881bc5c..3878796d3e 100644 --- a/platform-api/internal/handler/project.go +++ b/platform-api/internal/handler/project.go @@ -84,6 +84,7 @@ func (h *ProjectHandler) CreateProject(w http.ResponseWriter, r *http.Request) e WithLogMessage(fmt.Sprintf("failed to create project in org %s", organizationID)) } + setLocation(w, "projects", strOrEmpty(project.Id)) httputil.WriteJSON(w, http.StatusCreated, project) return nil } diff --git a/platform-api/internal/handler/secret.go b/platform-api/internal/handler/secret.go index 0b4e6db3d9..4dc052a0b3 100644 --- a/platform-api/internal/handler/secret.go +++ b/platform-api/internal/handler/secret.go @@ -90,6 +90,7 @@ func (h *SecretHandler) CreateSecret(w http.ResponseWriter, r *http.Request) err return apperror.Internal.Wrap(err).WithLogMessage("failed to create secret") } + setLocation(w, "secrets", resp.Handle) httputil.WriteJSON(w, http.StatusCreated, resp) return nil } diff --git a/platform-api/internal/handler/subscription_handler.go b/platform-api/internal/handler/subscription_handler.go index b584a970a2..27948e3bd2 100644 --- a/platform-api/internal/handler/subscription_handler.go +++ b/platform-api/internal/handler/subscription_handler.go @@ -116,6 +116,7 @@ func (h *SubscriptionHandler) CreateSubscription(w http.ResponseWriter, r *http. return apperror.Internal.Wrap(err). WithLogMessage(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) return nil } diff --git a/platform-api/internal/handler/subscription_plan_handler.go b/platform-api/internal/handler/subscription_plan_handler.go index 76163bfb9f..3986217467 100644 --- a/platform-api/internal/handler/subscription_plan_handler.go +++ b/platform-api/internal/handler/subscription_plan_handler.go @@ -219,6 +219,7 @@ func (h *SubscriptionPlanHandler) CreateSubscriptionPlan(w http.ResponseWriter, return apperror.Internal.Wrap(err). WithLogMessage(fmt.Sprintf("failed to resolve subscription plan identity for org %s", orgId)) } + setLocation(w, "subscription-plans", created.Handle) httputil.WriteJSON(w, http.StatusCreated, resp) return nil } diff --git a/platform-api/resources/openapi.yaml b/platform-api/resources/openapi.yaml index 4b2084e83f..b477e6778d 100644 --- a/platform-api/resources/openapi.yaml +++ b/platform-api/resources/openapi.yaml @@ -62,6 +62,9 @@ paths: responses: '201': description: Organization registered successfully + headers: + Location: + $ref: '#/components/headers/Location' content: application/json: schema: @@ -202,6 +205,9 @@ paths: responses: '201': description: Project created successfully + headers: + Location: + $ref: '#/components/headers/Location' content: application/json: schema: @@ -396,6 +402,9 @@ paths: responses: '201': description: API created successfully + headers: + Location: + $ref: '#/components/headers/Location' content: application/json: schema: @@ -622,6 +631,9 @@ paths: responses: '201': description: API key created successfully + headers: + Location: + $ref: '#/components/headers/Location' content: application/json: schema: @@ -760,6 +772,9 @@ paths: responses: '201': description: API deployed successfully + headers: + Location: + $ref: '#/components/headers/Location' content: application/json: schema: @@ -1011,6 +1026,9 @@ paths: responses: '201': description: LLM provider template created successfully + headers: + Location: + $ref: '#/components/headers/Location' content: application/json: schema: @@ -1159,6 +1177,9 @@ paths: responses: '201': description: New version created successfully + headers: + Location: + $ref: '#/components/headers/Location' content: application/json: schema: @@ -1416,6 +1437,9 @@ paths: responses: '201': description: LLM provider created successfully + headers: + Location: + $ref: '#/components/headers/Location' content: application/json: schema: @@ -1607,6 +1631,9 @@ paths: responses: '201': description: LLM provider deployed successfully + headers: + Location: + $ref: '#/components/headers/Location' content: application/json: schema: @@ -1936,6 +1963,9 @@ paths: responses: '201': description: API key created successfully + headers: + Location: + $ref: '#/components/headers/Location' content: application/json: schema: @@ -2046,6 +2076,9 @@ paths: responses: '201': description: LLM proxy created successfully + headers: + Location: + $ref: '#/components/headers/Location' content: application/json: schema: @@ -2240,6 +2273,9 @@ paths: responses: '201': description: LLM proxy deployed successfully + headers: + Location: + $ref: '#/components/headers/Location' content: application/json: schema: @@ -2518,6 +2554,9 @@ paths: responses: '201': description: API key created successfully + headers: + Location: + $ref: '#/components/headers/Location' content: application/json: schema: @@ -2628,6 +2667,9 @@ paths: responses: '201': description: MCP proxy created successfully + headers: + Location: + $ref: '#/components/headers/Location' content: application/json: schema: @@ -2820,6 +2862,9 @@ paths: responses: '201': description: MCP proxy deployed successfully + headers: + Location: + $ref: '#/components/headers/Location' content: application/json: schema: @@ -3122,6 +3167,9 @@ paths: responses: '201': description: Gateway registered successfully + headers: + Location: + $ref: '#/components/headers/Location' content: application/json: schema: @@ -3315,6 +3363,9 @@ paths: responses: '201': description: New token generated successfully + headers: + Location: + $ref: '#/components/headers/Location' content: application/json: schema: @@ -3599,6 +3650,9 @@ paths: responses: '201': description: Application created successfully + headers: + Location: + $ref: '#/components/headers/Location' content: application/json: schema: @@ -4058,6 +4112,9 @@ paths: responses: '201': description: Subscription plan created successfully + headers: + Location: + $ref: '#/components/headers/Location' content: application/json: schema: @@ -4215,6 +4272,9 @@ paths: responses: '201': description: Subscription created successfully + headers: + Location: + $ref: '#/components/headers/Location' content: application/json: schema: @@ -4498,6 +4558,9 @@ paths: responses: '201': description: Secret created successfully. + headers: + Location: + $ref: '#/components/headers/Location' content: application/json: schema: @@ -4696,6 +4759,12 @@ paths: $ref: '#/components/responses/InternalServerError' components: + headers: + Location: + description: URL of the newly created resource. + schema: + type: string + format: uri securitySchemes: OAuth2Security: type: oauth2 From 270baa155b5c758b6bd3c8fc59fca81c33ce985b Mon Sep 17 00:00:00 2001 From: Thushani Jayasekera Date: Wed, 8 Jul 2026 12:47:07 +0530 Subject: [PATCH 07/12] Refactor error handling in API and gateway handlers to utilize typed error responses This commit updates various API and gateway handlers to replace specific error checks with a more streamlined approach using typed error responses. This change enhances clarity and consistency in error handling across the application, ensuring that specific errors are propagated without unnecessary wrapping. Additionally, it improves the maintainability of the code by reducing repetitive error handling logic. --- platform-api/internal/handler/api_key.go | 30 +-- platform-api/internal/handler/gateway.go | 103 ++++------- platform-api/internal/handler/llm_apikey.go | 13 +- .../internal/handler/llm_proxy_apikey.go | 13 +- platform-api/internal/handler/organization.go | 18 +- platform-api/internal/handler/project.go | 46 ++--- platform-api/internal/handler/secret.go | 8 +- .../internal/handler/subscription_handler.go | 26 ++- .../handler/subscription_plan_handler.go | 25 +-- .../internal/middleware/error_mapper.go | 11 ++ .../internal/middleware/error_mapper_test.go | 28 +++ platform-api/internal/service/api.go | 15 +- platform-api/internal/service/apikey.go | 13 +- .../internal/service/custom_policy_test.go | 20 +- platform-api/internal/service/deployment.go | 3 +- platform-api/internal/service/gateway.go | 93 +++++----- .../service/gateway_endpoints_test.go | 8 +- platform-api/internal/service/llm.go | 57 +++--- platform-api/internal/service/llm_apikey.go | 13 +- .../internal/service/llm_deployment.go | 11 +- .../internal/service/llm_proxy_apikey.go | 13 +- .../internal/service/mcp_deployment.go | 5 +- platform-api/internal/service/organization.go | 12 +- platform-api/internal/service/project.go | 30 +-- .../internal/service/secret_service.go | 8 +- .../service/subscription_plan_service.go | 19 +- .../internal/service/subscription_service.go | 53 +++--- platform-api/internal/utils/error_mapper.go | 171 ------------------ platform-api/internal/webhook/receiver.go | 6 + 29 files changed, 348 insertions(+), 523 deletions(-) delete mode 100644 platform-api/internal/utils/error_mapper.go diff --git a/platform-api/internal/handler/api_key.go b/platform-api/internal/handler/api_key.go index a014789df3..200b00c8d4 100644 --- a/platform-api/internal/handler/api_key.go +++ b/platform-api/internal/handler/api_key.go @@ -97,14 +97,10 @@ func (h *APIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Request) err // Create the API key and broadcast to gateways if err := h.apiKeyService.CreateAPIKey(r.Context(), apiHandle, constants.RestApi, orgId, userId, &req); err != nil { - // Handle specific error cases - if errors.Is(err, constants.ErrAPINotFound) { - return apperror.ArtifactNotFound.Wrap(err) + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } - if errors.Is(err, constants.ErrGatewayUnavailable) { - return apperror.GatewayConnectionUnavailable.Wrap(err) - } - return apperror.Internal.Wrap(err). WithLogMessage(fmt.Sprintf("failed to create API key %q for API %s in org %s by user %s", name, apiHandle, orgId, userId)) } @@ -172,14 +168,10 @@ func (h *APIKeyHandler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) err // Update the API key and broadcast to gateways if err := h.apiKeyService.UpdateAPIKey(r.Context(), apiHandle, constants.RestApi, orgId, keyName, userId, &req); err != nil { - // Handle specific error cases - if errors.Is(err, constants.ErrAPINotFound) { - return apperror.ArtifactNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrGatewayUnavailable) { - return apperror.GatewayConnectionUnavailable.Wrap(err) + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } - return apperror.Internal.Wrap(err). WithLogMessage(fmt.Sprintf("failed to update API key %s for API %s in org %s by user %s", keyName, apiHandle, orgId, userId)) } @@ -223,14 +215,10 @@ func (h *APIKeyHandler) RevokeAPIKey(w http.ResponseWriter, r *http.Request) err // Revoke the API key and broadcast to gateways if err := h.apiKeyService.RevokeAPIKey(r.Context(), apiHandle, constants.RestApi, orgId, keyName, userId); err != nil { - // Handle specific error cases - if errors.Is(err, constants.ErrAPINotFound) { - return apperror.ArtifactNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrGatewayUnavailable) { - return apperror.GatewayConnectionUnavailable.Wrap(err) + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } - return apperror.Internal.Wrap(err). WithLogMessage(fmt.Sprintf("failed to revoke API key %s for API %s in org %s by user %s", keyName, apiHandle, orgId, userId)) } diff --git a/platform-api/internal/handler/gateway.go b/platform-api/internal/handler/gateway.go index bcf86ab3b2..db5c879097 100644 --- a/platform-api/internal/handler/gateway.go +++ b/platform-api/internal/handler/gateway.go @@ -113,23 +113,12 @@ func (h *GatewayHandler) CreateGateway(w http.ResponseWriter, r *http.Request) e gateway, err := h.gatewayService.RegisterGateway(orgId, req.Id, req.DisplayName, description, req.Endpoints, isCritical, functionalityType, version, createdBy, properties) if err != nil { - errMsg := err.Error() - - // Check for specific error types - if strings.Contains(errMsg, "organization not found") { - return apperror.OrganizationNotFound.Wrap(err) - } - - if strings.Contains(errMsg, "already exists") { - return apperror.GatewayNameConflict.Wrap(err) - } - - if strings.Contains(errMsg, "required") || strings.Contains(errMsg, "invalid") || - strings.Contains(errMsg, "must") || strings.Contains(errMsg, "cannot") { - return apperror.ValidationFailed.Wrap(err, errMsg) + // The service constructs typed catalog errors (not found, conflict, + // validation) at the point of failure — pass them through untouched. + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } - - // Internal server error return apperror.Internal.Wrap(err).WithLogMessage("failed to register gateway") } @@ -171,18 +160,13 @@ func (h *GatewayHandler) GetGateway(w http.ResponseWriter, r *http.Request) erro gateway, err := h.gatewayService.GetGateway(gatewayId, orgId) if err != nil { - errMsg := err.Error() - - // Check for specific error types - if strings.Contains(errMsg, "not found") { - return apperror.GatewayNotFound.Wrap(err) + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } - - if strings.Contains(errMsg, "invalid UUID") { + 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 retrieve gateway") } @@ -206,8 +190,9 @@ func (h *GatewayHandler) GetGatewayStatus(w http.ResponseWriter, r *http.Request status, err := h.gatewayService.GetGatewayStatus(orgId, gatewayIdPtr) if err != nil { - if strings.Contains(err.Error(), "gateway not found") { - return apperror.GatewayNotFound.Wrap(err) + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } return apperror.Internal.Wrap(err).WithLogMessage("failed to get gateway status") } @@ -310,12 +295,10 @@ func (h *GatewayHandler) ListTokens(w http.ResponseWriter, r *http.Request) erro tokens, err := h.gatewayService.ListTokens(gatewayId, orgId) if err != nil { - errMsg := err.Error() - - if strings.Contains(errMsg, "gateway not found") { - return apperror.GatewayNotFound.Wrap(err) + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } - return apperror.Internal.Wrap(err).WithLogMessage("failed to list tokens") } @@ -342,18 +325,10 @@ func (h *GatewayHandler) RotateToken(w http.ResponseWriter, r *http.Request) err } response, err := h.gatewayService.RotateToken(gatewayId, orgId, createdBy) if err != nil { - errMsg := err.Error() - - // Check for specific error types - if strings.Contains(errMsg, "gateway not found") { - return apperror.GatewayNotFound.Wrap(err) - } - - if strings.Contains(errMsg, "maximum") || strings.Contains(errMsg, "Revoke") { - return apperror.GatewayTokenLimitReached.Wrap(err) + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } - - // Internal server error return apperror.Internal.Wrap(err).WithLogMessage("failed to rotate token") } @@ -389,15 +364,10 @@ func (h *GatewayHandler) RevokeToken(w http.ResponseWriter, r *http.Request) err return err } if err := h.gatewayService.RevokeToken(gatewayId, tokenId, orgId, revokedBy); err != nil { - errMsg := err.Error() - - if strings.Contains(errMsg, "not found") { - if strings.Contains(errMsg, "gateway") { - return apperror.GatewayNotFound.Wrap(err) - } - return apperror.GatewayTokenNotFound.Wrap(err) + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } - return apperror.Internal.Wrap(err).WithLogMessage("failed to revoke token") } @@ -453,18 +423,9 @@ func (h *GatewayHandler) SyncCustomPolicy(w http.ResponseWriter, r *http.Request policy, err := h.gatewayService.SyncCustomPolicy(gatewayId, orgId, policyName, version) if err != nil { - msg := err.Error() - if strings.Contains(msg, "gateway not found") { - return apperror.GatewayNotFound.Wrap(err) - } - if strings.Contains(msg, "not found in gateway manifest") { - return apperror.CustomPolicyVersionNotFnd.Wrap(err) - } - if strings.Contains(msg, "not a custom policy") || strings.Contains(msg, "manifest is not available") { - return apperror.PolicyInvalidState.Wrap(err) - } - if strings.Contains(msg, "already exists") || strings.Contains(msg, "patch version updates are not allowed") || strings.Contains(msg, "cannot downgrade") { - return apperror.PolicyVersionConflict.Wrap(err) + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } return apperror.Internal.Wrap(err).WithLogMessage("failed to sync custom policy") } @@ -488,11 +449,9 @@ func (h *GatewayHandler) GetCustomPolicy(w http.ResponseWriter, r *http.Request) policy, err := h.gatewayService.GetCustomPolicyByUUID(orgId, policyUUID, version) if err != nil { - if errors.Is(err, constants.ErrCustomPolicyNotFound) { - return apperror.CustomPolicyNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrCustomPolicyVersionMismatch) { - return apperror.CustomPolicyVersionNotFnd.Wrap(err) + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } return apperror.Internal.Wrap(err).WithLogMessage("failed to get custom policy") } @@ -515,12 +474,14 @@ func (h *GatewayHandler) DeleteCustomPolicy(w http.ResponseWriter, r *http.Reque } if err := h.gatewayService.DeleteCustomPolicyByUUID(orgId, policyUUID, version); err != nil { + var appErr *apperror.Error + if errors.As(err, &appErr) { + 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.ErrCustomPolicyVersionMismatch) { - return apperror.CustomPolicyVersionNotFnd.Wrap(err) - } if errors.Is(err, constants.ErrCustomPolicyInUse) { return apperror.PolicyInUse.Wrap(err) } diff --git a/platform-api/internal/handler/llm_apikey.go b/platform-api/internal/handler/llm_apikey.go index 18b54f7c58..c85261b867 100644 --- a/platform-api/internal/handler/llm_apikey.go +++ b/platform-api/internal/handler/llm_apikey.go @@ -70,8 +70,9 @@ func (h *LLMProviderAPIKeyHandler) ListAPIKeys(w http.ResponseWriter, r *http.Re response, err := h.apiKeyService.ListLLMProviderAPIKeys(r.Context(), providerID, orgID, callerUserID) if err != nil { - if errors.Is(err, constants.ErrAPINotFound) { - return apperror.ArtifactNotFound.Wrap(err) + var appErr *apperror.Error + 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)) @@ -155,11 +156,9 @@ func (h *LLMProviderAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.R response, err := h.apiKeyService.CreateLLMProviderAPIKey(r.Context(), providerID, orgID, userID, &req) if err != nil { - if errors.Is(err, constants.ErrAPINotFound) { - return apperror.ArtifactNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrGatewayUnavailable) { - return apperror.GatewayConnectionUnavailable.Wrap(err) + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } return apperror.Internal.Wrap(err). diff --git a/platform-api/internal/handler/llm_proxy_apikey.go b/platform-api/internal/handler/llm_proxy_apikey.go index 8eda38dd2a..ccb85e8a61 100644 --- a/platform-api/internal/handler/llm_proxy_apikey.go +++ b/platform-api/internal/handler/llm_proxy_apikey.go @@ -70,8 +70,9 @@ func (h *LLMProxyAPIKeyHandler) ListAPIKeys(w http.ResponseWriter, r *http.Reque response, err := h.apiKeyService.ListLLMProxyAPIKeys(r.Context(), proxyID, orgID, callerUserID) if err != nil { - if errors.Is(err, constants.ErrAPINotFound) { - return apperror.ArtifactNotFound.Wrap(err) + var appErr *apperror.Error + 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)) @@ -155,11 +156,9 @@ func (h *LLMProxyAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Requ response, err := h.apiKeyService.CreateLLMProxyAPIKey(r.Context(), proxyID, orgID, userID, &req) if err != nil { - if errors.Is(err, constants.ErrAPINotFound) { - return apperror.ArtifactNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrGatewayUnavailable) { - return apperror.GatewayConnectionUnavailable.Wrap(err) + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } return apperror.Internal.Wrap(err). diff --git a/platform-api/internal/handler/organization.go b/platform-api/internal/handler/organization.go index 20214b4a1a..2dfe162fb0 100644 --- a/platform-api/internal/handler/organization.go +++ b/platform-api/internal/handler/organization.go @@ -90,11 +90,9 @@ func (h *OrganizationHandler) RegisterOrganization(w http.ResponseWriter, r *htt idpOrgRefUUID, _ := middleware.GetIdpOrgRefFromRequest(r) org, err := h.orgService.RegisterOrganization(id, handle, req.DisplayName, req.Region, idpOrgRefUUID, performedBy) if err != nil { - if errors.Is(err, constants.ErrHandleExists) { - return apperror.OrganizationExists.Wrap(err) - } - if errors.Is(err, constants.ErrOrganizationExists) { - return apperror.OrganizationExists.Wrap(err) + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } return apperror.Internal.Wrap(err). WithLogMessage("failed to create organization") @@ -123,8 +121,9 @@ func (h *OrganizationHandler) HeadOrganization(w http.ResponseWriter, r *http.Re _, err := h.orgService.GetOrganizationByHandle(handle) if err != nil { - if errors.Is(err, constants.ErrOrganizationNotFound) { - return apperror.OrganizationNotFound.Wrap(err) + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } return apperror.Internal.Wrap(err). WithLogMessage(fmt.Sprintf("failed to get organization by handle %s", handle)) @@ -140,8 +139,9 @@ func (h *OrganizationHandler) GetOrganizationByID(w http.ResponseWriter, r *http org, err := h.orgService.GetOrganizationByHandle(handle) if err != nil { - if errors.Is(err, constants.ErrOrganizationNotFound) { - return apperror.OrganizationNotFound.Wrap(err) + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } return apperror.Internal.Wrap(err). WithLogMessage(fmt.Sprintf("failed to get organization by handle %s", handle)) diff --git a/platform-api/internal/handler/project.go b/platform-api/internal/handler/project.go index 3878796d3e..fe29f667b8 100644 --- a/platform-api/internal/handler/project.go +++ b/platform-api/internal/handler/project.go @@ -71,14 +71,9 @@ func (h *ProjectHandler) CreateProject(w http.ResponseWriter, r *http.Request) e } project, err := h.projectService.CreateProject(&req, organizationID, actor) if err != nil { - if errors.Is(err, constants.ErrProjectExists) { - return apperror.ProjectExists.Wrap(err) - } - if errors.Is(err, constants.ErrOrganizationNotFound) { - return apperror.OrganizationNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrInvalidProjectName) { - return apperror.ValidationFailed.Wrap(err, "Project displayName is required") + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } return apperror.Internal.Wrap(err). WithLogMessage(fmt.Sprintf("failed to create project in org %s", organizationID)) @@ -104,8 +99,9 @@ func (h *ProjectHandler) GetProject(w http.ResponseWriter, r *http.Request) erro project, err := h.projectService.GetProjectByHandle(projectId, orgID) if err != nil { - if errors.Is(err, constants.ErrProjectNotFound) { - return apperror.ProjectNotFound.Wrap(err) + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } return apperror.Internal.Wrap(err). WithLogMessage(fmt.Sprintf("failed to get project %s in org %s", projectId, orgID)) @@ -125,8 +121,9 @@ func (h *ProjectHandler) ListProjects(w http.ResponseWriter, r *http.Request) er projects, err := h.projectService.GetProjectsByOrganization(orgID) if err != nil { - if errors.Is(err, constants.ErrOrganizationNotFound) { - return apperror.OrganizationNotFound.Wrap(err) + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } return apperror.Internal.Wrap(err). WithLogMessage(fmt.Sprintf("failed to list projects in org %s", orgID)) @@ -177,15 +174,13 @@ func (h *ProjectHandler) UpdateProject(w http.ResponseWriter, r *http.Request) e } project, err := h.projectService.UpdateProject(projectId, &req, orgID, actor) if err != nil { - if errors.Is(err, constants.ErrProjectNotFound) { - return apperror.ProjectNotFound.Wrap(err) + 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") } - if errors.Is(err, constants.ErrProjectExists) { - return apperror.ProjectExists.Wrap(err) - } return apperror.Internal.Wrap(err). WithLogMessage(fmt.Sprintf("failed to update project %s in org %s", projectId, orgID)) } @@ -212,20 +207,9 @@ func (h *ProjectHandler) DeleteProject(w http.ResponseWriter, r *http.Request) e return err } if err := h.projectService.DeleteProject(projectId, orgID, actor); err != nil { - if errors.Is(err, constants.ErrProjectNotFound) { - return apperror.ProjectNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrOrganizationMustHAveAtLeastOneProject) { - return apperror.ValidationFailed.Wrap(err, "Organization must have at least one project") - } - if errors.Is(err, constants.ErrProjectHasAssociatedAPIs) { - return apperror.ValidationFailed.Wrap(err, "Project has associated APIs") - } - if errors.Is(err, constants.ErrProjectHasAssociatedMCPProxies) { - return apperror.ValidationFailed.Wrap(err, "Project has associated MCP proxies") - } - if errors.Is(err, constants.ErrProjectHasAssociatedApplications) { - return apperror.ValidationFailed.Wrap(err, "Project has associated applications") + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } return apperror.Internal.Wrap(err). WithLogMessage(fmt.Sprintf("failed to delete project %s in org %s", projectId, orgID)) diff --git a/platform-api/internal/handler/secret.go b/platform-api/internal/handler/secret.go index 4dc052a0b3..8ad8feb8f3 100644 --- a/platform-api/internal/handler/secret.go +++ b/platform-api/internal/handler/secret.go @@ -81,12 +81,14 @@ 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) } - if errors.Is(err, constants.ErrInvalidSecretType) { - return apperror.ValidationFailed.Wrap(err, "Invalid secret type: must be GENERIC or CERTIFICATE") - } return apperror.Internal.Wrap(err).WithLogMessage("failed to create secret") } diff --git a/platform-api/internal/handler/subscription_handler.go b/platform-api/internal/handler/subscription_handler.go index 27948e3bd2..354298b028 100644 --- a/platform-api/internal/handler/subscription_handler.go +++ b/platform-api/internal/handler/subscription_handler.go @@ -102,11 +102,9 @@ func (h *SubscriptionHandler) CreateSubscription(w http.ResponseWriter, r *http. } sub, err := h.subscriptionService.CreateSubscription(req.APIID, req.Kind, orgId, req.SubscriberID, req.ApplicationID, req.SubscriptionPlanID, "", req.Status, actor) if err != nil { - if errors.Is(err, constants.ErrAPINotFound) { - return apperror.ArtifactNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrSubscriptionAlreadyExists) { - return apperror.SubscriptionExists.Wrap(err) + var appErr *apperror.Error + 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)) @@ -177,8 +175,9 @@ func (h *SubscriptionHandler) ListSubscriptions(w http.ResponseWriter, r *http.R } list, total, err := h.subscriptionService.ListSubscriptionsByFilters(orgId, apiIDPtr, subscriberIDPtr, appIDPtr, statusPtr, limit, offset) if err != nil { - if errors.Is(err, constants.ErrAPINotFound) { - return apperror.ArtifactNotFound.Wrap(err) + var appErr *apperror.Error + 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)) @@ -251,8 +250,9 @@ func (h *SubscriptionHandler) GetSubscription(w http.ResponseWriter, r *http.Req } sub, err := h.subscriptionService.GetSubscription(subscriptionId, orgId) if err != nil { - if errors.Is(err, constants.ErrSubscriptionNotFound) { - return apperror.SubscriptionNotFound.Wrap(err) + var appErr *apperror.Error + 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)) @@ -300,11 +300,9 @@ func (h *SubscriptionHandler) UpdateSubscription(w http.ResponseWriter, r *http. } sub, err := h.subscriptionService.UpdateSubscription(subscriptionId, orgId, subscriberID, status, actor) if err != nil { - if errors.Is(err, constants.ErrSubscriptionNotFound) { - return apperror.SubscriptionNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrSubscriptionSubscriberMismatch) { - return apperror.SubscriptionForbidden.Wrap(err) + var appErr *apperror.Error + 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)) diff --git a/platform-api/internal/handler/subscription_plan_handler.go b/platform-api/internal/handler/subscription_plan_handler.go index 3986217467..dc9513369c 100644 --- a/platform-api/internal/handler/subscription_plan_handler.go +++ b/platform-api/internal/handler/subscription_plan_handler.go @@ -208,8 +208,9 @@ func (h *SubscriptionPlanHandler) CreateSubscriptionPlan(w http.ResponseWriter, } created, err := h.planService.CreatePlan(orgId, actor, plan) if err != nil { - if errors.Is(err, constants.ErrSubscriptionPlanAlreadyExists) { - return apperror.SubscriptionPlanExists.Wrap(err) + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } return apperror.Internal.Wrap(err). WithLogMessage(fmt.Sprintf("failed to create subscription plan for org %s", orgId)) @@ -289,8 +290,9 @@ func (h *SubscriptionPlanHandler) GetSubscriptionPlan(w http.ResponseWriter, r * plan, err := h.planService.GetPlan(planId, orgId) if err != nil { - if errors.Is(err, constants.ErrSubscriptionPlanNotFound) { - return apperror.SubscriptionPlanNotFound.Wrap(err) + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } return apperror.Internal.Wrap(err). WithLogMessage(fmt.Sprintf("failed to get subscription plan %s in org %s", planId, orgId)) @@ -374,15 +376,13 @@ 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") } - if errors.Is(err, constants.ErrSubscriptionPlanNotFound) { - return apperror.SubscriptionPlanNotFound.Wrap(err) - } - if errors.Is(err, constants.ErrSubscriptionPlanAlreadyExists) { - return apperror.SubscriptionPlanExists.Wrap(err) - } return apperror.Internal.Wrap(err). WithLogMessage(fmt.Sprintf("failed to update subscription plan %s in org %s", planId, orgId)) } @@ -420,8 +420,9 @@ func (h *SubscriptionPlanHandler) DeleteSubscriptionPlan(w http.ResponseWriter, } err = h.planService.DeletePlan(planId, orgId, actor) if err != nil { - if errors.Is(err, constants.ErrSubscriptionPlanNotFound) { - return apperror.SubscriptionPlanNotFound.Wrap(err) + var appErr *apperror.Error + if errors.As(err, &appErr) { + return err } return apperror.Internal.Wrap(err). WithLogMessage(fmt.Sprintf("failed to delete subscription plan %s in org %s", planId, orgId)) diff --git a/platform-api/internal/middleware/error_mapper.go b/platform-api/internal/middleware/error_mapper.go index 5c547995ee..62e9281fd3 100644 --- a/platform-api/internal/middleware/error_mapper.go +++ b/platform-api/internal/middleware/error_mapper.go @@ -24,6 +24,7 @@ import ( "runtime/debug" "github.com/wso2/api-platform/platform-api/internal/apperror" + "github.com/wso2/api-platform/platform-api/internal/utils" "github.com/google/uuid" ) @@ -76,6 +77,16 @@ func writeMappedError(w http.ResponseWriter, r *http.Request, slogger *slog.Logg 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 == utils.CodeCommonInternalError && appErr.Cause != nil { + var inner *apperror.Error + if !errors.As(appErr.Cause, &inner) { + break + } + appErr = inner + } logFields := []any{ "trackingId", trackID, diff --git a/platform-api/internal/middleware/error_mapper_test.go b/platform-api/internal/middleware/error_mapper_test.go index 407d55886b..dc49b74ed5 100644 --- a/platform-api/internal/middleware/error_mapper_test.go +++ b/platform-api/internal/middleware/error_mapper_test.go @@ -145,6 +145,34 @@ func TestMapErrorsPlainErrorFallsBackToGeneric500(t *testing.T) { } } +func TestMapErrorsPrefersInnerTypedErrorOverGenericInternalWrapper(t *testing.T) { + // A service returns a specific typed error; a handler fallback blindly + // wraps it in a generic Internal. The mapper must surface the specific + // error, not the 500 wrapper. + var logBuf bytes.Buffer + h := MapErrors(testLogger(&logBuf), func(w http.ResponseWriter, r *http.Request) error { + serviceErr := apperror.GatewayNotFound.New().WithLogMessage("gateway g1 missing") + return apperror.Internal.Wrap(serviceErr).WithLogMessage("failed to get gateway") + }) + + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest("GET", "/api/v0.9/gateways/g1", nil)) + + if rec.Code != http.StatusNotFound { + t.Fatalf("expected 404 from the inner typed error, got %d", rec.Code) + } + var body utils.ErrorResponse + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("invalid JSON body: %v", err) + } + if body.Code != utils.CodeGatewayNotFound { + t.Errorf("expected code %q, got %q", utils.CodeGatewayNotFound, body.Code) + } + if !strings.Contains(logBuf.String(), "gateway g1 missing") { + t.Error("expected the inner error's log message to be logged") + } +} + func TestMapErrorsRecoversPanic(t *testing.T) { var logBuf bytes.Buffer h := MapErrors(testLogger(&logBuf), func(w http.ResponseWriter, r *http.Request) error { diff --git a/platform-api/internal/service/api.go b/platform-api/internal/service/api.go index f041c7b27f..42be4e8ce8 100644 --- a/platform-api/internal/service/api.go +++ b/platform-api/internal/service/api.go @@ -28,6 +28,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" @@ -184,7 +185,7 @@ func (s *APIService) modelToRESTAPI(apiModel *model.API) (*api.RESTAPI, error) { // GetAPIByUUID retrieves an API by its ID func (s *APIService) GetAPIByUUID(apiUUID, orgUUID string) (*api.RESTAPI, error) { if apiUUID == "" { - return nil, errors.New("API id is required") + return nil, apperror.ValidationFailed.New("API id is required") } apiModel, err := s.apiRepo.GetAPIByUUID(apiUUID, orgUUID) @@ -227,7 +228,7 @@ func (s *APIService) HandleExistsCheck(orgUUID string) func(string) bool { // This is a lightweight operation that only fetches minimal metadata. func (s *APIService) getAPIUUIDByHandle(handle, orgUUID string) (string, error) { if handle == "" { - return "", errors.New("API handle is required") + return "", apperror.ValidationFailed.New("API handle is required") } metadata, err := s.apiRepo.GetAPIMetadataByHandle(handle, orgUUID) @@ -280,7 +281,7 @@ func (s *APIService) GetAPIsByOrganization(orgUUID string, projectHandle string) // UpdateAPI updates an existing API func (s *APIService) UpdateAPI(apiUUID string, req *api.RESTAPI, orgUUID, updatedBy string) (*api.RESTAPI, error) { if apiUUID == "" { - return nil, errors.New("API id is required") + return nil, apperror.ValidationFailed.New("API id is required") } // Get existing API @@ -357,7 +358,7 @@ func (s *APIService) ensureRESTRuntimeArtifactUnchanged(existing, updated *model // DeleteAPI deletes an API func (s *APIService) DeleteAPI(apiUUID, orgUUID, deletedBy string) error { if apiUUID == "" { - return errors.New("API id is required") + return apperror.ValidationFailed.New("API id is required") } // Check if API exists @@ -573,7 +574,7 @@ func (s *APIService) validateCreateAPIRequest(req *api.CreateRESTAPIRequest, org return constants.ErrInvalidAPIVersion } if strings.TrimSpace(req.ProjectId) == "" { - return errors.New("project id is required") + return apperror.ValidationFailed.New("project id is required") } nameVersionExists, err := s.apiRepo.CheckAPIExistsByNameAndVersionInOrganization(req.DisplayName, req.Version, orgUUID, "") @@ -604,12 +605,12 @@ func (s *APIService) validateCreateAPIRequest(req *api.CreateRESTAPIRequest, org case constants.APITypeWebSub: // For WebSub APIs, ensure that at least one channel is defined if req.Operations != nil && len(*req.Operations) > 0 { - return errors.New("WebSub APIs cannot have operations defined") + return apperror.ValidationFailed.New("WebSub APIs cannot have operations defined") } case constants.APITypeHTTP: // For HTTP APIs, ensure that at least one operation is defined if req.Channels != nil && len(*req.Channels) > 0 { - return errors.New("HTTP APIs cannot have channels defined") + return apperror.ValidationFailed.New("HTTP APIs cannot have channels defined") } } diff --git a/platform-api/internal/service/apikey.go b/platform-api/internal/service/apikey.go index e6005aeeb9..a2a562baa3 100644 --- a/platform-api/internal/service/apikey.go +++ b/platform-api/internal/service/apikey.go @@ -29,6 +29,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" @@ -287,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 constants.ErrAPINotFound + return apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) } apiId := apiMetadata.ID @@ -297,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 constants.ErrGatewayUnavailable + return apperror.GatewayConnectionUnavailable.Wrap(constants.ErrGatewayUnavailable) } // Resolve key name (required for DB uniqueness; derive from request or generate) @@ -413,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 constants.ErrAPINotFound + return apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) } apiId := apiMetadata.ID @@ -425,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 constants.ErrGatewayUnavailable + return apperror.GatewayConnectionUnavailable.Wrap(constants.ErrGatewayUnavailable) } // Hash the API key with all configured algorithms before storage and broadcast @@ -524,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 constants.ErrAPINotFound + return apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) } apiId := apiMetadata.ID @@ -534,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 constants.ErrGatewayUnavailable + return apperror.GatewayConnectionUnavailable.Wrap(constants.ErrGatewayUnavailable) } // Fetch UUID before revoke for consistent audit record (CREATE uses UUID, not name) diff --git a/platform-api/internal/service/custom_policy_test.go b/platform-api/internal/service/custom_policy_test.go index 9540b0c56c..a9b2780eae 100644 --- a/platform-api/internal/service/custom_policy_test.go +++ b/platform-api/internal/service/custom_policy_test.go @@ -197,7 +197,7 @@ func TestSyncCustomPolicy(t *testing.T) { policyName: "rate-limit", version: "1.0.0", wantErr: true, - errContains: "gateway not found", + errContains: "GATEWAY_NOT_FOUND", }, { name: "gateway not found - nil returned", @@ -205,7 +205,7 @@ func TestSyncCustomPolicy(t *testing.T) { policyName: "rate-limit", version: "1.0.0", wantErr: true, - errContains: "gateway not found", + errContains: "GATEWAY_NOT_FOUND", }, { name: "gateway belongs to different org", @@ -213,7 +213,7 @@ func TestSyncCustomPolicy(t *testing.T) { policyName: "rate-limit", version: "1.0.0", wantErr: true, - errContains: "gateway not found", + errContains: "GATEWAY_NOT_FOUND", }, // manifest validation @@ -233,7 +233,7 @@ func TestSyncCustomPolicy(t *testing.T) { policyName: "rate-limit", version: "1.0.0", wantErr: true, - errContains: "gateway manifest is not available", + errContains: "POLICY_INVALID_STATE", }, { name: "policy not found in manifest", @@ -244,7 +244,7 @@ func TestSyncCustomPolicy(t *testing.T) { policyName: "rate-limit", version: "1.0.0", wantErr: true, - errContains: "not found in gateway manifest", + errContains: "CUSTOM_POLICY_VERSION_NOT_FOUND", }, { name: "policy version mismatch in manifest", @@ -255,7 +255,7 @@ func TestSyncCustomPolicy(t *testing.T) { policyName: "rate-limit", version: "1.0.0", wantErr: true, - errContains: "not found in gateway manifest", + errContains: "CUSTOM_POLICY_VERSION_NOT_FOUND", }, { name: "policy is not a custom policy (wso2 managed)", @@ -266,7 +266,7 @@ func TestSyncCustomPolicy(t *testing.T) { policyName: "rate-limit", version: "1.0.0", wantErr: true, - errContains: "not a custom policy", + errContains: "POLICY_INVALID_STATE", }, // version conflict rules @@ -280,7 +280,7 @@ func TestSyncCustomPolicy(t *testing.T) { policyName: "rate-limit", version: "1.2.0", wantErr: true, - errContains: "already exists", + errContains: "POLICY_VERSION_CONFLICT", }, { name: "patch version update is not allowed", @@ -292,7 +292,7 @@ func TestSyncCustomPolicy(t *testing.T) { policyName: "rate-limit", version: "1.2.1", wantErr: true, - errContains: "patch version updates are not allowed", + errContains: "POLICY_VERSION_CONFLICT", }, { name: "downgrade is not allowed", @@ -304,7 +304,7 @@ func TestSyncCustomPolicy(t *testing.T) { policyName: "rate-limit", version: "1.1.0", wantErr: true, - errContains: "cannot downgrade", + errContains: "POLICY_VERSION_CONFLICT", }, // custom policy successful paths diff --git a/platform-api/internal/service/deployment.go b/platform-api/internal/service/deployment.go index 1e36c92ec4..e8240f0634 100644 --- a/platform-api/internal/service/deployment.go +++ b/platform-api/internal/service/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/dto" "github.com/wso2/api-platform/platform-api/internal/model" @@ -909,7 +910,7 @@ func (s *DeploymentService) RestoreDeploymentByHandle(apiHandle, deploymentID, g // getUUIDByHandle retrieves the artifact UUID by its handle from the artifact table func (s *DeploymentService) getUUIDByHandle(handle, orgUUID string) (string, error) { if handle == "" { - return "", errors.New("artifact handle is required") + return "", apperror.ValidationFailed.New("artifact handle is required") } artifact, err := s.artifactRepo.GetByHandle(handle, orgUUID) diff --git a/platform-api/internal/service/gateway.go b/platform-api/internal/service/gateway.go index ee0fba72f4..b2840f63a3 100644 --- a/platform-api/internal/service/gateway.go +++ b/platform-api/internal/service/gateway.go @@ -23,10 +23,10 @@ import ( "encoding/base64" "encoding/hex" "encoding/json" - "errors" "fmt" "log/slog" "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" @@ -329,10 +329,10 @@ func (s *GatewayService) SyncCustomPolicy(gatewayID, orgID, policyName, version gateway, err := s.gatewayRepo.GetByUUID(gatewayID) if err != nil || gateway == nil { - return nil, errors.New("gateway not found") + return nil, apperror.GatewayNotFound.Wrap(err) } if gateway.OrganizationID != orgID { - return nil, errors.New("gateway not found") + return nil, apperror.GatewayNotFound.New().WithLogMessage("gateway belongs to a different organization") } raw, err := s.gatewayRepo.GetGatewayManifest(gatewayID) @@ -342,7 +342,7 @@ func (s *GatewayService) SyncCustomPolicy(gatewayID, orgID, policyName, version } if len(raw) == 0 { s.slogger.Error("gateway manifest is not available", slog.String("gateway_id", gatewayID), slog.String("org_id", orgID)) - return nil, errors.New("gateway manifest is not available") + return nil, apperror.PolicyInvalidState.New().WithLogMessage("gateway manifest is not available") } var policies []GatewayPolicyDefinition @@ -359,11 +359,13 @@ func (s *GatewayService) SyncCustomPolicy(gatewayID, orgID, policyName, version } if found == nil { s.slogger.Error("policy not found in gateway manifest", slog.String("gateway_id", gatewayID), slog.String("org_id", orgID), slog.String("policy_name", policyName), slog.String("version", version)) - return nil, fmt.Errorf("policy '%s' version '%s' not found in gateway manifest", policyName, version) + return nil, apperror.CustomPolicyVersionNotFnd.New(). + WithLogMessage(fmt.Sprintf("policy '%s' version '%s' not found in gateway manifest", policyName, version)) } if found.ManagedBy != constants.PolicyManagedByCustomer { s.slogger.Error("policy is not a custom policy", slog.String("gateway_id", gatewayID), slog.String("org_id", orgID), slog.String("policy_name", policyName), slog.String("version", version)) - return nil, fmt.Errorf("policy '%s' version '%s' is not a custom policy", policyName, version) + return nil, apperror.PolicyInvalidState.New(). + WithLogMessage(fmt.Sprintf("policy '%s' version '%s' is not a custom policy", policyName, version)) } var policyDefJSON json.RawMessage @@ -378,7 +380,7 @@ func (s *GatewayService) SyncCustomPolicy(gatewayID, orgID, policyName, version incomingVer, err := parseVersion(version) if err != nil { s.slogger.Error("invalid version format", slog.String("org_id", orgID), slog.String("policy_name", policyName), slog.String("version", version)) - return nil, fmt.Errorf("invalid version '%s': %w", version, err) + return nil, apperror.ValidationFailed.Wrap(err, fmt.Sprintf("Invalid version '%s'", version)) } existingPolicies, err := s.customPolicyRepo.GetCustomPoliciesByName(orgID, policyName) @@ -418,16 +420,19 @@ func (s *GatewayService) SyncCustomPolicy(gatewayID, orgID, policyName, version } if existingVer.Minor == incomingVer.Minor && existingVer.Patch == incomingVer.Patch { // Exact same version — already exists. - return nil, fmt.Errorf("custom policy '%s' version '%s' already exists", policyName, version) + return nil, apperror.PolicyVersionConflict.New(). + WithLogMessage(fmt.Sprintf("custom policy '%s' version '%s' already exists", policyName, version)) } if existingVer.Minor == incomingVer.Minor && existingVer.Patch != incomingVer.Patch { // Same major.minor, different patch — patch update, not allowed. - return nil, fmt.Errorf("patch version updates are not allowed for policy '%s': existing '%s', incoming '%s'", - policyName, sameMajorVersionedPolicy.Version, version) + return nil, apperror.PolicyVersionConflict.New(). + WithLogMessage(fmt.Sprintf("patch version updates are not allowed for policy '%s': existing '%s', incoming '%s'", + policyName, sameMajorVersionedPolicy.Version, version)) } if incomingVer.Minor < existingVer.Minor { - return nil, fmt.Errorf("cannot downgrade policy '%s' from '%s' to '%s'", - policyName, sameMajorVersionedPolicy.Version, version) + return nil, apperror.PolicyVersionConflict.New(). + WithLogMessage(fmt.Sprintf("cannot downgrade policy '%s' from '%s' to '%s'", + policyName, sameMajorVersionedPolicy.Version, version)) } // New minor version — update the existing record. policy.UUID = sameMajorVersionedPolicy.UUID @@ -485,10 +490,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, constants.ErrCustomPolicyNotFound + return nil, apperror.CustomPolicyNotFound.Wrap(constants.ErrCustomPolicyNotFound) } if policy.Version != version { - return nil, constants.ErrCustomPolicyVersionMismatch + return nil, apperror.CustomPolicyVersionNotFnd.Wrap(constants.ErrCustomPolicyVersionMismatch) } return policy, nil } @@ -500,10 +505,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 constants.ErrCustomPolicyNotFound + return apperror.CustomPolicyNotFound.Wrap(constants.ErrCustomPolicyNotFound) } if policy.Version != version { - return constants.ErrCustomPolicyVersionMismatch + return apperror.CustomPolicyVersionNotFnd.Wrap(constants.ErrCustomPolicyVersionMismatch) } if err := s.customPolicyRepo.DeleteCustomPolicyIfUnused(orgID, policyUUID); err != nil { @@ -567,7 +572,7 @@ func (s *GatewayService) RegisterGateway(orgID string, id *string, displayName, return nil, fmt.Errorf("failed to query organization: %w", err) } if org == nil { - return nil, errors.New("organization not found") + return nil, apperror.OrganizationNotFound.New() } // 3. Check gateway handle uniqueness within organization @@ -576,7 +581,8 @@ func (s *GatewayService) RegisterGateway(orgID string, id *string, displayName, return nil, fmt.Errorf("failed to check gateway handle uniqueness: %w", err) } if existing != nil { - return nil, fmt.Errorf("gateway with handle '%s' already exists in this organization", name) + return nil, apperror.GatewayNameConflict.New(). + WithLogMessage(fmt.Sprintf("gateway with handle '%s' already exists in this organization", name)) } // 4. Generate UUID for gateway @@ -666,7 +672,7 @@ func (s *GatewayService) GetGateway(gatewayId, orgId string) (*api.GatewayRespon } if gateway == nil { - return nil, errors.New("gateway not found") + return nil, apperror.GatewayNotFound.New() } return s.gatewayModelToAPI(gateway) @@ -770,7 +776,7 @@ func (s *GatewayService) ListTokens(gatewayId, orgId string) ([]api.TokenInfoRes return nil, fmt.Errorf("failed to query gateway: %w", err) } if gateway == nil { - return nil, errors.New("gateway not found") + return nil, apperror.GatewayNotFound.New() } activeTokens, err := s.gatewayRepo.GetActiveTokensByGatewayUUID(gateway.ID) @@ -806,7 +812,7 @@ func (s *GatewayService) RotateToken(gatewayId, orgId, createdBy string) (*api.T return nil, fmt.Errorf("failed to query gateway: %w", err) } if gateway == nil { - return nil, errors.New("gateway not found") + return nil, apperror.GatewayNotFound.New() } // 2. Count active tokens @@ -817,7 +823,8 @@ func (s *GatewayService) RotateToken(gatewayId, orgId, createdBy string) (*api.T // 3. Check max 2 active tokens limit if activeCount >= 2 { - return nil, errors.New("maximum 2 active tokens allowed. Revoke old tokens before rotating") + return nil, apperror.GatewayTokenLimitReached.New(). + WithLogMessage("maximum 2 active tokens allowed; revoke old tokens before rotating") } // 4. Generate new plain-text token @@ -861,7 +868,7 @@ func (s *GatewayService) RevokeToken(gatewayId, tokenId, orgId, revokedBy string return fmt.Errorf("failed to query gateway: %w", err) } if gateway == nil { - return errors.New("gateway not found") + return apperror.GatewayNotFound.New() } token, err := s.gatewayRepo.GetTokenByUUID(tokenId) @@ -869,10 +876,10 @@ func (s *GatewayService) RevokeToken(gatewayId, tokenId, orgId, revokedBy string return fmt.Errorf("failed to query token: %w", err) } if token == nil { - return errors.New("token not found") + return apperror.GatewayTokenNotFound.New() } if token.GatewayID != gateway.ID { - return errors.New("token not found") + return apperror.GatewayTokenNotFound.New().WithLogMessage("token belongs to a different gateway") } if err := s.gatewayRepo.RevokeToken(tokenId, revokedBy); err != nil { @@ -886,7 +893,7 @@ func (s *GatewayService) RevokeToken(gatewayId, tokenId, orgId, revokedBy string func (s *GatewayService) GetGatewayStatus(orgID string, gatewayId *string) (*api.GatewayStatusListResponse, error) { // Validate organizationId is provided and valid if strings.TrimSpace(orgID) == "" { - return nil, errors.New("organization ID is required") + return nil, apperror.ValidationFailed.New("organization ID is required") } var gateways []*model.Gateway @@ -899,7 +906,7 @@ func (s *GatewayService) GetGatewayStatus(orgID string, gatewayId *string) (*api return nil, fmt.Errorf("failed to get gateway: %w", err) } if gateway == nil { - return nil, errors.New("gateway not found") + return nil, apperror.GatewayNotFound.New() } gateways = []*model.Gateway{gateway} } else { @@ -939,66 +946,66 @@ func (s *GatewayService) UpdateGatewayActiveStatus(gatewayId string, isActive bo func (s *GatewayService) validateGatewayInput(orgID, name, displayName string, endpoints []string, functionalityType string) error { // Organization ID validation if strings.TrimSpace(orgID) == "" { - return errors.New("organization ID is required") + return apperror.ValidationFailed.New("organization ID is required") } if _, err := uuid.Parse(orgID); err != nil { - return errors.New("invalid organization ID format") + return apperror.ValidationFailed.Wrap(err, "invalid organization ID format") } // Gateway name validation name = strings.TrimSpace(name) if name == "" { - return errors.New("gateway name is required") + return apperror.ValidationFailed.New("gateway name is required") } if len(name) < 3 { - return errors.New("gateway name must be at least 3 characters") + return apperror.ValidationFailed.New("gateway name must be at least 3 characters") } if len(name) > 64 { - return errors.New("gateway name must not exceed 64 characters") + return apperror.ValidationFailed.New("gateway name must not exceed 64 characters") } // Check pattern: ^[a-z0-9-]+$ namePattern := regexp.MustCompile(`^[a-z0-9-]+$`) if !namePattern.MatchString(name) { - return errors.New("gateway name must contain only lowercase letters, numbers, and hyphens") + return apperror.ValidationFailed.New("gateway name must contain only lowercase letters, numbers, and hyphens") } // No leading/trailing hyphens if strings.HasPrefix(name, "-") || strings.HasSuffix(name, "-") { - return errors.New("gateway name cannot start or end with a hyphen") + return apperror.ValidationFailed.New("gateway name cannot start or end with a hyphen") } // Display name validation displayName = strings.TrimSpace(displayName) if displayName == "" { - return errors.New("display name is required") + return apperror.ValidationFailed.New("display name is required") } if len(displayName) > 128 { - return errors.New("display name must not exceed 128 characters") + return apperror.ValidationFailed.New("display name must not exceed 128 characters") } // Endpoints validation if len(endpoints) == 0 { - return errors.New("at least one endpoint is required") + return apperror.ValidationFailed.New("at least one endpoint is required") } for _, endpoint := range endpoints { endpoint = strings.TrimSpace(endpoint) if endpoint == "" { - return errors.New("endpoint must not be empty") + return apperror.ValidationFailed.New("endpoint must not be empty") } if len(endpoint) > 255 { - return errors.New("endpoint must not exceed 255 characters") + return apperror.ValidationFailed.New("endpoint must not exceed 255 characters") } } // Gateway type validation functionalityType = strings.TrimSpace(functionalityType) if functionalityType == "" { - return errors.New("gateway functionality type is required") + return apperror.ValidationFailed.New("gateway functionality type is required") } if !constants.ValidGatewayFunctionalityType[functionalityType] { - return fmt.Errorf("gateway type must be one of: %s, %s, %s", - constants.GatewayFunctionalityTypeRegular, constants.GatewayFunctionalityTypeAI, constants.GatewayFunctionalityTypeEvent) + return apperror.ValidationFailed.New(fmt.Sprintf("gateway type must be one of: %s, %s, %s", + constants.GatewayFunctionalityTypeRegular, constants.GatewayFunctionalityTypeAI, constants.GatewayFunctionalityTypeEvent)) } return nil @@ -1011,7 +1018,7 @@ func generateToken() (string, error) { tokenBytes := make([]byte, 32) _, err := rand.Read(tokenBytes) if err != nil { - return "", errors.New("failed to generate secure random token") + return "", apperror.Internal.Wrap(err).WithLogMessage("failed to generate secure random token") } token := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(tokenBytes) return token, nil diff --git a/platform-api/internal/service/gateway_endpoints_test.go b/platform-api/internal/service/gateway_endpoints_test.go index 04d4b647e3..d3ecee1a0e 100644 --- a/platform-api/internal/service/gateway_endpoints_test.go +++ b/platform-api/internal/service/gateway_endpoints_test.go @@ -61,8 +61,8 @@ func TestRegisterGatewayEndpoints(t *testing.T) { if err == nil { t.Fatal("RegisterGateway() expected error for nil endpoints, got nil") } - if err.Error() != "at least one endpoint is required" { - t.Errorf("error = %q, want %q", err.Error(), "at least one endpoint is required") + if !strings.Contains(err.Error(), "at least one endpoint is required") { + t.Errorf("error = %q, want it to contain %q", err.Error(), "at least one endpoint is required") } }) @@ -72,8 +72,8 @@ func TestRegisterGatewayEndpoints(t *testing.T) { if err == nil { t.Fatal("RegisterGateway() expected error for empty endpoints, got nil") } - if err.Error() != "at least one endpoint is required" { - t.Errorf("error = %q, want %q", err.Error(), "at least one endpoint is required") + if !strings.Contains(err.Error(), "at least one endpoint is required") { + t.Errorf("error = %q, want it to contain %q", err.Error(), "at least one endpoint is required") } }) diff --git a/platform-api/internal/service/llm.go b/platform-api/internal/service/llm.go index ba584af7ee..33555811ea 100644 --- a/platform-api/internal/service/llm.go +++ b/platform-api/internal/service/llm.go @@ -29,6 +29,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" @@ -225,7 +226,7 @@ func (s *LLMProviderTemplateService) Create(orgUUID, createdBy string, req *api. handle := makeTemplateHandle(baseHandle, version) if req.ManagedBy != nil && strings.TrimSpace(*req.ManagedBy) == constants.PolicyManagedByWSO2 { - return nil, constants.ErrLLMProviderTemplateManagedByReserved + return nil, apperror.LLMProviderTemplateManagedByReserved.Wrap(constants.ErrLLMProviderTemplateManagedByReserved) } exists, err := s.repo.Exists(handle, orgUUID) @@ -233,7 +234,7 @@ func (s *LLMProviderTemplateService) Create(orgUUID, createdBy string, req *api. return nil, fmt.Errorf("failed to check template exists: %w", err) } if exists { - return nil, constants.ErrLLMProviderTemplateExists + return nil, apperror.LLMProviderTemplateExists.Wrap(constants.ErrLLMProviderTemplateExists) } m := &model.LLMProviderTemplate{ @@ -263,7 +264,7 @@ func (s *LLMProviderTemplateService) Create(orgUUID, createdBy string, req *api. if err := s.repo.Create(m); err != nil { if isSQLiteUniqueConstraint(err) { - return nil, constants.ErrLLMProviderTemplateExists + return nil, apperror.LLMProviderTemplateExists.Wrap(constants.ErrLLMProviderTemplateExists) } return nil, fmt.Errorf("failed to create template: %w", err) } @@ -316,7 +317,7 @@ func (s *LLMProviderTemplateService) Get(orgUUID, handle string) (*api.LLMProvid return nil, fmt.Errorf("failed to get template: %w", err) } if m == nil { - return nil, constants.ErrLLMProviderTemplateNotFound + return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) } return s.toTemplateAPI(m) } @@ -329,7 +330,7 @@ func (s *LLMProviderTemplateService) Update(orgUUID, handle, updatedBy string, r return nil, constants.ErrInvalidInput } if req.ManagedBy != nil && strings.TrimSpace(*req.ManagedBy) == constants.PolicyManagedByWSO2 { - return nil, constants.ErrLLMProviderTemplateManagedByReserved + return nil, apperror.LLMProviderTemplateManagedByReserved.Wrap(constants.ErrLLMProviderTemplateManagedByReserved) } existing, err := s.repo.GetByID(handle, orgUUID) @@ -337,10 +338,10 @@ func (s *LLMProviderTemplateService) Update(orgUUID, handle, updatedBy string, r return nil, fmt.Errorf("failed to resolve template: %w", err) } if existing == nil { - return nil, constants.ErrLLMProviderTemplateNotFound + return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) } if existing.ManagedBy == "wso2" { - return nil, constants.ErrLLMProviderTemplateReadOnly + return nil, apperror.LLMProviderTemplateReadOnly.Wrap(constants.ErrLLMProviderTemplateReadOnly) } if req.Version != "" && req.Version != existing.Version { @@ -392,7 +393,7 @@ func (s *LLMProviderTemplateService) Update(orgUUID, handle, updatedBy string, r if err := s.repo.Update(m); err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, constants.ErrLLMProviderTemplateNotFound + return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) } return nil, fmt.Errorf("failed to update template: %w", err) } @@ -412,7 +413,7 @@ func (s *LLMProviderTemplateService) Update(orgUUID, handle, updatedBy string, r return nil, fmt.Errorf("failed to fetch updated template: %w", err) } if updated == nil { - return nil, constants.ErrLLMProviderTemplateNotFound + return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) } _ = s.auditRepo.Record("UPDATE", updated.UUID, "llm_provider_template", orgUUID, updatedBy) @@ -501,7 +502,7 @@ func (s *LLMProviderTemplateService) CreateVersion(orgUUID, groupID, createdBy s return nil, fmt.Errorf("failed to check template family: %w", err) } if count == 0 { - return nil, constants.ErrLLMProviderTemplateNotFound + return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) } baseHandle := groupID @@ -533,9 +534,9 @@ func (s *LLMProviderTemplateService) CreateVersion(orgUUID, groupID, createdBy s if err := s.repo.CreateNewVersion(m); err != nil { switch { case errors.Is(err, sql.ErrNoRows): - return nil, constants.ErrLLMProviderTemplateNotFound + return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) case errors.Is(err, constants.ErrLLMProviderTemplateVersionExists): - return nil, constants.ErrLLMProviderTemplateVersionExists + return nil, apperror.LLMProviderTemplateVersionExists.Wrap(constants.ErrLLMProviderTemplateVersionExists) default: return nil, fmt.Errorf("failed to create new template version: %w", err) } @@ -559,7 +560,7 @@ func (s *LLMProviderTemplateService) CopyVersion(orgUUID, fromTemplateID, toTemp return nil, fmt.Errorf("failed to resolve source template version: %w", err) } if source == nil { - return nil, constants.ErrLLMProviderTemplateNotFound + return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) } groupID := source.GroupID @@ -631,7 +632,7 @@ func (s *LLMProviderTemplateService) ListVersions(orgUUID, groupID string, limit return nil, fmt.Errorf("failed to count template versions: %w", err) } if total == 0 { - return nil, constants.ErrLLMProviderTemplateNotFound + return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) } items, err := s.repo.ListVersions(groupID, orgUUID, limit, offset) if err != nil { @@ -671,7 +672,7 @@ func (s *LLMProviderTemplateService) GetVersion(orgUUID, groupID, version string return nil, fmt.Errorf("failed to get template version: %w", err) } if m == nil { - return nil, constants.ErrLLMProviderTemplateNotFound + return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) } return s.toTemplateAPI(m) } @@ -692,12 +693,12 @@ func (s *LLMProviderTemplateService) SetVersionEnabled(orgUUID, groupID, version return nil, fmt.Errorf("failed to resolve template version: %w", err) } if target == nil { - return nil, constants.ErrLLMProviderTemplateNotFound + return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) } // 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, constants.ErrLLMProviderTemplateNotToggleable + return nil, apperror.LLMProviderTemplateNotToggleable.Wrap(constants.ErrLLMProviderTemplateNotToggleable) } if err := ensureOriginMutable(target.Origin); err != nil { return nil, err @@ -708,12 +709,12 @@ func (s *LLMProviderTemplateService) SetVersionEnabled(orgUUID, groupID, version return nil, fmt.Errorf("failed to check template version usage: %w", err) } if inUse > 0 { - return nil, constants.ErrLLMProviderTemplateInUse + return nil, apperror.LLMProviderTemplateInUse.Wrap(constants.ErrLLMProviderTemplateInUse) } } if err := s.repo.SetEnabled(groupID, orgUUID, v, enabled); err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, constants.ErrLLMProviderTemplateNotFound + return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) } return nil, fmt.Errorf("failed to set template version enabled: %w", err) } @@ -722,7 +723,7 @@ func (s *LLMProviderTemplateService) SetVersionEnabled(orgUUID, groupID, version return nil, fmt.Errorf("failed to reload template version: %w", err) } if m == nil { - return nil, constants.ErrLLMProviderTemplateNotFound + return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) } return s.toTemplateAPI(m) } @@ -742,10 +743,10 @@ func (s *LLMProviderTemplateService) DeleteVersion(orgUUID, groupID, version str return fmt.Errorf("failed to resolve template version: %w", err) } if target == nil { - return constants.ErrLLMProviderTemplateNotFound + return apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) } if target.ManagedBy == "wso2" { - return constants.ErrLLMProviderTemplateReadOnly + return apperror.LLMProviderTemplateReadOnly.Wrap(constants.ErrLLMProviderTemplateReadOnly) } // Block deletion while any provider built from this specific version still depends on it. inUse, err := s.repo.CountProvidersUsingTemplate(groupID, orgUUID, v) @@ -753,11 +754,11 @@ func (s *LLMProviderTemplateService) DeleteVersion(orgUUID, groupID, version str return fmt.Errorf("failed to check template version usage: %w", err) } if inUse > 0 { - return constants.ErrLLMProviderTemplateInUse + return apperror.LLMProviderTemplateInUse.Wrap(constants.ErrLLMProviderTemplateInUse) } if err := s.repo.DeleteVersion(groupID, orgUUID, v); err != nil { if errors.Is(err, sql.ErrNoRows) { - return constants.ErrLLMProviderTemplateNotFound + return apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) } return fmt.Errorf("failed to delete template version: %w", err) } @@ -775,7 +776,7 @@ func (s *LLMProviderTemplateService) SetEnabledByHandle(orgUUID, handle string, return nil, fmt.Errorf("failed to resolve template: %w", err) } if target == nil { - return nil, constants.ErrLLMProviderTemplateNotFound + return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) } return s.SetVersionEnabled(orgUUID, target.GroupID, target.Version, enabled) } @@ -789,7 +790,7 @@ func (s *LLMProviderTemplateService) DeleteByHandle(orgUUID, handle string) erro return fmt.Errorf("failed to resolve template: %w", err) } if target == nil { - return constants.ErrLLMProviderTemplateNotFound + return apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) } return s.DeleteVersion(orgUUID, target.GroupID, target.Version) } @@ -837,7 +838,7 @@ func (s *LLMProviderService) Create(orgUUID, createdBy string, req *api.LLMProvi } } if tpl == nil { - return nil, constants.ErrLLMProviderTemplateNotFound + return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) } // Determine handle: use provided id or auto-generate from displayName @@ -1054,7 +1055,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, constants.ErrLLMProviderTemplateNotFound + return nil, apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) } // Validate {{ secret "..." }} placeholders in the upstream config diff --git a/platform-api/internal/service/llm_apikey.go b/platform-api/internal/service/llm_apikey.go index f99ca7cb79..eb936237e0 100644 --- a/platform-api/internal/service/llm_apikey.go +++ b/platform-api/internal/service/llm_apikey.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" @@ -72,7 +73,7 @@ func (s *LLMProviderAPIKeyService) ListLLMProviderAPIKeys( return nil, fmt.Errorf("failed to get LLM provider: %w", err) } if provider == nil { - return nil, constants.ErrAPINotFound + return nil, apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) } keys, err := s.apiKeyRepo.ListByArtifact(provider.UUID) @@ -123,7 +124,7 @@ func (s *LLMProviderAPIKeyService) DeleteLLMProviderAPIKey( return fmt.Errorf("failed to get LLM provider: %w", err) } if provider == nil { - return constants.ErrAPINotFound + return apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) } existingKey, err := s.apiKeyRepo.GetByArtifactAndName(provider.UUID, keyName) @@ -132,11 +133,11 @@ func (s *LLMProviderAPIKeyService) DeleteLLMProviderAPIKey( return fmt.Errorf("failed to look up API key: %w", err) } if existingKey == nil { - return constants.ErrAPIKeyNotFound + return apperror.LLMProviderAPIKeyNotFound.Wrap(constants.ErrAPIKeyNotFound) } if userID != "" && existingKey.CreatedBy != userID { - return constants.ErrAPIKeyForbidden + return apperror.LLMProviderAPIKeyForbidden.Wrap(constants.ErrAPIKeyForbidden) } if err := s.apiKeyRepo.Delete(provider.UUID, keyName); err != nil { @@ -188,7 +189,7 @@ func (s *LLMProviderAPIKeyService) CreateLLMProviderAPIKey( } if provider == nil { s.slogger.Warn("LLM provider not found", "providerId", providerID, "organizationId", orgID) - return nil, constants.ErrAPINotFound + return nil, apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) } apiKey, err := utils.GenerateAPIKey() @@ -231,7 +232,7 @@ func (s *LLMProviderAPIKeyService) CreateLLMProviderAPIKey( if len(gateways) == 0 { s.slogger.Warn("No gateways found for organization", "organizationId", orgID) - return nil, constants.ErrGatewayUnavailable + return nil, apperror.GatewayConnectionUnavailable.Wrap(constants.ErrGatewayUnavailable) } 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 79c5c1ff3f..b64f2cc96e 100644 --- a/platform-api/internal/service/llm_deployment.go +++ b/platform-api/internal/service/llm_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/deploymenttransform" "github.com/wso2/api-platform/platform-api/internal/dto" @@ -604,24 +605,24 @@ func (s *LLMProviderDeploymentService) GetLLMProviderDeployment(providerID, depl func (s *LLMProviderDeploymentService) getTemplateHandle(templateUUID, orgUUID string) (string, error) { if templateUUID == "" { - return "", constants.ErrLLMProviderTemplateNotFound + return "", apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) } tpl, err := s.templateRepo.GetByUUID(templateUUID, orgUUID) if err != nil { return "", fmt.Errorf("failed to resolve template: %w", err) } if tpl == nil { - return "", constants.ErrLLMProviderTemplateNotFound + return "", apperror.LLMProviderTemplateNotFound.Wrap(constants.ErrLLMProviderTemplateNotFound) } return tpl.ID, nil } func generateLLMProviderDeploymentYAML(provider *model.LLMProvider, templateHandle string) (dto.LLMProviderDeploymentYAML, error) { if provider == nil { - return dto.LLMProviderDeploymentYAML{}, errors.New("provider is required") + return dto.LLMProviderDeploymentYAML{}, apperror.Internal.New().WithLogMessage("generateLLMProviderDeploymentYAML: provider is nil") } if templateHandle == "" { - return dto.LLMProviderDeploymentYAML{}, errors.New("template handle is required") + 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 @@ -1768,7 +1769,7 @@ func (s *LLMProxyDeploymentService) GetLLMProxyDeployment(proxyID, deploymentID, func generateLLMProxyDeploymentYAML(proxy *model.LLMProxy) (dto.LLMProxyDeploymentYAML, error) { if proxy == nil { - return dto.LLMProxyDeploymentYAML{}, errors.New("proxy is required") + return dto.LLMProxyDeploymentYAML{}, apperror.Internal.New().WithLogMessage("generateLLMProxyDeploymentYAML: proxy is nil") } if proxy.Configuration.Provider == "" { return dto.LLMProxyDeploymentYAML{}, constants.ErrInvalidInput diff --git a/platform-api/internal/service/llm_proxy_apikey.go b/platform-api/internal/service/llm_proxy_apikey.go index afda2bcadb..c34638dbad 100644 --- a/platform-api/internal/service/llm_proxy_apikey.go +++ b/platform-api/internal/service/llm_proxy_apikey.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" @@ -72,7 +73,7 @@ func (s *LLMProxyAPIKeyService) ListLLMProxyAPIKeys( return nil, fmt.Errorf("failed to get LLM proxy: %w", err) } if proxy == nil { - return nil, constants.ErrAPINotFound + return nil, apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) } keys, err := s.apiKeyRepo.ListByArtifact(proxy.UUID) @@ -123,7 +124,7 @@ func (s *LLMProxyAPIKeyService) DeleteLLMProxyAPIKey( return fmt.Errorf("failed to get LLM proxy: %w", err) } if proxy == nil { - return constants.ErrAPINotFound + return apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) } existingKey, err := s.apiKeyRepo.GetByArtifactAndName(proxy.UUID, keyName) @@ -132,12 +133,12 @@ func (s *LLMProxyAPIKeyService) DeleteLLMProxyAPIKey( return fmt.Errorf("failed to look up API key: %w", err) } if existingKey == nil { - return constants.ErrAPIKeyNotFound + return apperror.LLMProxyAPIKeyNotFound.Wrap(constants.ErrAPIKeyNotFound) } // Non-admin callers (userID != "") must be the key creator. if userID != "" && existingKey.CreatedBy != userID { - return constants.ErrAPIKeyForbidden + return apperror.LLMProxyAPIKeyForbidden.Wrap(constants.ErrAPIKeyForbidden) } if err := s.apiKeyRepo.Delete(proxy.UUID, keyName); err != nil { @@ -189,7 +190,7 @@ func (s *LLMProxyAPIKeyService) CreateLLMProxyAPIKey( } if proxy == nil { s.slogger.Warn("LLM proxy not found", "proxyId", proxyID, "organizationId", orgID) - return nil, constants.ErrAPINotFound + return nil, apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) } apiKey, err := utils.GenerateAPIKey() @@ -222,7 +223,7 @@ func (s *LLMProxyAPIKeyService) CreateLLMProxyAPIKey( if len(gateways) == 0 { s.slogger.Warn("No gateways found for organization", "organizationId", orgID) - return nil, constants.ErrGatewayUnavailable + return nil, apperror.GatewayConnectionUnavailable.Wrap(constants.ErrGatewayUnavailable) } apiKeyHashesJSON, err := buildAPIKeyHashesJSON(apiKey, []string{defaultHashingAlgorithm}) diff --git a/platform-api/internal/service/mcp_deployment.go b/platform-api/internal/service/mcp_deployment.go index 3b1ae10d2d..a955cd8c66 100644 --- a/platform-api/internal/service/mcp_deployment.go +++ b/platform-api/internal/service/mcp_deployment.go @@ -22,13 +22,14 @@ package service import ( "errors" "fmt" - "log/slog" "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" "github.com/wso2/api-platform/platform-api/internal/utils" + "log/slog" "strings" "time" @@ -697,7 +698,7 @@ func (s *MCPDeploymentService) deleteMCPProxyDeployment(proxyUUID string, deploy // getMCPProxyUUIDByHandle retrieves the artifact UUID by its handle from the artifact table func (s *MCPDeploymentService) getMCPProxyUUIDByHandle(handle, orgUUID string) (string, error) { if handle == "" { - return "", errors.New("artifact handle is required") + return "", apperror.ValidationFailed.New("artifact handle is required") } artifact, err := s.artifactRepo.GetByHandle(handle, orgUUID) diff --git a/platform-api/internal/service/organization.go b/platform-api/internal/service/organization.go index 7534b2c056..244d59ef5f 100644 --- a/platform-api/internal/service/organization.go +++ b/platform-api/internal/service/organization.go @@ -22,7 +22,7 @@ import ( "log/slog" "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" "github.com/wso2/api-platform/platform-api/internal/utils" @@ -92,7 +92,7 @@ func (s *OrganizationService) RegisterOrganization(id string, handle string, nam handle = generated } else { if err := utils.ValidateHandle(handle); err != nil { - return nil, err + return nil, apperror.ValidationFailed.Wrap(err, err.Error()) } } @@ -103,9 +103,9 @@ func (s *OrganizationService) RegisterOrganization(id string, handle string, nam } if existingOrg != nil { if existingOrg.ID == id { - return nil, constants.ErrOrganizationExists + return nil, apperror.OrganizationExists.New().WithLogMessage("an organization with this UUID already exists") } - return nil, constants.ErrHandleExists + return nil, apperror.OrganizationExists.New().WithLogMessage("an organization with this handle already exists") } if name == "" { @@ -196,7 +196,7 @@ func (s *OrganizationService) GetOrganizationByUUID(orgId string) (*api.Organiza } if orgModel == nil { - return nil, constants.ErrOrganizationNotFound + return nil, apperror.OrganizationNotFound.New() } org, convErr := s.modelToAPI(orgModel) @@ -239,7 +239,7 @@ func (s *OrganizationService) GetOrganizationByHandle(handle string) (*api.Organ } if orgModel == nil { - return nil, constants.ErrOrganizationNotFound + return nil, apperror.OrganizationNotFound.New() } org, convErr := s.modelToAPI(orgModel) diff --git a/platform-api/internal/service/project.go b/platform-api/internal/service/project.go index ece7557f55..0d4ecc5b33 100644 --- a/platform-api/internal/service/project.go +++ b/platform-api/internal/service/project.go @@ -19,12 +19,12 @@ package service import ( "fmt" - "log/slog" "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" + "log/slog" "time" ) @@ -70,7 +70,7 @@ func (s *ProjectService) RegisterDeletionGuard(guard ProjectDeletionGuard) { func (s *ProjectService) CreateProject(req *api.CreateProjectRequest, organizationID, actor string) (*api.Project, error) { if req.DisplayName == "" { - return nil, constants.ErrInvalidProjectName + return nil, apperror.ValidationFailed.New("Project displayName is required") } org, err := s.orgRepo.GetOrganizationByUUID(organizationID) @@ -78,7 +78,7 @@ func (s *ProjectService) CreateProject(req *api.CreateProjectRequest, organizati return nil, err } if org == nil { - return nil, constants.ErrOrganizationNotFound + return nil, apperror.OrganizationNotFound.New() } // Determine handle: use provided id or auto-generate from displayName @@ -90,7 +90,7 @@ func (s *ProjectService) CreateProject(req *api.CreateProjectRequest, organizati return nil, err } if existing != nil { - return nil, constants.ErrProjectExists + return nil, apperror.ProjectExists.New() } } else { handle, err = utils.GenerateHandle(req.DisplayName, func(h string) bool { @@ -109,7 +109,7 @@ func (s *ProjectService) CreateProject(req *api.CreateProjectRequest, organizati } for _, p := range existingProjects { if p.Name == req.DisplayName { - return nil, constants.ErrProjectExists + return nil, apperror.ProjectExists.New() } } @@ -152,7 +152,7 @@ func (s *ProjectService) GetProjectByHandle(handle, orgId string) (*api.Project, return nil, err } if projectModel == nil { - return nil, constants.ErrProjectNotFound + return nil, apperror.ProjectNotFound.New() } org, err := s.orgRepo.GetOrganizationByUUID(projectModel.OrganizationID) @@ -173,7 +173,7 @@ func (s *ProjectService) GetProjectsByOrganization(organizationID string) ([]api return nil, err } if org == nil { - return nil, constants.ErrOrganizationNotFound + return nil, apperror.OrganizationNotFound.New() } projectModels, err := s.projectRepo.GetProjectsByOrganizationID(organizationID) @@ -204,7 +204,7 @@ func (s *ProjectService) UpdateProject(handle string, req *api.Project, orgId, a return nil, err } if project == nil { - return nil, constants.ErrProjectNotFound + return nil, apperror.ProjectNotFound.New() } if req.DisplayName != project.Name { @@ -214,7 +214,7 @@ func (s *ProjectService) UpdateProject(handle string, req *api.Project, orgId, a } for _, existingProject := range existingProjects { if existingProject.Name == req.DisplayName && existingProject.Handle != handle { - return nil, constants.ErrProjectExists + return nil, apperror.ProjectExists.New() } } project.Name = req.DisplayName @@ -249,7 +249,7 @@ func (s *ProjectService) DeleteProject(handle, orgId, actor string) error { return err } if project == nil { - return constants.ErrProjectNotFound + return apperror.ProjectNotFound.New() } projects, err := s.projectRepo.GetProjectsByOrganizationID(project.OrganizationID) @@ -257,7 +257,7 @@ func (s *ProjectService) DeleteProject(handle, orgId, actor string) error { return err } if len(projects) <= 1 { - return constants.ErrOrganizationMustHAveAtLeastOneProject + return apperror.ValidationFailed.New("Organization must have at least one project") } apis, err := s.apiRepo.GetAPIsByProjectUUID(project.ID, orgId) @@ -265,7 +265,7 @@ func (s *ProjectService) DeleteProject(handle, orgId, actor string) error { return err } if len(apis) > 0 { - return constants.ErrProjectHasAssociatedAPIs + return apperror.ValidationFailed.New("Project has associated APIs") } mcpProxiesCount, err := s.mcpProxyRepo.CountByProject(orgId, project.ID) @@ -273,7 +273,7 @@ func (s *ProjectService) DeleteProject(handle, orgId, actor string) error { return err } if mcpProxiesCount > 0 { - return constants.ErrProjectHasAssociatedMCPProxies + return apperror.ValidationFailed.New("Project has associated MCP proxies") } // applications no longer cascade-delete with the project (the project_uuid foreign key was @@ -284,7 +284,7 @@ func (s *ProjectService) DeleteProject(handle, orgId, actor string) error { return err } if appCount > 0 { - return constants.ErrProjectHasAssociatedApplications + return apperror.ValidationFailed.New("Project has associated applications") } for _, guard := range s.deletionGuards { diff --git a/platform-api/internal/service/secret_service.go b/platform-api/internal/service/secret_service.go index 49332f4757..0f260f6708 100644 --- a/platform-api/internal/service/secret_service.go +++ b/platform-api/internal/service/secret_service.go @@ -25,6 +25,7 @@ import ( "fmt" "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" @@ -90,7 +91,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, constants.ErrInvalidSecretType + return nil, apperror.ValidationFailed.Wrap(constants.ErrInvalidSecretType, "Invalid secret type: must be GENERIC or CERTIFICATE") } exists, err := s.repo.Exists(orgID, req.Handle) @@ -98,7 +99,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, constants.ErrSecretAlreadyExists + return nil, apperror.SecretExists.Wrap(constants.ErrSecretAlreadyExists) } ciphertext, err := s.vault.Encrypt(context.Background(), req.Value) @@ -237,7 +238,8 @@ func (s *SecretService) ValidateSecretRefs(orgID, configText string) error { } if len(missing) > 0 { - return fmt.Errorf("%w: %v", constants.ErrSecretRefMissing, missing) + return apperror.ValidationFailed.Wrap(fmt.Errorf("%w: %v", constants.ErrSecretRefMissing, missing), + "One or more referenced secrets do not exist") } return nil } diff --git a/platform-api/internal/service/subscription_plan_service.go b/platform-api/internal/service/subscription_plan_service.go index bd2b7828c1..23cf4dcc17 100644 --- a/platform-api/internal/service/subscription_plan_service.go +++ b/platform-api/internal/service/subscription_plan_service.go @@ -23,6 +23,7 @@ import ( "fmt" "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" @@ -92,7 +93,7 @@ func (s *SubscriptionPlanService) CreatePlan(orgUUID, actor string, plan *model. return nil, err } if exists { - return nil, constants.ErrSubscriptionPlanAlreadyExists + return nil, apperror.SubscriptionPlanExists.Wrap(constants.ErrSubscriptionPlanAlreadyExists) } plan.OrganizationUUID = orgUUID @@ -102,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, constants.ErrInvalidThrottleLimitUnit + return nil, apperror.ValidationFailed.Wrap(constants.ErrInvalidThrottleLimitUnit, "Invalid throttle limit unit") } if err := s.planRepo.Create(plan); err != nil { @@ -132,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, constants.ErrSubscriptionPlanNotFound + return nil, apperror.SubscriptionPlanNotFound.Wrap(constants.ErrSubscriptionPlanNotFound) } return nil, err } if plan == nil { - return nil, constants.ErrSubscriptionPlanNotFound + return nil, apperror.SubscriptionPlanNotFound.Wrap(constants.ErrSubscriptionPlanNotFound) } return plan, nil } @@ -157,12 +158,12 @@ func (s *SubscriptionPlanService) UpdatePlan(handle, orgUUID, actor string, upda existing, err := s.planRepo.GetByHandleAndOrg(handle, orgUUID) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, constants.ErrSubscriptionPlanNotFound + return nil, apperror.SubscriptionPlanNotFound.Wrap(constants.ErrSubscriptionPlanNotFound) } return nil, err } if existing == nil { - return nil, constants.ErrSubscriptionPlanNotFound + return nil, apperror.SubscriptionPlanNotFound.Wrap(constants.ErrSubscriptionPlanNotFound) } if update.Name != nil { @@ -183,7 +184,7 @@ func (s *SubscriptionPlanService) UpdatePlan(handle, orgUUID, actor string, upda } if update.ThrottleLimitUnit != nil { if !constants.ValidThrottleLimitUnits[*update.ThrottleLimitUnit] { - return nil, constants.ErrInvalidThrottleLimitUnit + return nil, apperror.ValidationFailed.Wrap(constants.ErrInvalidThrottleLimitUnit, "Invalid throttle limit unit") } existing.ThrottleLimitUnit = *update.ThrottleLimitUnit } @@ -227,12 +228,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 constants.ErrSubscriptionPlanNotFound + return apperror.SubscriptionPlanNotFound.Wrap(constants.ErrSubscriptionPlanNotFound) } return err } if existing == nil { - return constants.ErrSubscriptionPlanNotFound + return apperror.SubscriptionPlanNotFound.Wrap(constants.ErrSubscriptionPlanNotFound) } 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 5d4427075a..d8923822a2 100644 --- a/platform-api/internal/service/subscription_service.go +++ b/platform-api/internal/service/subscription_service.go @@ -23,6 +23,7 @@ import ( "fmt" "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" @@ -84,14 +85,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 "", constants.ErrAPINotFound + return "", apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) } 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 "", constants.ErrAPINotFound + return "", apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) } return metadata.ID, nil } @@ -99,7 +100,7 @@ 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 "", constants.ErrAPINotFound + return "", apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) } apiModel, err := s.apiRepo.GetAPIByUUID(apiId, orgUUID) if err != nil { @@ -113,12 +114,12 @@ 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 "", constants.ErrAPINotFound + return "", apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) } return "", fmt.Errorf("failed to resolve API by handle: %w", err) } if metadata == nil { - return "", constants.ErrAPINotFound + return "", apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound) } return metadata.ID, nil } @@ -172,14 +173,14 @@ func (s *SubscriptionService) CreateSubscription(apiId, kind, orgUUID string, su } if subscriberID == "" { - return nil, fmt.Errorf("subscriberId is required") + return nil, apperror.ValidationFailed.New("subscriberId is required") } exists, err := s.subscriptionRepo.ExistsByAPIAndSubscriber(apiUUID, subscriberID, orgUUID) if err != nil { return nil, err } if exists { - return nil, constants.ErrSubscriptionAlreadyExists + return nil, apperror.SubscriptionExists.Wrap(constants.ErrSubscriptionAlreadyExists) } // subscriptionPlanId carries the Developer Portal subscription plan handle. Resolve it to the @@ -188,12 +189,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, constants.ErrSubscriptionPlanNotFound + return nil, apperror.SubscriptionPlanNotFound.Wrap(constants.ErrSubscriptionPlanNotFound) } return nil, err } if plan == nil { - return nil, constants.ErrSubscriptionPlanNotFound + return nil, apperror.SubscriptionPlanNotFound.Wrap(constants.ErrSubscriptionPlanNotFound) } subscriptionPlanId = &plan.UUID } @@ -257,12 +258,12 @@ func (s *SubscriptionService) GetSubscription(subscriptionId, orgUUID string) (* sub, err := s.subscriptionRepo.GetByID(subscriptionId, orgUUID) if err != nil { if errors.Is(err, constants.ErrSubscriptionNotFound) { - return nil, constants.ErrSubscriptionNotFound + return nil, apperror.SubscriptionNotFound.Wrap(constants.ErrSubscriptionNotFound) } return nil, err } if sub == nil { - return nil, constants.ErrSubscriptionNotFound + return nil, apperror.SubscriptionNotFound.Wrap(constants.ErrSubscriptionNotFound) } return sub, nil } @@ -316,15 +317,15 @@ func (s *SubscriptionService) UpdateSubscription(subscriptionId, orgUUID, subscr sub, err := s.subscriptionRepo.GetByID(subscriptionId, orgUUID) if err != nil { if errors.Is(err, constants.ErrSubscriptionNotFound) { - return nil, constants.ErrSubscriptionNotFound + return nil, apperror.SubscriptionNotFound.Wrap(constants.ErrSubscriptionNotFound) } return nil, err } if sub == nil { - return nil, constants.ErrSubscriptionNotFound + return nil, apperror.SubscriptionNotFound.Wrap(constants.ErrSubscriptionNotFound) } if sub.SubscriberID != subscriberID { - return nil, constants.ErrSubscriptionSubscriberMismatch + return nil, apperror.SubscriptionForbidden.Wrap(constants.ErrSubscriptionSubscriberMismatch) } if status != "" { st := model.SubscriptionStatus(status) @@ -382,15 +383,15 @@ func (s *SubscriptionService) ChangePlan(subscriptionId, orgUUID, subscriberID, sub, err := s.subscriptionRepo.GetByID(subscriptionId, orgUUID) if err != nil { if errors.Is(err, constants.ErrSubscriptionNotFound) { - return nil, constants.ErrSubscriptionNotFound + return nil, apperror.SubscriptionNotFound.Wrap(constants.ErrSubscriptionNotFound) } return nil, err } if sub == nil { - return nil, constants.ErrSubscriptionNotFound + return nil, apperror.SubscriptionNotFound.Wrap(constants.ErrSubscriptionNotFound) } if sub.SubscriberID != subscriberID { - return nil, constants.ErrSubscriptionSubscriberMismatch + return nil, apperror.SubscriptionForbidden.Wrap(constants.ErrSubscriptionSubscriberMismatch) } // planHandle carries the Developer Portal subscription plan handle. Resolve it to the plan's @@ -398,12 +399,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, constants.ErrSubscriptionPlanNotFound + return nil, apperror.SubscriptionPlanNotFound.Wrap(constants.ErrSubscriptionPlanNotFound) } return nil, err } if planRecord == nil { - return nil, constants.ErrSubscriptionPlanNotFound + return nil, apperror.SubscriptionPlanNotFound.Wrap(constants.ErrSubscriptionPlanNotFound) } plan := planRecord.UUID sub.SubscriptionPlanID = &plan @@ -446,20 +447,20 @@ func (s *SubscriptionService) ChangePlan(subscriptionId, orgUUID, subscriberID, // deployed. subscriberID must match the stored subscriber_id. func (s *SubscriptionService) RegenerateToken(subscriptionId, orgUUID, subscriberID, newToken string) (*model.Subscription, error) { if newToken == "" { - return nil, fmt.Errorf("subscription token is required") + return nil, apperror.ValidationFailed.New("subscription token is required") } sub, err := s.subscriptionRepo.GetByID(subscriptionId, orgUUID) if err != nil { if errors.Is(err, constants.ErrSubscriptionNotFound) { - return nil, constants.ErrSubscriptionNotFound + return nil, apperror.SubscriptionNotFound.Wrap(constants.ErrSubscriptionNotFound) } return nil, err } if sub == nil { - return nil, constants.ErrSubscriptionNotFound + return nil, apperror.SubscriptionNotFound.Wrap(constants.ErrSubscriptionNotFound) } if sub.SubscriberID != subscriberID { - return nil, constants.ErrSubscriptionSubscriberMismatch + return nil, apperror.SubscriptionForbidden.Wrap(constants.ErrSubscriptionSubscriberMismatch) } if err := s.subscriptionRepo.UpdateToken(subscriptionId, orgUUID, newToken); err != nil { @@ -503,15 +504,15 @@ func (s *SubscriptionService) DeleteSubscription(subscriptionId, orgUUID, subscr sub, err := s.subscriptionRepo.GetByID(subscriptionId, orgUUID) if err != nil { if errors.Is(err, constants.ErrSubscriptionNotFound) { - return constants.ErrSubscriptionNotFound + return apperror.SubscriptionNotFound.Wrap(constants.ErrSubscriptionNotFound) } return err } if sub == nil { - return constants.ErrSubscriptionNotFound + return apperror.SubscriptionNotFound.Wrap(constants.ErrSubscriptionNotFound) } if sub.SubscriberID != subscriberID { - return constants.ErrSubscriptionSubscriberMismatch + return apperror.SubscriptionForbidden.Wrap(constants.ErrSubscriptionSubscriberMismatch) } if err := s.subscriptionRepo.Delete(subscriptionId, orgUUID); err != nil { diff --git a/platform-api/internal/utils/error_mapper.go b/platform-api/internal/utils/error_mapper.go deleted file mode 100644 index 5fff3a3de5..0000000000 --- a/platform-api/internal/utils/error_mapper.go +++ /dev/null @@ -1,171 +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 utils - -import ( - "errors" - "fmt" - "net/http" - "strings" - - "github.com/wso2/api-platform/platform-api/internal/constants" - - "github.com/go-playground/validator/v10" -) - -// makeError creates a standardized error response tuple -func makeError(status int, message string) (int, interface{}) { - return status, NewErrorResponse(status, http.StatusText(status), message) -} - -// FormatValidationError converts validator errors to user-friendly messages (public API) -func FormatValidationError(err error) string { - var validationErrors validator.ValidationErrors - if !errors.As(err, &validationErrors) { - return err.Error() // Not a validation error, return as-is - } - return formatValidationError(validationErrors) -} - -// formatValidationError converts ValidationErrors to user-friendly messages (internal) -func formatValidationError(validationErrors validator.ValidationErrors) string { - var messages []string - for _, fieldError := range validationErrors { - fieldName := getUserFriendlyFieldName(fieldError.Field()) - message := getValidationErrorMessage(fieldName, fieldError.Tag(), fieldError.Param()) - messages = append(messages, message) - } - return strings.Join(messages, "; ") -} - -// getUserFriendlyFieldName maps struct field names to user-friendly field names -func getUserFriendlyFieldName(fieldName string) string { - fieldMap := map[string]string{ - "Name": "name", - "Description": "description", - "APIID": "API ID", - "Provider": "provider", - "APIName": "API name", - "APIHandle": "API handle", - "APIDescription": "API description", - "APIVersion": "API version", - "APIType": "API type", - "APIStatus": "API status", - "ProductionURL": "production URL", - } - - if friendly, exists := fieldMap[fieldName]; exists { - return friendly - } - return strings.ToLower(fieldName) -} - -// getValidationErrorMessage creates user-friendly validation error messages -func getValidationErrorMessage(fieldName, tag, param string) string { - switch tag { - case "required": - return fmt.Sprintf("%s is required", fieldName) - case "min": - return fmt.Sprintf("%s must be at least %s characters long", fieldName, param) - case "max": - return fmt.Sprintf("%s must not exceed %s characters", fieldName, param) - case "url": - return fmt.Sprintf("%s must be a valid URL", fieldName) - case "hostname": - return fmt.Sprintf("%s must be a valid hostname", fieldName) - case "oneof": - return fmt.Sprintf("%s must be one of: %s", fieldName, strings.ReplaceAll(param, " ", ", ")) - default: - return fmt.Sprintf("%s is invalid", fieldName) - } -} - -// GetErrorResponse maps domain errors and validation errors to HTTP status and error response -func GetErrorResponse(err error) (int, interface{}) { - // First check if it's a validation error - var validationErrors validator.ValidationErrors - if errors.As(err, &validationErrors) { - userFriendlyMessage := formatValidationError(validationErrors) - return makeError(http.StatusBadRequest, userFriendlyMessage) - } - - // Handle domain/business logic errors - switch { - // Organization errors - case errors.Is(err, constants.ErrOrganizationNotFound): - return makeError(http.StatusNotFound, "Organization not found") - case errors.Is(err, constants.ErrOrganizationExists): - return makeError(http.StatusConflict, "Organization already exists with the given UUID") - case errors.Is(err, constants.ErrInvalidHandle): - return makeError(http.StatusBadRequest, "Invalid handle format") - case errors.Is(err, constants.ErrMultipleOrganizations): - return makeError(http.StatusInternalServerError, "Multiple organizations found") - - // Project errors - case errors.Is(err, constants.ErrProjectExists): - return makeError(http.StatusConflict, "Project already exists in organization") - case errors.Is(err, constants.ErrProjectNotFound): - return makeError(http.StatusNotFound, "Project not found") - case errors.Is(err, constants.ErrInvalidProjectName): - return makeError(http.StatusBadRequest, "Invalid project name") - case errors.Is(err, constants.ErrOrganizationMustHAveAtLeastOneProject): - return makeError(http.StatusBadRequest, "Organization must have at least one project") - case errors.Is(err, constants.ErrProjectHasAssociatedAPIs): - return makeError(http.StatusBadRequest, "Project has associated APIs") - case errors.Is(err, constants.ErrProjectHasAssociatedApplications): - return makeError(http.StatusBadRequest, "Project has associated applications") - - // API errors - case errors.Is(err, constants.ErrAPINotFound): - return makeError(http.StatusNotFound, "API not found") - case errors.Is(err, constants.ErrAPIAlreadyExists): - return makeError(http.StatusConflict, "API already exists in project") - case errors.Is(err, constants.ErrInvalidAPIContext): - return makeError(http.StatusBadRequest, "Invalid API context format") - case errors.Is(err, constants.ErrInvalidAPIVersion): - return makeError(http.StatusBadRequest, "Invalid API version format") - case errors.Is(err, constants.ErrInvalidAPIName): - return makeError(http.StatusBadRequest, "Invalid API name format") - case errors.Is(err, constants.ErrInvalidLifecycleState): - return makeError(http.StatusBadRequest, "Invalid lifecycle state") - case errors.Is(err, constants.ErrInvalidAPIType): - return makeError(http.StatusBadRequest, "Invalid API type") - case errors.Is(err, constants.ErrInvalidTransport): - return makeError(http.StatusBadRequest, "Invalid transport protocol") - case errors.Is(err, constants.ErrInvalidDeployment): - return makeError(http.StatusBadRequest, "Invalid API deployment") - case errors.Is(err, constants.ErrUpstreamRequired): - return makeError(http.StatusBadRequest, "Upstream configuration is required") - - // Artifact errors - case errors.Is(err, constants.ErrArtifactNotFound): - return makeError(http.StatusNotFound, "Artifact not found") - case errors.Is(err, constants.ErrArtifactExists): - return makeError(http.StatusConflict, "Artifact already exists") - case errors.Is(err, constants.ErrArtifactInvalidKind): - return makeError(http.StatusBadRequest, "Invalid artifact kind") - - // Gateway errors - case errors.Is(err, constants.ErrGatewayNotFound): - return makeError(http.StatusNotFound, "Gateway not found") - - // Default case for unknown errors - default: - return makeError(http.StatusInternalServerError, "Internal Server Error") - } -} diff --git a/platform-api/internal/webhook/receiver.go b/platform-api/internal/webhook/receiver.go index 86eb76178f..c9b9bc8a99 100644 --- a/platform-api/internal/webhook/receiver.go +++ b/platform-api/internal/webhook/receiver.go @@ -219,6 +219,12 @@ func (r *Receiver) resolveOrgUUID(env *Envelope) error { // 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. 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): return apperror.ValidationFailed.Wrap(err, "Failed to process event") From b052ad1f5c7fed41bef1455b0aa4e4db64f637c0 Mon Sep 17 00:00:00 2001 From: Thushani Jayasekera Date: Wed, 8 Jul 2026 13:07:30 +0530 Subject: [PATCH 08/12] Improve error handling in SyncCustomPolicy method by separating gateway retrieval errors. Now logs specific internal errors while maintaining clarity for gateway not found scenarios. --- platform-api/internal/service/gateway.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/platform-api/internal/service/gateway.go b/platform-api/internal/service/gateway.go index b2840f63a3..e64a68e19e 100644 --- a/platform-api/internal/service/gateway.go +++ b/platform-api/internal/service/gateway.go @@ -328,8 +328,11 @@ func (s *GatewayService) SyncCustomPolicy(gatewayID, orgID, policyName, version policyName = strings.ToLower(policyName) gateway, err := s.gatewayRepo.GetByUUID(gatewayID) - if err != nil || gateway == nil { - return nil, apperror.GatewayNotFound.Wrap(err) + if err != nil { + return nil, apperror.Internal.Wrap(err).WithLogMessage("failed to get gateway") + } + if gateway == nil { + return nil, apperror.GatewayNotFound.New() } if gateway.OrganizationID != orgID { return nil, apperror.GatewayNotFound.New().WithLogMessage("gateway belongs to a different organization") From c927a0b73fe9ca6ef7c0171768a5ae0d12f2737d Mon Sep 17 00:00:00 2001 From: Malintha Amarasinghe Date: Wed, 8 Jul 2026 13:36:41 +0530 Subject: [PATCH 09/12] Apply suggestions from code review Co-authored-by: Malintha Amarasinghe --- platform-api/internal/utils/codes.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platform-api/internal/utils/codes.go b/platform-api/internal/utils/codes.go index fd48ff6919..5bfd914d0d 100644 --- a/platform-api/internal/utils/codes.go +++ b/platform-api/internal/utils/codes.go @@ -24,8 +24,8 @@ import "net/http" // the fallback used by NewErrorResponse when a handler hasn't been migrated // to a more specific domain code via NewErrorResponseWithCode. const ( - CodeCommonValidationFailed = "COMMON_VALIDATION_FAILED" - CodeCommonUnauthorized = "COMMON_UNAUTHORIZED" + CodeCommonValidationFailed = "VALIDATION_FAILED" + CodeCommonUnauthorized = "UNAUTHORIZED" CodeCommonForbidden = "COMMON_FORBIDDEN" CodeCommonNotFound = "COMMON_NOT_FOUND" CodeCommonConflict = "COMMON_CONFLICT" From db510bc1d0ecdd02d5d1332c145ba21d8c87ab00 Mon Sep 17 00:00:00 2001 From: Thushani Jayasekera Date: Wed, 8 Jul 2026 15:37:43 +0530 Subject: [PATCH 10/12] Refactor error handling to use centralized error response structure This commit introduces a new error response structure across the application by replacing references to the previous utils.ErrorResponse with a unified apperror.ErrorResponse. It also updates error codes to use constants defined in the new codes.go file, enhancing consistency and maintainability in error handling. Additionally, various tests and handlers have been updated to reflect these changes, ensuring that error responses are standardized throughout the codebase. --- platform-api/internal/apperror/apperror.go | 34 ++-- .../internal/apperror/apperror_test.go | 14 +- platform-api/internal/apperror/catalog.go | 166 +++++++++--------- .../internal/{utils => apperror}/codes.go | 56 ++---- .../internal/{utils => apperror}/error.go | 32 +++- platform-api/internal/apperror/write.go | 6 +- platform-api/internal/dto/secret.go | 2 +- .../internal/handler/gateway_internal.go | 153 ++++++++-------- .../handler/llm_provider_integration_test.go | 4 +- .../internal/middleware/error_mapper.go | 7 +- .../internal/middleware/error_mapper_test.go | 21 ++- .../handler/artifact_guard_response.go | 6 +- .../eventgateway/handler/identity_helper.go | 4 +- .../eventgateway/handler/webbroker_api.go | 35 ++-- .../handler/webbroker_api_deployment.go | 56 +++--- .../eventgateway/handler/webbroker_apikey.go | 55 +++--- .../eventgateway/handler/websub_api.go | 35 ++-- .../handler/websub_api_deployment.go | 56 +++--- .../handler/websub_api_hmac_secret.go | 45 ++--- .../eventgateway/handler/websub_apikey.go | 55 +++--- platform-api/resources/openapi.yaml | 14 +- 21 files changed, 426 insertions(+), 430 deletions(-) rename platform-api/internal/{utils => apperror}/codes.go (79%) rename platform-api/internal/{utils => apperror}/error.go (73%) diff --git a/platform-api/internal/apperror/apperror.go b/platform-api/internal/apperror/apperror.go index 05b419ee5f..c5d1081305 100644 --- a/platform-api/internal/apperror/apperror.go +++ b/platform-api/internal/apperror/apperror.go @@ -29,25 +29,23 @@ import ( "runtime" "strings" - "github.com/wso2/api-platform/platform-api/internal/utils" - "github.com/go-playground/validator/v10" ) // Error carries everything the error mapper needs to log a failure and write -// the client-facing utils.ErrorResponse: the catalog code and HTTP status, +// the client-facing ErrorResponse: the catalog code and HTTP status, // the sanitized client message, optional field-level validation errors and // structured details, plus internal-only diagnostics (log message, wrapped // cause, and the call stack captured at construction time). type Error struct { - Code string // catalog code, e.g. utils.CodeCommonValidationFailed - HTTPStatus int // status to write - Message string // client-facing message - FieldErrors []utils.FieldError // optional, for validation failures - Details any // optional structured metadata - LogMessage string // internal-only detail; never sent to client - Cause error // wrapped original error, for logging/unwrapping - Stack []uintptr // captured at construction time, see New + Code string // catalog code, e.g. CodeCommonValidationFailed + HTTPStatus int // status to write + Message string // client-facing message + FieldErrors []FieldError // optional, for validation failures + Details any // optional structured metadata + LogMessage string // internal-only detail; never sent to client + Cause error // wrapped original error, for logging/unwrapping + Stack []uintptr // captured at construction time, see New } // Error satisfies the error interface. It intentionally includes only the @@ -63,24 +61,24 @@ func (e *Error) Error() string { func (e *Error) Unwrap() error { return e.Cause } // NewValidation converts a request-validation failure into an Error, -// mirroring utils.NewValidationErrorResponse: validator.ValidationErrors -// become field-level errors under COMMON_VALIDATION_FAILED; anything else +// mirroring NewValidationErrorResponse: validator.ValidationErrors +// become field-level errors under VALIDATION_FAILED; anything else // maps to the generic "Invalid input." message with the original error kept // as the cause for internal logging. func NewValidation(err error) *Error { var ve validator.ValidationErrors if errors.As(err, &ve) { - fieldErrors := make([]utils.FieldError, 0, len(ve)) + fieldErrors := make([]FieldError, 0, len(ve)) for _, fe := range ve { - fieldErrors = append(fieldErrors, utils.FieldError{ + fieldErrors = append(fieldErrors, FieldError{ Field: fe.Field(), Message: fmt.Sprintf("The field %s is %s", fe.Field(), fe.Tag()), }) } - return newWithSkip(3, utils.CodeCommonValidationFailed, http.StatusBadRequest, + return newWithSkip(3, CodeCommonValidationFailed, http.StatusBadRequest, "Validation failed for the request parameters.").WithFieldErrors(fieldErrors) } - return newWithSkip(3, utils.CodeCommonValidationFailed, http.StatusBadRequest, "Invalid input.").WithCause(err) + return newWithSkip(3, CodeCommonValidationFailed, http.StatusBadRequest, "Invalid input.").WithCause(err) } // newWithSkip captures the stack skipping the given number of frames so the @@ -110,7 +108,7 @@ func (e *Error) WithLogMessage(msg string) *Error { // WithFieldErrors attaches field-level validation errors surfaced to the // client in the errors[] array. -func (e *Error) WithFieldErrors(fe []utils.FieldError) *Error { +func (e *Error) WithFieldErrors(fe []FieldError) *Error { e.FieldErrors = fe return e } diff --git a/platform-api/internal/apperror/apperror_test.go b/platform-api/internal/apperror/apperror_test.go index bb08d18034..5b94f5420b 100644 --- a/platform-api/internal/apperror/apperror_test.go +++ b/platform-api/internal/apperror/apperror_test.go @@ -25,8 +25,6 @@ import ( "net/http/httptest" "strings" "testing" - - "github.com/wso2/api-platform/platform-api/internal/utils" ) func TestDefNewCapturesStack(t *testing.T) { @@ -45,7 +43,7 @@ func TestDefNewFormatsMessage(t *testing.T) { if e.Message != "API name is required" { t.Errorf("unexpected message %q", e.Message) } - if e.Code != utils.CodeCommonValidationFailed || e.HTTPStatus != http.StatusBadRequest { + if e.Code != CodeCommonValidationFailed || e.HTTPStatus != http.StatusBadRequest { t.Errorf("Def did not carry its declared code/status: %+v", e) } @@ -77,7 +75,7 @@ func TestDefWrapAttachesCause(t *testing.T) { if !errors.As(wrapped, &appErr) { t.Fatal("expected errors.As to find *Error through a %w wrap") } - if appErr.Code != utils.CodeCommonInternalError { + if appErr.Code != CodeCommonInternalError { t.Errorf("unexpected code %q", appErr.Code) } } @@ -100,8 +98,8 @@ func TestNewValidationFallback(t *testing.T) { if e.HTTPStatus != http.StatusBadRequest { t.Errorf("expected 400, got %d", e.HTTPStatus) } - if e.Code != utils.CodeCommonValidationFailed { - t.Errorf("expected %s, got %s", utils.CodeCommonValidationFailed, e.Code) + if e.Code != CodeCommonValidationFailed { + t.Errorf("expected %s, got %s", CodeCommonValidationFailed, e.Code) } if e.Message != "Invalid input." { t.Errorf("unexpected message %q", e.Message) @@ -116,11 +114,11 @@ func TestWriteHTTP(t *testing.T) { if rec.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d", rec.Code) } - var body utils.ErrorResponse + var body ErrorResponse if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("invalid JSON body: %v", err) } - if body.Status != "error" || body.Code != utils.CodeProjectNotFound { + if body.Status != "error" || body.Code != CodeProjectNotFound { t.Errorf("unexpected envelope: %+v", body) } if strings.Contains(rec.Body.String(), "sql:") || strings.Contains(rec.Body.String(), "internal detail") { diff --git a/platform-api/internal/apperror/catalog.go b/platform-api/internal/apperror/catalog.go index d1ab40898a..b9cbeffead 100644 --- a/platform-api/internal/apperror/catalog.go +++ b/platform-api/internal/apperror/catalog.go @@ -19,15 +19,13 @@ package apperror import ( "net/http" - - "github.com/wso2/api-platform/platform-api/internal/utils" ) // This file is the error catalog: every client-facing error condition is // declared here exactly once, binding its code, HTTP status, and message // template together (see resources/ERROR_CATALOG_PLAN.md). Codes reference -// the string constants in utils/codes.go, which stay the single source for -// the code values while handlers not yet on the error-mapper pattern still +// the string constants in codes.go, which stay the single source for the +// code values while handlers not yet on the error-mapper pattern still // build responses from them directly. // // Message templates are client-sterile: no internal detail, no file paths, @@ -53,138 +51,138 @@ func def(code string, status int, messageFmt string) Def { // auth-failure rule in error-handling.md; the specific reason travels in the // wrapped cause / log message only. var ( - Unauthorized = def(utils.CodeCommonUnauthorized, http.StatusUnauthorized, "Invalid or expired credentials.") - Forbidden = def(utils.CodeCommonForbidden, http.StatusForbidden, "You do not have permission to perform this action.") - ValidationFailed = def(utils.CodeCommonValidationFailed, http.StatusBadRequest, "%s") - NotFound = def(utils.CodeCommonNotFound, http.StatusNotFound, "The requested resource could not be found.") - Conflict = def(utils.CodeCommonConflict, http.StatusConflict, "The request conflicts with the current state of the resource.") - NotAcceptable = def(utils.CodeCommonNotAcceptable, http.StatusNotAcceptable, "The requested media type is not supported.") - UnprocessableEntity = def(utils.CodeCommonUnprocessableEntity, http.StatusUnprocessableEntity, "The request could not be processed.") - Internal = def(utils.CodeCommonInternalError, http.StatusInternalServerError, "An unexpected error occurred.") - ServiceUnavailable = def(utils.CodeCommonServiceUnavailable, http.StatusServiceUnavailable, "The service is temporarily unavailable.") - TooManyRequests = def(utils.CodeCommonTooManyRequests, http.StatusTooManyRequests, "%s") + Unauthorized = def(CodeCommonUnauthorized, http.StatusUnauthorized, "Invalid or expired credentials.") + Forbidden = def(CodeCommonForbidden, http.StatusForbidden, "You do not have permission to perform this action.") + ValidationFailed = def(CodeCommonValidationFailed, http.StatusBadRequest, "%s") + NotFound = def(CodeCommonNotFound, http.StatusNotFound, "The requested resource could not be found.") + Conflict = def(CodeCommonConflict, http.StatusConflict, "The request conflicts with the current state of the resource.") + NotAcceptable = def(CodeCommonNotAcceptable, http.StatusNotAcceptable, "The requested media type is not supported.") + UnprocessableEntity = def(CodeCommonUnprocessableEntity, http.StatusUnprocessableEntity, "The request could not be processed.") + Internal = def(CodeCommonInternalError, http.StatusInternalServerError, "An unexpected error occurred.") + ServiceUnavailable = def(CodeCommonServiceUnavailable, http.StatusServiceUnavailable, "The service is temporarily unavailable.") + TooManyRequests = def(CodeCommonTooManyRequests, http.StatusTooManyRequests, "%s") ) // REST API entries. var ( - RESTAPINotFound = def(utils.CodeRESTAPINotFound, http.StatusNotFound, "The specified REST API could not be found.") + RESTAPINotFound = def(CodeRESTAPINotFound, http.StatusNotFound, "The specified REST API could not be found.") // RESTAPIExists covers three distinct conflicts (handle, name+version, // project) — the call site supplies which one. - RESTAPIExists = def(utils.CodeRESTAPIExists, http.StatusConflict, "%s") - RESTAPIDeploymentValidationFailed = def(utils.CodeRESTAPIDeploymentValidationFailed, http.StatusBadRequest, "%s") + RESTAPIExists = def(CodeRESTAPIExists, http.StatusConflict, "%s") + RESTAPIDeploymentValidationFailed = def(CodeRESTAPIDeploymentValidationFailed, http.StatusBadRequest, "%s") ) // LLM provider / proxy entries. var ( - LLMProviderNotFound = def(utils.CodeLLMProviderNotFound, http.StatusNotFound, "The specified LLM provider could not be found.") - LLMProviderRefNotFound = def(utils.CodeLLMProviderRefNotFound, http.StatusBadRequest, "The referenced LLM provider could not be found.") - LLMProviderExists = def(utils.CodeLLMProviderExists, http.StatusConflict, "An LLM provider with this ID already exists.") - LLMProviderLimitReached = def(utils.CodeLLMProviderLimitReached, http.StatusConflict, "LLM provider limit reached for the organization.") - LLMProviderAPIKeyNotFound = def(utils.CodeLLMProviderAPIKeyNotFound, http.StatusNotFound, "The specified API key could not be found.") - LLMProviderAPIKeyForbidden = def(utils.CodeLLMProviderAPIKeyForbidden, http.StatusForbidden, "You do not have permission to access this API key.") - LLMProviderDeploymentValidationFailed = def(utils.CodeLLMProviderDeploymentValidationFailed, http.StatusBadRequest, "%s") - - LLMProxyNotFound = def(utils.CodeLLMProxyNotFound, http.StatusNotFound, "The specified LLM proxy could not be found.") - LLMProxyExists = def(utils.CodeLLMProxyExists, http.StatusConflict, "An LLM proxy with this ID already exists.") - LLMProxyLimitReached = def(utils.CodeLLMProxyLimitReached, http.StatusConflict, "LLM proxy limit reached for the organization.") - LLMProxyAPIKeyNotFound = def(utils.CodeLLMProxyAPIKeyNotFound, http.StatusNotFound, "The specified API key could not be found.") - LLMProxyAPIKeyForbidden = def(utils.CodeLLMProxyAPIKeyForbidden, http.StatusForbidden, "You do not have permission to access this API key.") - LLMProxyDeploymentValidationFailed = def(utils.CodeLLMProxyDeploymentValidationFailed, http.StatusBadRequest, "%s") + LLMProviderNotFound = def(CodeLLMProviderNotFound, http.StatusNotFound, "The specified LLM provider could not be found.") + LLMProviderRefNotFound = def(CodeLLMProviderRefNotFound, http.StatusBadRequest, "The referenced LLM provider could not be found.") + LLMProviderExists = def(CodeLLMProviderExists, http.StatusConflict, "An LLM provider with this ID already exists.") + LLMProviderLimitReached = def(CodeLLMProviderLimitReached, http.StatusConflict, "LLM provider limit reached for the organization.") + LLMProviderAPIKeyNotFound = def(CodeLLMProviderAPIKeyNotFound, http.StatusNotFound, "The specified API key could not be found.") + LLMProviderAPIKeyForbidden = def(CodeLLMProviderAPIKeyForbidden, http.StatusForbidden, "You do not have permission to access this API key.") + LLMProviderDeploymentValidationFailed = def(CodeLLMProviderDeploymentValidationFailed, http.StatusBadRequest, "%s") + + LLMProxyNotFound = def(CodeLLMProxyNotFound, http.StatusNotFound, "The specified LLM proxy could not be found.") + LLMProxyExists = def(CodeLLMProxyExists, http.StatusConflict, "An LLM proxy with this ID already exists.") + LLMProxyLimitReached = def(CodeLLMProxyLimitReached, http.StatusConflict, "LLM proxy limit reached for the organization.") + LLMProxyAPIKeyNotFound = def(CodeLLMProxyAPIKeyNotFound, http.StatusNotFound, "The specified API key could not be found.") + LLMProxyAPIKeyForbidden = def(CodeLLMProxyAPIKeyForbidden, http.StatusForbidden, "You do not have permission to access this API key.") + LLMProxyDeploymentValidationFailed = def(CodeLLMProxyDeploymentValidationFailed, http.StatusBadRequest, "%s") ) // LLM provider template entries. var ( - LLMProviderTemplateNotFound = def(utils.CodeLLMProviderTemplateNotFound, http.StatusNotFound, "The specified LLM provider template could not be found.") - LLMProviderTemplateVersionNotFound = def(utils.CodeLLMProviderTemplateVersionNotFound, http.StatusNotFound, "The specified LLM provider template version could not be found.") - LLMProviderTemplateRefNotFound = def(utils.CodeLLMProviderTemplateRefNotFound, http.StatusBadRequest, "The referenced LLM provider template could not be found.") - LLMProviderTemplateExists = def(utils.CodeLLMProviderTemplateExists, http.StatusConflict, "An LLM provider template with this ID already exists.") - LLMProviderTemplateVersionExists = def(utils.CodeLLMProviderTemplateVersionExists, http.StatusConflict, "This template version already exists.") - LLMProviderTemplateManagedByReserved = def(utils.CodeLLMProviderTemplateManagedByReserved, http.StatusBadRequest, "'wso2' is reserved and cannot be used as managedBy on custom templates.") - LLMProviderTemplateInUse = def(utils.CodeLLMProviderTemplateInUse, http.StatusConflict, "This template version is in use by one or more providers.") - LLMProviderTemplateReadOnly = def(utils.CodeLLMProviderTemplateReadOnly, http.StatusForbidden, "Built-in templates are read-only and cannot be modified.") - LLMProviderTemplateNotToggleable = def(utils.CodeLLMProviderTemplateNotToggleable, http.StatusForbidden, "Only built-in templates can be enabled or disabled.") + LLMProviderTemplateNotFound = def(CodeLLMProviderTemplateNotFound, http.StatusNotFound, "The specified LLM provider template could not be found.") + LLMProviderTemplateVersionNotFound = def(CodeLLMProviderTemplateVersionNotFound, http.StatusNotFound, "The specified LLM provider template version could not be found.") + LLMProviderTemplateRefNotFound = def(CodeLLMProviderTemplateRefNotFound, http.StatusBadRequest, "The referenced LLM provider template could not be found.") + LLMProviderTemplateExists = def(CodeLLMProviderTemplateExists, http.StatusConflict, "An LLM provider template with this ID already exists.") + LLMProviderTemplateVersionExists = def(CodeLLMProviderTemplateVersionExists, http.StatusConflict, "This template version already exists.") + LLMProviderTemplateManagedByReserved = def(CodeLLMProviderTemplateManagedByReserved, http.StatusBadRequest, "'wso2' is reserved and cannot be used as managedBy on custom templates.") + LLMProviderTemplateInUse = def(CodeLLMProviderTemplateInUse, http.StatusConflict, "This template version is in use by one or more providers.") + LLMProviderTemplateReadOnly = def(CodeLLMProviderTemplateReadOnly, http.StatusForbidden, "Built-in templates are read-only and cannot be modified.") + LLMProviderTemplateNotToggleable = def(CodeLLMProviderTemplateNotToggleable, http.StatusForbidden, "Only built-in templates can be enabled or disabled.") ) // Gateway entries. var ( - GatewayNotFound = def(utils.CodeGatewayNotFound, http.StatusNotFound, "The specified gateway could not be found.") - GatewayTokenNotFound = def(utils.CodeGatewayTokenNotFound, http.StatusNotFound, "The specified gateway token could not be found.") - GatewayConnectionUnavailable = def(utils.CodeGatewayConnectionUnavailable, http.StatusServiceUnavailable, "No gateway connections are currently available.") - GatewayHasActiveDeployments = def(utils.CodeGatewayHasActiveDeployments, http.StatusConflict, "The gateway has active deployments and cannot be deleted.") - GatewayNameConflict = def(utils.CodeGatewayNameConflict, http.StatusConflict, "A gateway with this name already exists.") - GatewayTokenLimitReached = def(utils.CodeGatewayTokenLimitReached, http.StatusConflict, "Gateway token limit reached.") + GatewayNotFound = def(CodeGatewayNotFound, http.StatusNotFound, "The specified gateway could not be found.") + GatewayTokenNotFound = def(CodeGatewayTokenNotFound, http.StatusNotFound, "The specified gateway token could not be found.") + GatewayConnectionUnavailable = def(CodeGatewayConnectionUnavailable, http.StatusServiceUnavailable, "No gateway connections are currently available.") + GatewayHasActiveDeployments = def(CodeGatewayHasActiveDeployments, http.StatusConflict, "The gateway has active deployments and cannot be deleted.") + GatewayNameConflict = def(CodeGatewayNameConflict, http.StatusConflict, "A gateway with this name already exists.") + GatewayTokenLimitReached = def(CodeGatewayTokenLimitReached, http.StatusConflict, "Gateway token limit reached.") ) // Deployment entries, shared across REST API / LLM provider / LLM proxy / // MCP proxy deployment operations. DeploymentNotActive's verb is the artifact // kind, e.g. "API", "LLM provider". var ( - DeploymentBaseNotFound = def(utils.CodeDeploymentBaseNotFound, http.StatusNotFound, "The specified base deployment could not be found.") - DeploymentRestoreConflict = def(utils.CodeDeploymentRestoreConflict, http.StatusConflict, "Cannot restore the currently deployed deployment, or the deployment is invalid.") - DeploymentNotFound = def(utils.CodeDeploymentNotFound, http.StatusNotFound, "The specified deployment could not be found.") - DeploymentNotActive = def(utils.CodeDeploymentNotActive, http.StatusConflict, "No active deployment found for this %s on the gateway.") - DeploymentGatewayMismatch = def(utils.CodeDeploymentGatewayMismatch, http.StatusBadRequest, "Deployment is bound to a different gateway.") - DeploymentActive = def(utils.CodeDeploymentActive, http.StatusConflict, "Cannot delete an active deployment - undeploy it first.") - DeploymentInvalidStatus = def(utils.CodeDeploymentInvalidStatus, http.StatusBadRequest, "The specified deployment status filter is invalid.") + DeploymentBaseNotFound = def(CodeDeploymentBaseNotFound, http.StatusNotFound, "The specified base deployment could not be found.") + DeploymentRestoreConflict = def(CodeDeploymentRestoreConflict, http.StatusConflict, "Cannot restore the currently deployed deployment, or the deployment is invalid.") + DeploymentNotFound = def(CodeDeploymentNotFound, http.StatusNotFound, "The specified deployment could not be found.") + DeploymentNotActive = def(CodeDeploymentNotActive, http.StatusConflict, "No active deployment found for this %s on the gateway.") + DeploymentGatewayMismatch = def(CodeDeploymentGatewayMismatch, http.StatusBadRequest, "Deployment is bound to a different gateway.") + DeploymentActive = def(CodeDeploymentActive, http.StatusConflict, "Cannot delete an active deployment - undeploy it first.") + DeploymentInvalidStatus = def(CodeDeploymentInvalidStatus, http.StatusBadRequest, "The specified deployment status filter is invalid.") ) // MCP proxy entries. var ( - MCPProxyNotFound = def(utils.CodeMCPProxyNotFound, http.StatusNotFound, "The specified MCP proxy could not be found.") - MCPProxyExists = def(utils.CodeMCPProxyExists, http.StatusConflict, "An MCP proxy with this ID already exists.") - MCPProxyLimitReached = def(utils.CodeMCPProxyLimitReached, http.StatusConflict, "MCP proxy limit reached for the organization.") - MCPProxyDeploymentValidationFailed = def(utils.CodeMCPProxyDeploymentValidationFailed, http.StatusBadRequest, "%s") + MCPProxyNotFound = def(CodeMCPProxyNotFound, http.StatusNotFound, "The specified MCP proxy could not be found.") + MCPProxyExists = def(CodeMCPProxyExists, http.StatusConflict, "An MCP proxy with this ID already exists.") + MCPProxyLimitReached = def(CodeMCPProxyLimitReached, http.StatusConflict, "MCP proxy limit reached for the organization.") + MCPProxyDeploymentValidationFailed = def(CodeMCPProxyDeploymentValidationFailed, http.StatusBadRequest, "%s") ) // Organization / project / application entries. var ( - OrganizationNotFound = def(utils.CodeOrganizationNotFound, http.StatusNotFound, "The specified organization could not be found.") - OrganizationExists = def(utils.CodeOrganizationExists, http.StatusConflict, "An organization with this name already exists.") - ProjectNotFound = def(utils.CodeProjectNotFound, http.StatusNotFound, "The specified project could not be found.") - ProjectRefNotFound = def(utils.CodeProjectRefNotFound, http.StatusBadRequest, "The referenced project could not be found.") - ProjectExists = def(utils.CodeProjectExists, http.StatusConflict, "A project with this name already exists in the organization.") - ApplicationNotFound = def(utils.CodeApplicationNotFound, http.StatusNotFound, "The specified application could not be found.") - ApplicationExists = def(utils.CodeApplicationExists, http.StatusConflict, "An application with this name already exists.") + OrganizationNotFound = def(CodeOrganizationNotFound, http.StatusNotFound, "The specified organization could not be found.") + OrganizationExists = def(CodeOrganizationExists, http.StatusConflict, "An organization with this name already exists.") + ProjectNotFound = def(CodeProjectNotFound, http.StatusNotFound, "The specified project could not be found.") + ProjectRefNotFound = def(CodeProjectRefNotFound, http.StatusBadRequest, "The referenced project could not be found.") + ProjectExists = def(CodeProjectExists, http.StatusConflict, "A project with this name already exists in the organization.") + ApplicationNotFound = def(CodeApplicationNotFound, http.StatusNotFound, "The specified application could not be found.") + ApplicationExists = def(CodeApplicationExists, http.StatusConflict, "An application with this name already exists.") ) // Subscription entries. var ( - SubscriptionNotFound = def(utils.CodeSubscriptionNotFound, http.StatusNotFound, "The specified subscription could not be found.") - SubscriptionExists = def(utils.CodeSubscriptionExists, http.StatusConflict, "A subscription for this application and API already exists.") - SubscriptionForbidden = def(utils.CodeSubscriptionForbidden, http.StatusForbidden, "You do not have permission to access this subscription.") - SubscriptionPlanNotFound = def(utils.CodeSubscriptionPlanNotFound, http.StatusNotFound, "The specified subscription plan could not be found.") - SubscriptionPlanExists = def(utils.CodeSubscriptionPlanExists, http.StatusConflict, "A subscription plan with this name already exists.") + SubscriptionNotFound = def(CodeSubscriptionNotFound, http.StatusNotFound, "The specified subscription could not be found.") + SubscriptionExists = def(CodeSubscriptionExists, http.StatusConflict, "A subscription for this application and API already exists.") + SubscriptionForbidden = def(CodeSubscriptionForbidden, http.StatusForbidden, "You do not have permission to access this subscription.") + SubscriptionPlanNotFound = def(CodeSubscriptionPlanNotFound, http.StatusNotFound, "The specified subscription plan could not be found.") + SubscriptionPlanExists = def(CodeSubscriptionPlanExists, http.StatusConflict, "A subscription plan with this name already exists.") ) // Custom policy entries. var ( - PolicyVersionConflict = def(utils.CodePolicyVersionConflict, http.StatusConflict, "The policy version conflicts with an existing version.") - PolicyInvalidState = def(utils.CodePolicyInvalidState, http.StatusConflict, "The policy is not in a valid state for this operation.") - PolicyInUse = def(utils.CodePolicyInUse, http.StatusConflict, "The policy is in use and cannot be deleted.") - CustomPolicyNotFound = def(utils.CodeCustomPolicyNotFound, http.StatusNotFound, "The specified policy could not be found.") - CustomPolicyVersionNotFnd = def(utils.CodeCustomPolicyVersionNotFound, http.StatusNotFound, "The specified policy version could not be found.") + PolicyVersionConflict = def(CodePolicyVersionConflict, http.StatusConflict, "The policy version conflicts with an existing version.") + PolicyInvalidState = def(CodePolicyInvalidState, http.StatusConflict, "The policy is not in a valid state for this operation.") + PolicyInUse = def(CodePolicyInUse, http.StatusConflict, "The policy is in use and cannot be deleted.") + CustomPolicyNotFound = def(CodeCustomPolicyNotFound, http.StatusNotFound, "The specified policy could not be found.") + CustomPolicyVersionNotFnd = def(CodeCustomPolicyVersionNotFound, http.StatusNotFound, "The specified policy version could not be found.") ) // Secret entries. var ( - SecretNotFound = def(utils.CodeSecretNotFound, http.StatusNotFound, "The specified secret could not be found.") - SecretExists = def(utils.CodeSecretExists, http.StatusConflict, "A secret with this name already exists.") - SecretInUse = def(utils.CodeSecretInUse, http.StatusConflict, "The secret is in use and cannot be deleted.") + SecretNotFound = def(CodeSecretNotFound, http.StatusNotFound, "The specified secret could not be found.") + SecretExists = def(CodeSecretExists, http.StatusConflict, "A secret with this name already exists.") + SecretInUse = def(CodeSecretInUse, http.StatusConflict, "The secret is in use and cannot be deleted.") ) // Artifact entries — generic artifact-reference flows and the // data-plane-origin guard. The guard produces client-appropriate messages at // the call site, so these are passthrough templates. var ( - ArtifactNotFound = def(utils.CodeArtifactNotFound, http.StatusNotFound, "The specified artifact could not be found.") - ArtifactExists = def(utils.CodeArtifactExists, http.StatusConflict, "The artifact already exists.") - ArtifactReadOnly = def(utils.CodeArtifactReadOnly, http.StatusForbidden, "%s") - ArtifactRuntimeImmutable = def(utils.CodeArtifactRuntimeImmutable, http.StatusForbidden, "%s") - ArtifactDeployed = def(utils.CodeArtifactDeployed, http.StatusConflict, "%s") + ArtifactNotFound = def(CodeArtifactNotFound, http.StatusNotFound, "The specified artifact could not be found.") + ArtifactExists = def(CodeArtifactExists, http.StatusConflict, "The artifact already exists.") + ArtifactReadOnly = def(CodeArtifactReadOnly, http.StatusForbidden, "%s") + ArtifactRuntimeImmutable = def(CodeArtifactRuntimeImmutable, http.StatusForbidden, "%s") + ArtifactDeployed = def(CodeArtifactDeployed, http.StatusConflict, "%s") ) // Application API key entries. var ( - ApplicationAPIKeyNotFound = def(utils.CodeApplicationAPIKeyNotFound, http.StatusNotFound, "The specified API key could not be found.") - ApplicationAPIKeyForbidden = def(utils.CodeApplicationAPIKeyForbidden, http.StatusForbidden, "You do not have permission to access this API key.") + 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.") ) diff --git a/platform-api/internal/utils/codes.go b/platform-api/internal/apperror/codes.go similarity index 79% rename from platform-api/internal/utils/codes.go rename to platform-api/internal/apperror/codes.go index 5bfd914d0d..28a7f018fd 100644 --- a/platform-api/internal/utils/codes.go +++ b/platform-api/internal/apperror/codes.go @@ -15,52 +15,24 @@ * */ -package utils +package apperror -import "net/http" - -// Common, domain-agnostic catalog codes. These mirror the COMMON_* codes -// documented as shared response examples in resources/openapi.yaml, and are -// the fallback used by NewErrorResponse when a handler hasn't been migrated -// to a more specific domain code via NewErrorResponseWithCode. +// Common, domain-agnostic catalog codes. These mirror the codes documented +// as shared response examples in resources/openapi.yaml, and are +// the fallback used by NewErrorResponse when a handler hasn't been +// migrated to a more specific domain code via NewErrorResponseWithCode. const ( CodeCommonValidationFailed = "VALIDATION_FAILED" CodeCommonUnauthorized = "UNAUTHORIZED" - CodeCommonForbidden = "COMMON_FORBIDDEN" - CodeCommonNotFound = "COMMON_NOT_FOUND" - CodeCommonConflict = "COMMON_CONFLICT" - CodeCommonNotAcceptable = "COMMON_NOT_ACCEPTABLE" - CodeCommonUnprocessableEntity = "COMMON_UNPROCESSABLE_ENTITY" - CodeCommonInternalError = "COMMON_INTERNAL_ERROR" - CodeCommonServiceUnavailable = "COMMON_SERVICE_UNAVAILABLE" - CodeCommonTooManyRequests = "COMMON_TOO_MANY_REQUESTS" -) - -// commonCodeByStatus maps an HTTP status code to its fallback catalog code. -// 400 maps to COMMON_VALIDATION_FAILED (not a separate COMMON_BAD_REQUEST) to -// match the single code documented for the shared BadRequest response in -// resources/openapi.yaml. -var commonCodeByStatus = map[int]string{ - http.StatusBadRequest: CodeCommonValidationFailed, - http.StatusUnauthorized: CodeCommonUnauthorized, - http.StatusForbidden: CodeCommonForbidden, - http.StatusNotFound: CodeCommonNotFound, - http.StatusConflict: CodeCommonConflict, - http.StatusNotAcceptable: CodeCommonNotAcceptable, - http.StatusUnprocessableEntity: CodeCommonUnprocessableEntity, - http.StatusInternalServerError: CodeCommonInternalError, - http.StatusServiceUnavailable: CodeCommonServiceUnavailable, - http.StatusTooManyRequests: CodeCommonTooManyRequests, -} - -// codeForStatus returns the fallback catalog code for an HTTP status, -// defaulting to COMMON_INTERNAL_ERROR for unmapped statuses. -func codeForStatus(httpStatus int) string { - if code, ok := commonCodeByStatus[httpStatus]; ok { - return code - } - return CodeCommonInternalError -} + CodeCommonForbidden = "FORBIDDEN" + CodeCommonNotFound = "NOT_FOUND" + CodeCommonConflict = "CONFLICT" + CodeCommonNotAcceptable = "NOT_ACCEPTABLE" + CodeCommonUnprocessableEntity = "UNPROCESSABLE_ENTITY" + CodeCommonInternalError = "INTERNAL_ERROR" + CodeCommonServiceUnavailable = "SERVICE_UNAVAILABLE" + CodeCommonTooManyRequests = "TOO_MANY_REQUESTS" +) // LLM provider/proxy domain codes, matching the examples documented in // resources/openapi.yaml for the corresponding operations. diff --git a/platform-api/internal/utils/error.go b/platform-api/internal/apperror/error.go similarity index 73% rename from platform-api/internal/utils/error.go rename to platform-api/internal/apperror/error.go index ff95a0c566..a37fc5fdcf 100644 --- a/platform-api/internal/utils/error.go +++ b/platform-api/internal/apperror/error.go @@ -15,7 +15,7 @@ * */ -package utils +package apperror import ( "encoding/json" @@ -52,10 +52,38 @@ type FieldError struct { Message string `json:"message"` } +// commonCodeByStatus maps an HTTP status code to its fallback catalog code, +// for handlers that build an ErrorResponse via NewErrorResponse instead of +// picking a specific catalog code via NewErrorResponseWithCode. 400 maps to +// CodeCommonValidationFailed (not a separate BAD_REQUEST) to match the +// single code documented for the shared BadRequest response in +// resources/openapi.yaml. +var commonCodeByStatus = map[int]string{ + http.StatusBadRequest: CodeCommonValidationFailed, + http.StatusUnauthorized: CodeCommonUnauthorized, + http.StatusForbidden: CodeCommonForbidden, + http.StatusNotFound: CodeCommonNotFound, + http.StatusConflict: CodeCommonConflict, + http.StatusNotAcceptable: CodeCommonNotAcceptable, + http.StatusUnprocessableEntity: CodeCommonUnprocessableEntity, + http.StatusInternalServerError: CodeCommonInternalError, + http.StatusServiceUnavailable: CodeCommonServiceUnavailable, + http.StatusTooManyRequests: CodeCommonTooManyRequests, +} + +// codeForStatus returns the fallback catalog code for an HTTP status, +// defaulting to CodeCommonInternalError for unmapped statuses. +func codeForStatus(httpStatus int) string { + if code, ok := commonCodeByStatus[httpStatus]; ok { + return code + } + return CodeCommonInternalError +} + // NewErrorResponse builds a standard ErrorResponse for the given HTTP status. // title is retained for call-site compatibility but is no longer surfaced in // the response body; the catalog code is derived from httpStatus (see -// codeForStatus in codes.go), and description[0], when given, becomes the +// codeForStatus above), and description[0], when given, becomes the // human-readable message. Handlers that need a specific catalog code should // use NewErrorResponseWithCode instead. func NewErrorResponse(httpStatus int, title string, description ...string) ErrorResponse { diff --git a/platform-api/internal/apperror/write.go b/platform-api/internal/apperror/write.go index 51d8402ac9..3026213ce3 100644 --- a/platform-api/internal/apperror/write.go +++ b/platform-api/internal/apperror/write.go @@ -20,13 +20,11 @@ package apperror import ( "net/http" - "github.com/wso2/api-platform/platform-api/internal/utils" - "github.com/wso2/go-httpkit/httputil" ) // WriteHTTP is the one place in the codebase that serializes an *Error onto -// an http.ResponseWriter as the standard utils.ErrorResponse shape. Both the +// an http.ResponseWriter as the standard ErrorResponse shape. Both the // error mapper (middleware.MapErrors) and the pre-routing auth middleware // call it, so the wire format cannot drift between the two paths. // @@ -34,7 +32,7 @@ import ( // can quote it back for log correlation; the mapper passes it only for 5xx // responses. Callers with no correlation ID to expose pass "". func WriteHTTP(w http.ResponseWriter, e *Error, trackingID string) { - httputil.WriteJSON(w, e.HTTPStatus, utils.ErrorResponse{ + httputil.WriteJSON(w, e.HTTPStatus, ErrorResponse{ Status: "error", Code: e.Code, Message: e.Message, diff --git a/platform-api/internal/dto/secret.go b/platform-api/internal/dto/secret.go index c35edde45f..ac1aef88e4 100644 --- a/platform-api/internal/dto/secret.go +++ b/platform-api/internal/dto/secret.go @@ -90,7 +90,7 @@ type SecretSyncListResponse struct { // SecretReferenceDTO identifies a resource that references a secret. Carried // in the standard error response's `details.references` when a delete is -// blocked by SECRET_IN_USE (see utils.ErrorResponse.Details). +// blocked by SECRET_IN_USE (see apperror.ErrorResponse.Details). type SecretReferenceDTO struct { Type string `json:"type"` Handle string `json:"handle"` diff --git a/platform-api/internal/handler/gateway_internal.go b/platform-api/internal/handler/gateway_internal.go index c0cbaa7a35..ae67b01c34 100644 --- a/platform-api/internal/handler/gateway_internal.go +++ b/platform-api/internal/handler/gateway_internal.go @@ -21,12 +21,13 @@ import ( "encoding/json" "errors" "fmt" - "log/slog" - "net/http" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/constants" "github.com/wso2/api-platform/platform-api/internal/dto" "github.com/wso2/api-platform/platform-api/internal/model" "github.com/wso2/api-platform/platform-api/internal/utils" + "log/slog" + "net/http" "strconv" "strings" "time" @@ -93,15 +94,15 @@ func (h *GatewayInternalAPIHandler) authenticateRequest(w http.ResponseWriter, r 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, utils.NewErrorResponse(401, "Unauthorized", + 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, utils.NewErrorResponse(401, "Unauthorized", + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Invalid or expired API key")) } else { h.slogger.Error("Authentication failed", "clientIP", clientIP, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Error while validating API key")) } return "", "", false @@ -118,7 +119,7 @@ func (h *GatewayInternalAPIHandler) GetAPI(w http.ResponseWriter, r *http.Reques apiID := r.PathValue("apiId") if apiID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API ID is required")) return } @@ -126,16 +127,16 @@ func (h *GatewayInternalAPIHandler) GetAPI(w http.ResponseWriter, r *http.Reques api, err := h.gatewayInternalService.GetActiveDeploymentByGateway(apiID, orgID, gatewayID) if err != nil { if errors.Is(err, constants.ErrDeploymentNotActive) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "No active deployment found for this API on this gateway")) return } if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "API not found")) return } - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to get API")) return } @@ -144,7 +145,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, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to create API package")) return } @@ -180,7 +181,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, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", err.Error())) return } // 'total' is advisory: log a mismatch but proceed with what the zip actually contained. @@ -206,7 +207,7 @@ func (h *GatewayInternalAPIHandler) GetLLMProvider(w http.ResponseWriter, r *htt providerID := r.PathValue("providerId") if providerID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Provider ID is required")) return } @@ -214,16 +215,16 @@ func (h *GatewayInternalAPIHandler) GetLLMProvider(w http.ResponseWriter, r *htt provider, err := h.gatewayInternalService.GetActiveLLMProviderDeploymentByGateway(providerID, orgID, gatewayID) if err != nil { if errors.Is(err, constants.ErrDeploymentNotActive) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "No active deployment found for this LLM provider on this gateway")) return } if errors.Is(err, constants.ErrLLMProviderNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "LLM provider not found")) return } - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to get LLM provider")) return } @@ -232,7 +233,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, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to create LLM provider package")) return } @@ -256,7 +257,7 @@ func (h *GatewayInternalAPIHandler) GetLLMProxy(w http.ResponseWriter, r *http.R proxyID := r.PathValue("proxyId") if proxyID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Proxy ID is required")) return } @@ -264,16 +265,16 @@ func (h *GatewayInternalAPIHandler) GetLLMProxy(w http.ResponseWriter, r *http.R proxy, err := h.gatewayInternalService.GetActiveLLMProxyDeploymentByGateway(proxyID, orgID, gatewayID) if err != nil { if errors.Is(err, constants.ErrDeploymentNotActive) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "No active deployment found for this LLM proxy on this gateway")) return } if errors.Is(err, constants.ErrLLMProxyNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "LLM proxy not found")) return } - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to get LLM proxy")) return } @@ -282,7 +283,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, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to create LLM proxy package")) return } @@ -311,7 +312,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, utils.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Invalid 'since' parameter. Expected ISO 8601 format (e.g., 2026-03-04T10:00:00Z)")) return } @@ -321,12 +322,12 @@ 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, utils.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "Gateway not found")) return } h.slogger.Error("Failed to get gateway deployments", "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to get deployments")) return } @@ -344,7 +345,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, utils.NewErrorResponse(406, "Not Acceptable", + httputil.WriteJSON(w, http.StatusNotAcceptable, apperror.NewErrorResponse(406, "Not Acceptable", "This endpoint only supports Accept: application/x-tar+gzip")) return } @@ -352,13 +353,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, utils.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Invalid request body: "+err.Error())) return } if len(req.DeploymentIDs) == 0 { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "At least one deployment ID is required")) return } @@ -367,12 +368,12 @@ func (h *GatewayInternalAPIHandler) BatchFetchDeployments(w http.ResponseWriter, contentMap, err := h.gatewayInternalService.GetDeploymentContentBatch(orgID, gatewayID, req.DeploymentIDs) if err != nil { if errors.Is(err, constants.ErrGatewayNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(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, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to get deployment content")) return } @@ -381,7 +382,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, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to create deployment package")) return } @@ -413,7 +414,7 @@ func (h *GatewayInternalAPIHandler) GetSubscriptions(w http.ResponseWriter, r *h "clientIP", clientIP, "organizationId", orgID, "apiId", apiID) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API ID is required")) return } @@ -425,7 +426,7 @@ func (h *GatewayInternalAPIHandler) GetSubscriptions(w http.ResponseWriter, r *h "organizationId", orgID, "gatewayId", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "API not found")) return } @@ -434,7 +435,7 @@ func (h *GatewayInternalAPIHandler) GetSubscriptions(w http.ResponseWriter, r *h "apiId", apiID, "organizationId", orgID, "gatewayId", gatewayID) - httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponse(403, "Forbidden", + httputil.WriteJSON(w, http.StatusForbidden, apperror.NewErrorResponse(403, "Forbidden", "API is not associated with this gateway")) return } @@ -443,7 +444,7 @@ func (h *GatewayInternalAPIHandler) GetSubscriptions(w http.ResponseWriter, r *h "organizationId", orgID, "gatewayId", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to verify API deployment")) return } @@ -455,7 +456,7 @@ func (h *GatewayInternalAPIHandler) GetSubscriptions(w http.ResponseWriter, r *h "apiId", apiID, "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "API not found")) return } @@ -463,7 +464,7 @@ func (h *GatewayInternalAPIHandler) GetSubscriptions(w http.ResponseWriter, r *h "apiId", apiID, "organizationId", orgID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to get subscriptions")) return } @@ -483,7 +484,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, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to get subscription plans")) return } @@ -500,7 +501,7 @@ func (h *GatewayInternalAPIHandler) GetMCPProxy(w http.ResponseWriter, r *http.R } proxyID := r.PathValue("proxyId") if proxyID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Proxy ID is required")) return } @@ -513,18 +514,18 @@ func (h *GatewayInternalAPIHandler) GetMCPProxy(w http.ResponseWriter, r *http.R } if errors.Is(err, constants.ErrDeploymentNotActive) { 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, utils.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "No active deployment found for this MCP proxy on this gateway")) return } if errors.Is(err, constants.ErrMCPProxyNotFound) { h.slogger.Error("MCP proxy not found", "clientIP", clientIP, "proxyID", proxyID, "orgID", orgID, "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(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, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to get MCP proxy")) return } @@ -533,7 +534,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, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to create MCP proxy package")) return } @@ -557,7 +558,7 @@ func (h *GatewayInternalAPIHandler) GetWebSubAPI(w http.ResponseWriter, r *http. apiID := r.PathValue("apiId") if apiID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API ID is required")) return } @@ -570,18 +571,18 @@ func (h *GatewayInternalAPIHandler) GetWebSubAPI(w http.ResponseWriter, r *http. } if errors.Is(err, constants.ErrDeploymentNotActive) { 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, utils.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "No active deployment found for this WebSub API on this gateway")) return } if errors.Is(err, constants.ErrWebSubAPINotFound) { h.slogger.Error("WebSub API not found", "clientIP", clientIP, "apiID", apiID, "orgID", orgID, "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(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, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to get WebSub API")) return } @@ -590,7 +591,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, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to create WebSub API package")) return } @@ -614,7 +615,7 @@ func (h *GatewayInternalAPIHandler) GetWebBrokerAPI(w http.ResponseWriter, r *ht apiID := r.PathValue("apiId") if apiID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API ID is required")) return } @@ -627,18 +628,18 @@ func (h *GatewayInternalAPIHandler) GetWebBrokerAPI(w http.ResponseWriter, r *ht } if errors.Is(err, constants.ErrDeploymentNotActive) { 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, utils.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "No active deployment found for this WebBroker API on this gateway")) return } if errors.Is(err, constants.ErrWebBrokerAPINotFound) { h.slogger.Error("WebBroker API not found", "clientIP", clientIP, "apiID", apiID, "orgID", orgID, "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(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, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to get WebBroker API")) return } @@ -647,7 +648,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, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to create WebBroker API package")) return } @@ -676,27 +677,27 @@ 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, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(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, utils.NewErrorResponse(409, "Conflict", err.Error())) + 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, utils.NewErrorResponse(409, "Conflict", err.Error())) + httputil.WriteJSON(w, http.StatusConflict, apperror.NewErrorResponse(409, "Conflict", err.Error())) return } if errors.Is(err, constants.ErrGatewayNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", err.Error())) + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", err.Error())) return } h.slogger.Error("Failed to store gateway manifest", "gatewayID", gatewayID, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to store gateway manifest")) return } @@ -714,7 +715,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, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) return } httputil.WriteJSON(w, http.StatusOK, keys) @@ -730,7 +731,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, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) return } httputil.WriteJSON(w, http.StatusOK, keys) @@ -746,7 +747,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, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) return } httputil.WriteJSON(w, http.StatusOK, keys) @@ -762,7 +763,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, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) return } httputil.WriteJSON(w, http.StatusOK, keys) @@ -778,7 +779,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, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to get API keys")) return } httputil.WriteJSON(w, http.StatusOK, keys) @@ -796,7 +797,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, utils.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Invalid request body: artifactIds is required and must be a non-empty array")) return } @@ -804,7 +805,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, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to check artifact existence")) return } @@ -840,20 +841,20 @@ func (h *GatewayInternalAPIHandler) GetWebSubAPIHmacSecrets(w http.ResponseWrite apiID := r.PathValue("apiId") if apiID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(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, utils.NewErrorResponse(503, "Service Unavailable", "HMAC secret management is not configured on this server")) + httputil.WriteJSON(w, http.StatusServiceUnavailable, apperror.NewErrorResponse(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, utils.NewErrorResponse(500, "Internal Server Error", "Failed to get HMAC secrets")) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to get HMAC secrets")) return } @@ -862,7 +863,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, utils.NewErrorResponse(500, "Internal Server Error", "Failed to decrypt HMAC secret")) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to decrypt HMAC secret")) return } items = append(items, dto.GatewayHmacSecretInfo{Name: s.Handle, Secret: plaintext}) @@ -886,7 +887,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, utils.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Invalid 'updatedAfter' parameter. Expected RFC3339 format.")) return } @@ -898,7 +899,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, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to retrieve secrets")) return } @@ -920,7 +921,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, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to decrypt secret")) return } @@ -943,7 +944,7 @@ func (h *GatewayInternalAPIHandler) GetGatewaySecretValue(w http.ResponseWriter, handle := r.PathValue("handle") if handle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Secret handle is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Secret handle is required")) return } @@ -951,22 +952,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, utils.NewErrorResponse(500, "Internal Server Error", "Failed to verify secret access")) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to verify secret access")) return } if !deployed { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Secret not found")) + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(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, utils.NewErrorResponse(404, "Not Found", "Secret not found")) + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(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, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to decrypt secret")) return } diff --git a/platform-api/internal/handler/llm_provider_integration_test.go b/platform-api/internal/handler/llm_provider_integration_test.go index 7da88a6eb2..789eb62cb5 100644 --- a/platform-api/internal/handler/llm_provider_integration_test.go +++ b/platform-api/internal/handler/llm_provider_integration_test.go @@ -157,8 +157,8 @@ func TestLLMProviderHTTP_CreateRequiresOrg_401(t *testing.T) { if body["status"] != "error" { t.Errorf("expected status=error, got %v", body["status"]) } - if body["code"] != "COMMON_UNAUTHORIZED" { - t.Errorf("expected code=COMMON_UNAUTHORIZED, got %v", body["code"]) + if body["code"] != "UNAUTHORIZED" { + t.Errorf("expected code=UNAUTHORIZED, got %v", body["code"]) } } diff --git a/platform-api/internal/middleware/error_mapper.go b/platform-api/internal/middleware/error_mapper.go index 62e9281fd3..b232b59627 100644 --- a/platform-api/internal/middleware/error_mapper.go +++ b/platform-api/internal/middleware/error_mapper.go @@ -24,7 +24,6 @@ import ( "runtime/debug" "github.com/wso2/api-platform/platform-api/internal/apperror" - "github.com/wso2/api-platform/platform-api/internal/utils" "github.com/google/uuid" ) @@ -32,7 +31,7 @@ import ( // ErrorHandlerFunc is the handler signature for routes that participate in // centralized error mapping: instead of writing HTTP error responses inline, // the handler returns an error (ideally an *apperror.Error) and MapErrors -// logs it and writes the standard utils.ErrorResponse. Success responses are +// logs it and writes the standard apperror.ErrorResponse. Success responses are // still written directly by the handler — the mapper only owns the error path. type ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request) error @@ -40,7 +39,7 @@ type ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request) error // on the mux. It is the single catch point for handler errors: it recovers // panics into a structured 500, maps *apperror.Error values onto the wire // via apperror.WriteHTTP, and collapses any other error into a generic 500 -// COMMON_INTERNAL_ERROR so internal details never reach the client. +// INTERNAL_ERROR so internal details never reach the client. func MapErrors(slogger *slog.Logger, next ErrorHandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { defer func() { @@ -80,7 +79,7 @@ func writeMappedError(w http.ResponseWriter, r *http.Request, slogger *slog.Logg // 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 == utils.CodeCommonInternalError && appErr.Cause != nil { + for appErr.Code == apperror.CodeCommonInternalError && appErr.Cause != nil { var inner *apperror.Error if !errors.As(appErr.Cause, &inner) { break diff --git a/platform-api/internal/middleware/error_mapper_test.go b/platform-api/internal/middleware/error_mapper_test.go index dc49b74ed5..6e7f09e8b4 100644 --- a/platform-api/internal/middleware/error_mapper_test.go +++ b/platform-api/internal/middleware/error_mapper_test.go @@ -28,7 +28,6 @@ import ( "testing" "github.com/wso2/api-platform/platform-api/internal/apperror" - "github.com/wso2/api-platform/platform-api/internal/utils" ) func testLogger(buf *bytes.Buffer) *slog.Logger { @@ -48,11 +47,11 @@ func TestMapErrorsAppError(t *testing.T) { if rec.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d", rec.Code) } - var body utils.ErrorResponse + var body apperror.ErrorResponse if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("invalid JSON body: %v", err) } - if body.Status != "error" || body.Code != utils.CodeProjectNotFound || + if body.Status != "error" || body.Code != apperror.CodeProjectNotFound || body.Message != "The specified project could not be found." { t.Errorf("unexpected body: %+v", body) } @@ -84,7 +83,7 @@ func TestMapErrorsSeveritySplit(t *testing.T) { if strings.Contains(log, `"stack"`) { t.Error("4xx must not log a stack trace") } - var body utils.ErrorResponse + var body apperror.ErrorResponse if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("invalid JSON body: %v", err) } @@ -130,11 +129,11 @@ func TestMapErrorsPlainErrorFallsBackToGeneric500(t *testing.T) { if rec.Code != http.StatusInternalServerError { t.Fatalf("expected 500, got %d", rec.Code) } - var body utils.ErrorResponse + var body apperror.ErrorResponse if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("invalid JSON body: %v", err) } - if body.Code != utils.CodeCommonInternalError || body.Message != "An unexpected error occurred." { + if body.Code != apperror.CodeCommonInternalError || body.Message != "An unexpected error occurred." { t.Errorf("unexpected body: %+v", body) } if strings.Contains(rec.Body.String(), "10.0.0.5") { @@ -161,12 +160,12 @@ func TestMapErrorsPrefersInnerTypedErrorOverGenericInternalWrapper(t *testing.T) if rec.Code != http.StatusNotFound { t.Fatalf("expected 404 from the inner typed error, got %d", rec.Code) } - var body utils.ErrorResponse + var body apperror.ErrorResponse if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("invalid JSON body: %v", err) } - if body.Code != utils.CodeGatewayNotFound { - t.Errorf("expected code %q, got %q", utils.CodeGatewayNotFound, body.Code) + if body.Code != apperror.CodeGatewayNotFound { + t.Errorf("expected code %q, got %q", apperror.CodeGatewayNotFound, body.Code) } if !strings.Contains(logBuf.String(), "gateway g1 missing") { t.Error("expected the inner error's log message to be logged") @@ -185,11 +184,11 @@ func TestMapErrorsRecoversPanic(t *testing.T) { if rec.Code != http.StatusInternalServerError { t.Fatalf("expected 500, got %d", rec.Code) } - var body utils.ErrorResponse + var body apperror.ErrorResponse if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("invalid JSON body: %v", err) } - if body.Code != utils.CodeCommonInternalError { + if body.Code != apperror.CodeCommonInternalError { t.Errorf("unexpected code %q", body.Code) } if strings.Contains(rec.Body.String(), "abc123") { diff --git a/platform-api/plugins/eventgateway/handler/artifact_guard_response.go b/platform-api/plugins/eventgateway/handler/artifact_guard_response.go index 6b5c649a87..6babbda30f 100644 --- a/platform-api/plugins/eventgateway/handler/artifact_guard_response.go +++ b/platform-api/plugins/eventgateway/handler/artifact_guard_response.go @@ -23,8 +23,8 @@ 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/api-platform/platform-api/internal/utils" "github.com/wso2/go-httpkit/httputil" ) @@ -39,10 +39,10 @@ import ( func respondArtifactGuardError(w http.ResponseWriter, err error) bool { switch { case errors.Is(err, constants.ErrArtifactReadOnly): - httputil.WriteJSON(w, http.StatusForbidden, utils.NewErrorResponse(403, "Forbidden", err.Error())) + httputil.WriteJSON(w, http.StatusForbidden, apperror.NewErrorResponse(403, "Forbidden", err.Error())) return true case errors.Is(err, constants.ErrArtifactDeployed): - httputil.WriteJSON(w, http.StatusConflict, utils.NewErrorResponse(409, "Conflict", err.Error())) + httputil.WriteJSON(w, http.StatusConflict, apperror.NewErrorResponse(409, "Conflict", err.Error())) return true default: return false diff --git a/platform-api/plugins/eventgateway/handler/identity_helper.go b/platform-api/plugins/eventgateway/handler/identity_helper.go index c5af825d55..cafdd8cafa 100644 --- a/platform-api/plugins/eventgateway/handler/identity_helper.go +++ b/platform-api/plugins/eventgateway/handler/identity_helper.go @@ -23,8 +23,8 @@ import ( "log/slog" "net/http" + "github.com/wso2/api-platform/platform-api/internal/apperror" "github.com/wso2/api-platform/platform-api/internal/service" - "github.com/wso2/api-platform/platform-api/internal/utils" "github.com/wso2/go-httpkit/httputil" ) @@ -39,7 +39,7 @@ func resolveActor(w http.ResponseWriter, r *http.Request, identity *service.Iden actor, err := identity.InternalUserID(r) if err != nil { slogger.Error("Failed to resolve user identity", "action", action, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to resolve user identity")) return "", false } diff --git a/platform-api/plugins/eventgateway/handler/webbroker_api.go b/platform-api/plugins/eventgateway/handler/webbroker_api.go index 43087858ac..ecba1bde25 100644 --- a/platform-api/plugins/eventgateway/handler/webbroker_api.go +++ b/platform-api/plugins/eventgateway/handler/webbroker_api.go @@ -28,6 +28,7 @@ import ( "strings" "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/service" @@ -66,14 +67,14 @@ func (h *WebBrokerAPIHandler) RegisterRoutes(mux *http.ServeMux) { func (h *WebBrokerAPIHandler) CreateWebBrokerAPI(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } var req api.WebBrokerAPI if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.slogger.Error("WebBroker API request validation failed", "org_id", orgID, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Invalid request body")) return } @@ -95,13 +96,13 @@ func (h *WebBrokerAPIHandler) CreateWebBrokerAPI(w http.ResponseWriter, r *http. func (h *WebBrokerAPIHandler) ListWebBrokerAPIs(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } projectID := strings.TrimSpace(r.URL.Query().Get("projectId")) if projectID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "projectId query parameter is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "projectId query parameter is required")) return } @@ -139,7 +140,7 @@ func (h *WebBrokerAPIHandler) ListWebBrokerAPIs(w http.ResponseWriter, r *http.R func (h *WebBrokerAPIHandler) GetWebBrokerAPI(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } @@ -157,7 +158,7 @@ func (h *WebBrokerAPIHandler) GetWebBrokerAPI(w http.ResponseWriter, r *http.Req func (h *WebBrokerAPIHandler) UpdateWebBrokerAPI(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } @@ -166,12 +167,12 @@ func (h *WebBrokerAPIHandler) UpdateWebBrokerAPI(w http.ResponseWriter, r *http. var req api.WebBrokerAPI if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.slogger.Error("WebBroker API update validation failed", "org_id", orgID, "api_id", id, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Invalid request body")) return } if err := utils.ValidateHandleImmutable(id, req.Id); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "WebBroker API id is immutable and cannot be changed")) return } @@ -193,7 +194,7 @@ func (h *WebBrokerAPIHandler) UpdateWebBrokerAPI(w http.ResponseWriter, r *http. func (h *WebBrokerAPIHandler) DeleteWebBrokerAPI(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } @@ -218,21 +219,21 @@ func (h *WebBrokerAPIHandler) handleServiceError(w http.ResponseWriter, err erro } switch { case errors.Is(err, constants.ErrHandleImmutable): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", err.Error())) case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", err.Error())) case errors.Is(err, constants.ErrWebBrokerAPINotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "WebBroker API not found")) + 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, utils.NewErrorResponse(409, "Conflict", "WebBroker API with this ID already exists")) + 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, utils.NewErrorResponse(409, "Conflict", "WebBroker API limit reached for the organization")) + 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, utils.NewErrorResponse(400, "Bad Request", "Project not found")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Project not found")) case errors.Is(err, constants.ErrDevPortalNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "DevPortal not found")) + 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, utils.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) + 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 10c0fa30fd..c8249b0406 100644 --- a/platform-api/plugins/eventgateway/handler/webbroker_api_deployment.go +++ b/platform-api/plugins/eventgateway/handler/webbroker_api_deployment.go @@ -27,10 +27,10 @@ import ( "strings" "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/service" - "github.com/wso2/api-platform/platform-api/internal/utils" egservice "github.com/wso2/api-platform/platform-api/plugins/eventgateway/service" "github.com/wso2/go-httpkit/httputil" @@ -66,32 +66,32 @@ func (h *WebBrokerAPIDeploymentHandler) RegisterRoutes(mux *http.ServeMux) { func (h *WebBrokerAPIDeploymentHandler) DeployWebBrokerAPI(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } apiId := r.PathValue("webBrokerApiId") if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API ID is required")) return } var req api.DeployRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", err.Error())) return } if req.Name == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "name is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "name is required")) return } if req.Base == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "base is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "base is required")) return } if strings.TrimSpace(req.GatewayId) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "gatewayId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "gatewayId is required")) return } @@ -115,7 +115,7 @@ func (h *WebBrokerAPIDeploymentHandler) DeployWebBrokerAPI(w http.ResponseWriter func (h *WebBrokerAPIDeploymentHandler) UndeployDeployment(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } @@ -123,11 +123,11 @@ func (h *WebBrokerAPIDeploymentHandler) UndeployDeployment(w http.ResponseWriter deploymentId := r.PathValue("deploymentId") gatewayId := r.URL.Query().Get("gatewayId") if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "deploymentId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "deploymentId is required")) return } if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "gatewayId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "gatewayId is required")) return } @@ -147,7 +147,7 @@ func (h *WebBrokerAPIDeploymentHandler) UndeployDeployment(w http.ResponseWriter func (h *WebBrokerAPIDeploymentHandler) RestoreDeployment(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } @@ -155,11 +155,11 @@ func (h *WebBrokerAPIDeploymentHandler) RestoreDeployment(w http.ResponseWriter, deploymentId := r.PathValue("deploymentId") gatewayId := r.URL.Query().Get("gatewayId") if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "deploymentId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "deploymentId is required")) return } if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "gatewayId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "gatewayId is required")) return } @@ -179,13 +179,13 @@ func (h *WebBrokerAPIDeploymentHandler) RestoreDeployment(w http.ResponseWriter, func (h *WebBrokerAPIDeploymentHandler) GetDeployments(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } apiId := r.PathValue("webBrokerApiId") if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API ID is required")) return } @@ -220,7 +220,7 @@ func (h *WebBrokerAPIDeploymentHandler) GetDeployments(w http.ResponseWriter, r func (h *WebBrokerAPIDeploymentHandler) GetDeployment(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } @@ -240,7 +240,7 @@ func (h *WebBrokerAPIDeploymentHandler) GetDeployment(w http.ResponseWriter, r * func (h *WebBrokerAPIDeploymentHandler) DeleteDeployment(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } @@ -258,27 +258,27 @@ 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, utils.NewErrorResponse(404, "Not Found", "WebBroker API not found")) + 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, utils.NewErrorResponse(404, "Not Found", "Gateway not found")) + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "Gateway not found")) case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Deployment not found")) + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "Deployment not found")) case errors.Is(err, constants.ErrBaseDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Base deployment not found")) + 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, utils.NewErrorResponse(409, "Conflict", "No active deployment found for this API on the gateway")) + 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, utils.NewErrorResponse(409, "Conflict", "Cannot delete an active deployment - undeploy it first")) + 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, utils.NewErrorResponse(409, "Conflict", "Cannot restore currently deployed deployment")) + 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, utils.NewErrorResponse(409, "Conflict", "Deployment cannot be restored: only ARCHIVED or UNDEPLOYED deployments are eligible")) + 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, utils.NewErrorResponse(400, "Bad Request", "Deployment is bound to a different gateway")) + 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, utils.NewErrorResponse(400, "Bad Request", "API must have at least one backend service configured")) + 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, utils.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) + 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 7412f9d61f..67082b4124 100644 --- a/platform-api/plugins/eventgateway/handler/webbroker_apikey.go +++ b/platform-api/plugins/eventgateway/handler/webbroker_apikey.go @@ -27,6 +27,7 @@ import ( "net/http" "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/service" @@ -65,13 +66,13 @@ func (h *WebBrokerAPIKeyHandler) RegisterRoutes(mux *http.ServeMux) { func (h *WebBrokerAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } apiHandle := r.PathValue("webBrokerApiId") if apiHandle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API handle is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API handle is required")) return } @@ -88,12 +89,12 @@ func (h *WebBrokerAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Req var req api.CreateAPIKeyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Invalid request body")) return } if req.ApiKey == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API key value is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API key value is required")) return } @@ -103,7 +104,7 @@ func (h *WebBrokerAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Req } else { generatedName, err := utils.GenerateHandle(req.DisplayName, nil) if err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Failed to generate API key name")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Failed to generate API key name")) return } name = generatedName @@ -112,15 +113,15 @@ 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) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "WebBroker API not found")) + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "WebBroker API not found")) return } if errors.Is(err, constants.ErrGatewayUnavailable) { - httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponse(503, "Service Unavailable", "No gateway connections available")) + httputil.WriteJSON(w, http.StatusServiceUnavailable, apperror.NewErrorResponse(503, "Service Unavailable", "No gateway connections available")) return } h.slogger.Error("Failed to create API key for WebBroker API", "apiHandle", apiHandle, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to create API key")) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to create API key")) return } @@ -135,19 +136,19 @@ func (h *WebBrokerAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Req func (h *WebBrokerAPIKeyHandler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } apiHandle := r.PathValue("webBrokerApiId") if apiHandle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API handle is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API handle is required")) return } keyName := r.PathValue("apiKeyId") if keyName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Key name is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Key name is required")) return } @@ -165,34 +166,34 @@ func (h *WebBrokerAPIKeyHandler) UpdateAPIKey(w http.ResponseWriter, r *http.Req var req api.UpdateAPIKeyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.slogger.Warn("Invalid API key update request", "orgId", orgID, "apiHandle", apiHandle, "keyName", keyName, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body: "+err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Invalid request body: "+err.Error())) return } if req.ApiKey == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API key value is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API key value is required")) return } // Validate that the name in the request body (if provided) matches the URL path parameter if err := utils.ValidateHandleImmutable(keyName, req.Name); err != nil { h.slogger.Warn("API key name mismatch", "orgId", orgID, "apiHandle", apiHandle, "urlKeyName", keyName, "bodyKeyName", *req.Name) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", fmt.Sprintf("API key name mismatch: name in request body '%s' must match the key name in URL '%s'", *req.Name, keyName))) return } if err := h.apiKeyService.UpdateAPIKey(r.Context(), apiHandle, constants.WebBrokerApi, orgID, keyName, userId, &req); err != nil { if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "WebBroker API not found")) + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "WebBroker API not found")) return } if errors.Is(err, constants.ErrGatewayUnavailable) { - httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponse(503, "Service Unavailable", "No gateway connections available")) + httputil.WriteJSON(w, http.StatusServiceUnavailable, apperror.NewErrorResponse(503, "Service Unavailable", "No gateway connections available")) return } h.slogger.Error("Failed to update API key for WebBroker API", "apiHandle", apiHandle, "keyName", keyName, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to update API key")) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to update API key")) return } @@ -208,7 +209,7 @@ func (h *WebBrokerAPIKeyHandler) UpdateAPIKey(w http.ResponseWriter, r *http.Req func (h *WebBrokerAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } @@ -216,11 +217,11 @@ func (h *WebBrokerAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.Req keyName := r.PathValue("apiKeyId") if apiHandle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API handle is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API handle is required")) return } if keyName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Key name is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Key name is required")) return } @@ -231,19 +232,19 @@ 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) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "WebBroker API not found")) + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "WebBroker API not found")) return } if errors.Is(err, constants.ErrAPIKeyNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "API key not found")) + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "API key not found")) return } if errors.Is(err, constants.ErrGatewayUnavailable) { - httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponse(503, "Service Unavailable", "No gateway connections available")) + httputil.WriteJSON(w, http.StatusServiceUnavailable, apperror.NewErrorResponse(503, "Service Unavailable", "No gateway connections available")) return } h.slogger.Error("Failed to delete API key for WebBroker API", "apiHandle", apiHandle, "keyName", keyName, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to delete API key")) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to delete API key")) return } @@ -254,11 +255,11 @@ func (h *WebBrokerAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.Req func (h *WebBrokerAPIKeyHandler) handleServiceError(w http.ResponseWriter, err error) { switch { case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", err.Error())) case errors.Is(err, constants.ErrWebBrokerAPINotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "WebBroker API not found")) + 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, utils.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) + 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 f1ec3305b8..b3a6fdba9e 100644 --- a/platform-api/plugins/eventgateway/handler/websub_api.go +++ b/platform-api/plugins/eventgateway/handler/websub_api.go @@ -28,6 +28,7 @@ import ( "strings" "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/service" @@ -66,14 +67,14 @@ func (h *WebSubAPIHandler) RegisterRoutes(mux *http.ServeMux) { func (h *WebSubAPIHandler) CreateWebSubAPI(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } var req api.WebSubAPI if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.slogger.Error("WebSub API request validation failed", "org_id", orgID, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Invalid request body")) return } @@ -95,13 +96,13 @@ func (h *WebSubAPIHandler) CreateWebSubAPI(w http.ResponseWriter, r *http.Reques func (h *WebSubAPIHandler) ListWebSubAPIs(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } projectID := strings.TrimSpace(r.URL.Query().Get("projectId")) if projectID == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "projectId query parameter is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "projectId query parameter is required")) return } @@ -130,7 +131,7 @@ func (h *WebSubAPIHandler) ListWebSubAPIs(w http.ResponseWriter, r *http.Request func (h *WebSubAPIHandler) GetWebSubAPI(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } @@ -148,7 +149,7 @@ func (h *WebSubAPIHandler) GetWebSubAPI(w http.ResponseWriter, r *http.Request) func (h *WebSubAPIHandler) UpdateWebSubAPI(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } @@ -157,12 +158,12 @@ func (h *WebSubAPIHandler) UpdateWebSubAPI(w http.ResponseWriter, r *http.Reques var req api.WebSubAPI if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.slogger.Error("WebSub API update validation failed", "org_id", orgID, "api_id", id, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Invalid request body")) return } if err := utils.ValidateHandleImmutable(id, req.Id); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "WebSub API id is immutable and cannot be changed")) return } @@ -184,7 +185,7 @@ func (h *WebSubAPIHandler) UpdateWebSubAPI(w http.ResponseWriter, r *http.Reques func (h *WebSubAPIHandler) DeleteWebSubAPI(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } @@ -209,21 +210,21 @@ func (h *WebSubAPIHandler) handleServiceError(w http.ResponseWriter, err error) } switch { case errors.Is(err, constants.ErrHandleImmutable): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", err.Error())) case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", err.Error())) case errors.Is(err, constants.ErrWebSubAPINotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "WebSub API not found")) + 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, utils.NewErrorResponse(409, "Conflict", "WebSub API with this ID already exists")) + 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, utils.NewErrorResponse(409, "Conflict", "WebSub API limit reached for the organization")) + 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, utils.NewErrorResponse(400, "Bad Request", "Project not found")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Project not found")) case errors.Is(err, constants.ErrDevPortalNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "DevPortal not found")) + 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, utils.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) + 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 18c064851c..fec8db9d8e 100644 --- a/platform-api/plugins/eventgateway/handler/websub_api_deployment.go +++ b/platform-api/plugins/eventgateway/handler/websub_api_deployment.go @@ -27,10 +27,10 @@ import ( "strings" "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/service" - "github.com/wso2/api-platform/platform-api/internal/utils" egservice "github.com/wso2/api-platform/platform-api/plugins/eventgateway/service" "github.com/wso2/go-httpkit/httputil" @@ -66,32 +66,32 @@ func (h *WebSubAPIDeploymentHandler) RegisterRoutes(mux *http.ServeMux) { func (h *WebSubAPIDeploymentHandler) DeployWebSubAPI(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } apiId := r.PathValue("webSubApiId") if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API ID is required")) return } var req api.DeployRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", err.Error())) return } if req.Name == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "name is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "name is required")) return } if req.Base == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "base is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "base is required")) return } if strings.TrimSpace(req.GatewayId) == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "gatewayId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "gatewayId is required")) return } @@ -115,7 +115,7 @@ func (h *WebSubAPIDeploymentHandler) DeployWebSubAPI(w http.ResponseWriter, r *h func (h *WebSubAPIDeploymentHandler) UndeployDeployment(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } @@ -123,11 +123,11 @@ func (h *WebSubAPIDeploymentHandler) UndeployDeployment(w http.ResponseWriter, r deploymentId := r.PathValue("deploymentId") gatewayId := r.URL.Query().Get("gatewayId") if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "deploymentId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "deploymentId is required")) return } if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "gatewayId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "gatewayId is required")) return } @@ -147,7 +147,7 @@ func (h *WebSubAPIDeploymentHandler) UndeployDeployment(w http.ResponseWriter, r func (h *WebSubAPIDeploymentHandler) RestoreDeployment(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } @@ -155,11 +155,11 @@ func (h *WebSubAPIDeploymentHandler) RestoreDeployment(w http.ResponseWriter, r deploymentId := r.PathValue("deploymentId") gatewayId := r.URL.Query().Get("gatewayId") if deploymentId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "deploymentId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "deploymentId is required")) return } if gatewayId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "gatewayId is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "gatewayId is required")) return } @@ -179,13 +179,13 @@ func (h *WebSubAPIDeploymentHandler) RestoreDeployment(w http.ResponseWriter, r func (h *WebSubAPIDeploymentHandler) GetDeployments(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } apiId := r.PathValue("webSubApiId") if apiId == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API ID is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API ID is required")) return } @@ -220,7 +220,7 @@ func (h *WebSubAPIDeploymentHandler) GetDeployments(w http.ResponseWriter, r *ht func (h *WebSubAPIDeploymentHandler) GetDeployment(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } @@ -240,7 +240,7 @@ func (h *WebSubAPIDeploymentHandler) GetDeployment(w http.ResponseWriter, r *htt func (h *WebSubAPIDeploymentHandler) DeleteDeployment(w http.ResponseWriter, r *http.Request) { orgId, exists := middleware.GetOrganizationFromRequest(r) if !exists { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } @@ -258,27 +258,27 @@ 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, utils.NewErrorResponse(404, "Not Found", "WebSub API not found")) + 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, utils.NewErrorResponse(404, "Not Found", "Gateway not found")) + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "Gateway not found")) case errors.Is(err, constants.ErrDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Deployment not found")) + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "Deployment not found")) case errors.Is(err, constants.ErrBaseDeploymentNotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "Base deployment not found")) + 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, utils.NewErrorResponse(409, "Conflict", "No active deployment found for this API on the gateway")) + 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, utils.NewErrorResponse(409, "Conflict", "Cannot delete an active deployment - undeploy it first")) + 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, utils.NewErrorResponse(409, "Conflict", "Cannot restore currently deployed deployment")) + 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, utils.NewErrorResponse(409, "Conflict", "Deployment cannot be restored: only ARCHIVED or UNDEPLOYED deployments are eligible")) + 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, utils.NewErrorResponse(400, "Bad Request", "Deployment is bound to a different gateway")) + 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, utils.NewErrorResponse(400, "Bad Request", "API must have at least one backend service configured")) + 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, utils.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) + 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 e6d499c50a..360e4b70b2 100644 --- a/platform-api/plugins/eventgateway/handler/websub_api_hmac_secret.go +++ b/platform-api/plugins/eventgateway/handler/websub_api_hmac_secret.go @@ -27,6 +27,7 @@ import ( "net/http" "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" @@ -63,7 +64,7 @@ func (h *WebSubAPIHmacSecretHandler) RegisterRoutes(mux *http.ServeMux) { func (h *WebSubAPIHmacSecretHandler) featureUnavailable(w http.ResponseWriter) bool { if h.secretService == nil { - httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponse(503, "Service Unavailable", + httputil.WriteJSON(w, http.StatusServiceUnavailable, apperror.NewErrorResponse(503, "Service Unavailable", "HMAC secret management is not configured on this server")) return true } @@ -77,24 +78,24 @@ func (h *WebSubAPIHmacSecretHandler) CreateHmacSecret(w http.ResponseWriter, r * } orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } apiHandle := r.PathValue("webSubApiId") if apiHandle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API handle is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API handle is required")) return } var req api.WebSubAPIHmacSecretRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Invalid request body")) return } if req.Secret != nil && *req.Secret == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "secret must not be empty; omit the field to auto-generate")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "secret must not be empty; omit the field to auto-generate")) return } @@ -120,7 +121,7 @@ func (h *WebSubAPIHmacSecretHandler) CreateHmacSecret(w http.ResponseWriter, r * info, err := h.toSecretInfo(secret) if err != nil { h.slogger.Error("Failed to resolve HMAC secret identity", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to generate HMAC secret")) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to generate HMAC secret")) return } httputil.WriteJSON(w, http.StatusCreated, api.WebSubAPIHmacSecretCreationResponse{ @@ -137,13 +138,13 @@ func (h *WebSubAPIHmacSecretHandler) ListHmacSecrets(w http.ResponseWriter, r *h } orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } apiHandle := r.PathValue("webSubApiId") if apiHandle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API handle is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API handle is required")) return } @@ -158,7 +159,7 @@ func (h *WebSubAPIHmacSecretHandler) ListHmacSecrets(w http.ResponseWriter, r *h info, err := h.toSecretInfo(s) if err != nil { h.slogger.Error("Failed to resolve HMAC secret identity", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to list HMAC secrets")) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to list HMAC secrets")) return } items = append(items, *info) @@ -173,14 +174,14 @@ func (h *WebSubAPIHmacSecretHandler) DeleteHmacSecret(w http.ResponseWriter, r * } orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } apiHandle := r.PathValue("webSubApiId") secretName := r.PathValue("secretName") if apiHandle == "" || secretName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API handle and secret name are required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API handle and secret name are required")) return } @@ -199,25 +200,25 @@ func (h *WebSubAPIHmacSecretHandler) RegenerateHmacSecret(w http.ResponseWriter, } orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } apiHandle := r.PathValue("webSubApiId") secretName := r.PathValue("secretName") if apiHandle == "" || secretName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API handle and secret name are required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API handle and secret name are required")) return } var req api.WebSubAPIHmacSecretRegenerateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil && !errors.Is(err, io.EOF) { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Invalid request body")) return } if req.Secret != nil && *req.Secret == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "secret must not be empty; omit the field to auto-generate")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "secret must not be empty; omit the field to auto-generate")) return } @@ -243,7 +244,7 @@ func (h *WebSubAPIHmacSecretHandler) RegenerateHmacSecret(w http.ResponseWriter, info, err := h.toSecretInfo(secret) if err != nil { h.slogger.Error("Failed to resolve HMAC secret identity", "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to regenerate HMAC secret")) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to regenerate HMAC secret")) return } httputil.WriteJSON(w, http.StatusOK, api.WebSubAPIHmacSecretCreationResponse{ @@ -256,19 +257,19 @@ 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, utils.NewErrorResponse(404, "Not Found", "WebSub API not found")) + 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, utils.NewErrorResponse(404, "Not Found", "HMAC secret not found")) + 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, utils.NewErrorResponse(409, "Conflict", "An HMAC secret with this name already exists")) + 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, utils.NewErrorResponse(400, "Bad Request", "Secret value must be at least 32 characters")) + 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, utils.NewErrorResponse(503, "Service Unavailable", "HMAC secret management is not configured on this server")) + 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, utils.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) } } diff --git a/platform-api/plugins/eventgateway/handler/websub_apikey.go b/platform-api/plugins/eventgateway/handler/websub_apikey.go index b4df96b9c2..2c5ad5c7cc 100644 --- a/platform-api/plugins/eventgateway/handler/websub_apikey.go +++ b/platform-api/plugins/eventgateway/handler/websub_apikey.go @@ -27,6 +27,7 @@ import ( "net/http" "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/service" @@ -65,13 +66,13 @@ func (h *WebSubAPIKeyHandler) RegisterRoutes(mux *http.ServeMux) { func (h *WebSubAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } apiHandle := r.PathValue("webSubApiId") if apiHandle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API handle is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API handle is required")) return } @@ -88,12 +89,12 @@ func (h *WebSubAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Reques var req api.CreateAPIKeyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Invalid request body")) return } if req.ApiKey == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API key value is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API key value is required")) return } @@ -103,7 +104,7 @@ func (h *WebSubAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Reques } else { generatedName, err := utils.GenerateHandle(req.DisplayName, nil) if err != nil { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Failed to generate API key name")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Failed to generate API key name")) return } name = generatedName @@ -112,15 +113,15 @@ 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) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "WebSub API not found")) + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "WebSub API not found")) return } if errors.Is(err, constants.ErrGatewayUnavailable) { - httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponse(503, "Service Unavailable", "No gateway connections available")) + httputil.WriteJSON(w, http.StatusServiceUnavailable, apperror.NewErrorResponse(503, "Service Unavailable", "No gateway connections available")) return } h.slogger.Error("Failed to create API key for WebSub API", "apiHandle", apiHandle, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to create API key")) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to create API key")) return } @@ -135,19 +136,19 @@ func (h *WebSubAPIKeyHandler) CreateAPIKey(w http.ResponseWriter, r *http.Reques func (h *WebSubAPIKeyHandler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } apiHandle := r.PathValue("webSubApiId") if apiHandle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API handle is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API handle is required")) return } keyName := r.PathValue("apiKeyId") if keyName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Key name is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Key name is required")) return } @@ -165,34 +166,34 @@ func (h *WebSubAPIKeyHandler) UpdateAPIKey(w http.ResponseWriter, r *http.Reques var req api.UpdateAPIKeyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.slogger.Warn("Invalid API key update request", "orgId", orgID, "apiHandle", apiHandle, "keyName", keyName, "error", err) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Invalid request body: "+err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Invalid request body: "+err.Error())) return } if req.ApiKey == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API key value is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API key value is required")) return } // Validate that the name in the request body (if provided) matches the URL path parameter if err := utils.ValidateHandleImmutable(keyName, req.Name); err != nil { h.slogger.Warn("API key name mismatch", "orgId", orgID, "apiHandle", apiHandle, "urlKeyName", keyName, "bodyKeyName", *req.Name) - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", fmt.Sprintf("API key name mismatch: name in request body '%s' must match the key name in URL '%s'", *req.Name, keyName))) return } if err := h.apiKeyService.UpdateAPIKey(r.Context(), apiHandle, constants.WebSubApi, orgID, keyName, userId, &req); err != nil { if errors.Is(err, constants.ErrAPINotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "WebSub API not found")) + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "WebSub API not found")) return } if errors.Is(err, constants.ErrGatewayUnavailable) { - httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponse(503, "Service Unavailable", "No gateway connections available")) + httputil.WriteJSON(w, http.StatusServiceUnavailable, apperror.NewErrorResponse(503, "Service Unavailable", "No gateway connections available")) return } h.slogger.Error("Failed to update API key for WebSub API", "apiHandle", apiHandle, "keyName", keyName, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to update API key")) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to update API key")) return } @@ -209,7 +210,7 @@ func (h *WebSubAPIKeyHandler) UpdateAPIKey(w http.ResponseWriter, r *http.Reques func (h *WebSubAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.Request) { orgID, ok := middleware.GetOrganizationFromRequest(r) if !ok { - httputil.WriteJSON(w, http.StatusUnauthorized, utils.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) + httputil.WriteJSON(w, http.StatusUnauthorized, apperror.NewErrorResponse(401, "Unauthorized", "Organization claim not found in token")) return } @@ -217,11 +218,11 @@ func (h *WebSubAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.Reques keyName := r.PathValue("apiKeyId") if apiHandle == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "API handle is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "API handle is required")) return } if keyName == "" { - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", "Key name is required")) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", "Key name is required")) return } @@ -232,19 +233,19 @@ 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) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "WebSub API not found")) + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "WebSub API not found")) return } if errors.Is(err, constants.ErrAPIKeyNotFound) { - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "API key not found")) + httputil.WriteJSON(w, http.StatusNotFound, apperror.NewErrorResponse(404, "Not Found", "API key not found")) return } if errors.Is(err, constants.ErrGatewayUnavailable) { - httputil.WriteJSON(w, http.StatusServiceUnavailable, utils.NewErrorResponse(503, "Service Unavailable", "No gateway connections available")) + httputil.WriteJSON(w, http.StatusServiceUnavailable, apperror.NewErrorResponse(503, "Service Unavailable", "No gateway connections available")) return } h.slogger.Error("Failed to delete API key for WebSub API", "apiHandle", apiHandle, "keyName", keyName, "error", err) - httputil.WriteJSON(w, http.StatusInternalServerError, utils.NewErrorResponse(500, "Internal Server Error", "Failed to delete API key")) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "Failed to delete API key")) return } @@ -255,11 +256,11 @@ func (h *WebSubAPIKeyHandler) DeleteAPIKey(w http.ResponseWriter, r *http.Reques func (h *WebSubAPIKeyHandler) handleServiceError(w http.ResponseWriter, err error) { switch { case errors.Is(err, constants.ErrInvalidInput): - httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponse(400, "Bad Request", err.Error())) + httputil.WriteJSON(w, http.StatusBadRequest, apperror.NewErrorResponse(400, "Bad Request", err.Error())) case errors.Is(err, constants.ErrWebSubAPINotFound): - httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found", "WebSub API not found")) + 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, utils.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) + httputil.WriteJSON(w, http.StatusInternalServerError, apperror.NewErrorResponse(500, "Internal Server Error", "An unexpected error occurred")) } } diff --git a/platform-api/resources/openapi.yaml b/platform-api/resources/openapi.yaml index b477e6778d..416d7d9ef2 100644 --- a/platform-api/resources/openapi.yaml +++ b/platform-api/resources/openapi.yaml @@ -8468,7 +8468,7 @@ components: $ref: '#/components/schemas/Error' example: status: error - code: COMMON_UNAUTHORIZED + code: UNAUTHORIZED message: Authorization header is required, or the token is invalid or expired. BadRequest: @@ -8479,7 +8479,7 @@ components: $ref: '#/components/schemas/Error' example: status: error - code: COMMON_VALIDATION_FAILED + code: VALIDATION_FAILED message: The request failed validation. errors: - field: spec.context @@ -8492,7 +8492,7 @@ components: $ref: '#/components/schemas/Error' example: status: error - code: COMMON_FORBIDDEN + code: FORBIDDEN message: You do not have permission to perform this action. NotFound: description: Not Found. The specified resource does not exist. @@ -8502,7 +8502,7 @@ components: $ref: '#/components/schemas/Error' example: status: error - code: COMMON_NOT_FOUND + code: NOT_FOUND message: The specified resource does not exist. Conflict: description: Conflict. Specified resource already exists. @@ -8512,7 +8512,7 @@ components: $ref: '#/components/schemas/Error' example: status: error - code: COMMON_CONFLICT + code: CONFLICT message: The specified resource already exists. InternalServerError: description: Internal Server Error. @@ -8522,7 +8522,7 @@ components: $ref: '#/components/schemas/Error' example: status: error - code: COMMON_INTERNAL_ERROR + code: INTERNAL_ERROR message: An unexpected error occurred. trackingId: 4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11 ServiceUnavailable: @@ -8533,7 +8533,7 @@ components: $ref: '#/components/schemas/Error' example: status: error - code: COMMON_SERVICE_UNAVAILABLE + code: SERVICE_UNAVAILABLE message: Secrets management is not configured. Set PLATFORM_SECRET_ENCRYPTION_KEY. GatewayConnectionUnavailable: From f1cff12d759b470f4143c29493f6368fe06a2c8d Mon Sep 17 00:00:00 2001 From: Thushani Jayasekera Date: Wed, 8 Jul 2026 16:16:03 +0530 Subject: [PATCH 11/12] Add Origin method to apperror for enhanced error tracing This commit introduces the Origin method to the apperror.Error struct, which captures the file and line number where the error was constructed. This information is now logged in the error_mapper, providing clearer context for error origins, distinct from the log call site. This enhancement improves error traceability and debugging capabilities across the application. --- platform-api/internal/apperror/apperror.go | 25 +++++++++++++++++++ .../internal/middleware/error_mapper.go | 6 +++++ 2 files changed, 31 insertions(+) diff --git a/platform-api/internal/apperror/apperror.go b/platform-api/internal/apperror/apperror.go index c5d1081305..67cb25318a 100644 --- a/platform-api/internal/apperror/apperror.go +++ b/platform-api/internal/apperror/apperror.go @@ -126,6 +126,31 @@ func (e *Error) WithCause(err error) *Error { return e } +// Origin symbolizes just the first frame of the stack captured at +// construction time — the file:line of the Def.New/Def.Wrap call site where +// the error actually originated, e.g. "/internal/handler/application.go:142". +// The path is trimmed to start at "/internal/" so logs carry a short, +// repo-relative path instead of the full build-machine absolute path. This is +// distinct from slog's own "source" attribute, which always points at the +// mapper's log call site (middleware/error_mapper.go) regardless of where +// the error was created, and is therefore useless for locating the throw +// site. Returns "" if no stack was captured. +func (e *Error) Origin() string { + if len(e.Stack) == 0 { + return "" + } + frames := runtime.CallersFrames(e.Stack) + frame, _ := frames.Next() + if frame.File == "" { + return "" + } + file := frame.File + if idx := strings.Index(file, "/internal/"); idx != -1 { + file = file[idx:] + } + return fmt.Sprintf("%s:%d", file, frame.Line) +} + // StackString symbolizes the stack captured at construction time. Kept as // raw []uintptr on the struct (cheap to capture) and only symbolized lazily // when the mapper actually logs it, rather than paying runtime.CallersFrames diff --git a/platform-api/internal/middleware/error_mapper.go b/platform-api/internal/middleware/error_mapper.go index b232b59627..6985936c40 100644 --- a/platform-api/internal/middleware/error_mapper.go +++ b/platform-api/internal/middleware/error_mapper.go @@ -100,6 +100,12 @@ func writeMappedError(w http.ResponseWriter, r *http.Request, slogger *slog.Logg 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 { From 09d4e00ac1522df4131dd0aaa040320547105591 Mon Sep 17 00:00:00 2001 From: Thushani Jayasekera Date: Wed, 8 Jul 2026 17:05:31 +0530 Subject: [PATCH 12/12] Enhance error logging in SyncCustomPolicy method --- platform-api/internal/service/gateway.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/platform-api/internal/service/gateway.go b/platform-api/internal/service/gateway.go index e64a68e19e..8ee28c3e22 100644 --- a/platform-api/internal/service/gateway.go +++ b/platform-api/internal/service/gateway.go @@ -329,7 +329,8 @@ func (s *GatewayService) SyncCustomPolicy(gatewayID, orgID, policyName, version gateway, err := s.gatewayRepo.GetByUUID(gatewayID) if err != nil { - return nil, apperror.Internal.Wrap(err).WithLogMessage("failed to get gateway") + s.slogger.Error("failed to get gateway", slog.String("gateway_id", gatewayID), slog.String("org_id", orgID), slog.String("error", err.Error())) + return nil, apperror.GatewayNotFound.New() } if gateway == nil { return nil, apperror.GatewayNotFound.New()