-
Notifications
You must be signed in to change notification settings - Fork 5.4k
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
Making concurrent HTTP/2 requests with fetch
may fail with REFUSED_CONNECTION
#21789
Comments
This may be related to #21749 as well. |
hyperium/h2#732 should also reduce the chance of us running into REFUSED_CONNECTION errors on startup. |
I just ran into this today while trying to make 35 connections at once with some naiive code: const reqs = instances.map((x) =>
fetch(
`https://cdn.xeiaso.net/file/christine-static/${basePath}/index${i}.ts`,
{
headers: {
"fly-force-instance": x,
},
},
)
); Is there a way to limit the number of requests this makes in-flight or am I just screwed? |
v1.45.5 in WSL 2. I tried fetching a URL with bytes stream The URL is HTTP/2 as well. Below is the error logs I collected.
Edited. Also tried with v1.39.1 (it outputs more detailed).
|
@osddeitf it looks like that error message |
@magurotuna Thanks for willing to help. The URL I fetch is not something of top-secret anyway, I just want to read the content (filename, filesize) of a zip file: I fetched a lot of small segment (<100 bytes). You can try it yourself by looping 1->50 and read the file with header like I tried to run the fetch logics in Nodejs and it never throws. So my workaround now is to run the fetch in a Nodejs child process, then get the read bytes from the main Deno process. |
@magurotuna I tried to make a minimal reproduce code: const offset = 2 ** 32 + 2**31
for (let i = 0; i < 100; i++) {
const start= offset + i * 40
const fet = () => fetch(url, {
headers: {
// Range: `bytes=${start}`
Range: `bytes=${start}-${start+100000}`
}
})
const res = await fet()
if (!res.body) {
throw new Error('no body')
}
try {
const reader = res.body.getReader()
const arr = await reader.read()
console.log(i, arr.value?.byteLength)
}
catch (e) {
console.log(e)
console.log('Try again with bytes()')
const res = await fet()
const arr = await res.bytes()
console.log(i, arr.byteLength)
}
}
Edited. Tried release the resource manually: await reader.cancel()
reader.releaseLock() The loop would run more interations until it throws. I suspect that there's some sort of shared state (or internal buffer) between fetch requests. And I think it shouldn't be normal to have some state shared. At least we should be able to implement retry logics (like I did above), but it's not work. Why 2 different requests failed the same though? |
@osddeitf Thanks for the code snippet. I ran that on my local machine several times and it always hung at around I also tried the manual cancellation pattern (i.e. inserting |
@magurotuna It usually throws when each read chunk size is 16k, if 8k then it hangs instead. With body size 100k bytes, it throws around 60 without manual cancelation. |
Thanks. I tried with bigger body size (1MB) and more iterations (500), but wasn't able to reproduce throwing errors. It always hangs for me. I guess it's maybe because of my network speed which is fast (over 1Gpbs)? Anyways, here are what I've found so far which I think may be helpful for you: 1. Disable HTTP/2You can disable HTTP/2 and enforce HTTP1 like below if you want. Note that you'd need const client = Deno.createHttpClient({
http2: false,
http1: true,
});
for (let i = 0; i < 100; i++) {
const res = await fetch("https://example.com", { client });
// work with res...
} In my environment, this trick makes the entire execution succeed always with no hang. (Note that 2. Reused connectionBy default Deno's for (let i = 0; i < 100; i++) {
const client = Deno.createHttpClient({});
const res = await fetch("https://example.com", { client });
// work with res...
} This creates a new connection in each iteration, which also resulted in the successful execution in my environment without disabling HTTP/2, in exchange of paying a little bit of an extra cost for establishing a new connection every time. 3. Why did manual cancellation alleviate the issueHTTP/2 has its own flow control mechanism. After you have read some amount of received data from the reader, the flow control mechanism tells the server that "You can send me another XXX bytes" (this kind of message is called "WINDOW_UPDATE" where window refers to the size that the remote peer can send to me). By properly calling |
Thank you, I'll try the approach you mentioned. I fetch a lot of small segments so I'll use HTTP/1.
It's not working correctly, so I think there should be some bug there. |
I have the same issue. When trying to stream multiple files from GCS, the fetch freezes without an error, and I can't make any new requests as they freeze too. Downgrading to HTTP/1 solved the issue. Either way, I lost a good few hours on this.
|
@raaymax Thanks for the report. It would be really helpful if you could prepare as minimal an example as possible that can reproduce the issue you ran into and share that with us. |
No matter what, I can't reproduce it in tests, and I really tried. But when I run my app, it just fails after a few moments of use. I have no idea why this is happening or what's causing it, but switching to HTTP/1 for server-to-GCS communication works for me. I'll let you know if I find any more useful information. update: I finally found the root cause. It was just some response body stream that wasn't consumed in a specific scenario. The number of these unclosed streams was accumulating over time, and eventually, every new fetch stopped working. Interestingly, when using HTTP/1, fetch wasn't blocked at all. |
Thanks for the additional info. How does your code work with the response body stream? Like I mentioned in #21789 (comment), one of the typical scenarios that can lead to hanging is that the response stream is neither consumed nor canceled, which causes HTTP/2 flow control mechanism to stop further data transmission once the window is used up. |
Version: Deno 1.39.1
Problem
The following code reliably fails.
Error message:
An interesting observation is that if we change the URL to another one (say https://fresh.deno.dev) it works with no issue even with higher concurrency.
Investigation
I found and created a minimal reproducible example which is available at https://github.com/magurotuna/deno-fetch-h2-stream-error-repro.
In short, once the HTTP/2 connection is established, the client makes a lot of concurrent requests to the server before the server's initial
SETTINGS
frame reaches the client which should tell the client the maximum number of concurrent streams allowed. The requests over the limit are rejected withREFUSED_STREAM
error.Note that the client behavior of opening too many concurrent streams before receiving the initial
SETTINGS
frame from the server is not a violation according to the RFC. Both client and server are behaving correctly.Solution
Some of the existing HTTP/2 client implementations do retry requests in case of
REFUSED_STREAM
error. We should probably do the same though at what layer we should do it is not necessarily clear.The most obvious place would be in
reqwest
; I opened a PR inreqwest
repo that adds the retry logic. seanmonstar/reqwest#2081If this PR gets merged and shipped, we can upgrade
reqwest
version we're using in Deno and the issue should be fixed. I filed this issue to track the current status.The text was updated successfully, but these errors were encountered: