Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cache-bypass-on-cookie: The set-cookie header is not propagated from server when bypassing cache #12

Open
PetrDlouhy opened this issue Sep 30, 2020 · 4 comments

Comments

@PetrDlouhy
Copy link

The set-cookie header is not propagated from server when bypassing cache in cache-bypass-on-cookie.

@Salubritas
Copy link

I have a CF worker based on this code and just tested for this. I am seeing the same behavior of the set-cookie header not making it through.

Just looking at the code, I can't see what the problem might be. The Response object has no special property for cookies, it's just the headers as a whole that get populated in the constructor.

https://developers.cloudflare.com/workers/runtime-apis/response

Seems strange that all the other headers get populated but not set-cookie.

@PetrDlouhy
Copy link
Author

I was trying to fix this by setting Cache Level: Bypass Page Rule on cf_cache_bust urls as you can see in these commits:
PetrDlouhy@8aca0a6
PetrDlouhy@28a93b0
But I was not able to make it working properly with real requests, although it worked in the debug console.

I ended up with reversing the behaviour - setting force_cache=true query parameter and caching only those urls. So far it seems, that this works. My not-yet-finished code is:

 /**
 * Main worker entry point.
 */
addEventListener("fetch", event => {
  //event.passThroughOnException();
  const request = event.request;
  if (!bypassCache(request)) {
    event.respondWith(handleRequest(request));
  }
});

/**
 * Do all of the work to bypass the cache
 * @param {Request} request - Original request
 */
async function handleRequest(request) {
    // Clone the request so we can add a no-cache, no-store Cache-Control request header.
    let init = {
      method: request.method,
      headers: [...request.headers],
      redirect: "manual",
      body: request.body,
      cf: { cacheTtl: 86400 }
    };

    // Use a unique URL (query params) to make SURE the cache is busted for this request
    let uniqueUrl = await generateUniqueUrl(request);
    let newRequest = new Request(uniqueUrl, init);
    newRequest.headers.set('cache-control', 'max-age=86400');
    // newRequest.headers.set('Cache-Control', request.headers.get('Cache-Control'));
    // newRequest.headers.set('Date', request.headers.get('Date'));

    // For debugging, clone the response and add some debug headers
    let response = await fetch(newRequest);
    let newResponse = new Response(response.body, response);
    newResponse.headers.set('X-Bypass-Cache', 'Forced');

    return newResponse;
}

/**
 * Determine if the given request needs to bypass the cache.
 * @param {Request} request - inbound request.
 * @returns {bool} true if the cache should be bypassed
 */
function bypassCache(request) {
  let needsBypass = false;

  // Bypass the cache for all requests to a URL that matches any of the URL path bypass patterns
  const url = new URL(request.url);
  const path = url.pathname + url.search;

  if (NO_BYPASS_URL_PATTERNS.length) {
    for (let pattern of NO_BYPASS_URL_PATTERNS) {
      if (path.match(pattern)) {
        return true;
      }
    }
  }
  if (BYPASS_URL_PATTERNS.length) {
    for (let pattern of BYPASS_URL_PATTERNS) {
      if (path.match(pattern)) {
        needsBypass = true;
        break;
      }
    }
  }

  // Bypass the cache if the request contains a cookie that starts with one of the pre-configured prefixes
  if (!needsBypass) {
    const cookieHeader = request.headers.get('cookie');
    if (cookieHeader && cookieHeader.length && BYPASS_COOKIE_PREFIXES.length) {
      const cookies = cookieHeader.split(';');
      for (let cookie of cookies) {

        // If cache-cookie is set, drive cache explicitly
        if(cookie.trim() == 'cache-cookie=no-cache'){
          return true;
        };
        if(cookie.trim() == 'cache-cookie=cache'){
          return false;
        };

        // See if the cookie starts with any of the logged-in user prefixes
        for (let prefix of BYPASS_COOKIE_PREFIXES) {
          if (cookie.trim().startsWith(prefix)) {
            needsBypass = true;
            break;
          }
        }
        if (needsBypass) {
          break;
        }
      }
    }
  }

  return needsBypass;
}

/**
 * Generate a unique URL so it will never match in the cache.
 * This is a bit of a hack since there is no way to explicitly bypass the Cloudflare cache (yet)
 * and requires that the origin will ignore unknown query parameters.
 * @param {Request} request - Original request
 */
async function generateUniqueUrl(request) {
  let url = request.url;
  if (url.indexOf('?') >= 0) {
    url += '&';
  } else {
    url += '?';
  }
  url += 'force_cache=true';
  return url;
}

@Salubritas
Copy link

Salubritas commented Sep 30, 2020

The response handling looks the same. Do you see the set-cookie header coming through with this code?

Your code looks similar to mine, in that I didn't go for the unique URL approach to forcing cache bypass. By coincidence I noticed a problem today with redirects not working because of the cache bypass query string being appended, so I made a change to work around that today (the 404 part).

/*
This script is based on
https://github.com/pmeenan/cf-workers/blob/master/cache-bypass-on-cookie/cache-bypass-on-cookie.js
*/

// Cookie prefixes that cause a request to bypass the cache when present.
const BYPASS_COOKIE_PREFIXES = [
  "wordpress_logged_in_"
];

// URL paths to bypass the cache (each pattern is a regex)
const BYPASS_URL_PATTERNS = [
    /^\/reviews\/[^\/]+\/$/,
    /^\/b\/[^\/]+\/$/
];

 /**
 * Main worker entry point.
 */
addEventListener("fetch", event => {
  const request = event.request;
  if (bypassCache(request)) {
    event.respondWith(handleRequest(request));
  }
});

/**
 * Do all of the work to bypass the cache
 * @param {Request} request - Original request
 */
async function handleRequest(request) {
    // Clone the request so we can add a no-cache, no-store Cache-Control request header.
    let init = {
      method: request.method,
      headers: [...request.headers],
      redirect: "manual",
      body: request.body,
      cf: { cacheTtl: 0 }
    };

    // Use a new URL to tell CF not to use the cache
    let newUrl = await generateNewUrl(request);
    let newRequest = new Request(newUrl, init);
    newRequest.headers.set('Cache-Control', 'no-cache, no-store');

    // Clone the response and add a response header
    let response = await fetch(newRequest);
    if(response.status == 404) {
        // Try again without ?bypasscache=1 parameter
        // The parameter means that Yoast redirect rules don't match so Yoast redirects will never work for logged in users
        newRequest = new Request(request.url, init);
        newRequest.headers.set('Cache-Control', 'no-cache, no-store');
        response = await fetch(newRequest);
    }
    let newResponse = new Response(response.body, response);
    newResponse.headers.set('X-Cookie-Bypass', 'Logged In');
    return newResponse;
}

/**
 * Determine if the given request needs to bypass the cache.
 * @param {Request} request - inbound request.
 * @returns {bool} true if the cache should be bypassed
 */
function bypassCache(request) {
  let needsBypass = false;
  
  // Only bypass for requests to URLs that match one of the URL path patterns
  const url = new URL(request.url);
  const path = url.pathname; // + url.search
  let urlBypass = false;
  if (BYPASS_URL_PATTERNS.length) {
    for (let pattern of BYPASS_URL_PATTERNS) {
      if (path.match(pattern)) {
        urlBypass = true;
        break;
      }
    }
  }

  // Only bypass if the request contains a cookie that starts with one of the pre-configured prefixes
  let cookieBypass = false;
  if (urlBypass) {
    const cookieHeader = request.headers.get('cookie');
    if (cookieHeader && cookieHeader.length && BYPASS_COOKIE_PREFIXES.length) {
      const cookies = cookieHeader.split(';');
      for (let cookie of cookies) {
        // See if the cookie starts with any of the logged-in user prefixes
        for (let prefix of BYPASS_COOKIE_PREFIXES) {
          if (cookie.trim().startsWith(prefix)) {
            cookieBypass = true;
            break;
          }
        }
        if (cookieBypass) {
          break;
        }
      }
    }
  }

  needsBypass = urlBypass && cookieBypass;

  return needsBypass;
}

/**
 * Generate new URL
 * @param {Request} request - Original request
 */
async function generateNewUrl(request) {
  let url = request.url;
  if (url.indexOf('?') >= 0) {
    url += '&';
  } else {
    url += '?';
  }
  url += 'bypasscache=1';
  return url;
}

@PetrDlouhy
Copy link
Author

@Salubritas With the reversed behaviour the set-cookie header gets delivered just fine. The requequest will come with cf-cache-status: DYNAMIC unless it is cached.

With the approach I tried at first (those commits) I did see the original set-cookie header only in the debug console. With real requests it ended up with cf-cache-status: MISS and set-cookie header from the CF caching engine. It was strange, but I was not able to get over that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants