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.
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.
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.
Probablement ce qui suit se passe à Tomcat.
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.
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