Fehler beim Übertragen von Base64-kodierten Dateiinhalten - nicht new String(bytes) benutzen!


#1

Meine Infrastruktur:

Firefox <------> WebServlet auf Tomcat <------> Server

Ich verwende kein weiteres Framework, nur die von Tomcat mitgelieferten Jars servlet-api.jar und jaxws-api-2.3.0.jar.

Mein Problem:

Ich versuche eine serverseitig erzeugte Exceldatei im .xlsx-Format, welche sich direkt auf dem Server auch problemlos öffnen lässt, über mein im Tomcat laufendes WebServlet im Browser herunterzuladen. Mit Textdateien klappt das bisher ganz problemlos, mit .xlsx-Dateien leider nicht, Excel mag die heruntergeladenen Dateien einfach nicht öffnen und sie unterscheiden sich auch inhaltlich, wenn auch nicht in der Größe der Datei.

Zuerst habe ich gedacht, dass es am falschen ContentType liegen könnte und habe diesen auf

"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"

gesetzt.

Das hat nicht zum Erfolg geführt.

Zusätzlich setze ich in für jeden Download:

response.setHeader("Content-disposition", "attachment; filename=" + downloadFilename);

damit der Browser den Speichern-Dialog öffnet.

Dann habe ich auf dem Server den Dateiinhalt in Base64 kodiert und übertragen:

public String readLocalBinaryFileToBase64FileContent(String filename) {
    byte[] bytes = readSmallBinaryFile(filename);
    Base64.Encoder encoder = Base64.getEncoder();
    byte[] encodedBytes = encoder.encode(bytes);
    String base64FileContent = new String(encodedBytes);
    return base64FileContent;
}

private byte[] readSmallBinaryFile(String filename) {
    try {
        Path path = Paths.get(filename);
        return Files.readAllBytes(path);
    }
    catch (IOException exception) {
        throw new RuntimeException(exception);
    }
}

Und entsprechend im WebServlet diesen wieder dekodiert mit:

public String createBinaryStringFromBase64FileContent(String base64FileContent) {
    byte[] encodedBytes = base64FileContent.getBytes();
    Base64.Decoder decoder = Base64.getDecoder();
    byte[] decodedBytes = decoder.decode(encodedBytes);
    String decoded = new String(decodedBytes);
    return decoded;
}

Abgesendet wird er im WebServlet wie jeder andere Inhalt (erzeugte HTML-Seite, Text-Dateiinhalt zum Download, …):

private void sendResponse(String payload) {
    try {
        tryToSendResponse(payload);
    }
    catch(Exception e) {
        throw new HTTPException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
    }
}

private void tryToSendResponse(String payload) throws IOException {
    OutputStream out = response.getOutputStream();
    out.write(payload.getBytes());
    out.flush();
}

Zuletzt habe ich noch den binären ContentType probiert:

"application/octet-stream"

in der Hoffnung, dass es dann klappt, tut es aber auch nicht.

Für ContentTypes siehe https://wiki.selfhtml.org/wiki/MIME-Type/Übersicht

Hat jemand eine Idee, was ich noch versuchen könnte? Ich habe schon sehr viel
gegoogelt, bin aber gerade mit meinem Latein am Ende.


#2

von deiner beschreibung gehe ich mal davon aus, dass die Datei entweder bei senden oder bereits beim lesen verändert wird.
Vorallem, dass du die Bytes des files in einen String packst finde ich seltsam.

byte[] decodedBytes = decoder.decode(encodedBytes);
String decoded = new String(decodedBytes);
return decoded;

Warum gibts du das file nicht als binary zurück sondern als String? Ich vermute, dass Java String hier intern etwas macht. Da xlsx ja ein Zip format ist ist nur die kleinste Binäre veränderung macht das ganze file ungültig.


#3

Ja das kann gut sein, ich habe gerade mal testweise die Datei im WebServlet zwischengespeichert. Dort ist sie schon defekt und exakt identisch zu der heruntergeladenen Datei. Die Fehlerursache liegt also nicht im ContentType oder dergleichen, sondern im Übertragen.

Ich probiere es mal ohne den Zwischenschritt byte[] -> String -> byte[] und berichte!


#4

man sieht es schon sehr gut in meinem Testprogramm:

package stc.io;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Base64;

public class ReadAndWriteBinaryFilesBase64StandAlone {

    private static final String FILE_NAME = "c:/temp/javaBinaryFileTest/Excel_original.xlsx";
    private static final String BASE64_FILE_NAME = "c:/temp/javaBinaryFileTest/Excel_base64.txt";
    private static final String OUTPUT_FILE_NAME = "c:/temp/javaBinaryFileTest/Excel_copy.xlsx";

    byte[] readSmallBinaryFile(String filename) throws IOException {
        Path path = Paths.get(filename);
        return Files.readAllBytes(path);
    }

    private void writeBytesBase64(byte[] bytes, String base64FileName) throws IOException {
        Base64.Encoder encoder = Base64.getEncoder();
        byte[] encodedBytes = encoder.encode(bytes);
        writeSmallBinaryFile(encodedBytes, base64FileName);
    }

    private byte[] readBytesBase64(String base64FileName) throws IOException {
        byte[] encodedBytes = readSmallBinaryFile(base64FileName);
        Base64.Decoder decoder = Base64.getDecoder();
        byte[] decodesBytes = decoder.decode(encodedBytes);

        String dumm = new String(decodesBytes);  // <------------------------ HIER
        decodesBytes = dumm.getBytes();          // <------------------------ HIER

        return decodesBytes;
    }

    public void writeSmallBinaryFile(byte[] bytes, String filename) throws IOException {
        Path path = Paths.get(filename);
        Files.write(path, bytes); // creates, overwrites
    }

    public static void main(String... aArgs) throws IOException {
        ReadAndWriteBinaryFilesBase64StandAlone binary = new ReadAndWriteBinaryFilesBase64StandAlone();
        byte[] bytes = binary.readSmallBinaryFile(FILE_NAME);
        System.out.println("bytes = " + Arrays.toString(bytes));
        System.out.println("bytes.length() = " + bytes.length);
        binary.writeBytesBase64(bytes, BASE64_FILE_NAME);
        bytes = binary.readBytesBase64(BASE64_FILE_NAME);
        System.out.println("bytes = " + Arrays.toString(bytes));
        System.out.println("bytes.length() = " + bytes.length);
        binary.writeSmallBinaryFile(bytes, OUTPUT_FILE_NAME);
    }

}

Das erzeugt 19321 statt 11204 Bytes. Lässt man die beiden markierten Zeilen mit dem String dumm weg, dann funktioniert es.


#5

Übertragen wird nun so:

private void sendResponse(byte[] payload) {
    try {
        tryToSendResponse(payload);
    }
    catch(Exception e) {
        throw new HTTPException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
    }
}

private void tryToSendResponse(byte[] payload) throws IOException {
    OutputStream out = response.getOutputStream();
    out.write(payload);
    out.flush();
}

Und dorthin übergebe ich die Base-64-dekodierten Bytes ohne Umweg über

String content = new String(bytes)

und

content.getBytes()

Irriger Weise war ich der Ansicht, dass diese Operationen seiteneffektfrei sein müssten. Ich finde es auch relativ traurig, dass sie es nicht sind, auch wenn es dafür bestimmt triftige Gründe beim Design der Klasse String gab.

Problem gelost, danke! Ich hatte ja schon gearbwohnt, ob der String-Kosntruktor Unsinn anstellt, hätte ich das mal erst getestet, dann hätte ich uns allen den thread erspart.

Ich habe den Titel mal angepasst, weil die ursprünglich von mir vermutete Stelle gar nicht das Problem war.


#6

new String(bytes).getBytes() gibt auch wieder das ursprüngliche zurück und verändert es nicht, vorausgesetzt bytes enthält keinen Unsinn.

Problem ist dabei, dass du den Base64-String nimmst, das wieder zu Binärdaten dekodierst, und aus den Binärdaten einen String machst. Zufällige Binärdaten sind aber selten ein sinnvoller String, der Fall wird auch entsprechend im Javadoc erwähnt:

The behavior of this constructor when the given bytes are not valid in the default charset is unspecified.

Warum kodierst du das überhaupt in Base64?
Wenn ich das richtig sehe, liegt die Datei aufm Server und du musst sie von da ausliefern?


#7

new String(…) hust hust keuch

Base64.getEncoder().encodeToString(…)

Umgekehrt ist es aber quark 'ne Zip Datei zum String zu dekodieren wie @mrBrown schon erklaerte.

HTTP kann nur Text, binaeere Daten muessen da nach Base64 (= Text) kodiert werden um uebertragen zu werden. Wenn man aber eine Datei bekommst die in Base64 kodiert ist, sollte man das nicht in einen String umwandeln nach dem dekodieren, speziell bei Zip Dateien, .xlsx Dateien sind genau das :wink:


#8

Nein kann es nicht, mit dem richtigen ContentType kann es durchaus mehr. Vielleicht macht es intern wieder Base64 daraus und der Browser modelt das zurück, das kann ich nicht sagen. Aber es klappt so :slight_smile:

Ich dachte das hätte die Infrastruktur oben angedeutet:

Firefox <------> WebServlet auf Tomcat <------> Server

Das WebServlet ist relativ klein und dumm. Der Rest passiert auf einem Server, kommuniziert wird über eine message oriented middelware (in dem Fall Apache ActiveMQ). Da übertrage ich die Antwort als Text-Nachricht, dafür die Base64 -Kodierung.

Nun ohne den bösen new String(bytes);-Konstruktor passt ja auch alles. :sparkler:


#9

https://tools.ietf.org/html/rfc2616

Base64 nimmt man um Binaerdaten in Text zu wandeln, das ist der Grund warum du das Zipfile (.xlsx) erst encoden musst.
HyperText Transfer Protocol :wink: