fix: Rettungsstation loggt auch abgelehnte Methoden und Protokollfehler

do_POST (und PUT/DELETE/HEAD) riefen reject() auf, ohne eine Logzeile zu
schreiben, und das ueberschriebene log_message() legte damit unbeabsichtigt
auch log_error() lahm (BaseHTTPRequestHandler ruft es standardmaessig auf,
z.B. bei einer kaputten Request-Zeile oder einem Timeout, bevor ueberhaupt
ein Handler wie do_GET erreicht wird). Fuer das einzige Diagnosewerkzeug im
Rettungsfall darf keine Anfrage stumm bleiben.

do_POST loggt jetzt wie do_GET explizit, und log_error() ist separat
ueberschrieben statt implizit mit log_message() stillgelegt zu werden.

Die neuen Tests fuer den 405-Fall lesen den rohen Datei-Deskriptor des
Server-Prozesses statt ueber TextIOWrapper.readline() zu gehen - Letzteres
kann mehr Zeilen aus der Pipe in den eigenen Puffer lesen, als es
zurueckgibt, was select() auf dem naechsten Datei-Deskriptor faelschlich
"nichts da" melden liess. Der Test fuer die kaputte Request-Zeile ruft
log_error() direkt auf: ein echter kaputter Rohsocket wird in dieser
Sandbox von einer Netzwerkschicht abgefangen, bevor er den Server erreicht
(sichtbar an einem untergeschobenen "Server: aiohttp"-Header).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Michael Sauer
2026-09-13 01:29:56 +02:00
co-authored by Claude Sonnet 5
parent 435884039b
commit 1d7d087cff
2 changed files with 90 additions and 0 deletions
+14
View File
@@ -138,12 +138,26 @@ class Handler(BaseHTTPRequestHandler):
def log_message(self, fmt: str, *args) -> None: # noqa: D102 - quieter default log
pass
def log_error(self, fmt: str, *args) -> None:
# BaseHTTPRequestHandler.log_error() calls log_message() by default, which we
# silence above in favour of our own log() calls - but that also silences it for
# requests that never reach do_GET/do_POST (a malformed request line, a client
# timeout mid-request). The station is the only diagnostic tool available during a
# rescue, so those cases must still produce a line instead of nothing.
try:
client = self.client_address[0]
except (AttributeError, IndexError):
client = "?"
print(f"{now_iso()} {client} ERROR {fmt % args}", flush=True)
def log(self, status: int, nbytes: int, started: float) -> None:
print(f"{now_iso()} {self.client_address[0]} {self.path} {status} "
f"{nbytes}B {time.monotonic() - started:.1f}s", flush=True)
def do_POST(self) -> None:
started = time.monotonic()
self.reject(405, "method not allowed")
self.log(405, 0, started)
do_PUT = do_DELETE = do_HEAD = do_POST