Skip to content

Resumable downloads corrupt binary output due to manual header/print streaming #255

@DavRet

Description

@DavRet

Summary

  • Extension: beechit/fal-securedownload
  • TYPO3 version: 12 LTS
  • PHP version: 8.2
  • Storage driver: Local file system
  • Issue: Enabling the “resumable download” option corrupts binary file delivery (e.g., .xls)

Steps to Reproduce

  1. Install/enable fal_securedownload on a TYPO3 v12 site.
  2. Set EXTENSIONS > fal_securedownload > resumable_download = 1.
  3. Protect a binary file (e.g., .xls) and request it via index.php?eID=dumpFile&f=...&token=....
  4. Open the downloaded file in the corresponding application (Excel).

Expected Result

  • TYPO3 returns the file through a PSR-7 ResponseInterface, with correct Content-Type, Content-Disposition, and Content-Length.
  • The file opens without errors or “repaired file” warnings.

Actual Result

  • The response is generated via manual header() / print output in ModifyFileDumpEventListener::dumpFileContents().
  • Downloads contain corrupt bytes; Excel reports “file was repaired” and strips formatting.
  • Curl traces show inconsistencies in Content-Length and leading bytes compared to the original file.

Environment Details

  • TYPO3: 12.4.37
  • fal_securedownload: 5.0.7
  • PHP: 8.2.x FPM
  • Web server: Apache/2.4.59
  • Compression: zlib.output_compression off

Workaround

  • Set resumable_download = 0, which forces TYPO3 to use the core streamFile() response and resolves the corruption.

Suspected Root Cause

  • The resumable branch bypasses TYPO3’s PSR-7 streaming:
    • Manually emits headers (header(), Content-Length, Accept-Ranges).
    • Uses ob_clean(), while (ob_get_level()) { ob_end_clean(); }, fread() loop, and print.
    • Calls exit, preventing further middleware cleanup.
  • Any earlier output (BOM/whitespace/cookies) or header injection results in file corruption.

Proposed Fix / Ideas

  • Replace the manual streaming logic with a PSR-7 compliant implementation:
    • Use $event->setResponse($streamingResponse) with a StreamInterface that supports ranges.
    • Avoid direct header() calls; rely on ResponseInterface::withHeader.
    • Remove global exit usage and depend on the event response.
  • Alternatively, disable resumables by default until a PSR-7 approach is implemented.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions