Hello.
I'm witnessing a weird behavior of tus-php.
I'm trying upload files via LTE, but the uploaded files sometimes get broken and also may contain extra data.
For debugging purposes, I created a test file data.bin which contains 1259520 bytes of 8-byte integers (little endian) starting with 1 (0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00) — this way we can easily see how the data gets misplaced in the uploaded file.
I tried to upload my test file data.bin from a PC connected to an LTE modem, and this file ended up having size of 1269760 bytes (+10240 extra bytes). Besides, this file at the offset 294912 contains data from the very beginning of the original file (from the offset 0), also at the offset 1261568 (which is already beyond the original file) it starts all over from the beginning of the original.
Here's the uploaded file stats (file size is 1269760 instead of 1259520):
$ ll a0081539-b8e9-47f8-b5ee-83e4f8a449f8_6894ab9b48a27
-rw-r--r-- 1 user user 1269760 aug 7 16:36 a0081539-b8e9-47f8-b5ee-83e4f8a449f8_6894ab9b48a27
And here's the Redis record taken from the server after uploading has finished:
{
"offset": 294912,
"name": "a0081539-b8e9-47f8-b5ee-83e4f8a449f8",
"size": 1259520,
"checksum": "",
"location": "https://example.com/tus/af81d203-3cd4-4eeb-9162-99091c38f63a",
"file_path": "/var/www/uploads/a0081539-b8e9-47f8-b5ee-83e4f8a449f8_6894ab9b48a27",
"metadata": {
"filename": "a0081539-b8e9-47f8-b5ee-83e4f8a449f8"
},
"created_at": "Thu, 07 Aug 2025 13:35:23 GMT",
"expires_at": "Fri, 08 Aug 2025 13:35:23 GMT",
"upload_type": "normal"
}
Please note that the offset is somehow not equal to either of 1259520 or 1269760, which is weird. If I understand correctly, successful uploads have both offset and size fields equal.
I searched through issues and found this #391 issue which is closed due to inactivity.
The author is correct though: you open output file with the a flag (append mode) and the fseek function you call on the file later has no effect if the file is opened in this mode.
As per the PHP documentation (https://www.php.net/manual/en/function.fopen.php):
'a': In this mode, fseek() has no effect, writes are always appended.
To make fseek really work you would probably need to use the c mode (or maybe another one that doesn't render fseek useless), from the same source:
'c': Open the file for writing only. If the file does not exist, it is created.
If it exists, it is neither truncated (as opposed to 'w'), nor the call to this
function fails (as is the case with 'x'). The file pointer is positioned on the
beginning of the file.
To Reproduce
Hard to reproduce, as it happens like 1-2 times per 1000 uploads (or even more rarely), maybe due to LTE connectivity issues. See the Additional context below for more info.
Expected behavior
Uploaded file should be a bit-by-bit copy of the original.
Additional context
Here's an excerpt from the log of my client application that does the file uploads with some comments on what's going on (sorry, I can't share the code):
// ...
// Upload created
[2025-08-07T16:35:22] debug File location on server: "https://example.com/tus/af81d203-3cd4-4eeb-9162-99091c38f63a"
// Scheduler calls the UploadFile() function
[2025-08-07T16:35:22] debug Offering offset for TUS upload: 0
[2025-08-07T16:35:23] debug Sending 1048576 bytes at offset 0
[2025-08-07T16:36:23] debug cURL runtime error (PATCH): 28 Operation timed out after 60001 milliseconds with 0 bytes received
[2025-08-07T16:36:23] debug Error while sending chunk: "Operation timed out after 60001 milliseconds with 0 bytes received"
[2025-08-07T16:36:23] debug Warning: failed to upload chunk at offset 0 (attempt 1/3)
// Waiting for 2 secs before retrying
[2025-08-07T16:36:25] debug Sending 1048576 bytes at offset 0
[2025-08-07T16:36:26] debug Unknown error, will retry; HTTP status: 416
[2025-08-07T16:36:26] debug Warning: failed to upload chunk at offset 0 (attempt 2/3)
// Waiting for 2 secs before retrying
[2025-08-07T16:36:28] debug Sending 1048576 bytes at offset 0
[2025-08-07T16:36:29] debug Unknown error, will retry; HTTP status: 416
[2025-08-07T16:36:29] debug Warning: failed to upload chunk at offset 0 (attempt 3/3)
// UploadFile() method returns false, saving the current offset (0) and allowing the scheduler to resume the upload using the saved offset
// Scheduler calls the UploadFile() function again
[2025-08-07T16:36:32] debug Offering offset for TUS upload: 0
[2025-08-07T16:36:32] debug Server demands new offset: 1269760 instead of offered (0). Obeying
// ...
The Unknown error, will retry is written when all the other conditions I expect in the code, were not met (like 'no response', or HTTP codes 200..399, 409, 408, 403, 404, etc.). Error 416 is triggered here due to appending data beyond the original file size:
|
if ($this->offset > $totalBytes) { |
|
throw new OutOfRangeException('The uploaded file is corrupt.'); |
|
} |
Also, please note the
Server demands new offset: 1269760 instead of offered (0). Obeying line in the log: it means that my client software detected the
409 response code in which server replied with
Upload-Offset: 1269760, which is beyond the original file size. Imo, it should respond with
416 in this case.
What I think is happening, is that tus-php somehow ignores the offset (or maybe loses its value due to connection issues) offered by my client in the PATCH requests and appends the data to the file because of using a flag instead of c. So all in all, looks like the real issue here is not using the append mode (which is still incorrect if you intend fseek to really move the write pointer), but incorrect handling of the offset. Otherwise, tus php would reject my offering with the 409 status code. While I understand that I should treat 416 as a fatal error and I will, the upload gets wrecked on the server before I receive this code, so I can't do anything about it.
Screenshots of `vbindiff` (hexadecimal file display and comparison)
First mismatch:
Extra data in the end:
Hello.
I'm witnessing a weird behavior of tus-php.
I'm trying upload files via LTE, but the uploaded files sometimes get broken and also may contain extra data.
For debugging purposes, I created a test file
data.binwhich contains1259520bytes of 8-byte integers (little endian) starting with1(0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00) — this way we can easily see how the data gets misplaced in the uploaded file.I tried to upload my test file
data.binfrom a PC connected to an LTE modem, and this file ended up having size of1269760bytes (+10240extra bytes). Besides, this file at the offset294912contains data from the very beginning of the original file (from the offset0), also at the offset1261568(which is already beyond the original file) it starts all over from the beginning of the original.Here's the uploaded file stats (file size is
1269760instead of1259520):And here's the Redis record taken from the server after uploading has finished:
{ "offset": 294912, "name": "a0081539-b8e9-47f8-b5ee-83e4f8a449f8", "size": 1259520, "checksum": "", "location": "https://example.com/tus/af81d203-3cd4-4eeb-9162-99091c38f63a", "file_path": "/var/www/uploads/a0081539-b8e9-47f8-b5ee-83e4f8a449f8_6894ab9b48a27", "metadata": { "filename": "a0081539-b8e9-47f8-b5ee-83e4f8a449f8" }, "created_at": "Thu, 07 Aug 2025 13:35:23 GMT", "expires_at": "Fri, 08 Aug 2025 13:35:23 GMT", "upload_type": "normal" }Please note that the offset is somehow not equal to either of
1259520or1269760, which is weird. If I understand correctly, successful uploads have bothoffsetandsizefields equal.I searched through issues and found this #391 issue which is closed due to inactivity.
The author is correct though: you open output file with the
aflag (append mode) and thefseekfunction you call on the file later has no effect if the file is opened in this mode.As per the PHP documentation (https://www.php.net/manual/en/function.fopen.php):
To make
fseekreally work you would probably need to use thecmode (or maybe another one that doesn't renderfseekuseless), from the same source:To Reproduce
Hard to reproduce, as it happens like 1-2 times per 1000 uploads (or even more rarely), maybe due to LTE connectivity issues. See the Additional context below for more info.
Expected behavior
Uploaded file should be a bit-by-bit copy of the original.
Additional context
Here's an excerpt from the log of my client application that does the file uploads with some comments on what's going on (sorry, I can't share the code):
The
Unknown error, will retryis written when all the other conditions I expect in the code, were not met (like 'no response', or HTTP codes 200..399, 409, 408, 403, 404, etc.). Error416is triggered here due to appending data beyond the original file size:tus-php/src/File.php
Lines 333 to 335 in a466a9b
Also, please note the
Server demands new offset: 1269760 instead of offered (0). Obeyingline in the log: it means that my client software detected the409response code in which server replied withUpload-Offset: 1269760, which is beyond the original file size. Imo, it should respond with416in this case.What I think is happening, is that tus-php somehow ignores the offset (or maybe loses its value due to connection issues) offered by my client in the PATCH requests and appends the data to the file because of using
aflag instead ofc. So all in all, looks like the real issue here is not using the append mode (which is still incorrect if you intendfseekto really move the write pointer), but incorrect handling of the offset. Otherwise, tus php would reject my offering with the409status code. While I understand that I should treat416as a fatal error and I will, the upload gets wrecked on the server before I receive this code, so I can't do anything about it.Screenshots of `vbindiff` (hexadecimal file display and comparison)
First mismatch: