Simulating Failures & Error Scenarios
Comprehensive guide to testing error scenarios, failure modes, and edge cases in your Omise integration. Learn how to simulate network failures, payment declines, API errors, and implement robust error handling.
Overview​
Testing failure scenarios is critical for building a robust payment integration. Your application must handle errors gracefully, provide clear feedback to users, and implement proper retry logic. This guide covers all failure scenarios you should test before going live.
Why Test Failures?​
- User Experience: Provide clear, helpful error messages
- Data Integrity: Prevent duplicate charges and data corruption
- System Reliability: Handle network issues and timeouts gracefully
- Security: Detect and prevent fraudulent transactions
- Compliance: Meet payment card industry requirements
- Business Continuity: Maintain operations during partial outages
Types of Failures to Test​
- Payment Declines: Card declines, insufficient funds, fraud detection
- Network Failures: Connection errors, timeouts, DNS failures
- API Errors: Invalid requests, authentication errors, rate limits
- 3D Secure Failures: Authentication failures, timeout, cancellation
- Webhook Failures: Delivery failures, signature verification errors
- System Errors: Server errors, database failures, integration issues
Payment Decline Scenarios​
Testing Specific Decline Reasons​
Use these test cards to simulate specific decline scenarios:
// JavaScript - Testing all decline scenarios
const Omise = require('omise')({
secretKey: 'skey_test_xxxxxxxxxx'
});
const declineTestCases = [
{
card: '4000000000000101',
expectedCode: 'insufficient_funds',
description: 'Insufficient funds'
},
{
card: '4000000000000119',
expectedCode: 'processing_error',
description: 'Processing error'
},
{
card: '4000000000000127',
expectedCode: 'lost_card',
description: 'Lost card'
},
{
card: '4000000000000135',
expectedCode: 'stolen_card',
description: 'Stolen card'
},
{
card: '4000000000000143',
expectedCode: 'expired_card',
description: 'Expired card'
},
{
card: '4000000000000150',
expectedCode: 'invalid_security_code',
description: 'Invalid CVV'
},
{
card: '4000000000000192',
expectedCode: 'failed_fraud_check',
description: 'Failed fraud check'
},
{
card: '4000000000000234',
expectedCode: 'payment_rejected',
description: 'Payment rejected'
}
];
async function testAllDeclines() {
for (const testCase of declineTestCases) {
try {
console.log(`Testing: ${testCase.description}`);
const token = await Omise.tokens.create({
card: {
name: 'Test User',
number: testCase.card,
expiration_month: 12,
expiration_year: 2026,
security_code: '123'
}
});
const charge = await Omise.charges.create({
amount: 100000,
currency: 'THB',
card: token.id
});
// Verify the expected decline
if (charge.status === 'failed') {
console.log(`✓ ${testCase.description}: ${charge.failure_code}`);
// Assert expected failure code
if (charge.failure_code === testCase.expectedCode) {
console.log(` Expected code matched: ${testCase.expectedCode}`);
} else {
console.error(` Expected: ${testCase.expectedCode}, Got: ${charge.failure_code}`);
}
} else {
console.error(`✗ Expected failed charge, got: ${charge.status}`);
}
} catch (error) {
console.error(`✗ ${testCase.description} error:`, error.message);
}
}
}
testAllDeclines();
# Python - Testing decline scenarios with proper handling
import omise
from typing import List, Dict
omise.api_secret = 'skey_test_xxxxxxxxxx'
decline_test_cases = [
{
'card': '4000000000000101',
'expected_code': 'insufficient_funds',
'description': 'Insufficient funds',
'user_message': 'Your card has insufficient funds. Please use another card.'
},
{
'card': '4000000000000119',
'expected_code': 'processing_error',
'description': 'Processing error',
'user_message': 'Unable to process payment. Please try again.'
},
{
'card': '4000000000000127',
'expected_code': 'lost_card',
'description': 'Lost card',
'user_message': 'This card has been reported lost. Please use another card.'
},
{
'card': '4000000000000135',
'expected_code': 'stolen_card',
'description': 'Stolen card',
'user_message': 'This card has been reported stolen. Please contact your bank.'
},
{
'card': '4000000000000192',
'expected_code': 'failed_fraud_check',
'description': 'Failed fraud check',
'user_message': 'Payment declined for security reasons. Please contact your bank.'
}
]
def get_user_friendly_message(failure_code: str) -> str:
"""Convert failure codes to user-friendly messages"""
messages = {
'insufficient_funds': 'Your card has insufficient funds. Please use another card.',
'processing_error': 'Unable to process payment. Please try again.',
'lost_card': 'This card has been reported lost. Please use another card.',
'stolen_card': 'This card cannot be used. Please contact your bank.',
'expired_card': 'Your card has expired. Please use another card.',
'invalid_security_code': 'The security code (CVV) is incorrect.',
'failed_fraud_check': 'Payment declined for security reasons.',
'payment_rejected': 'Payment was declined. Please try another card.'
}
return messages.get(failure_code, 'Payment declined. Please try another card.')
def test_decline_scenario(test_case: Dict) -> Dict:
"""Test a single decline scenario"""
try:
token = omise.Token.create(
card={
'name': 'Test User',
'number': test_case['card'],
'expiration_month': 12,
'expiration_year': 2026,
'security_code': '123'
}
)
charge = omise.Charge.create(
amount=100000,
currency='THB',
card=token.id
)
result = {
'success': False,
'description': test_case['description'],
'status': charge.status,
'failure_code': charge.failure_code if charge.status == 'failed' else None,
'failure_message': charge.failure_message if charge.status == 'failed' else None,
'user_message': get_user_friendly_message(charge.failure_code) if charge.status == 'failed' else None
}
# Verify expected outcome
if charge.status == 'failed' and charge.failure_code == test_case['expected_code']:
result['success'] = True
print(f"✓ {test_case['description']}: {charge.failure_code}")
else:
print(f"✗ {test_case['description']}: Expected {test_case['expected_code']}, got {charge.failure_code}")
return result
except omise.errors.BaseError as e:
print(f"✗ {test_case['description']} error: {e.message}")
return {
'success': False,
'description': test_case['description'],
'error': str(e)
}
def test_all_declines():
"""Test all decline scenarios"""
results = []
for test_case in decline_test_cases:
result = test_decline_scenario(test_case)
results.append(result)
# Summary
successful = sum(1 for r in results if r.get('success'))
print(f"\n{successful}/{len(results)} tests passed")
return results
if __name__ == '__main__':
test_all_declines()
Handling Decline Messages​
Provide user-friendly error messages:
# Ruby - Converting failure codes to user messages
require 'omise'
Omise.api_key = 'skey_test_xxxxxxxxxx'
class PaymentErrorHandler
DECLINE_MESSAGES = {
'insufficient_funds' => 'Your card has insufficient funds. Please use another payment method.',
'processing_error' => 'Unable to process your payment. Please try again.',
'lost_card' => 'This card has been reported lost. Please use another card.',
'stolen_card' => 'This card cannot be used. Please contact your bank.',
'expired_card' => 'Your card has expired. Please update your payment information.',
'invalid_security_code' => 'The CVV/security code is incorrect. Please check and try again.',
'invalid_card_number' => 'The card number is invalid. Please check and try again.',
'invalid_expiry_date' => 'The expiry date is invalid. Please check and try again.',
'transaction_not_permitted' => 'This transaction is not permitted on your card.',
'failed_fraud_check' => 'Payment declined for security reasons. Please contact your bank.',
'payment_rejected' => 'Payment was declined. Please try another payment method.',
'card_restricted' => 'Your card has restrictions that prevent this payment.',
'exceeds_withdrawal_limit' => 'Payment amount exceeds your card limit.',
'issuer_unavailable' => 'Card issuer is temporarily unavailable. Please try again later.'
}.freeze
def self.get_user_message(failure_code)
DECLINE_MESSAGES[failure_code] || 'Payment declined. Please try another payment method.'
end
def self.should_retry?(failure_code)
# These failures might succeed on retry
retryable = %w[processing_error issuer_unavailable timeout_error]
retryable.include?(failure_code)
end
def self.handle_payment_failure(charge)
{
message: get_user_message(charge.failure_code),
code: charge.failure_code,
can_retry: should_retry?(charge.failure_code),
suggestion: get_suggestion(charge.failure_code)
}
end
def self.get_suggestion(failure_code)
case failure_code
when 'insufficient_funds'
'Try a different card or add funds to this card'
when 'invalid_security_code', 'invalid_card_number', 'invalid_expiry_date'
'Double-check your card details and try again'
when 'processing_error', 'issuer_unavailable'
'Wait a moment and try again'
when 'failed_fraud_check'
'Contact your bank to authorize this payment'
else
'Use a different payment method'
end
end
end
# Usage example
def process_payment_with_error_handling(token_id)
begin
charge = Omise::Charge.create(
amount: 100_000,
currency: 'THB',
card: token_id
)
if charge.status == 'failed'
error_info = PaymentErrorHandler.handle_payment_failure(charge)
puts "Payment failed: #{error_info[:message]}"
puts "Suggestion: #{error_info[:suggestion]}"
puts "Can retry: #{error_info[:can_retry]}"
return { success: false, error: error_info }
end
{ success: true, charge: charge }
rescue Omise::Error => e
puts "Error: #{e.message}"
{ success: false, error: e.message }
end
end
Network Failure Simulation​
Testing Timeouts​
Simulate network timeouts in your application:
// JavaScript - Testing timeout handling
const Omise = require('omise')({
secretKey: 'skey_test_xxxxxxxxxx'
});
// Configure custom timeout (in milliseconds)
const axios = require('axios');
const https = require('https');
async function testTimeoutHandling() {
const timeout = 5000; // 5 seconds
const customAgent = new https.Agent({
timeout: timeout
});
try {
const token = await Omise.tokens.create({
card: {
name: 'Test User',
number: '4242424242424242',
expiration_month: 12,
expiration_year: 2026,
security_code: '123'
}
});
// Simulate timeout with very short timeout
const chargePromise = Omise.charges.create({
amount: 100000,
currency: 'THB',
card: token.id
});
// Create a timeout promise
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Request timeout')), timeout);
});
// Race between charge and timeout
const charge = await Promise.race([chargePromise, timeoutPromise]);
console.log('Charge completed:', charge.id);
} catch (error) {
if (error.message === 'Request timeout') {
console.log('✓ Timeout handled correctly');
// Implement retry logic
await retryWithBackoff();
} else {
console.error('Unexpected error:', error.message);
}
}
}
async function retryWithBackoff(attempt = 1, maxAttempts = 3) {
const backoffDelay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
console.log(`Retry attempt ${attempt} after ${backoffDelay}ms`);
await new Promise(resolve => setTimeout(resolve, backoffDelay));
try {
// Retry the operation
const result = await performPayment();
return result;
} catch (error) {
if (attempt < maxAttempts) {
return retryWithBackoff(attempt + 1, maxAttempts);
}
throw error;
}
}
testTimeoutHandling();
<?php
// PHP - Testing network timeouts with retry logic
require_once 'vendor/autoload.php';
define('OMISE_SECRET_KEY', 'skey_test_xxxxxxxxxx');
define('OMISE_API_VERSION', '2019-05-29');
class NetworkFailureHandler {
private $maxRetries = 3;
private $timeout = 30; // seconds
public function createChargeWithRetry($tokenId, $amount, $currency) {
$attempt = 0;
while ($attempt < $this->maxRetries) {
try {
$attempt++;
// Set timeout for the request
$opts = [
'http' => [
'timeout' => $this->timeout
]
];
$context = stream_context_create($opts);
$charge = OmiseCharge::create([
'amount' => $amount,
'currency' => $currency,
'card' => $tokenId
], null, null, $context);
echo "Charge successful on attempt $attempt\n";
return $charge;
} catch (Exception $e) {
$errorMessage = $e->getMessage();
// Check if it's a timeout or network error
if ($this->isNetworkError($e)) {
echo "Network error on attempt $attempt: $errorMessage\n";
if ($attempt < $this->maxRetries) {
$backoffTime = $this->calculateBackoff($attempt);
echo "Retrying in {$backoffTime}s...\n";
sleep($backoffTime);
} else {
throw new Exception("Max retries exceeded: $errorMessage");
}
} else {
// Non-retryable error
throw $e;
}
}
}
}
private function isNetworkError($exception) {
$networkErrors = [
'timeout',
'connection',
'socket',
'network',
'timed out',
'could not resolve host'
];
$message = strtolower($exception->getMessage());
foreach ($networkErrors as $error) {
if (strpos($message, $error) !== false) {
return true;
}
}
return false;
}
private function calculateBackoff($attempt) {
// Exponential backoff: 2^attempt seconds, max 10 seconds
return min(pow(2, $attempt), 10);
}
}
// Usage
$handler = new NetworkFailureHandler();
try {
$token = OmiseToken::create([
'card' => [
'name' => 'Test User',
'number' => '4242424242424242',
'expiration_month' => 12,
'expiration_year' => 2026,
'security_code' => '123'
]
]);
$charge = $handler->createChargeWithRetry(
$token['id'],
100000,
'THB'
);
echo "Payment successful: {$charge['id']}\n";
} catch (Exception $e) {
echo "Payment failed: {$e->getMessage()}\n";
}
?>
Testing Connection Failures​
// Go - Testing connection failures and retries
package main
import (
"context"
"fmt"
"net"
"net/http"
"time"
"github.com/omise/omise-go"
"github.com/omise/omise-go/operations"
)
type RetryConfig struct {
MaxRetries int
BaseDelay time.Duration
MaxDelay time.Duration
Timeout time.Duration
}
func createHTTPClientWithTimeout(timeout time.Duration) *http.Client {
return &http.Client{
Timeout: timeout,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
}
}
func isRetryableError(err error) bool {
if err == nil {
return false
}
// Network errors are retryable
if _, ok := err.(net.Error); ok {
return true
}
// Timeout errors are retryable
if err == context.DeadlineExceeded {
return true
}
return false
}
func calculateBackoff(attempt int, config RetryConfig) time.Duration {
delay := config.BaseDelay * time.Duration(1<<uint(attempt))
if delay > config.MaxDelay {
delay = config.MaxDelay
}
return delay
}
func createChargeWithRetry(client *omise.Client, tokenID string, amount int64, currency string, config RetryConfig) (*omise.Charge, error) {
var lastErr error
for attempt := 0; attempt < config.MaxRetries; attempt++ {
if attempt > 0 {
backoff := calculateBackoff(attempt-1, config)
fmt.Printf("Retry attempt %d after %v\n", attempt+1, backoff)
time.Sleep(backoff)
}
charge, err := client.CreateCharge(&operations.CreateCharge{
Amount: amount,
Currency: currency,
Card: tokenID,
})
if err == nil {
if attempt > 0 {
fmt.Printf("Charge successful on attempt %d\n", attempt+1)
}
return charge, nil
}
lastErr = err
if !isRetryableError(err) {
fmt.Printf("Non-retryable error: %v\n", err)
return nil, err
}
fmt.Printf("Retryable error on attempt %d: %v\n", attempt+1, err)
}
return nil, fmt.Errorf("max retries exceeded: %v", lastErr)
}
func testNetworkFailures() {
config := RetryConfig{
MaxRetries: 3,
BaseDelay: 1 * time.Second,
MaxDelay: 10 * time.Second,
Timeout: 30 * time.Second,
}
httpClient := createHTTPClientWithTimeout(config.Timeout)
client, _ := omise.NewClient(
"pkey_test_xxxxxxxxxx",
"skey_test_xxxxxxxxxx",
)
client.HTTPClient = httpClient
client.APIVersion = "2019-05-29"
// Create token
token, err := client.CreateToken(&operations.CreateToken{
Name: "Test User",
Number: "4242424242424242",
ExpirationMonth: 12,
ExpirationYear: 2026,
SecurityCode: "123",
})
if err != nil {
fmt.Printf("Error creating token: %v\n", err)
return
}
// Create charge with retry logic
charge, err := createChargeWithRetry(
client,
token.ID,
100000,
"THB",
config,
)
if err != nil {
fmt.Printf("Payment failed: %v\n", err)
return
}
fmt.Printf("Payment successful: %s\n", charge.ID)
}
func main() {
testNetworkFailures()
}
3D Secure Failure Scenarios​
Testing 3DS Authentication Failures​
# Python - Testing 3D Secure failures
import omise
omise.api_secret = 'skey_test_xxxxxxxxxx'
omise.api_version = '2019-05-29'
def test_3ds_authentication_failure():
"""Test failed 3DS authentication"""
try:
# Use card that fails 3DS authentication
token = omise.Token.create(
card={
'name': 'Test User',
'number': '4000000000000010', # 3DS enrolled, auth fails
'expiration_month': 12,
'expiration_year': 2026,
'security_code': '123'
}
)
charge = omise.Charge.create(
amount=100000,
currency='THB',
card=token.id,
return_uri='https://example.com/complete'
)
print(f'Initial Status: {charge.status}') # 'pending'
print(f'Authorize URI: {charge.authorize_uri}')
# After customer attempts 3DS, charge will fail
# Simulate checking the charge after 3DS
import time
time.sleep(2) # In real scenario, wait for redirect
updated_charge = omise.Charge.retrieve(charge.id)
print(f'Final Status: {updated_charge.status}') # 'failed'
print(f'Failure Code: {updated_charge.failure_code}') # '3ds_authentication_failed'
except omise.errors.BaseError as e:
print(f'Error: {e.message}')
def test_3ds_timeout():
"""Test 3DS timeout scenario"""
try:
token = omise.Token.create(
card={
'name': 'Test User',
'number': '4000000000000002', # 3DS enrolled
'expiration_month': 12,
'expiration_year': 2026,
'security_code': '123'
}
)
charge = omise.Charge.create(
amount=100000,
currency='THB',
card=token.id,
return_uri='https://example.com/complete'
)
# In test mode, 3DS charges expire after 15 minutes
# Check if charge expired due to timeout
print(f'Charge created: {charge.id}')
print(f'Expires at: {charge.expires_at}')
# In production, implement webhook handler for charge.expire event
except omise.errors.BaseError as e:
print(f'Error: {e.message}')
def test_3ds_cancellation():
"""Test user cancelling 3DS authentication"""
try:
token = omise.Token.create(
card={
'name': 'Test User',
'number': '4000000000000028', # 3DS enrolled, rejected
'expiration_month': 12,
'expiration_year': 2026,
'security_code': '123'
}
)
charge = omise.Charge.create(
amount=100000,
currency='THB',
card=token.id,
return_uri='https://example.com/complete'
)
# User cancels on 3DS page
updated_charge = omise.Charge.retrieve(charge.id)
if updated_charge.status == 'failed':
print(f'3DS rejected: {updated_charge.failure_code}')
except omise.errors.BaseError as e:
print(f'Error: {e.message}')
if __name__ == '__main__':
print("Testing 3DS authentication failure...")
test_3ds_authentication_failure()
print("\nTesting 3DS timeout...")
test_3ds_timeout()
print("\nTesting 3DS cancellation...")
test_3ds_cancellation()
API Error Scenarios​
Testing Invalid Requests​
// JavaScript - Testing API validation errors
const Omise = require('omise')({
secretKey: 'skey_test_xxxxxxxxxx'
});
async function testValidationErrors() {
const testCases = [
{
name: 'Invalid amount (negative)',
data: { amount: -100, currency: 'THB', card: 'tok_test' },
expectedError: 'invalid_amount'
},
{
name: 'Invalid amount (below minimum)',
data: { amount: 100, currency: 'THB', card: 'tok_test' },
expectedError: 'invalid_amount'
},
{
name: 'Invalid currency',
data: { amount: 100000, currency: 'XXX', card: 'tok_test' },
expectedError: 'invalid_currency'
},
{
name: 'Missing required field',
data: { amount: 100000, currency: 'THB' },
expectedError: 'missing_card'
},
{
name: 'Invalid token',
data: { amount: 100000, currency: 'THB', card: 'invalid_token' },
expectedError: 'invalid_card'
}
];
for (const testCase of testCases) {
try {
console.log(`\nTesting: ${testCase.name}`);
const charge = await Omise.charges.create(testCase.data);
console.log('✗ Expected error but charge succeeded');
} catch (error) {
console.log(`✓ Error caught: ${error.code}`);
console.log(` Message: ${error.message}`);
if (error.code === testCase.expectedError) {
console.log(` Expected error matched`);
}
}
}
}
async function testAuthenticationErrors() {
// Test with invalid API key
const badClient = require('omise')({
secretKey: 'skey_test_invalid'
});
try {
const charges = await badClient.charges.list();
console.log('✗ Expected authentication error');
} catch (error) {
if (error.code === 'authentication_failure') {
console.log('✓ Authentication error handled correctly');
}
}
}
async function testRateLimiting() {
// Simulate rapid requests to test rate limiting
const requests = [];
for (let i = 0; i < 100; i++) {
requests.push(
Omise.charges.list({ limit: 1 })
.catch(error => {
if (error.code === 'rate_limit_exceeded') {
console.log('✓ Rate limit detected');
return { rateLimited: true };
}
throw error;
})
);
}
const results = await Promise.allSettled(requests);
const rateLimited = results.filter(r =>
r.status === 'fulfilled' && r.value?.rateLimited
).length;
console.log(`Rate limited: ${rateLimited} requests`);
}
async function runAllAPITests() {
console.log('=== Testing Validation Errors ===');
await testValidationErrors();
console.log('\n=== Testing Authentication Errors ===');
await testAuthenticationErrors();
console.log('\n=== Testing Rate Limiting ===');
await testRateLimiting();
}
runAllAPITests();
Handling API Errors Gracefully​
# Ruby - Comprehensive API error handling
require 'omise'
Omise.api_key = 'skey_test_xxxxxxxxxx'
class OmiseErrorHandler
# Map Omise errors to user-friendly messages
ERROR_MESSAGES = {
'authentication_failure' => 'System configuration error. Please contact support.',
'invalid_card' => 'Invalid card information. Please check and try again.',
'invalid_amount' => 'Invalid payment amount.',
'invalid_currency' => 'Unsupported currency.',
'rate_limit_exceeded' => 'Too many requests. Please wait and try again.',
'service_unavailable' => 'Service temporarily unavailable. Please try again later.',
'internal_server_error' => 'An error occurred. Please try again later.'
}.freeze
def self.handle_error(error)
case error
when Omise::AuthenticationError
handle_authentication_error(error)
when Omise::InvalidRequestError
handle_invalid_request_error(error)
when Omise::ServiceError
handle_service_error(error)
when Omise::RateLimitError
handle_rate_limit_error(error)
else
handle_generic_error(error)
end
end
def self.handle_authentication_error(error)
# Log error for admin
log_error('AUTHENTICATION', error)
{
code: 'authentication_failure',
message: ERROR_MESSAGES['authentication_failure'],
user_action: 'contact_support',
retryable: false
}
end
def self.handle_invalid_request_error(error)
log_error('INVALID_REQUEST', error)
{
code: error.code,
message: ERROR_MESSAGES[error.code] || error.message,
user_action: 'fix_input',
retryable: false
}
end
def self.handle_service_error(error)
log_error('SERVICE_ERROR', error)
{
code: 'service_unavailable',
message: ERROR_MESSAGES['service_unavailable'],
user_action: 'retry_later',
retryable: true
}
end
def self.handle_rate_limit_error(error)
log_error('RATE_LIMIT', error)
{
code: 'rate_limit_exceeded',
message: ERROR_MESSAGES['rate_limit_exceeded'],
user_action: 'wait_and_retry',
retryable: true,
retry_after: 60 # seconds
}
end
def self.handle_generic_error(error)
log_error('GENERIC', error)
{
code: 'internal_server_error',
message: ERROR_MESSAGES['internal_server_error'],
user_action: 'retry_later',
retryable: true
}
end
def self.log_error(type, error)
# In production, log to your logging service
puts "[#{type}] #{error.class}: #{error.message}"
puts error.backtrace.first(5).join("\n") if error.backtrace
end
end
# Usage in application
def create_payment_with_error_handling(token_id, amount, currency)
begin
charge = Omise::Charge.create(
amount: amount,
currency: currency,
card: token_id
)
if charge.status == 'successful'
return { success: true, charge: charge }
elsif charge.status == 'failed'
return {
success: false,
error: {
code: charge.failure_code,
message: charge.failure_message,
user_action: 'use_different_card'
}
}
end
rescue Omise::Error => e
error_info = OmiseErrorHandler.handle_error(e)
return { success: false, error: error_info }
rescue StandardError => e
# Catch any unexpected errors
OmiseErrorHandler.log_error('UNEXPECTED', e)
return {
success: false,
error: {
code: 'unexpected_error',
message: 'An unexpected error occurred',
user_action: 'contact_support'
}
}
end
end
Webhook Failure Scenarios​
Testing Webhook Delivery Failures​
# Python - Testing webhook delivery and retry logic
import hmac
import hashlib
import json
from flask import Flask, request, jsonify
app = Flask(__name__)
class WebhookTester:
def __init__(self, secret_key):
self.secret_key = secret_key
self.received_events = []
self.failure_mode = None
def verify_signature(self, payload, signature):
"""Verify webhook signature"""
expected_signature = hmac.new(
self.secret_key.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected_signature, signature)
def simulate_failure(self, mode):
"""Set failure mode for testing"""
self.failure_mode = mode
@app.route('/webhooks', methods=['POST'])
def handle_webhook(self):
"""Webhook endpoint with failure simulation"""
payload = request.get_data(as_text=True)
signature = request.headers.get('X-Omise-Signature', '')
# Test signature verification failure
if self.failure_mode == 'invalid_signature':
print('Simulating invalid signature rejection')
return jsonify({'error': 'Invalid signature'}), 401
# Verify signature
if not self.verify_signature(payload, signature):
print('Invalid webhook signature')
return jsonify({'error': 'Invalid signature'}), 401
# Test timeout
if self.failure_mode == 'timeout':
print('Simulating timeout (not responding)')
import time
time.sleep(35) # Omise timeout is 30 seconds
# Test server error
if self.failure_mode == 'server_error':
print('Simulating 500 error')
return jsonify({'error': 'Internal server error'}), 500
# Test temporary failure (will be retried)
if self.failure_mode == 'temporary_failure':
print('Simulating temporary failure')
if len(self.received_events) < 3: # Fail first 3 attempts
return jsonify({'error': 'Temporary error'}), 503
# Success on retry
self.failure_mode = None
# Parse webhook data
try:
event = json.loads(payload)
self.received_events.append(event)
print(f'Webhook received: {event["key"]}')
print(f'Event type: {event.get("data", {}).get("object")}')
# Process event
self.process_event(event)
return jsonify({'received': True}), 200
except json.JSONDecodeError:
return jsonify({'error': 'Invalid JSON'}), 400
def process_event(self, event):
"""Process webhook event"""
object_type = event.get('data', {}).get('object')
if object_type == 'charge':
self.handle_charge_event(event)
elif object_type == 'refund':
self.handle_refund_event(event)
elif object_type == 'transfer':
self.handle_transfer_event(event)
def handle_charge_event(self, event):
"""Handle charge events"""
charge = event['data']
event_key = event['key']
if event_key == 'charge.complete':
print(f'Charge completed: {charge["id"]}')
elif event_key == 'charge.create':
print(f'Charge created: {charge["id"]}')
elif event_key == 'charge.expire':
print(f'Charge expired: {charge["id"]}')
def handle_refund_event(self, event):
"""Handle refund events"""
refund = event['data']
print(f'Refund created: {refund["id"]}')
def handle_transfer_event(self, event):
"""Handle transfer events"""
transfer = event['data']
print(f'Transfer: {transfer["id"]}')
# Test webhook failures
def test_webhook_scenarios():
tester = WebhookTester('webhook_secret_key')
# Test 1: Valid webhook
print('\n=== Test 1: Valid Webhook ===')
tester.simulate_failure(None)
# Send test webhook...
# Test 2: Invalid signature
print('\n=== Test 2: Invalid Signature ===')
tester.simulate_failure('invalid_signature')
# Send test webhook...
# Test 3: Timeout
print('\n=== Test 3: Timeout ===')
tester.simulate_failure('timeout')
# Send test webhook...
# Test 4: Server error
print('\n=== Test 4: Server Error ===')
tester.simulate_failure('server_error')
# Send test webhook...
# Test 5: Temporary failure with retry
print('\n=== Test 5: Temporary Failure (Retry) ===')
tester.simulate_failure('temporary_failure')
# Send test webhook multiple times...
if __name__ == '__main__':
test_webhook_scenarios()
Testing Retry Logic​
Implementing Exponential Backoff​
// JavaScript - Exponential backoff retry logic
class RetryHandler {
constructor(options = {}) {
this.maxRetries = options.maxRetries || 3;
this.baseDelay = options.baseDelay || 1000; // 1 second
this.maxDelay = options.maxDelay || 30000; // 30 seconds
this.factor = options.factor || 2;
this.jitter = options.jitter !== false; // Add jitter by default
}
calculateDelay(attempt) {
// Exponential backoff: baseDelay * (factor ^ attempt)
let delay = this.baseDelay * Math.pow(this.factor, attempt);
// Cap at maximum delay
delay = Math.min(delay, this.maxDelay);
// Add jitter to prevent thundering herd
if (this.jitter) {
delay = delay * (0.5 + Math.random() * 0.5);
}
return Math.floor(delay);
}
async retry(fn, options = {}) {
const shouldRetry = options.shouldRetry || this.defaultShouldRetry;
const onRetry = options.onRetry || this.defaultOnRetry;
let lastError;
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
if (attempt > 0) {
const delay = this.calculateDelay(attempt - 1);
await onRetry(attempt, delay, lastError);
await this.sleep(delay);
}
return await fn();
} catch (error) {
lastError = error;
if (attempt === this.maxRetries || !shouldRetry(error)) {
throw error;
}
}
}
}
defaultShouldRetry(error) {
// Retry on network errors and 5xx server errors
const retryableErrors = [
'ETIMEDOUT',
'ECONNRESET',
'ECONNREFUSED',
'ENETUNREACH',
'EAI_AGAIN'
];
if (retryableErrors.includes(error.code)) {
return true;
}
// Retry on 5xx errors
if (error.statusCode >= 500 && error.statusCode < 600) {
return true;
}
// Retry on rate limit
if (error.code === 'rate_limit_exceeded') {
return true;
}
return false;
}
defaultOnRetry(attempt, delay, error) {
console.log(
`Retry attempt ${attempt} after ${delay}ms. ` +
`Error: ${error.message}`
);
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Usage example
const Omise = require('omise')({
secretKey: 'skey_test_xxxxxxxxxx'
});
const retryHandler = new RetryHandler({
maxRetries: 3,
baseDelay: 1000,
maxDelay: 10000
});
async function createChargeWithRetry(tokenId, amount, currency) {
return retryHandler.retry(
async () => {
const charge = await Omise.charges.create({
amount: amount,
currency: currency,
card: tokenId
});
return charge;
},
{
onRetry: (attempt, delay, error) => {
console.log(
`Retrying charge creation (attempt ${attempt}) ` +
`after ${delay}ms due to: ${error.message}`
);
}
}
);
}
// Test the retry logic
async function testRetryLogic() {
try {
const token = await Omise.tokens.create({
card: {
name: 'Test User',
number: '4242424242424242',
expiration_month: 12,
expiration_year: 2026,
security_code: '123'
}
});
const charge = await createChargeWithRetry(
token.id,
100000,
'THB'
);
console.log('Charge successful:', charge.id);
} catch (error) {
console.error('Charge failed after retries:', error.message);
}
}
testRetryLogic();
Best Practices for Failure Testing​
1. Test All Failure Modes​
Create a comprehensive test suite covering all failure scenarios:
// Comprehensive failure test suite
const failureTests = {
// Payment declines
'insufficient_funds': '4000000000000101',
'lost_card': '4000000000000127',
'stolen_card': '4000000000000135',
'expired_card': '4000000000000143',
'invalid_cvv': '4000000000000150',
'fraud_check': '4000000000000192',
// 3D Secure failures
'3ds_failed': '4000000000000010',
'3ds_rejected': '4000000000000028',
// Network simulations
'timeout': async () => { /* Timeout simulation */ },
'connection_error': async () => { /* Connection error */ },
// API errors
'invalid_amount': { amount: -100 },
'invalid_currency': { currency: 'XXX' },
'missing_card': { /* No card field */ }
};
2. Implement Proper Error Logging​
Log all errors with sufficient context for debugging:
import logging
import json
from datetime import datetime
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def log_payment_error(error_type, error_data, context=None):
"""Log payment errors with full context"""
log_entry = {
'timestamp': datetime.utcnow().isoformat(),
'error_type': error_type,
'error_data': error_data,
'context': context or {}
}
logger.error(f'Payment Error: {json.dumps(log_entry, indent=2)}')
# Send to monitoring service in production
# send_to_monitoring(log_entry)
3. Monitor Failure Rates​
Track and alert on error rates:
class FailureMonitor
def initialize
@failures = Hash.new(0)
@total_attempts = 0
end
def record_attempt
@total_attempts += 1
end
def record_failure(failure_type)
@failures[failure_type] += 1
end
def failure_rate(failure_type = nil)
return 0 if @total_attempts.zero?
if failure_type
(@failures[failure_type].to_f / @total_attempts * 100).round(2)
else
(@failures.values.sum.to_f / @total_attempts * 100).round(2)
end
end
def report
puts "Total Attempts: #{@total_attempts}"
puts "Total Failures: #{@failures.values.sum}"
puts "Overall Failure Rate: #{failure_rate}%"
puts "\nBreakdown by type:"
@failures.each do |type, count|
puts " #{type}: #{count} (#{failure_rate(type)}%)"
end
end
end
4. Test Idempotency​
Ensure duplicate requests don't cause issues:
async function testIdempotency() {
const idempotencyKey = `test_${Date.now()}`;
// Make the same request twice
const [charge1, charge2] = await Promise.all([
Omise.charges.create(
{
amount: 100000,
currency: 'THB',
card: token.id
},
{ 'Idempotency-Key': idempotencyKey }
),
Omise.charges.create(
{
amount: 100000,
currency: 'THB',
card: token.id
},
{ 'Idempotency-Key': idempotencyKey }
)
]);
// Both should return the same charge
console.assert(charge1.id === charge2.id, 'Idempotency failed!');
}
5. Test Concurrent Failures​
Test how your system handles multiple simultaneous failures:
import asyncio
import omise
omise.api_secret = 'skey_test_xxxxxxxxxx'
async def test_concurrent_failures():
"""Test multiple failures happening simultaneously"""
failure_cards = [
'4000000000000101', # Insufficient funds
'4000000000000119', # Processing error
'4000000000000127', # Lost card
'4000000000000135', # Stolen card
'4000000000000192', # Fraud check
]
tasks = []
for card_number in failure_cards:
task = create_and_test_failure(card_number)
tasks.append(task)
results = await asyncio.gather(*tasks, return_exceptions=True)
# Analyze results
for i, result in enumerate(results):
if isinstance(result, Exception):
print(f'Card {i}: Unexpected error - {result}')
else:
print(f'Card {i}: {result["failure_code"]}')
async def create_and_test_failure(card_number):
"""Create token and charge, expect failure"""
token = omise.Token.create(
card={
'name': 'Test User',
'number': card_number,
'expiration_month': 12,
'expiration_year': 2026,
'security_code': '123'
}
)
charge = omise.Charge.create(
amount=100000,
currency='THB',
card=token.id
)
return {
'status': charge.status,
'failure_code': charge.failure_code
}
Troubleshooting​
Common Issues​
Issue: Retries causing duplicate charges​
Cause: Not using idempotency keys
Solution: Always use idempotency keys for payment requests:
const charge = await Omise.charges.create(
{
amount: 100000,
currency: 'THB',
card: token.id
},
{ 'Idempotency-Key': uniqueKey }
);
Issue: Webhook endpoint timing out​
Cause: Processing webhooks synchronously
Solution: Process webhooks asynchronously:
@app.route('/webhooks', methods=['POST'])
def handle_webhook():
# Verify and queue immediately
event = request.get_json()
queue_for_processing(event)
# Return 200 quickly
return jsonify({'received': True}), 200
def queue_for_processing(event):
# Add to queue for async processing
event_queue.put(event)
Issue: Tests passing but production failing​
Cause: Not testing with realistic data and scenarios
Solution: Use production-like test data and scenarios:
// Test with realistic amounts
const testAmounts = [2000, 5000, 10000, 50000, 100000, 500000];
// Test with realistic names
const testNames = [
'John Smith',
'MarÃa GarcÃa',
'สมชาย ใจดี',
'Tân Nguyễn'
];
Frequently Asked Questions​
How do I test specific decline reasons?​
Use the test card numbers listed in the Declined Card Test Numbers section. Each card triggers a specific decline reason.
Should I retry failed payments automatically?​
Only retry on transient errors (timeouts, connection failures, 5xx errors). Never retry on permanent failures (invalid card, insufficient funds, fraud detection).
How long should I wait between retries?​
Use exponential backoff starting at 1 second, doubling each retry, with a maximum of 30 seconds. Add jitter to prevent thundering herd problems.
How do I handle webhook delivery failures?​
Omise automatically retries failed webhooks. Ensure your endpoint:
- Responds within 30 seconds
- Returns 200 status code
- Processes events asynchronously
- Handles duplicate events (idempotency)
What's the difference between retryable and non-retryable errors?​
Retryable: Network errors, timeouts, 5xx server errors, rate limits Non-retryable: Invalid request data, authentication errors, card declines, fraud detection
How do I test 3D Secure failures?​
Use the test card 4000000000000010 which fails 3DS authentication, or 4000000000000028 which is rejected by the issuer.
Should I log failed payment attempts?​
Yes, log all failures with sufficient context (amount, currency, failure reason, timestamp) for debugging and fraud detection. Never log full card numbers or CVV codes.
How do I test webhook signature verification?​
Generate a valid HMAC-SHA256 signature using your webhook secret key and the payload. Test both valid and invalid signatures to ensure your verification works correctly.
Can I test rate limiting in test mode?​
Yes, make rapid successive API calls to trigger rate limiting. Omise uses the same rate limits in test and production modes.
How often should I run failure tests?​
Run failure tests:
- On every deployment (CI/CD pipeline)
- Weekly as part of regression testing
- After any payment-related code changes
- When integrating new payment methods
Related Resources​
- Test Cards & Data - Complete test card numbers and payment data
- Testing Webhooks - Webhook testing strategies and tools
- Error Codes Reference - Complete list of error codes
- API Authentication - Understanding API keys and authentication
- 3D Secure Guide - Understanding 3DS authentication
- Webhooks Guide - Implementing webhook handlers
- Best Practices - Payment integration best practices
Next Steps​
- Implement error handling for all failure scenarios
- Set up comprehensive logging for debugging
- Test retry logic with exponential backoff
- Implement webhook verification and failure handling
- Test all decline scenarios with test cards
- Monitor failure rates in production
- Set up alerts for unusual error patterns
- Document error handling for your team
Ready to test webhooks? Check out Testing Webhooks.
Need test card data? See Test Cards & Data.