fix(api): leere Listen als [] statt null -- Kalender blieb weiss

Ein Konto ohne Kalender bekam auf GET /api/v1/calendars die Antwort
`null`, und die Ansicht brach ab:

    for (const cal of calendars)   ->  "e is not iterable"

Nicht der Kalender ist schuld, sondern eine Eigenheit, die jeden
Listenendpunkt betrifft: In Go ist eine Liste ohne Eintraege oft nil,
und encoding/json schreibt dafuer null statt []. Jede Store-Funktion,
die mit `var out []T` anfaengt, liefert das bei leerem Ergebnis --
Favoriten, Papierkorb, Versionen, Freigaben, Etiketten. Der Kalender war
nur der, der daran zerbricht.

Deshalb die Regel dort, wo alle Antworten durchlaufen: response.JSON
macht aus einer nil-Liste eine leere. Einzelne Objekte und nil bleiben
unberuehrt. In den Stores waere es die falsche Stelle -- es gibt
Dutzende Listenendpunkte, und der naechste faengt wieder mit
`var out []T` an. Eine Regel, an die man sich erinnern muss, ist keine.

Dazu zwei Dinge, die derselbe Fall ans Licht brachte:

  - Der Web-Weg legte, anders als CalDAV, keinen Standardkalender an.
    Ein neues Konto stand vor einer leeren Ansicht und musste erraten,
    dass es selbst einen anlegen soll -- waehrend ein CalDAV-Client, der
    sich vorher verbunden hatte, bereits einen sah, den die Oberflaeche
    nicht zeigte.
  - Die Ansicht faengt eine unerwartete Antwort jetzt selbst ab (?? []).
    Der Server schickt kein null mehr; eine Seite sollte daran trotzdem
    nicht zerbrechen.

Drei Tests in pkg/response: leere Liste wird [], befuellte bleibt
unveraendert, Objekte und nil bleiben, was sie sind.

Zur Korrektur meiner frueheren Erklaerung: Die fehlenden Cache-Header
waren ein echtes Problem, aber nicht die Ursache dieses Fehlers. Der
gemeldete Bundle-Name stimmte diesmal mit dem ausgelieferten ueberein --
daran war zu sehen, dass es kein alter Stand sein konnte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-09-07 19:56:54 +02:00
1 parent 55dcf517f2
commit fbaf24bdbd
4 files changed
+107 -2

No files matched your search

+26 -1
View File
@@ -3,6 +3,7 @@ package response
import (
"encoding/json"
"net/http"
"reflect"
)
type envelope struct {
@@ -13,7 +14,31 @@ type envelope struct {
func JSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(envelope{Data: data})
json.NewEncoder(w).Encode(envelope{Data: leereListeStattNull(data)})
}
// leereListeStattNull macht aus einer nicht belegten Liste eine leere.
//
// In Go ist eine Liste ohne Einträge oft nil, und `encoding/json` schreibt
// dafür `null` statt `[]`. Jede Store-Funktion, die ihre Liste mit
// `var out []T` beginnt, liefert das bei leerem Ergebnis – und jede
// Oberfläche, die darüber läuft, bricht dann ab.
//
// Genau so ist die Kalenderansicht bei einem Konto ohne Kalender
// weiß geblieben: `for (const cal of calendars)` über `null`.
//
// Der Ort dafür ist hier und nicht in den Stores: Es gibt Dutzende
// Listenendpunkte, und der nächste wird wieder mit `var out []T` anfangen.
// Eine Regel, an die man sich erinnern muss, ist keine.
func leereListeStattNull(data any) any {
if data == nil {
return data
}
v := reflect.ValueOf(data)
if v.Kind() == reflect.Slice && v.IsNil() {
return reflect.MakeSlice(v.Type(), 0, 0).Interface()
}
return data
}
func Error(w http.ResponseWriter, status int, msg string) {