JSON views from namespaced controller are not resolved when running from WAR file
This probably related with the closed issue #186
Environment Linux amd64, OpenJDK 1.8.0_192, Grails 3.3.9, Grails View 1.2.9
Steps to reproduce
- Clone the sample project at https://github.com/delfianto/grails-json-view-test
- Run the grails app with
run-app - Test the endpoint
/message?text=helloand/v1/message?text=hello(they work) - Build war file using
grails assemble - Run the war file using
java -jar grails-json-view-test-0.1.war - The endpoint with namespace will throw an exception while the endpoint without namespace works
Description
When the grails app is started by run-app command, JSON view for namespaced controllers works fine. If the application is started from a war archive somehow the view does not resolve properly, resulting in the exception below:
2018-12-29 14:25:45.670 DEBUG --- [0.0-9090-exec-2] g.views.ResolvableGroovyTemplateEngine : No template found for path [v1/message/index.gson] and locale [en_US]
2018-12-29 14:25:45.670 DEBUG --- [0.0-9090-exec-2] g.views.ResolvableGroovyTemplateEngine : No template found for path [v1/v1/message/index.gson] and locale [en_US]
2018-12-29 14:25:45.670 DEBUG --- [0.0-9090-exec-2] g.views.ResolvableGroovyTemplateEngine : No template found for path [index.gson] and locale [en_US]
2018-12-29 14:25:45.671 ERROR --- [0.0-9090-exec-2] .a.c.c.C.[.[.[.[grailsDispatcherServlet] : Servlet.service() for servlet [grailsDispatcherServlet] in context with path [] threw exception [Could not resolve view with name 'index' in servlet with name 'grailsDispatcherServlet'] with root cause
javax.servlet.ServletException: Could not resolve view with name 'index' in servlet with name 'grailsDispatcherServlet'
at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1266)
at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1041)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:984)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:635)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.boot.web.filter.ApplicationContextHeaderFilter.doFilterInternal(ApplicationContextHeaderFilter.java:55)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.boot.actuate.trace.WebRequestTraceFilter.doFilterInternal(WebRequestTraceFilter.java:111)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.grails.web.servlet.mvc.GrailsWebRequestFilter.doFilterInternal(GrailsWebRequestFilter.java:77)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.grails.web.filters.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:67)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.CorsFilter.doFilterInternal(CorsFilter.java:96)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.boot.actuate.autoconfigure.MetricsFilter.doFilterInternal(MetricsFilter.java:103)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:493)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:800)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:806)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1498)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
I cloned the project and run ./gradlew assemble && java -jar build/libs/grails-json-view-test-0.1.war. Then I get the following responses:
$ curl http://localhost:8080/message?text=hello
{"message":"hello"}
$
$
$ curl http://localhost:8080/v1/message?text=hello
{"message":"hello"}
$
btw... I don't think your responseFormats property has any impact on the app's behavior.
Found a new interesting result from my testing, if I run the war file using java -jar build/libs/grails-json-view-test-0.1.war from the project directory it worked just as your post above.
If I move the war file somewhere else, let's say my home directory, or ~/Downloads then the namespaced controller will throw exception. Actually I found this issue initially when deploying war file into a Google App Engine instance and noticing that all of my namespaced controller keeps throwing exception.
Thanks for the feedback.
Apparently this bug affects all deployment in production environment, noticed this when trying to deploy grails app to our staging server (not running war but using traditional deployment with Jetty).
Here's some log on a non-namespaced controller:
2019-01-22 13:39:14.801 DEBUG --- [44-32] grails.views.ResolvableGroovyTemplateEngine : Found template class [emvazo_core_app_index_gson] for path [/app/index.gson]
2019-01-22 13:39:14.814 TRACE --- [44-32] grails.views.mvc.GenericGroovyTemplateView : Rendering view with name 'null' with model {pluginManager=grails.plugins.DefaultGrailsPluginManager@58a68fca, grailsApplication=grails.core.DefaultGrailsApplication@658255aa} and static attributes {}
2019-01-22 13:39:14.815 DEBUG --- [44-32] grails.views.ResolvableGroovyTemplateEngine : Found cached template for path [/app/index.gson] and locale [en_US]
And here's some log from a namespaced controller, notice v1 becomes 1
2019-01-22 13:44:37.925 TRACE --- [53-24] ricGroovyTemplateResolver : Attempting to load class [emvazo_core_1_country_show_en_hal_gson] for template [v1/country/show_en_hal.gson]
2019-01-22 13:44:37.926 TRACE --- [53-24] ricGroovyTemplateResolver : Attempting to load class [1_country_show_en_hal_gson] for template [v1/country/show_en_hal.gson]
2019-01-22 13:44:37.926 TRACE --- [53-24] ricGroovyTemplateResolver : Attempting to load class [i18n_1_country_show_en_hal_gson] for template [v1/country/show_en_hal.gson]
...
After debugging and setting some breakpoints in the classes, I found that this issue may have originated from resolveTemplateName(String scope, String path) method in GenericGroovyTemplateResolver.groovy class.
static String resolveTemplateName(String scope, String path) {
path = path.substring(1) // remove leading slash '/'
path = path.replace(File.separatorChar, UNDERSCORE_CHAR)
path = path.replace(SLASH_CHAR, UNDERSCORE_CHAR)
path = path.replace(DOT_CHAR, UNDERSCORE_CHAR)
if(scope) {
scope = scope.replaceAll(/[\W\s]/, String.valueOf(UNDERSCORE_CHAR))
path = "${scope}_${path}"
}
return path
}
The first line will always remove a single character from the path even if they does not contains any leading forward slash character. This can make the classloader to never found the classname because somehow the path parameter that comes from a namespaced controller does not contains a leading forward slash, thus v1 becomes 1 or system becomes ystem.
Perhaps a better solution for removing the leading slash character is to use regex like:
path.replaceAll('^/+', '')
pushed a fix to check if path starts with '/' to 2.0.x branch.
@davydotcom Could you please verify if this is fixed in one of the recent releases, if yes? please attach the milestone and close this issue.