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”.

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

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; = '_blank';


onMounted( () => {
      .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