# Fejl 503 og timeouts

> Forstå hvilke endpoints der afhænger af offentlige registre i realtid, håndter 503 upstream_unavailable med Retry-After, og sæt fornuftige timeouts i klienten.
>
> https://danadresse.dk/guides/fejl-503-og-timeouts · verified 2026-11-02
De fleste kald bliver besvaret fra vores egen database, også når de offentlige registre er nede. Nogle berigelser henter data live fra Datafordeleren og andre offentlige kilder, og de kan svare 503, når kilden ikke svarer. Denne guide viser hvilke, hvad svaret indeholder, og hvordan du indstiller timeouts og genforsøg.

## Hvad afhænger af hvad

| Endpoint | Datakilde ved kaldet | Kan give 503 |
|---|---|---|
| Alle DAWA-stier (`/autocomplete`, `/adresser`, `/datavask/…`, `/postnumre`, `/replikering/…` osv.) | Vores database | Nej |
| `/api/v1/datavask/parse`, `/api/v1/enrich/distance`, `/api/v1/enrich/validate/…` | Vores database / lokal beregning | Nej |
| `/api/v1/enrich/adresser/{id}/dagi`, `/bbr`, `/enhed`, `/virksomheder` | Vores database | Nej |
| `/api/v1/enrich/adresser/{id}/ejendom`, `/property`, `/installations` | Datafordeleren live, gemt i 24 timer pr. adresse | Ja, `upstream_unavailable` og `upstream_busy` |
| `/api/v1/enrich/adresser/{id}/climate` | DHM og DMI Klimaatlas live, gemt i 24 timer | Kun `upstream_busy`. Ellers markeres de dele, der mangler, i svaret |
| `/api/v1/enrich/adresser/{id}/all` | Alle ovenstående på én gang | Nej. Dele, der fejler, er `null` |

Adressedata bliver opdateret fra Datafordeleren i baggrunden. Er Datafordeleren nede, får du stadig svar, men ændringer fra de seneste timer kan mangle, indtil synkroniseringen er tilbage.

## upstream_unavailable

Datafordeleren har planlagte servicevinduer og af og til nedbrud. I et servicevindue afviser den alle nøgler, og så kan vi ikke hente data, der ikke allerede ligger i vores cache. Svaret er:

```http id=upstream-headers run=no
HTTP/2 503
retry-after: 60
cache-control: no-store
content-type: application/json
```

```json id=upstream-body run=no
{
  "error": {
    "code": "upstream_unavailable",
    "message": "An upstream data register is temporarily unavailable. Retry in a minute.",
    "details": null
  }
}
```

Det samme svar kommer, hvis Datafordeleren ikke svarer inden for vores egen timeout.

Hvad du gør:

- Prøv igen efter det antal sekunder, `Retry-After` angiver. Browserkode kan også læse headeren, fordi API'et giver adgang til den via CORS. Et servicevindue kan vare fra minutter til timer, så lad ventetiden vokse efter et par forsøg.
- Vis brugeren adressen uden berigelsen, og hent den senere. Adressedata er stadig tilgængelige.
- Har du hentet berigelsen for den samme adresse inden for de sidste 24 timer, kommer den fra cache og virker også under nedbruddet.
- Se driftsstatus på [/status](/status).

## upstream_busy

```json id=busy-body run=no
{
  "error": {
    "code": "upstream_busy",
    "message": "Enrichment is briefly rate-limited to protect the upstream register. Retry in a moment.",
    "details": null
  }
}
```

Alle kunder deler én nøgle til de offentlige registre. Kommer der mange berigelseskald for adresser, der ikke ligger i cache, på samme tid, holder vi kortvarigt igen. Svaret er 503 med `Retry-After: 2`. Vent to sekunder og prøv igen. Skal du berige mange adresser, så gør det i et jævnt tempo i stedet for i store parallelle bølger.

## 502 og 504 fra proxyen

En 502 eller 504 uden JSON-body kommer fra proxyen foran API'et: en timeout på en langsom forespørgsel eller et kort øjeblik under en udrulning. Body er en HTML-side, så din fejlhåndtering må ikke antage JSON. Behandl dem som 503 uden `Retry-After`: vent et par sekunder og prøv igen. En fejl fra selve API'et, også en 500, har altid en JSON-body med en `code`.

## Timeouts i din klient

Uden timeout kan en hængende forbindelse blokere en tråd eller en worker i lang tid. Sæt altid både en forbindelses-timeout og en læse-timeout:

| Kald | Læse-timeout |
|---|---|
| `/autocomplete` i et inputfelt | 3-5 sekunder. Brugeren skriver videre, og det næste kald erstatter det gamle |
| Adresseopslag, datavask, lister | 10-15 sekunder |
| `ejendom`, `property`, `installations`, `all` første gang for en adresse | 60 sekunder. Kaldet slår op i flere registre efter hinanden |
| `/replikering/udtraek` | Ingen samlet timeout. Brug en læse-timeout pr. blok, fordi svaret strømmer |

Et eksempel med `httpx`, der har separate timeouts for forbindelse og læsning:

```python id=httpx-timeouts run=live expect="Aarhus"
import os

import httpx

timeout = httpx.Timeout(connect=5.0, read=15.0, write=10.0, pool=5.0)
with httpx.Client(
    base_url="https://api.danadresse.dk",
    headers={"X-Api-Key": os.environ["DANADRESSE_API_KEY"]},
    timeout=timeout,
) as client:
    r = client.get("/postnumre/8000")
    r.raise_for_status()
    print(r.json()["navn"])
```

## Genforsøg for berigelser

Funktionen her henter en berigelse og prøver igen ved 502, 503 og 504 og ved timeouts. Den bruger `Retry-After`, når headeren findes, og ellers en voksende ventetid. 4xx-fejl bliver ikke prøvet igen, fordi de ikke går væk af sig selv. Berigelser kræver en live-nøgle:

```python id=enrich-retry run=syntax
import os
import random
import time

import httpx

API = "https://api.danadresse.dk"
RETRY_STATUS = {502, 503, 504}

client = httpx.Client(
    base_url=API,
    headers={"X-Api-Key": os.environ["DANADRESSE_API_KEY"]},
    timeout=httpx.Timeout(connect=5.0, read=60.0, write=10.0, pool=5.0),
)


def enrich(adgangsadresse_id: str, kind: str = "ejendom", attempts: int = 5) -> dict | None:
    """Hent /api/v1/enrich/adresser/{id}/{kind}. None, hvis kilden stadig er nede."""
    for attempt in range(attempts):
        try:
            r = client.get(f"/api/v1/enrich/adresser/{adgangsadresse_id}/{kind}")
        except (httpx.TimeoutException, httpx.TransportError):
            r = None
        if r is not None and r.status_code not in RETRY_STATUS:
            r.raise_for_status()  # 4xx: ret kaldet, prøv ikke igen
            return r.json()
        retry_after = r.headers.get("Retry-After", "") if r is not None else ""
        wait = int(retry_after) if retry_after.isdigit() else min(60, 2 ** attempt)
        time.sleep(wait + random.random())
    return None


if __name__ == "__main__":
    data = enrich("0a3f507c-0624-32b8-e044-0003ba298018")
    print("Ingen data lige nu" if data is None else sorted(data))
```

Til lange batchjobs er det bedre at lægge adresser, der fejler, i en kø og prøve dem igen senere end at vente i løkken. Se [Retries og backoff i produktion](/guides/retries-og-backoff).

## Næste skridt

- [Grænser, kvoter og 429](/guides/graenser-og-429)
- [Retries og backoff i produktion](/guides/retries-og-backoff)
- [BBR-bygningsdata](/guides/bbr-bygningsdata)
