32

In play framework 1, you could use in the routes file something like this (check documentation at http://www.playframework.org/documentation/1.2.5/routes#syntax)

GET     /clients/?       Clients.index

so that the route will match /api/clients and also /api/clients/

How can I achieve the same in play framework 2?

opensas
  • 60,462
  • 79
  • 252
  • 386

6 Answers6

71

From SEO point of view the same link with trailing slash is other one than link without it. It is highly recommended to always use one schema (trailed or un-trailed links).

Although there are different schools which one is better the most important is to make a 301 redirect from 'wrong' URL to the correct. You can achieve it quite easy in Play with a 'Dynamic part spanning several /'.

Personally I prefer un-trailed version, maybe because implementing it in the Play is just like writing few simple lines. Add to your routes file this rule, somewhere at the beginning (keep the slash - it's important as it's NOT considered as next slash in the spanning-group, and allows to match trailed URL's easily):

GET  /*path/  controllers.Application.untrail(path: String)

then you can just make a redirect in the controller - to the param, so it will be without the slash at the end:

Java

public static Result untrail(String path) {
   return movedPermanently("/" + path);
}

Scala

def untrail(path: String) = Action { 
  MovedPermanently("/" + path)
}

Until now, all routes ending with the slash will be redirected to the un-trailed version. Easy :)

Of course it's highly recommended to use reverse router for generating correct URL's - to minimalize redundant redirects. Also if you're hardcoding the URL somewhere (ie. in some JS or in external application) it's also better to write correct ones instead converting them every time. If you're planning to publish some public API make a note in documentation, which pattern does your application prefer, so developers will be warned and (maybe) will prepare correct calls.

What's more - it most important for GET routes as they are a subject to manipulation from the client's side. While using POST, PUT, DELETE and others you don't need (or rather, you should't) to care about redirects as they can not be changed by the user and in that way you need to remember which way you choose. In case of wrong call ie. for POST, just return a 404 error - so the developer of the 3-rd part application will be obligated to use correct endings.

biesior
  • 55,576
  • 10
  • 125
  • 182
  • 1
    Thanks. And in Scala, this looks like: def untrail(path: String) = Action { MovedPermanently("/%s".format(path)) } – Chris Martin May 19 '13 at 03:28
  • @ChristopherMartin, thanx, I copied your sample into answer – biesior May 20 '13 at 07:43
  • This solution worked very well in Play 2.1.2 scala. I added routes for GET, POST, PUT and DELETE and so far they seem to redirect properly. – Eneko Alonso Aug 04 '13 at 03:38
  • 1
    @biesior Is it still the only workaround to this or have they added any feature regarding this in Play 2.3.2? – ajay Jul 28 '14 at 09:48
  • Doesn't redirection add unnecessary load on server as well as increase the page load time ? ( Client (with /) -> Server(with /):process path -> Client (redirect to new url witohut /) -> Server (without /) ? – Anshul Nov 04 '14 at 05:33
  • @Anshul it's _cheapest_ possible workaround anyway, on the other hand you should generate all your links in one schema so redirecting should be rather rare, just in case when user writes the address himself – biesior Nov 04 '14 at 07:48
  • 1
    Hello @biesior, thanks for your reply. There is one corner case, which opens 'Open Redirect' issue (https://cwe.mitre.org/data/definitions/601.html). Just FYI: in case the `path` has something like `/////somesite.com/` the browser will visit the other site. – Barys Dec 23 '19 at 12:37
  • Be careful, this answer will strip query parameters – Druska Nov 19 '20 at 21:58
9

I've managed to come up with something, it wasn't as simple as I hoped, but it's no rocket science either

import play.api.mvc.RequestHeader

import play.api.Play.current

class NormalizedRequest(request: RequestHeader) extends RequestHeader {

  val headers = request.headers
  val queryString = request.queryString
  val remoteAddress = request.remoteAddress
  val method = request.method

  val path = request.path.stripSuffix("/")
  val uri = path + {
    if(request.rawQueryString == "") ""
    else "?" + request.rawQueryString
  }
}

object NormalizedRequest {
  def apply(request: RequestHeader) = new NormalizedRequest(request)
}

ans then I use it like this in Global.scala

override def onRouteRequest(request: RequestHeader): Option[Handler] = {
  super.onRouteRequest(NormalizedRequest(request))
}
opensas
  • 60,462
  • 79
  • 252
  • 386
6

Updated the example by @opensas and @lloydmeta for play 2.5

/**
  * HttpRequestHandler that removes trailing slashes from requests.
  */
class TrailingSlashNormaliserHttpRequestHandler(router: Router, errorHandler: HttpErrorHandler, configuration: HttpConfiguration, filters: HttpFilters) extends HttpRequestHandler {

  private val default = new DefaultHttpRequestHandler(router, errorHandler, configuration, filters)

  override def handlerForRequest(request: RequestHeader): (RequestHeader, Handler) = {
    default.handlerForRequest(removeTrailingSlash(request))
  }

  private def removeTrailingSlash(origReq: RequestHeader): RequestHeader = {
    if (origReq.path.endsWith("/") && origReq.path != "/") {
      val path = origReq.path.stripSuffix("/")
      if (origReq.rawQueryString.isEmpty) {
        origReq.copy(path = path, uri = path)
      }else {
        origReq.copy(path = path, uri = path + s"?${origReq.rawQueryString}")
      }
    } else {
      origReq
    }
  }
}

see https://www.playframework.com/documentation/2.5.x/ScalaHttpRequestHandlers for instructions on how to apply the handler

Somatik
  • 4,723
  • 3
  • 37
  • 49
3

An update to the other answers here, for Play 2.8:

import play.api.OptionalDevContext
import play.api.http._
import play.api.mvc.{Handler, RequestHeader}
import play.api.routing.Router
import play.core.WebCommands

import javax.inject.Inject

class TrailingSlashHandler @Inject() (
  webCommands: WebCommands,
  optDevContext: OptionalDevContext,
  router: Router,
  errorHandler: HttpErrorHandler,
  configuration: HttpConfiguration,
  filters: HttpFilters
) extends DefaultHttpRequestHandler(
    webCommands,
    optDevContext,
    router,
    errorHandler,
    configuration,
    filters
  ) {

  override def handlerForRequest(request: RequestHeader): (RequestHeader, Handler) =
    super.handlerForRequest(removeTrailingSlash(request))

  private def removeTrailingSlash(originalRequest: RequestHeader): RequestHeader =
    if (originalRequest.path.endsWith("/") && originalRequest.path != "/") {
      val normalizedPath = originalRequest.path.stripSuffix("/")
      val normalizedUri =
        if (originalRequest.queryString.isEmpty) originalRequest.target.uriString.stripSuffix("/")
        else originalRequest.target.uriString.replaceFirst("/\\?", "?")
      originalRequest.withTarget(
        originalRequest.target
          .withPath(normalizedPath)
          .withUriString(normalizedUri)
      )
    } else {
      originalRequest
    }
}

And then just register via placing this in your conf: play.http.requestHandler = your.package.here.TrailingSlashHandler

yashap
  • 45
  • 5
1

This is based on opensas's answer, just simplified a bit to reuse Play's built-in copy method on RequestHeader so that all the things in the original RequestHeader are kept, like id, tags, version, secure, etc.

import play.api.GlobalSettings
import play.api.mvc.{Handler, RequestHeader}

trait TrailingSlashNormaliser extends GlobalSettings {

  def removeTrailingSlash(origReq: RequestHeader): RequestHeader = {
    if (origReq.path.endsWith("/")) {
      val path = origReq.path.stripSuffix("/")
      if (origReq.rawQueryString.isEmpty)
        origReq.copy(path = path, uri = path)
      else
        origReq.copy(path = path, uri = path + s"?${origReq.rawQueryString}")
    } else {
      origReq
    }
  }

  override def onRouteRequest(request: RequestHeader): Option[Handler] = 
    super.onRouteRequest(removeTrailingSlash(request))

}

/**
 * Global object that removes trailing slashes from requests.
 */
object Global extends TrailingSlashNormaliser
Community
  • 1
  • 1
lloydmeta
  • 1,289
  • 1
  • 15
  • 25
0

Add the entry twice in your route file. One with the slash and one without.

i.am.michiel
  • 10,281
  • 7
  • 50
  • 86