Error Handling - FreshPerf/PVE4J GitHub Wiki

Error Handling

This guide covers error handling strategies in PVE4J.

Table of Contents

Exception Types

PVE4J operations can throw two main types of exceptions:

ProxmoxAPIError

The primary exception type for API-related errors.

import fr.freshperf.pve4j.throwable.ProxmoxAPIError;

try {
    // API operation
} catch (ProxmoxAPIError e) {
    System.err.println("Error message: " + e.getMessage());
    System.err.println("HTTP status code: " + e.getStatusCode());
    System.err.println("Response body: " + e.getResponseBody());
    System.err.println("Request URL: " + e.getUrl());
}

ProxmoxAPIError provides:

  • getMessage() - Human-readable error message (formatted with status, URL, response)
  • getStatusCode() - HTTP status code (400, 401, 404, 500, etc.) or -1 if not available
  • getResponseBody() - Full response body from Proxmox API, or null
  • getUrl() - The URL that was requested, or null

InterruptedException

Thrown when a thread is interrupted during an operation.

try {
    // Long-running operation
} catch (InterruptedException e) {
    System.err.println("Operation was interrupted");
    Thread.currentThread().interrupt(); // Restore interrupt status
}

Basic Error Handling

Simple Try-Catch

try {
    PveVersion version = proxmox.getVersion().execute();
    System.out.println("Version: " + version.getVersion());
} catch (ProxmoxAPIError e) {
    System.err.println("API Error: " + e.getMessage());
} catch (InterruptedException e) {
    System.err.println("Operation interrupted");
    Thread.currentThread().interrupt();
}

Handle Multiple Operations

try {
    // Get version
    PveVersion version = proxmox.getVersion().execute();
    System.out.println("Version: " + version.getVersion());
    
    // Get cluster status
    List<PveClusterStatus> status = proxmox.getCluster().getStatus().execute();
    System.out.println("Cluster nodes: " + status.size());
    
} catch (ProxmoxAPIError e) {
    System.err.println("Failed to retrieve information: " + e.getMessage());
    System.err.println("Status code: " + e.getStatusCode());
} catch (InterruptedException e) {
    System.err.println("Operation interrupted");
    Thread.currentThread().interrupt();
}

Advanced Error Handling

Error Classification by Status Code

public class ProxmoxErrorHandler {
    
    public void handleError(ProxmoxAPIError e) {
        int statusCode = e.getStatusCode();
        
        switch (statusCode) {
            case 400 -> handleBadRequest(e);
            case 401 -> handleUnauthorized(e);
            case 403 -> handleForbidden(e);
            case 404 -> handleNotFound(e);
            case 500 -> handleServerError(e);
            default -> handleGenericError(e);
        }
    }
    
    private void handleBadRequest(ProxmoxAPIError e) {
        System.err.println("Bad Request: Invalid parameters");
        System.err.println("Details: " + e.getResponseBody());
    }
    
    private void handleUnauthorized(ProxmoxAPIError e) {
        System.err.println("Unauthorized: Check your API token");
        System.err.println("Token may be expired or invalid");
    }
    
    private void handleForbidden(ProxmoxAPIError e) {
        System.err.println("Forbidden: Insufficient permissions");
        System.err.println("Check user roles and ACL settings");
    }
    
    private void handleNotFound(ProxmoxAPIError e) {
        System.err.println("Not Found: Resource does not exist");
        System.err.println("URL: " + e.getUrl());
    }
    
    private void handleServerError(ProxmoxAPIError e) {
        System.err.println("Server Error: Proxmox internal error");
        System.err.println("This might be temporary, consider retrying");
    }
    
    private void handleGenericError(ProxmoxAPIError e) {
        System.err.println("Error " + e.getStatusCode() + ": " + e.getMessage());
    }
}

Parsing Error Response

public class ErrorParser {
    
    public void parseAndLogError(ProxmoxAPIError e) {
        try {
            JsonObject response = JsonParser.parseString(e.getResponseBody())
                    .getAsJsonObject();
            
            if (response.has("errors")) {
                JsonObject errors = response.getAsJsonObject("errors");
                System.err.println("Validation errors:");
                errors.entrySet().forEach(entry -> 
                        System.err.println("  " + entry.getKey() + ": " + entry.getValue()));
            }
            
        } catch (Exception parseError) {
            // If parsing fails, log the raw response
            System.err.println("Raw error response: " + e.getResponseBody());
        }
    }
}

Contextual Error Messages

public class VMOperations {
    
    public void startVM(Proxmox proxmox, String node, int vmid) {
        try {
            PveTask task = proxmox.getNodes()
                    .get(node)
                    .getQemu()
                    .get(vmid)
                    .start()
                    .waitForCompletion(proxmox)
                    .execute();
            
            System.out.println("VM " + vmid + " started successfully");
            
        } catch (ProxmoxAPIError e) {
            String context = String.format("Failed to start VM %d on node %s", vmid, node);
            handleVMError(context, e);
        } catch (InterruptedException e) {
            System.err.println("VM start operation was interrupted");
            Thread.currentThread().interrupt();
        }
    }
    
    private void handleVMError(String context, ProxmoxAPIError e) {
        System.err.println(context);
        System.err.println("Error: " + e.getMessage());
        
        if (e.getStatusCode() == 500 && e.getResponseBody().contains("already running")) {
            System.out.println("Note: VM appears to be already running");
        } else if (e.getStatusCode() == 404) {
            System.err.println("VM does not exist or node is incorrect");
        } else {
            System.err.println("Status code: " + e.getStatusCode());
            System.err.println("Response: " + e.getResponseBody());
        }
    }
}

Common Errors

Authentication Errors (401)

try {
    proxmox.getVersion().execute();
} catch (ProxmoxAPIError e) {
    if (e.getStatusCode() == 401) {
        System.err.println("Authentication failed!");
        System.err.println("Verify your API token:");
        System.err.println("  1. Check token format: USER@REALM!TOKENID=SECRET");
        System.err.println("  2. Verify token exists in Proxmox");
        System.err.println("  3. Check token is not expired");
        System.err.println("  4. Ensure token has required permissions");
    }
}

Resource Not Found (404)

public boolean vmExists(Proxmox proxmox, String node, int vmid) {
    try {
        proxmox.getNodes()
                .get(node)
                .getQemu()
                .get(vmid)
                .getConfig()
                .execute();
        return true;
    } catch (ProxmoxAPIError e) {
        if (e.getStatusCode() == 404) {
            return false;
        }
        throw new RuntimeException("Error checking VM existence", e);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RuntimeException("Interrupted", e);
    }
}

Permission Errors (403)

try {
    proxmox.getNodes()
            .get("pve-node-01")
            .getQemu()
            .get(100)
            .delete()
            .execute();
} catch (ProxmoxAPIError e) {
    if (e.getStatusCode() == 403) {
        System.err.println("Permission denied!");
        System.err.println("The API token does not have permission to delete VMs");
        System.err.println("Required role: PVEVMAdmin or Administrator");
    }
}

SSL Certificate Errors

try {
    Proxmox proxmox = Proxmox.create("pve.example.com", 8006, "token");
    proxmox.getVersion().execute();
} catch (ProxmoxAPIError e) {
    if (e.getMessage().contains("SSL") || e.getMessage().contains("certificate")) {
        System.err.println("SSL Certificate Error!");
        System.err.println("Solutions:");
        System.err.println("  1. Install valid SSL certificate on Proxmox");
        System.err.println("  2. Use SecurityConfig.insecure() for self-signed certs (dev only)");
        System.err.println("  3. Import self-signed cert to Java trust store");
    }
}

Connection Errors

try {
    proxmox.getVersion().execute();
} catch (ProxmoxAPIError e) {
    if (e.getMessage().contains("Connection refused") || 
        e.getMessage().contains("timeout")) {
        System.err.println("Connection Error!");
        System.err.println("Check:");
        System.err.println("  1. Proxmox host is reachable");
        System.err.println("  2. Port 8006 is accessible");
        System.err.println("  3. Firewall allows connection");
        System.err.println("  4. Proxmox API service is running");
    }
}

Retry Strategies

Built-in Retry

PVE4J has built-in retry support on ProxmoxRequest:

PveVersion version = proxmox.getVersion()
        .retry(3)
        .retryDelay(Duration.ofSeconds(2))
        .execute();

Simple Manual Retry

public <T> T executeWithRetry(Callable<T> operation, int maxAttempts) {
    int attempt = 0;
    Exception lastException = null;
    
    while (attempt < maxAttempts) {
        try {
            return operation.call();
        } catch (ProxmoxAPIError e) {
            lastException = e;
            attempt++;
            
            if (attempt < maxAttempts) {
                System.err.println("Attempt " + attempt + " failed, retrying...");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("Interrupted during retry", ie);
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("Operation failed", e);
        }
    }
    
    throw new RuntimeException("Operation failed after " + maxAttempts + " attempts", 
            lastException);
}

// Usage
PveVersion version = executeWithRetry(() -> 
    proxmox.getVersion().execute(), 3);

Exponential Backoff

public class RetryHandler {
    
    public <T> T executeWithExponentialBackoff(
            Callable<T> operation, 
            int maxAttempts,
            long initialDelayMs) {
        
        int attempt = 0;
        long delay = initialDelayMs;
        
        while (attempt < maxAttempts) {
            try {
                return operation.call();
                
            } catch (ProxmoxAPIError e) {
                attempt++;
                
                if (attempt >= maxAttempts) {
                    throw new RuntimeException("Max retry attempts reached", e);
                }
                
                // Only retry on specific errors
                if (!isRetryable(e)) {
                    throw new RuntimeException("Non-retryable error", e);
                }
                
                System.err.println("Attempt " + attempt + " failed, waiting " + 
                        delay + "ms before retry...");
                
                try {
                    Thread.sleep(delay);
                    delay *= 2; // Exponential backoff
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("Interrupted", ie);
                }
                
            } catch (Exception e) {
                throw new RuntimeException("Unexpected error", e);
            }
        }
        
        throw new RuntimeException("Should never reach here");
    }
    
    private boolean isRetryable(ProxmoxAPIError e) {
        int status = e.getStatusCode();
        return status >= 500 || status == 408 || status == 429;
    }
}

Conditional Retry

public class SmartRetry {
    
    public <T> T executeWithConditionalRetry(
            Callable<T> operation,
            int maxAttempts) {
        
        for (int attempt = 1; attempt <= maxAttempts; attempt++) {
            try {
                return operation.call();
                
            } catch (ProxmoxAPIError e) {
                // Don't retry on authentication or permission errors
                if (e.getStatusCode() == 401 || e.getStatusCode() == 403) {
                    throw new RuntimeException("Authentication/Permission error", e);
                }
                
                // Don't retry on not found or bad request
                if (e.getStatusCode() == 404 || e.getStatusCode() == 400) {
                    throw new RuntimeException("Client error", e);
                }
                
                // Retry on server errors
                if (e.getStatusCode() >= 500 && attempt < maxAttempts) {
                    System.err.println("Server error, retrying... (attempt " + 
                            attempt + "/" + maxAttempts + ")");
                    try {
                        Thread.sleep(2000 * attempt);
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt();
                        throw new RuntimeException("Interrupted", ie);
                    }
                } else {
                    throw new RuntimeException("Operation failed", e);
                }
                
            } catch (Exception e) {
                throw new RuntimeException("Unexpected error", e);
            }
        }
        
        throw new RuntimeException("Max retries reached");
    }
}

Custom Exception for Business Logic

public class VMNotFoundException extends RuntimeException {
    private final int vmid;
    
    public VMNotFoundException(int vmid) {
        super("VM " + vmid + " not found");
        this.vmid = vmid;
    }
    
    public int getVmid() {
        return vmid;
    }
}

public PveQemuConfig getVMConfig(Proxmox proxmox, String node, int vmid) {
    try {
        return proxmox.getNodes()
                .get(node)
                .getQemu()
                .get(vmid)
                .getConfig()
                .execute();
    } catch (ProxmoxAPIError e) {
        if (e.getStatusCode() == 404) {
            throw new VMNotFoundException(vmid);
        }
        throw new RuntimeException("Failed to get VM config", e);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RuntimeException("Interrupted", e);
    }
}

Next Steps