For years I've been serving my apps' WebServerResources directly from apps rather than the webserver, wrote a little about it here last year: Taking control of WebServerResources in WebObjects.

It's always worked great, but there was still some stuff remaining to do, most of it now done, so, a year later, here's a follow-up about how and what ERXAppBasedResourceManager and ERXAppBasedResourceRequestHandler in wonder-slim do today.

Where we left off

Here's last year's list, and what became of it:

ThenNow
All resources cached forever on the server sideResources are still cached in production but the cache is now optional/bounded. By default, 64 MB of resources are cached, but that's configurable using er.extensions.ERXAppBasedResourceRequestHandler.cacheMegabytes. Files over 1 MB are always streamed rather than cached (resource lookup time matters less for large files). Lookups of missing files are remembered separately, so asking for a missing file multiple times won't always trigger a search of every bundle.
Hardcoded cache headersURLs are generated with a hash of the resource's content now. A stamped URL is cached for a year, unstamped URLs get revalidated with an ETag, meaning they get a 304 when unchanged.
No control over client side cachingFixed, but not with a query parameter as I planned: a resource URL now carries a stamp of the file's content as mentioned above. More on that below.
No localized resourcesStill not supported. Nobody has asked, me included. See end of article.
Other stuff I hadn't hit back thenHit a few: video and audio (range requests), big files, missing files, and deploys where old and new instances run side by side.

Generating URLs: the resource manager

Nothing changes in the templates. <wo:img filename="logo.png" />, WOResourceURL, stylesheets and scripts all ask the resource manager for a URL, ERXAppBasedResourceManager hands back one that points at the application itself:

/res/app/css/site.css
/res/AjaxSlim/ajaxslim.js

Here, res is the request handler,, app/AjaxSlim is the "framework" and the rest is the resource's path in the bundle's WebServerResources folder. Short URLs are the default in slim so that's the whole URL there, but the long form (.../App.woa/res/app/css/site.css) also works and is generated if short URL generation is inactive.

Only files in a bundle's webserver resources folder are served, checked by where the file sits in the bundle. Asking for /res/app/Properties or a model file returns a 404, not your database password. Sounds obvious, but it's the kind of thing that's easy to get subtly wrong (.. in a path, a folder called WebServerResources somewhere it shouldn't be), so it's checked carefully and tested.

Where the files come from depends on how the app runs. Deployed, they come from the built .woa and the framework jars. In development they come straight from the project folders. So in dev; edit a stylesheet, reload, done.

Serving them: the request handler

ERXAppBasedResourceRequestHandler answers the /res/ URLs. It finds the resource through the resource manager, sends it with a content type, and in production keeps what it found in memory. A few things in there have grown up since last year.

  1. The cache is bounded. It holds at most 64 MB of content, and when it's full, the resource requested least recently goes. Before, it just kept everything, usually fine for small resources but potentially dangerous.
  2. Big files are streamed. Anything over 1 MB is read from its bundle (a file, or an entry in a jar) for each request instead of being held in memory. The cache only remembers its name and size.
  3. Missing files are remembered. Looking up a resource that isn't there searches every bundle, which takes most of a millisecond. So the handler remembers up to 10,000 paths it knows are missing, and a repeated request for one gets a 404 immediately.
  4. Range requests work. Media players ask for parts of a file (Safari won't even play a video without it). The handler answers a single Range: bytes=… with 206 Partial Content, a range past the end with 416, and honours If-Range. A request for several ranges just gets the whole file, which the spec allows.
  5. Unchanged resources aren't sent again. Every resource has an ETag, the stamp of its content (more on stamps next), and a request with a matching If-None-Match gets a 304 Not Modified with no body.

None of this is anything a web server doesn't do. That's kind of the point: it's what you'd expect from a web server, so the application had to learn to do it too.

Cached for good: content stamps

Last year's plan was a version query parameter. What I ended up with is a stamp of the file's content, in the file name:

/res/app/css/site@3f9c1e07ab.css

The stamp is the first ten hex digits of the file's SHA-256. In production, the resource manager stamps every URL it generates, computing a resource's stamp the first time it's asked for and keeping it (a deployed app's files don't change while it runs). Change the file, deploy, and the URL changes with it.

That's what makes caching easy. A request for a resource by its current stamp gets

Cache-Control: public, max-age=31536000, immutable

so the browser keeps it for a year and never even asks again. Anything else (a URL without a stamp, or one with an old stamp) gets no-cache and the ETag, meaning "keep it, but check first", and the check is a cheap 304.

Why in the file name and not a query string or a folder? Two reasons. Relative references inside a stamped stylesheet, like url(../img/logo.png), still resolve to the right place. And some caches ignore query strings, which would make every version look like the same file.

While old and new instances run side by side, a page from a new instance might potentially request a resource with a resource stamp it doesn't have yet. In that case, the old instance will serve the resource, but with no-cache, so the browser doesn't hold on to the wrong file for a year.

The files at the root: public resources

Sometimes you just need a resource served and don't want to involve WO's URLs -and some files have to live the root, like /favicon.ico, /robots.txt, /sitemap.xml, /.well-known/security.txt etc. I used to write a routes for these, but wonder-slim has learned a trick from ng-objects and now allows resources to be served directly from a reserved folder.

Put them in public inside your webserver resources folder, and turn it on in your application's constructor:

RouteTable.defaultRouteTable().setFallbackRouteHandler( new ERXPublicResources() );

public/favicon.ico then answers /favicon.ico, public/.well-known/security.txt answers /.well-known/security.txt, and so on. They're served by the same handler, exactly as at their /res/app/public/… URLs, ETags and all.

It sits at the end of the routing chain, so a route always wins over a file, and a URL that isn't a file goes on to the 404. The folder is indexed the first time it's used, so a URL that isn't in it is a miss without any lookup, which matters, since every URL nobody else claims ends up here (hello, /wp-login.php). In development the index is rebuilt on a miss, so a file you just added is served without a restart.

Development and production

Same URLs, same handler, same code path. What differs is only what has to, because in development the files change while the app runs:

DevelopmentProduction
Files come fromyour project foldersthe built .woa and framework jars
URLs stampednoyes
Held in memoryno, read fresh every timeyes, up to 64 MB
Cache-Controlno-cachea year and immutable for a stamped URL, no-cache otherwise
ETags and 304yesyes
Public files indexedagain on a missonce

So if something works in development, it works the same way in production, and that was the whole point of dropping the split install in the first place.

What's next

Still a few things to do, which I haven't found pressing so far:

  1. Stamped references inside stylesheets. An image a stylesheet refers to with url(...) isn't stamped, so it's revalidated rather than cached for good. Cheap, but not free. Stamping those references on the way out would fix it, and the same machinery could do a bit of templating (#141).
  2. Letting the application decide about caching. Some resources shouldn't be cached at all, others could be cached even without a stamp. Right now that's decided for you (#140).
  3. Localized resources. Still not supported and not currently planned. If you need them, let me know, that's the only way to move them up the list.

If you're on wonder-slim, you already have all of this, nothing to configure apart from the public folder. If you try it and something doesn't behave, pipe up, either on the WOCommunity chat or by filing an issue on the wonder-slim repo.