Skip to content

Add static path precompressed option#791

Open
oliverlambson wants to merge 1 commit intoemmett-framework:masterfrom
oliverlambson:static-files
Open

Add static path precompressed option#791
oliverlambson wants to merge 1 commit intoemmett-framework:masterfrom
oliverlambson:static-files

Conversation

@oliverlambson
Copy link
Contributor

@oliverlambson oliverlambson commented Jan 25, 2026

Closes #577

Carries on the work from #646, changes from that PR:

  • adds lazy caching of sidecar file existence
  • adds full support for RFC 7231 Accept-Encoding negotiation (client order, q=, *)
  • adds zstd as an encoding type (and prioritises it highest)

example

GET /static/styles.css HTTP/1.1
Accept-Encoding: br, gzip
  1. ./static/styles.css.br - if exists, serve with Content-Encoding: br
  2. ./static/styles.css.gz - if exists, serve with Content-Encoding: gzip
  3. ./static/styles.css - fallback to uncompressed
HTTP/1.1 200 OK
Content-Type: text/css
Content-Encoding: br
Vary: Accept-Encoding

changes to public apis

granian app:app \
  --static-path-mount ./static \
  --static-path-route /static \
  --static-path-expires 31536000 \
  --static-path-precompressed  # new (default disabled)
from granian import Granian

server = Granian(
    "app:app",
    static_path_mount="./static",
    static_path_route="/static",
    static_path_expires=31536000,
    static_path_precompressed=True,  # new (default false)
)

Comment on lines 183 to 190
self.static_path = (
(
static_path_route,
str(static_path_mount.resolve()),
(str(static_path_expires) if static_path_expires else None),
self.static_files = (
StaticFilesSettings(
prefix=static_path_route,
mount=str(static_path_mount.resolve()),
expires=str(static_path_expires) if static_path_expires else None,
precompressed=static_path_precompressed,
Copy link
Contributor Author

@oliverlambson oliverlambson Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is from original PR. I kept it because the alternative of growing the config tuple grow to include a positional bool feels wrong. It does result in this diff being quite a lot bigger though (it flows through everywhere). I don't think this breaks any public APIs since it's only on the Worker classes, the Server classes just get an extra optional arg

Comment on lines +75 to +83
# only use one interface here since the negotiation logic in Rust is
# interface-agnostic and tests take long to run
@pytest.mark.asyncio
@pytest.mark.parametrize('server_static_files_precompressed', ['rsgi'], indirect=True)
@pytest.mark.parametrize(
'accept_encoding,expected_encoding',
[
# multiple encodings - client order wins when q-values equal (first one wins)
('br, gzip', 'br'),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aside: it may be worth optimising test fixture startup time, or if not possible add xdist to run tests in parallel

headers.insert(
"cache-control",
CACHE_CONTROL,
HeaderValue::from_str(&format!("max-age={expires}")).unwrap(),
Copy link
Contributor Author

@oliverlambson oliverlambson Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be nice to be able to configure public/private and immutable for this header too

@gi0baro gi0baro modified the milestones: 2.7, 2.8 Jan 26, 2026
@gi0baro
Copy link
Member

gi0baro commented Jan 31, 2026

@oliverlambson thank you for your contribution ❤️

While I originally planned to review this for 2.7, given the amount of changes this introduces, and also the fact that 2.7 is already quite a big set of changes, I rescheduled this for 2.8.

Gonna review the code as soon as I can, probably in the following weeks.

@oliverlambson
Copy link
Contributor Author

oliverlambson commented Jan 31, 2026

@gi0baro that sounds great, thanks!

Adds support for serving pre-compressed static files with .br, .gz, or .zst
extensions. When enabled via --static-path-precompressed, the server will:

- Check Accept-Encoding header for supported encodings (br, gzip, zstd)
- Serve compressed sidecar files if they exist (e.g., file.js.br for file.js)
- Respect client q-value priorities for encoding selection
- Fall back to uncompressed file if no sidecar exists
- Use lazy caching to avoid repeated filesystem checks for sidecars

Works with the existing multi-mount and dir-to-file rewrite features.
@oliverlambson
Copy link
Contributor Author

@gi0baro thanks for 2.7 release, have rebased and updated this branch to suppored the new multi-mount config. Chose to keep the compression option as a global since expires and dir-to-file are both global too

Copy link
Member

@gi0baro gi0baro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @oliverlambson, here's my first pass on this.

Before going into code-specific things, I would:

  • drop the caching mechanism entirely. It adds a bunch of code-complexity, and it's not clear at the time being whether its presence is needed or not for performance reasons. I'm up to to reconsider some caching mechanism once the implementation is completed and we actually verify we're not satisfied with the performance we see.
  • make the supported encodings an actual parameter configurable by the user. To my perspective it doesn't really makes sense to check for all the possible file formats every time while the user which sets up Granian to serve pre-compressed assets knows which file formats are actually available. With this change, checking for all the formats becomes a worst case scenario which the user voluntarily requested. This probably also allows to refactor the amount of nested for-loop checks in the code and use HashSet instead.

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

Successfully merging this pull request may close these issues.

Feature request: Pre-compressed static files

2 participants