Filters

plumber supports both “endpoints” and “filters.” Endpoints use an annotation like @get or @post and are the serving functions you’re probably accustomed to seeing in plumber. An incoming request will go through each of the available endpoints until it finds one that is willing to serve it. A request will only be served by a single endpoint (the first one it encounters that it “matches.” Read more about endpoints here.

Filters in plumber behave differently. A request may go through multiple filters before it is served by an endpoint. Thus, filters are your opportunity to transform the request as it passes through – either modifying existing information or supplementing it with additional info. All the filters in your file will be evaluated in the order in which they’re defined*. In the example below, you’ll see two filters: auth-user and require-auth.

Example Filters

The definition of auth-user is contained on lines 11-34. It offers a simple, albeit silly, mechanism for determing whether or not a request is coming from a logged in user. Real authentication systems would rely on encrypted cookies or session tokens, but this filter merely looks to see if the request has a parameter named username which would be provided by our built-in query string filters. So a request like http://https://demo.rplumber.io/filters/about?username=john would pass in the username of john to this function, while a request without that parameter would leave username empty. So this filter examines the provided username and, if it finds one, looks it up in our “user database” (in this case, a data.frame defined on lines 1-5). If it doesn’t find the username in the database, it stop()s to indicate that an error has occurred when processing this request. If it does find the username in the database, it modifies the req object as it’s passing through. This is one of the key tricks of filters – filters allow you to attach new data on the req object as it’s passing through and that new/updated data will be available to later filters and any endpoints this request encounters.

The next filter is named require-auth and is defined on lines 38-48. This filter goes one step further than the previous filter; it doesn’t just look to see if the username was provided, it requires that the user is logged in. If the user is not logged in, then it doesn’t forward() the request. forward() is a critical call in plumber filters. When you call forward() in a filter, you’re telling plumber to continue on in the flow of processing the request – i.e. whatever remaining filters and endpoint are available for this request will be used. If you do not call forward() in a filter, plumber assumes that your filter has finished processing the request itself, and will not continue the execution with the remaining filters and endpoints. In this example, lines 42-43 show an example of this. You’ll notice that if req$user is null, then we set the HTTP status of the response to 401 (a status code which means that authentication is required), then we return a list that has an error field in it, and we do not forward(). This means that whatever value was returned should be sent directly back to the user without any further evaluation. If you get to the require-auth filter and are not already authenticated (i.e. there is not user field added to your req), then you will proceed no further.

Example Endpoints and @preempt

We define two endpoints in the example below. The endpoint corresponding to @get /me is defined on lines 51-56 and is just like any other endpoint you’ve seen before. However, keep in mind that this endpoint will be evaluated after all of the available filters have been executed! Thus, if the request encountered an error in auth-user (from an invalid username) or didn’t pass through require-auth (because a username wasn’t provided), then it would never make it to this point. Thus, it’s safe to assume that if this endpoint is executing, the request must have satisfactorily passed through whatever filters exist in front of it.

Lastly, we have the @get /about endpoint on lines 64-66. This endpoint looks similar to a traditional endpoints, but has one special annotation on line 62 named @preempt. @preempt is a way of telling plumber to position this filter in front of some defined filter. So plumber now knows to execute this endpoint before executing the require-auth filter. All other filters up to that point will be evaluated as usual, but an incoming request will have an opportunity to be served by this preemptive endpoint before the require-auth filter runs. If the request can’t be served by this endpoint (i.e. it’s not a GET request for /about), then it will continue on through the remaining filters until it finds an endpoint that can serve it.

Try changing the username to see how it affects the results from the GET requests below.

* This is subject to change. For now, you should put define all of your filters above any endpoint in your R file. See this GitHub issue for more details.

Set Username

Select username for this request:

GET /about


          
Loading....

GET /me


        
Loading...

filters-example.R

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
        users <- data.frame(
  id=1:2,
  username=c("joe", "kim"),
  groups=c("users", "admin,users")
)

#* Filter that grabs the "username" querystring parameter.
#* You should, of course, use a real auth system, but
#* this shows the principles involved.
#* @filter auth-user
function(req, username=""){
  # Since username is a querystring param, we can just
  # expect it to be available as a parameter to the
  # filter (plumber magic).

  # This is a work-around for https://github.com/trestletech/plumber/issues/12
  # and shouldn't be necessary long-term
  req$user <- NULL

  if (username == ""){
    # No username provided
  } else if (username %in% users$username){
    # username is valid

    req$user <- users[users$username == username,]

  } else {
    # username was provided, but invalid
    stop("No such username: ", username)
  }

  # Continue on
  forward()
}

#* Now require that all users must be authenticated.
#* @filter require-auth
function(req, res){
  if (is.null(req$user)){
    # User isn't logged in

    res$status <- 401 # Unauthorized
    list(error="You must login to access this resource.")
  } else {
    # user is logged in. Move on...
    forward()
  }
}

#* @get /me
function(req){
  # Safe to assume we have a user, since we've been
  # through all the filters and would have gotten an
  # error earlier if we weren't.
  list(user=req$user)
}

#* Get info about the service. We preempt the
#* require-auth filter because we want authenticated and
#* unauthenticated users alike to be able to access this
#* endpoint.
#* @preempt require-auth
#* @get /about
function(){
  list(descript="This is a demo service that uses authentication!")
}