Caching JSONP ESI using shielding, Symfony2 workaround, issues when hitting shield POP directly


#1

I am trying to cache the content of JSONP requests using an adaptation of the method outlined on https://www.fastly.com/blog/using-esi-part-2-leveraging-vcl-and-esi-to-use-jsonp.

We have had to modify the process as passing a full ESI tag as the callback breaks the validation within the Symfony2 JsonResponse::setCallback method (https://github.com/symfony/symfony/blob/2.3/src/Symfony/Component/HttpFoundation/JsonResponse.php#L73). We have changed the process so that instead of the callback name being the ESI injected piece of the response, the actual API call response is.

The relevant VCL is:

vcl_recv

if (req.url == "/esi/JSONP/ESI-TEMPLATE") {
    error 760 "JSONP ESI";
}

# If URL is an API call and includes callback=, rewrite to an ESI template
if (req.url ~ "^/api/" && req.url ~ "[\?&]callback=([\w\[\]\._]+)") {
    set req.http.X-Callback = regsub(req.url, ".*[\?&]callback=([\w\[\]\._]+).*", "\1");

    set req.http.X-Esi-Url = req.url;
    # Remove cachebusting parameter
    set req.http.X-Esi-Url = regsub(req.http.X-Esi-Url, "&?_=[\d]+", "");
    # Remove callback parameter
    set req.http.X-Esi-Url = regsub(req.http.X-Esi-Url, "&?callback=([\w\[\]\._]+)", "");
    # Remove jsonp parameter
    set req.http.X-Esi-Url = regsub(req.http.X-Esi-Url, "&?jsonp(=([\w\[\]\._]+)?)?", "");
    # Remove a trailing ?
    set req.http.X-Esi-Url = regsub(req.http.X-Esi-Url, "\?$", "");
    # Fix any accidental ?&
    set req.http.X-Esi-Url = regsub(req.http.X-Esi-Url, "\?&", "?");

    set req.url = "/esi/JSONP/ESI-TEMPLATE";

    return (pass);
}

vcl_error

if (obj.status == 760) {
    set obj.http.Content-Type = "application/javascript";
    set obj.status = 200;
    set obj.response = "OK";
    set obj.http.Surrogate-Control = "abc=ESI/1.0";

    # We add an empty comment at the start in order to
    # protect against content sniffing attacks.
    # See https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
    synthetic "/**/" req.http.X-Callback {"(<esi:include src=""} "http://" req.http.host req.http.X-Esi-Url {"" />);"};
    return (deliver);
}

What we are seeing is that if a request hits a shield POP directly (we use IAD for shielding), the /esi/JSONP/ESI-TEMPLATE request hits our backend which issues a 404 response.

After discussing with support, we implemented a second backend, configured identically, with the exception of using a different shield, and use it as follows:

(req.url ~ "^/esi" || req.url ~ "^/api.*[\?&]callback=") && 
((server.datacenter == "IAD" && !req.http.Fastly-FF) ||
(server.datacenter == "JFK" && req.http.Fastly-FF))

So for URLs that start with either /esi or for URLs that start with /api that contain a callback parameter, use the JFK shielded backend if:

  1. the POP is IAD and it’s not gone through a different POP (so directly from client,) or
  2. the POP is JFK and it has gone through a different POP (meaning it came from IAD, because of #1)

#2 is needed to prevent JFK from trying to send the request back to IAD.

From logging request to Papertrail, I can actually see this working for most IAD-hitting requests, but every so often, a request hits an IAD POP first, and isn’t sent to JFK, resulting in a backend request, and ultimately a 404, and I’m not sure why.