树莓派Google Calendar API报redirect_uris未定义错误(Windows正常)
问题
在树莓派上搭建调用Google Calendar API的Node.js服务器时,出现错误:TypeError: Cannot read property 'redirect_uris' of undefined。该代码在Windows系统下可正常运行,仅Google相关功能异常,其他功能经简单网页测试确认正常。
错误栈
(node:9146) ExperimentalWarning: The http2 module is an experimental API. Servern lyssnar på port 3000 TypeError: Cannot read property 'redirect_uris' of undefined at authenticate (/home/pi/server/node_modules/@google-cloud/local-auth/build/src/index.js:50:15) at authorize (/home/pi/server/server.js:69:18) at <anonymous> at process._tickCallback (internal/process/next_tick.js:188:7)
相关代码
// Imports: const fs = require('fs').promises; const path = require('path'); const process = require('process'); const {authenticate} = require('@google-cloud/local-auth'); const {google} = require('googleapis'); const express = require("express"); const cors = require("cors"); // If modifying these scopes, delete token.json. const SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']; // 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 = path.join(process.cwd(), 'token.json'); const CREDENTIALS_PATH = path.join(process.cwd(), 'token.json'); // Food Menu list var foodList = []; // Google Calendar API /** * Reads previously authorized credentials from the save file. * * @return {Promise<OAuth2Client|null>} */ async function loadSavedCredentialsIfExist() { try { const content = await fs.readFile(TOKEN_PATH); const credentials = JSON.parse(content); return google.auth.fromJSON(credentials); } catch (err) { return null; } } /** * Serializes credentials to a file compatible with GoogleAUth.fromJSON. * * @param {OAuth2Client} client * @return {Promise<void>} */ async function saveCredentials(client) { const content = await fs.readFile(CREDENTIALS_PATH); const keys = JSON.parse(content); const key = keys.installed || keys.web; const payload = JSON.stringify({ type: 'authorized_user', client_id: key.client_id, client_secret: key.client_secret, refresh_token: client.credentials.refresh_token, }); await fs.writeFile(TOKEN_PATH, payload); } /** * Load or request or authorization to call APIs. * */ async function authorize() { let client = await loadSavedCredentialsIfExist(); if (client) { return client; } client = await authenticate({ scopes: SCOPES, keyfilePath: CREDENTIALS_PATH, }); if (client.credentials) { await saveCredentials(client); } return client; } /** * Lists the next 10 events on the user's primary calendar. * @param {google.auth.OAuth2} auth An authorized OAuth2 client. */ async function listEvents(auth) { // Settings: const calendar = google.calendar({version: 'v3', auth}); const s_calendarId = "primary"; const s_timeMin = new Date().toISOString(); const s_maxResult = 5; const s_singleEvents = true; const s_orderBy = "startTime"; // Getting events const g_event = await calendar.events.list({ calendarId: s_calendarId, timeMin: s_timeMin, maxResults: s_maxResult, singleEvents: s_singleEvents, orderBy: s_orderBy, }); // DEBUGING: Events const events = g_event.data.items; if (!events || events.length === 0) { console.log('No upcoming events found.'); // Return Event None return false; }else{ console.log('Upcoming 10 events:'); const e = {}; events.map((event, i) => { const start = event.start.dateTime || event.start.date; const end = event.end.dateTime || event.end.date; console.log(`${start} - ${event.summary}`); // Adding to "e" e[(event.summary + start)] = { name: event.summary, start: start, end: end, user: event.creator.email, description: event.description } }); // Returning "e" return e; } } // Website and Server communication: const app = express(); app.use(express.json(), cors()); // Middleware for reading JSON-data // Request for calendar info from base-website app.post('/calendar', (req, res) => { const data = req.body; // Data request // Sending data to website authorize().then(x => { // Calls list events with x from authorize listEvents(x).then(x => { console.log("Sending....") // Fixing calendar info into data and sending it to server const data = { calendar: x, food: foodList } res.send(data); // End DEBUG console.log("Sent: " + x); }); // Console error if there are any }).catch(console.error); }); // Request for adding food to base-website app.post('/food_add', (req, res) => { const data = req.body.food; // Getting req data of food to add foodList.push(data); // DEBUG console.log(foodList); }); // Request for getting food from base-website app.post('/food_get', (req, res) => { // Getting food list res.send(foodList); }); // Request for removing food from base-website (food list) app.post('/food_remove', (req, res) => { // Getting food to remove const data = req.body.food; // Removing food for (let i = 0; i < foodList.length; i++) { // Checking if it is the correct food if(foodList[i] == data) { console.log(foodList.splice(i, 1)); } } // Debug console.log("Removing Food: " + data + " from foodList[]"); console.log(foodList); }); app.listen(3000, () => { console.log(`Servern lyssnar på port 3000`); });
解决方案
错误原因
代码存在路径赋值错误:CREDENTIALS_PATH被错误指向token.json,但它实际应该指向从Google云平台下载的OAuth2客户端密钥文件(通常命名为credentials.json)。
Windows上能运行是因为之前已生成有效token.json,loadSavedCredentialsIfExist()直接加载文件跳过了authenticate()流程;树莓派上无有效token.json,触发authenticate()时,程序试图从token.json中读取redirect_uris字段,但token.json的结构里没有该字段,因此抛出错误。
修复步骤
修正路径赋值
将代码中:const CREDENTIALS_PATH = path.join(process.cwd(), 'token.json');修改为:
const CREDENTIALS_PATH = path.join(process.cwd(), 'credentials.json');上传密钥文件
将从Google云平台下载的credentials.json(包含installed或web字段,内有redirect_uris)上传到树莓派项目根目录。清理无效token(可选)
如果树莓派上存在无效的token.json,直接删除,让程序重新走授权流程生成新的有效token。重新运行服务
重启Node.js服务,首次运行会引导完成OAuth授权(树莓派无桌面环境时,需复制授权链接到其他设备完成验证,再将授权码粘贴回终端)。
验证
授权完成后调用/calendar接口,应能正常获取Google日历事件数据,不再出现redirect_uris未定义的错误。
内容的提问来源于stack exchange,提问作者Alvin Hansen

