如何调试Apps Script导入日历事件时的BadRequest错误?
基于谷歌开发者平台的「填充团队休假日历」示例脚本开发的Apps Script,原本正常运行数月,近期未做任何修改,调用Calendar.Events.import(event, TEAM_CALENDAR_ID)时突然报错:
GoogleJsonResponseException: API call to calendar.events.import failed with error: Bad Request
调试仅返回400 Bad Request,无额外细节。导入的event变量来自Calendar.Events.list(...)的返回结果,是API生成的JS事件对象。
最小复现示例
let TEAM_CALENDAR_ID = '<calendar id goes here>'; function testImport() { const now = new Date(); let user = Session.getActiveUser(); // Fetch next 10 events let events = Calendar.Events.list(user.getEmail(), { timeMin: now.toISOString(), singleEvents: true, orderBy: 'startTime', maxResults: 10 }); if (events.items.length === 0) { console.error('No events found'); return; } // Use next upcoming event for this user let event = events.items[0]; // Set event fields event.organizer = { id: TEAM_CALENDAR_ID }; event.attendees = []; try { Calendar.Events.import(event, TEAM_CALENDAR_ID); } catch (e) { console.error('Error attempting to import event: %s.', e.toString()); } }
完整脚本
// Set the ID of the team calendar to add events to. You can find the calendar's // ID on the settings page. let TEAM_CALENDAR_ID = '<calendar ID here>'; // Set the email address of the Google Group that contains everyone in the team. // Ensure the group has less than 500 members to avoid timeouts. let GROUP_EMAIL = '<group email address here>'; let MONTHS_IN_ADVANCE = 3; /** * Sets up the script to run automatically every hour. */ function setup() { let triggers = ScriptApp.getProjectTriggers(); if (triggers.length > 0) { throw new Error('Triggers are already setup.'); } ScriptApp.newTrigger('sync').timeBased().everyHours(1).create(); // Runs the first sync immediately. sync(); } /** * Looks through the group members' public calendars and adds any * 'vacation' or 'out of office' events to the team calendar. */ function sync() { // Defines the calendar event date range to search. let today = new Date(); let maxDate = new Date(); maxDate.setMonth(maxDate.getMonth() + MONTHS_IN_ADVANCE); // Determines the time the the script was last run. let lastRun = PropertiesService.getScriptProperties().getProperty('lastRun'); lastRun = lastRun ? new Date(lastRun) : null; // Gets the list of users in the Google Group. let users = GroupsApp.getGroupByEmail(GROUP_EMAIL).getUsers(); // For each user, finds Out Of Office events, and import // each to the team calendar. let count = 0; users.forEach(function(user) { let events = findEvents(user, today, maxDate, lastRun); events.forEach(function(event) { importEvent(user, event); count++; }); }); PropertiesService.getScriptProperties().setProperty('lastRun', today); console.log('Updated ' + count + ' events'); } /** * In a given user's calendar, looks for Out Of Office events within the * specified date range and returns any such events found. * @param {Session.User} user The user to retrieve events for. * @param {Date} start The starting date of the range to examine. * @param {Date} end The ending date of the range to examine. * @param {Date} optSince A date indicating the last time this script was run. * @return {Calendar.Event[]} An array of calendar events. */ function findEvents(user, start, end, optSince) { let params = { timeMin: formatDateAsRFC3339(start), timeMax: formatDateAsRFC3339(end), showDeleted: true, }; if (optSince) { // This prevents the script from examining events that have not been // modified since the specified date (that is, the last time the // script was run). params.updatedMin = formatDateAsRFC3339(optSince); } let pageToken = null; let events = []; do { params.pageToken = pageToken; let response; try { response = Calendar.Events.list(user.getEmail(), params); } catch (e) { console.error('Error retriving events for %s: %s; skipping', user, e.toString()); continue; } events = events.concat(response.items.filter(function(item) { return shoudImportEvent(user, item); })); pageToken = response.nextPageToken; } while (pageToken); return events; } /** * Determines if the given event should be imported into the shared team * calendar. * @param {Session.User} user The user that is attending the event. * @param {Calendar.Event} event The event being considered. * @return {boolean} True if the event should be imported. */ function shoudImportEvent(user, event) { // Skip events that are not Out Of Office if (event.eventType != "outOfOffice") { return false; } // If the user is the creator of the event, always imports it. if (!event.organizer || event.organizer.email == user.getEmail()) { return true; } // Only imports events the user has accepted. if (!event.attendees) { return false; } let matching = event.attendees.filter(function(attendee) { return attendee.self; }); return matching.length > 0 && matching[0].responseStatus == 'accepted'; } /** * Imports the given event from the user's calendar into the shared team * calendar. * @param {string} username The team member that is attending the event. * @param {Calendar.Event} event The event to import. */ function importEvent(user, event) { let username = user.getEmail().split('@')[0]; username = username.charAt(0).toUpperCase() + username.slice(1); event.summary = '[' + username + '] ' + event.summary; event.organizer = { id: TEAM_CALENDAR_ID, }; event.attendees = []; let action = event.status == "confirmed" ? "Importing" : "Removing"; console.log('%s: %s on %s', action, event.summary, event.start.getDateTime()); try { Calendar.Events.import(event, TEAM_CALENDAR_ID); } catch (e) { console.error('Error attempting to import event: %s. Skipping.', e.toString()); } } /** * Returns an RFC3339 formated date String corresponding to the given * Date object. * @param {Date} date a Date. * @return {string} a formatted date string. */ function formatDateAsRFC3339(date) { return Utilities.formatDate(date, 'UTC', 'yyyy-MM-dd\'T\'HH:mm:ssZ'); }
1. 捕获完整错误详情
默认的e.toString()仅返回基础错误信息,需获取e.details查看API返回的具体错误原因。修改importEvent中的catch块:
catch (e) { if (e.details) { console.error('Full error details:', JSON.stringify(e.details, null, 2)); } console.error('Error attempting to import event: %s. Skipping.', e.toString()); }
此操作会打印API返回的具体字段错误(如必填字段缺失、格式错误等)。
2. 清理冗余字段
Calendar.Events.list返回的事件对象包含大量只读字段(如id、etag、created、updated等),这些字段会在调用import时引发冲突。需创建新的事件对象,仅保留必要可写字段:
function importEvent(user, event) { let username = user.getEmail().split('@')[0]; username = username.charAt(0).toUpperCase() + username.slice(1); // 仅保留必要字段,避免只读字段干扰 let cleanEvent = { summary: '[' + username + '] ' + event.summary, description: event.description, start: event.start, end: event.end, eventType: event.eventType, status: event.status, organizer: { id: TEAM_CALENDAR_ID }, attendees: [] }; let action = event.status == "confirmed" ? "Importing" : "Removing"; console.log('%s: %s on %s', action, cleanEvent.summary, cleanEvent.start.getDateTime()); try { Calendar.Events.import(cleanEvent, TEAM_CALENDAR_ID); } catch (e) { if (e.details) { console.error('Full error details:', JSON.stringify(e.details, null, 2)); } console.error('Error attempting to import event: %s. Skipping.', e.toString()); } }
只读字段会被API判定为无效输入,这是引发400错误的最常见原因。
3. 检查权限与日历设置
- 确认脚本使用的账号拥有共享日历的编辑权限,若为团队日历,检查共享设置是否允许该账号修改。
- 检查共享日历是否被设为只读,或存在特殊访问限制。
4. 验证日期时间格式
尽管事件对象来自API,仍可能存在时区问题。打印event.start和event.end的内容,确认格式符合RFC3339规范且时区信息正确。
5. 测试单个事件
在最小复现脚本中,手动指定简单事件对象进行测试,排除原事件对象的问题:
function testSimpleImport() { let testEvent = { summary: 'Test OOO Event', start: { dateTime: new Date().toISOString() }, end: { dateTime: new Date(Date.now() + 3600000).toISOString() }, // 1小时后 eventType: 'outOfOffice', status: 'confirmed', organizer: { id: TEAM_CALENDAR_ID }, attendees: [] }; try { Calendar.Events.import(testEvent, TEAM_CALENDAR_ID); console.log('Test event imported successfully'); } catch (e) { if (e.details) { console.error('Full error details:', JSON.stringify(e.details, null, 2)); } console.error('Error:', e.toString()); } }
若测试成功,说明问题出在原事件对象的冗余字段或特定属性上。
内容的提问来源于stack exchange,提问作者epalm

