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

Specify HTTP response headers for static assets #3126

Open
mmuurr opened this issue Nov 2, 2020 · 6 comments
Open

Specify HTTP response headers for static assets #3126

mmuurr opened this issue Nov 2, 2020 · 6 comments

Comments

@mmuurr
Copy link

mmuurr commented Nov 2, 2020

I've noticed that for some types of direct HTTP request handlers (e.g. to download data in an app), Shiny attaches a cache-control header to the response. In other cases, there's no such header attached, most notably in the case of serving static assets (like JavaScript files) via the canonical www path or via paths added through addResourcePath().

In some testing of an application of mine that requires JavaScript files to power parts of the front-end (FE), I've found that my browser(s) will at times cache the static assets, leading to out-of-sync FE and server-side Shiny code, typically breaking the application. Clearing cache fixes this, but asking my users to manage their local cache won't work too well :-).

I wonder if it'd be possible to register additional HTTP headers to be sent back with assets served via non-socket requests. One way to do this (that I can imagine) would be to register such headers at the resourcePath-level during the call to addResourcePath(). Then any file served through a matching resourcePath would inherit those headers and have them served up by Shiny on the response.

This might be useful for lots of potential reasons, but my use case is very specifically for the cache-control header, and I'd generally be fine setting a pretty short TTL (e.g. 1 hour) for any file served by my application, and thus setting this as a 'global' Shiny option() would work, too.

@mmuurr
Copy link
Author

mmuurr commented Feb 25, 2025

Hi, me again :-)

Any thought on if this is doable? I figured I'd ping and ask about it again before taking the plunge to work out an alternative solution that circumvents resourcePath.

@gadenbuie
Copy link
Member

In some testing of an application of mine that requires JavaScript files to power parts of the front-end (FE), I've found that my browser(s) will at times cache the static assets, leading to out-of-sync FE and server-side Shiny code, typically breaking the application.

How are you including these JavaScript files in your app? All of your suggestions are reasonable ideas to explore, for sure, but there may be a way to help you avoid this without needing to change Shiny:

  • Using includeScript() in-lines the JavaScript in a <script> tag so it wouldn't be cached.
  • Using htmlDependency() is easier for loading an entire bundle of JS and CSS assets and gives you a version parameter that you can change when your assets change (breaking the cache).
  • Many JavaScript bundlers, e.g. esbuild, parcel and rollup, have features for adding hashes to built assets filenames to break caching.

@mmuurr
Copy link
Author

mmuurr commented Feb 25, 2025

@gadenbuie thanks for the reply -- those are fair points.

I'm a fan of the htmlDependency() approach but I've often had trouble with its very strict version arg:

mydep <- htmlDependency(name = "foo", version = "abcdef0", src = ".", script = "myscript.js")
attachDependencies(tags$html(), mydep)
# Error: invalid version specification ‘abcdef0’ 

This error goes away when using something closer to a semver tag for version, but that's not always the best approach (the above example uses a 7-digit hex code akin to an abbreviated git hash).

Perhaps I should open an Issue about that in the {htmltools} repo and rethink this Issue based on what I learn over there.

@gadenbuie
Copy link
Member

This error goes away when using something closer to a semver tag for version, but that's not always the best approach (the above example uses a 7-digit hex code akin to an abbreviated git hash).

Perhaps I should open an Issue about that in the {htmltools} repo and rethink this Issue based on what I learn over there.

I can save you a little time to let you know that htmlDependency() needs a version number in which one version number can be unequivocally greater than, less than, or equal to another. That's a strict requirement fundamental to quite a few features of HTML dependencies. In practice, the version number has to be compatible with numeric_version(), but it's really about being able to compare two versions of the same dependency and unequivocally choose the newer.

@mmuurr
Copy link
Author

mmuurr commented Feb 25, 2025

Thanks for that info. I do understand why that's done, but I also think it's a bit heavy-handed and believe:

  • the developer can/should accept some of the responsibility of making sure they're not including multiple versions of the same dependency in their app, and thus ...
  • there should be some way to opt-out of this strict version-checking logic (though with warnings).

Is there additional documentation anywhere on some of these details for the HTML dependency logic?
Even with the above thoughts (re: the version requirements), I think I'm still a bit confused as to how that version parameter busts caches.
If I change the version to something valid, like so:

mydep <- htmlDependency(name = "foo", version = "1.2.3", src = ".", script = "myscript.js")
attachDependencies(tags$html(), mydep)

... when inspecting the Shiny application I find the loaded resource will still ultimately use the 'raw' filename (e.g. "myscript.js" in the example).
I also don't see any variation in the response headers when this file is served.
And so while I completely understand the use of version to help decide which resource to include (in the case of naming conflicts), I don't see how that impacts the browser's cache.

Sorry to dwell on this, if there's a Gist somewhere that I just haven't found, that'd be helpful (and/or I can just dig through the code when I have some free cycles for a better understanding).

Either way, thanks for your continued work on Shiny -- it's great 😄!

@gadenbuie
Copy link
Member

Thanks for that info. I do understand why that's done, but I also think it's a bit heavy-handed and believe:

* the developer can/should accept some of the responsibility of making sure they're not including multiple versions of the same dependency in their app, and thus ...

* there should be some way to opt-out of this strict version-checking logic (though with warnings).

A core design goal of htmlDependency() is to manage web dependencies across many different extension packages and in many different environments (e.g. R Markdown, Quarto, Shiny, etc.). The version behavior is important because it lets extension packages declare or provide dependencies and we can resolve contention for similar dependencies, or scenarios where extensions attempt to load different versions of the same dependency.

As a fundamental design goal, this behavior is well established and unlikely to change.

Even with the above thoughts (re: the version requirements), I think I'm still a bit confused as to how that version parameter busts caches.

Browsers use the full path to the resource. By default the path includes the dependency version, so changing the version should change the path to the dependency, although there are options that control this behavior and that allow you to opt out of this.

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

No branches or pull requests

2 participants