1. Grenzen der API: Wachstum schneller als Blinzeln
Wenn Sie gerade erst anfangen, eine REST API zu entwickeln, gibt es ein angenehmes Gefühl: „Oh, ich kann einen Endpoint für jeden Geschäftswunsch hinzufügen“. Das ist wie der Kauf einer Kiste voller Aufkleber: Man möchte alles damit bekleben – den Laptop, die Tasse, die Katze (die Katze hat nicht darum gebeten, aber wen fragt man schon?). Das Problem ist, dass jeder neue Endpoint nicht nur ein Plus an Funktionalität, sondern auch ein Minus an Wartbarkeit ist: Dokumentation, Tests, Fehlerbehandlung, Kompatibilität, zukünftige Änderungen und dann noch die Fragen anderer: „Warum ist es hier so und dort anders?“
Zu diesem Zeitpunkt ist die Karte von v1 bereits sichtbar. Der Statuswechsel blieb innerhalb von PATCH /tasks/{taskId}, Kommentare gingen in eine Unterressource, und Tags in einen Read-Only-Lookup. Und gerade nach einer solchen Karte möchte man noch Massenoperationen hinzufügen, aber hier ist es wichtig, rechtzeitig innezuhalten: Batch/Bulk ist nicht mehr nur ein kleiner Endpoint, sondern eine separate Komplexität mit Ergebnissen und Fehlern.
In der non-CRUD-Welt ist diese Gefahr besonders groß, weil die „CRUD-Matrix“ Sie nicht mehr zurückhält. Und wenn man sich nicht auf Grenzen einigt, verwandelt sich die API schnell in eine Sammlung von Befehlen: /tasks/bulkComplete, /tasks/changeStatus, /tasks/addCommentAndArchive, /tasks/pleaseMakeMeHappy. Der letzte ist natürlich ein Scherz… obwohl ich Controller gesehen habe, die emotional genau so aussehen.
Unser Ziel ist es, die API kompakt und vorhersagbar zu halten. Und das so, dass Sie beim Öffnen von Swagger oder einfach einer Liste von Pfaden das System verstehen können, anstatt sich wie ein Archäologe zu fühlen, der ein altes „Controller-Alphabet“ gefunden hat.
Drei Prüfungen vor einem neuen Endpoint
Wenn Sie einen neuen non-CRUD-Endpoint hinzufügen, ist das richtige Denkmodell sehr einfach: Wir fügen keine Methode in den Controller ein, wir erweitern den externen Vertrag. Daher ist es nützlich, sich drei Fragen zu stellen. Dies ist kein Zauberspruch, sondern eine gewöhnliche Methode, um nicht ins Chaos zu stürzen, wenn das Geschäft nach „einem weiteren kleinen Knopf im Admin-Bereich“ fragt und am Ende zehn neue URLs erscheinen.
Um es noch praktischer zu machen, fassen wir diese Fragen in einer Tabelle zusammen – wie ein Mini-Stand „Gesunder-Menschenverstand-Check“.
| Frage | Was versuchen Sie herauszufinden | Wie sieht das in der Task Tracker API aus |
|---|---|---|
| 1) Welche Ressource ist das? | Ist dies eine Zustandsänderung einer vorhandenen Ressource, eine neue Unterressource oder ein Nachschlagewerk? | PATCH /tasks/{taskId} ändert den Zustand der Aufgabe; /tasks/{taskId}/comments — Unterressource; /tags — Lookup |
| 2) Warum ist der Pfad genau so? | Wird der Pfad ressourcenorientiert gelesen oder ist es ein getarnter Befehl? | /tasks/{id} — zentral. /completeTask — Befehl (schlechter Stil für unser Projekt) |
| 3) Welche Status und Fehler hat das Szenario? | Sind es , 404, 409, 200/201/204? Welcher code in ProblemDetail? |
Unzulässiger Statuswechsel — 409 + INVALID_STATUS_TRANSITION; fehlende Aufgabe — 404 + TASK_NOT_FOUND |
Beachten Sie: Diese Fragen erfordern keine Kenntnis „aller RFCs auswendig“. Sie erfordern nur, sich daran zu erinnern, dass API ein Vertrag ist. Sobald Sie beginnen, diese drei Fragen laut zu beantworten (idealerweise so, dass ein Kollege Sie versteht), stellt sich plötzlich heraus, dass 70 % der „neuen Endpoints“ nur verschiedene Formen derselben Operation sind oder der Versuch, zwei Verantwortlichkeiten in einen Pfad zu packen.
2. Batch und Bulk: einfache Unterschiede
Die Wörter Batch und Bulk werden oft synonym verwendet, und dadurch wird die Diskussion zu einem Brei: Sie streiten scheinbar über dasselbe, stellen sich aber tatsächlich verschiedene Dinge vor. Um eine „Begriffsschlacht“ zu vermeiden, legen wir einfache Definitionen genau im Sinne dieses Kurses fest.
Eine Batch-Operation ist eine einzelne Anfrage, die eine Liste von Eingabeelementen enthält und darum bittet, diese „einzeln“ innerhalb eines Aufrufs zu verarbeiten. Ein typisches Beispiel: „Ändere den Status dieser 10 Aufgaben, hier sind ihre IDs“. Eine Bulk-Operation ist eine Massenaktion auf eine Menge von Ressourcen, wobei die Menge nicht durch eine Liste von IDs, sondern durch eine Bedingung oder allgemeine Regel bestimmt wird: „Archiviere alle DONE-Aufgaben älter als Datum X“ oder „Lösche alle Aufgaben mit dem Tag obsolete“.
Auf dem Papier sehen beide Ansätze aus wie „Anfragenersparnis“. Und ja, wenn Sie 1000 Aufgaben haben und ihnen einzeln den Status ändern, wird es laut. Aber Batch/Bulk haben einen unangenehmen Preis: Sie zerstören fast immer das einfache Modell von Antworten und Fehlern. Was bei einem einzelnen PATCH klar ist, wird in einem Massenszenario zu „Was zählt überhaupt als Erfolg?“
Zur Veranschaulichung vergleichen wir „eine einzelne Aufgabe“ und „eine Massenaktion“ nicht in Code-Begriffen, sondern in Vertragsbegriffen:
| Szenario | Erfolg | Fehler | Warum alles relativ einfach ist |
|---|---|---|---|
| PATCH /tasks/{id} | 200 OK mit neuer Repräsentation | 404 / 409 / 400 in einem Format | Ein Objekt in der Anfrage, eine Antwort, ein Fehler |
| Batch: „verarbeite eine Liste von Aufgaben“ | „alle erfolgreich“ oder „Teilerfolg“ | Fehler können für verschiedene Elemente unterschiedlich sein | Ein per-Item-Ergebnis ist nötig, sonst versteht der Client nicht, was passiert ist |
| Bulk: „verarbeite eine Menge nach Bedingung“ | „einige geändert“ | Ein Teil der Ressourcen kann in Konflikt geraten, ein Teil verschwinden | Beschreibung des Auswahlkriteriums, des Anwendungsergebnisses und eines Fehlerberichts erforderlich |
Und hier kommen wir sanft zum Hauptgedanken: Batch/Bulk ist nicht „ein weiterer Controller“. Es ist eine ganz andere Komplexität auf der Ebene der Ergebnissemantik. Deshalb nehmen wir sie im Lernvertrag v1 bewusst nicht auf.
3. Batch/Bulk: Teilweiser Erfolg und Fehler
Das heimtückischste Problem von Batch/Bulk-Endpoints ist der teilweise Erfolg. Er tritt nicht auf, weil jemand schlechten Code geschrieben hat, sondern weil die reale Welt heterogen ist: Aufgaben haben unterschiedliche Status, unterschiedliche Regeln, ein Teil kann gelöscht worden sein, ein Teil bereits archiviert, ein Teil kollidiert mit Geschäftsbeschränkungen. Und wenn Sie in einer Anfrage viele Entitäten anfassen, werden Sie fast garantiert auf die Situation stoßen „hier hat es geklappt, hier nicht“.
Stellen Sie sich eine Anfrage „Archiviere diese 5 Aufgaben“ vor: zwei davon sind DONE (Übergang zu ARCHIVED erlaubt), eine ist TODO (Übergang nicht erlaubt), eine bereits ARCHIVED (daraus kann man nicht raus, aber Sie versuchen zu archivieren – sie ist ja schon archiviert), und eine Aufgabe wurde zwischendurch gelöscht. In einem Batch-Request treten gleichzeitig Szenarien von 200, 404, 409 auf. Und dann beginnt die Lieblingsbeschäftigung: „Welchen HTTP-Status soll man der gesamten Antwort geben?“
Wenn Sie 200 OK setzen, sagen Sie sozusagen: „Im Großen und Ganzen ist alles in Ordnung.“ Aber das kann Fehler verbergen. Wenn Sie 409 Conflict setzen, sagen Sie: „Im Großen und Ganzen ein Konflikt.“ Aber ein Teil der Operationen könnte erfolgreich gewesen sein. 404 passt auch nicht. Es gibt den exotischen 207 Multi-Status (aus der WebDAV-Welt), aber er wird selten in gewöhnlichen kommerziellen APIs verwendet, und viele Clients/Integrationen erwarten ihn nicht.
Und selbst wenn Sie einen Status gewählt haben, bleibt eine noch wichtigere Frage: Wie sieht der Antwortkörper aus? Mit ProblemDetail war alles schön: einheitliches Format, code, title, detail, instance. Aber Batch/Bulk zwingt Sie oft dazu, einen „Bericht“ zurückzugeben – eine per-Item-Liste von Ergebnissen. Und das ist weder ProblemDetail noch TaskDetailsResponse. Das ist ein dritter Antworttyp, und zwar ein komplexer.
Schauen Sie, wie schnell das DTO selbst in einer „minimalen“ Version von Batch anwächst.
package com.example.tasktracker.api.dto.request;
import java.util.List;
// Request-DTO: eine Operation (Statuswechsel), aber für einen Satz von Aufgaben (Batch).
public record TaskBatchStatusChangeRequest(
List<TaskStatusChangeItem> items
) {
// Ein Element des Batches: welche Aufgabe und auf welchen Status.
// Wichtig: Auch hier treten bereits Validierungsfragen auf (Format von taskId, zulässige Statuswerte).
public record TaskStatusChangeItem(String taskId, String status) {}
}
Auf den ersten Blick kompakt. Aber jetzt brauchen Sie eine Antwort, die dem Client mitteilt, was mit jedem Element passiert ist:
package com.example.tasktracker.api.dto.response;
import java.util.List;
// Response-DTO: Für Batch wird fast immer ein "Bericht" pro Element benötigt.
public record TaskBatchStatusChangeResponse(
List<TaskBatchItemResult> results
) {
// Ergebnis für eine Aufgabe.
// success=true/false sieht einfach aus, aber in der Praxis wächst das "Warum nicht" schnell:
// manchmal wird code benötigt, manchmal detail, manchmal aktueller Status, manchmal Validierungsfehler pro Feld usw.
public record TaskBatchItemResult(String taskId, boolean success, String errorCode) {}
}
Und schon sehen Sie: Wir haben ein neues Antwortformat geschaffen, das weder dem Detail/Summary einer Aufgabe noch ProblemDetail ähnelt. Und errorCode wird hier mit ziemlicher Sicherheit nicht ausreichen: für 409 benötigen Sie möglicherweise ein Detail zum aktuellen und neuen Status, für 404 einfach die Tatsache des Fehlens, für Validation Field Errors usw.
Dann wird es noch interessanter: Sie müssen erklären, ob der Batch-Request wiederholt werden kann. Wenn wiederholt – Sie dürfen das System nicht durch doppelte Anwendung „zerstören“. Aber in einer Massenanfrage ist ein Teil bereits angewendet, ein Teil nicht. Es stellt sich heraus, dass Idempotenz sogar auf der Ebene „wie kann der Client sicher retryen“ komplexer wird.
In unserem Kurs können wir bereits idempotent-ish Operationen auf der Ebene einzelner PUT/DELETE durchführen und haben recht ehrlich diskutiert, dass transportbezogene Idempotenz und Geschäftseffekt nicht dasselbe sind. Batch/Bulk fügt dem eine weitere Schicht hinzu: „Idempotenz eines Batches“.
Deshalb führen wir sie in der kanonischen Task Tracker API v1 nicht ein: nicht weil „es nicht geht“, sondern weil dies eine separate Komplexität ist, die nicht dem Erlernen des Ressourcenmodells dient, sondern beginnt, mit ihm um Aufmerksamkeit zu konkurrieren.
4. Wann Batch/Bulk gerechtfertigt sind: in v1 – nicht
Batch/Bulk haben legitime Gründe zu existieren. Sie entstehen, wenn das Netzwerk teuer ist, der Client kein Browser ist, sondern eine Integration, wenn der Overhead reduziert werden muss, wenn es für den Server einfacher ist, mit einer Anfrage eine große Arbeit zu erledigen, als dass der Client hundertmal einzeln anfragt. In großen Systemen kommen Massenoperationen tatsächlich vor: Import, Migrationen, Bereinigungen, Massenstatusänderung nach Auswahl.
Aber solche Systeme haben normalerweise auch eine „erwachsene“ Infrastruktur um sich herum. Es werden verständliche Berichte benötigt, Korrelationsfelder, die Verwaltung von Teilerfolgen, manchmal eine „Job“-Ressource, die abgefragt werden kann, zusätzliche Garantien. Das ist alles in Ordnung… es ist einfach ein anderer Maßstab der Diskussion.
In unserem Lernprojekt erstellen wir eine API, deren Hauptfokus ein lesbares Ressourcenmodell und eine ehrliche Fehlersemantik ist. Und wir haben eine hervorragende Alternative zu Batch-Operationen: die Aktion für eine Ressource mit dem bereits existierenden und stabilen Endpoint durchzuführen.
Wenn Sie beispielsweise den Status von drei Aufgaben ändern müssen, könnte das wie drei normale PATCH-Requests aussehen. Ja, das sind drei Anfragen. Dafür hat jede Anfrage eine verständliche Antwort: entweder 200 OK mit der neuen Aufgabe, oder 404, oder 409 – alles in einem Vertrag.
So könnte das in einer .http-Datei aussehen (und das ist ein sehr „menschliches“ Szenario für Entwicklung und Debugging):
### Change status of task 1
PATCH http://localhost:8080/api/v1/tasks/11111111-1111-1111-1111-111111111111
Content-Type: application/json
{
"status": "IN_PROGRESS"
}
### Change status of task 2
PATCH http://localhost:8080/api/v1/tasks/22222222-2222-2222-2222-222222222222
Content-Type: application/json
{
"status": "DONE"
}
Das sieht etwas „manuell“ aus, hat aber einen großen Vorteil: Der Einzelressourcen-Vertrag bleibt der wichtigste, und Sie vermehren kein neues Antwortformat, um in einem Lernprojekt ein paar Anfragen zu sparen.
5. Vertragsprüfung: Schutz vor RPC-Chaos
Am Ende des Tages haben wir eine ausgereifte non-CRUD-Oberfläche: Statusübergänge, Kommentare, Tags-Lookup. Und das Wichtigste jetzt ist, nicht ein „zweites Steuerungssystem“ hinzuzufügen, das dasselbe tut, aber auf einem anderen Weg. Daher ist es nützlich, eine Prüfung durchzuführen: buchstäblich die API mit den Augen eines externen Verbrauchers zu betrachten und zu fragen: „Verstehe ich, wie man das nutzt?“
Um die Prüfung nicht abstrakt, sondern wiederholbar zu machen, ist es praktisch, mehrere „Schnitte“ durchzugehen – als ob Sie überprüfen, ob die Möbel nach dem Zusammenbau nicht wackeln. Unten ist eine Variante eines „Audits ohne Fanatismus“, die man tatsächlich per Hand sogar auf Junior-Level durchführen kann.
| Was prüfen wir | Welche Frage stellen wir uns | Was halten wir für ein gutes Zeichen |
|---|---|---|
| Ressourcenkarte | Welche Hauptressourcen, welche Unterressourcen? | tasks — Haupt; comments — unter tasks; tags — Lookup |
| Einziger Sinnpunkt | Gibt es einen kanonischen Pfad für die Operation? | Statuswechsel über PATCH /tasks/{id}, nicht über zwei verschiedene Pfade |
| HTTP-Methoden | Entspricht die Methode dem Sinn? | Kommentar erstellen — POST, löschen — DELETE, lesen — GET |
| Status | Sagt der Status dem Client die Wahrheit? | Unzulässiger Statuswechsel — 409, nicht „na, dann nehmen wir 400“ |
| Fehler | Ist das Fehlerformat einheitlich? | Überall ProblemDetail + code, ohne „String message an einer Stelle, Array errors an anderer“ |
| Pfadnamen | Sieht der Pfad ressourcenorientiert aus? | Keine Verben im Pfad wie /complete, /changeStatus, /bulkArchive |
Diese Prüfung ist auch psychologisch nützlich: Sie gibt Ihnen das Recht, „nein“ zu neuen Endpoints zu sagen, nicht aufgrund von Geschmack, sondern aufgrund der Integrität des Vertrags. Und, was wichtig ist, dieses „Nein“ klingt nicht wie „ich will nicht“, sondern wie „das schafft eine zweite Möglichkeit, dasselbe zu tun, und erschwert dem Client das Leben“.
Ein kleines Schema, das bei Entscheidungen helfen kann, könnte so aussehen:
flowchart TD
A["Ein neues Szenario ist aufgetreten"] --> B{"Ist das eine Zustandsänderung einer vorhandenen Task?"}
B -->|Ja| C["PATCH /tasks/{taskId} über TaskPatchRequest"]
B -->|Nein| D{"Sind das abhängige Daten mit kleinem Lebenszyklus?"}
D -->|Ja| E["Unterressource /tasks/{taskId}/..."]
D -->|Nein| F{"Ist das ein Nachschlagewerk/Lookup für den Client?"}
F -->|Ja| G["GET /tags oder ähnlicher Lookup"]
F -->|Nein| H["Wahrscheinlich ist das ein anderes Subsystem oder Out-of-Scope-Komplexität"]
Das Diagramm ist nicht perfekt, aber es erinnert an das Wesentliche: Wir suchen zuerst die Ressourcenform, anstatt einen neuen „Action-Endpoint“ zu erfinden.
6. Anti-Beispiel: Vertragsduplizierung
Ein sehr typischer Fehler in der non-CRUD-Welt ist es, einen neuen Endpoint „zur Bequemlichkeit des Frontends“ hinzuzufügen, der einen bereits bestehenden Vertrag dupliziert. Am Ende beginnt das Frontend nach einem Monat, beide Pfade zu nutzen, weil „hier bequemer, dort schneller“, und dann versuchen Sie, das Verhalten der beiden Pfade zu synchronisieren. Das ist etwa so, als hätte man zwei Schaltflächen „Speichern“ in einem Fenster, wobei die eine save macht und die andere auch save, aber ohne Validierung (und keiner erinnert sich, warum).
Hier ist ein Beispiel für das, was wir zu vermeiden versuchen:
package com.example.tasktracker.api.controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class TaskStatusController {
@PostMapping("/api/v1/tasks/{taskId}/changeStatus")
void changeStatus(@PathVariable String taskId) {
// Anti-Beispiel: Es entsteht ein zweiter URI für dieselbe Geschäftsoperation.
// Mit der Zeit werden diese beiden Pfade fast unvermeidlich auseinanderdriften:
// Status, Request-Body, Fehlercodes, Antwortformat.
}
}
Selbst wenn Sie intern denselben Service aufrufen, entsteht ein zweiter URI für dieselbe Operation. Nach einiger Zeit wird jemand sicher fragen: „Und welcher ist der “richtige”?“ Und, schlimmer noch, es entsteht die Versuchung, das Verhalten ein wenig auseinanderzubringen: hier 204 zurückgeben, dort 200, hier JSON akzeptieren, dort Query-Parameter, hier die Übergangsmatrix prüfen, dort vergessen.
Die richtige Variante für unseren Vertrag ist es, den Statuswechsel innerhalb des bereits existierenden PATCH zu halten:
package com.example.tasktracker.api.controller;
import com.example.tasktracker.api.dto.request.TaskPatchRequest;
import com.example.tasktracker.api.dto.response.TaskDetailsResponse;
import com.example.tasktracker.domain.service.TaskService;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/tasks")
class TaskController {
private final TaskService taskService;
TaskController(TaskService taskService) {
// Der Controller erhält den Service per DI und enthält keine Geschäftslogik für Statusübergänge.
this.taskService = taskService;
}
@PatchMapping("/{taskId}")
TaskDetailsResponse patch(@PathVariable String taskId,
@RequestBody TaskPatchRequest request) {
// Kanonischer Pfad: Änderung des Zustands/der Repräsentation der Task-Ressource über PATCH.
// Die gesamte Validierungs- und Konfliktlogik (z.B. 409) sollte innerhalb des Service/Domänen liegen.
return taskService.patchTask(taskId, request);
}
}
Hier gibt es keine „separate Aktion“. Es gibt eine Änderung der Ressourcenrepräsentation. Und das hält die API im Zustand „ein Sinn – ein Pfad“.
7. Typische Fehler bei non-CRUD-Grenzen
Abschließend ist es wichtig, typische Fehler zu besprechen – nicht um zu „schimpfen“, sondern damit Sie sie im Voraus erkennen. In der Regel entstehen sie nicht aus böser Absicht, sondern aus dem Wunsch, „es bequem zu machen“ oder „es schneller zu machen“. Das Problem ist nur, dass die Bequemlichkeit für ein Team heute morgen teure Unterstützung für alle sein kann.
Fehler Nr. 1: Hinzufügen eines Endpoints für jedes Geschäftsverb, weil es so einfacher ist, es dem Produkt zu erklären.
Das ist eine sehr menschliche Falle: Das Geschäft sagt „archivieren“, Sie machen /archiveTask. Dann sagt das Geschäft „entarchivieren“, Sie machen /unarchiveTask. Dann „sperren“, „entsperren“, „wiedereröffnen“… und Sie wachen in einer Welt auf, in der URIs ein Wörterbuch von Verben sind. Die richtige Gewohnheit ist: zuerst nach der Ressource und ihrem Zustand zu suchen und dann Änderungen durch PATCH/PUT oder eine Unterressource auszudrücken.
Fehler Nr. 2: Batch/Bulk „für alle Fälle“ machen, ohne die Frage nach dem teilweisen Erfolg zu beantworten.
Massenoperationen erzeugen fast immer Teilergebnisse. Wenn Sie nicht im Voraus erklären können, welchen HTTP-Status es geben wird, wie der Antwortkörper aussehen wird und wie der Client versteht, was genau nicht geklappt hat – dann fügen Sie einen Endpoint hinzu, der ein Eigenleben entwickeln wird. In einem Lernprojekt ist es besser, ehrlich mit wiederholten Einzelanfragen auszukommen, als einen neuen Antworttyp und ein neues Fehlerformat einzuführen.
Fehler Nr. 3: Mehrere Verantwortlichkeiten in einem Pfad mischen, weil das „weniger Anfragen bedeutet“.
Endpoints wie /tasks/{id}/comments-and-status oder /tasks/{id}/closeAndAddComment sehen sparsam aus, machen den Vertrag aber undurchsichtig. Der Client kann die Eingabe nur schwer validieren, Fehler schwer verarbeiten, schwer verstehen, was bei Teilerfolg passiert ist. In einem REST-Vertrag ist es besser, bei „ein Endpoint – eine verständliche Aufgabe“ zu bleiben, auch wenn das zwei Anfragen statt einer bedeutet.
Fehler Nr. 4: Denselben Sinn mit verschiedenen URIs duplizieren.
Wenn der Status über PATCH /tasks/{id} geändert werden kann, dann schafft das Auftauchen von /tasks/{id}/changeStatus einen zweiten kanonischen Pfad. Und zwei Kanons in einem Projekt bedeuten normalerweise „tatsächlich gar keinen Kanon“. Duplizierung ist selten sofort sichtbar, aber nach ein paar Wochen führt sie zu Abweichungen: unterschiedliche Status, unterschiedliche Fehler, unterschiedliche Dokumentation. Die Vorbeugung ist einfach: ein Sinn – ein Pfad.
Fehler Nr. 5: Den Geschäftskonflikt 409 durch einen „universellen 400“ ersetzen, weil „es ja trotzdem ein Fehler ist“.
Das scheint eine Kleinigkeit zu sein, zerstört aber in der Praxis die Client-Semantik. Ungültiger JSON oder leerer title – das ist 400, weil die Eingabe strukturell schlecht ist. Unzulässiger Statuswechsel – das ist 409, weil die Eingabe normal ist, aber mit den Systemregeln kollidiert. Wenn man alles in 400 verwandelt, verliert der Client die Fähigkeit, zwischen „ich habe Müll gesendet“ und „ich habe eine korrekte Anfrage gesendet, aber sie ist verboten“ zu unterscheiden.
GO TO FULL VERSION