Permalink
Browse files

Add support of the 'Vary' header in response

With this patch, Yaws will add 'Accept-Encoding' in 'Vary' header if the
support of gzip compression is enabled or if the response is compressed.
The 'Vary' header can be set using 'yaws:outh_set_vary(Fields)' or by
returning '{header, {vary, Fields}}' from scripts (where Fields is a list
of header names).
  • Loading branch information...
1 parent f5f62b6 commit d748f923815fe8100bcbec02d7d786fd0b39ca00 @capflam capflam committed Nov 30, 2012
Showing with 126 additions and 6 deletions.
  1. +45 −2 doc/yaws.tex
  2. +1 −0 include/yaws.hrl
  3. +4 −0 man/yaws_api.5
  4. +49 −2 src/yaws.erl
  5. +27 −2 test/t5/app_test.erl
View
@@ -1653,8 +1653,51 @@ \section{All out/1 return values}
stream data, in this case, the response is not chunked encoded.
-\item \verb+{header, H}+ Accumulates a HTTP header. Used by for
- example the \verb+yaws_api:setcookie/2-6+ function.
+\item \verb+{header, H}+ Accumulates a HTTP header. The trailing CRNL which is
+ supposed to end all HTTP headers must NOT be added. It is added by the server.
+ The following list of headers are given special treatment.
+
+ \begin{itemize}
+ \item \verb+{connection, What}+ - This sets the Connection: header. If
+ \textit{What} is the special value \textit{\"close\"}, the connection will
+ be closed once the yaws page is delivered to the client.
+ \item \verb+{server, What}+ - Sets the \textit{Server:} header. By setting
+ this header, the server's signature will be dynamically overloaded.
+ \item \verb+{location, What}+ - Sets the \textit{Location:} header. This
+ header is typically combined with the \textit{\{status, 302\}} return
+ value.
+ \item \verb+{cache_control, What}+ - Sets the \textit{Cache-Control:}
+ header.
+ \item \verb+{expires, What}+ - Sets the \textit{Expires:} header.
+ \item \verb+{date, What}+ - Sets the \textit{Date:} header.
+ \item \verb+{allow, What}+ - Sets the \textit{Allow:} header.
+ \item \verb+{last_modified, What}+ - Sets the \textit{Last-Modified:}
+ header.
+ \item \verb+{etag, What}+ - Sets the \textit{Etag:} header.
+ \item \verb+{set_cookie, What}+ - Prepends a \textit{Set-Cookie:} header to
+ the list of previously set \textit{Set-Cookie} headers.
+ \item \verb+{content_range, What}+ - Sets the \textit{Content-Range:}
+ header.
+ \item \verb+{content_type, What}+ - Sets the \textit{Content-Type:} header.
+ \item \verb+{content_encoding, What}+ - Sets the \textit{Content-Encoding:}
+ header. If this header is defined, no deflate is performed by \Yaws\. So
+ you can compress data by yourself.
+ \item \verb+{content_length, What}+ - Normally \Yaws\ will ship Yaws pages
+ using \textit{Transfer-Encoding: chunked}. This is because we generally
+ can't know how long a yaws page will be. If we for some reason want to
+ force a \textit{Content-Length:} header (and we actually do know the
+ length of the content, we can force yaws to not ship the page chunked.
+ \item \verb+{transfer_encoding, What}+ - Sets the
+ \textit{Transfer-Encoding:} header.
+ \item \verb+{www_authenticate, What}+ - Sets the \textit{WWW-Authenticate:}
+ header.
+ \item \verb+{vary, What}+ - Sets the \textit{Vary:} header.
+ \end{itemize}
+
+ All other headers must be added using the normal HTTP syntax. Example:
+\begin{verbatim}
+{header, {"My-X-Header", "gadong"}} or {header, "My-X-Header: gadong"}
+\end{verbatim}
\item \verb+{header, {H, erase}}+ A specific case of the previous
directive; use this to remove a specific header from a response. For
View
@@ -347,6 +347,7 @@
content_encoding,
transfer_encoding,
www_authenticate,
+ vary,
other % misc other headers
}).
View
@@ -866,6 +866,10 @@ Sets the Transfer-Encoding: header.
Sets the WWW-Authenticate: header.
+\fI{vary, What}\fR
+
+Sets the Vary: header.
+
All other headers must be added using the normal HTTP syntax.
Example:
View
@@ -51,6 +51,7 @@
outh_set_dcc/2,
outh_set_transfer_encoding_off/0,
outh_set_auth/1,
+ outh_set_vary/1,
outh_clear_headers/0,
outh_fix_doclose/0,
dcc/2]).
@@ -68,7 +69,8 @@
make_www_authenticate_header/1,
make_etag/1,
make_content_type_header/1,
- make_date_header/0]).
+ make_date_header/0,
+ make_vary_header/1]).
-export([outh_get_status_code/0,
outh_get_contlen/0,
@@ -79,6 +81,7 @@
outh_get_content_encoding/0,
outh_get_content_encoding_header/0,
outh_get_content_type/0,
+ outh_get_vary_fields/0,
outh_serialize/0]).
-export([accumulate_header/1, headers_to_str/1,
@@ -1120,6 +1123,10 @@ outh_set_auth(Headers) ->
end,
put(outh, H2).
+outh_set_vary(Fields) ->
+ put(outh, (get(outh))#outh{vary = make_vary_header(Fields)}),
+ ok.
+
outh_fix_doclose() ->
H = get(outh),
if
@@ -1307,6 +1314,12 @@ make_date_header() ->
H
end.
+make_vary_header(Fields) ->
+ case lists:member("*", Fields) of
+ true -> ["Vary: ", "*", "\r\n"];
+ false -> ["Vary: ", join_sep(Fields, ", "), "\r\n"]
+ end.
+
%% access functions into the outh record
@@ -1346,6 +1359,12 @@ outh_get_content_type() ->
[_, Mime, _] -> Mime
end.
+outh_get_vary_fields() ->
+ case (get(outh))#outh.vary of
+ undefined -> [];
+ [_, Fields, _] -> split_sep(Fields, $,)
+ end.
+
outh_serialize() ->
H = get(outh),
Code = case H#outh.status of
@@ -1382,6 +1401,26 @@ outh_serialize() ->
end,
{LM, E, CC}
end,
+
+ %% Add 'Accept-Encoding' in the 'Vary:' header if the compression is enabled
+ %% or if the response is compressed _AND_ if the response has a non-empty
+ %% body.
+ SC=get(sc),
+ Vary = case (?sc_has_deflate(SC) orelse H#outh.encoding == deflate) of
+ true when H#outh.contlen /= undefined, H#outh.contlen /= 0;
+ H#outh.act_contlen /= undefined, H#outh.act_contlen /= 0 ->
+ Fields = outh_get_vary_fields(),
+ Fun = fun("*") -> true;
+ (F) -> (to_lower(F) == "accept-encoding")
+ end,
+ case lists:any(Fun, Fields) of
+ true -> H#outh.vary;
+ false -> make_vary_header(["Accept-Encoding"|Fields])
+ end;
+ _ ->
+ H#outh.vary
+ end,
+
Headers = [noundef(H#outh.connection),
noundef(H#outh.server),
noundef(H#outh.location),
@@ -1398,6 +1437,7 @@ outh_serialize() ->
noundef(H#outh.set_cookie),
noundef(H#outh.transfer_encoding),
noundef(H#outh.www_authenticate),
+ noundef(Vary),
noundef(H#outh.other)],
{StatusLine, Headers}.
@@ -1530,6 +1570,11 @@ accumulate_header({www_authenticate, What}) ->
accumulate_header({"WWW-Authenticate", What}) ->
accumulate_header({www_authenticate, What});
+accumulate_header({vary, What}) ->
+ put(outh, (get(outh))#outh{vary = ["Vary: ", What, "\r\n"]});
+accumulate_header({"Vary", What}) ->
+ accumulate_header({vary, What});
+
%% non-special headers (which may be special in a future Yaws version)
accumulate_header({Name, What}) when is_list(Name) ->
H = get(outh),
@@ -1589,7 +1634,9 @@ erase_header(transfer_encoding) ->
erase_header(www_authenticate) ->
put(outh, (get(outh))#outh{www_authenticate=undefined});
erase_header(location) ->
- put(outh, (get(outh))#outh{location=undefined}).
+ put(outh, (get(outh))#outh{location=undefined});
+erase_header(vary) ->
+ put(outh, (get(outh))#outh{vary=undefined}).
getuid() ->
case os:type() of
View
@@ -24,17 +24,32 @@ start() ->
deflate_disabled() ->
io:format("deflate_disabled\n", []),
- %% Static content (and cached)
+ %% Static content (and cached) - Not supported by server
Uri1 = "http://localhost:8000/1000.txt",
?line {ok, "200", Hdrs1, _} =
ibrowse:send_req(Uri1, [{"Accept-Encoding", "gzip, deflate"}], get),
?line undefined = proplists:get_value("Content-Encoding", Hdrs1),
+ ?line undefined = proplists:get_value("Vary", Hdrs1),
- %% Dynamic content
+ %% Dynamic content - Not supported by server
Uri2 = "http://localhost:8000/index.yaws",
?line {ok, "200", Hdrs2, _} =
ibrowse:send_req(Uri2, [{"Accept-Encoding", "gzip, deflate"}], get),
?line undefined = proplists:get_value("Content-Encoding", Hdrs2),
+ ?line undefined = proplists:get_value("Vary", Hdrs2),
+
+ %% Static content (and cached) - Not supported by client
+ Uri3 = "http://localhost:8001/1000.txt",
+ ?line {ok, "200", Hdrs3, _} =
+ ibrowse:send_req(Uri3, [], get),
+ ?line undefined = proplists:get_value("Content-Encoding", Hdrs3),
+ ?line "Accept-Encoding" = proplists:get_value("Vary", Hdrs3),
+
+ %% Dynamic content - Not supported by client
+ Uri4 = "http://localhost:8001/index.yaws",
+ ?line {ok, "200", Hdrs4, _} = ibrowse:send_req(Uri4, [], get),
+ ?line undefined = proplists:get_value("Content-Encoding", Hdrs4),
+ ?line "Accept-Encoding" = proplists:get_value("Vary", Hdrs4),
ok.
deflate_enabled() ->
@@ -45,6 +60,7 @@ deflate_enabled() ->
?line {ok, "200", Hdrs1, Body1} =
ibrowse:send_req(Uri1, [{"Accept-Encoding", "gzip, deflate"}], get),
?line "gzip" = proplists:get_value("Content-Encoding", Hdrs1),
+ ?line "Accept-Encoding" = proplists:get_value("Vary", Hdrs1),
?line true = is_binary(zlib:gunzip(Body1)),
%% Partial content is not compressed for small (and catched) files
@@ -54,12 +70,14 @@ deflate_enabled() ->
{"Range", "bytes=100-499"}], get),
?line undefined = proplists:get_value("Content-Encoding", Hdrs2),
?line "400" = proplists:get_value("Content-Length", Hdrs2),
+ ?line "Accept-Encoding" = proplists:get_value("Vary", Hdrs2),
%% Dynamic content
Uri3 = "http://localhost:8001/index.yaws",
?line {ok, "200", Hdrs3, Body3} =
ibrowse:send_req(Uri3, [{"Accept-Encoding", "gzip, deflate"}], get),
?line "gzip" = proplists:get_value("Content-Encoding", Hdrs3),
+ ?line "Accept-Encoding" = proplists:get_value("Vary", Hdrs3),
?line true = is_binary(zlib:gunzip(Body3)),
ok.
@@ -73,25 +91,29 @@ deflate_empty_response() ->
ibrowse:send_req(Uri1, [{"Accept-Encoding", "gzip, deflate"}], get),
?line undefined = proplists:get_value("Content-Encoding", Hdrs1),
?line "0" = proplists:get_value("Content-Length", Hdrs1),
+ ?line undefined = proplists:get_value("Vary", Hdrs1),
Uri2 = "http://localhost:8001/0.txt",
?line {ok, "200", Hdrs2, _} =
ibrowse:send_req(Uri2, [{"Accept-Encoding", "gzip, deflate"}], get),
?line undefined = proplists:get_value("Content-Encoding", Hdrs2),
?line "0" = proplists:get_value("Content-Length", Hdrs2),
+ ?line undefined = proplists:get_value("Vary", Hdrs2),
%% Dynamic content
Uri3 = "http://localhost:8000/emptytest",
?line {ok, "200", Hdrs3, _} =
ibrowse:send_req(Uri3, [{"Accept-Encoding", "gzip, deflate"}], get),
?line undefined = proplists:get_value("Content-Encoding", Hdrs3),
?line "0" = proplists:get_value("Content-Length", Hdrs3),
+ ?line undefined = proplists:get_value("Vary", Hdrs3),
Uri4 = "http://localhost:8001/emptytest",
?line {ok, "200", Hdrs4, _} =
ibrowse:send_req(Uri4, [{"Accept-Encoding", "gzip, deflate"}], get),
?line undefined = proplists:get_value("Content-Encoding", Hdrs4),
?line "0" = proplists:get_value("Content-Length", Hdrs4),
+ ?line undefined = proplists:get_value("Vary", Hdrs4),
ok.
@@ -105,6 +127,7 @@ deflate_streamcontent() ->
?line "gzip" = proplists:get_value("Content-Encoding", Hdrs1),
?line "chunked" = proplists:get_value("Transfer-Encoding", Hdrs1),
?line undefined = proplists:get_value("Content-Length", Hdrs1),
+ ?line "Accept-Encoding" = proplists:get_value("Vary", Hdrs1),
%% Partial content is not compressed for large files
Uri2 = "http://localhost:8001/10000.txt",
@@ -114,6 +137,7 @@ deflate_streamcontent() ->
?line undefined = proplists:get_value("Content-Encoding", Hdrs2),
?line undefined = proplists:get_value("Transfer-Encoding", Hdrs2),
?line "100" = proplists:get_value("Content-Length", Hdrs2),
+ ?line "Accept-Encoding" = proplists:get_value("Vary", Hdrs2),
%% Dynamic content (chunked)
Uri3 = "http://localhost:8001/streamtest",
@@ -122,6 +146,7 @@ deflate_streamcontent() ->
?line "gzip" = proplists:get_value("Content-Encoding", Hdrs3),
?line "chunked" = proplists:get_value("Transfer-Encoding", Hdrs3),
?line undefined = proplists:get_value("Content-Length", Hdrs3),
+ ?line "Accept-Encoding" = proplists:get_value("Vary", Hdrs3),
ok.

0 comments on commit d748f92

Please sign in to comment.