Zum Inhalt springen

Laravel mit Xero verbinden, ohne den Mandanten zu verlieren

Xero tauscht das Refresh-Token bei jeder Erneuerung aus. Zwei gleichzeitige Worker machen daraus eine dauerhaft getrennte Organisation. Dieser Fehler und die vier danach.

7 Min. Lesezeit

Die Integration geht an einem Dienstag live. Sie überträgt Rechnungen, sie holt Zahlungen, die Buchhaltung hört auf, Zahlen zwischen zwei Bildschirmen abzutippen, und alle sind zufrieden. Elf Tage später ist die Organisation eines Kunden getrennt und lässt sich nur wieder verbinden, indem dieser erneut durch den Zustimmungsbildschirm geht. Es wurde nichts deployt. Niemand hat den Code angefasst.

Passiert ist, dass zwei Ihrer Queue-Worker dasselbe Token in derselben Sekunde erneuert haben.

Das Refresh-Token ist einmal verwendbar

Das Access-Token von Xero lebt dreißig Minuten. Damit kommt jeder zurecht. Was Teams erwischt, ist der zweite Teil: Beim Erneuern bekommen Sie ein neues Refresh-Token, und das alte wird sofort ungültig. Es gibt eine Kulanzzeit von sechzig Sekunden, danach ist es weg.

Nehmen Sie also zwei Jobs für denselben Mandanten, die wenige Millisekunden auseinander starten, beide ein abgelaufenes Access-Token finden und beide den Refresh-Endpunkt aufrufen. Der erste erhält die Token B und speichert sie. Der zweite hat dasselbe alte Token geschickt und bekommt innerhalb der Kulanzzeit ebenfalls eine gültige Antwort - ein anderes Paar, Token C - und schreibt die darüber. Die Zeile hält jetzt C, Xeros letzte Ausgabe war C, alles sieht in Ordnung aus. Lassen Sie dasselbe unter echter Last laufen, mit bereits verbrauchter Kulanzzeit, dann scheitert der zweite Aufruf, Ihr Fehlerbehandler schreibt nichts, und die Zeile hält weiterhin ein verbranntes Refresh-Token. Die Organisation ist getrennt, und die einzige Abhilfe ist eine erneute Zustimmung.

Die Verteidigung lautet: Genau ein Prozess darf für einen Mandanten erneuern, und die anderen warten auf dessen Ergebnis, statt es selbst zu versuchen.

public function accessToken(XeroConnection $connection): string
{
    if ($connection->expires_at->isAfter(now()->addMinutes(2))) {
        return $connection->access_token;
    }
 
    return Cache::lock("xero:refresh:{$connection->tenant_id}", 30)
        ->block(20, function () use ($connection) {
            $connection->refresh();          // neu lesen; jemand war vielleicht schneller
 
            if ($connection->expires_at->isAfter(now()->addMinutes(2))) {
                return $connection->access_token;
            }
 
            return $this->exchange($connection);
        });
}

Drei Details darin tragen. Die zwei Minuten Puffer sorgen dafür, dass kein Token an einen Job geht, der neunzig Sekunden hinter einer langsamen Anfrage in der Warteschlange steht. Das erneute Lesen innerhalb der Sperre macht den unterlegenen Worker billig - er wacht auf, sieht ein frisches Token und benutzt es. Und block statt get bedeutet, dass der zweite Worker wartet, statt false zurückzugeben und einen Job scheitern zu lassen, an dem nichts falsch war.

Schreiben Sie das neue Paar in einer Transaktion, und behandeln Sie einen gescheiterten Tausch als Zustandsänderung statt als Ausnahme. Sagt Xero, das Refresh-Token sei ungültig, ist die Verbindung tot; sie als tot zu markieren und den Kunden zu informieren ist das korrekte Verhalten, vierzigmal zu wiederholen nicht.

Eine Verbindung ist kein Unternehmen

Die Autorisierung gilt nicht einem Benutzer und nicht dem Unternehmen Ihres Kunden. Sie gilt einem Mandanten, identifiziert durch eine tenantId, die nach der Zustimmung vom Connections-Endpunkt zurückkommt - und eine einzige Person am Zustimmungsbildschirm kann Ihnen drei davon geben, wenn sie drei Organisationen verwaltet.

Das ist ab Tag eins relevant, weil es Ihr Schema bestimmt. Jeder synchronisierte Datensatz trägt den Mandanten, zu dem er gehört, jeder API-Aufruf schickt dessen Header mit, und jede Abfrage, die nach "der Xero-Rechnung zu dieser Bestellung" sucht, filtert darauf. Teams, die das überspringen, weil der erste Kunde eine Organisation hatte, schreiben die Migration später während eines Zwischenfalls - der denkbar schlechteste Zeitpunkt, um einer Tabelle mit zwei Millionen Zeilen eine Spalte hinzuzufügen.

Es ist außerdem relevant, weil die Menge nicht feststeht. Eine Organisation kann in Xeros eigener Oberfläche aus Ihrer Anwendung entfernt werden, von jemandem, der Ihre nie gesehen hat. Prüfen Sie den Connections-Endpunkt regelmäßig, nicht nur bei der Zustimmung, und gleichen Sie ihn gegen Ihre Tabelle ab.

Das Limit sind vier Limits

Sie bekommen sechzig Aufrufe pro Minute und Mandant, fünftausend pro Tag und Mandant, zehntausend pro Minute über Ihre gesamte Anwendung und eine Obergrenze für gleichzeitige Anfragen. Sie scheitern gleich, mit einer 429 und einem Retry-After, aber sie bedeuten Unterschiedliches, und die Antwort sagt Ihnen im Header X-Rate-Limit-Problem, welches es war.

Ein Minutenlimit ist ein Taktproblem, und die Antwort ist, die Queue dieses Mandanten zu verlangsamen. Ein Tageslimit ist ein Entwurfsproblem, und kein Backoff der Welt behebt es - Sie holen Dinge, die Sie bereits haben. Das Anwendungslimit ist jenes, das allen Kunden gleichzeitig den Dienstag verdirbt, weil der Erstimport eines einzigen Kunden läuft. Genau das ist das Argument für eine Queue pro Mandant statt einer gemeinsamen.

Nehmen Sie Retry-After wörtlich. Laravels RateLimited-Middleware am Job, mit exakt der Sekundenzahl aus dem Header zurückgestellt, ist die ganze Umsetzung, und sie ist besser als jeder exponentielle Backoff, den Sie schreiben würden, weil die Zahl keine Vermutung ist.

Webhooks sagen, dass sich etwas geändert hat, nicht was

Die Webhook-Nachricht von Xero enthält einen Ressourcentyp, einen Mandanten, eine ID und einen Zeitstempel. Sie enthält nicht die Rechnung. Sie bekommen die Nachricht und holen die Daten anschließend, was einen Webhook zu einem Hinweis zum Lesen macht und nicht zu einem Schreibvorgang.

Zwei Dinge am Endpunkt sind ungewöhnlich genug, um sie vor dem Bau zu kennen. Xero prüft ihn mit einer Intent-to-receive-Nachricht, die Sie korrekt beantworten müssen, bevor das Abonnement aktiv wird, und die Signaturprüfung ist ein HMAC über den Rohtext des Bodys - alles, was die Anfrage vor dem Hashen neu serialisiert, scheitert auf eine Weise, die nach falschem Schlüssel aussieht. Und die Zustellung ist nicht geduldig: Bestätigen Sie mit einer 200 und sonst nichts, legen Sie die ID auf eine Queue, und holen Sie die Daten danach.

Bauen Sie den Polling-Pfad trotzdem. Eine Abfrage mit If-Modified-Since liefert alles, was sich seit Ihrer letzten erfolgreichen Synchronisation geändert hat, und es ist ihr gleichgültig, ob Ihr Endpunkt erreichbar war. Diese eine Abfrage macht aus einer Störung eine Verzögerung statt einer Lücke in Ihren Daten.

Die Abstimmung ist das Projekt

Die Technik oben ist zwei Wochen. Was den Rest ausmacht, ist die Entscheidung darüber, was wahr ist.

Ihre Anwendung hat eine Rechnung. Xero hat eine Rechnung. Die Buchhaltung hat die in Xero am Donnerstag bearbeitet, weil sie dort arbeitet. Jemand hat eine Gutschrift dagegen erstellt. Eine Zahlung kam über einen Betrag herein, den keiner der beiden Datensätze erwartet, weil der Kunde drei Rechnungen in einer Überweisung beglichen hat. Nichts davon ist eine API-Frage.

Was vor dem Code entschieden und aufgeschrieben gehört: welches System welches Feld besitzt, was passiert, wenn beide Seiten dasselbe ändern, ob eine in Xero stornierte Rechnung die Bestellung in Ihrer Anwendung storniert oder nur markiert, wie eine Teilzahlung zugeordnet wird, und was Ihre Rechnungsnummerierung tut, wenn Xero in manchen Rechtsordnungen das führende System für die Nummerierung ist und in anderen nicht. Unsere Arbeit im Integration Engineering besteht größtenteils aus diesem Gespräch, und die API-Aufrufe fallen daraus ab.

Machen Sie jeden Schreibvorgang idempotent, solange Sie dabei sind. Xero akzeptiert einen Idempotenzschlüssel, und die Queue darunter ist at-least-once, also wird aus einem doppelten Job sonst eine doppelte Rechnung in der Buchhaltung eines anderen.

Wann man das nicht bauen sollte

Wenn die Anforderung lautet, dass die Buchhaltung Umsatz nach Produkt sehen will, liefert ein Export mit einer Tabellenkalkulation das diese Woche und eine bidirektionale Synchronisation in zwei Monaten. Bei vierzig Rechnungen im Monat ist das Tageslimit nicht Ihr Problem und nichts von alledem auch - ein nächtlicher Export genügt und kostet ein Zehntel des Codes.

Bauen Sie die Synchronisation, wenn die Menge an Datensätzen die manuelle Erfassung zu echten Kosten macht, wenn beide Seiten tatsächlich bearbeitet werden, oder wenn etwas nachgelagert innerhalb von Minuten auf eine Zahlung reagieren muss. In diesen Fällen zahlt sich die Abstimmungsarbeit aus. Außerhalb davon ist es viel Technik, um eine Aufgabe abzuschaffen, die jemanden zwanzig Minuten pro Woche gekostet hat.

Liegt das Hauptbuch in Sage statt in Xero, gilt das meiste davon weiterhin und die Zugriffsfrage nicht - das ist ein anderer Artikel, denn Sage ist kein einzelnes Produkt.

Verwandte Fragen

Können wir die Xero-Token in der users-Tabelle ablegen?
Nur wenn ein Benutzer jemals genau eine Organisation autorisiert, und darauf sollten Sie nicht wetten. Die Autorisierung gehört zu einer Verbindung - einem Mandanten, auf den Ihre Anwendung Zugriff bekommen hat - und ein einziger Xero-Login kann mehrere davon freigeben. Eine eigene Tabelle, mit der Mandanten-ID als Schlüssel, ist die richtige Ablage.
Brauchen wir Webhooks überhaupt, wenn wir pollen?
Sie brauchen eines von beiden und meistens beides. Polling mit einem Modified-Since-Filter hält Sie nach einer Störung korrekt, weil es nicht wissen muss, was Sie verpasst haben. Webhooks sorgen dafür, dass die Anwendung zwischen den Abfragen aktuell wirkt. Keines ersetzt das andere, und ein Webhook allein verliert Ereignisse, sobald Ihr Endpunkt eine Stunde ausfällt.
Was passiert, wenn der Kunde uns in Xero trennt?
Der nächste Aufruf liefert 401 und Ihr gespeichertes Refresh-Token ist tot. Vorher benachrichtigt Sie niemand. Deshalb muss der getrennte Zustand ein echter Zustand in Ihrem eigenen Schema sein, mit einer Erklärung in der Oberfläche - und nicht eine Ausnahme, die in einem Log landet, das niemand liest.
Wie lange dauert so eine Integration?
Der Autorisierungsfluss und die erste erfolgreich übertragene Rechnung sind eine Woche. Die Abstimmungsregeln - welche Seite bei welchem Feld gewinnt, was mit einer Gutschrift geschieht, wie eine Teilzahlung zugeordnet wird - sind der Rest, und das ist ein Gespräch mit der Buchhaltung und keine technische Schätzung.

← Zurück zu allen Artikeln

Anrufen+1 848 272 7583WhatsApp+90 850 308 5436E-Mailinfo@codefacture.comKontaktseite