[JAVA] Le fichier CSV que j'ai pu télécharger a soudainement commencé à apparaître sur la page.

Dans le cadre des travaux urgents, nous avons publié une application Web qui télécharge les données saisies par plusieurs personnes au format CSV. J'ai utilisé l'OSS suivant.

Il n'y avait pas de problème particulier le jour du début de l'opération, et lorsque j'ai quitté le bureau et que j'ai bu avec mes amis, mon smartphone était bruyant. .. .. Lorsque j'ai reçu un appel entrant, j'ai reçu un message disant "Téléchargement impossible". Quand j'ai vérifié les détails, il a dit: "Jusqu'à il y a 3 heures, un fichier CSV était sorti lorsque je l'ai téléchargé, mais soudainement le CSV est venu pour être affiché sur le navigateur."

J'ai fait une erreur avec un ami, j'ai arrêté de boire et j'ai commencé à enquêter sur la cause. Pour le moment, je savais que cela fonctionnerait si je le réparais comme ça, alors je l'ai publié avec la priorité de service en premier. Cependant, le principe reste flou, nous avons donc mené une enquête continue.

Détails de la correction

Vous trouverez ci-dessous le code simplifié du processus de téléchargement à l'origine de ce problème et le code modifié.

  @PostMapping
  fun export(response: HttpServletResponse) {
    /**
     *Réponse CSV.Ecrire dans outputStream.
     */

    response.contentType = "text/csv"
    response.setHeader("Content-Disposition", "attachment; filename=export.csv")
    response.outputStream.flush()
  }
  @PostMapping
  fun export(response: HttpServletResponse) {
    response.contentType = "text/csv"
    response.setHeader("Content-Disposition", "attachment; filename=export.csv")

    /**
     *Réponse CSV.Ecrire dans outputStream.
     */

    response.outputStream.flush()
  }

Le point est de savoir si Content-Type et Content-Disposition sont après ou avant l'écriture dans outputStream. Des enquêtes ultérieures ont montré que si Content-Disposition était en tête, le fichier serait téléchargé.

La raison pour laquelle j'ai défini Content-Disposition après avoir écrit dans outputStream est que l'article auquel je faisais référence se trouvait être comme ça, et je pense que la définition de l'en-tête et l'écriture dans le corps (flux) sont des processus indépendants. Voilà pourquoi.

Recherche de cause

Cependant, même si Content-Disposition est défini après l'écriture dans outputStream, le fichier peut être téléchargé normalement pendant un certain temps, de sorte qu'il peut être déduit que la taille de la sortie CSV a un effet. J'ai donc entré la taille des données à l'écran et créé un programme de reproduction qui produit un CSV de cette taille.

  @PostMapping
  fun export(@RequestParam size: Int, response: HttpServletResponse) {
    (1..size).forEach {
      response.outputStream.print("a")
    }
    response.contentType = "text/csv"
    response.setHeader("Content-Disposition", "attachment; filename=export.csv")
    response.outputStream.flush()
  }

Ensuite, l'en-tête de réponse a confirmé par les outils de développement de Chrome que la taille est de 8 Ko

Connection: keep-alive
Content-Disposition: attachment; filename=export.csv
Content-Type: text/csv
Date: Sat, 08 Feb 2020 10:26:07 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

C'était 9KByte

Connection: keep-alive
Date: Sat, 08 Feb 2020 10:27:48 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

Content-Disposition ne disparaît-il pas ainsi? En fin de compte, 8191 octets est le premier et 8192 octets est le second. Je pense que c'est parce que 8192, qui est une puissance de 2, dépasse quelque chose de défini.

Considération

Probablement ce qui suit se passe à Tomcat.

Cause enquête 2

Ainsi, tout en surveillant rapidement le paquet avec tcpdump, j'ai essayé d'écrire étape par étape de 8190 octets à 1 octet avec le débogueur, et il a été confirmé que l'en-tête et le corps HTTP étaient envoyés au navigateur lorsque 8192 octets ont été écrits. ..

Au fait, lorsque je suis intervenu lors de l'écriture de 8192 octets, j'ai trouvé le code suivant.

java:org.apache.catalina.connector.OutputBuffer


    public void append(byte src[], int off, int len) throws IOException {
        if (bb.remaining() == 0) {
            appendByteArray(src, off, len);
        } else {
            int n = transfer(src, off, len, bb);
            len = len - n;
            off = off + n;
            if (isFull(bb)) {
                flushByteBuffer();
                appendByteArray(src, off, len);
            }
        }
    }

bb est une variable membre de type ByteBuffer et les données de transmission sont stockées dans bb dans la méthode appendByteArray et de transfert. Avec la méthode isFull, elle devient vraie lorsque la taille stockée dans bb atteint le montant autorisé, et elle est envoyée au navigateur avec la méthode flushByteBuffer.

Si vous suivez la méthode flushByteBuffer plus loin,

java:org.apache.coyote.http11.Http11OutputBuffer


    @Override
    public int doWrite(ByteBuffer chunk) throws IOException {

        if (!response.isCommitted()) {
            // Send the connector a request for commit. The connector should
            // then validate the headers, send them (using sendHeaders) and
            // set the filters accordingly.
            response.action(ActionCode.COMMIT, null);
        }

        if (lastActiveFilter == -1) {
            return outputStreamOutputBuffer.doWrite(chunk);
        } else {
            return activeFilters[lastActiveFilter].doWrite(chunk);
        }
    }

S'il n'est pas validé, envoyez un en-tête HTTP pendant le processus de validation pour définir l'indicateur de validation. Après cela, le corps HTTP est écrit.

Considération 2

C'est pourquoi le code que j'ai fait facilement a été publié sans le remarquer car il s'est comporté comme prévu, mais compte tenu de la difficulté du côté de Tomcat à traiter différentes tailles, c'est une implémentation naturelle, là C'était une histoire que je suis allée jusqu'au bout pour devenir accro.


(08/02/2020) Publié. (2020/2/9) Correction d'une description incorrecte de la taille à laquelle l'événement bascule d'avant en arrière dans l'enquête sur la cause. Ajout de l'enquête de cause 2 et de la considération.

Recommended Posts

Le fichier CSV que j'ai pu télécharger a soudainement commencé à apparaître sur la page.
J'étais accro à la mise à jour de la déclaration dans MyBatis
À propos de la question pour laquelle j'étais accro à l'utilisation de hashmap
J'ai créé un outil pour afficher la différence du fichier CSV
J'ai pu déployer l'application Docker + laravel + MySQL sur Heroku!
En vérifiant le fonctionnement de Java sous Linux, j'ai pu comprendre la compilation et la hiérarchie.
Une histoire à laquelle j'étais accro à deux reprises avec le paramètre de démarrage automatique de Tomcat 8 sur CentOS 8
Je souhaite télécharger un fichier sur Internet en utilisant Ruby et l'enregistrer localement (avec prudence)
[Ruby] J'ai essayé de résumer les méthodes fréquentes dans paiza
[Ruby] J'ai essayé de résumer les méthodes fréquentes avec paiza ②
J'étais accro à la méthode du rouleau
J'étais accro au test Spring-Batch
J'étais accro à l'utilisation de RXTX avec Sierra
Double-cliquez pour ouvrir le fichier jar sous Windows
Un site facile à comprendre lorsque j'ai commencé à apprendre Spring Boot
Une histoire à laquelle j'étais accro lors de l'obtention d'une clé qui a été automatiquement essayée sur MyBatis
J'étais accro à NoSuchMethodError dans Cloud Endpoints
[Ruby] Incompréhension que j'utilisais le module [Débutant]
Supprimez complètement le fichier de migration que vous n'avez pas réussi à supprimer
Je souhaite simplifier la sortie du journal sur Android
J'ai dû déterminer où se trouvait le dossier des plugins eclipse sur mon Mac. (Note)
[Java] J'ai essayé de créer un jeu Janken que les débutants peuvent exécuter sur la console