`rsconnect::deployApp()` - Bundle wrongly identified as API

Updating the rsconnect package from 0.8.29 to 1.0.1 has introduced unwanted behavior that leads to failing deployment to the rsconnect server. In a nutshell, it now tries to publish a Shiny app as an API.

We are using a app-in-a-package structure to develop a shiny app. The app requires an API counterpart, which we develop in the same package, as was suggested by @barret here:

.
├── app.R
├── DESCRIPTION
├── inst
│   └── plumber
|         └── api
|              └── plumber.R  <----  The culprit
├── R
│   ├── server.R
│   └── ui.R
...

app.R has the push-button deployment button in RStudio. This used to work fine, but since the upgrade to rsconnect 1.0.1 I get the following error during the deployment to server stage when I use either the button or rsconnect::deployApp():

── Deploying to server ─────────────────────────────
Error in POST():
! https://{our-server-address}/__api__/applications/768/deploy failed with HTTP status 403
you cannot change the type of content once deployed. This content is 'shiny', but the bundle is identified as 'api'. Try deploying this bundle as a new
content item rather than updating an existing one.

When I try to publish the app as a new item, as suggested in the message, it also tries to publish it as an API.

This happens even though there is a manifest.json with

  "metadata": {
    "appmode": "shiny",

Apparently, the server thinks that I want to publish the API that's in the bundle rather than the Shiny app.

A workaround is to rename plumber.R to something different. Downgrading rsconnect to 0.8.29 also solves the issue. Both are not very attractive solutions.

Is there a way to tell the server to ignore the API in the package?

1 Like

Are you providing any arguments to rsconnect::deployApp()? I have appFiles in mind in particular. You might also try setting appPrimaryDoc explicitly.

Try calling rsconnect::writeManifest() with those arguments specified to see what you get without actually having to go through the deployment.

Hi, thank you for your suggetion. This has helped me to reverse engineer the issue:

Undesired Behavior

With a golem-style app-layout and an additional plumber API in inst/plumber/api/plumber.R, rsconnect:::inferAppMode() infers that the project has appmode: api, not appmode: shiny.

This function is called when using rsconnect::writeManifest(), rsconnect::bundleApp(), which is called by rsconnect::deployApp(), as well as when using the deploy button in the RStudio interface.

For example:

> rsconnect::writeManifest()
...
> jsonlite::read_json("manifest.json")$metadata$appmode
# [1] "api"

Consequently, rsconnect::deployApp() and the deployButton will wrongly try to deploy the project as an API instead of as a shiny app.

Creating a manifest.json in the directory and simply changing appmode to shiny in that file doesn't solve the issue, because deployApp() calls bundleApp(), which generates its own manifest.json straight in a temporary bundle directory, so the manifest.json file in the project directory is ignored.

The root of the Problem

rsconnect:::inferAppMode() checks for evidence of a plumber API (and returns) before it checks for evidence of a Shiny App (as can be seen here).

The behavior was introduced in rsconnect version 1.0.0, because

  • Prior to 1.0.0, inferAppMode() was called on a non-recursive list of the files in in the directory via bundleFiles(), i.e., only the top level was searched for evidence of the different app modes (so inst/api/plumber.R would not have been part of the list).
  • Since 1.0.0, the function gets a recursive list of all files in the bundle (via listDeploymentFiles()), which, in our case, now includes 'inst/api/plumber.R`
  • This is due to a change in the internal function listBundleFIles(), which now calls a function that returns a recursive list of files (old version in 0.8.29 vs. new version in 1.0.0)
  • This is despite a comment stating that it only checks files in the root dir

Suggested solution: setting appPrimaryDoc = "app.R"

This throws an error:

> rsconnect::writeManifest(appPrimaryDoc = "app.R")`

Error in `checkAppLayout()`:
! Project must not contain both app.R and a single-file Shiny app.
Run `rlang::last_trace()` to see where the error occurred.

This error is thrown by a conditional in rsconnect::checkAppLayout(appDir, appPrimaryDoc), a function that checks whether the app directory has any of the expected layouts. This function is called by both, writeManifest() and bundleApp() via rsconnect:::appMetadata() and it explicitely prevents the user from setting app.R to be the appPrimaryDoc:

appFilesBase <- tolower(list.files(appDir))
...
primaryIsRScript <- identical(tolower(tools::file_ext(appPrimaryDoc)), "r")
if (primaryIsRScript && "app.r" %in% appFilesBase) {
  cli::cli_abort("Project must not contain both {.file app.R} and a single-file Shiny app.")
}

A solution is to add an additional condition to test, whether appPrimaryDoc == app.R. Ideally, this would have to be fixed in {rsconnect} itself, but one can temporarily inject a fix like this:

checkAppLayout_modified <- function (appDir, appPrimaryDoc = NULL) 
{
  ...
  if (primaryIsRScript && "app.r" %in% appFilesBase && tolower(appPrimaryDoc) != "app.r") {
    cli::cli_abort("Project must not contain both {.file app.R} and a single-file Shiny app.")
  }
  ...
}

assignInNamespace("checkAppLayout", checkAppLayout_modified, ns = "rsconnect")
rsconnect::writeManifest(appPrimaryDoc = "app.R")
Complete functional code
# A solution is to prevent the error by checking whether `tolower(appPrimaryDoc) != "app.r"`

checkAppLayout_original <- rsconnect:::checkAppLayout  # for restoring it later

checkAppLayout_modified <- function (appDir, appPrimaryDoc = NULL) 
{
  cli::cli_alert_warning("Using custom function injected into the rsconnect namespace.")
  appFilesBase <- tolower(list.files(appDir))
  wwwFiles <- tolower(list.files(file.path(appDir, "www/")))
  primaryIsRScript <- identical(tolower(tools::file_ext(appPrimaryDoc)), 
                                "r")
  
  if (primaryIsRScript && "app.r" %in% appFilesBase && tolower(appPrimaryDoc) != "app.r") {
    cli::cli_abort("Project must not contain both {.file app.R} and a single-file Shiny app.")
  }
  satisfiedLayouts <- c(shinyAndUi = all(c("server.r", "ui.r") %in% 
                                           appFilesBase), shinyAndIndex = "server.r" %in% appFilesBase && 
                          "index.html" %in% wwwFiles, app = primaryIsRScript || 
                          any("app.r" %in% appFilesBase), Rmd = any(grepl(glob2rx("*.rmd"), 
                                                                          appFilesBase)), Qmd = any(grepl(glob2rx("*.qmd"), appFilesBase)), 
                        static = any(grepl("(?:html?|pdf)$", appFilesBase)), 
                        plumber = any(c("entrypoint.r", "plumber.r") %in% appFilesBase))
  if (any(satisfiedLayouts)) {
    return()
  }
  cli::cli_abort(c("Cancelling deployment: invalid project layout.", 
                   i = "Expecting one of the following publication types:", 
                   ` ` = "1. A Shiny app with `app.R` or `server.R` + `ui.R`", 
                   ` ` = "2. R Markdown (`.Rmd`) or Quarto (`.qmd`) documents.", 
                   ` ` = "3. A website containing `.html` and/or `.pdf` files.", 
                   ` ` = "4. A plumber API with `plumber.R` or `entrypoint.R`."))
}

assignInNamespace("checkAppLayout", checkAppLayout_modified, ns = "rsconnect")

rsconnect::writeManifest(appPrimaryDoc = "app.R")

appmode <- jsonlite::read_json("manifest.json")$metadata$appmode
if (appmode == "shiny") {
  rsconnect::deployApp(appPrimaryDoc = "app.R")
} else {
  cli::cli_abort("Something went wrong, `appmode` is `{ appmode }`.")
}

This allows me to publish to rsconnect succesfully, since the erorr isn't thrown anymore and the generated manifest.json now has appmode: shiny. However, the solution requires some R black magic and it still doesn't allow me to use the deploy Button in RStudio, because there I cannot supply the appPrimaryDoc argument.

It looks like currently there is no straightforward solution to use the project structure we have with the latest version of rsconnect.


Generally, I think, the preference of an API over an App by deployApp() is unexpected and should be avoided. If I wanted to deploy the API that's in the package, I'd expect to use deployApi() to do so.

There are a number of potential upstream fixes I can think of, but I think it's best if the project maintainers look into this.

I suppose I will open an issue on Github about this.

Is there any simple solution I'm overlooking?

No, a GitHub issue is probably the best way forward here. Thanks for the thorough write-up!

Alright, thank you! Seems like there is no good fix at the moment.
I have opened an issue in the repo and will keep using the old version of the package until this is fixed. Deployment fails because bundle is wrongly identified as API by `inferAppMode()` · Issue #942 · rstudio/rsconnect · GitHub

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.

If you have a query related to it or one of the replies, start a new topic and refer back with a link.