Summary
When a network fetch fails (HTTP error status, IO error, or abort), the NetHandler for the request is silently dropped without any callback. If the request was a render-blocking ("critical") resource such as a <link rel=stylesheet> in <head>, its request id is never removed from BaseDocument::pending_critical_resources, so has_pending_critical_resources() returns true forever and painting is permanently blocked (paint_scene early-returns, and blitz-shell skips rendering).
Details
-
NetHandler only has a success path:
pub trait NetHandler: Send + Sync + 'static {
fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes);
}
(packages/blitz-traits/src/net.rs)
-
blitz-net's Provider::fetch logs errors and drops the handler without invoking anything:
match result {
Ok((response_url, bytes)) => { handler.bytes(response_url, bytes); ... }
Err(e) => {
tracing::error!(url = url.as_str(), error = ?e, "Error fetching");
}
};
(packages/blitz-net/src/lib.rs)
-
The unblocking path is Document::load_resource, which removes the request id from pending_critical_resources before checking success/failure — so failures that are delivered as Err through ResourceHandler::respond (e.g. invalid UTF-8 CSS) unblock correctly. Only failures at the net-provider level never reach it.
Browsers treat a failed render-blocking stylesheet as "loaded with zero rules" and unblock rendering.
Suggested fix
- Give
NetHandler an explicit failure path (e.g. fn error(self: Box<Self>, err: String) with a default no-op, or change bytes to take a Result), and make blitz-net (and other providers) deliver failures.
- Belt-and-braces: a
Drop impl on ResourceHandler that sends an Err ResourceLoadResponse if the handler was dropped without ever responding — this also covers third-party NetProvider implementations that drop handlers without calling anything (including the abort path).
- Optionally, a timeout on render-blocking so a hung (never-completing) fetch cannot blank the page forever.
Related: this failure mode becomes more impactful with the proposed fix for #689 (deferring style resolution, not just painting, while critical resources are pending).
Summary
When a network fetch fails (HTTP error status, IO error, or abort), the
NetHandlerfor the request is silently dropped without any callback. If the request was a render-blocking ("critical") resource such as a<link rel=stylesheet>in<head>, its request id is never removed fromBaseDocument::pending_critical_resources, sohas_pending_critical_resources()returnstrueforever and painting is permanently blocked (paint_sceneearly-returns, andblitz-shellskips rendering).Details
NetHandleronly has a success path:(
packages/blitz-traits/src/net.rs)blitz-net'sProvider::fetchlogs errors and drops the handler without invoking anything:(
packages/blitz-net/src/lib.rs)The unblocking path is
Document::load_resource, which removes the request id frompending_critical_resourcesbefore checking success/failure — so failures that are delivered asErrthroughResourceHandler::respond(e.g. invalid UTF-8 CSS) unblock correctly. Only failures at the net-provider level never reach it.Browsers treat a failed render-blocking stylesheet as "loaded with zero rules" and unblock rendering.
Suggested fix
NetHandleran explicit failure path (e.g.fn error(self: Box<Self>, err: String)with a default no-op, or changebytesto take aResult), and makeblitz-net(and other providers) deliver failures.Dropimpl onResourceHandlerthat sends anErrResourceLoadResponseif the handler was dropped without ever responding — this also covers third-partyNetProviderimplementations that drop handlers without calling anything (including the abort path).Related: this failure mode becomes more impactful with the proposed fix for #689 (deferring style resolution, not just painting, while critical resources are pending).