Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ jobs:
- "3.11"
- "3.12"
- "3.13"
- "3.14"

steps:
- uses: actions/checkout@v4
Expand Down
36 changes: 35 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,39 @@ Don't forget to remove deprecated code on each major release!

## [Unreleased]

- Nothing (yet)

## [4.1.0] - 2026-03-07

!!! tip

This release includes some changes to the default behavior of `ServeStatic` for security hardening. If you are affected by any of these changes, please read the relevant sections in the documentation on `allow_unsafe_symlinks`.

### Added

- Added support for `zstd` compression on Python 3.14+.
- Added support for the top-level `servestatic` module to run as a Django app.
- Added Django system checks to test for common misconfigurations.
- Added a new `allow_unsafe_symlinks` configuration option for WSGI/ASGI
- Added a new `SERVESTATIC_ALLOW_UNSAFE_SYMLINKS` configuration option for Django.
- Added `jxl` image support.

### Changed

- Improved event-loop handling for ASGI file iterator.
- Installing `servestatic` as a Django app is now the suggested configuration. A warning will appear if it is not detected in `INSTALLED_APPS` when `DEBUG` is `True`.
- `servestatic.runserver_nostatic` is no longer the recommended Django app installation path. This import path will be retained to ease `WhiteNoise` to `ServeStatic` migration, but now the documentation recommends to use the top-level `servestatic` module instead.
- For security purposes, `ServeStatic` will no longer follow unsafe symlinks by default. If your symlinks point to files outside of your static root, it is highly recommended to copy them instead. This behavior can be disabled for trusted deployments using `allow_unsafe_symlinks` / `SERVESTATIC_ALLOW_UNSAFE_SYMLINKS`.

### Fixed

- Fixed a range-request edge case where the last byte could be requested but would not be served.

### Security

- Hardened `autorefresh` path matching to prevent potential path traversal or path clobbering.
- Hardened static file resolution to block symlink breakout by default.

## [4.0.0] - 2026-03-05

### Added
Expand Down Expand Up @@ -129,7 +162,8 @@ Don't forget to remove deprecated code on each major release!

- Forked from [`whitenoise`](https://github.com/evansd/whitenoise) to add ASGI support.

[Unreleased]: https://github.com/Archmonger/ServeStatic/compare/4.0.0...HEAD
[Unreleased]: https://github.com/Archmonger/ServeStatic/compare/4.1.0...HEAD
[4.1.0]: https://github.com/Archmonger/ServeStatic/compare/4.0.0...4.1.0
[4.0.0]: https://github.com/Archmonger/ServeStatic/compare/3.1.0...4.0.0
[3.1.0]: https://github.com/Archmonger/ServeStatic/compare/3.0.2...3.1.0
[3.0.2]: https://github.com/Archmonger/ServeStatic/compare/3.0.1...3.0.2
Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,39 +19,39 @@

_Production-grade static file server for Python WSGI & ASGI._

_This project is a fork of [WhiteNoise](https://github.com/evansd/whitenoise) for [ASGI support, bug fixes, new features, and performance upgrades](https://archmonger.github.io/ServeStatic/latest/changelog/)._
_This project is a fork of [WhiteNoise](https://github.com/evansd/whitenoise) for [ASGI support, bug fixes, security updates, new features, and performance upgrades](https://archmonger.github.io/ServeStatic/latest/changelog/)._

---

`ServeStatic` simplifies static file serving with minimal lines of configuration. It can operate standalone or transform your existing app into a self-contained unit, without relying on external services like nginx or Amazon S3. This can simplify any production deployment, but is especially useful for platforms like Heroku, OpenShift, and other PaaS providers.
`ServeStatic` simplifies static file serving with minimal lines of configuration. It enables you to create a self-contained unit (WSGI, ASGI, Django, or 'standalone') without requiring external services like Nginx or Amazon S3. This can simplify any production deployment, but is especially useful for platforms like Heroku, OpenShift, and other PaaS providers.

It is designed to work seamlessly with CDNs to ensure high performance for traffic-intensive sites, and is compatible with any ASGI/WSGI app. Extra features and auto-configuration are available for [Django](https://www.djangoproject.com/) users.
It is designed to work seamlessly with CDNs to ensure high performance for traffic-intensive sites, and is compatible with any ASGI/WSGI app. A command-line interface is provided to perform common tasks such as compression, file-name hashing, and manifest generation. Extra features and auto-configuration are available for [Django](https://www.djangoproject.com/) users.

Best practices are automatically handled by `ServeStatic`, such as:
When using `ServeStatic`, best practices are automatically handled such as:

- Automatically serving compressed content
- Proper handling of `Accept-Encoding` and `Vary` headers
- Setting far-future cache headers for immutable static files.

To get started or learn more about `ServeStatic`, visit the [documentation](https://archmonger.github.io/ServeStatic/).
Visit the [documentation](https://archmonger.github.io/ServeStatic/) to get started or learn more about `ServeStatic` .

## Frequently Asked Questions

### Isn't serving static files from Python horribly inefficient?

The short answer to this is that if you care about performance and efficiency then you should be using `ServeStatic` behind a CDN like CloudFront. Due to the caching headers `ServeStatic` sends, the vast majority of static requests will be served directly by the CDN without touching your application, so it really doesn't make much difference how efficient `ServeStatic` is.
The short answer to this is that if you care about performance and efficiency then you should be using `ServeStatic` behind a CDN (such as CloudFront). Due to the caching headers `ServeStatic` sends, the vast majority of static requests will be served directly by the CDN without touching your application, so it really doesn't make much difference how efficient `ServeStatic` is.

That said, `ServeStatic` is pretty efficient. Because it only has to serve a fixed set of files it does all the work of finding files and determining the correct headers upfront on initialization. Requests can then be served with little more than a dictionary lookup to find the appropriate response. Also, when used with gunicorn (and most other WSGI servers) the actual business of pushing the file down the network interface is handled by the kernel's very efficient `sendfile` syscall, not by Python.

### Shouldn't I be pushing my static files to S3 (using Django-Storages)?

No, you shouldn't. The main problem with this approach is that Amazon S3 cannot currently selectively serve compressed content to your users. Compression using the gzip or the modern brotli algorithm can make dramatic reductions in load time and bandwidth usage. But, in order to do this correctly the server needs to examine the `Accept-Encoding` header of the request to determine which compression formats are supported, and return an appropriate `Vary` header so that intermediate caches know to do the same. This is exactly what `ServeStatic` does, but Amazon S3 currently provides no means of doing this.
No, you shouldn't. The main problem with this approach is that Amazon S3 cannot currently selectively serve compressed content to your users. Compression using gzip, zstd (Python 3.14+), or brotli can make dramatic reductions in load time and bandwidth usage. But, in order to do this correctly the server needs to examine the `Accept-Encoding` header of the request to determine which compression formats are supported, and return an appropriate `Vary` header so that intermediate caches know to do the same. This is exactly what `ServeStatic` does, but Amazon S3 currently provides no means of doing this.

The second problem with a push-based approach to handling static files is that it adds complexity and fragility to your deployment process: extra libraries specific to your storage backend, extra configuration and authentication keys, and extra tasks that must be run at specific points in the deployment in order for everything to work. With the CDN-as-caching-proxy approach that `ServeStatic` takes there are just two bits of configuration: your application needs the URL of the CDN, and the CDN needs the URL of your application. Everything else is just standard HTTP semantics. This makes your deployments simpler, your life easier, and you happier.

### What's the point in `ServeStatic` when I can use `apache`/`nginx`?

There are two answers here. One is that ServeStatic is designed to work in situations where `apache`, `nginx`, and the like aren't easily available. But more importantly, it's easy to underestimate what's involved in serving static files correctly. Does your few lines of nginx configuration distinguish between files which might change and files which will never change and set the cache headers appropriately? Did you add the right CORS headers so that your fonts load correctly when served via a CDN? Did you turn on the special nginx setting which allows it to send gzip content in response to an `HTTP/1.0` request, which for some reason CloudFront still uses? Did you install the extension which allows you to serve brotli-encoded content to modern browsers?
There are two answers here. One is that ServeStatic is designed to work in situations where `apache`, `nginx`, and the like aren't easily available. But more importantly, it's easy to underestimate what's involved in serving static files correctly. Does your few lines of nginx configuration distinguish between files which might change and files which will never change and set the cache headers appropriately? Did you add the right CORS headers so that your fonts load correctly when served via a CDN? Did you turn on the special nginx setting which allows it to send gzip content in response to an `HTTP/1.0` request? Did you install, enable, and configure support for zstd and brotli compression?

None of this is rocket science, but it's fiddly and annoying and `ServeStatic` takes care of all it for you.

Expand Down
24 changes: 20 additions & 4 deletions docs/src/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ You can either run this during development and commit your generated/compressed
$ servestatic --help
usage: servestatic [-h] [--all] [--hash] [--manifest] [--merge-manifest]
[--compress] [--clear] [-q] [--copy-original] [--no-gzip]
[--no-brotli]
[--no-brotli] [--no-zstd] [--zstd-dict ZSTD_DICT]
[--zstd-dict-raw] [--zstd-level ZSTD_LEVEL]
[-e EXCLUDE]
src dest

Expand All @@ -34,7 +35,8 @@ options:
--merge-manifest Merge the new manifest with an existing manifest in the
dest directory. Fails if the existing manifest is not
found. (default: False)
--compress Generate compressed versions (gzip/brotli) of files.
--compress Generate compressed versions (gzip/zstd/brotli) of
files.
(default: False)
--clear Empty the destination directory before processing.
(default: False)
Expand All @@ -46,6 +48,16 @@ options:
--compress). (default: True)
--no-brotli Don't produce brotli '.br' files (only applies with
--compress). (default: True)
--no-zstd Don't produce zstd '.zstd' files (only applies with
--compress). (default: True)
--zstd-dict ZSTD_DICT
Path to a zstd dictionary file (only applies with
--compress). (default: None)
--zstd-dict-raw Treat the zstd dictionary as raw content (only applies
with --compress). (default: False)
--zstd-level ZSTD_LEVEL
Compression level for zstd output (only applies with
--compress). (default: None)
-e EXCLUDE, --exclude EXCLUDE
Glob pattern(s) to exclude from processing
(compression/hashing). These files are still copied.
Expand All @@ -54,9 +66,13 @@ options:

## Compression Details

When ServeStatic builds its list of available files, it optionally checks for corresponding files with a `.gz` and a `.br` suffix (e.g., `scripts/app.js`, `scripts/app.js.gz` and `scripts/app.js.br`). If it finds them, it will assume that they are (respectively) gzip and [brotli](https://en.wikipedia.org/wiki/Brotli) compressed versions of the original file and it will serve them in preference to the uncompressed version whenever clients indicate they support that compression format.
When ServeStatic builds its list of available files, it optionally checks for corresponding files with `.gz`, `.zstd`, and `.br` suffixes (e.g., `scripts/app.js`, `scripts/app.js.gz`, `scripts/app.js.zstd`, and `scripts/app.js.br`). If it finds them, it will serve those compressed versions when clients indicate support via `Accept-Encoding`.

On Python 3.14+, zstd support is built in via the standard library.

On older Python versions, Brotli support is available by installing the [Brotli](https://pypi.org/project/Brotli/) package.

In order for brotli compression to work, the [Brotli](https://pypi.org/project/Brotli/) python package must be installed.
You can also pass a custom zstd dictionary with `--zstd-dict` and optionally mark it raw with `--zstd-dict-raw`.

## Hash Details

Expand Down
5 changes: 5 additions & 0 deletions docs/src/dictionary.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
wsgi
asgi
gzip
zstd
filesystem
mimetype
mimetypes
Expand Down Expand Up @@ -36,3 +37,7 @@ linters
linting
pytest
formatters
misconfigurations
encodings
symlink
symlinks
53 changes: 49 additions & 4 deletions docs/src/django-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,63 @@ The W3C [explicitly state](https://www.w3.org/TR/cors/#security) that this behav

---

## `SERVESTATIC_ALLOW_UNSAFE_SYMLINKS`

**Default:** `False`

Controls whether symlinks that resolve outside configured static roots are allowed.

By default, ServeStatic blocks symlink breakout so requests cannot escape the configured static directory tree. Set this to `True` only if you intentionally depend on symlinks that point outside your static roots and you trust those links.

---

## `SERVESTATIC_SKIP_COMPRESS_EXTENSIONS`

**Default:** `('jpg', 'jpeg', 'png', 'gif', 'webp','zip', 'gz', 'tgz', 'bz2', 'tbz', 'xz', 'br', 'swf', 'flv', 'woff', 'woff2')`
**Default:** `('jpg', 'jpeg', 'png', 'gif', 'webp','zip', 'gz', 'tgz', 'bz2', 'tbz', 'xz', 'br', 'zstd', 'swf', 'flv', 'woff', 'woff2')`

File extensions to skip when compressing.

Because the compression process will only create compressed files where this results in an actual size saving, it would be safe to leave this list empty and attempt to compress all files. However, for files which we're confident won't benefit from compression, it speeds up the process if we just skip over them.

---

## `SERVESTATIC_USE_ZSTD`

**Default:** `True`

Enable or disable zstd output generation when `compression.zstd` is available (Python 3.14+).

---

## `SERVESTATIC_ZSTD_DICTIONARY`

**Default:** `None`

Optional zstd dictionary to improve compression ratio for your asset corpus.

This setting can be either:

- a filesystem path to a trained dictionary file, or
- raw dictionary bytes / a prebuilt zstd dictionary object supplied by custom storage subclass logic.

---

## `SERVESTATIC_ZSTD_DICTIONARY_IS_RAW`

**Default:** `False`

Set to `True` if `SERVESTATIC_ZSTD_DICTIONARY` points to a raw-content dictionary.

---

## `SERVESTATIC_ZSTD_LEVEL`

**Default:** `None`

Optional zstd compression level.

---

## `SERVESTATIC_ADD_HEADERS_FUNCTION`

**Default:** `None`
Expand Down Expand Up @@ -196,9 +243,7 @@ If your deployment is more complicated than this (for instance, if you are using

Stores only files with hashed names in `STATIC_ROOT`.

By default, Django's hashed static files system creates two copies of each file in `STATIC_ROOT`: one using the original name, e.g. `app.js`, and one using the hashed name, e.g. `app.db8f2edc0c8a.js`. If `ServeStatic`'s compression backend is being used this will create another two copies of each of these files (using Gzip and Brotli compression) resulting in six output files for each input file.

In some deployment scenarios it can be important to reduce the size of the build artifact as much as possible. This setting removes the "unhashed" version of the file (which should be not be referenced in any case) which should reduce the space required for static files by half.
This setting removes the "unhashed" version of the file (which should be not be referenced in any case) which should reduce the space required for static files. In some deployment scenarios it can be important to reduce the size of the build artifact as much as possible.

This setting is only effective if the `ServeStatic` storage backend is being used.

Expand Down
Loading