Skip to content
This repository was archived by the owner on Mar 10, 2025. It is now read-only.

Commit 75b822d

Browse files
committed
Merge pull request #3 from DevL/additional_options
Enable extraction multiple headers and configure callback for missing headers.
2 parents 230aacb + 261dcaa commit 75b822d

File tree

5 files changed

+160
-46
lines changed

5 files changed

+160
-46
lines changed

README.md

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ An Elixir Plug for requiring and extracting a given header.
1111
Update your `mix.exs` file and run `mix deps.get`.
1212
```elixir
1313
defp deps do
14-
[{:plug_require_header, "~> 0.2"}]
14+
[{:plug_require_header, "~> 0.3"}]
1515
end
1616
```
1717

@@ -21,25 +21,51 @@ defmodule MyPhoenixApp.MyController do
2121
use MyPhoenixApp.Web, :controller
2222
alias Plug.Conn.Status
2323

24-
plug PlugRequireHeader, api_key: "x-api-key"
24+
plug PlugRequireHeader, headers: [api_key: "x-api-key"]
2525
plug :action
2626

2727
def index(conn, _params) do
2828
conn
29-
|> put_status Status.code(:ok)
29+
|> put_status(Status.code :ok)
3030
|> text "The API key used is: #{conn.assigns[:api_key]}"
3131
end
3232
end
3333
```
3434
Notice how the first value required header `"x-api-key"` has been extracted and can be retrieved using `conn.assigns[:api_key]`. An alternative is to use `Plug.Conn.get_req_header/2` to get all the values associated with a given header.
3535

36-
By default, a missing header will return a status code of 403 (forbidden) and halt the plug pipeline, i.e. no subsequent plugs will be executed. The same is true if the required header is explicitly set to nil. This behaviour is to be configurable in a future version.
36+
By default, a missing header will return a status code of 403 (forbidden) and halt the plug pipeline, i.e. no subsequent plugs will be executed. The same is true if the required header is explicitly set to `nil`. This behaviour however is configurable.
37+
```elixir
38+
defmodule MyPhoenixApp.MyOtherController do
39+
use MyPhoenixApp.Web, :controller
40+
alias Plug.Conn.Status
41+
42+
plug PlugRequireHeader, headers: [api_key: "x-api-key"], on_missing: {__MODULE__, :handle_missing_header}
43+
plug :action
44+
45+
def index(conn, _params) do
46+
conn
47+
|> put_status(Status.code :ok)
48+
|> text "The API key used is: #{conn.assigns[:api_key]}"
49+
end
50+
51+
def handle_missing_header(conn, missing_header_key) do
52+
conn
53+
|> send_resp(Status.code(:bad_request), "Missing header: #{missing_header_key}")
54+
|> halt
55+
end
56+
end
57+
```
58+
If the header is missing or set to `nil` the status code, a status code of 400 (bad request) will be returned before the plug pipeline is halted. Notice that the function specified as a callback needs to be a public function as it'll be invoked from another module.
59+
60+
Lastly, it's possible to extract multiple headers at the same time.
61+
62+
```elixir
63+
plug PlugRequireHeader, headers: [api_key: "x-api-key", magic: "x-magic"]
64+
```
3765

3866
## Planned features
3967

40-
* Require and extract multiple header keys and not just one.
41-
* Make the action taken when a required header is missing configurable.
68+
* Make the action taken when a required header is missing more configurable.
4269
* given an atom -> look up the Plug.Conn.Status code.
4370
* given an integer -> treat it as a status code.
44-
* given a function -> invoke the function and pass it the `conn` struct and the missing header/connection key pair.
4571
* configurable responses and content-types, e.g. JSON.

lib/plug_require_header.ex

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ defmodule PlugRequireHeader do
22
import Plug.Conn
33
alias Plug.Conn.Status
44

5-
@vsn "0.2.1"
5+
@vsn "0.3.0"
66
@doc false
77
def version, do: @vsn
88

@@ -11,37 +11,68 @@ defmodule PlugRequireHeader do
1111
"""
1212

1313
@doc """
14-
Initialises the plug given a keyword list of the following format.
14+
Initialises the plug given a keyword list.
15+
"""
16+
def init(options), do: options
17+
18+
@doc """
19+
Extracts the required headers and assigns them to the connection struct.
20+
21+
## Arguments
22+
23+
`conn` - the Plug.Conn connection struct
24+
`options` - a keyword list broken down into mandatory and optional options
25+
26+
### Mandatory options
27+
28+
`:headers` - a keyword list of connection key and header key pairs.
29+
Each pair has the format `[<connection_key>: <header_key>]` where
30+
* the `<connection_key>` atom is the connection key to assign the value of
31+
the header.
32+
* the `<header_key>` binary is the header key to be required and extracted.
1533
16-
[<connection_key>: <header_key>]
34+
### Optional options
1735
18-
* The `<connection_key>` atom is the connection key to assign the value of the header.
19-
* The `<header_key>` binary is the header key to be required and extracted.
36+
`:on_missing` - specifies how to handle a missing header. It can be one of
37+
the following:
38+
39+
* a callback function with and arity of 2, specified as a tuple of
40+
`{module, function}`. The function will be called with the `conn` struct
41+
and the missing header key. Notice that the callback may be invoked once
42+
per required header.
2043
"""
21-
def init(options) do
22-
options |> List.first
44+
def call(conn, options) do
45+
callback = on_missing(Keyword.fetch options, :on_missing)
46+
headers = Keyword.fetch! options, :headers
47+
extract_header_keys(conn, headers, callback)
2348
end
2449

25-
@doc """
26-
Extracts the required headers and assign them to the connection struct.
27-
"""
28-
def call(conn, {connection_key, header_key}) do
29-
extract_header_key(conn, connection_key, header_key)
50+
defp on_missing({:ok, {module, function}}) do
51+
fn (conn, missing_header_key) ->
52+
apply module, function, [conn, missing_header_key]
53+
end
54+
end
55+
defp on_missing(_), do: &halt_connection/2
56+
57+
defp extract_header_keys(conn, [], _callback), do: conn
58+
defp extract_header_keys(conn, [header|remaining_headers], callback) do
59+
extract_header_key(conn, header, callback)
60+
|> extract_header_keys(remaining_headers, callback)
3061
end
3162

32-
defp extract_header_key(conn, connection_key, header_key) do
63+
defp extract_header_key(conn, {connection_key, header_key}, callback) do
3364
case List.keyfind(conn.req_headers, header_key, 0) do
34-
{^header_key, nil} -> halt_connection(conn)
65+
{^header_key, nil} -> callback.(conn, header_key)
3566
{^header_key, value} -> assign_connection_key(conn, connection_key, value)
36-
_ -> halt_connection(conn)
67+
_ -> callback.(conn, header_key)
3768
end
3869
end
3970

4071
defp assign_connection_key(conn, key, value) do
4172
conn |> assign(key, value)
4273
end
4374

44-
defp halt_connection(conn) do
75+
defp halt_connection(conn, _) do
4576
conn
4677
|> send_resp(Status.code(:forbidden), "")
4778
|> halt

mix.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ defmodule PlugRequireHeader.Mixfile do
44
def project do
55
[
66
app: :plug_require_header,
7-
version: "0.2.1",
7+
version: "0.3.0",
88
name: "PlugRequireHeader",
99
source_url: "https://github.com/DevL/plug_require_header",
1010
elixir: "~> 1.0",
@@ -22,7 +22,7 @@ defmodule PlugRequireHeader.Mixfile do
2222

2323
defp package do
2424
[
25-
contributors: ["Lennart Fridén"],
25+
contributors: ["Lennart Fridén", "Kim Persson"],
2626
files: ["lib", "mix.exs", "README*", "LICENSE*"],
2727
licenses: ["MIT"],
2828
links: %{"GitHub" => "https://github.com/DevL/plug_require_header"}

test/plug_require_header_test.exs

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,55 +3,76 @@ defmodule PlugRequireHeaderTest do
33
use Plug.Test
44
alias Plug.Conn.Status
55

6-
@options TestApp.init([])
7-
86
test "block request missing the required header" do
97
connection = conn(:get, "/")
10-
response = TestApp.call(connection, @options)
8+
response = TestApp.call(connection, [])
119

1210
assert response.status == Status.code(:forbidden)
1311
assert response.resp_body == ""
1412
end
1513

1614
test "block request with a header set, but without the required header" do
1715
connection = conn(:get, "/") |> put_req_header("x-wrong-header", "whatever")
18-
response = TestApp.call(connection, @options)
16+
response = TestApp.call(connection, [])
1917

2018
assert response.status == Status.code(:forbidden)
2119
assert response.resp_body == ""
2220
end
2321

2422
test "block request with the required header set to nil" do
2523
connection = conn(:get, "/") |> put_nil_header("x-api-key")
26-
response = TestApp.call(connection, @options)
24+
response = TestApp.call(connection, [])
2725

2826
assert response.status == Status.code(:forbidden)
2927
assert response.resp_body == ""
3028
end
3129

3230
test "extract the required header and assign it to the connection" do
33-
api_key = "12345"
34-
35-
connection = conn(:get, "/") |> put_req_header("x-api-key", api_key)
36-
response = TestApp.call(connection, @options)
31+
connection = conn(:get, "/") |> put_req_header("x-api-key", "12345")
32+
response = TestApp.call(connection, [])
3733

3834
assert response.status == Status.code(:ok)
39-
assert response.resp_body == api_key
35+
assert response.resp_body == "API key: 12345"
4036
end
4137

4238
test "extract the required header even if multiple headers are set" do
43-
api_key = "12345"
44-
4539
connection = conn(:get, "/")
46-
|> put_req_header("x-api-key", api_key)
40+
|> put_req_header("x-api-key", "12345")
4741
|> put_req_header("x-wrong-header", "whatever")
48-
response = TestApp.call(connection, @options)
42+
response = TestApp.call(connection, [])
43+
44+
assert response.status == Status.code(:ok)
45+
assert response.resp_body == "API key: 12345"
46+
end
47+
48+
test "invoke a callback function if the required header is missing" do
49+
connection = conn(:get, "/")
50+
response = TestAppWithCallback.call(connection, [])
51+
52+
assert response.status == Status.code(:precondition_failed)
53+
assert response.resp_body == "Missing header: x-api-key"
54+
end
55+
56+
test "extract multiple required headers" do
57+
connection = conn(:get, "/")
58+
|> put_req_header("x-api-key", "12345")
59+
|> put_req_header("x-secret", "handshake")
60+
response = TestAppWithCallbackAndMultipleRequiredHeaders.call(connection, [])
4961

5062
assert response.status == Status.code(:ok)
51-
assert response.resp_body == api_key
63+
assert response.resp_body == "API key: 12345 and the secret handshake"
64+
end
65+
66+
test "invoke a callback function if any of the required headers are missing" do
67+
connection = conn(:get, "/")
68+
|> put_req_header("x-api-key", "12345")
69+
response = TestAppWithCallbackAndMultipleRequiredHeaders.call(connection, [])
70+
71+
assert response.status == Status.code(:bad_request)
72+
assert response.resp_body == "Missing header: x-secret"
5273
end
5374

54-
defp put_nil_header(%Plug.Conn{req_headers: headers} = conn, key) when is_binary(key) do
75+
defp put_nil_header(%Plug.Conn{req_headers: headers} = conn, key) when is_binary(key) do
5576
%{conn | req_headers: :lists.keystore(key, 1, headers, {key, nil})}
5677
end
5778
end

test/test_helper.exs

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,50 @@
11
ExUnit.start()
22

3+
defmodule AppMaker do
4+
defmacro __using__(options) do
5+
quote do
6+
use Plug.Router
7+
alias Plug.Conn.Status
8+
9+
plug PlugRequireHeader, unquote(options)
10+
plug :match
11+
plug :dispatch
12+
end
13+
end
14+
end
15+
316
defmodule TestApp do
4-
use Plug.Router
5-
alias Plug.Conn.Status
17+
use AppMaker, headers: [api_key: "x-api-key"]
618

7-
plug PlugRequireHeader, api_key: "x-api-key"
8-
plug :match
9-
plug :dispatch
19+
get "/" do
20+
send_resp(conn, Status.code(:ok), "API key: #{conn.assigns[:api_key]}")
21+
end
22+
end
23+
24+
defmodule TestAppWithCallback do
25+
use AppMaker, headers: [api_key: "x-api-key"], on_missing: {__MODULE__, :callback}
1026

1127
get "/" do
1228
send_resp(conn, Status.code(:ok), "#{conn.assigns[:api_key]}")
1329
end
30+
31+
def callback(conn, missing_header_key) do
32+
conn
33+
|> send_resp(Status.code(:precondition_failed), "Missing header: #{missing_header_key}")
34+
|> halt
35+
end
36+
end
37+
38+
defmodule TestAppWithCallbackAndMultipleRequiredHeaders do
39+
use AppMaker, headers: [api_key: "x-api-key", secret: "x-secret"], on_missing: {__MODULE__, :callback}
40+
41+
get "/" do
42+
send_resp(conn, Status.code(:ok), "API key: #{conn.assigns[:api_key]} and the secret #{conn.assigns[:secret]}")
43+
end
44+
45+
def callback(conn, missing_header_key) do
46+
conn
47+
|> send_resp(Status.code(:bad_request), "Missing header: #{missing_header_key}")
48+
|> halt
49+
end
1450
end

0 commit comments

Comments
 (0)