How Angular handle chunked HTTP response? - c#

My ASP.NET API (.NET Core 3.1) application generates an excel file, saves it in local dir, reads like a fileStream and sends it to the client application (Angular 11.2.9). Everythings work fine in the local machine, but these two applications are running in OpenShift. My environment has response size limits and then the excel file is larger than 10 MB Angular starts crashing because it can't read the blob response correctly. To fix this I implemented Chunks in HTTP response. But I don't know how to handle these responses in Angular and I can't find any example of how to do that. Maybe someone can help this out?
My controller:
[HttpPost]
[Authorize]
public async Task<IActionResult> GenerateReport([FromBody] ReportDataFilteredModel data)
{
if (data.Report == null)
throw new ApiException("Report was not selected!");
var filePath = await _reportService.GenerateReport(data);
var response = HttpContext.Features.Get<IHttpResponseBodyFeature>();
response.DisableBuffering();
await response.StartAsync();
int i = 0;
var chunks = _fileService.ReadChunks(filePath, 1048576).ToList();
foreach (var block in chunks)
{
i++;
await response.Writer.WriteAsync(new ReadOnlyMemory<byte>(block));
await response.Writer.FlushAsync();
await Task.Delay(100);
Log.Information($"{i} chunk was send. Left {chunks.Count - i} more");
}
await response.CompleteAsync();
Log.Information("Excel was send succesfuly.");
return Ok();
}
My service.ts:
this.fileService.generateReport(this.reportDataFilteredModel).subscribe(
(response: Blob) => {
console.log(response);
const blob: any = new Blob([response], { type: 'application/vnd.ms-excel' });
const url = window.URL.createObjectURL(blob);
fileSaver.saveAs(blob, excelFileName);
this.notifier.showSuccessNotification();
},
(_error: HttpErrorResponse) => {
this.isExportingData = false;
this.notifier.showCustomNotification('error', _error.error);
console.log(_error);
},
() => {
this.isExportingData = false;
});
My API call method:
generateReport(appliedReportFilters: ReportDataFilteredModel): any{
const options = {
headers: new HttpHeaders({
Authorization: 'Bearer' + ' ' + localStorage.getItem('token'),
Connection: 'keep-alive'
}),
responseType: 'arraybuffer' as 'arraybuffer',
reportProgress: true
};
return this.http.post(`${environment.apiUrl}/api/reports`, appliedReportFilters, options)
.pipe(
catchError(err => {
if (err instanceof HttpErrorResponse || err.error instanceof ArrayBuffer || err.error.type === 'application/json') {
return new Promise<any>((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e: Event) => {
try {
const errmsg = JSON.parse((e.target as any).result);
reject(new HttpErrorResponse({
error: errmsg.Message,
headers: err.headers,
status: err.status,
statusText: err.statusText,
url: err.url ?? ''
}));
} catch (e) {
reject(err);
}
};
reader.onerror = (e) => {
reject(err);
};
reader.readAsText(err.error);
});
}
return throwError(err);
}));}
I tried without pipe and how I understand it doesn't have any difference.
I take examples of Chunks from there: link

Related

.NET Web API Blueimp multiple file upload error "Unexpected end of MIME multipart stream. MIME multipart message is not complete."

I am building a video management app that allows for uploading multiple videos to Azure Storage which is then encoded by Azure Media Services.
My issue is that if I upload just 1 file at a time with blueimp, everything works fine. When I add more than one file to the upload, I get the error on the second file.
Unexpected end of MIME multipart stream. MIME multipart message is not complete.
I have read that it could be to the stream missing the end of file terminator, so I added in the suggested tweak to append the line terminator (per this article ASP.NET Web API, unexpected end of MIME multi-part stream when uploading from Flex FileReference) with no luck.
If I post as single files (by either iterating over the files selected for upload) and send them as individual posts, it works. My issue is that I want to select several files as well as add additional metadata and hit one submit button. When I do it this way, the first file uploads, and the second one appears to start to upload but then I get the error 500 "Unexpected end of MIME multipart stream. MIME multipart message is not complete" message.
Here is my upload code (Web API):
[HttpPost]
public async Task<HttpResponseMessage> UploadMedia()
{
HttpResponseMessage result = null;
var httpRequest = HttpContext.Current.Request;
if (httpRequest.Headers["content-type"] != null)
{
httpRequest.Headers.Remove("content-type");
}
httpRequest.Headers.Add("enctype", "multipart/form-data");
if (httpRequest.Files.Count > 0)
{
var docfiles = new List<string>();
foreach (string file in httpRequest.Files)
{
var postedFile = httpRequest.Files[file];
var filePath = HttpContext.Current.Server.MapPath("~/" + postedFile.FileName);
string assignedSectionList = string.Empty;
postedFile.SaveAs(filePath);
docfiles.Add(filePath);
string random = Helpers.Helper.RandomDigits(10).ToString();
string ext = System.IO.Path.GetExtension(filePath);
string newFileName = (random + ext).ToLower();
MediaType mediaType = MediaType.Video;
if (newFileName.Contains(".mp3"))
{
mediaType = MediaType.Audio;
}
if (httpRequest.Form["sectionList"] != null)
{
assignedSectionList = httpRequest.Form["sectionList"];
}
MediaUploadQueue mediaUploadQueueItem = new MediaUploadQueue();
mediaUploadQueueItem.OriginalFileName = postedFile.FileName;
mediaUploadQueueItem.FileName = newFileName;
mediaUploadQueueItem.UploadedDateTime = DateTime.UtcNow;
mediaUploadQueueItem.LastUpdatedDateTime = DateTime.UtcNow;
mediaUploadQueueItem.Status = "pending";
mediaUploadQueueItem.Size = postedFile.ContentLength;
mediaUploadQueueItem.Identifier = random;
mediaUploadQueueItem.MediaType = mediaType;
mediaUploadQueueItem.AssignedSectionList = assignedSectionList;
db.MediaUploadQueue.Add(mediaUploadQueueItem);
db.SaveChanges();
byte[] chunk = new byte[httpRequest.ContentLength];
httpRequest.InputStream.Read(chunk, 0, Convert.ToInt32(httpRequest.ContentLength));
var provider = new AzureStorageMultipartFormDataStreamProviderNoMod(new AzureMediaServicesHelper().container);
provider.fileNameOverride = newFileName;
await Request.Content.ReadAsMultipartAsync(provider); //this uploads it to the storage account
//AzureMediaServicesHelper amsHelper = new AzureMediaServicesHelper();
string assetId = amsHelper.CommitAsset(mediaUploadQueueItem); //begin the process of encoding the file
mediaUploadQueueItem.AssetId = assetId;
db.SaveChanges();
////start the encoding
amsHelper.EncodeAsset(assetId);
}
result = Request.CreateResponse(HttpStatusCode.Created, docfiles);
}
else
{
result = Request.CreateResponse(HttpStatusCode.BadRequest);
}
return result;
}
Here is the code for the upload handler that sends to Azure Blob storage
public override Stream GetStream(HttpContent parent, HttpContentHeaders headers)
{
if (parent == null) throw new ArgumentNullException(nameof(parent));
if (headers == null) throw new ArgumentNullException(nameof(headers));
if (!_supportedMimeTypes.Contains(headers.ContentType.ToString().ToLower()))
{
throw new NotSupportedException("Only jpeg and png are supported");
}
// Generate a new filename for every new blob
var fileName = Guid.NewGuid().ToString();
if (!String.IsNullOrEmpty(fileNameOverride))
fileName = fileNameOverride;
CloudBlockBlob blob = _blobContainer.GetBlockBlobReference(fileName);
if (headers.ContentType != null)
{
// Set appropriate content type for your uploaded file
blob.Properties.ContentType = headers.ContentType.MediaType;
}
this.FileData.Add(new MultipartFileData(headers, blob.Name));
return blob.OpenWrite();
}
Here is the javascript code. The first one is sending the files individual as separate posts, it works.
$("#fileupload").fileupload({
autoUpload: false,
dataType: "json",
add: function (e, data) {
data.context = $('<p class="file">')
.append($('<a target="_blank">').text(data.files[0].name))
.appendTo(document.body);
data.submit();
},
progress: function (e, data) {
var progress = parseInt((data.loaded / data.total) * 100, 10);
data.context.css("background-position-x", 100 - progress + "%");
},
done: function (e, data) {
data.context
.addClass("done")
.find("a")
.prop("href", data.result.files[0].url);
}
});
This code below does not work. It pushes all the files into and array and sends them in one single post. This one fails on the second file. If I upload just one file using this code, it works.
var filesList = new Array();
$(function () {
$('#fileupload').fileupload({
autoUpload: false,
dropZone: $('#dropzone'),
add: function (e, data) {
filesList.push(data.files[0]);
data.context = $('<div class="file"/>', { class: 'thumbnail pull-left' }).appendTo('#files');
var node = $('<p />').append($('<span/>').text(data.files[0].name).data(data));
node.appendTo(data.context);
},
progress: function (e, data) { //Still working on this part
//var progress = parseInt((data.loaded / data.total) * 100, 10);
//data.context.css("background-position-x", 100 - progress + "%");
},
}).on('fileuploadprocessalways', function (e, data) {
var index = data.index,
file = data.files[index],
node = $(data.context.children()[index]);
if (file.preview) {
node.prepend('<br>').prepend(file.preview);
}
if (file.error) {
node.append('<br>').append($('<span class="text-danger"/>').text(file.error));
}
}).prop('disabled', !$.support.fileInput)
.parent().addClass($.support.fileInput ? undefined : 'disabled');
$("#uploadform").submit(function (event) {
if (filesList.length > 0) {
console.log("multi file submit");
event.preventDefault();
$('#fileupload').fileupload('send', { files: filesList })
.success(function (result, textStatus, jqXHR) { console.log('success'); })
.error(function (jqXHR, textStatus, errorThrown) { console.log('error'); })
.complete(function (result, textStatus, jqXHR) {
console.log('complete: ' + JSON.stringify(result)); //The error 500 is returned here. In fiddler, it shows and error 500. If I try to trap in Visual Studio, I can't seem to pinpoint the exception.
// window.location='back to view-page after submit?'
});
} else {
console.log("plain default form submit");
}
});
});
Any thoughts on why this would be happening? I have tried every approach I can think about with no luck. Thank you in advance!
I want to point out that the architecture of your code might cause timeouts or errors.
I would first upload everything to azure storage, storage the status in cache or database.
then I would fire a background job (hangfire, azure functions, webjobs) to process uploading to media service to do the other stuff.
I would suggest doing this asynchronously from the user input.
as per the documentation of dropzone make sure you add name in the HTML tag
<form action="/file-upload" class="dropzone">
<div class="fallback">
<input name="file" type="file" multiple />
</div>
</form>
if you are doing it programatically:
function param() {
return "files";
}
Dropzone.options.myDropzone = {
uploadMultiple: true,
paramName: param,
}
on the backend you need to add \r\n after each stream:

Passing potential huge files in chunks to Web API

I have to pass potential huge files from an ASP.NET Core middle Server to an ASP.NET backend.
I can’t use the ASP.NET backend web API directly, I have to go over a MVC Controller.
Currently my middle Server gets the file in Chunks (and does some verification), saves it to disk and after it completes it rereads it in chunks to pass it forward.
Is there an easy way to pass the chunks without buffering the file?
I currently got this:
MVC Controler:
[HttpPost]
public ActionResult UploadChunk(IFormFile fileChunk, string chunkMetadata)
{
if (!string.IsNullOrEmpty(chunkMetadata))
{
var metaDataObject = JsonConvert.DeserializeObject<ChunkMetadata>(chunkMetadata);
...
AppendContentToFile(tempFilePath, fileChunk); //writes file with FileMode.Append,
}
}
my upload to back end [Edit]:
public IHttpActionResult FileUpload(string fileUri)
{
try
{
if (Request.Content.IsMimeMultipartContent())
{
var configProvider = Resolve<IApplicationConfigurationProvider>();
var uploadRootDir = configProvider.TemporaryFileUploadPath;
var streamProvider = new MultipartStreamProvider(uploadRootDir);
// If the file is huge and is not split into chunks, the 'ReadAsMultipartAsync' call
// takes as long as full file has been copied
var readResult = Request.Content.ReadAsMultipartAsync(streamProvider).Result;
var fileSvc = Resolve<IFileService>();
string targetFilePath = string.Empty;
foreach (MultipartFileData fileData in streamProvider.FileData)
{
ContentDispositionHeaderValue contentDisposition = fileData.Headers.ContentDisposition;
string fileName = contentDisposition.FileName;
if (!GetFileName(fileName, out var targetFileName))
{
return BadRequest($"ContentDisposition.FileName must match 'file' of URI-query! Actual: {targetFileName}");
}
var rawSourceFileInfo = new FileInfo(targetFileName);
if (contentDisposition.Size.HasValue && contentDisposition.Size.Value > 0)
{
if (!fileSvc.CreateNewFilePath(fileUri, new PathOptions(true), out var validFileFullPath))
{
return BadRequest($"Unable to create physical-path from fileId='{fileUri}'");
}
targetFilePath = validFileFullPath.FullName;
fileSvc.AddChunk(validFileFullPath.FullName, contentDisposition.Size.Value, fileData.LocalFileName);
}
else
{
return BadRequest("File upload must set a valid file-length in ContentDisposition");
}
}
return Ok(targetFilePath);
}
else
{
return BadRequest("File upload must be a 'IsMimeMultipartContent'");
}
}
catch (Exception error)
{
LogError(error, "FileUpload");
return InternalServerError(error);
}
}
Thanks in advance for any help!
[Edit]
my not working call from client to back end:
<script>
$(document).ready(function (e) {
$("#uploadImage").on('change', (function (e) {
// append file input to form data
var fileInput = document.getElementById('uploadImage');
var file = fileInput.files[0];
var formData = new FormData();
formData.append('uploadImage', file);
$.ajax({
url: "http://localhost/service/filestore/v1/upload?fileUri=someText",
type: "POST",
data: formData,
contentType: false,
cache: false,
processData: false,
success: function (data) {
if (data == 'invalid') {
// invalid file format.
$("#err").html("Invalid File !").fadeIn();
}
else {
// view uploaded file.
$("#preview").html(data).fadeIn();
$("#form")[0].reset();
}
},
error: function (e) {
$("#err").html(e).fadeIn();
}
});
}));
});
</script>

Having trouble uploading image from Angular 8 to ASP.Netcore Web API

I have spent the whole day trying to get this to work, I have looked at the following:
https://www.talkingdotnet.com/upload-file-angular-5-asp-net-core-2-1-web-api/
https://code-maze.com/upload-files-dot-net-core-angular/
and many more than I can count.
All I want is to send an a form and an image. The errors am getting are:
Missing content-type boundary
Incorrect Content-Type
The function evaluation requires all threads to run
In angular
register(user: UserViewModel, logo: File) {
// We use formData because we can't send file as an object
const formData = new FormData();
formData.append("user", JSON.stringify(user));
formData.append("logo", logo);
console.log(formData);
return this.http.post<UserRegisterViewModel>(`${UserAPI.API_User}/${"register"}`, formData).pipe(map(user => {
return user;
}));
}
Than my c# code looks like so
[HttpPost, DisableRequestSizeLimit]
[AllowAnonymous]
[Route("register")]
//[Consumes("application/json", "multipart/form-data")]
public async Task<IActionResult> RegisterAsync()
{
IFormFile logo = null;
try
{
// Get the logo
logo = Request.Form.Files[0];
// Get the user json string
var userJson = Request.Form["user"];
If you want to fetch the file from Request.Form . you can follow below code sample :
Client side :
const formData = new FormData();
formData.append('file', fileToUpload, fileToUpload.name);
this.http.post('https://localhost:5001/api/upload', formData, {reportProgress: true, observe: 'events'})
.subscribe(event => {
if (event.type === HttpEventType.UploadProgress)
this.progress = Math.round(100 * event.loaded / event.total);
else if (event.type === HttpEventType.Response) {
this.message = 'Upload success.';
this.onUploadFinished.emit(event.body);
}
});
Server side :
[HttpPost, DisableRequestSizeLimit]
public IActionResult Upload()
{
try
{
var file = Request.Form.Files[0];
var folderName = Path.Combine("StaticFiles", "Images");
var pathToSave = Path.Combine(Directory.GetCurrentDirectory(), folderName);
.....
}
catch (Exception ex)
{
....
}
}
Or you can get file with FromForm:
Client side :
let fileToUpload = <File>files[0];
const formData = new FormData();
formData.append('file', fileToUpload, fileToUpload.name);
this.http.post('YourURL', formData, {headers: {
'Accept': 'application/json',
'Content-Disposition' : 'multipart/form-data'
},reportProgress: true, observe: 'events'})
.subscribe(event => {
....
});
Then server side will be :
[HttpPost, DisableRequestSizeLimit]
public IActionResult Upload([FromForm(Name = "file")] IFormFile file)
{
}
This fixed it, I had a break point on
logo = Request.Form.Files[0];
For some reason there is a bug in VS2017 and 2019.
https://developercommunity.visualstudio.com/content/problem/146887/vs2017-v4-requestform-debug.html

How to make progress-bar in angular 2 and .net core

I'm trying to create a progressbar in .netCore and in Angular 8.
The server, .net core, has a loop to make all persons document and I want to show making progress in the angular side.
e.g. loop 65/100 => 65%
In angular side I make a server like this:
public download(fileName: string): Observable<DownloadResponse> {
let downloadURL = `urlToDownloadDocument`;
return this.httpClient.get(downloadURL, {
reportProgress: true,
observe: 'events'
, responseType: 'blob'
}).pipe(map((event) => {
this.downloadResponse.status = event.type;
switch (event.type) {
case HttpEventType.Sent:
this.downloadResponse.filePath = null;
return this.downloadResponse;
case HttpEventType.DownloadProgress:
this.downloadResponse.message = Math.round(100 * event.loaded / event.total).toString() + '%';
this.downloadResponse.filePath = null;
return this.downloadResponse;
case HttpEventType.ResponseHeader:
this.downloadResponse.message = 'Finished';
this.downloadResponse.filePath = event.url;
return this.downloadResponse;
case HttpEventType.Response:
this.downloadResponse.message = 'Transfering';
this.downloadResponse.filePath = event.url;
this.downloadResponse.body = event.body;
this.Transfer(event.body, fileName);
return this.downloadResponse;
default:
this.downloadResponse.message = `Unhandled Case ${event.type}`;
this.downloadResponse.filePath = null;
return this.downloadResponse;
}
})
);
}
private Transfer(blob: Blob, fileName){
let link = document.createElement('a');
link.target = '_blank';
link.href = window.URL.createObjectURL(blob);
link.setAttribute("download", fileName);
link.click();
}
And in .net has a controller which returns an FileStramResult
public IActionResult getAllDocument(...){
...
return File(stream, "application/zip", "Documents.zip");
}
I think that an "observe: 'events'" in angular just get actions in the browser and can't observe actions in the .net server.
I searched about signarlR to get communications between both side but I couldn't find any article saying about download progressbar in Angular 2 and in .netCore beside of this link Angular 2 / .NET Core Progress bar
that seek to find a way to calculate a progress in a query and don't say about how to show a progress in a view.
I found my solution.
After reading this tutorial about signalR and Angular 2
https://rukshan.dev/2019/05/how-to-notify-your-angular-7-app-using-signalr
I changed a little bit
IHub:
Task DownloadResponseMessage(string message, int percentage);
Task GetConnectionId();
(someClass : Hub<...>):
public string GetConnectionId()
{
var result = Context.ConnectionId;
return result;
}
to Send the progress:
_hubContext.Clients.Client(connectionId).DownloadResponseMessage(message, percentage);
And in Angular:
ngOnInit() {
this.connection = new signalR.HubConnectionBuilder()
.configureLogging(signalR.LogLevel.Information)
.withUrl("http://localhost:5627/download")
.build();
this.connection.start().then(function () {
console.log('Connected!');
}).catch(function (err) {
console.error(err);
});
console.log(this.connection);
this.connection.on("DownloadResponseMessage", (message: string, percentage: number) => {
this.message = message;
this.percentage = percentage;
});
}
getConnectionId(){
return this.connection.invoke("GetConnectionId")
.then((connectionId:any) => {
console.log(connectionId);
this.connectionId = connectionId;
},
e => {
console.log(e);
});
}
click(){
this.getConnectionId().then(() => {
this.httpClient.get("http://localhost:5627/api/message?connectionId="+this.connectionId).subscribe();
})
}

IE11 saves file from Web API without extension

I have Angular SPA which downloads a file from ASP.NET Web API 2 using the following approach:
Angular Code
$scope.downloadFile = function(httpPath) {
// Use an arraybuffer
$http.get(httpPath, { responseType: 'arraybuffer' })
.success( function(data, status, headers) {
var octetStreamMime = 'application/octet-stream';
var success = false;
// Get the headers
headers = headers();
// Get the filename from the x-filename header or default to "download.bin"
var filename = headers['x-filename'] || 'download.bin';
// Determine the content type from the header or default to "application/octet-stream"
var contentType = headers['content-type'] || octetStreamMime;
try
{
// Try using msSaveBlob if supported
console.log("Trying saveBlob method ...");
var blob = new Blob([data], { type: contentType });
if(navigator.msSaveBlob)
navigator.msSaveBlob(blob, filename);
else {
// Try using other saveBlob implementations, if available
var saveBlob = navigator.webkitSaveBlob || navigator.mozSaveBlob || navigator.saveBlob;
if(saveBlob === undefined) throw "Not supported";
saveBlob(blob, filename);
}
console.log("saveBlob succeeded");
success = true;
}
catch(ex)
{
console.log("saveBlob method failed with the following exception:");
console.log(ex);
}
if(!success)
{
// Get the blob url creator
var urlCreator = window.URL || window.webkitURL || window.mozURL || window.msURL;
if(urlCreator)
{
// Try to use a download link
var link = document.createElement('a');
if('download' in link)
{
// Try to simulate a click
try
{
// Prepare a blob URL
console.log("Trying download link method with simulated click ...");
var blob = new Blob([data], { type: contentType });
var url = urlCreator.createObjectURL(blob);
link.setAttribute('href', url);
// Set the download attribute (Supported in Chrome 14+ / Firefox 20+)
link.setAttribute("download", filename);
// Simulate clicking the download link
var event = document.createEvent('MouseEvents');
event.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
link.dispatchEvent(event);
console.log("Download link method with simulated click succeeded");
success = true;
}
catch(ex) {
console.log("Download link method with simulated click failed with the following exception:");
console.log(ex);
}
}
if(!success)
{
// Fallback to window.location method
try
{
// Prepare a blob URL
// Use application/octet-stream when using window.location to force download
console.log("Trying download link method with window.location ...");
var blob = new Blob([data], { type: octetStreamMime });
var url = urlCreator.createObjectURL(blob);
window.location = url;
console.log("Download link method with window.location succeeded");
success = true;
} catch(ex) {
console.log("Download link method with window.location failed with the following exception:");
console.log(ex);
}
}
}
}
if(!success)
{
//Fallback to window.open method
console.log("No methods worked for saving the arraybuffer, using last resort window.open");
window.open(httpPath, '_blank', '');
}
})
.error(function(data, status) {
console.log("Request failed with status: " + status);
// Optionally write the error out to scope
$scope.errorDetails = "Request failed with status: " + status;
});
};
httpPath is a url of my Web API Controller. The controller has the following code:
Web API Code
[HttpGet]
[Authorize]
public async Task<HttpResponseMessage> DownloadItems(string path)
{
try
{
Stream stream = await documentsRepo.getStream(path);
HttpResponseMessage result = Request.CreateResponse(HttpStatusCode.OK);
result.Content = new StreamContent(stream);
result.Content.Headers.ContentDisposition =
new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment")
{
FileName = "document.zip",
};
result.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
result.Content.Headers.ContentLength = stream.Length;
return result;
}
catch (Exception e)
{
return Request.CreateResponse(HttpStatusCode.InternalServerError, e);
}
}
The above setup works perfectly well in all browsers (a .zip file is downloaded), except IE11.
For unknown reason, IE11 downloads the file without .zip extension
Any idea what could be wrong with my API Controller?

Categories