File download via Webapi controller - handling errors - c#

I am working on an application that provides some links for users to download files.
The page itself is served up by an MVC controller but the links are pointing to a WebAPI controller running on a separate domain.
(I would have preferred same domain but for various reasons it has to be a separate project and it will run on a separate domain. I don't think CORS is part of the issue anyway as this is not using XHR, but I mention it just in case).
So in development, the main MVC project is http://localhost:56626/Reports/
And the links on the page might look like this:
Report 12345
where port 51288 is hosting the Web API.
The WebAPI controller uses ReportID to locate a file, and write its contents into the response stream, setting the disposition as an attachment:
//security.permission checks and scaffolding/database interaction
//left out for clarity
try
{
string filename = #"C:\ReportFiles\TestReport.csv";
var stream = new FileStream(path, FileMode.Open);
result.Content = new StreamContent(stream);
result.Content.Headers.ContentType = new MediaTypeHeaderValue("text/csv");
var disp = new ContentDispositionHeaderValue("attachment");
disp.FileName = "TestReport.csv";
result.Content.Headers.ContentDisposition = disp;
return result;
}
catch(Exception ex)
{
//how to return a response that won't redirect on error?
}
By doing this the user can then click on the link and without any redirection, the user gets prompted to save or open the file, which is what I want; they stay on the original page with the links and just get an Open/Save dialog from the browser.
The problem arises when something goes wrong in the Web API controller - either an exception or some internal logic condition that means the file cannot be downloaded.
In this case when clicking the link, the download doesn't happen (obviously) and they get taken to the target URL instead i.e http://localhost:51288/api/ReportDownload?ReportID=12345 which is not desirable for my requirements.
I would much rather be able to catch the error somehow on the client-side by returning for e.g. HTTP 500 in the response, and just display a message to the user that the download failed.
Now to be honest, I don't even understand how the browser can do the "in place" File/Save dialog in the first place:
I always thought if you click a link that has no explicit target attribute,the browser would just open the new request in your current tab i.e it's just another GET request to the target URL, but it seems this is not the case
The browser seems to be doing a hidden background fetch of the target URL in this case (same behaviour in FF,Chrome and IE) which I cannot even see in the F12 tools.
The F12 Network log shows no activity at all except in the specific case where the response has NOT been setup as Content-Disposition: attachment i.e an error -only in this case do I see the (failed) HTTP GET being logged in the Network request list.
I suppose I could just catch any exception in the controller and send back a dummy file called "Error.csv" with contents "Ha Ha Nope!" or something similar, but that would be a last resort...any ideas welcome!

If the user clicks on the link, the browser will follow it - then depending on the response headers and browser configuration, it'll either show the file dialog or render directly - you can't really change that behavior (apart from using preventDefault when the link is clicked, which kind of defeats the purpose).
I'd suggest taking a closer look at http://jqueryfiledownload.apphb.com/ which lets you do something like this:
$.fileDownload('some/file/url')
.done(function () { alert('File download a success!'); })
.fail(function () { alert('File download failed!'); });
Then you could bind the download action using jQuery.

Related

How to send GET request from internet explorer to Asp.Net Core API?

I am developing Asp.Net Core 3.1 API, Everything working as expected when I send a GET request from google chrome, Edge, Postman. But when I send GET request from internet explorer it starts to download a file default.json with the content as the response of GET request.
Defualt Action method:
public IEnumerable<string> Get()
{
return new string[] { "Welcome" };
}
default.json content:
[
"Welcome"
]
I search on the internet but could not find anything useful.
FVI, I have the same observation when I run the API using visual studio or the deployed API on the server using IIS.
IE Version: 11.900.18362.0
So I have to questions.
Does IE not support this, Is this default behavior of IE?
If Yes then how can it be fixed?
This is IE default behavior, and comes down to it simply doesn't know how to treat content with mime types like */json, hence suggest a download.
Assuming this is for users in general, and you simply want to display the json data in a browser, you could convert the content server side to text.
public ContentResult Get()
{
var jsondata = new string[] { "Welcome" };
return Content(JsonSerializer.Serialize(jsondata));
}
If you are going to do something with the actual json data, which one usually does when consuming an api, you will use some kind of client side script (e.g. Ajax as in below sample, or similar) to get the content, and in those cases there won't be any problem, like the one you encountered.
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/your-method', true);
xhr.onload = function (e) {
if (this.status == 200) {
var jsonstring = this.responseText;
// do something with the json string, e.g. JSON.parse(jsonstring)
}
};
xhr.send();
Here's a couple of posts that suggests to change the registry, though they won't be viable unless it is for a local computer of your own (and if it is, picking a browser that works out-of-the-box must be easier).
Display JSON in IE as HTML Without Download
How can I convince IE to simply display application/json rather than offer to download it?
Edit
As suggested in a comment, yet another option would be to change the mime type explicit:
Json response download in IE(7~10)

How to clean up existing response in webapi?

There is a authentication library that I have to use that helpfully does things like
Response.Redirect(url, false);
inside of it's method calls. I can't change this libraries code and it's fine for MVC style apps but in angular SPA -> WebApi apps this is just awful.
I really need a 401 otherwise I get into trouble with CORS when my angular scripts, using $http, try to call out to the auth server on another domain in response to the 302, that's if it even could as the Response.Redirect also sends down the object moved html and the angle brackets cause an error to be thrown.
Since I have to make the call to the auth library first the Response.Redirect is already in the response pipeline and so I need to clean it up to remove the body content and convert the 302 into a 401. I thought I could just:
return new HttpWebResponse(StatusCode.UnAuthorized){
Content = new StringContent("data");
}
but this just gets appended to the response and doesn't replace it plus I also need the Location: header which I can't seem to access via WebApi methods.
So instead I've had to do this in my ApiController:
var ctxw = this.Request.Properties["MS_HtpContext"] as HttpContextWrapper;
var ctx = ctxw.ApplicationInstance.Context;
var url = ctx.Response.RedirectLocation;
ctx.Response.ClearContent();
return new HttpWebResponse(StatusCode.UnAuthorized){
Content = new StringContent(url);
}
But this seems terrible and counter to webapi "feel". Plus I'm tied to the controller in doing this. I can't get the wrapper in a MessageHandler for example.
What I'd like to do is monitor the response for a given route in a message handler or in an AuthorizationFilterAttribute, if its a 302, I want to read it's headers, take what I want, wipe it and replace it with my own "fresh" response as a 401. How can I do this?
You might want to write your own ActionFilter and override its OnActionExecuted method where you can access HttpActionExecutedContext. From there, you can check response code, for example, and overwrite response with whatever you want.
Ref: https://msdn.microsoft.com/en-us/library/system.web.http.filters.actionfilterattribute.onactionexecuted%28v=vs.118%29.aspx#M:System.Web.Http.Filters.ActionFilterAttribute.OnActionExecuted%28System.Web.Http.Filters.HttpActionExecutedContext%29

Cannot figure out how this jQuery code is working

So there is a camera taking images every couple seconds and storing those new images with new files names on the server. When a request is made to "mypage", server side the latest images are loaded up and returned in the response. The images subsequently being refreshed with this jQuery code:
(function($) {
$(function() {
var refreshInterval = 5; // Number of seconds between image refreshes
$('#deskshare-grid img').each(function() {
$(this).data('src', $(this).attr('src'));
});
function refreshImages() {
$.get('?r=' + Math.random(), function(response) {
$('#deskshare-grid img').each(function(index) {
$(this).attr('src', $(response).find('#deskshare-grid img').eq(index).attr('src'));
});
setTimeout(refreshImages, refreshInterval * 1000);
});
}
setTimeout(refreshImages, refreshInterval * 1000);
});
})(jQuery);
The jQuery code I shared works and that is great, I didn't write the code and I want to know how it works.
My mind is stuck on the fact that a request was made for the page, the most recent image was retrieved on the server using C# and those images are included in the response. When a more recent image is created, it has a new file name.
How can jQuery refresh the photo for a file name it does not know client side?
Particularly this part of the code is confusing me:
$.get('?r=' + Math.random(), function(response) {
What is the url request for this $.get? I see the network tab of my F12 tools showing the new image responses but I do not understand how an image with a different file name could be requested with jQuery.
UPDATE
The accepted answer is correct but I wanted to elaborate. This jQuery is requesting the entire page again. The HTML response contains new image urls from the server. jQuery is used to parse the response, get the latest image urls and than update the existing HTML content with those new image urls parsed out of the response. This way there is no page flicker by trying to just refresh the entire page.
It does get request to the same page, Math.random() is to make easier to view each request. When you make a request to ? that is the same page.
What is the url request for this $.get?
The first parameter in the $.get is a relative url. This means that the url it's trying to access will be something along the lines of www.yourdomain.com/whatever?r=.
The "?" indicates the start of a query string, the "r" is the start of the request variable and whatever follows the equal sign is the query itself. In this particular case the query is just a randomly generated number that is sent up to the server. Without seeing the server-side code it would appear as if the filename is generated on the client and sent up to the server in this manner, and is probably used to name the file then on the server-side then.
The $.get('?r=' + Math.random(), function(response) { could be this two things:
Its changing the URL as a trick to not to get cached content.
Is server side required as dummy or something that we don't really know.
I recommend to look at the request's and response's headers for each 5'' call.

Design - Log user downloads

I have to log the download requests for each file that is grabbed off of a website. This log has to have the Employee ID, Solution ID, IP Address. I've used quite a few methods -
First, I was using a model where I was putting the path of the file
in an anchor tag . Whenever the user clicked on this anchor tag, I
was generating an AJAX request to log the file download.
But the huge drawback of this is that the user can just copy the file and paste it in a seperate window to get the file. That would ensure that the download was not logged.
Second,
When I was processing the ajax request in the web method in a page. I tried transmitting the file through HttpResponse, but that didn't work either.
HttpContext.Current.Response.TransmitFile("filename");
jQuery ajax call kept failing, and I never got the file on the client side.
The key thing is, I have to do the whole thing without refreshing the page.
I'm wondering if this is possible at all...
You could implement a IHttpHandler that logs the request, retrieves the file and serves it. This way, even if the link is copied and pasted directly, it would still log it.
public class SimpleHandler : IHttpHandler
{
public bool IsReusable
{
get { return false; }
}
public void ProcessRequest(HttpContext context)
{
string fileToServe = context.Request.QueryString["file"];
if (!string.IsNullOrEmpty(fileToServe))
{
//Log request here...
context.Response.ContentType = "content type for your file here";
context.Response.WriteFile("~/path/" + fileToServe);
}
}
}
You could use the AJAX method, with an identifier in the link to be used as a parameter value to refer to the file - rather than storing the full path - and have your web method return the serialized data of the file.
So, your web method might look something like this:
[WebMethod]
public static string GetDownload(string someIdentifier) {
// get the contents of your file, then...
// do your logging, and...
var serializer = new JavaScriptSerializer();
return serializer.Serialize(fileByteArrayOrSuch);
}
Then handle the file contents on the client side. There will doubtless be some more trifling elements to add to your function for the sake of logging; but the bottom line is, your AJAX can both handle logging and then process the download request.
This is very much possible – you need to return a file as a response to an action (Mvc) or to an aspx page (webforms). So when the action or aspx page is hit you can log the request and write the file to the response.
Edit: for webforms example, see this SO question
For mvc:
public ActionResult DownloadFile(string fileHint)
{
// log what you want here.
string filePath = "determine the path of the file to download maybe using fileHint";
return File(filePath, "application/octet-stream"); // second argument represents file type; in this case, it's a binary file.
}

Sending a 410 Gone header, then redirecting

I've got a page that checks if a user is logged in or the file is public then pushes a PDF to the browser via Response.WriteFile.
Works great except when Google indexes a file and then we remove the file. So I'm looking at adding a 410 Gone to the Response.Status and then redirecting to our error.aspx page.
Firebug tells me that it gets a "302 Found" status code on the document page when a file is deleted. I'm expecting a 410.
Redirect code is:
Response.Status = "410 Gone";
Response.AddHeader("Location", Request.Url.ToString());
Response.Redirect("error.aspx");
Could someone please tell me what I'm getting wrong please?
Redirection is done by sending a status that indicates that the resource is available somewhere else, such as 301 Moved Permanently or 302 Found. You can't send two status codes in the same response. Either the requested resource does not exist (so you send 410) or it does exist at some other location (so you send 301 or 302 or whatever).
I don't think you should be redirecting to an error page, though, because an error message isn't a separate resource that should have its own URL. If a client requests a file and the file is gone, send a 410 Gone status with the error message as the response body — that way the error message comes back from the URL of the nonexistent file itself. A search engine will see the status code and understand that the file is gone, and a browser will show the response body to the user so he can read the error message.
If you look at the spec for 410 Gone, it states that "no forwarding address is known", so a redirect does not seem valid. You can return that same body on that response that you would from error.aspx if you want human users to see something.
The requested resource is no longer available at the server and no
forwarding address is known. This condition is expected to be
considered permanent. Clients with link editing capabilities SHOULD
delete references to the Request-URI after user approval. If the
server does not know, or has no facility to determine, whether or not
the condition is permanent, the status code 404 (Not Found) SHOULD be
used instead. This response is cacheable unless indicated otherwise.
The 410 response is primarily intended to assist the task of web
maintenance by notifying the recipient that the resource is
intentionally unavailable and that the server owners desire that
remote links to that resource be removed. Such an event is common for
limited-time, promotional services and for resources belonging to
individuals no longer working at the server's site. It is not
necessary to mark all permanently unavailable resources as "gone" or
to keep the mark for any length of time -- that is left to the
discretion of the server owner.
You can also use this way, first change your desire status.
Response.Status = "410 Gone";
Response.AddHeader("Location", Request.Url.ToString());
ScriptManager.RegisterStartupScript(this, this.GetType(), "redirectScript", "window.location.href=error.aspx", true);`
So, in that you get your desire page and status as well.
This is how I had to do a 301 moved permantently response. It should be similar
//in Global.asax.cs
protected virtual void Application_BeginRequest (Object sender, EventArgs e)
{
if(Request.Url.Host=="www.earlz.biz.tm" || Request.Url.Host=="earlz.biz.tm" || Request.Url.Host=="www.lastyearswishes.com"){
Response.Status = "301 Moved Permanently";
Response.AddHeader("Location","http://lastyearswishes.com"+Request.Url.PathAndQuery);
CompleteRequest(); //I believe this is the missing piece in your code.
}
}
I have been solving similar issue. If the page with resource is removed from the web, I woul like to tell Google Bot 410 Gone, remove from cache, but I would like to offer an alternative similar page to the visitor.
I have solved it like this:
public ActionResult RealEstate(int id, string title)
{
...prepare the model
if (realEstateModel.Result.OfferState == OfferState.Deleted)
{
var alternativeSearchResult = PrepareAlternative(realEstateModel);
return Gone410(alternativeSearchResult, context);
}
else
return View(realEstateModel);
}
Gone410.cshtml look slike this:
#model Bla.ModelGone410
#{
Layout = null;
Html.RenderAction("Index", "Search",
new
{
type = Model.type,
Category = Model.Category,
city_id = Model.city_id,
...
});
}
and RealEstate.cshtml:
#model Bla.realEstateModel
#{
Layout = null;
}
This is realestate view...
This gives the response 410 to google bot and search alternative for the user with no redirect.

Categories