Content Negotiation

I have a bilangual website and want, depending on the Accept-Language header, deliver either the german or the english websites. Apache has the module ‘mod_negotiation’ for this. Is there something similar for Caddy?
If not, what are the alternatives?

Hi @georg.cachinger,

Both the rewrite and redir directives support conditional operators based on request headers (in the format {>Header}.

For example, assuming:

  • Your English site is located under /en
  • Your German site is located under /de
  • You want to serve English as a default
  • You don’t want the URL to change by locale

You might structure a rewrite-based solution like this:

example.com
rewrite {
  if {path} not_starts_with /de
  if {path} not_starts_with /en
  if {>Accept-Language} is de
  to /de{uri}
}
rewrite {
  if {path} not_starts_with /de
  if {path} not_starts_with /en
  to /en{uri}
}

A redir solution would be quite similar, the major difference being exposing the localisation prefix to the user in their URL bars.

https://caddyserver.com/docs/rewrite
https://caddyserver.com/docs/redir
https://caddyserver.com/docs/placeholders

Thanks for the response. I will try to implement it as suggested. I will give you feedback as soon as I have done (approx. end of june).

PS: I wonder why the search couldn’t find anything concerning “content negotiation an Caddy”. May something for the docs.

I should also mention this project:

It’s not been added to the official plugin list, though, so I’m not sure where it’s at right now in terms of stability/compatibility. I remember it coming up here and there a while back on these forums, though.

I’m trying to implement the suggested rewrite solution, but end up with a syntax error.

This is my Caddyfile:

http://printmaps-osm.de:8383 {
    root /home/printmaps/test/printmaps-osm.de
    gzip
    log ./access.log
    errors ./error.log
    rewrite {
      if {path} not_starts_with /de
      if {path} not_starts_with /en
      if {>Accept-Language} is de
      to /de{uri}
    }
    rewrite {
      if {path} not_starts_with /de
      if {path} not_starts_with /en
      to /en{uri}
    }
}

… and the error message:

printmaps@mercator:~/test$ caddy
Activating privacy features... done.
2017/06/26 11:19:20 Invalid operator not_starts_with

Hi @georg.cachinger,

Apologies for that one, I’ve mistakenly advised people a few times on this particular feature.

The operator not_starts_with is available on the master branch of the Caddy repository, but there hasn’t been an actual release with it yet. It should be usable with version 0.10.4. The docs were mistakenly updated early.

Until then, you can use not_match instead, like so:

    rewrite {
      if {path} not_match ^/de.*
      if {path} not_match ^/en.*
      if {>Accept-Language} is de
      to /de{uri}
    }

Once 0.10.4 comes out, it’ll be benificial to swap to not_starts_with at that time due to the much more efficient nature of substring search vs regex matching.

That’s my directory structure:

design.css
favicon.png
robots.txt
en/
de/
files/
images/
markers/

The stuff in “files, images and markers” are shared by the german and the english website. An image is referenced for example as “…/images/europe.png”. But this doesn’t work, the file is not found. I couldn’t figure out what’s going wrong …

http://printmaps-osm.de:8383 {
    root /home/printmaps/test/printmaps-osm.de
    gzip
    log ./access.log
    errors ./error.log
    rewrite {
      if {path} not_match ^/de.*
      if {path} not_match ^/en.*
      if {>Accept-Language} match ^de.*
      to /de{uri}
    }
    rewrite {
      if {path} not_match ^/de.*
      if {path} not_match ^/en.*
      if {>Accept-Language} not_match ^de.*
      to /en{uri}
    }
}

I know there’s some path.Clean()ing going on under the hood of rewrite, and that gets rid of .. elements… Pretty sure that would be after it gets prepended, though, so it should come out correctly regardless.

Might be helpful if we can see an example of the request rewriting in action. That should shine some light on where the config may have gone awry for what we want to achieve.

Change your log directive slightly:

log / ./access.log "{common} {rewrite_path}"

Then start up your server, provoke a few 404s, and let us know what you get from the logs.

1 Like

Here is the log after calling the rewritten english main page (index.html):

- /en/
- /en/images/template.png
- /design.css
- /en/images/europe.png
- /en/images/seperator.gif
- /en/images/hover.png

The main page calls:

...
<link href="../design.css" rel="stylesheet" type="text/css">
<link rel="icon" type="image/png" href="../favicon.png">
...
<img src="../images/template.png" style="border-width: 1px; border-style: solid; border-color: lightgray;">
...
<img src="../images/europe.png" style="border-width: 1px; border-style: solid; border-color: lightgray;">

Err, is that the whole log? It didn’t print out the pre rewrite request with the usual details? I expected each line to have a normal log line followed by the rewritten section.

Yes, that was the complete log file.

http://printmaps-osm.de:8383 {
    root /home/printmaps/test/printmaps-osm.de
    gzip
    log / ./access.log "{common} {rewrite_path}"
    errors ./error.log
    rewrite {
      if {path} not_match ^/de.*
      if {path} not_match ^/en.*
      if {>Accept-Language} match ^de.*
      to /de{uri}
    }
    rewrite {
      if {path} not_match ^/de.*
      if {path} not_match ^/en.*
      if {>Accept-Language} not_match ^de.*
      to /en{uri}
    }
}

That’s very confusing. {common} should expand out to the full default logging string. Is your Caddy up-to-date?

Anyway, try this:

    rewrite {
      if {path} not_match ^/de.*
      if {path} not_match ^/en.*
      if {>Accept-Language} match ^de.*
      to {path} /de{uri}
    }
    rewrite {
      if {path} not_match ^/de.*
      if {path} not_match ^/en.*
      if {>Accept-Language} not_match ^de.*
      to {path} /en{uri}
    }

That behaviour will first try to serve the real file that matches the request. If it’s not an existing file on disk, Caddy will then try with /en or /de prepended, as appropriate.

Then you should be able to remove the .. elements from your src and href attributes and use the absolute path.

Sorry, absolute pathes are no an option for me. Dito it’s no option to copy the shared data (files, images, …) into the language directories (de, en).

To answer your question concerning the version:

printmaps@mercator:~/test$ caddy -version
Caddy 0.10.2

printmaps@mercator:~/test$ lsb_release -a
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 16.04.2 LTS
Release:	16.04
Codename:	xenial

Maybe interesting:

- /en/
- /design.css
- /en/images/template.png

The first reference (link href=“…/design.css” …) is working, but the next (img src=“…/images/template.png” …) isn’t. Confusing …

No problem, using to {path} /de{uri} instead should solve the issue regardless of whether you use relative links - I mention it only for the sake of cleaning up the HTML a little, since the .. will be both unnecessary and useless (Caddy will ignore it, squishing /de and ../favicon.png together to create /favicon.png anyway). If it’s needed for some other reason, no worries.

Not too much of a mystery; see -

   rewrite {
      if {path} not_match ^/de.*

/design.css matches ^/de.* by regex. It’s not caught by either rewrite.

You could instead use ^/de(/.*)?$, which will only match either /de or /de/*. Same for ^/en(/.*)?$. A minor optimisation until the relase of 0.10.4 and the much faster not_starts_with becomes usable.

… good point !

That’s my latest Caddyfile (as suggested):

http://printmaps-osm.de:8383 {
    root /home/printmaps/test/printmaps-osm.de
    gzip
    log / ./access.log "{common} {rewrite_path}"
    errors ./error.log
    rewrite {
      if {path} not_match ^/de.*
      if {path} not_match ^/en.*
      if {>Accept-Language} match ^de.*
      to {path} /de{uri}
    }
    rewrite {
      if {path} not_match ^/de.*
      if {path} not_match ^/en.*
      if {>Accept-Language} not_match ^de.*
      to {path} /en{uri}
    }
}

Hmm, the result is this:

And the access.log:

- /

If it’s helpful, I could send you copy of the website.

:man_facepalming:

The idea was that Caddy would reject the first option if it wasn’t a file and go to the second, but Caddy’s criteria is that it checks for a file unless it ends with a trailing slash, then it checks for a directory instead. Since / technically ends in a trailing slash, and it is a real directory, so Caddy gives it the go ahead, and there’s obviously no index file directly under /

Could try reversing them. Try the /de/ version, if that fails, fall back to the original path. Might just work.

to /de{uri} {path}

Also, 0.10.2 has a known problem with using the {common} placeholder – it was fixed in 0.10.3 I think, or will definitely go out in 0.10.4 this week. :wink:

2 Likes

Now everything is working fine … thanks for the support. Conclusion:

This is the directory structure:

de/
en/
files/
images/
markers/
design.css
favicon.png
robots.txt

And this the Caddyfile:

http://printmaps-osm.de:8080 {
    root /home/printmaps/printwebsite/printmaps-osm.de
    gzip
    log ./access.log
    errors ./error.log
    rewrite {
      if {path} not_match ^/de.*
      if {path} not_match ^/en.*
      if {>Accept-Language} match ^de.*
      to /de{uri} {path}
    }
    rewrite {
      if {path} not_match ^/de.*
      if {path} not_match ^/en.*
      if {>Accept-Language} not_match ^de.*
      to /en{uri} {path}
    }
}

FYI: You can find the german and english websites here (depending on the language settings in your browser): http://printmaps-osm.de:8080/

BTW: My project, a web service for generating large-format, OSM-based maps, is also written in go.

1 Like