Downloading files¶
This document describes how file downloads work in EDRI, including resumable downloads (HTTP Range) and high-throughput streaming via SharedMemoryPipe.
Two common download strategies¶
EDRI supports two primary ways to serve downloads:
File-backed response (seekable, size-known)
best for classic HTTP downloads
supports
Rangeresume automatically
Streamed response (on-the-fly generation)
best for generating large outputs dynamically (e.g. zipping many files)
can use
SharedMemoryPipefor throughputdoes not support HTTP
Rangeresume (size may be unknown)
File-backed downloads (recommended when possible)¶
If your response can point to a real file on disk (a pathlib.Path), EDRI can provide a full HTTP download experience.
Resume (HTTP Range)¶
When the response body is backed by pathlib.Path, EDRI automatically:
parses the client's
Rangeheaderrequires an
If-Rangeheader matching the file'sETag— Range is only honored whenIf-Rangematches exactlyIf
If-Rangematches and the range is valid: returns 206 Partial Content with aContent-Rangeheader and the requested byte sliceIf
If-Rangedoes not match or is missing: serves the full file (200 OK)If the range is malformed (unparseable): the
Rangeis ignored and the full file is served (200 OK), per RFC 7233If the range is well-formed but unsatisfiable (e.g. entirely past the end of the file): returns 416 Range Not Satisfiable with
Content-Range: bytes */<size>and an empty body
Note: Without an
If-Rangeheader (or if it doesn't match theETag), Range requests are ignored and the full file is returned.
Developer note: respond with the file as usual. EDRI handles Range logic automatically.
Errors reaching the file¶
When the pathlib.Path cannot be served, EDRI maps the failure to a status before the response body starts:
file does not exist (
FileNotFoundError) → 404 Not Foundany other filesystem error — stale handle, connection reset, backend down (
OSError) → 503 Service Unavailableresponseattributefilenot filled (AttributeError) → 500 Internal Server Error
If a read fails after streaming has begun (headers already sent), the status can no longer change: the failure is logged and the stream is aborted.
All filesystem operations (stat, open, seek, read, close) run in a worker thread so they never block the asyncio event loop.
Streamed downloads (on-the-fly generation)¶
Some downloads are too large to materialize first, or you want to generate them dynamically.
Example use cases:
creating a ZIP archive on the fly from many files
streaming a generated report
exporting large datasets
Lifetime¶
SharedMemoryPipe instances are per-request:
created for one request/response flow
used to transfer the payload
cleaned up when the request completes
Resume limitation¶
Classic HTTP resume via Range requires a seekable, fixed-size representation.
For streamed outputs where total size is unknown (or not stable), HTTP Range resume is not supported.
Declaring a file download¶
EDRI provides a File helper type for describing downloadable files.
Below are two short examples showing both supported approaches.
Event and response definition¶
from edri.api.dataclass import File
from edri.dataclass.event import Event, event
from edri.dataclass.response import Response, response
@response
class DownloadArchiveResponse(Response):
file: File
@event
class DownloadArchive(Event):
response: DownloadArchiveResponse
Option A: Path-backed download (resume-friendly)¶
Use this when the file exists on disk or you can generate it fully before responding.
from pathlib import Path
self.event.response.file = File(
file_name="archive.zip",
mime_type="application/zip",
path=Path("/tmp/archive.zip"),
)
✔ Supports HTTP Range resume (206 Partial Content).
Response headers¶
EDRI sets the following headers for file downloads.
Common headers (all downloads)¶
For both Path-backed and streamed downloads:
Content-Type is set to the file’s MIME type.
Content-Disposition is set to
attachmentand includes a UTF-8 encoded filename using thefilename*=syntax.
This ensures correct media type handling and proper filename support across browsers.
Path-backed downloads (pathlib.Path)¶
Additional headers:
Accept-Ranges: bytes – indicates that byte-range requests are supported.
ETag – typically based on the file fingerprint. Required for proper HTTP
Rangeresume behavior (must matchIf-Range).
If the client sends a satisfiable Range header with a matching If-Range:
Content-Length is set to the size of the returned range.
Content-Range describes the returned byte range (
bytes <start>-<end>/<size>).HTTP status: 206 Partial Content.
If the range is well-formed but unsatisfiable (matching If-Range):
Content-Range is
bytes */<size>.HTTP status: 416 Range Not Satisfiable (empty body).
If no Range header is present, If-Range does not match, or the Range is malformed:
Content-Length is set to the total file size.
HTTP status 200 OK is returned.
Implementation note¶
SharedMemoryPipe is not selected automatically by EDRI.
They are explicitly created by the developer — typically inside a Worker.
The intended pattern is:
A Worker creates the
SharedMemoryPipeThe Worker assigns the pipe reader to
event.response.fileThe Worker sends the event
The Worker streams bytes into the pipe
This design keeps heavy data transfer out of the Manager thread and enables high-throughput streaming directly from the Worker to the API client.