My current theory is that there is a bug in Facebook fileserver that means the connection is sometimes not being closed even though the data has been sent, so the connection stays open until the timeout. In the absence of the (optional) content-length header being sent by Facebook's fileserver, cURL can't know if the payload is complete, and so hangs.
My current solution is to 'prime' the fileserver by first making a request for the image without a body, like this:
$ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_NOBODY, 1); curl_exec($ch);
This is a pretty quick process, since there is no image being returned. I actually do this in the background using asynchronous multi curl, so I can get on with doing some other processing.
After priming the fileserver, subsequent requests for the files are even quicker than before, as the content-length is known.
This is a bit of a clumsy approach, but in the absence of any response from Facebook so far I'm not sure what else to do.