Repository navigation
Fetch body streams are not full duplex #1254
Description
With request body streams and response body streams one would think it is now possible to use fetch for full duplex communication over HTTP, like in the example below:
const intervalStream = new ReadableStream({
start(c) {
let count = 0;
const timer = setInterval(() => {
console.log("sent!");
c.enqueue("Hello\n");
if (count === 5) {
clearInterval(timer);
c.close();
}
count++;
}, 1000);
},
}).pipeThrough(new TextEncoderStream());
const resp = await fetch("https://full-duplex-server.deno.dev/", {
method: "POST",
body: intervalStream,
});
console.log("foo");
const reader = resp.body.pipeThrough(new TextDecoderStream()).getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
console.log(value);
}
console.log("done!");In this example the server just pipes the incoming request body stream into the response body steam with no modification.
If full duplex communication was working, you would see "sent!" and "Hello\n" being logged in close proximity to each other, live. This is however not the case, because the promise returned from fetch does not resolve until the entire request body stream has been sent and closed.
The solution here would be to resolve the Promise<Response> returned from fetch before the request body stream has completed. To do this, body upload streaming has to happen in parallel. The main issue with the spec in this area is that the promise returned from fetch is expected to reject if a chunk of the request body stream is not a Uint8Array (behaviour is tested in https://staging.wpt.fyi/results/fetch/api/basic/request-upload.any.html).
Are there any plans to resolve this shortcoming before request body streaming is sthbilized in browsers? HTTP/2 and QUIC are very capable duplex stream protocols (HTTP/1 is theoretically also capable) and it would be a shame if that power was not exposed to the web platform.
Activity
lucacasonato commentedon Jun 16, 2021
To be clear, I am not explicitly vouching for duplex streams to make it into the initial request body uploading implementation in browsers, I just want to make sure that it would be possible to add down the line. Deno implements
fetchw/ request body streaming, and our implementation is capable of full duplex too, but it has to deviate from the spec to allow that. The backing HTTP implementations for both Node.js and Servo are also capable of full duplex body streams.annevk commentedon Jun 16, 2021
See #229 for some history. Since then, I've learned that this would require a lot of work in browsers and it's not planned to be supported initially.
However, I do think we should allow for it in theory, so if you could elaborate on what prohibits it that would help.
lucacasonato commentedon Jun 16, 2021
The promise returned from
fetchdoes not resolve until the entirerequest.bodyReadableStream has been read and uploaded. This is fundamentally incompatible with duplex streaming, as you can not get a handle to theresponse.bodyuntil you closerequest.body.These tests demonstrate that issue: https://github.com/web-platform-tests/wpt/blob/master/fetch/api/basic/request-upload.any.js. It is expected that the promise returned from
fetchrejects if a chunk that is pulled off therequest.bodyis not aUint8Array. Instead this error should be communicated via cancelling therequest.bodyReadableStreamthe given error.annevk commentedon Jun 16, 2021
Does the specification also have such issues?
Now that you mention that example though I realize it is rather tricky if not impossible to do well here and be forward compatible. I suspect that applications will have the same kind of dependencies and browsers changing this will break them.
sleevi commentedon Jun 16, 2021
Yeah, there seems some overlap with #95 / #538 in terms of upload/request processing.
annevk commentedon Jun 16, 2021
cc @yoichio @yutakahirano
lucacasonato commentedon Jun 16, 2021
As far as I can tell, yes. The request body is sent synchronously before receiving response headers (not in parallel). See the "To transmit request’s body body, run these steps:" section in HTTP-network fetch step 8.5. Essentially what needs to happen is, that this needs to be changed to happen in parallel with receiving the response headers.
To elaborate on this: many (likely most) web servers are programmed to only send back response headers after the request body has been received and processed (from now on referred to as category one servers). Because the promise returned from
fetchwould only resolve once the response headers are received. For the user the behaviour is that "fetch completes after the request body is fully sent". This behaviour would not change once full duplex streaming is implemented in browsers because the response headers are not received by the browser until the request body has been sent (because only then are they sent by the server).For servers that send back response headers before the request body has been received (from now on referred to as category two servers), the promise returned from
fetchwould still only resolve once the request body is fully sent (because as you say, browsers do not support full duplex HTTP right now). Once browsers implement full duplex HTTP the promise returned fromfetchwould resolve after the response headers are received, regardless of the state of the request body. This is technically a minor change in behaviour, but one that could be marked as a spec compliance bug that Chrome, FF, and Safari have. The Deno or Servo implementations could implement full duplex right away.Because the Python WPT test server probably falls into category one, the WPT I mentioned above is technically correct, but only under the assumption that the server sends headers after the response body is received. For cases where you need to handle both category one and category two servers you should handle the "cancellation" of the
ReadableStream. This would be the only way to get notified of request body errors for category two servers.TLDR, to solve this following would need to be resolved in the spec:
lucacasonato commentedon Jun 16, 2021
One question this brings up: if request body sending on HTTP-network fetch happens in parallel, how and where are errors during request body sending propagated to the user?
annevk commentedon Jun 16, 2021
In the case where the caller is not using upload streams? I suppose they would need
FetchObserver(#607).lucacasonato commentedon Jun 16, 2021
Yeah. For users that are using upload streams it can be surfaced as a cancellation of the stream.
Maybe the error could be surfaced as a rejection of the promise returned by
fetchif the response headers have not yet been received, or alternatively as an error on the response body stream. Sort of like how aborting fetch works, but with a custom error message (the error that request body upload errored with), instead of anAbortError.This would introduce the constraint that request body uploading must not outlive response body download. Good chance that is already a HTTP spec constraint anyway, but not sure.
ricea commentedon Jun 17, 2021
My view:
annevk commentedon Jun 17, 2021
@ricea the assertion that HTTP is half-duplex seems in contradiction with it allowing the client to consume the response before it has transmitted the request body. I tend to agree with regards to the cost/benefit tradeoff and what Fetch should specify (although I would like for things to be cooler).
lucacasonato commentedon Jun 17, 2021
@ricea Some comments:
HTTP is not half duplex. It allows for simultaneous request body and response body streaming at a per request level. This is supported in both HTTP/1.1. and HTTP/2. Here are some discussions around the topic from 8 years ago: https://datatracker.ietf.org/doc/html/draft-zhu-http-fullduplex#section-2.1. Note that this is full duplex on a per request level, not full duplex on a connection level (the latter is only supported in HTTP/2, and browsers make ample use of it). Go, Node.js, and many other runtimes support full-duplex HTTP body streams.
Regarding request smuggling: full-duplex is not any more vulnerable to this than any other request body streaming (#966). Whatever the ultimate restrictions are that result from the #966 discussion, they would apply to full-duplex request body streaming too.
Regarding the effort required for implementation: it is very understandable that there are no resources to implement this now. My concern is that with the current spec wording there is also no way to implement it in the future, even if opinion on this matter changes. We are effectively locked into half-duplex for fetch forever. I think the specification should leave room for specifying this behaviour in the future - who knows what happens 5 years from now :-)
Also some non-browser engines implement the Fetch specification now, or are planning to in the future (Deno, Node.js, Cloudflare Workers). I think it is worth discussing if the specification should cater to these audiences too - I think APIs being the same across browsers and servers is a great thing that nearly no other language can provide. Also the Servo network stack is already capable of full-duplex HTTP.
23 remaining items
piranna commentedon Jul 24, 2023
ioquatix commentedon Jul 26, 2023
Okay, that's a fair enough level of quantification and I think we can aim for that. Is it okay if I start collecting references to those things on this thread or should I document it elsewhere?
Evidence of User Difficulty
Evidence that HTTP is Full Duplex
From Roy Fielding himself:

Evidence of Implementations
annevk commentedon Jul 26, 2023
Here is fine, but maybe continue updating that comment so everyone watching the repository doesn't get a stream of small updates. (I also don't think we need evidence HTTP is full duplex. Apart from Adam I haven't seen anyone contest that.)
helmturner commentedon Oct 31, 2023
@ioquatix I'd like to add to that. I recently started contributing to tRPC, and the implementation of streaming functionality (in particularly high demand thanks to ChatGPT) is being partially blocked by this.
The use case for us was the ability to batch requests that include streamed responses. In order to cancel a single stream without cancelling the entire batch we need to get a message from server to client mid-response, potentially when that server is a stateless 'serverless' function. We already eagerly write minimal Headers, and the intent would have been to use Trailers where necessary.
Without this, users are forced to pick between either batching OR streaming, or they have to implement some pretty painstaking request orchestration.
Re: 'Implementer support'... huh? As was already mentioned many times, server runtimes are implementing this already. In fact, it wasn't until our group read the spec, built a POC in node, AND did extra in-browser testing that we found out this was a no-go.
Finally, I want to add to the long list of obvious use-cases: simple push-messaging in React Native apps, without being forced to use Messaging APN providers.
gRPC and Firebase are perfect examples of how useful this sort of thing would be (though, also probably a factor in why this isn't supported by the Chrome team...)
benlesh commentedon Feb 23, 2024
Commenting to illustrate need:
I have a direct need for this as well. Working on a private code base, but the use case is we have a lot of streaming that we're currently multiplexing over a WebSocket. Most of these streams are simply "1 request, N responses", however 2 or 3 of them are bidirectional. Moving to http/2 for "1 request, N responses" case would be a boon for us, because we'd be able to let http/2 do the multiplexing for us, less JavaScript, less code running on the server and the client (because there's no need to wrap something in an "envelope" and match it up on both sides). The lack of bidirectional communication (full duplex) in fetch is really the only thing holding me up from moving everything over to http/2.
There's always
WebTransportof course, but I feel like http/3 is an even taller order, I don't think it's supported in Node yet, and it's unlikely that the support is great in other languages our teams deal with.fix(lite/release): pass "duplex: 'half'" option to tarball request
dojyorin commentedon Jun 9, 2025
There are many reasons why this is necessary, but I think it's especially important on corporate intranets.
Most of the time, such networks have (unnecessary) limitations on protocols like UDP/WebTransport and WebSocket that provide full-duplex communication.
The protocol itself for full-duplex fetch is just plain old HTTP, so it can provide full-duplex communication while avoiding these limitations.
AjaniBilby commentedon Jun 16, 2025
otyebis commentedon Jul 9, 2025
piranna commentedon Jul 9, 2025
@otyebis i'm also infuriated why such a basic feature is not a available yet, when it's pretty simple to achieve with any TCP socket, but if you would have read the other comments, you'll see it's NOT so simple to implement, since web browsers network libraries have been optimized since 30 years ago for the half-duplex request-response scheme, and modify them to support full-duplex bi-directional communication would means a massive architecture change for them, so don't hope to have It solved soon.
Also, spec writers are sometimes doing that in their spare time, and they doesn't own you anything, so relax your tone, because that's not constructive and only make people to don't employ time to work for free for people writting messages like yours that doesn't deserve It. And by the way, creating an account just only to write that message, it's of very bad taste and total lack of etiquete.
Hashbrown777 commentedon Jul 11, 2025
AjaniBilby commentedon Jul 11, 2025
Hashbrown777 commentedon Jul 11, 2025
AjaniBilby commentedon Jul 11, 2025