Skip to main content

Error Types

The Zitadel Ruby SDK defines a hierarchy of exceptions to help you handle different error scenarios appropriately.

Exception Hierarchy

StandardError
└── Zitadel::Client::ZitadelError (base exception)
    └── Zitadel::Client::ApiError (HTTP errors)
  • ZitadelError: Base class for all SDK exceptions (lib/zitadel/client/zitadel_error.rb:7)
  • ApiError: HTTP error responses from the Zitadel API (lib/zitadel/client/api_error.rb:9)

ApiError Class

The ApiError exception is raised when the API returns a non-2xx HTTP status code. It provides detailed information about the failure.

Properties

error = Zitadel::Client::ApiError.new(code, headers, body)

error.code              # Integer - HTTP status code (404, 500, etc.)
error.response_headers  # Hash - HTTP response headers
error.response_body     # String - Raw response body
error.message          # String - "Error {code}"

Example Usage

require 'zitadel-client'

client = Zitadel::Client::Zitadel.with_access_token(
  "https://example.us1.zitadel.cloud",
  "your-token"
)

begin
  response = client.users.get_user_by_id(
    Zitadel::Client::UserServiceGetUserByIdRequest.new(
      user_id: "non-existent-id"
    )
  )
rescue Zitadel::Client::ApiError => e
  puts "HTTP Status: #{e.code}"
  puts "Response Body: #{e.response_body}"
  puts "Headers: #{e.response_headers}"
end
ApiError is raised by the call_api method in lib/zitadel/client/api_client.rb:79 when a request fails.

Common HTTP Error Codes

400 - Bad Request

Invalid request parameters or malformed request body.
begin
  client.users.add_human_user(
    Zitadel::Client::UserServiceAddHumanUserRequest.new(
      username: "",  # Invalid: empty username
      profile: Zitadel::Client::UserServiceSetHumanProfile.new(
        given_name: "John",
        family_name: "Doe"
      )
    )
  )
rescue Zitadel::Client::ApiError => e
  if e.code == 400
    puts "Invalid request: #{e.response_body}"
    # Parse error details and show user-friendly message
  end
end

401 - Unauthorized

Authentication failed - invalid or expired credentials.
begin
  client.users.list_users(
    Zitadel::Client::UserServiceListUsersRequest.new(limit: 10)
  )
rescue Zitadel::Client::ApiError => e
  if e.code == 401
    puts "Authentication failed. Please check your credentials."
    # Refresh token or re-authenticate
  end
end
A 401 error may indicate expired credentials. If using Client Credentials flow, the SDK should automatically refresh tokens. Check your authenticator configuration.

403 - Forbidden

Authentication succeeded but the service user lacks required permissions.
begin
  client.organizations.delete_organization(
    Zitadel::Client::OrganizationServiceDeleteOrganizationRequest.new(
      organization_id: "123456"
    )
  )
rescue Zitadel::Client::ApiError => e
  if e.code == 403
    puts "Permission denied. Service user lacks organization deletion privileges."
    # Request permission grant from administrator
  end
end

404 - Not Found

Requested resource doesn’t exist.
begin
  user = client.users.get_user_by_id(
    Zitadel::Client::UserServiceGetUserByIdRequest.new(user_id: "invalid-id")
  )
rescue Zitadel::Client::ApiError => e
  if e.code == 404
    puts "User not found"
    return nil
  end
end

429 - Too Many Requests

Rate limit exceeded.
begin
  users = client.users.list_users(
    Zitadel::Client::UserServiceListUsersRequest.new(limit: 100)
  )
rescue Zitadel::Client::ApiError => e
  if e.code == 429
    retry_after = e.response_headers['Retry-After']&.first&.to_i || 60
    puts "Rate limit exceeded. Retry after #{retry_after} seconds."
    sleep(retry_after)
    retry
  end
end

500 - Internal Server Error

Server-side error in the Zitadel API.
begin
  response = client.users.add_human_user(request)
rescue Zitadel::Client::ApiError => e
  if e.code >= 500
    puts "Server error occurred. Please try again later."
    # Log error for investigation
    logger.error("Zitadel API Error: #{e.code} - #{e.response_body}")
  end
end

Network Errors

The SDK also handles network-level errors that occur before receiving an HTTP response.

Timeout Errors

Raised when a request exceeds the configured timeout.
client = Zitadel::Client::Zitadel.with_private_key(
  "https://example.us1.zitadel.cloud",
  "key.json"
) do |config|
  config.timeout = 5  # 5 seconds
end

begin
  client.users.list_users(
    Zitadel::Client::UserServiceListUsersRequest.new(limit: 1000)
  )
rescue RuntimeError => e
  if e.message == 'Connection timed out'
    puts "Request timed out after 5 seconds"
    # Retry with exponential backoff
  end
end
Timeout errors are raised as RuntimeError with message "Connection timed out" from lib/zitadel/client/api_client.rb:75.

Network Connectivity Errors

Raised when the SDK cannot connect to the API.
begin
  client.users.get_user_by_id(request)
rescue RuntimeError => e
  if e.message.start_with?('Network error')
    puts "Cannot connect to Zitadel API: #{e.message}"
    # Check network connectivity or API status
  end
end

Comprehensive Error Handling Pattern

Basic Pattern

require 'zitadel-client'

client = Zitadel::Client::Zitadel.with_access_token(
  "https://example.us1.zitadel.cloud",
  ENV['ZITADEL_TOKEN']
)

begin
  response = client.users.add_human_user(
    Zitadel::Client::UserServiceAddHumanUserRequest.new(
      username: "john.doe",
      profile: Zitadel::Client::UserServiceSetHumanProfile.new(
        given_name: "John",
        family_name: "Doe"
      ),
      email: Zitadel::Client::UserServiceSetHumanEmail.new(
        email: "[email protected]"
      )
    )
  )
  
  puts "User created successfully: #{response.user_id}"
  
rescue Zitadel::Client::ApiError => e
  case e.code
  when 400
    puts "Invalid request: #{e.response_body}"
  when 401
    puts "Authentication failed"
  when 403
    puts "Permission denied"
  when 404
    puts "Resource not found"
  when 429
    puts "Rate limit exceeded"
  when 500..599
    puts "Server error occurred"
  else
    puts "Unexpected error: #{e.code} - #{e.response_body}"
  end
  
rescue RuntimeError => e
  if e.message == 'Connection timed out'
    puts "Request timed out"
  elsif e.message.start_with?('Network error')
    puts "Network error: #{e.message}"
  else
    raise  # Re-raise unexpected errors
  end
  
rescue StandardError => e
  puts "Unexpected error: #{e.class} - #{e.message}"
  raise
end

Advanced Pattern with Retry Logic

require 'zitadel-client'

def create_user_with_retry(client, user_data, max_retries: 3)
  retries = 0
  backoff = 1  # Initial backoff in seconds
  
  begin
    response = client.users.add_human_user(user_data)
    puts "User created: #{response.user_id}"
    return response
    
  rescue Zitadel::Client::ApiError => e
    case e.code
    when 429  # Rate limit
      retry_after = e.response_headers['Retry-After']&.first&.to_i || 60
      puts "Rate limited. Waiting #{retry_after} seconds..."
      sleep(retry_after)
      retry
      
    when 500..599  # Server errors - retry with backoff
      if retries < max_retries
        retries += 1
        puts "Server error (#{e.code}). Retry #{retries}/#{max_retries} in #{backoff}s"
        sleep(backoff)
        backoff *= 2  # Exponential backoff
        retry
      else
        raise  # Max retries exceeded
      end
      
    when 400, 403, 404  # Client errors - don't retry
      puts "Request failed: #{e.code} - #{e.response_body}"
      return nil
      
    else
      raise  # Unexpected error code
    end
    
  rescue RuntimeError => e
    if e.message == 'Connection timed out' && retries < max_retries
      retries += 1
      puts "Timeout. Retry #{retries}/#{max_retries} in #{backoff}s"
      sleep(backoff)
      backoff *= 2
      retry
    else
      raise
    end
  end
end

# Usage
client = Zitadel::Client::Zitadel.with_private_key(
  "https://example.us1.zitadel.cloud",
  "key.json"
)

user_data = Zitadel::Client::UserServiceAddHumanUserRequest.new(
  username: "jane.doe",
  profile: Zitadel::Client::UserServiceSetHumanProfile.new(
    given_name: "Jane",
    family_name: "Doe"
  )
)

result = create_user_with_retry(client, user_data, max_retries: 5)

Error Logging Best Practices

Log with Context

require 'logger'

logger = Logger.new('zitadel.log')

begin
  response = client.users.get_user_by_id(request)
rescue Zitadel::Client::ApiError => e
  logger.error({
    error: 'ApiError',
    code: e.code,
    message: e.message,
    response_body: e.response_body,
    request: {
      method: 'get_user_by_id',
      user_id: request.user_id
    },
    timestamp: Time.now.iso8601
  }.to_json)
  
  raise  # Re-raise after logging
end

Structured Error Responses

class ZitadelService
  def get_user(user_id)
    response = @client.users.get_user_by_id(
      Zitadel::Client::UserServiceGetUserByIdRequest.new(user_id: user_id)
    )
    
    { success: true, data: response }
    
  rescue Zitadel::Client::ApiError => e
    {
      success: false,
      error: {
        type: 'api_error',
        code: e.code,
        message: parse_error_message(e.response_body),
        details: e.response_body
      }
    }
    
  rescue RuntimeError => e
    {
      success: false,
      error: {
        type: 'network_error',
        message: e.message
      }
    }
  end
  
  private
  
  def parse_error_message(body)
    JSON.parse(body)['message'] rescue body
  end
end
# app/services/zitadel_user_service.rb
class ZitadelUserService
  class Error < StandardError; end
  class AuthenticationError < Error; end
  class PermissionError < Error; end
  class NotFoundError < Error; end
  class RateLimitError < Error; end
  class ServerError < Error; end
  
  def initialize
    @client = ZITADEL_CLIENT  # Initialized in config/initializers/zitadel.rb
  end
  
  def create_user(username:, email:, first_name:, last_name:)
    request = Zitadel::Client::UserServiceAddHumanUserRequest.new(
      username: username,
      profile: Zitadel::Client::UserServiceSetHumanProfile.new(
        given_name: first_name,
        family_name: last_name
      ),
      email: Zitadel::Client::UserServiceSetHumanEmail.new(
        email: email
      )
    )
    
    response = with_error_handling do
      @client.users.add_human_user(request)
    end
    
    response.user_id
  end
  
  private
  
  def with_error_handling
    yield
  rescue Zitadel::Client::ApiError => e
    handle_api_error(e)
  rescue RuntimeError => e
    handle_runtime_error(e)
  end
  
  def handle_api_error(error)
    case error.code
    when 401
      raise AuthenticationError, "Zitadel authentication failed"
    when 403
      raise PermissionError, "Insufficient permissions: #{error.response_body}"
    when 404
      raise NotFoundError, "Resource not found"
    when 429
      raise RateLimitError, "Rate limit exceeded"
    when 500..599
      Rails.logger.error("Zitadel server error: #{error.code} - #{error.response_body}")
      raise ServerError, "Zitadel service unavailable"
    else
      Rails.logger.error("Unexpected Zitadel error: #{error.code} - #{error.response_body}")
      raise Error, "Unexpected error: #{error.code}"
    end
  end
  
  def handle_runtime_error(error)
    if error.message == 'Connection timed out'
      raise Error, "Request to Zitadel timed out"
    elsif error.message.start_with?('Network error')
      raise Error, "Network error: #{error.message}"
    else
      raise
    end
  end
end

# Usage in controller
class UsersController < ApplicationController
  def create
    service = ZitadelUserService.new
    user_id = service.create_user(
      username: params[:username],
      email: params[:email],
      first_name: params[:first_name],
      last_name: params[:last_name]
    )
    
    render json: { user_id: user_id }, status: :created
    
  rescue ZitadelUserService::PermissionError => e
    render json: { error: e.message }, status: :forbidden
  rescue ZitadelUserService::NotFoundError => e
    render json: { error: e.message }, status: :not_found
  rescue ZitadelUserService::RateLimitError => e
    render json: { error: e.message }, status: :too_many_requests
  rescue ZitadelUserService::Error => e
    render json: { error: e.message }, status: :internal_server_error
  end
end

Testing Error Handling

require 'rspec'

RSpec.describe ZitadelUserService do
  let(:client) { instance_double(Zitadel::Client::Zitadel) }
  let(:service) { described_class.new(client) }
  
  describe '#create_user' do
    context 'when API returns 403' do
      it 'raises PermissionError' do
        allow(client.users).to receive(:add_human_user)
          .and_raise(Zitadel::Client::ApiError.new(403, {}, 'Forbidden'))
        
        expect {
          service.create_user(
            username: 'test',
            email: '[email protected]',
            first_name: 'Test',
            last_name: 'User'
          )
        }.to raise_error(ZitadelUserService::PermissionError)
      end
    end
    
    context 'when request times out' do
      it 'raises Error with timeout message' do
        allow(client.users).to receive(:add_human_user)
          .and_raise(RuntimeError, 'Connection timed out')
        
        expect {
          service.create_user(username: 'test', email: '[email protected]')
        }.to raise_error(ZitadelUserService::Error, /timed out/)
      end
    end
  end
end

Next Steps

Build docs developers (and LLMs) love