undici icon indicating copy to clipboard operation
undici copied to clipboard

fix(api-stream): Add throwOnError support in undici.stream

Open assalielmehdi opened this issue 3 years ago • 10 comments

#1753

assalielmehdi avatar Nov 09 '22 23:11 assalielmehdi

This doesn't handle body properly, see differences between api-request.js and api-stream.js in onData and onComplete. Whatever stream is doing requires .write() and .end(), but new Readable requires .push() and .push(null)

await undici.request('https://api.github.com/notfound', {
  throwOnError: true
})
ResponseStatusCodeError: Response status code 403: Forbidden {
  code: 'UND_ERR_RESPONSE_STATUS_CODE',
  body: '\r\n' +
    'Request forbidden by administrative rules. Please make sure your request has a User-Agent header (https://docs.github.com/en/rest/overview/resources-in-the-rest-api#user-agent-required). Check https://developer.github.com for other possible causes.\r\n',
  status: 403,
  statusCode: 403,
  headers: {
    'cache-control': 'no-cache',
    'content-type': 'text/html; charset=utf-8',
    'strict-transport-security': 'max-age=31536000',
    'x-content-type-options': 'nosniff',
    'x-frame-options': 'deny',
    'x-xss-protection': '0',
    'content-security-policy': "default-src 'none'; style-src 'unsafe-inline'",
    connection: 'close'
  }
}
await undici.stream(
  "https://api.github.com/notfound",
  { throwOnError: true },
  () => fs.createWriteStream("response.txt")
);
ResponseStatusCodeError: Response status code 403: Forbidden {
  code: 'UND_ERR_RESPONSE_STATUS_CODE',
  body: undefined,
  status: 403,
  statusCode: 403,
  headers: {
    'cache-control': 'no-cache',
    'content-type': 'text/html; charset=utf-8',
    'strict-transport-security': 'max-age=31536000',
    'x-content-type-options': 'nosniff',
    'x-frame-options': 'deny',
    'x-xss-protection': '0',
    'content-security-policy': "default-src 'none'; style-src 'unsafe-inline'",
    connection: 'close'
  }
}

mdashlw avatar Nov 11 '22 05:11 mdashlw

Hi @mdashlw, thank you for your comment. I think that it's pretty normal for the body to be undefined in the case of undici.stream() since the HTTP body is written to the writable stream created by the factory. However, I had to make sure that the writable in finished before throwing an error. We can retrieve the body from a PassThrough opaque if needed (see unit test).

assalielmehdi avatar Nov 12 '22 00:11 assalielmehdi

Hi @mcollina, thank you 🙏🏼. In docs, options is already of type RequestOptions that extends DispatchOptions, and throwOnError is already explained in DispatchOptions. I see nothing more to add in docs. WDYT?

assalielmehdi avatar Nov 12 '22 01:11 assalielmehdi

https://github.com/nodejs/undici/pull/1767/commits/8f7a6d79ff2d43af6997770a710e0086057e9ba3 kind of re-introduces the entire issue. This will create response.txt with the actual response and still throw ResponseStatusCodeError.

await undici.stream(
  "https://api.github.com",
  { throwOnError: true },
  () => fs.createWriteStream("response.txt")
);
ResponseStatusCodeError: Response status code 403: Forbidden {
  code: 'UND_ERR_RESPONSE_STATUS_CODE',
  body: undefined
  ...
}
response.txt
Request forbidden by administrative rules. Please make sure your request has a User-Agent header (https://docs.github.com/en/rest/overview/resources-in-the-rest-api#user-agent-required). Check https://developer.github.com for other possible causes.

IMO the most intuitive and convenient way would be to throw an error with special body handling, as in .request(), and not to call the factory.

mdashlw avatar Nov 12 '22 05:11 mdashlw

Since in undici.request(), the body is consumed before throwing the error, I figured we want the same from undici.stream(). I fixed the code to throw an error before even calling the factory, and updated the unit test accordingly.

@mdashlw for the body handling in undici.request(), the body is consumed before throwing, and we can't do so in undici.stream() unless we call the factory. So body will always be undefined in the error object.

assalielmehdi avatar Nov 12 '22 09:11 assalielmehdi

You can consume the body without calling the factory.

mcollina avatar Nov 12 '22 09:11 mcollina

I updated the logic, to throw before calling the factory, and I handled the body so the thrown error contains it. I updated the tests accordingly. I hope that is the behavior we expect.

assalielmehdi avatar Nov 12 '22 11:11 assalielmehdi

Codecov Report

Base: 90.28% // Head: 90.24% // Decreases project coverage by -0.03% :warning:

Coverage data is based on head (94dbebd) compared to base (93fd794). Patch coverage: 100.00% of modified lines in pull request are covered.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1767      +/-   ##
==========================================
- Coverage   90.28%   90.24%   -0.04%     
==========================================
  Files          58       58              
  Lines        5188     5199      +11     
==========================================
+ Hits         4684     4692       +8     
- Misses        504      507       +3     
Impacted Files Coverage Δ
lib/core/util.js 97.93% <ø> (ø)
lib/api/api-stream.js 100.00% <100.00%> (ø)
lib/fetch/body.js 95.87% <0.00%> (-0.46%) :arrow_down:
lib/fetch/index.js 83.27% <0.00%> (-0.38%) :arrow_down:

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

:umbrella: View full report at Codecov.
:loudspeaker: Do you have feedback about the report comment? Let us know in this issue.

codecov-commenter avatar Nov 12 '22 11:11 codecov-commenter

Hi @mcollina, can you review the changes please? 😊

assalielmehdi avatar Nov 13 '22 11:11 assalielmehdi

there are ci failures caused by a node-fetch test (npm run test:node-fetch)

KhafraDev avatar Nov 13 '22 21:11 KhafraDev

This will be fixed once we move throwOnError to an interceptor.

https://github.com/nodejs/undici/issues/2835

ronag avatar Feb 25 '24 09:02 ronag