Fehlerbehandlung
Verstehen und beheben Sie API-Fehler effektiv mit diesem umfassenden Leitfaden
Übersicht
Die SteuerMappePro API verwendet standardisierte HTTP-Statuscodes und strukturierte JSON-Fehlerantworten. Alle Fehler enthalten aussagekräftige Fehlermeldungen und optional zusätzliche Details zur Fehlerbehebung.
Standardisiert
HTTP-Statuscodes gemäß RFC 7231 für einheitliche Fehlerbehandlung
Detailliert
Strukturierte JSON-Antworten mit Fehlercode und Beschreibung
HTTP-Statuscodes Übersicht
Die API verwendet folgende HTTP-Statuscode-Kategorien:
| Code-Bereich | Bedeutung | Aktion |
|---|---|---|
| 2xx | Erfolg | Anfrage erfolgreich verarbeitet |
| 4xx | Client-Fehler | Problem mit der Anfrage - Client muss korrigieren |
| 5xx | Server-Fehler | Problem auf Server-Seite - Wiederholung möglich |
Fehler-Antwortformat
Alle Fehlerantworten folgen einem konsistenten JSON-Format:
Basis-Format
{
"error": "Kurze Fehlerbeschreibung",
"details": "Optional: Detaillierte Informationen",
"field": "Optional: Betroffenes Feld bei Validierungsfehlern"
}Validierungsfehler
Bei mehreren Validierungsfehlern wird ein Array mit Details zurückgegeben:
{
"error": "Validierungsfehler",
"details": [
{
"field": "email",
"message": "Ungültige E-Mail-Adresse"
},
{
"field": "organization",
"message": "Organisation ist erforderlich"
}
]
}Rate Limit Fehler
Rate Limit Fehler enthalten zusätzliche Header und Retry-Informationen:
{
"error": "Zu viele Anfragen. Bitte versuchen Sie es später erneut.",
"retryAfter": 3600
}Rate Limit Headers
Retry-After: Sekunden bis Wiederholung möglichX-RateLimit-Limit: Maximum erlaubte AnfragenX-RateLimit-Remaining: Verbleibende AnfragenX-RateLimit-Reset: Zeitstempel der Limit-Zurücksetzung
Vollständige Fehlercode-Referenz
400Bad Request
Die Anfrage ist ungültig oder enthält fehlerhafte Parameter.
| Fehler | Beschreibung | Lösung |
|---|---|---|
| Validierungsfehler | Pflichtfelder fehlen oder ungültig | Prüfen Sie die details für spezifische Feldvalidierung |
| Ungültiger Partner-Code | Partner-Code existiert nicht oder ist inaktiv | Verwenden Sie einen gültigen, aktiven Partner-Code |
| E-Mail bereits vergeben | E-Mail-Adresse wird bereits verwendet | Verwenden Sie eine andere E-Mail-Adresse oder nutzen Sie Login |
| E-Mail-Adresse nicht zulässig | Temporäre/Wegwerf-E-Mail nicht erlaubt | Verwenden Sie eine permanente geschäftliche E-Mail-Adresse |
| invalid_request | OAuth: Ungültiges Request-Format | Prüfen Sie JSON-Struktur und erforderliche Felder |
401Unauthorized
Authentifizierung ist erforderlich oder fehlgeschlagen.
| Fehler | Beschreibung | Lösung |
|---|---|---|
| Unauthorized | Kein Authorization-Header vorhanden | Fügen Sie Authorization: Bearer TOKEN Header hinzu |
| invalid_client | OAuth: Client ID oder Secret ungültig | Prüfen Sie Ihre API-Zugangsdaten |
| invalid_token | Access Token abgelaufen oder ungültig | Fordern Sie einen neuen Access Token an |
| INVALID_API_KEY | Google API-Schlüssel ungültig | Kontaktieren Sie den Administrator - Konfigurationsproblem |
Token-Ablauf
403Forbidden
Authentifizierung erfolgreich, aber fehlende Berechtigung für die Ressource.
| Fehler | Beschreibung | Lösung |
|---|---|---|
| Forbidden - Admin access required | Admin-Rechte erforderlich | Nur Administratoren können diese Operation ausführen |
| insufficient_scope | OAuth: Fehlender Scope | Fordern Sie Token mit erforderlichen Scopes an |
404Not Found
Die angeforderte Ressource existiert nicht.
| Fehler | Beschreibung | Lösung |
|---|---|---|
| User not found | Benutzer mit angegebener ID existiert nicht | Prüfen Sie die Benutzer-ID |
| Batch not found | Batch-Verarbeitung existiert nicht | Prüfen Sie die Batch-ID |
422Unprocessable Entity
Die Anfrage ist syntaktisch korrekt, kann aber nicht verarbeitet werden.
Beispiel: Validierung fehlgeschlagen
{
"error": "Validierungsfehler",
"details": [
{
"field": "vatId",
"message": "USt-IdNr. muss mit Ländercode beginnen (z.B. DE123456789)"
}
]
}429Too Many Requests
Rate Limit überschritten. Die Anfrage wurde abgelehnt, um Missbrauch zu verhindern.
| Fehler | Limit | Zeitfenster | Sperre |
|---|---|---|---|
| Registrierung | 10 Versuche | 1 Stunde | 1 Stunde (exponentiell) |
| Login | 5 Versuche | 15 Minuten | 30 Minuten (exponentiell) |
| Partner-Code Validierung | 5 Versuche | 1 Minute | 1 Minute |
| OAuth Token | 10 Versuche | 1 Minute | 5 Minuten |
| RATE_LIMIT_EXCEEDED | Google API Limit | Variabel | 60 Sekunden (empfohlen) |
Exponentielles Backoff
500Internal Server Error
Ein unerwarteter Serverfehler ist aufgetreten.
| Fehler | Beschreibung | Aktion |
|---|---|---|
| Fehler bei der Verarbeitung | Allgemeiner Serverfehler | Wiederholen Sie die Anfrage mit exponentiellem Backoff |
| INTERNAL_ERROR | Google API interner Fehler | Automatische Wiederholung nach 5 Sekunden |
503Service Unavailable
Der Service ist vorübergehend nicht verfügbar (Wartung oder Überlastung).
| Fehler | Beschreibung | Aktion |
|---|---|---|
| Service Unavailable | Service vorübergehend nicht erreichbar | Wiederholen nach Retry-After Header |
| QUOTA_EXCEEDED | Google API Quota erschöpft | Kontaktieren Sie den Administrator |
Retry-Strategien und Exponentielles Backoff
Implementieren Sie robuste Retry-Logik für transiente Fehler (Netzwerkprobleme, Server-Überlastung).
Welche Fehler sollten wiederholt werden?
| Status Code | Wiederholbar? | Strategie |
|---|---|---|
| 408 | ✓ Ja | Request Timeout - Exponentielles Backoff |
| 429 | ✓ Ja | Rate Limit - Nach Retry-After |
| 500 | ✓ Ja | Server Error - Exponentielles Backoff (max 3x) |
| 502, 503, 504 | ✓ Ja | Gateway/Service Errors - Exponentielles Backoff |
| 400, 401, 403, 404 | ✗ Nein | Client Error - Wiederholung sinnlos |
TypeScript Retry-Implementierung
Einfache Retry-Logik
interface RetryConfig {
maxRetries: number;
initialDelayMs: number;
maxDelayMs: number;
backoffMultiplier: number;
}
const DEFAULT_RETRY_CONFIG: RetryConfig = {
maxRetries: 3,
initialDelayMs: 1000, // 1 Sekunde
maxDelayMs: 30000, // 30 Sekunden
backoffMultiplier: 2 // Verdopplung
};
async function fetchWithRetry<T>(
url: string,
options: RequestInit,
config: RetryConfig = DEFAULT_RETRY_CONFIG
): Promise<T> {
let lastError: Error;
let delay = config.initialDelayMs;
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
try {
const response = await fetch(url, options);
// Erfolg
if (response.ok) {
return await response.json();
}
// Nicht wiederholbare Fehler
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
const error = await response.json();
throw new Error(`Client error (${response.status}): ${error.error}`);
}
// Rate Limit - nutze Retry-After Header
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : delay;
if (attempt < config.maxRetries) {
console.log(`Rate limited. Retrying after ${waitTime}ms...`);
await sleep(waitTime);
continue;
}
}
// Server-Fehler (5xx) - wiederholbar
if (response.status >= 500) {
throw new Error(`Server error: ${response.status}`);
}
} catch (error) {
lastError = error as Error;
// Letzter Versuch - Fehler werfen
if (attempt === config.maxRetries) {
throw lastError;
}
// Exponentielles Backoff
console.log(`Attempt ${attempt + 1} failed. Retrying in ${delay}ms...`);
await sleep(delay);
delay = Math.min(delay * config.backoffMultiplier, config.maxDelayMs);
}
}
throw lastError!;
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}Verwendungsbeispiel
// Batch erstellen mit automatischen Retries
try {
const batch = await fetchWithRetry(
'https://api.steuermappe-pro.de/v1/batches',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
batch_name: 'Steuer 2024',
mandant: { /* ... */ },
documents: [ /* ... */ ]
})
},
{
maxRetries: 3,
initialDelayMs: 1000,
maxDelayMs: 30000,
backoffMultiplier: 2
}
);
console.log('Batch erstellt:', batch.batch_id);
} catch (error) {
console.error('Fehler nach allen Versuchen:', error);
// Fehlerbehandlung für finale Fehler
}Erweiterte Retry-Logik mit Jitter
Jitter verhindert "Thundering Herd" Probleme bei vielen gleichzeitigen Clients:
function calculateBackoffWithJitter(
baseDelay: number,
attempt: number,
maxDelay: number
): number {
// Exponentielles Backoff
const exponentialDelay = baseDelay * Math.pow(2, attempt);
// Full Jitter: Zufälliger Wert zwischen 0 und exponentialDelay
const delay = Math.random() * exponentialDelay;
// Maximal-Grenze
return Math.min(delay, maxDelay);
}
// Verwendung
const delay = calculateBackoffWithJitter(1000, attempt, 30000);
await sleep(delay);Best Practice
Error Handling Best Practices
1. Token-Verwaltung
class SteuerMappeClient {
private accessToken: string | null = null;
private tokenExpiresAt: number = 0;
async getValidToken(): Promise<string> {
const now = Date.now();
// Token noch gültig? (mit 5 Min Puffer)
if (this.accessToken && this.tokenExpiresAt > now + 5 * 60 * 1000) {
return this.accessToken;
}
// Neuen Token anfordern
const tokenResponse = await fetch(
'https://api.steuermappe-pro.de/v1/oauth/token',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
scope: 'batches:write batches:read results:read'
})
}
);
if (!tokenResponse.ok) {
throw new Error('Token request failed');
}
const data = await tokenResponse.json();
this.accessToken = data.access_token;
this.tokenExpiresAt = now + data.expires_in * 1000;
return this.accessToken;
}
async makeAuthenticatedRequest(url: string, options: RequestInit) {
const token = await this.getValidToken();
const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`
}
});
// Token abgelaufen während Request?
if (response.status === 401) {
// Token zurücksetzen und nochmal versuchen
this.accessToken = null;
this.tokenExpiresAt = 0;
const newToken = await this.getValidToken();
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${newToken}`
}
});
}
return response;
}
}2. Strukturierte Fehlerbehandlung
// Custom Error-Klassen
class ApiError extends Error {
constructor(
message: string,
public statusCode: number,
public errorCode?: string,
public details?: any
) {
super(message);
this.name = 'ApiError';
}
}
class RateLimitError extends ApiError {
constructor(
message: string,
public retryAfter: number
) {
super(message, 429, 'RATE_LIMIT_EXCEEDED');
this.name = 'RateLimitError';
}
}
class AuthenticationError extends ApiError {
constructor(message: string, errorCode: string) {
super(message, 401, errorCode);
this.name = 'AuthenticationError';
}
}
// Error-Parsing
async function parseApiError(response: Response): Promise<never> {
const body = await response.json().catch(() => ({}));
switch (response.status) {
case 401:
throw new AuthenticationError(
body.error || 'Authentication failed',
body.error_code || 'UNAUTHORIZED'
);
case 429:
const retryAfter = response.headers.get('Retry-After');
throw new RateLimitError(
body.error || 'Rate limit exceeded',
retryAfter ? parseInt(retryAfter) : 3600
);
case 400:
case 422:
throw new ApiError(
body.error || 'Validation failed',
response.status,
'VALIDATION_ERROR',
body.details
);
default:
throw new ApiError(
body.error || 'Unknown error',
response.status
);
}
}
// Verwendung
try {
const response = await fetch(/* ... */);
if (!response.ok) {
await parseApiError(response);
}
return await response.json();
} catch (error) {
if (error instanceof RateLimitError) {
console.log(`Rate limited. Retry after ${error.retryAfter}s`);
// Spezifische Behandlung
} else if (error instanceof AuthenticationError) {
console.log('Authentication failed. Refreshing token...');
// Token refresh
} else if (error instanceof ApiError) {
console.log(`API Error: ${error.message} (Status: ${error.statusCode})`);
} else {
console.error('Unexpected error:', error);
}
}3. Circuit Breaker Pattern
Verhindert wiederholte Anfragen an einen fehlgeschlagenen Service:
class CircuitBreaker {
private failureCount = 0;
private lastFailureTime = 0;
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
constructor(
private threshold: number = 5, // Fehler bis Circuit öffnet
private timeout: number = 60000, // Zeit bis Circuit wieder schließt (60s)
private resetTimeout: number = 30000 // Zeit im HALF_OPEN Zustand
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'OPEN') {
const now = Date.now();
// Genug Zeit vergangen?
if (now - this.lastFailureTime >= this.timeout) {
console.log('Circuit breaker: HALF_OPEN');
this.state = 'HALF_OPEN';
} else {
throw new Error(`Circuit breaker OPEN. Retry after ${
Math.ceil((this.timeout - (now - this.lastFailureTime)) / 1000)
}s`);
}
}
try {
const result = await fn();
// Erfolg - Circuit schließen
if (this.state === 'HALF_OPEN') {
console.log('Circuit breaker: CLOSED');
this.state = 'CLOSED';
this.failureCount = 0;
}
return result;
} catch (error) {
this.failureCount++;
this.lastFailureTime = Date.now();
// Threshold erreicht?
if (this.failureCount >= this.threshold) {
console.log('Circuit breaker: OPEN');
this.state = 'OPEN';
}
throw error;
}
}
}
// Verwendung
const breaker = new CircuitBreaker(5, 60000);
try {
const result = await breaker.execute(() =>
fetch('https://api.steuermappe-pro.de/v1/batches')
);
} catch (error) {
console.error('Request failed or circuit open:', error);
}4. Logging und Monitoring
interface ErrorLogEntry {
timestamp: string;
method: string;
url: string;
statusCode: number;
errorCode?: string;
message: string;
requestId?: string;
duration: number;
}
class ApiLogger {
async logRequest(
method: string,
url: string,
startTime: number,
response: Response,
error?: Error
) {
const entry: ErrorLogEntry = {
timestamp: new Date().toISOString(),
method,
url,
statusCode: response.status,
message: error?.message || response.statusText,
requestId: response.headers.get('X-Request-ID') || undefined,
duration: Date.now() - startTime
};
// In Produktionsumgebung: an Logging-Service senden
if (response.status >= 400) {
console.error('API Error:', JSON.stringify(entry, null, 2));
// Optional: An Error-Tracking-Service senden
// await sendToSentry(entry);
} else {
console.log('API Request:', JSON.stringify(entry, null, 2));
}
}
}
// Verwendung
const logger = new ApiLogger();
const startTime = Date.now();
try {
const response = await fetch(url, options);
await logger.logRequest(method, url, startTime, response);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return await response.json();
} catch (error) {
await logger.logRequest(method, url, startTime, response, error as Error);
throw error;
}Häufige Fehler und Fehlerbehebung
"invalid_client" beim Token-Request
Ursache: Client ID oder Secret ist falsch oder veraltet.
- ✓ Prüfen Sie, dass Client ID und Secret korrekt kopiert wurden
- ✓ Achten Sie auf Leerzeichen am Anfang/Ende
- ✓ Verwenden Sie Umgebungsvariablen statt Hardcoding
- ✓ Kontaktieren Sie Administrator für neue Credentials
"Rate limit exceeded" bei Registrierung
Ursache: Zu viele Registrierungsversuche von derselben IP.
- ✓ Warten Sie die angegebene Zeit im
retryAfterFeld - ✓ Prüfen Sie
X-RateLimit-ResetHeader - ✓ Implementieren Sie Client-seitiges Rate Limiting
- ✓ Vermeiden Sie automatisierte Massenregistrierungen
"E-Mail-Adresse nicht zulässig"
Ursache: Disposable/Temporary E-Mail-Adresse verwendet.
- ✓ Verwenden Sie eine permanente geschäftliche E-Mail
- ✓ Vermeiden Sie Dienste wie Mailinator, 10minutemail, etc.
- ✓ Akzeptierte Domains: Unternehmens-E-Mails, Gmail, Outlook, etc.
"insufficient_scope" bei API-Anfrage
Ursache: Access Token hat nicht die erforderlichen Berechtigungen.
- ✓ Prüfen Sie die erforderlichen Scopes für den Endpoint
- ✓ Fordern Sie einen neuen Token mit allen benötigten Scopes an
- ✓ Beispiel: Batch-Erstellung benötigt
batches:write - ✓ Kontaktieren Sie Administrator für Scope-Erweiterung
"QUOTA_EXCEEDED" bei Google API
Ursache: Google Cloud API-Quota erschöpft.
- ✓ Dieser Fehler ist nicht wiederholbar
- ✓ Kontaktieren Sie den Administrator
- ✓ Administrator muss Quota in Google Cloud Console erhöhen
- ✓ Batch-Verarbeitungen werden pausiert bis Quota verfügbar
Error Handling testen
Testen Sie Ihre Fehlerbehandlung mit diesen Strategien:
1. Mock-Responses verwenden
// Test für Rate Limit Handling
describe('Rate Limit Error Handling', () => {
it('should retry after rate limit with exponential backoff', async () => {
// Mock erste Anfrage mit 429
fetchMock.mockResponseOnce(
JSON.stringify({ error: 'Rate limit exceeded', retryAfter: 2 }),
{
status: 429,
headers: { 'Retry-After': '2' }
}
);
// Mock zweite Anfrage erfolgreich
fetchMock.mockResponseOnce(
JSON.stringify({ batch_id: 'b_123' }),
{ status: 202 }
);
const result = await fetchWithRetry(/* ... */);
expect(result.batch_id).toBe('b_123');
expect(fetchMock).toHaveBeenCalledTimes(2);
});
it('should throw after max retries', async () => {
// Mock alle Anfragen mit 500
fetchMock.mockResponse(
JSON.stringify({ error: 'Internal server error' }),
{ status: 500 }
);
await expect(fetchWithRetry(/* ... */, { maxRetries: 3 }))
.rejects.toThrow('Server error: 500');
expect(fetchMock).toHaveBeenCalledTimes(4); // Initial + 3 retries
});
});2. Integration-Tests
// Integration Test mit echten API-Aufrufen
describe('API Error Handling Integration', () => {
it('should handle invalid token and refresh', async () => {
const client = new SteuerMappeClient({
clientId: 'test_client',
clientSecret: 'test_secret'
});
// Erster Request mit abgelaufenem Token
const response = await client.getBatch('b_123');
// Client sollte automatisch Token refreshen
expect(response.batch_id).toBe('b_123');
});
it('should respect rate limits', async () => {
const requests = Array.from({ length: 15 }, (_, i) =>
api.validatePartnerCode('ABC123')
);
const results = await Promise.allSettled(requests);
const rateLimited = results.filter(r =>
r.status === 'rejected' &&
r.reason.statusCode === 429
);
// Erwarte dass einige Rate Limited wurden
expect(rateLimited.length).toBeGreaterThan(0);
});
});Test-Umgebung
Support kontaktieren
Bei anhaltenden Problemen oder Fragen zur Fehlerbehandlung: