-
Notifications
You must be signed in to change notification settings - Fork 296
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
AbortSignal consumers can potentialy emit abort event on it #784
Comments
A consumer could do that and depending on who looks at the event (and how, e.g., |
That's also something that scares me |
If someone gets a handle on your signal object they can also make the But maybe I'm misunderstanding the threat model. |
That part is secured by the spec. If we really want the Controller/Signal distinction to make sense, what could potentially be done, while Regarding polyfills, it's still easy with Symbols to let the AbortSignal expose access to the Out of that topic, the |
signal = new AbortController().signal;
Object.defineProperty(signal, "aborted", { value: 1 });
console.log(signal.aborted); // 1 |
To me such code example, if not fixed, is only removing any interest to separated the controller from the signal. If the API is meant to provide a real "protection" that some frameworks, libraries, vendor featured API might want, it should probably be done completely. (Don't get me wrong @domenic I really appreciate the huge work done on CancelablePromise, and then AbortController)
I agree, but I also think that there is a global effort to improve the web platform against existing leaks & inconsistencies. I'm thinking of things like sandboxes, ES controller = new AbortController();
signal = controller.signal;
Object.seal(signal);
Object.defineProperty(signal, "aborted", { value: 1 });
VM165:3 Uncaught TypeError: Cannot define property aborted, object is not extensible
at Function.defineProperty (<anonymous>)
at <anonymous>:3:8
console.log(signal.aborted); // false
controller.abort();
console.log(signal.aborted); // true So nothing prevent from enforcing this API isolation if we want a "safe" signal |
I still don't really understand the threat model or what you think this API is meant to provide. Its design follows established precedent of many web platform classes. If you cannot depend on this class, there are many other cases you cannot depend on either, so I'm not sure why we're discussing this in isolation. |
Apologies for the length.
While there might be some security use cases the bigger problem is not security threats but rather the developer hazards it presents. Implicit ContractsThe root of problems like introduced with Developers tend to have a good idea of what an object can and should do by the interface it provides. ExampleLet's take However nothing is enforcing that this assumption work, there's no technical reason But people don't do delete But where does this implicit contract come from? Well it's from a number of sources:
These sources combine to give developers an good idea of what the implicit contract for a given programming feature is and what is and isn't expected behavior. The problem with
|
Browsers don't use And reusing existing patterns is primarily done to avoid introducing new categories of bugs and design issues. If there's a better way to do something, it'll get popular and eventually adopted (see promises), but thus far that hasn't happened for events. |
I think it's problematic with other features from ECMA262 that there's hidden features. I strongly wish both hosts (browsers, +others e.g. Node) would use the same interface to things which they are started too (I'll elaborate on this at the bottom).
But this presupposes the solution for cancellation needed to be events. The primary reason This is something I've noticed and find unfortunate with web standards generally and something I think the TC39 overwhemingly does well. I see there to be almost a fork with 2 different "Javascripts", the WHATWG and W3C where everything is exposed via WebIDL with separate specs that depend heavily on each other to function. And the TC39 version where everything is self-contained and offers clear extension points but avoids having lots of cross-dependencies. There's also a significantly different processes, with the TC39 there's a clear process with stages that are extremely thorough for a feature to reach completion, and the WHATWG and W3C where design is less thorough but gets delivered a lot more quickly. However I do find the consequence of this kinda annoying, for the most part since the stage-system for ECMA2562 was introduced I think the features produced are highly polished, very future proof and widely applicable to their use cases. However results do tend to happen a lot slower (at times bordering on a snail's pace) and it can a lot of time for simple changes to come to fruition. On the flip side I've noticed the WHATWG and W3C move significantly faster but at a noticeable drop in quality of solutions that aren't as widely applicable or contain fewer anti-features and footguns. Relating this to However because of the fairly fast nature of WHATWG's development existing features were used leading to a situation where we have something that is less widely applicable† and less polished* than what would've been produced if this were considered as a fundamental primitive that could be used widely throughout all Javascript code, not just browsers. This has knock on effects, because in adopting † Because anyone who wants to use * Compared to something like Cancellation in C# which is widely supported and ties into all parts of the system well. To just give you an idea of how ridiculous I find it when saying just use IDL Interfaces requiredinterface AbortController {
constructor();
[SameObject] readonly attribute AbortSignal signal;
void abort();
};
[Exposed=(Window,Worker)]
interface AbortSignal : EventTarget {
readonly attribute boolean aborted;
attribute EventHandler onabort;
};
[Exposed=(Window,Worker,AudioWorklet)]
interface EventTarget {
constructor();
void addEventListener(DOMString type
, EventListener? callback
, optional (AddEventListenerOptions or boolean) options
= {});
void removeEventListener(DOMString type
, EventListener? callback
, optional (EventListenerOptions or boolean) options
= {});
boolean dispatchEvent(Event event
);
};
callback interface EventListener {
void handleEvent
(Event event
);
};
dictionary EventListenerOptions {
boolean capture = false;
};
dictionary AddEventListenerOptions : EventListenerOptions {
boolean passive = false;
boolean once = false;
};
[Exposed=(Window,Worker,AudioWorklet)]
interface Event {
constructor(DOMString type
, optional EventInit eventInitDict
= {});
readonly attribute DOMString type;
readonly attribute EventTarget? target;
readonly attribute EventTarget? srcElement; // historical
readonly attribute EventTarget? currentTarget;
sequence<EventTarget> composedPath();
const unsigned short NONE = 0;
const unsigned short CAPTURING_PHASE = 1;
const unsigned short AT_TARGET = 2;
const unsigned short BUBBLING_PHASE = 3;
readonly attribute unsigned short eventPhase;
void stopPropagation();
attribute boolean cancelBubble; // historical alias of .stopPropagation
void stopImmediatePropagation();
readonly attribute boolean bubbles;
readonly attribute boolean cancelable;
attribute boolean returnValue; // historical
void preventDefault();
readonly attribute boolean defaultPrevented;
readonly attribute boolean composed;
[Unforgeable] readonly attribute boolean isTrusted;
readonly attribute DOMHighResTimeStamp timeStamp;
void initEvent(DOMString type
, optional boolean bubbles
= false, optional boolean cancelable
= false); // historical
};
dictionary EventInit {
boolean bubbles
= false;
boolean cancelable
= false;
boolean composed
= false;
};
[Exposed=(Window,Worker),
Serializable]
interface DOMException { // but see below note about ECMAScript binding
constructor(optional DOMString message
= "", optional DOMString name
= "Error");
readonly attribute DOMString name;
readonly attribute DOMString message;
readonly attribute unsigned short code;
const unsigned short INDEX_SIZE_ERR = 1;
const unsigned short DOMSTRING_SIZE_ERR = 2;
const unsigned short HIERARCHY_REQUEST_ERR = 3;
const unsigned short WRONG_DOCUMENT_ERR = 4;
const unsigned short INVALID_CHARACTER_ERR = 5;
const unsigned short NO_DATA_ALLOWED_ERR = 6;
const unsigned short NO_MODIFICATION_ALLOWED_ERR = 7;
const unsigned short NOT_FOUND_ERR = 8;
const unsigned short NOT_SUPPORTED_ERR = 9;
const unsigned short INUSE_ATTRIBUTE_ERR = 10;
const unsigned short INVALID_STATE_ERR = 11;
const unsigned short SYNTAX_ERR = 12;
const unsigned short INVALID_MODIFICATION_ERR = 13;
const unsigned short NAMESPACE_ERR = 14;
const unsigned short INVALID_ACCESS_ERR = 15;
const unsigned short VALIDATION_ERR = 16;
const unsigned short TYPE_MISMATCH_ERR = 17;
const unsigned short SECURITY_ERR = 18;
const unsigned short NETWORK_ERR = 19;
const unsigned short ABORT_ERR = 20;
const unsigned short URL_MISMATCH_ERR = 21;
const unsigned short QUOTA_EXCEEDED_ERR = 22;
const unsigned short TIMEOUT_ERR = 23;
const unsigned short INVALID_NODE_TYPE_ERR = 24;
const unsigned short DATA_CLONE_ERR = 25;
}; This is a crazy amount of complexity an environment needs to add just to add a "primitive" for cancellation. Especially when all the use cases for cancellation can be done within a fairly tiny interface: Simplied AbortControllerclass AbortSignal {
#getAbortState;
#addHandler;
#removeHandler;
constructor({ getAbortState, addHandler, removeHandler }) {
this.#addHandler = addHandler;
this.#removeHandler = removeHandler;
this.#getAbortState = getAbortState;
}
get aborted() {
return this.#getAbortState();
}
register(abortHandler) {
this.#addHandler(abortHandler);
}
unregister(abortHandler) {
this.#removeHandler(abortHandler);
}
}
class AbortController {
#aborted = false;
#handlers = new Set();
constructor() {
this.signal = new AbortSignal({
getAbortState: () => this.#aborted,
addHandler: (abortHandler) => {
if (!this.#aborted) {
this.#handlers.add(abortHandler)
}
},
removeHandler: (abortHandler) => this.#handlers.delete(abortHandler),
});
}
abort() {
if (this.#aborted) return;
for (const handler of this.#handlers) {
try {
handler();
} catch (err) {
// Perhaps through an AggregateError?
}
}
}
} |
I guess I don't find it a lot of overhead as I suspect most environments need events. And whether to use events and how was something that was carefully considered during the design of this feature, which took quite a while. Also, anything that needs cancelation will use |
Node has an event system too already, I know that they're generally not very interested in adding a second similar event system if they can avoid it. I have noticed a sort've "holier than thou" attitude quite a lot in the WHATWG towards Node and it's quite frustrating given that in a lot of cases a simpler design would help a lot for simpler interoperability. |
I've put some effort into finding solutions that work for both Node's event system and the one defined in the DOM Standard, but nothing has gained much traction so far (most recently |
As many developers coming to the AbortController/AbortSignal API, I got a bit concerned by its "complexity", meaning... "Not a big deal but... Couldn't the signal & controller classes be merged?"
The main raised reason was about separation of concerns, forbidding a consumer to be able to call
abort()
on the signalBut looking at the API specification, I don't see anything preventing the consumer from calling
signal.dispatchEvent(new Event('abort'));
I don't see much control on that topic in polyfills neither
Am I missing something ?
The text was updated successfully, but these errors were encountered: