Etikettenvorlage: der Umlaut aus macOS sprengte den Download

macOS legt Dateinamen zerlegt ab - das "ae" in "Lagerplaetze.lbx" ist dort
nicht ein Zeichen, sondern "a" plus ein kombinierender Trema-Akzent. Genau
diese Form landet beim Hochladen in der Datenbank. Starlette schreibt
Header aber als latin-1, und fuer den Akzent gibt es dort nichts - die
Route starb mit UnicodeEncodeError und antwortete 500, obwohl der Name am
Bildschirm voellig harmlos aussah.

Der Header wird jetzt nach RFC 6266 zweigleisig gebaut: filename= traegt
eine ASCII-Fassung als Rueckfall, filename*= den echten Namen
prozentkodiert. Browser bevorzugen filename* und zeigen den Umlaut richtig.

Derselbe Riegel liegt auf dem CSV-Download, wo der frei getippte
Vorlagenname in den Header geht - gleicher Defekt, nur noch nicht
aufgefallen.

Fuer die Download-Route gab es bisher keinen Test; zwei kommen dazu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-10 14:47:52 +02:00
co-authored by Claude Opus 5
parent b3256457d2
commit 94f8ed4db9
2 changed files with 84 additions and 4 deletions
+49 -1
View File
@@ -6,7 +6,9 @@ deshalb ohne HTTP, direkt am Ergebnis.
import io
import json
import unicodedata
import zipfile
from urllib.parse import unquote
import pytest
from fastapi import HTTPException
@@ -23,7 +25,7 @@ from app.models import (
Role,
User,
)
from app.routers.etiketten import _pruefe_lbx, spalten
from app.routers.etiketten import _pruefe_lbx, datei_herunterladen, spalten
from app.services.etiketten import csv_erzeugen, konfiguration_lesen
@@ -351,3 +353,49 @@ def test_vorlage_uebersteht_die_sicherung(db, admin, kabel):
neue_fd = zweite.query(FieldDefinition).filter(FieldDefinition.label == "Länge").one()
assert feld_spalte == f"feld:{neue_fd.id}"
zweite.close()
# --------------------------------------------------------------------------
# Download der hinterlegten .lbx
# --------------------------------------------------------------------------
def _disposition_pruefen(antwort, erwarteter_name):
"""Header muss sich senden lassen UND den echten Namen tragen."""
roh = antwort.headers["content-disposition"]
# Starlette kodiert Header als latin-1. Wirft das hier, antwortet die
# Route im Betrieb mit 500 statt mit der Datei.
roh.encode("latin-1")
stern = roh.split("filename*=UTF-8''", 1)[1]
assert unquote(stern) == erwarteter_name
def test_lbx_mit_zerlegtem_umlaut_laesst_sich_herunterladen(db, admin):
"""macOS legt Dateinamen ZERLEGT ab: das "ä" ist dort "a" + U+0308.
Am Bildschirm sieht das wie die zusammengesetzte Form aus, aber nur die
passt in latin-1 - die Kodierung, mit der HTTP-Header geschrieben werden.
Ein Name, den der Dateiwaehler eines Macs liefert, darf den Download
nicht sprengen.
"""
name = unicodedata.normalize("NFD", "Lagerplätze.lbx")
v = _vorlage(db, ["uid"])
v.datei_name = name
v.datei = _zip(["label.xml"])
db.commit()
antwort = datei_herunterladen(vorlage_id=v.id, db=db, _=admin)
assert antwort.body == _zip(["label.xml"])
_disposition_pruefen(antwort, name)
def test_lbx_mit_schlichtem_namen_bleibt_unveraendert(db, admin):
"""Der haeufige Fall darf von der Umstellung nicht beruehrt werden."""
v = _vorlage(db, ["uid"])
v.datei_name = "kabel.lbx"
v.datei = _zip(["label.xml"])
db.commit()
antwort = datei_herunterladen(vorlage_id=v.id, db=db, _=admin)
assert 'filename="kabel.lbx"' in antwort.headers["content-disposition"]
_disposition_pruefen(antwort, "kabel.lbx")