info@yenlo.com
ned
Menu
API Management 6 min

Hoe ontwerp je je ReST API’s voor precies één keer verwerken

Hoe ontwerp je REST-API's die betrouwbaar zijn, fouten effectief verwerken en dubbele verwerking voorkomen? In deze blog bespreken we de kracht van idempotente operaties en verkennen we strategieën om API-ontwerpen te vereenvoudigen en tegelijkertijd exact-eenmalige verwerking te garanderen. Ontdek hoe veelvoorkomende valkuilen bij POST-verzoeken de betrouwbaarheid kunnen beïnvloeden en leer waarom PUT, in combinatie met voorwaardelijke headers, je API-architectuur kan transformeren.

Hans Bot
Hans Bot
Senior Solution Architect
Hoe ontwerp je je ReST API's voor precies één keer verwerken

Onlangs kwam ik een interessante discussie tegen op Erik Wilde’s uitstekende YouTube-kanaal “Getting APIs to Work”. Het biedt een klein, maar waardevol inzicht voor je REST API-ontwerp. In mijn ogen verdient dit RESTful API-idee zeker een breder publiek om van te leren. Dus hier is wat ik ervan heb meegenomen.

API-idempotentie is een van die onderwerpen waarvan je weet dat ze belangrijk zijn, maar die toch zelden belangrijk genoeg lijken om in je Minimum Viable Product terecht te komen. En raad eens? Het haalt zelden de planning van een latere sprint. De noodzaak voelt gewoon nooit urgent genoeg om aan te pakken.

Voor veel architecten is precies één keer verwerken een reden om event-driven architecturen te promoten. Hier zorgt de broker ervoor dat elk bericht wordt afgeleverd. En zodra je een transactionele client implementeert, ben je al bijna bij precies één keer verwerken. Je hoeft alleen nog maar een RPO=0 op je doelsysteem te realiseren, of het systeem herstartbestendig te maken. In het boek van de architect is dat makkelijk. Gelukkig is er een nog makkelijkere manier. En zo doe je dat.

Wat is het idee?

Het idee is verleidelijk eenvoudig. Als je alleen idempotente REST API-operaties gebruikt, kun je ze blijven herhalen totdat ze slagen. En als je ze blijft herhalen, zelfs nadat ze zijn geslaagd, misschien omdat de bevestiging ergens onderweg is kwijtgeraakt, heeft dat gewoonweg geen effect op de status van de resource (het ‘ding’ achter de URL – een Uniform Resource Locator).

Wanneer je een REST API ontwerpt, kun je kiezen uit vier hoofdbewerkingen: POST, PUT, GET en DELETE. Van deze methoden is POST de enige die van nature niet idempotent is. Als je twee keer dezelfde representatie van een object POST, voeg je twee resources toe aan de collectie in je datastore. Zo werkt het (ervan uitgaande dat je naar een collectie POST, zoals aanbevolen in REST). Soms is dat precies wat je wilt, maar vaker is het een ongewenst neveneffect van slechte foutafhandeling of slecht UI-ontwerp. En daarom zou je kunnen overwegen om POST helemaal te verbannen.

Te radicaal idee? Niet haalbaar? Nou, het is veruit de eenvoudigste implementatie om volledige idempotentie te bereiken die ik ben tegengekomen. Lees verder om te ontdekken waarom.

Welk probleem proberen we op te lossen?

Op het internet gaan dingen voortdurend mis. Het komt wel eens voor dat je een POST-aanvraag verstuurt, maar nooit een antwoord terugkrijgt. Dit kan betekenen dat de resource server je aanvraag nooit heeft verwerkt, in welk geval je deze opnieuw wilt versturen. Maar het is ook mogelijk dat de aanvraag goed is verwerkt, maar het antwoord verloren is gegaan. Nu wil je je aanvraag niet opnieuw versturen, want dat zou een duplicaat introduceren. En we weten allemaal hoe pijnlijk het kan zijn om die veilig te dedupliceren.

Maar hoe kom je erachter wat er precies is gebeurd? Je zou een HEAD-methode kunnen proberen, maar daarvoor heb je de Identifier van de resource nodig die misschien wel of niet is aangemaakt. Met een POST is de resource server meestal verantwoordelijk voor het toekennen van die identifier. Je kunt deze vinden in het antwoordbericht dat je nooit hebt ontvangen. Dus zit je vast en moet je een gokje wagen.

wp advanced api management guide
Whitepaper Gids Voor Geavanceerd API-Management

Aanbevelingen om je te helpen jouw eigen architectuur te ontwerpen en daarbij een selectie te maken

Nu downloaden

Wat is precies het alternatief?

Je kunt natuurlijk een PUT gebruiken in plaats van een POST. Je kunt een object zo vaak PUTten als je wilt, het zal hoogstens één resource aanmaken. Je neemt gewoon een willekeurige GUID als identifier van de resource die je PUT. Dat is sowieso een goede gewoonte. Je moet geen gemakkelijk te raden identifiers zoals een sequentie, een e-mailadres of een BSN gebruiken, toch? En of de client of de server de GUID uitreikt, maakt eigenlijk niet uit, toch?

Maar dit heeft ook enkele nadelen. Eén randgeval is regressie. Als je erover nadenkt, staat een POST gelijk aan een INSERT in de datastore, terwijl een PUT gelijkstaat aan een UPSERT. Stel, je hebt een nieuwe bestelling gePUT, maar het antwoord is verloren gegaan. Terwijl je wacht op een timeout, zijn een of meer updates verwerkt. De voorraad is bijgewerkt en een betalingsverzoek is uitgegeven. Nu zet je de status van de resource terug naar zijn oorspronkelijke staat. Slechte dingen zullen ongetwijfeld gebeuren. Maar het blijkt dat je dat volledig kunt voorkomen.

De magie van dit patroon zit in het gebruik van voorwaardelijke aanvragen (bekijk RFC 7232 voor details). Met deze API Design-truc wordt de HTTP-methode alleen verwerkt wanneer aan een voorwaarde is voldaan. Het wordt vaak gebruikt in combinatie met een E-tag, of Entity-tag, als handtekening van een antwoord. Aangezien je het object eerder hebt gePUT maar geen bevestiging hebt ontvangen, wil je nu het object PUTten als en alleen als de resource nog niet bestaat. Het blijkt dat er een gemakkelijke manier is om dat te doen, zonder een E-tag-waarde van de server te kennen. Voeg gewoon de volgende header toe aan je PUT, en je kunt het zo vaak proberen als je wilt.

If-None-Match “*”

Effectief verandert deze voorwaarde een PUT-aanvraag van een UPSERT in een idempotente INSERT. Het geeft aan om de resource alleen te PUTten als er niets is om mee te matchen. Met andere woorden, als de resource nog niet bestaat. Een lege E-tag, als het ware. Anders krijg je een statuscode HTTP:304 (Not Modified). Er wordt niets bijgewerkt. Briljant.

Is er een addertje onder het gras?

Niet echt, voor zover ik kan bedenken. Natuurlijk is er een heel kleine kans dat er al een andere resource met dezelfde GUID in je gebruikersstore bestaat. Maar dat kan eenvoudig worden omzeild met dezelfde truc. Als je dezelfde header toevoegt aan het initiële PUT-verzoek, zou een 304 bij de eerste poging aangeven dat de gegenereerde GUID in feite niet uniek is. Presto!

Dat laat alleen de oneindig kleine kans dat de 304 bij het initiële verzoek verloren gaat en je de 304 bij de herhaalde poging ten onrechte als succes interpreteert. Als dat je zorgen baart, kun je altijd een HEAD-verzoek invoegen voor je eerste PUT. Als je een HTTP:200 ontvangt, is het je geluksdag. Overweeg een lot te kopen, maar vergeet niet om een nieuwe GUID te genereren voordat je je resource opnieuw PUT.

Is er nog een andere manier?

Op het web is er altijd een andere manier. In dit geval, als je echt gehecht bent aan het gebruik van POST in plaats van PUT bij het toevoegen van een resource, overweeg dan dit. Er is niets in de HTTP-specificatie dat je ervan weerhoudt om naar een geïdentificeerde resource te POSTen, in plaats van naar een collectie. POSTen naar een collectie is gewoon een praktijk in RESTful API Design. En deze identifier kan evengoed een door de client gegenereerde GUID zijn. Je zou een HTTP:409 krijgen in geval van conflict en dat precies op dezelfde manier afhandelen als de 304. Maar het zou nooit iets overschrijven.

De event-driven aanpak die ik in de inleiding noemde, zou ook kunnen werken. Je zou de toepassing van CQRS kunnen overwegen. Hier is een broker tussen de client applicatie en de back-end. Zodra het creatieverzoek is bevestigd, garandeert de broker de levering. Met andere woorden, de client hoeft niet te wachten, maar krijgt bijna onmiddellijk zijn bevestiging. Je zou waarschijnlijk een AsyncAPI gebruiken om een creatiegebeurtenis naar de broker te publiceren, met behulp van een CloudEvents-beschrijving. De Resource Identifier kan net zo goed worden gegenereerd door de client als door de back-end. De back-end zou zijn antwoord ofwel publiceren naar het reply-kanaal waarop je client is geabonneerd, of als een directe callback als de client een webhook heeft opgegeven. Klinkt ingewikkeld? Niet zo erg, tenminste als je al geïnvesteerd hebt in de eventing-capability. Maar de Idempotent REST API wint absoluut de eenvoudwedstrijd.

Wat te kiezen?

Er zijn meerdere opties met verschillende voor- en nadelen. Als Integration Architect is het analyseren van de afwegingen tussen meerdere architecturale alternatieven, zowel groot als klein, mijn dagelijkse werk. Ik kan je helpen een weloverwogen keuze te maken. Bovendien zal ik ervoor zorgen dat de gekozen optie de steun krijgt van de belanghebbenden. Neem gerust contact met ons op.

Ben je klaar om je POST achter je te PUTten? Geef je de voorkeur aan het idempotent gebruik van de POST? Of accepteer je lui de kwetsbaarheid van je POST?

ned
Sluiten