Feature proposal: emit relative routes to links and assets

92 views
Skip to first unread message

Sean Glover

unread,
Aug 22, 2017, 11:35:28 AM8/22/17
to play-fram...@googlegroups.com
Hi Play! dev community,

I would like to propose a new feature for Play! to allow for relative controller endpoint and asset routing within a webapp.  This feature will make deployment of Play! apps much easier in environments where infrastructure (i.e. webservers, load balancers) cannot be configured.  Instead of writing rewrite rulesets or calculating relative paths by hand, you can just turn on a configuration in Play! and deploy your app.  I found several threads on the play-users mailing list, as well as stack overflow, where this request has been made, but that didn't have a satisfactory conclusion.

In my case, I experienced this issue using the DC/OS admin router, which is basically just an nginx process on a publicly facing node of a DC/OS cluster that dynamically rewrites requests to internally hosted services.  From the admin router's README:

When your container/task has its own IP (typically when running in a virtual network), http://<dcos-cluster>/service/<application ID> is forwarded to the container/task's IP 
...
The endpoint should only use relative links for links and referenced assets such as .js and .css files. This is due to the fact that the linked resources will be reachable only in their relative location <dcos-cluster>/services/<application ID><link>.


The `play.http.context` configuration is brittle because it assumes that webserver rewrite rules are configured properly.  In situations where a load balancer or webserver is simply rewriting traffic to the root of the destination web app server, then `play.http.context` will not help.

Example) 

Consumer facing webserver: http://acme.com
Consumer facing webserver configured to rewrite traffic to Play! app on route: /service/myapp
Internal web server hosting Play! app: http://10.10.10.10
Play! configured with `play.http.context`: /
  1. Request made to consumer facing webserver
    http://acme.com/service/myapp
  2. Webserver rewrites request to Play! app
    http://10.10.10.10/
  3. Play! app serves root route to end user
  4. End user's browser attempts to resolve an asset (controller endpoint link, static asset, etc.)
    /public/bootstrap.css
  5. Webserver requests
    http://acme.com/public/bootstrap.css
  6. Webserver never rewrites request to Play! app
    Webserver returns incorrect asset or 404
With Play! configured with `play.http.context`: /service/myapp
  1. Request made to consumer facing webserver
    http://acme.com/service/myapp
  2. Webserver rewrites request to Play! app
    http://10.10.10.10/
  3. Play! app returns 404 because there are no routes available at a higher level then defined by `play.http.context`
There are many ways to work around this problem.  It could be handled on the client or server side, but I think the most sane solution would be provide a new method or overload in the reverse routing code generated by Play! framework.  When requesting a controller endpoint or asset, a relative route could be calculated given the current route the user is visiting.  In the above example, when visiting the root route of the play app the emitted page would generate a relative path to bootstrap.css instead of an absolute one (`public/bootstrap.css` vs `/public/bootstrap.css`). 

Relative route generation examples:

Requested: /public/bootstrap.css
  • Current route: /
    Emitted route: ./public/bootstrap.css
  • Current route: /foobar
    Emitted route: ./public/bootstrap.css
  • Current route: /foobar/
    Emitted route: ./../public/bootstrap.css
  • Current route: /foobar/baz/
    Emitted route: ./../../public/bootstrap.css
Requested: /foobar
  • Current route: /
    Emitted route: ./foobar
  • Current route: /foobar/baz
    Emitted route: ./../foobar
Requested: /
  • Current route: /
    Emitted route: ./
  • Current route: /foobar
    Emitted route: ./
  • Current route: /foobar/baz
    Emitted route: ./../
Given the current context of the request (the actual URL to the view in the process of being rendered), you can calculate the relative URL of any other asset or link in the page.  My workaround solution was to create an implicit type definition on `Call` to add a `relative` method which accepted an implicit `Request`.  Then I included the implicit Request in any view I needed to make calls to the reverse router.  Here's a gist that summarizes my impl.


Based on feedback to this proposal I would be more than happy to contribute a PR to provide a similar capability within the framework itself.

Regards,
Sean

--
Senior Software Engineer, Lightbend, Inc.



Dominik Dorn

unread,
Aug 22, 2017, 3:20:36 PM8/22/17
to Sean Glover, play-fram...@googlegroups.com
(as I just deleted a whole bunch of text):

Is you're issue with reverse routing of the assets controller - and this not honouring the play.http.context thing ? If yes, this is a bug which should be filed in the issue-tracker. 

If you're issue is about something else, then this is about application design (and I should paste my original answer now...) 
imho, if you're composing your applications through a "kind of" "frontend-proxy", you're backend applications should already have the url-structure, that you're expecting, so that no complicated url-rewriting has to take place. 
links to assets from views should always be absolute (relative to the host), while assets themselves (js, css) can have relative includes/relations. 

so, please clarify if this is a issue in reverse-routing not taking care of play.http.context or another thing.


kind regards,
dominik

Sean Glover

unread,
Aug 22, 2017, 4:59:47 PM8/22/17
to Dominik Dorn, play-fram...@googlegroups.com
Hi Dominik,

This is not an issue with the reverse router or `play.http.context`, that works as expected.

The issue is that when the consumer facing webserver (or "frontend-proxy", as you put it) does simple URL rewriting (i.e. mounts your web app http://acme.com/service/foo => http://ip-with-my-hosted-webapp/), then assets or links will never resolve properly because they're absolute (relative to the host).  I would challenge that this is not an application issue alone, because Play! does not give me a way to make any routes relative out of the box, using the reverse router.  Why do links to assets from views always have to be absolute (relative to the host)?

I'm open to suggestions on how to make links to assets work within this type of webserver configuration I mention in my original post (I also included a link from the DC/OS adminrouter docs with the relative assets requirement).

Regards,
Sean

Sean Glover

unread,
Aug 22, 2017, 5:16:22 PM8/22/17
to Dominik Dorn, play-fram...@googlegroups.com
To elaborate on the example in my last post: If I were to set `play.http.context` to `/service/foo` then due to the way the DC/OS adminrouter rewrites the URL I would have to actually need to request http://acme.com/service/foo/service/foo to get to my webapp.  That would serve my root Play! route correct (albeit the request URL is undesirable), but all assets the root route requests would be absolute to the host (i.e.  http://acme.com/service/foo/public/bootstrap.css), and because from the play app's perspective I haven't requested the path configured in `play.http.context` the right asset wouldn't be returned or 404..

Sean

Dominik Dorn

unread,
Aug 22, 2017, 5:58:10 PM8/22/17
to Sean Glover, play-fram...@googlegroups.com
well, the issue is, that the "frontend-proxy" / mesos admin is rewriting urls going in, but not modifying html going out.. how is the play app ever going to know that any rewriting actually happens? 

there are two possible solutions:
a) the frontend proxy does not only rewrite the urls in, but also the html going out.. this way the play app does not need to know about the rewritten paths
b) the frontend proxy does no rewriting and the play app knows the correct paths.. this requires you to serve you're play app at /servicea/.* and also make sure any paths are correct. 

imho, the fault here is at the mesos implementation. 




James Roper

unread,
Aug 22, 2017, 8:28:04 PM8/22/17
to Dominik Dorn, Sean Glover, play-fram...@googlegroups.com
I think adding a

def relative(implicit req: RequestHeader): Call

method to Call would be a great feature addition, not just for your use case, but there's a whole range of situations where things get served at different URL paths to what the server rendering them thinks the path is - for example, if dynamically rendering stylesheets, you need image links to be relative because they end up getting served from different URLs by a CDN.

Your implementation is not robust though, it won't work for example if serving the site from a URL that is shallower than the URL that the Play app is serving it at, eg, if the browser is requesting:

/foo/my/path

And this is being rewritten to:

/bar/baz/my/path

Then the following URL:

/bar/baz/my/path/asset.css

when you create a relative link using your code will result in a relative link of:

../../../my/path/asset.css

which relative to /foo/my/path is illegal and will result in the wrong URL being requested. What it should be doing is looking for common prefixes of path segments, which if done correctly would yield the following relative URL:

path/asset.css

Another thing to consider, what if the following URL is requested:

/bar/baz/my/./path

Not sure what client would ever issue such a request, but it's probably a good idea to canonicalize both URLs before doing the relativisation.


--
You received this message because you are subscribed to the Google Groups "Play framework dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to play-framework-dev+unsub...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.



--

Sean Glover

unread,
Aug 23, 2017, 11:29:38 AM8/23/17
to James Roper, Dominik Dorn, play-fram...@googlegroups.com
@James.  Thanks for the reply.  Yes, my implementation doesn't cover a few corner cases, but it was enough for my needs so I deferred further effort until I floated the idea on this list.  I considered sharing common prefixes to create a more efficient route, but I hadn't thought of the use case you described, good catch.

@Dominik.  Fair points.  I think there's room for improvement with the DC/OS admin router.  Unfortunately, both suggestions would require me to update rewrite rules in nginx (with varying degrees of complexity & success), which isn't viable in this case.

If there are no objections I'll work on a PR.  I'll follow up this thread with more information.

Sean


To unsubscribe from this group and stop receiving emails from it, send an email to play-framework-dev+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Sean Glover

unread,
Sep 18, 2017, 2:43:37 PM9/18/17
to James Roper, Dominik Dorn, play-fram...@googlegroups.com
Hi,

I finally got around to submitting a PR yesterday.  I have an open question about how to handle documentation.


The travis build for Scala 2.12 generated a pile of test failures that don't appear related to anything I've done: https://travis-ci.org/playframework/playframework/jobs/276665020 .  Can anyone shed some light here?

Regards,
Sean
Reply all
Reply to author
Forward
0 new messages