调用返回ContentService的Google Apps Script出现返回类型不支持错误求助
问题:调用Google Apps Script返回不支持的返回类型错误
我创建了一个返回ContentService.createTextOutput("Hello")的简单Google Apps Script,将其关联到Google Cloud项目,并编写了JavaScript程序以获取返回值。可以成功执行、部署该脚本,且使用部署页面的链接时一切正常,但JavaScript程序返回错误提示:
"The script completed but the returned value is not a supported return type."
查看日志可知脚本已执行完成。我的Cloud项目已启用Drive API(尽管未使用),且创建令牌时已指定该权限范围。相关JavaScript代码如下:
example.ts
import * as google from "googleapis"; import { authorizePromise } from "./authorizePromise"; import * as dotenv from "dotenv"; dotenv.config(); main(); async function main() { const auth = await authorizePromise(); console.log("abut to call"); callAppsScript(auth); } /** * Call an Apps Script function to list the folders in the user's root * Drive folder. * * @param {google.auth.OAuth2} auth An authorized OAuth2 client. */ function callAppsScript(auth) { var scriptId = process.env.SCRIPT_ID; console.log("Script ID: ", scriptId); var script = new google.script_v1.Script({}); // Make the API request. The request object is included here as 'resource'. console.log("running", auth); script.scripts.run( { auth: auth, scriptId: scriptId, requestBody: { function: "doGet", devMode: true }, }, function (err, resp) { if (err) { // The API encountered a problem before the script started executing. console.log("The API returned an error: " + err); return; } if (resp.error) { // The API executed, but the script returned an error. // Extract the first (and only) set of error details. The values of this // object are the script's 'errorMessage' and 'errorType', and an array // of stack trace elements. var error = resp.error.details[0]; console.log("Script error message: " + error.errorMessage); console.log("Script error stacktrace:"); if (error.scriptStackTraceElements) { // There may not be a stacktrace if the script didn't start executing. for (var i = 0; i < error.scriptStackTraceElements.length; i++) { var trace = error.scriptStackTraceElements[i]; console.log("\t%s: %s", trace.function, trace.lineNumber); } } } else { // The structure of the result will depend upon what the Apps Script // function returns. Here, the function returns an Apps Script Object // with String keys and values, and so the result is treated as a // Node.js object (folderSet). console.log( "debug", resp, "response", Object.keys(resp), Object.values(resp), // "resp data", // Object.keys(resp.data), // "xxxxx", // Object.values(resp.data), // "xxxxx", // Object.keys(resp.data.response), // "xxxxx", // Object.values(resp.data.response), JSON.stringify(resp) ); var folderSet = resp.data.response; if (Object.keys(folderSet).length == 0) { console.log("No folders returned!"); } else { console.log("Folders under your root folder:"); Object.keys(folderSet).forEach(function (id) { console.log("\t%s (%s)", folderSet[id], id); }); } } } ); }
authorizePromise.ts
const fs = require("fs"); const readline = require("readline"); const { google } = require("googleapis"); // If modifying these scopes, delete token.json. const SCOPES = ["https://www.googleapis.com/auth/drive"]; // The file token.json stores the user's access and refresh tokens, and is // created automatically when the authorization flow completes for the first // time. const TOKEN_PATH = "token.json"; /** * Create an OAuth2 client with the given credentials, and then execute the * given callback function. * @param {Object} credentials The authorization client credentials. * @param {function} callback The callback to call with the authorized client. */ export async function authorizePromise() { const credentials = JSON.parse( fs.readFileSync("credentials.json").toString() ); return new Promise((resolve) => { resolve(authorize(credentials, resolve)); }); } async function authorize(credentials, callback) { console.log("authorize", credentials); const { client_secret, client_id, redirect_uris } = credentials.installed; console.log("authorize 3", client_secret, client_id, redirect_uris); const oAuth2Client = new google.auth.OAuth2( client_id, client_secret, redirect_uris[0] ); // Check if we have previously stored a token. let token; try { token = JSON.parse(fs.readFileSync(TOKEN_PATH).toString()); } catch { token = await getAccessTokenPromise(oAuth2Client); } console.log("token", token); oAuth2Client.setCredentials(token); console.log("authorize 4"); callback(oAuth2Client); } function getAccessTokenPromise(oAuth2Client) { return new Promise((resolve) => { getAccessToken(oAuth2Client, resolve); }); } /** * Get and store new token after prompting for user authorization, and then * execute the given callback with the authorized OAuth2 client. * @param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for. * @param {getEventsCallback} callback The callback for the authorized client. */ function getAccessToken(oAuth2Client, callback) { const authUrl = oAuth2Client.generateAuthUrl({ access_type: "offline", scope: SCOPES, }); console.log("Authorize this app by visiting this url:", authUrl); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); rl.question("Enter the code from that page here: ", (code) => { rl.close(); oAuth2Client.getToken(code, (err, token) => { if (err) return console.error("Error retrieving access token", err); console.log("token", token); oAuth2Client.setCredentials(token); console.log("Going here"); console.log("g", JSON.stringify(token)); // Store the token to disk for later program executions fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => { console.log("Err?", token, err); if (err) return console.error(err); console.log("Token stored to", TOKEN_PATH); }); callback(oAuth2Client); }); }); }
原因分析
doGet函数是为HTTP请求场景设计的,返回的ContentService对象是Google Apps Script用于构建HTTP响应的专用类型。但通过Google Apps Script API直接调用函数时,不支持返回这类服务端响应对象,只能返回JSON、字符串、数字、数组等可序列化的基础数据类型。
解决方案
方案1:新增专门供API调用的函数
修改Google Apps Script代码,保留原doGet用于HTTP访问,新增一个返回基础类型的函数:
// 专门供API调用的函数,返回字符串 function getHello() { return "Hello"; } // 原HTTP请求处理函数 function doGet() { return ContentService.createTextOutput("Hello"); }
然后修改JavaScript代码中的调用目标,改为调用getHello:
// 修改requestBody中的function字段 requestBody: { function: "getHello", devMode: true },
方案2:在doGet中兼容两种调用场景
如果不想新增函数,可以通过参数判断调用来源,返回对应类型:
function doGet(e) { // 通过传入的参数判断是否为API调用 if (e && e.parameter.isApiCall) { return "Hello"; } // 正常HTTP请求返回ContentService对象 return ContentService.createTextOutput("Hello"); }
调用时传递参数:
requestBody: { function: "doGet", devMode: true, parameters: [{isApiCall: true}] // 传递标识参数 },
额外优化建议
- 权限范围收紧:当前使用的
https://www.googleapis.com/auth/drive权限过大,如果不需要操作Drive资源,建议替换为更窄的权限,比如https://www.googleapis.com/auth/script.run或https://www.googleapis.com/auth/script.external_request,降低权限风险。 - 确认部署权限:开发模式下调用脚本时,需确保部署设置中允许对应用户访问(比如勾选"Allow anyone, even anonymous"或指定内部用户范围)。
内容的提问来源于stack exchange,提问作者Ethan Strominger
相关产品推荐
相关产品推荐

