whatwg/fetch

Streaming upload

Closed this issue Β· 76 comments

Past discussions: #425, #966, ...

Please also see: other upload-streaming-blocking issues.

Let's discuss whether we want to have the streaming upload feature, i.e., a way to attach a ReadableStream to a Request, on the Web.
Last time we discussed, all the browser vendors agreed that supporting full-duplex (c.f., #1254) would be very difficult, so I'd like to exclude the option from this discussion. Feel free discuss that at #1254 or another issue.

We, Chrome, have a partial implementation, but failed to show its effectiveness thourgh a past experiment.

We have two paths forward:

  1. Get more support from other browser vendors and web developers, and ship this feature.
  2. Give up this feature and remove the logic from the spec.

Thoughts?

(Feel free to restate your position here even if you've already done that in #425).

It's a great opportunity to make the web a bit more bidirectional.

ReadableStream means on-the-fly transformation, the first obvious candidate being... encryption.

Use case: I want to submit a form (to an openstack POST-handler) which contains attachments encrypted on-the-fly. In the current situation, client memory is ~ twice the file size what is unsuitable for multi-GB uploads.

The lack of ReadableStream upload incentives the use of chunking as an alternative (which has the additional benefit of being easily resumable).
Since I believe streams find their use for large data (unpredictable size), reliability (resumable/error-handing), if not directly involved, needs to at least be considered by implementations.

I'd like to echo #425 (comment) . This feature is desperately needed for streaming media from a client to a server.

The alternatives like WebSockets etc. all need manual, custom code on both the client and server side, which makes interoperability practically impossible. That's more like FTP. The beauty of HTTP is its simplicity.

We can stream from server to client in response bodies, so we should also be able to stream from client to server in request bodies. In the same way that I can stream a video from server to client, I should be able to stream a video or audio from client to server. This avoids having to create custom protocols and servers and clients for streaming media.

One important project of mine died exactly at this point, because this wasn't possible.

From an implementation point of view, HTTP is pretty elegant, at least, HTTP/1 is. HTTP/2 trades questionable performance gains for elegance, and HTTP/3 goes further in this direction without any real semantic advantages. Honestly, I'm so disappointed that we are reinventing the network protocol at the application layer, but that's probably for another discussion.

Browsers need to implement the full semantics of HTTP. Even since HTTP/1.1 full bi-directional streaming is possible with chunked encoding. Of course you can argue that it's not used in the real world, but it's a true chicken and egg problem, of course if browsers don't support it no one can use it.

From an implementation perspective, I can only imagine that browsers put "full semantics of HTTP" in the too hard basket and instead invent things like WebSockets and WebTransport, WebRTC which are all ridiculously complicated to implement correctly on the client and server, have various compatibility issues and honestly don't solve the problem as well as bi-directional HTTP would.

I respect that I probably don't see the full picture from the perspective of a browser developer, but I have implemented a full HTTP/1 stack in C++ and HTTP/1 + HTTP/2 in Ruby (and hopefully soon 3), both client and server, including on top of that client and server WebSockets, including on top of that, interfaces for web applications to build on. So, I think I do have a pretty good perspective when I say, I wish browsers would just fully commit to bi-directional HTTP vs all these extra layers on top. Not only is it hard for developers, it's hard to communicate the reason why it's built like this, because frankly, the reasons aren't very good.

Thank you for your feedback.

I'm happy to make some more efforts, meaning I'm happy to make another try to spec and implement the minimal viable ship.

  • The feature is available only on HTTP/2 or HTTP/3 (#966).
  • Full bidirectional streaming is out of scope (#1254).

If you are happy with them, I'll fix the spec and implementation, file a TAG review and send an intent to ship to blink-dev. With your support ("developer's opinions"), I may be able to get enough support from API owners.

If this is the best middle ground we can find to start with, I support it. However, I'd also say, full-duplex streaming and HTTP/1 streaming are definitely possible in practice, and the only limitation seems to be browser implementors. Because, I've personally implemented all of that already.

It might be useful to allow specs to be full-duplex and to support HTTP/1.1 as optional, e.g. node.js might choose to support this because they don't depend on a browser for the client/server implementation limitations.

@yutakahirano

HTTP/1.1 support is essential for me.

I need this as internal application API between different components of a distributed system. Both end points are usually in the same internal local network, so there's typically no need for SSL encryption, and there will not be valid CA certificates. The clients may be either a) node.js, or b) small devices with an ESP32 using HTTP/1.1, or c) a webpage in a browser on the open web using HTTP/2, or a wild combination of all of those. Custom protocols based on WebSockets, WebRTC or other layers which require complicated server and client implementations are out of question. The entire point of using HTTP APIs is to create a standard for interoperable devices, with many different implementations, and making it really easy to implement new clients of various kinds, within hours of dev time.

I was really surprised to find out that streaming upload for fetch() is not implemented. I took it for granted that if I can download and upload files, and a stream can be returned from server to client, a stream can also be sent from client to server. There are many applications: Essentially all the use cases for server->client streaming, just that the connection is established by the other end.

Such holes in the specs need to be avoided, where it works in some cases but not in others, e.g. file download works, file upload works, stream download works, but stream upload doesn't work. The elegance of HTTP is that there is a simple concept, and you can cover most use cases with this same concept. This is unlike FTP, and many other more complicated protocols with complex layers and multiple channels that were all superceded by HTTP, due to the power of its simplicity.

#425 discussed a number of approaches for HTTP/1.1. A stream could be terminated by dropping the connection, or it could use chunking. My preference would be that both methods work.

I worked with the Chrome team on the experiment mentioned by Yutaka, with Google's user-facing services (gmail etc).

Unfortunately the API designed for the Origin Trial didn't allow us to collect the data needed, i.e. % of broken H1 streams due to chunked TE. The main reason is that HTTPS traffic default to H2+.

To make the experiment work, we need an Origin Trial API that forces HTTP/1.1 over HTTPS.

Let me state our position more clearly.

We, Chrome Web Networking team, have limited bandwith available on this feature. We are happy to re-try shipping the MVP part (no HTTP/1.1, no full-duplex) ([A]), but we don't have bandwith to drive and resolve the HTTP/1.1 or full-duplex discussions.

So, I want to know If you want to use the MVP part. At this moment @drzraf (comment) and @ioquatix (comment) do, right?

I understand that some of you want more, and don't want to use streaming upload if it doesn't support HTTP/1.1 and/or full-duplex. Unfortunally, we (again, Chrome) will not have enough bandwith to drive discussions to resolve these problems in the near future.

If you want to drive such discussions instead of us, that will be greatly appreciated. We don't want to block you at all ([B]).

If few people want the MVP part, and no one wants to drive the blocking discussions, then I think we should remove the feature from the spec ([C]).

So, in addition to the two options I originally described ([A] and [C]), there is a way to support HTTP/1.1 and/or full-duplex ([B]), but Chrome doesn't have bandwidth to pursue this option, and we need another owner to pursue the option.

I'm fine with [A] as long as it doesn't prevent [B] in the future. However I'd be literally unable to use it to replace WebSockets without full-duplex support, which is what I'd like to do. HTTP/1.1 is less of an issue except that there is no reason why it can't be supported in theory (as has been demonstrated in non-browser environments). I'd rather make some progress in the right direction than none at all.

I agree with @ioquatix - I'd been keen to have access to the feature at least in HTTP/2. I already have fallback functionality, but it would be great to use streaming upload in environments where it is available. My application is deployed internally for businessness, so we can know for the software environment of a particular business that the feature is always available (or always not available).

Let me confirm. @ioquatix, it sounds like you're not planning to use the feature unless the full-duplex communication is implemented. Is that correct?

This would also help in some WebAssembly scenarios as other languages support streaming uploads via Streams / Pipes or other concepts. For example .NET is waiting for it: dotnet/runtime#36634

Thank you @campersau!
Let me confirm: do you think the feature would be useful even without HTTP/1.1 support and full-duplex communication?

@yutakahirano It wouldn't be as useful as with HTTP/1.1 be but still an improvement. Full-duplex communication isn't needed, at least for .NET.

FWIW, none of the use cases I have in mind would need full-duplex streams. All of the cases I encountered were either sending or receiving streams, but never both in both directions at the same time in the same request.

@yutakahirano I want full duplex streaming but I won't let that get in the way of progress towards that goal. Since I basically want to replace WebSockets with something simpler/better.

@drzraf (comment), @martin-traverse(comment) and @campersau(comment) have shown their support.

@annevk, I'd like to try shipping the MVP, given the developers' support.
Steps:

  1. Land #1444.
  2. Remove "upload-stream-blocking" from #966 and #1254. I'm fine with keeping them open, given there are people interested in the features.
  3. File a TAG review.
  4. Request positions to Mozilla and Apple.
  5. (I need to fix our implementation.)
  6. Send an intent to ship to blink-dev.

Are you fine with these?

@yutakahirano If you choose to not implement streaming upload for HTTP/1.1 in Chrome, that's an engineering resources choice. However, I do not see a reason to explicitly forbid it in the spec. The spec applies to all of Chromium, Firefox, Apple WebKit, and node.js fetch(). In other words, is there a reason why node.js should be explicitly forbidden to implement streaming upload in fetch with HTTP/1.1 , if they want to implement it?

@benbucksch The reason why we don't support HTTP/1.1 is not engineering resource, but a security concern. See this comment for example. If you want to allow HTTP/1.1, that's great, I'll ask you to drive the discussion at #966. I don't want #966 to block the MVP part, and I'm proposing blocking it for now, because blocking a feature now and allowing it later is easier than allowing it now and blocking it later.

The comment you cite does not mention security concerns. Are you saying that you want to explicitly support SSL-breaking man-in-the-middle boxen, and would like to remove a feature from HTTP, because such encryption-breaking boxes might not explicitly support it? Or am I misunderstanding you?

The spec currently does allow HTTP/1.1. Now, PR #1444 is proposing to explicitly forbid it. That's new. I do not see a good reason to forbid it and to stop others from implementing it.

Instead, could you change the spec to more clearly state how you intend to support this feature with HTTP/2, allowing you to implement what you call "MVP", but could you leave the HTTP/1.1 part of the spec simply as-is, allowing others like Firefox or node.js to implement it for HTTP/1.1 as well?

The #966 (comment) does not mention security concerns. Are you saying that you want to explicitly support SSL-breaking man-in-the-middle boxen, and would like to remove a feature from HTTP, because such encryption-breaking boxes might not explicitly support it? Or am I misunderstanding you?

Maybe this comment is clearer? "Security issue" may be a wrong term, but I think it's clear there is an issue.

The spec currently does allow HTTP/1.1. Now, PR #1444 is proposing to explicitly forbid it. That's new. I do not see a good reason to forbid it and to stop others from implementing it.

Yes, and concerns have been raised from multiple people at #966. We need to address the concerns before shipping this feature. The easiest way is blocking HTTP/1.1, and that's what I'm doing.

Instead, could you change the spec to more clearly state how you intend to support this feature with HTTP/2, allowing you to implement what you call "MVP", but could you leave the HTTP/1.1 part of the spec simply as-is, allowing others like Firefox or node.js to implement it for HTTP/1.1 as well?

That's explicitly opposed in #966.

I'd be opposed to leaving the decision whether to support chunked encoding up to implementers.
#966 (comment)

Suggestion
4. Require implementors to minimally support the above (i.e they MUST attempt to support chunked encoding if the above invariants hold)
#966 (comment)

I recommend you to discuss at #966.

I'm proposing to block HTTP/1.1 for now. Blocking it for now doesn't mean blocking it forever. When you get an agreement at #966, we'll be able to allow it.

Please ship. I'd rather upload streams than forms.

Remove "upload-stream-blocking" from #1254.

I don't know how valuable my voice here is, but I am strongly opposed to shipping this without tackling #1254 in some way. From my current understanding, with the currently proposed spec there is no backwards compatible way towards eventual duplex streaming (or spec compliant duplex streaming outside of browsers). This means that by shipping this as is, we are closing the door for duplex streaming entirely.

I think to be able to move this forward we need to find a way to make the specified behaviour work for both duplex capable and duplex incapable implementations at the same time. This way implementers that are capable of duplex streams can support them, and engines that don't can still support upload streams without duplex capabilities now, then potentially move towards full duplex support over time in a backwards compatible way.

I think Cloudflare and Node.js may have similar concerns as me about full duplex support. cc @jasnell @mcollina.

Agreed. Even for upload-only streaming, I would expect the promise to resolve in the exact moment when the upload stream can start. I think that matches the download stream logic, too.

From my current understanding, with the currently proposed spec there is no backwards compatible way towards eventual duplex streaming (or spec compliant duplex streaming outside of browsers). This means that by shipping this as is, we are closing the door for duplex streaming entirely.

Just for example, we can add fullDuplex member into RequestInit to express the intention, in the future. Browsers which cannot handle full-duplex fetch will throw an exception when it sees fullDuplex: true. I'd say it is a reasonable extension, what do you think?

yuta, a fullDuplex: true flag would not solve the problem that I mentioned for upload, whereas @lucacasonato 's suggestion solves both problems at once, is straight-forward, and matches the download stream API. For download streams, the connection and streaming body are 2 separate steps, which can be awaited separately. The same would make sense for upload. It makes for a more natural API for streaming upload now, and also helps for full-duplex in the future.

I'm supportive of exploring a full-duplex solution in #1254. We can still make some tweaks, though I worry that due to blobs we might already expose half-duplex behavior in the API so would need some kind of opt-in as @yutakahirano suggests. And to be clear, I don't think we currently have a concrete proposal for making full-duplex work that is complete/good.

As for HTTP/1.1, I'm not sure we ever had multi-implementer support for doing that and we certainly have implementer support for not doing that. The current specification also doesn't really describe how it ought to work. Setting a chunked header doesn't magically chunk the contents. So removing that until we have a concrete proposal for how it ought to work and doesn't run into middleware issues seems good.

I suggest we keep this issue open until we have found a resolution around full-duplex. Luca said he was able to look into it soonish.

Let's assume we want to send a 10MB file to a server via HTTP. We make a POST request, send it to the server and get a 200 response. Everything is fine.

Here we interpret the 200 as a sign from the server that it received (and processed) the entire request successfully. See the definition of 200 status code. If the server responded with a 200 response and then it turns out that the server didn't receive the request, it sounds very strange.

This means before responding with a 200 response, the server should receive the entire request, including the request content.

It is true that in some cases the server doesn't need the entire request content to respond (error cases, for example). On the other hand, without special knowledge about the server, the client shouldn't expect that it will get a response without sending all the request content.

This is why I think the client should explicitly express an intent to initiate a full-duplex communication. By default, the client should expect a response after sending the request content. Note that https://datatracker.ietf.org/doc/html/draft-zhu-http-fullduplex#section-4.1 was aware of this.

Full-duplex streaming is completely controlled by the server
application, and should only be enabled for those clients that have
been explicitly identified by the server.
...
For non-controlled client applications, the client may advise its
capability of full-duplex streaming via URL parameters or headers
(for example, "X-Accept-Streaming: full-duplex;timeout=30").

@yutakahirano thanks for your concerns and ideas.

Regarding this, I think a practical example might be helpful. So, here is a real example of the full duplex streaming over HTTP/1.1:

https://github.com/socketry/async-http-full-duplex-streaming

The example includes both the client and server in the same process so the log output is interleaved for easy interpretation (i.e. when headers are received, when body is streamed, etc).

Regarding the timing of the response from the server before the body is fully received, I think the conclusion you can draw is that (1) it's perfectly reasonable behaviour and (2) it's up to the server when to send the response, either before or after reading the entire body. At the very least, it's semantically possible to do either option and handle that at the server level.

Hope this helps and feel free to experiment with the implementation to consider different use cases. You can also do HTTP/2 with the same code, instructions on how to do this are included.

If there is something practical I can help with regarding test code or experimentation with order of operations, please don't hesitate to let me know.

This is why #1438 (comment). By default, the client should expect a response after sending the request content. Note that https://datatracker.ietf.org/doc/html/draft-zhu-http-fullduplex#section-4.1 was aware of this.

Is that RFC 8 years old? I think the nature of bi-directional full-duplex streaming as a semantic was really clarified in the HTTP/2 specification. No expectation is needed to initiate a full-duplex stream as it's the default, and semantically it seems 100% compatible with HTTP/1.1. This is demonstrated by the example given above.

@yutakahirano I am not sure https://datatracker.ietf.org/doc/html/draft-zhu-http-fullduplex is relevant to us at all, considering that it only deals with HTTP/1.1, and we forbid request streaming for HTTP/1.1.

Most non-browser HTTP clients support full duplex streaming for HTTP/2.

@yutakahirano I am not sure https://datatracker.ietf.org/doc/html/draft-zhu-http-fullduplex is relevant to us at all, considering that it only deals with HTTP/1.1, and we forbid request streaming for HTTP/1.1.

OK, then let's forget about it.

I'm curious about your previous comment,

From my current understanding, with the currently proposed spec there is no backwards compatible way towards eventual duplex streaming (or spec compliant duplex streaming outside of browsers). This means that by shipping this as is, we are closing the door for duplex streaming entirely.

From my viewpoint, the current streaming upload MVP doesn't change the full-duplex status. It doesn't contain full-duplex support, but it also doesn't block its possibility in the future.

Can you elaborate on your comment a bit more?

Sure. I wasn't very clear. I meant: "we are blocking full duplex support that doesn't require a flag on the request to enable". In my eyes it would be very unfortunate if full duplex always required a flag to opt in, especially considering how well HTTP/2 and HTTP/3 support full duplex. This statement is made on the assumption that if upload streaming were to ship in implementations with only simplex support, some users may start making requests to servers that process requests as full duplex, and then if/once those implementations do support full duplex streams the user facing behavior on the client would change and break existing code.

Let me start by elaborating the problem of duplex and simplex in the same API:

A little diagram of a full duplex request:

            : Time                :`
Conn init   : ...                 :
Header send :    ..               :
Req stream  :      .............  :
Header recv :              ..     :
Resp stream :                .... :

If a client does not support full duplex streaming, this would be exposed in the Fetch API as follows:

                     : Time 
Req stream done      :                  x  :
`fetch` promise done :                  x  :
Resp stream done     :                   x :

If a client does support full duplex streaming, it would be exposed as so:

                     : Time 
Req stream done      :                  x  :
`fetch` promise done :               x     :
Resp stream done     :                   x :

This means that with full duplex streaming, request errors can not be returned via the Promise returned from fetch, because in some cases the promise will already be resolved by the time an error occurs in the request stream.

After thinking about this some more, I think that the same API supporting both duplex and simplex streaming without a toggle is going to be rather problematic, and not ever backwards compatible (see #1254 for more on that).

Maybe a streaming: "simplex" | "duplex" option would work? If we set the default to "duplex" and have implementations which are simplex only error on that mode, we can allow the default to be duplex and still allow. That means that to use upload streaming in a simplex implementation, one would always need to specify streaming: "simplex".

This essentially inverts the fullDuplex option that you had proposed elsewhere.

@annevk Suggests duplex: "full" | "half" is a better name for the option. I agree.

IMO the default should be half, without requiring specifying a value, since that's the model fetch already has today. Opting into the other model (i.e. opting in to duplex: "full") would be best, instead of setting the default to something that's not supported in the spec and thus requiring people to add a redundant flag whenever they pass a ReadableStream body.

Ok, so from chatting on Matrix, I think this approach would solve all of my concerns, and I think all of the ones brought up in this thread and #1254 too:

  1. A new duplex option is added to RequestInit. It has valid values of "half" and "full".
  2. If the body option is unset, or is set to anything other than a ReadableStream, duplex defaults to "half".
  3. If the body option is a ReadableStream, duplex must be manually specified by the user (no default).
  4. For now, browsers will only support duplex: "half".
  5. Because in Deno there are existing users that rely on ReadableStream bodies being duplex: "full", Deno will need to deviate from spec by defaulting to duplex: "full" for when body is a ReadableStream. For non-ReadableStream bodies, the default would match duplex: "half" (and thus match browsers).
  6. duplex: "half" is the currently specified behavior where the Promise returned from fetch only resolves when both the request body stream is fully sent and the response headers have been received.
  7. duplex: "full" is a future addition to the specification that would cause the Promise returned from fetch to resolve immediately when the response headers have been received, regardless of if the request body has been fully sent. We are happy to do the spec work for this addition in a follow up.

Does anyone have concerns that this approach does not resolve?

I'm happy with #1438 (comment). Thanks!

PR is here: #1457

@lucacasonato , @yutakahirano : Step 6 from #1438 (comment) would not work for me. I am going to stream live audio/video data, so the fetch() promise would never complete. That makes no sense.

Also, I wouldn't know when I can start streaming. I need to know when the response headers are returned. It makes no sense to start streaming, if the server is not going to give a 404 error, or 401, or all kinds of other issues that can be trivially sorted out before. Also, if the fetch promise does not resolve, how do I know when I can start streaming? I am not going to start streaming blindly. Even if there is some kind of event, that is an unnatural API to block the await fetch() and then continue in some event handler. Why block, if processing needs to continue by sending the stream?

It would make sense that the fetch promise resolves once the response headers are received and the stream can start, not when it finishes. This would also match the down stream API, where fetch() promise resolves once the response headers are returned, but before the down stream is completed. The up stream should behave exactly the same.

Please remove step 6 and just do the same as in step 7. This would be a more consistent API, more logical - because I have a clear moment when I can start streaming -, and also work better for live streaming cases to resolve the fetch().

@benbucksch You are looking for full duplex streaming (#1254). I agree that streaming upload without full duplex has a very limited scope of usefulness, but browsers do not have the resources to implement full duplex right now, so it's either no upload streaming at all or half duplex upload streaming.

Still 🀞 for full duplex in the future. We are making sure the API is designed in such a way that full duplex can be added without breaking anyone in the future.

@benbucksch You are looking for full duplex streaming (#1254)

No, I need only half duplex streaming, only upstream. I do not need full duplex. All my comments apply to half duplex upload streaming only.

@benbucksch You want to upload a stream, you don't want to download a stream, and you want response headers before starting uploading the stream, right?

What you describe is full duplex in my books:

  • Half duplex: you fully send data before you can receive anything.
  • Full duplex: you can receive something before you are fully done sending.

In your case you are receiving something (response headers), before the request is fully sent, so I think this is full duplex.

@lucacasonato Full duplex (in my understanding) means that there are streams in both directions. Here's what I expected for half-duplex, upload-only stream:

  1. connection opened, including TLS
  2. request headers sent
  3. response headers recieved
  4. fetch() promise resolves (or has been rejected as early as possible, if there were problems in the previous steps)
  5. Upload stream starts
  6. The stream may finish or just abort at some point.
  7. The server sends the response body, or not in case of an abort. The response body is fixed length (and often completely irrelevant). Therefore, you cannot call this a "full duplex".

Full duplex (in my understanding) means that there are streams in both directions.

Not per-se streams - any bi-di communication that does not happen in order.

The size of the response body is irrelevant to the discussion about this being half of full duplex. What makes it full duplex is that you are receiving anything from the server before being finished with sending request data. The something you are receiving are the response headers. You are racing the receiving of the response body (which comes after headers, and may be fixed size), with the sending of the request body. You are receiving and sending at once: thus this is full duplex.

The definition doesn't really matter anyway, because the point is that what you would require is out of order sending and receiving of data, which is something that browsers are not planning to implement at this time (this is the blocker for #1254).

You are racing the receiving of the response body

No, because the response body is sent after the request body (stream) is received and the upload stream finished. In the steps I outlined, there is no race.

@lucacasonato How it would work in your proposal? How do I know that the server won't give me a 404 or 401? I am not going to start streaming a video camera, and then the server gives me a 401, and I have to go through auth, and then restart the stream. That won't work. When can I start sending the stream? See my questions in #1438 (comment)

You are receiving the response headers before you have finished sending the request body, which means you are sending and receiving out of order, which is the thing that browsers are unable to handle at this time.

How it would work in your proposal?

I am not sure what proposal you mean.

  • With full duplex streams, what you expect to work will just work.
  • With the currently specified ordered half duplex streams, I have no idea what happens if the request body stream is endless. I don't know of other HTTP clients that behave like the current specification text, so I don't have a frame of reference.

@benbucksch

Let's assume the full-duplex mode is specified. Let's assume we have stream which we want to upload.
The following is what you want, I think.

const reader = stream.getReader();
const p = fetch(url, { method: 'POST', duplex: 'full', body: new ReadableStream({
  start(c) { return p; },
  pull(c) { return reader.read().then((v) => { c.enqueue(v); }); },
  cancel(r) { return reader.cancel(r); },
  })});

I can't imagine there is anything other than half and full duplex. To avoid using a string for such a case, why not just use a boolean toggle, e.g. fullDuplex: true or bidirectional: true or streaming: true?

@lucacasonato wrote:

browsers are unable to handle [it] at this time

Of course, otherwise we wouldn't be here talking about it. Having worked on the Firefox network library in the past (but not currently), I don't see a reason why it can't be done.

I have no idea what happens if the request body stream is endless

That would be an important case to spec properly, because live streaming is the one point where streaming uploads are indispensible.

Other cases can be worked around with Buffers. Live streaming cannot, because not even the size of the upload body is known. We can already send large bodies (with workarounds like Buffer and File, which can do random-access of files, even on slow file servers, even without buffering before starting the upload), and we can do live streaming downstream from server to client, but there's currently no way to do upstream live streaming from client to server. If you spec it as you proposed, it would be unusable for exactly that one case which absolutely needs streaming uploads and which cannot be done at all currently.

@yutakahirano wrote:

assume the full-duplex mode is specified

Thanks for the suggestion, but you specifically excluded full-duplex from the spec for the time being, so it would remain impossible.

"Full duplex" would be like a phone call, whereas I'm talking about "security camera" or "live podcast creator" mode, where only one side sends. At no point are both sides sending at the same time. There's only an upload, and the steps could be strictly sequential, including the headers.

there's currently no way to do upstream live streaming from client to server. If you spec it as #1438 (comment), it would be unusable for exactly that one case which absolutely needs streaming uploads and which cannot be done at all currently.

From my reading of the proposed specification, you can live stream the upload provided you don't care about the response status / headers. Are you specifically stating that live upload should allow the response status / headers to be read as soon as they are available while still streaming the upload?

OK, let's assume that as client, I start streaming immediately after sending the request headers. Let's say the server says "401 you need to authenticate first" (which is particularly important with OAuth2, where I can never know whether the server has revoked or accepts my tokens). How would I know that the server wants me to re-auth first and won't accept any of my data until I re-auth? Given that it's live streaming, I cannot wait until the end of the stream, otherwise we'd have data loss.

Of course, otherwise we wouldn't be here talking about it. Having worked on the Firefox network library in the past (but not currently), I don't see a reason why it can't be done.

I can not speak to how this would work in Gecko, but from my understanding there is strong pushback from some implementers (specifically Chromium) on anything which doesn't conform to specifically this ordering of events in HTTP: 1. req headers sent, 2. req body sent, 3. resp headers received, 4. resp body received.

I also want to once again clarify that I don't care about upload streaming with the above mentioned ordering of events at all. I don't have any usecases for it. I am only involved here to ensure we don't accidentally close the door to out of order (full duplex) communication in the future.

If you spec it as #1438 (comment), it would be unusable for exactly that one case which absolutely needs streaming uploads and which cannot be done at all currently.

I disagree you can not do it, but I agree it's not ideal and you probably shouldn't do it. As stated above, I think ordered half duplex communication as I proposed above is not very useful. Chromium however wants to ship something, and can not currently ship full duplex (or anything where HTTP event ordering isn't ordered as described above), so our choices are currently to do either:

  1. Do nothing, and wait until Chromium comes around to full duplex support.
  2. Ship half duplex as specified above, and leave full duplex to later.

In no scenario are we getting anything where HTTP events happen in a different order (ie full duplex) right now.

Of course, otherwise we wouldn't be here talking about it. Having worked on the Firefox network library in the past (but not currently), I don't see a reason why it can't be done.

I can not speak to how this would work in Gecko, but from my understanding there is strong pushback from some implementers (specifically Chromium) on anything which doesn't conform to specifically this ordering of events in HTTP: 1. req headers sent, 2. req body sent, 3. resp headers received, 4. resp body received.

That's true for Chromium. I talked about this with other browser vendors at TPAC 2019, their reactions were they were fine with the half-duplex option, they thought full-duplex would be more difficult.

@benbucksch, what you want is what we call full-duplex in this issue. You could say calling it full-duplex is wrong, but that wouldn't change the situation.

My opinion stays the same. We should make the MVP open to future extentions, ship the MVP as soon as possible, and continue discussing full-duplex at #1254.

@yutakahirano In the proposal from comment #1438 (comment) , all I'm suggesting is to not treat step 6 differently than step 7, and to not change the flow in some cases and not others, but to do for step 6 the same as for step 7, which also matches the flow for downstream. Is that a possible compromise? If you don't want to implement parts of it, that's your decision. But it would make the spec cleaner, more straight-forward, with less special cases. Would that be feasible?

@yutakahirano mentioned:

TPAC 2019

Reading the protocol (search for "fetch#425"), I see you discussed the authentication error case that I mentioned here as well: "
[annevk] same for authentication challenge? other things that would cause multiple requests?
[yutaka]: ok, I'm fine to implement in this case
"
I'm not sure I understand your response "ok to implement this case" correctly. Were you saying that you would ensure that a 401 is handled and passed on immediately? As a special case?

In your API proposal, how exactly would that look like for a caller? I start a live streaming upload, e.g. a meeting recording, or a security camera feed upload, and the server says 401. If I receive the 401 error only after the stream ends, we have data loss.

In other words, whatever the API, the browser cannot sit on server errors, but needs to report them back as soon as the server sends them. This is true even for non-live streams. Streams are unnecessary for a few KB, but they can be very long, even hours of upload. To ignore errors and pass them on only after the stream ends, is way too late, causes useless re-transmisssion of large files at best, or even data loss. I cannot see any other solution than what I proposed. Or maybe you have something in mind how to solve this?

all I'm suggesting is to not treat step 6 differently than step 7, and to not change the flow in some cases and not others, but to do for step 6 the same as for step 7, which also matches the flow for downstream. Is that a possible compromise?

I don't understand your proposal. Step 6 and step 7 are different option values representing different behaviors.

I'm not sure I understand your response "ok to implement this case" correctly. Were you saying that you would ensure that a 401 is handled and passed on immediately? As a special case?

I said I was happy to cancel the session when we see 401 (as well as redirects).
That's actually specced now.

https://fetch.spec.whatwg.org/#http-network-or-cache-fetch

  1. If response’s status is 401, httpRequest’s response tainting is not "cors", includeCredentials is true, and request’s window is an environment settings object, then:
    1. ...
    2. If request’s body is non-null, then:
      1. If request’s body’s source is null, then return a network error.

Working with redirects / authentication requests is difficult because for non-streaming requests we repeat the request body. We don't want to store the entire stream for potential redirects / authentication requests, so we decided to cancel the requests in such cases. Of course, it is open to extention, so if you want to add an alternative behavior, please feel free to file an issue.

In other words, whatever the API, the browser cannot sit on server errors, but needs to report them back as soon as the server sends them. This is true even for non-live streams. Streams are unnecessary for a few KB, but they can be very long, even hours of upload. To ignore errors and pass them on only after the stream ends, is way too late, causes useless re-transmisssion of large files at best, or even data loss. I cannot see any other solution than what I proposed. Or maybe you have something in mind how to solve this?

This is true, but this is generally true for requests with big bodies. The server can send a response and reset the stream when it thinks it doesn't need furthur request body.

I don't understand your proposal.
Step 6 and step 7 are different option values representing different behaviors.

Yes, I am proposing to make the behavior consistent for all cases. That makes it more consistent for developers, and clearer for error cases, and for later full-duplex use. I.e. in both full and half duplex:
"Step 6: The Promise returned from fetch to resolves immediately when the response headers have been received, regardless of whether the request body has been sent."

It is my understanding that you rejected the idea, although would solve the error case as well.

cancel the session when we see 401 (as well as redirects).
then return a network error.

I see. Thanks. How would the caller be able to distinguish between a connection error, 401 auth error, and 302 redirect? Because the recovery action in each case needs to be different (e.g. get new OAuth2 token), so the caller needs to know which case happened. Unfortunately, the "network error" spec specifies that the status message must be empty, so I wouldn't know how to get that information. How did you envision that?

@benbucksch returning the response before the data has been transmitted is full duplex. You might not care for response body streaming, but that doesn't change the fact that returning the response while the request is still in progress is full duplex behavior and not something that will happen at this point in browsers.

@benbucksch I feel your pain and as it stands half duplex behaviour as proposed might not be very useful in some cases. But it opens a path forward to full duplex and I'm certainly positive on implementing something which opens the door on a future full duplex implementation. It also sounds like in the error case, the streaming upload would be cancelled. I would argue we should drop this behaviour if/when we implement full duplex streaming, as I don't believe a 4xx should mean "stop streaming the body". Assuming I'm understanding the proposed spec.

It is my understanding that you rejected the idea, although would solve the error case as well.

@benbucksch Yeah, as I said before, what you want is what we call full-duplex in this issue. Removing the "half-duplex" behavior is unacceptable to me.

@annevk: I understand and respect that. I below, I assume that it's implemented as @yutakahirano suggests.

However, the question above remains about server errors which happen early on, e.g. authentication. I am still not clear how a web app would know that it needs to re-authenticate.

@yutakahirano specced:

cancel the session when we see 401 (as well as redirects).
then return a network error.

How would the caller be able to distinguish between a connection error, 401 auth error, and 302 redirect? Unfortunately, the "network error" spec specifies that the status message must be empty, so I wouldn't know how to get that information. The recovery action in each case needs to be different (e.g. get new OAuth2 token), so the caller needs to know which kind of error happened. How can the caller know which error it was?

One option would be to create a new "server error" (instead of "network error"), where the status message and code fields are populated. Or you could allow "network error" to have an error message and code. Or maybe you or somebody have a better idea.

We discussed error on redirects at #538. See this comment and below. @annevk was reluctant to reveal the information.

Redirects and server-initiated authentication prompts. And the rejection is with the normal TypeError with perhaps a console-only message of the reason (which is "cannot be replayed" btw). We should not expose the failure reason to web content.

#538 (comment)

@ioquatix

I can't imagine there is anything other than half and full duplex. To avoid using a string for such a case, why not just use a boolean toggle, e.g. fullDuplex: true or bidirectional: true or streaming: true?

I weakly prefer duplex: 'full' | 'half' because it's symmetric. IIUC, when we decided to delay the decision on the default value, we chose the option to be symmetric.

For example, the absense of fullDuplex option sounds closer to fullDuplex: false than fullDuplex: true. I'd like to avoid that situation given we don't know what the absense ends up meaning.

I hope that I am not doing the "useless" "Me Too" here, but I wanted to chime and say how important having this functionality would be. We are building a tool in Go that we will deploy via wasm and their runtime does not support streaming POST:

https://go.googlesource.com/go/+/refs/heads/master/src/net/http/roundtrip_js.go#96

Using an older version of Chromium from during the Origin Trial I was able to add support to Go's wasm runtime and everything worked great. It would be wonderful to have this support.

In case you are wondering, our project is: https://github.com/network-quality/goresponsiveness

Thank you all for the tremendous effort you are dedicating to this feature!

@benbucksch essentially you would not use this with an arbitrary endpoint. For that you would want full duplex. And even then it might end up being problematic if the server or intermediary (which could only be used by X% of your end users) does not support full duplex. It's not clear to me that could ever work well.

Not sure if a lot of people use this pattern, but our app was using this code to check if streaming upload is enabled, and it broke for users using browsers based on 105 beta of chromium:

  public static doesSupportStreamBody (): boolean {
    // This works because the browser adds a Content-Type header of text/plain;charset=UTF-8 to the request 
    // if the body is text. 
    // The browser only treats the body as text if it doesn't support request streams, 
    // otherwise it won't add a Content-Type header at all.
    return ReadableStream && !new Request('', {
        body: new ReadableStream(),
        method: 'POST',
      }).headers.has('Content-Type')
  }

We had to add this to fix it:

        // @ts-expect-error
        duplex: 'half',

Not sure if a lot of people use this pattern, but our app was using this code to check if streaming upload is enabled, and it broke for users using browsers based on 105 beta of chromium:

  public static doesSupportStreamBody (): boolean {
    // This works because the browser adds a Content-Type header of text/plain;charset=UTF-8 to the request 
    // if the body is text. 
    // The browser only treats the body as text if it doesn't support request streams, 
    // otherwise it won't add a Content-Type header at all.
    return ReadableStream && !new Request('', {
        body: new ReadableStream(),
        method: 'POST',
      }).headers.has('Content-Type')
  }

We had to add this to fix it:

        // @ts-expect-error
        duplex: 'half',

Thank you for noting this. It appears that this requirement is documented in the fetch standard but nowhere else. I am planning on submitting a few pull requests for MDN to add something.

Thoughts?

It seems like it's also not supported on Safari and getting the error "Error sending request: ReadableStream uploading is not supported".

And this check against safari does not work

Not sure if a lot of people use this pattern, but our app was using this code to check if streaming upload is enabled, and it broke for users using browsers based on 105 beta of chromium:

  public static doesSupportStreamBody (): boolean {
    // This works because the browser adds a Content-Type header of text/plain;charset=UTF-8 to the request 
    // if the body is text. 
    // The browser only treats the body as text if it doesn't support request streams, 
    // otherwise it won't add a Content-Type header at all.
    return ReadableStream && !new Request('', {
        body: new ReadableStream(),
        method: 'POST',
      }).headers.has('Content-Type')
  }

We had to add this to fix it:

        // @ts-expect-error
        duplex: 'half',

Use the snippet here instead WebKit/standards-positions#24 (comment)

Looks like it isn't supported in Firefox, if anyone tried to implement any workaround in Firefox please let me know. Here is the Firefox bug report: https://bugzilla.mozilla.org/show_bug.cgi?id=1387483

My use case is also similar as we encrypt then upload and loading the entire file in memory is not an option (also not optimal)

I suppose will be better to provide progress callback for fetch instead try to implement so complicated API like Streams.
Streaming upload is important, but I believe 90% people need just indicator. Even more it's already supported by XMLHttpRequest