相同参数调用DocuSign createClickwrap返回404问题咨询
DocuSign调用createClickwrap方法返回404问题排查
问题现象
- 调用createClickwrap方法时收到404 Not Found错误
- DocuSign官方Quickstart生成的演示项目调用同方法可正常执行,无报错
- 自研项目调用该方法必现上述错误
- 调试对比两个项目传入方法的参数,包括
accountId在内的所有入参完全一致
相关实现代码
docusign controller 层代码
const docuSignService = require('./docusign_esign_service'); const demoDocumentsPath = path.resolve(__dirname, '../demo_documents'); const { createClickwrap } = require('./createClickWrap'); async getDocusignRecieptService() { const authResponse = await docuSignService.authenticate(); if (authResponse) { const docTermsPdf = 'Term_Of_Service.pdf'; const docFile = path.resolve(demoDocumentsPath, docTermsPdf); const { basePath, accessToken, apiAccountId } = authResponse; const { clickwrapId } = await createClickwrap({ docFile, basePath, accessToken, accountId: apiAccountId }); const res = await activateClickwrap({ clickwrapId, basePath, accessToken, accountId: apiAccountId }); console.log({ res }); } }
docuSignService.js 认证模块代码
const SCOPES = ['signature', 'impersonation', 'openid', 'click.manage', 'click.send']; const fs = require('fs'); const docusign = require('docusign-esign'); class DocusingService { async authenticate() { const jwtLifeSec = 10 * 60, // 申请的JWT有效期为10分钟 dsApi = new docusign.ApiClient(); dsApi.setOAuthBasePath(process.env.dsOauthServer.replace('https://', '')); // 此处仅需传入域名 let rsaKey = fs.readFileSync(process.env.privateKeyLocation); try { const results = await dsApi.requestJWTUserToken( process.env.dsJWTClientId, process.env.impersonatedUserGuid, SCOPES, rsaKey, jwtLifeSec ); const accessToken = results.body.access_token; // 获取用户信息 const userInfoResults = await dsApi.getUserInfo(accessToken); // 取默认账号信息 let userInfo = userInfoResults.accounts.find((account) => account.isDefault === 'true'); return { accessToken: results.body.access_token, apiAccountId: userInfo.accountId, basePath: `${userInfo.baseUri}/restapi` }; } catch (e) { let body = e.response && e.response.body; // 判断错误来源 if (body) { // 应用未获得用户授权 if (body.error && body.error === 'consent_required') { if (this.getConsent()) { return this.authenticate(); } } else { // 已获得授权,打印接口错误状态码和返回内容 this._debug_log(`\nAPI problem: Status code ${e.response.status}, message body: ${JSON.stringify(body, null, 4)}\n\n`); } } } } getConsent() { var urlScopes = SCOPES.join('+'); // 构造授权跳转URL var redirectUri = 'https://developers.docusign.com/platform/auth/consent'; var consentUrl = `${process.env.dsOauthServer}/oauth/auth?response_type=code&` + `scope=${urlScopes}&client_id=${process.env.dsJWTClientId}&` + `redirect_uri=${redirectUri}`; throw new Error(`Open the following URL in your browser to grant consent to the application: ${consentUrl}`); } getArgs(apiAccountId, accessToken, basePath, signerEmail, signerName, id, agreementData, redirect_uri) { const envelopeArgs = { signerEmail: signerEmail, signerName: signerName, status: 'sent', signerClientId: id, dsReturnUrl: redirect_uri, agreement: agreementData }; const args = { accessToken: accessToken, basePath: basePath, accountId: apiAccountId, envelopeArgs: envelopeArgs }; return args; } } module.exports = new DocusingService();
createClickWrap.js 点击包装创建逻辑代码
const createClickwrap = async ({ docFile, clickwrapName = 'clickwrapName', basePath, accessToken, accountId }) => { // 构造请求体 // 创建设置项模型 const displaySettings = docusignClick.DisplaySettings.constructFromObject({ consentButtonText: 'I Agree', displayName: 'Terms of Service', downloadable: true, format: 'modal', hasAccept: true, mustRead: true, requireAccept: true, documentDisplay: 'document' }); // 创建文档模型 // 读取文件并做base64编码,存入文档实体,文件不存在时会抛出异常 const documentPdfExample = fs.readFileSync(docFile); const encodedExampleDocument = Buffer.from(documentPdfExample).toString('base64'); const document = docusignClick.Document.constructFromObject({ documentBase64: encodedExampleDocument, documentName: 'Terms of Service', fileExtension: 'pdf', order: 0 }); // 创建clickwrap请求模型 const clickwrapRequest = docusignClick.ClickwrapRequest.constructFromObject({ displaySettings, documents: [document], name: clickwrapName, requireReacceptance: true }); // 初始化Click API客户端 const dsApiClient = new docusignClick.ApiClient(); dsApiClient.setBasePath(basePath); dsApiClient.addDefaultHeader('Authorization', 'Bearer ' + accessToken); const accountApi = new docusignClick.AccountsApi(dsApiClient); // 发起创建clickwrap请求 let result = null; try { result = await accountApi.createClickwrap(accountId, { clickwrapRequest }); } catch (e) { debugger; console.log(e); } debugger; console.log(`Clickwrap was created. ClickwrapId ${result.clickwrapId}`); return result; }; module.exports = { createClickwrap };
参数对比截图
- 正常运行的演示项目入参:

- 自研项目入参:

完整报错堆栈
"Error: Not Found at Request.callback (/Users/and/test/node_modules/docusign-click/node_modules/superagent/lib/node/index.js:696:15) at IncomingMessage.<anonymous> (/Users/and/test/node_modules/docusign-click/node_modules/superagent/lib/node/index.js:906:18) at IncomingMessage.emit (node:events:539:35) at IncomingMessage.emit (node:domain:475:12) at endReadableNT (node:internal/streams/readable:1345:12) at processTicksAndRejections (node:internal/process/task_queues:83:21)"
问题根因与修复方案
核心问题是Click API的basePath配置错误。
当前认证逻辑中拼接的${userInfo.baseUri}/restapi是DocuSign eSignature(电子签名)API专用的路径前缀,Click API不使用/restapi后缀,直接使用userInfo.baseUri作为根路径即可。
你当前把eSignature专用的basePath传给了Click API客户端,最终请求会被拼成错误路径:{baseUri}/restapi/clickapi/v1/accounts/...,而Click API的正确路径格式为{baseUri}/clickapi/v1/accounts/...,多余的/restapi路径段会导致服务端无法匹配对应路由,直接返回404 Not Found。这也是为什么参数看起来完全一致但调用失败——实际请求的接口地址根本不对。
官方Quickstart项目内部会对不同API模块使用对应的basePath:eSignature接口用带/restapi的路径,Click、Monitor等其他API直接使用根baseUri,自研项目里复用了eSignature的basePath给Click模块,才会触发这个问题。
修复方式:给Click API客户端设置basePath时去掉/restapi后缀即可,示例修改如下:
// createClickWrap.js 中修改setBasePath逻辑 dsApiClient.setBasePath(basePath.replace('/restapi', ''));
修改后请求路径会匹配Click API的路由规则,404错误即可解决。
内容的提问来源于stack exchange,提问作者Matt
相关产品推荐
相关产品推荐

