API Errors

This guide covers error handling for the Teabar API, including error codes, response formats, and best practices.

Error Response Format

All API errors follow a consistent format:

{
  "code": "not_found",
  "message": "Environment 'env_abc123' not found",
  "details": [
    {
      "@type": "type.googleapis.com/teabar.v1.ErrorInfo",
      "reason": "ENVIRONMENT_NOT_FOUND",
      "domain": "teabar.dev",
      "metadata": {
        "environmentId": "env_abc123"
      }
    }
  ]
}

Error Codes

Standard Codes

CodeHTTP StatusDescription
ok200Success (not an error)
cancelled499Request cancelled by client
unknown500Unknown error
invalid_argument400Invalid request parameters
deadline_exceeded504Request timed out
not_found404Resource doesn’t exist
already_exists409Resource already exists
permission_denied403Insufficient permissions
resource_exhausted429Rate limit or quota exceeded
failed_precondition400Operation not allowed in current state
aborted409Operation aborted (conflict)
out_of_range400Value out of valid range
unimplemented501Feature not implemented
internal500Internal server error
unavailable503Service unavailable
data_loss500Unrecoverable data loss
unauthenticated401Authentication required

Domain-Specific Reasons

Error details include domain-specific reason codes:

Authentication Errors:

ReasonDescription
TOKEN_EXPIREDAuthentication token has expired
TOKEN_INVALIDToken format or signature invalid
TOKEN_REVOKEDToken has been revoked
INSUFFICIENT_SCOPEToken lacks required scope

Environment Errors:

ReasonDescription
ENVIRONMENT_NOT_FOUNDEnvironment doesn’t exist
ENVIRONMENT_NOT_RUNNINGEnvironment is not in running state
ENVIRONMENT_ALREADY_RUNNINGEnvironment is already running
ENVIRONMENT_PROVISIONING_FAILEDProvisioning error
ENVIRONMENT_QUOTA_EXCEEDEDToo many environments

Blueprint Errors:

ReasonDescription
BLUEPRINT_NOT_FOUNDBlueprint doesn’t exist
BLUEPRINT_INVALIDBlueprint validation failed
BLUEPRINT_IN_USEBlueprint used by environments

Organization Errors:

ReasonDescription
ORGANIZATION_NOT_FOUNDOrganization doesn’t exist
PROJECT_NOT_FOUNDProject doesn’t exist
MEMBER_NOT_FOUNDMember doesn’t exist
INVITATION_EXPIREDInvitation has expired

Error Details

ErrorInfo

Standard error information:

{
  "@type": "type.googleapis.com/teabar.v1.ErrorInfo",
  "reason": "ENVIRONMENT_QUOTA_EXCEEDED",
  "domain": "teabar.dev",
  "metadata": {
    "limit": "10",
    "current": "10"
  }
}

BadRequest

Field-level validation errors:

{
  "@type": "type.googleapis.com/google.rpc.BadRequest",
  "fieldViolations": [
    {
      "field": "name",
      "description": "Name must be lowercase alphanumeric"
    },
    {
      "field": "ttl",
      "description": "TTL must be between 1h and 30d"
    }
  ]
}

RetryInfo

When to retry failed requests:

{
  "@type": "type.googleapis.com/google.rpc.RetryInfo",
  "retryDelay": "30s"
}

QuotaFailure

Quota violation details:

{
  "@type": "type.googleapis.com/google.rpc.QuotaFailure",
  "violations": [
    {
      "subject": "environments",
      "description": "Environment quota exceeded: 10/10"
    }
  ]
}

Handling Errors

TypeScript/JavaScript

import { ConnectError, Code } from "@connectrpc/connect";

try {
  await client.createEnvironment(request);
} catch (err) {
  if (err instanceof ConnectError) {
    switch (err.code) {
      case Code.NotFound:
        console.error("Resource not found:", err.message);
        break;
      case Code.PermissionDenied:
        console.error("Permission denied:", err.message);
        break;
      case Code.ResourceExhausted:
        // Extract retry delay if available
        const retryInfo = err.findDetails(RetryInfo);
        if (retryInfo.length > 0) {
          const delay = retryInfo[0].retryDelay;
          console.log(`Rate limited. Retry after ${delay}`);
        }
        break;
      default:
        console.error("API error:", err.message);
    }
  }
}

Go

import (
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

resp, err := client.CreateEnvironment(ctx, req)
if err != nil {
    st, ok := status.FromError(err)
    if !ok {
        return fmt.Errorf("unknown error: %w", err)
    }
    
    switch st.Code() {
    case codes.NotFound:
        return fmt.Errorf("not found: %s", st.Message())
    case codes.PermissionDenied:
        return fmt.Errorf("permission denied: %s", st.Message())
    case codes.ResourceExhausted:
        // Check for retry info in details
        for _, detail := range st.Details() {
            if ri, ok := detail.(*errdetails.RetryInfo); ok {
                delay := ri.GetRetryDelay().AsDuration()
                time.Sleep(delay)
                return retry(ctx, req)
            }
        }
    default:
        return fmt.Errorf("API error: %s", st.Message())
    }
}

Python

from grpc import StatusCode

try:
    response = client.CreateEnvironment(request)
except grpc.RpcError as e:
    code = e.code()
    
    if code == StatusCode.NOT_FOUND:
        print(f"Not found: {e.details()}")
    elif code == StatusCode.PERMISSION_DENIED:
        print(f"Permission denied: {e.details()}")
    elif code == StatusCode.RESOURCE_EXHAUSTED:
        print(f"Rate limited: {e.details()}")
    else:
        print(f"Error: {e.details()}")

Retry Strategies

Retryable Errors

These errors are typically safe to retry:

CodeRetry?Notes
unavailableYesService temporarily down
resource_exhaustedYesAfter delay from RetryInfo
deadline_exceededMaybeDepends on idempotency
abortedYesTransaction conflict
internalMaybeDepends on operation

Non-Retryable Errors

CodeRetry?Notes
invalid_argumentNoFix request first
not_foundNoResource doesn’t exist
already_existsNoResource exists
permission_deniedNoCheck permissions
unauthenticatedNoRe-authenticate first

Exponential Backoff

async function withRetry<T>(
  operation: () => Promise<T>,
  maxRetries: number = 3
): Promise<T> {
  let lastError: Error;
  
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await operation();
    } catch (err) {
      if (err instanceof ConnectError) {
        if (!isRetryable(err.code)) {
          throw err;
        }
        lastError = err;
        const delay = Math.pow(2, i) * 1000 + Math.random() * 1000;
        await sleep(delay);
      } else {
        throw err;
      }
    }
  }
  
  throw lastError!;
}

Common Error Scenarios

Authentication Errors

{
  "code": "unauthenticated",
  "message": "Invalid or expired token",
  "details": [{
    "reason": "TOKEN_EXPIRED",
    "metadata": {
      "expiredAt": "2024-03-10T00:00:00Z"
    }
  }]
}

Resolution: Refresh token or re-authenticate.

Validation Errors

{
  "code": "invalid_argument",
  "message": "Invalid request",
  "details": [{
    "@type": "type.googleapis.com/google.rpc.BadRequest",
    "fieldViolations": [
      {"field": "name", "description": "Required field"}
    ]
  }]
}

Resolution: Fix request parameters per field violations.

Rate Limiting

{
  "code": "resource_exhausted",
  "message": "Rate limit exceeded",
  "details": [{
    "@type": "type.googleapis.com/google.rpc.RetryInfo",
    "retryDelay": "30s"
  }]
}

Resolution: Wait for specified delay, then retry.

See Also

ende