Failed attempt to integrate "Advanced upload flow using tus, for large videos" with Vue.js and PHP

I’m trying to do integration Direct creator uploads · Cloudflare Stream docs. Since we use Vue.js and PHP, the example from the documentation doesn’t really fit. I tried to modify it for our development languages and encountered a number of issues that I can’t overcome on my own. So, the example from the documentation was divided into two parts, namely:

  1. “Create your own API endpoint that returns an upload URL”.
  2. “Use this API endpoint with your tus client”.

To accomplish the first part, I used a PHP backend. A request to the Cloudflare API was created through curl and even got a response according to the documentation.

The second point, however, was implemented in Vue.js and had to use libraries (installed using npm): “@uppy/core”, “@uppy/tus”, “@uppy/drag-drop”, “@uppy/progress-bar”, “@uppy/dashboard”.

Problem list:

  1. Since frontend and backend are on different subdomains (or even domains) then when @uppy/tus tries to knock on backend to get “upload URL” due to giving information in header instead of json string - many CORS errors occur. For example: “Access to XMLHttpRequest at ‘${backend_domain}’ from origin ‘${frontend_domain}’ has been blocked by CORS policy: Request header field upload-length is not allowed by Access-Control-Allow-Headers in preflight response.”
  2. If you even temporarily skip the CORS errors, you’ll get the following response from Cloudflare API (the second step): “Only HEAD and PATCH requests allowed at this endpoint. Please refer to TUS protocol specification at https://tus.io/”.

If there is a working integration for this task or understanding how to solve current problems - I will be very grateful for help.

Hi @vrohovyi, thanks for reaching out.

For 1 – have you setup CORS properly on your server to handle the request? Unfortunately, I don’t have a PHP + Vue example on hand with Stream, but there is a reference implementation I added for Workers in Typescript. You can see an example CORs change that just allows everything:

If your case, you would want the explicit subdomain specified in Access-Control-Allow-Origin, the methods PATCH, POST and HEAD specified in Access-Control-Allow-Methods, etc.

Additional context on the reference implementation and TUS in general for Stream can be seen here:

Cloudflare Stream TUS Uploads.

Do you have any code snippets you’d be open to sharing that reproduces your issues (client + server)?

1 Like

Thank you for your reply.
PHP backend:

    $fileSize = $_SERVER['HTTP_UPLOAD_LENGTH'];

    // Determine the file ID from the URL path.
    $fileId = basename(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));

    /**/

    $headers = [
        "--header 'Authorization: bearer ". Config::CLOUDFLARE_API_TOKEN. "'",
        "--header 'Tus-Resumable: 1.0.0'",
    ];
    if (file_exists($fileId)) {
        $headers[] = "--header 'Upload-Length: " . filesize($fileId) . "'";
    } elseif (!empty($fileSize)) {
        $headers[] = "--header 'Upload-Length: " . $fileSize . "'";
    } else {
        $headers[] = "--header 'Upload-Length: 196300'";
    }

    $command = "curl -I -X POST '".Config::CLOUDFLARE_ENDPOINT."' " . implode(" ", $headers);
    $response = shell_exec($command);

    $header_size = strpos($response, "\r\n\r\n");
    $header_string = substr($response, 0, $header_size);
    $body = substr($response, $header_size + 4);

    $headers_array = explode("\r\n", $header_string);
    $headers_assoc = [];

    foreach ($headers_array as $header) {
        $header_parts = explode(": ", $header);

        if (count($header_parts) > 1) {
            $headers_assoc[$header_parts[0]] = trim($header_parts[1]);
        }
    }



    header('Access-Control-Expose-Headers: Location');
    header('Access-Control-Allow-Origin: *');
    header('Access-Control-Allow-Headers: *');
    //header("Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, upload-length");
    header('Location: ' . $headers_assoc['location']);

    //header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
    //header('Access-Control-Expose-Headers: Tus-Resumable, Location');
    header('Tus-Resumable', '1.0.0');
    // Return a 200 status code to the preflight request
    if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
        http_response_code(200);
        exit();
    }

Vue frontend:

< template>
< main>
< h2>Test Upload< /h2>
< div id=“drag-drop-area” style=“height: 300px”>< /div>
< div class=“for-ProgressBar”>< /div>
< button type=“button” id=“pick-files”>Pick Files< /button>
< div class=“uploaded-files” style=“margin-top: 50px”>
< ol>< /ol>
< /div>
< /main>
< /template>

< script>
import {inject, onMounted} from ‘vue’;
import Uppy from ‘@uppy/core’;
import Tus from ‘@uppy/tus’;
import DragDrop from ‘@uppy/drag-drop’;
import ProgressBar from ‘@uppy/progress-bar’;
import Dashboard from ‘@uppy/dashboard’;

import ‘@uppy/core/dist/style.min.css’;
import ‘@uppy/drag-drop/dist/style.min.css’;
import ‘@uppy/progress-bar/dist/style.min.css’;
import ‘@uppy/dashboard/dist/style.min.css’;

export default {
name: ‘MyComponent’,
setup() {
let apiPath = inject(‘apiPath’);
const uppy = new Uppy({debug: true, autoProceed: true});

const onUploadSuccess = el => (file, response) => {
  const li = document.createElement('li');
  const a = document.createElement('a');
  a.href = response.uploadURL;
  a.target = '_blank';
  a.appendChild(document.createTextNode(file.name));
  li.appendChild(a);

  document.querySelector(el).appendChild(li);
};

onMounted( () => {
  uppy
      .use(Dashboard, {trigger: '#pick-files'})
      .use(DragDrop, {target: '#drag-drop-area'})
      .use(ProgressBar, {target: document.querySelector('.for-ProgressBar'), hideAfterFinish: false})
      .use(Tus, {
        endpoint: `${apiPath}cloudflare/getUploadUrl`,
        chunkSize: 150 * 1024 * 1024
      })
      .on('upload-success', onUploadSuccess('.uploaded-files ol'));
});

}
}
< /script>
P.S. I added extra spaces in the html tags to make the code display properly

@schachte any luck?)

The frontend was fine. The issue was resolved by migrating the functionality from my own backend to the Worker tool from Cloudflare

For those encountering TUS issues with Stream in PHP, you can use this repo as a reference for getting everything set up correctly.

@vrohovyi presented two errors on its message that are actually not connected. This reply concerns only the second error, on which they got the message "Only HEAD and PATCH requests allowed at this endpoint. Please refer to TUS protocol specification at https://tus.io/”.

I’ve been investigating this problem for the last three days and I finally found its origin. It seems it’s quite a niche problem that should be affecting only specific PHP versions, and it might even be related to a known PHP bug.

First of all, I wouldn’t recommend to follow the repository proposed by @schachte (GitHub - Schachte/cloudflare-stream-tus-php: TUS upload implementation for Cloudflare Stream in PHP), as I found there are some concerning irregularities on the PHP server side implementation that only add to the confusion. @schachte please contact me if you need clarification on that.

The second error exposed by @vrohovyi, the one I was also getting, should only affect setups with the following specifications:

  • The server endpoint is implemented using PHP.

  • You’re using Tus uploads, called “Direct creator uploads” by Cloudflare.

  • You’re using a front-end Tus client (usually a Javascript client) to provide your users a way to realiably upload videos to Cloudflare.

And this is how it works, overly simplified for the sake of explaining the problem at hand:

  • Step 1

A user in your application uses a Tus client to upload a video file. The Tus client performs a request to and endpoint in your server.

  • Step 2

The endpoint in your server uses Cloudflare’s API to create a new Tus upload. This new upload has a unique Id and its own unique upload URL.

  • Step 3

The endpoint in your server replies to the Tus client with the information he needs to start uploading directly to the Tus server (A Cloudflare server).

  • Step 4 The Tus client now has all he needs to upload the file. The first thing he does is perform a HEAD request to Cloudflare to prepare for the next upload step.

  • Step 5 The Tus client performs multiple PATCH requests to Cloudflare containing fragments of the file to upload.

The problem presented by @vrohovyi is seen at Step 4, where Cloudflare replies with the HTTP error 405 (Method not allowed), and returns the following explanation: “Only HEAD and PATCH requests allowed at this endpoint. Please refer to TUS protocol specification […]”

This is caused because Cloudflare expects a HEAD request as the Tus protocol mandates, but the Tus Javascript client performs a GET request instead.

Not much information can be found on the net about this problem, an after investigating many leads, I was able to discard the following causes:

  • Even though some proxies, VPNs and even NGINX configurations will change incoming HEAD requests to GET for caching purposes, this was not the source of the problem.

  • The problem does not seem to be related to Cloduflare’s Tus implementation.

  • Even though some Cloudflare domains are being blocked by governments in certain countries like Spain (the domain that Cloduflare uses for Tus uploading is currently blocked in Spain), this is not the cause of this problem.

I’ve been able to trace the origin of the problem, which lies in the response provided by the PHP endpoint on Step 3:

The Tus client expects the reply to the endpoint to include a Location header containing the URL of the Tus endpoint where uploading requests must be done. This header is added via PHP with a line as simple as this:


header('Location: '.$tusUrl);

I observed that when specifying the Location header in PHP, the web server replied with a 302 (Temporarily moved) response code instead of a 200 (Found) code.

This happened even if you forced a 200 response code with something like:


http_response_code(200);

Or even when really trying to force it with:


header("HTTP/1.1 200 Accepted", true, 200);

PHP seems to be forcing a 302 HTTP response code when a Location header is set. This might’ve been an expected behavior, but I observed that certain PHP versions act differently on the same scenario.

I confirmed that this is the behavior that finally causes the Tus Javascript client to perform a wrong request to the Tus server: Receiving a 302 redirection HTTP code instead of a 200 HTTP code.

I haven’t investigated the precise reason why Tus client implementations react this way, but here are my suspects, and my suggestions for whoever wants to dig in deeper:

  • The HTTP stack used by Javascript Tus libraries might be automatically following the 302 redirect provided by the back-end endpoint, which messes up the rest of the implementation. Investigate Javascript Tus client implementations to find out why a GET request to the Tus server is performed instead of a HEAD request whenever the reply received by the back-end endpoint returns a 302 instead of a 200.

  • I observed this error happening in PHP version 8.2, but not in version 8.0. Investigate the changes within those two versions to find out wheter this is an expected behavior or a PHP bug. Perhaps this is an already documented PHP bug.

  • Is switching to a 302 HTTP code whenver a Location header is sent a correct behavior as per the HTTP protocol specs, or is it a bug?

The solution

I haven’t found a clean solution to this problem, but here are my two alternatives:

  • Switch to a PHP version that does not automatically set a 302 HTTP response code whenever a Location header is added.

  • Change your back-end endpoint to return a 201 (Created) HTTP response instead of a 200. This does not trigger the change to a 302 code in PHP 8.2, but it gets interpreted correctly by the Javascript Tus client, thus solving the issue, at least for the time being.

Some notes

  • You don’t need a fully-featured uploader Javascript client library like Uppy if you need to only provide a simple video uploading method to your users. GitHub - tus/tus-js-client: A pure JavaScript client for the tus resumable upload protocol is small, lightweight and works completely fine. Uppy, the one proposed in Cloudflare’s documentation is much bigger but has lots of interesting benefits and functionalities. Choose the one you really need.

  • This problem should also affect other Tus servers, not only Cloudflare’s.

  • This problem should also affect any kind of upload, not only videos.

  • Returning a 201 (Created) HTTP code on the back-end endpoint call might actually make more sense, since “Creating” the upload is exactly what that endpoint is doing.

1 Like

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.