Permalink
Browse files

add yaws_api:merge_header/2 and yaws_api:merge_header/3 funs

The merge_header functions allow HTTP headers and their values to be set
and combined in a #headers{} record.
  • Loading branch information...
1 parent 80b5767 commit bead34b1d8690fbbe3e02a6d16ad7ba5a8a1bd7f @vinoski vinoski committed Mar 13, 2013
Showing with 92 additions and 19 deletions.
  1. +2 −0 doc/yaws.tex
  2. +33 −5 man/yaws_api.5
  3. +45 −14 src/yaws_api.erl
  4. +12 −0 test/eunit/headers.erl
View
2 doc/yaws.tex
@@ -1224,6 +1224,7 @@ \subsection{Arg rewrite}
\begin{itemize}
\item \verb+set_header/2+, \verb+set_header/3+
\item \verb+get_header/2+, \verb+get_header/3+
+\item \verb+merge_header/2+, \verb+merge_header/3+
\item \verb+delete_header/2+
\end{itemize}
@@ -2905,6 +2906,7 @@ \section{Server Part}
\begin{itemize}
\item \verb+set_header/2+, \verb+set_header/3+
\item \verb+get_header/2+, \verb+get_header/3+
+\item \verb+merge_header/2+, \verb+merge_header/3+
\item \verb+delete_header/2+
\end{itemize}
View
38 man/yaws_api.5
@@ -534,7 +534,17 @@ record. The return list is suitable for retransmit.
Returns a list of reformatted header values from a #headers{} record, with
each element of the list formatted via a call to \fIFormatFun\fR. This
enables converting #headers{} records into various lists of headers and
-their values.
+their values. Note that sometimes the \fISet-Cookie\fR header will contain
+a tuple value of the form \fI{multi, ValueList}\fR \[em] see
+\fImerge_header/2\fR below for details \[em] so formatting functions should
+be prepared to handle such a tuple. They should handle it by formatting
+each member of \fIValueList\fR as a separate \fISet-Cookie\fR header, then
+returning all such header-value pairs in a list. Note that this implies
+that sometimes the return values of \fIreformat_header/1\fR and
+\fIreformat_header/2\fR can be a multi-level list. The \fI{multi,
+ValueList}\fR construct results only from calls to \fImerge_header/2\fR or
+\fImerge_header/3\fR, where multiple values are set in separate calls for
+the same header.
.TP
\fBset_header(Headers, {Header, Value})\fR
@@ -545,10 +555,28 @@ Sets header \fIHeader\fR with value \fIValue\fR in the #headers{} record
.TP
\fBset_header(Headers, Header, Value)\fR
-Sets header \fIHeader\fR with value \fIValue\fR in the #headers{} record
-\fIHeaders\fR, and returns a new #headers{} record. Using the atom
-\fIundefined\fR for \fIValue\fR effectively deletes the header, same as
-\fIdelete_header/2\fR.
+Same as \fIset_header/2\fR above, except \fIHeader\fR and \fIValue\fR are
+not passed in a tuple.
+
+.TP
+\fBmerge_header(Headers, {Header, Value})\fR
+Merges value \fIValue\fR for header \fIHeader\fR with any existing value
+for that header in the #headers{} record \fIHeaders\fR, and returns a new
+#headers{} record. Using the atom \fIundefined\fR for \fIValue\fR simply
+returns \fIHeaders\fR. Otherwise, \fIValue\fR is merged with any existing
+value already present in the \fIHeaders\fR record for header \fIHeader\fR,
+comma-separated from that existing value. If no such value exists in the
+\fIHeaders\fR record, the effect is the same as \fIset_header/2\fR. Note
+that for the \fISet-Cookie\fR header, values are not comma-separated but
+are instead collected into a tuple \fI{multi, ValueList}\fR where
+\fIValueList\fR is the collection of \fISet-Cookie\fR values. This implies
+that any formatting fun passed to \fIreformat_header/2\fR must be prepared
+to handle such tuples.
+
+.TP
+\fBmerge_header(Headers, Header, Value)\fR
+Same as \fImerge_header/2\fR above, except \fIHeader\fR and \fIValue\fR are
+not passed in a tuple.
.TP
\fBget_header(Headers, Header)\fR
View
59 src/yaws_api.erl
@@ -92,7 +92,8 @@
headers_content_encoding/1, headers_authorization/1,
headers_transfer_encoding/1, headers_x_forwarded_for/1, headers_other/1]).
--export([set_header/2, set_header/3, get_header/2, get_header/3, delete_header/2]).
+-export([set_header/2, set_header/3, merge_header/2, merge_header/3,
+ get_header/2, get_header/3, delete_header/2]).
-import(lists, [flatten/1, reverse/1]).
@@ -1049,8 +1050,11 @@ set_status_code(Code) ->
%% returns [ Header1, Header2 .....]
reformat_header(H) ->
- FormatFun = fun(Hname, Str) ->
- lists:flatten(io_lib:format("~s: ~s",[Hname, Str]))
+ FormatFun = fun(Hname, {multi, Values}) ->
+ [lists:flatten(io_lib:format("~s: ~s", [Hname, Val])) ||
+ Val <- Values];
+ (Hname, Str) ->
+ lists:flatten(io_lib:format("~s: ~s", [Hname, Str]))
end,
reformat_header(H, FormatFun).
reformat_header(H, FormatFun) ->
@@ -1300,6 +1304,38 @@ set_header(#headers{}=Hdrs, Header, undefined) ->
set_header(#headers{}=Hdrs, Header, Value) ->
set_header(Hdrs, {lower, string:to_lower(Header)}, Value).
+merge_header(#headers{}=Hdrs, {Header, Value}) ->
+ merge_header(Hdrs, Header, Value).
+
+merge_header(#headers{}=Hdrs, _Header, undefined) ->
+ Hdrs;
+merge_header(#headers{}=Hdrs, Header, Value) when is_atom(Header) ->
+ merge_header(Hdrs, atom_to_list(Header), Value);
+merge_header(#headers{}=Hdrs, Header, Value) when is_binary(Header) ->
+ merge_header(Hdrs, binary_to_list(Header), Value);
+merge_header(#headers{}=Hdrs, Header, Value) when is_binary(Value) ->
+ merge_header(Hdrs, Header, binary_to_list(Value));
+merge_header(Hdrs, {lower, "set-cookie"}=LHdr, Value) ->
+ NewValue = case get_header(Hdrs, LHdr) of
+ undefined ->
+ {multi, [Value]};
+ {multi, MultiVal} ->
+ {multi, MultiVal ++ [Value]};
+ ExistingValue ->
+ {multi, [ExistingValue, Value]}
+ end,
+ set_header(Hdrs, LHdr, NewValue);
+merge_header(Hdrs, {lower, _Header}=LHdr, Value) ->
+ NewValue = case get_header(Hdrs, LHdr) of
+ undefined ->
+ Value;
+ ExistingValue ->
+ ExistingValue ++ ", " ++ Value
+ end,
+ set_header(Hdrs, LHdr, NewValue);
+merge_header(#headers{}=Hdrs, Header, Value) ->
+ merge_header(Hdrs, {lower, string:to_lower(Header)}, Value).
+
get_header(#headers{}=Hdrs, connection) ->
Hdrs#headers.connection;
get_header(#headers{}=Hdrs, {lower, "connection"}) ->
@@ -1456,17 +1492,12 @@ erlang_header_name("proxy-connection") -> 'Proxy-Connection';
erlang_header_name(Name) -> capitalize_header(Name).
capitalize_header(Name) ->
- capitalize_header2(Name, "").
-
-capitalize_header2([C | Rest], "") when C >= $a andalso C =< $z ->
- capitalize_header2(Rest, [C - $a + $A]);
-capitalize_header2([$-, C | Rest], Result) when C >= $a andalso C =< $z ->
- capitalize_header2(Rest, [C - $a + $A, $- | Result]);
-capitalize_header2([C | Rest], Result) ->
- capitalize_header2(Rest, [C | Result]);
-capitalize_header2([], Result) ->
- lists:reverse(Result).
-
+ %% Before R16B Erlang capitalized words inside header names only for
+ %% headers less than 20 characters long. In R16B that length was raised
+ %% to 50. Using decode_packet lets us be portable.
+ {ok, {http_header, _, Result, _, _}, _} =
+ erlang:decode_packet(httph, list_to_binary([Name, <<": x\r\n\r\n">>]), []),
+ Result.
reformat_request(#http_request{method = bad_request}) ->
["Bad request"];
View
12 test/eunit/headers.erl
@@ -43,6 +43,18 @@ delete_headers_test() ->
end, {10, Headers}, lists:seq(1,10)),
ok.
+merge_headers_test() ->
+ Hdrs0 = create_headers(10),
+ Hdrs1 = yaws_api:merge_header(Hdrs0, <<"x-header-7">>, <<"another-value">>),
+ Val1 = yaws_api:get_header(Hdrs1, 'x-header-7'),
+ Expected1 = lists:sort(["value7", "another-value"]),
+ Expected1 = lists:sort(string:tokens(Val1, ", ")),
+ Hdrs2 = yaws_api:set_header(Hdrs1, "set-cookie", "user=joe"),
+ Hdrs3 = yaws_api:merge_header(Hdrs2, "set-cookie", "domain=erlang.org"),
+ Val2 = yaws_api:get_header(Hdrs3, "set-cookie"),
+ {multi, ["user=joe", "domain=erlang.org"]} = Val2,
+ ok.
+
create_headers(N) ->
lists:foldl(fun({Hdr,Val}, Hdrs) ->
yaws_api:set_header(Hdrs, Hdr, Val)

0 comments on commit bead34b

Please sign in to comment.