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"],