Skip to content

Conversation

lukewarlow
Copy link
Member

@lukewarlow lukewarlow commented Jul 25, 2025

This is not ready for review this is WIP.

  • At least two implementers are interested (and none opposed):
  • Tests are written and can be reviewed and commented upon at:
  • Implementation bugs are filed:
    • Chromium: …
    • Gecko: …
    • WebKit: …
    • Deno (not for CORS changes): …
  • MDN issue is filed: …
  • The top of this comment includes a clear commit message to use.

(See WHATWG Working Mode: Changes for more details.)


Preview | Diff

callback FetchObserverCallback = undefined (FetchObserver requestObserver, FetchObserver responseObserver);

[Exposed=(Window,Worker)]
interface FetchObserver : EventTarget {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observer probably isn't the best name here?

<li><p>Return <var>p</var>.
</ol>
</div>

TEMPORARY <dfn id=event-fetchobserver-progress event for=FetchObserver><code>progress</code></dfn>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to define the event somewhere properly. (potentially taking the event definitions from XHR?)

@@ -9132,6 +9205,119 @@ done only by navigations). The <a>fetch controller</a> is also used to
<a for="fetch controller">process the next manual redirect</a> for <a for=/>requests</a> with
<a for=request>redirect mode</a> set to "<code>manual</code>".

<h2 id=interface-progressevent>Interface {{ProgressEvent}}</h2>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is taken from XHR and should be removed from XHR in a corresponding PR.


<h3 id=firing-events-using-the-progressevent-interface>Firing events using the {{ProgressEvent}} interface</h3>

<p>To <dfn id=concept-event-fire-progress>fire a progress event</dfn> named <var>e</var> at
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should fetch really fire sync events? It would match XHR but I wonder if these should be queued instead?


<p><em>This section is non-normative.</em>

<p>The suggested {{Event/type}}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of these events aren't used in Fetch so this table should probably be kept in the XHR spec.

</ol>

<li>
<p>Let <var>processResponse</var> given a <var>response</var> be these steps:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to do progress tracking for response we'll need to do it differently to XHR, XHR consumes the body inside of this algorithm but fetch obviously can't.

One idea is to wrap response's body with a wrapper readable stream that allows use to fire progress events when the body actually gets read. This way we don't consume anything to report progress, (we can make it so we only wrapper if there's a responseObserver with a progress event listeners).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FetchObserverCallback observer;
};

callback FetchObserverCallback = undefined (FetchObserver requestObserver, FetchObserver responseObserver);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Firefox has the below IDL. I split it so the observers still fire events named progress but it's maybe fine to keep it as a single eventtarget and just change the event names?

[Exposed=Window]
callback interface ObserverCallback {
  undefined handleEvent(FetchObserver observer);
};

enum FetchState {
  // Pending states
  "requesting", "responding",
  // Final states
  "aborted", "errored", "complete"
};

[Exposed=(Window,Worker),
 Pref="dom.fetchObserver.enabled"]
interface FetchObserver : EventTarget {
  readonly attribute FetchState state;

  // Events
  attribute EventHandler onstatechange;
  attribute EventHandler onrequestprogress;
  attribute EventHandler onresponseprogress;
};

@@ -7558,6 +7559,14 @@ dictionary RequestInit {
RequestDuplex duplex;
RequestPriority priority;
any window; // can only be set to null
FetchObserverCallback observer;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this actually be in RequestInit? I'm thinking it should probably be in a FetchInit subtype that inherits RequestInit? That way it isn't also passed to the Request constructor?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we leave it in requestinit we'll have to add a way to track it across from Request into the fetch call if you do

const req = new Request(...);
fetch(req);

@@ -8584,10 +8593,67 @@ method steps are:
https://github.com/whatwg/dom/issues/1031#issuecomment-1233206400 -->
</ol>

<li><p>Let <var>hasUploadListeners</var> be false.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably be hasRequestListeners

@lukewarlow
Copy link
Member Author

An alternative design that could be considered is to just make Request and event target and fire progress events at the request object directly. Would require authors to construct a request attach a listener and then pass to fetch but might be okay?

@jakearchibald
Copy link
Collaborator

@lukewarlow I wonder if that could get confusing in cases where request/response isn't used in a fetch. Eg, if we made these things transferrable/clonable, would you expect progress events when getting a Response from indexeddb?

@lukewarlow
Copy link
Member Author

lukewarlow commented Sep 15, 2025

Yeah that's a good point. Perhaps it's just okay that they only fire progress within fetch? Or perhaps it would be doable to fire them elsewhere (if it would even make sense)?

My thinking is if the vast majority of cases support progress events then the nicer API is probably a worthy trade off?

Response is actually where things are less clear in general though.

@annevk
Copy link
Member

annevk commented Sep 15, 2025

Request and Response are very much designed to be data classes. Note also how fetch() and fetchLater() essentially create a copy as one of their first steps. I don't think they should become more stateful than what is needed for the Body mixin.

@lukewarlow
Copy link
Member Author

Okay in that case I think the addition should be moved to a fetchInit rather than being part of requestInit, easy enough to do. That way it keeps it separate from request entirely.

@mindplay-dk
Copy link

Okay in that case I think the addition should be moved to a fetchInit rather than being part of requestInit, easy enough to do. That way it keeps it separate from request entirely.

what does that look like?

a third argument to fetch?

or like interface FetchInit extends RequestInit with extra options?

@lukewarlow
Copy link
Member Author

Yeah the FetchInit dictionary would extend RequestInit

Comment on lines +8618 to +8619
<li><p>If one or more <a event><code>progress</code></a> event listeners were added to
<var>requestObserver</var>, then set <var>hasUploadListeners</var> to true.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean there's a specific time-window where listeners can be added? Isn't that… bad?

<ol>
<li><p>If <var>hasUploadListeners</var> is false, then return.

<li><p>Increase <var>requestBodyTransmitted</var> by <var>bytesLength</var>.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bytesLength isn't defined.

@jakearchibald
Copy link
Collaborator

jakearchibald commented Sep 16, 2025

I wonder if this should be a single observer object, making it easier to pass around. This also makes more sense if we want somewhere to put an abort event.

Also, this would be a good opportunity to move to a more modern pattern of having the details on the target object, rather than on the event object.

interface FetchObserver extends EventTarget {
  requestLengthComputable: boolean;
  responseLengthComputable: boolean;

  requestLoaded: number;
  responseLoaded: number;

  requestTotal: number;
  responseTotal: number;

  onrequestprogress: (event: Event) => void;
  onresponseprogress: (event: Event) => void;
}

We could include requeststart and responsestart events which indicate when the LengthComputable properties have settled.

@Terreii
Copy link

Terreii commented Sep 16, 2025

Awsome!
But should/could this work be synced with the AI/Translation people? They are working on a progress method, too.
It would be too bad if we had two (three if you count XMLHttpRequest) methods for progress observing on the web.

const translator = await Translator.create({
  sourceLanguage: 'es',
  targetLanguage: 'fr',
  monitor(m) {
    m.addEventListener('downloadprogress', (e) => {
      console.log(`Downloaded ${e.loaded * 100}%`);
    });
  },
});

I think @domenic is responsible for it.

@jakearchibald
Copy link
Collaborator

jakearchibald commented Sep 16, 2025

@Terreii thanks for raising this. monitor is a good name actually.

@jakearchibald
Copy link
Collaborator

webmachinelearning/translation-api#61

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

5 participants