Siirry dokumentaation sisältöön
HintadataDocs

Python-webhook-vastaanotin

Tämä yhden tilauksen vastaanotin tarkistaa täsmälleen vastaanotetut tavut SDK:lla, hylkää virheellisen tai vanhentuneen allekirjoituksen ja tallentaa tapahtumatunnisteen sekä yhden paikallisen ilmoitusrivin atomisesti SQLiteen. Duplikaatti kuitataan ilman uutta ilmoitusta. webhook.test tarkistetaan ja kirjataan, mutta se ei luo ilmoitusta. Tuntemattomat tapahtumatyypit kuitataan ilman liiketoimintamuutosta. Esimerkki ei lähetä sähköpostia eikä muuta ulkoista järjestelmää.

Tässä oppaassa
  1. 1Rakenna pysyvä paikallinen saapumisjono
  2. 2Asenna ja määritä
  3. 3Tallenna vastaanotin
  4. 4Varmenna ennen julkista käyttöönottoa
  5. 5Sijoita HTTPS-palvelusi taakse
  6. 6Laajenna säilyttäen duplikaattisuojaus
  7. 7Hyväksy kaksi avainta vain vaihdon aikana
Tässä oppaassa
  1. 1Rakenna pysyvä paikallinen saapumisjono
  2. 2Asenna ja määritä
  3. 3Tallenna vastaanotin
  4. 4Varmenna ennen julkista käyttöönottoa
  5. 5Sijoita HTTPS-palvelusi taakse
  6. 6Laajenna säilyttäen duplikaattisuojaus
  7. 7Hyväksy kaksi avainta vain vaihdon aikana

Rakenna pysyvä paikallinen saapumisjono

Tämä yhden tilauksen vastaanotin tarkistaa täsmälleen vastaanotetut tavut SDK:lla, hylkää virheellisen tai vanhentuneen allekirjoituksen ja tallentaa tapahtumatunnisteen sekä yhden paikallisen ilmoitusrivin atomisesti SQLiteen. Duplikaatti kuitataan ilman uutta ilmoitusta. webhook.test tarkistetaan ja kirjataan, mutta se ei luo ilmoitusta. Tuntemattomat tapahtumatyypit kuitataan ilman liiketoimintamuutosta. Esimerkki ei lähetä sähköpostia eikä muuta ulkoista järjestelmää.

Asenna ja määritä

Käytä erillistä tyhjää hakemistoa. Node.js 22.13+ sisältää node:sqlite-moduulin ja Python 3.11+ sqlite3-moduulin. Aseta WEBHOOK_SECRET yksityisesti tilauksen luonnissa palautetuksi allekirjoitussalaisuudeksi, ei API-avaimeksi. WEBHOOK_DB osoittaa pysyvään kirjoitettavaan paikalliseen tiedostoon. Tietokanta ja salaisuudet kuuluvat yhdelle tilaukselle; älä sekoita asiakkaiden työtiloja. Käytä paikallisessa varmennuksessa kertakäyttöistä tietokantaa ja vähintään 32-merkkistä vain testiä varten luotua salaisuutta.

bashillustrative
Esimerkin luotettavuus: illustrativeNäyttää muodon tai idean; älä käytä sellaisenaan tuotannossa.
uv venv
uv pip install https://hintadata.fi/sdk/price_compare-3.1.0.tar.gz

Tallenna vastaanotin

Tallenna nimellä receiver.py. Vastaanotin kuuntelee vain paikallista loopback-osoitetta portissa 8080.

pythonillustrative
Esimerkin luotettavuus: illustrativeNäyttää muodon tai idean; älä käytä sellaisenaan tuotannossa.
import os
import sqlite3
from http.server import BaseHTTPRequestHandler, HTTPServer
from price_compare.webhooks import verify_webhook

secrets = [value for value in (os.environ.get("WEBHOOK_SECRET"), os.environ.get("WEBHOOK_PREVIOUS_SECRET")) if value]
if not secrets or any(len(value) < 32 for value in secrets):
    raise ValueError("Configure the webhook signing secret")
db = sqlite3.connect(os.environ.get("WEBHOOK_DB", "receiver.sqlite"), timeout=5)
db.executescript("CREATE TABLE IF NOT EXISTS received (id TEXT PRIMARY KEY); CREATE TABLE IF NOT EXISTS alerts (event_id TEXT PRIMARY KEY, product_id TEXT NOT NULL);")

class Receiver(BaseHTTPRequestHandler):
    def log_message(self, *args):
        pass  # Do not log incoming headers or payloads.

    def reply(self, status):
        self.send_response(status)
        self.send_header("Content-Length", "0")
        self.end_headers()

    def do_POST(self):
        self.connection.settimeout(10)
        if self.path != "/events":
            self.reply(404)
            return
        # The TLS proxy must forward an unmodified, bounded body with Content-Length.
        try:
            length = int(self.headers.get("Content-Length", "-1"))
            if self.headers.get("Transfer-Encoding") or not 0 <= length <= 262144:
                self.reply(413)
                return
            raw = self.rfile.read(length)
            if len(raw) != length:
                self.reply(400)
                return
        except (ValueError, OSError):
            self.reply(400)
            return
        event = None
        for secret in secrets:
            try:
                event = verify_webhook(raw, self.headers.get("X-Webhook-Signature", ""), secret)
                break
            except (ValueError, TypeError):
                continue
        if event is None:
            self.reply(400)
            return
        if event["type"] not in ("watchlist.triggered", "webhook.test"):
            self.reply(204)
            return
        product = event["data"].get("canonical_product_id")
        if event["type"] == "watchlist.triggered" and (not isinstance(product, str) or not product):
            self.reply(400)
            return
        try:
            with db:
                db.execute("BEGIN IMMEDIATE")
                inserted = db.execute("INSERT OR IGNORE INTO received (id) VALUES (?)", (event["id"],))
                if inserted.rowcount and event["type"] == "watchlist.triggered":
                    # The local inbox row is the effect; no external network call.
                    db.execute("INSERT INTO alerts (event_id, product_id) VALUES (?, ?)", (event["id"], product))
        except sqlite3.Error:
            self.reply(503)
            return
        self.reply(204)

if __name__ == "__main__":
    server = HTTPServer(("127.0.0.1", int(os.environ.get("PORT", "8080"))), Receiver)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        pass
    finally:
        server.server_close()
        db.close()
bashillustrative
Esimerkin luotettavuus: illustrativeNäyttää muodon tai idean; älä käytä sellaisenaan tuotannossa.
uv run --no-project python receiver.py

Varmenna ennen julkista käyttöönottoa

Lähetä paikallisella testisalaisuudella allekirjoitettu watchlist.triggered-tapahtuma, jossa on canonical_product_id. Odota 204-vastausta ja yhtä riviä sekä received- että alerts-taulussa. Lähetä sama tapahtuma uudelleen, käynnistä vastaanotin sitten uudelleen samalla tietokannalla ja lähetä vielä kerran: vastausten pitää olla 204 ja molempien rivimäärien edelleen yksi. Muuta yhtä rungon tavua allekirjoittamatta uudelleen: odota 400-vastausta ja muuttumattomia rivimääriä. Allekirjoita yli viisi minuuttia vanhalla aikaleimalla: odota 400-vastausta. Allekirjoitettu webhook.test palauttaa 204:n ilman ilmoitusta. Pysyvän tallennuksen epäonnistuessa odota 503:a, älä 204:ää. Nämä tarkistukset ajetaan myös repossa täsmälleen julkaistulle koodille.

Sijoita HTTPS-palvelusi taakse

Paikallinen kuuntelija ei ole julkinen tuotantopalvelin. Sijoita se hallitun HTTPS-käänteisvälityspalvelimen taakse porttiin 443. Välityksen on säilytettävä rungon tavut, rajattava runko 256 KiB:iin ja pyynnön kesto sekä välitettävä Content-Length (Python-esimerkki ei hyväksy chunked-siirtoa). Aja tässä esimerkissä yhtä prosessia pysyvällä tallennuksella, rajatuilla tiedosto-oikeuksilla, varmuuskopioinnilla ja valvotulla uudelleenkäynnistyksellä. Rekisteröi julkinen /events-osoite vasta käyttöönoton jälkeen; localhost- tai yksityisosoitteita ei voi rekisteröidä. Älä julkaise tietokantaa tai lokita salaisuuksia ja pyyntörunkoja.

Laajenna säilyttäen duplikaattisuojaus

Ilmoitusrivi on tarkoituksella koko liiketoimintavaikutus. Jos lisäät sähköpostin tai ulkoisen toiminnon, tallenna pysyvä lähetysjonon rivi samaan transaktioon ja anna työntekijän toimittaa se idempotenssiavaimella. Älä merkitse tapahtumaa vastaanotetuksi ja tee sitten suojaamatonta ulkoista kutsua. Tapahtumat voivat saapua epäjärjestyksessä: tämä esimerkki lisää ilmoituksia eikä ylikirjoita vahdin nykytilaa. Säilytä vastaanotetut tunnisteet koko uusintajakson, vähintään seitsemän päivää; esimerkki ei poista niitä automaattisesti.

Hyväksy kaksi avainta vain vaihdon aikana

Aseta dokumentoidun prepare/activate-vaihdon ajaksi toinen salaisuus WEBHOOK_SECRET-arvoksi ja toinen WEBHOOK_PREVIOUS_SECRET-arvoksi, ja käynnistä uudelleen. Molemmat hyväksytään, joten uuden avaimen voi asentaa ennen aktivointia. Aktivoinnin ja vähintään 60 sekunnin päällekkäisyyden jälkeen säilytä vain uusi WEBHOOK_SECRET ja käynnistä uudelleen. Älä jätä vanhaa avainta pysyvästi käyttöön. Jonotettu testi ei todista toimitusta: varmista delivered-tila ja received-rivi.

Oliko tästä sivusta apua?