This file contains a general toolkit for routing and combining bits of HTTP-handling logic. It is similar in spirit to Rook (and Rack, and WSGI, and Connect, and…) but adds cascading and routing.
This file is called “middleware” because that’s the term used for these bits of logic in these other frameworks. However, our code uses the word “handler” so we’ll stick to that for the rest of this document; just know that they’re basically the same concept.
A handler (or sometimes, httpHandler) is a function that takes a req
parameter–a request object as described in the Rook specification–and returns NULL
, or an httpResponse
.
httpResponse <- function(status = 200,
content_type = "text/html; charset=UTF-8",
content = "",
headers = list()) {
# Make sure it's a list, not a vector
headers <- as.list(headers)
if (is.null(headers$`X-UA-Compatible`))
headers$`X-UA-Compatible` <- "chrome=1"
resp <- list(status = status, content_type = content_type, content = content,
headers = headers)
class(resp) <- 'httpResponse'
return(resp)
}
You can think of a web application as being simply an aggregation of these functions, each of which performs one kind of duty. Each handler in turn gets a look at the request and can decide whether it knows how to handle it. If so, it returns an httpResponse
and processing terminates; if not, it returns NULL
and the next handler gets to execute. If the final handler returns NULL
, a 404 response should be returned.
We have a similar construct for websockets: websocket handlers or wsHandlers. These take a single ws
argument which is the websocket connection that was just opened, and they can either return TRUE
if they are handling the connection, and NULL
to pass responsibility on to the next wsHandler.
Since it’s so common for httpHandlers to be invoked in this “cascading” fashion, we’ll introduce a function that takes zero or more handlers and returns a single handler. And while we’re at it, making a directory of static content available is such a common thing to do, we’ll allow strings representing paths to be used instead of handlers; any such strings we encounter will be converted into staticHandler
objects.
joinHandlers <- function(handlers) {
# Zero handlers; return a null handler
if (length(handlers) == 0)
return(function(req) NULL)
# Just one handler (function)? Return it.
if (is.function(handlers))
return(handlers)
handlers <- lapply(handlers, function(h) {
if (is.character(h))
return(staticHandler(h))
else
return(h)
})
# Filter out NULL
handlers <- handlers[!sapply(handlers, is.null)]
if (length(handlers) == 0)
return(function(req) NULL)
if (length(handlers) == 1)
return(handlers[[1]])
function(req) {
for (handler in handlers) {
response <- handler(req)
if (!is.null(response))
return(response)
}
return(NULL)
}
}
Note that we don’t have an equivalent of joinHandlers
for wsHandlers. It’s easy to imagine it, we just haven’t needed one.
Handlers do not have a built-in notion of routing. Conceptually, given a list of handlers, all the handlers are peers and they all get to see every request (well, up until the point that a handler returns a response).
You could implement routing in each handler by checking the request’s PATH_INFO
field, but since it’s such a common need, let’s make it simple by introducing a routeHandler
function. This is a handler decorator and it’s responsible for 1) filtering out requests that don’t match the given route, and 2) temporarily modifying the request object to take the matched part of the route off of the PATH_INFO
(and add it to the end of SCRIPT_NAME
). This way, the handler doesn’t need to figure out about what part of its URL path has already been matched via routing.
(BTW, it’s safe for routeHandler
calls to nest.)
routeHandler <- function(prefix, handler) {
force(prefix)
force(handler)
if (identical("", prefix))
return(handler)
if (length(prefix) != 1 || !isTRUE(grepl("^/[^\\]+$", prefix))) {
stop("Invalid URL prefix \"", prefix, "\"")
}
pathPattern <- paste("^\\Q", prefix, "\\E/", sep = "")
function(req) {
if (isTRUE(grepl(pathPattern, req$PATH_INFO))) {
origScript <- req$SCRIPT_NAME
origPath <- req$PATH_INFO
on.exit({
req$SCRIPT_NAME <- origScript
req$PATH_INFO <- origPath
}, add = TRUE)
pathInfo <- substr(req$PATH_INFO, nchar(prefix)+1, nchar(req$PATH_INFO))
req$SCRIPT_NAME <- paste(req$SCRIPT_NAME, prefix, sep = "")
req$PATH_INFO <- pathInfo
return(handler(req))
} else {
return(NULL)
}
}
}
We have a version for websocket handlers as well. Pity about the copy/paste job.
routeWSHandler <- function(prefix, wshandler) {
force(prefix)
force(wshandler)
if (identical("", prefix))
return(wshandler)
if (length(prefix) != 1 || !isTRUE(grepl("^/[^\\]+$", prefix))) {
stop("Invalid URL prefix \"", prefix, "\"")
}
pathPattern <- paste("^\\Q", prefix, "\\E/", sep = "")
function(ws) {
req <- ws$request
if (isTRUE(grepl(pathPattern, req$PATH_INFO))) {
origScript <- req$SCRIPT_NAME
origPath <- req$PATH_INFO
on.exit({
req$SCRIPT_NAME <- origScript
req$PATH_INFO <- origPath
}, add = TRUE)
pathInfo <- substr(req$PATH_INFO, nchar(prefix)+1, nchar(req$PATH_INFO))
req$SCRIPT_NAME <- paste(req$SCRIPT_NAME, prefix, sep = "")
req$PATH_INFO <- pathInfo
return(wshandler(ws))
} else {
return(NULL)
}
}
}
Now let’s actually write some handlers. Note that these functions aren’t themselves handlers, you call them and they return a handler. Handler factory functions, if you will.
Here’s one that serves up static assets from a directory.
staticHandler <- function(root) {
force(root)
return(function(req) {
if (!identical(req$REQUEST_METHOD, 'GET'))
return(NULL)
path <- req$PATH_INFO
if (is.null(path))
return(httpResponse(400, content="<h1>Bad Request</h1>"))
if (path == '/')
path <- '/index.html'
abs.path <- resolve(root, path)
if (is.null(abs.path))
return(NULL)
ext <- tools::file_ext(abs.path)
content.type <- getContentType(ext)
response.content <- readBin(abs.path, 'raw', n=file.info(abs.path)$size)
return(httpResponse(200, content.type, response.content))
})
}
The handler manager gives you a place to register handlers (of both http and websocket varieties) and provides an httpuv-compatible set of callbacks for invoking them.
Create one of these, make zero or more calls to addHandler
and addWSHandler
methods (order matters–first one wins!), and then pass the return value of createHttpuvApp
to httpuv’s startServer
function.
HandlerManager <- setRefClass("HandlerManager",
fields = list(
handlers = "list",
wsHandlers = "list"
),
methods = list(
addHandler = function(handler) {
if (length(handlers) == 0)
handlers <<- list(handler)
else
handlers <<- c(handlers, list(handler))
},
addWSHandler = function(wsHandler) {
if (length(wsHandlers) == 0)
wsHandlers <<- list(wsHandler)
else
wsHandlers <<- c(wsHandlers, list(wsHandler))
},
clear = function() {
handlers <<- list()
wsHandlers <<- list()
},
createHttpuvApp = function() {
list(
onHeaders = function(req) {
maxSize <- getOption('shiny.maxRequestSize', 5 * 1024 * 1024)
if (maxSize <= 0)
return(NULL)
reqSize <- 0
if (length(req$CONTENT_LENGTH) > 0)
reqSize <- as.numeric(req$CONTENT_LENGTH)
else if (length(req$HTTP_TRANSFER_ENCODING) > 0)
reqSize <- Inf
if (reqSize > maxSize) {
return(list(status = 413L,
headers = list(
'Content-Type' = 'text/plain'
),
body = 'Maximum upload size exceeded'))
}
else {
return(NULL)
}
},
call = .httpServer(
function (req) {
for (handler in handlers) {
result <- handler(req)
if (!is.null(result))
return(result)
}
return(NULL)
},
getOption('shiny.sharedSecret', NULL)
),
onWSOpen = function(ws) {
for (wsHandler in wsHandlers) {
result <- wsHandler(ws)
if (!is.null(result))
return(result)
}
return(NULL)
}
)
},
.httpServer = function(handler, sharedSecret) {
filter <- getOption('shiny.http.response.filter', NULL)
if (is.null(filter))
filter <- function(req, response) response
function(req) {
if (!is.null(sharedSecret)
&& !identical(sharedSecret, req$HTTP_SHINY_SHARED_SECRET)) {
return(list(status=403,
body='<h1>403 Forbidden</h1><p>Shared secret mismatch</p>',
headers=list('Content-Type' = 'text/html')))
}
response <- handler(req)
if (is.null(response))
response <- httpResponse(404, content="<h1>Not Found</h1>")
headers <- as.list(response$headers)
headers$'Content-Type' <- response$content_type
response <- filter(req, response)
return(list(status=response$status,
body=response$content,
headers=headers))
}
}
)
)
See server.R to see actual implementation and usage of handlers in the context of Shiny.