http://www.perlmonks.org?node_id=11128741

cristofayre has asked for the wisdom of the Perl Monks concerning the following question:

This is partially PERL and partially javascript (Sorry)

Basically, I want to send an HTTPRequest to the server. If the file =DOES NOT= exist, it throws back an error as a TEXT response. If it DOES exist, it passes back the file as a BLOB. (Not really sure how to send a file as a BLOB from PERL - which is the point of this question)

** Using "Content-type: attachment; application/zip" and "content-disposition" to invoke the save dialouge I can just about understand ... but that will not report an error if the file doesn't exist. (The file is accessed by user entering a code number into a form, so a wrong number needs to throw an error warning)

Here is the basic code:

CLIENT SIDE: function downFile(){ var formData = new FormData(); formData.set('email','$email'); formData.set('gf_line','$line'); formData.set('code','$code'); var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4){ if (this.status == 200) { if (xhttp.responseType == 'text' || xhttp.responseType == +'') { alert('Sorry, but there appears to be an error\\nThis coul +d be:\\n\\n- The email address entered\\ndoes not match the email in +database\\n\\n- The code you entered is incorrect\\n\\nPlease check y +our details, and try again'); window.history.back(); return false; } else if (xhttp.responseType == 'blob'){ // Create a new Blob object using the response data of the + onload object var blob = new Blob([this.response], {type: 'application/z +ip'}); //Create a link element, hide it, direct it towards the bl +ob, and then 'click' it programatically let a = document.createElement("a"); a.style = "display: none"; document.body.appendChild(a); //Create a DOMString representing the blob and point the l +ink element towards it let url = window.URL.createObjectURL(blob); a.href = url; a.download = 'Branded Files.zip'; //programatically click the link to trigger the download a.click(); //release the reference to the file by revoking the Object + URL window.URL.revokeObjectURL(url); } } } xhttp.open("POST", "download.pl", true); xhttp.send(formData); } </script> SERVER SIDE: ... .. . if ($inEmail ne $email){ print "content-type: text/html\n\n"; print "500"; # Returned as a "TEXT" response exit; } else{ open FILE, "<", "../data/$user/$dir/$code/$fileCode.zip"; binmode FILE; while(<FILE>){ $file.=$_; } close FILE; #print "content-type: text/html\n\n"; #print "content-type: application/zip\n"; #print "content-disposition: attachment; filename='Branded Files'\n\n" +; print $file; # Returned as a "BLOB" response

Both responses return as text

The theory is that the code in the else statement will create a fake anchor, load the blob into it, emulate a click and thus invoke the "SAVE" dialogue. Admit this is "copy / paste" so - whilst I understand what it's doing - it's not my code

In a nutshell, I want an alert if the file does not exist, or a SAVE dialogue if it DOES exist, ideally without navigating away from the page. There may be an easier way to do it, so open to suggestions

Replies are listed 'Best First'.
Re: Sending file as "TEXT" or "BLOB"
by bliako (Monsignor) on Feb 24, 2021 at 20:58 UTC

    Communicating with headers is OK but it can be more versatile if you implement your own communication protocol. Simply because you can as you are in total control of both server and client code. So, why not make your communication a bit more verbose and standard by always including 1) operation-status, 2) text message, 3) optional html message, 4) optional JavaScript code to execute (just joking), 5) optional data. All wrapped as JSON hash for example.

    Client-to-server communication via sending an XHR (XMLHttpRequest) (generally: AJAX) works on the "same page". No need to reload or move to a new page. The client sends a request to the server which responds with running a script (in your case "download.pl") or routing to a controller, etc. The results are passed back to the caller, i.e. the client's browser and notify your XMLHttpRequest object (your xhhttp) by means of activating callbacks you implemented when you created the object. That's provided the client did not move to a new page or reloaded!

    You can implement a number of different callbacks or you can implement just one, onreadystatechange just as you did in your code. This function will be called each time the communication changes a state, and accompanied with appropriate data. And so it can, for example, report on progress, bytes transfered etc, report a server error (500,404) or process the received data if server did send something without complaining. See https://javascript.info/xmlhttprequest for more details.

    The callback can then notify the user by *dynamically* creating or changing an HTML element, e.g. a text box's content or popping up a message. This is well documented on the internet.

    So, what I suggest is you keep your callback handler mostly unchanged. On state=4 (XHR completed), as you now do, check the server response and alert the user if it was not 200/OK. If it was, then unwrap the "complex" data you received from server in order to check its status, e.g. file-too-big, permission-denied, no-such-file-do-you-want-to-create, (an arbitrarily long list!) etc. And also present the user with the error message/instructions the server sent you. And/or process the data part and decode your image. Which, btw, must be text-encoded (e.g. via base64).

    On the server side it looks like this:

    ... use JSON; use MIME::Base64; my $ret_data = { 'status' => 5, # success 'message' => "here is the file $filename you asked" 'message-html' => "<b>success blah blah for $fileame</b>", 'data' => MIME::Base64::encode_base64($image_binary_data) }; # if using CGI.pm use CGI; my $cgi = CGI->new; print $cgi->header(-type => "application/json", -charset => "utf-8"); print JSON::encode_json($ret_data); # or if using Mojolicious $c->render(json => $ret_data);

    1 minute Edit: here is the data I would send on a failure, e.g. file permissions or file-too-big-to-send, or ... One advantage of this method is that you can still send data even on errors.

    my $ret_data = { 'status' => -1, # file was too big and I truncated it 'message' => "here is the file $filename you asked but it is truncat +ed" 'message-html' => "<b>success blah blah for $fileame but truncated</ +b>", 'data' => truncate($data) };

    bw, bliako

Re: Sending file as "TEXT" or "BLOB"
by haukex (Archbishop) on Feb 24, 2021 at 22:23 UTC

    First, I would very strongly recommend you look into something like jQuery to make your life with JavaScript easier, and that you don't hand-craft your responses from Perl. Though I'd normally recommend something like Mojolicious, that may be too big of a jump here, and at the very least look into one of the CGI.pm alternatives, like maybe CGI::Lite. See also UP-TO-DATE Comparison of CGI Alternatives.

    Anyway, on to the problem at hand: XMLHttpRequest.responseType is a setting for the request*, and is not a return value indicating the type of the response. In theory, you could look at xhttp.getResponseHeader('content-type') instead. However, you might want to consider not differentiating responses on Content-Type, but on HTTP status codes instead. And as bliako described, another option is to wrap your data in JSON, so you can send status responses to clients that way, then HTTP error codes can be reserved for lower-level errors.

    * Update: To clarify, perhaps a better way to describe it is a configuration setting for the XHR for what kind of a response the XHR should expect, and my understanding from reading up on it and testing a bit is that its value is not changed by the XHR depending on what it receives as a response.

      Regarding XMLHttpRequest.responseType, and me not being a JavaScript expert at all but just a user, it looks to be a way to tell the XHR how to decode the received data in order to produce a native JS object. So setting xhr.responseType = 'json'; prior to the request, on completion of the request and receive of server data one can simply do var responseObj = xhr.response; If it's an array, JSON, blob or text, the XHR object will do the conversion for you. If one does not want to use this feature or plans to receive data which is not decodable by XHR (i.e. let's say it's hashed or encrypted) then use your standard decoder var responseObj = myDecoder(xhr.responseText); In my case (JSON data) that decoder is JSON.parse()

      That also explains why yes, there is a json http header but there is none for some of the other supported XHR types *specifically*, e.g. ArrayBuffer

      bw, bliako

Re: Sending file as "TEXT" or "BLOB"
by jcb (Parson) on Feb 25, 2021 at 02:29 UTC

    The correct solution here is to use the HTTP status code. This is why HTTP responses include a status code.

    If the file exists, the server returns 200 OK and the file as application/zip. If the file does not exist, the server returns 404 Not Found. The client then distinguishes based on this.status: 200 for a BLOB is included and 404 for an error.

    Here are the relevant bits of the revised JavaScript: (obviously untested)

    xhttp.onreadystatechange = function() { if (this.readystate == 4) { if (this.status == 200) { if (xhttp.responseType == 'blob') { // existing goofy file download code here } else { // report internal error } } else if (this.status = 404) { // report error here } } }

    As for the Perl code on the server side, you did not provide a SSCCE, so I can give only a broad outline of how the server processing should work:

    1. [whatever initial setup is needed]
    2. Read the form data from the client.
    3. Determine if the requirements to download a file have been met, and if so, which file is to be downloaded.
    4. Produce a reply, with either a 200 OK status if presenting a file, or 404 Not Found otherwise.
      The correct solution here is ...

      While that is an option, there is another way to look at it: this means that the client can't differentiate between an error the developer made, such as a typo in the URL or moving download.pl to another location, and an error the user made in requesting the file from download.pl, such as the target file not existing. That is an argument for adding an additional protocol layer, e.g. with JSON, as described in the other replies - 200 OK would mean the client reached the server successfully, and any other HTTP status code means that there is an issue in communicating with the server.

      Produce a reply, with either a 200 OK status if presenting a file, or 404 Not Found otherwise.

      TIMTOWTDI applies here as well. Since the script in the OP is simply serving up a raw file, this could theoretically be done more efficiently by the webserver itself, and the CGI script could be giving a 3xx redirect to that file. Granted, we currently don't have enough information about what the OP is doing to know if there might be access rights or similar to consider, but again, this is a suggestion to think about it in a more TIMTOWTDI way.

Re: Sending file as "TEXT" or "BLOB"
by Anonymous Monk on Feb 24, 2021 at 17:08 UTC
    If the file does exist, it should return content-type: application/zip. If it does not, the usual practice is to send application/json containing an error-message packet. Exactly what help do you need here?