Merge tag 'v2.12.0'
diff --git a/.gitignore b/.gitignore
index 8f4edf4..bb59f19 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@
 .DS_Store
 /TEST-*.xml
 /deps
+/.rebar
 *.swp
 *.beam
 *.dump
diff --git a/.travis.yml b/.travis.yml
index 43dad1a..d9c6fd8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,6 +2,7 @@
 notifications:
   email: false
 otp_release:
-  - R15B02
+  - 17.1
+  - 17.0
+  - R16B03-1
   - R15B03
-  - R16B
diff --git a/CHANGES.md b/CHANGES.md
index 89c8078..24a59d6 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,7 +1,66 @@
-Version 2.8.1 released XXXX-XX-XX
+Version 2.12.0 released 2015-01-16
+
+* Send "Connection: close" header when the server is going to close
+  a Keep-Alive connection, usually due to unread data from the
+  client
+  https://github.com/mochi/mochiweb/issues/146
+
+Version 2.11.2 released 2015-01-16
+
+* Fix regression introduced in #147
+  https://github.com/mochi/mochiweb/pull/147
+
+Version 2.11.1 released 2015-01-16
+
+* Accept range end position which exceededs the resource size
+  https://github.com/mochi/mochiweb/pull/147
+
+Version 2.11.0 released 2015-01-12
+
+* Perform SSL handshake after releasing acceptor back into the pool,
+  and slow accept rate when file descriptors are not available,
+  to mitigate a potential DoS attack. Adds new mochiweb_socket
+  functions transport_accept/1 and finish_accept/1 which should be
+  used in preference to the now deprecated accept/1 function.
+  https://github.com/mochi/mochiweb/issues/138
+
+Version 2.10.1 released 2015-01-11
+
+* Fixes issue with SSL and mochiweb_websocket. Note that
+  mochiweb_websocket is still experimental and the API
+  is subject to change in future versions.
+  https://github.com/mochi/mochiweb/pull/144
+
+Version 2.10.0 released 2014-12-17
+
+* Added new `recbuf` option to mochiweb_http to allow the receive
+  buffer to be configured.
+  https://github.com/mochi/mochiweb/pull/134
+
+Version 2.9.2 released 2014-10-16
+
+* Add timeouts to SSL connect to prevent DoS by opening a connection
+  and not doing anything.
+  https://github.com/mochi/mochiweb/pull/140
+* Prevent using ECDH cipher in R16B because it is broken
+  https://github.com/mochi/mochiweb/pull/140
+* For default SSL connections, remove usage of sslv3 and not-so-secure
+  ciphers.
+  https://github.com/mochi/mochiweb/pull/140
+
+Version 2.9.1 released 2014-09-29
+
+* Fix Makefile rule for building docs
+  https://github.com/mochi/mochiweb/issues/135
+* Minimize gen_tcp:send calls to optimize performance.
+  https://github.com/mochi/mochiweb/pull/137
+
+Version 2.9.0 released 2014-06-24
 
 * Increased timeout in test suite for FreeBSD
   https://github.com/mochi/mochiweb/pull/121
+* Updated rebar to v2.5.0 and fixed associated build issues
+  https://github.com/mochi/mochiweb/issues/131
 
 Version 2.8.0 released 2014-01-01
 
diff --git a/Makefile b/Makefile
index b94be4b..33601c7 100644
--- a/Makefile
+++ b/Makefile
@@ -6,25 +6,18 @@
 .PHONY: all edoc test clean build_plt dialyzer app
 
 all:
-	@$(REBAR) get-deps compile
+	@$(REBAR) prepare-deps
 
-edoc:
+edoc: all
 	@$(REBAR) doc
 
 test:
 	@rm -rf .eunit
 	@mkdir -p .eunit
-	@$(REBAR) skip_deps=true eunit
+	@$(REBAR) eunit
 
 clean:
 	@$(REBAR) clean
 
-build_plt:
-	@$(REBAR) build-plt
-
-dialyzer:
-	@$(REBAR) dialyze
-
 app:
-	@$(REBAR) create template=mochiwebapp dest=$(DEST) appid=$(PROJECT)
-
+	@$(REBAR) -r create template=mochiwebapp dest=$(DEST) appid=$(PROJECT)
diff --git a/scripts/new_mochiweb.erl b/scripts/new_mochiweb.erl
deleted file mode 100755
index f49ed39..0000000
--- a/scripts/new_mochiweb.erl
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/usr/bin/env escript
-%% -*- mode: erlang -*-
--export([main/1]).
-
-%% External API
-
-main(_) ->
-    usage().
-
-%% Internal API
-
-usage() ->
-    io:format(
-        "new_mochiweb.erl has been replaced by a rebar template!\n"
-        "\n"
-        "To create a new mochiweb using project:\n"
-        "   make app PROJECT=project_name\n"
-        "\n"
-        "To create a new mochiweb using project in a specific directory:\n"
-        "   make app PROJECT=project_name PREFIX=$HOME/projects/\n"
-        "\n"
-    ),
-    halt(1).
diff --git a/src/mochiweb.app.src b/src/mochiweb.app.src
index b3b3d83..8cb43ac 100644
--- a/src/mochiweb.app.src
+++ b/src/mochiweb.app.src
@@ -1,7 +1,7 @@
 %% This is generated from src/mochiweb.app.src
 {application, mochiweb,
  [{description, "MochiMedia Web Server"},
-  {vsn, "2.8.1"},
+  {vsn, "2.12.0"},
   {modules, []},
   {registered, []},
   {env, []},
diff --git a/src/mochiweb.erl b/src/mochiweb.erl
index 927322d..5c4201c 100644
--- a/src/mochiweb.erl
+++ b/src/mochiweb.erl
@@ -51,10 +51,15 @@
 uri(HttpString) when is_list(HttpString) ->
     HttpString.
 
-%% @spec new_request({Socket, Request, Headers}) -> MochiWebRequest
+%% @spec new_request( {Socket, Request, Headers}
+%%                  | {Socket, Opts, Request, Headers} ) -> MochiWebRequest
 %% @doc Return a mochiweb_request data structure.
 new_request({Socket, {Method, HttpUri, Version}, Headers}) ->
+    new_request({Socket, [], {Method, HttpUri, Version}, Headers});
+
+new_request({Socket, Opts, {Method, HttpUri, Version}, Headers}) ->
     mochiweb_request:new(Socket,
+                         Opts,
                          Method,
                          uri(HttpUri),
                          Version,
diff --git a/src/mochiweb_acceptor.erl b/src/mochiweb_acceptor.erl
index ebbaf45..8a58fcf 100644
--- a/src/mochiweb_acceptor.erl
+++ b/src/mochiweb_acceptor.erl
@@ -8,24 +8,46 @@
 
 -include("internal.hrl").
 
--export([start_link/3, init/3]).
+-export([start_link/3, start_link/4, init/4]).
+
+-define(EMFILE_SLEEP_MSEC, 100).
 
 start_link(Server, Listen, Loop) ->
-    proc_lib:spawn_link(?MODULE, init, [Server, Listen, Loop]).
+    start_link(Server, Listen, Loop, []).
 
-init(Server, Listen, Loop) ->
+start_link(Server, Listen, Loop, Opts) ->
+    proc_lib:spawn_link(?MODULE, init, [Server, Listen, Loop, Opts]).
+
+do_accept(Server, Listen) ->
     T1 = os:timestamp(),
-    case catch mochiweb_socket:accept(Listen) of
+    case mochiweb_socket:transport_accept(Listen) of
         {ok, Socket} ->
             gen_server:cast(Server, {accepted, self(), timer:now_diff(os:timestamp(), T1)}),
-            call_loop(Loop, Socket);
-        {error, closed} ->
-            exit(normal);
-        {error, timeout} ->
-            init(Server, Listen, Loop);
-        {error, esslaccept} ->
+            mochiweb_socket:finish_accept(Socket);
+        Other ->
+            Other
+    end.
+
+init(Server, Listen, Loop, Opts) ->
+    case catch do_accept(Server, Listen) of
+        {ok, Socket} ->
+            call_loop(Loop, Socket, Opts);
+        {error, Err} when Err =:= closed orelse
+                          Err =:= esslaccept orelse
+                          Err =:= timeout ->
             exit(normal);
         Other ->
+            %% Mitigate out of file descriptor scenario by sleeping for a
+            %% short time to slow error rate
+            case Other of
+                {error, emfile} ->
+                    receive
+                    after ?EMFILE_SLEEP_MSEC ->
+                            ok
+                    end;
+                _ ->
+                    ok
+            end,
             error_logger:error_report(
               [{application, mochiweb},
                "Accept failed error",
@@ -33,18 +55,11 @@
             exit({error, accept_failed})
     end.
 
-call_loop({M, F}, Socket) ->
-    M:F(Socket);
-call_loop({M, F, [A1]}, Socket) ->
-    M:F(Socket, A1);
-call_loop({M, F, A}, Socket) ->
-    erlang:apply(M, F, [Socket | A]);
-call_loop(Loop, Socket) ->
-    Loop(Socket).
-
-%%
-%% Tests
-%%
--ifdef(TEST).
--include_lib("eunit/include/eunit.hrl").
--endif.
+call_loop({M, F}, Socket, Opts) ->
+    M:F(Socket, Opts);
+call_loop({M, F, [A1]}, Socket, Opts) ->
+    M:F(Socket, Opts, A1);
+call_loop({M, F, A}, Socket, Opts) ->
+    erlang:apply(M, F, [Socket, Opts | A]);
+call_loop(Loop, Socket, Opts) ->
+    Loop(Socket, Opts).
diff --git a/src/mochiweb_base64url.erl b/src/mochiweb_base64url.erl
index ab5aaec..5f552e0 100644
--- a/src/mochiweb_base64url.erl
+++ b/src/mochiweb_base64url.erl
@@ -8,13 +8,13 @@
 %% '_' is used in place of '/' (63),
 %% padding is implicit rather than explicit ('=').
 
--spec encode(iolist()) -> binary().
+-spec encode(iolist() | binary()) -> binary().
 encode(B) when is_binary(B) ->
     encode_binary(B);
 encode(L) when is_list(L) ->
     encode_binary(iolist_to_binary(L)).
 
--spec decode(iolist()) -> binary().
+-spec decode(iolist() | binary()) -> binary().
 decode(B) when is_binary(B) ->
     decode_binary(B);
 decode(L) when is_list(L) ->
diff --git a/src/mochiweb_http.erl b/src/mochiweb_http.erl
index 38d51d4..1ea1f15 100644
--- a/src/mochiweb_http.erl
+++ b/src/mochiweb_http.erl
@@ -6,7 +6,7 @@
 -module(mochiweb_http).
 -author('bob@mochimedia.com').
 -export([start/1, start_link/1, stop/0, stop/1]).
--export([loop/2]).
+-export([loop/3]).
 -export([after_response/2, reentry/1]).
 -export([parse_range_request/1, range_skip_length/2]).
 
@@ -40,7 +40,7 @@
 %%     Option = {name, atom()} | {ip, string() | tuple()} | {backlog, integer()}
 %%              | {nodelay, boolean()} | {acceptor_pool_size, integer()}
 %%              | {ssl, boolean()} | {profile_fun, undefined | (Props) -> ok}
-%%              | {link, false}
+%%              | {link, false} | {recbuf, non_negative_integer()}
 %% @doc Start a mochiweb server.
 %%      profile_fun is used to profile accept timing.
 %%      After each accept, if defined, profile_fun is called with a proplist of a subset of the mochiweb_socket_server state and timing information.
@@ -52,20 +52,20 @@
 start_link(Options) ->
     mochiweb_socket_server:start_link(parse_options(Options)).
 
-loop(Socket, Body) ->
+loop(Socket, Opts, Body) ->
     ok = mochiweb_socket:setopts(Socket, [{packet, http}]),
-    request(Socket, Body).
+    request(Socket, Opts, Body).
 
-request(Socket, Body) ->
+request(Socket, Opts, Body) ->
     ok = mochiweb_socket:setopts(Socket, [{active, once}]),
     receive
         {Protocol, _, {http_request, Method, Path, Version}} when Protocol == http orelse Protocol == ssl ->
             ok = mochiweb_socket:setopts(Socket, [{packet, httph}]),
-            headers(Socket, {Method, Path, Version}, [], Body, 0);
+            headers(Socket, Opts, {Method, Path, Version}, [], Body, 0);
         {Protocol, _, {http_error, "\r\n"}} when Protocol == http orelse Protocol == ssl ->
-            request(Socket, Body);
+            request(Socket, Opts, Body);
         {Protocol, _, {http_error, "\n"}} when Protocol == http orelse Protocol == ssl ->
-            request(Socket, Body);
+            request(Socket, Opts, Body);
         {tcp_closed, _} ->
             mochiweb_socket:close(Socket),
             exit(normal);
@@ -73,7 +73,7 @@
             mochiweb_socket:close(Socket),
             exit(normal);
         Other ->
-            handle_invalid_msg_request(Other, Socket)
+            handle_invalid_msg_request(Other, Socket, Opts)
     after ?REQUEST_RECV_TIMEOUT ->
         mochiweb_socket:close(Socket),
         exit(normal)
@@ -84,25 +84,25 @@
             ?MODULE:after_response(Body, Req)
     end.
 
-headers(Socket, Request, Headers, _Body, ?MAX_HEADERS) ->
+headers(Socket, Opts, Request, Headers, _Body, ?MAX_HEADERS) ->
     %% Too many headers sent, bad request.
     ok = mochiweb_socket:setopts(Socket, [{packet, raw}]),
-    handle_invalid_request(Socket, Request, Headers);
-headers(Socket, Request, Headers, Body, HeaderCount) ->
+    handle_invalid_request(Socket, Opts, Request, Headers);
+headers(Socket, Opts, Request, Headers, Body, HeaderCount) ->
     ok = mochiweb_socket:setopts(Socket, [{active, once}]),
     receive
         {Protocol, _, http_eoh} when Protocol == http orelse Protocol == ssl ->
-            Req = new_request(Socket, Request, Headers),
+            Req = new_request(Socket, Opts, Request, Headers),
             call_body(Body, Req),
             ?MODULE:after_response(Body, Req);
         {Protocol, _, {http_header, _, Name, _, Value}} when Protocol == http orelse Protocol == ssl ->
-            headers(Socket, Request, [{Name, Value} | Headers], Body,
+            headers(Socket, Opts, Request, [{Name, Value} | Headers], Body,
                     1 + HeaderCount);
         {tcp_closed, _} ->
             mochiweb_socket:close(Socket),
             exit(normal);
         Other ->
-            handle_invalid_msg_request(Other, Socket, Request, Headers)
+            handle_invalid_msg_request(Other, Socket, Opts, Request, Headers)
     after ?HEADERS_RECV_TIMEOUT ->
         mochiweb_socket:close(Socket),
         exit(normal)
@@ -115,31 +115,31 @@
 call_body(Body, Req) ->
     Body(Req).
 
--spec handle_invalid_msg_request(term(), term()) -> no_return().
-handle_invalid_msg_request(Msg, Socket) ->
-    handle_invalid_msg_request(Msg, Socket, {'GET', {abs_path, "/"}, {0,9}}, []).
+-spec handle_invalid_msg_request(term(), term(), term()) -> no_return().
+handle_invalid_msg_request(Msg, Socket, Opts) ->
+    handle_invalid_msg_request(Msg, Socket, Opts, {'GET', {abs_path, "/"}, {0,9}}, []).
 
--spec handle_invalid_msg_request(term(), term(), term(), term()) -> no_return().
-handle_invalid_msg_request(Msg, Socket, Request, RevHeaders) ->
+-spec handle_invalid_msg_request(term(), term(), term(), term(), term()) -> no_return().
+handle_invalid_msg_request(Msg, Socket, Opts, Request, RevHeaders) ->
     case {Msg, r15b_workaround()} of
         {{tcp_error,_,emsgsize}, true} ->
             %% R15B02 returns this then closes the socket, so close and exit
             mochiweb_socket:close(Socket),
             exit(normal);
         _ ->
-            handle_invalid_request(Socket, Request, RevHeaders)
+            handle_invalid_request(Socket, Opts, Request, RevHeaders)
     end.
 
--spec handle_invalid_request(term(), term(), term()) -> no_return().
-handle_invalid_request(Socket, Request, RevHeaders) ->
-    Req = new_request(Socket, Request, RevHeaders),
+-spec handle_invalid_request(term(), term(), term(), term()) -> no_return().
+handle_invalid_request(Socket, Opts, Request, RevHeaders) ->
+    Req = new_request(Socket, Opts, Request, RevHeaders),
     Req:respond({400, [], []}),
     mochiweb_socket:close(Socket),
     exit(normal).
 
-new_request(Socket, Request, RevHeaders) ->
+new_request(Socket, Opts, Request, RevHeaders) ->
     ok = mochiweb_socket:setopts(Socket, [{packet, raw}]),
-    mochiweb:new_request({Socket, Request, lists:reverse(RevHeaders)}).
+    mochiweb:new_request({Socket, Opts, Request, lists:reverse(RevHeaders)}).
 
 after_response(Body, Req) ->
     Socket = Req:get(socket),
@@ -150,11 +150,9 @@
         false ->
             Req:cleanup(),
             erlang:garbage_collect(),
-            ?MODULE:loop(Socket, Body)
+            ?MODULE:loop(Socket, mochiweb_request:get(opts, Req), Body)
     end.
 
-parse_range_request("bytes=0-") ->
-    undefined;
 parse_range_request(RawRange) when is_list(RawRange) ->
     try
         "bytes=" ++ RangeString = RawRange,
@@ -186,11 +184,9 @@
             {R, Size - R};
         {_OutOfRange, none} ->
             invalid_range;
-        {Start, End} when 0 =< Start, Start =< End, End < Size ->
-            {Start, End - Start + 1};
-        {Start, End} when 0 =< Start, Start =< End, End >= Size ->
-            {Start, Size - Start};
-        {_OutOfRange, _End} ->
+        {Start, End} when Start >= 0, Start < Size, Start =< End ->
+            {Start, erlang:min(End + 1, Size) - Start};
+        {_InvalidStart, _InvalidEnd} ->
             invalid_range
     end.
 
@@ -207,7 +203,7 @@
     ?assertEqual([{none, 20}], parse_range_request("bytes=-20")),
 
     %% trivial single range
-    ?assertEqual(undefined, parse_range_request("bytes=0-")),
+    ?assertEqual([{0, none}], parse_range_request("bytes=0-")),
 
     %% invalid, single ranges
     ?assertEqual(fail, parse_range_request("")),
@@ -254,6 +250,7 @@
     ?assertEqual({BodySize, 0}, range_skip_length({none, 0}, BodySize)),
     ?assertEqual({0, BodySize}, range_skip_length({none, BodySize}, BodySize)),
     ?assertEqual({0, BodySize}, range_skip_length({0, none}, BodySize)),
+    ?assertEqual({0, BodySize}, range_skip_length({0, BodySize + 1}, BodySize)),
     BodySizeLess1 = BodySize - 1,
     ?assertEqual({BodySizeLess1, 1},
                  range_skip_length({BodySize - 1, none}, BodySize)),
@@ -281,6 +278,8 @@
                  range_skip_length({-1, none}, BodySize)),
     ?assertEqual(invalid_range,
                  range_skip_length({BodySize, none}, BodySize)),
+    ?assertEqual(invalid_range,
+                 range_skip_length({BodySize + 1, BodySize + 5}, BodySize)),
     ok.
 
 -endif.
diff --git a/src/mochiweb_multipart.erl b/src/mochiweb_multipart.erl
index a83a88c..90bc949 100644
--- a/src/mochiweb_multipart.erl
+++ b/src/mochiweb_multipart.erl
@@ -374,7 +374,7 @@
               body_end,
               eof],
     TestCallback = fun (Next) -> test_callback(Next, Expect) end,
-    ServerFun = fun (Socket) ->
+    ServerFun = fun (Socket, _Opts) ->
                         ok = mochiweb_socket:send(Socket, BinContent),
                         exit(normal)
                 end,
@@ -410,7 +410,7 @@
               body_end,
               eof],
     TestCallback = fun (Next) -> test_callback(Next, Expect) end,
-    ServerFun = fun (Socket) ->
+    ServerFun = fun (Socket, _Opts) ->
                         ok = mochiweb_socket:send(Socket, BinContent),
                         exit(normal)
                 end,
@@ -447,7 +447,7 @@
                  "--AaB03x--",
                  ""], "\r\n"),
     BinContent = iolist_to_binary(Content),
-    ServerFun = fun (Socket) ->
+    ServerFun = fun (Socket, _Opts) ->
                         ok = mochiweb_socket:send(Socket, BinContent),
                         exit(normal)
                 end,
@@ -500,7 +500,7 @@
               body_end,
               eof],
     TestCallback = fun (Next) -> test_callback(Next, Expect) end,
-    ServerFun = fun (Socket) ->
+    ServerFun = fun (Socket, _Opts) ->
                         ok = mochiweb_socket:send(Socket, BinContent),
                         exit(normal)
                 end,
@@ -552,7 +552,7 @@
               body_end,
               eof],
     TestCallback = fun (Next) -> test_callback(Next, Expect) end,
-    ServerFun = fun (Socket) ->
+    ServerFun = fun (Socket, _Opts) ->
                         ok = mochiweb_socket:send(Socket, BinContent),
                         exit(normal)
                 end,
@@ -605,7 +605,7 @@
               body_end,
               eof],
     TestCallback = fun (Next) -> test_callback(Next, Expect) end,
-    ServerFun = fun (Socket) ->
+    ServerFun = fun (Socket, _Opts) ->
                         ok = mochiweb_socket:send(Socket, BinContent),
                         exit(normal)
                 end,
@@ -681,7 +681,7 @@
               body_end,
               eof],
     TestCallback = fun (Next) -> test_callback(Next, Expect) end,
-    ServerFun = fun (Socket) ->
+    ServerFun = fun (Socket, _Opts) ->
                         ok = mochiweb_socket:send(Socket, BinContent),
                         exit(normal)
                 end,
@@ -729,7 +729,7 @@
               body_end,
               eof],
     TestCallback = fun (Next) -> test_callback(Next, Expect) end,
-    ServerFun = fun (Socket) ->
+    ServerFun = fun (Socket, _Opts) ->
                         ok = mochiweb_socket:send(Socket, BinContent),
                         exit(normal)
                 end,
@@ -856,7 +856,7 @@
               body_end,
               eof],
     TestCallback = fun (Next) -> test_callback(Next, Expect) end,
-    ServerFun = fun (Socket) ->
+    ServerFun = fun (Socket, _Opts) ->
                         ok = mochiweb_socket:send(Socket, BinContent),
                         exit(normal)
                 end,
diff --git a/src/mochiweb_request.erl b/src/mochiweb_request.erl
index 859e2d6..c97070f 100644
--- a/src/mochiweb_request.erl
+++ b/src/mochiweb_request.erl
@@ -11,7 +11,7 @@
 
 -define(QUIP, "Any of you quaids got a smint?").
 
--export([new/5]).
+-export([new/5, new/6]).
 -export([get_header_value/2, get_primary_header_value/2, get_combined_header_value/2, get/2, dump/1]).
 -export([send/2, recv/2, recv/3, recv_body/1, recv_body/2, stream_body/4]).
 -export([start_response/2, start_response_length/2, start_raw_response/2]).
@@ -49,17 +49,22 @@
 %% @spec new(Socket, Method, RawPath, Version, headers()) -> request()
 %% @doc Create a new request instance.
 new(Socket, Method, RawPath, Version, Headers) ->
-    {?MODULE, [Socket, Method, RawPath, Version, Headers]}.
+    new(Socket, [], Method, RawPath, Version, Headers).
+
+%% @spec new(Socket, Opts, Method, RawPath, Version, headers()) -> request()
+%% @doc Create a new request instance.
+new(Socket, Opts, Method, RawPath, Version, Headers) ->
+    {?MODULE, [Socket, Opts, Method, RawPath, Version, Headers]}.
 
 %% @spec get_header_value(K, request()) -> undefined | Value
 %% @doc Get the value of a given request header.
-get_header_value(K, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) ->
+get_header_value(K, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, Headers]}) ->
     mochiweb_headers:get_value(K, Headers).
 
-get_primary_header_value(K, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) ->
+get_primary_header_value(K, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, Headers]}) ->
     mochiweb_headers:get_primary_value(K, Headers).
 
-get_combined_header_value(K, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) ->
+get_combined_header_value(K, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, Headers]}) ->
     mochiweb_headers:get_combined_value(K, Headers).
 
 %% @type field() = socket | scheme | method | raw_path | version | headers | peer | path | body_length | range
@@ -70,24 +75,24 @@
 %%      an ssl socket will be returned as <code>{ssl, SslSocket}</code>.
 %%      You can use <code>SslSocket</code> with the <code>ssl</code>
 %%      application, eg: <code>ssl:peercert(SslSocket)</code>.
-get(socket, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
+get(socket, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) ->
     Socket;
-get(scheme, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
+get(scheme, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) ->
     case mochiweb_socket:type(Socket) of
         plain ->
             http;
         ssl ->
             https
     end;
-get(method, {?MODULE, [_Socket, Method, _RawPath, _Version, _Headers]}) ->
+get(method, {?MODULE, [_Socket, _Opts, Method, _RawPath, _Version, _Headers]}) ->
     Method;
-get(raw_path, {?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) ->
+get(raw_path, {?MODULE, [_Socket, _Opts, _Method, RawPath, _Version, _Headers]}) ->
     RawPath;
-get(version, {?MODULE, [_Socket, _Method, _RawPath, Version, _Headers]}) ->
+get(version, {?MODULE, [_Socket, _Opts, _Method, _RawPath, Version, _Headers]}) ->
     Version;
-get(headers, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) ->
+get(headers, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, Headers]}) ->
     Headers;
-get(peer, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+get(peer, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     case mochiweb_socket:peername(Socket) of
         {ok, {Addr={10, _, _, _}, _Port}} ->
             case get_header_value("x-forwarded-for", THIS) of
@@ -108,7 +113,7 @@
         {error, enotconn} ->
             exit(normal)
     end;
-get(path, {?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) ->
+get(path, {?MODULE, [_Socket, _Opts, _Method, RawPath, _Version, _Headers]}) ->
     case erlang:get(?SAVE_PATH) of
         undefined ->
             {Path0, _, _} = mochiweb_util:urlsplit_path(RawPath),
@@ -118,7 +123,7 @@
         Cached ->
             Cached
     end;
-get(body_length, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+get(body_length, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     case erlang:get(?SAVE_BODY_LENGTH) of
         undefined ->
             BodyLength = body_length(THIS),
@@ -127,26 +132,29 @@
         {cached, Cached} ->
             Cached
     end;
-get(range, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+get(range, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     case get_header_value(range, THIS) of
         undefined ->
             undefined;
         RawRange ->
             mochiweb_http:parse_range_request(RawRange)
-    end.
+    end;
+get(opts, {?MODULE, [_Socket, Opts, _Method, _RawPath, _Version, _Headers]}) ->
+    Opts.
 
 %% @spec dump(request()) -> {mochiweb_request, [{atom(), term()}]}
 %% @doc Dump the internal representation to a "human readable" set of terms
 %%      for debugging/inspection purposes.
-dump({?MODULE, [_Socket, Method, RawPath, Version, Headers]}) ->
+dump({?MODULE, [_Socket, Opts, Method, RawPath, Version, Headers]}) ->
     {?MODULE, [{method, Method},
                {version, Version},
                {raw_path, RawPath},
+               {opts, Opts},
                {headers, mochiweb_headers:to_list(Headers)}]}.
 
 %% @spec send(iodata(), request()) -> ok
 %% @doc Send data over the socket.
-send(Data, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
+send(Data, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) ->
     case mochiweb_socket:send(Socket, Data) of
         ok ->
             ok;
@@ -157,13 +165,13 @@
 %% @spec recv(integer(), request()) -> binary()
 %% @doc Receive Length bytes from the client as a binary, with the default
 %%      idle timeout.
-recv(Length, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+recv(Length, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     recv(Length, ?IDLE_TIMEOUT, THIS).
 
 %% @spec recv(integer(), integer(), request()) -> binary()
 %% @doc Receive Length bytes from the client as a binary, with the given
 %%      Timeout in msec.
-recv(Length, Timeout, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
+recv(Length, Timeout, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) ->
     case mochiweb_socket:recv(Socket, Length, Timeout) of
         {ok, Data} ->
             put(?SAVE_RECV, true),
@@ -174,7 +182,7 @@
 
 %% @spec body_length(request()) -> undefined | chunked | unknown_transfer_encoding | integer()
 %% @doc  Infer body length from transfer-encoding and content-length headers.
-body_length({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+body_length({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     case get_header_value("transfer-encoding", THIS) of
         undefined ->
             case get_combined_header_value("content-length", THIS) of
@@ -193,13 +201,13 @@
 %% @spec recv_body(request()) -> binary()
 %% @doc Receive the body of the HTTP request (defined by Content-Length).
 %%      Will only receive up to the default max-body length of 1MB.
-recv_body({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+recv_body({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     recv_body(?MAX_RECV_BODY, THIS).
 
 %% @spec recv_body(integer(), request()) -> binary()
 %% @doc Receive the body of the HTTP request (defined by Content-Length).
 %%      Will receive up to MaxBody bytes.
-recv_body(MaxBody, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+recv_body(MaxBody, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     case erlang:get(?SAVE_BODY) of
         undefined ->
             % we could use a sane constant for max chunk size
@@ -219,11 +227,11 @@
         Cached -> Cached
     end.
 
-stream_body(MaxChunkSize, ChunkFun, FunState, {?MODULE,[_Socket,_Method,_RawPath,_Version,_Headers]}=THIS) ->
+stream_body(MaxChunkSize, ChunkFun, FunState, {?MODULE,[_Socket,_Opts,_Method,_RawPath,_Version,_Headers]}=THIS) ->
     stream_body(MaxChunkSize, ChunkFun, FunState, undefined, THIS).
 
 stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength,
-            {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+            {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     Expect = case get_header_value("expect", THIS) of
                  undefined ->
                      undefined;
@@ -263,23 +271,16 @@
 %% @doc Start the HTTP response by sending the Code HTTP response and
 %%      ResponseHeaders. The server will set header defaults such as Server
 %%      and Date if not present in ResponseHeaders.
-start_response({Code, ResponseHeaders}, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
-    HResponse = mochiweb_headers:make(ResponseHeaders),
-    HResponse1 = mochiweb_headers:default_from_list(server_headers(),
-                                                    HResponse),
-    start_raw_response({Code, HResponse1}, THIS).
+start_response({Code, ResponseHeaders}, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+    start_raw_response({Code, ResponseHeaders}, THIS).
 
 %% @spec start_raw_response({integer(), headers()}, request()) -> response()
 %% @doc Start the HTTP response by sending the Code HTTP response and
 %%      ResponseHeaders.
-start_raw_response({Code, ResponseHeaders}, {?MODULE, [_Socket, _Method, _RawPath, Version, _Headers]}=THIS) ->
-    F = fun ({K, V}, Acc) ->
-                [mochiweb_util:make_io(K), <<": ">>, V, <<"\r\n">> | Acc]
-        end,
-    End = lists:foldl(F, [<<"\r\n">>],
-                      mochiweb_headers:to_list(ResponseHeaders)),
-    send([make_version(Version), make_code(Code), <<"\r\n">> | End], THIS),
-    mochiweb:new_response({THIS, Code, ResponseHeaders}).
+start_raw_response({Code, ResponseHeaders}, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+    {Header, Response} = format_response_header({Code, ResponseHeaders}, THIS),
+    send(Header, THIS),
+    Response.
 
 
 %% @spec start_response_length({integer(), ioheaders(), integer()}, request()) -> response()
@@ -288,18 +289,44 @@
 %%      will set header defaults such as Server
 %%      and Date if not present in ResponseHeaders.
 start_response_length({Code, ResponseHeaders, Length},
-                      {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+                      {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     HResponse = mochiweb_headers:make(ResponseHeaders),
     HResponse1 = mochiweb_headers:enter("Content-Length", Length, HResponse),
     start_response({Code, HResponse1}, THIS).
 
+%% @spec format_response_header({integer(), ioheaders()} | {integer(), ioheaders(), integer()}, request()) -> iolist()
+%% @doc Format the HTTP response header, including the Code HTTP response and
+%%      ResponseHeaders including an optional Content-Length of Length. The server
+%%      will set header defaults such as Server
+%%      and Date if not present in ResponseHeaders.
+format_response_header({Code, ResponseHeaders}, {?MODULE, [_Socket, _Opts, _Method, _RawPath, Version, _Headers]}=THIS) ->
+    HResponse = mochiweb_headers:make(ResponseHeaders),
+    HResponse1 = mochiweb_headers:default_from_list(server_headers(), HResponse),
+    HResponse2 = case should_close(THIS) of
+                     true ->
+                         mochiweb_headers:enter("Connection", "close", HResponse1);
+                     false ->
+                         HResponse1
+                 end,
+    F = fun ({K, V}, Acc) ->
+                [mochiweb_util:make_io(K), <<": ">>, V, <<"\r\n">> | Acc]
+        end,
+    End = lists:foldl(F, [<<"\r\n">>], mochiweb_headers:to_list(HResponse2)),
+    Response = mochiweb:new_response({THIS, Code, HResponse2}),
+    {[make_version(Version), make_code(Code), <<"\r\n">> | End], Response};
+format_response_header({Code, ResponseHeaders, Length},
+                       {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+    HResponse = mochiweb_headers:make(ResponseHeaders),
+    HResponse1 = mochiweb_headers:enter("Content-Length", Length, HResponse),
+    format_response_header({Code, HResponse1}, THIS).
+
 %% @spec respond({integer(), ioheaders(), iodata() | chunked | {file, IoDevice}}, request()) -> response()
 %% @doc Start the HTTP response with start_response, and send Body to the
 %%      client (if the get(method) /= 'HEAD'). The Content-Length header
 %%      will be set by the Body length, and the server will insert header
 %%      defaults.
 respond({Code, ResponseHeaders, {file, IoDevice}},
-        {?MODULE, [_Socket, Method, _RawPath, _Version, _Headers]}=THIS) ->
+        {?MODULE, [_Socket, _Opts, Method, _RawPath, _Version, _Headers]}=THIS) ->
     Length = mochiweb_io:iodevice_size(IoDevice),
     Response = start_response_length({Code, ResponseHeaders, Length}, THIS),
     case Method of
@@ -311,7 +338,7 @@
               IoDevice)
     end,
     Response;
-respond({Code, ResponseHeaders, chunked}, {?MODULE, [_Socket, Method, _RawPath, Version, _Headers]}=THIS) ->
+respond({Code, ResponseHeaders, chunked}, {?MODULE, [_Socket, _Opts, Method, _RawPath, Version, _Headers]}=THIS) ->
     HResponse = mochiweb_headers:make(ResponseHeaders),
     HResponse1 = case Method of
                      'HEAD' ->
@@ -333,34 +360,32 @@
                          HResponse
                  end,
     start_response({Code, HResponse1}, THIS);
-respond({Code, ResponseHeaders, Body}, {?MODULE, [_Socket, Method, _RawPath, _Version, _Headers]}=THIS) ->
-    Response = start_response_length({Code, ResponseHeaders, iolist_size(Body)}, THIS),
+respond({Code, ResponseHeaders, Body}, {?MODULE, [_Socket, _Opts, Method, _RawPath, _Version, _Headers]}=THIS) ->
+    {Header, Response} = format_response_header({Code, ResponseHeaders, iolist_size(Body)}, THIS),
     case Method of
-        'HEAD' ->
-            ok;
-        _ ->
-            send(Body, THIS)
+        'HEAD' -> send(Header, THIS);
+        _      -> send([Header, Body], THIS)
     end,
     Response.
 
 %% @spec not_found(request()) -> response()
 %% @doc Alias for <code>not_found([])</code>.
-not_found({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+not_found({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     not_found([], THIS).
 
 %% @spec not_found(ExtraHeaders, request()) -> response()
 %% @doc Alias for <code>respond({404, [{"Content-Type", "text/plain"}
 %% | ExtraHeaders], &lt;&lt;"Not found."&gt;&gt;})</code>.
-not_found(ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+not_found(ExtraHeaders, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     respond({404, [{"Content-Type", "text/plain"} | ExtraHeaders],
              <<"Not found.">>}, THIS).
 
 %% @spec ok({value(), iodata()} | {value(), ioheaders(), iodata() | {file, IoDevice}}, request()) ->
 %%           response()
 %% @doc respond({200, [{"Content-Type", ContentType} | Headers], Body}).
-ok({ContentType, Body}, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ok({ContentType, Body}, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     ok({ContentType, [], Body}, THIS);
-ok({ContentType, ResponseHeaders, Body}, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ok({ContentType, ResponseHeaders, Body}, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     HResponse = mochiweb_headers:make(ResponseHeaders),
     case THIS:get(range) of
         X when (X =:= undefined orelse X =:= fail) orelse Body =:= chunked ->
@@ -393,7 +418,7 @@
 %% @spec should_close(request()) -> bool()
 %% @doc Return true if the connection must be closed. If false, using
 %%      Keep-Alive should be safe.
-should_close({?MODULE, [_Socket, _Method, _RawPath, Version, _Headers]}=THIS) ->
+should_close({?MODULE, [_Socket, _Opts, _Method, _RawPath, Version, _Headers]}=THIS) ->
     ForceClose = erlang:get(?SAVE_FORCE_CLOSE) =/= undefined,
     DidNotRecv = erlang:get(?SAVE_RECV) =:= undefined,
     ForceClose orelse Version < {1, 0}
@@ -419,7 +444,7 @@
 %% @spec cleanup(request()) -> ok
 %% @doc Clean up any junk in the process dictionary, required before continuing
 %%      a Keep-Alive request.
-cleanup({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}) ->
+cleanup({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) ->
     L = [?SAVE_QS, ?SAVE_PATH, ?SAVE_RECV, ?SAVE_BODY, ?SAVE_BODY_LENGTH,
          ?SAVE_POST, ?SAVE_COOKIE, ?SAVE_FORCE_CLOSE],
     lists:foreach(fun(K) ->
@@ -429,7 +454,7 @@
 
 %% @spec parse_qs(request()) -> [{Key::string(), Value::string()}]
 %% @doc Parse the query string of the URL.
-parse_qs({?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) ->
+parse_qs({?MODULE, [_Socket, _Opts, _Method, RawPath, _Version, _Headers]}) ->
     case erlang:get(?SAVE_QS) of
         undefined ->
             {_, QueryString, _} = mochiweb_util:urlsplit_path(RawPath),
@@ -442,12 +467,12 @@
 
 %% @spec get_cookie_value(Key::string, request()) -> string() | undefined
 %% @doc Get the value of the given cookie.
-get_cookie_value(Key, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+get_cookie_value(Key, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     proplists:get_value(Key, parse_cookie(THIS)).
 
 %% @spec parse_cookie(request()) -> [{Key::string(), Value::string()}]
 %% @doc Parse the cookie header.
-parse_cookie({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+parse_cookie({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     case erlang:get(?SAVE_COOKIE) of
         undefined ->
             Cookies = case get_header_value("cookie", THIS) of
@@ -465,7 +490,7 @@
 %% @spec parse_post(request()) -> [{Key::string(), Value::string()}]
 %% @doc Parse an application/x-www-form-urlencoded form POST. This
 %%      has the side-effect of calling recv_body().
-parse_post({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+parse_post({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     case erlang:get(?SAVE_POST) of
         undefined ->
             Parsed = case recv_body(THIS) of
@@ -489,7 +514,7 @@
 %% @doc The function is called for each chunk.
 %%      Used internally by read_chunked_body.
 stream_chunked_body(MaxChunkSize, Fun, FunState,
-                    {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+                    {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     case read_chunk_length(THIS) of
         0 ->
             Fun({0, read_chunk(0, THIS)}, FunState);
@@ -501,13 +526,14 @@
             stream_chunked_body(MaxChunkSize, Fun, NewState, THIS)
     end.
 
-stream_unchunked_body(0, Fun, FunState, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}) ->
+stream_unchunked_body(0, Fun, FunState, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) ->
     Fun({0, <<>>}, FunState);
 stream_unchunked_body(Length, Fun, FunState,
-                      {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) when Length > 0 ->
-    PktSize = case Length > ?RECBUF_SIZE of
+                      {?MODULE, [_Socket, Opts, _Method, _RawPath, _Version, _Headers]}=THIS) when Length > 0 ->
+    RecBuf = mochilists:get_value(recbuf, Opts, ?RECBUF_SIZE),
+    PktSize = case Length > RecBuf of
         true ->
-            ?RECBUF_SIZE;
+            RecBuf;
         false ->
             Length
     end,
@@ -517,7 +543,7 @@
 
 %% @spec read_chunk_length(request()) -> integer()
 %% @doc Read the length of the next HTTP chunk.
-read_chunk_length({?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
+read_chunk_length({?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) ->
     ok = mochiweb_socket:setopts(Socket, [{packet, line}]),
     case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of
         {ok, Header} ->
@@ -534,7 +560,7 @@
 %% @spec read_chunk(integer(), request()) -> Chunk::binary() | [Footer::binary()]
 %% @doc Read in a HTTP chunk of the given length. If Length is 0, then read the
 %%      HTTP footers (as a list of binaries, since they're nominal).
-read_chunk(0, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
+read_chunk(0, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) ->
     ok = mochiweb_socket:setopts(Socket, [{packet, line}]),
     F = fun (F1, Acc) ->
                 case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of
@@ -550,7 +576,7 @@
     ok = mochiweb_socket:setopts(Socket, [{packet, raw}]),
     put(?SAVE_RECV, true),
     Footers;
-read_chunk(Length, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
+read_chunk(Length, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) ->
     case mochiweb_socket:recv(Socket, 2 + Length, ?IDLE_TIMEOUT) of
         {ok, <<Chunk:Length/binary, "\r\n">>} ->
             Chunk;
@@ -559,23 +585,23 @@
     end.
 
 read_sub_chunks(Length, MaxChunkSize, Fun, FunState,
-                {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) when Length > MaxChunkSize ->
+                {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) when Length > MaxChunkSize ->
     Bin = recv(MaxChunkSize, THIS),
     NewState = Fun({size(Bin), Bin}, FunState),
     read_sub_chunks(Length - MaxChunkSize, MaxChunkSize, Fun, NewState, THIS);
 
 read_sub_chunks(Length, _MaxChunkSize, Fun, FunState,
-                {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+                {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     Fun({Length, read_chunk(Length, THIS)}, FunState).
 
 %% @spec serve_file(Path, DocRoot, request()) -> Response
 %% @doc Serve a file relative to DocRoot.
-serve_file(Path, DocRoot, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+serve_file(Path, DocRoot, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     serve_file(Path, DocRoot, [], THIS).
 
 %% @spec serve_file(Path, DocRoot, ExtraHeaders, request()) -> Response
 %% @doc Serve a file relative to DocRoot.
-serve_file(Path, DocRoot, ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+serve_file(Path, DocRoot, ExtraHeaders, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     case mochiweb_util:safe_relative_path(Path) of
         undefined ->
             not_found(ExtraHeaders, THIS);
@@ -595,11 +621,11 @@
 directory_index(FullPath) ->
     filename:join([FullPath, "index.html"]).
 
-maybe_redirect([], FullPath, ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+maybe_redirect([], FullPath, ExtraHeaders, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     maybe_serve_file(directory_index(FullPath), ExtraHeaders, THIS);
 
 maybe_redirect(RelPath, FullPath, ExtraHeaders,
-               {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}=THIS) ->
+               {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, Headers]}=THIS) ->
     case string:right(RelPath, 1) of
         "/" ->
             maybe_serve_file(directory_index(FullPath), ExtraHeaders, THIS);
@@ -620,7 +646,7 @@
             respond({301, MoreHeaders, Body}, THIS)
     end.
 
-maybe_serve_file(File, ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+maybe_serve_file(File, ExtraHeaders, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     case file:read_file_info(File) of
         {ok, FileInfo} ->
             LastModified = httpd_util:rfc1123_date(FileInfo#file_info.mtime),
@@ -719,7 +745,7 @@
 %%         accepted_encodings(["gzip", "deflate", "identity"]) ->
 %%            ["deflate", "gzip", "identity"]
 %%
-accepted_encodings(SupportedEncodings, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+accepted_encodings(SupportedEncodings, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     AcceptEncodingHeader = case get_header_value("Accept-Encoding", THIS) of
         undefined ->
             "";
@@ -757,7 +783,7 @@
 %%      5) For an "Accept" header with value "text/*; q=0.0, */*":
 %%         accepts_content_type("text/plain") -> false
 %%
-accepts_content_type(ContentType1, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+accepts_content_type(ContentType1, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     ContentType = re:replace(ContentType1, "\\s", "", [global, {return, list}]),
     AcceptHeader = accept_header(THIS),
     case mochiweb_util:parse_qvalues(AcceptHeader) of
@@ -806,7 +832,7 @@
 %%         accepts_content_types(["application/json", "text/html"]) ->
 %%             ["text/html", "application/json"]
 %%
-accepted_content_types(Types1, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+accepted_content_types(Types1, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     Types = lists:map(
         fun(T) -> re:replace(T, "\\s", "", [global, {return, list}]) end,
         Types1),
@@ -846,7 +872,7 @@
             [Type || {_Q, Type} <- lists:sort(SortFun, TypesQ)]
     end.
 
-accept_header({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+accept_header({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
     case get_header_value("Accept", THIS) of
         undefined ->
             "*/*";
diff --git a/src/mochiweb_socket.erl b/src/mochiweb_socket.erl
index 76b018c..1e35e15 100644
--- a/src/mochiweb_socket.erl
+++ b/src/mochiweb_socket.erl
@@ -4,15 +4,22 @@
 
 -module(mochiweb_socket).
 
--export([listen/4, accept/1, recv/3, send/2, close/1, port/1, peername/1,
-         setopts/2, type/1]).
+-export([listen/4,
+         accept/1, transport_accept/1, finish_accept/1,
+         recv/3, send/2, close/1, port/1, peername/1,
+         setopts/2, getopts/2, type/1]).
 
 -define(ACCEPT_TIMEOUT, 2000).
+-define(SSL_TIMEOUT, 10000).
+-define(SSL_HANDSHAKE_TIMEOUT, 20000).
+
 
 listen(Ssl, Port, Opts, SslOpts) ->
     case Ssl of
         true ->
-            case ssl:listen(Port, Opts ++ SslOpts) of
+            Opts1 = add_unbroken_ciphers_default(Opts ++ SslOpts),
+            Opts2 = add_safe_protocol_versions(Opts1),
+            case ssl:listen(Port, Opts2) of
                 {ok, ListenSocket} ->
                     {ok, {ssl, ListenSocket}};
                 {error, _} = Err ->
@@ -22,26 +29,74 @@
             gen_tcp:listen(Port, Opts)
     end.
 
-accept({ssl, ListenSocket}) ->
-    % There's a bug in ssl:transport_accept/2 at the moment, which is the
-    % reason for the try...catch block. Should be fixed in OTP R14.
-    try ssl:transport_accept(ListenSocket) of
+add_unbroken_ciphers_default(Opts) ->
+    Default = filter_unsecure_cipher_suites(ssl:cipher_suites()),
+    Ciphers = filter_broken_cipher_suites(proplists:get_value(ciphers, Opts, Default)),
+    [{ciphers, Ciphers} | proplists:delete(ciphers, Opts)].
+
+filter_broken_cipher_suites(Ciphers) ->
+	case proplists:get_value(ssl_app, ssl:versions()) of
+		"5.3" ++ _ ->
+            lists:filter(fun(Suite) ->
+                                 string:left(atom_to_list(element(1, Suite)), 4) =/= "ecdh"
+                         end, Ciphers);
+        _ ->
+            Ciphers
+    end.
+
+filter_unsecure_cipher_suites(Ciphers) ->
+    lists:filter(fun
+                    ({_,des_cbc,_}) -> false;
+                    ({_,_,md5}) -> false;
+                    (_) -> true
+                 end,
+                 Ciphers).
+
+add_safe_protocol_versions(Opts) ->
+    case proplists:is_defined(versions, Opts) of
+        true ->
+            Opts;
+        false ->
+            Versions = filter_unsafe_protcol_versions(proplists:get_value(available, ssl:versions())),
+            [{versions, Versions} | Opts]
+    end.
+
+filter_unsafe_protcol_versions(Versions) ->
+    lists:filter(fun
+                    (sslv3) -> false;
+                    (_) -> true
+                 end,
+                 Versions).
+
+%% Provided for backwards compatibility only
+accept(ListenSocket) ->
+    case transport_accept(ListenSocket) of
         {ok, Socket} ->
-            case ssl:ssl_accept(Socket) of
-                ok ->
-                    {ok, {ssl, Socket}};
-                {error, _} = Err ->
-                    Err
-            end;
+            finish_accept(Socket);
         {error, _} = Err ->
             Err
-    catch
-        error:{badmatch, {error, Reason}} ->
-            {error, Reason}
+    end.
+
+transport_accept({ssl, ListenSocket}) ->
+    case ssl:transport_accept(ListenSocket, ?SSL_TIMEOUT) of
+        {ok, Socket} ->
+            {ok, {ssl, Socket}};
+        {error, _} = Err ->
+            Err
     end;
-accept(ListenSocket) ->
+transport_accept(ListenSocket) ->
     gen_tcp:accept(ListenSocket, ?ACCEPT_TIMEOUT).
 
+finish_accept({ssl, Socket}) ->
+    case ssl:ssl_accept(Socket, ?SSL_HANDSHAKE_TIMEOUT) of
+        ok ->
+            {ok, {ssl, Socket}};
+        {error, _} = Err ->
+            Err
+    end;
+finish_accept(Socket) ->
+    {ok, Socket}.
+
 recv({ssl, Socket}, Length, Timeout) ->
     ssl:recv(Socket, Length, Timeout);
 recv(Socket, Length, Timeout) ->
@@ -77,6 +132,11 @@
 setopts(Socket, Opts) ->
     inet:setopts(Socket, Opts).
 
+getopts({ssl, Socket}, Opts) ->
+    ssl:getopts(Socket, Opts);
+getopts(Socket, Opts) ->
+    inet:getopts(Socket, Opts).
+
 type({ssl, _}) ->
     ssl;
 type(_) ->
diff --git a/src/mochiweb_socket_server.erl b/src/mochiweb_socket_server.erl
index a3d4da3..7f8587e 100644
--- a/src/mochiweb_socket_server.erl
+++ b/src/mochiweb_socket_server.erl
@@ -18,11 +18,11 @@
         {port,
          loop,
          name=undefined,
-         %% NOTE: This is currently ignored.
          max=2048,
          ip=any,
          listen=null,
          nodelay=false,
+         recbuf=?RECBUF_SIZE,
          backlog=128,
          active_sockets=0,
          acceptor_pool_size=16,
@@ -74,7 +74,16 @@
 parse_options(Options) ->
     parse_options(Options, #mochiweb_socket_server{}).
 
-parse_options([], State) ->
+parse_options([], State=#mochiweb_socket_server{acceptor_pool_size=PoolSize,
+                                                max=Max}) ->
+    case Max < PoolSize of
+        true ->
+            error_logger:info_report([{warning, "max is set lower than acceptor_pool_size"},
+                                      {max, Max},
+                                      {acceptor_pool_size, PoolSize}]);
+        false ->
+            ok
+    end,
     State;
 parse_options([{name, L} | Rest], State) when is_list(L) ->
     Name = {local, list_to_atom(L)},
@@ -108,13 +117,13 @@
     parse_options(Rest, State#mochiweb_socket_server{backlog=Backlog});
 parse_options([{nodelay, NoDelay} | Rest], State) ->
     parse_options(Rest, State#mochiweb_socket_server{nodelay=NoDelay});
+parse_options([{recbuf, RecBuf} | Rest], State) when is_integer(RecBuf) ->
+    parse_options(Rest, State#mochiweb_socket_server{recbuf=RecBuf});
 parse_options([{acceptor_pool_size, Max} | Rest], State) ->
     MaxInt = ensure_int(Max),
     parse_options(Rest,
                   State#mochiweb_socket_server{acceptor_pool_size=MaxInt});
 parse_options([{max, Max} | Rest], State) ->
-    error_logger:info_report([{warning, "TODO: max is currently unsupported"},
-                              {max, Max}]),
     MaxInt = ensure_int(Max),
     parse_options(Rest, State#mochiweb_socket_server{max=MaxInt});
 parse_options([{ssl, Ssl} | Rest], State) when is_boolean(Ssl) ->
@@ -156,13 +165,15 @@
             false
     end.
 
-init(State=#mochiweb_socket_server{ip=Ip, port=Port, backlog=Backlog, nodelay=NoDelay}) ->
+init(State=#mochiweb_socket_server{ip=Ip, port=Port, backlog=Backlog,
+                                   nodelay=NoDelay, recbuf=RecBuf}) ->
     process_flag(trap_exit, true),
+
     BaseOpts = [binary,
                 {reuseaddr, true},
                 {packet, 0},
                 {backlog, Backlog},
-                {recbuf, ?RECBUF_SIZE},
+                {recbuf, RecBuf},
                 {exit_on_close, false},
                 {active, false},
                 {nodelay, NoDelay}],
@@ -182,28 +193,45 @@
 new_acceptor_pool(Listen,
                   State=#mochiweb_socket_server{acceptor_pool=Pool,
                                                 acceptor_pool_size=Size,
+                                                recbuf=RecBuf,
                                                 loop=Loop}) ->
+    LoopOpts = [{recbuf, RecBuf}],
     F = fun (_, S) ->
-                Pid = mochiweb_acceptor:start_link(self(), Listen, Loop),
+                Pid = mochiweb_acceptor:start_link(
+                    self(), Listen, Loop, LoopOpts
+                ),
                 sets:add_element(Pid, S)
         end,
     Pool1 = lists:foldl(F, Pool, lists:seq(1, Size)),
     State#mochiweb_socket_server{acceptor_pool=Pool1}.
 
-listen(Port, Opts, State=#mochiweb_socket_server{ssl=Ssl, ssl_opts=SslOpts}) ->
+listen(Port, Opts, State=#mochiweb_socket_server{ssl=Ssl, ssl_opts=SslOpts,
+                                                 recbuf=RecBuf}) ->
     case mochiweb_socket:listen(Ssl, Port, Opts, SslOpts) of
         {ok, Listen} ->
+            %% XXX: `recbuf' value which is passed to `gen_tcp'
+            %% and value reported by `inet:getopts(P, [recbuf])' may
+            %% differ. They depends on underlying OS. From linux mans:
+            %%
+            %% The kernel doubles this value (to allow space for
+            %% bookkeeping overhead) when it is set using setsockopt(2),
+            %% and this doubled value is returned by getsockopt(2).
+            %%
+            %% See: man 7 socket | grep SO_RCVBUF
             {ok, ListenPort} = mochiweb_socket:port(Listen),
             {ok, new_acceptor_pool(
                    Listen,
                    State#mochiweb_socket_server{listen=Listen,
-                                                port=ListenPort})};
+                                                port=ListenPort,
+                                                recbuf=RecBuf})};
         {error, Reason} ->
             {stop, Reason}
     end.
 
 do_get(port, #mochiweb_socket_server{port=Port}) ->
     Port;
+do_get(waiting_acceptors, #mochiweb_socket_server{acceptor_pool=Pool}) ->
+    sets:size(Pool);
 do_get(active_sockets, #mochiweb_socket_server{active_sockets=ActiveSockets}) ->
     ActiveSockets.
 
@@ -271,16 +299,39 @@
 
 recycle_acceptor(Pid, State=#mochiweb_socket_server{
                         acceptor_pool=Pool,
+                        acceptor_pool_size=PoolSize,
                         listen=Listen,
                         loop=Loop,
+                        max=Max,
+                        recbuf=RecBuf,
                         active_sockets=ActiveSockets}) ->
+    LoopOpts = [{recbuf, RecBuf}],
     case sets:is_element(Pid, Pool) of
         true ->
-            Acceptor = mochiweb_acceptor:start_link(self(), Listen, Loop),
-            Pool1 = sets:add_element(Acceptor, sets:del_element(Pid, Pool)),
-            State#mochiweb_socket_server{acceptor_pool=Pool1};
+            Pool1 = sets:del_element(Pid, Pool),
+            case ActiveSockets + sets:size(Pool1) < Max of
+                true ->
+                    Acceptor = mochiweb_acceptor:start_link(
+                        self(), Listen, Loop, LoopOpts
+                    ),
+                    Pool2 = sets:add_element(Acceptor, Pool1),
+                    State#mochiweb_socket_server{acceptor_pool=Pool2};
+                false ->
+                    State#mochiweb_socket_server{acceptor_pool=Pool1}
+            end;
         false ->
-            State#mochiweb_socket_server{active_sockets=ActiveSockets - 1}
+            case sets:size(Pool) < PoolSize of
+                true ->
+                    Acceptor = mochiweb_acceptor:start_link(
+                        self(), Listen, Loop, LoopOpts
+                    ),
+                    Pool1 = sets:add_element(Acceptor, Pool),
+                    State#mochiweb_socket_server{active_sockets=ActiveSockets,
+                                                 acceptor_pool=Pool1};
+                false ->
+                    State#mochiweb_socket_server{active_sockets=ActiveSockets - 1,
+                                                 acceptor_pool=Pool}
+            end
     end.
 
 handle_info(Msg, State) when ?is_old_state(State) ->
diff --git a/src/mochiweb_websocket.erl b/src/mochiweb_websocket.erl
index cc3127e..2768a3e 100644
--- a/src/mochiweb_websocket.erl
+++ b/src/mochiweb_websocket.erl
@@ -27,6 +27,9 @@
 
 -export([loop/5, upgrade_connection/2, request/5]).
 -export([send/3]).
+-ifdef(TEST).
+-compile(export_all).
+-endif.
 
 loop(Socket, Body, State, WsVersion, ReplyChannel) ->
     ok = mochiweb_socket:setopts(Socket, [{packet, 0}, {active, once}]),
@@ -44,7 +47,7 @@
         {tcp_error, _, _} ->
             mochiweb_socket:close(Socket),
             exit(normal);
-        {tcp, _, WsFrames} ->
+        {Proto, _, WsFrames} when Proto =:= tcp orelse Proto =:= ssl ->
             case parse_frames(WsVersion, WsFrames, Socket) of
                 close ->
                     mochiweb_socket:close(Socket),
@@ -214,7 +217,7 @@
         {tcp_error, _, _} ->
             mochiweb_socket:close(Socket),
             exit(normal);
-        {tcp, _, Continuation} ->
+        {Proto, _, Continuation} when Proto =:= tcp orelse Proto =:= ssl ->
             parse_hybi_frames(Socket, <<PartFrame/binary, Continuation/binary>>,
                               Acc);
         _ ->
@@ -276,11 +279,3 @@
   {Buffer, Rest};
 parse_hixie(<<H, T/binary>>, Buffer) ->
   parse_hixie(T, <<Buffer/binary, H>>).
-
-%%
-%% Tests
-%%
--ifdef(TEST).
--include_lib("eunit/include/eunit.hrl").
--compile(export_all).
--endif.
diff --git a/support/templates/mochiwebapp_skel/src/mochiapp_web.erl b/support/templates/mochiwebapp_skel/src/mochiapp_web.erl
index 8976265..5fe455a 100644
--- a/support/templates/mochiwebapp_skel/src/mochiapp_web.erl
+++ b/support/templates/mochiwebapp_skel/src/mochiapp_web.erl
@@ -44,9 +44,8 @@
                       {type, Type}, {what, What},
                       {trace, erlang:get_stacktrace()}],
             error_logger:error_report(Report),
-            %% NOTE: mustache templates need \\ because they are not awesome.
             Req:respond({500, [{"Content-Type", "text/plain"}],
-                         "request failed, sorry\\n"})
+                         "request failed, sorry\n"})
     end.
 
 %% Internal API
diff --git a/support/templates/mochiwebapp_skel/start-dev.sh b/support/templates/mochiwebapp_skel/start-dev.sh
index fb7c45e..65c1692 100755
--- a/support/templates/mochiwebapp_skel/start-dev.sh
+++ b/support/templates/mochiwebapp_skel/start-dev.sh
@@ -1,6 +1,7 @@
 #!/bin/sh
-# NOTE: mustache templates need \\ because they are not awesome.
-exec erl -pa ebin edit deps/*/ebin -boot start_sasl \\
-    -sname {{appid}}_dev \\
-    -s {{appid}} \\
+exec erl \
+    -pa ebin deps/*/ebin \
+    -boot start_sasl \
+    -sname {{appid}}_dev \
+    -s {{appid}} \
     -s reloader
diff --git a/test/mochiweb_socket_server_tests.erl b/test/mochiweb_socket_server_tests.erl
new file mode 100644
index 0000000..c64f5b7
--- /dev/null
+++ b/test/mochiweb_socket_server_tests.erl
@@ -0,0 +1,149 @@
+-module(mochiweb_socket_server_tests).
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+socket_server(Opts, ServerFun) ->
+    ServerOpts = [{ip, "127.0.0.1"}, {port, 0}, {backlog, 5}, {loop, ServerFun}],
+    {ok, Server} = mochiweb_socket_server:start(ServerOpts ++ Opts),
+    Port = mochiweb_socket_server:get(Server, port),
+    {Server, Port}.
+
+echo_loop(Socket) ->
+    ok = mochiweb_socket:setopts(Socket, [{active, once}]),
+    receive
+        {_Protocol, _, Data} ->
+            gen_tcp:send(Socket, Data),
+            echo_loop(Socket);
+        {tcp_closed, Socket} ->
+            ok
+    end.
+
+start_client_conns(Port, NumClients, ClientFun, ClientArgs, Tester) ->
+    Opts = [binary, {active, false}, {packet, 1}],
+    lists:foreach(fun (_N) ->
+                          case gen_tcp:connect("127.0.0.1", Port, Opts) of
+                              {ok, Socket} ->
+                                  spawn_link(fun() -> ClientFun(Socket, ClientArgs) end);
+                              {error, E} ->
+                                  Tester ! {client_conn_error, E}
+                          end
+                  end, lists:seq(1, NumClients)).
+
+client_fun(_Socket, []) -> ok;
+client_fun(Socket, [{close_sock} | Cmds]) ->
+    mochiweb_socket:close(Socket),
+    client_fun(Socket, Cmds);
+client_fun(Socket, [{send_pid, To} | Cmds]) ->
+    To ! {client, self()},
+    client_fun(Socket, Cmds);
+client_fun(Socket, [{send, Data, Tester} | Cmds]) ->
+    case gen_tcp:send(Socket, Data) of
+        ok -> ok;
+        {error, E} -> Tester ! {client_send_error, self(), E}
+    end,
+    client_fun(Socket, Cmds);
+client_fun(Socket, [{recv, Length, Timeout, Tester} | Cmds]) ->
+    case gen_tcp:recv(Socket, Length, Timeout) of
+        {ok, _} -> ok;
+        {error, E} -> Tester ! {client_recv_error, self(), E}
+    end,
+    client_fun(Socket, Cmds);
+client_fun(Socket, [{wait_msg, Msg} | Cmds]) ->
+    receive
+        M when M =:= Msg -> ok
+    end,
+    client_fun(Socket, Cmds);
+client_fun(Socket, [{send_msg, Msg, To} | Cmds]) ->
+    To ! {Msg, self()},
+    client_fun(Socket, Cmds).
+
+test_basic_accept(Max, PoolSize, NumClients, ReportTo) ->
+    Tester = self(),
+
+    ServerOpts = [{max, Max}, {acceptor_pool_size, PoolSize}],
+    ServerLoop =
+        fun (Socket, _Opts) ->
+                Tester ! {server_accepted, self()},
+                mochiweb_socket:setopts(Socket, [{packet, 1}]),
+                echo_loop(Socket)
+        end,
+    {Server, Port} = socket_server(ServerOpts, ServerLoop),
+
+    Data = <<"data">>,
+    Timeout = 2000,
+    ClientCmds = [{send_pid, Tester}, {wait_msg, go},
+                  {send, Data, Tester}, {recv, size(Data), Timeout, Tester},
+                  {close_sock}, {send_msg, done, Tester}],
+    start_client_conns(Port, NumClients, fun client_fun/2, ClientCmds, Tester),
+
+    EventCount = min(NumClients, max(Max, PoolSize)),
+
+    ConnectLoop =
+        fun (Loop, Connected, Accepted, Errors) ->
+                case (length(Accepted) + Errors >= EventCount
+                        andalso length(Connected) + Errors >= NumClients) of
+                    true -> {Connected, Accepted};
+                    false ->
+                        receive
+                            {server_accepted, ServerPid} ->
+                                Loop(Loop, Connected, [ServerPid | Accepted], Errors);
+                            {client, ClientPid} ->
+                                Loop(Loop, [ClientPid | Connected], Accepted, Errors);
+                            {client_conn_error, _E} ->
+                                Loop(Loop, Connected, Accepted, Errors + 1)
+                        end
+                end
+        end,
+    {Connected, Accepted} = ConnectLoop(ConnectLoop, [], [], 0),
+
+    ActiveAfterConnect = mochiweb_socket_server:get(Server, active_sockets),
+    WaitingAfterConnect = mochiweb_socket_server:get(Server, waiting_acceptors),
+
+    lists:foreach(fun(Pid) -> Pid ! go end, Connected),
+    WaitLoop =
+        fun (Loop, Done) ->
+                case (length(Done) >= length(Connected)) of
+                    true ->
+                        ok;
+                    false ->
+                        receive
+                            {done, From} ->
+                                Loop(Loop, [From | Done])
+                        end
+                end
+        end,
+    ok = WaitLoop(WaitLoop, []),
+
+    mochiweb_socket_server:stop(Server),
+
+    ReportTo ! {result, {length(Accepted),
+                         ActiveAfterConnect, WaitingAfterConnect}}.
+
+normal_acceptor_test_fun() ->
+    %        {Max, PoolSize, NumClients,
+    %         {ExpectedAccepts,
+    %          ExpectedActiveAfterConnect, ExpectedWaitingAfterConnect}
+    Tests = [{3, 1, 1,   {1,   1, 1}},
+             {3, 1, 2,   {2,   2, 1}},
+             {3, 1, 3,   {3,   3, 0}},
+             {3, 3, 3,   {3,   3, 0}},
+             {1, 3, 3,   {3,   3, 0}}, % Max is overridden to PoolSize
+             {3, 2, 6,  {3,   3, 0}}
+            ],
+    [fun () ->
+             Self = self(),
+             spawn(fun () ->
+                           test_basic_accept(Max, PoolSize, NumClients, Self)
+                   end),
+             Result = receive {result, R} -> R end,
+             ?assertEqual(Expected, Result)
+     end || {Max, PoolSize, NumClients, Expected} <- Tests].
+
+-define(LARGE_TIMEOUT, 40).
+
+normal_acceptor_test_() ->
+    Tests = normal_acceptor_test_fun(),
+    {timeout, ?LARGE_TIMEOUT, Tests}.
+
+-endif.
diff --git a/test/mochiweb_test_util.erl b/test/mochiweb_test_util.erl
new file mode 100644
index 0000000..2fbf14f
--- /dev/null
+++ b/test/mochiweb_test_util.erl
@@ -0,0 +1,126 @@
+-module(mochiweb_test_util).
+-export([with_server/3, client_request/4, sock_fun/2,
+         read_server_headers/1, drain_reply/3]).
+-include("mochiweb_test_util.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+ssl_cert_opts() ->
+    EbinDir = filename:dirname(code:which(?MODULE)),
+    CertDir = filename:join([EbinDir, "..", "support", "test-materials"]),
+    CertFile = filename:join(CertDir, "test_ssl_cert.pem"),
+    KeyFile = filename:join(CertDir, "test_ssl_key.pem"),
+    [{certfile, CertFile}, {keyfile, KeyFile}].
+
+with_server(Transport, ServerFun, ClientFun) ->
+    ServerOpts0 = [{ip, "127.0.0.1"}, {port, 0}, {loop, ServerFun}],
+    ServerOpts = case Transport of
+        plain ->
+            ServerOpts0;
+        ssl ->
+            ServerOpts0 ++ [{ssl, true}, {ssl_opts, ssl_cert_opts()}]
+    end,
+    {ok, Server} = mochiweb_http:start_link(ServerOpts),
+    Port = mochiweb_socket_server:get(Server, port),
+    Res = (catch ClientFun(Transport, Port)),
+    mochiweb_http:stop(Server),
+    Res.
+
+sock_fun(Transport, Port) ->
+    Opts = [binary, {active, false}, {packet, http}],
+    case Transport of
+        plain ->
+            {ok, Socket} = gen_tcp:connect("127.0.0.1", Port, Opts),
+            fun (recv) ->
+                    gen_tcp:recv(Socket, 0);
+                ({recv, Length}) ->
+                    gen_tcp:recv(Socket, Length);
+                ({send, Data}) ->
+                    gen_tcp:send(Socket, Data);
+                ({setopts, L}) ->
+                    inet:setopts(Socket, L);
+                (get) ->
+                    Socket
+            end;
+        ssl ->
+            {ok, Socket} = ssl:connect("127.0.0.1", Port, [{ssl_imp, new} | Opts]),
+            fun (recv) ->
+                    ssl:recv(Socket, 0);
+                ({recv, Length}) ->
+                    ssl:recv(Socket, Length);
+                ({send, Data}) ->
+                    ssl:send(Socket, Data);
+                ({setopts, L}) ->
+                    ssl:setopts(Socket, L);
+                (get) ->
+                    {ssl, Socket}
+            end
+    end.
+
+client_request(Transport, Port, Method, TestReqs) ->
+    client_request(sock_fun(Transport, Port), Method, TestReqs).
+
+client_request(SockFun, _Method, []) ->
+    {the_end, {error, closed}} = {the_end, SockFun(recv)},
+    ok;
+client_request(SockFun, Method,
+               [#treq{path=Path, body=Body, xreply=ExReply} | Rest]) ->
+    Request = [atom_to_list(Method), " ", Path, " HTTP/1.1\r\n",
+               client_headers(Body, Rest =:= []),
+               "\r\n",
+               Body],
+    ok = SockFun({setopts, [{packet, http}]}),
+    ok = SockFun({send, Request}),
+    case Method of
+        'GET' ->
+            {ok, {http_response, {1,1}, 200, "OK"}} = SockFun(recv);
+        'POST' ->
+            {ok, {http_response, {1,1}, 201, "Created"}} = SockFun(recv);
+        'CONNECT' ->
+            {ok, {http_response, {1,1}, 200, "OK"}} = SockFun(recv)
+    end,
+    Headers = read_server_headers(SockFun),
+    ?assertMatch("MochiWeb" ++ _, mochiweb_headers:get_value("Server", Headers)),
+    ?assert(mochiweb_headers:get_value("Date", Headers) =/= undefined),
+    ?assert(mochiweb_headers:get_value("Content-Type", Headers) =/= undefined),
+    ContentLength = list_to_integer(mochiweb_headers:get_value("Content-Length", Headers)),
+    {payload, ExReply} = {payload, drain_reply(SockFun, ContentLength, <<>>)},
+    client_request(SockFun, Method, Rest).
+
+read_server_headers(SockFun) ->
+    ok = SockFun({setopts, [{packet, httph}]}),
+    Headers = read_server_headers(SockFun, mochiweb_headers:empty()),
+    ok = SockFun({setopts, [{packet, raw}]}),
+    Headers.
+
+read_server_headers(SockFun, Headers) ->
+    case SockFun(recv) of
+        {ok, http_eoh} ->
+            Headers;
+        {ok, {http_header, _, Header, _, Value}} ->
+            read_server_headers(
+              SockFun,
+              mochiweb_headers:insert(Header, Value, Headers))
+    end.
+
+client_headers(Body, IsLastRequest) ->
+    ["Host: localhost\r\n",
+     case Body of
+        <<>> ->
+            "";
+        _ ->
+            ["Content-Type: application/octet-stream\r\n",
+             "Content-Length: ", integer_to_list(byte_size(Body)), "\r\n"]
+     end,
+     case IsLastRequest of
+         true ->
+             "Connection: close\r\n";
+         false ->
+             ""
+     end].
+
+drain_reply(_SockFun, 0, Acc) ->
+    Acc;
+drain_reply(SockFun, Length, Acc) ->
+    Sz = erlang:min(Length, 1024),
+    {ok, B} = SockFun({recv, Sz}),
+    drain_reply(SockFun, Length - Sz, <<Acc/bytes, B/bytes>>).
diff --git a/test/mochiweb_test_util.hrl b/test/mochiweb_test_util.hrl
new file mode 100644
index 0000000..99fdc4e
--- /dev/null
+++ b/test/mochiweb_test_util.hrl
@@ -0,0 +1 @@
+-record(treq, {path, body= <<>>, xreply= <<>>}).
diff --git a/test/mochiweb_tests.erl b/test/mochiweb_tests.erl
index c8bc8ac..209971b 100644
--- a/test/mochiweb_tests.erl
+++ b/test/mochiweb_tests.erl
@@ -1,28 +1,9 @@
 -module(mochiweb_tests).
 -include_lib("eunit/include/eunit.hrl").
-
--record(treq, {path, body= <<>>, xreply= <<>>}).
-
-ssl_cert_opts() ->
-    EbinDir = filename:dirname(code:which(?MODULE)),
-    CertDir = filename:join([EbinDir, "..", "support", "test-materials"]),
-    CertFile = filename:join(CertDir, "test_ssl_cert.pem"),
-    KeyFile = filename:join(CertDir, "test_ssl_key.pem"),
-    [{certfile, CertFile}, {keyfile, KeyFile}].
+-include("mochiweb_test_util.hrl").
 
 with_server(Transport, ServerFun, ClientFun) ->
-    ServerOpts0 = [{ip, "127.0.0.1"}, {port, 0}, {loop, ServerFun}],
-    ServerOpts = case Transport of
-        plain ->
-            ServerOpts0;
-        ssl ->
-            ServerOpts0 ++ [{ssl, true}, {ssl_opts, ssl_cert_opts()}]
-    end,
-    {ok, Server} = mochiweb_http:start_link(ServerOpts),
-    Port = mochiweb_socket_server:get(Server, port),
-    Res = (catch ClientFun(Transport, Port)),
-    mochiweb_http:stop(Server),
-    Res.
+    mochiweb_test_util:with_server(Transport, ServerFun, ClientFun).
 
 request_test() ->
     R = mochiweb_request:new(z, z, "/foo/bar/baz%20wibble+quux?qs=2", z, []),
@@ -148,6 +129,7 @@
     ClientFun = new_client_fun('GET', TestReqs),
     ok = with_server(Transport, ServerFun, ClientFun),
     ok.
+
 do_POST(Transport, Size, Times) ->
     ServerFun = fun (Req) ->
                         Body = Req:recv_body(),
@@ -165,86 +147,57 @@
 
 new_client_fun(Method, TestReqs) ->
     fun (Transport, Port) ->
-            client_request(Transport, Port, Method, TestReqs)
+            mochiweb_test_util:client_request(Transport, Port, Method, TestReqs)
     end.
 
-client_request(Transport, Port, Method, TestReqs) ->
-    Opts = [binary, {active, false}, {packet, http}],
-    SockFun = case Transport of
-        plain ->
-            {ok, Socket} = gen_tcp:connect("127.0.0.1", Port, Opts),
-            fun (recv) ->
-                    gen_tcp:recv(Socket, 0);
-                ({recv, Length}) ->
-                    gen_tcp:recv(Socket, Length);
-                ({send, Data}) ->
-                    gen_tcp:send(Socket, Data);
-                ({setopts, L}) ->
-                    inet:setopts(Socket, L)
-            end;
-        ssl ->
-            {ok, Socket} = ssl:connect("127.0.0.1", Port, [{ssl_imp, new} | Opts]),
-            fun (recv) ->
-                    ssl:recv(Socket, 0);
-                ({recv, Length}) ->
-                    ssl:recv(Socket, Length);
-                ({send, Data}) ->
-                    ssl:send(Socket, Data);
-                ({setopts, L}) ->
-                    ssl:setopts(Socket, L)
-            end
-    end,
-    client_request(SockFun, Method, TestReqs).
+close_on_unread_data_test() ->
+    ok = with_server(
+           plain,
+           fun mochiweb_request:not_found/1,
+           fun close_on_unread_data_client/2).
 
-client_request(SockFun, _Method, []) ->
-    {the_end, {error, closed}} = {the_end, SockFun(recv)},
-    ok;
-client_request(SockFun, Method,
-               [#treq{path=Path, body=Body, xreply=ExReply} | Rest]) ->
-    Request = [atom_to_list(Method), " ", Path, " HTTP/1.1\r\n",
-               client_headers(Body, Rest =:= []),
-               "\r\n",
-               Body],
-    ok = SockFun({send, Request}),
-    case Method of
-        'GET' ->
-            {ok, {http_response, {1,1}, 200, "OK"}} = SockFun(recv);
-        'POST' ->
-            {ok, {http_response, {1,1}, 201, "Created"}} = SockFun(recv);
-        'CONNECT' ->
-            {ok, {http_response, {1,1}, 200, "OK"}} = SockFun(recv)
-    end,
-    ok = SockFun({setopts, [{packet, httph}]}),
-    {ok, {http_header, _, 'Server', _, "MochiWeb" ++ _}} = SockFun(recv),
-    {ok, {http_header, _, 'Date', _, _}} = SockFun(recv),
-    {ok, {http_header, _, 'Content-Type', _, _}} = SockFun(recv),
-    {ok, {http_header, _, 'Content-Length', _, ConLenStr}} = SockFun(recv),
-    ContentLength = list_to_integer(ConLenStr),
-    {ok, http_eoh} = SockFun(recv),
-    ok = SockFun({setopts, [{packet, raw}]}),
-    {payload, ExReply} = {payload, drain_reply(SockFun, ContentLength, <<>>)},
+close_on_unread_data_client(Transport, Port) ->
+    SockFun = mochiweb_test_util:sock_fun(Transport, Port),
+    %% A normal GET request should not trigger this behavior
+    Request0 = string:join(
+                 ["GET / HTTP/1.1",
+                  "Host: localhost",
+                  "",
+                  ""],
+                 "\r\n"),
     ok = SockFun({setopts, [{packet, http}]}),
-    client_request(SockFun, Method, Rest).
-
-client_headers(Body, IsLastRequest) ->
-    ["Host: localhost\r\n",
-     case Body of
-        <<>> ->
-            "";
-        _ ->
-            ["Content-Type: application/octet-stream\r\n",
-             "Content-Length: ", integer_to_list(byte_size(Body)), "\r\n"]
-     end,
-     case IsLastRequest of
-         true ->
-             "Connection: close\r\n";
-         false ->
-             ""
-     end].
-
-drain_reply(_SockFun, 0, Acc) ->
-    Acc;
-drain_reply(SockFun, Length, Acc) ->
-    Sz = erlang:min(Length, 1024),
-    {ok, B} = SockFun({recv, Sz}),
-    drain_reply(SockFun, Length - Sz, <<Acc/bytes, B/bytes>>).
+    ok = SockFun({send, Request0}),
+    ?assertMatch(
+       {ok, {http_response, {1, 1}, 404, _}},
+       SockFun(recv)),
+    Headers0 = mochiweb_test_util:read_server_headers(SockFun),
+    ?assertEqual(
+       undefined,
+       mochiweb_headers:get_value("Connection", Headers0)),
+    Len0 = list_to_integer(
+             mochiweb_headers:get_value("Content-Length", Headers0)),
+    _Body0 = mochiweb_test_util:drain_reply(SockFun, Len0, <<>>),
+    %% Re-use same socket
+    Request = string:join(
+                ["POST / HTTP/1.1",
+                 "Host: localhost",
+                 "Content-Type: application/json",
+                 "Content-Length: 2",
+                 "",
+                 "{}"],
+                "\r\n"),
+    ok = SockFun({setopts, [{packet, http}]}),
+    ok = SockFun({send, Request}),
+    ?assertMatch(
+       {ok, {http_response, {1, 1}, 404, _}},
+       SockFun(recv)),
+    Headers = mochiweb_test_util:read_server_headers(SockFun),
+    %% Expect to see a Connection: close header when we know the
+    %% server will close the connection re #146
+    ?assertEqual(
+       "close",
+       mochiweb_headers:get_value("Connection", Headers)),
+    Len = list_to_integer(mochiweb_headers:get_value("Content-Length", Headers)),
+    _Body = mochiweb_test_util:drain_reply(SockFun, Len, <<>>),
+    ?assertEqual({error, closed}, SockFun(recv)),
+    ok.
diff --git a/test/mochiweb_websocket_tests.erl b/test/mochiweb_websocket_tests.erl
index 890aa17..eb8de5b 100644
--- a/test/mochiweb_websocket_tests.erl
+++ b/test/mochiweb_websocket_tests.erl
@@ -82,3 +82,79 @@
        mochiweb_websocket:parse_hixie_frames(
          <<0,102,111,111,255,0,98,97,114,255>>,
          [])).
+
+end_to_end_test_factory(ServerTransport) ->
+    mochiweb_test_util:with_server(
+      ServerTransport,
+      fun end_to_end_server/1,
+      fun (Transport, Port) ->
+              end_to_end_client(mochiweb_test_util:sock_fun(Transport, Port))
+      end).
+
+end_to_end_server(Req) ->
+    ?assertEqual("Upgrade", Req:get_header_value("connection")),
+    ?assertEqual("websocket", Req:get_header_value("upgrade")),
+    {ReentryWs, _ReplyChannel} = mochiweb_websocket:upgrade_connection(
+                                   Req,
+                                   fun end_to_end_ws_loop/3),
+    ReentryWs(ok).
+
+end_to_end_ws_loop(Payload, State, ReplyChannel) ->
+    %% Echo server
+    lists:foreach(ReplyChannel, Payload),
+    State.
+
+end_to_end_client(S) ->
+    %% Key and Accept per https://tools.ietf.org/html/rfc6455
+    UpgradeReq = string:join(
+                   ["GET / HTTP/1.1",
+                    "Host: localhost",
+                    "Upgrade: websocket",
+                    "Connection: Upgrade",
+                    "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==",
+                    "",
+                    ""], "\r\n"),
+    ok = S({send, UpgradeReq}),
+    {ok, {http_response, {1,1}, 101, _}} = S(recv),
+    read_expected_headers(
+      S,
+      [{'Upgrade', "websocket"},
+       {'Connection', "Upgrade"},
+       {'Content-Length', "0"},
+       {"Sec-Websocket-Accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="}]),
+    %% The first message sent over telegraph :)
+    SmallMessage = <<"What hath God wrought?">>,
+    ok = S({send,
+       << 1:1, %% Fin
+          0:1, %% Rsv1
+          0:1, %% Rsv2
+          0:1, %% Rsv3
+          2:4, %% Opcode, 1 = text frame
+          1:1, %% Mask on
+          (byte_size(SmallMessage)):7, %% Length, <125 case
+          0:32, %% Mask (trivial)
+          SmallMessage/binary >>}),
+    {ok, WsFrames} = S(recv),
+    << 1:1, %% Fin
+       0:1, %% Rsv1
+       0:1, %% Rsv2
+       0:1, %% Rsv3
+       1:4, %% Opcode, text frame (all mochiweb suports for now)
+       MsgSize:8, %% Expecting small size
+       SmallMessage/binary >> = WsFrames,
+    ?assertEqual(MsgSize, byte_size(SmallMessage)),
+    ok.
+
+read_expected_headers(S, D) ->
+    Headers = mochiweb_test_util:read_server_headers(S),
+    lists:foreach(
+      fun ({K, V}) ->
+              ?assertEqual(V, mochiweb_headers:get_value(K, Headers))
+      end,
+      D).
+
+end_to_end_http_test() ->
+    end_to_end_test_factory(plain).
+
+end_to_end_https_test() ->
+    end_to_end_test_factory(ssl).