Description
The analysis server's PreviewSite.handleGetRequest
function attempts to catch exceptions occuring while handling a get request, so that an error response may be returned instead:
Future<void> handleGetRequest(HttpRequest request) async {
...
try {
...
return respond(request, DartFilePage(this, unitInfo)); // (1)
...
} catch (exception, stackTrace) { // (2)
try {
await respond(
request,
createExceptionPageWithPath(path, '$exception', stackTrace),
HttpStatus.internalServerError);
} catch (exception, stackTrace) {
...
}
}
}
Where the respond
method is itself asynchronous:
Future<void> respond(HttpRequest request, Page page,
[int code = HttpStatus.ok]) async {
HttpResponse response = request.response;
response.statusCode = code;
...
response.write(await page.generate(request.uri.queryParameters));
response.close();
}
The intended behavior here is that when an HttpRequest
is received, a DartFilePage
object is created and passed to respond
. Then respond
calls its generate
method to generate HTML text. If an exception occurs during page.generate
, then it should be caught by the catch
block at (2), and so the HttpRequest
should be completed with an error response. But that doesn't happen. What happens instead that the HttpRequest
is never completed, so the analysis server appears to hang.
After a lot of single stepping in the debugger and scratching my head, I've figured out the reason the code isn't behaving as intended: since there is no await
keyword at (1), as soon as a Future
is received from the call to respond
, that future is returned from handleGetRequest
, and then the caller waits for it to complete. If respond
's call to page.generate
, encounters an exception, that occurs after handleGetRequest
has already exited, so its try/catch is no longer in scope, and the exception propagates to the caller.
I've been able to get the intended behavior by changing (1) to use return await
rather than just return
. This ensures that handleGetRequest
actually waits for the future returned by respond
to complete, so that if an exception occurs during page.generate
, the try/catch will still be in scope, and the intended exception page will be generated.
I think there's a bit of a user trap here. Usually return await
and return
are equivalent (because of the "implicit await" behavior of return statements inside of async functions) , so I've gotten into the habit of changing one into the other without really thinking carefully. But when the return
statement is inside of a try
block, the behavior is very different, and shortening return await
to just return
is almost certainly a big mistake.
I don't really have a concrete proposal for what to do about this--perhaps the best answer is just to have a lint that detects this problematic pattern (cc @pq). But I figured I would start by reaching out to the language folks to see if you have any other ideas.