Skip to content

[API Platform ] Refactor error handling in API handlers to use standardized error res#2507

Merged
Thushani-Jayasekera merged 15 commits into
wso2:mainfrom
Thushani-Jayasekera:err_resp_fix
Jul 8, 2026
Merged

[API Platform ] Refactor error handling in API handlers to use standardized error res#2507
Thushani-Jayasekera merged 15 commits into
wso2:mainfrom
Thushani-Jayasekera:err_resp_fix

Conversation

@Thushani-Jayasekera

@Thushani-Jayasekera Thushani-Jayasekera commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

$subject

Breaking changes: APR-004

The Error schema in resources/openapi.yaml has been changed to comply with
APR-004 ("Errors must use the standard error response shape"). This is a
breaking change for any existing client parsing error response bodies.

What changed

Old shape:

{
  "code": 404,
  "title": "Not Found",
  "details": "The specified resource does not exist",
  "errors": [ { "any": "object" } ]
}

Why this couldn't be an autofix

Renaming/retyping response body fields is a breaking change by definition
(autoFixable: false per the APR-004 rule). It was applied directly here
because the fix was explicitly requested, not because it's safe to roll out
silently — treat this as a versioned/coordinated API change, not a patch.

Error Handling in Platform API

This document describes how HTTP errors are constructed, logged, and
serialized in Platform API, for reviewers of the error-handling refactor.

Problem

Before this change, every handler built its own error response inline:

httputil.WriteJSON(w, http.StatusNotFound, utils.NewErrorResponse(404, "Not Found",
    "Project not found"))

This meant:

  • The same failure could come back with different codes/messages depending
    on which handler hit it (e.g. "Project not found" vs "project could not be
    found").
  • Nothing stopped a handler from leaking a raw DB error, a stack trace, or an
    internal path into the response body.
  • Auth failures sometimes said why they failed ("token expired" vs "user
    not found"), which is an enumeration/probing risk.
  • 500s and 400s were logged (or not) inconsistently, with no way to
    correlate a client-visible failure with the corresponding server log line.
  • Every handler paid the cost of hand-rolling status/code/message triples,
    which drift over time.

Design

apperror.Error — one typed error, not an inline response

internal/apperror defines Error, a typed error that carries everything
the wire response and the log line need:

type Error struct {
    Code        string             // catalog code, e.g. "PROJECT_NOT_FOUND"
    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
}

Handlers no longer write error responses themselves — they return an
error:

func (h *ProjectHandler) GetProject(w http.ResponseWriter, r *http.Request) error {
    project, err := h.projectService.GetProjectByHandle(projectId, orgID)
    if err != nil {
        if errors.Is(err, constants.ErrProjectNotFound) {
            return apperror.ProjectNotFound.Wrap(err)
        }
        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
}

The catalog — code, status, and message bound together

internal/apperror/catalog.go declares every client-facing error condition
exactly once, as a Def binding a stable code, an HTTP status, and a
message template:

ProjectNotFound = def(utils.CodeProjectNotFound, http.StatusNotFound,
    "The specified project could not be found.")
RESTAPIExists = def(utils.CodeRESTAPIExists, http.StatusConflict, "%s") // call-site-specific

An *apperror.Error can only be constructed through:

  • Def.New(args...) — a business-rule failure with no underlying error
    (e.g. "displayName is required").
  • Def.Wrap(cause, args...) — a failure caused by a lower-level error (DB,
    downstream call); cause is a required argument so it can't be forgotten,
    and it's never sent to the client.
  • apperror.NewValidation(err) — converts validator.ValidationErrors into
    field-level errors under COMMON_VALIDATION_FAILED.

Because construction only goes through these paths, a handler can't pair a
code with the wrong status, or drift a message away from what's declared in
the catalog. catalog_test.go asserts catalog integrity (unique codes, valid
statuses) as a unit test, and Unauthorized is asserted to be the only
401 entry with a fixed message (see below).

MapErrors — one place that writes the response and the log line

internal/middleware/error_mapper.go adapts a handler that returns error
into a normal http.HandlerFunc, and is the single place that turns an
error into bytes on the wire and a line in the log:

mux.HandleFunc("GET /projects/{projectId}", middleware.MapErrors(h.slogger, h.GetProject))

MapErrors:

  1. Recovers panics into a structured 500 (COMMON_INTERNAL_ERROR), logging
    the panic value, path, method, and stack — the client never sees a raw
    panic.
  2. If the handler returns an error that isn't an *apperror.Error, wraps it
    as apperror.Internal — so an unhandled Go error can never leak its
    .Error() text to the client.
  3. Applies a severity split for logging:
    • 4xx = a client outcome, not a system fault. Logged at WARN,
      without a stack trace.
    • 5xx = a system fault. Logged at ERROR, with the origin stack
      attached, and a trackingId is both logged and echoed in the response
      body so the client can quote it back for support/correlation.
if appErr.HTTPStatus >= http.StatusInternalServerError {
    slogger.Error("request failed", logFields...) // + stack
    apperror.WriteHTTP(w, appErr, trackID)         // trackingId in body
    return
}
slogger.Warn("request failed", logFields...)
apperror.WriteHTTP(w, appErr, "")                  // no trackingId — 4xx is self-explanatory

apperror.WriteHTTP is the single function that serializes an *Error
onto the wire, so the two callers that need it (MapErrors, and the
pre-routing auth middleware — see below) can never drift the wire format
apart.

Every error carries all of its details in one place, at the point where the
failure is detected. Whoever detects a failure constructs one typed error and
returns it — nothing else. A single mapper at the routing layer owns the rest:
logging (with tracking ID and origin stack), and serializing the sanitized
client-facing HTTP response.

service / handler                     middleware.MapErrors
─────────────────                     ────────────────────
return apperror.X.New()      ──────►  log (code, status, detail, cause,
  .WithLogMessage(...)                 stack for 5xx, trackingId)
  .WithCause(err)                     write utils.ErrorResponse via
                                       apperror.WriteHTTP

Rules of thumb:

  • Construct the error where the failure is detected — usually the service
    layer. The stack captured at construction is only useful if it points at the
    true origin.

  • Client-facing text comes from the catalog; internal detail goes in
    WithLogMessage(...); the underlying error goes in Wrap(cause) /
    WithCause(err). Never put internal detail in the client message.

  • Handlers pass typed errors through untouched. A handler's error branch
    should be, at most:

    if err != nil {
        var appErr *apperror.Error
        if errors.As(err, &appErr) {
            return err // service already said everything
        }
        // legacy sentinel translation (being phased out) ...
        return apperror.Internal.Wrap(err).WithLogMessage("context")
    }
  • No string-matching on err.Error() to decide the HTTP status. That was
    the source of silent 500s (message text drifts, matching breaks silently).

  • The mapper prefers an inner typed error when a handler fallback blindly
    wrapped it in a generic Internal, so a specific service error can never be
    masked into a 500 by a stale handler fallback.

Unified auth failures

The auth middleware (internal/middleware/auth.go) runs ahead of routing,
so it can't return through MapErrors. It calls the same
apperror.WriteHTTP directly via a small writeError helper, keeping the
wire format identical to the mapped path. Every authentication failure —
missing header, malformed header, expired/invalid/revoked token — returns
the same apperror.Unauthorized payload:

{ "status": "error", "code": "COMMON_UNAUTHORIZED", "message": "Invalid or expired credentials." }

The specific reason ("password mismatch", "token expired", "user not found", ...) is passed as reason and only ever reaches the server log,
never the response body — this is what prevents user/credential
enumeration. These are still routine 4xx client outcomes, so they log at
WARN, matching the severity split above.

Wire format

Every error response is the same shape (utils.ErrorResponse):

{
  "status": "error",
  "code": "PROJECT_NOT_FOUND",
  "message": "The specified project could not be found.",
  "errors": [{ "field": "displayName", "message": "..." }],
  "details": { "...": "..." },
  "trackingId": "4f1c6f2e-8a4b-4c93-b1de-9f2f6f0c2a11"
}
  • code is the stable, machine-readable catalog code.
  • errors is populated only for field-level validation failures.
  • details carries optional structured metadata specific to code.
  • trackingId is present only on 5xx responses — a bare UUID with no
    source markers (no file/line/env baked in), so it's safe to log and safe
    to hand to a client without revealing internals.

What this fixes

  • No more internal leakage. DB errors, stack traces, and internal
    identifiers stay in Cause/LogMessage/Stack, which only the log line
    sees — the client only ever gets Code/Message/FieldErrors/Details.
  • No more auth enumeration. Every auth failure is byte-for-byte
    identical regardless of cause.
  • Consistent codes and messages. A given failure (e.g. "project already
    exists") now returns the same code and message everywhere, instead of
    drifting per handler. As part of this change, several messages were also
    tightened for consistency, e.g.:
    • SECRET_NOT_FOUND: "Secret not found""The specified secret could not be found."
    • SECRET_EXISTS: "A secret with this name already exists in this scope""A secret with this name already exists."
    • SUBSCRIPTION_EXISTS: "A subscription already exists for this API.""A subscription for this application and API already exists."
  • A real bug fix. "Create subscription plan with a missing id" used to
    fall through to a generic 500 COMMON_INTERNAL_ERROR; it now correctly
    returns 400 COMMON_VALIDATION_FAILED with a field-specific message.
  • Correlatable 5xxs. Every 5xx now carries a trackingId in both the
    response and the log line, so a reported failure can be traced straight
    to its server-side log entry (including the origin stack).

Guardrails

platform-api/scripts/check-error-handling.sh (wired into make check-error-handling) enforces the pattern in CI:

  1. Migrated handlers may not write 4xx/5xx responses inline — they must
    return an *apperror.Error for MapErrors to handle.
  2. apperror.Error{} may only be constructed inside the apperror package
    (i.e. via a catalog Def), never as a struct literal elsewhere.
  3. Wrapping an error with fmt.Errorf must use %w, not %v/%s, so
    errors.As can still find the underlying *apperror.Error through the
    chain.
  4. No ad-hoc error code strings in handlers — new codes must be declared in
    the catalog.

Fixes APR-005

POST operations that return 201 Created now include a Location header pointing to the new resource.

  • Added shared Location header component to platform-api/resources/openapi.yaml, referenced from all 21 201 responses.
  • Added setLocation helper (internal/handler/location.go) and wired it into every create/deploy handler.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces a new apperror package (typed Error, catalog Defs, WriteHTTP, MapErrors middleware) and migrates platform-api and eventgateway plugin handlers, middleware, and services to return coded apperror values instead of writing JSON directly. Updates the secret conflict DTO, OpenAPI error schemas/examples, and adds/updates related tests.

Changes

Standardized error codes and responses

Layer / File(s) Summary
apperror core, catalog, and legacy contract
platform-api/internal/apperror/*.go, platform-api/internal/utils/error.go
Defines apperror.Error, Def/catalog entries, NewValidation, WriteHTTP, MapErrors middleware adapter, catalog code constants, and updates the legacy ErrorResponse/FieldError schema.
Secret DTO and service/handler error mapping
platform-api/internal/dto/secret.go, platform-api/internal/service/secret_service.go, platform-api/internal/handler/secret.go, platform-api/internal/handler/secret_integration_test.go
Replaces SecretDeleteConflictResponse with SecretInUseDetails, migrates secret create/list/get/update/delete flows to typed apperror responses including SECRET_IN_USE details.
Middleware, auth login, websocket, webhook errors
platform-api/internal/middleware/auth.go, .../authorization.go, platform-api/internal/handler/identity_helper.go, .../auth_login.go, .../websocket.go, platform-api/internal/webhook/receiver.go
Converts auth/authorization middleware, identity resolution, login, websocket connect, and webhook receive flows to return/construct apperror values.
REST API, deployment, API key handler/service mapping
platform-api/internal/handler/api*.go, .../location.go, platform-api/internal/service/api.go, .../apikey.go, .../deployment.go
Migrates REST API CRUD, deployments, and API key lifecycle handlers/services to error-returning apperror flows.
Application, organization, project, subscription mapping
platform-api/internal/handler/application.go, .../organization.go, .../project.go, .../subscription_handler.go, .../subscription_plan_handler.go, .../apikey_user.go, corresponding services
Converts application/organization/project/subscription CRUD, associations, and lifecycle endpoints/services to typed apperror responses.
Gateway, custom policy, MCP proxy mapping
platform-api/internal/handler/gateway.go, .../mcp.go, .../mcp_deployment.go, platform-api/internal/service/gateway.go, custom policy tests
Migrates gateway/token/manifest/custom-policy and MCP proxy handlers/services to apperror-based responses.
LLM provider/proxy/template/API key/deployment mapping
platform-api/internal/handler/llm*.go, platform-api/internal/service/llm*.go, platform-api/internal/handler/llm_provider_integration_test.go
Converts LLM provider/proxy/template/API key/deployment endpoints/services to apperror responses, adds integration test suite.
Event gateway plugin error responses
platform-api/plugins/eventgateway/handler/*.go, platform-api/internal/handler/gateway_internal.go
Switches eventgateway plugin handlers and internal gateway API handlers from utils.NewErrorResponse to apperror.NewErrorResponse.
OpenAPI schema and examples
platform-api/resources/openapi.yaml
Updates Error/FieldError schemas, Location headers, shared response references, and error examples across operations.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • wso2/api-platform#2356: Adds/handles ErrArtifactRuntimeImmutable in the artifact guard/update flow, directly related to this PR's mapping of that error to apperror.ArtifactRuntimeImmutable.

Suggested reviewers: RakhithaRR, Tharsanan1, VirajSalaka, tharindu1st, malinthaprasan, AnuGayan, chamilaadhi, Arshardh, dushaniw, tgtshanika, senthuran16, tharikaGitHub, CrowleyRajapakse, hisanhunais, HiranyaKavishani, HeshanSudarshana, ashera96, pubudu538, Krishanx92, PasanT9, renuka-fernando

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers purpose and approach, but it omits most required template sections like goals, tests, security checks, and related PRs. Add the missing template sections: Goals, User stories, Documentation, Automation tests, Security checks, Samples, Related PRs, and Test environment.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: refactoring API handler error handling to standardized responses.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain modules listed in go.work or their selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
platform-api/internal/middleware/auth.go (1)

108-124: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Auth-failure messages diverge across missing/malformed/invalid-token branches.

writeJSONError now emits a uniform envelope, but the message text differs across the three 401 branches: "Authorization header is required" (Line 110), "Invalid authorization header format..." (Line 116), and "Invalid or expired credentials." (Line 123). As per coding guidelines, **/*.go: "return the exact same client-facing payload and HTTP status for invalid, expired, missing, or revoked credentials: 401 Unauthorized with {"error":"unauthorized","message":"Invalid or expired credentials."}". Missing header is explicitly one of the covered states, so these three should return identical text.

🔒️ Proposed fix to unify auth-failure messaging
 			authHeader := r.Header.Get("Authorization")
 			if authHeader == "" {
-				writeJSONError(w, http.StatusUnauthorized, "Authorization header is required")
+				writeJSONError(w, http.StatusUnauthorized, "Invalid or expired credentials.")
 				return
 			}

 			tokenString := strings.TrimPrefix(authHeader, "Bearer ")
 			if tokenString == authHeader {
-				writeJSONError(w, http.StatusUnauthorized, "Invalid authorization header format. Expected: Bearer <token>")
+				writeJSONError(w, http.StatusUnauthorized, "Invalid or expired credentials.")
 				return
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/middleware/auth.go` around lines 108 - 124, The auth
failure handling in auth middleware is inconsistent across the missing-header,
malformed-header, and invalid-token branches, which should all return the same
client-facing 401 payload. Update the Authorization checks in the auth
middleware flow around validateLocalJWT and writeJSONError so all three paths
use the exact same unauthorized message, matching the existing invalid/expired
credentials response. Keep the status as 401 Unauthorized and ensure the
response body stays uniform for missing, malformed, expired, or revoked tokens.

Source: Coding guidelines

🧹 Nitpick comments (5)
platform-api/internal/handler/gateway.go (1)

134-138: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Avoid forwarding the raw service error string to clients.

This validation branch is keyword-matched on the service error and then returns the raw errMsg as the client-facing message. Unlike the sibling branches (which use sterile text), this can leak internal/lower-layer error detail. Prefer a sterile, code-appropriate message and keep the raw error only in the log line above.

As per coding guidelines: "never expose raw database errors, stack traces, internal microservice names, network topology, or file system paths to the client; sanitize internal errors for logging and map them to sterile user-facing error objects before marshaling."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/handler/gateway.go` around lines 134 - 138, The
validation branch in gateway.go is leaking the raw service error text to clients
by passing errMsg into NewErrorResponseWithCode after the keyword check in the
gateway handler. Update the error response in this branch to use a sterile,
client-safe message consistent with the other branches, while keeping the full
err value only in h.slogger.Error inside the gateway handler logic. Ensure the
user-facing payload does not include internal service details, and leave the
logging path unchanged for debugging.

Source: Coding guidelines

platform-api/internal/handler/llm_provider_integration_test.go (1)

67-70: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Use a parameterized query instead of string concatenation.

OpenGrep flags this INSERT as built via string concatenation. provOrg is a hardcoded constant here so there's no real injection risk today, but the pattern is easy to copy-paste into a future test with actual variable input.

🛡️ Suggested fix
-	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 {
+	if _, err = db.Exec(`INSERT INTO organizations (uuid, handle, display_name, region, idp_organization_ref_uuid, created_at, updated_at)
+		VALUES (?, 'test-prov-org', 'Test Prov Org', 'default', 'idp-ref', datetime('now'), datetime('now'))`, provOrg); err != nil {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/handler/llm_provider_integration_test.go` around lines
67 - 70, The INSERT in the test setup is built with string concatenation even
though provOrg is currently constant, so replace it with a parameterized Exec
call in the same test helper to avoid copy-pasteable SQL construction patterns.
Update the db.Exec call in llm_provider_integration_test.go to pass the
organization values as arguments rather than embedding them into the SQL text,
keeping the existing insert logic and t.Fatalf handling unchanged.

Source: Linters/SAST tools

platform-api/internal/handler/organization.go (2)

56-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Decode-error path skips structured validation details.

The body-decode failure here returns a flat CodeCommonValidationFailed message, while CreateApplication/CreateProject use utils.NewValidationErrorResponse(w, err) for the same scenario, which presumably yields field-level errors[] detail. Consider using the same helper here for consistency with the new standardized contract this PR is establishing.

♻️ Suggested consistency fix
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
-		httputil.WriteJSON(w, http.StatusBadRequest, utils.NewErrorResponseWithCode(
-			utils.CodeCommonValidationFailed, "Invalid request body"))
+		utils.NewValidationErrorResponse(w, err)
 		return
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/handler/organization.go` around lines 56 - 77, The
decode-error path in the organization create handler is returning only a flat
validation message instead of the structured field-level validation payload used
elsewhere. Update the request body decode failure branch in the organization
handler to use the same helper as CreateApplication and CreateProject, namely
utils.NewValidationErrorResponse, so the response format stays consistent with
the standardized validation contract.

74-78: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Missing error log on UUID generation failure.

Unlike the sibling failure branch below (line 108), this 500 path doesn't log genErr before responding, losing observability for this rare failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/handler/organization.go` around lines 74 - 78, In the
organization handler’s UUID generation failure branch around GenerateUUID, add
an error log before returning the 500 response so genErr is not dropped; mirror
the sibling error-handling path used later in the same function by logging the
failure with context, then keep the existing httputil.WriteJSON response
unchanged.
platform-api/internal/handler/subscription_handler.go (1)

78-84: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Decode-error paths bypass structured validation details, same as organization.go.

Both CreateSubscription and UpdateSubscription return a flat message on JSON decode failure, while CreateApplication/CreateProject use utils.NewValidationErrorResponse(w, err) for equivalent decode failures. For a consistent client-facing contract, consider using the same helper here.

Also applies to: 322-327

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/handler/subscription_handler.go` around lines 78 - 84,
The JSON decode failure path in CreateSubscription and UpdateSubscription is
returning only a generic error response instead of the structured validation
details used by CreateApplication and CreateProject. Update the decode-error
handling in the subscription handler methods to use
utils.NewValidationErrorResponse(w, err) for failed
json.NewDecoder(r.Body).Decode calls, keeping the existing slogger error logging
and the same request validation contract as the other handlers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@platform-api/internal/handler/api_deployment.go`:
- Around line 67-72: The JSON decode failure handling in api_deployment.go is
returning the raw decoder error directly instead of using the shared validation
response path. Update the handler that decodes api.DeployRequest to route decode
errors through the same helper used by CreateAPI, UpdateAPI, and
AddGatewaysToAPI in api.go, so the response is built via
utils.NewValidationErrorResponse rather than
utils.NewErrorResponseWithCode(err.Error()).

In `@platform-api/internal/handler/api_key.go`:
- Around line 178-183: The JSON decode failure path in apiKeyHandler should not
return the raw encoding/json error text in the client response. Update the
decode error handling in the request body decode block to keep the detailed
error only in the slogger.Warn call, and return the same sanitized generic
message used by CreateAPIKey, preserving the existing validation error code
without appending err.Error().

In `@platform-api/internal/handler/application.go`:
- Around line 531-583: `ErrOrganizationNotFound` is mapped inconsistently across
handlers, so align the status-code contract in the `CreateProject` handling path
to match the existing `application.go` and `ListProjects` behavior. Update the
`errors.Is(err, constants.ErrOrganizationNotFound)` branch in the `project.go`
handler(s) so it returns the same HTTP status as the `CodeOrganizationNotFound`
response everywhere, using the existing error-code mapping and handler names to
locate the inconsistency.

In `@platform-api/internal/handler/llm_deployment.go`:
- Around line 74-79: `DeployLLMProvider` and `DeployLLMProxy` are returning the
raw JSON decode error to clients via `utils.NewErrorResponseWithCode`, which
leaks internal parsing details and differs from the standard contract. Update
the decode-failure handling in `llm_deployment.go` to match the other
request-body validation paths in `llm.go`, `llm_apikey.go`, and
`llm_proxy_apikey.go` by sending a fixed user-facing message like “Invalid
request body” with `utils.CodeCommonValidationFailed`. Keep the detailed `err`
only for server-side logging if needed, but do not marshal `err.Error()` into
the response.

In `@platform-api/internal/handler/llm.go`:
- Around line 110-115: The org-claim authorization check is duplicated across
multiple LLM handlers, causing repeated Unauthorized response logic and risking
drift. Add a shared helper in llm.go, such as requireOrgID, that wraps
middleware.GetOrganizationFromRequest and returns the org ID or writes the
CodeCommonUnauthorized JSON error with the same message. Update each handler in
llm.go and the related copies in llm_apikey.go, llm_deployment.go, and
llm_proxy_apikey.go to call this helper instead of inlining the block.

In `@platform-api/internal/handler/organization.go`:
- Around line 129-133: Restore the tenant-match guard in HeadOrganization before
any existence lookup: compare the requested organization ID against the org ID
from the request context/token and return the existing forbidden response when
they differ. Re-enable this check in
platform-api/internal/handler/organization.go within HeadOrganization so callers
cannot probe other tenants’ organization handles, and keep the rest of the
existence flow unchanged.

In `@platform-api/internal/handler/project.go`:
- Around line 76-92: Align the ErrOrganizationNotFound handling in project.go’s
create-project error path with the other org-not-found mappings in ListProjects
and application.go’s writeApplicationError. Update the branch that checks
errors.Is(err, constants.ErrOrganizationNotFound) to return the same HTTP status
used elsewhere for CodeOrganizationNotFound, so the status/code contract is
consistent across endpoints.

In `@platform-api/internal/utils/error.go`:
- Around line 51-63: The doc comment on NewErrorResponse is inaccurate because
title is still used as the fallback response message when description is empty
or omitted. Update the comment to match the actual fallback behavior in
NewErrorResponse and clarify that title is only superseded when a non-empty
description[0] is provided, so callers and maintainers do not treat title as
unused.

---

Outside diff comments:
In `@platform-api/internal/middleware/auth.go`:
- Around line 108-124: The auth failure handling in auth middleware is
inconsistent across the missing-header, malformed-header, and invalid-token
branches, which should all return the same client-facing 401 payload. Update the
Authorization checks in the auth middleware flow around validateLocalJWT and
writeJSONError so all three paths use the exact same unauthorized message,
matching the existing invalid/expired credentials response. Keep the status as
401 Unauthorized and ensure the response body stays uniform for missing,
malformed, expired, or revoked tokens.

---

Nitpick comments:
In `@platform-api/internal/handler/gateway.go`:
- Around line 134-138: The validation branch in gateway.go is leaking the raw
service error text to clients by passing errMsg into NewErrorResponseWithCode
after the keyword check in the gateway handler. Update the error response in
this branch to use a sterile, client-safe message consistent with the other
branches, while keeping the full err value only in h.slogger.Error inside the
gateway handler logic. Ensure the user-facing payload does not include internal
service details, and leave the logging path unchanged for debugging.

In `@platform-api/internal/handler/llm_provider_integration_test.go`:
- Around line 67-70: The INSERT in the test setup is built with string
concatenation even though provOrg is currently constant, so replace it with a
parameterized Exec call in the same test helper to avoid copy-pasteable SQL
construction patterns. Update the db.Exec call in
llm_provider_integration_test.go to pass the organization values as arguments
rather than embedding them into the SQL text, keeping the existing insert logic
and t.Fatalf handling unchanged.

In `@platform-api/internal/handler/organization.go`:
- Around line 56-77: The decode-error path in the organization create handler is
returning only a flat validation message instead of the structured field-level
validation payload used elsewhere. Update the request body decode failure branch
in the organization handler to use the same helper as CreateApplication and
CreateProject, namely utils.NewValidationErrorResponse, so the response format
stays consistent with the standardized validation contract.
- Around line 74-78: In the organization handler’s UUID generation failure
branch around GenerateUUID, add an error log before returning the 500 response
so genErr is not dropped; mirror the sibling error-handling path used later in
the same function by logging the failure with context, then keep the existing
httputil.WriteJSON response unchanged.

In `@platform-api/internal/handler/subscription_handler.go`:
- Around line 78-84: The JSON decode failure path in CreateSubscription and
UpdateSubscription is returning only a generic error response instead of the
structured validation details used by CreateApplication and CreateProject.
Update the decode-error handling in the subscription handler methods to use
utils.NewValidationErrorResponse(w, err) for failed
json.NewDecoder(r.Body).Decode calls, keeping the existing slogger error logging
and the same request validation contract as the other handlers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 773216ea-d3bf-432f-901f-a7d0d37148d3

📥 Commits

Reviewing files that changed from the base of the PR and between a73f12e and 60d4c81.

📒 Files selected for processing (27)
  • platform-api/internal/dto/secret.go
  • platform-api/internal/handler/api.go
  • platform-api/internal/handler/api_deployment.go
  • platform-api/internal/handler/api_key.go
  • platform-api/internal/handler/apikey_user.go
  • platform-api/internal/handler/application.go
  • platform-api/internal/handler/artifact_guard_response.go
  • platform-api/internal/handler/auth_login.go
  • platform-api/internal/handler/gateway.go
  • platform-api/internal/handler/identity_helper.go
  • platform-api/internal/handler/llm.go
  • platform-api/internal/handler/llm_apikey.go
  • platform-api/internal/handler/llm_deployment.go
  • platform-api/internal/handler/llm_provider_integration_test.go
  • platform-api/internal/handler/llm_proxy_apikey.go
  • platform-api/internal/handler/mcp.go
  • platform-api/internal/handler/mcp_deployment.go
  • platform-api/internal/handler/organization.go
  • platform-api/internal/handler/project.go
  • platform-api/internal/handler/secret.go
  • platform-api/internal/handler/secret_integration_test.go
  • platform-api/internal/handler/subscription_handler.go
  • platform-api/internal/handler/subscription_plan_handler.go
  • platform-api/internal/middleware/auth.go
  • platform-api/internal/utils/codes.go
  • platform-api/internal/utils/error.go
  • platform-api/resources/openapi.yaml

Comment thread platform-api/internal/handler/api_deployment.go
Comment thread platform-api/internal/handler/api_key.go
Comment thread platform-api/internal/handler/application.go Outdated
Comment thread platform-api/internal/handler/llm_deployment.go
Comment thread platform-api/internal/handler/llm.go
Comment thread platform-api/internal/handler/organization.go Outdated
Comment thread platform-api/internal/handler/project.go Outdated
Comment thread platform-api/internal/apperror/error.go
@malinthaprasan

malinthaprasan commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Shall we use an exception mapper pattern when implementing this?

ie. a relevant implementation throws a custom error with all the required details eg:
{ code, httpmessage, additionalErrors, logmessage, errorstracktrack}

Then a middleware wraps the server response function, traps all the errors and convert to an HTTP response and send, it will log the error too.

The idea is, once we have this, whenever we need to throw an error, we throw an error and thats it..
the mapper should take care of everything.. sending http response, logging etc

Try if you can track all details of an error in one place, eg:
https://github.com/wso2/carbon-apimgt/blob/master/components/apimgt/org.wso2.carbon.apimgt.api/src/main/java/org/wso2/carbon/apimgt/api/ExceptionCodes.java#L32

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.
@Thushani-Jayasekera

Thushani-Jayasekera commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Server logs

time=2026-07-08T12:56:52.657+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=3a395e17-f728-42da-a33a-0d6100e872c6 code=COMMON_VALIDATION_FAILED status=400 path=/api/portal/v0.9/auth/login method=POST detail="username and password are required"
time=2026-07-08T12:56:52.774+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=94dc0dfb-6eeb-48c5-9a6c-447a1e3ba78c code=COMMON_UNAUTHORIZED status=401 path=/api/portal/v0.9/auth/login method=POST detail="login failed: password mismatch"
time=2026-07-08T12:56:52.798+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=035db358-4278-4854-9c95-a5c7e6166677 code=COMMON_UNAUTHORIZED status=401 path=/api/portal/v0.9/auth/login method=POST detail="login failed: user not found"
time=2026-07-08T12:56:53.229+05:30 level=WARN source=middleware/auth.go:106 msg="request failed" trackingId=8b0f149a-f5db-4907-97d1-edec42cdf738 code=COMMON_UNAUTHORIZED status=401 reason="local JWT validation failed: invalid token: token is malformed: could not JSON decode header: invalid character '\\u0081' looking for beginning of value"
time=2026-07-08T12:56:53.250+05:30 level=WARN source=middleware/auth.go:106 msg="request failed" trackingId=3fc6cf17-1eab-4381-a527-b6b950bed9d9 code=COMMON_UNAUTHORIZED status=401 reason="authorization header missing"
time=2026-07-08T12:56:53.293+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=f222b96e-ee84-4bd9-9be9-1de77809e6e3 code=PROJECT_EXISTS status=409 path=/api/v0.9/projects method=POST detail="A project with this name already exists in the organization."
time=2026-07-08T12:56:53.314+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=7b9e7068-b47d-40d8-91e5-754b5fcbfcbf code=COMMON_VALIDATION_FAILED status=400 path=/api/v0.9/projects method=POST detail="Project displayName is required"
time=2026-07-08T12:56:53.337+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=128c23dd-0afa-40f1-a9b6-def9a14962bd code=PROJECT_EXISTS status=409 path=/api/v0.9/projects method=POST detail="A project with this name already exists in the organization."
time=2026-07-08T12:56:53.378+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=b354b788-d545-487e-848f-34bda3bbce3a code=PROJECT_NOT_FOUND status=404 path=/api/v0.9/projects/does-not-exist method=GET detail="The specified project could not be found."
time=2026-07-08T12:56:53.400+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=376ddfa8-2e90-4da5-a61f-a58127d7e6fe code=PROJECT_NOT_FOUND status=404 path=/api/v0.9/projects/not_a_valid_handle!! method=GET detail="The specified project could not be found."
time=2026-07-08T12:56:53.435+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=a5b97aca-35bf-41f8-b8f8-3846c28a9f4c code=COMMON_VALIDATION_FAILED status=400 path=/api/v0.9/applications method=GET detail="Project ID is required"
time=2026-07-08T12:56:53.482+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=85974ebd-8635-43ad-b4a6-31823ee5f12d code=APPLICATION_EXISTS status=409 path=/api/v0.9/applications method=POST detail="failed to create application in project test-project for org 019f3d39-aee1-7a93-bb96-cf71c37af683 by user eef7f4c4-a9d5-423a-9e24-242340571e13" cause="application already exists in project"
time=2026-07-08T12:56:53.509+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=68ffd239-0a4f-4d86-9217-9333f84c441e code=APPLICATION_EXISTS status=409 path=/api/v0.9/applications method=POST detail="failed to create application in project test-project for org 019f3d39-aee1-7a93-bb96-cf71c37af683 by user eef7f4c4-a9d5-423a-9e24-242340571e13" cause="application already exists in project"
time=2026-07-08T12:56:53.528+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=c6049449-4256-4e7a-b6e5-23a4ea834ba7 code=COMMON_VALIDATION_FAILED status=400 path=/api/v0.9/applications method=POST detail="displayName is required"
time=2026-07-08T12:56:53.548+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=5d6cebad-f526-497a-873c-16d6c338c04f code=PROJECT_NOT_FOUND status=404 path=/api/v0.9/applications method=POST detail="failed to create application in project does-not-exist for org 019f3d39-aee1-7a93-bb96-cf71c37af683 by user eef7f4c4-a9d5-423a-9e24-242340571e13" cause="project not found"
time=2026-07-08T12:56:53.568+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=e87cd1b8-f8ea-4c2f-bf2f-df44e130bc9f code=APPLICATION_NOT_FOUND status=404 path=/api/v0.9/applications/does-not-exist method=GET detail="failed to get application does-not-exist in org 019f3d39-aee1-7a93-bb96-cf71c37af683" cause="application not found"
time=2026-07-08T12:56:53.587+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=d0655436-dee9-4edd-8904-005d584755d4 code=COMMON_VALIDATION_FAILED status=400 path=/api/v0.9/applications/test-app/api-keys method=POST detail="At least one API key mapping is required"
time=2026-07-08T12:56:53.607+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=bdc2cd7c-10e4-441d-b4eb-d077487ab1aa code=COMMON_VALIDATION_FAILED status=400 path=/api/v0.9/applications/does-not-exist/api-keys method=POST detail="At least one API key mapping is required"
time=2026-07-08T12:56:53.630+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=914e734b-4e24-42ab-9b83-acb8970d6879 code=COMMON_VALIDATION_FAILED status=400 path=/api/v0.9/rest-apis method=GET detail="projectId query parameter is required"
time=2026-07-08T12:56:53.651+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=6a0a12dd-6ab1-4fb3-9a08-52515d5acdeb code=REST_API_EXISTS status=409 path=/api/v0.9/rest-apis method=POST detail="API handle already exists in org 019f3d39-aee1-7a93-bb96-cf71c37af683" cause="handle already exists"
time=2026-07-08T12:56:53.677+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=4486ef2e-1fa6-48a5-aa1b-3949b3c4372c code=COMMON_VALIDATION_FAILED status=400 path=/api/v0.9/rest-apis method=POST detail="API context is required"
time=2026-07-08T12:56:53.699+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=e127cda9-7363-4257-8509-ff237f0b1b7e code=PROJECT_NOT_FOUND status=404 path=/api/v0.9/rest-apis method=POST detail="project not found in org 019f3d39-aee1-7a93-bb96-cf71c37af683" cause="project not found"
time=2026-07-08T12:56:53.741+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=8f2db93d-f10d-4a13-aec4-f5c06577d542 code=REST_API_EXISTS status=409 path=/api/v0.9/rest-apis method=POST detail="API handle already exists in org 019f3d39-aee1-7a93-bb96-cf71c37af683" cause="handle already exists"
time=2026-07-08T12:56:53.775+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=5640a04d-cf8b-4131-8c62-e80d376c9a4e code=REST_API_NOT_FOUND status=404 path=/api/v0.9/rest-apis/does-not-exist method=GET detail="API does-not-exist not found in org 019f3d39-aee1-7a93-bb96-cf71c37af683" cause="api not found"
time=2026-07-08T12:56:53.813+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=6bdfa5d3-edc8-4fbd-800c-25cfb2e3000a code=REST_API_NOT_FOUND status=404 path=/api/v0.9/rest-apis/does-not-exist method=DELETE detail="API does-not-exist not found in org 019f3d39-aee1-7a93-bb96-cf71c37af683" cause="api not found"
time=2026-07-08T12:56:54.076+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=aafcafb2-5d63-401c-ad4a-35ff26c54ee9 code=LLM_PROVIDER_TEMPLATE_NOT_FOUND status=404 path=/api/v0.9/llm-provider-templates/does-not-exist method=GET detail="The specified LLM provider template could not be found." cause="LLM_PROVIDER_TEMPLATE_NOT_FOUND: The specified LLM provider template could not be found.: llm provider template not found"
time=2026-07-08T12:56:54.121+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=8fd6fa99-8893-4ba1-b5dc-43fd52c9d3b4 code=LLM_PROVIDER_EXISTS status=409 path=/api/v0.9/llm-providers method=POST detail="An LLM provider with this ID already exists." cause="llm provider already exists"
time=2026-07-08T12:56:54.142+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=f45f242b-416b-4056-9628-d1aa683ba7b0 code=LLM_PROVIDER_TEMPLATE_REF_NOT_FOUND status=400 path=/api/v0.9/llm-providers method=POST detail="The referenced LLM provider template could not be found." cause="LLM_PROVIDER_TEMPLATE_NOT_FOUND: The specified LLM provider template could not be found.: llm provider template not found"
time=2026-07-08T12:56:54.163+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=8abfec36-6b41-43dd-a01b-231203a5284b code=LLM_PROVIDER_EXISTS status=409 path=/api/v0.9/llm-providers method=POST detail="An LLM provider with this ID already exists." cause="llm provider already exists"
time=2026-07-08T12:56:54.185+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=2db564bc-b442-4d93-9362-97b69fbfd562 code=COMMON_VALIDATION_FAILED status=400 path=/api/v0.9/llm-providers method=POST detail="Invalid input" cause="invalid input parameters"
time=2026-07-08T12:56:54.209+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=20014517-b85c-4f0a-bf2d-542c2dafaf84 code=LLM_PROVIDER_NOT_FOUND status=404 path=/api/v0.9/llm-providers/does-not-exist method=GET detail="The specified LLM provider could not be found." cause="llm provider not found"
time=2026-07-08T12:56:54.231+05:30 level=WARN source=service/llm_apikey.go:191 msg="LLM provider not found" providerId=does-not-exist organizationId=019f3d39-aee1-7a93-bb96-cf71c37af683
time=2026-07-08T12:56:54.231+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=bb4dd3b3-9724-4318-8393-1a70be61515d code=ARTIFACT_NOT_FOUND status=404 path=/api/v0.9/llm-providers/does-not-exist/api-keys method=POST detail="The specified artifact could not be found." cause="api not found"
time=2026-07-08T12:56:54.253+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=0837699b-9756-4d1d-88c6-eed0ed0b7b94 code=COMMON_VALIDATION_FAILED status=400 path=/api/v0.9/llm-proxies method=GET detail="projectId query parameter is required"
time=2026-07-08T12:56:54.277+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=377edf37-9b5c-47bb-952f-4e8d8a029fdd code=LLM_PROXY_EXISTS status=409 path=/api/v0.9/llm-proxies method=POST detail="An LLM proxy with this ID already exists." cause="llm proxy already exists"
time=2026-07-08T12:56:54.297+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=f3eac25e-b6a3-4a64-b6c8-79229c45d840 code=LLM_PROVIDER_REF_NOT_FOUND status=400 path=/api/v0.9/llm-proxies method=POST detail="The referenced LLM provider could not be found." cause="llm provider not found"
time=2026-07-08T12:56:54.316+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=6dacc4fb-4546-46ae-a1f2-699996f361ae code=LLM_PROXY_NOT_FOUND status=404 path=/api/v0.9/llm-proxies/does-not-exist method=GET detail="The specified LLM proxy could not be found." cause="llm proxy not found"
time=2026-07-08T12:56:54.359+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=0adb00a4-819e-438a-97e8-031a06a4199d code=GATEWAY_NOT_FOUND status=404 path=/api/v0.9/gateways/00000000-0000-0000-0000-000000000000 method=GET detail="The specified gateway could not be found."
time=2026-07-08T12:56:54.398+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=89f7a030-7e92-4389-82b4-926f3b6351cc code=SUBSCRIPTION_PLAN_EXISTS status=409 path=/api/v0.9/subscription-plans method=POST detail="A subscription plan with this name already exists." cause="subscription plan with this name already exists for the organization"
time=2026-07-08T12:56:54.418+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=5c3496ae-37bb-48af-9134-0961fe62f88b code=SUBSCRIPTION_PLAN_EXISTS status=409 path=/api/v0.9/subscription-plans method=POST detail="A subscription plan with this name already exists." cause="subscription plan with this name already exists for the organization"
time=2026-07-08T12:56:54.439+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=be39ec6a-ca8f-4a40-ac6e-3916d9f5b274 code=COMMON_VALIDATION_FAILED status=400 path=/api/v0.9/subscription-plans method=POST detail="id is required"
time=2026-07-08T12:56:54.482+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=131c8ed8-fd8b-41b7-8be3-a7257da4cd56 code=SUBSCRIPTION_EXISTS status=409 path=/api/v0.9/subscriptions method=POST detail="A subscription for this application and API already exists." cause="application is already subscribed to this API"
time=2026-07-08T12:56:54.501+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=188ba012-d3f3-4d66-803a-bf2abb47297d code=SUBSCRIPTION_EXISTS status=409 path=/api/v0.9/subscriptions method=POST detail="A subscription for this application and API already exists." cause="application is already subscribed to this API"
time=2026-07-08T12:56:54.523+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=19918d10-e257-4716-9519-91908d4492cd code=SUBSCRIPTION_EXISTS status=409 path=/api/v0.9/subscriptions method=POST detail="A subscription for this application and API already exists." cause="application is already subscribed to this API"
time=2026-07-08T12:56:54.543+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=8af010bd-82cc-4b12-8207-cec4a6134bbf code=COMMON_VALIDATION_FAILED status=400 path=/api/v0.9/subscriptions method=POST detail="kind is required"
time=2026-07-08T12:56:54.566+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=b1f6419f-103d-48d0-b059-d993b9afb964 code=SUBSCRIPTION_NOT_FOUND status=404 path=/api/v0.9/subscriptions/00000000-0000-0000-0000-000000000000 method=GET detail="The specified subscription could not be found." cause="subscription not found"
time=2026-07-08T12:56:54.607+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=bb6ebb4a-f2a3-4954-8560-eaae827e8d26 code=SECRET_EXISTS status=409 path=/api/v0.9/secrets method=POST detail="A secret with this name already exists." cause="secret already exists for this organization and handle"
time=2026-07-08T12:56:54.633+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=d2bd37d6-171a-43cf-906d-23a1a8a6b191 code=SECRET_EXISTS status=409 path=/api/v0.9/secrets method=POST detail="A secret with this name already exists." cause="secret already exists for this organization and handle"
time=2026-07-08T12:56:54.688+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=d148ac57-fcd5-4817-9925-3d627be2afb0 code=SECRET_NOT_FOUND status=404 path=/api/v0.9/secrets/does-not-exist method=GET detail="The specified secret could not be found." cause="secret not found"
time=2026-07-08T12:56:54.734+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=0213f204-6976-4fca-b58f-8d15dce2077b code=ORGANIZATION_NOT_FOUND status=404 path=/api/v0.9/organizations/00000000-0000-0000-0000-000000000000 method=GET detail="The specified organization could not be found."
time=2026-07-08T12:56:54.758+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=3fbf0f30-3724-4fa6-a2d6-28e345ef0202 code=COMMON_VALIDATION_FAILED status=400 path=/api/v0.9/projects method=POST detail="Invalid input." cause="unexpected EOF"
time=2026-07-08T12:56:54.779+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=b767b571-0e99-41fd-a2aa-2c66f1bcf253 code=REST_API_NOT_FOUND status=404 path=/api/v0.9/rest-apis/../../etc/passwd method=GET detail="API ../../etc/passwd not found in org 019f3d39-aee1-7a93-bb96-cf71c37af683" cause="api not found"
time=2026-07-08T12:56:54.824+05:30 level=WARN source=middleware/error_mapper.go:114 msg="request failed" trackingId=e7471b85-3636-4344-9ea4-40a56bb6c967 code=PROJECT_NOT_FOUND status=404 path=/api/v0.9/applications method=GET detail="failed to list applications for project ' OR '1'='1 in org 019f3d39-aee1-7a93-bb96-cf71c37af683" cause="project not found"

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
platform-api/internal/handler/gateway.go (1)

76-78: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Cap request bodies in gateway create/update handlers.
json.NewDecoder(r.Body).Decode(&req) has no size guard here, and the server middleware chain doesn’t apply a body limit. Add http.MaxBytesReader here or a shared middleware so oversized requests are rejected before decode.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/handler/gateway.go` around lines 76 - 78, The gateway
request decoding in the create/update flow is unbounded, so oversized bodies can
still reach json.NewDecoder(r.Body).Decode(&req). Add a size cap before
decoding, either directly in the handler path around the gateway request decode
or via shared middleware applied to these routes, and make sure the existing
validation/error flow in the gateway handler returns a clear rejection when the
limit is exceeded.

Source: Coding guidelines

🧹 Nitpick comments (1)
platform-api/internal/handler/gateway.go (1)

115-134: 🎯 Functional Correctness | 🔵 Trivial | 🏗️ Heavy lift

Brittle substring-based error classification for RegisterGateway.

Errors from RegisterGateway are classified by matching substrings in err.Error() ("organization not found", "already exists", "required"/"invalid"/"must"/"cannot"). The same file's UpdateGateway (line 250) instead uses errors.Is(err, constants.ErrGatewayNotFound) against a sentinel error — a more robust pattern already established here. RotateToken (lines 344-358) has the same substring-matching issue, including a fragile capitalization-sensitive check for "Revoke".

If the underlying service's error wording changes, these mappings silently fall through to apperror.Internal (500), misrepresenting client errors as server errors.

Prefer exposing typed/sentinel errors from the service layer (as already done for ErrGatewayNotFound) and using errors.Is/errors.As here instead of string matching.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/handler/gateway.go` around lines 115 - 134, The
RegisterGateway error handling is using fragile substring matching on
err.Error(), which can misclassify client errors as Internal when service
wording changes. Update the gateway handler to use typed or sentinel errors from
the service layer, and switch the checks in RegisterGateway and the similar
RotateToken logic to errors.Is/errors.As, following the existing UpdateGateway
pattern with constants.ErrGatewayNotFound. Keep the final fallback to
apperror.Internal only for truly unexpected errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@platform-api/internal/handler/gateway.go`:
- Around line 76-78: The gateway request decoding in the create/update flow is
unbounded, so oversized bodies can still reach
json.NewDecoder(r.Body).Decode(&req). Add a size cap before decoding, either
directly in the handler path around the gateway request decode or via shared
middleware applied to these routes, and make sure the existing validation/error
flow in the gateway handler returns a clear rejection when the limit is
exceeded.

---

Nitpick comments:
In `@platform-api/internal/handler/gateway.go`:
- Around line 115-134: The RegisterGateway error handling is using fragile
substring matching on err.Error(), which can misclassify client errors as
Internal when service wording changes. Update the gateway handler to use typed
or sentinel errors from the service layer, and switch the checks in
RegisterGateway and the similar RotateToken logic to errors.Is/errors.As,
following the existing UpdateGateway pattern with constants.ErrGatewayNotFound.
Keep the final fallback to apperror.Internal only for truly unexpected errors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 19231e6b-8bb6-4d91-871e-d3a45ac0d169

📥 Commits

Reviewing files that changed from the base of the PR and between 2d66374 and e887a34.

📒 Files selected for processing (20)
  • platform-api/internal/handler/api.go
  • platform-api/internal/handler/api_deployment.go
  • platform-api/internal/handler/api_key.go
  • platform-api/internal/handler/application.go
  • platform-api/internal/handler/artifact_guard_response.go
  • platform-api/internal/handler/gateway.go
  • platform-api/internal/handler/llm.go
  • platform-api/internal/handler/llm_apikey.go
  • platform-api/internal/handler/llm_deployment.go
  • platform-api/internal/handler/llm_proxy_apikey.go
  • platform-api/internal/handler/location.go
  • platform-api/internal/handler/location_test.go
  • platform-api/internal/handler/mcp.go
  • platform-api/internal/handler/mcp_deployment.go
  • platform-api/internal/handler/organization.go
  • platform-api/internal/handler/project.go
  • platform-api/internal/handler/secret.go
  • platform-api/internal/handler/subscription_handler.go
  • platform-api/internal/handler/subscription_plan_handler.go
  • platform-api/resources/openapi.yaml
🚧 Files skipped from review as they are similar to previous changes (17)
  • platform-api/internal/handler/artifact_guard_response.go
  • platform-api/internal/handler/llm_proxy_apikey.go
  • platform-api/internal/handler/api_key.go
  • platform-api/internal/handler/llm_apikey.go
  • platform-api/internal/handler/mcp_deployment.go
  • platform-api/internal/handler/application.go
  • platform-api/internal/handler/project.go
  • platform-api/internal/handler/api_deployment.go
  • platform-api/internal/handler/llm.go
  • platform-api/internal/handler/subscription_plan_handler.go
  • platform-api/internal/handler/api.go
  • platform-api/internal/handler/secret.go
  • platform-api/internal/handler/organization.go
  • platform-api/internal/handler/llm_deployment.go
  • platform-api/internal/handler/mcp.go
  • platform-api/internal/handler/subscription_handler.go
  • platform-api/resources/openapi.yaml

…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.
@Thushani-Jayasekera

Copy link
Copy Markdown
Contributor Author

Platform API — Valid & Invalid Request Log

Run at: Wed Jul 8 12:56:52 +0530 2026

Authentication

Login — missing username/password (invalid)

POST https://localhost:9243/api/portal/v0.9/auth/login
Response:
{"status":"error","code":"COMMON_VALIDATION_FAILED","message":"username and password are required"}

HTTP_STATUS:400

Login — wrong password (invalid)

POST https://localhost:9243/api/portal/v0.9/auth/login
Response:
{"status":"error","code":"COMMON_UNAUTHORIZED","message":"Invalid or expired credentials."}

HTTP_STATUS:401

Login — unknown username (invalid, enumeration check)

POST https://localhost:9243/api/portal/v0.9/auth/login
Response:
{"status":"error","code":"COMMON_UNAUTHORIZED","message":"Invalid or expired credentials."}

HTTP_STATUS:401

Login — valid credentials (valid)

POST https://localhost:9243/api/portal/v0.9/auth/login
Response:
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3ODM1MjQ0MTMsImlhdCI6MTc4MzQ5NTYxMywiaXNzIjoicGxhdGZvcm0tYXBpIiwib3JnX2hhbmRsZSI6ImRlZmF1bHQiLCJvcmdfbmFtZSI6IkRlZmF1bHQiLCJvcmdhbml6YXRpb24iOiIwMTlmM2QzOS1hZWUxLTdhOTMtYmI5Ni1jZjcxYzM3YWY2ODMiLCJzY29wZSI6ImFwOm9yZ2FuaXphdGlvbjptYW5hZ2UgYXA6Z2F0ZXdheTptYW5hZ2UgYXA6Z2F0ZXdheV9jdXN0b21fcG9saWN5Om1hbmFnZSBhcDpyZXN0X2FwaTptYW5hZ2UgYXA6bGxtX3Byb3ZpZGVyOm1hbmFnZSBhcDpsbG1fcHJveHk6bWFuYWdlIGFwOm1jcF9wcm94eTptYW5hZ2UgYXA6d2ViYnJva2VyX2FwaTptYW5hZ2UgYXA6d2Vic3ViX2FwaTptYW5hZ2UgYXA6YXBwbGljYXRpb246bWFuYWdlIGFwOnN1YnNjcmlwdGlvbjptYW5hZ2UgYXA6c3Vic2NyaXB0aW9uX3BsYW46bWFuYWdlIGFwOnByb2plY3Q6bWFuYWdlIGFwOmxsbV90ZW1wbGF0ZTptYW5hZ2UgYXA6ZGV2cG9ydGFsOm1hbmFnZSBhcDphcGlfa2V5OnJlYWQgYXA6c2VjcmV0Om1hbmFnZSIsInN1YiI6ImFkbWluIiwidXNlcm5hbWUiOiJhZG1pbiJ9.7tY1xvFmXJvLaNMOjq7MrUhXM2HoPwexJL1zRvF3ZwU","expires_at":1783524413}

HTTP_STATUS:200

Access protected endpoint with garbage token (invalid)

GET https://localhost:9243/api/v0.9/rest-apis
Response:
{"status":"error","code":"COMMON_UNAUTHORIZED","message":"Invalid or expired credentials."}

HTTP_STATUS:401

Access protected endpoint with no auth header (invalid)

GET https://localhost:9243/api/v0.9/rest-apis
Response:
{"status":"error","code":"COMMON_UNAUTHORIZED","message":"Invalid or expired credentials."}

HTTP_STATUS:401

Projects

List projects (valid)

GET https://localhost:9243/api/v0.9/projects
Response:
{"count":1,"list":[{"createdAt":"2026-07-07T21:07:19.29437+05:30","createdBy":"admin","description":"A test project","displayName":"Test Project","id":"test-project","organizationId":"default","updatedAt":"2026-07-07T21:07:19.29437+05:30"}],"pagination":{"limit":1,"offset":0,"total":1}}

HTTP_STATUS:200

Create project - valid

POST https://localhost:9243/api/v0.9/projects
Response:
{"status":"error","code":"PROJECT_EXISTS","message":"A project with this name already exists in the organization."}

HTTP_STATUS:409

Create project - missing required 'displayName' field (invalid)

POST https://localhost:9243/api/v0.9/projects
Response:
{"status":"error","code":"COMMON_VALIDATION_FAILED","message":"Project displayName is required"}

HTTP_STATUS:400

Create project - duplicate name (invalid, conflict)

POST https://localhost:9243/api/v0.9/projects
Response:
{"status":"error","code":"PROJECT_EXISTS","message":"A project with this name already exists in the organization."}

HTTP_STATUS:409

Get project by valid ID (valid)

GET https://localhost:9243/api/v0.9/projects/test-project
Response:
{"createdAt":"2026-07-07T21:07:19.29437+05:30","createdBy":"admin","description":"A test project","displayName":"Test Project","id":"test-project","organizationId":"default","updatedAt":"2026-07-07T21:07:19.29437+05:30","updatedBy":"admin"}

HTTP_STATUS:200

Get project by non-existent ID (invalid, not found)

GET https://localhost:9243/api/v0.9/projects/does-not-exist
Response:
{"status":"error","code":"PROJECT_NOT_FOUND","message":"The specified project could not be found."}

HTTP_STATUS:404

Get project by malformed ID with special chars (invalid, bad request)

GET https://localhost:9243/api/v0.9/projects/not_a_valid_handle!!
Response:
{"status":"error","code":"PROJECT_NOT_FOUND","message":"The specified project could not be found."}

HTTP_STATUS:404

Applications

List applications - missing required projectId query param (invalid)

GET https://localhost:9243/api/v0.9/applications
Response:
{"status":"error","code":"COMMON_VALIDATION_FAILED","message":"Project ID is required"}

HTTP_STATUS:400

List applications (valid)

GET https://localhost:9243/api/v0.9/applications?projectId=test-project
Response:
{"count":1,"list":[{"createdAt":"2026-07-07T21:07:19.460089+05:30","createdBy":"admin","description":"A test application","displayName":"Test App","id":"test-app","projectId":"test-project","type":"genai","updatedAt":"2026-07-07T21:07:19.460089+05:30"}],"pagination":{"limit":20,"offset":0,"total":1}}

HTTP_STATUS:200

Create application - valid

POST https://localhost:9243/api/v0.9/applications
Response:
{"status":"error","code":"APPLICATION_EXISTS","message":"An application with this name already exists."}

HTTP_STATUS:409

Create application - duplicate name (invalid, conflict)

POST https://localhost:9243/api/v0.9/applications
Response:
{"status":"error","code":"APPLICATION_EXISTS","message":"An application with this name already exists."}

HTTP_STATUS:409

Create application - empty body (invalid, validation)

POST https://localhost:9243/api/v0.9/applications
Response:
{"status":"error","code":"COMMON_VALIDATION_FAILED","message":"displayName is required"}

HTTP_STATUS:400

Create application - invalid project reference (invalid, bad request)

POST https://localhost:9243/api/v0.9/applications
Response:
{"status":"error","code":"PROJECT_NOT_FOUND","message":"The specified project could not be found."}

HTTP_STATUS:404

Get application by non-existent ID (invalid, not found)

GET https://localhost:9243/api/v0.9/applications/does-not-exist
Response:
{"status":"error","code":"APPLICATION_NOT_FOUND","message":"The specified application could not be found."}

HTTP_STATUS:404

Create application API key - valid

POST https://localhost:9243/api/v0.9/applications/test-app/api-keys
Response:
{"status":"error","code":"COMMON_VALIDATION_FAILED","message":"At least one API key mapping is required"}

HTTP_STATUS:400

Create application API key for non-existent application (invalid, not found)

POST https://localhost:9243/api/v0.9/applications/does-not-exist/api-keys
Response:
{"status":"error","code":"COMMON_VALIDATION_FAILED","message":"At least one API key mapping is required"}

HTTP_STATUS:400

REST APIs

List REST APIs (valid)

GET https://localhost:9243/api/v0.9/rest-apis
Response:
{"status":"error","code":"COMMON_VALIDATION_FAILED","message":"projectId query parameter is required"}

HTTP_STATUS:400

Create REST API - valid

POST https://localhost:9243/api/v0.9/rest-apis
Response:
{"status":"error","code":"REST_API_EXISTS","message":"An API with this handle already exists."}

HTTP_STATUS:409

Create REST API - missing required fields (invalid, validation)

POST https://localhost:9243/api/v0.9/rest-apis
Response:
{"status":"error","code":"COMMON_VALIDATION_FAILED","message":"API context is required"}

HTTP_STATUS:400

Create REST API - invalid project reference (invalid, bad request)

POST https://localhost:9243/api/v0.9/rest-apis
Response:
{"status":"error","code":"PROJECT_NOT_FOUND","message":"The specified project could not be found."}

HTTP_STATUS:404

Create REST API - duplicate handle+version (invalid, conflict)

POST https://localhost:9243/api/v0.9/rest-apis
Response:
{"status":"error","code":"REST_API_EXISTS","message":"An API with this handle already exists."}

HTTP_STATUS:409

Get REST API by non-existent ID (invalid, not found)

GET https://localhost:9243/api/v0.9/rest-apis/does-not-exist
Response:
{"status":"error","code":"REST_API_NOT_FOUND","message":"The specified REST API could not be found."}

HTTP_STATUS:404

Delete non-existent REST API (invalid, not found)

DELETE https://localhost:9243/api/v0.9/rest-apis/does-not-exist
Response:
{"status":"error","code":"REST_API_NOT_FOUND","message":"The specified REST API could not be found."}

HTTP_STATUS:404

LLM Provider Templates

List LLM provider templates (valid)

GET https://localhost:9243/api/v0.9/llm-provider-templates
Response:
{"count":7,"list":[{"createdAt":"2026-07-07T21:06:58.344819+05:30","displayName":"AWS Bedrock","enabled":true,"groupId":"awsbedrock","id":"awsbedrock","isLatest":true,"logoUrl":"https://raw.githubusercontent.com/nomadxd/openapi-connectors/main/openapi/aws.bedrock/icon.png","managedBy":"wso2","readOnly":false,"updatedAt":"2026-07-07T21:06:58.344819+05:30","version":"v1.0"},{"createdAt":"2026-07-07T21:06:58.343772+05:30","displayName":"Anthropic","enabled":true,"groupId":"anthropic","id":"anthropic","isLatest":true,"logoUrl":"https://raw.githubusercontent.com/nomadxd/openapi-connectors/main/openapi/anthropic.claude/icon.png","managedBy":"wso2","readOnly":false,"updatedAt":"2026-07-07T21:06:58.343772+05:30","version":"v1.0"},{"createdAt":"2026-07-07T21:06:58.345417+05:30","displayName":"Azure AI Foundry","enabled":true,"groupId":"azureai-foundry","id":"azureai-foundry","isLatest":true,"logoUrl":"https://raw.githubusercontent.com/nomadxd/openapi-connectors/main/openapi/azure.openai/icon.png","managedBy":"wso2","readOnly":false,"updatedAt":"2026-07-07T21:06:58.345417+05:30","version":"v1.0"},{"createdAt":"2026-07-07T21:06:58.345924+05:30","displayName":"Azure OpenAI","enabled":true,"groupId":"azure-openai","id":"azure-openai","isLatest":true,"logoUrl":"https://raw.githubusercontent.com/nomadxd/openapi-connectors/refs/heads/main/openapi/azure.openai.v2/icon.png","managedBy":"wso2","readOnly":false,"updatedAt":"2026-07-07T21:06:58.345924+05:30","version":"v1.0"},{"createdAt":"2026-07-07T21:06:58.346424+05:30","displayName":"Gemini","enabled":true,"groupId":"gemini","id":"gemini","isLatest":true,"managedBy":"wso2","readOnly":false,"updatedAt":"2026-07-07T21:06:58.346424+05:30","version":"v1.0"},{"createdAt":"2026-07-07T21:06:58.347056+05:30","displayName":"Mistral","enabled":true,"groupId":"mistralai","id":"mistralai","isLatest":true,"logoUrl":"https://raw.githubusercontent.com/nomadxd/openapi-connectors/main/openapi/mistral/icon.png","managedBy":"wso2","readOnly":false,"updatedAt":"2026-07-07T21:06:58.347056+05:30","version":"v1.0"},{"createdAt":"2026-07-07T21:06:58.347675+05:30","displayName":"OpenAI","enabled":true,"groupId":"openai","id":"openai","isLatest":true,"logoUrl":"https://raw.githubusercontent.com/nomadxd/openapi-connectors/main/openapi/openai/icon.png","managedBy":"wso2","readOnly":false,"updatedAt":"2026-07-07T21:06:58.347675+05:30","version":"v1.0"}],"pagination":{"limit":20,"offset":0,"total":7}}

HTTP_STATUS:200

Get LLM provider template by non-existent ID (invalid, not found)

GET https://localhost:9243/api/v0.9/llm-provider-templates/does-not-exist
Response:
{"status":"error","code":"LLM_PROVIDER_TEMPLATE_NOT_FOUND","message":"The specified LLM provider template could not be found."}

HTTP_STATUS:404

LLM Providers

List LLM providers (valid)

GET https://localhost:9243/api/v0.9/llm-providers
Response:
{"count":1,"list":[{"createdAt":"2026-07-07T21:07:20.201831+05:30","createdBy":"admin","displayName":"Test OpenAI Provider","id":"test-openai-provider","readOnly":false,"template":"awsbedrock","updatedAt":"2026-07-07T21:07:20.201831+05:30","version":"v1.0"}],"pagination":{"limit":20,"offset":0,"total":1}}

HTTP_STATUS:200

Create LLM provider - valid

POST https://localhost:9243/api/v0.9/llm-providers
Response:
{"status":"error","code":"LLM_PROVIDER_EXISTS","message":"An LLM provider with this ID already exists."}

HTTP_STATUS:409

Create LLM provider - invalid template reference (invalid, bad request)

POST https://localhost:9243/api/v0.9/llm-providers
Response:
{"status":"error","code":"LLM_PROVIDER_TEMPLATE_REF_NOT_FOUND","message":"The referenced LLM provider template could not be found."}

HTTP_STATUS:400

Create LLM provider - duplicate handle (invalid, conflict)

POST https://localhost:9243/api/v0.9/llm-providers
Response:
{"status":"error","code":"LLM_PROVIDER_EXISTS","message":"An LLM provider with this ID already exists."}

HTTP_STATUS:409

Create LLM provider - missing required fields (invalid, validation)

POST https://localhost:9243/api/v0.9/llm-providers
Response:
{"status":"error","code":"COMMON_VALIDATION_FAILED","message":"Invalid input"}

HTTP_STATUS:400

Get LLM provider by non-existent ID (invalid, not found)

GET https://localhost:9243/api/v0.9/llm-providers/does-not-exist
Response:
{"status":"error","code":"LLM_PROVIDER_NOT_FOUND","message":"The specified LLM provider could not be found."}

HTTP_STATUS:404

Create LLM provider API key for non-existent provider (invalid, not found)

POST https://localhost:9243/api/v0.9/llm-providers/does-not-exist/api-keys
Response:
{"status":"error","code":"ARTIFACT_NOT_FOUND","message":"The specified artifact could not be found."}

HTTP_STATUS:404

LLM Proxies

List LLM proxies (valid)

GET https://localhost:9243/api/v0.9/llm-proxies
Response:
{"status":"error","code":"COMMON_VALIDATION_FAILED","message":"projectId query parameter is required"}

HTTP_STATUS:400

Create LLM proxy - valid

POST https://localhost:9243/api/v0.9/llm-proxies
Response:
{"status":"error","code":"LLM_PROXY_EXISTS","message":"An LLM proxy with this ID already exists."}

HTTP_STATUS:409

Create LLM proxy - invalid provider reference (invalid, bad request)

POST https://localhost:9243/api/v0.9/llm-proxies
Response:
{"status":"error","code":"LLM_PROVIDER_REF_NOT_FOUND","message":"The referenced LLM provider could not be found."}

HTTP_STATUS:400

Get LLM proxy by non-existent ID (invalid, not found)

GET https://localhost:9243/api/v0.9/llm-proxies/does-not-exist
Response:
{"status":"error","code":"LLM_PROXY_NOT_FOUND","message":"The specified LLM proxy could not be found."}

HTTP_STATUS:404

Gateways

List gateways (valid)

GET https://localhost:9243/api/v0.9/gateways
Response:
{"count":0,"list":[],"pagination":{"limit":0,"offset":0,"total":0}}

HTTP_STATUS:200

Get gateway by non-existent ID (invalid, not found)

GET https://localhost:9243/api/v0.9/gateways/00000000-0000-0000-0000-000000000000
Response:
{"status":"error","code":"GATEWAY_NOT_FOUND","message":"The specified gateway could not be found."}

HTTP_STATUS:404

Subscription Plans

List subscription plans (valid)

GET https://localhost:9243/api/v0.9/subscription-plans
Response:
{"count":1,"subscriptionPlans":[{"createdAt":"2026-07-07T21:07:20.551212+05:30","createdBy":"admin","displayName":"Gold Plan","id":"gold","limits":[],"organizationId":"default","status":"ACTIVE","updatedAt":"2026-07-07T21:07:20.551212+05:30"}]}

HTTP_STATUS:200

Create subscription plan - valid

POST https://localhost:9243/api/v0.9/subscription-plans
Response:
{"status":"error","code":"SUBSCRIPTION_PLAN_EXISTS","message":"A subscription plan with this name already exists."}

HTTP_STATUS:409

Create subscription plan - duplicate name (invalid, conflict)

POST https://localhost:9243/api/v0.9/subscription-plans
Response:
{"status":"error","code":"SUBSCRIPTION_PLAN_EXISTS","message":"A subscription plan with this name already exists."}

HTTP_STATUS:409

Create subscription plan - missing required fields (invalid, validation)

POST https://localhost:9243/api/v0.9/subscription-plans
Response:
{"status":"error","code":"COMMON_VALIDATION_FAILED","message":"id is required"}

HTTP_STATUS:400

Subscriptions

List subscriptions (valid)

GET https://localhost:9243/api/v0.9/subscriptions
Response:
{"count":2,"pagination":{"limit":20,"offset":0,"total":2},"subscriptions":[{"apiId":"test-rest-api","applicationId":"does-not-exist","createdAt":"2026-07-07T21:07:20.669167+05:30","createdBy":"admin","id":"ce1efd53-bbd4-419b-8c3c-c3d86beae49a","kind":"RestApi","organizationId":"default","status":"ACTIVE","subscriberId":"does-not-exist","subscriptionPlanId":"0ee2eb47-2612-454d-bf90-3403500728ec","subscriptionPlanName":"Gold Plan","updatedAt":"2026-07-07T21:07:20.669167+05:30"},{"apiId":"test-rest-api","applicationId":"test-app","createdAt":"2026-07-07T21:07:20.647059+05:30","createdBy":"admin","id":"7f467841-ffe7-478a-b3e8-69beac920eb7","kind":"RestApi","organizationId":"default","status":"ACTIVE","subscriberId":"test-app","subscriptionPlanId":"0ee2eb47-2612-454d-bf90-3403500728ec","subscriptionPlanName":"Gold Plan","updatedAt":"2026-07-07T21:07:20.647059+05:30"}]}

HTTP_STATUS:200

Create subscription - valid

POST https://localhost:9243/api/v0.9/subscriptions
Response:
{"status":"error","code":"SUBSCRIPTION_EXISTS","message":"A subscription for this application and API already exists."}

HTTP_STATUS:409

Create subscription - non-existent application (invalid, not found/bad request)

POST https://localhost:9243/api/v0.9/subscriptions
Response:
{"status":"error","code":"SUBSCRIPTION_EXISTS","message":"A subscription for this application and API already exists."}

HTTP_STATUS:409

Create subscription - duplicate (same app+api) (invalid, conflict)

POST https://localhost:9243/api/v0.9/subscriptions
Response:
{"status":"error","code":"SUBSCRIPTION_EXISTS","message":"A subscription for this application and API already exists."}

HTTP_STATUS:409

Create subscription - missing required 'kind' field (invalid, validation)

POST https://localhost:9243/api/v0.9/subscriptions
Response:
{"status":"error","code":"COMMON_VALIDATION_FAILED","message":"kind is required"}

HTTP_STATUS:400

Get subscription by non-existent ID (invalid, not found)

GET https://localhost:9243/api/v0.9/subscriptions/00000000-0000-0000-0000-000000000000
Response:
{"status":"error","code":"SUBSCRIPTION_NOT_FOUND","message":"The specified subscription could not be found."}

HTTP_STATUS:404

Secrets

List secrets (valid)

GET https://localhost:9243/api/v0.9/secrets
Response:
{"list":[{"id":"test-secret","displayName":"Test Secret","type":"GENERIC","provider":"IN_BUILT","status":"ACTIVE","hash":"hmac-sha256:add7afaf8c45fe874ff4300b0fa1e32cc125de647a28c2c959374b49808b53b3","createdBy":"admin","createdAt":"2026-07-07T21:07:20.77713+05:30","updatedAt":"2026-07-07T21:07:20.77713+05:30"}],"pagination":{"total":1,"offset":0,"limit":25}}

HTTP_STATUS:200

Create secret - valid (multipart/form-data)

POST https://localhost:9243/api/v0.9/secrets
Response:
{"status":"error","code":"SECRET_EXISTS","message":"A secret with this name already exists."}

HTTP_STATUS:409

Create secret - duplicate name (invalid, conflict, multipart/form-data)

POST https://localhost:9243/api/v0.9/secrets
Response:
{"status":"error","code":"SECRET_EXISTS","message":"A secret with this name already exists."}

HTTP_STATUS:409

Get secret by non-existent ID (invalid, not found)

GET https://localhost:9243/api/v0.9/secrets/does-not-exist
Response:
{"status":"error","code":"SECRET_NOT_FOUND","message":"The specified secret could not be found."}

HTTP_STATUS:404

Organizations

List organizations (valid)

GET https://localhost:9243/api/v0.9/organizations
Response:
{"count":1,"list":[{"createdAt":"2026-07-07T21:06:58.337695+05:30","displayName":"Default","id":"default","region":"us","updatedAt":"2026-07-07T21:06:58.337695+05:30"}],"pagination":{"limit":20,"offset":0,"total":1}}

HTTP_STATUS:200

Get organization by non-existent ID (invalid, not found)

GET https://localhost:9243/api/v0.9/organizations/00000000-0000-0000-0000-000000000000
Response:
{"status":"error","code":"ORGANIZATION_NOT_FOUND","message":"The specified organization could not be found."}

HTTP_STATUS:404

Malformed and edge-case requests

Malformed JSON body (invalid, bad request)

POST https://localhost:9243/api/v0.9/projects
Response:
{"status":"error","code":"COMMON_VALIDATION_FAILED","message":"Invalid input."}

HTTP_STATUS:400

Path traversal attempt in ID param (invalid)

GET https://localhost:9243/api/v0.9/rest-apis/..%2f..%2fetc%2fpasswd
Response:
{"status":"error","code":"REST_API_NOT_FOUND","message":"The specified REST API could not be found."}

HTTP_STATUS:404

SQL-injection-like payload in query (invalid/should be handled safely)

GET https://localhost:9243/api/v0.9/applications?projectId=%27%20OR%20%271%27%3D%271
Response:
{"status":"error","code":"PROJECT_NOT_FOUND","message":"The specified project could not be found."}

HTTP_STATUS:404

Wrong HTTP method on collection endpoint (invalid)

PATCH https://localhost:9243/api/v0.9/rest-apis
Response:
Method Not Allowed

HTTP_STATUS:405

Unknown route (invalid, 404)

GET https://localhost:9243/api/v0.9/does-not-exist
Response:
404 page not found

HTTP_STATUS:404

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
platform-api/internal/service/llm.go (1)

798-946: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Wrap the organization lookup error in LLMProviderService.Create
LLMHandler.CreateLLMProvider already maps the other ErrLLMProvider* and ErrInvalidInput cases; ErrOrganizationNotFound is the only raw sentinel left here, so a missing org still falls through to a generic 500 instead of 404.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/service/llm.go` around lines 798 - 946, In
LLMProviderService.Create, the organization lookup path still returns
ErrOrganizationNotFound as a raw sentinel, unlike the other provider validation
errors. Update the orgRepo.GetOrganizationByUUID branch so the
missing-organization case is wrapped with the same provider-specific error
handling used elsewhere in this service, allowing LLMHandler.CreateLLMProvider
to map it to the correct 404 response. Keep the change localized to the org
validation block and preserve the existing wrapped error for lookup failures.
platform-api/internal/handler/organization.go (1)

137-152: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

GetOrganizationByID is missing the same tenant-match guard as HeadOrganization.
It reads organizationId from the path and returns the full organization record without checking the token claim, so an authenticated caller can fetch another tenant’s organization details. Mirror the claim check before GetOrganizationByHandle.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/handler/organization.go` around lines 137 - 152,
GetOrganizationByID is missing the tenant-match check that HeadOrganization
already performs, so it can return another tenant’s organization data. Update
OrganizationHandler.GetOrganizationByID to read the org claim from the request
context and compare it against the organizationId path value before calling
orgService.GetOrganizationByHandle; if they don’t match, return the appropriate
forbidden/unauthorized app error. Keep the existing GetOrganizationByHandle flow
unchanged after the guard.

Source: Coding guidelines

🧹 Nitpick comments (3)
platform-api/internal/service/project.go (1)

246-288: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider 409 Conflict instead of 400 for delete-blocked-by-dependent-resource checks.

DeleteProject's refusal cases (org must keep ≥1 project, project has associated APIs/MCP proxies/applications) are well-formed requests blocked by current resource state, not malformed/invalid input — the canonical case for 409 Conflict rather than 400 Bad Request (e.g., S3 returns 409 when deleting a non-empty bucket). Since this PR already introduces a breaking change to the error response shape, it's a good opportunity to also correct these status codes so clients can distinguish "fix your request" (400) from "resolve resource state first" (409).

♻️ Suggested direction
 	if len(projects) <= 1 {
-		return apperror.ValidationFailed.New("Organization must have at least one project")
+		return apperror.ProjectDeletionBlocked.New("Organization must have at least one project")
 	}
 	...
 	if len(apis) > 0 {
-		return apperror.ValidationFailed.New("Project has associated APIs")
+		return apperror.ProjectDeletionBlocked.New("Project has associated APIs")
 	}

(requires adding a ProjectDeletionBlocked catalog entry with http.StatusConflict, or reusing an existing 409-mapped def if one exists in the catalog.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/service/project.go` around lines 246 - 288, In
DeleteProject, the dependency-based refusal paths are returning a
validation-style 400 when the request is blocked by current resource state;
update these checks to use a 409 Conflict response instead. Add or reuse a
catalog error for project deletion being blocked (for the organization minimum
project check, and the API/MCP proxy/application association checks) so
ProjectService.DeleteProject returns a conflict-mapped error rather than
apperror.ValidationFailed.New.
platform-api/internal/service/llm_apikey.go (1)

76-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Inconsistent/duplicate logging across sibling "not found" paths.

CreateLLMProviderAPIKey manually logs s.slogger.Warn(...) immediately before returning the typed apperror (Lines 191-193, 233-236), which duplicates the WARN-level log the error-mapper middleware already emits for 4xx apperror responses. Meanwhile ListLLMProviderAPIKeys (Line 76) and DeleteLLMProviderAPIKey (Lines 127, 136-141) return the same typed errors with no manual log at all, so the same failure condition is logged differently depending on which endpoint hit it.

Consider consistently attaching identifier context via .WithLogMessage(...) on the returned apperror and dropping the standalone slogger.Warn calls, so every endpoint gets exactly one structured log line with consistent context.

♻️ Example consolidation
 	if provider == nil {
-		s.slogger.Warn("LLM provider not found", "providerId", providerID, "organizationId", orgID)
-		return nil, apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound)
+		return nil, apperror.ArtifactNotFound.Wrap(constants.ErrAPINotFound).
+			WithLogMessage(fmt.Sprintf("LLM provider %s not found in org %s", providerID, orgID))
 	}

Also applies to: 127-127, 136-141, 191-193, 233-236

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/service/llm_apikey.go` at line 76, The not-found paths
in LLM provider API key handlers are logged inconsistently and sometimes twice,
especially in CreateLLMProviderAPIKey versus ListLLMProviderAPIKeys and
DeleteLLMProviderAPIKey. Update the returned apperror values in the relevant
methods to attach identifier context with .WithLogMessage(...) and remove the
standalone s.slogger.Warn(...) calls in CreateLLMProviderAPIKey so the
middleware emits one structured log line per failure. Keep the handling aligned
across ListLLMProviderAPIKeys, DeleteLLMProviderAPIKey, and
CreateLLMProviderAPIKey by using the same typed not-found error and log-message
pattern.
platform-api/internal/service/llm_deployment.go (1)

606-632: 🎯 Functional Correctness | 🔵 Trivial | ⚖️ Poor tradeoff

Migration stops at the nil/empty checks; sibling validation errors 2 lines away stay raw.

generateLLMProviderDeploymentYAML now returns apperror.Internal.New()... for a nil provider (622) and empty templateHandle (625), but the very next checks — provider.Configuration.Upstream == nil (628) and empty main.URL/main.Ref (632) — still return the raw constants.ErrInvalidInput. Same pattern in generateLLMProxyDeploymentYAML: nil proxy (1772) is wrapped, but proxy.Configuration.Provider == "" (1775) is not. Per the handler fallback pattern used elsewhere in this PR, these unwrapped returns will surface as generic 500s instead of proper 400s once they propagate to an HTTP handler.

If these two specific checks were deliberately scoped as "internal invariant violations" (justifying apperror.Internal) while everything else is deferred to a follow-up, that's reasonable — but worth confirming the remaining ErrInvalidInput/fmt.Errorf returns here are intentionally out of scope for this PR rather than an oversight.

Also applies to: 1770-1776

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/service/llm_deployment.go` around lines 606 - 632, The
validation paths in generateLLMProviderDeploymentYAML and
generateLLMProxyDeploymentYAML are inconsistent: the nil/empty guard cases use
apperror.Internal, but the adjacent input checks still return raw
constants.ErrInvalidInput, which will bypass the new handler fallback behavior.
Update the remaining sibling validations in generateLLMProviderDeploymentYAML
(provider.Configuration.Upstream and main URL/ref) and
generateLLMProxyDeploymentYAML (proxy.Configuration.Provider) to return the same
wrapped error style used by the other early-return checks so the service exposes
proper client-side validation instead of generic failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@platform-api/internal/service/gateway.go`:
- Around line 330-336: The gateway lookup in gateway.GetGatewayByUUID is
collapsing repository failures and missing records into the same GatewayNotFound
path. Split the `err != nil` and `gateway == nil` cases in
`s.gatewayRepo.GetByUUID` handling: return an `apperror.Internal`-style error
for real DB/repository failures, and only use `apperror.GatewayNotFound` when
the gateway is actually nil, matching the error handling pattern used by
`GetGateway`.

---

Outside diff comments:
In `@platform-api/internal/handler/organization.go`:
- Around line 137-152: GetOrganizationByID is missing the tenant-match check
that HeadOrganization already performs, so it can return another tenant’s
organization data. Update OrganizationHandler.GetOrganizationByID to read the
org claim from the request context and compare it against the organizationId
path value before calling orgService.GetOrganizationByHandle; if they don’t
match, return the appropriate forbidden/unauthorized app error. Keep the
existing GetOrganizationByHandle flow unchanged after the guard.

In `@platform-api/internal/service/llm.go`:
- Around line 798-946: In LLMProviderService.Create, the organization lookup
path still returns ErrOrganizationNotFound as a raw sentinel, unlike the other
provider validation errors. Update the orgRepo.GetOrganizationByUUID branch so
the missing-organization case is wrapped with the same provider-specific error
handling used elsewhere in this service, allowing LLMHandler.CreateLLMProvider
to map it to the correct 404 response. Keep the change localized to the org
validation block and preserve the existing wrapped error for lookup failures.

---

Nitpick comments:
In `@platform-api/internal/service/llm_apikey.go`:
- Line 76: The not-found paths in LLM provider API key handlers are logged
inconsistently and sometimes twice, especially in CreateLLMProviderAPIKey versus
ListLLMProviderAPIKeys and DeleteLLMProviderAPIKey. Update the returned apperror
values in the relevant methods to attach identifier context with
.WithLogMessage(...) and remove the standalone s.slogger.Warn(...) calls in
CreateLLMProviderAPIKey so the middleware emits one structured log line per
failure. Keep the handling aligned across ListLLMProviderAPIKeys,
DeleteLLMProviderAPIKey, and CreateLLMProviderAPIKey by using the same typed
not-found error and log-message pattern.

In `@platform-api/internal/service/llm_deployment.go`:
- Around line 606-632: The validation paths in generateLLMProviderDeploymentYAML
and generateLLMProxyDeploymentYAML are inconsistent: the nil/empty guard cases
use apperror.Internal, but the adjacent input checks still return raw
constants.ErrInvalidInput, which will bypass the new handler fallback behavior.
Update the remaining sibling validations in generateLLMProviderDeploymentYAML
(provider.Configuration.Upstream and main URL/ref) and
generateLLMProxyDeploymentYAML (proxy.Configuration.Provider) to return the same
wrapped error style used by the other early-return checks so the service exposes
proper client-side validation instead of generic failures.

In `@platform-api/internal/service/project.go`:
- Around line 246-288: In DeleteProject, the dependency-based refusal paths are
returning a validation-style 400 when the request is blocked by current resource
state; update these checks to use a 409 Conflict response instead. Add or reuse
a catalog error for project deletion being blocked (for the organization minimum
project check, and the API/MCP proxy/application association checks) so
ProjectService.DeleteProject returns a conflict-mapped error rather than
apperror.ValidationFailed.New.
🪄 Autofix (Beta)

❌ Autofix failed (check again to retry)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 235834f6-45ea-4bf8-8fc7-f94d7c1a6742

📥 Commits

Reviewing files that changed from the base of the PR and between e887a34 and 270baa1.

📒 Files selected for processing (29)
  • platform-api/internal/handler/api_key.go
  • platform-api/internal/handler/gateway.go
  • platform-api/internal/handler/llm_apikey.go
  • platform-api/internal/handler/llm_proxy_apikey.go
  • platform-api/internal/handler/organization.go
  • platform-api/internal/handler/project.go
  • platform-api/internal/handler/secret.go
  • platform-api/internal/handler/subscription_handler.go
  • platform-api/internal/handler/subscription_plan_handler.go
  • platform-api/internal/middleware/error_mapper.go
  • platform-api/internal/middleware/error_mapper_test.go
  • platform-api/internal/service/api.go
  • platform-api/internal/service/apikey.go
  • platform-api/internal/service/custom_policy_test.go
  • platform-api/internal/service/deployment.go
  • platform-api/internal/service/gateway.go
  • platform-api/internal/service/gateway_endpoints_test.go
  • platform-api/internal/service/llm.go
  • platform-api/internal/service/llm_apikey.go
  • platform-api/internal/service/llm_deployment.go
  • platform-api/internal/service/llm_proxy_apikey.go
  • platform-api/internal/service/mcp_deployment.go
  • platform-api/internal/service/organization.go
  • platform-api/internal/service/project.go
  • platform-api/internal/service/secret_service.go
  • platform-api/internal/service/subscription_plan_service.go
  • platform-api/internal/service/subscription_service.go
  • platform-api/internal/utils/error_mapper.go
  • platform-api/internal/webhook/receiver.go
💤 Files with no reviewable changes (1)
  • platform-api/internal/utils/error_mapper.go
✅ Files skipped from review due to trivial changes (1)
  • platform-api/internal/service/deployment.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • platform-api/internal/webhook/receiver.go
  • platform-api/internal/middleware/error_mapper.go
  • platform-api/internal/handler/subscription_plan_handler.go
  • platform-api/internal/handler/subscription_handler.go

Comment thread platform-api/internal/service/gateway.go
…ay retrieval errors. Now logs specific internal errors while maintaining clarity for gateway not found scenarios.

resp, err := h.secretService.Update(orgID, handle, userID, &req)
if err != nil {
if errors.Is(err, constants.ErrSecretNotFound) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to get rid of this secondary mapping layer
I.e: the service itself returns the error apperror.SecretNotFound.Wrap(err), then we don't need to do anything after that right?

@malinthaprasan

malinthaprasan commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Approach overall LGTM. We can merge this.

But as a secondary effort, would like to see if we can address this https://github.com/wso2/api-platform/pull/2507/changes#r3542205890 and completely get rid of the errors defined in the constants package.

Comment thread platform-api/internal/utils/codes.go Outdated
Comment thread platform-api/internal/apperror/codes.go
Comment thread platform-api/internal/apperror/error.go
Co-authored-by: Malintha Amarasinghe <malintha.prasan@gmail.com>
Comment thread platform-api/internal/utils/codes.go Outdated
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.
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.
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Autofix skipped. No unresolved CodeRabbit review comments with fix instructions found.

@Thushani-Jayasekera

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
platform-api/resources/openapi.yaml (2)

8528-8537: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Avoid leaking the internal env var name in the client-facing 503 body.

The ServiceUnavailable example message names an internal configuration variable (PLATFORM_SECRET_ENCRYPTION_KEY). Since this is the shape returned to API clients, it discloses internal deployment/config details. Prefer a sterile client message and keep the remediation hint in server logs/docs.

🔒️ Suggested example adjustment
-            code: SERVICE_UNAVAILABLE
-            message: Secrets management is not configured. Set PLATFORM_SECRET_ENCRYPTION_KEY.
+            code: SERVICE_UNAVAILABLE
+            message: Secrets management is not currently configured.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/resources/openapi.yaml` around lines 8528 - 8537, The
`ServiceUnavailable` example in `components/schemas/Error` is leaking an
internal configuration variable name to API clients. Update the example message
to a generic client-facing phrase in the `ServiceUnavailable` response, and keep
any setup hint about `PLATFORM_SECRET_ENCRYPTION_KEY` out of the OpenAPI body
example; use a sterile message in this schema entry and reference the deployment
detail only in server-side logs or docs.

6645-6652: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Add trackingId to the 503 examples. trackingId is present on every 5xx response in the schema and in the error writer, so the ServiceUnavailable and GatewayConnectionUnavailable examples should include it too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/resources/openapi.yaml` around lines 6645 - 6652, The 503
response examples for ServiceUnavailable and GatewayConnectionUnavailable are
missing the trackingId field even though 5xx responses include it in the schema
and error writer. Update the OpenAPI examples in openapi.yaml to include
trackingId alongside the existing error fields, using the same uuid-style value
pattern as the other server-error examples.
🧹 Nitpick comments (5)
platform-api/internal/apperror/apperror.go (1)

129-152: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Origin() only strips the /internal/ prefix, not /plugins/.

This trims logged paths for handlers/services under internal/, but errors originating in platform-api/plugins/eventgateway/... (part of this same PR stack, cohort 8) won't match "/internal/" and will fall through with the full build-machine absolute path in Origin()/log output.

♻️ Suggested generalization
 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:]
-	}
+	for _, marker := range []string{"/internal/", "/plugins/"} {
+		if idx := strings.Index(file, marker); idx != -1 {
+			file = file[idx:]
+			break
+		}
+	}
 	return fmt.Sprintf("%s:%d", file, frame.Line)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/apperror/apperror.go` around lines 129 - 152, The
Origin() method in Error currently trims only the "/internal/" prefix, so errors
created under other repo roots like "/plugins/" still log full absolute paths.
Update Origin() to generalize the path normalization logic by trimming any
repo-relative prefix needed for error origins, while preserving the existing
frame capture and file:line formatting, so calls through Error.Origin()
consistently return short paths for both internal and plugin-originated errors.
platform-api/plugins/eventgateway/handler/webbroker_apikey.go (1)

116-124: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

5xx responses lack tracking IDs — handlers bypass middleware.MapErrors.

These handlers write error responses directly via httputil.WriteJSON + apperror.NewErrorResponse, which does not generate or attach a trackingId. The PR objective states "tracking IDs for 5xx responses," but 5xx errors from these handlers won't carry correlation IDs, and the server-side slogger.Error calls have no tracking ID to match.

Consider migrating these eventgateway handlers to the error-returning pattern (middleware.MapErrors) used by api.go, or at minimum generate a UUID and include it in both the log and the ErrorResponse.TrackingID for 5xx responses.

Also applies to: 195-196, 246-247

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/plugins/eventgateway/handler/webbroker_apikey.go` around lines
116 - 124, 5xx responses from WebBroker API handlers are written directly, so
they never get a tracking ID or pass through middleware.MapErrors. Update the
WebBroker API error path in the handler that logs via h.slogger.Error to use the
same error-returning pattern as api.go, or explicitly create a tracking ID for
5xx cases and attach it to both the ErrorResponse and the log entry. Apply the
same fix in the related eventgateway handler branches referenced by the review
so all server errors are correlatable.
platform-api/plugins/eventgateway/handler/websub_api_deployment.go (1)

280-282: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

5xx response lacks tracking ID.

The default case in handleDeploymentError logs at ERROR and returns a 500, but the response has no trackingId for client-side correlation. This handler bypasses middleware.MapErrors, which generates tracking IDs for 5xx responses in the api.go path. Consider migrating to the error-returning pattern or generating a UUID here for both the log and response.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/plugins/eventgateway/handler/websub_api_deployment.go` around
lines 280 - 282, The default branch in handleDeploymentError returns a 500
without a trackingId, so client-side correlation is lost. Update
handleDeploymentError in websub_api_deployment.go to either follow the
error-returning flow used by middleware.MapErrors or generate a UUID in this
branch and include it in both the h.slogger.Error call and the
apperror.NewErrorResponse payload. Make sure the tracking ID is present in the
JSON response for the default case, alongside the existing apiId and error
context.
platform-api/plugins/eventgateway/handler/websub_apikey.go (1)

66-133: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Near-duplicate of webbroker_apikey.go — consider consolidation.

CreateAPIKey, UpdateAPIKey, DeleteAPIKey, and handleServiceError are structurally identical to webbroker_apikey.go, differing only in the API type constant (WebSubApi vs WebBrokerApi), path parameter name, and string messages. Any bug fix (e.g., the err.Error() and tracking ID issues above) must be applied to both files. Consider extracting a shared handler or a generic apiKeyHandler parameterized by API type to eliminate the duplication.

Also applies to: 136-207, 210-253, 256-266

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/plugins/eventgateway/handler/websub_apikey.go` around lines 66 -
133, The WebSub API key handler methods are nearly identical to the WebBroker
versions, so any fix must be duplicated manually in both places unless you
consolidate them. Refactor WebSubAPIKeyHandler’s CreateAPIKey and the related
API key methods to reuse a shared/generic handler, parameterized by the API type
constant, path value name, and response strings, so fixes like error formatting
and tracking ID handling live in one place. Use the matching symbols
CreateAPIKey, UpdateAPIKey, DeleteAPIKey, and handleServiceError as the
extraction points.
platform-api/plugins/eventgateway/handler/websub_api_hmac_secret.go (1)

124-124: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

5xx responses lack tracking IDs — handlers bypass middleware.MapErrors.

Lines 124, 162, 247 (identity resolution 500s) and lines 267–272 (handleServiceError 503/500 cases) all log via slogger.Error but return responses without a trackingId. The PR objective states "tracking IDs for 5xx responses," but these direct-write paths don't generate one. Consider migrating to the error-returning pattern used by api.go, or generate a UUID and include it in both the log and ErrorResponse.TrackingID.

Also applies to: 162-162, 247-247, 267-272

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/plugins/eventgateway/handler/websub_api_hmac_secret.go` at line
124, The direct 5xx response paths in the websub handler are bypassing
middleware.MapErrors, so they never attach a tracking ID. Update the HMAC secret
and identity-resolution flows in websub_api_hmac_secret.go, including the
handleServiceError 503/500 branches, to use the same error-returning pattern as
api.go or explicitly generate a UUID and pass it through both slogger.Error and
apperror.NewErrorResponse as TrackingID. Keep the response construction and
logging consistent via the relevant handler functions (such as
handleServiceError) so every 5xx includes a trackingId.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@platform-api/internal/handler/gateway_internal.go`:
- Around line 97-102: The API-key auth failure branch in gateway_internal.go
returns different 401 payloads for missing versus invalid tokens; update the
authentication handling in the relevant handler logic so both cases use the same
client-facing response. In the branch that handles API key checks around the
existing Unauthorized responses, replace the separate messages with the
standardized 401 payload and status for both missing and invalid credentials,
keeping the warning log in place but making the response identical for all auth
failures.
- Around line 353-357: The JSON decode paths in the gateway handler are reading
directly from unbounded request bodies, which can allocate too much memory;
update the request parsing in the DeploymentsBatchFetchRequest decode flow and
the other Decode call sites in the same handler to wrap r.Body with a configured
io.LimitReader before json.NewDecoder. Source the byte limit from existing
configuration, and when the limit is exceeded return a generic 413 Payload Too
Large response instead of surfacing the raw decode error.

In `@platform-api/plugins/eventgateway/handler/webbroker_api_deployment.go`:
- Line 69: The missing-organization-claim branches in
webbroker_api_deployment.go are returning a different client-facing 401 payload
than other auth failures. Update each unauthorized response in the affected
handlers to use the same uniform payload and status as the existing
credential-failure paths, matching the shared unauthorized response format used
by the relevant handler methods in this file.
- Around line 79-82: The deploy handler currently decodes the request body
directly in webbroker_api_deployment.go without any size cap. Update the deploy
flow in the handler that calls json.NewDecoder on r.Body to first wrap the body
with io.LimitReader using a byte limit sourced from configuration, then decode
from that limited reader. If the payload exceeds the configured limit, return a
generic 413 response instead of exposing decode details.

In `@platform-api/plugins/eventgateway/handler/webbroker_api.go`:
- Line 70: The unauthorized branches in webbroker_api.go are leaking specific
missing-claim details; update the response in the affected handler paths
(including the checks around the referenced claims) to use the same uniform 401
payload as other auth failures. Replace the apperror.NewErrorResponse-based
messages with the shared unauthorized response body and status used throughout
the auth flow, so WebBroker auth failures always return the exact same
client-facing message for invalid, expired, missing, or revoked credentials.
- Around line 74-77: Apply the shared request body size limit before decoding in
WebBroker API handlers, since `json.NewDecoder(r.Body)` in `handleWebBrokerAPI`
and the other decode path can still read oversized input; wrap the incoming
`r.Body` with the configured limit helper used elsewhere in the service before
passing it to the decoder. Update the error handling in `handleWebBrokerAPI` and
the second decode site to detect limit violations and return a generic HTTP 413
response, while keeping other decode failures mapped to the existing bad-request
handling.

In `@platform-api/plugins/eventgateway/handler/webbroker_apikey.go`:
- Line 169: The request validation path in webbroker_apikey.go is leaking
internal error details by sending err.Error() back to the client in both the
JSON decode failure and ErrInvalidInput cases. Update the handler logic around
the request parsing/validation flow in the relevant webhook API methods to
return a generic message like the other endpoints (for example, via
apperror.NewValidation or a static "Invalid request body") and keep the raw
error only in server-side logs if needed. Use the existing patterns in api.go
and websub_api_hmac_secret.go as the reference for the response shape and
message content.

In `@platform-api/plugins/eventgateway/handler/websub_api_deployment.go`:
- Line 81: The JSON decode error handling in the websub deployment handler is
exposing raw internal error text to clients. Update the response in the handler
that uses httputil.WriteJSON and apperror.NewErrorResponse to stop passing
err.Error(), and replace it with a static client-safe message like the one used
in the sibling websub_api_hmac_secret.go handler (“Invalid request body”). Keep
the existing status code and error response structure, but ensure the handler
only returns a generic message while preserving the underlying error for
internal logging if needed.

In `@platform-api/plugins/eventgateway/handler/websub_api.go`:
- Line 70: The WebSub auth failure responses in the handler should use the same
standard 401 payload as every other authentication failure instead of exposing
token details. Update the unauthorized branches in the WebSub API response
paths, including the org-claim-missing check and the other listed auth failure
sites, to return the exact client-facing body
`{"error":"unauthorized","message":"Invalid or expired credentials."}` with HTTP
401 via the existing response helper or equivalent shared auth error path.
- Around line 74-77: Cap the request body before decoding in the WebSub API
handlers that use json.NewDecoder on api.WebSubAPI and the other affected decode
path. Wrap r.Body with a configured io.LimitReader (or equivalent size guard)
before reading, and if the limit is exceeded return HTTP 413 with a generic
error message instead of attempting to decode. Keep the existing
validation/error logging flow for normal decode failures, but ensure oversized
bodies short-circuit early in the handler.

---

Outside diff comments:
In `@platform-api/resources/openapi.yaml`:
- Around line 8528-8537: The `ServiceUnavailable` example in
`components/schemas/Error` is leaking an internal configuration variable name to
API clients. Update the example message to a generic client-facing phrase in the
`ServiceUnavailable` response, and keep any setup hint about
`PLATFORM_SECRET_ENCRYPTION_KEY` out of the OpenAPI body example; use a sterile
message in this schema entry and reference the deployment detail only in
server-side logs or docs.
- Around line 6645-6652: The 503 response examples for ServiceUnavailable and
GatewayConnectionUnavailable are missing the trackingId field even though 5xx
responses include it in the schema and error writer. Update the OpenAPI examples
in openapi.yaml to include trackingId alongside the existing error fields, using
the same uuid-style value pattern as the other server-error examples.

---

Nitpick comments:
In `@platform-api/internal/apperror/apperror.go`:
- Around line 129-152: The Origin() method in Error currently trims only the
"/internal/" prefix, so errors created under other repo roots like "/plugins/"
still log full absolute paths. Update Origin() to generalize the path
normalization logic by trimming any repo-relative prefix needed for error
origins, while preserving the existing frame capture and file:line formatting,
so calls through Error.Origin() consistently return short paths for both
internal and plugin-originated errors.

In `@platform-api/plugins/eventgateway/handler/webbroker_apikey.go`:
- Around line 116-124: 5xx responses from WebBroker API handlers are written
directly, so they never get a tracking ID or pass through middleware.MapErrors.
Update the WebBroker API error path in the handler that logs via h.slogger.Error
to use the same error-returning pattern as api.go, or explicitly create a
tracking ID for 5xx cases and attach it to both the ErrorResponse and the log
entry. Apply the same fix in the related eventgateway handler branches
referenced by the review so all server errors are correlatable.

In `@platform-api/plugins/eventgateway/handler/websub_api_deployment.go`:
- Around line 280-282: The default branch in handleDeploymentError returns a 500
without a trackingId, so client-side correlation is lost. Update
handleDeploymentError in websub_api_deployment.go to either follow the
error-returning flow used by middleware.MapErrors or generate a UUID in this
branch and include it in both the h.slogger.Error call and the
apperror.NewErrorResponse payload. Make sure the tracking ID is present in the
JSON response for the default case, alongside the existing apiId and error
context.

In `@platform-api/plugins/eventgateway/handler/websub_api_hmac_secret.go`:
- Line 124: The direct 5xx response paths in the websub handler are bypassing
middleware.MapErrors, so they never attach a tracking ID. Update the HMAC secret
and identity-resolution flows in websub_api_hmac_secret.go, including the
handleServiceError 503/500 branches, to use the same error-returning pattern as
api.go or explicitly generate a UUID and pass it through both slogger.Error and
apperror.NewErrorResponse as TrackingID. Keep the response construction and
logging consistent via the relevant handler functions (such as
handleServiceError) so every 5xx includes a trackingId.

In `@platform-api/plugins/eventgateway/handler/websub_apikey.go`:
- Around line 66-133: The WebSub API key handler methods are nearly identical to
the WebBroker versions, so any fix must be duplicated manually in both places
unless you consolidate them. Refactor WebSubAPIKeyHandler’s CreateAPIKey and the
related API key methods to reuse a shared/generic handler, parameterized by the
API type constant, path value name, and response strings, so fixes like error
formatting and tracking ID handling live in one place. Use the matching symbols
CreateAPIKey, UpdateAPIKey, DeleteAPIKey, and handleServiceError as the
extraction points.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3cb6d9e5-016e-42d1-b4b4-a88857bb3e27

📥 Commits

Reviewing files that changed from the base of the PR and between 270baa1 and 4956865.

📒 Files selected for processing (27)
  • platform-api/internal/apperror/apperror.go
  • platform-api/internal/apperror/apperror_test.go
  • platform-api/internal/apperror/catalog.go
  • platform-api/internal/apperror/codes.go
  • platform-api/internal/apperror/error.go
  • platform-api/internal/apperror/write.go
  • platform-api/internal/dto/secret.go
  • platform-api/internal/handler/api.go
  • platform-api/internal/handler/gateway_internal.go
  • platform-api/internal/handler/llm.go
  • platform-api/internal/handler/llm_provider_integration_test.go
  • platform-api/internal/handler/mcp.go
  • platform-api/internal/middleware/error_mapper.go
  • platform-api/internal/middleware/error_mapper_test.go
  • platform-api/internal/service/api.go
  • platform-api/internal/service/gateway.go
  • platform-api/internal/service/llm.go
  • platform-api/plugins/eventgateway/handler/artifact_guard_response.go
  • platform-api/plugins/eventgateway/handler/identity_helper.go
  • platform-api/plugins/eventgateway/handler/webbroker_api.go
  • platform-api/plugins/eventgateway/handler/webbroker_api_deployment.go
  • platform-api/plugins/eventgateway/handler/webbroker_apikey.go
  • platform-api/plugins/eventgateway/handler/websub_api.go
  • platform-api/plugins/eventgateway/handler/websub_api_deployment.go
  • platform-api/plugins/eventgateway/handler/websub_api_hmac_secret.go
  • platform-api/plugins/eventgateway/handler/websub_apikey.go
  • platform-api/resources/openapi.yaml
🚧 Files skipped from review as they are similar to previous changes (12)
  • platform-api/internal/apperror/write.go
  • platform-api/internal/dto/secret.go
  • platform-api/internal/middleware/error_mapper.go
  • platform-api/internal/middleware/error_mapper_test.go
  • platform-api/internal/service/api.go
  • platform-api/internal/handler/llm_provider_integration_test.go
  • platform-api/internal/apperror/apperror_test.go
  • platform-api/internal/apperror/catalog.go
  • platform-api/internal/service/llm.go
  • platform-api/internal/handler/mcp.go
  • platform-api/internal/handler/llm.go
  • platform-api/internal/service/gateway.go

Comment thread platform-api/internal/handler/gateway_internal.go
Comment thread platform-api/internal/handler/gateway_internal.go
Comment thread platform-api/plugins/eventgateway/handler/webbroker_api.go
Comment thread platform-api/plugins/eventgateway/handler/webbroker_api.go
Comment thread platform-api/plugins/eventgateway/handler/webbroker_apikey.go
Comment thread platform-api/plugins/eventgateway/handler/websub_api_deployment.go
Comment thread platform-api/plugins/eventgateway/handler/websub_api.go
Comment thread platform-api/plugins/eventgateway/handler/websub_api.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants