Injecting CORS headers in response, larger requests fail

We’re hosting a lot of audio, video and image files on a CDN (outside of Cloudflare). We’re currently running into an issue where the CDN doesn’t seem to pass on CORS headers correctly. In order to mitigate this issue I’ve set up a Cloudflare worker using the following code:

addEventListener('fetch', event => {
  console.log(event.request);
  event.respondWith(handle(event.request))
})

async function handle(request) {
  if (request.method === "OPTIONS") {
    return handleOptions(request)
  } else if (request.method === "GET") {
    let fetchedFile = await fetch(request);
    let response = new Response(fetchedFile.body, {headers: fetchedFile.headers});
    response.headers.append("Access-Control-Allow-Origin", "*");
    return response
  } else if (request.method === "HEAD" ||
             request.method === "POST"){
    fetch(request)
  } else {
    return new Response(null, {
      status: 405,
      statusText: "Method Not Allowed",
    })
  }
}

// We support the GET, POST, HEAD, and OPTIONS methods from any origin,
// and accept the Content-Type header on requests. These headers must be
// present on all responses to all CORS requests. In practice, this means
// all responses to OPTIONS requests.
const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, HEAD, POST, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type",
}

function handleOptions(request) {
  if (request.headers.get("Origin") !== null &&
      request.headers.get("Access-Control-Request-Method") !== null &&
      request.headers.get("Access-Control-Request-Headers") !== null) {
    // Handle CORS pre-flight request.
    return new Response(null, {
      headers: corsHeaders
    })
  } else {
    // Handle standard OPTIONS request.
    return new Response(null, {
      headers: {
        "Allow": "GET, HEAD, POST, OPTIONS",
      }
    })
  }
}

The problem I’m running into is that the code doesn’t work correctly for larger files. When I use this for smaller files like images or MP3 files, I can see the headers being injected and the file is being served and displayed by the browser.

If I however try to do this with a file larger than ±1.5mb, for example a video file of 6.6mb, strange things start to happen. The requests suddenly gets split into multiple requests of 6.1mb, 6.6mb and 55kb. This seems to confuse the browser (both Chrome and Firefox) and the video will not be played. Firefox gives a message saying “wrong mime type”. The headers are being injected, but because it comes back in seemingly 3 pieces, it won’t play.

When I try the same request using curl however and try to playback the video using VLC, there’s nothing wrong with it.

Does anyone know why this is happening or how to resolve it?

Hey @rene6 - have you tried using the streaming API? Not sure that it will be the fix but could be a good start.

The general idea is that you set up a stream, which you can immediately return to the client, while the Workers script finishes populating the pipe with the remainder of the response:

addEventListener('fetch', event => {
  event.respondWith(fetchAndStream(event.request))
})

async function fetchAndStream(request) {
  let response = await fetch(request)
  let { readable, writable } = new TransformStream()
  response.body.pipeTo(writable)
  return new Response(readable, response)
}

Here’s the docs: https://workers.Cloudflare.com/docs/reference/runtime/apis/streams/

1 Like

Hi @signalnerve,

I’ve had a look at the Streaming API just now and it seems to resolve my issue. I ended up still having to turn the response into a new Response so I could inject the extra headers (they’re read-only after all), but after that it worked smoothly.

addEventListener('fetch', event => {
  // console.log(event.request);
  event.respondWith(handle(event.request))
})

async function handle(request) {
  if (request.method === "OPTIONS") {
    return handleOptions(request)
  } else if (request.method === "GET") {
    
    let response = await fetch(request)

    let { readable, writable } = new TransformStream()
    response.body.pipeTo(writable)
    
    let newResponse = new Response(readable, response); // Turn stream into a new response to make headers writeable
    newResponse.headers.append("Access-Control-Allow-Origin", "*"); // Add CORS header
    return newResponse
    
  } else if (request.method === "HEAD" ||
             request.method === "POST"){
    fetch(request)
  } else {
    return new Response(null, {
      status: 405,
      statusText: "Method Not Allowed",
    })
  }
}

// We support the GET, POST, HEAD, and OPTIONS methods from any origin,
// and accept the Content-Type header on requests. These headers must be
// present on all responses to all CORS requests. In practice, this means
// all responses to OPTIONS requests.
const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, HEAD, POST, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type",
}

function handleOptions(request) {
  if (request.headers.get("Origin") !== null &&
      request.headers.get("Access-Control-Request-Method") !== null &&
      request.headers.get("Access-Control-Request-Headers") !== null) {
    // Handle CORS pre-flight request.
    return new Response(null, {
      headers: corsHeaders
    })
  } else {
    // Handle standard OPTIONS request.
    return new Response(null, {
      headers: {
        "Allow": "GET, HEAD, POST, OPTIONS",
      }
    })
  }
}

There’s one odd thing I’m still seeing though. The browser works fine, but whenever I try to retrieve the same URL using curl I end up with an error 500, where the browser gives a 206.

CURL output:

HTTP/2 500 
date: Wed, 31 Jul 2019 10:42:07 GMT
content-type: text/html; charset=UTF-8
content-length: 3670
set-cookie: __cfduid=db08ac826dec280581874e118a00052701564569727; expires=Thu, 30-Jul-20 10:42:07 GMT; path=/; domain=.example.com; HttpOnly; Secure
expect-ct: max-age=604800, report-uri="https://report-uri.Cloudflare.com/cdn-cgi/beacon/expect-ct"
server: Cloudflare
cf-ray: 4feef6becde59d24-AMS

Is this expected behaviour when using the Streaming API?

Hey @rene6, glad that implementation worked in browser! And yep, responses are immutable, so needing to make a new response makes sense.

As for the curl issue - unfortunately, I’m not sure! As far as I know, there shouldn’t be any differences in browser, but maybe other folks here will know of something?

1 Like

Hi @signalnerve,

After doing some more testing, unfortunately I do run into issues. When using Firefox to retrieve the assets the browser alternates between “Status 200 OK” and “Status 500” worker threw an exception. This happens between every refresh.

An example asset would be: https://assets.123zing.nl/production/uploads/image/file/73/small_4d9ee9d8-574a-4192-85c3-d378d2d93d80_1453845438.jpg?v=1453845438

Could you guys look closer into this?

So looking further I came across the following topic:

This sounded exactly like my case. So I’ve implemented the following:

addEventListener('fetch', event => {
  // console.log(event.request);
  event.respondWith(handle(event.request))
})

async function handle(request) {
  if (request.method === "OPTIONS") {
    return handleOptions(request)
  } else if (request.method === "GET") {
    
    let response = await fetch(request)

    // Handle status 304
    if (response.status !== 200) {
      return response
    }
    
    let { readable, writable } = new TransformStream()
    response.body.pipeTo(writable)
        
    let newResponse = new Response(readable, response);
    newResponse.headers.append("Access-Control-Allow-Origin", "*");
    return newResponse
    
  } else if (request.method === "HEAD" ||
             request.method === "POST"){
    fetch(request)
  } else {
    return new Response(null, {
      status: 405,
      statusText: "Method Not Allowed",
    })
  }
}

// We support the GET, POST, HEAD, and OPTIONS methods from any origin,
// and accept the Content-Type header on requests. These headers must be
// present on all responses to all CORS requests. In practice, this means
// all responses to OPTIONS requests.
const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, HEAD, POST, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type",
}

function handleOptions(request) {
  if (request.headers.get("Origin") !== null &&
      request.headers.get("Access-Control-Request-Method") !== null &&
      request.headers.get("Access-Control-Request-Headers") !== null) {
    // Handle CORS pre-flight request.
    return new Response(null, {
      headers: corsHeaders
    })
  } else {
    // Handle standard OPTIONS request.
    return new Response(null, {
      headers: {
        "Allow": "GET, HEAD, POST, OPTIONS",
      }
    })
  }
}

So basically I check if there’s a status 200 or not. If it isn’t, than it was probably a 304 from the origin and it should just be passed on. This seems to solve the flip flopping (we’ll monitor over the next few hours). Though curl still gives a status 500.