check-response.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. 'use strict'
  2. const errors = require('./errors.js')
  3. const { Response } = require('minipass-fetch')
  4. const defaultOpts = require('./default-opts.js')
  5. const { log } = require('proc-log')
  6. const { redact: cleanUrl } = require('@npmcli/redact')
  7. /* eslint-disable-next-line max-len */
  8. const moreInfoUrl = 'https://github.com/npm/cli/wiki/No-auth-for-URI,-but-auth-present-for-scoped-registry'
  9. const checkResponse =
  10. async ({ method, uri, res, startTime, auth, opts }) => {
  11. opts = { ...defaultOpts, ...opts }
  12. if (res.headers.has('npm-notice') && !res.headers.has('x-local-cache')) {
  13. log.notice('', res.headers.get('npm-notice'))
  14. }
  15. if (res.status >= 400) {
  16. logRequest(method, res, startTime)
  17. if (auth && auth.scopeAuthKey && !auth.token && !auth.auth) {
  18. // we didn't have auth for THIS request, but we do have auth for
  19. // requests to the registry indicated by the spec's scope value.
  20. // Warn the user.
  21. log.warn('registry', `No auth for URI, but auth present for scoped registry.
  22. URI: ${uri}
  23. Scoped Registry Key: ${auth.scopeAuthKey}
  24. More info here: ${moreInfoUrl}`)
  25. }
  26. return checkErrors(method, res, startTime, opts)
  27. } else {
  28. res.body.on('end', () => logRequest(method, res, startTime, opts))
  29. if (opts.ignoreBody) {
  30. res.body.resume()
  31. return new Response(null, res)
  32. }
  33. return res
  34. }
  35. }
  36. module.exports = checkResponse
  37. function logRequest (method, res, startTime) {
  38. const elapsedTime = Date.now() - startTime
  39. const attempt = res.headers.get('x-fetch-attempts')
  40. const attemptStr = attempt && attempt > 1 ? ` attempt #${attempt}` : ''
  41. const cacheStatus = res.headers.get('x-local-cache-status')
  42. const cacheStr = cacheStatus ? ` (cache ${cacheStatus})` : ''
  43. const urlStr = cleanUrl(res.url)
  44. // If make-fetch-happen reports a cache hit, then there was no fetch
  45. if (cacheStatus === 'hit') {
  46. log.http(
  47. 'cache',
  48. `${urlStr} ${elapsedTime}ms${attemptStr}${cacheStr}`
  49. )
  50. } else {
  51. log.http(
  52. 'fetch',
  53. `${method.toUpperCase()} ${res.status} ${urlStr} ${elapsedTime}ms${attemptStr}${cacheStr}`
  54. )
  55. }
  56. }
  57. function checkErrors (method, res, startTime, opts) {
  58. return res.buffer()
  59. .catch(() => null)
  60. .then(body => {
  61. let parsed = body
  62. try {
  63. parsed = JSON.parse(body.toString('utf8'))
  64. } catch {
  65. // ignore errors
  66. }
  67. if (res.status === 401 && res.headers.get('www-authenticate')) {
  68. const auth = res.headers.get('www-authenticate')
  69. .split(/,\s*/)
  70. .map(s => s.toLowerCase())
  71. if (auth.indexOf('ipaddress') !== -1) {
  72. throw new errors.HttpErrorAuthIPAddress(
  73. method, res, parsed, opts.spec
  74. )
  75. } else if (auth.indexOf('otp') !== -1) {
  76. throw new errors.HttpErrorAuthOTP(
  77. method, res, parsed, opts.spec
  78. )
  79. } else {
  80. throw new errors.HttpErrorAuthUnknown(
  81. method, res, parsed, opts.spec
  82. )
  83. }
  84. } else if (
  85. res.status === 401 &&
  86. body != null &&
  87. /one-time pass/.test(body.toString('utf8'))
  88. ) {
  89. // Heuristic for malformed OTP responses that don't include the
  90. // www-authenticate header.
  91. throw new errors.HttpErrorAuthOTP(
  92. method, res, parsed, opts.spec
  93. )
  94. } else {
  95. throw new errors.HttpErrorGeneral(
  96. method, res, parsed, opts.spec
  97. )
  98. }
  99. })
  100. }