Skip to content

Allow for custom headers on initial (GET, non-WS) response #4319

@mmuurr

Description

@mmuurr

I've recently migrated a Shiny application to using the UI-as-a-function approach, allowing for increased control on the initial request (to be able to serve a non-Shiny-app response). This is combined with Shiny's HTML template system, also allowing for finer control of the app's returned page (on the initial, non-websocket, request). The general structure looks like so:

ui <- function(req) {
  custom_header_value(req)
  if (some_test(req)) {
    # return the static, non-app response
    shiny::httpResponse(
      content = nonapp_content(),
      headers = list("x-custom-header" = custom_header_value(req))
    )
  } else {
    # return the shiny app response
    shiny::htmlTemplate(
      text_ = the_template(),
      document_ = TRUE,
      named, template, variables, here
    )
  }
}

This works great until I also want to include that x-custom-header in the Shiny app response.

My initial thought was to simply set htmlTemplate()'s return value as the content of httpResponse():

## does NOT work:
shiny::httpResponse(
  content = shiny::htmlTemplate(...),
  headers = list("x-custom-header" = custom_header_value(req)
)

... but I quickly realized httpResponse() wants a fully-baked, i.e. rendered, HTML document, which htmlTemplate() does not provide.

That rendering step (going from an "html_document" and "shiny.tag.list" to a fully-baked HTML string happens here, in shiny:::uiHttpHandler() (called via shiny::shinyApp()):

shiny/R/shinyui.R

Lines 256 to 261 in e3cf4fb

if (inherits(uiValue, "httpResponse")) {
return(uiValue)
} else {
html <- renderPage(uiValue, showcaseMode, testMode)
return(httpResponse(200, content=html))
}

A small chunk of uiHttpHandler() is used for determining the values for the other two arguments to renderPage(): showcase and testMode.
In my particular case, those two will always be disabled/FALSE, and so I was able to complete my initial goal like so:

ui <- function(req) {
  custom_header_value(req)
  if (some_test(req)) {
    # return the static, non-app response
    shiny::httpResponse(
      content = nonapp_content(),
      headers = list("x-custom-header" = custom_header_value(req, app = FALSE))
    )
  } else {
    # return the shiny app response
    shiny::httpResponse(
      content =
        shiny::htmlTemplate(
          text_ = the_template(),
          document_ = TRUE,
          named, template, variables, here
        ) |>
        shiny:::renderPage(),  ## now we have a fully-baked HTML string
      headers = list("x-custom-header" = custom_header_value(req, app =TRUE))
    )
  }
}

This required accessing the non-exported shiny:::renderPage() function, which never feels great to put into production code, but I was otherwise pretty stumped for a way to add a header to the app's initial (pre-websocket) response.

Perhaps there's another way to do this?

If not, maybe the showcase- and testMode-determining logic can be 'officially' factored-out of uiHttpResponse() along with making renderPage() exported?
This way one could use the (working) pattern above while also retaining the potential use of those two flags (without simply re-implementing that flag-determining logic themselves ... i.e. don't repeat yourself).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions