v0.4.0

Protocol

  • Added the Creation With Upload extension: clients may now send the first part of an upload in the creation POST by setting Content-Type: application/offset+octet-stream, saving a round trip. The response carries Upload-Offset alongside Location, and a body that arrives complete finalises the upload immediately, firing on_complete_upload from the creation request. Advertised as creation-with-upload in Tus-Extension.
  • Fixed Tus-Extension advertisement: the Expiration extension has been implemented since v0.2.0 but was never advertised, so clients had no way to discover it. It is now listed whenever an :expiration_period is configured.
  • Fixed missing Tus-Resumable on error responses: the PATCH 404/409/413/400 branches and the POST 413 branch omitted the header, which strict clients may read as a response from a non-tus server. The 412 version-rejection responses remain without it, matching the reference implementation.

Behaviour Changes

  • A request without Tus-Resumable now returns 412 Precondition Failed with a Tus-Version header, instead of 400 Bad Request with no header, as the protocol requires. Callers matching on 400 for a missing version header need to expect 412.

Dependencies

  • Upgraded Plug: 1.5.0 -> 1.20.3. The declared requirement moved from ~> 1.3 to >= 1.15.4 and < 2.0.0, which excludes the releases affected by CVE-2026-54892, CVE-2026-56813 and CVE-2026-56814 while staying permissive for downstream applications. Pulls in mime 2.0.7 and adds plug_crypto and telemetry as transitive dependencies.
  • Removed the uuid dependency: that package has not been published since
    1. Upload ids are now generated by an internal Tussle.UID module, which produces RFC 9562 version 4 UUIDs from :crypto.strong_rand_bytes/1. The output format is unchanged (36-character lowercase hyphenated), so storage layouts and tus_storage_s3 are unaffected.
  • Upgraded ex_doc (dev only): 0.40.1 -> 0.40.3.

Improvements

  • Toolchain: pinned to Erlang/OTP 29.0.5 and Elixir 1.20.3, with CI bumped to match.
  • Docs: added docs/UPSTREAM_COMPAT.md recording Tussle's standing against upstream tus clients and the IETF resumable upload draft, with an effort estimate for each remaining gap.
  • Docs: documented the CORS configuration browser clients need. An incomplete Access-Control-Expose-Headers list is a quiet failure mode: browsers hide Location and Upload-Offset from JavaScript unless they are explicitly exposed, so uploads start but can never resume.
  • Replaced the deprecated use Plug.Test in the test suite with direct imports.

v0.3.1

Bug Fixes

  • Fixed upload-offset header: Previously returned the old file.offset instead of new_offset after appending data, causing clients to retry uploading the same chunk repeatedly.

Improvements

  • Configurable read_body options: Added application config options for tuning request body reading:

    • read_body_length - max body size (default: 100MB)
    • read_body_read_length - chunk size for throughput (default: 256KB)
    • read_body_timeout - timeout per read (default: 30s)
  • Error logging: Added Logger.error calls for failed operations to aid debugging.

v0.3.0

CloudFlare HEAD-to-GET Compatibility

Added support for CDNs (notably CloudFlare) that convert HEAD requests to GET requests.

  • Added Tussle.get/2 function that mirrors Tussle.head/2 behavior
  • Added Tussle.Routes module with add_tus_routes/1 macro for easy route setup
  • Updated controller to include get/2 action
  • Fixed read_body/2 to properly accumulate chunked request bodies in PATCH handler
  • Added CDN-Cache-Control: no-store header to HEAD/GET responses

Why this matters: CloudFlare's caching layer converts HEAD requests to GET, which unexpectedly violates the expectations of the TUS protocol. The new GET route and add_tus_routes macro restore compatibility.

Usage

# In your router:
import Tussle.Routes

scope "/files", MyAppWeb do
  pipe_through :api
  add_tus_routes UploadController
end

v0.2.0

Fork of the original tus package, renamed to Tussle.

Changes from original tus package

  • Renamed package from tus to tussle to allow publishing updates to Hex
  • Updated to Elixir 1.18+ / OTP 28
  • Replaced deprecated use Mix.Config with import Config
  • Replaced deprecated Supervisor.Spec.worker/3 with modern child spec syntax
  • Added Tussle.Storage behaviour module (from ringods)
  • Added @type t() typespec to Tussle.File (from ringods)
  • Fixed metadata parsing to return map instead of list (from ringods)
  • Added @behaviour Tussle.Storage to Tussle.Storage.Local

Merged community contributions

  • Storage behaviour and File typespecs (ringods via bucha)
  • Metadata as map fix (ringods via bucha)
  • Expiration protocol (davec82)
  • on_complete_upload result checking (davec82)
  • Empty metadata handling (davec82)
  • Location prefix support (zkessin)
  • Storage provider offset control (Clause-Logic)
  • init_file callback (marcinkoziej)
  • Missing config handling (marcinkoziej)
  • source_url fix (feng19)
  • mix.exs formatting fix (joeljuca)