Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable CORS in Plumber #66

Open
VinodPathak opened this issue Dec 19, 2016 · 28 comments
Open

Enable CORS in Plumber #66

VinodPathak opened this issue Dec 19, 2016 · 28 comments
Labels
docs Related to documentation only

Comments

@VinodPathak
Copy link

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
Copy link

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

@trestletech trestletech added the type: bug Maintainers have validated that it is a real bug in the project code label Jun 13, 2017
@trestletech trestletech added this to the v1.0 milestone Jun 13, 2017
@trestletech trestletech modified the milestones: v0.5.0, 0.4.0 Jul 7, 2017
@scottmmjackson
Copy link
Contributor

@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
Copy link
Contributor

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 Don't wildcard allow-origin by default. #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.

@trestletech trestletech removed this from the 0.4.0 milestone Jul 19, 2017
@joelgombin
Copy link

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

joelgombin added a commit to CMARUE/APIs that referenced this issue Aug 28, 2017
@trestletech trestletech added this to the v0.4.4 milestone Oct 16, 2017
joelgombin added a commit to CMARUE/APIs that referenced this issue Oct 23, 2017
@trestletech trestletech modified the milestones: v0.4.4, v0.4.5 Nov 15, 2017
@diplodata
Copy link

@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
Copy link

nuest commented Feb 26, 2018

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
Copy link

nteetor commented Sep 5, 2018

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, #143 (comment), with the @preempt line removed does work. Thank you, @joelgombin.

@shizidushu
Copy link

#' @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
Copy link

nteetor commented Sep 5, 2018

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

@schloerke schloerke added docs Related to documentation only and removed type: bug Maintainers have validated that it is a real bug in the project code labels Dec 12, 2018
@fdrennan
Copy link

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

@bala7123
Copy link

bala7123 commented Mar 6, 2019

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
Copy link
Collaborator

@bala7123 the code provided by @shizidushu in #66 (comment) should exist within your /script1.R file.

@kurt-o-sys
Copy link

Thie solution given by #66 (comment) 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
Copy link
Collaborator

@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
Copy link

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

This was referenced Jul 15, 2020
@AntonWijbenga
Copy link

AntonWijbenga commented Dec 3, 2020

#' @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
Copy link
Collaborator

meztez commented Dec 3, 2020

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
Copy link

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
Copy link
Collaborator

meztez commented Dec 4, 2020

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
Copy link

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
Copy link

#' @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
Copy link
Collaborator

meztez commented May 20, 2021

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
Copy link

emilmahler commented May 20, 2021

@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
Copy link
Collaborator

meztez commented May 20, 2021

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
Copy link

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
Copy link

emilmahler commented May 21, 2021

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
Copy link

philibe commented Jun 21, 2023

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
Copy link

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs Related to documentation only
Projects
None yet
Development

No branches or pull requests