FHEMWEB: Sporadisch fehlende SVG-Plots mit plotfork

Begonnen von Flachzange, 06 September 2026, 11:57:33

Vorheriges Thema - Nächstes Thema

Flachzange

Hallo zusammen,

bei mir wurden in Räumen mit vielen SVG-Plots immer wieder einzelne Diagramme nicht angezeigt. Nach einem Neuladen war dann häufig ein anderes Diagramm betroffen. Oft waren mehrere Diagramme betroffen.

Das Problem trat sowohl beim direkten Zugriff auf FHEMWEB als auch über einen Reverse-Proxy auf. Dort war passend dazu folgender Fehler zu sehen:

recv() failed (104: Connection reset by peer) while reading response header from upstream

Der entscheidende Test war:

attr WEB plotfork 0
Damit trat das Problem nicht mehr auf. Mit plotfork 1 war es wieder reproduzierbar.

Offenbar beendet sich der Fork-Kindprozess nach der SVG-Ausgabe, ohne dem HTTP/1.1-Client vorher mitzuteilen, dass die Verbindung geschlossen wird. Browser beziehungsweise Proxy können die Verbindung deshalb für den nächsten SVG-Request wiederverwenden und laufen dann sporadisch in einen Connection Reset.

Ich habe testweise in 01_FHEMWEB.pm im Child-Zweig folgende Zeile ergänzt:

} elsif(defined($pid)){                   # child
  delete $hash->{BUF};
  $hash->{isChild} = 1;
  $FW_headerlines .= "Connection: close\r\n";
}

Seitdem laufen die SVGs auch mit plotfork 1 zuverlässig und weiterhin performant. Mehrfaches Neuladen mit vielen parallelen Diagrammen erzeugt weder fehlende Plots noch weitere Connection Resets.

Getestet habe ich mit:

01_FHEMWEB.pm 31325 2026-06-01
plotEmbed 2
plotfork 1

Vielleicht könnte jemand prüfen, ob "Connection: close" bei Antworten aus dem Fork-Kindprozess grundsätzlich ergänzt werden sollte?

Danke und Gruß

Chris

rudolfkoenig

Ich gehe davon aus, dass es an der Kombination Browser/Client liegt, da plotEmbed=2/plotfork=1 seit langem die Voreinstellung ist fuer alle Multiprozessor-Rechner.

Was hast Du fuer ein OS beim Client und beim Server, und was fuer einen Browser verwendet Du?

Kannst Du bitte pruefen, ob das FHEMWEB Attribut closeConn eine Abhilfe ist?


Flachzange

Hallo Rudolf,

ich habe es vorhin getestet. Das Problem tritt damit ebenfalls auf und um ehrlich zu sein überrascht mich das nicht, denn closeConn bildet ja nur das ab, was plotfork 1 im jeweiligen Child-Thread eh macht: den Socket schließen.

Das zugrundeliegende Probleme scheint zu sein, dass die jeweiligen Clients die Verbindungen aus Effizienzgründen wiederverwenden möchten, weil sie davon ausgehen, dass sie noch existieren (was aber nicht der Fall ist).

Das Problem habe ich schon sehr sehr lange, eher Jahre als Monate.

Getestete Clients:
- Firefox z.B. Version 155 unter Windows 10/11
- Edge z.B. Version 152 unter Windows 10/11
- Nginx Proxy Manager 2.15.1 (docker)

Die Tatsache, dass es auch mit NPM als Reverse Proxy auftritt schließt für mich ein spezifisches Client-Problem praktisch aus.

FHEM läuft ebenfalls unter Docker im Image: Derzeit: ghcr.io/fhem/fhem-docker:5-threaded-bookworm

Aber wie gesagt: Ich mache die Beobachtunge schon sehr lange, war nur lange auch zu träge, um mich damit zu beschäftigen.

Sieht dann so bei mir aus:
Du darfst diesen Dateianhang nicht ansehen.

Sidey

Hast Du das Problem auch, dass wenn Du dir die Icons anzeigen lässt, dass dann auch welche Fehlen?


Grüße Sidey
Nutze: SIGNALDuino, Homematic, Raspberry Pi, MQTT, Alexa, Docker, AlexaFhem,zigbee2mqtt, tasmota

Maintainer von: SIGNALduino, SD_WS*, fhem-docker, alexa-fhem-docker, fhempy-docker, WebAuth, fhem-mcp, midea-mqtt, whatsmeow-mqtt, alexa-cookie-service
https://github.com/sidey79?tab=repositories

Flachzange

Welche Icons meinst du? Gefühlt ist das aber eine andere Baustelle, weil die ja vermulich nicht per plotfork geholt werden

rudolfkoenig

Ich habe die Zeile eingebaut, sollte nicht schaden.

Leider kann ich das Problem weder nachstellen (mit Windows und Linux Clients), noch verstehe ich, warum diese Zeile eine Auswirkung hat.
Dass die zu wiederverwendende Verbindung zugemacht ist, sollte der Browser auch ohne Headerzeile merken.
Wieviele Plots sind auf der Seite zu sehen? Meine "Plot-Seite" hat seit vielen Jahren 14, und ich habe dieses Problem noch nie gemerkt.

Sidey

Hallo zusammen,

ich verfolge seit dem Frühjahr ein Problem, das ich bisher für ein reines
Reverse-Proxy-Thema gehalten habe. Durch diesen Thread ist mir klar
geworden, dass dahinter derselbe Mechanismus steckt wie bei den fehlenden
Plots hier, nur an einer anderen Stelle im Code. Weil rudolfkoenig
schreibt, dass er es nicht reproduzieren kann, schildere ich den Weg
dorthin etwas ausführlicher — vielleicht hilft die Kette bei der
Nachstellung.

Mein Aufbau: FHEM im Docker-Container, FHEMWEB auf Port 8083 mit HTTPS,
davor Caddy als Reverse Proxy, dahinter noch eine Authentifizierung.
Betroffen sind bei mir keine Plots, sondern ganz normale statische Bilder
unter /fhem/images/*, am deutlichsten beim Anzeigen vieler Icons.

Wie ich darauf gekommen bin

Aufgefallen ist es zuerst im Proxy-Log. Über 24 Stunden hatte ich 1041
Requests an FHEM, davon 145 mit Status 502. Die Fehlerursachen waren fast
ausschließlich zwei:

  89x  http: server closed idle connection
  56x  EOF

Im FHEM-Log stand zu diesen Zeitpunkten nichts Auffälliges, keine
Abstürze, FHEM lief durch. Die Fehler lagen also auf der Verbindung
zwischen Proxy und FHEM, nicht in FHEM selbst.

Meine erste Vermutung war ein Idle-Timeout: Der Proxy hält eine
Verbindung länger offen als FHEM, greift auf eine bereits geschlossene
zurück, fertig. Diese Erklärung ist aber gescheitert. Nach einem
Neustart des Proxys kamen die ersten 502 bereits nach rund 20 Sekunden —
viel zu früh für ein Timeout. Auch das Verkürzen der Keepalive-Zeit auf
der Proxy-Seite hat nichts geändert. Zeit war also nicht der Auslöser.

Reproduzierbar wurde es erst über Parallelität. Einzeln aufgerufen
liefert jede der betroffenen Bild-URLs sauber 200. Im Lasttest gegen
dieselben URLs:

  10 parallel: 160/160 OK
  20 parallel: 160/160 OK
  30 parallel: 160/160 OK
  40 parallel: Fehler

Das passt zum Alltagsbild: Ein einzelnes Icon im Browser aufrufen geht
immer, eine Seite mit vielen Icons auf einmal nicht.

Anschließend habe ich FHEMWEB temporär auf verbose 5 gesetzt und die
Logs beider Seiten über dasselbe Zeitfenster korreliert. Der
aufschlussreichste Teil war das FHEM-Log selbst:

  Connection accepted from WEB_<ip>_50300
  GET /fhem/images/default/<datei>.png HTTP/1.1
  WEB_<ip>_50300 GET /fhem/images/default/<datei>.png; BUFLEN:0
  Connection closed for WEB_<ip>_50300: EOF
  Connection accepted from WEB_<ip>_50306
  GET /fhem/images/default/<datei>.png HTTP/1.1
  WEB_<ip>_50306 GET /fhem/images/default/<datei>.png; BUFLEN:0
  Connection closed for WEB_<ip>_50306: EOF
  ...

Also eine eigene TCP-Verbindung pro Bild, Quellports fortlaufend
hochzählend, keine einzige Wiederverwendung — obwohl in jeder
Anfragezeile HTTP/1.1 steht.

Die Ursache im Code

FHEMWEB antwortet in der Statuszeile immer mit HTTP/1.1, sendet dabei
aber nie einen Connection-Header. Ich habe das gegen mein laufendes FHEM
gegengeprüft; in der Antwort gibt es kein Connection-Feld.

Gleichzeitig entscheidet FW_closeConn in 01_FHEMWEB.pm so:

  if(!$FW_httpheader{Connection} || $cc) {
      TcpServer_Close($hash, 1, !$hash->{inform});
  }

Die Verbindung wird also genau dann geschlossen, wenn der eingehende
Request kein Connection-Feld enthielt. Auf den Wert kommt es nicht an,
nur auf die Anwesenheit.

Und genau das ist bei Proxies mit Upstream-Keepalive der Normalfall:
Caddy sendet bei HTTP/1.1 keinen expliziten Connection-Header, weil
Keep-Alive dort der Default ist. Die verbreitete nginx-Rezeptur mit
"proxy_http_version 1.1" plus "proxy_set_header Connection ''" entfernt
ihn ebenfalls. FHEM sieht also kein Connection-Feld, schließt die
Verbindung — und teilt das der Gegenseite nicht mit.

Der Zusammenhang mit diesem Thread

Damit ist es aus meiner Sicht dieselbe Ursache, die Flachzange
beschrieben hat, nur eine Ebene allgemeiner. Bei den Plots endet die
Verbindung, weil sich der geforkte Kindprozess nach der Ausgabe beendet.
Bei mir endet sie, weil FW_closeConn sie schließt. In beiden Fällen
enthält die Antwort kein "Connection: close", und in beiden Fällen läuft
die Gegenseite in einen Reset beim Wiederverwenden.

Deshalb greift der Fix in r31631 für meinen Fall nicht. Er setzt

  $FW_headerlines .= "Connection: close\r\n"; # 145413
ausschließlich im Kindprozess-Zweig nach dem fork. Für die Plots löst
das die Sache. Für alle übrigen Antworten bleibt es dabei, dass FHEM die
Verbindung schließt, ohne es anzukündigen.

Ergänzend zur Einordnung als Windows-Thema: Bei mir hängen die Fehler am
Proxy-Upstream und sind vom Betriebssystem des Browsers unabhängig. Ich
sehe sie auch dann, wenn gar kein Browser beteiligt ist, sondern nur ein
Lasttest gegen die URLs läuft.

Warum der Header überhaupt etwas ändert

rudolfkoenig hatte gefragt, warum das Setzen des Headers das Verhalten
beeinflusst, wo der Client ein Schließen doch selbst erkennen sollte.

Es ist eine Race Condition, kein Erkennungsproblem. Der Proxy nimmt eine
Verbindung aus seinem Keepalive-Pool und schreibt den nächsten Request
darauf, ohne sie vorher zu prüfen. Ob das FIN vom Server zu diesem
Zeitpunkt schon eingetroffen und verarbeitet ist, ist reine
Timing-Frage — und unter Last geht das Rennen eben manchmal verloren.
Genau deshalb verlangt RFC 7230 Abschnitt 6.6, dass ein Server, der nach
der Antwort schließt, das vorher mit "Connection: close" ankündigt. Ohne
diese Ankündigung kann der Client die Wiederverwendung nicht zuverlässig
vermeiden. Das erklärt auch, warum der Fehler nur sporadisch und
lastabhängig auftritt und beim Einzelaufruf nie.

Vorschlag

"Immer Connection: close" wäre die falsche Richtung. Wiederverwendete
Verbindungen reduzieren die Last spürbar — pro Verbindung entfallen der
TCP-Handshake und bei HTTPS der komplette TLS-Handshake, und gerade beim
Seitenaufbau mit vielen kleinen Requests ist das der teuerste Anteil.
Das Ziel sollte sein, Keep-Alive korrekt zu unterstützen, nicht es
abzuschalten.

Der eigentliche Punkt ist die Bedingung selbst. Entschieden wird allein
anhand der Anwesenheit eines Connection-Headers im Request. Das ist
HTTP/1.0-Logik: Dort war Schließen der Default und man musste
"Connection: keep-alive" explizit anfordern. Unter HTTP/1.1 ist es genau
umgekehrt — die Verbindung ist per Default persistent, und
"Connection: close" ist die Ausnahme. FHEMWEB antwortet aber überall mit
HTTP/1.1.

Praktisch führt das zu einem verkehrten Ergebnis: Ein Proxy, der
"Connection: close" schickt, weil er wirklich schließen will, hält die
Verbindung nach dieser Logik offen. Ein Proxy, der nichts schickt, weil
er nach HTTP/1.1 Keep-Alive erwartet, bekommt sie geschlossen — ohne
Ankündigung.

Interessant dabei: Die HTTP-Version wird in FW_Read bereits geparst

  my ($method, $arg, $httpvers) = split(" ", $FW_httpheader[0], 3)
aber $httpvers wird im ganzen Modul nirgendwo ausgewertet. Die nötige
Information liegt also schon vor.

Mein Vorschlag wäre daher zweistufig.

Stufe 1, reiner Bugfix, ohne Änderung am Lastverhalten: Überall dort, wo
FHEMWEB die Verbindung anschließend tatsächlich schließt, vorher
"Connection: close" mitsenden. Das behebt die sporadischen Resets
unabhängig davon, ob geforkt wurde. Umsetzen ließe sich das an einer
einzigen Stelle, weil $FW_headerlines nur einmal aufgebaut wird und
danach in alle Antwortpfade einfließt.

Stufe 2, optional und zur Diskussion: Die Persistenzentscheidung nach
HTTP-Semantik treffen statt nach bloßer Header-Anwesenheit, etwa so:

  my $conn = $FW_httpheader{Connection};
  my $keepAlive = ($httpvers && $httpvers =~ m/1\.1/)
                    ? (!$conn || $conn !~ m/close/i)
                    : ( $conn && $conn =~ m/keep-alive/i);

Das Ergebnis würde einmal ermittelt, am Connection-Hash abgelegt und
sowohl für den Response-Header als auch für die Entscheidung in
FW_closeConn verwendet. Dann können Ankündigung und tatsächliches
Verhalten nicht mehr auseinanderlaufen.

Der Nebeneffekt wäre, dass HTTP/1.1-Clients und Proxies erstmals
zuverlässig Keep-Alive bekommen, statt pro Request eine neue Verbindung
aufzumachen — mein verbose-5-Log oben zeigt ja, dass das heute nicht
passiert.

Zwei Punkte wären dabei zu beachten: Der bestehende Sonderfall über das
Attribut closeConn und die iOS-Erkennung am User-Agent muss weiterhin
Vorrang haben und das Keep-Alive überstimmen können. Und mehr offen
gehaltene Verbindungen bedeuten mehr gleichzeitig belegte
Dateideskriptoren — falls das ein Bedenken ist, wäre Stufe 1 allein
bereits ausreichend, um den Fehler zu beheben.

Ich teste gern beide Varianten in meinem Setup gegen, dort ist der
Fehler mit dem parallelen Bildabruf zuverlässig reproduzierbar.


Grüße Sidey
Nutze: SIGNALDuino, Homematic, Raspberry Pi, MQTT, Alexa, Docker, AlexaFhem,zigbee2mqtt, tasmota

Maintainer von: SIGNALduino, SD_WS*, fhem-docker, alexa-fhem-docker, fhempy-docker, WebAuth, fhem-mcp, midea-mqtt, whatsmeow-mqtt, alexa-cookie-service
https://github.com/sidey79?tab=repositories

Flachzange

Zitat von: rudolfkoenig am 08 September 2026, 08:25:40Ich habe die Zeile eingebaut, sollte nicht schaden.

Leider kann ich das Problem weder nachstellen (mit Windows und Linux Clients), noch verstehe ich, warum diese Zeile eine Auswirkung hat.
Dass die zu wiederverwendende Verbindung zugemacht ist, sollte der Browser auch ohne Headerzeile merken.
Wieviele Plots sind auf der Seite zu sehen? Meine "Plot-Seite" hat seit vielen Jahren 14, und ich habe dieses Problem noch nie gemerkt.

Dank dir. Bei mir sind es auf der einen Seite 17 Plots. Dort konnte ich es zuverlässig reproduzieren.