diff --git a/doc/yaws.tex b/doc/yaws.tex index 308ee46b6..0e0550339 100644 --- a/doc/yaws.tex +++ b/doc/yaws.tex @@ -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 diff --git a/include/yaws.hrl b/include/yaws.hrl index d4ec585ee..e153a3718 100644 --- a/include/yaws.hrl +++ b/include/yaws.hrl @@ -347,6 +347,7 @@ content_encoding, transfer_encoding, www_authenticate, + vary, other % misc other headers }). diff --git a/man/yaws_api.5 b/man/yaws_api.5 index 55a3d5c79..ea3932200 100644 --- a/man/yaws_api.5 +++ b/man/yaws_api.5 @@ -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: diff --git a/src/yaws.erl b/src/yaws.erl index 1fd486458..3c3f752b6 100644 --- a/src/yaws.erl +++ b/src/yaws.erl @@ -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 diff --git a/test/t5/app_test.erl b/test/t5/app_test.erl index d043a6652..2c88d9f17 100644 --- a/test/t5/app_test.erl +++ b/test/t5/app_test.erl @@ -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,12 +91,14 @@ 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", @@ -86,12 +106,14 @@ deflate_empty_response() -> 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.