Sharkey, 502-Fehler und Timeouts: Wie kaputte WebSocket-Frames meine Instanz abgeschossen haben

Nach meinem Umzug auf einen anderen Server bekam ich immer wieder Fehler im Frontend, weil irgend eine CSS Datei nicht geladen werden könnte, oder auch 502 Fehler im der iOS „Aria“. Aber die eigentliche Ursache war nicht das Frontend. Was es war lest ihr im Artikel.

Das war ein Fall, bei dem man den Browset verdächtigt, den Browsercache leert und kurz über Vite schimpft. Genau so fing es bei fedihub.space an: sporadische 502-Fehler, Timeouts und auf dem iPhone die unschöne Meldung “Importing a module script failed”. Dabei hatte ich mich erst vor kurzem schon wieder mit Docker-Feinheiten beschäftigt, etwa in meinem älteren Beitrag zu automatischen Container-Updates.

Wenn der Browser lügt, musst du tiefer schauen

Das Gemeine an solchen Fehlern: Vorne wirkt alles harmlos. Die JavaScript-Datei ließ sich per curl sauber abrufen, der Server lieferte sie korrekt als application/javascript aus, und auch beim Caching sah erst einmal nichts komplett kaputt aus. Dazu passt, dass Nginx WebSockets grundsätzlich sauber per Upgrade-Header weiterreichen kann, wenn der Reverse Proxy korrekt konfiguriert ist. Das ist kein exotischer Sonderfall, sondern seit Jahren normaler Teil des Stacks.  

Trotzdem blieb das Gefühl: Das eigentliche Problem sitzt nicht im Browser. Es sitzt irgendwo dahinter.

Der erste echte Hinweis stand im Nginx-Log

Erst die Nginx-Logs haben den Nebel etwas gelichtet. Dort tauchte nicht nur der bekannte 502 auf, sondern vor allem dieser Satz:

<code>recv() failed (104: Connection reset by peer) while reading response header from upstream</code>Code-Sprache: HTML, XML (xml)

Das war der Moment, in dem klar wurde: Nginx selbst ist hier nicht der Täter. Der Reverse Proxy meldet nur, dass der Upstream wegbricht. Also nicht „Nginx kaputt“, sondern „Sharkey antwortet nicht mehr“. Genau das ist bei 502-Fehlern die entscheidende Denkbewegung. Der Proxy zeigt den Schmerz nur an, die Ursache liegt oft im Dienst dahinter.

Der Container lief – und startete trotzdem ständig neu

Der nächste Fund war fast noch fieser. Denn der Webcontainer war nicht tot, sondern wirkte auf den ersten Blick gesund:

sharkey-web-1 restart=93 oom=false exit=0 status=runningCode-Sprache: PHP (php)

Kein OOM-Kill, kein eindeutiger Crash mit spektakulärem Exit-Code, keine sofortige rote Warnlampe. Nur eben eine absurd hohe Restart-Zahl. Solche Fälle sind tückisch, weil status=running schnell falsche Sicherheit vermittelt. Der Dienst lebt gerade, aber er stirbt offenbar zwischendurch immer wieder.

Mit diesem Filter war die Spur dann plötzlich glasklar:

docker compose logs --since=12h web \ | grep -Ei 'Invalid WebSocket|RSV|WS_ERR|uncaught|ELIFECYCLE|exit code'Code-Sprache: JavaScript (javascript)

Der eigentliche Auslöser: kaputte WebSocket-Frames

Im Log stand dann der Treffer, den ich vorher eher im Netzwerkgerümpel als im Kernproblem vermutet hätte:

<code>RangeError: Invalid WebSocket frame: RSV1 must be clear RangeError: Invalid WebSocket frame: RSV2 and RSV3 must be clear code: 'WS_ERR_UNEXPECTED_RSV_2_3' The process is going to exit with code 0 ELIFECYCLE Command failed with exit code 7</code>Code-Sprache: JavaScript (javascript)

Damit war die Kette endlich vollständig. Ein ungültiger WebSocket-Request auf /streaming brachte den Sharkey-Webprozess per uncaughtException aus dem Tritt. Der Container startete neu, Nginx bekam in dieser Phase keine verwertbare Antwort mehr, meldete connection reset by peer, und draußen im Browser sah das Ganze dann plötzlich wie ein Modul- oder Asset-Problem aus.

Anders gesagt: Der Browser war nur der schlechteste Zeuge, nicht die Ursache.

So hängt der ganze Fehler zusammen

Genau diesen Zusammenhang fand ich am spannendsten, weil er so leicht in die falsche Richtung führt. Die Kette sah am Ende so aus:

Kaputter WebSocket-Request auf /streaming
→ Sharkey-Webprozess wirft eine unbehandelte Exception
→ Webcontainer startet neu
→ Nginx bekommt ein Reset vom Upstream
→ 502 und Timeouts
→ Browser zeigt Modul- oder Ladefehler

Wer im Fediverse unterwegs ist, stolpert ohnehin häufiger über Software, die viele Protokolle, Clients und Server miteinander verheiraten muss. Dass diese offene, föderierte Struktur viel Flexibilität bringt, aber eben auch mehr Reibungsflächen, beschreibt TechCrunch im Fediverse-Kontext ziemlich treffend.  

Die Behebung lag nicht im Cache, sondern im Proxy

Gelöst habe ich das Problem nicht mit einem Browser-Reset, sondern dort, wo es hingehörte: am /streaming-Endpoint. Ich habe in Nginx nur echte WebSocket-Upgrades zugelassen, die Proxy-Header sauber gesetzt und stumpfe Bots oder Scanner von diesem Pfad ferngehalten.

location /streaming {
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    if ($http_upgrade = "") { return 404; }

    proxy_pass http://127.0.0.1:3050;
}Code-Sprache: PHP (php)

Das ist kein Wundermittel für jede Sharkey-Störung. Aber in diesem Fall war genau das die Stelle, an der aus merkwürdigen Frontend-Symptomen wieder ein stabiler Dienst wurde.

Was ich aus dem Fehler mitnehme

Die Datenbankprüfung war trotzdem nicht umsonst. VACUUM, Dead Tuples und der Blick auf Tabellen wie latest_note oder note waren sinnvoll, nur eben nicht die Hauptursache der 502er. Und genau das macht solche Incidents lehrreich: Du arbeitest dich Schicht für Schicht durch das System, statt dich in der ersten plausiblen Theorie festzufahren.

Am Ende war es kein klassisches Datenbankproblem, kein Safari-Cache und auch kein fehlendes Vite-Asset. Es war ein kaputter WebSocket-Frame, der den Sharkey-Webprozess zuverlässig aus dem Tritt gebracht hat. Und manchmal ist genau das die fieseste Art von Fehler: vorne siehst du nur eine harmlose Ladefehlermeldung, hinten stirbt aber der halbe Dienst.

Schreibe einen Kommentar