Skip to content

Instantly share code, notes, and snippets.

@jcheng5
Last active February 2, 2024 10:10
  • Star 19 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save jcheng5/2aaff19e67079840350d08361fe7fb20 to your computer and use it in GitHub Desktop.
Accepting POST requests from Shiny

Accepting POST requests from Shiny

(This post was motivated by a talk by @jnolis at CascadiaRConf 2021)

Recent versions of Shiny have an undocumented feature for handling POST requests that are not associated with any specific Shiny session. (Note that this functionality is missing our normal level of polish; it's a fairly low-level hook that I needed to make some things possible, but doesn't make anything easy.)

In a nutshell, it works by replacing your traditional ui object with a function(req), and then marking that function with an attribute indicating that it knows how to handle both GET and POST:

library(shiny)

ui <- function(req) {
  # The `req` object is a Rook environment
  # See https://github.com/jeffreyhorner/Rook#the-environment
  if (identical(req$REQUEST_METHOD, "GET")) {
    fluidPage(
      # as normal...
    )
  } else if (identical(req$REQUEST_METHOD, "POST")) {
    # Handle the POST
    query_params <- parseQueryString(req$QUERY_STRING)
    body_bytes <- req$rook.input$read(-1)
    
    # Be sure to return a response
    httpResponse(
      status = 200L,
      content_type = "application/json",
      content = '{"status": "ok"}'
    )
  }
}
attr(ui, "http_methods_supported") <- c("GET", "POST")

server <- function(input, output, session) {
  # same as usual
}

shinyApp(ui, server)

Whether the request is a GET or POST, the response from the ui function can either be:

  • A normal Shiny UI object, which will be handled in the usual way
  • A shiny::httpResponse() object, which will be returned to the client verbatim(-ish)
  • NULL if the request is not applicable; Shiny will fall back to other logic, and ultimately return 404 if appropriate
  • Async is supported, you can return a promise that resolves to any of the above

Handling URL paths besides /

The example above works with GET or POST, but only on the root (/) URL path. If you want to handle GET and/or POST requests on other URLs, like /callback or whatever, the shinyApp function has a uiPattern argument that you can use to control exactly which URL paths should be routed to the ui function (for example, uiPattern = ".*"). The ui function can then look at req$PATH_INFO to see what the actual URL path was.

@mdoucleff
Copy link

Please note that Shiny Server rejects POST requests with 400 response codes. I've been unable to find a way to disable this behavior.
So if Shiny Server is part of your setup, your Shiny process will never see the POST request.

@jcheng5
Copy link
Author

jcheng5 commented Jan 24, 2023

@mdoucleff Hmmm, I'm not able to reproduce that. Using the example in the gist and the latest Shiny Server, I get this:

jcheng@barcelona:~/Development/shiny-server2$ curl -vX POST http://localhost:3838/post/
*   Trying 127.0.0.1:3838...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 3838 (#0)
> POST /post/ HTTP/1.1
> Host: localhost:3838
> User-Agent: curl/7.68.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< X-Powered-By: Shiny Server
< date: Tue, 24 Jan 2023 00:15:43 GMT
< x-ua-compatible: IE=edge,chrome=1
< content-type: application/json
< connection: close
< content-length: 16
< Vary: Accept-Encoding
< 
* Closing connection 0
{"status": "ok"}

Also, various features of Shiny itself require POST requests to work (like file uploads).

@mdoucleff
Copy link

mdoucleff commented Jan 24, 2023 via email

@ismirsehregal
Copy link

ismirsehregal commented Mar 28, 2023

@mdoucleff have you found a workaround regarding the www handler? The same question just came up here.

@hg77
Copy link

hg77 commented Mar 28, 2023

@ismirsehregal
I think I found the root cause in https://github.com/rstudio/shiny/blob/main/R/server.R#L395
httpuv is handling the requests before they hit the R handlers. In the above line

"session" = excludeStaticPath(),

/session is excluded. And

curl -vX POST http://localhost:3838/session/post 

hits the R handlers.

So to make /postpath work you would have to somehow get "postpath"=excludeStaticPath() into .globals$resourcePaths.

Something like (this does not work)

shiny:::.globals$resourcePaths <- append(shiny:::.globals$resourcePaths, list("postpath" = httpuv::excludeStaticPath()))

Clean solution would be if
addResourcePath ((https://github.com/rstudio/shiny/blob/main/R/server-resource-paths.R#L44) could accept excludeStaticPath().

@mdoucleff
Copy link

@ismirsehregal

I removed the www subdir from the app directory and served the static content via the (nginx) reverse proxy, which was something I wanted to do anyway to reduce app load for simple static content requests.

@Ginsed2019
Copy link

As mentioned by @hg77 this can be solved by excluding POST path.
This can be done by:

shiny_env <- shiny:::.globals
shiny_env$resourcePaths <- c(shiny_env$resourcePaths, list("postpath" = httpuv::excludeStaticPath()))

Also see: https://community.rstudio.com/t/why-post-request-in-rshiny-returns-bad-request-when-www-directory-is-present/162970

@rouuuge
Copy link

rouuuge commented Jun 6, 2023

hi,

is it somehow possible to start the session after a post request? I like to pass an access_token to the shiny-application. so far i have:

shiny:::handlerManager$addHandler(
  function(req) {
    if (identical(req$REQUEST_METHOD, "POST")) {
      body_bytes <- req$rook.input$read(-1)
      dataString <- rawToChar(body_bytes)
      access_token <- sub("^access_token=(.*)$", "\\1", dataString)
      print(access_token)
      
      return(httpResponse(
        status = 200L,
        content_type = "application/json",
        content = '{"status": "ok"}'
      ))
    }
  },
  "key"
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment