Es war Jahre her, dass ich an einem Fehler wirklich festhing. An der Sorte, bei der man den Laptop zuklappt, ihn eine Stunde später wieder öffnet, und das Problem sitzt immer noch da und grinst einen an.
Dieser hier dauerte zehn Stunden. Es war außerdem mein erstes Java/Kotlin-Projekt, und das gehört zur Geschichte dazu. Ich muss zugeben: Mir hat das Rätsel gefallen. Dieses Gefühl, etwas zu umkreisen, das eigentlich unmöglich sein sollte, und es dann langsam in die Enge zu treiben, ist ein großer Teil dessen, warum ich überhaupt zu diesem Beruf gekommen bin.
Der Fehler: Bild-Uploads zu Cloudflare R2 scheiterten immer wieder mit einem 500. Jede Konfigurationsdatei war korrekt. Jede Umgebungsvariable war gesetzt. Trotzdem zeigte die App auf den falschen Server. Hier steht, was mich die Suche gelehrt hat: Lektionen, die für jedes Backend gelten, mit einer Prise Spring Boot.
Die Ausgangslage
Ein Spring-Boot-Service lädt Immobilienfotos zu Cloudflare R2 hoch (S3-kompatibler Speicher). Die Uploads lieferten HTTP 500. Der Stacktrace zeigte auf eine Bibliothek zur Bildskalierung, die eine UnsupportedFormatException warf. Der Schuldige schien klar: Skalierung entfernen, neu deployen, fertig.
Fertig war gar nichts. Das war Lektion null: Der lauteste Fehler ist nicht immer der echte.
Lektion 1: Prüfen Sie, was tatsächlich läuft
Der Skalierungscode war im Quelltext längst gelöscht. Trotzdem trat der Absturz weiter auf, mit frischen Zeitstempeln. Der Grund war beschämend einfach: Die laufende .jar war ein alter Build. Jede Korrektur der vorangegangenen Stunden steckte in Quelldateien, die nie in das ausgelieferte Artefakt kompiliert worden waren.
Eine Konfigurationsdatei auf der Platte, eine korrigierte Zeile in application.yml, eine Korrektur in einer .kt-Datei: Nichts davon zählt, bevor es gebaut und der Prozess aus dem neuen Build neu gestartet ist. Wenn das Verhalten Ihrem Quelltext widerspricht, sollte Ihre erste Frage lauten: Führt der Prozess diesen Code überhaupt aus?
So prüfen Sie das konkret:
- Stellen Sie sicher, dass der Zeitstempel des Build-Artefakts neuer ist als Ihre letzte Änderung.
- Lesen Sie die Konfiguration im laufenden Artefakt. Bei einer Spring-Boot-Jar:
unzip -p app.jar BOOT-INF/classes/application.yml. - Durchsuchen Sie die Live-Logs mit grep nach dem Symptom, gefiltert auf einen aktuellen Zeitraum.
Lektion 2: Maßgeblich ist die Umgebung des Prozesses
Sobald die Jar frisch war, tauchte ein zweiter Fehler auf. Die App protokollierte als Speicher-Endpoint localhost:9000 statt der R2-URL, obwohl die Umgebungsvariable eindeutig in der .env-Datei gesetzt war, die der Service lud.
Also habe ich nachgesehen, was der Prozess tatsächlich hatte:
Bash
cat /proc/<pid>/environ | tr '\0' '\n' | grep -i storage Die Variable stand da, in der Umgebung des Prozesses. Und trotzdem löste Spring den Standardwert localhost auf. Ein solches Ergebnis fühlt sich unmöglich an, und „unmöglich“ ist ein Signal dafür, dass Sie die falsche Ebene testen.
Lektion 3: Erzwingen Sie den Wert mit der höchsten Priorität, um den Fehler einzugrenzen
Wenn sich die Auflösung der Konfiguration falsch verhält und Sie nicht sehen, warum, hören Sie auf, Konfiguration zu lesen, und machen Sie ein Experiment, das sich nicht überschreiben lässt. In Spring stehen JVM-Systemeigenschaften (-D) an der Spitze der Rangfolge, über Umgebungsvariablen und YAML-Standardwerten:
Bash
java -Dapp.storage.endpoint=https://...r2.cloudflarestorage.com -jar app.jar Die App protokollierte sofort den R2-Endpoint. Dieser eine Test teilte das Problem sauber in zwei Hälften: Die Jar und der Anwendungscode waren in Ordnung; der Fehler lag vollständig darin, wie der vom Service-Manager gestartete Prozess eine Umgebungsvariable an eine Property band. Ich hatte stundenlang den Code verdächtigt. Das Experiment hat ihn in fünfzehn Sekunden entlastet.
Lektion 4: Eine funktionierende Lösung und eine perfekte Lösung sind zwei verschiedene Dinge
Die Lösung bestand darin, die Speicherwerte als -D-Systemeigenschaften direkt in die systemd-Unit einzutragen und so zu umgehen, was auch immer die Bindung der Umgebungsdatei verhinderte. Das funktioniert. Elegant ist es nicht: Secrets in ExecStart sind in ps sichtbar, und die eigentliche Frage, warum die Umgebungsvariable nicht gebunden wird, bleibt offen. Eine funktionierende Lösung auszuliefern und das Aufräumen als eigene Aufgabe anzulegen ist trotzdem besser, als blockiert zu bleiben, während man der Eleganz hinterherjagt.
Die Lektion hinter den Lektionen
Der Code war nie kaputt. Zehn Stunden lösten sich in Luft auf, weil jede richtige Korrektur auf etwas angewendet wurde, das gar nicht lief: erst auf ein veraltetes Binary, dann auf einen Mechanismus für Umgebungsvariablen, der stillschweigend nicht band.
Eines gehört ehrlicherweise dazu: Ich habe das mit einem KI-Assistenten durchgearbeitet, und auch er hat die Ursache nicht sofort erkannt. Er brachte einige falsche Theorien ins Spiel (eine Eigenheit verschachtelter Platzhalter, eine Phantom-Properties-Datei, ein überschreibendes Profil), bevor wir bei der Wahrheit ankamen. Was uns tatsächlich weitergebracht hat, waren Experimente, die Möglichkeiten ausschließen konnten: die Live-Umgebung des Prozesses prüfen, die in die laufende Jar eingebackene Konfiguration lesen, den Wert mit einem -D-Flag erzwingen. Daraus folgt eine eigene Lektion: Rechnen Sie bei einem vielschichtigen Fehler damit, sich Schritt für Schritt zur Ursache vorzutesten, mit einem Werkzeug genauso wie allein.
Wenn die Konfiguration korrekt aussieht, das Verhalten aber etwas anderes sagt, befragen Sie direkt den laufenden Prozess: Welches Artefakt führt er aus, welche Umgebung hat er tatsächlich, und was tut er, wenn Sie den Wert an jeder Ebene vorbei erzwingen, die Ihnen etwas vormachen könnte?