Add header to _initial_ (non-websocket) Shiny app response

I'm using a UI function, like so:

ui <- function(req) {
  # Some short-circuiting checks here.

  # Everything good, let's return the app.
  shiny::htmlTemplate(
    text_ = some_html,
    document_ = TRUE,
    named,
    template,
    variables,
    here
  )
}

This works great: it returns the UI, Shiny front-end libraries, etc. to the client, which then initiates the websocket request (handled by Shiny's server() function). :tada:

BUT, now I need to add a custom HTTP header to this response, and it doesn't appear that this is doable (at least not directly), so I figured I'd ask here :crossed_fingers:

When returning a non-Shiny response (i.e. a static page) from the UI function, this is straight-forward:

ui <- function(req) {
  if (some_condition) {
    return(shiny::httpResponse(
      status = 403L,
      content_type = "text/html; charset=utf-8",
      content = "No soup for you!",
      headers = list("x-my-custom-header" = some_dynamic_value)
    ))
  }
}

So, naturally(?) I tried to combine the two and pass the shiny::htmlTemplate() object as the content argument in a similar response, like:

return(shiny::httpResponse(shiny::htmlTemplate(
  status = 200L,
  content_type = "text/html; charset=utf-8",
  content = shiny::htmlTemplate(
      text_ = some_html,
      document_ = TRUE,
      named,
      template,
      variables,
      here
    ),
  headers = list("x-my-custom-header" = some_dynamic_value)
)))

This does not work :confused:.

Has anyone here ever done this (i.e. added a header to the initial, non-websocket, request)?

FWIW -- I've found a non-standard solution, but I'm not a huge fan of it (as it uses the dreaded ::: operator :wink:).

Here's the final handler on the UI function's returned value:

shiny:::renderPage() replaces the template's placeholder (HTML comment) with the appropriate libraries in the page's <HEAD>. So, we can conceivably just call this ourselves, with the caveat that there's a bit of logic upstream for how showcaseMode and testMode are determined. If one knows that they're not using those modes (or feel like replicating that logic), we can, via the UI function itself, do something like:

ui <- function(req) {
  ## some upstream logic ...
  return(shiny::httpResponse(
    status = 200L,
    content_type = "text/html; charset=utf-8",
    content =
      shiny::htmlTemplate(
        text_ = some_html,
        document_ = TRUE,  ## if indeed it's a document :-)
        named,
        template,
        variables,
        here
      ) |> shiny:::renderPage(),
    headers = list("x-my-customer-header" = some_dynamic_value)
  ))
}

The shiny:::renderPage() function has showcaseMode = 0 and testMode = FALSE as default parameters and values, but we'll want to check this function's signature on each Shiny release to guard against possible API changes (and hope it doesn't disappear entirely!).

I'll be opening an Issue in GitHub to request some mechanism to add headers to this response, as stepping outside the exported namespace always feels a bit dirty/risky. A lightweight wrapper around uiValue that includes some HTTP directives seems easy-enough as an opt-in approach (e.g. a list attribute called http with elements like an optional headers list).

I'm still curious if anyone else has run into this problem and come up with an alternative solution!