Movatterモバイル変換


[0]ホーム

URL:


Skip to main content

On mobile? Send a link to your computer to download HTTP Toolkit there:

No spam, no newsletters - just a quick & easy download link

On mobile? Send a link to your computer to download HTTP Toolkit there:

No spam, no newsletters - just a quick & easy download link

Download for Windows
GO BACK TO BLOG

cors

caching

performance

Cache your CORS, for performance & profit

February 2021Author: Tim Perryopens in a new tab
GO BACK TO BLOG

CORS is a necessity for many APIs, but basic configurations can create a huge number of extra requests, slowing down every browser API client, and sending unnecessary traffic to your backend.

This can be a problem with a traditional API, but becomes a much larger issue with serverless platforms, where your billing is often directly tied to the number of requests received, so this can easily double your API costs.

All of this is unnecessary: it's happening because you don't know how caching works for CORS requests. Let's fix that.

What are CORS preflight requests?

Before your browser makes any request that crosses origins (e.g. example.com to api.example.com) if it's not asimple requestopens in a new tab then the browser sends a preflight request first, and waits for a successful response before it sends the real request.

This preflight request is an OPTIONS request to the server, describing the request the browser wants to send, and asking permission first. It looks something like:

OPTIONS /v1/documentsHost: https://api.example.comOrigin: https://example.comAccess-Control-Request-Method: PUTAccess-Control-Request-Headers: origin, x-requested-with

The server has to respond with headers that confirm it's happy to accept the request, and the browser will wait to send the real request until this happens.

If you want to check exactly how these CORS rules work, and how you should respond, play around withWill it CORS? to test out the possibilities.

In practice, almost all cross-origin API requests will require these preflight requests, notably including:

  • Any request with a JSON or XML body
  • Any request including credentials
  • Any request that isn't GET, POST or HEAD
  • Any exchange that streams the request or response body
  • Use of any headers other thanAccept,Accept-Language,Content-Language andContent-Type

Why is this bad?

Each of these requests blocks your real request for at least the round-trip time to your server. OPTIONS requests aren't cacheable by default, so your CDN won't usually handle them, and this will have to hit your server every time.

They are cached in clients, but only for 5 seconds by default. If a web page polls your API, making a request every 10 seconds, it'll repeat the preflight check every 10 seconds too.

In many cases this effectively doubles the latency of your API for all browser clients. From the end user's point of view, your performance is halved! And as I'm sure you've heard a hundred times, a few hundred milliseconds of delay translates to big differences in conversion rates & user satisfaction. This is pretty bad.

In addition, it can add meaningful extra load & cost to your API servers.

This applies especially with serverless billing models. Platforms including AWS Lambda, Netlify Functions, Cloudflare Workers and Google Cloud Functions all bill based on the number of function invocations, and these preflight requests count towards that like any other. Serverless can be free when you're small, but becomes more expensive once large production systems are in play, and potentially doubling your costs is a huge hit!

Even without serverless, this can still catch you out badly. If you expect a large percentage of your APIs requests to be handled by your CDN, it can be a major surprise when adding a custom header to browser requests creates an extra request right through to your backend servers for every single client request.

How can you cache preflight responses?

There two steps of caching you should put in place for these:

  • Caching in the browser, so individual clients don't repeat the same preflight request unnecessarily.
  • Caching in your CDN layer, where possible, to treat these as constant responses so your backend servers/functions don't have to handle them.

CORS caching for browsers

To cache CORS responses in browsers, just add this header to your preflight responses:

Access-Control-Max-Age: 86400

This is a cache time in seconds.

Browser limit this: Firefox caps the value at 86400 (24 hours) while all Chromium-based browsers cap it at 7200 (2 hours). Making this request once every 2 hours instead of before every API request can be a big improvement in user experience though, and setting the value higher to ensure that even longer lifetimes apply where possible is an easy win.

CORS caching for CDNs

To cache CORS responses in CDNs and other proxies between the browser and your API server, add:

Cache-Control: public, max-age=86400Vary: origin

This caches the response in public caches (e.g. CDNs) for 24 hours, which should be enough for most cases without risking cache invalidation becoming a problem. For initial testing, you might want to set the cache time shorter, and increase it once you're happy that everything is set up correctly.

It's important to note that thisisn't standard (OPTIONS is defined as not cacheable by default) but it does appear to be widely supported by most CDNs, who will happily cache OPTIONS responses that explicitly opt-in like this. Some may require this to be manually enabled, so do test this in your configuration.

In the worst case, if this is not supported by your CDN it will just be ignored, so there's no real downside.

TheVary header here is important: this tells the cache to use this response only for other requests with the sameOrigin header (requests from the same cross-origin source), in addition to using the same URL.

If you don't set aVary header, you can have big problems. Preflight responses often include anAccess-Control-Allow-Origin header that matches the incomingOrigin value. If you cache the response without settingVary then the response with one origin might be used for a request with a different origin, which will fail the CORS checks and block the request completely.

If you're using other CORS response headers that depend on the request, you should include them here too, e.g:

Access-Control-Allow-Headers: my-custom-headerAccess-Control-Allow-Methods: GET, POST, PUT, DELETEVary: Access-Control-Request-Headers, Access-Control-Request-Method

If you want to test any of this this out right now, installHTTP Toolkit, add a rule that matches your requests, launch an intercepted browser, and you can try manually injecting these headers into API responses to see exactly how browsers handle them.

Configuration examples

How do you configure this in your case? There's some some helpful ready-to-go examples below. In each case, I'm assuming you already have preflight CORS handling set up, so we're just thinking about how to add caching on top of this.

Caching CORS with AWS Lambda

To enable CORS with AWS Lambda, you can either manually return the headers above in your HTTP response, or you canconfigure API Gatewayopens in a new tab to handle CORS for you.

If you use API Gateway's configuration, this allows you to configure theAccess-Control-Max-Age header, but will not setCache-Control by default, so if you're using CloudFront or another CDN, you should manually configure that andVary too.

Alternatively, you can control this all yourself in a preflight lambda handler, like so:

Code example

Code exampleexports.handler = async (event) => {    const response = {        statusCode: 200,        headers: {            // Keep your existing CORS headers:            "Access-Control-Allow-Origin": event.headers['origin'],            // ...            // And add these:            "Access-Control-Max-Age": 86400,            "Cache-Control": "public, max-age=86400",            "Vary": "origin"        }    };    return response;};

CloudFront specifically includesseparate configurationopens in a new tab that enables caching for OPTIONS responses, so you should ensure this is enabled if you're usingCache-Control here.

If you're using theServerless frameworkopens in a new tab, you can do this automatically in yourserverless.yml instead, for example:

Code example

Code examplefunctions:  hello:    handler: handler.hello    events:      - http:          path: hello          method: get          cors:            origin: '*'            maxAge: 86400            cacheControl: 'public, max-age=86400'

Caching CORS in Node.js

If you're using Express, Connect, or a framework based on them, then you're probably using thecorsopens in a new tab module to handle CORS.

By default this doesn't enable any kind of caching at all, but you can configureAccess-Control-Max-Age by passing amaxAge value.

You can't easily configureCache-Control, so if you're using a CDN you probably want to do something slightly more complicated:

Code example

Code exampleapp.use(cors({    // Set the browser cache time for preflight responses    maxAge: 86400,    preflightContinue: true // Allow us to manually add to preflights}));// Add cache-control to preflight responses in a separate middleware:app.use((req, res, next) => {    if (req.method === 'OPTIONS') {        res.setHeader('Cache-Control', 'public, max-age=86400');        // No Vary required: cors sets it already set automatically        res.end();    } else {        next();    }});

Caching CORS in Python

Django'sdjango-cors-headersopens in a new tab module includes a reasonable default of 86400 as itsAccess-Control-Max-Age value.

Meanwhile Flask'sFlask-Corsopens in a new tab module enables no caching at all by default, but it can be enabled by passingmax_age=86400 as an option in your existing configuration

With that, you can ensure that browser properly cache these responses. If you want CDN caching too then you'll need to manually configureCache-Control. Unfortunately, as far as I can tell, neither module supports custom configuration or an easy workaround for this, so if CDN caching is important to you then you'll probably need to manually handle preflight requests, or wrap these modules yourself.

Caching CORS with Java Spring

With Spring, you're probably already using the@CrossOrigin annotation to handle CORS requests.

By default Spring will set a 30 minutesAccess-Control-Max-Age header with this, adding relatively short caching in each individual browser, but won't set aCache-Control header.

I'd suggest you increase the max age to 24 hours (86400 seconds, the maximum used by any browser) by setting themaxAge option, and also add theCache-Control header if you're using a CDN. Spring's built-in CORS configuration doesn't have support for doing the latter automatically, but you can easily add the header yourself using a response filter:

Code example

Code example@Componentpublic class AddPreflightCacheControlWebFilter implements WebFilter {    @Override    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {        if (CorsUtils.isPreFlightRequest(exchange.getRequest())) {            exchange.getResponse()                .getHeaders()                .add("Cache-Control", "public, max-age=86400");        }        return chain.filter(exchange);    }}

I hope this helps improve your CORS performance and reduce your API traffic! Have thoughts or questions? Feel free to in touch onTwitteropens in a new tab ordirectly.

Debugging APIs and want to inspect, rewrite & mock live traffic? Try outHTTP Toolkitopens in a new tab right now. Open-source one-click HTTP(S) interception & debugging for web, Android, servers & more.

Suggest changes to this pageon GitHubopens in a new tab

Share this post:

Blog newsletter

Become an HTTP & debugging expert, by subscribing to receive new posts like these emailed straight to your inbox:

Relatedcontent

security

caching

http


[8]ページ先頭

©2009-2025 Movatter.jp