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.
uv venv
uv pip install https://hintadata.fi/sdk/price_compare-3.1.0.tar.gzTallenna vastaanotin
Tallenna nimellä receiver.py. Vastaanotin kuuntelee vain paikallista loopback-osoitetta portissa 8080.
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()
uv run --no-project python receiver.pyVarmenna 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.