Errors getting cached by APQ

Our architecture looks a bit like this

Untitled Diagram-Page-3

We have APQ enabled in Apollo Server and the Client is also using that. CloudFront is responsible for caching the GQL responses (upon GET). The problem is when there is an internal server error in the graphql layer, the error response is cached as well. How can we avoid that?

Hello! As an initial thought, does this section of the CloudFront docs help? If not, feel free to follow up!

Thanks @StephenBarlow for the response. I should’ve put a bit more detail to my question. The problem is when we have an internal server error in GraphQL, it returns the response such as the following HTTP code of 200. I believe that gets cached by CloudFront. Is there a way to customize the Response Code being sent by GraphQL in case of error?

{
  "errors": [
    {
      "message": "Request failed after 3 retries.",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "newsArticles"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "stacktrace": [
            "Error: Request failed after 3 retries.",
          ]
        }
      }
    }
  ],
  "data": {
    "myQuery": null
  }
}

After doing a bit more digging into the behaviour, it looks like CloudFront is caching the response by default if cache headers are not sent by the server. Do you have any good caching policy starting point?

https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/controlling-the-cache-key.html

Ah right, so! Apollo Server returns a 200 if an error is thrown inside a resolver, because the response still might include partial data from other resolvers that execute successfully. So CloudFront wouldn’t even consider such a response to be an “error” response.

I admit I’m not well versed on CloudFront, but there is a potential solution on the Apollo Server side: you can create an Apollo Server plugin to modify a response’s cache-control headers whenever one of these 200-with-errors operations occurs.

The plugin could hook into the willSendResponse event, like so:

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [{
    requestDidStart() {
      return {
        // Called whenever Apollo Server is about to respond
        willSendResponse(requestContext) {
          if (requestContext.response.errors) {
            requestContext.response.http.headers.set("cache-control", "max-age=0");
          }
        },
      };
    }
  }]
});

I believe this would prevent CloudFront from caching the response unless you have a minimum TTL set. For more general-purpose, field-level cache control in Apollo Server, also check out this article if you haven’t yet.

1 Like

I like this solution. I will give it a go and get back to you.

I believe I can also use didEncounterErrors hook for this one?

    console.log('Request started!');
    return {
      didEncounterErrors(requestContext) {
        console.log('error occurred');
        requestContext.response.http.headers.set('cache-control', 'max-age=5');
      },
    };
  },

You can indeed! :+1:

One caveat, although I don’t think it affects your use case: if Apollo Server encounters a parsing or validation error (e.g., a query is malformed or a field name is misspelled) and therefore throws a proper 400 instead of the aforementioned “200-with-errors”, the response will not incorporate any header modifications you make in your plugin. This is because 400 responses are generated by separate logic.

(This caveat is resolved in the upcoming Apollo Server 3, which uses the same logic to generate responses regardless of error state.)

1 Like