Can I abort a large FileStreamResult download on server side? - asp.net-mvc

I have an action on which I simply download a file. Sometimes the user wants the download to be aborted and not to wait to be finished if the file was large.
private IActionResult Download(string path)
{
var length = new FileInfo(path).Length;
Response.Headers.Add("size", length.ToString());
Stream str = System.IO.File.OpenRead(path);
return File(str, "application/x-zip-compressed");
}
What I would like to know is if on backend we can abort that process, maybe storing on ram some task to be cancelled on another process.
EDITED for better understanding: the file downloads into a different device which calls the download method after I order, from my browser, that this device has to download the file. So the task need to be aborted on server side by calling an action from my browser.

On the server side, you can get notification when the request is aborted via HttpContext.RequestAborted, which is a CancellationToken that is cancelled when the underlying connection for the request is aborted. So you could pass it down into asynchronous methods that you call if they support cancellation, or you could hook a callback to process cancellation logic via CancellationToken.Register().
However, in your code sample, this is not necessary. All you are doing is creating a FileStream without reading the contents of the file. The actual reading of the file content is performed by the framework, when it processes the FileStreamResult which is created by the call to File(str, "application/x-zip-compressed"). As you can see from the source code for FileResultExecutorBase, the framework will automatically cancel the file reading when HttpContext.RequestAborted is cancelled.

Well, I've finally solved this issue by handling a global collection of HttpContexts. I store the download request HttpContext when my download action is called and the download starts. Also, I have a different action/method/whatelse where I read the context and I call .Abort() method.
private void CancelDownload(string key) //action, method...
{
string uniqueKeyDownload = externalDevice.ID + "#" + fileID;
log.Information("Cancelling download 1/3..." + uniqueKeyDownload );
if (Startup.MyDictionaryContexts.ContainsKey(uniqueKeyDownload ))
{
log.Information("Cancelling download 2/3..." + uniqueKeyDownload );
if (!Startup.MyListDownloadsCancelled.Contains(uniqueKeyDownload ))
{
log.Information("Cancelling download 3/3..." + uniqueKeyDownload );
Startup.MyListDownloadsCancelled.Add(uniqueKeyDownload );
Startup.MyDictionaryContexts[uniqueKeyDownload ].Abort();
}
}
}
public IActionResult Download(string path)
{
string uniqueKeyDownload = externalDevice.ID + "#" + fileID;
if (!Startup.MyDictionaryContexts.ContainsKey(uniqueKeyDownload ))
{
Startup.MyDictionaryContexts.Add(uniqueKeyDownload, HttpContext);
}
HttpContext.Response.OnCompleted(async () =>
{
//remove context from dictionary
if (Startup.MyDictionaryContexts.ContainsKey(uniqueKeyDownload))
Startup.MyDictionaryContexts.Remove(uniqueKeyDownload);
//Log if event was fired on completion or abort call
if (Startup.MyListDownloadsCancelled.Contains(uniqueKeyDownload))
{
Startup.MyListDownloadsCancelled.Remove(uniqueKeyDownload);
log.Information("DOWNLOAD CANCELLED!!!");
}
else
log.Information("DOWNLOAD COMPLETED!!!");
});
//if you are downloading on browser and make recalls, manage this block
// with conditions and return NoContent() or similar
var length = new FileInfo(path).Length;
Response.Headers.Add("size", length.ToString());
Stream str = System.IO.File.OpenRead(path);
log.Information("DOWNLOAD STARTED..." + uniqueKeyDownload);
return File(str, "application/x-zip-compressed");
}
I've realized that, depending on the client that is downloading the file, there is different behaviors. If the client is a http client from Java (my case), there will be a socket exception at client and that's all. But in case you are downloading on a browser (tested with Chrome), after calling .Abort() the browser will request one more time the download Action and, in this case, you will have to manage some flags to return NoContent() or similar instead of return File(..) again.

Related

Response Header issue on Azure Web Application

I am not sure what is happening here.
When I run my web application locally and click a button to download a file, the file is downloaded fine and Response header as you can see in the attached screenshot where it says local.
But when I publish the application to azure web app. Somehow the download button stops working. I checked the Response Header and you can see the difference.
What would cause this problem? The code is the same? Is there any settings that I should be setting in azure web app in azure portal?
Updated to add code
I have debugged remotely to figure out what is going on as #Amor suggested.
It is so strange that When I debug on my local machine first ExportTo action gets hit which prepares the TempData then Download action gets called once the first action completed with ajax call.
However, this is not the case when I debug remotely. Somehow the ExportTo action never gets called. It directly calls the Download action. As a result the TempData null checking is always null.
But why? Why on earth and how that is possible? Is there something cached somewhere?
I have wiped the content of web application on the remote and re-publish evertyhing to ensure everything is updated. But still no success.
here is the code:
[HttpPost]
public virtual ActionResult ExportTo(SearchVm searchVm)
{
var data = _companyService.GetCompanieBySearchTerm(searchVm).Take(150).ToList();
string handle = Guid.NewGuid().ToString();
TempData[handle] = data;
var fileName = $"C-{handle}.xlsx";
var locationUrl = Url.Action("Download", new { fileGuid = handle, fileName });
var downloadUrl = Url.Action("Download");
return Json(new { success = true, locationUrl, guid = handle, downloadUrl }, JsonRequestBehavior.AllowGet);
}
[HttpGet]
public ActionResult Download(string fileGuid, string fileName)
{
if (TempData[fileGuid] != null)
{
var fileNameSafe = $"C-{fileGuid}.xlsx";
var data = TempData[fileGuid] as List<Company>;
using (MemoryStream ms = new MemoryStream())
{
GridViewExtension.WriteXlsx(GetGridSettings(fileNameSafe), data, ms);
MVCxSpreadsheet mySpreadsheet = new MVCxSpreadsheet();
ms.Position = 0;
mySpreadsheet.Open("myDoc", DocumentFormat.Xlsx, () =>
{
return ms;
});
mySpreadsheet.Document.Worksheets.Insert(0);
var image = Server.MapPath("~/images/logo.png");
var worksheet = mySpreadsheet.Document.Worksheets[0];
worksheet.Name = "Logo";
worksheet.Pictures.AddPicture(image, worksheet.Cells[0, 0]);
byte[] result = mySpreadsheet.SaveCopy(DocumentFormat.Xlsx);
DocumentManager.CloseDocument("myDoc");
Response.Clear();
//Response.AppendHeader("Set-Cookie", "fileDownload=true; path=/");
Response.ContentType = "application/force-download";
Response.AddHeader("content-disposition", $"attachment; filename={fileNameSafe}");
Response.BinaryWrite(result);
Response.End();
}
}
return new EmptyResult();
}
here is the javascript:
var exportData = function (urlExport) {
console.log('Export to link in searchController: ' + urlExport);
ExportButton.SetEnabled(false);
var objData = new Object();
var filterData = companyFilterData(objData);
console.log(filterData);
$.post(urlExport, filterData)
.done(function (data) {
console.log(data.locationUrl);
window.location.href = data.locationUrl;
});
};
When Export button is clicked exportData function is called:
var exportToLink = '#Url.Action("ExportTo")';
console.log('Export to link in index: '+exportToLink);
SearchController.exportData(exportToLink);
As I mentioned that this code works perfectly on the local machine. something weird is happening on azure webapp that ExportTo action breakpoint is never gets hit.
I am not sure what else I could change to get the ExportTo action hit?
Based on the Response Header of Azure Web App, we find that the value of Content-Length is 0. It means that no data has been sent from web app server side.
In ASP.NET MVC, we can response file using following ways.
The first way, send the file which hosted on server. For this way, please check whether the excel file has been uploaded to Azure Web App. You could use Kudu or FTP to the folder to check whether the file is exist.
string fileLocation = Server.MapPath("~/Content/myfile.xlsx");
string contentType = System.Net.Mime.MediaTypeNames.Application.Octet;
string fileName = "file.xlsx";
return File(fileLocation, contentType, fileName);
The second way, we can read the file from any location(database, server or azure storage) and send the file content to client side. For this way, please check whether the file has been read successfully. You can remote debug your azure web app to check whether the file content hasn't been read in the right way.
byte[] fileContent = GetFileContent();
string contentType = System.Net.Mime.MediaTypeNames.Application.Octet;
string fileName = "file.xlsx";
return File(fileContent, contentType, fileName);
5/27/2017 Update
Somehow the ExportTo action never gets called. It directly calls the Download action. As a result the TempData null checking is always null.
How many instances does your Web App assigned? If your Web App have multi instances, the ExportTo request is handled by one instance and the Download request is handled by another instance. Since the TempData is store in memory of dedicated instance, it can't be got from another instance. According to the remote debug document. I find out the reason why the ExportTo action never gets called.
If you do have multiple web server instances, when you attach to the debugger you'll get a random instance, and you have no way to ensure that subsequent browser requests will go to that instance.
To solve this issue, I suggest you response the data directly from the ExportTo action or save the temp data in Azure blob storage which can't be accessed from multi instances.

HttpClient request is not working in asp .net Project

I am using HttpClient to make a request to an api. This code is located in a class library project shared with two aditional projects, a Console and a Asp.Net Mvc project. When I make a request from the Console project it works great, but in the asp project it blocks in the line
using(Stream responseStream = await response.Content.ReadAsStreamAsync()
this is my request code
private async Task<dynamic> ReadJson(string url)
{
HttpResponseMessage response = await httpClient.GetAsync(url);
if (response.StatusCode == System.Net.HttpStatusCode.NoContent)
throw new RateLimitException();
if (response.StatusCode == System.Net.HttpStatusCode.Forbidden)
throw new AccessDeniedException();
if (response.StatusCode != System.Net.HttpStatusCode.OK)
throw new Exception("Error: " + response.StatusCode);
using (Stream responseStream = await response.Content.ReadAsStreamAsync())
using (StreamReader sr = new StreamReader(responseStream, System.Text.Encoding.UTF8))
{
string json = sr.ReadToEnd();
return JObject.Parse(json);
}
}
I am making the same call to the method from the Console and the Asp.Net project. From the console works but the asp .net project blocks in the line when reads the response content
Most probably this deadlock occurs because the controller action that calls ReadJson function is synchronous. You need to make the action async. You can find an excellent explanation of this deadlock here. (All the credits go to Stephen Cleary)
Quick summary is:
/ My "library" method.
public static async Task<JObject> GetJsonAsync(Uri uri)
{
using (var client = new HttpClient())
{
var jsonString = await client.GetStringAsync(uri);
return JObject.Parse(jsonString);
}
}
// My "top-level" method.
public class MyController : ApiController
{
public string Get()
{
var jsonTask = GetJsonAsync(...);
return jsonTask.Result.ToString();
}
}
What Causes the Deadlock
The top-level method calls GetJsonAsync (within the UI/ASP.NET context). GetJsonAsync starts the REST request by calling
HttpClient.GetStringAsync (still within the context).
GetStringAsync returns an uncompleted Task, indicating the REST request is not complete.
GetJsonAsync awaits the Task returned by GetStringAsync. The context is captured and will be used to continue running the
GetJsonAsync method later. GetJsonAsync returns an uncompleted
Task, indicating that the GetJsonAsync method is not complete.
The top-level method synchronously blocks on the Task returned by GetJsonAsync. This blocks the context thread.
… Eventually, the REST request will complete. This completes the Task that was returned by GetStringAsync.
The continuation for GetJsonAsync is now ready to run, and it waits for the context to be available so it can execute in the context.
Deadlock. The top-level method is blocking the context thread, waiting for GetJsonAsync to complete, and GetJsonAsync is waiting for
the context to be free so it can complete.
Preventing the Deadlock
There are two best practices that avoid this situation:
In your “library” async methods, use ConfigureAwait(false) wherever possible.
Don’t block on Tasks; use async all the way down.

Getting 401 Unauthorized from Google API Resumable Update

I am trying to integrate upload of arbitrary files to Google Docs into an existing application. This used to work before using resumable upload became mandatory. I am using Java client libraries.
The application is doing the upload in 2 steps:
- get the resourceId of the file
- upload the data
To get the resourceId I am uploading a 0-size file (i.e. Content-Length=0). I am passing ?convert=false in the resumable URL (i.e. https://docs.google.com/feeds/upload/create-session/default/private/full?convert=false).
I am passing "application/octet-stream" as content-type. This seems to work, though I do get different resourcesIds - "file:..." resourceIds for things like images, but "pdf:...." resourceIds for PDFs.
The second step constructs a URL based on the resourceId obtained previously and performs a search (getEntry). The URL is in the form of https://docs.google.com/feeds/default/private/full/file%3A.....
Once the entry is found the ResumableGDataFileUploader is used to update the content (0-byte file) with the actual data from the file being uploaded. This operation fails with 401 Unauthorized response when building ResumableGDataFileUploader instance.
I've tried with ?convert=false as well as ?new-revision=true and both of these at the same time. The result is the same.
The relevant piece of code:
MediaFileSource mediaFile = new MediaFileSource(
tempFile, "application/octet-stream");
final ResumableGDataFileUploader.Builder builder =
new ResumableGDataFileUploader.Builder(client, mediaFile, documentListEntry);
builder.executor(MoreExecutors.sameThreadExecutor());
builder.requestType(ResumableGDataFileUploader.RequestType.UPDATE);
// This is where it fails
final ResumableGDataFileUploader resumableGDataFileUploader = builder.build();
resumableGDataFileUploader.start();
return tempFile.length();
The "client" is an instance of DocsService, configured to use OAuth. It is used to find "documentListEntry" immediately before the given piece of code.
I had to explicitly specify request type, since it seems the client library code contains a bug causing NullPointerException for "update existing entry" case.
I have a suspicion that the issue is specifically in the sequence of actions (upload 0-byte file to get the resourceId, then update with actual file) but I can't figure out why it doesn't work.
Please help?
This code snippet works for me using OAuth 1.0 and OAuth 2.0:
static void uploadDocument(DocsService client) throws IOException, ServiceException,
InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(10);
File file = new File("<PATH/TO/FILE>");
String mimeType = DocumentListEntry.MediaType.fromFileName(file.getName()).getMimeType();
DocumentListEntry documentEntry = new DocumentListEntry();
documentEntry.setTitle(new PlainTextConstruct("<DOCUMENT TITLE>"));
int DEFAULT_CHUNK_SIZE = 2 * 512 * 1024;
ResumableGDataFileUploader.Builder builder =
new ResumableGDataFileUploader.Builder(
client,
new URL(
"https://docs.google.com/feeds/upload/create-session/default/private/full?convert=false"),
new MediaFileSource(file, mimeType), documentEntry).title(file.getName())
.requestType(RequestType.INSERT).chunkSize(DEFAULT_CHUNK_SIZE).executor(executor);
ResumableGDataFileUploader uploader = builder.build();
Future<ResponseMessage> msg = uploader.start();
while (!uploader.isDone()) {
try {
Thread.sleep(100);
} catch (InterruptedException ie) {
throw ie; // rethrow
}
}
DocumentListEntry uploadedEntry = uploader.getResponse(DocumentListEntry.class);
// Print the document's ID.
System.out.println(uploadedEntry.getId());
System.out.println("Upload is done!");
}

Struts2 JMS request processing long running process

I have a Struts2 Action class that places a JMS Fetch request for a list of Trade in a JMS Queue. This JMS Fetch message is processed by an external process and can take either a few seconds or even few minutes depending on the number of Trade files to be processed by the external task processing app.
I want to know how to handle this HTTP Request with an appropriate response. Does the client wait till the list of Trades is returned? (client (UI) has to action on it and has nothing else to do meanwhile).
The way I approached it is
HTTP Request -->
Struts2 Action -->
Invokes a Runnable to run in a separate Thread (separate from Action class)
UI waits
Action class thread sleeps till runnable does it's job
When Task completed, return list of Trades to UI
Flow is as follows:
Place JMS Fetch Request on Queue1
ExecutorService for Runnable
CClass cclass = new CClass();
final ExecutorService execSvc = Executors.newFixedThreadPool(1);
execSvc.execute(cclass);
Where CClass implements runnable returning a list of Trades:
List<Trade> tradesList = new ArrayList<Trade>();
#Override
public void run() {
while (true) {
try {
Message message = msgConsumer.receive(); // SYNCHRONOUS / NO MDB
if (message == null){
break;
}
if (message instanceof TextMessage) {
TextMessage txtMessage = (TextMessage) message;
Trade trade = TradeBuilder.buildTradeFromInputXML(txtMessage);
if (trade != null) {
tradesList.add(trade); // tradeList is a CClass class variable
}
}
} catch (JMSException e) {
logger.error("JMSException occurred ", e);
}
}
closeConnection();
}
And while this runnableis executing, I do a Thread.sleep in Action class (to let the Runnable execute in the separate Thread)
// In Action class
try {
Thread.sleep(5000); // some time till when the runnable will get executed
} catch (InterruptedException e) {
e.printStackTrace();
}
execSvc.shutdown();
Problem is If I use Callable with a FutureTask and do a get() , that will be blocking till any result is returned. If I do a Runnable, I have to put Action class Thread to sleep till runnable has executed and tradeList is available.
Using Runnable approach, I am able to get couple of hundred records back to UI giving a 5 second Thread.sleep() in main Action class, but only partially constructed tradeList when thousands of records are to be fetched and shown in UI.
This is clearly Not a fail-proof approach.
Any better approach to suggest ? Please elucidate steps for processing in one complete request - response flow.
Yes there is a much better approach when making a standard HTTP request (with ajax you can do other things).
You want to look at the Struts2 Execute and Wait Interceptor Which has most of the functionality you've already implemented. Also look at the token interceptor... which could be useful (it prevents duplicate requests, but doesn't provide a happy wait screen like exec and wait does).

ASP.NET MVC 3 provide large file download to browser while server retrieves file from elsewhere

I need to provide a file-download feature where the web server retrieves the file from another source (via HTTP) and simultaneously streams it to the browser. I am guessing that using MVC's Controller.File ActionResult will not work, but I wrote a prototype like this anyway:
public ActionResult Download()
{
HttpWebRequest webRequest = (HttpWebRequest)HttpWebRequest.Create("http://somewhere/somefile.pdf");
HttpWebResponse webResponse = (HttpWebResponse)webRequest.GetResponse();
Stream stream = webResponse.GetResponseStream();
var mimeType = "application/pdf";
var fileName = "somefile.pdf";
return File(stream, mimeType, fileName);
}
This works fine, but there is no way to call Close() on the HttpWebResponse and Stream after the return statement. The help on the HttpWebResponse.GetResponseStream method says, "You must call either the Stream.Close or the HttpWebResponse.Close method to close the stream and release the connection for reuse. It is not necessary to call both Stream.Close and HttpWebResponse.Close, but doing so does not cause an error. Failure to close the stream will cause your application to run out of connections."
Should I create an HttpHandler and manually read bytes from the source stream and write them out to the response, along the lines of this or this? Is there another approach I'm not aware of?
While I'm not directly familiar with trying something like this, my first though was to do what you suggested in regards to reading in the stream, closing the connection, then returning the bytes as the response. Being a stream, I don't know how you can get around leaving it open for the sake of returning its contents as you do in your prototype, but then being able to close it when you're done.

Resources