Zpět na domů

Zpracování webhooků v Pythonu: FastAPI + Redis

Článek popisuje přechod od synchronního zpracování webhooků k asynchronní architektuře s FastAPI a Redis. Uvádí se kód API-přijímače a workeru, diskuse o idempotenci, bezpečnosti a typických chybách. Vhodné pro middle/senior vývojáře.

FastAPI + Redis: spolehlivé webhooky bez ztrát
Advertisement 728x90

Spolehlivá zpracování webhooků v FastAPI s Redis frontou

Webhooky od platebních služeb vyžadují rychlou odezvu – obvykle ne více než 5 sekund. Synchronní zpracování na koncovém bodu vede k časovým limitům při zpožděních v databázi nebo externích službách. Příklad počáteční implementace:

@app.post("/webhook")
async def webhook(request: dict):
    update_database(request)
    send_email_to_user(request)
    return {"status": "ok"}

Tento přístup je zranitelný: selhání serveru během zpracování způsobí ztrátu dat, duplicitní oznámení způsobí chyby jedinečnosti v DB. Řešením je oddělení přijetí a zpracování prostřednictvím fronty úloh.

Architektura s frontou úloh

Koncový bod API přijme webhook, ověří podpis, zajistí idempotence a uloží úlohu do Redis fronty. Samostatný worker asynchronně provádí obchodní logiku. Výhody:

Google AdInline article slot
  • Okamžitá HTTP odpověď (50 ms).
  • Uložení úloh při pádu workera.
  • Horizontální škálování (více workerů).

Komponenty:

  • FastAPI pro API.
  • Redis jako fronta (LPUSH/BRPOP).
  • Workers v samostatných procesech.

Implementace přijímače API

Koncový bod se zaměřuje na bezpečnost a rychlost. Ověřuje HMAC podpis, vylučuje duplikáty podle transaction_id a přidává úlohu do fronty.

from fastapi import FastAPI, HTTPException, Header
from pydantic import BaseModel
import redis
import json
import hashlib
import os

app = FastAPI()
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
SECRET = os.getenv('WEBHOOK_SECRET', 'test_secret')

def check_signature(body: str, sign: str) -> bool:
    my_sign = hashlib.sha256(f"{body}{SECRET}".encode()).hexdigest()
    return my_sign == sign

@app.post("/api/payment")
async def payment_webhook(request: dict, x_sign: str = Header(None)):
    if not check_signature(json.dumps(request), x_sign):
        raise HTTPException(status_code=403, detail="Bad sign")
    
    task_id = request.get('transaction_id')
    if r.exists(f"processed:{task_id}"):
        return {"status": "duplicate"}
    
    task = {
        "id": task_id,
        "amount": request.get('amount'),
        "user_id": request.get('user_id')
    }
    r.lpush("payment_queue", json.dumps(task))
    
    return {"status": "accepted"}

Klíčové praktiky:

Google AdInline article slot
  • HMAC podpis zabrání podvržení požadavků.
  • Redis klíč processed:{id} zajišťuje idempotenci.
  • LPUSH pro přidání do fronty.

Worker pro pozadní zpracování

Worker běží v cyklu, extrahuje úlohy blokujícím BRPOP (timeout 5s), provádí logiku, označuje jako zpracované (TTL 1 den). Při chybě se vrátí do fronty s prodlevou.

import redis
import json
import time
import logging

logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)

def process_task(data):
    log.info(f"Zpracovávám platbu {data['id']}")
    time.sleep(1) 
    return True

def main():
    r = redis.Redis(host='localhost', port=6379)
    log.info("Worker spuštěn...")
    
    while True:
        task = r.brpop("payment_queue", timeout=5)
        if not task:
            continue
        _, raw_data = task
        data = json.loads(raw_data)
        
        try:
            success = process_task(data)
            if success:
                r.setex(f"processed:{data['id']}", 86400, "1")
                log.info(f"Úloha {data['id']} dokončena")
        except Exception as e:
            log.error(f"Chyba: {e}")
            r.lpush("payment_queue", raw_data)
            time.sleep(5)

if __name__ == "__main__":
    main()

Běžné problémy a jejich řešení

  • Chybějící ověření podpisu: Zranitelnost proti falešným POST požadavkům. Oprava – HMAC s tajným klíčem.
  • Přechodnost Redis: Data v paměti se ztrácejí při restartu. Řešení – RDB/AOF persistence nebo PostgreSQL jako fronta.
  • Cyklus chyb ve workerovi: Úloha se neustále vrací. Oprava – počítadlo pokusů, dead letter queue, prodlevy.

Další rizika:

  • Časové limity externích služeb: použít timeouts ve funkci process_task.
  • Škálování: sledovat délku fronty (LLEN), přidávat workerů.

Co je důležité

  • Idempotence: Klíče processed:{id} zabrání duplikátům.
  • Bezpečnost: Vždy ověřujte HMAC podpis webhooků.
  • Spolehlivost: Fronty uchovávají úlohy při selhání.
  • Škálovatelnost: Samostatné workerů umožňují paralelní zpracování.
  • Monitorování: Logujte metriky fronty a chyb.

— Editorial Team

Google AdInline article slot
Advertisement 728x90

Číst dál