Debugging workers specific javascript errors


#1

My worker script has been afflicted with a inconsitently occuring, unreproducible bug. It is a cloudflare specific error, however, the only message I get, with no stack trace available, is the following:

TypeError: This ReadableStream is disturbed (has already been read from), and cannot be used as a body.

I previously had a similar experience with something about reading a response from another request, but I was eventually able to resolve that.

Does anybody have any ideas about how to resolve this issue or how exactly this occurs? I only ever read request and response headers in my script, I’m not sure how a stream could have been “disturbed”.

@harris, is there a way to see the stack trace of this error?


#2

Curious - can you try testing with cloudrunner?


#3

I have not, but considering that I don’t know how to reproduce the bug, I’m not sure if it will help very much.

Thanks for linking it though, I’ll give it a try anyways.


#5

I’d rather not. I would like to be able to debug it myself in case something similar happens again.


#6

Hi @ssttevee,

I’m curious how you are observing the exception – e.g. in the console in the preview service, or reported via response header/body in production? It’s uncommon for an exception to truly have no stack trace (i.e., no stack property), and it would help me understand the issue if I knew the context in which the exception was observed.

Both Request and Response bodies become disturbed when they are:

  • read from. This can happen explicitly by calling response.body.getReader().read(), or implicitly through the Body.text(), .arrayBuffer(), .formData(), and .json() methods.
  • canceled. This can only happen by calling response.body.cancel() or response.body.getReader().cancel().

Request bodies additionally become disturbed when one request is used to construct a new Request without replacing its body (e.g., new Request(oldRequest)), and when they are passed to fetch().

Response bodies additionally become disturbed when they are sent to the eyball (via event.respondWith()) or to cache (via cache.put()).

Since explicit reading and cancelation would presumably be obvious in your script, I’d focus on searching for code paths where one of the latter cases can occur twice. For instance, cache.put()ing a Response object, then using it as an eyeball response, or conditionally reconstructing a Request, then using it in a fetch().

Another thing to consider is that some code paths might work with null bodies but might throw when there is a body – e.g., GET versus POST requests, or 204 status versus 200 status responses.

Harris


#7

I actually see this most often on production. I have seen it occur in the preview as well, but only once because the occurrences go away after a while and I’m usually not quick enough to open the preview to see it happen in preview.

I actually just attach a catch statement to the handler before handing it off event.respondWith()

addEventListener('fetch', async (event) => event.respondWith(
    routeRequest(event).catch((err) => {
        console.log(err);
        return new Response('500 Internal Server Error\n\n' + err.toString() + '\n\n' + err.stack), {
            status: 500,
        });
    }),
));

The exact output I get is:

500 Internal Server Error

TypeError: This ReadableStream is disturbed (has already been read from), and cannot be used as a body.

TypeError: This ReadableStream is disturbed (has already been read from), and cannot be used as a body.

Thanks, that was exactly it. I had set up my script to retry the request after a timeout. I changed it to use Request.clone() and it seems to be working fine now.


#8

That would do it! Glad you got to the bottom of it.

By the way, the event handler function passed to addEventListener() shouldn’t be async. As long as you don’t try to await anything before calling event.respondWith(), it doesn’t matter too much, but it’s sort of a footgun waiting to go off.

Harris