Upload Progress

Upload Progress and Client-Side FormData

In the browser, new FormData(form) builds the same multipart/form-data body as a submit, so the PHP side of an AJAX upload is unchanged. For a progress bar, send it with XMLHttpRequest and read loaded and total in xhr.upload.onprogress; fetch() has no upload progress (see Front-End Web Development).

PHP can also track it. When a PHP_SESSION_UPLOAD_PROGRESS field arrives before the files, PHP keeps $_SESSION['upload_progress_<value>'] updated with content_length, bytes_processed, done and per-file details while the body streams in:

progress.php: report an upload's progress as JSONPHP
<?php
session_start(['read_and_close' => true]);        // read, unlock, never write
$p = $_SESSION[ini_get('session.upload_progress.prefix') . ($_GET['id'] ?? '')] ?? null;
header('Content-Type: application/json');
echo json_encode($p ? [intdiv(100 * $p['bytes_processed'], $p['content_length']), $p['done']]
  : null), "\n";
Output
$ curl -s -c up.txt 127.0.0.1:8215/cart.php                     # start a session
$ curl -s -b up.txt --limit-rate 150k -F PHP_SESSION_UPLOAD_PROGRESS=scan1 \
  -F 'images[]=@scan.png' 127.0.0.1:8215/gallery.php > upload.txt &
$ for i in 1 2 3 4 5; do sleep 1; curl -s -b up.txt 127.0.0.1:8215/progress.php?id=scan1; done
[18,false]
[37,false]
[56,false]
[56,false]
null
$ wait; cat upload.txt
scan.png     813242  image/png

The throttled 813 KB upload reported progress each second, and session.upload_progress.cleanup removed the key at the end. read_and_close frees the session lock at once, so polling never delays the upload's own updates. This needs the Apache 129 module: PHP-FPM behind FastCGI sees the body only once it is complete, and php -S printed only null. XHR events work everywhere.