Writing binary content directly to the client bypassing the Grails view layer - grails

The following action is meant to write the binary content of bytes directly to the client completely bypassing the Grails view layer:
def actionName = {
byte[] bytes = ...
ServletOutputStream out = response.getOutputStream()
out.write(bytes)
out.flush()
out.close()
return false
}
I was under the impression that return false would make Grails completely skip the view layer. However, that appears not to be the case since the above code still makes Grails search for /WEB-INF/grails-app/views/controllerName/actionName.jsp (which fails with a 404, since no such file exists).
Question:
Given the code above, how do I completely bypass the view layer in Grails?

You should return null or nothing at all, which is interpreted as null. Here's some working code from an action that sends a dynamically generated PDF:
def pdf = {
byte[] content = ...
String filename = ...
response.contentType = 'application/octet-stream'
response.setHeader 'Content-disposition', "attachment; filename=\"$filename\""
response.outputStream << content
response.outputStream.flush()
}

It appears as Grails tries to render the view if response.contentType.startsWith('text/html'). This seems to be a known bug, see GRAILS-1223.
Here are two work arounds:
Use render(contentType: "text/html", text: htmlString) as suggested in GRAILS-1223. This will bypass the view layer.
Clear the content type with response.contentType = ''. This will also bypass the view layer. However, note that the content will be served to the end-user without Content-Type which can confuse some browsers.

Related

MVC - FileContentResult sends corrupted pdf

I have a simple action method that returns a PDF document, that gets shown in an <iframe> with an <embed> tag, and every few calls to this method will return a corrupted PDF. (I've determined its corrupted by using dev tools to save the response from the server)
Action Method:
public FileContentResult GetPdfReport(string Id)
{
Response.AppendHeader("Content-Disposition", "inline; filename=report.pdf");
var content = System.IO.File.ReadAllBytes(Server.MapPath("~/Reports/Testfile.pdf"));
System.IO.File.WriteAllBytes(Server.MapPath("~/Reports/debugReport.pdf"), content);
return File(content, "application/pdf");
}
View Content:
<embed id="widgetResponsePdf" src="#Url.Action("GetPdfReport", "WidgetResponse", new { Id = "123" })" type="application/pdf" onmouseout="mouseOutHandler();" />
The files TestFile.pdf and debugReport.pdf open just fine when I get a corrupted PDF, and there is no difference in the request and response header between the normal request/response and the corrupted request/response.
Is there some setting in IIS that I am missing that could be causing the inconsistent behavior between requests, or could this be caused solely by a network issue?
In our case, the IFrame has a src attribute that points to a partial view that loads the <embed> content, which then has a src attribute that points to the actual PDF file instead of a PartialView that returns a FileContentResult. The example below has been simplified from our actual implementation
Partial View
<iframe> <!-- iframe is actually loaded from another partial view, it is show here to simplify things -->
<embed
src='#Url.Content(Model.ReportFileName)'
type="application/pdf">
</embed>
</iframe>
Controller
public PartialView GetPdfReport(string Id)
{
var model = PDFResponseModel
{
ReportFileName = Server.MapPath("~/Reports/Testfile.pdf")
};
return PartialView(model);
}
Due to a site restriction for IE support, our users (intranet site) are required to have AdobeAcrobat installed to view PDF's. While this solution works for us, this may not be desirable for internet facing websites.

How to show error view after CSV export after directly writing to HttpContext.Current.Response?

In an ASP.NET MVC web application I write directly to HttpContext.Current.Response to export to a CSV file.
This is done in an action in my controller. So I do something like this:
try
{
HttpContext.Current.Response.Clear();
HttpContext.Current.Response.ClearContent();
HttpContext.Current.Response.ClearHeaders();
HttpContext.Current.Response.Cookies.Clear();
HttpContext.Current.Response.Charset = System.Text.UTF8Encoding.UTF8.WebName;
HttpContext.Current.Response.ContentEncoding = System.Text.UTF8Encoding.UTF8;
HttpContext.Current.Response.AppendHeader("Pragma", "no-cache");
HttpContext.Current.Response.AppendHeader("Content-Disposition", fileName);
HttpContext.Current.Response.ContentType = "text/csv";
HttpContext.Current.Response.Write("a,b,c\n");
}
catch(Exception)
{
HttpContext.Current.Response.Flush();
HttpContext.Current.Response.SuppressContent = true;
return RedirectToAction("Error");
//return PartialView("ErrorExportingData"); //Prefered!
}
This works without any problem, except that the page is not redirected (or the partial view is not displayed). I guess the problem is that a complete response was already created (and completed).
I flush the response and suppress the content before the redirect, so the exception stack trace does not end up in my CSV file. After this I somehow need to build a new response.
My question is: In this situation, How can I redirect to an error page, after an exception was thrown?
(If somebody wonders why I want to write directly to HttpContext.Current.Response? This is because it is the fastest way to write many records to a CSV file, using a SqlDataReader.)

XSSFWorkbook#write() results in empty response

I'm trying to get my JSF to export a spreadsheet for download. I'm using Apache's POI library for the Excel document writing. I get the following error in an alert box when the code runs:
emptyResponse: An empty response was received from the server.
The method generating the spreadsheet and exporting to the OutputStream is below (I have renamed classes, methods etc for simplicity sake).
private void generateSpreadsheet(Object object) throws Exception {
FacesContext context = FacesContext.getCurrentInstance();
HttpServletResponse response = (HttpServletResponse)context.getExternalContext().getResponse();
String fileName = object.getProperty() + ".xlsx";
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-Disposition", "attachment;filename=\"" + fileName +"\"");
OutputStream os = response.getOutputStream();
Workbook wb = new XSSFWorkbook();
Sheet sheet = wb.createSheet("Sheet 1");
Row row = sheet.createRow(0);
Cell cell = row.createCell(0);
cell.setCellValue("test");
wb.write(os);
os.flush();
os.close();
FacesContext.getCurrentInstance().responseComplete();
}
Any advice much appreciated, thanks.
If it makes a difference, I'm using AJAX (<f:ajax> tag) on the form submit that calls this method.
It makes definitely difference. You can not download files by Ajax. Ajax is executed by JavaScript code. But JavaScript has no facilities to force a Save As dialogue or to execute the platform default application associated with the file's mime type (thankfully; that would have been a major security/intrusion problem). Further, the JSF Ajax API expects a XML response in a specified structure conform the JSF specs. When you send a complete Excel file instead, the whole Ajax response would be ignored as ununderstandable garbage by the JSF Ajax API.
You need to send a normal synchronous request. Remove the <f:ajax> tag from the command link/button. The current page will remain the same anyway if the download is sent as an attachment.

CSS file only loads in IE when fetched via an ASP.NET MVC Controler + Action

I'm serving up a site's CSS content via an ASP.NET MVC's Controller + Action. The css "file" appears to deliver correctly down the wire but only IE will apply it. Other browsers (Firefox, Opera, Chrome) ignore the CSS file and render the page without styling. IE8 works perfectly.
This is the essential code that I'm using to return the CSS via the controller and action:
public void CSS(string version)
{
string cssFile = Server.MapPath("/site.css");
string cssContents = System.IO.File.ReadAllText(cssFile);
Response.Write(cssContents);
}
Note that the version can be any string. I have tried with strings such as "myversion.css", "1.css", "1234", "arbitrary" etc.
The all work in IE8 but not in any other browser. Any ideas?
You'll have to send the proper Content-type: text/css header.
Firefox will ignore the style sheet otherwise, and output a warning in its error console. Same goes probably for the other Gecko/Webkit based browsers.
This was one of those cases where after typing out the question and pondering how to phrase it and then posting it the answer dawned on me. When I returned here Pekka had correctly answered the question. If anybody's interested here's some code that fixes the problem:
string fileLocation = Request.PhysicalApplicationPath + #"site.css";
ContentResult fcr = new ContentResult();
fcr.Content = System.IO.File.ReadAllText(fileLocation);
fcr.ContentType = "text/css";
return fcr;

Render view to string followed by redirect results in exception

So here's the issue: I'm building e-mails to be sent by my application by rendering full view pages to strings and sending them. This works without any problem so long as I'm not redirecting to another URL on the site afterwards. Whenever I try, I get "System.Web.HttpException: Cannot redirect after HTTP headers have been sent."
I believe the problem comes from the fact I'm reusing the context from the controller action where the call for creating the e-mail comes from. More specifically, the HttpResponse from the context. Unfortunately, I can't create a new HttpResponse that makes use of HttpWriter because the constructor of that class is unreachable, and using any other class derived from TextWriter causes response.Flush() to throw an exception, itself.
Does anyone have a solution for this?
public static string RenderViewToString(
ControllerContext controllerContext,
string viewPath,
string masterPath,
ViewDataDictionary viewData,
TempDataDictionary tempData)
{
Stream filter = null;
ViewPage viewPage = new ViewPage();
//Right, create our view
viewPage.ViewContext = new ViewContext(controllerContext,
new WebFormView(viewPath, masterPath), viewData, tempData);
//Get the response context, flush it and get the response filter.
var response = viewPage.ViewContext.HttpContext.Response;
//var response = new HttpResponseWrapper(new HttpResponse
// (**TextWriter Goes Here**));
response.Flush();
var oldFilter = response.Filter;
try
{
//Put a new filter into the response
filter = new MemoryStream();
response.Filter = filter;
//Now render the view into the memorystream and flush the response
viewPage.ViewContext.View.Render(viewPage.ViewContext,
viewPage.ViewContext.HttpContext.Response.Output);
response.Flush();
//Now read the rendered view.
filter.Position = 0;
var reader = new StreamReader(filter, response.ContentEncoding);
return reader.ReadToEnd();
}
finally
{
//Clean up.
if (filter != null)
filter.Dispose();
//Now replace the response filter
response.Filter = oldFilter;
}
}
You'd have to initiate a new request.
Bit, do you really want to send emails synchronously this way? If the mail server is down, the user could be waiting a good while.
I always put emails in an offline queue and have a service mail them. You might consider using the Spark template engine for this.
One other approach is to not redirect but write out a page with a meta redirect tag
Here is an alternative method for rendering a view to a string that never results in data being output to the response (therefore it should avoid your problem): http://craftycodeblog.com/2010/05/15/asp-net-mvc-render-partial-view-to-string/
To render a regular view instead of a partial view, you'll need to change "ViewEngines.Engines.FindPartialView" to "ViewEngines.Engines.FindView".
Have a look at the MVC Contrib EmailTemplateService which does exactly what you are after.
http://mvccontrib.googlecode.com/svn/trunk/src/MVCContrib/Services/EmailTemplateService.cs
Sorry Chris, not quite sure what I was thinking but I obviously didn't read the question. While I cannot give you a way around this, I can tell you why you are getting the error - HttpResponse.Flush() sends the headers before flushing the content to your filter. This sets a flag inside the response so that when you try to redirect you get the exception.
Using reflector to look at the code inside Flush, I can't see any clean way for you to get around this without a lot of reflection and other nastiness.

Resources