Worker crashes 1 minute after request canceled

I’m developing a worker to serve audio files from R2 Storage. When I start the worker with wrangler dev and make a request using Postman (or any other similar tool), everything works just fine.
But when I put the link in an html audio tag like so:


Then on initial page load the browser makes a request to the specified resource and cancels the request pretty much instantly so it can get the headers and metadata of the audio file.
Then every time after exactly 1 minute the worker crashes. This only occurs when the client cancels a request while data is transferred.

I’ve also tried to deploy it, but then everything works fine.

Does anyone know how to fix this, or how to prevent this from happening?

Thanks for help.

Can you share some more details about what the code is actually doing? (proxying to R2; how? Are you using the R2 bindings? S3 API?)

Also, if possible, the actual code you’re using would be great so we can help troubleshoot further.

I just want to get the file from R2 Storage with the RADIO_BUCKET binding and send it to the client. I also want to support Range Requests. These however seem to work perfectly fine.

I try to achieve this by doing the following:

  • Get the R2 object metadata
  • Try to get the ‘Range’ header
  • Try to parse the ‘Range’ header
  • If the Range header is present and in the correct format it will return the specified byte range of the object.
  • If the Range header is not present or in not correct format then return the full R2 object.
Here is my code
export default {
  async fetch(request: Request, env: Env, context: ExecutionContext) {
    // handle /audio/ requests
    if (request.url.match(/\/audio\//)) {
      const response = await audio(request, env);
      return response;
    } else {
      return new Response("Not found", { status: 404 });

async function audio(request: Request, env: Env) {
  // Get file name from path
  const url = new URL(request.url);
  const key = url.pathname.split("/")[2];

  // Get object metadata
  const objectData = await env.RADIO_BUCKET.head(key);
  if (!objectData) return new Response("Not found", { status: 404 });

  // Get range header
  const rangeHeader = request.headers.get("Range");

  // If no range header, return full object
  if (!rangeHeader) {
    const object = await env.RADIO_BUCKET.get(key);
    return objectResponse(object!, 200);

  // If range header, return partial object
  const range = parseRangeHeader(rangeHeader, objectData.size);
  if (!range) return new Response("Range not satisfiable", { status: 416 });
  const { start, end } = range;
  const object = await env.RADIO_BUCKET.get(key, {
    range: {
      offset: start,
      length: end - start + 1,
  if (!object) return new Response("Not found", { status: 404 });

  return objectResponse(object, 206);

function objectResponse(object: R2ObjectBody, status: 206 | 200): Response {
  // Set headers
  const headers = new Headers();

  headers.set("Access-Control-Allow-Origin", "*");
  headers.set("Accept-Ranges", "bytes");
  headers.set("etag", object.httpEtag);

  // If partial content, set content range header
  if (status === 206) {
    const objectRange = object.range as { offset: number; length: number };
    const start = objectRange.offset;
    const end = objectRange.offset + objectRange.length - 1;
    const size = object.size;
    headers.set("Content-Range", `bytes ${start}-${end}/${size}`);

  return new Response(object.body, {
    status: status,

function parseRangeHeader(range: string, size: number) {
  if (range.startsWith("bytes=")) {
    const parts = range.replace("bytes=", "").split("-");
    const start = parseInt(parts[0], 10);
    const end = parts[1] ? parseInt(parts[1], 10) : size - 1;
    return { start, end };
  return null;
Here is the error
            throw a;

Error [ERR_HTTP2_STREAM_ERROR]: Stream closed with error code NGHTTP2_PROTOCOL_ERROR
    at new NodeError (node:internal/errors:399:5)
    at ClientHttp2Stream._destroy (node:internal/http2/core:2347:13)
    at _destroy (node:internal/streams/destroy:109:10)
    at ClientHttp2Stream.destroy (node:internal/streams/destroy:71:5)
    at Http2Stream.onStreamClose (node:internal/http2/core:550:12)
Emitted 'error' event on ClientHttp2Stream instance at:
    at emitErrorNT (node:internal/streams/destroy:151:8)
    at emitErrorCloseNT (node:internal/streams/destroy:116:3)
    at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {

Node.js v19.8.0