changed
CHANGELOG.md
|
@@ -1,5 +1,14 @@
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+ ## v1.15.3 (2024-01-16)
|
4
|
+
|
5
|
+ ### Enhancements
|
6
|
+
|
7
|
+ * Allow setting the port on the connection in tests
|
8
|
+ * Allow returning `{:ok, payload}` on inform
|
9
|
+ * Allow custom exceptions in `validate_utf8` option
|
10
|
+ * Allow skipping sent body on chunked replies
|
11
|
+
|
3
12
|
## v1.15.2 (2023-11-14)
|
4
13
|
|
5
14
|
### Enhancements
|
changed
README.md
|
@@ -30,7 +30,7 @@ There are two options at the moment:
|
30
30
|
```elixir
|
31
31
|
def deps do
|
32
32
|
[
|
33
|
- {:bandit, "~> 0.6"}
|
33
|
+ {:bandit, "~> 1.0"}
|
34
34
|
]
|
35
35
|
end
|
36
36
|
```
|
|
@@ -75,7 +75,7 @@ and the `websocket_adapter` project for the WebSocket bits. Since we need differ
|
75
75
|
routes, we will use the built-in `Plug.Router` for that:
|
76
76
|
|
77
77
|
```elixir
|
78
|
- Mix.install([:plug, :bandit, :websock_adapter])
|
78
|
+ Mix.install([:bandit, :websock_adapter])
|
79
79
|
|
80
80
|
defmodule EchoServer do
|
81
81
|
def init(options) do
|
|
@@ -146,12 +146,11 @@ On a production system, you likely want to start your Plug pipeline under your a
|
146
146
|
$ mix new my_app --sup
|
147
147
|
```
|
148
148
|
|
149
|
- Add both `:plug` and `:plug_cowboy` as dependencies in your `mix.exs`:
|
149
|
+ Add `:plug_cowboy` (or `:bandit`) as a dependency to your `mix.exs`:
|
150
150
|
|
151
151
|
```elixir
|
152
152
|
def deps do
|
153
153
|
[
|
154
|
- {:plug, "~> 1.14"},
|
155
154
|
{:plug_cowboy, "~> 2.0"}
|
156
155
|
]
|
157
156
|
end
|
changed
hex_metadata.config
|
@@ -1,6 +1,6 @@
|
1
1
|
{<<"links">>,[{<<"GitHub">>,<<"https://github.com/elixir-plug/plug">>}]}.
|
2
2
|
{<<"name">>,<<"plug">>}.
|
3
|
- {<<"version">>,<<"1.15.2">>}.
|
3
|
+ {<<"version">>,<<"1.15.3">>}.
|
4
4
|
{<<"description">>,<<"Compose web applications with functions">>}.
|
5
5
|
{<<"elixir">>,<<"~> 1.10">>}.
|
6
6
|
{<<"app">>,<<"plug">>}.
|
changed
lib/plug.ex
|
@@ -8,8 +8,9 @@ defmodule Plug do
|
8
8
|
|
9
9
|
### Function plugs
|
10
10
|
|
11
|
- A function plug is any function that receives a connection and a set of
|
12
|
- options and returns a connection. Its type signature must be:
|
11
|
+ A function plug is by definition any function that receives a connection
|
12
|
+ and a set of options and returns a connection. Function plugs must have
|
13
|
+ the following type signature:
|
13
14
|
|
14
15
|
(Plug.Conn.t, Plug.opts) :: Plug.Conn.t
|
15
16
|
|
|
@@ -52,8 +53,7 @@ defmodule Plug do
|
52
53
|
|
53
54
|
## The Plug pipeline
|
54
55
|
|
55
|
- The `Plug.Builder` module provides conveniences for building plug
|
56
|
- pipelines.
|
56
|
+ The `Plug.Builder` module provides conveniences for building plug pipelines.
|
57
57
|
"""
|
58
58
|
|
59
59
|
@type opts ::
|
|
@@ -72,18 +72,17 @@ defmodule Plug do
|
72
72
|
require Logger
|
73
73
|
|
74
74
|
@doc """
|
75
|
- Run a series of Plugs at runtime.
|
75
|
+ Run a series of plugs at runtime.
|
76
76
|
|
77
77
|
The plugs given here can be either a tuple, representing a module plug
|
78
78
|
and their options, or a simple function that receives a connection and
|
79
79
|
returns a connection.
|
80
80
|
|
81
|
- If any of the plugs halt, the remaining plugs are not invoked. If the
|
82
|
- given connection was already halted, none of the plugs are invoked
|
83
|
- either.
|
81
|
+ If any plug halts, the connection won't invoke the remaining plugs. If the
|
82
|
+ given connection was already halted, none of the plugs are invoked either.
|
84
83
|
|
85
|
- While `Plug.Builder` works at compile-time, this is a straight-forward
|
86
|
- alternative that works at runtime.
|
84
|
+ While `Plug.Builder` is designed to operate at compile-time, the `run` function
|
85
|
+ serves as a straightforward alternative for runtime executions.
|
87
86
|
|
88
87
|
## Examples
|
89
88
|
|
|
@@ -91,7 +90,7 @@ defmodule Plug do
|
91
90
|
|
92
91
|
## Options
|
93
92
|
|
94
|
- * `:log_on_halt` - a log level to be used if a Plug halts
|
93
|
+ * `:log_on_halt` - a log level to be used if a plug halts
|
95
94
|
|
96
95
|
"""
|
97
96
|
@spec run(Plug.Conn.t(), [{module, opts} | (Plug.Conn.t() -> Plug.Conn.t())], Keyword.t()) ::
|
|
@@ -135,11 +134,11 @@ defmodule Plug do
|
135
134
|
defp do_run(conn, [], _level), do: conn
|
136
135
|
|
137
136
|
@doc """
|
138
|
- Forwards requests to another Plug setting the connection to a trailing subpath of the request.
|
137
|
+ Forwards requests to another plug while setting the connection to a trailing subpath of the request.
|
139
138
|
|
140
|
- The `path_info` on the forwarded connection will only include the trailing segments
|
141
|
- of the request path supplied to forward, while `conn.script_name` will
|
142
|
- retain the correct base path for e.g. url generation.
|
139
|
+ The `path_info` on the forwarded connection will only include the request path trailing segments
|
140
|
+ supplied to the `forward` function. The `conn.script_name` attribute retains the correct base path,
|
141
|
+ e.g., url generation.
|
143
142
|
|
144
143
|
## Example
|
changed
lib/plug/adapters/test/conn.ex
|
@@ -36,6 +36,8 @@ defmodule Plug.Adapters.Test.Conn do
|
36
36
|
})
|
37
37
|
}
|
38
38
|
|
39
|
+ conn_port = if conn.port != 0, do: conn.port, else: 80
|
40
|
+
|
39
41
|
%Plug.Conn{
|
40
42
|
conn
|
41
43
|
| adapter: {__MODULE__, state},
|
|
@@ -43,7 +45,7 @@ defmodule Plug.Adapters.Test.Conn do
|
43
45
|
method: method,
|
44
46
|
owner: owner,
|
45
47
|
path_info: split_path(uri.path),
|
46
|
- port: uri.port || 80,
|
48
|
+ port: uri.port || conn_port,
|
47
49
|
remote_ip: conn.remote_ip || {127, 0, 0, 1},
|
48
50
|
req_headers: req_headers,
|
49
51
|
request_path: uri.path,
|
changed
lib/plug/conn.ex
|
@@ -1320,10 +1320,19 @@ defmodule Plug.Conn do
|
1320
1320
|
`get_http_protocol/1` to retrieve the protocol and version.
|
1321
1321
|
"""
|
1322
1322
|
@spec inform(t, status, Keyword.t()) :: t
|
1323
|
- def inform(%Conn{} = conn, status, headers \\ []) do
|
1323
|
+ def inform(%Conn{adapter: {adapter, _}} = conn, status, headers \\ []) do
|
1324
1324
|
status_code = Plug.Conn.Status.code(status)
|
1325
|
- adapter_inform(conn, status_code, headers)
|
1326
|
- conn
|
1325
|
+
|
1326
|
+ case adapter_inform(conn, status_code, headers) do
|
1327
|
+ :ok ->
|
1328
|
+ conn
|
1329
|
+
|
1330
|
+ {:ok, payload} ->
|
1331
|
+ put_in(conn.adapter, {adapter, payload})
|
1332
|
+
|
1333
|
+ {:error, :not_supported} ->
|
1334
|
+ conn
|
1335
|
+ end
|
1327
1336
|
end
|
1328
1337
|
|
1329
1338
|
@doc """
|
|
@@ -1339,7 +1348,10 @@ defmodule Plug.Conn do
|
1339
1348
|
:ok ->
|
1340
1349
|
conn
|
1341
1350
|
|
1342
|
- _ ->
|
1351
|
+ {:ok, payload} ->
|
1352
|
+ put_in(conn.adapter, {adapter, payload})
|
1353
|
+
|
1354
|
+ {:error, :not_supported} ->
|
1343
1355
|
raise "inform is not supported by #{inspect(adapter)}." <>
|
1344
1356
|
"You should either delete the call to `inform!/3` or switch to an " <>
|
1345
1357
|
"adapter that does support informational such as Plug.Cowboy"
|
|
@@ -1356,8 +1368,9 @@ defmodule Plug.Conn do
|
1356
1368
|
raise AlreadySentError
|
1357
1369
|
end
|
1358
1370
|
|
1359
|
- defp adapter_inform(%Conn{adapter: {adapter, payload}}, status, headers),
|
1360
|
- do: adapter.inform(payload, status, headers)
|
1371
|
+ defp adapter_inform(%Conn{adapter: {adapter, payload}}, status, headers) do
|
1372
|
+ adapter.inform(payload, status, headers)
|
1373
|
+ end
|
1361
1374
|
|
1362
1375
|
@doc """
|
1363
1376
|
Request a protocol upgrade from the underlying adapter.
|
changed
lib/plug/conn/adapter.ex
|
@@ -84,9 +84,11 @@ defmodule Plug.Conn.Adapter do
|
84
84
|
a chunked response to the client.
|
85
85
|
|
86
86
|
Webservers are advised to return `nil` as the sent_body,
|
87
|
- as the body can no longer be manipulated. However, the
|
88
|
- test implementation returns the actual body so it can
|
89
|
- be used during testing.
|
87
|
+ since this function does not actually produce a body.
|
88
|
+ However, the test implementation returns an empty binary
|
89
|
+ as the body in order to be consistent with the built-up
|
90
|
+ body returned by subsequent calls to the test implementation's
|
91
|
+ `chunk/2` function
|
90
92
|
"""
|
91
93
|
@callback send_chunked(payload, status :: Conn.status(), headers :: Conn.headers()) ::
|
92
94
|
{:ok, sent_body :: binary | nil, payload}
|
|
@@ -97,13 +99,14 @@ defmodule Plug.Conn.Adapter do
|
97
99
|
If the request has method `"HEAD"`, the adapter should
|
98
100
|
not send the response to the client.
|
99
101
|
|
100
|
- Webservers are advised to return `:ok` and not modify
|
101
|
- any further state for each chunk. However, the test
|
102
|
- implementation returns the actual body and payload so
|
103
|
- it can be used during testing.
|
102
|
+ Webservers are advised to return `nil` as the sent_body,
|
103
|
+ since the complete sent body depends on the sum of all
|
104
|
+ calls to this function. However, the test implementation
|
105
|
+ tracks the overall body and payload so it can be used
|
106
|
+ during testing.
|
104
107
|
"""
|
105
108
|
@callback chunk(payload, body :: Conn.body()) ::
|
106
|
- :ok | {:ok, sent_body :: binary, payload} | {:error, term}
|
109
|
+ :ok | {:ok, sent_body :: binary | nil, payload} | {:error, term}
|
107
110
|
|
108
111
|
@doc """
|
109
112
|
Reads the request body.
|
|
@@ -131,7 +134,7 @@ defmodule Plug.Conn.Adapter do
|
131
134
|
should be returned.
|
132
135
|
"""
|
133
136
|
@callback inform(payload, status :: Conn.status(), headers :: Keyword.t()) ::
|
134
|
- :ok | {:error, term}
|
137
|
+ :ok | {:ok, payload()} | {:error, term()}
|
135
138
|
|
136
139
|
@doc """
|
137
140
|
Attempt to upgrade the connection with the client.
|
changed
lib/plug/conn/query.ex
|
@@ -82,12 +82,21 @@ defmodule Plug.Conn.Query do
|
82
82
|
For stateful decoding, see `decode_init/0`, `decode_each/2`, and `decode_done/2`.
|
83
83
|
"""
|
84
84
|
|
85
|
+ @typedoc """
|
86
|
+ Stateful decoder accumulator.
|
87
|
+
|
88
|
+ See `decode_init/0`, `decode_each/2`, and `decode_done/2`.
|
89
|
+ """
|
90
|
+ @typedoc since: "1.16.0"
|
91
|
+ @opaque decoder() :: map()
|
92
|
+
|
85
93
|
@doc """
|
86
94
|
Decodes the given `query`.
|
87
95
|
|
88
96
|
The `query` is assumed to be encoded in the "x-www-form-urlencoded" format.
|
89
97
|
The format is decoded at first. Then, if `validate_utf8` is `true`, the decoded
|
90
|
- result is validated for proper UTF-8 encoding.
|
98
|
+ result is validated for proper UTF-8 encoding. `validate_utf8` may also be
|
99
|
+ an atom with a custom exception to raise.
|
91
100
|
|
92
101
|
`initial` is the initial "accumulator" where decoded values will be added.
|
93
102
|
|
|
@@ -142,8 +151,10 @@ defmodule Plug.Conn.Query do
|
142
151
|
raise invalid_exception, "invalid urlencoded params, got #{value}"
|
143
152
|
else
|
144
153
|
binary ->
|
145
|
- if validate_utf8 do
|
146
|
- Plug.Conn.Utils.validate_utf8!(binary, invalid_exception, "urlencoded params")
|
154
|
+ case validate_utf8 do
|
155
|
+ true -> Plug.Conn.Utils.validate_utf8!(binary, invalid_exception, "urlencoded params")
|
156
|
+ false -> :ok
|
157
|
+ module -> Plug.Conn.Utils.validate_utf8!(binary, module, "urlencoded params")
|
147
158
|
end
|
148
159
|
|
149
160
|
binary
|
|
@@ -154,16 +165,30 @@ defmodule Plug.Conn.Query do
|
154
165
|
Starts a stateful decoder.
|
155
166
|
|
156
167
|
Use `decode_each/2` and `decode_done/2` to decode and complete.
|
168
|
+ See `decode_each/2` for examples.
|
157
169
|
"""
|
170
|
+ @spec decode_init() :: decoder()
|
158
171
|
def decode_init(), do: %{root: []}
|
159
172
|
|
160
173
|
@doc """
|
161
|
- Decodes the given tuple.
|
174
|
+ Decodes the given `pair` tuple.
|
162
175
|
|
163
176
|
It parses the key and stores the value into the current
|
164
|
- accumulator. The keys and values are not assumed to be
|
165
|
- encoded in "x-www-form-urlencoded".
|
177
|
+ accumulator `decoder`. The keys and values are not assumed to be
|
178
|
+ encoded in `"x-www-form-urlencoded"`.
|
179
|
+
|
180
|
+ ## Examples
|
181
|
+
|
182
|
+ iex> decoder = Plug.Conn.Query.decode_init()
|
183
|
+ iex> decoder = Plug.Conn.Query.decode_each({"foo", "bar"}, decoder)
|
184
|
+ iex> decoder = Plug.Conn.Query.decode_each({"baz", "bat"}, decoder)
|
185
|
+ iex> Plug.Conn.Query.decode_done(decoder)
|
186
|
+ %{"baz" => "bat", "foo" => "bar"}
|
187
|
+
|
166
188
|
"""
|
189
|
+ @spec decode_each({term(), term()}, decoder()) :: decoder()
|
190
|
+ def decode_each(pair, decoder)
|
191
|
+
|
167
192
|
def decode_each({"", value}, map) do
|
168
193
|
insert_keys([{:root, ""}], value, map)
|
169
194
|
end
|
|
@@ -224,9 +249,24 @@ defmodule Plug.Conn.Query do
|
224
249
|
end
|
225
250
|
|
226
251
|
@doc """
|
227
|
- Finishes stateful decoding and returns a map.
|
252
|
+ Finishes stateful decoding and returns a map with the decoded pairs.
|
253
|
+
|
254
|
+ `decoder` is the stateful decoder returned by `decode_init/0` and `decode_each/2`.
|
255
|
+ `initial` is an enumerable of key-value pairs that functions as the initial
|
256
|
+ accumulator for the returned map (see examples below).
|
257
|
+
|
258
|
+ ## Examples
|
259
|
+
|
260
|
+ iex> decoder = Plug.Conn.Query.decode_init()
|
261
|
+ iex> decoder = Plug.Conn.Query.decode_each({"foo", "bar"}, decoder)
|
262
|
+ iex> Plug.Conn.Query.decode_done(decoder, %{"initial" => true})
|
263
|
+ %{"foo" => "bar", "initial" => true}
|
264
|
+
|
228
265
|
"""
|
229
|
- def decode_done(map, initial \\ []), do: finalize_map(map.root, Enum.to_list(initial), map)
|
266
|
+ @spec decode_done(decoder(), Enumerable.t()) :: %{optional(String.t()) => term()}
|
267
|
+ def decode_done(%{root: root} = decoder, initial \\ []) do
|
268
|
+ finalize_map(root, Enum.to_list(initial), decoder)
|
269
|
+ end
|
230
270
|
|
231
271
|
defp finalize_pointer(key, map) do
|
232
272
|
case Map.fetch!(map, key) do
|
changed
lib/plug/parsers/multipart.ex
|
@@ -27,7 +27,7 @@ defmodule Plug.Parsers.MULTIPART do
|
27
27
|
headers
|
28
28
|
|
29
29
|
* `:validate_utf8` - specifies whether multipart body parts should be validated
|
30
|
- as utf8 binaries. Defaults to true
|
30
|
+ as utf8 binaries. It is either a boolean or a custom exception to raise
|
31
31
|
|
32
32
|
* `:multipart_to_params` - a MFA that receives the multipart headers and the
|
33
33
|
connection and it must return a tuple of `{:ok, params, conn}`
|
|
@@ -184,8 +184,15 @@ defmodule Plug.Parsers.MULTIPART do
|
184
184
|
{:ok, limit, body, conn} =
|
185
185
|
parse_multipart_body(Plug.Conn.read_part_body(conn, opts), limit, opts, "")
|
186
186
|
|
187
|
- if Keyword.get(opts, :validate_utf8, true) do
|
188
|
- Plug.Conn.Utils.validate_utf8!(body, Plug.Parsers.BadEncodingError, "multipart body")
|
187
|
+ case Keyword.get(opts, :validate_utf8, true) do
|
188
|
+ true ->
|
189
|
+ Plug.Conn.Utils.validate_utf8!(body, Plug.Parsers.BadEncodingError, "multipart body")
|
190
|
+
|
191
|
+ false ->
|
192
|
+ :ok
|
193
|
+
|
194
|
+ module ->
|
195
|
+ Plug.Conn.Utils.validate_utf8!(body, module, "multipart body")
|
189
196
|
end
|
190
197
|
|
191
198
|
{conn, limit, [{name, headers, body} | acc]}
|
changed
mix.exs
|
@@ -1,7 +1,7 @@
|
1
1
|
defmodule Plug.MixProject do
|
2
2
|
use Mix.Project
|
3
3
|
|
4
|
- @version "1.15.2"
|
4
|
+ @version "1.15.3"
|
5
5
|
@description "Compose web applications with functions"
|
6
6
|
@xref_exclude [Plug.Cowboy, :ssl]
|
7
7
|
|
|
@@ -45,12 +45,18 @@ defmodule Plug.MixProject do
|
45
45
|
def deps do
|
46
46
|
[
|
47
47
|
{:mime, "~> 1.0 or ~> 2.0"},
|
48
|
- {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0"},
|
48
|
+ {:plug_crypto, plug_crypto_version()},
|
49
49
|
{:telemetry, "~> 0.4.3 or ~> 1.0"},
|
50
50
|
{:ex_doc, "~> 0.21", only: :docs}
|
51
51
|
]
|
52
52
|
end
|
53
53
|
|
54
|
+ if System.get_env("PLUG_CRYPTO_2_0", "true") == "true" do
|
55
|
+ defp plug_crypto_version, do: "~> 1.1.1 or ~> 1.2 or ~> 2.0"
|
56
|
+ else
|
57
|
+ defp plug_crypto_version, do: "~> 1.1.1 or ~> 1.2"
|
58
|
+ end
|
59
|
+
|
54
60
|
defp package do
|
55
61
|
%{
|
56
62
|
licenses: ["Apache-2.0"],
|