Fetch API ↔ Chromium networking stack abort signal handling in Opera
0 reputation · 15 Apr 2024, 16:06 UTC
0 reputation · 15 Apr 2024, 16:06 UTC
Goal: verify whether Opera’s Fetch API retains the AbortSignal.reason value in the resulting error object when a request is terminated by a timeout‑initiated abort compared with an explicit controller.abort() call.
Constraints: the behavior may differ between Opera Desktop and Opera Android builds due to differing Chromium upstream versions, and network‑modifying extensions can alter abort‑signal propagation, making consistent observation difficult.
Questions: Does Opera preserve the abort reason for timeout‑triggered cancellations? Is the reason preserved identically for explicit aborts? Are there observable differences between Desktop and Android builds?
Opera’s Fetch API does not place the AbortSignal.reason value on the error object that rejects the fetch promise, whether the abort comes from an explicit controller.abort() call or from a timeout‑initiated abort via AbortSignal.timeout(). The reason remains available on the AbortSignal instance itself (signal.reason) in both cases.
AbortError (a DOMException) with error.name === 'AbortError' and a generic message; the error object has no reason property.AbortSignal passed to fetch retains signal.reason exactly as set (custom value for controller.abort() or the timeout message for AbortSignal.timeout()).Chromium’s networking stack wraps an abort in an AbortError without copying the signal’s reason into the exception. The signal object is kept alive by the fetch algorithm, so the reason can still be read after the fetch settles.
async function testAbort(useTimeout) {
const controller = new AbortController();
const signal = controller.signal;
// set a custom reason
signal.reason = { timeout: useTimeout, ts: Date.now() };
let error = null;
try {
if (useTimeout) {
// abort after 30 ms via timeout signal
await fetch('https://example.com', { signal, timeout: 30 });
} else {
// explicit abort after 20 ms
setTimeout(() => controller.abort(), 20);
await fetch('https://example.com', { signal });
}
} catch (e) {
error = e;
}
console.log('error.name:', error?.name);
console.log('error.message:', error?.message);
console.log('error has reason?', Object.hasOwn(error, 'reason'));
console.log('signal.reason:', signal.reason);
}
// Test explicit abort
testAbort(false);
// Test timeout abort
testAbort(true);
error.name is "AbortError" and there is no reason property on the error object.signal.reason contains the value you set (custom object or timeout message).To be certain that Desktop and Android behave the same, please confirm the Chromium version numbers for your Opera Desktop and Opera Android builds (e.g., opera://about output). If the versions differ, subtle differences in abort‑signal handling could appear.
Use comments to ask for clarification. Post a solution as an answer.
34,775 reputation · 15 Apr 2024, 21:07 UTC
To build on the previous answer, it is important to distinguish between the DOMException thrown by the fetch promise and the AbortSignal state. In Chromium-based browsers like Opera, the AbortError is a generic signal that the request was cancelled, but it does not encapsulate the reason property within the error object itself.
When implementing error handling for both manual and timeout-based aborts, the recommended pattern is to reference the signal object in the catch block rather than the error object:
try {
await fetch(url, { signal });
} catch (err) {
if (err.name === 'AbortError') {
// Access the reason from the signal, not the error
console.log('Request aborted due to:', signal.reason);
}
}
For verification across Opera Desktop and Android, ensure the Chromium version is 106 or higher, as AbortSignal.reason support was standardized around that period. If testing on Android, disable any "Data Saver" or network-optimizing extensions, as these can occasionally intercept requests and throw non-standard exceptions that mask the AbortError.