En Ruby-klient med Net::HTTP og Faraday
Kald Danadresse fra Ruby og Rails med Net::HTTP eller Faraday, med timeouts, keep-alive, fejlkoder og Retry-After ved 429.
Denne guide viser to Ruby-klienter til Danadresse. Den første bruger kun standardbiblioteket (Net::HTTP og JSON) og virker i scripts, Sinatra og Rails uden nye gems. Den anden bruger Faraday, som mange Rails-projekter allerede har. Begge dækker autocomplete, opslag på id, datavask, reverse geokodning, timeouts, fejlkoder og 429.
Nøglen læses fra miljøvariablen DANADRESSE_API_KEY. I Rails kan du lægge den i credentials og sætte miljøvariablen derfra, men hold den på serveren.
1. Net::HTTP
Gem filen som danadresse.rb. Kører du den direkte med ruby danadresse.rb, laver den seks kald som demo:
# danadresse.rb: en lille klient til Danadresse API med Net::HTTP (kun standardbiblioteket) require "json" require "net/http" require "uri" module Danadresse Adresse = Struct.new(:id, :betegnelse, :postnr, :lon, :lat, keyword_init: true) # kategori kun på topniveau (ingen score); afstand 0 = præcis match Vask = Struct.new(:kategori, :kandidater, :adresse_id, :tekst, :afstand, keyword_init: true) do # A = præcis match, B = én sikker match med små stavefejl. C: lad brugeren vælge def sikker? %w[A B].include?(kategori) && !adresse_id.nil? end end class Error < StandardError attr_reader :status, :code, :retry_after, :details def initialize(status, code, message, retry_after = nil, details = nil) super("HTTP #{status} #{code}: #{message}") @status = status @code = code # stabil kode, fx "invalid_api_key" @retry_after = retry_after @details = details # fx resets_at og upgrade_url ved quota_exceeded end end class Client MAX_RETRIES = 2 def initialize(api_key: ENV.fetch("DANADRESSE_API_KEY"), base_url: ENV.fetch("DANADRESSE_BASE_URL", "https://api.danadresse.dk")) @api_key = api_key @base = URI(base_url) end def autocomplete(q, per_side: 5) get("/autocomplete", q: q, per_side: per_side) end def adresse(id) d = get("/adresser/#{URI.encode_www_form_component(id)}") til_adresse(d, d["adgangsadresse"]) end def vask(betegnelse) svar = get("/datavask/adresser", betegnelse: betegnelse) res = svar["resultater"] || [] # aktueladresse = den gældende version (afviger, hvis den fundne er nedlagt) a = res.dig(0, "aktueladresse") || res.dig(0, "adresse") Vask.new(kategori: svar["kategori"], kandidater: res.size, adresse_id: a&.dig("id"), tekst: a && "#{a['vejnavn']} #{a['husnr']}, #{a['postnr']} #{a['postnrnavn']}", afstand: res.dig(0, "vaskeresultat", "afstand")) end def reverse(lon, lat) d = get("/adgangsadresser/reverse", x: lon, y: lat) til_adresse(d, d) end private def til_adresse(d, ag) lon, lat = ag.dig("adgangspunkt", "koordinater") Adresse.new(id: d["id"], betegnelse: d["adressebetegnelse"], postnr: ag.dig("postnummer", "nr"), lon: lon, lat: lat) end def get(path, params = {}) uri = @base.dup uri.path = path uri.query = URI.encode_www_form(params) unless params.empty? attempt = 0 begin res = http.request(Net::HTTP::Get.new(uri, "X-Api-Key" => @api_key, "Accept" => "application/json")) return JSON.parse(res.body) if res.is_a?(Net::HTTPSuccess) raise fejl(res) rescue Error => e # 429 (minutgrænse) og 503 med Retry-After kan prøves igen; quota_exceeded har ingen Retry-After raise unless [429, 503].include?(e.status) && e.retry_after && attempt < MAX_RETRIES attempt += 1 sleep([e.retry_after, 30].min) retry rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNRESET, Errno::ECONNREFUSED raise if attempt >= MAX_RETRIES attempt += 1 @http = nil # åbn en ny forbindelse sleep(2**attempt) retry end end # Én keep-alive-forbindelse pr. klient (ikke trådsikker: brug én klient pr. tråd) def http @http ||= Net::HTTP.start(@base.host, @base.port, use_ssl: @base.scheme == "https", open_timeout: 3, read_timeout: 10) end def fejl(res) body = JSON.parse(res.body) rescue {} body = {} unless body.is_a?(Hash) # DAWA-stier: {type,title,details,code} /api/v1: {error:{code,message}} err = body["error"].is_a?(Hash) ? body["error"] : body ra = res["Retry-After"] Error.new(res.code.to_i, err["code"], err["title"] || err["message"] || res.message, ra&.match?(/\A\d+\z/) ? ra.to_i : nil, err["details"]) end end end if $PROGRAM_NAME == __FILE__ api = Danadresse::Client.new api.autocomplete("Rådhuspladsen 1, 4200", per_side: 3).each { |f| puts "forslag: #{f['forslagstekst']}" } a = api.adresse("0a3f50ae-b4ee-32b8-e044-0003ba298018") puts "opslag: #{a.betegnelse} #{a.lon} #{a.lat}" ["Strandvejen 100, 2900 Hellerup", "Rentemestervej 8, 2. tv, 2400 København NV"].each do |tekst| v = api.vask(tekst) puts "datavask: #{v.kategori} #{v.kandidater} #{v.sikker? ? v.tekst : 'lad brugeren vælge'}" end puts "reverse: #{api.reverse(12.5683, 55.6761).betegnelse}" begin api.adresse("00000000-0000-0000-0000-000000000000") rescue Danadresse::Error => e puts "fejl: #{e.status} #{e.code}" end end
Output:
forslag: Rådhuspladsen 1, 4200 Slagelse forslag: Rådhuspladsen 1, st. tv, 4200 Slagelse forslag: Rådhuspladsen 1, st. th, 4200 Slagelse opslag: Rådhuspladsen 1, st. th, 4200 Slagelse 11.35623868 55.40464424 datavask: A 1 Strandvejen 100, 2900 Hellerup datavask: C 7 lad brugeren vælge reverse: Rådhuspladsen 3, 1550 København V fejl: 404 not_found
Det vigtigste i klassen:
- Keep-alive.
Net::HTTP.startåbner én forbindelse, som genbruges til alle kald. Det sparer et TLS-håndtryk pr. kald. EnNet::HTTP-forbindelse er ikke trådsikker, så brug én klient pr. tråd, fx i en Puma-worker medThread.current[:danadresse] ||= Danadresse::Client.new. - Timeouts.
open_timeout: 3ogread_timeout: 10sætter loft over ventetiden. Ved timeout lukkes forbindelsen, og der åbnes en ny ved næste forsøg. - Fejlformater. DAWA-stierne svarer
{"type","title","details","code"}, og/api/v1/*svarer{"error":{"code","message","details"}}, for alle fejl, også 500.fejllæser begge, så du kan forgrene påe.codeog læsee.details. - Retry. 429 og 503 med
Retry-After(1 til 60 sekunder ved minutgrænsen) prøves igen efter den angivne ventetid, højst 30 sekunder og højst to gange.quota_exceededhar ingenRetry-Afterog rejses med det samme. Afviste kald tælles og faktureres ikke.
2. Faraday
Med Faraday 2 og faraday-retry klarer middleware'en netværksfejl og timeouts. 429 håndteres i klassen selv. Grunden er, at quota_exceeded også er en 429, og den skal ikke gentages:
# danadresse_faraday.rb (Gemfile: gem "faraday", "~> 2.0"; gem "faraday-retry") require "faraday" require "faraday/retry" module Danadresse class Error < StandardError attr_reader :status, :code, :retry_after, :details def initialize(status, code, message, retry_after = nil, details = nil) super("HTTP #{status} #{code}: #{message}") @status = status @code = code @retry_after = retry_after @details = details end end class FaradayClient MAX_RETRIES = 2 def initialize(api_key: ENV.fetch("DANADRESSE_API_KEY"), base_url: ENV.fetch("DANADRESSE_BASE_URL", "https://api.danadresse.dk")) @conn = Faraday.new(url: base_url, headers: { "X-Api-Key" => api_key }) do |f| f.options.open_timeout = 3 f.options.timeout = 10 # Kun netværksfejl og timeouts; 429 håndteres nedenfor, så quota_exceeded ikke gentages f.request :retry, max: MAX_RETRIES, interval: 0.5, backoff_factor: 2, exceptions: [Faraday::ConnectionFailed, Faraday::TimeoutError] f.response :json end end def autocomplete(q, per_side: 5) get("/autocomplete", q: q, per_side: per_side) end def adresse(id) get("/adresser/#{id}") end def vask(betegnelse) get("/datavask/adresser", betegnelse: betegnelse) end def reverse(lon, lat) get("/adgangsadresser/reverse", x: lon, y: lat) end private def get(path, params = {}) attempt = 0 loop do res = @conn.get(path, params) return res.body if res.success? body = res.body.is_a?(Hash) ? res.body : {} # DAWA-stier: {type,title,details,code} /api/v1: {error:{code,message}} err = body["error"].is_a?(Hash) ? body["error"] : body ra = res.headers["retry-after"] retry_after = ra&.match?(/\A\d+\z/) ? ra.to_i : nil if [429, 503].include?(res.status) && retry_after && attempt < MAX_RETRIES attempt += 1 sleep([retry_after, 30].min) next end raise Error.new(res.status, err["code"], err["title"] || err["message"] || "HTTP #{res.status}", retry_after, err["details"]) end end end end
f.response :json parser JSON-svar til en Hash, også ved fejl, så res.body["code"] virker direkte.
3. I Rails
Opret klienten ét sted og brug den fra modeller, jobs og controllers:
# config/initializers/danadresse.rb require Rails.root.join("lib/danadresse") # app/models/kunde.rb class Kunde < ApplicationRecord validate :adresse_findes, if: :adresse_id_changed? private def adresse_findes Danadresse::Client.new.adresse(adresse_id) rescue Danadresse::Error => e errors.add(:adresse_id, "findes ikke") if e.code == "not_found" raise unless e.code == "not_found" end end
Gem adressens id i stedet for kun teksten. Med id'et kan du altid hente den aktuelle adresse. Se Gem adresse-id'et.
Fejlkoder
| Status | code | Handling |
|---|---|---|
| 401 | missing_api_key, invalid_api_key | Tjek miljøvariablen. Prøv ikke igen |
| 403 | key_blocked, sandbox_scope, ip_not_allowed | Ret nøglen eller planen |
| 404 | not_found | Id'et findes ikke |
| 429 | rate_limit_per_minute | Klienten venter Retry-After |
| 429 | quota_exceeded | Gratis: månedskvoten og bufferen er brugt. Betalt: budgetloftet er nået. Prøv aldrig igen. e.details["resets_at"] fortæller, hvornår der svares igen. Se Grænser, kvoter og 429 |
| 500 | internal_error | En fejl hos os. Log den, og prøv igen senere |
| 503 | upstream_unavailable | Et offentligt register er nede. Klienten venter |
Datavask
vask returnerer kategorien fra topniveau (A, B eller C), antallet af kandidater, afstand og den bedste adresse. Kategorien findes kun på topniveau, og der er intet score-felt. A er en præcis match. B er præcis én sikker match med små stavefejl: gem også den, med den officielle tekst. sikker? er sand for A og B. C er usikker: vis kandidaterne, og lad brugeren vælge. I eksemplet giver "Strandvejen 100, 2900 Hellerup" A med én kandidat, og "Rentemestervej 8, 2. tv, 2400 København NV" giver C med syv, fordi det nummer ikke har en 2. tv. betegnelse læser selv etage og dør.
Prøv kaldet fra terminalen:
curl -s -G "https://api.danadresse.dk/datavask/adresser" \ --data-urlencode "betegnelse=Strandvejen 100, 2900 Hellerup" \ -H "X-Api-Key: $DANADRESSE_API_KEY" | jq '{kategori, kandidater: (.resultater | length)}'
Næste skridt
- Fejlformatet: DAWA-stier og /api/v1
- Grænser, kvoter og 429
- Samme klient i Python: Python-klient
Relaterede guides
Markdown-kilde Fejl i guiden? Skriv til os API-reference (Swagger)