I am downloading a file through Grails, and am recording the fact that this file has been downloaded by this user, with code like:
class MyController {
private void recordTrackDownload() {
def d = new Download(session.user, "/path/to/file")
d.save()
}
def download = {
def f = new File("/path/to/file")
recordTrackDownload()
response.contentType = "mime/type"
response.outputStream << f.newInputStream()
response.outputStream.flush()
}
}
And I am seeing several records of the download appear for each click. I suspect that this has something to do with the several requests recorded by Firefox and the "Transfer-Encoding: chunked" header I am seeing in Live HTTP Headers. (This behavior is also apparent in Chromium)
I am also seeing an error logged in the terminal, which is probably be related, and may be a bug in Grails, or just a logged and ignored bug, but it seems to not stop the file from actually downloading:
2010-01-14 18:46:16,623 [http-8080-4] ERROR errors.GrailsExceptionResolver - Executing action [download] of controller [com.tunited.music.DownloadTrackController] caused exception: ClientAbortException: java.net.SocketException: Connection reset
org.codehaus.groovy.grails.web.servlet.mvc.exceptions.ControllerExecutionException: Executing action [download] of controller [com.tunited.music.DownloadTrackController] caused exception: ClientAbortException: j
ava.net.SocketException: Connection reset
at java.lang.Thread.run(Thread.java:636)
Caused by: org.codehaus.groovy.runtime.InvokerInvocationException: ClientAbortException: java.net.SocketException: Connection reset
... 1 more
Caused by: ClientAbortException: java.net.SocketException: Connection reset
at com.tunited.music.DownloadTrackController$_closure2.doCall(DownloadTrackController.groovy:35)
at com.tunited.music.DownloadTrackController$_closure2.doCall(DownloadTrackController.groovy)
... 1 more
Caused by: java.net.SocketException: Connection reset
at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:113)
at java.net.SocketOutputStream.write(SocketOutputStream.java:153)
... 3 more
2010-01-14 18:46:17,091 [http-8080-4] ERROR view.GroovyPageView - Error processing GroovyPageView: getOutputStream() has already been called for this response
java.lang.IllegalStateException: getOutputStream() has already been called for this response
at home_ed_dev_nomad_tunited_grails_app_views_error_gsp.run(home_ed_dev_nomad_tunited_grails_app_views_error_gsp:19)
at java.lang.Thread.run(Thread.java:636)
2010-01-14 18:46:17,092 [http-8080-4] ERROR errors.GrailsExceptionResolver - getOutputStream() has already been called for this response
java.lang.IllegalStateException: getOutputStream() has already been called for this response
at java.lang.Thread.run(Thread.java:636)
2010-01-14 18:46:17,100 [http-8080-4] ERROR view.GroovyPageView - Error processing GroovyPageView: getOutputStream() has already been called for this response
java.lang.IllegalStateException: getOutputStream() has already been called for this response
at home_ed_dev_nomad_tunited_grails_app_views_error_gsp.run(home_ed_dev_nomad_tunited_grails_app_views_error_gsp:19)
at java.lang.Thread.run(Thread.java:636)
2010-01-14 18:46:17,103 [http-8080-4] ERROR [/].[grails] - Servlet.service() for servlet grails threw exception
java.lang.IllegalStateException: getOutputStream() has already been called for this response
at org.apache.catalina.connector.Response.getWriter(Response.java:610)
at org.apache.catalina.connector.ResponseFacade.getWriter(ResponseFacade.java:198)
at org.codehaus.groovy.grails.web.sitemesh.GrailsPageResponseWrapper$5.activateDestination(GrailsPageResponseWrapper.java:141)
at org.codehaus.groovy.grails.web.sitemesh.GrailsRoutablePrintWriter.getDestination(GrailsRoutablePrintWriter.java:41)
at org.codehaus.groovy.grails.web.sitemesh.GrailsRoutablePrintWriter.flush(GrailsRoutablePrintWriter.java:159)
at org.codehaus.groovy.grails.web.util.GrailsPrintWriter.flush(GrailsPrintWriter.java:98)
...
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:293)
at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:849)
at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:583)
at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:454)
at java.lang.Thread.run(Thread.java:636)
2010-01-14 18:46:17,106 [http-8080-4] ERROR [/].[default] - Servlet.service() for servlet default threw exception
java.lang.IllegalStateException: getOutputStream() has already been called for this response
at org.apache.catalina.connector.Response.getWriter(Response.java:610)
at org.apache.catalina.connector.ResponseFacade.getWriter(ResponseFacade.java:198)
at org.codehaus.groovy.grails.web.sitemesh.GrailsPageResponseWrapper$5.activateDestination(GrailsPageResponseWrapper.java:141)
at org.codehaus.groovy.grails.web.sitemesh.GrailsRoutablePrintWriter.getDestination(GrailsRoutablePrintWriter.java:41)
at org.codehaus.groovy.grails.web.sitemesh.GrailsRoutablePrintWriter.flush(GrailsRoutablePrintWriter.java:159)
at org.codehaus.groovy.grails.web.util.GrailsPrintWriter.flush(GrailsPrintWriter.java:98)
...
at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:849)
at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:583)
at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:454)
at java.lang.Thread.run(Thread.java:636)
I would ideally like my controller code to only be executed once, and the chunked file to be downloaded. I could do this by having several actions, the first of which records the download, and then redirects to the second which downloads the file. But putting another redirect in the way and having to secure the second url, using something like the flash scope, seems like a poor solution. It seems like there's just something I don't know about how the HTTP protocol or Grails works, and there's a simple way to fix this.
Any ideas ?