parsers.ex (13394B)
1 defmodule Plug.Parsers do 2 defmodule RequestTooLargeError do 3 @moduledoc """ 4 Error raised when the request is too large. 5 """ 6 7 defexception message: 8 "the request is too large. If you are willing to process " <> 9 "larger requests, please give a :length to Plug.Parsers", 10 plug_status: 413 11 end 12 13 defmodule UnsupportedMediaTypeError do 14 @moduledoc """ 15 Error raised when the request body cannot be parsed. 16 """ 17 18 defexception media_type: nil, plug_status: 415 19 20 def message(exception) do 21 "unsupported media type #{exception.media_type}" 22 end 23 end 24 25 defmodule BadEncodingError do 26 @moduledoc """ 27 Raised when the request body contains bad encoding. 28 """ 29 30 defexception message: nil, plug_status: 400 31 end 32 33 defmodule ParseError do 34 @moduledoc """ 35 Error raised when the request body is malformed. 36 """ 37 38 defexception exception: nil, plug_status: 400 39 40 def message(%{exception: exception}) do 41 "malformed request, a #{inspect(exception.__struct__)} exception was raised " <> 42 "with message #{inspect(Exception.message(exception))}" 43 end 44 end 45 46 @moduledoc """ 47 A plug for parsing the request body. 48 49 It invokes a list of `:parsers`, which are activated based on the 50 request content-type. Custom parsers are also supported by defining 51 a module that implements the behaviour defined by this module. 52 53 Once a connection goes through this plug, it will have `:body_params` 54 set to the map of params parsed by one of the parsers listed in 55 `:parsers` and `:params` set to the result of merging the `:body_params` 56 and `:query_params`. In case `:query_params` have not yet been parsed, 57 `Plug.Conn.fetch_query_params/2` is automatically invoked. 58 59 This plug will raise `Plug.Parsers.UnsupportedMediaTypeError` by default if 60 the request cannot be parsed by any of the given types and the MIME type has 61 not been explicitly accepted with the `:pass` option. 62 63 `Plug.Parsers.RequestTooLargeError` will be raised if the request goes over 64 the given limit. The default length is 8MB and it can be customized by passing 65 the `:length` option to the Plug. `:read_timeout` and `:read_length`, as 66 described by `Plug.Conn.read_body/2`, are also supported. 67 68 Parsers may raise a `Plug.Parsers.ParseError` if the request has a malformed 69 body. 70 71 This plug only parses the body if the request method is one of the following: 72 73 * `POST` 74 * `PUT` 75 * `PATCH` 76 * `DELETE` 77 78 For requests with a different request method, this plug will only fetch the 79 query params. 80 81 ## Options 82 83 * `:parsers` - a list of modules or atoms of built-in parsers to be 84 invoked for parsing. These modules need to implement the behaviour 85 outlined in this module. 86 87 * `:pass` - an optional list of MIME type strings that are allowed 88 to pass through. Any mime not handled by a parser and not explicitly 89 listed in `:pass` will `raise UnsupportedMediaTypeError`. For example: 90 91 * `["*/*"]` - never raises 92 * `["text/html", "application/*"]` - doesn't raise for those values 93 * `[]` - always raises (default) 94 95 * `:query_string_length` - the maximum allowed size for query strings 96 97 * `:validate_utf8` - boolean that tells whether or not we want to 98 validate that parsed binaries are utf8 strings. 99 100 * `:body_reader` - an optional replacement (or wrapper) for 101 `Plug.Conn.read_body/2` to provide a function that gives access to the 102 raw body before it is parsed and discarded. It is in the standard format 103 of `{Module, :function, [args]}` (MFA) and defaults to 104 `{Plug.Conn, :read_body, []}`. Note that this option is not used by 105 `Plug.Parsers.MULTIPART` which relies instead on other functions defined 106 in `Plug.Conn`. 107 108 All other options given to this Plug are forwarded to the parsers. 109 110 ## Examples 111 112 plug Plug.Parsers, 113 parsers: [:urlencoded, :multipart], 114 pass: ["text/*"] 115 116 Any other option given to Plug.Parsers is forwarded to the underlying 117 parsers. Therefore, you can use a JSON parser and pass the `:json_decoder` 118 option at the root: 119 120 plug Plug.Parsers, 121 parsers: [:urlencoded, :json], 122 json_decoder: Jason 123 124 Or directly to the parser itself: 125 126 plug Plug.Parsers, 127 parsers: [:urlencoded, {:json, json_decoder: Jason}] 128 129 It is also possible to pass the `:json_decoder` as a `{module, function, args}` tuple, 130 useful for passing options to the JSON decoder: 131 132 plug Plug.Parsers, 133 parsers: [:json], 134 json_decoder: {Jason, :decode!, [[floats: :decimals]]} 135 136 A common set of shared options given to Plug.Parsers is `:length`, 137 `:read_length` and `:read_timeout`, which customizes the maximum 138 request length you want to accept. For example, to support file 139 uploads, you can do: 140 141 plug Plug.Parsers, 142 parsers: [:urlencoded, :multipart], 143 length: 20_000_000 144 145 However, the above will increase the maximum length of all request 146 types. If you want to increase the limit only for multipart requests 147 (which is typically the ones used for file uploads), you can do: 148 149 plug Plug.Parsers, 150 parsers: [ 151 :urlencoded, 152 {:multipart, length: 20_000_000} # Increase to 20MB max upload 153 ] 154 155 ## Built-in parsers 156 157 Plug ships with the following parsers: 158 159 * `Plug.Parsers.URLENCODED` - parses `application/x-www-form-urlencoded` 160 requests (can be used as `:urlencoded` as well in the `:parsers` option) 161 * `Plug.Parsers.MULTIPART` - parses `multipart/form-data` and 162 `multipart/mixed` requests (can be used as `:multipart` as well in the 163 `:parsers` option) 164 * `Plug.Parsers.JSON` - parses `application/json` requests with the given 165 `:json_decoder` (can be used as `:json` as well in the `:parsers` option) 166 167 ## File handling 168 169 If a file is uploaded via any of the parsers, Plug will 170 stream the uploaded contents to a file in a temporary directory in order to 171 avoid loading the whole file into memory. For such, the `:plug` application 172 needs to be started in order for file uploads to work. More details on how the 173 uploaded file is handled can be found in the documentation for `Plug.Upload`. 174 175 When a file is uploaded, the request parameter that identifies that file will 176 be a `Plug.Upload` struct with information about the uploaded file (e.g. 177 filename and content type) and about where the file is stored. 178 179 The temporary directory where files are streamed to can be customized by 180 setting the `PLUG_TMPDIR` environment variable on the host system. If 181 `PLUG_TMPDIR` isn't set, Plug will look at some environment 182 variables which usually hold the value of the system's temporary directory 183 (like `TMPDIR` or `TMP`). If no value is found in any of those variables, 184 `/tmp` is used as a default. 185 186 ## Custom body reader 187 188 Sometimes you may want to customize how a parser reads the body from the 189 connection. For example, you may want to cache the body to perform verification 190 later, such as HTTP Signature Verification. This can be achieved with a custom 191 body reader that would read the body and store it in the connection, such as: 192 193 defmodule CacheBodyReader do 194 def read_body(conn, opts) do 195 {:ok, body, conn} = Plug.Conn.read_body(conn, opts) 196 conn = update_in(conn.assigns[:raw_body], &[body | (&1 || [])]) 197 {:ok, body, conn} 198 end 199 end 200 201 which could then be set as: 202 203 plug Plug.Parsers, 204 parsers: [:urlencoded, :json], 205 pass: ["text/*"], 206 body_reader: {CacheBodyReader, :read_body, []}, 207 json_decoder: Jason 208 209 """ 210 211 alias Plug.Conn 212 213 @callback init(opts :: Keyword.t()) :: Plug.opts() 214 215 @doc """ 216 Attempts to parse the connection's request body given the content-type type, 217 subtype, and its parameters. 218 219 The arguments are: 220 221 * the `Plug.Conn` connection 222 * `type`, the content-type type (e.g., `"x-sample"` for the 223 `"x-sample/json"` content-type) 224 * `subtype`, the content-type subtype (e.g., `"json"` for the 225 `"x-sample/json"` content-type) 226 * `params`, the content-type parameters (e.g., `%{"foo" => "bar"}` 227 for the `"text/plain; foo=bar"` content-type) 228 229 This function should return: 230 231 * `{:ok, body_params, conn}` if the parser is able to handle the given 232 content-type; `body_params` should be a map 233 * `{:next, conn}` if the next parser should be invoked 234 * `{:error, :too_large, conn}` if the request goes over the given limit 235 236 """ 237 @callback parse( 238 conn :: Conn.t(), 239 type :: binary, 240 subtype :: binary, 241 params :: Conn.Utils.params(), 242 opts :: Plug.opts() 243 ) :: 244 {:ok, Conn.params(), Conn.t()} 245 | {:error, :too_large, Conn.t()} 246 | {:next, Conn.t()} 247 248 @behaviour Plug 249 @methods ~w(POST PUT PATCH DELETE) 250 251 @impl true 252 def init(opts) do 253 {parsers, opts} = Keyword.pop(opts, :parsers) 254 {pass, opts} = Keyword.pop(opts, :pass, []) 255 {query_string_length, opts} = Keyword.pop(opts, :query_string_length, 1_000_000) 256 validate_utf8 = Keyword.get(opts, :validate_utf8, true) 257 258 unless parsers do 259 raise ArgumentError, "Plug.Parsers expects a set of parsers to be given in :parsers" 260 end 261 262 {convert_parsers(parsers, opts), pass, query_string_length, validate_utf8} 263 end 264 265 defp convert_parsers(parsers, root_opts) do 266 for parser <- parsers do 267 {parser, opts} = 268 case parser do 269 {parser, opts} when is_atom(parser) and is_list(opts) -> 270 {parser, Keyword.merge(root_opts, opts)} 271 272 parser when is_atom(parser) -> 273 {parser, root_opts} 274 end 275 276 module = 277 case Atom.to_string(parser) do 278 "Elixir." <> _ -> parser 279 reference -> Module.concat(Plug.Parsers, String.upcase(reference)) 280 end 281 282 # TODO: Remove this check in future releases once all parsers implement init/1 accordingly 283 if Code.ensure_compiled(module) == {:module, module} and 284 function_exported?(module, :init, 1) do 285 {module, module.init(opts)} 286 else 287 {module, opts} 288 end 289 end 290 end 291 292 @impl true 293 def call(%{method: method, body_params: %Plug.Conn.Unfetched{}} = conn, options) 294 when method in @methods do 295 {parsers, pass, query_string_length, validate_utf8} = options 296 %{req_headers: req_headers} = conn 297 298 conn = 299 Conn.fetch_query_params(conn, 300 length: query_string_length, 301 validate_utf8: validate_utf8 302 ) 303 304 case List.keyfind(req_headers, "content-type", 0) do 305 {"content-type", ct} -> 306 case Conn.Utils.content_type(ct) do 307 {:ok, type, subtype, params} -> 308 reduce( 309 conn, 310 parsers, 311 type, 312 subtype, 313 params, 314 pass, 315 query_string_length, 316 validate_utf8 317 ) 318 319 :error -> 320 reduce(conn, parsers, ct, "", %{}, pass, query_string_length, validate_utf8) 321 end 322 323 _ -> 324 {conn, params} = merge_params(conn, %{}, query_string_length, validate_utf8) 325 326 %{conn | params: params, body_params: %{}} 327 end 328 end 329 330 def call(%{body_params: body_params} = conn, {_, _, query_string_length, validate_utf8}) do 331 body_params = make_empty_if_unfetched(body_params) 332 {conn, params} = merge_params(conn, body_params, query_string_length, validate_utf8) 333 %{conn | params: params, body_params: body_params} 334 end 335 336 defp reduce( 337 conn, 338 [{parser, options} | rest], 339 type, 340 subtype, 341 params, 342 pass, 343 query_string_length, 344 validate_utf8 345 ) do 346 case parser.parse(conn, type, subtype, params, options) do 347 {:ok, body, conn} -> 348 {conn, params} = merge_params(conn, body, query_string_length, validate_utf8) 349 %{conn | params: params, body_params: body} 350 351 {:next, conn} -> 352 reduce(conn, rest, type, subtype, params, pass, query_string_length, validate_utf8) 353 354 {:error, :too_large, _conn} -> 355 raise RequestTooLargeError 356 end 357 end 358 359 defp reduce(conn, [], type, subtype, _params, pass, query_string_length, validate_utf8) do 360 if accepted_mime?(type, subtype, pass) do 361 {conn, params} = merge_params(conn, %{}, query_string_length, validate_utf8) 362 %{conn | params: params} 363 else 364 raise UnsupportedMediaTypeError, media_type: "#{type}/#{subtype}" 365 end 366 end 367 368 defp accepted_mime?(_type, _subtype, ["*/*"]), 369 do: true 370 371 defp accepted_mime?(type, subtype, pass), 372 do: "#{type}/#{subtype}" in pass || "#{type}/*" in pass 373 374 defp merge_params(conn, body_params, query_string_length, validate_utf8) do 375 %{params: params, path_params: path_params} = conn 376 params = make_empty_if_unfetched(params) 377 378 conn = 379 Plug.Conn.fetch_query_params(conn, 380 length: query_string_length, 381 validate_utf8: validate_utf8 382 ) 383 384 {conn, 385 conn.query_params 386 |> Map.merge(params) 387 |> Map.merge(body_params) 388 |> Map.merge(path_params)} 389 end 390 391 defp make_empty_if_unfetched(%Plug.Conn.Unfetched{}), do: %{} 392 defp make_empty_if_unfetched(params), do: params 393 end