PDF from PUT in new window via Blob URI - javascript

I have an API endpoint which on a PUT serves a PDF with Content-Type "application/pdf" (you might ask "why a PUT and not a GET?" and the answer to that is "it’s complicated and I can’t change it").
I want to (client side), request that PDF and display the result in a window.open() call (ie display the PDF in a new window). Previously I used a data URI to do this, ie:
window.open('data:application/pdf,<the data payload from the response>');
but in Chrome 60 this no longer works (you can’t use a data URI in a window.open, see https://bugs.chromium.org/p/chromium/issues/detail?id=751135 ).
I have now changed the client side code to do a fetch() of the same endpoint, create a blob from the response, call URL.createObjectURL() on the blob to get a url, and pass that url to the window.open() call. Ie I have something like:
fetch('http://somedomain.com/rest/getMyPdf', {
method: 'PUT',
... some headers & the body ...
}).then(function(response) {
return response.blob();
}).then(function(blob) {
window.open(URL.createObjectURL(blob));
});
This works in Firefox (I get a PDF in a new window), but in Chrome you just see the raw ASCII of the PDF contents (ie %PDF-1.3.....).
I’m guessing this is some sort of content-type issue, ie if I could set the content type on the url, then Chrome would interpret the response as a PDF. Any suggestions on how to do that?

Try
var data = response.arraybuffer();
Then create a Blob from there with the right content type
var = new Blob(data, 'application/pdf');

Related

Save File through JQuery AJAX Call

I am using JQuery to send an AJAX request to a Node server (using Hapi). The server responds with a PDF file correctly, but I am trying to save the file locally. The catch is that the PDF should only return if the POST sent the right data. It's "protected" in other words and I don't want to just expose it publicly.
Frontend code:
$.get('http://localhost:3000').done(function (data) {
console.log(data);
});
Backend code:
server.route({
method: 'GET',
path: '/',
handler: async (request, h) => {
return h.file('./static/sample.pdf', {
mode: 'attachment',
filename: 'sample.pdf'
})
}
});
I receive the data but nothing happens in the front-end UI. How can I download the PDF that is sent automatically?
You can achieve that with html5 using
Download it!
Notice that this works only for same-origin URLs, or the blob: and data: schemes. And this gets overridden by the Content-Disposition header from the server.
If you want to do this programatically you could do this
const element = document.createElement("a")
element.setAttribute(
"href",
// you could also use base64 encoding here like data:application/pdf;base64,
"data:text/plain;charset=utf-8," + encodeURIComponent('pdf binary content here')
)
element.setAttribute("download", "file.pdf")
element.style.display = "none"
document.body.appendChild(element)
element.click()
document.body.removeChild(element)
Anyway this is only a useful method if u want to create/modify the downloaded data from the client side, but if u are getting it as it is from the server side then its better just to open a new url, letting the browser handle it.

Retrieving response data from axios post differs from raw response data

I've been developing a Flask/Vue webapp and I've run into an issue regarding downloading of files. I'm serving a userupload from the backend using flask, which returns a file through send_from_directory(). This is called through a downloadFile method in Vue
downloadFile(key, name){
const path = '/api/ticket/filedownload'
this.$ajax.post(path, {address: key}, {responseType: 'arraybuffer',})
.then((response) => {
// Create response donwload link
const url = window.URL.createObjectURL(new Blob([response.data], response.headers['contentType']));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', name);
document.body.appendChild(link);
link.click();
})
.catch(error => {
console.log(error)
window.alert("File not found")
})
}
This method I found online in many places and seems to work for most people.
Yet the file that gets served at the front end "contains errors" or seems corrupt.
In my network tab I can clearly see that the post request was successful and the response is the actual image (e.g.) that was sent! When copying the response from the post result directly in my network tab (Mozilla, right mouse>copy>copy response) I was able to retrieve that the response was base64 encoded and could be decoded easily to the same image that was uploaded. However! When printing the response.data, the output is completely different and not in base64:
base64 from network tab: R0lGODlhWAJYAvcAABAQEL2cUntSG(etc...)
console.log(response.data):GIF89aX�X���������R{R�)�1)�1�j ���������ޔ��{)��j
I have tried supplying the response.data as a blob directly or by parsing it in a filereader to no avail. Our this.$ajax.post is a workaround for axios' post, which can be supplied with returnTypes such as blob, arraybuffer, text, anything, all with the same result.
My question is: How would I go ahead and access the response such as it was displayed in the network tab as I am capable of converting from base64, not an unknown format. Or how would I parse this response in any successful way.

In Javascript how can I redirect an Ajax response in window location

I currently have the following working piece of code (angular but applies to any JS framework):
var url = '/endpoint/to/my/file';
$http({
method: 'GET',
url: url
})
.success(function(jdata) {
window.location = url;
})
.error(function(je){
// display errors on page
});
The above is called after a form was completed and the user has clicked on "submit" (the real situation is a bit more complex than this but it is the same idea). I do the form check asynchronously, so there's no page reload.
If the request is successful, returns a binary (a pdf file), if not succesful, the request returns a 400 BadRequest with errors formatted in JS. So what I do is, if successful, I redirect to the same url to have the PDF otherwise I get the JSON error object and do something with it.
How can I refrain from making two requests if the requests is successful?
Note1: on the backend side I would like to keep only one route that does everything, check + return PDF
Note2: the current situation is pretty neat in my opinion, since I have an asynchronous form check and if successful the file downloads directly in the browser since I have "CONTENT-DISPOSITION" -> "attachment" in the HTTP header of the successful response
Update: additional information about the architecture as requested by Emile:
In my use case I have one endpoint that checks inputs (and other external requirements). For security reasons I cannot output the PDF if all requirements are not satisfied so I have to do the check prior to delivering the file ( the file is automatically generated) anyway. So having two endpoints would just be redundant and add some unnecessary complexity.
While writing I think an alternative solution could be to pass an argument on the endpoint while doing the check, so that if successful, it stops and does not generate the PDF, and then redirect to the same endpoint without the flag which will output the PDF.
So I do the check twice but only load (and generate - which is resource intensive) the file only once and I have only one endpoint...
Here's the adapted code:
var url = '/endpoint/to/my/file';
$http({
method: 'GET',
url: url+'?check'
})
.success(function(jdata) {
window.location = url;
})
.error(function(je){
// display errors on page
});
On the backend side (I use Play framework/Scala)
def myendpoint(onlyDoCheck: Boolean = false) = Action{implicit request =>
myForm.bindFromRequest.fold(
e => BadRequest(myErrors),
v => if(onlyDoCheck) Ok(simpleOkResponse) else Ok(doComputationgeneratefileandoutputfile)
)
}
The real deal
The best you could do is split your endpoint.
One for the form and the convenience of having errors without refresh.
Then, on success, redirect to your other endpoint which only downloads the file.
If the file was on the disk and wasn't auto-generated and required to be authenticated to be downloaded, you could hide the file behind a normal endpoint, do the checks, and return the file using X-Accel-Redirect header with nginx or X-Sendfile using apache.
The hack
Disclaimer: This is more of a hack than the best solution. As mention by #Iceman, Safari, IE, Safari-iOS, Opera-mini and some such browsers don't support this particular spec.
In your server-side endpoint, if the file is available without errors, you can set the header to the content-type of the file (like 'application/pdf') so the download will starts automatically.
If there are errors, don't set the header and return a json of the errors to inform the user through javascript.
Since we don't know what's behind, here's a python (Django) example:
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename=your_filename.pdf'
response.write(repport.printReport(participantId))
return response
You can handle the response in the ajax success callback:
$.ajax({
url: 'endpoint.php',
success: function(data) {
var blob = new Blob([data]);
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = "filename.pdf";
link.click();
}
});
You could also try the jQuery fileDownload plugin mentioned in this answer.

Creating Blob object from raw file downloaded from server with $http.

On server there is url with files stored.
Url is http://website.com/abc
I do $http get on this url.
$http.get(url, { responseType: "arraybuffer" });
I want to create Blob object from this. I am sure that object is of type png, because it had this extension before upload and it shows properly.
new Blob(result.data, {type: "image/png"});
I get message:
Failed to construct 'Blob': The 1st argument is neither an array, nor does it have indexed properties.
Response from server http://website.com/abc GET in developer console looks like:
ÿØÿàJFIFÿþ;CREATOR: gd-jpeg v1.0 (using IJG JPEG v62), quality = 60
ÿÛC

 ' .)10.)-,3:J>36F7,-#WAFLNRSR2>ZaZP`JQROÿÛC&&O5-5OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOÿÂÒ"ÿÄÿÄÿÚæå2ag\Ý#úDê3[Zdfc5±Ô¢¬æ)K<`¤l2¸ÊánYR±aA`Í%RÈfbz!¤tÞÑ-µd7ZªÀ[hz¨f¥>©cAV¬{3á R³F0 W>~c³"ðÈìøÖ²ÇBÙ³±+
½ò9tµ°õ
I tried to set Blob type to application/octet-stream and also do $http.get without specified responseType.
How can I create a proper Blob file?
I need Blob File, to create File object which is an entry data to implemented logic to display files in slideshow with modals. I have implemented logic for files of type File, which were created by input forms - implementation was done for a need before uploading to server. Now it turns out that server doesn't return same files to me, but return only urls of files, which created an idea to convert from url to File in order to dont repeat in that logic.
Try
$http.get(url, { responseType: "blob" });
or
// missing `[]` at `js` at Question
new Blob([result.data], {type: "image/png"});
Note XMLHttpRequest responseType could also be set to "blob", see How to build PDF file from binary string returned from a web-service using javascript

How to load foreign image via POST request in browser?

My web application (HTML5 + JavaScript) needs to display PNG images that are generated by foreign web service.
However, that web service supports POST requests only. (More exactly, it does provide GET requests, but I have to transmit large arguments, due to which the GET URL becomes too long.)
Also, the web service has a different domain than the web application, and doesn't supply proper CORS headers, so Ajax (XMLHTTPRequest) doesn't work.
Is it still possible for my web application to load and display the foreign image via POST request?
I'm asking for a solution that is different from the following nasty workarounds, which are already well-known to me:
without setting up a local proxy that translates the request (and also circumvents the same-origin policy)
without using the remote proxy of some stranger
without using Flash
without using Java Applets
without using OS specific functionality such as ActiveX controls
However, a solution that fails to work with Internet Explorer is acceptible. Even a Firefox or Chrome specific solution is appreciated.
Horrible hack:
Submit a form to an iframe and have the image displayed in the iframe.
(But don't do this, it sounds like the web server is designed to avoid having images being embedded directly in other sites.)
I have some possible solutions...
Solution 1
If your image is less that 25kb you can do the following via YQL: select * from data.uri where url="http://jquery.com/jquery-wp-content/themes/jquery/images/logo-jquery#2x.png" As a result you can just grab the base64 image and carry on. To do a POST via YQL you should add something like and postdata="foo=foo&bar=bar" check out this article.
Caveat: The performance of this method is probably not great. There's a fair amount of latency making the hop from the end user to YQL to the service and then going all the way back. Also there is some server side processing YQL does to base64 encode the image and deliver some JSON response.
Solution 2
Get CORS enabled or go through some other proxy. Once you do so, if you still can't get base64 data then you need to do 2 things. First add a jQuery transport that handles binary. Second process the binary blob and convert it to base64.
Here is a jQuery Binary Transport I found
$.ajaxTransport("+binary", function(options, originalOptions, jqXHR){
// check for conditions and support for blob / arraybuffer response type
if (window.FormData && ((options.dataType && (options.dataType == 'binary')) || (options.data && ((window.ArrayBuffer && options.data instanceof ArrayBuffer) || (window.Blob && options.data instanceof Blob)))))
{
return {
// create new XMLHttpRequest
send: function(headers, callback){
// setup all variables
var xhr = new XMLHttpRequest(),
url = options.url,
type = options.type,
async = options.async || true,
// blob or arraybuffer. Default is blob
dataType = options.responseType || "blob",
data = options.data || null,
username = options.username || null,
password = options.password || null;
xhr.addEventListener('load', function(){
var data = {};
data[options.dataType] = xhr.response;
// make callback and send data
callback(xhr.status, xhr.statusText, data, xhr.getAllResponseHeaders());
});
xhr.open(type, url, async, username, password);
// setup custom headers
for (var i in headers ) {
xhr.setRequestHeader(i, headers[i] );
}
xhr.responseType = dataType;
xhr.send(data);
},
abort: function(){
jqXHR.abort();
}
};
}
});
Once you add the transport you can make any sort of AJAX request.
$.ajax({
type: "POST",
url: 'http://myservice.com/service/v1/somethingsomething',
dataType: 'binary',
success: function(imgData) {
var img = new Image(),
reader = new window.FileReader();
reader.readAsDataURL(imgData);
reader.onloadend = function() {
img.src = reader.result
$('#logo-events').append(img);
}
}
});
The reader should take the Blob and output a base64 version. When the reader is done converting/reading it will create and image and append it somewhere. GET or POST should not matter any more.
I found this related question: Post data to JsonP
And I think that it could be applicable in your case.
Basically, fire your jsonp request to your server (same-origin-policy should not be a problem), and load the response an <img>
Like #Quentin's answer, this hack uses a (hidden) Iframe

Categories

Resources