router.ex (20450B)
1 defmodule Plug.Router do 2 @moduledoc ~S""" 3 A DSL to define a routing algorithm that works with Plug. 4 5 It provides a set of macros to generate routes. For example: 6 7 defmodule AppRouter do 8 use Plug.Router 9 10 plug :match 11 plug :dispatch 12 13 get "/hello" do 14 send_resp(conn, 200, "world") 15 end 16 17 match _ do 18 send_resp(conn, 404, "oops") 19 end 20 end 21 22 Each route receives a `conn` variable containing a `Plug.Conn` 23 struct and it needs to return a connection, as per the Plug spec. 24 A catch-all `match` is recommended to be defined as in the example 25 above, otherwise routing fails with a function clause error. 26 27 The router is itself a plug, which means it can be invoked as: 28 29 AppRouter.call(conn, AppRouter.init([])) 30 31 Each `Plug.Router` has a plug pipeline, defined by `Plug.Builder`, 32 and by default it requires two plugs: `:match` and `:dispatch`. 33 `:match` is responsible for finding a matching route which is 34 then forwarded to `:dispatch`. This means users can easily hook 35 into the router mechanism and add behaviour before match, before 36 dispatch, or after both. See the `Plug.Builder` module for more 37 information. 38 39 ## Routes 40 41 get "/hello" do 42 send_resp(conn, 200, "world") 43 end 44 45 In the example above, a request will only match if it is a `GET` 46 request and the route is "/hello". The supported HTTP methods are 47 `get`, `post`, `put`, `patch`, `delete` and `options`. 48 49 A route can also specify parameters which will then be available 50 in the function body: 51 52 get "/hello/:name" do 53 send_resp(conn, 200, "hello #{name}") 54 end 55 56 This means the name can also be used in guards: 57 58 get "/hello/:name" when name in ~w(foo bar) do 59 send_resp(conn, 200, "hello #{name}") 60 end 61 62 The `:name` parameter will also be available in the function body as 63 `conn.params["name"]` and `conn.path_params["name"]`. 64 65 The identifier always starts with `:` and must be followed by letters, 66 numbers, and underscores, like any Elixir variable. It is possible for 67 identifiers to be either prefixed or suffixed by other words. For example, 68 you can include a suffix such as a dot delimited file extension: 69 70 get "/hello/:name.json" do 71 send_resp(conn, 200, "hello #{name}") 72 end 73 74 The above will match `/hello/foo.json` but not `/hello/foo`. 75 Other delimiters such as `-`, `@` may be used to denote suffixes. 76 77 Routes allow for globbing which will match the remaining parts 78 of a route. A glob match is done with the `*` character followed 79 by the variable name. Typically you prefix the variable name with 80 underscore to discard it: 81 82 get "/hello/*_rest" do 83 send_resp(conn, 200, "matches all routes starting with /hello") 84 end 85 86 But you can also assign the glob to any variable. The contents will 87 always be a list: 88 89 get "/hello/*glob" do 90 send_resp(conn, 200, "route after /hello: #{inspect glob}") 91 end 92 93 Opposite to `:identifiers`, globs do not allow prefix nor suffix 94 matches. 95 96 Finally, a general `match` function is also supported: 97 98 match "/hello" do 99 send_resp(conn, 200, "world") 100 end 101 102 A `match` will match any route regardless of the HTTP method. 103 Check `match/3` for more information on how route compilation 104 works and a list of supported options. 105 106 ## Parameter Parsing 107 108 Handling request data can be done through the 109 [`Plug.Parsers`](https://hexdocs.pm/plug/Plug.Parsers.html#content) plug. It 110 provides support for parsing URL-encoded, form-data, and JSON data as well as 111 providing a behaviour that others parsers can adopt. 112 113 Here is an example of `Plug.Parsers` can be used in a `Plug.Router` router to 114 parse the JSON-encoded body of a POST request: 115 116 defmodule AppRouter do 117 use Plug.Router 118 119 plug :match 120 121 plug Plug.Parsers, 122 parsers: [:json], 123 pass: ["application/json"], 124 json_decoder: Jason 125 126 plug :dispatch 127 128 post "/hello" do 129 IO.inspect conn.body_params # Prints JSON POST body 130 send_resp(conn, 200, "Success!") 131 end 132 end 133 134 It is important that `Plug.Parsers` is placed before the `:dispatch` plug in 135 the pipeline, otherwise the matched clause route will not receive the parsed 136 body in its `Plug.Conn` argument when dispatched. 137 138 `Plug.Parsers` can also be plugged between `:match` and `:dispatch` (like in 139 the example above): this means that `Plug.Parsers` will run only if there is a 140 matching route. This can be useful to perform actions such as authentication 141 *before* parsing the body, which should only be parsed if a route matches 142 afterwards. 143 144 ## Error handling 145 146 In case something goes wrong in a request, the router by default 147 will crash, without returning any response to the client. This 148 behaviour can be configured in two ways, by using two different 149 modules: 150 151 * `Plug.ErrorHandler` - allows the developer to customize exactly 152 which page is sent to the client via the `handle_errors/2` function; 153 154 * `Plug.Debugger` - automatically shows debugging and request information 155 about the failure. This module is recommended to be used only in a 156 development environment. 157 158 Here is an example of how both modules could be used in an application: 159 160 defmodule AppRouter do 161 use Plug.Router 162 163 if Mix.env == :dev do 164 use Plug.Debugger 165 end 166 167 use Plug.ErrorHandler 168 169 plug :match 170 plug :dispatch 171 172 get "/hello" do 173 send_resp(conn, 200, "world") 174 end 175 176 defp handle_errors(conn, %{kind: _kind, reason: _reason, stack: _stack}) do 177 send_resp(conn, conn.status, "Something went wrong") 178 end 179 end 180 181 ## Passing data between routes and plugs 182 183 It is also possible to assign data to the `Plug.Conn` that will 184 be available to any plug invoked after the `:match` plug. 185 This is very useful if you want a matched route to customize how 186 later plugs will behave. 187 188 You can use `:assigns` (which contains user data) or `:private` 189 (which contains library/framework data) for this. For example: 190 191 get "/hello", assigns: %{an_option: :a_value} do 192 send_resp(conn, 200, "world") 193 end 194 195 In the example above, `conn.assigns[:an_option]` will be available 196 to all plugs invoked after `:match`. Such plugs can read from 197 `conn.assigns` (or `conn.private`) to configure their behaviour 198 based on the matched route. 199 200 ## `use` options 201 202 All of the options given to `use Plug.Router` are forwarded to 203 `Plug.Builder`. See the `Plug.Builder` module for more information. 204 205 ## Telemetry 206 207 The router emits the following telemetry events: 208 209 * `[:plug, :router_dispatch, :start]` - dispatched before dispatching to a matched route 210 * Measurement: `%{system_time: System.system_time}` 211 * Metadata: `%{telemetry_span_context: term(), conn: Plug.Conn.t, route: binary, router: module}` 212 213 * `[:plug, :router_dispatch, :exception]` - dispatched after exceptions on dispatching a route 214 * Measurement: `%{duration: native_time}` 215 * Metadata: `%{telemetry_span_context: term(), conn: Plug.Conn.t, route: binary, router: module, kind: :throw | :error | :exit, reason: term(), stacktrace: list()}` 216 217 * `[:plug, :router_dispatch, :stop]` - dispatched after successfully dispatching a matched route 218 * Measurement: `%{duration: native_time}` 219 * Metadata: `%{telemetry_span_context: term(), conn: Plug.Conn.t, route: binary, router: module}` 220 221 """ 222 223 @doc false 224 defmacro __using__(opts) do 225 quote location: :keep do 226 import Plug.Router 227 @plug_router_to %{} 228 @before_compile Plug.Router 229 230 use Plug.Builder, unquote(opts) 231 232 @doc false 233 def match(conn, _opts) do 234 do_match(conn, conn.method, Plug.Router.Utils.decode_path_info!(conn), conn.host) 235 end 236 237 @doc false 238 def dispatch(%Plug.Conn{} = conn, opts) do 239 {path, fun} = Map.fetch!(conn.private, :plug_route) 240 241 try do 242 :telemetry.span( 243 [:plug, :router_dispatch], 244 %{conn: conn, route: path, router: __MODULE__}, 245 fn -> 246 conn = fun.(conn, opts) 247 {conn, %{conn: conn, route: path, router: __MODULE__}} 248 end 249 ) 250 catch 251 kind, reason -> 252 Plug.Conn.WrapperError.reraise(conn, kind, reason, __STACKTRACE__) 253 end 254 end 255 256 defoverridable match: 2, dispatch: 2 257 end 258 end 259 260 @doc false 261 defmacro __before_compile__(env) do 262 unless Module.defines?(env.module, {:do_match, 4}) do 263 raise "no routes defined in module #{inspect(env.module)} using Plug.Router" 264 end 265 266 router_to = Module.get_attribute(env.module, :plug_router_to) 267 init_mode = Module.get_attribute(env.module, :plug_builder_opts)[:init_mode] 268 269 defs = 270 for {callback, {mod, opts}} <- router_to do 271 if init_mode == :runtime do 272 quote do 273 defp unquote(callback)(conn, _opts) do 274 unquote(mod).call(conn, unquote(mod).init(unquote(Macro.escape(opts)))) 275 end 276 end 277 else 278 opts = mod.init(opts) 279 280 quote do 281 defp unquote(callback)(conn, _opts) do 282 require unquote(mod) 283 unquote(mod).call(conn, unquote(Macro.escape(opts))) 284 end 285 end 286 end 287 end 288 289 quote do 290 unquote_splicing(defs) 291 import Plug.Router, only: [] 292 end 293 end 294 295 @doc """ 296 Returns the path of the route that the request was matched to. 297 """ 298 @spec match_path(Plug.Conn.t()) :: String.t() 299 def match_path(%Plug.Conn{} = conn) do 300 {path, _fun} = Map.fetch!(conn.private, :plug_route) 301 path 302 end 303 304 ## Match 305 306 @doc """ 307 Main API to define routes. 308 309 It accepts an expression representing the path and many options 310 allowing the match to be configured. 311 312 The route can dispatch either to a function body or a Plug module. 313 314 ## Examples 315 316 match "/foo/bar", via: :get do 317 send_resp(conn, 200, "hello world") 318 end 319 320 match "/baz", to: MyPlug, init_opts: [an_option: :a_value] 321 322 ## Options 323 324 `match/3` and the other route macros accept the following options: 325 326 * `:host` - the host which the route should match. Defaults to `nil`, 327 meaning no host match, but can be a string like "example.com" or a 328 string ending with ".", like "subdomain." for a subdomain match. 329 330 * `:private` - assigns values to `conn.private` for use in the match 331 332 * `:assigns` - assigns values to `conn.assigns` for use in the match 333 334 * `:via` - matches the route against some specific HTTP method(s) specified 335 as an atom, like `:get` or `:put`, or a list, like `[:get, :post]`. 336 337 * `:do` - contains the implementation to be invoked in case 338 the route matches. 339 340 * `:to` - a Plug that will be called in case the route matches. 341 342 * `:init_opts` - the options for the target Plug given by `:to`. 343 344 A route should specify only one of `:do` or `:to` options. 345 """ 346 defmacro match(path, options, contents \\ []) do 347 compile(nil, path, options, contents, __CALLER__) 348 end 349 350 @doc """ 351 Dispatches to the path only if the request is a GET request. 352 See `match/3` for more examples. 353 """ 354 defmacro get(path, options, contents \\ []) do 355 compile(:get, path, options, contents, __CALLER__) 356 end 357 358 @doc """ 359 Dispatches to the path only if the request is a HEAD request. 360 See `match/3` for more examples. 361 """ 362 defmacro head(path, options, contents \\ []) do 363 compile(:head, path, options, contents, __CALLER__) 364 end 365 366 @doc """ 367 Dispatches to the path only if the request is a POST request. 368 See `match/3` for more examples. 369 """ 370 defmacro post(path, options, contents \\ []) do 371 compile(:post, path, options, contents, __CALLER__) 372 end 373 374 @doc """ 375 Dispatches to the path only if the request is a PUT request. 376 See `match/3` for more examples. 377 """ 378 defmacro put(path, options, contents \\ []) do 379 compile(:put, path, options, contents, __CALLER__) 380 end 381 382 @doc """ 383 Dispatches to the path only if the request is a PATCH request. 384 See `match/3` for more examples. 385 """ 386 defmacro patch(path, options, contents \\ []) do 387 compile(:patch, path, options, contents, __CALLER__) 388 end 389 390 @doc """ 391 Dispatches to the path only if the request is a DELETE request. 392 See `match/3` for more examples. 393 """ 394 defmacro delete(path, options, contents \\ []) do 395 compile(:delete, path, options, contents, __CALLER__) 396 end 397 398 @doc """ 399 Dispatches to the path only if the request is an OPTIONS request. 400 See `match/3` for more examples. 401 """ 402 defmacro options(path, options, contents \\ []) do 403 compile(:options, path, options, contents, __CALLER__) 404 end 405 406 @doc """ 407 Forwards requests to another Plug. The `path_info` of the forwarded 408 connection will exclude the portion of the path specified in the 409 call to `forward`. If the path contains any parameters, those will 410 be available in the target Plug in `conn.params` and `conn.path_params`. 411 412 ## Options 413 414 `forward` accepts the following options: 415 416 * `:to` - a Plug the requests will be forwarded to. 417 * `:init_opts` - the options for the target Plug. It is the preferred 418 mechanism for passing options to the target Plug. 419 * `:host` - a string representing the host or subdomain, exactly like in 420 `match/3`. 421 * `:private` - values for `conn.private`, exactly like in `match/3`. 422 * `:assigns` - values for `conn.assigns`, exactly like in `match/3`. 423 424 If `:init_opts` is undefined, then all remaining options are passed 425 to the target plug. 426 427 ## Examples 428 429 forward "/users", to: UserRouter 430 431 Assuming the above code, a request to `/users/sign_in` will be forwarded to 432 the `UserRouter` plug, which will receive what it will see as a request to 433 `/sign_in`. 434 435 forward "/foo/:bar/qux", to: FooPlug 436 437 Here, a request to `/foo/BAZ/qux` will be forwarded to the `FooPlug` 438 plug, which will receive what it will see as a request to `/`, 439 and `conn.params["bar"]` will be set to `"BAZ"`. 440 441 Some other examples: 442 443 forward "/foo/bar", to: :foo_bar_plug, host: "foobar." 444 forward "/baz", to: BazPlug, init_opts: [plug_specific_option: true] 445 446 """ 447 defmacro forward(path, options) do 448 quote bind_quoted: [path: path, options: options] do 449 {target, options} = Keyword.pop(options, :to) 450 {options, plug_options} = Keyword.split(options, [:via, :host, :private, :assigns]) 451 plug_options = Keyword.get(plug_options, :init_opts, plug_options) 452 453 if is_nil(target) or not is_atom(target) do 454 raise ArgumentError, message: "expected :to to be an alias or an atom" 455 end 456 457 {target, target_opts} = 458 case Atom.to_string(target) do 459 "Elixir." <> _ -> {target, target.init(plug_options)} 460 _ -> {{__MODULE__, target}, plug_options} 461 end 462 463 @plug_forward_target target 464 @plug_forward_opts target_opts 465 466 # Delegate the matching to the match/3 macro along with the options 467 # specified by Keyword.split/2. 468 match path <> "/*glob", options do 469 Plug.forward( 470 var!(conn), 471 var!(glob), 472 @plug_forward_target, 473 @plug_forward_opts 474 ) 475 end 476 end 477 end 478 479 ## Match Helpers 480 481 @doc false 482 def __route__(method, path, guards, options) do 483 {method, guards} = build_methods(List.wrap(method || options[:via]), guards) 484 {params, match, guards, post_match} = Plug.Router.Utils.build_path_clause(path, guards) 485 params = Plug.Router.Utils.build_path_params_match(params) 486 private = extract_merger(options, :private) 487 assigns = extract_merger(options, :assigns) 488 host_match = Plug.Router.Utils.build_host_match(options[:host]) 489 {quote(do: conn), method, match, post_match, params, host_match, guards, private, assigns} 490 end 491 492 @doc false 493 def __put_route__(conn, path, fun) do 494 Plug.Conn.put_private(conn, :plug_route, {append_match_path(conn, path), fun}) 495 end 496 497 defp append_match_path(%Plug.Conn{private: %{plug_route: {base_path, _}}}, path) do 498 base_path <> path 499 end 500 501 defp append_match_path(%Plug.Conn{}, path) do 502 path 503 end 504 505 # Entry point for both forward and match that is actually 506 # responsible to compile the route. 507 defp compile(method, expr, options, contents, caller) do 508 {callback, options} = 509 cond do 510 Keyword.has_key?(contents, :do) -> 511 {wrap_function_do(contents[:do]), expand_options(options, caller)} 512 513 Keyword.has_key?(options, :do) -> 514 {body, options} = Keyword.pop(options, :do) 515 {wrap_function_do(body), expand_options(options, caller)} 516 517 options[:to] -> 518 options = expand_options(options, caller) 519 520 callback = 521 quote unquote: false do 522 &(unquote(callback) / 2) 523 end 524 525 options = 526 quote do 527 {callback, options} = Plug.Router.__to__(unquote(caller.module), unquote(options)) 528 options 529 end 530 531 {callback, options} 532 533 true -> 534 raise ArgumentError, message: "expected one of :to or :do to be given as option" 535 end 536 537 {path, guards} = extract_path_and_guards(expr) 538 539 quote bind_quoted: [ 540 method: method, 541 path: path, 542 options: options, 543 guards: Macro.escape(guards, unquote: true), 544 callback: Macro.escape(callback, unquote: true) 545 ] do 546 route = Plug.Router.__route__(method, path, guards, options) 547 {conn, method, match, post_match, params, host, guards, private, assigns} = route 548 549 defp do_match(unquote(conn), unquote(method), unquote(match), unquote(host)) 550 when unquote(guards) do 551 unquote_splicing(post_match) 552 unquote(private) 553 unquote(assigns) 554 555 params = unquote({:%{}, [], params}) 556 557 merge_params = fn 558 %Plug.Conn.Unfetched{} -> params 559 fetched -> Map.merge(fetched, params) 560 end 561 562 conn = update_in(unquote(conn).params, merge_params) 563 conn = update_in(conn.path_params, merge_params) 564 565 Plug.Router.__put_route__(conn, unquote(path), unquote(callback)) 566 end 567 end 568 end 569 570 @doc false 571 def __to__(module, options) do 572 {to, options} = Keyword.pop(options, :to) 573 {init_opts, options} = Keyword.pop(options, :init_opts, []) 574 575 router_to = Module.get_attribute(module, :plug_router_to) 576 callback = :"plug_router_to_#{map_size(router_to)}" 577 router_to = Map.put(router_to, callback, {to, init_opts}) 578 Module.put_attribute(module, :plug_router_to, router_to) 579 {Macro.var(callback, nil), options} 580 end 581 582 defp wrap_function_do(body) do 583 quote do 584 fn var!(conn), var!(opts) -> 585 _ = var!(opts) 586 unquote(body) 587 end 588 end 589 end 590 591 defp expand_options(opts, caller) do 592 if Macro.quoted_literal?(opts) do 593 Macro.prewalk(opts, &expand_alias(&1, caller)) 594 else 595 opts 596 end 597 end 598 599 defp expand_alias({:__aliases__, _, _} = alias, env), 600 do: Macro.expand(alias, %{env | function: {:init, 1}}) 601 602 defp expand_alias(other, _env), do: other 603 604 defp extract_merger(options, key) when is_list(options) do 605 if option = Keyword.get(options, key) do 606 quote do 607 conn = update_in(conn.unquote(key), &Map.merge(&1, unquote(Macro.escape(option)))) 608 end 609 end 610 end 611 612 # Convert the verbs given with `:via` into a variable and guard set that can 613 # be added to the dispatch clause. 614 defp build_methods([], guards) do 615 {quote(do: _), guards} 616 end 617 618 defp build_methods([method], guards) do 619 {Plug.Router.Utils.normalize_method(method), guards} 620 end 621 622 defp build_methods(methods, guards) do 623 methods = Enum.map(methods, &Plug.Router.Utils.normalize_method(&1)) 624 var = quote do: method 625 guards = join_guards(quote(do: unquote(var) in unquote(methods)), guards) 626 {var, guards} 627 end 628 629 defp join_guards(fst, true), do: fst 630 defp join_guards(fst, snd), do: quote(do: unquote(fst) and unquote(snd)) 631 632 # Extract the path and guards from the path. 633 defp extract_path_and_guards({:when, _, [path, guards]}), do: {extract_path(path), guards} 634 defp extract_path_and_guards(path), do: {extract_path(path), true} 635 636 defp extract_path({:_, _, var}) when is_atom(var), do: "/*_path" 637 defp extract_path(path), do: path 638 end