Skip to content

FileTransfer.uploadFile with chunkedMode: true defaults to multipart/form-data unless Content-Type is manually set, and reports contentLength: -1 on iOS #10

@LukasGrubis

Description

@LukasGrubis

Bug Report

Capacitor Version

@capacitor/cli: 7.4.2
@capacitor/core: 7.4.2
@capacitor/ios: 7.4.2
@capacitor/android: 7.4.2 (not used in this case)
@capacitor/filesystem: 7.1.2
@capacitor/file-transfer: 1.0.1
@capawesome/capacitor-file-picker: 7.2.0

Platform(s)

iOS 18.5 (physical device tested)
Android not tested for this issue

Current Behavior

Using @capacitor/[email protected], this upload call:

await FileTransfer.uploadFile({
  chunkedMode: true,
  path: filePath,
  url: uploadFileUrl,
  progress: true,
  headers: {
    // Needed manually to avoid multipart upload
    'Content-Type': 'application/octet-stream',
  },
});

Without the Content-Type header:

  • The request defaults to multipart/form-data; boundary=... — even though chunkedMode: true is specified.
  • Laravel fails to detect the uploaded file using $request->file('file'), $request->hasFile(), or $request->allFiles().
  • Laravel’s $request->getContent() returns raw stream data, but no usable metadata or file parsing is possible.

With Content-Type: application/octet-stream:

  • Laravel does receive the raw stream via $request->getContent() — and we're able to successfully save it to a file manually (e.g., using Storage::put()).
  • However, Laravel does not treat the stream as an uploaded file (i.e., it does not appear in $request->file() or $request->hasFile()).
  • This behavior differs from standard multipart/form-data uploads, where Laravel automatically parses and exposes uploaded files.

Additional Issue:

The progress event reports:

{ bytes: number, contentLength: -1 }
  • This prevents reliable upload progress calculation, even though the file size is known and can be read using Filesystem.stat() before upload.

Expected Behavior

  • With chunkedMode: true, the plugin should default to Content-Type: application/octet-stream (or at least warn about needing it).
  • Laravel should receive either a proper file in $request->file() or at least be clearly documented that stream handling must be manual.
  • The progress.contentLength should return the actual file size, if known, for accurate progress indicators.

Code Reproduction

iOS Side (Capacitor)

await FileTransfer.uploadFile({
  chunkedMode: true,
  path: filePath,
  url: uploadFileUrl,
  progress: true,
  headers: {
    'Content-Type': 'application/octet-stream',
  },
});

Laravel Controller

public function upload(Request $request)
{
    $data = [
        'method' => $request->method(),
        'headers' => $request->headers->all(),
        'file' => $request->file('file'),
        'has_file' => $request->hasFile('file'),
        'all_files' => $request->allFiles(),
        'raw_content' => $request->getContent(),
    ];

    return response()->json($data);
}

Other Technical Details

Environment:

  • Capacitor Core: 7.4.2
  • Plugin: @capacitor/[email protected]
  • Laravel: 12.x
  • Nuxt: 3.17.3
  • Vue: 3.5.18
  • Node.js: [e.g., 20.x]
  • Device: iPhone running iOS 18.5
  • Backend: Laravel with default file upload handling (no special middleware)

Additional Context

  • The default fallback to multipart/form-data causes silent file loss unless explicitly overridden.
  • chunkedMode should imply binary streaming or at least be documented with required headers.
  • The -1 value for contentLength in progress makes % calculation unreliable.
  • Other Capacitor plugins and tooling are working as expected.

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