workerd: Improve "The script will never generate a response." debuggability
The The script will never generate a response. error is generally a very hard error to debug, the only source on the internet being this blog post.
We recently had this issue thrown on rare occasions with the following code:
import { createClient } from "@urql/core"
const client = createClient({ url: 'https://api.example.com/graphql' })
export default {
fetch(request) {
const response = client.query(someQuery);
return Response(JSON.stringify(response), { headers: { "content-type": "application/json" })
}
}
When heavily requested, the worker sometimes aborted with The script will never generate a response.. Luckily, this was the only piece of code that could possibly store promises in a global context (To solve it we called createClient on a per-request basis), but I can imagine this would be very hard to debug in a more complicated worker that isn’t so careful about saving promises in a global context.
Is there any way to make this error more debuggable?
About this issue
- Original URL
- State: open
- Created 2 years ago
- Comments: 18 (8 by maintainers)
Ok, just a status update… did some digging and improving this is definitely going to be a challenge…
Take the following example:
In this example, the promise that is not being resolved leading to the “The script will never generate a response” error/warning is obviously the
await promisewe create but are never resolving. However, the actual warning here is triggered by the outer promise representing the call to the asyncfetch(...)handler. THAT is the promise that the internal i/o handler is waiting on. Unfortunately, by the time we cancel things and emit this warning, we have no visibility into what other promises are stopping that outer promise from being resolved. JavaScript and V8 just don’t give us the tools necessary to easily know what it is we’re waiting on – could be i/o, could be a manually resolved promise like in the example, could be a promise from another request, could be a timer that got canceled, could be lots of things that would supposed to be resolving thatawait promisethat we just don’t have visibility on.We could implement a mechanism with the appropriate book keeping that might provide the information we need but it would depend on the Oh So Slow V8 Promise Hooks API and wouldn’t necessarily be guaranteed to work in every case… simply because what we really need to know to make this more debuggable is to know where the promise is supposed to be resolved or rejected, not where it was created or waited on. The closest the promise hook API would allow us to determine is the stack where a promise was created and the stack where it was waited on… and even then we don’t have visibility to selectively capture that information only for specific promises so we’d have to capture it for all of them… which is sllloooowwww.
Anyway, I haven’t given up. I still want to try to find some way of improving this, but so far it’s proving to be quite difficult.
Eventually we want to solve this kind of problem by having every event run in a separate global scope, but that will require some deep changes to V8 to be able to do in a way that performs well, and we don’t know exactly how soon we’ll be able to build it. Before we do that, we’ll introduce alternative API that people can use for explicit cross-event caching when desired.
In the shorter term, this is pretty hard for us to solve, because we don’t really have enough visibility into the V8 microtask loop to know that the request has decided to wait on a promise that originates from another concurrent request. All we know is that the request has no further I/O or timers scheduled yet hasn’t returned a result, therefore it seems permanently hung.
Maybe when we implement AsyncLocalContext-like support, we could leverage that? Not sure.