shiny app download button and future_promise: Error in enc2utf8: argument is not a character vector

629 views Asked by At

I am trying to create a download handler in shiny, but using future_promise() because it is possible that writing the file could take some time. Here is a working example of what I'd like to do, but without using the async framework:

A working .Rmd shiny app: when you click on the button, it writes 10 random deviates to a file and offers it as a download. I added a delay of 5 seconds.

---
title: "download, no futures"
runtime: shiny
output: html_document
---

```{r setup, include=FALSE}
library(dplyr)
knitr::opts_chunk$set(echo = FALSE)
```

This version works.

```{r}
renderUI({
  
  button_reactive <- reactive({
    y = rnorm(10)
    Sys.sleep(5)
    tf = tempfile(fileext = ".txt")
    cat(c(y,'\n'), sep='\n', file = tf)
    d = readBin(con = tf, what = "raw", n = file.size(tf))
    return(list(fn = basename(tf), d = d))
  })
  
  output$button <- downloadHandler(
      filename = function() {
        button_reactive() %>%
          `[[`('fn')
      },
      content = function(f) {
        d = button_reactive() %>%
          `[[`('d')
        con = file(description = f, open = "wb")
        writeBin(object = d, con = con)
        close(con)
      }
    )
  
  shiny::downloadButton(outputId = "button", label="Download")
})

I'm trying to implement this in the async framework using future_promise. Here's the {future}/{promises} version:

---
title: "download futures"
runtime: shiny
output: html_document
---

```{r setup, include=FALSE}
library(future)
library(promises)
plan(multisession)
library(dplyr)
knitr::opts_chunk$set(echo = FALSE)
```

This version yields this error on download attempt, reported in the R console:

```
Warning: Error in enc2utf8: argument is not a character vector
  [No stack trace available]
```

```{r}
renderUI({
  
  button_reactive <- reactive({
    future_promise({
      y = rnorm(10)
      Sys.sleep(5)
      tf = tempfile(fileext = ".txt")
      cat(c(y,'\n'), sep='\n', file = tf)
      d = readBin(con = tf, what = "raw", n = file.size(tf))
    return(list(fn = basename(tf), d = d))
    }, seed = TRUE)
  })
  
  output$button <- downloadHandler(
      filename = function() {
        button_reactive() %...>%
          `[[`('fn')
      },
      content = function(f) {
        con = file(description = f, open = "wb")
        d = button_reactive() %...>%
          `[[`('d') %...>%
          writeBin(object = ., con = con)
        close(con)
      }
    )
  
  shiny::downloadButton(outputId = "button", label="Download")
})

When I click the button in Firefox, I get no file and in the R console, this is shown:

Warning: Error in enc2utf8: argument is not a character vector
  [No stack trace available]

After some debugging, I believe this occurs because whatever is running the download handler is running the filename function, expecting a character vector, and getting a promise. But I'm not sure how to fix this.

I saw this question, in which the asker seems to have the same problem, but no solution was offered (and their example was not reproducible).

How can I fix this?

1

There are 1 answers

1
All Downhill From Here On BEST ANSWER

Promises work with R Markdown, but there is some good and bad news.

The good news

Promises work on downloadHandler

In brief, promises can be used in lieu of a return value: it is simply an output value that is provided at some later point in time. So for any output object, including the downloadHandler, you can provide a promise rather than an output value.

A promise consists of a future_promise() function, which performs some slow-running operation (typically in a different R session) and a resolution part (which is the part that follows the %...>% operator) that picks up the results and provides resolution. The combination of both is the promise.

The downloadHandler is a bit special in that it doesn't receive an object as output, but expect a file of name f written to disk (and therefore a NULL return value). Your original code returned a close(con), which was a blocker to making the code work (but not the cause of the error).

For promises to work on downloadHandler, the file-written-to-disk must be replaced by a promise. In your code, however, your last line was close(con), which is not a promise. First point of order is therefore to offload file Writing to a function, which can then be the resolving part of the future construct.

downloadHandler doesn't seem to support promises for the filename part, as mentioned by @Waldi. I have no supporting info for this.

The bad news

Promises don't make a lot of sense in an R markdown context

As explained in this article, promises can be used in a Shiny context, and prevent the locking up of the server across sessions. Within a single session, the event loop waits for all promises to resolve before rendering output, effective leading to the very same stuck UI that we've all learned to love. Only when a second session is active, will promises yield any performance benefit.

Full example for using downloadHandler with promises

The below code is an adaptation of your code above, with three small differences:

  • The various future and resolve functions have been isolated
  • downloadHandler filename argument is now static
  • downloadHandler content argument provides a full promise

Keeping the preamble

---
title: "download futures"
runtime: shiny
output: html_document
---

```{r setup, include=FALSE}
library(future)
library(promises)
plan(multisession)
library(dplyr)
knitr::opts_chunk$set(echo = FALSE)
```

Define two standalone function for more clarity. Note that writeFile takes care of all I/O here, including closing the connection

```{r}
createFile = function(){
  y = rnorm(10)
  Sys.sleep(1)
  tf = tempfile(fileext = ".txt")
  cat(c(y,'\n'), sep='\n', file = tf)
  d = readBin(con = tf, what = "raw", n = file.size(tf))
  return(list(fn = basename(tf), d = d))
  }

writeFile = function (fut, f){
    x = fut[['d']]
    con = file(description = f, open = "wb")
    writeBin(object = x, con = con) 
    close(con)
}
```

UI part: note that content now returns a promise.

```{r}
renderUI({
  
  testPromise = reactive({
    future_promise({createFile()}, seed=T) %...>% (function (x) (x))()
  })
  
  fileName = reactive({
    testPromise() %...>% '[['('fn')
  })
  
  output$button <- downloadHandler(
      filename = function() {
        'test.txt'
        # This doesn't work - filename apparently doesn't support promises
        # fileName() 
        
      },
      content = function(f) {
        # Content needs to receive promise as return value, so including resolution
        testPromise() %...>% writeFile(., f)
      }
    )
  
  shiny::downloadButton(outputId = "button", label="Download")
})
```