I have implemented what I think is the best over-engineered solution, at least for our use case. We are not using Amasty's Geo-IP Redirect. Instead we chose to develop our own custom solution which probably ended up being quite similar. Plugin to frontend controller dispatch method I'm guessing?
Our use case
We run multiple websites, each with a single group/store and all under the same domain. Our websites relate to our service regions so have very dynamic countries set in the Magento config.
Our requirements
- First time visitors should never bypass varnish cache, at least not for a page render.
- Varnish should serve the correct version from cache based on geo-IP.
- Magento config should still determine which website is served - so geoIP vmods and hard-coded regexes are out.
Our Solution
Let's fetch the X-Magento-Vary header from the backend using a light-weight endpoint, then use it to serve the correct page from cache.
This will deliver visitors to the correct region first-go, whist minimally impacting performance.
After a lot of research I decided to solve this using the cUrl VMOD. The HTTP VMOD was not an option for us because we are using Varnish v4.
Here's the basic rap:
Add a light-weight endpoint to a custom Magento module for returning the X-Magento-Vary header. I added a simple controller that returns RESULT_RAW with no content, just headers. This endpoint should execute the logic in your redirect module to set the correct store, most importantly by updating the HTTP context values to ensure the correct vary string is returned in the cookies.
Install the curl VMOD: https://github.com/varnish/libvmod-curl
Merge the following into your .vcl, replacing the curl path with your light-weight endpoint
`
... Import the cUrl VMOD
import curl;
...
sub vcl_recv {
... # After all your return(pass) logic, get vary header from the backend if not yet checked
# For first time visitors, determine X-Magento-Vary hash via backend API call that uses geo-ip
if (req.http.cookie !~ "X-Varnish-GeoIP=") {
# Copy required headers. Host and Proto prevent redirect,
# Forwarded-For sets correct IP for geo-IP lookup behind proxies
curl.header_add("X-Forwarded-For: " + req.http.X-Forwarded-For);
curl.header_add("X-Forwarded-Proto: https");
curl.header_add("Host: " + req.http.host);
curl.get("http://127.0.0.1:8080/geoip/redirect/varnishService");
if (curl.header("Set-Cookie") ~ "X-Magento-Vary="){
if (req.http.cookie){
set req.http.cookie = req.http.cookie +
"; " + regsub(curl.header("Set-Cookie"), "^.*?(X-Magento-Vary=[^;]+);*.*$", "\1");
} else {
set req.http.cookie = regsub(curl.header("Set-Cookie"), "^.*?(X-Magento-Vary=[^;]+);*.*$", "\1");
}
# Set store cookies so it's sent back to the backend for a cache MISS. This prevents subsequent geo-IP lookup
if (curl.header("Set-Cookie") ~ "store="){
set req.http.cookie = req.http.cookie +
"; " + regsub(curl.header("Set-Cookie"), "^.*?(store=[^;]+);*.*$", "\1");
}
}
}
...
}
...
sub vcl_deliver {
... An attempt has been made to get the Vary header, this cookie stops subsequent checks.
if (curl.status() > 0){
# It would be better to not rely on user-sent cookie but AFAIK there's not a way to store a persistent variable in varnish.
if (resp.http.Set-Cookie){
set resp.http.Set-Cookie = resp.http.Set-Cookie + "; X-Varnish-GeoIP=true; path=/; domain=" + req.http.Host + ";";
} else {
set resp.http.Set-Cookie = "X-Varnish-GeoIP=true; path=/; domain=" + req.http.Host + ";";
}
}
...
}
`
The X-Varnish-GeoIP cookie ensures that the backend is only called once in most cases. This was a better solution than relying on store or session cookies.
Troubleshooting
I encountered various issues whilst implementing my solution. I was testing across both inbuilt and varnish configured instances of Magento since I wanted to be able to turn off varnish if we ran into any issues. Here some gotchas I ran into.
We were initially using a controller_action_predispatch event observer for our GeoIP redirect.
This was causing a cache miss for in-built cache on the first time visits to stores other than the default.
The reason that this happened is because page cache uses an around plugin which always executes before the event dispatch.
We moved our redirect logic to a beforeMethod plugin with a sortOrder of -1 to ensure it always executes before the page cache plugins.
When you do the switch to a new store view, make sure you also update the \Magento\Framwork\App\Http\Context object with new values.
Not doing so can cause the returned page to be cached using the wrong X-Magento-Vary string.
Below is the code we use to set a new store view in our beforeDispatch plugin (closely mimics \Magento\Store\Model\StoreSwitcher).
Note that the default store doesn't have store cookie or vary header set, which is the Magento default.
`
$this->storeManager->setCurrentStore($store->getCode());
$defaultStoreView = $this->storeManager->getDefaultStoreView();
if ($defaultStoreView !== null) {
if ($defaultStoreView->getId() === $store->getId()) {
$this->storeCookieManager->deleteStoreCookie($store);
$this->httpContext->unsValue(Store::ENTITY);
$this->httpContext->unsValue(Context::CONTEXT_CURRENCY);
} else {
$this->httpContext->setValue(Store::ENTITY, $store->getCode(), $defaultStoreView->getCode());
$this->httpContext->setValue(Context::CONTEXT_CURRENCY, $store->getDefaultCurrencyCode(), $defaultStoreView->getDefaultCurrencyCode());
$this->storeCookieManager->setStoreCookie($store);
}
}
`
Another Solution?
This issue would more or less vanish if we were using separate domains for each store view.
Magento generated vcl adds the domain to the hash, so with separate domains the backend lookup in this solution is essentially replaced by whatever domain is visited. However in that case, users which entered via the wrong domain wouldn't be redirected to the right website. That is one other advantage of our solution, albeit the overhead of a backend API call.