1'use strict' 2 3const errors = require('./errors.js') 4const { Response } = require('minipass-fetch') 5const defaultOpts = require('./default-opts.js') 6const log = require('proc-log') 7const cleanUrl = require('./clean-url.js') 8 9/* eslint-disable-next-line max-len */ 10const moreInfoUrl = 'https://github.com/npm/cli/wiki/No-auth-for-URI,-but-auth-present-for-scoped-registry' 11const checkResponse = 12 async ({ method, uri, res, startTime, auth, opts }) => { 13 opts = { ...defaultOpts, ...opts } 14 if (res.headers.has('npm-notice') && !res.headers.has('x-local-cache')) { 15 log.notice('', res.headers.get('npm-notice')) 16 } 17 18 if (res.status >= 400) { 19 logRequest(method, res, startTime) 20 if (auth && auth.scopeAuthKey && !auth.token && !auth.auth) { 21 // we didn't have auth for THIS request, but we do have auth for 22 // requests to the registry indicated by the spec's scope value. 23 // Warn the user. 24 log.warn('registry', `No auth for URI, but auth present for scoped registry. 25 26URI: ${uri} 27Scoped Registry Key: ${auth.scopeAuthKey} 28 29More info here: ${moreInfoUrl}`) 30 } 31 return checkErrors(method, res, startTime, opts) 32 } else { 33 res.body.on('end', () => logRequest(method, res, startTime, opts)) 34 if (opts.ignoreBody) { 35 res.body.resume() 36 return new Response(null, res) 37 } 38 return res 39 } 40 } 41module.exports = checkResponse 42 43function logRequest (method, res, startTime) { 44 const elapsedTime = Date.now() - startTime 45 const attempt = res.headers.get('x-fetch-attempts') 46 const attemptStr = attempt && attempt > 1 ? ` attempt #${attempt}` : '' 47 const cacheStatus = res.headers.get('x-local-cache-status') 48 const cacheStr = cacheStatus ? ` (cache ${cacheStatus})` : '' 49 const urlStr = cleanUrl(res.url) 50 51 log.http( 52 'fetch', 53 `${method.toUpperCase()} ${res.status} ${urlStr} ${elapsedTime}ms${attemptStr}${cacheStr}` 54 ) 55} 56 57function 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} 101