You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
The RetryHandler receives a duplicate body when the server does not support Range requests. #3356
Hmm, yeah this indeed is a bug, often due to the fact that we buffer the response in an effort of providing a range request over a next retry.
It's not an easy fix, as the data get's automatically forwarded to the main handler, so the fetch handler is the one that buffered the body. We can either have a flag that disables that range request or change the way we buffer the body (maybe keep it within the Retry handler) to identify if a request can be safely resumed with the buffered body or totally discard it.
Servers are not meant to reply in a specific way, if they do not support it, they can safely reply with a HTTP 200.
It is more like the limitation of combination between fetch and RetryAgent. fetch is using Readable for the response, once the data chunk is received you have no way to pause the buffering.
Note that even changing the internal state of Readable is not working, since the Readable is immediately consumed by the Reader which own it's internal state too.
We may buffer the response inside RetryAgent and call onData when completed, but I doubt you want this behavior.
Yeah, is basically what @climba03003 suggested; once as long as we started buffering we cannot revert what we've already buffered.
So, here I'm also not really a fan of buffering ourselves but rather better fail on it, but wanted to put all options over the table. But believe that failing might be the best, so we avoid double-buffering.
So, here I'm also not really a fan of buffering ourselves but rather better fail on it, but wanted to put all options over the table. But believe that failing might be the best, so we avoid double-buffering.
If it throw in this case, what is the meaning of retry handler exist?
The issue is demonstrating a slow network or sudden peak of the server will break the retry mechanism.
Throwing an error in such common case means the retry handler only provide limited usage.
Another solution is remember the received bytes and skip all already handled bytes.
But I can see more issue will comes when it change to this method. For example,
Must ensure body checksum exist ( e.g. Etag ), otherwise the payload cannot be safely concatenated.
The issue is demonstrating a slow network or sudden peak of the server will break the retry mechanism.
Throwing an error in such common case means the retry handler only provide limited usage.
I'd say that the biggest problem comes with the fact that the servers might or might not support the range request. If a network issue occurs or the server just breaks, the retry can identify and continue the next retry based on the last byte read.
I believe that the retry needs to also be smart enough to understand the situations where it cannot recover, avoiding the duplication of the body based on HTTP semantics might be better for user sanity, so they can understand as well under which circumstances it might/might-not set a retry handler for that given origin.
I also imagine a situation when they want to disable that, but that's a topic of another issue
I'd say that the biggest problem comes with the fact that the servers might or might not support the range request.
I believe we should think of the worst case, most server do not support Range header.
I don't think an API server will ever want to support Range header, but content server do want.
If a network issue occurs or the server just breaks, the retry can identify and continue the next retry based on the last byte read.
Yes, but the current implementation is heavily based on existence of Range header.
Once the Range header is not exist, the data is easily do break.
The current use case will be limited to either
When no data is ever consumed.
The server must support Range when data is consumed.
We have no way to knows if the down stream ever consumed the data, so provide a fix to block seems unrealistic.
Or just block when no Range header is provide, but it further restrict the use-case.
I believe that the retry needs to also be smart enough to understand the situations where it cannot recover, avoiding the duplication of the body based on HTTP semantics might be better for user sanity, so they can understand as well under which circumstances it might/might-not set a retry handler for that given origin.
The retry should support any methods that are safe (or even idempotent).
It should not rely on the existence of headers, but behavior should be improved based on headers (like the current Range and Etag).
If it throw in this case, what is the meaning of retry handler exist?
The retry handler is useful to retry in case there is a bad response code or an error in a network condition. I used it with APIs all the time.
The case of a truncated response is tricky.
In theory, a server that responds with a partial response should return 206. So, if we have already starting to flow the response, and the connection is interrupted, if the server does not respond with a 206 we could just error with "server does not support range requests".
The retry handler is useful to retry in case there is a bad response code or an error in a network condition.
It is only true for bad response code, but not really for network condition. I means network interruption in the middle.
So, if we have already starting to flow the response, and the connection is interrupted, if the server does not respond with a 206 we could just error with "server does not support range requests".
The problem is that the onData is always called and passing on.
You have no way to determine if it means to be consumed by user.
So, the solution turns out to be, always throw no matter the response is consumed or not.
'use strict'const{ tspl }=require('@matteo.collina/tspl')const{ test, after }=require('node:test')const{ createServer }=require('node:http')const{ once }=require('node:events')const{ fetch, Agent, RetryAgent }=require('..')test('https://github.com/nodejs/undici/issues/3356',async(t)=>{t=tspl(t,{plan: 2})letshouldRetry=trueconstserver=createServer()server.on('request',(req,res)=>{res.writeHead(200,{'content-type': 'text/plain'})if(shouldRetry){shouldRetry=falseres.flushHeaders()res.write('h')setTimeout(()=>{res.end('ello world!')},1000)}else{res.end('hello world!')}})server.listen(0)awaitonce(server,'listening')after(async()=>{server.close()awaitonce(server,'close')})constagent=newRetryAgent(newAgent({bodyTimeout: 500}),{errorCodes: ['UND_ERR_BODY_TIMEOUT']})constresponse=awaitfetch(`http://localhost:${server.address().port}`,{dispatcher: agent})setTimeout(async()=>{console.log('start consume')t.equal(response.status,200)t.equal(awaitresponse.text(),'hello world!')// you can see even delayed the consume, it is the same result.},2000)awaitt.completed})
Activity
metcoder95 commentedon Jun 23, 2024
Hmm, yeah this indeed is a bug, often due to the fact that we buffer the response in an effort of providing a range request over a next retry.
It's not an easy fix, as the data get's automatically forwarded to the main handler, so the fetch handler is the one that buffered the body. We can either have a flag that disables that range request or change the way we buffer the body (maybe keep it within the Retry handler) to identify if a request can be safely resumed with the buffered body or totally discard it.
Servers are not meant to reply in a specific way, if they do not support it, they can safely reply with a
HTTP 200.ronag commentedon Jun 24, 2024
@metcoder95 I'm not sure I understand what the problem is? Cna you try to explain? Or huddle me on slack.
ronag commentedon Jun 24, 2024
Ah, I see now. You need to fail the retry if position > 0 and response is not a range request. See how https://github.com/nxtedition/nxt-undici/blob/main/lib/interceptor/response-retry.js does it. In particular look at https://github.com/nxtedition/nxt-undici/blob/main/lib/interceptor/response-retry.js#L116.
climba03003 commentedon Jun 24, 2024
It is more like the limitation of combination between
fetchandRetryAgent.fetchis usingReadablefor the response, once the data chunk is received you have no way to pause the buffering.Note that even changing the internal state of
Readableis not working, since theReadableis immediately consumed by theReaderwhich own it's internal state too.We may buffer the response inside
RetryAgentand callonDatawhen completed, but I doubt you want this behavior.metcoder95 commentedon Jun 25, 2024
Yeah, is basically what @climba03003 suggested; once as long as we started buffering we cannot revert what we've already buffered.
So, here I'm also not really a fan of buffering ourselves but rather better fail on it, but wanted to put all options over the table. But believe that failing might be the best, so we avoid double-buffering.
climba03003 commentedon Jun 25, 2024
If it throw in this case, what is the meaning of retry handler exist?
The issue is demonstrating a slow network or sudden peak of the server will break the retry mechanism.
Throwing an error in such common case means the retry handler only provide limited usage.
Another solution is remember the received bytes and skip all already handled bytes.
But I can see more issue will comes when it change to this method. For example,
Etag), otherwise the payload cannot be safely concatenated.metcoder95 commentedon Jun 26, 2024
I'd say that the biggest problem comes with the fact that the servers might or might not support the range request. If a network issue occurs or the server just breaks, the retry can identify and continue the next retry based on the last byte read.
I believe that the
retryneeds to also be smart enough to understand the situations where it cannot recover, avoiding the duplication of the body based on HTTP semantics might be better for user sanity, so they can understand as well under which circumstances it might/might-not set a retry handler for that given origin.I also imagine a situation when they want to disable that, but that's a topic of another issue
climba03003 commentedon Jun 26, 2024
I believe we should think of the worst case, most server do not support
Rangeheader.I don't think an API server will ever want to support
Rangeheader, but content server do want.Yes, but the current implementation is heavily based on existence of
Rangeheader.Once the
Rangeheader is not exist, the data is easily do break.The current use case will be limited to either
Rangewhen data is consumed.We have no way to knows if the down stream ever consumed the data, so provide a fix to block seems unrealistic.
Or just block when no
Rangeheader is provide, but it further restrict the use-case.The retry should support any methods that are safe (or even idempotent).
It should not rely on the existence of headers, but behavior should be improved based on headers (like the current
RangeandEtag).mcollina commentedon Jun 26, 2024
The retry handler is useful to retry in case there is a bad response code or an error in a network condition. I used it with APIs all the time.
The case of a truncated response is tricky.
In theory, a server that responds with a partial response should return 206. So, if we have already starting to flow the response, and the connection is interrupted, if the server does not respond with a 206 we could just error with "server does not support range requests".
climba03003 commentedon Jun 26, 2024
It is only true for bad response code, but not really for network condition. I means network interruption in the middle.
The problem is that the
onDatais always called and passing on.You have no way to determine if it means to be consumed by user.
So, the solution turns out to be, always throw no matter the response is consumed or not.
metcoder95 commentedon Jun 30, 2024
Yes, aborting the request might be the best option for now. Only upon responses of servers not supporting
rangerequests.