Skip to content

Unable to purge post after changing status from Draft to Published #223

@predaytor

Description

@predaytor

When the status of a post changes from Draft to Published, the query for specific post (project in the example) does not have an associated key as it did before. That is, we can't invalidate the keys, it will always be HIT until the Cache-Control expires.

Even updating the data in such a post does not purge anything because it no longer has an associated key.
All other events are working fine, as far as the post was not in Draft state (or restored from trash) and then turned to Published again.

Querying a completely new created post works fine.


Testing custom Project Post type (single post is present):

Query example (testing via Insomnia):

(projects) http://localhost/wp/graphql?query={projects{edges{node{id,title}}}}
(project) http://localhost/wp/graphql?query={project(id:"our-slug",idType:SLUG){id,title}}


First run:

projects query -> MISS
c42fab2f4dc81c75f2e4b6180f1b5d0cdfd0fb22f863231bebab2f2a02bd3e10 graphql:Query list:project cG9zdDozNDU=

project query -> MISS
51404600df1e2ceed74fbd14251755e4b7958a369dc28ae551d6e5ff2a64831f graphql:Query cG9zdDozNDU=


Change status to Draft:

[14-Jun-2023 09:21:47 UTC] (graphql_purge) key: cG9zdDozNDU=, event: post_DELETE, user: 1, page: /wp/wp-admin/admin-ajax.php
[14-Jun-2023 09:21:47 UTC] (graphql_purge) key: skipped:post, event: post_DELETE, user: 1, page: /wp/wp-admin/admin-ajax.php

projects query -> MISS
c42fab2f4dc81c75f2e4b6180f1b5d0cdfd0fb22f863231bebab2f2a02bd3e10 graphql:Query list:project

project query -> MISS
51404600df1e2ceed74fbd14251755e4b7958a369dc28ae551d6e5ff2a64831f graphql:Query


Change status back to Published:

[14-Jun-2023 09:22:52 UTC] (graphql_purge) key: list:project, event: post_CREATE, user: 1, page: /wp/wp-admin/admin-ajax.php

projects query -> MISS
c42fab2f4dc81c75f2e4b6180f1b5d0cdfd0fb22f863231bebab2f2a02bd3e10 graphql:Query list:project cG9zdDozNDU=

project query -> HIT
51404600df1e2ceed74fbd14251755e4b7958a369dc28ae551d6e5ff2a64831f graphql:Query


Post is updated:

[14-Jun-2023 09:26:35 UTC] (graphql_purge) key: cG9zdDozNDU=, event: post_UPDATE, user: 1, page: /wp/wp-admin/admin-ajax.php
[14-Jun-2023 09:26:35 UTC] (graphql_purge) key: skipped:post, event: post_UPDATE, user: 1, page: /wp/wp-admin/admin-ajax.php

projects query -> MISS
c42fab2f4dc81c75f2e4b6180f1b5d0cdfd0fb22f863231bebab2f2a02bd3e10 graphql:Query list:project cG9zdDozNDU=

project query -> HIT
51404600df1e2ceed74fbd14251755e4b7958a369dc28ae551d6e5ff2a64831f graphql:Query


New post is published:

[14-Jun-2023 09:32:00 UTC] (graphql_purge) key: list:project, event: post_CREATE, user: 1, page: /wp-json/wp/v2/project/1038

projects query -> MISS
c42fab2f4dc81c75f2e4b6180f1b5d0cdfd0fb22f863231bebab2f2a02bd3e10 graphql:Query list:project cG9zdDoxMDM4 cG9zdDozNDU=

project query (new post)-> MISS
d9154add95f1fabfe6844d1716c23cd1b7ef0c4ca43d1ba867219e16859a88ed graphql:Query cG9zdDoxMDM4


purge-varnish-cache.php:

<?php

function purge_varnish_cache( $purge_keys ) {
    // Determine the URL to be purged based on server host
    $server_host = isset($_SERVER['HTTP_X_FORWARDED_HOST']) ? $_SERVER['HTTP_X_FORWARDED_HOST'] : $_SERVER['HTTP_HOST'];
    // The server must be running Varnish Proxy, we must be able to target the host with a PURGE request (http only)
    $purge_url = 'http://' . $server_host . '/';

    // Create headers for the request with the purge keys
    $headers = array(
        'Xkey: ' . $purge_keys
    );

    // Initialize and configure a cURL request
    $curl = curl_init();

    curl_setopt_array(
        $curl,
        array(
            CURLOPT_URL            => $purge_url,                  // URL to purge
            CURLOPT_CUSTOMREQUEST  => 'PURGE',                     // HTTP method as PURGE
            CURLOPT_RETURNTRANSFER => true,                        // Return response as string
            CURLOPT_HTTPHEADER     => $headers,                    // Set headers
        )
    );

    // Execute the cURL request
    curl_exec( $curl );

    // Check for errors and log error message if any
    if ( curl_errno( $curl ) ) {
        $error_message = curl_error( $curl );
        error_log( print_r( $error_message, true ) );
    }

    // Close the cURL session
    curl_close( $curl );
}

// Add an action hook for purging cache using WP Graphql Smart Cache plugin
add_action( 'graphql_purge', function ( $purge_keys ) {

    purge_varnish_cache( $purge_keys );

}, 10, 1 );

default.vcl Varnish config:

vcl 4.1;

import xkey;
import std;

backend default {
    .host = "app.internal";
    .port = "8080";
}

acl purge {
    "0.0.0.0"/0;  // Allow all IP addresses
    "::0"/0;      // Allow all IPv6 addresses
}

sub vcl_recv {
    # Remove empty query string parameters
    # e.g.: www.example.com/index.html?
    if (req.url ~ "\?$") {
        set req.url = regsub(req.url, "\?$", "");
    }

    # Remove port number from host header
    set req.http.Host = regsub(req.http.Host, ":[0-9]+", "");

    # Sorts query string parameters alphabetically for cache normalization purposes
    set req.url = std.querysort(req.url);

    # Remove the proxy header to mitigate the httpoxy vulnerability
    # See https://httpoxy.org/
    unset req.http.proxy;

    # Add X-Forwarded-Proto header when using https
    if (!req.http.X-Forwarded-Proto) {
        if(std.port(server.ip) == 443 || std.port(server.ip) == 8443) {
            set req.http.X-Forwarded-Proto = "https";
        } else {
            set req.http.X-Forwarded-Proto = "http";
        }
    }

    # Set the X-Forwarded-Host header to the proxy host
    set req.http.X-Forwarded-Host = "proxy-app.internal";

    # Purge logic to remove objects from the cache.
    if (req.method == "PURGE") {
        if (client.ip !~ purge) {
            return (synth(403, "Forbidden"));
        }

        if (req.http.xkey) {
            set req.http.n-gone = xkey.softpurge(req.http.xkey);
            return (synth(200, "Invalidated " + req.http.n-gone + " objects"));
        }
        return(synth(200, "Purged"));
    }

    # Only handle relevant HTTP request methods
    if (
        req.method != "GET" &&
        req.method != "HEAD" &&
        req.method != "PUT" &&
        req.method != "POST" &&
        req.method != "PATCH" &&
        req.method != "TRACE" &&
        req.method != "OPTIONS" &&
        req.method != "DELETE"
    ) {
        return (pipe);
    }

    # Remove tracking query string parameters used by analytics tools
    if (req.url ~ "(\?|&)(utm_source|utm_medium|utm_campaign|utm_content|gclid|cx|ie|cof|siteurl)=") {
        set req.url = regsuball(req.url, "&(utm_source|utm_medium|utm_campaign|utm_content|gclid|cx|ie|cof|siteurl)=([A-z0-9_\-\.%25]+)", "");
        set req.url = regsuball(req.url, "\?(utm_source|utm_medium|utm_campaign|utm_content|gclid|cx|ie|cof|siteurl)=([A-z0-9_\-\.%25]+)", "?");
        set req.url = regsub(req.url, "\?&", "?");
        set req.url = regsub(req.url, "\?$", "");
    }

    # Only cache GET and HEAD requests
    if (req.method != "GET" && req.method != "HEAD") {
        set req.http.X-Cacheable = "NO:REQUEST-METHOD";
        return(pass);
    }

    # Mark static files with the X-Static-File header, and remove any cookies
    # X-Static-File is also used in vcl_backend_response to identify static files
    if (req.url ~ "^[^?]*\.(7z|avi|bmp|bz2|css|csv|doc|docx|eot|flac|flv|gif|gz|ico|jpeg|jpg|js|less|mka|mkv|mov|mp3|mp4|mpeg|mpg|odt|ogg|ogm|opus|otf|pdf|png|ppt|pptx|rar|rtf|svg|svgz|swf|tar|tbz|tgz|ttf|txt|txz|wav|webm|webp|woff|woff2|xls|xlsx|xml|xz|zip)(\?.*)?$") {
        set req.http.X-Static-File = "true";
        unset req.http.Cookie;
        return(hash);
    }

    # No caching of special URLs, logged in users and some plugins
    if (
        req.http.Cookie ~ "wordpress_(?!test_)[a-zA-Z0-9_]+|wp-postpass|comment_author_[a-zA-Z0-9_]+|woocommerce_cart_hash|woocommerce_items_in_cart|wp_woocommerce_session_[a-zA-Z0-9]+|wordpress_logged_in_|comment_author|PHPSESSID" ||
        req.http.Authorization ||
        req.url ~ "add_to_cart" ||
        req.url ~ "edd_action" ||
        req.url ~ "nocache" ||
        req.url ~ "^/addons" ||
        req.url ~ "^/bb-admin" ||
        req.url ~ "^/bb-login.php" ||
        req.url ~ "^/bb-reset-password.php" ||
        req.url ~ "^/cart" ||
        req.url ~ "^/checkout" ||
        req.url ~ "^/control.php" ||
        req.url ~ "^/login" ||
        req.url ~ "^/logout" ||
        req.url ~ "^/lost-password" ||
        req.url ~ "^/my-account" ||
        req.url ~ "^/product" ||
        req.url ~ "^/register" ||
        req.url ~ "^/register.php" ||
        req.url ~ "^/server-status" ||
        req.url ~ "^/signin" ||
        req.url ~ "^/signup" ||
        req.url ~ "^/stats" ||
        req.url ~ "^/wc-api" ||
        req.url ~ "^/wp-admin" ||
        req.url ~ "^/wp-comments-post.php" ||
        req.url ~ "^/wp-cron.php" ||
        req.url ~ "^/wp-login.php" ||
        req.url ~ "^/wp-activate.php" ||
        req.url ~ "^/wp-mail.php" ||
        req.url ~ "^/wp-login.php" ||
        req.url ~ "^\?add-to-cart=" ||
        req.url ~ "^\?wc-api=" ||
        req.url ~ "^/preview=" ||
        req.url ~ "^/\.well-known/acme-challenge/"
    ) {
        set req.http.X-Cacheable = "NO:Logged in/Got Sessions";

        if (req.http.X-Requested-With == "XMLHttpRequest") {
            set req.http.X-Cacheable = "NO:Ajax";
        }

        return(pass);
    }

    # Remove any cookies left
    unset req.http.Cookie;

    return(hash);
}

sub vcl_hash {
    if (req.http.X-Forwarded-Proto) {
        # Create cache variations depending on the request protocol
        hash_data(req.http.X-Forwarded-Proto);
    }
}

sub vcl_backend_response {
    # Inject URL & Host header into the object for asynchronous banning purposes
    set beresp.http.x-url = bereq.url;
    set beresp.http.x-host = bereq.http.host;

    # If we dont get a Cache-Control header from the backend
    # we default to 1h cache for all objects
    if (!beresp.http.Cache-Control) {
        set beresp.ttl = 1h;
        set beresp.http.X-Cacheable = "YES:Forced";
    }

    # If the file is marked as static we cache it for 1 day
    if (bereq.http.X-Static-File == "true") {
        unset beresp.http.Set-Cookie;
        set beresp.http.X-Cacheable = "YES:Forced";
        set beresp.ttl = 1d;
    }

    if (beresp.http.Set-Cookie) {
        set beresp.http.X-Cacheable = "NO:Got Cookies";
    } elseif (beresp.http.Cache-Control ~ "private") {
        set beresp.http.X-Cacheable = "NO:Cache-Control=private";
    }

    # The cached document should be "tagged" using the values returned by the x-graphql-keys header
    if (beresp.http.x-graphql-keys) {
        set beresp.http.xkey = beresp.http.x-graphql-keys;
    }
}

sub vcl_deliver {
    # Debug header
    if (req.http.X-Cacheable) {
        set resp.http.X-Cacheable = req.http.X-Cacheable;
    } elseif (obj.uncacheable) {
        if(!resp.http.X-Cacheable) {
            set resp.http.X-Cacheable = "NO:UNCACHEABLE";
        }
    } elseif (!resp.http.X-Cacheable) {
        set resp.http.X-Cacheable = "YES";
    }

    set resp.http.X-Cache-Hits = obj.hits;

    if (obj.hits > 0) {
        set resp.http.X-Cache = "HIT";
    } else {
        set resp.http.X-Cache = "MISS";
    }

    # Cleanup of headers
    unset resp.http.x-url;
    unset resp.http.x-host;
    unset resp.http.via;
    unset resp.http.x-varnish;
}

Metadata

Metadata

Assignees

Labels

status: actionableReady for work to begintype: bugIssue that causes incorrect or unexpected behavior

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions