diff --git a/demo/demo.js b/demo/demo.js index a966cef..910ad1d 100644 --- a/demo/demo.js +++ b/demo/demo.js @@ -17,7 +17,7 @@ var cos = new COS({ ChunkSize: 1024 * 1024 * 8, // 控制分片大小,单位 B,在同园区上传可以设置较大的分片大小 Proxy: '', Protocol: 'https:', - FollowRedirect: false, + Timeout: 10000, }); var TaskId; diff --git a/index.d.ts b/index.d.ts index f4afe23..8cd403d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -198,6 +198,7 @@ declare namespace COS { Ip?: string; /** 默认将host加入签名计算,关闭后可能导致越权风险,建议保持为true */ ForceSignHost?: boolean; + AutoSwitchHost?: boolean; /** 获取签名的回调方法,如果没有 SecretId、SecretKey 时,必选 */ getAuthorization?: ( options: GetAuthorizationOptions, @@ -311,6 +312,10 @@ declare namespace COS { message: string; /** 兼容老的错误信息字段,不建议使用,可能是参数错误、客户端出错、或服务端返回的错误 */ error: string | Error | { Code: string; Message: string }; + /** 当前请求的Url */ + url: string; + /** 当前请求的method */ + method: string; } /** 回调的错误格式,其中服务端返回错误码可查看 @see https://cloud.tencent.com/document/product/436/7730 */ type CosError = null | CosSdkError; diff --git a/package.json b/package.json index 3f9fead..c923053 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cos-nodejs-sdk-v5", - "version": "2.12.6", + "version": "2.13.0", "description": "cos nodejs sdk v5", "main": "index.js", "types": "index.d.ts", diff --git a/sdk/advance.js b/sdk/advance.js index df19f5a..d3cddfa 100644 --- a/sdk/advance.js +++ b/sdk/advance.js @@ -25,10 +25,7 @@ function sliceUploadFile(params, callback) { // 上传过程中出现错误,返回错误 ep.on('error', function (err) { if (!self._isRunningTask(TaskId)) return; - var _err = { - UploadId: params.UploadData.UploadId || '', - err: err, - }; + err.UploadId = params.UploadData.UploadId || ''; return callback(_err); }); @@ -652,6 +649,7 @@ function uploadSliceItem(params, callback) { util.getFileMd5(md5Body, function (err, md5) { var contentMd5 = md5 ? util.binaryBase64(md5) : ''; var PartItem = UploadData.PartList[PartNumber - 1]; + var switchHost = false; Async.retry( ChunkRetryTimes, function (tryCallback) { @@ -671,9 +669,13 @@ function uploadSliceItem(params, callback) { Headers: headers, onProgress: params.onProgress, ContentMD5: contentMd5, + SwitchHost: switchHost, }, function (err, data) { if (!self._isRunningTask(TaskId)) return; + if (err) { + switchHost = err?.switchHost; + } if (err) return tryCallback(err); PartItem.Uploaded = true; return tryCallback(null, data); @@ -682,6 +684,9 @@ function uploadSliceItem(params, callback) { }); }, function (err, data) { + if (err) { + delete err.switchHost; + } if (!self._isRunningTask(TaskId)) return; return callback(err, data); } diff --git a/sdk/base.js b/sdk/base.js index fb44adf..2f8fdca 100644 --- a/sdk/base.js +++ b/sdk/base.js @@ -3020,6 +3020,7 @@ function multipartUpload(params, callback) { headers: params.Headers, onProgress: params.onProgress, body: params.Body || null, + SwitchHost: params.SwitchHost, }, function (err, data) { if (err) return callback(err); @@ -3644,9 +3645,7 @@ var getSignHost = function (opt) { region: useAccelerate ? 'accelerate' : opt.Region, }); var urlHost = url.replace(/^https?:\/\/([^/]+)(\/.*)?$/, '$1'); - var standardHostReg = new RegExp('^([a-z\\d-]+-\\d+\\.)?(cos|cosv6|ci|pic)\\.([a-z\\d-]+)\\.myqcloud\\.com$'); - if (standardHostReg.test(urlHost)) return urlHost; - return ''; + return urlHost; }; // 异步获取签名 @@ -3747,6 +3746,7 @@ function getAuthorizationAsync(params, callback) { Token: StsData.Token || '', ClientIP: StsData.ClientIP || '', ClientUA: StsData.ClientUA || '', + SignFrom: 'client', }; cb(null, AuthData); }; @@ -3870,6 +3870,7 @@ function getAuthorizationAsync(params, callback) { var AuthData = { Authorization: Authorization, SecurityToken: self.options.SecurityToken || self.options.XCosSecurityToken, + SignFrom: 'client', }; cb(null, AuthData); return AuthData; @@ -3878,9 +3879,11 @@ function getAuthorizationAsync(params, callback) { return ''; } -// 调整时间偏差 +// 判断当前请求出错时能否重试 function allowRetry(err) { - var allowRetry = false; + var self = this; + var canRetry = false; + var networkError = false; var isTimeError = false; var serverDate = (err.headers && (err.headers.date || err.headers.Date)) || (err.error && err.error.ServerTime); try { @@ -3894,6 +3897,7 @@ function allowRetry(err) { } } catch (e) {} if (err) { + // 调整时间偏差 if (isTimeError && serverDate) { var serverTime = Date.parse(serverDate); if ( @@ -3902,15 +3906,48 @@ function allowRetry(err) { ) { console.error('error: Local time is too skewed.'); this.options.SystemClockOffset = serverTime - Date.now(); - allowRetry = true; + canRetry = true; } } else if (Math.floor(err.statusCode / 100) === 5) { - allowRetry = true; + canRetry = true; } else if (err.code === 'ECONNRESET') { - allowRetry = true; + canRetry = true; + } + /** + * 归为网络错误 + * 1、no statusCode + * 2、statusCode === 3xx || 4xx || 5xx && no requestId + */ + if (!err.statusCode) { + canRetry = self.options.AutoSwitchHost; + networkError = true; + } else { + const statusCode = Math.floor(err.statusCode / 100); + const requestId = err?.headers && err?.headers['x-cos-request-id']; + if ([3, 4, 5].includes(statusCode) && !requestId) { + canRetry = self.options.AutoSwitchHost; + networkError = true; + } } } - return allowRetry; + return { canRetry, networkError }; +} + +/** + * requestUrl:请求的url,用于判断是否cos主域名,true才切 + * clientCalcSign:是否客户端计算签名,服务端返回的签名不能切,true才切 + * networkError:是否未知网络错误,true才切 + * */ +function canSwitchHost({ requestUrl, clientCalcSign, networkError }) { + if (!this.options.AutoSwitchHost) return false; + if (!requestUrl) return false; + if (!clientCalcSign) return false; + if (!networkError) return false; + const commonReg = /^https?:\/\/[^\/]*\.cos\.[^\/]*\.myqcloud\.com(\/.*)?$/; + const accelerateReg = /^https?:\/\/[^\/]*\.cos\.accelerate\.myqcloud\.com(\/.*)?$/; + // 当前域名是cos主域名才切换 + const isCommonCosHost = commonReg.test(requestUrl) && !accelerateReg.test(requestUrl); + return isCommonCosHost; } // 获取签名并发起请求 @@ -3937,6 +3974,11 @@ function submitRequest(params, callback) { params.SignHost || getSignHost.call(this, { Bucket: params.Bucket, Region: params.Region, Url: params.url }); var next = function (tryTimes) { var oldClockOffset = self.options.SystemClockOffset; + if (params.SwitchHost) { + // 更换要签的host + SignHost = SignHost.replace(/myqcloud.com/, 'tencentcos.cn'); + } + getAuthorizationAsync.call( self, { @@ -3951,18 +3993,20 @@ function submitRequest(params, callback) { ResourceKey: params.ResourceKey, Scope: params.Scope, ForceSignHost: self.options.ForceSignHost, + SwitchHost: params.SwitchHost, }, function (err, AuthData) { if (err) return callback(err); params.AuthData = AuthData; _submitRequest.call(self, params, function (err, data) { - if ( - err && - !(params.body && params.body.pipe) && - !params.outputStream && - tryTimes < 2 && - (oldClockOffset !== self.options.SystemClockOffset || allowRetry.call(self, err)) - ) { + let canRetry = false; + let networkError = false; + if (err) { + const info = allowRetry.call(self, err); + canRetry = info.canRetry || oldClockOffset !== self.options.SystemClockOffset; + networkError = info.networkError; + } + if (err && !(params.body && params.body.pipe) && !params.outputStream && tryTimes < 2 && canRetry) { if (params.headers) { delete params.headers.Authorization; delete params.headers['token']; @@ -3971,8 +4015,23 @@ function submitRequest(params, callback) { params.headers['x-cos-security-token'] && delete params.headers['x-cos-security-token']; params.headers['x-ci-security-token'] && delete params.headers['x-ci-security-token']; } + // 进入重试逻辑时 需判断是否需要切换cos备用域名 + const switchHost = canSwitchHost.call(self, { + requestUrl: err?.url || '', + clientCalcSign: AuthData?.SignFrom === 'client', + networkError, + }); + params.SwitchHost = switchHost; next(tryTimes + 1); } else { + if (err && params.Action === 'name/cos:UploadPart') { + const switchHost = canSwitchHost.call(self, { + requestUrl: err?.url || '', + clientCalcSign: AuthData?.SignFrom === 'client', + networkError, + }); + err.switchHost = switchHost; + } callback(err, data); } }); @@ -4018,6 +4077,10 @@ function _submitRequest(params, callback) { region: region, object: object, }); + if (params.SwitchHost) { + // 更换请求的url + url = url.replace(/myqcloud.com/, 'tencentcos.cn'); + } if (params.action) { url = url + '?' + params.action; } @@ -4113,6 +4176,8 @@ function _submitRequest(params, callback) { retResponse && retResponse.statusCode && (attrs.statusCode = retResponse.statusCode); retResponse && retResponse.headers && (attrs.headers = retResponse.headers); if (err) { + opt.url && (attrs.url = opt.url); + opt.method && (attrs.method = opt.method); err = util.extend(err || {}, attrs); callback(err, null); } else { diff --git a/sdk/cos.js b/sdk/cos.js index 621749e..ea99d11 100644 --- a/sdk/cos.js +++ b/sdk/cos.js @@ -44,6 +44,7 @@ var defaultOptions = { UserAgent: '', ConfCwd: '', ForceSignHost: true, // 默认将host加入签名计算,关闭后可能导致越权风险,建议保持为true + AutoSwitchHost: true, // 动态秘钥,优先级Credentials > SecretId/SecretKey。注意Cred内是小写的secretId、secretKey Credentials: { secretId: '', @@ -101,6 +102,7 @@ var COS = function (options) { console.error('error: SecretKey format is incorrect. Please check'); } if (util.isWeb()) { + console.log('Tip: 使用 electron 等跨平台技术可正常使用Nodejs SDK,请忽略下方浏览器环境警告'); console.warn( 'warning: cos-nodejs-sdk-v5 不支持浏览器使用,请改用 cos-js-sdk-v5,参考文档: https://cloud.tencent.com/document/product/436/11459' ); @@ -108,6 +110,12 @@ var COS = function (options) { 'warning: cos-nodejs-sdk-v5 does not support browsers. Please use cos-js-sdk-v5 instead, See: https://cloud.tencent.com/document/product/436/11459' ); } + if (this.options.ForcePathStyle) { + console.warn( + 'cos-nodejs-sdk-v5不再支持使用path-style,仅支持使用virtual-hosted-style,参考文档:https://cloud.tencent.com/document/product/436/96243' + ); + throw new Error('ForcePathStyle is not supported'); + } event.init(this); task.init(this); diff --git a/sdk/util.js b/sdk/util.js index 0c13ba0..5cce052 100644 --- a/sdk/util.js +++ b/sdk/util.js @@ -375,7 +375,8 @@ var hasMissingParams = function (apiName, params) { apiName.indexOf('Object') > -1 || apiName.indexOf('multipart') > -1 || apiName === 'sliceUploadFile' || - apiName === 'abortUploadTask' + apiName === 'abortUploadTask' || + apiName === 'uploadFile' ) { if (!Bucket) return 'Bucket'; if (!Region) return 'Region';