1

In our project we are using graphql. All the query and the mutation are post request. I have configured the varnish with magento. But it seems that post request will be bypassed. Since all my query is a post graphql reuest its not really caching by varnish. When I change the graphql query as get its getting cached in varnish.But I am in a doubt is that a good idea to send a long query as param. How can I cache graphql post query in varnish. Below is my vcl file:

import std;

 backend default {
   .host = "127.0.0.1";
   .port = "5000";
   .first_byte_timeout = 600s;
.probe = {
    .url = "/pub/health_check.php";
    .timeout = 2s;
    .interval = 5s;
    .window = 10;
    .threshold = 5;
   }
}

acl purge {
"127.0.0.1";
}

sub vcl_recv {
if (req.method == "PURGE") {
    if (client.ip !~ purge) {
        return (synth(405, "Method not allowed"));
    }
    # To use the X-Pool header for purging varnish during automated deployments, make sure the X-Pool header
    # has been added to the response in your backend server config. This is used, for example, by the
    # capistrano-magento2 gem for purging old content from varnish during it's deploy routine.
    if (!req.http.X-Magento-Tags-Pattern && !req.http.X-Pool) {
        return (synth(400, "X-Magento-Tags-Pattern or X-Pool header required"));
    }
    if (req.http.X-Magento-Tags-Pattern) {
      ban("obj.http.X-Magento-Tags ~ " + req.http.X-Magento-Tags-Pattern);
    }
    if (req.http.X-Pool) {
      ban("obj.http.X-Pool ~ " + req.http.X-Pool);
    }
    return (synth(200, "Purged"));
}

if (req.method != "GET" &&
    req.method != "HEAD" &&
    req.method != "PUT" &&
    req.method != "POST" &&
    req.method != "TRACE" &&
    req.method != "OPTIONS" &&
    req.method != "DELETE") {
      /* Non-RFC2616 or CONNECT which is weird. */
      return (pipe);
}

# We only deal with GET and HEAD by default
if (req.method != "GET" && req.method != "HEAD") {
    return (pass);
}

# Bypass shopping cart, checkout and search requests
if (req.url ~ "/checkout" || req.url ~ "/catalogsearch") {
    return (pass);
}

# Bypass health check requests
if (req.url ~ "/pub/health_check.php") {
    return (pass);
}

# Set initial grace period usage status
set req.http.grace = "none";

# normalize url in case of leading HTTP scheme and domain
set req.url = regsub(req.url, "^http[s]?://", "");

# collect all cookies
std.collect(req.http.Cookie);

# Compression filter. See https://www.varnish-cache.org/trac/wiki/FAQ/Compression
if (req.http.Accept-Encoding) {
    if (req.url ~ "\.(jpg|jpeg|png|gif|gz|tgz|bz2|tbz|mp3|ogg|swf|flv)$") {
        # No point in compressing these
        unset req.http.Accept-Encoding;
    } elsif (req.http.Accept-Encoding ~ "gzip") {
        set req.http.Accept-Encoding = "gzip";
    } elsif (req.http.Accept-Encoding ~ "deflate" && req.http.user-agent !~ "MSIE") {
        set req.http.Accept-Encoding = "deflate";
    } else {
        # unknown algorithm
        unset req.http.Accept-Encoding;
    }
}

# Remove all marketing get parameters to minimize the cache objects
if (req.url ~ "(\?|&)(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=") {
    set req.url = regsuball(req.url, "(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=[-_A-z0-9+()%.]+&?", "");
    set req.url = regsub(req.url, "[?|&]+$", "");
}

# Static files caching
if (req.url ~ "^/(pub/)?(media|static)/") {
    # Static files should not be cached by default
    return (pass);

    # But if you use a few locales and don't use CDN you can enable caching static files by commenting previous line (#return (pass);) and uncommenting next 3 lines
    #unset req.http.Https;
    #unset req.http.X-Forwarded-Proto;
    #unset req.http.Cookie;
}

# Authenticated GraphQL requests should not be cached by default
if (req.url ~ "/graphql" && req.http.Authorization ~ "^Bearer") {
    return (pass);
}

return (hash);
}

sub vcl_hash {
if (req.http.cookie ~ "X-Magento-Vary=") {
    hash_data(regsub(req.http.cookie, "^.*?X-Magento-Vary=([^;]+);*.*$", "\1"));
}

# For multi site configurations to not cache each other's content
if (req.http.host) {
    hash_data(req.http.host);
} else {
    hash_data(server.ip);
}

# To make sure http users don't see ssl warning
if (req.http.X-Forwarded-Proto) {
    hash_data(req.http.X-Forwarded-Proto);
}


if (req.url ~ "/graphql") {
    call process_graphql_headers;
}
}

sub process_graphql_headers {
if (req.http.Store) {
    hash_data(req.http.Store);
}
if (req.http.Content-Currency) {
    hash_data(req.http.Content-Currency);
}
}

sub vcl_backend_response {

set beresp.grace = 3d;

if (beresp.http.content-type ~ "text") {
    set beresp.do_esi = true;
}

if (bereq.url ~ "\.js$" || beresp.http.content-type ~ "text") {
    set beresp.do_gzip = true;
}

if (beresp.http.X-Magento-Debug) {
    set beresp.http.X-Magento-Cache-Control = beresp.http.Cache-Control;
}

# cache only successfully responses and 404s
if (beresp.status != 200 && beresp.status != 404) {
    set beresp.ttl = 0s;
    set beresp.uncacheable = true;
    return (deliver);
} elsif (beresp.http.Cache-Control ~ "private") {
    set beresp.uncacheable = true;
    set beresp.ttl = 86400s;
    return (deliver);
}

# validate if we need to cache it and prevent from setting cookie
if (beresp.ttl > 0s && (bereq.method == "GET" || bereq.method == "HEAD")) {
    unset beresp.http.set-cookie;
}

   if (beresp.ttl <= 0s ||
   beresp.http.Surrogate-control ~ "no-store" ||
   (!beresp.http.Surrogate-Control &&
   beresp.http.Cache-Control ~ "no-cache|no-store") ||
   beresp.http.Vary == "*") {
    # Mark as Hit-For-Pass for the next 2 minutes
    set beresp.ttl = 120s;
    set beresp.uncacheable = true;
}

return (deliver);
}

sub vcl_deliver {
if (resp.http.X-Magento-Debug) {
    if (resp.http.x-varnish ~ " ") {
        set resp.http.X-Magento-Cache-Debug = "HIT";
        set resp.http.Grace = req.http.grace;
    } else {
        set resp.http.X-Magento-Cache-Debug = "MISS";
    }
} else {
    unset resp.http.Age;
}

# Not letting browser to cache non-static files.
if (resp.http.Cache-Control !~ "private" && req.url !~ "^/(pub/)?(media|static)/") {
    set resp.http.Pragma = "no-cache";
    set resp.http.Expires = "-1";
    set resp.http.Cache-Control = "no-store, no-cache, must-revalidate, max-age=0";
}

unset resp.http.X-Magento-Debug;
unset resp.http.X-Magento-Tags;
unset resp.http.X-Powered-By;
unset resp.http.Server;
unset resp.http.X-Varnish;
unset resp.http.Via;
unset resp.http.Link;
}

sub vcl_hit {
if (obj.ttl >= 0s) {
    # Hit within TTL period
    return (deliver);
}
if (std.healthy(req.backend_hint)) {
    if (obj.ttl + 300s > 0s) {
        # Hit after TTL expiration, but within grace period
        set req.http.grace = "normal (healthy server)";
        return (deliver);
    } else {
        # Hit after TTL and grace expiration
        return (restart);
    }
} else {
    # server is not healthy, retrieve from cache
    set req.http.grace = "unlimited (unhealthy server)";
    return (deliver);
}
}

1 Answer 1

3

It is possible to cache HTTP POST requests in Varnish. It is also possible to limit this to the /graphql endpoint.

Bypassing standard behavior

The first thing you have to do, is circumvent the following rule:

if (req.method != "GET" && req.method != "HEAD") {
    return (pass);
}

You can add an extra conditional in front of the previous snippet that checks the request method and the URL pattern. Here's an example:

if(req.method == "POST" && req.url ~ "^/graphql") {
    set req.http.x-method = req.method;
    return(hash);
}

This will make sure the GET/HEAD check is never reached.

Preserve POST request

Varnish will also convert HTTP POST request into HTTP GET requests. We need to override this as well using the x-method header we set in the previous snippet.

You'll have to add the following to make sure the request remains an HTTP POST request:

sub vcl_backend_fetch {
    if(bereq.http.x-method == "POST") {
        set bereq.http.method = "POST";
    }
}

Differentiating GraphQL posts

The next step is figuring out how to differentiate one GraphQL post request from another. How is this done? Via the URL? Via request headers? via the request body?

If this is done via request headers or the URL, we can also create cache variations using the hash_data() function within sub vcl_hash {}.

If we depend on the request body, this will be a lot more difficult. You can use the std.cache_req_body() function to cache the entire request body. But as soon as one character is different in the request body, it will be stored as another object.

There is a risk of a very low hit rate if the entire request body is used as a cache variation. But that depends on how the request body is composed for GraphQL queries.

More granular differentiation

If you want to inspect the request body and create cache variations on a specific part of the request body, this is also possible, but it will require Varnish Enterprise, the commercial version of Varnish.

Varnish Enterprise offers the Xbody module that allows you to inspect the request body. There is also a JSON module in Varnish Enterprise, which you can use if the request body contains JSON data.

For on-premise setups, this requires a license. For deployments in the Cloud, there are official machine images where the Cloud providers handles licensing and charges on a per-hour basis.

2
  • Thanks alot for this detailed solution. I tried adding the (req.method == "POST"...) condtion just above the (req.method != "GET"....). And restarted the varnish and checked the request. It showing : "message": "Syntax Error: Unexpected <EOF>", "extensions": { "category": "graphql" },. Same request i tried without the req.mthd==post and its working but not caching. I think varnish calling the get request. And I also added the vcl_backend_fetch
    – user43415
    Jan 6, 2021 at 20:07
  • Please share the updated VCL so we can have a look at what's going on. Please also include the output of the varnishlog -g request -q "ReqUrl ~ '^/graphql'", which will log the incoming GraphQL request. Jan 7, 2021 at 8:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.