{ }
GET
REST API · Symfony · OpenAPI · Testing
Datei-Upload-Endpoints in Symfony dokumentieren und testen
von multipart/form-data bis zur vollständigen Testabdeckung

Upload-Endpoints sind in REST-APIs eine häufige Fehlerquelle — falsch dokumentierte Content-Types, fehlende Validierung und unvollständige Tests machen sie zur Blackbox. Dieser Leitfaden zeigt, wie Symfony-Upload-Endpoints mit OpenAPI 3.1 korrekt beschrieben, serverseitig abgesichert und mit PHPUnit vollständig getestet werden.

15 Min. Lesezeit multipart/form-data · NelmioApiDocBundle · PHPUnit · Validierung Symfony 7.x · OpenAPI 3.1 · PHP 8.4

1. Warum Upload-Endpoints besondere Sorgfalt erfordern

Datei-Upload-Endpoints unterscheiden sich grundlegend von JSON-basierten REST-Endpoints: Sie verwenden multipart/form-data statt application/json, das Framework-Routing muss speziell konfiguriert sein und die Validierung muss über die reine Schema-Prüfung hinausgehen. In der Praxis entstehen Fehler durch falsch gesetzten Content-Type-Header, fehlende Größenbeschränkungen in PHP und Nginx sowie durch unvollständige OpenAPI-Beschreibungen, die Integratoren in die Irre führen.

Ein weiterer kritischer Punkt ist die Sicherheit: Upload-Endpoints sind ein primäres Angriffsziel für das Hochladen von Schadcode getarnt als Bild oder PDF. Die MIME-Type-Überprüfung darf sich nicht auf den vom Client gesendeten Content-Type verlassen, sondern muss die tatsächliche Dateimagie per finfo prüfen. Wer zusätzlich die Dateierweiterung validiert, die Dateigröße begrenzt und Uploads in ein nicht-öffentliches Verzeichnis speichert, schließt die häufigsten Angriffsvektoren.

2. Der Symfony-Controller: UploadedFile und Validierung

In Symfony repräsentiert die Klasse Symfony\Component\HttpFoundation\File\UploadedFile hochgeladene Dateien aus dem $request->files-Bag. Sie kapselt Originalname, MIME-Type, Dateigröße und temporären Speicherpfad. Der Controller sollte so schlank wie möglich bleiben: Die eigentliche Logik — Validierung, Umbenennung, Speicherung — gehört in einen dedizierten Service, den der Controller über Dependency Injection erhält. Das erleichtert das Testen erheblich, weil der Service unabhängig vom HTTP-Layer getestet werden kann.

Ein häufiger Fehler in Symfony-Upload-Controllern: Der Entwickler prüft $file->isValid(), geht aber davon aus, dass dies alle Fehler abdeckt. In Wirklichkeit prüft isValid() nur, ob der PHP-Upload fehlerfrei abgeschlossen wurde (kein PHP-Upload-Fehler-Code). Dateityp, Größe und inhaltliche Gültigkeit müssen danach separat validiert werden. Symfony Validator-Constraints wie File mit maxSize, mimeTypes und mimeTypesMessage decken diese Fälle sauber ab und integrieren sich nahtlos in den Validation-Layer.


# src/Controller/Api/DocumentUploadController.php
<?php
declare(strict_types=1);

namespace App\Controller\Api;

use App\Service\DocumentUploadService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;

#[Route('/api/v1/documents', name: 'api_documents_')]
final class DocumentUploadController extends AbstractController
{
    public function __construct(
        private readonly DocumentUploadService $uploadService,
        private readonly ValidatorInterface $validator,
    ) {}

    #[Route('/upload', name: 'upload', methods: ['POST'])]
    public function upload(Request $request): JsonResponse
    {
        $file = $request->files->get('document');

        if ($file === null) {
            return $this->json(['error' => 'No file uploaded', 'code' => 'MISSING_FILE'], 422);
        }

        $result = $this->uploadService->process($file, $request->request->all());

        return $this->json($result, 201);
    }
}

3. OpenAPI-Schema für multipart/form-data korrekt definieren

In OpenAPI 3.1 wird ein Datei-Upload-Endpoint mit requestBody, content und dem Media-Type multipart/form-data beschrieben. Das Schema-Objekt definiert die Felder des Formulars: normale Textfelder als string und die Datei selbst als type: string, format: binary. Dieser binary-Hinweis signalisiert OpenAPI-Tools und Client-Generatoren, dass dieses Feld keine normale Zeichenkette, sondern Binärdaten enthält und im Multipart-Body als Datei-Part übertragen wird.

Für den Download-Fall — wenn die API eine Datei zurückgibt statt JSON — verwendet man in der Response-Beschreibung ebenfalls content: application/octet-stream oder den spezifischen MIME-Type (z.B. application/pdf) mit schema: { type: string, format: binary }. Die encoding-Eigenschaft im requestBody ermöglicht, für einzelne Felder den Content-Type zu übersteuern — etwa wenn ein Teil des Multipart-Bodies selbst JSON sein soll. Das ist ein fortgeschrittenes OpenAPI-Feature, das bei komplexen Upload-Endpunkten (Datei plus strukturierte Metadaten) den Upload-Workflow sauber abbildet.


# openapi/paths/documents-upload.yaml
/api/v1/documents/upload:
  post:
    operationId: uploadDocument
    summary: Upload a document file with metadata
    tags:
      - Documents
    requestBody:
      required: true
      content:
        multipart/form-data:
          schema:
            type: object
            required:
              - document
              - category
            properties:
              document:
                type: string
                format: binary
                description: The document file (PDF, max 10 MB)
              category:
                type: string
                enum: [invoice, contract, report]
                description: Document category for classification
              description:
                type: string
                maxLength: 500
                description: Optional description text
          encoding:
            document:
              contentType: application/pdf, image/jpeg, image/png
    responses:
      '201':
        description: Document uploaded successfully
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DocumentUploadResponse'
      '422':
        $ref: '#/components/responses/ValidationError'

4. NelmioApiDocBundle-Annotation in der Praxis

NelmioApiDocBundle erlaubt es, OpenAPI-Dokumentation direkt im Symfony-Controller über PHP-Attribute zu pflegen. Das #[OA\RequestBody]-Attribut mit #[OA\MediaType] für multipart/form-data beschreibt den Upload-Endpoint präzise. Der Vorteil gegenüber separaten YAML-Dateien: Die Dokumentation bleibt nah am Code und wird beim Refactoring leichter mitgepflegt. Nachteil: Bei sehr komplexen Schemas werden die Attribute unübersichtlich lang. Hier empfiehlt sich eine hybride Strategie: Einfache Endpoints direkt im Controller annotieren, komplexe Schemas in externe YAML-Referenzen auslagern.

Ein häufiger Stolperstein mit NelmioApiDocBundle und Upload-Endpoints: Wenn der Controller den UploadedFile-Typ als Parameter-Hint verwendet, versucht das Bundle, diesen als Schema zu generieren — was zu falschen Dokumentation führt. Die Lösung ist, den Upload-Parameter explizit über das #[OA\Property(type: 'string', format: 'binary')]-Attribut zu definieren und die automatische Schema-Generierung für diesen Parameter zu übersteuern. Mit nelmio_api_doc.areas können Upload-Endpoints in separate API-Bereiche aufgeteilt werden, was für interne vs. externe Dokumentation nützlich ist.

5. Dateityp, Größe und Inhaltsvalidierung serverseitig

Die korrekte Validierung von Upload-Dateien besteht aus mehreren Schichten. Symfony's File-Constraint übernimmt die erste Schicht: maximale Dateigröße und erlaubte MIME-Types werden deklarativ als Constraint-Attribut konfiguriert. Diese Validierung ist notwendig, aber nicht hinreichend, weil der MIME-Type aus den Datei-Metadaten stammt und manipuliert werden kann. Die zweite Schicht ist die PHP-eigene finfo_open(FILEINFO_MIME_TYPE)-Funktion, die den tatsächlichen MIME-Type anhand der Datei-Magie ermittelt — unabhängig vom gesendeten Content-Type-Header.

Für PDF-Uploads empfiehlt sich als dritte Schicht eine inhaltliche Validierung: Das PDF muss mit dem %PDF--Magic-Byte beginnen und darf keine JavaScript-Aktionen enthalten. Für Bilduploads kann getimagesize() sicherstellen, dass die Datei tatsächlich ein gültiges Bild ist. Gespeichert werden sollte die Datei immer außerhalb des öffentlich erreichbaren public/-Verzeichnisses, mit einem zufälligen Dateinamen (UUID oder Hash) ohne Bezug zum Originalnamen. Der Originalname wird separat in der Datenbank gespeichert und bei Bedarf für den Download-Header verwendet.


# src/Service/DocumentUploadService.php
<?php
declare(strict_types=1);

namespace App\Service;

use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Validator\ValidatorInterface;

final class DocumentUploadService
{
    private const ALLOWED_MIME_TYPES = ['application/pdf', 'image/jpeg', 'image/png'];
    private const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB

    public function __construct(
        private readonly ValidatorInterface $validator,
        private readonly string $uploadDirectory,
    ) {}

    public function process(UploadedFile $file, array $metadata): array
    {
        // Layer 1: Symfony constraint validation
        $violations = $this->validator->validate($file, [
            new Assert\File(
                maxSize: '10M',
                mimeTypes: self::ALLOWED_MIME_TYPES,
                mimeTypesMessage: 'Only PDF and images are allowed.',
            ),
        ]);

        if (count($violations) > 0) {
            throw new \InvalidArgumentException((string) $violations);
        }

        // Layer 2: finfo MIME check (bypass client-supplied Content-Type)
        $finfo = new \finfo(FILEINFO_MIME_TYPE);
        $detectedMime = $finfo->file($file->getPathname());

        if (!in_array($detectedMime, self::ALLOWED_MIME_TYPES, true)) {
            throw new \InvalidArgumentException("Detected MIME type not allowed: $detectedMime");
        }

        // Store with random filename outside public/ directory
        $newFilename = bin2hex(random_bytes(16)) . '.' . $file->guessExtension();
        $file->move($this->uploadDirectory, $newFilename);

        return [
            'id' => $newFilename,
            'originalName' => $file->getClientOriginalName(),
            'size' => $file->getSize(),
            'mimeType' => $detectedMime,
        ];
    }
}

6. PHPUnit-Tests für Upload-Endpoints schreiben

PHPUnit-Tests für Upload-Endpoints in Symfony nutzen die WebTestCase-Klasse und den integrierten HTTP-Client. Eine echte Datei wird mit der UploadedFile-Klasse im Test instanziiert — bei Symfony-Funktionstests mit dem dritten Parameter test: true, der den PHP-Upload-Fehlercode überschreibt. So lassen sich auch Fehlerszenarien testen: Zu große Dateien, falsche MIME-Types und fehlende Pflichtfelder können mit speziell präparierten Fixture-Dateien abgebildet werden.

Wichtig für zuverlässige Upload-Tests: Das Verzeichnis, in das Uploads gespeichert werden, sollte im Testmodus ein temporäres Verzeichnis sein, das nach jedem Test bereinigt wird. Symfony's Service-Container-Parameter ermöglichen dies über die Konfigurationsdatei config/packages/test/services.yaml. Contract-Tests mit Spectator oder einem OpenAPI-Validator stellen sicher, dass die Antwortstruktur des Endpoints mit der dokumentierten OpenAPI-Spezifikation übereinstimmt — eine zweite Absicherungsebene zusätzlich zu den funktionalen Tests.

7. Manuelle Tests mit curl und Insomnia

Für manuelle Tests von Upload-Endpoints ist curl das präziseste Werkzeug, weil es die exakte HTTP-Anfrage sichtbar macht. Die korrekte curl-Syntax für Multipart-Uploads verwendet -F für Formularfelder und die spezielle @-Syntax für Dateien. Mit --verbose und -D - werden Request-Header, Response-Header und Body gleichzeitig angezeigt, was bei der Fehlersuche unverzichtbar ist. Das häufigste Problem bei manuellen Tests: curl setzt Content-Type: multipart/form-data automatisch mit der richtigen Boundary — wer diesen Header manuell überschreibt, bricht die Anfrage.

Insomnia und Postman bieten grafische Oberflächen für Multipart-Uploads und können direkt aus der importierten OpenAPI-Spezifikation Anfragen generieren. Das ist besonders wertvoll für QA-Teams, die keine Kommandozeilen-Erfahrung haben. Beide Tools zeigen auch den tatsächlich gesendeten HTTP-Body an, was bei der Diagnose von Boundary-Problemen oder falsch kodierten Felder hilft. Eine vollständige Insomnia-Collection als JSON im Repository zu versionieren, sodass das Team reproduzierbare Testfälle hat, ist eine einfache aber effektive Praxis.

8. Fehlerbehandlung und aussagekräftige Fehlermeldungen

Upload-Endpoints liefern häufig kryptische Fehlermeldungen zurück, wenn etwas schiefläuft. Ein 500er ohne Details, wenn die Datei zu groß ist, frustriert Integratoren und macht Debugging schwierig. Die richtige Strategie: Jeden Fehlerfall mit einem spezifischen HTTP-Status und einem strukturierten Fehlerobjekt im RFC 7807 (Problem Details)-Format beantworten. 422 Unprocessable Entity für Validierungsfehler, 413 Payload Too Large wenn PHP oder Nginx die Dateigröße ablehnen, 415 Unsupported Media Type für unerlaubte MIME-Types.

Ein spezielles Problem bei Upload-Endpoints: PHP und Nginx begrenzen die Upload-Größe unabhängig voneinander. Wenn Nginx client_max_body_size überschritten wird, sieht PHP die Anfrage gar nicht und gibt einen 413-Fehler zurück — direkt von Nginx, nicht vom Controller. Wenn PHP's upload_max_filesize oder post_max_size überschritten wird, ist $_FILES leer und $_POST ebenfalls. Der Controller muss diesen Fall explizit prüfen: Wenn keine Dateien im Request sind und der Content-Type trotzdem multipart/form-data ist, war wahrscheinlich die Datei zu groß für PHP.

Fehlerfall HTTP-Status Error-Code Ursache / Lösung
Kein Datei-Part 422 MISSING_FILE Pflichtfeld fehlt im Multipart-Body
Falscher MIME-Type 415 UNSUPPORTED_MEDIA finfo-Check schlägt fehl
Datei zu groß (PHP) 413 FILE_TOO_LARGE upload_max_filesize erhöhen oder Client informieren
Datei zu groß (Nginx) 413 NGINX_LIMIT client_max_body_size in nginx.conf anpassen
PHP-Upload-Fehler 500 UPLOAD_ERROR isValid() false — PHP-Fehlercode auslesen

Mironsoft

REST API Design, Symfony-Entwicklung und OpenAPI-Dokumentation

Symfony-APIs mit sauberer Upload-Logik und vollständiger Dokumentation?

Wir entwerfen und implementieren Upload-Endpoints, die sicher, gut dokumentiert und vollständig getestet sind — mit OpenAPI 3.1, PHPUnit-Abdeckung und strukturierter Fehlerbehandlung.

API-Design

Upload-Endpoints nach REST-Prinzipien und OpenAPI 3.1 korrekt modellieren

Sicherheit

MIME-Validierung, Größenbeschränkungen und sichere Dateispeicherung implementieren

Testing

PHPUnit-Testsuite für alle Upload-Szenarien inkl. Fehlerpfade aufbauen

10. Zusammenfassung

Datei-Upload-Endpoints in Symfony richtig zu implementieren und zu dokumentieren erfordert Sorgfalt auf mehreren Ebenen gleichzeitig. Der Controller bleibt schlank und delegiert an einen Service. Die Validierung besteht aus mindestens zwei Schichten: Symfony-Constraints für deklarative Regeln und finfo für MIME-Type-Überprüfung anhand der Dateimagie. Die OpenAPI-Dokumentation beschreibt multipart/form-data korrekt mit format: binary für Dateifelder und listet alle Fehlerfälle mit HTTP-Status und Fehlerobjektstruktur auf.

PHPUnit-Tests decken alle relevanten Szenarien ab: erfolgreicher Upload, fehlende Datei, falscher MIME-Type, zu große Datei. Manuell sind curl und Insomnia die präzisesten Werkzeuge, weil sie den tatsächlichen HTTP-Traffic zeigen. Nginx und PHP müssen unabhängig voneinander auf konsistente Upload-Größenlimits konfiguriert sein. Wer alle diese Aspekte zusammenbringt, baut Upload-Endpoints, die für Integratoren berechenbar, für Angreifer unattraktiv und für das eigene Team wartbar sind.

Datei-Upload-Endpoints — Das Wichtigste auf einen Blick

OpenAPI-Dokumentation

multipart/form-data mit format: binary für Dateifelder. Encoding-Objekt für Content-Type pro Part. Alle Fehlerfälle mit HTTP-Status dokumentieren.

Validierungsschichten

Symfony File-Constraint + finfo MIME-Check + inhaltliche Validierung. Nie auf Client-seitigen Content-Type vertrauen.

Sicheres Speichern

Außerhalb von public/, zufälliger Dateiname (UUID), Originaldateiname in Datenbank. Nginx und PHP-Größenlimits konsistent konfigurieren.

Testing

PHPUnit WebTestCase mit UploadedFile-Fixtures. curl mit -F für manuelle Tests. OpenAPI-Validator für Contract-Tests in CI.

11. FAQ: Datei-Upload-Endpoints in Symfony

1Wie beschreibe ich einen Datei-Upload in OpenAPI 3.1?
requestBody mit content: multipart/form-data. Das Dateifeld erhält type: string, format: binary. Das encoding-Objekt erlaubt Content-Type pro Part.
2Wie greife ich in Symfony auf die Datei zu?
$request->files->get('fieldname') gibt ein UploadedFile-Objekt. isValid() prüfen, danach MIME-Type und Größe separat validieren.
3Warum dem Client-MIME-Type nicht vertrauen?
Der Header kann manipuliert werden. finfo prüft die tatsächliche Dateimagie unabhängig vom gesendeten Content-Type-Header.
4Nginx lehnt Upload vor PHP ab?
client_max_body_size in nginx.conf muss mindestens so groß wie upload_max_filesize in php.ini sein. Sonst gibt Nginx direkt 413 zurück.
5PHPUnit-Test für Upload schreiben?
WebTestCase mit $client->request() und UploadedFile im files-Array. Dritter Parameter true für simulierten erfolgreichen PHP-Upload.
6curl für Upload-Tests?
curl -X POST -F 'document=@datei.pdf' -F 'category=invoice' https://api.example.com/upload. @ sendet die Datei als Binärdaten.
7Wo Dateien speichern?
Außerhalb public/, zufälliger Dateiname. Originalname nur in der Datenbank und im Content-Disposition-Header beim Download.
8HTTP-Status bei Upload-Validierungsfehler?
422 für Constraint-Fehler, 413 für Größenlimits, 415 für unerlaubte MIME-Types. Immer strukturiertes Fehlerobjekt zurückgeben.
9Download-Endpoint in OpenAPI beschreiben?
Response mit content: application/pdf und schema: {type: string, format: binary}. Content-Disposition im headers-Objekt der Response dokumentieren.
10Multipart-Part mit JSON?
Ja. encoding-Objekt in OpenAPI und Content-Type-Header pro Part ermöglichen Datei plus JSON-Metadaten in einem einzigen Request.