HTML5 <audio> Safari live broadcast vs not - audio

I'm attempting to embed an HTML5 audio element pointing to MP3 or OGG data served by a PHP file . When I view the page in Safari, the controls appear, but the UI says "Live Broadcast." When I click play, the audio starts as expected. Once it ends, however, I can't start it playing again by clicking play. Even using the JS API on the audio element and setting currentTime to 0 fails with an index error exception.
I suspected the headers from the PHP script were the problem, particularly missing a content length. But that's not the case. The response headers include a proper Content- Length to indicate the audio has finite size. Furthermore, everything works as expected in Firefox 3.5+. I can click play on the audio element multiple times to hear the sound replay.
If I remove the PHP script from the equation and serve up a static copy of the MP3 file, everything works fine in Safari.
Does this mean Safari is treating audio src URLs with query parameters differently than URLs that don't have them? Anyone have any luck getting this to work?
My simple example page is:
<!DOCTYPE html>
<html>
<head></head>
<body>
<audio controls autobuffer>
<source src="say.php?text=this%20is%20a%20test&format=.ogg" />
<source src="say.php?text=this%20is%20a%20test&format=.mp3" />
</audio>
</body>
</html>
HTTP Headers from PHP script:
HTTP/1.x 200 OK
Date: Sun, 03 Jan 2010 15:39:34 GMT
Server: Apache
X-Powered-By: PHP/5.2.10
Content-Length: 8993
Keep-Alive: timeout=2, max=98
Connection: Keep-Alive
Content-Type: audio/mpeg
HTTP Headers from direct file access:
HTTP/1.x 200 OK
Date: Sun, 03 Jan 2010 20:06:59 GMT
Server: Apache
Last-Modified: Sun, 03 Jan 2010 03:20:02 GMT
Etag: "a404b-c3f-47c3a14937c80"
Accept-Ranges: bytes
Content-Length: 8993
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: audio/mpeg
I tried hard-coding the Accept-Ranges header into the script too, but no luck.

Can you post the headers sent by the server both with and without the PHP script? I'm wondering if the PHP script is sending a different Content-Type than serving the files normally.
It would also be a good idea to specify the type attribute on the source elements, so the browser does not have to download both clips to determine if it can play them.
I cannot reproduce your problem. I have tried to recreate the problem in Safari 4.0.4, and the current WebKit nightly, with the following test page. I am simply using mod_rewrite to dispatch to different formats based on a parameter instead of PHP, but I don't think that should make a difference, unless somehow PHP is modifying the file.
<!DOCTYPE html>
<title>Auido test</title>
<audio controls autobuffer>
<source src="gnossienne-no-1?foo=bar&format=.mp4">
<source src="gnossienne-no-1?foo=bar&format=.ogg">
</audio>
Can you try my example out and let me know if it works for you?
edit Ah. After poking around at it a bit more, it appears that the problem is due to an odd way that the <audio> element in Safari behaves in attempting to determine the size of the content.
Here's an excerpt from a packet capture of Safari upon encountering an <audio> element pointing to a file served directly from Apache. As you can see, it first tries to fetch the first two bytes of the media, presumably so it can get a Content-Length back, and possibly other headers. It then tries to fetch the whole thing. Then, inexplicably, it tries to fetch the first two bytes again, but passes appropriate caching headers to get a "304 Not Modified" response. And finally, still inexplicably, it fetches the last 3440 bytes of the file all over again. It does all of these in separate TCP connections, which adds considerable overhead, in addition to the overhead of fetching the data a couple of times.
GET /stackoverflow/audio-test/say-noid3?foo=bar&format=.mp3 HTTP/1.1
Host: ephemera.continuation.org
Range: bytes=0-1
Connection: close
User-Agent: Apple Mac OS X v10.6.2 CoreMedia v1.0.0.10C540
Accept: */*
Accept-Encoding: identity
Cookie: [redacted]
HTTP/1.1 206 Partial Content
Date: Tue, 05 Jan 2010 02:12:48 GMT
Server: Apache
Last-Modified: Tue, 05 Jan 2010 02:02:08 GMT
ETag: "b2a80ad-11f6-47c6139aaa800"
Accept-Ranges: bytes
Content-Length: 2
Content-Range: bytes 0-1/4598
Connection: close
Content-Type: audio/mpeg
# 2 bytes of data
GET /stackoverflow/audio-test/say-noid3?foo=bar&format=.mp3 HTTP/1.1
Host: ephemera.continuation.org
Range: bytes=0-4597
Connection: close
User-Agent: Apple Mac OS X v10.6.2 CoreMedia v1.0.0.10C540
Accept: */*
Accept-Encoding: identity
Cookie: [redacted]
HTTP/1.1 206 Partial Content
Date: Tue, 05 Jan 2010 02:12:48 GMT
Server: Apache
Last-Modified: Tue, 05 Jan 2010 02:02:08 GMT
ETag: "b2a80ad-11f6-47c6139aaa800"
Accept-Ranges: bytes
Content-Length: 4598
Content-Range: bytes 0-4597/4598
Connection: close
Content-Type: audio/mpeg
# 4598 bytes of data
GET /stackoverflow/audio-test/say-noid3?foo=bar&format=.mp3 HTTP/1.1
Host: ephemera.continuation.org
Range: bytes=0-1
Connection: close
User-Agent: Apple Mac OS X v10.6.2 CoreMedia v1.0.0.10C540
Accept: */*
Accept-Encoding: identity
Cookie: [redacted]
If-None-Match: "b2a80ad-11f6-47c6139aaa800"
If-Modified-Since: Tue, 05 Jan 2010 02:02:08 GMT
HTTP/1.1 304 Not Modified
Date: Tue, 05 Jan 2010 02:12:49 GMT
Server: Apache
Connection: close
ETag: "b2a80ad-11f6-47c6139aaa800"
# no data
GET /stackoverflow/audio-test/say-noid3?foo=bar&format=.mp3 HTTP/1.1
Host: ephemera.continuation.org
Range: bytes=1158-4597
Connection: close
User-Agent: Apple Mac OS X v10.6.2 CoreMedia v1.0.0.10C540
Accept: */*
Accept-Encoding: identity
Cookie: [redacted]
HTTP/1.1 206 Partial Content
Date: Tue, 05 Jan 2010 02:12:49 GMT
Server: Apache
Last-Modified: Tue, 05 Jan 2010 02:02:08 GMT
ETag: "b2a80ad-11f6-47c6139aaa800"
Accept-Ranges: bytes
Content-Length: 3440
Content-Range: bytes 1158-4597/4598
Connection: close
Content-Type: audio/mpeg
# 3440 bytes of data
Anyhow, on to how it deals with the output of your PHP script. Here, Safari again tries to download the first two bytes, but your script ignores the Range request and returns the whole thing. Apparently, WebKit doesn't like that, and so it tries again, without the Range request. Again, your script sends the full contents. Safari now tries once more, adding an Icy-Metadata header, which indicates it thinks that it's downloading a stream and wants streaming metadata to be sent. It finally accepts the output of that, and the <audio> element can play.
GET /say.php?text=this%20is%20a%20test&format=.mp3 HTTP/1.1
Host: tts.mindtrove.info
Range: bytes=0-1
Connection: close
User-Agent: Apple Mac OS X v10.6.2 CoreMedia v1.0.0.10C540
Accept: */*
Accept-Encoding: identity
HTTP/1.1 200 OK
Date: Tue, 05 Jan 2010 02:14:28 GMT
Server: Apache
X-Powered-By: PHP/5.2.10
Content-Length: 4598
Connection: close
Content-Type: audio/mpeg
# 4598 bytes of data
GET /say.php?text=this%20is%20a%20test&format=.mp3 HTTP/1.1
Host: tts.mindtrove.info
Connection: close
User-Agent: Apple Mac OS X v10.6.2 CoreMedia v1.0.0.10C540
Accept: */*
HTTP/1.1 200 OK
Date: Tue, 05 Jan 2010 02:14:28 GMT
Server: Apache
X-Powered-By: PHP/5.2.10
Content-Length: 4598
Connection: close
Content-Type: audio/mpeg
# 4598 bytes of data
GET /say.php?text=this%20is%20a%20test&format=.mp3 HTTP/1.1
Host: tts.mindtrove.info
Accept: */*
User-Agent: Apple Mac OS X v10.6.2 CoreMedia v1.0.0.10C540
Icy-Metadata: 1
Connection: close
HTTP/1.1 200 OK
Date: Tue, 05 Jan 2010 02:14:28 GMT
Server: Apache
X-Powered-By: PHP/5.2.10
Content-Length: 4598
Connection: close
Content-Type: audio/mpeg
# 4598 bytes of data
In summary, it appears that Safari (or more accurately, QuickTime, which Safari uses to handle all media and media downloading) has a completely braindamaged approach to downloading media. Something in the way you send your data back, probably the fact that you don't respond to Range requests, makes it think that you are sending streaming media, causing it to download the content repeatedly (though even when confronted with a server that does respond to a Range request, it still does several more requests than it really needs to).
My advice would be to try to respond appropriately to Range requests; when serving up media, browsers will likely use them to try to minimize bandwidth, by only buffering as much as they need to be able to play through (although have the autobuffer attribute which indicates that you would like them to buffer the whole thing, browsers may ignore that). I would recommend using X-Sendfile to let Apache deal with serving the file, caching, and range requests, but you appear to be on Dreamhost, which doesn't have mod_xsendfile installed, so you're going to have to roll your own Range handling.

Old topic but still valid in 2019. We finally found a solution... Below PHP script sample will consider Safari's "Range" header request.
$bytes_total = strlen($data);
if (isset($_SERVER['HTTP_RANGE'])) {
$byte_range = explode('-',trim(str_ireplace('bytes=','',$_SERVER['HTTP_RANGE'])));
$byte_from = $byte_range[0];
$byte_to = ($byte_range[1]+1);
$data = substr($data,$byte_from,$byte_to);
header('HTTP/1.1 206 Partial Content');
}
else {
$byte_from = 0;
$byte_to = $bytes_total;
}
$length = strlen($data);
header('Content-type: '.$content_type);
header('Accept-Ranges: bytes');
header('Content-length: ' . $length);
header('Content-Range: bytes '.$byte_from.'-'.$byte_to.'/'.$bytes_total);
header('Content-Transfer-Encoding: binary');
print($data);

For the record, while both Pochang and Chris are correct that you need the Content-Range header to fix this problem in Safari, Chrome requires one extra header that must be included for setting currentTime to work properly:
header( 'Accept-Ranges: bytes');
Note that you don't actually have to respond correctly to the request's Range header, you just have to include this in the response.

I got the same problem.
The key point is the Content-Range header.
Everything works fine after I add it to the mp3-output php.

Pochang is correct. Including a Content-Range header in the PHP response will cause Safari to behave properly. It also allows seeking (media.currentTime = 0;) without the dreaded INDEX_SIZE_ERR in Safari, though not in Chrome.
The PHP code for the header is:
$len = strlen( $data );
$shortlen = $len - 1;
header( 'Content-Range: bytes 0-'.$shortlen.'/'.$len);

And in 2020 it is still actual question.
Simply adding Content-Range header does not work.
Bellow is my implementation(based on some answers here).
$content_length = strlen($media_total);
$total_bytes = $content_length;
$content_length_1 = $content_length - 1;
if (isset($_SERVER['HTTP_RANGE'])) {
$byte_range = explode('-',trim(str_ireplace('bytes=','',$_SERVER['HTTP_RANGE'])));
$byte_from = $byte_range[0];
$byte_to = intval($byte_range[1]);
$byte_to = $byte_to == 0 ? $content_length_1 : $byte_to;
$media_total = substr($media_total,$byte_from,$byte_to);
$content_length = strlen($media_total);
header('HTTP/1.1 206 Partial Content');
}
else {
$byte_from = 0;
$byte_to = $content_length_1;
}
$content_range = 'bytes '.$byte_from.'-' . $byte_to . '/' . $total_bytes;
header('Accept-Ranges: bytes');
header("Content-Range: ".$content_range);
header("Content-type: ".$type);
header("Content-length: ".$content_length);
header('Content-Transfer-Encoding: binary');
echo $media_total;
exit;
Original question here: Timing problem for generated audio in some browsers

Related

Why is CloudFront not forwarding my Content-Type header for an svg served from S3?

I'm trying to load a page from CloudFront, and the svg is showing up as a missing image.
When I look into the response headers, I see that when I load the S3 bucket directly, the response contains the proper content type: image/svg+xml
$ curl -I https://s3-eu-west-1.amazonaws.com/pages.ivizone.com/1/19/1509969889/images/kenzo-logo-v2.svg
HTTP/1.1 200 OK
x-amz-id-2: k3+bRpJLp+avBaUWO4VSgB+Djxb+nebnGJs3u6kQ0rMeX95h3XeLHA03XYaWioat+JqNG6x61x8=
x-amz-request-id: 43D8ED0E9EB4490C
Date: Mon, 06 Nov 2017 15:06:13 GMT
Last-Modified: Mon, 06 Nov 2017 14:08:00 GMT
ETag: "4b8f9e399ec9bc166040a2641cf33fb3"
Accept-Ranges: bytes
Content-Type: image/svg+xml
Content-Length: 9484
Server: AmazonS3
However when I pass through CloudFront, the header is missing:
$ curl -I https://pages.ivizone.com/1/19/1509969889/images/kenzo-logo-v2.svg
HTTP/1.1 200 OK
Content-Length: 9484
Connection: keep-alive
Date: Mon, 06 Nov 2017 14:01:01 GMT
Last-Modified: Mon, 06 Nov 2017 12:04:52 GMT
ETag: "4b8f9e399ec9bc166040a2641cf33fb3"
Server: AmazonS3
X-Cache: RefreshHit from cloudfront
Via: 1.1 ed9babcd75a95b818a6df1694ba95225.cloudfront.net (CloudFront)
X-Amz-Cf-Id: va4AIkAzw7-tNZ-qQo4KA_czM29tFQAzmNH_P0wjYd_TiboSBAyohA==
As a result, this is causing problems rendering my images.
Would anyone know why Cloudfront strips the header, and how to fix it?
Thanks!
Ok, It looks like I screwed up somewhere. When uploading the svg image to S3, I had to add the content type string to the S3 Object metadata:
"image/svg+xml"
(no spaces)
Once I added this on upload, the image was served properly.
S3 doesn't send a content-type header by default, so my browser probably interpreted the svg in an incorrect format. By specifying the header, it knew how to handle it

Caching local files

I use Privoxy or Proxomitron to inject custom Javascript tags into websites which then load scripts from a local python server (on localhost:8888):
... <script type="text/javascript"
src="http://localhost:8888/tweakscript.js"
></script></body>
Some of these script tags are huge third party javascript libraries which are also stored on my computer. They never change but are reloaded each time. I want to cache them.
i tried these headers, without success:
HTTP/1.1 200 OK
Content-type: text/javascript; charset=UTF-8
Vary: Accept-Encoding
Date: Sun, 04 Oct 2015 03:24:14 GMT # the current date
Last-Modified: Fri, 02 Oct 2015 06:34:40 GMT # never changes
Expires: Fri, 01 Apr 2016 03:24:14 GMT # one month in future
Cache-Control: public, max-age=15552000 # cache for one year
Access-Control-Allow-Origin: * # Content Security Policy
Access-Control-Allow-Methods: GET, POST, OPTIONS
Connection: close
(javascript code here)
How can i make the webbrowser cache these files?
OK, i found the solution:
When requesting files, the browser adds an If-Modified-Since request header, eg:
If-Modified-Since: Tue, 28 Jul 2015 22:48:42 GMT
If the server then sends as response ...
HTTP/1.1 304 Not Modified
... then the browser will load the file from cache.

Cannot gzip compress static files in Nodejs

Gzipping static files does not work as required (as I think). I used gzippo and express.compress(). Both gzipp the files one time. There is no Content-Encoding:gzip if I refresh the page again.
Her is how I setup my express app:
var gzippo = require('gzippo');
var express = require('express');
var app = express();
app.use(express.compress());
app.use(app.router);
app.use(gzippo.staticGzip(__dirname + '/www'));
This is what Chrome network Response Headers show after page update:
EDITED for full request headers
GET / HTTP/1.1
Host: myhost.com
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/ *;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.146 Safari/537.36
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Cookie: __utma=161759779.498387976.1381482631.1394444924.1394395346.80; __utmb=161759779.3.10.1394395346; __utmc=161759779; __utmz=161759779.1394444924.79.7.utmcsr=gtmetrix.com|utmccn=(referral)|utmcmd=referral|utmcct=/reports/myhost.com/5iXAs1ej/retest
If-None-Match: "25020-1394452200000"
If-Modified-Since: Mon, 10 Mar 2014 11:50:00 GMT
If I refresh again it shows: Edited with full response headers.
HTTP/1.1 304 Not Modified
x-powered-by: Express
accept-ranges: bytes
etag: "25020-1394452200000"
date: Mon, 10 Mar 2014 10:51:45 GMT
cache-control: public, max-age=0
last-modified: Mon, 10 Mar 2014 11:50:00 GMT
If I edit the page again I get Content-Encoding:gzip but only one time.
I don't know if there is something wrong with my express setup.
NOTE: I serve my page as: res.sendfile(__dirname + '/www/index.html');
If I edit the page again I get Content-Encoding:gzip but only one time. I don't know if there is something wrong with my express setup.
All is well. The first time your server sends the file with gzip compression. The second time the normal etag cacheing mechanism comes into play, and since the file has not been modified, the server tells the browser "you already have the right version of this file" and thus there is no need for the server to send a response body at all, just headers, thus no need for any Content-Encoding header.

Data attached to POST request is gone during redirection to GET with CURL

I wrote followed command to send POST with JSON data to server. The server must redirect my request and send GET with the same data:
curl -L -i -XPOST \
-d 'id=105' \
-d 'json={"orderBy":0,"maxResults":50}' http://mysite.com/ctlClient/
I get response:
HTTP/1.1 302 Found
Date: Thu, 04 Jul 2013 13:12:08 GMT
Server: Apache
X-Powered-By: PHP/5.3.19
Set-Cookie: PHPSESSID=1hn0g8d7gtfl4nghjvab63btmk2; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
location: http://mysite.com/fwf/online/
Content-Length: 0
Connection: close
Content-Type: text/html
HTTP/1.1 200 OK
Date: Thu, 04 Jul 2013 13:12:08 GMT
Server: Apache
X-Powered-By: PHP/5.3.19
Set-Cookie: PHPSESSID=16akc7kdcoet71ipjflk9o9cnm5; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 1
Connection: close
Content-Type: text/html
From access-log I see:
"POST /ctlClient/ HTTP/1.1" 302 - "-" "Apache-HttpClient/4.1 (java 1.5)"
"GET /fwf/online/ HTTP/1.1" 200 1 "-" "Apache-HttpClient/4.1 (java 1.5)"
So far so good,
The problem is that GET doesn't receives my data added to post. Sounds like during redirect my data dismissed somehow. From Android client it works, therefore its not Server side problem.
What I need to do to pass POST data to GET request?
Thank you very much,
[EDIT]
#nif offerd to upgrade CURL, i did , to 7.28.0.
Still get the same problem
[INFO]
1st time i go to http://mysite.com/ctlClient/index.php where:
case 105: // id=105
session_unset();
session_start();
foreach($_POST as $key => $value){$_SESSION[$key] = $value;}
ctlGotoSameDomain("/fwf/online/"); // <- aka redirect
return true;
after redirect i go to /fwf/online/index.php and there my request is empty:
public function __construct() {
$this->json = isset($_SESSION['json']) ? $_SESSION['json'] : null;
msqLogFile("fwf/post", Array('post' => 'Request: '.$this->json));
}
http://mysite.com/ctlClient/index.php get 2 parameters properly: id and json
From curl's manpage:
When curl follows a redirect and the request is not a plain GET (for example POST or PUT), it will do the following request with a GET if the HTTP response was 301, 302, or 303. If the response code was any other 3xx code, curl will re-send the following request using the same unmodified method.
Edit
I did some research and found out, that it might be a problem with your curl version. Newer version will honour the -XPOST option and will POST to the redirected location as well. But older versions had an own option for this, i.e. --post301 and --post302. According to their manpage:
--post301
Tells curl to respect RFC 2616/10.3.2 and not convert POST requests into GET
requests when following a 301 redirection. The non-RFC behaviour is ubiquitous
in web browsers, so curl does the conversion by default to maintain
consistency. However, a server may require a POST to remain a POST after such
a redirection. This option is meaningful only when using -L, --location
(Added in 7.17.1)
--post302
Tells curl to respect RFC 2616/10.3.2 and not convert POST requests into GET
requests when following a 302 redirection. The non-RFC behaviour is ubiquitous
in web browsers, so curl does the conversion by default to maintain
consistency. However, a server may require a POST to remain a POST after such
a redirection. This option is meaningful only when using -L, --location
(Added in 7.19.1)
References:
Following redirects with curl
HTTP RFC
I need to add -b to my script to enable the cookies. CURL by default doesn't use them and this issue caused to session ID change. Therefore no data transferred.
curl -b -L -i -X POST \
-d 'id=105' \
-d 'json={"orderBy":0,"maxResults":50}' http://mysite.com/ctlClient/
Now its working

How can I get Varnish to hit on requests of static files on Cloudcontrol?

I'm serving static files (images, javascript, css files) from a (hopefully) cookieless domain also mapped to my cloudcontrol deployment. Here are the request and reponse headers. I see no cookie header in the request, ETag and date check should satisfy, so I would expect that the varnish proxy in front of the cloudcontrol deployment would fetch the request and serve it, but everytime I try it out all static files are served from the Apache processes according to the response header. Any tipps appreciated.
Request URL:http://static.hotelpress.mobi/bundles/viermediamagazine/icons/social/Facebook_64.png
Request Method:GET
Status Code:304 Not Modified
Request Headers
Accept:*/*
Accept-Charset:ISO-8859-1,utf-8;q=0.7,*;q=0.3
Accept-Encoding:gzip,deflate,sdch
Accept-Language:de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4
Cache-Control:max-age=0
Connection:keep-alive
Host:static.hotelpress.mobi
If-Modified-Since:Sat, 20 Apr 2013 18:23:31 GMT
If-None-Match:"6008d436-1108-4daceeec74ec0"
Referer:---stripped out or my boss kills me---
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.65 Safari/537.31
Response Headers
Accept-Ranges:bytes
Age:0
Connection:keep-alive
Date:Sat, 20 Apr 2013 18:31:33 GMT
ETag:"6008d436-1108-4daceeec74ec0"
Last-Modified:Sat, 20 Apr 2013 18:23:31 GMT
Server:Apache
Via:1.1 varnish
X-Varnish:995972028
X-varnish-cache:MISS
Assuming that Varnish is passing through all your Apache headers, it appears that you are not setting any headers telling Varnish to cache.
Varnish does cache silently for 2 minutes by default with no headers, but you probably want more than that.
You should also remove the Etag, for the reasons you say. More information on Etags is here.
If you have fingerprinted assets (per deploy/change), you should set those in Apache for 1 year.
Any others can be as long as you can stand (remembering that this may stop you frequently updating those assets, because they may be cached somewhere).
Here are the lines you need in apache:
<LocationMatch "^/path/to/fingerprinted/assets/.*$">
Header unset ETag
FileETag None
# RFC says only cache for 1 year
ExpiresActive On
ExpiresDefault "access plus 1 year"
Header append Cache-Control "public"
</LocationMatch>
and for others:
<LocationMatch "^/bundles/viermediamagazine/icons/.*$">
Header unset ETag
FileETag None
ExpiresActive On
ExpiresDefault "access plus 1 week"
Header append Cache-Control "public"
</LocationMatch>
You can use as many locations as you want - just make sure they do not overlap!
The example request you posted contains
Cache-Control:max-age=0
which prevents cached answers iirc. You could also try if setting a Cache-Control: max-age=<x> header in your response helps.
Extending the other answers: Here's a sample request to an app on cloudControl, that caches (when the ?c=1). In any case send requests multiple times until you get hits consistently to make sure all Varnish instances have cached the response.
$ curl -v http://impresstw.cloudcontrolled.com/?c=1
* About to connect() to impresstw.cloudcontrolled.com port 80 (#0)
* Trying 46.137.184.215...
* connected
* Connected to impresstw.cloudcontrolled.com (46.137.184.215) port 80 (#0)
> GET /?c=1 HTTP/1.1
> User-Agent: curl/7.27.0
> Host: impresstw.cloudcontrolled.com
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/html; charset=UTF-8
< Server: TornadoServer/2.4.1
< Cache-Control: max-age=36000, must-revalidate
< Expires: Tue, 23 Apr 2013 20:18:12 GMT
< Content-Length: 13
< Accept-Ranges: bytes
< Date: Tue, 23 Apr 2013 10:18:28 GMT
< X-Varnish: 1434600184 1434599691
< Age: 16
< Via: 1.1 varnish
< Connection: keep-alive
< X-varnish-cache: HIT
<

Resources