Skip to content

Errors

HarborGuard returns errors in a single envelope across every endpoint. The HTTP status is always set; the JSON body adds a machine-readable code, a human message, and an optional list of validation issues.

Error envelope

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "image: Required, scanners: must contain at least 1 element",
    "issues": [
      { "path": "image", "message": "Required" },
      { "path": "scanners", "message": "must contain at least 1 element" }
    ]
  }
}

issues appears only for validation failures. Each issue carries a dotted JSON path into the request body and the offending message.

Error codes

CodeHTTPMeaning
BAD_REQUEST400Malformed request — bad JSON, bad query params, missing required body
VALIDATION_ERROR400Body parsed but failed schema validation; see issues
UNAUTHORIZED401No credential or unknown / expired credential
FORBIDDEN403Credential is valid but role is below the endpoint's minimum
PLAN_LIMIT402The organization has exhausted a plan-based quota (scans, members, registries)
NOT_FOUND404Resource does not exist or is not visible to your organization
RATE_LIMITED429Sliding-window rate limit exceeded — see Retry-After
INTERNAL_ERROR500Unexpected server error; the original cause is logged but not leaked
SERVICE_UNAVAILABLE503Database or cache temporarily unavailable; retry with backoff
TIMEOUT504Request exceeded the server statement timeout

Specific status notes

  • 404 responses use the resource name in the message: "Scan not found", "Registry not found". Treat them identically across resources.
  • 402 PLAN_LIMIT is not a payment failure — it means a resource cap on your current plan is reached. Upgrade or free a slot.
  • 503 SERVICE_UNAVAILABLE typically indicates a transient connection issue; retry after 1–5 s.
  • 500 INTERNAL_ERROR is intentionally opaque. The full stack is logged server-side; capture the request URL and timestamp when contacting support.

Handling errors

const res = await fetch(url, { headers })
if (!res.ok) {
  const body = await res.json().catch(() => ({}))
  const code = body?.error?.code ?? "UNKNOWN"
 
  if (code === "RATE_LIMITED") {
    const retry = Number(res.headers.get("retry-after") ?? "5")
    await new Promise(r => setTimeout(r, retry * 1000))
    return doRequest()
  }
 
  if (code === "VALIDATION_ERROR") {
    for (const issue of body.error.issues ?? []) {
      console.error(`${issue.path}: ${issue.message}`)
    }
  }
 
  throw new Error(`${code}: ${body.error?.message ?? res.statusText}`)
}

Webhook errors

The Stripe webhook endpoint (POST /api/billing/webhook) rejects malformed signatures with 400 BAD_REQUEST and never retries from the HarborGuard side. It acknowledges unknown event types with 200 so the upstream provider does not retry indefinitely.

On this page