rstudio / plumber

Turn your R code into a web API.
https://www.rplumber.io
Other
1.38k stars 256 forks source link

Enable CORS in Plumber #66

Open VinodPathak opened 7 years ago

VinodPathak commented 7 years ago

I am trying to make a POST request through my chrome browser, but its showing error of preflight request. Please suggest a way to enable CORS at Plumber side.

SushantVarshney commented 7 years ago

I am also getting Response for preflight has invalid HTTP status code 404 when making POST request through chrome.

scottmmjackson commented 6 years ago

@trestletech may come up with a push-button solution at some point, but for now I'm pretty sure that you can totally do this:

#* @filter cors
cors <- function(res) {
    res$setHeader("Access-Control-Allow-Origin", "*") # Or whatever
    plumber::forward()
}

#* @preempt cors
#* @get /myroute
myRoute <- function() {
    # Do some CORS requests!
}
trestletech commented 6 years ago

I spent a little time looking into this today. The bad news is that it's going to get worse before it gets better. 😬

Two decisions:

  1. The filter that @scottmmjackson suggests above is basically the default behavior for Plumber currently, and that feels like a security risk. So we'll be backing away from that behavior and prohibiting all CORS requests out of the box.
  2. We should have a nice set of handlers that make it easy to be more permissive about CORS. As of #144 Plumber now supports the OPTIONS verb, so you could in theory build a CORS-compliant service yourself, but it takes a lot of digging to get all the headers set correctly. We should wrap all that up in a nice filter that you could use easily in Plumber. So I'll leave this ticket open to represent that work.
joelgombin commented 6 years ago

For now does @scottmmjackson's approach still work in v0.4.0?

diplodata commented 6 years ago

@trestletech I wonder if there's been any activity or further thinking on this front? I'm hugely excited about plumber, but without CORS support it's much harder to harness its potential.

nuest commented 6 years ago

How about extending the advanced Docker example, which already has an nginx webserver, with code how to add CORS headers to an endpoint that nginx is a proxy for?

nginx as a web server is made for such tasks, and if you're really running an app in production you often run it behind a webserver (for https, for example) anyway.

nteetor commented 5 years ago

I think it's important to note that the workaround above did not work for me and at least one another. The modified example here, https://github.com/trestletech/plumber/issues/143#issuecomment-335792178, with the @preempt line removed does work. Thank you, @joelgombin.

shizidushu commented 5 years ago
#' @filter cors
cors <- function(req, res) {

  res$setHeader("Access-Control-Allow-Origin", "*")

  if (req$REQUEST_METHOD == "OPTIONS") {
    res$setHeader("Access-Control-Allow-Methods","*")
    res$setHeader("Access-Control-Allow-Headers", req$HTTP_ACCESS_CONTROL_REQUEST_HEADERS)
    res$status <- 200 
    return(list())
  } else {
    plumber::forward()
  }

}

@nteetor Please try the above code. It is working for me

nteetor commented 5 years ago

Yes, I was able to get cors up and running, thank you though.

fdrennan commented 5 years ago

By simply adding the code provided by @shizidushu at the top of my plumber file, it solved this issue for me.

bala7123 commented 5 years ago

Hi. i am not able to resolve my cors issue. The cors function(req,res) solution provided by @shizidushu need any parameters ? I have the following code in my main R script.

r22 <- plumb("/script1.R")

r22$run(port=xxxx, host="x.x.x.x")

should i pass r22 or any other code to the function ? Please respond its urgent. thanks a ton

schloerke commented 5 years ago

@bala7123 the code provided by @shizidushu in https://github.com/trestletech/plumber/issues/66#issuecomment-418660334 should exist within your /script1.R file.

kurt-o-sys commented 4 years ago

Thie solution given by https://github.com/rstudio/plumber/issues/66#issuecomment-418660334 doesn't work for me: the filter is never called. I'm having programmatic routers (see code below). Whenver I request OPTIONS, the response is

{
    "error": [
        "404 - Resource Not Found"
    ]
}

My code:

router_logic <- plumber$new("R/route_logic.R")
router_info <- PlumberStatic$new("./public/info/")

router <- plumber$new()

#' @filter cors
cors <- function(req, res) {

  res$setHeader("Access-Control-Allow-Origin", "*")

  if (req$REQUEST_METHOD == "OPTIONS") {
    res$setHeader("Access-Control-Allow-Methods","*")
    res$setHeader("Access-Control-Allow-Headers", req$HTTP_ACCESS_CONTROL_REQUEST_HEADERS)
    res$status <- 200
    return(list())
  } else {
    plumber::forward()
  }

}

router$mount("/logic", router_logic)
router$mount("/public", router_info)
router$handle("GET", "/", function(req, res){
  include_file("./public/logic/DESCRIPTION", res, "text/plain")
})

#' @title startServer
#' @description Start the api server using environmental variable 'PORT', if not passed as an argument
#'
#' @param port port to bind the application to
#' @return none
#' @examples
#' #startServer()
#' @export
startServer <- function(port) {
  if (missing(port))
    port <- Sys.getenv("PORT")
  if (port == "")
    port <- "8000"
  router$run(host = '0.0.0.0',
             port = strtoi(port),
             swagger = function(pr_, spec, ...) {
               spec$info$title<-"..."
               spec$info$description<-"..."
               spec$info$version<-" 0.0.1"
               spec$servers[[1]]$description<-"..."
               spec
             })
}
schloerke commented 4 years ago

@kurt-o-sys

The filter must be added programmatically, if the router is also defined programatically.

(The #' @filter cors will not have any effect in your setup.)

Instead, add it directly:

router$filter("cors", cors)
kurt-o-sys commented 4 years ago

Aah, thanks! I thought is was something like that, but couldn't find it in the docs. I may have missed it... Solved.

AntonWijbenga commented 3 years ago
#' @filter cors
cors <- function(req, res) {

  res$setHeader("Access-Control-Allow-Origin", "*")

  if (req$REQUEST_METHOD == "OPTIONS") {
    res$setHeader("Access-Control-Allow-Methods","*")
    res$setHeader("Access-Control-Allow-Headers", req$HTTP_ACCESS_CONTROL_REQUEST_HEADERS)
    res$status <- 200 
    return(list())
  } else {
    plumber::forward()
  }

}

Thank you for this (see above). It works! Many solutions only state a simpler version like below:

res$setHeader("Access-Control-Allow-Origin", "*")
plumber::forward()

However, when calling the API with a JSON POST body and a header set explicitly to "content-type: application/json", this simpler version fails with a CORS error anyway. The more elaborate version by shizidushu, however, works!

Can someone explain what happens in the 'if' part? The request I described runs through the function twice (automatically). The first time only the 'if' part is executed, the second time the 'else'. How is this happening?

I do undertand the plumber::forward() command. But in the 'if' part an empty list is returned. How is it that the code-excution doesn't stop there? Plumber calls itself somehow?

meztez commented 3 years ago

Your client is calling plumber the second time with the headers set in the if.

If you do not do a plumber::forward(), plumber respond back to the client

AntonWijbenga commented 3 years ago

So.... because the header in the response to the client (browser side) is altered/set by the server (plumber side), the client, based on that 'new' header in the response resends its request using a different header? (which then ends up in the 'else' part).

meztez commented 3 years ago

Use your browser debugger to inspect browser traffic with your plumber server. You can see the headers from there.

https://developers.google.com/web/tools/chrome-devtools/network/

AntonWijbenga commented 3 years ago

Thanks meztez for pointing me in the right direction. I did inspect the browser debugger en saw that it it indeed sends two API calls when using the javascript 'fetch' method and setting Content-Type: application/json. Doing that triggers the browser to do a 'preflight' check using the OPTIONS request. After that succeeds, it does the POST request.

It is explained in more detail in this answer on stackoverflow: https://stackoverflow.com/questions/46904400/why-do-i-get-an-options-request-after-making-a-post-request/46904470#46904470

That also explains why the more elaborate cors filter (as posted by @shizidushu) works and the simpler version (without the if/else) doesn't.

emilmahler commented 3 years ago
#' @filter cors
cors <- function(req, res) {

  res$setHeader("Access-Control-Allow-Origin", "*")

  if (req$REQUEST_METHOD == "OPTIONS") {
    res$setHeader("Access-Control-Allow-Methods","*")
    res$setHeader("Access-Control-Allow-Headers", req$HTTP_ACCESS_CONTROL_REQUEST_HEADERS)
    res$status <- 200 
    return(list())
  } else {
    plumber::forward()
  }

}

@nteetor Please try the above code. It is working for me

If you have two domains that you want to restrict CORS to, how do you do this? I've only seen examples with one address or *.

meztez commented 3 years ago

You could build something from this knowledge https://stackoverflow.com/questions/1653308/access-control-allow-origin-multiple-origin-domains.

Sounds like the recommended way to do it is to have your server read the Origin header from the client, compare that to the list of domains you would like to allow, and if it matches, echo the value of the Origin header back to the client as the Access-Control-Allow-Origin header in the response.

#' @filter cors
cors <- function(req, res) {

  if (req$HTTP_ORIGIN %in% c("domain1", "domain2") {

    res$setHeader("Access-Control-Allow-Origin", req$HTTP_ORIGIN)

    if (req$REQUEST_METHOD == "OPTIONS") {
      res$setHeader("Access-Control-Allow-Methods",req$HTTP_ORIGIN)
      res$setHeader("Access-Control-Allow-Headers", req$HTTP_ACCESS_CONTROL_REQUEST_HEADERS)
      res$status <- 200 
      return(list())
    } else {
      plumber::forward()
    }

  }

}
emilmahler commented 3 years ago

@meztez the value of req is <environment: 0x55c0d2c876d8> while req$HTTP_ORIGIN is null. In order to find this, I did:

# Enable CORS Filtering
#' @filter cors
cors <- function(req, res) {

  require(req)
  print(req)
  print(req$HTTP_ORIGIN)

  if (req$HTTP_ORIGIN %in% c("http://localhost:3000",
                             "https://testingdomain.com",
                             "https://productiondomain.com")) {

    res$setHeader("Access-Control-Allow-Origin", req$HTTP_ORIGIN)

    if (req$REQUEST_METHOD == "OPTIONS") {
      res$setHeader("Access-Control-Allow-Methods",req$HTTP_ORIGIN)
      res$setHeader("Access-Control-Allow-Headers", req$HTTP_ACCESS_CONTROL_REQUEST_HEADERS)
      res$status <- 200 
      return(list())
    } else {
      plumber::forward()
    }
  }
}

What am I missing?

meztez commented 3 years ago

Nothing. The code I provided was a mock, I'm not sure of the exact header that would contain the origin in your case.

yes req is an environment.

# Enable CORS Filtering
#' @filter cors
cors <- function(req, res) {

  # For debug/dev purpose remove when you find which header you need
  for (h in grep("^HTTP", ls(envir = req), value = TRUE)) { print(h); print(req[[h]])}

  if (req$HTTP_ORIGIN %in% c("http://localhost:3000",
                             "https://testingdomain.com",
                             "https://productiondomain.com")) {

    res$setHeader("Access-Control-Allow-Origin", req$HTTP_ORIGIN)

    if (req$REQUEST_METHOD == "OPTIONS") {
      res$setHeader("Access-Control-Allow-Methods",req$HTTP_ORIGIN)
      res$setHeader("Access-Control-Allow-Headers", req$HTTP_ACCESS_CONTROL_REQUEST_HEADERS)
      res$status <- 200 
      return(list())
    } else {
      plumber::forward()
    }
  }
}
emilmahler commented 3 years ago

There appears to be three variables it could be when HTTP_SEC_FETCH_MODE == "cors":

  1. req$HTTP_REFERER
  2. req$HTTP_HOST
  3. req$REMOTE_ADDR

I went with the first, as follows:

# Enable CORS Filtering
#' @filter cors
cors <- function(req, res) {
  safe_domains <- c("http://localhost:3000",
                    "https://testingdomain.com",
                    "https://productiondomain.com")

  if (any(grepl(pattern = paste0(safe_domains,collapse="|"), req$HTTP_REFERER,ignore.case=T))) {
    res$setHeader("Access-Control-Allow-Origin", req$HTTP_REFERER)

    if (req$REQUEST_METHOD == "OPTIONS") {
      res$setHeader("Access-Control-Allow-Methods",req$HTTP_REFERER)
      res$setHeader("Access-Control-Allow-Headers", req$HTTP_ACCESS_CONTROL_REQUEST_HEADERS)
      res$status <- 200
      return(list())
    } else {
      plumber::forward()
    }
  }
}  

Looking at Mozilla cors docs, it looks like the origin is the base domain while the referer is the full path.

emilmahler commented 3 years ago

The above solution is unstable, and you can never see the swagger documentation. Right now I have this working solution (implemented without fully understanding plumber::forward()):


# Enable CORS Filtering
#' @filter cors
cors <- function(req, res) {
  safe_domains <- c("http://localhost:3000",
                    "https://testingdomain.com",
                    "https://productiondomain.com")

  if (any(grepl(pattern = paste0(safe_domains,collapse="|"), req$HTTP_REFERER,ignore.case=T))) {
    res$setHeader("Access-Control-Allow-Origin", sub("/$","",req$HTTP_REFERER)) #Have to remove last slash, for some reason

    if (req$REQUEST_METHOD == "OPTIONS") {
      res$setHeader("Access-Control-Allow-Methods","GET,HEAD,PUT,PATCH,POST,DELETE") #This is how node.js does it
      res$setHeader("Access-Control-Allow-Headers", req$HTTP_ACCESS_CONTROL_REQUEST_HEADERS)
      res$status <- 200
      return(list())
    } else {
      plumber::forward()
    }
  } else {
    plumber::forward()
  }
} 
philibe commented 1 year ago

As @schloerke said in #604 "CORS implementation" (Jul 21, 2020), to enable CORS he recommended the code of the comment of @shizidushu on Sep 5, 2018 in this current issue. (Thanks @shizidushu). (It works for me).

But before that I had missed the use of #* @preempt cors and finally re-read the doc.

@preempt is not like a decorator to activate the cors() function, it's to disable cors() for some endpoints because this filter cors() function is actived for all the endpoints of the plumber service launched.

Once this filter is defined, each endpoint will allow “cross-domain” requests. It’s possible to disable it for some, by appending the line #* @preempt cors before the declaration of a function like this : #* @preempt cors #* @get /sub cors_disabled <- function(a, b){ as.numeric(a) - as.numeric(b) } Understand that, this is a temporary workaround and can constitute critical security issues if some endpoints that shouldn’t be exposed through CORS and have been enabled by default.

from https://www.rplumber.io/articles/security.html#cross-origin-resource-sharing-cors

ncullen93 commented 8 months ago

So funny that I just did the same thing ^ spent 30 minutes trying all of this until I really read the last comment and realized you have to TAKE AWAY #* @preempt cors to get it to work. Counter-intuitive, but now it works!