fix(updates): neem het webadres van je releases ook aan
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 4m23s

Het adres uit de adresbalk van Forgejo of GitHub geeft HTML terug, en de
enige melding was een JSONDecodeError. Dat adres wordt nu omgerekend naar
/api/v1/repos/...; komt er toch HTML, dan zegt de melding wat er mis is.
Een https://-schema voor het imageveld wordt weggehaald, want een
image-adres heeft er nooit een.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9Vcwu7owJZ3nj5dCf4VFT
This commit is contained in:
Ramon 2026-08-07 15:13:53 +02:00
parent b4a41afc1c
commit 5068997599
6 changed files with 181 additions and 7 deletions

View file

@ -1,7 +1,18 @@
# v0.8.30-beta — In ontwikkeling
# v0.8.30-beta — De updater neemt genoegen met het adres uit je adresbalk
Nog niets uitgebracht sinds v0.8.29. Wat hier onder komt te staan gaat mee in
de volgende uitgave; bij het uitbrengen wordt deze kop de releaseversie.
Wie in Forgejo naar zijn releases keek en dat adres in de instellingen plakte,
kreeg een webpagina in plaats van JSON — en als enige melding
`JSONDecodeError: Expecting value: line 1 column 1 (char 0)`. Daar leidt
niemand uit af dat er `/api/v1/repos/` in het adres had moeten staan.
- **Het adres wordt omgerekend.** `https://git.example.nl/bes-r/server-up` en
`.../releases` worden allebei het juiste API-adres; GitHub krijgt zijn eigen
`api.github.com`. Een adres dat al klopt of dat we niet herkennen blijft staan.
- **Een begrijpelijke melding** als er tóch HTML terugkomt, met wat er dan wél
in het veld hoort.
- **`https://` voor het imageveld wordt weggehaald.** Een image-adres heeft
nooit een schema, dus `docker pull https://…` faalde op iets wat er in het
veld goed uitzag.
# v0.8.29 — UniFi start weer op een Raspberry Pi 4

View file

@ -82,6 +82,15 @@ def image_repo(info: dict | None = None) -> str:
"""
expliciet = (cfg.load().get("UPDATE_IMAGE") or "").strip()
if expliciet:
# Een image-adres heeft nooit een schema. Wie het uit zijn adresbalk
# plakt neemt `https://` mee, en dan faalt `docker pull` op iets wat er
# in het veld goed uitziet. Weghalen is hier eenduidig: er bestaat geen
# geldig image dat zo begint.
for schema in ("https://", "http://"):
if expliciet.lower().startswith(schema):
expliciet = expliciet[len(schema):]
break
expliciet = expliciet.strip("/")
return expliciet.rsplit(":", 1)[0] if _has_tag(expliciet) else expliciet
img = (info or inspect_self()).get("image", "")
if not img:

View file

@ -78,11 +78,46 @@ def image_tag(release_tag: str) -> str:
return tag[1:] if tag.startswith("v") and _parse(tag) else tag
def api_url(url: str) -> str:
"""Maak van een geplakt adres het API-adres.
Wie in Forgejo of GitHub naar zijn releases kijkt en dat adres uit de
adresbalk plakt, geeft het adres van een wébpagina op. Dat levert HTML op,
en de enige melding die je daarna kreeg was `JSONDecodeError: Expecting
value: line 1 column 1` waaruit niemand opmaakt dat er `/api/v1/` had
moeten staan. Dus rekenen we het om in plaats van erover te vallen.
https://git.voorbeeld.nl/bes-r/server-up/releases
https://git.voorbeeld.nl/api/v1/repos/bes-r/server-up/releases
https://github.com/bes-r/server-up/releases
https://api.github.com/repos/bes-r/server-up/releases
"""
schoon = (url or "").strip().rstrip("/")
if not schoon or "/api/" in schoon:
return schoon
m = re.match(r"^(https?)://([^/]+)/([^/]+)/([^/]+?)(?:/releases.*|\.git)?$",
schoon)
if not m:
return schoon
schema, host, eigenaar, repo = m.groups()
if host.lower() in ("github.com", "www.github.com"):
return f"https://api.github.com/repos/{eigenaar}/{repo}/releases"
return f"{schema}://{host}/api/v1/repos/{eigenaar}/{repo}/releases"
def _releases(url: str) -> list[dict]:
req = urllib.request.Request(
url, headers={"Accept": "application/json", "User-Agent": "server-up"})
with urllib.request.urlopen(req, timeout=8) as r:
data = json.loads(r.read().decode("utf-8"))
rauw = r.read().decode("utf-8", "replace")
try:
data = json.loads(rauw)
except ValueError:
# Bijna altijd een webpagina in plaats van de API. Zeg dát, in plaats
# van de gebruiker met een JSON-fout te laten zitten.
raise ValueError(
f"{url} geeft geen JSON terug maar een webpagina. Vul het "
"API-adres in, met /api/v1/repos/ erin.") from None
return data if isinstance(data, list) else [data]
@ -98,7 +133,7 @@ def check(current: str, force: bool = False) -> dict:
return dict(_CACHE)
c = cfg.load()
url = (c.get("UPDATE_API_URL") or "").strip()
url = api_url(c.get("UPDATE_API_URL") or "")
ch = channel()
res = {"enabled": bool(url), "current": current, "channel": ch,
"current_is_prerelease": is_prerelease(current),

View file

@ -138,7 +138,7 @@
"release_notes": "Release notes",
"view_release": "View release",
"update_api_url": "Releases API URL",
"update_api_hint": "Forgejo/GitHub Releases API returning the latest release. Empty = disabled.",
"update_api_hint": "Address of your releases. You may paste the address from your browser; Server Up converts it to the API address with /api/v1/repos/ itself. Empty = disabled.",
"include_prerelease": "Include pre-releases (beta)",
"login": "Sign in",
"login_failed": "Sign-in failed",

View file

@ -138,7 +138,7 @@
"release_notes": "Release notes",
"view_release": "Bekijk release",
"update_api_url": "Releases-API URL",
"update_api_hint": "Forgejo/GitHub Releases-API die de laatste release teruggeeft. Leeg = uitgeschakeld.",
"update_api_hint": "Adres van je releases. Je mag het adres uit de adresbalk plakken; Server Up rekent het zelf om naar het API-adres met /api/v1/repos/. Leeg = uitgeschakeld.",
"include_prerelease": "Pre-releases (beta) meenemen",
"login": "Inloggen",
"login_failed": "Inloggen mislukt",

119
tests/test_update_adres.py Normal file
View file

@ -0,0 +1,119 @@
"""Het adres van de releases-API en van het image, zoals mensen ze invullen.
Wie in Forgejo naar zijn releases kijkt en dat adres uit de adresbalk plakt,
geeft het adres van een webpagina op. De enige melding die daarop volgde was
`JSONDecodeError: Expecting value: line 1 column 1 (char 0)`, en daar leidt
niemand uit af dat er `/api/v1/repos/` in had moeten staan.
"""
import sys
from pathlib import Path
import pytest
WORTEL = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(WORTEL / "server-up"))
# ── Het releases-adres ───────────────────────────────────────────────────────
@pytest.mark.parametrize("ingevuld,verwacht", [
# Wat je uit de adresbalk van Forgejo plakt.
("https://git.voorbeeld.nl/bes-r/server-up/releases",
"https://git.voorbeeld.nl/api/v1/repos/bes-r/server-up/releases"),
# Zonder /releases erachter.
("https://git.voorbeeld.nl/bes-r/server-up",
"https://git.voorbeeld.nl/api/v1/repos/bes-r/server-up/releases"),
# Met poort, en http.
("http://10.0.20.22:3000/bes-r/server-up/releases",
"http://10.0.20.22:3000/api/v1/repos/bes-r/server-up/releases"),
# De kloon-URL.
("https://git.voorbeeld.nl/bes-r/server-up.git",
"https://git.voorbeeld.nl/api/v1/repos/bes-r/server-up/releases"),
# GitHub heeft zijn API op een eigen host.
("https://github.com/bes-r/server-up/releases",
"https://api.github.com/repos/bes-r/server-up/releases"),
])
def test_webadres_wordt_een_apiadres(env, ingevuld, verwacht):
from core import updater
assert updater.api_url(ingevuld) == verwacht
@pytest.mark.parametrize("al_goed", [
"https://git.voorbeeld.nl/api/v1/repos/bes-r/server-up/releases",
"https://api.github.com/repos/bes-r/server-up/releases",
])
def test_een_apiadres_blijft_ongemoeid(env, al_goed):
from core import updater
assert updater.api_url(al_goed) == al_goed
def test_leeg_blijft_leeg(env):
from core import updater
assert updater.api_url("") == ""
assert updater.api_url(" ") == ""
def test_onherkenbaar_adres_blijft_staan(env):
"""Niet slimmer doen dan we zijn: wat we niet herkennen laten we met rust,
zodat een eigen proxy-adres blijft werken."""
from core import updater
raar = "https://voorbeeld.nl/een/diep/pad/met/releases.json"
assert updater.api_url(raar) == raar
def test_html_geeft_een_begrijpelijke_fout(env, monkeypatch):
"""De melding moet zeggen wát er mis is, niet dat JSON niet parseert."""
from core import updater
class _Antwoord:
def read(self):
return b"<!DOCTYPE html>\n<html lang=\"en-US\">"
def __enter__(self):
return self
def __exit__(self, *a):
return False
monkeypatch.setattr(updater.urllib.request, "urlopen",
lambda *a, **k: _Antwoord())
with pytest.raises(ValueError) as fout:
updater._releases("https://git.voorbeeld.nl/bes-r/server-up/releases")
melding = str(fout.value)
assert "webpagina" in melding
assert "/api/v1/repos/" in melding
assert "JSONDecode" not in melding
def test_check_gebruikt_het_omgerekende_adres(env, monkeypatch):
from core import updater
env["core"].patch({"UPDATE_API_URL":
"https://git.voorbeeld.nl/bes-r/server-up/releases"})
gebruikt = []
monkeypatch.setattr(updater, "_releases",
lambda u: gebruikt.append(u) or [
{"tag_name": "v9.9.99", "prerelease": False}])
res = updater.check("0.8.28", force=True)
assert gebruikt == [
"https://git.voorbeeld.nl/api/v1/repos/bes-r/server-up/releases"]
assert res["update_available"] is True
# ── Het imageadres ───────────────────────────────────────────────────────────
def test_schema_voor_het_image_wordt_weggehaald(env):
"""`docker pull https://…` bestaat niet, maar in het veld ziet het er goed
uit. Er is geen geldig image dat met een schema begint, dus weghalen kan."""
from core import selfupdate
env["core"].patch({"UPDATE_IMAGE": "https://git.voorbeeld.nl/bes-r/server-up"})
assert selfupdate.image_repo({}) == "git.voorbeeld.nl/bes-r/server-up"
env["core"].patch({"UPDATE_IMAGE": "http://10.0.20.22:3000/bes-r/server-up/"})
assert selfupdate.image_repo({}) == "10.0.20.22:3000/bes-r/server-up"
def test_image_zonder_schema_blijft_gelijk(env):
from core import selfupdate
env["core"].patch({"UPDATE_IMAGE": "git.voorbeeld.nl/bes-r/server-up"})
assert selfupdate.image_repo({}) == "git.voorbeeld.nl/bes-r/server-up"
# Een tag hoort er niet in; die komt van de release.
env["core"].patch({"UPDATE_IMAGE": "git.voorbeeld.nl/bes-r/server-up:0.8.28"})
assert selfupdate.image_repo({}) == "git.voorbeeld.nl/bes-r/server-up"