Vandaag hebben we een update gedaan aan de website die een bugje verhielp. Een aantal mensen hadden bij ons al aangegeven dat de downloadknop oneindig lang op "bezig..." bleef staan, terwijl er al wel een download gestart was. Dit is een verwarrende boodschap, want: is de website nu nog bezig met iets belangrijks, of betekent de knop "bezig" dat de download nog aan de gang is?

Het had te maken met een verandering die we een maand of 2 geleden hadden gedaan, júist om de functionaliteit van die downloadknop te verbeteren.
Feitelijk zijn er 2 manieren om een download te starten vanuit javascript (er vanuit gaande dat PHP de download verzorgt):

  • Via een window.location = "download.php";
  • Met een <iframe src="download.php"/>

Beide methodes zijn in feite 'blind'. Ze weten niet hoe lang het duurt voordat PHP antwoord geeft met de gewenste download. Ze vuren hun aanvragen af, en daarna is er geen callback wanneer die aanvragen voltooid zijn. In de meeste gevallen maakt dit niet uit, maar in sommige gevallen is dit lastig.
Zo hebben wij een "share" pagina die pas wordt geladen nádat de download is gestart. Maar ook in andere gevallen, waar bijvoorbeeld dynamisch een .PDF bestand aan wordt gemaakt, en een loader vertoond moet worden, is het lastig wanneer je in het duister tast hoe lang het duurt voordat de download beschikbaar is.

Van oudsher was het bij ons op de website zo dat de download aangeroepen werd met een window.location, waarna er (natte vinger werk) 4 seconden werd gewacht voordat de share pagina geladen werd. De share pagina dient ook een beetje als een "succes-pagina", omdat er berichten op staan die dat aanduiden.
Soms gebeurde het echter, wanneer de server te druk was, dat de window.location afgevuurd werd maar het langer dan 4 seconden duurde voordat er een antwoord kwam van de server. De gebruiker zag dan een succes-pagina, zonder dat er écht resultaat terug was gekomen. In het ergeste geval bedacht de gebruiker zich "laat ik het opnieuw proberen", klikte op "home", en brak daarmee de window.location aanvraag naar de server af. Als we het dan hebben over usability: that's real bad usability.

2 Maanden geleden dus was daarin een verandering. Dit was de oplossing die we gevonden hadden:

var ifr = $('<iframe/>', {
src: "download.php",
style: 'display:none;',
ready: function(){
goToResultpage();
}
});

$('body').append(ifr);

De download wordt ingeladen in een onzichtbaar iframe, waarna pas als het iframe de 'ready' state bereikt de share pagina in wordt geladen. Ik denk de 'ready' flag een cocktail is van verschillende elementen, waaronder het onLoad() event waarschijnlijk een belangrijke rol speelt. Zonder jQuery had het er dus zo uitgezien:

container = document.getElementById("iframe_div");
iframe = "<iframe src='download.php' onLoad='goToResultpage()'/>"
container.innerHTML = iframe;

Door de download in te laden in een iframe, waarna er een event afvuurt wanneer het laden van de pagina "download.php" klaar is (en daarmee dus ook de download direct start), kan er exact gehandeld worden, zonder te schatten hoe lang dingen duren.

Alles werkte vlekkeloos... Tenminste, bij ons in Firefox. Maar naarmate de tijd verstreek kwamen er berichten binnen dat het in andere browsers niet zo goed werkte. Vooral browsers die óf niet goed iframes ondersteunden, of die het "onload" event niet afvuurde wanneer de teruggekomen content de header "Content-Disposition: attachment" had. En eerlijk gezegd: een downloadknop die vast blijft staan is nog veel erger dan een sharepagina die iets te vroeg in beeld komt.

So: back to square one, zoals men dat dan zegt. We stonden op het punt de gemaakte aanpassingen weer terug te draaien, totdat ik een géniale out-of-the-box aanpak vond voor dit probleem. Ookal hadden we al eerder gezocht naar alle mogelijke oplossingen, we hadden deze pagina over het hoofd gezien: detecting the file download dialog in the browser.
En dit is, zoals men zegt, a diamond in the rough.

oplossing

Zoals gezegd: out of the box.
De oplossing is om niet alleen de client te betrekken bij het probleem, maar ook de server:

  • Genereer cliënt side een unieke token
  • Stuur de unieke token mee met de download-aanvraag naar de server. Bijvoorbeeld: document.location = "download.php?id=token"
  • Wanneer de server de download klaar heeft en op het punt staat om terug te geven, laat dan de server ook een cookie zetten in zijn antwoord, met die unieke token.
  • Laat ondertussen, nadat de cliënt de aanvraag verzonden heeft, de cliënt periodiek controleren of de inhoud van het cookie gelijk is aan de token.
  • pas wanneer de inhoud van het cookie gelijk is aan de token, heeft de server zijn antwoord verzonden (en is dus de download gestart)

In andere woorden:

client side

var token = generate_token();

document.location = "download.php?id=token";

var waitForCookie = setInterval(function(){
var cookieValue = readCookie('video-download-id');
if (cookieValue == token){

//do your stuff here

window.clearInterval(waitForCookie);
}
}, 200);

Server Side

$token = $_GET["token"];

//do important download stuff here

header('Content-Disposition: attachment; filename="your download!"');
setcookie("video-download-id",$token,time()+60,"/");

readfile("url-to-file");

 

Geniaal! :)