If you arrive at this stackoverflow question and your server appears to be returning the correct headers, you may have a silly issue: Chrome Dev Tools 'Disable cache' is likely interfering with your test.
You are likely unintentionally bypassing the very caching that you are trying to test by opening the Chrome network tab.
Given that pretty much the only way you test OPTIONS request behavior is in the Dev Tools console of your browser (how else would you know that you were making the options requests?) there's an important "gotcha" here:
If you have the 'Disable cache' checkbox checked at the very top of the network tab then the OPTIONS cache will be completely ignored.
This makes sense, but is unintuitive since as a dev you normally always have 'Disable cache' checked when in the network tab since you don't want stuff you're debugging cached pretty much ever. But indeed, that checkbox will also bypass the OPTIONS cache, not just assets caches you usually think about, so even if your server is set up correctly the browser will just request options on every single request until you uncheck that box.
Hope this helps someone!
Tangential rant, this is poor design by the chrome devtools team, OPTIONS should get special treatment and its own checkbox or something, as well as the ability to hide them specifically from cluttering up your request list when you're trying to debug actual requests. Very frustrating. Having no way to filter "requests I make" from "requests the browser makes as protocol overhead" in a debug tool is silly. Yes, you can INVERT a method:OPTIONS filter to filter them out, but then you can't use the filter for anything else, which creates a worse clutter problem to deal with when zeroing in on a problem... :)