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
TheApiError 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
Complete error handling example with Rails
Complete error handling example with Rails
# 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
- Enable Debugging to troubleshoot errors
- Review Configuration for timeout and retry settings
- Learn about Service Users permissions