¶«Den følelsen» når du skal bestille noe viktig…
Du sitter på hytta og skal til å bestille. Nettet har vært tregt og ustabilt i dag, så du gruer deg til å trykke på bestill knappen. Men dette haster og må gjøres nå, så du trykker «bestill». Ganske kjapt kommer det en feilmelding: «Beklager noe gikk feil, prøv igjen». Du banner stille inni deg. Du prøver igjen… og igjen… og igjen. Endelig kommer beskjeden: «Takk for din bestilling». Du puster lettet ut, da gikk det vel greit likevel.
Noen dager senere får du 4 pakker i posten, og 4 regninger, det gikk visst ikke så greit likevel.
¶APIet var ikke bygget for å håndtere duplikate kall
Internett er per definisjon ustabilt. Det er basert på asynkrone meldinger som sendes etter «best effort» gjerne gjennom f.eks. luft, kablet nett og rutere før det håndteres av servere og databaser. Hvor som helst på veien kan det oppstå feil. Vi har bygget masse systemer og infrastruktur rundt dette, så i praksis feiler det veldig sjeldent. Men hvis du har mange brukere og mye trafikk, vil det uunngåelig oppstå feil. Det er derfor viktig å ha idempotente APIer.
MDN definerer idempotens på denne måten:
An HTTP method is idempotent if the intended effect on the server of making a single request is the same as the effect of making several identical requests.
Dette betyr at klienten, som i vårt tilfelle er en person, faktisk kan trykke bestill knappen flere ganger, uten å være bekymret. Men sånn er det altså ikke alltid.
MDN sier at GET, HEAD, OPTIONS, PUT og DELETE skal være idempotente. Men hva så med POST, det er gjerne denne vi bruker med «Bestill».
Løsningen på dette problemet har vi visst om lenge. Vi bruker en nøkkel som gjerne kalles idempotency-key. Denne nøkkelen settes av klienten, og gjør at server kan identifisere om et kall har vært gjort før. I så fall responderer den direkte med den tidligere responsen. Så utrolig enkelt! Dette funker for alle HTTP metoder, også POST. Likevel opplever jeg at veldig få tenker på dette, og det håper jeg å endre her.
Prinsippet er enkelt, men implementasjonen er naturligvis ikke fullt så enkel. Den er likevel enkel nok til å kunne forklares i en ganske kort bloggpost.
Å implementere dette trenger ikke være så krevende. Du må typisk ha en generell idempotens handler, som kan wrappe alle dine andre vanlige http handlere. Den har kun ansvaret for idempotens. Hvordan den skal operere kan beskrives med noen få eksempler.
¶Scenarier
- Når en ny forespørsel kommer inn:
- Sjekk om du har sett idempotency-key før. Det har du ikke, så lagre idempotency-key og si at response er pending
- Send forespørsel videre til faktisk handler
- Når handler responderer, lagre idempotency-key sammen med response, og marker som done
- Når allerede håndtert forespørsel kommer inn:
- Sjekk om du har sett idempotency-key før. Det har du, og resultat er done, da svarer du umiddelbart med det den opprinnelige handler svarte.
- Slapp av og vit at bruker har kun klart å bestille en gang.
- Når en duplikat forespørsel kommer inn, og original ikke er ferdig:
- Sjekk om du har sett idempotency-key før. Det har du, men du ser at svaret er pending. Blokker forespørsel.
- Poll/abonner på status av original response handler
- Når du ser status done, responder med originale handlers response.
¶Men hva om original forespørsel feiler?
Her begynner det å bli litt tricky, og svaret er «det kommer an på». Dette kan være en sporadisk serverfeil, som kan potensielt repareres. Det kan derfor være verdt å prøve på nytt, men idempotens handler må vite at dette faktisk er greit. F.eks. kan det hende operasjonen i seg selv ikke er idempotent, den kan være avhengig av andre ikke idempotente tredjeparter. I så fall må du kanskje respondere med den originale feilen. I andre tilfeller vet du at operasjonen er idempotent, og da kan du gjøre kallet på nytt, og kanskje denne gangen går det bra.
¶En generell løsning
Som du forhåpentligvis forstår er dette en svært god teknikk for å gjøre APIene vi bygger mer robuste og brukervennlige. Det er også en helt generell mekanisme som kan brukes til mer enn POST. Og jeg vil påstå vi også burde bruke dem for de såkalte naturlig idempotente verb f.eks. DELETE.
Idempotens sier at det er tilstanden på server som betyr noe, at dersom du gjør samme kall to ganger, så vil tilstanden være identisk. Det er klart at å slette noe flere ganger, har samme effekt, «noe» forblir slettet. Men, jeg vil påstå, for å hjelpe klienten, bør APIet respondere med det samme hver gang.
Vanligvis så vil DELETE forespørsel nummer to respondere «404 Not Found» eller «410 Gone». Dette er en «klient feil». Ved bruk av idempotency-key, så ville idempotens handler svart med f.eks. «204 No Content», som var original responskode. For klienten er dette mye enklere. Den slipper spesialhåndtering her, noe som vi ofte glemmer.
Hadde dette vært «Bestill» knappen, og klient kode ikke hadde håndtert f.eks. 410, så ville bruker stadig fått «Beklager, en feil har oppstått, prøv igjen». Bruker ville ikke endt opp med med 4 pakker i posten, men en ganske elendig brukeropplevelse uansett.
¶Noe for ditt API?
Nå som du er klar over problemet og kjenner til en løsning, så vil jeg utfordre deg på dette. Dette bør være standarden, så kan vi heller diskutere når vi ikke trenger det. Altså, der det er greit å droppe det. F.eks. der konsekvensene av duplikater er aksepterbare, eller håndtert på andre måter.
Men, til syvende og sist er det bare du og dine kolleger som kan avgjøre om dere trenger dette, selvsagt. Det er klart at det innebærer noe arbeid å få dette på plass.
Noen typer operasjoner er viktigere enn andre. For eksempel, hvis du jobber med pengetransaksjoner, tipper jeg at du allerede visste alt dette. I andre tilfeller, der du kontrollerer både klient og server, har du kanskje funnet andre løsninger. Uansett, dette er et nyttig verktøy i verktøykassen, som jeg håper du vil vurdere neste gang du skal lage et robust API.