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-BereichBedeutungAktion
2xxErfolgAnfrage erfolgreich verarbeitet
4xxClient-FehlerProblem mit der Anfrage - Client muss korrigieren
5xxServer-FehlerProblem 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öglich
  • X-RateLimit-Limit: Maximum erlaubte Anfragen
  • X-RateLimit-Remaining: Verbleibende Anfragen
  • X-RateLimit-Reset: Zeitstempel der Limit-Zurücksetzung

Vollständige Fehlercode-Referenz

400Bad Request

Die Anfrage ist ungültig oder enthält fehlerhafte Parameter.

FehlerBeschreibungLösung
ValidierungsfehlerPflichtfelder fehlen oder ungültigPrüfen Sie die details für spezifische Feldvalidierung
Ungültiger Partner-CodePartner-Code existiert nicht oder ist inaktivVerwenden Sie einen gültigen, aktiven Partner-Code
E-Mail bereits vergebenE-Mail-Adresse wird bereits verwendetVerwenden Sie eine andere E-Mail-Adresse oder nutzen Sie Login
E-Mail-Adresse nicht zulässigTemporäre/Wegwerf-E-Mail nicht erlaubtVerwenden Sie eine permanente geschäftliche E-Mail-Adresse
invalid_requestOAuth: Ungültiges Request-FormatPrüfen Sie JSON-Struktur und erforderliche Felder

401Unauthorized

Authentifizierung ist erforderlich oder fehlgeschlagen.

FehlerBeschreibungLösung
UnauthorizedKein Authorization-Header vorhandenFügen Sie Authorization: Bearer TOKEN Header hinzu
invalid_clientOAuth: Client ID oder Secret ungültigPrüfen Sie Ihre API-Zugangsdaten
invalid_tokenAccess Token abgelaufen oder ungültigFordern Sie einen neuen Access Token an
INVALID_API_KEYGoogle API-Schlüssel ungültigKontaktieren Sie den Administrator - Konfigurationsproblem

Token-Ablauf

Access Tokens sind 1 Stunde gültig. Implementieren Sie Token-Refresh-Logik, um bei 401-Fehlern automatisch einen neuen Token anzufordern.

403Forbidden

Authentifizierung erfolgreich, aber fehlende Berechtigung für die Ressource.

FehlerBeschreibungLösung
Forbidden - Admin access requiredAdmin-Rechte erforderlichNur Administratoren können diese Operation ausführen
insufficient_scopeOAuth: Fehlender ScopeFordern Sie Token mit erforderlichen Scopes an

404Not Found

Die angeforderte Ressource existiert nicht.

FehlerBeschreibungLösung
User not foundBenutzer mit angegebener ID existiert nichtPrüfen Sie die Benutzer-ID
Batch not foundBatch-Verarbeitung existiert nichtPrü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.

FehlerLimitZeitfensterSperre
Registrierung10 Versuche1 Stunde1 Stunde (exponentiell)
Login5 Versuche15 Minuten30 Minuten (exponentiell)
Partner-Code Validierung5 Versuche1 Minute1 Minute
OAuth Token10 Versuche1 Minute5 Minuten
RATE_LIMIT_EXCEEDEDGoogle API LimitVariabel60 Sekunden (empfohlen)

Exponentielles Backoff

Bei wiederholten Verstößen gegen Rate Limits verdoppelt sich die Sperrdauer exponentiell: 1h → 2h → 4h → 8h (maximal 24 Stunden).

500Internal Server Error

Ein unerwarteter Serverfehler ist aufgetreten.

FehlerBeschreibungAktion
Fehler bei der VerarbeitungAllgemeiner ServerfehlerWiederholen Sie die Anfrage mit exponentiellem Backoff
INTERNAL_ERRORGoogle API interner FehlerAutomatische Wiederholung nach 5 Sekunden

503Service Unavailable

Der Service ist vorübergehend nicht verfügbar (Wartung oder Überlastung).

FehlerBeschreibungAktion
Service UnavailableService vorübergehend nicht erreichbarWiederholen nach Retry-After Header
QUOTA_EXCEEDEDGoogle API Quota erschöpftKontaktieren 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 CodeWiederholbar?Strategie
408✓ JaRequest Timeout - Exponentielles Backoff
429✓ JaRate Limit - Nach Retry-After
500✓ JaServer Error - Exponentielles Backoff (max 3x)
502, 503, 504✓ JaGateway/Service Errors - Exponentielles Backoff
400, 401, 403, 404✗ NeinClient 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

Verwenden Sie Jitter (Zufallskomponente) im Backoff, um zu verhindern, dass viele Clients gleichzeitig retries durchführen und den Server überlasten.

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 retryAfter Feld
  • ✓ Prüfen Sie X-RateLimit-Reset Header
  • ✓ 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

Verwenden Sie eine separate Test-Umgebung oder Sandbox für Integration-Tests, um Produktions-Rate-Limits nicht zu beeinträchtigen.

Support kontaktieren

Bei anhaltenden Problemen oder Fragen zur Fehlerbehandlung:

API Support

Technische Fragen zur API und Fehlerbehandlung

api@steuermappe-pro.de

Security Issues

Sicherheitsprobleme und kompromittierte Credentials

security@steuermappe-pro.de