Retries og backoff

Se hvilke fejl du skal prøve igen, og hvilke du aldrig skal, og brug eksponentiel backoff med jitter i Python, Node og PHP.

4 min Niveau: Øvet ✓ Kode testet

Et kald kan fejle, fordi du har ramt en grænse, fordi et offentligt register bag API'et er nede et øjeblik, eller fordi netværket hikker. Nogle af de fejl går over af sig selv, og så giver det mening at prøve igen. Andre går aldrig over, og et nyt forsøg bruger bare din kvote og din tid.

Guiden viser, hvordan du skelner, og giver en retry-funktion i tre sprog.

Hvad du prøver igen, og hvad du lader være

Fejlsvar har en maskinlæsbar code. På DAWA-stierne står den i roden af svaret ({"type": …, "title": …, "code": "rate_limit_per_minute"}), på /api/v1 under error.code. Se Fejlformatet.

StatuscodePrøv igen?
429rate_limit_per_minuteJa, efter Retry-After (sekunder til minuttet nulstilles, 1-60)
429rate_limit_per_visitorJa, efter Retry-After (sekunder til minuttet nulstilles, 1-60). Kun for browsernøgler
429quota_exceededNej. Gratisplanens kvote og buffer er brugt, eller dit budgetloft er nået (details.reason). details.resets_at fortæller, hvornår den nulstilles
429rate_limit_exceededNej. Månedsgrænsen er nået. Se details.reset_at
503upstream_unavailableJa, efter Retry-After. Et offentligt register, fx Datafordeleren, svarer ikke lige nu
502, 504–Ja, et par gange
Netværksfejl, timeout–Ja, et par gange
500internal_errorHøjst én gang. Fortsætter fejlen, så skriv til os via support
400, 422invalid_query_parameter, validation_errorNej. Ret forespørgslen
401missing_api_key, invalid_api_keyNej. Ret nøglen
403alle koderNej. Fx key_blocked, origin_not_allowed, sandbox_scope
404route_not_found eller ukendt idNej

quota_exceeded har bevidst ingen Retry-After. Den rigtige ventetid er dage eller uger, og en klient, der venter så længe i en løkke, er værre end en, der giver op.

Kald, der bliver afvist med 429, tæller ikke med i dit forbrug og koster ikke noget. De bliver heller ikke besvaret, og en klient, der prøver igen med det samme, får bare et nyt 429. Respektér Retry-After.

Prøv kun GET-kald og POST /datavask/… igen. De ændrer intet. Opretter et kald noget, fx et bulk-job eller en webhook, så tjek, om det første forsøg lykkedes, før du sender det igen.

Eksponentiel backoff med jitter

Vent længere for hvert forsøg: 0,5 s, 1 s, 2 s, 4 s. Vælg et tilfældigt tal mellem 0 og den grænse (såkaldt "full jitter"), så mange klienter, der fejlede samtidig, ikke prøver igen i samme sekund. Sender serveren Retry-After, så vent mindst så længe. Sæt et loft over både ventetid og antal forsøg.

I et adressefelt i browseren skal du ikke prøve igen. Brugeren skriver videre, og næste tastetryk laver et nyt kald. Retry hører hjemme i baggrundsjob og serverkode.

Python

Python#retry-python
import os
import random
import time

import requests

PROEV_IGEN_STATUS = {429, 502, 503, 504}
ALDRIG_KODER = {"quota_exceeded", "rate_limit_exceeded"}


def fejlkode(r: requests.Response) -> str | None:
    try:
        body = r.json()
    except ValueError:
        return None
    return body.get("code") or (body.get("error") or {}).get("code")


def hent_med_retry(session, url, params=None, forsoeg=5, base=0.5, loft=30.0):
    for n in range(forsoeg):
        sidste = n == forsoeg - 1
        try:
            r = session.get(url, params=params, timeout=10)
        except (requests.ConnectionError, requests.Timeout):
            if sidste:
                raise
            time.sleep(random.uniform(0, min(loft, base * 2 ** n)))
            continue
        if r.status_code not in PROEV_IGEN_STATUS or fejlkode(r) in ALDRIG_KODER or sidste:
            r.raise_for_status()
            return r
        vent = random.uniform(0, min(loft, base * 2 ** n))
        retry_after = r.headers.get("Retry-After", "")
        if retry_after.isdigit():
            vent = max(vent, float(retry_after))
        time.sleep(min(vent, loft))
    raise RuntimeError("ikke nået")


session = requests.Session()
session.headers["X-Api-Key"] = os.environ["DANADRESSE_API_KEY"]
r = hent_med_retry(session, "https://api.danadresse.dk/autocomplete",
                   params={"q": "Rådhuspladsen 1", "per_side": 1})
print(r.json()[0]["forslagstekst"])

Node

JavaScript#retry-node
const PROEV_IGEN_STATUS = new Set([429, 502, 503, 504]);
const ALDRIG_KODER = new Set(["quota_exceeded", "rate_limit_exceeded"]);
const sov = (ms) => new Promise((r) => setTimeout(r, ms));

async function fejlkode(res) {
  try {
    const body = await res.clone().json();
    return body.code ?? body.error?.code ?? null;
  } catch {
    return null;
  }
}

export async function hentMedRetry(url, init = {}, { forsoeg = 5, base = 500, loft = 30000 } = {}) {
  for (let n = 0; n < forsoeg; n++) {
    const sidste = n === forsoeg - 1;
    let res;
    try {
      res = await fetch(url, { ...init, signal: AbortSignal.timeout(10000) });
    } catch (err) {
      if (sidste) throw err;
      await sov(Math.random() * Math.min(loft, base * 2 ** n));
      continue;
    }
    if (!PROEV_IGEN_STATUS.has(res.status) || ALDRIG_KODER.has(await fejlkode(res)) || sidste) {
      if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
      return res;
    }
    let vent = Math.random() * Math.min(loft, base * 2 ** n);
    const retryAfter = Number(res.headers.get("retry-after"));
    if (retryAfter > 0) vent = Math.max(vent, retryAfter * 1000);
    await sov(Math.min(vent, loft));
  }
  throw new Error("ikke nået");
}

const url = new URL("https://api.danadresse.dk/autocomplete");
url.search = new URLSearchParams({ q: "Rådhuspladsen 1", per_side: "1" });
const res = await hentMedRetry(url, { headers: { "X-Api-Key": process.env.DANADRESSE_API_KEY } });
console.log((await res.json())[0].forslagstekst);

PHP

PHP#retry-php
<?php
function hentMedRetry(string $url, string $apiKey, int $forsoeg = 5, float $base = 0.5, float $loft = 30.0): array
{
    for ($n = 0; $n < $forsoeg; $n++) {
        $sidste = $n === $forsoeg - 1;
        $retryAfter = null;
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 10,
            CURLOPT_HTTPHEADER => ["X-Api-Key: $apiKey"],
            CURLOPT_HEADERFUNCTION => function ($ch, $linje) use (&$retryAfter) {
                if (stripos($linje, 'Retry-After:') === 0) {
                    $retryAfter = (int) trim(substr($linje, 12));
                }
                return strlen($linje);
            },
        ]);
        $body = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        $netfejl = curl_errno($ch) !== 0;
        curl_close($ch);

        $jitter = mt_rand() / mt_getrandmax() * min($loft, $base * 2 ** $n);
        if ($netfejl) {
            if ($sidste) {
                throw new RuntimeException('Netværksfejl');
            }
            usleep((int) ($jitter * 1e6));
            continue;
        }
        $data = json_decode($body, true) ?? [];
        $kode = $data['code'] ?? ($data['error']['code'] ?? null);
        $proevIgen = in_array($status, [429, 502, 503, 504], true)
            && !in_array($kode, ['quota_exceeded', 'rate_limit_exceeded'], true);
        if (!$proevIgen || $sidste) {
            if ($status >= 400) {
                throw new RuntimeException("HTTP $status: $body");
            }
            return $data;
        }
        $vent = max($jitter, (float) ($retryAfter ?? 0));
        usleep((int) (min($vent, $loft) * 1e6));
    }
    throw new RuntimeException('ikke nået');
}

$data = hentMedRetry(
    'https://api.danadresse.dk/autocomplete?q=' . rawurlencode('Rådhuspladsen 1') . '&per_side=1',
    getenv('DANADRESSE_API_KEY')
);
echo $data[0]['forslagstekst'], PHP_EOL;

Når det stadig fejler

Når alle forsøg er brugt, så log status, code og tidspunktet og lad fejlen gå videre til din egen fejlhåndtering. Logger du hele URL'en, så fjern api_key, hvis nøglen står i den.

Ser du mange 503 upstream_unavailable, så kig på status-siden. De kommer fra berigelses-endpoints under /api/v1/enrich/, som henter data live fra offentlige registre. Adresseopslag, autocomplete og datavask svarer fra vores egen database og påvirkes ikke.

Næste skridt

Relaterede guides

Markdown-kilde Fejl i guiden? Skriv til os API-reference (Swagger)

Prøv det med en gratis nøgle

2 000 kald/måned på gratisplanen. Intet kreditkort.

Opret konto →