changed CHANGELOG.md
 
@@ -1,3 +1,13 @@
1
+ # v0.5.0
2
+
3
+ * Enhancements
4
+ * Update to Elixir v0.14.0
5
+ * Update Cowboy adapter to v0.10.0
6
+ * Add `Plug.Conn.read_body/2`
7
+
8
+ * Backwards incompatible changes
9
+ * `Plug.Parsers` now expect `:length` instead of `:limit` and also accept `:read_length` and `:read_timeout`
10
+
1
11
# v0.4.4
2
12
3
13
* Enhancements
 
@@ -37,4 +47,4 @@
37
47
38
48
# v0.3.0
39
49
40
- * Definition of the Plug specification
\ No newline at end of file
50
+ * Definition of the Plug specification
changed hex_metadata.config
 
@@ -22,4 +22,4 @@
22
22
{<<"licenses">>,[<<"Apache 2">>]}.
23
23
{<<"links">>,#{<<"Github">> => <<"https://github.com/elixir-lang/plug">>}}.
24
24
{<<"requirements">>,#{}}.
25
- {<<"version">>,<<"0.4.4">>}.
25
+ {<<"version">>,<<"0.5.0">>}.
changed lib/plug/adapters/cowboy/conn.ex
 
@@ -2,15 +2,15 @@ defmodule Plug.Adapters.Cowboy.Conn do
2
2
@behaviour Plug.Conn.Adapter
3
3
@moduledoc false
4
4
5
- alias :cowboy_req, as: R
5
+ alias :cowboy_req, as: Request
6
6
7
7
def conn(req, transport) do
8
- {path, req} = R.path req
9
- {host, req} = R.host req
10
- {port, req} = R.port req
11
- {meth, req} = R.method req
12
- {hdrs, req} = R.headers req
13
- {qs, req} = R.qs req
8
+ {path, req} = Request.path req
9
+ {host, req} = Request.host req
10
+ {port, req} = Request.port req
11
+ {meth, req} = Request.method req
12
+ {hdrs, req} = Request.headers req
13
+ {qs, req} = Request.qs req
14
14
15
15
%Plug.Conn{
16
16
adapter: {__MODULE__, req},
 
@@ -25,7 +25,7 @@ defmodule Plug.Adapters.Cowboy.Conn do
25
25
end
26
26
27
27
def send_resp(req, status, headers, body) do
28
- {:ok, req} = R.reply(status, headers, body, req)
28
+ {:ok, req} = Request.reply(status, headers, body, req)
29
29
{:ok, nil, req}
30
30
end
31
31
 
@@ -33,31 +33,33 @@ defmodule Plug.Adapters.Cowboy.Conn do
33
33
%File.Stat{type: :regular, size: size} = File.stat!(path)
34
34
body_fun = fn(socket, transport) -> transport.sendfile(socket, path) end
35
35
36
- {:ok, req} = R.reply(status, headers, R.set_resp_body_fun(size, body_fun, req))
36
+ {:ok, req} = Request.reply(status, headers, Request.set_resp_body_fun(size, body_fun, req))
37
37
{:ok, nil, req}
38
38
end
39
39
40
40
def send_chunked(req, status, headers) do
41
- {:ok, req} = R.chunked_reply(status, headers, req)
41
+ {:ok, req} = Request.chunked_reply(status, headers, req)
42
42
{:ok, nil, req}
43
43
end
44
44
45
45
def chunk(req, body) do
46
- R.chunk(body, req)
46
+ Request.chunk(body, req)
47
47
end
48
48
49
- def stream_req_body(req, limit) do
50
- R.stream_body(limit, req)
49
+ def read_req_body(req, opts \\ []) do
50
+ Request.body(req, opts)
51
51
end
52
52
53
- def parse_req_multipart(req, limit, callback) do
54
- {:ok, limit, acc, req} = parse_multipart(R.multipart_data(req), limit, %{}, callback)
53
+ def parse_req_multipart(req, opts, callback) do
54
+ limit = Keyword.get(opts, :length, 8_000_000)
55
+ {:ok, limit, acc, req} = parse_multipart(Request.part(req), limit, opts, %{}, callback)
56
+
57
+ params = Enum.reduce(acc, %{}, &Plug.Conn.Query.decode_pair/2)
55
58
56
59
if limit > 0 do
57
- params = Enum.reduce(acc, %{}, &Plug.Conn.Query.decode_pair/2)
58
60
{:ok, params, req}
59
61
else
60
- {:too_large, req}
62
+ {:more, params, req}
61
63
end
62
64
end
63
65
 
@@ -73,53 +75,63 @@ defmodule Plug.Adapters.Cowboy.Conn do
73
75
74
76
## Multipart
75
77
76
- defp parse_multipart({:headers, headers, req}, limit, acc, callback) when limit >= 0 do
78
+ defp parse_multipart({:ok, headers, req}, limit, opts, acc, callback) when limit >= 0 do
77
79
case callback.(headers) do
78
80
{:binary, name} ->
79
- {:ok, limit, body, req} = parse_multipart_body(R.multipart_data(req), limit, "")
80
- parse_multipart(R.multipart_data(req), limit, Map.put(acc, name, body), callback)
81
+ {:ok, limit, body, req} = parse_multipart_body(Request.part_body(req, opts), limit, opts, "")
82
+ parse_multipart(Request.part(req), limit, opts, Map.put(acc, name, body), callback)
81
83
82
84
{:file, name, file, %Plug.Upload{} = uploaded} ->
83
- {:ok, limit, req} = parse_multipart_file(R.multipart_data(req), limit, file)
84
- parse_multipart(R.multipart_data(req), limit, Map.put(acc, name, uploaded), callback)
85
+ {:ok, limit, req} = parse_multipart_file(Request.part_body(req, opts), limit, opts, file)
86
+ parse_multipart(Request.part(req), limit, opts, Map.put(acc, name, uploaded), callback)
85
87
86
88
:skip ->
87
- {:ok, req} = R.multipart_skip(req)
88
- parse_multipart(R.multipart_data(req), limit, acc, callback)
89
+ {:ok, req} = Request.multipart_skip(req)
90
+ parse_multipart(Request.part(req), limit, opts, acc, callback)
89
91
end
90
92
end
91
93
92
- defp parse_multipart({:headers, _headers, req}, limit, acc, _callback) do
94
+ defp parse_multipart({:ok, _headers, req}, limit, _opts, acc, _callback) do
93
95
{:ok, limit, acc, req}
94
96
end
95
97
96
- defp parse_multipart({:eof, req}, limit, acc, _callback) do
98
+ defp parse_multipart({:done, req}, limit, _opts, acc, _callback) do
97
99
{:ok, limit, acc, req}
98
100
end
99
101
100
- defp parse_multipart_body({:body, tail, req}, limit, body) when limit >= 0 do
101
- parse_multipart_body(R.multipart_data(req), limit - byte_size(tail), body <> tail)
102
+ defp parse_multipart_body({:more, tail, req}, limit, opts, body) when limit >= 0 do
103
+ parse_multipart_body(Request.part_body(req), limit - byte_size(tail), opts, body <> tail)
102
104
end
103
105
104
- defp parse_multipart_body({:body, _tail, req}, limit, body) do
106
+ defp parse_multipart_body({:more, _tail, req}, limit, _opts, body) do
105
107
{:ok, limit, body, req}
106
108
end
107
109
108
- defp parse_multipart_body({:end_of_part, req}, limit, body) do
110
+ defp parse_multipart_body({:ok, tail, req}, limit, _opts, body) when limit >= byte_size(tail) do
111
+ {:ok, limit, body <> tail, req}
112
+ end
113
+
114
+ defp parse_multipart_body({:ok, _tail, req}, limit, _opts, body) do
109
115
{:ok, limit, body, req}
110
116
end
111
117
112
- defp parse_multipart_file({:body, tail, req}, limit, file) when limit >= 0 do
118
+ defp parse_multipart_file({:more, tail, req}, limit, opts, file) when limit >= 0 do
113
119
:file.write(file, tail)
114
- parse_multipart_file(R.multipart_data(req), limit - byte_size(tail), file)
120
+ parse_multipart_file(Request.part_body(req, opts), limit - byte_size(tail), opts, file)
115
121
end
116
122
117
- defp parse_multipart_file({:body, _tail, req}, limit, file) do
123
+ defp parse_multipart_file({:more, _tail, req}, limit, _opts, file) do
118
124
:file.close(file)
119
125
{:ok, limit, req}
120
126
end
121
127
122
- defp parse_multipart_file({:end_of_part, req}, limit, file) do
128
+ defp parse_multipart_file({:ok, tail, req}, limit, _opts, file) when limit >= byte_size(tail) do
129
+ :file.write(file, tail)
130
+ :file.close(file)
131
+ {:ok, limit, req}
132
+ end
133
+
134
+ defp parse_multipart_file({:ok, _tail, req}, limit, _opts, file) do
123
135
:file.close(file)
124
136
{:ok, limit, req}
125
137
end
changed lib/plug/adapters/test/conn.ex
 
@@ -44,13 +44,16 @@ defmodule Plug.Adapters.Test.Conn do
44
44
{:ok, body, %{state | chunks: body}}
45
45
end
46
46
47
- def stream_req_body(%{req_body: body} = state, _limit) when byte_size(body) == 0,
48
- do: {:done, state}
49
- def stream_req_body(%{req_body: body} = state, limit) do
50
- size = min(byte_size(body), limit)
47
+ def read_req_body(%{req_body: body} = state, opts \\ []) do
48
+ size = min(byte_size(body), Keyword.get(opts, :length, 8_000_000))
51
49
data = :binary.part(body, 0, size)
52
50
rest = :binary.part(body, size, byte_size(body) - size)
53
- {:ok, data, %{state | req_body: rest}}
51
+ tag =
52
+ case rest do
53
+ "" -> :ok
54
+ _ -> :more
55
+ end
56
+ {tag, data, %{state | req_body: rest}}
54
57
end
55
58
56
59
def parse_req_multipart(%{params: multipart} = state, _limit, _callback) do
changed lib/plug/conn.ex
 
@@ -352,6 +352,48 @@ defmodule Plug.Conn do
352
352
conn
353
353
end
354
354
355
+ @doc """
356
+ Reads the request body.
357
+
358
+ This function reads a chunk of the request body. If there is more data to be
359
+ read, then `{:more, partial_body, conn}` is returned. Otherwise
360
+ `{:ok, body, conn}` is returned. In case of error reading the socket,
361
+ `{:error, reason}` is returned as per `:gen_tcp.recv/2`.
362
+
363
+ Because the request body can be of any size, reading the body will only
364
+ work once, as Plug will not cache the result of these operations. If you
365
+ need to access the body multiple times, it is your responsibility to store
366
+ it. Finally keep in mind some plugs like `Plug.Parsers` may read the body,
367
+ so the body may be unavailable after accessing such plugs.
368
+
369
+ This function is able to handle both chunked and identity transfer-encoding
370
+ by default.
371
+
372
+ ## Options
373
+
374
+ * `:length` - sets the max body length to read, defaults to 8,000,000 bytes;
375
+ * `:read_length` - set the amount of bytes to read at one time, defaults to 1,000,000 bytes;
376
+ * `:read_timeout` - set the timeout for each chunk received, defaults to 15ms;
377
+
378
+ ## Example
379
+
380
+ {:ok, body, conn} = Plug.Conn.read_body(conn, length: 1_000_000)
381
+
382
+ """
383
+ @spec read_body(t, Keyword.t) :: {:ok, binary, t} |
384
+ {:more, binary, t} |
385
+ {:error, binary}
386
+ def read_body(%Conn{adapter: {adapter, state}} = conn, opts \\ []) do
387
+ case adapter.read_req_body(state, opts) do
388
+ {:ok, data, state} ->
389
+ {:ok, data, %{conn | adapter: {adapter, state}}}
390
+ {:more, data, state} ->
391
+ {:more, data, %{conn | adapter: {adapter, state}}}
392
+ {:error, reason} ->
393
+ {:error, reason}
394
+ end
395
+ end
396
+
355
397
@doc """
356
398
Fetches cookies from the request headers.
357
399
"""
changed lib/plug/conn/adapter.ex
 
@@ -71,13 +71,15 @@ defmodule Plug.Conn.Adapter do
71
71
:ok | {:ok, sent_body :: binary, payload} | {:error, term}
72
72
73
73
@doc """
74
- Streams the request body.
74
+ Reads the request body.
75
75
76
- An approximate limit of data to be read from the socket per stream
77
- can be passed as argument.
76
+ Read the docs in `Plug.Conn.read_body/2` for the supported
77
+ options and expected behaviour.
78
78
"""
79
- defcallback stream_req_body(payload, limit :: pos_integer) ::
80
- {:ok, data :: binary, payload} | {:done, payload}
79
+ defcallback read_req_body(payload, options :: Keyword.t) ::
80
+ {:ok, data :: binary, payload} |
81
+ {:more, data :: binary, payload} |
82
+ {:error, term}
81
83
82
84
@doc """
83
85
Parses a multipart request.
 
@@ -93,11 +95,11 @@ defmodule Plug.Conn.Adapter do
93
95
and contents should be written to the given `file`
94
96
* `:skip` - this multipart segment should be skipped
95
97
96
- This function can respond with one of the three following values:
98
+ This function may return a `:ok` or `:more` tuple. The first one is
99
+ returned when there is no more multipart data to be processed.
97
100
98
- * `{:ok, params, payload}` - the parameters are already processed as defined per `Conn.params`
99
- * `{:too_large, payload} - the request body goes over the given limit
101
+ For the supported options, please read `Plug.Conn.read_body/2` docs.
100
102
"""
101
- defcallback parse_req_multipart(payload, limit :: pos_integer, fun) ::
102
- {:ok, Conn.params, payload} | {:too_large, payload}
103
+ defcallback parse_req_multipart(payload, options :: Keyword.t, fun) ::
104
+ {:ok, Conn.params, payload} | {:more, Conn.params, payload}
103
105
end
changed lib/plug/conn/unfetched.ex
 
@@ -5,14 +5,18 @@ defmodule Plug.Conn.Unfetched do
5
5
defstruct [:aspect]
6
6
7
7
defimpl Access do
8
- def get(%Plug.Conn.Unfetched{aspect: aspect}, key) do
8
+ def get(unfetched, key) do
9
+ raise_no_access(unfetched, key)
10
+ end
11
+
12
+ def get_and_update(unfetched, key, _value) do
13
+ raise_no_access(unfetched, key)
14
+ end
15
+
16
+ defp raise_no_access(%Plug.Conn.Unfetched{aspect: aspect}, key) do
9
17
raise ArgumentError, message:
10
18
"trying to access key #{inspect key} but they were not yet fetched. " <>
11
19
"Please call Plug.Conn.fetch_#{aspect} before accessing it"
12
20
end
13
-
14
- def access(unfetched, key) do
15
- get(unfetched, key)
16
- end
17
21
end
18
22
end
changed lib/plug/parsers.ex
 
@@ -1,6 +1,6 @@
1
1
defmodule Plug.Parsers do
2
2
message = "the request is too large. If you are willing to process " <>
3
- "larger requests, please give a :limit to Plug.Parsers"
3
+ "larger requests, please give a :length to Plug.Parsers"
4
4
5
5
defmodule RequestTooLargeError do
6
6
@moduledoc """
 
@@ -10,9 +10,7 @@ defmodule Plug.Parsers do
10
10
defexception [:message]
11
11
12
12
defimpl Plug.Exception do
13
- def status(_exception) do
14
- 413
15
- end
13
+ def status(_exception), do: 413
16
14
end
17
15
end
18
16
 
@@ -24,9 +22,7 @@ defmodule Plug.Parsers do
24
22
defexception [:message]
25
23
26
24
defimpl Plug.Exception do
27
- def status(_exception) do
28
- 415
29
- end
25
+ def status(_exception), do: 415
30
26
end
31
27
end
32
28
 
@@ -39,8 +35,8 @@ defmodule Plug.Parsers do
39
35
These modules need to implement the behaviour
40
36
outlined in this module.
41
37
42
- * `:limit` - the request size limit we accept to parse.
43
- Defaults to 8,000,000 bytes.
38
+ All options supported by `Plug.Conn.read_body/2` are also
39
+ supported here.
44
40
45
41
## Examples
46
42
 
@@ -83,7 +79,7 @@ defmodule Plug.Parsers do
83
79
defcallback parse(Conn.t, type :: binary, subtype :: binary,
84
80
headers :: Keyword.t, opts :: Keyword.t) ::
85
81
{:ok, Conn.params, Conn.t} |
86
- {:too_large, Conn.t} |
82
+ {:error, :too_large, Conn.t} |
87
83
{:skip, Conn.t}
88
84
89
85
@behaviour Plug
 
@@ -92,7 +88,7 @@ defmodule Plug.Parsers do
92
88
parsers = Keyword.get(opts, :parsers) || raise_missing_parsers
93
89
opts
94
90
|> Keyword.put(:parsers, convert_parsers(parsers))
95
- |> Keyword.put_new(:limit, 8_000_000)
91
+ |> Keyword.put_new(:length, 8_000_000)
96
92
end
97
93
98
94
defp raise_missing_parsers do
 
@@ -129,7 +125,7 @@ defmodule Plug.Parsers do
129
125
%{conn | params: Map.merge(get, post)}
130
126
{:next, conn} ->
131
127
reduce(conn, t, type, subtype, headers, opts)
132
- {:too_large, _conn} ->
128
+ {:error, :too_large, _conn} ->
133
129
raise Plug.Parsers.RequestTooLargeError
134
130
end
135
131
end
changed lib/plug/parsers/multipart.ex
 
@@ -4,13 +4,12 @@ defmodule Plug.Parsers.MULTIPART do
4
4
5
5
def parse(%Conn{} = conn, "multipart", subtype, _headers, opts) when subtype in ["form-data", "mixed"] do
6
6
{adapter, state} = conn.adapter
7
- limit = Keyword.fetch!(opts, :limit)
8
7
9
- case adapter.parse_req_multipart(state, limit, &handle_headers/1) do
8
+ case adapter.parse_req_multipart(state, opts, &handle_headers/1) do
10
9
{:ok, params, state} ->
11
10
{:ok, params, %{conn | adapter: {adapter, state}}}
12
- {:too_large, state} ->
13
- {:too_large, %{conn | adapter: {adapter, state}}}
11
+ {:more, _params, state} ->
12
+ {:error, :too_large, %{conn | adapter: {adapter, state}}}
14
13
end
15
14
end
changed lib/plug/parsers/urlencoded.ex
 
@@ -3,30 +3,15 @@ defmodule Plug.Parsers.URLENCODED do
3
3
alias Plug.Conn
4
4
5
5
def parse(%Conn{} = conn, "application", "x-www-form-urlencoded", _headers, opts) do
6
- read_body(conn, Keyword.fetch!(opts, :limit))
6
+ case Conn.read_body(conn, opts) do
7
+ {:ok, body, conn} ->
8
+ {:ok, Plug.Conn.Query.decode(body), conn}
9
+ {:more, _data, conn} ->
10
+ {:error, :too_large, conn}
11
+ end
7
12
end
8
13
9
14
def parse(conn, _type, _subtype, _headers, _opts) do
10
15
{:next, conn}
11
16
end
12
-
13
- defp read_body(%Conn{adapter: {adapter, state}} = conn, limit) do
14
- case read_body({:ok, "", state}, "", limit, adapter) do
15
- {:too_large, state} ->
16
- {:too_large, %{conn | adapter: {adapter, state}}}
17
- {:ok, body, state} ->
18
- {:ok, Plug.Conn.Query.decode(body), %{conn | adapter: {adapter, state}}}
19
- end
20
- end
21
-
22
- defp read_body({:ok, buffer, state}, acc, limit, adapter) when limit >= 0,
23
- do: read_body(adapter.stream_req_body(state, 1_000_000),
24
- acc <> buffer, limit - byte_size(buffer), adapter)
25
- defp read_body({:ok, _, state}, _acc, _limit, _adapter),
26
- do: {:too_large, state}
27
-
28
- defp read_body({:done, state}, acc, limit, _adapter) when limit >= 0,
29
- do: {:ok, acc, state}
30
- defp read_body({:done, state}, _acc, _limit, _adapter),
31
- do: {:too_large, state}
32
17
end
changed mix.exs
 
@@ -3,8 +3,8 @@ defmodule Plug.Mixfile do
3
3
4
4
def project do
5
5
[app: :plug,
6
- version: "0.4.4",
7
- elixir: "~> 0.13.3 or ~> 0.14.0-dev",
6
+ version: "0.5.0",
7
+ elixir: "~> 0.14.0",
8
8
deps: deps,
9
9
package: package,
10
10
description: "A specification and conveniences for composable " <>
 
@@ -19,7 +19,7 @@ defmodule Plug.Mixfile do
19
19
end
20
20
21
21
def deps do
22
- [{:cowboy, "~> 0.9", github: "extend/cowboy", optional: true},
22
+ [{:cowboy, "~> 0.10.0", github: "extend/cowboy", optional: true},
23
23
{:ex_doc, github: "elixir-lang/ex_doc", only: [:docs]},
24
24
{:hackney, github: "benoitc/hackney", only: [:test]}]
25
25
end