Request Header Content-Type und dessen Zweckbestimmung

Zur Wiederherstellung der Daten serverseitig muss der Parser den Content-Type kennen.

Eine Diskussion unter Kollegen gab mir den Anlass zu diesem Artikel. Ein Kollege schrieb mir, dass er einen JSON-String wie gewöhnlich als Parameter sendet nach dem Schema: data=$jsonstring in einem POST Request. Und er zeigte sich verwundert, dass bestimmte Zeichen, wie z.B. & und + (Ampersand, Pluszeichen) einfach so verschwinden. Warum das so ist und was da genau passiert, erklärt dieser Artikel leicht verständlich.

Der fehlende Content-Type und RFC 3986

HTML-Designer kennen in Formularen für Datei-Upload das Attribut Enctype="multipart/form-data" im einleitenden Form-Element. Der Browser generiert anhand dieser Angabe den Request-Header Content-Type: multipart/form-data und somit bestimmt der serverseitige Parser den Algorithmus nach welchem die gesendeten Daten wiederherzustellen sind. Fehlt jedoch im Browser die Angabe des Enctye, wird kein solcher Header Content-Type gesendet sondern es wird serverseitig der Default angenommen welcher lautet: application/x-www-form-urlencoded, was natürlich die weitere Verarbeitung der gesendeten Daten entscheidend bestimmt: Somit kommt RFC 3986 zum Tragen, womit bestimmten Zeichen eine besondere Bedeutung zukommt. Beispielsweise erwartet der serverseitige Parser das & als Trennzeichen zwischen einzelnen Parametern, was das Verschwinden dieses Zeichen hiermit erklärt.

Ebensowenig geht das + Zeichen auf dem Transportwege (HTTP) verloren, sondern es wird serverseitig vom Parser als Leerzeichen angenommen. Technisch wäre ein Percent-Encoding (gem. RFC 3986) des JSON-Strings zwar richtig aber systematisch ist es falsch und außerdem überflüssig, denn:

JSON ist ein eigenständiger Content-Type

JSON ist keine Komponente des Content-Type application/x-www-form-urlencoded sondern ein eigenständiger Content-Type, nämlich application/json und bestimmt somit einen völlig anderen Algorithmus zur Wiederherstellung der Inhalte nach der Übertragung via HTTP.

Des Weiteren erlauben Übertragungen im Message-Body beliebige Bytesequenzen, in der Praxis betrifft das die Request-Methoden POST und PUT. Das heißt, dass ein JSON-String problemlos und auch ohne Prozentkodierung direkt via POST gesendet werden kann, ohne dass ein Verlust von irgendwelchen Zeichen zu befürchten ist. Wobei das Senden des passenden Content-Types dringend empfohlen ist. Ein AJAX-Request sieht dann beispielsweise so aus:

    xhr.open("POST", "/content_type.html", true);
    xhr.setRequestHeader("Content-Type", "application/json");
    xhr.send(jsonstring);

Anmerkung: JSON-Strings im URI via GET zu übertragen ist unsinnig und war auch nie die Zweckbestimmung von JSON.

Die serverseitige Verarbeitung hängt nun davon ab, ob der eingesetzte Parser mit dem Content-Type kann oder nicht. Für eine proprietäre Lösung werden POST- oder PUT-Daten gemäß CGI/1.1-Spezifikation direkt aus STDIN gelesen, hierzu setzt der Webserver eine Umgebungs-Variable CONTENT_LENGTH was die Anzahl der aus diesem Handler zu lesenden Bytes bestimmt.

Dennoch gibt es Möglichkeiten, mehrere Content-Types in einem Request zu senden, eine dieser Möglichkeiten beschreibt der Content-Type: multipart/form-data.

JavaScript FormData zum Senden mehrerer Content-Types

Betrachte untenstehenden Code als technisch wie systematisch korrekte Lösung zur anfangs gestellten Aufgabe:

    var jsonstring = JSON.stringify(['foo','bar','baz']);
    var foda = new FormData();
    foda.append('jsondata', new Blob([jsonstring], { type: 'application/json' }));
    foda.append('name','Fritz Müller');
    xhr.open("POST", "/content_type.html", true);
    xhr.send(foda);

Im Ergebnis dessen sendet das xhr-Objekt spontan einen Request-Header Content-Type: multipart/form-data; boundary=12345 und im Message-Body Folgendes (Boundary gekürzt, ein Schlüssel-Werte-Paar und ein Array als JSON):

--12345
Content-Disposition: form-data; name="name"

Fritz Müller
--12345
Content-Disposition: form-data; name="jsondata"; filename="blob"
Content-Type: application/json

['foo','bar','baz']
--12345--

Obenstehende Multipart-Message hat also mehrere Komponenten zum Inhalt und eine davon ist der JSON-String, für den der eigenständige Content-Type mitgesendet wird. Anhand des Content-Type-Headers im Request stellt der Parser die einzelnen Komponenten wiederher und so kann in PHP beispielsweise wie gewohnt in das $_POST und $_FILES Array gegriffen werden für den wahlfreien Zugriff.

Array $_FILES
(
    [jsondata] => Array
        (
            [name] => blob
            [type] => application/json
            [tmp_name] => C:\WINDOWS\Temp\php4.tmp
            [error] => 0
            [size] => 19
        )
)
Array $_POST
(
    [name] => Fritz Müller
)

PHP's json_decode() wäre auf den Inhalt der gesendeten Datei, siehe tmp_name anzuwenden. Einfacher, weil ohne Upload ist die direkte Zuweisung des JSON-String an einen Parameter:

    foda.append('jsondata',jsonstring);

# das ergibt im $_POST
Array
(
    [name] => Fritz Müller
    [jsondata] => ["foo","bar","baz"]
)

allerdings wird in diesem Fall der eigenständige Content-Type nicht übertragen. Merke: Eine Übertragung des Content-Type findet nur bei einem Upload statt, d.h., dazu muss es sich beim an das FormData-Objekt angehängten Parameter um einen JS-Datentype Blob oder File handeln. Das JS-FormData-Objekt erlaubt die Übertragung beliebiger Bytesequenzen, Zeichen denen eine spezielle Bedeutung zukommen würde, gibt es bei diesem Content-Type nicht.

Perl Specials

Der eigene Parser wird um den gewünschten Content-Type erweitert:

    elsif( $self->{CONTENT_TYPE} eq 'application/json' ){
        require JSON;
        my $json = JSON->new;
        $self->{json} = $json->decode($self->rawdata);
        $self->{param} = $self->{json}{param};
    }

Womit ohne weitere Änderung wie gewohnt die Methode $self->param('ParameterName'); aufgerufen werden kann oder es wird auf das Attribut zugegriffen: $self->json();

Für Legacy CGI.pm Benutzer

If POSTed data is not of type
application/x-www-form-urlencoded or multipart/form-data,
then the POSTed data will not be processed,
but instead be returned as-is in a parameter named POSTDATA.
To retrieve it, use code like this:

    my $data = $q->param('POSTDATA');

Likewise if PUTed data can be retrieved with code like this:

    my $data = $q->param('PUTDATA');

Content-Type und Schlüsselparameter

Nehmen wir an, es gibt eine Anwendung die nur *.jpeg-Dateien per HTTP-Message-Body bekommt. D.h., der zu verarbeitende Content-Type steht bereits in der Zweckbestimmung der Anwendung, was die Angabe des Content-Type image/jpeg praktisch überflüssig macht. Genau das wäre eine Gelegenheit über eigene Content-Types nachzudenken.


Datenschutzerklärung: Diese Seite dient rein privaten Zwecken. Auf den für diese Domäne installierten Seiten werden grundsätzlich keine personenbezogenen Daten erhoben. Das Loggen der Zugriffe mit Ihrer Remote Adresse erfolgt beim Provider soweit das technisch erforderlich ist. s​os­@rolf­rost.de.